186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = path.resolve(__dirname, "..");
|
|
const sourceDirectory = path.join(projectRoot, "src", "i18n", "source");
|
|
const generatedDirectory = path.join(projectRoot, "src", "i18n", "generated");
|
|
const activeLocales = ["da", "en", "sv", "de", "no"];
|
|
const mode = process.argv.includes("--check") ? "check" : "write";
|
|
|
|
const linkTokenPattern = /@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))/gu;
|
|
|
|
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
|
|
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
|
|
|
|
const stableJson = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
|
|
const writeJson = (filePath, value) => {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, stableJson(value), "utf8");
|
|
};
|
|
|
|
const listJsonFiles = (directory) => {
|
|
if (!fs.existsSync(directory)) {
|
|
return [];
|
|
}
|
|
|
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
return entries
|
|
.flatMap((entry) => {
|
|
const entryPath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
return listJsonFiles(entryPath);
|
|
}
|
|
return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
|
|
})
|
|
.sort((left, right) => left.localeCompare(right, "en", { sensitivity: "base" }));
|
|
};
|
|
|
|
const mergeMessages = (left = {}, right = {}) => {
|
|
const merged = { ...left };
|
|
|
|
for (const [key, value] of Object.entries(right ?? {})) {
|
|
if (isPlainObject(value) && isPlainObject(merged[key])) {
|
|
merged[key] = mergeMessages(merged[key], value);
|
|
} else {
|
|
merged[key] = value;
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
};
|
|
|
|
const readJsonFragments = (directory) => {
|
|
let merged = {};
|
|
for (const filePath of listJsonFiles(directory)) {
|
|
merged = mergeMessages(merged, readJson(filePath));
|
|
}
|
|
return merged;
|
|
};
|
|
|
|
const mapSourcePathToRuntimePath = (keyPath) => {
|
|
if (keyPath.startsWith("terms.glossary.")) {
|
|
return `words.generated.${keyPath.slice("terms.glossary.".length)}`;
|
|
}
|
|
|
|
if (keyPath.startsWith("terms.")) {
|
|
return `words.${keyPath.slice("terms.".length)}`;
|
|
}
|
|
|
|
if (keyPath.startsWith("phrases.compat.")) {
|
|
return `templates.generated.compat.${keyPath.slice("phrases.compat.".length)}`;
|
|
}
|
|
|
|
if (keyPath.startsWith("phrases.")) {
|
|
return `templates.${keyPath.slice("phrases.".length)}`;
|
|
}
|
|
|
|
return keyPath;
|
|
};
|
|
|
|
const rewriteLinks = (value) => {
|
|
if (typeof value === "string") {
|
|
return value.replace(linkTokenPattern, (token, modifier = "", literalTarget, pathTarget) => {
|
|
const target = literalTarget ?? pathTarget;
|
|
const mappedTarget = mapSourcePathToRuntimePath(target);
|
|
const quoteTarget = literalTarget || /[^\p{L}\p{N}_.-]/u.test(mappedTarget);
|
|
return quoteTarget ? `@${modifier}:{'${mappedTarget}'}` : `@${modifier}:${mappedTarget}`;
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => rewriteLinks(entry));
|
|
}
|
|
|
|
if (!isPlainObject(value)) {
|
|
return value;
|
|
}
|
|
|
|
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, rewriteLinks(entry)]));
|
|
};
|
|
|
|
const compileWords = (sourceTerms) => {
|
|
const { glossary, ...terms } = sourceTerms;
|
|
return {
|
|
grammar: terms.grammar ?? {},
|
|
actions: terms.actions ?? {},
|
|
entities: terms.entities ?? {},
|
|
statuses: terms.statuses ?? {},
|
|
domain: terms.domain ?? {},
|
|
services: terms.services ?? {},
|
|
units: terms.units ?? {},
|
|
replication: terms.replication ?? {},
|
|
generated: glossary ?? {},
|
|
};
|
|
};
|
|
|
|
const compileTemplates = (sourcePhrases) => {
|
|
const { compat, ...phrases } = sourcePhrases;
|
|
return {
|
|
...phrases,
|
|
generated: {
|
|
compat: compat ?? {},
|
|
},
|
|
};
|
|
};
|
|
|
|
const compileLocale = (locale) => {
|
|
const localeSourceDirectory = path.join(sourceDirectory, locale);
|
|
const sourceTerms = readJsonFragments(path.join(localeSourceDirectory, "terms"));
|
|
const sourcePhrases = readJsonFragments(path.join(localeSourceDirectory, "phrases"));
|
|
|
|
return rewriteLinks({
|
|
words: compileWords(sourceTerms),
|
|
templates: compileTemplates(sourcePhrases),
|
|
});
|
|
};
|
|
|
|
const compileGlobal = () => {
|
|
const shared = readJsonFragments(path.join(sourceDirectory, "global", "shared"));
|
|
const locales = {};
|
|
|
|
for (const locale of activeLocales) {
|
|
locales[locale] = readJsonFragments(path.join(sourceDirectory, "global", "locales", locale));
|
|
}
|
|
|
|
return rewriteLinks({
|
|
shared,
|
|
locales,
|
|
});
|
|
};
|
|
|
|
const compiledFiles = new Map([
|
|
[path.join(generatedDirectory, "global-v2.json"), compileGlobal()],
|
|
...activeLocales.map((locale) => [path.join(generatedDirectory, `${locale}-v2.json`), compileLocale(locale)]),
|
|
]);
|
|
|
|
if (mode === "check") {
|
|
const mismatches = [];
|
|
for (const [filePath, value] of compiledFiles.entries()) {
|
|
const expected = stableJson(value);
|
|
const actual = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "") : "";
|
|
if (actual !== expected) {
|
|
mismatches.push(path.relative(projectRoot, filePath));
|
|
}
|
|
}
|
|
|
|
if (mismatches.length > 0) {
|
|
console.error("Generated i18n v2 runtime files are out of date:");
|
|
for (const mismatch of mismatches) {
|
|
console.error(` ${mismatch}`);
|
|
}
|
|
console.error("Run npm run i18n:v2:compile.");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("Generated i18n v2 runtime files are up to date.");
|
|
} else {
|
|
for (const [filePath, value] of compiledFiles.entries()) {
|
|
writeJson(filePath, value);
|
|
}
|
|
console.log(`Compiled i18n v2 runtime files to ${path.relative(projectRoot, generatedDirectory)}.`);
|
|
}
|