refactor(i18n): split locales into per-namespace files
This commit is contained in:
128
scripts/split-locales.mjs
Normal file
128
scripts/split-locales.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* One-time helper to split flat locale JSON files (e.g. en.json)
|
||||
* into per-namespace files (e.g. en/shared.json, en/settings.json).
|
||||
*/
|
||||
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const LOCALES_DIR = path.join(ROOT, "src/locales");
|
||||
|
||||
async function ensureDir(dirPath) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
const RESERVED = new Set(["default", "function", "var", "let", "const", "import"]);
|
||||
|
||||
function toIdentifier(namespace, taken) {
|
||||
let base = namespace
|
||||
.replace(/[^a-zA-Z0-9_$]/g, "_")
|
||||
.replace(/^[^a-zA-Z_$]+/, "");
|
||||
if (!base) {
|
||||
base = "ns";
|
||||
}
|
||||
let candidate = base;
|
||||
let counter = 1;
|
||||
while (RESERVED.has(candidate) || taken.has(candidate)) {
|
||||
candidate = `${base}_${counter}`;
|
||||
counter += 1;
|
||||
}
|
||||
taken.add(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
async function splitLocaleFile(filePath, lang) {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse ${filePath}: ${err.message}`);
|
||||
}
|
||||
|
||||
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
||||
throw new Error(`Locale file ${filePath} must contain a JSON object`);
|
||||
}
|
||||
|
||||
const langDir = path.join(LOCALES_DIR, lang);
|
||||
await ensureDir(langDir);
|
||||
|
||||
const namespaces = Object.entries(data);
|
||||
if (namespaces.length === 0) {
|
||||
console.warn(`Locale ${lang} has no keys, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const identifiers = new Map();
|
||||
const taken = new Set();
|
||||
|
||||
for (const [namespace, value] of namespaces) {
|
||||
if (
|
||||
typeof value !== "object" ||
|
||||
value === null ||
|
||||
Array.isArray(value)
|
||||
) {
|
||||
throw new Error(
|
||||
`Locale ${lang} namespace "${namespace}" must be an object`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetPath = path.join(langDir, `${namespace}.json`);
|
||||
const payload = `${JSON.stringify(value, null, 2)}\n`;
|
||||
await fs.writeFile(targetPath, payload, "utf-8");
|
||||
|
||||
identifiers.set(namespace, toIdentifier(namespace, taken));
|
||||
}
|
||||
|
||||
const importLines = namespaces
|
||||
.map(([namespace]) => {
|
||||
const ident = identifiers.get(namespace);
|
||||
return `import ${ident} from "./${namespace}.json";`;
|
||||
})
|
||||
.join("\n");
|
||||
const exportBody = namespaces
|
||||
.map(([namespace]) => {
|
||||
const ident = identifiers.get(namespace);
|
||||
return ` "${namespace}": ${ident},`;
|
||||
})
|
||||
.join("\n");
|
||||
const indexContent = `${importLines}
|
||||
|
||||
const resources = {
|
||||
${exportBody}
|
||||
};
|
||||
|
||||
export default resources;
|
||||
`;
|
||||
await fs.writeFile(path.join(langDir, "index.ts"), indexContent, "utf-8");
|
||||
|
||||
await fs.rm(filePath);
|
||||
console.log(`Split ${lang}.json into ${namespaces.length} namespaces.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const entries = await fs.readdir(LOCALES_DIR, { withFileTypes: true });
|
||||
const localeFiles = entries.filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith(".json"),
|
||||
);
|
||||
|
||||
if (localeFiles.length === 0) {
|
||||
console.log("No flat locale JSON files found. Nothing to do.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of localeFiles) {
|
||||
const lang = entry.name.replace(/\.json$/, "");
|
||||
const filePath = path.join(LOCALES_DIR, entry.name);
|
||||
await splitLocaleFile(filePath, lang);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user