Files
clash-proxy/scripts/split-locales.mjs
2025-11-06 12:14:30 +08:00

132 lines
3.2 KiB
JavaScript

#!/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;
});