Files
authentik/web/scripts/build-locales.mjs
Teffen Ellis 342d9eb726 web: Locale selector UI fixes (#18972)
* Fix alignment, focus.

* Clean up.

* Tidy click area.

* Fix compatibility mode.

* Fix alignment.

* Fix issues surrounding labels, alignment, consistency.

* Update web/src/common/ui/locale/format.ts

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Tidy hover states.

* Tidy.

* Clean up parsing.

* Tidy comments, usage.

* Always use script naming over region.

* Remove unused.

* Spacing.

---------

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-12-23 18:40:02 +00:00

273 lines
7.4 KiB
JavaScript

/// <reference types="node" />
/**
* @file Lit Localize build script.
*
* @remarks
* Determines if all the Xliff translation source files are present and
* if the Typescript source files generated from those sources are up-to-date.
*
* If they are not, it runs the locale building script, intercepting the
* long spew of "this string is not translated" and replacing it with a
* summary of how many strings are missing with respect to the source locale.
*
* @import { Stats } from "node:fs";
*/
import * as fs from "node:fs/promises";
import path, { resolve } from "node:path";
import { generatePseudoLocaleModule } from "./pseudolocalize.mjs";
import { ConsoleLogger } from "#logger/node";
import { PackageRoot } from "#paths/node";
import { readConfigFileAndWriteSchema } from "@lit/localize-tools/lib/config.js";
import { RuntimeLitLocalizer } from "@lit/localize-tools/lib/modes/runtime.js";
//#region Setup
const missingMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sis\smissing/;
const outdatedMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sdoes\snot\sexist/;
const logger = ConsoleLogger.child({ name: "Locales" });
const localizeRules = readConfigFileAndWriteSchema(path.join(PackageRoot, "lit-localize.json"));
if (localizeRules.interchange.format !== "xliff") {
logger.error("Unsupported interchange type, expected 'xliff'");
process.exit(1);
}
const { sourceLocale } = localizeRules;
localizeRules.targetLocales = localizeRules.targetLocales.filter((locale) => {
return locale !== sourceLocale;
});
const XLIFFPath = resolve(PackageRoot, localizeRules.interchange.xliffDir);
const EmittedLocalesDirectory = resolve(
PackageRoot,
/** @type {string} */ (localizeRules.output.outputDir),
);
const targetLocales = localizeRules.targetLocales.filter((localeCode) => {
return localeCode !== "en-XA";
});
//#endregion
//#region Utilities
/**
* Cleans the emitted locales directory.
*/
async function cleanEmittedLocales() {
logger.info("♻️ Cleaning previously emitted locales...");
logger.info(`♻️ ${EmittedLocalesDirectory}`);
await fs.rm(EmittedLocalesDirectory, {
recursive: true,
force: true,
});
await fs.mkdir(EmittedLocalesDirectory, {
recursive: true,
});
logger.info(`♻️ Done!`);
}
/**
* Returns false if: the expected XLF file doesn't exist, The expected
* generated file doesn't exist, or the XLF file is newer (has a higher date)
* than the generated file. The missing XLF file is important enough it
* generates a unique error message and halts the build.
*
* @param {string} localeCode
* @returns {Promise<boolean>}
*/
async function checkIfEmittedFileCurrent(localeCode) {
const xliffPath = path.join(XLIFFPath, `${localeCode}.xlf`);
const emittedPath = path.join(EmittedLocalesDirectory, `${localeCode}.ts`);
/**
* @type {Stats}
*/
let xliffStat;
try {
xliffStat = await fs.stat(xliffPath);
} catch (_error) {
logger.error(`XLIFF source file missing for locale '${localeCode}': ${xliffPath}`);
process.exit(1);
}
/**
* @type {Stats}
*/
let emittedStat;
// If the generated file doesn't exist, of course it's not up to date.
try {
emittedStat = await fs.stat(emittedPath);
} catch (_error) {
return false;
}
// Possible if the script was interrupted between clearing and generating.
if (emittedStat.size === 0) {
return false;
}
// If the emitted file is the same age or newer (date is greater) than the xliff file, it's
// presumed to have been generated by that file and is up-to-date.
return emittedStat.mtimeMs >= xliffStat.mtimeMs;
}
/**
* Checks if all the locale source files are up-to-date with their XLIFF sources.
* @returns {Promise<boolean>}
*/
async function checkIfLocalesAreCurrent() {
logger.info("Reading locale configuration...");
logger.info(`Checking ${targetLocales.length} source files...`);
let outOfDateCount = 0;
await Promise.all(
targetLocales.map(async (localeCode) => {
const current = await checkIfEmittedFileCurrent(localeCode);
if (!current) {
logger.info(`Locale '${localeCode}' is out-of-date.`);
outOfDateCount++;
}
}),
);
return outOfDateCount === 0;
}
export async function generateLocaleModules() {
logger.info("Updating pseudo-locale...");
await generatePseudoLocaleModule();
logger.info("Generating locale modules...");
/**
* @type {Map<string, number>}
*/
const missingTranslationWarnings = new Map();
/**
* @type {Map<string, number>}
*/
const outdatedTranslationWarnings = new Map();
const initialConsoleWarn = console.warn;
console.warn = (arg0, ...args) => {
if (typeof arg0 !== "string") {
initialConsoleWarn(arg0, ...args);
return;
}
const [, matchedMissingTranslation] = arg0.match(missingMessagePattern) || [];
if (matchedMissingTranslation) {
const count = missingTranslationWarnings.get(matchedMissingTranslation) || 0;
missingTranslationWarnings.set(matchedMissingTranslation, count + 1);
logger.debug(arg0);
return;
}
const [, matchedOutdatedTranslation] = arg0.match(outdatedMessagePattern) || [];
if (matchedOutdatedTranslation) {
const count = outdatedTranslationWarnings.get(matchedOutdatedTranslation) || 0;
outdatedTranslationWarnings.set(matchedOutdatedTranslation, count + 1);
logger.debug(arg0);
return;
}
initialConsoleWarn(arg0, ...args);
};
// @ts-expect-error: Type is too broad.
const localizer = new RuntimeLitLocalizer(localizeRules);
await localizer.build();
const missingTranslationsReport = Array.from(missingTranslationWarnings)
.filter(([, count]) => count)
.sort(([, totalsA], [, totalsB]) => {
return totalsB - totalsA;
})
.map(([locale, count]) => `${locale}: ${count.toLocaleString()}`)
.join("\n");
logger.info(`Missing translations:\n${missingTranslationsReport || "None"}`);
const outdatedTranslationsReport = Array.from(outdatedTranslationWarnings)
.filter(([, count]) => count)
.sort(([, totalsA], [, totalsB]) => {
return totalsB - totalsA;
})
.map(([locale, count]) => `${locale}: ${count.toLocaleString()}`)
.join("\n");
logger.info(`Outdated translations:\n${outdatedTranslationsReport || "None"}`);
localizer.assertTranslationsAreValid();
logger.info("Complete.");
}
//#endregion
//#region Commands
async function delegateCommand() {
const command = process.argv[2];
switch (command) {
case "--clean":
return cleanEmittedLocales();
case "--check":
return checkIfLocalesAreCurrent();
case "--force":
return cleanEmittedLocales().then(generateLocaleModules);
}
const upToDate = await checkIfLocalesAreCurrent();
if (upToDate) {
logger.info("Locale is up-to-date!");
return;
}
logger.info("Locale ./src is out-of-date, rebuilding...");
return generateLocaleModules();
}
await delegateCommand()
.then(() => {
process.exit(0);
})
.catch((error) => {
logger.error(`Error during locale build: ${error}`);
process.exit(1);
});
//#endregion