feat(i18n): i18n:add-language – nytt språk i ett kommando

node scripts/i18n-add-language.mjs <kod> <bcp47> "<eget namn>"
Skapar locale-fil (sv-platshållare), registrerar i i18n.ts (import +
SUPPORTED_LANGUAGES + resources), lägger språkväljar-etiketten i alla filer.
Endast textinsättning → minimala diffar. Verifierat: testspråk -> typecheck+vakt gröna.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-21 02:32:31 +00:00
parent 2d59a3e636
commit 6d36ec7459
2 changed files with 103 additions and 1 deletions
+2 -1
View File
@@ -9,7 +9,8 @@
"ios": "expo start --ios",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"i18n:check": "node scripts/i18n-check.mjs"
"i18n:check": "node scripts/i18n-check.mjs",
"i18n:add-language": "node scripts/i18n-add-language.mjs"
},
"dependencies": {
"@app/analytics": "workspace:*",
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* i18n:add-language lägg till ett nytt språk i ett kommando.
*
* node scripts/i18n-add-language.mjs <kod> <bcp47-tagg> "<eget namn>"
* ex: node scripts/i18n-add-language.mjs el el-GR "Ελληνικά"
*
* Gör ALL plumbing automatiskt:
* 1. Skapar src/locales/<kod>/common.json kopia av svenska (platshållare),
* så appen funkar direkt och paritetsvakten är nöjd från start.
* 2. Registrerar språket i src/lib/i18n.ts (import + SUPPORTED_LANGUAGES + resources).
* 3. Lägger språkväljar-etiketten profile.language.<kod> = "<eget namn>" i ALLA
* locale-filer (annars faller vakten på saknad nyckel).
*
* Kvar för dig: översätt värdena i src/locales/<kod>/common.json och kör
* `pnpm --filter @app/mobile i18n:check` den listar exakt vad som ev. fattas.
*
* Endast textinsättning (aldrig omsortering) → minimala diffar på befintliga filer.
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
const ROOT = process.cwd(); // apps/mobile
const LOC = join(ROOT, "src/locales");
const I18N = join(ROOT, "src/lib/i18n.ts");
const [code, tag, ...labelParts] = process.argv.slice(2);
const label = labelParts.join(" ");
if (!code || !tag || !label) {
console.error('Användning: node scripts/i18n-add-language.mjs <kod> <bcp47-tagg> "<eget namn>"');
console.error('Exempel: node scripts/i18n-add-language.mjs el el-GR "Ελληνικά"');
process.exit(1);
}
if (!/^[a-z]{2,3}$/.test(code)) {
console.error(`Ogiltig språkkod "${code}" 23 gemener (t.ex. el, cs, uk).`);
process.exit(1);
}
if (!/^[a-z]{2,3}-[A-Za-z0-9]{2,4}$/.test(tag)) {
console.error(`Ogiltig BCP 47-tagg "${tag}" t.ex. el-GR, cs-CZ, pt-BR.`);
process.exit(1);
}
if (existsSync(join(LOC, code, "common.json"))) {
console.error(`src/locales/${code}/common.json finns redan avbryter.`);
process.exit(1);
}
const labelKey = `profile.language.${code}`;
const labelLine = ` ${JSON.stringify(labelKey)}: ${JSON.stringify(label)},`;
const ANCHOR = '"profile.language.sv":';
/** Sätt in etikettraden direkt efter profile.language.sv, annars först i objektet. */
function withLabel(raw) {
if (raw.includes(labelKey + '"') || raw.includes(labelKey + "'")) return raw; // finns redan
const lines = raw.split("\n");
let at = lines.findIndex((l) => l.includes(ANCHOR));
if (at === -1) at = lines.findIndex((l) => l.trim().startsWith("{")); // fallback: efter {
lines.splice(at + 1, 0, labelLine);
return lines.join("\n");
}
// ---- 1 + 3. Ny fil (kopia av sv + etikett) + etikett i alla befintliga -----
const svRaw = readFileSync(join(LOC, "sv/common.json"), "utf8");
mkdirSync(join(LOC, code), { recursive: true });
writeFileSync(join(LOC, code, "common.json"), withLabel(svRaw));
const existing = readdirSync(LOC).filter((d) => d !== code && statSync(join(LOC, d)).isDirectory());
for (const lang of existing) {
const p = join(LOC, lang, "common.json");
writeFileSync(p, withLabel(readFileSync(p, "utf8")));
}
// ---- 2. Registrera i i18n.ts (import + SUPPORTED_LANGUAGES + resources) -----
{
let lines = readFileSync(I18N, "utf8").split("\n");
const importRe = /^import \w+ from "\.\.\/locales\/\w+\/common\.json";$/;
let lastImport = -1;
lines.forEach((l, i) => importRe.test(l) && (lastImport = i));
lines.splice(lastImport + 1, 0, `import ${code} from "../locales/${code}/common.json";`);
const arrEnd = lines.findIndex((l) => l.trim() === "] as const;");
lines.splice(arrEnd, 0, ` { code: "${code}", tag: "${tag}", labelKey: "${labelKey}" },`);
const resRe = /^\s+\w+: \{ common: \w+ \},$/;
let lastRes = -1;
lines.forEach((l, i) => resRe.test(l) && (lastRes = i));
lines.splice(lastRes + 1, 0, ` ${code}: { common: ${code} },`);
writeFileSync(I18N, lines.join("\n"));
}
const keyCount = svRaw.split("\n").filter((l) => l.includes('": ')).length;
console.log(`\n✓ La till "${code}" (${tag}, "${label}").`);
console.log(
` • src/locales/${code}/common.json skapad (~${keyCount} nycklar, svenska platshållare)`,
);
console.log(` • registrerad i src/lib/i18n.ts (import + SUPPORTED_LANGUAGES + resources)`);
console.log(`${labelKey} = "${label}" i alla locale-filer`);
console.log(`\nNästa steg:`);
console.log(` 1. Översätt värdena i src/locales/${code}/common.json`);
console.log(` 2. pnpm --filter @app/mobile i18n:check`);
console.log(` 3. pnpm --filter @app/mobile typecheck`);