mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
web/i18n: Clean up locale scripts (#18163)
* web/i81n: Clean up locale scripts. * Fix logs, clean up caching. * Use previous script name. * Fix path.
This commit is contained in:
+183
-49
@@ -11,93 +11,227 @@
|
||||
* 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 { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
|
||||
* @import { Stats } from "node:fs";
|
||||
* @import { RuntimeOutputConfig } from "@lit/localize-tools/lib/types/modes.js"
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
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";
|
||||
|
||||
/**
|
||||
* @type {ConfigFile}
|
||||
*/
|
||||
const localizeRules = JSON.parse(
|
||||
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
|
||||
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 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 XLIFFPath = resolve(PackageRoot, localizeRules.interchange.xliffDir);
|
||||
|
||||
const EmittedLocalesDirectory = resolve(
|
||||
PackageRoot,
|
||||
/** @type {string} */ (localizeRules.output.outputDir),
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} loc
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function generatedFileIsUpToDateWithXliffSource(loc) {
|
||||
const xliff = path.join("./xliff", `${loc}.xlf`);
|
||||
const gened = path.join("./src/locales", `${loc}.ts`);
|
||||
const targetLocales = localizeRules.targetLocales.filter((localeCode) => {
|
||||
return localeCode !== "pseudo-LOCALE";
|
||||
});
|
||||
|
||||
// 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.
|
||||
//#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 xlfStat;
|
||||
let xliffStat;
|
||||
|
||||
try {
|
||||
xlfStat = statSync(xliff);
|
||||
xliffStat = await fs.stat(xliffPath);
|
||||
} catch (_error) {
|
||||
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
|
||||
logger.error(`XLIFF source file missing for locale '${localeCode}': ${xliffPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Stats}
|
||||
*/
|
||||
let genedStat;
|
||||
let emittedStat;
|
||||
|
||||
// If the generated file doesn't exist, of course it's not up to date.
|
||||
try {
|
||||
genedStat = statSync(gened);
|
||||
emittedStat = await fs.stat(emittedPath);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the generated file is the same age or newer (date is greater) than the xliff file, it's
|
||||
// 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 genedStat.mtimeMs >= xlfStat.mtimeMs;
|
||||
return emittedStat.mtimeMs >= xliffStat.mtimeMs;
|
||||
}
|
||||
|
||||
// For all the expected files, find out if any aren't up-to-date.
|
||||
const upToDate = localizeRules.targetLocales.reduce(
|
||||
(acc, loc) => acc && generatedFileIsUpToDateWithXliffSource(loc),
|
||||
true,
|
||||
);
|
||||
/**
|
||||
* 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...");
|
||||
|
||||
if (!upToDate) {
|
||||
const status = spawnSync("npm", ["run", "build-locales:build"], { encoding: "utf8" });
|
||||
logger.info(`Checking ${targetLocales.length} source files...`);
|
||||
|
||||
// Count all the missing message warnings
|
||||
const counts = status.stderr.split("\n").reduce((acc, line) => {
|
||||
const match = /^([\w-]+) message/.exec(line);
|
||||
if (!match) {
|
||||
return acc;
|
||||
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 localeWarnings = new Map();
|
||||
|
||||
const initialConsoleWarn = console.warn;
|
||||
|
||||
console.warn = (arg0, ...args) => {
|
||||
if (typeof arg0 !== "string") {
|
||||
initialConsoleWarn(arg0, ...args);
|
||||
return;
|
||||
}
|
||||
acc.set(match[1], (acc.get(match[1]) || 0) + 1);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const locales = Array.from(counts.keys());
|
||||
locales.sort();
|
||||
const [, matchedLocale] = arg0.match(missingMessagePattern) || [];
|
||||
|
||||
const report = locales
|
||||
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
|
||||
if (matchedLocale) {
|
||||
const count = localeWarnings.get(matchedLocale) || 0;
|
||||
|
||||
localeWarnings.set(matchedLocale, count + 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
initialConsoleWarn(arg0, ...args);
|
||||
};
|
||||
|
||||
// @ts-expect-error: Type is too broad.
|
||||
const localizer = new RuntimeLitLocalizer(localizeRules);
|
||||
|
||||
await localizer.build();
|
||||
|
||||
const report = Array.from(localeWarnings)
|
||||
.filter(([, count]) => count)
|
||||
.sort(([, totalsA], [, totalsB]) => {
|
||||
return totalsB - totalsA;
|
||||
})
|
||||
.map(([locale, count]) => `${locale}: ${count.toLocaleString()}`)
|
||||
.join("\n");
|
||||
|
||||
console.log(`Translation tables rebuilt.\n${report}\n`);
|
||||
logger.info(`Missing translations:\n${report}`);
|
||||
|
||||
localizer.assertTranslationsAreValid();
|
||||
|
||||
logger.info("Complete.");
|
||||
}
|
||||
|
||||
console.log("Locale ./src is up-to-date");
|
||||
//#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
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { PackageRoot } from "#paths/node";
|
||||
|
||||
import { isMain } from "@goauthentik/core/scripting/node";
|
||||
|
||||
import pseudolocale from "pseudolocale";
|
||||
|
||||
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
|
||||
@@ -22,6 +25,7 @@ import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.j
|
||||
|
||||
const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE");
|
||||
const targetLocales = [pseudoLocale];
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
/**
|
||||
* @type {ConfigFile}
|
||||
@@ -59,10 +63,16 @@ const pseudoMessagify = (message) => ({
|
||||
),
|
||||
});
|
||||
|
||||
const localizer = new TransformLitLocalizer(config);
|
||||
const { messages } = localizer.extractSourceMessages();
|
||||
const translations = messages.map(pseudoMessagify);
|
||||
const sorted = sortProgramMessages([...messages]);
|
||||
const formatter = makeFormatter(config);
|
||||
export async function generatePseudoLocaleModule() {
|
||||
const localizer = new TransformLitLocalizer(config);
|
||||
const { messages } = localizer.extractSourceMessages();
|
||||
const translations = messages.map(pseudoMessagify);
|
||||
const sorted = sortProgramMessages([...messages]);
|
||||
const formatter = makeFormatter(config);
|
||||
|
||||
formatter.writeOutput(sorted, new Map([[pseudoLocale, translations]]));
|
||||
await formatter.writeOutput(sorted, new Map([[pseudoLocale, translations]]));
|
||||
}
|
||||
|
||||
if (isMain(import.meta)) {
|
||||
generatePseudoLocaleModule();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user