Files
flint/scripts/i18n/sync-locales.mjs
T
2026-04-23 23:39:18 +02:00

149 lines
3.7 KiB
JavaScript

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const SUPPORTED_LOCALES = [
"en",
"es",
"de",
"ja",
"fr",
"pt",
"ru",
"it",
"nl",
"pl",
"zh",
"hi",
"ar",
"bn",
"id",
"tr",
];
const TARGETS = [
{
name: "bot",
directory: "apps/bot/locales",
},
{
name: "web",
directory: "apps/web/locales",
},
];
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
const CHECK_ONLY = process.argv.includes("--check");
const isObject = (value) => {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
};
const readJsonFile = (filePath) => {
const raw = readFileSync(filePath, "utf-8");
return JSON.parse(raw);
};
const writeJsonFile = (filePath, content) => {
writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`, "utf-8");
};
const shapeFromBase = (base, override) => {
if (Array.isArray(base)) {
return Array.isArray(override) ? override : base;
}
if (isObject(base)) {
const result = {};
const overrideObject = isObject(override) ? override : {};
for (const [key, value] of Object.entries(base)) {
result[key] = shapeFromBase(value, overrideObject[key]);
}
return result;
}
return typeof override === typeof base ? override : base;
};
const collectLeafPaths = (input, prefix = "", output = []) => {
if (isObject(input)) {
for (const [key, value] of Object.entries(input)) {
const nextPrefix = prefix.length > 0 ? `${prefix}.${key}` : key;
collectLeafPaths(value, nextPrefix, output);
}
return output;
}
output.push(prefix);
return output;
};
let hasError = false;
for (const target of TARGETS) {
const targetDirectory = path.resolve(REPO_ROOT, target.directory);
const defaultLocalePath = path.join(targetDirectory, "en.json");
if (!existsSync(defaultLocalePath)) {
console.error(
`[i18n:${target.name}] missing default locale file: ${defaultLocalePath}`,
);
hasError = true;
continue;
}
mkdirSync(targetDirectory, { recursive: true });
const baseDictionary = readJsonFile(defaultLocalePath);
const baseLeafPaths = new Set(collectLeafPaths(baseDictionary));
for (const locale of SUPPORTED_LOCALES) {
const localePath = path.join(targetDirectory, `${locale}.json`);
const localeExists = existsSync(localePath);
const currentDictionary = localeExists ? readJsonFile(localePath) : {};
const normalizedDictionary = shapeFromBase(
baseDictionary,
currentDictionary,
);
const normalizedText = JSON.stringify(normalizedDictionary, null, 2);
const currentText = JSON.stringify(currentDictionary, null, 2);
if (!CHECK_ONLY && (!localeExists || normalizedText !== currentText)) {
writeJsonFile(localePath, normalizedDictionary);
console.log(`[i18n:${target.name}] synced ${locale}.json`);
}
if (CHECK_ONLY) {
const localeLeafPaths = new Set(collectLeafPaths(currentDictionary));
const missingPaths = [...baseLeafPaths].filter(
(key) => !localeLeafPaths.has(key),
);
const extraPaths = [...localeLeafPaths].filter(
(key) => !baseLeafPaths.has(key),
);
if (missingPaths.length > 0 || extraPaths.length > 0) {
hasError = true;
console.error(`[i18n:${target.name}] ${locale}.json is out of sync`);
if (missingPaths.length > 0) {
console.error(` missing keys: ${missingPaths.join(", ")}`);
}
if (extraPaths.length > 0) {
console.error(` extra keys: ${extraPaths.join(", ")}`);
}
}
}
}
}
if (CHECK_ONLY && hasError) {
process.exitCode = 1;
}