diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b9acd13..f3fc01d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -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:*", diff --git a/apps/mobile/scripts/i18n-add-language.mjs b/apps/mobile/scripts/i18n-add-language.mjs new file mode 100644 index 0000000..f1fbb3f --- /dev/null +++ b/apps/mobile/scripts/i18n-add-language.mjs @@ -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 "" + * ex: node scripts/i18n-add-language.mjs el el-GR "Ελληνικά" + * + * Gör ALL plumbing automatiskt: + * 1. Skapar src/locales//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. = "" i ALLA + * locale-filer (annars faller vakten på saknad nyckel). + * + * Kvar för dig: översätt värdena i src/locales//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 ""'); + 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}" – 2–3 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`);