Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node

#region Locale

src/locales/*.ts

### Node ###
# Logs
logs
Expand Down
28 changes: 6 additions & 22 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
"private": true,
"scripts": {
"build": "wireit",
"build-locales": "wireit",
"build-locales:build": "wireit",
"build-locales": "node scripts/build-locales.mjs",
"build-proxy": "wireit",
"build:sfe": "npm run build -w @goauthentik/web-sfe",
"esbuild:watch": "node scripts/build-web.mjs --watch",
"extract-locales": "wireit",
"bundler:watch": "node scripts/build-web.mjs --watch",
"extract-locales": "lit-localize extract",
"format": "wireit",
"lint": "eslint --fix .",
"lint-check": "eslint --max-warnings 0 .",
Expand All @@ -27,7 +26,7 @@
"test": "vitest",
"test:e2e": "playwright test",
"tsc": "wireit",
"watch": "run-s build-locales esbuild:watch"
"watch": "run-s build-locales bundler:watch"
},
"type": "module",
"exports": {
Expand Down Expand Up @@ -55,6 +54,7 @@
},
"imports": {
"#styles/*.css": "./src/styles/*.css",
"#styles/*": "./src/styles/*.js",
"#common/*": "./src/common/*.js",
"#elements/*.css": "./src/elements/*.css",
"#elements/*": "./src/elements/*.js",
Expand Down Expand Up @@ -244,25 +244,9 @@
"build-locales"
]
},
"build-locales:build": {
"command": "lit-localize build"
},
"build-locales:repair": {
"locales:repair": {
"command": "prettier --write ./src/locale-codes.ts"
},
"build-locales": {
"command": "node scripts/build-locales.mjs",
"files": [
"./xliff/*.xlf"
],
"output": [
"./src/locales/*.ts",
"./src/locale-codes.ts"
]
},
"extract-locales": {
"command": "lit-localize extract"
},
"lint:components": {
"command": "lit-analyzer src"
},
Expand Down
226 changes: 180 additions & 46 deletions web/scripts/build-locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

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),
);

const targetLocales = localizeRules.targetLocales.filter((localeCode) => {
return localeCode !== "pseudo-LOCALE";
});

//#endregion

//#region Utilities

/**
* @type {ConfigFile}
* Cleans the emitted locales directory.
*/
const localizeRules = JSON.parse(
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
);
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} loc
* @returns {boolean}
* @param {string} localeCode
* @returns {Promise<boolean>}
*/
function generatedFileIsUpToDateWithXliffSource(loc) {
const xliff = path.join("./xliff", `${loc}.xlf`);
const gened = path.join("./src/locales", `${loc}.ts`);

// 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.
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...");

logger.info(`Checking ${targetLocales.length} source files...`);

let outOfDateCount = 0;

if (!upToDate) {
const status = spawnSync("npm", ["run", "build-locales:build"], { encoding: "utf8" });
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;
}

// 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;
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) || [];

if (matchedLocale) {
const count = localeWarnings.get(matchedLocale) || 0;

localeWarnings.set(matchedLocale, count + 1);

const report = locales
.map((locale) => `Locale '${locale}' has ${counts.get(locale)} missing translations`)
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
22 changes: 16 additions & 6 deletions web/scripts/pseudolocalize.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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}
Expand Down Expand Up @@ -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);

await formatter.writeOutput(sorted, new Map([[pseudoLocale, translations]]));
}

formatter.writeOutput(sorted, new Map([[pseudoLocale, translations]]));
if (isMain(import.meta)) {
generatePseudoLocaleModule();
}
2 changes: 1 addition & 1 deletion web/src/components/ak-status-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AKElement } from "#elements/Base";

import Styles from "#components/ak-status-label.css";

import { P4Disposition } from "src/styles/patternfly/constants";
import { P4Disposition } from "#styles/patternfly/constants";

import { msg } from "@lit/localize";
import { html } from "lit";
Expand Down
Loading
Loading