Repeatable, high-confidence process for adding a UI locale to WorldScript Studio. Since the SSOT registry landed (
i18n/locales.ts), adding a language is one registry entry + content + fonts β the locale list is no longer hand-maintained across eight files. Target: hours, not days.
- Decide the BCP-47 code (ISO 639-1 where one exists:
ru,ko,nlβ¦). Region/script variants (pt-BR,zh-Hant) need the deferred BCP-47 architecture change β not covered here. - Know the script (
latin/cyrillic/greek/cjk/arabic/hebrew) and direction. - Network access for the machine-translation step.
Add one entry to LOCALES in i18n/locales.ts β code, nativeName (endonym), englishName,
flag emoji, dir, status: 'beta', script, helpFallback: true. Everything downstream
(Language union, RTL_LOCALES, SUPPORTED_LOCALES, LanguageSelector, the parity/bundle/translate
scripts, the placeholder + registry tests) derives from this automatically.
If the script is new (first Cyrillic/Hangul/etc.):
- Extend
LocaleDescriptor['script']ini18n/locales.tsand the--font-ui-*mapping. - Add the Noto family to the Google Fonts
<link>inindex.htmland a:lang(<code>)rule inindex.cssbinding--font-uito the right stack (see the CJK/Greek block). - RTL scripts: the
[dir="rtl"]swap inindex.cssalready covers any new Arabic/Hebrew-script locale.
cp -r locales/en locales/<code> # 21 module JSONs, English placeholdersAdd the localized exonym portal.language.names.<code> to every locale's portal.json
(node scripts/check-i18n-keys.mjs --fix backfills English; translate the ~17 exonyms by hand).
Add a cold-start entry in services/i18nBootstrap.ts and the TTS locale in components/AiScratchpad.tsx.
The locale is MT-eligible automatically (filesystem-derived getMtLocales() in
scripts/i18n-locales.mjs). Seed a glossary block for <code> in
locales/translation-glossary.json first (brand terms verbatim: WorldScript Studio, Co-Pilot,
ProForge; domain terms: Manuscript, Plot Board, Synopsisβ¦), then:
node scripts/bulk-translate-locales.mjs --lang=<code> --all --delay=600 # glossary-anchored, placeholder-masked, resumable
pnpm run i18n:bundlehelp.json stays English fallback by design (helpFallback: true) β it is excluded from --all.
node scripts/check-suppressions.mjs # OK
pnpm run lint
pnpm run typecheck
pnpm run i18n:check # parity + bundle rebuild
pnpm run i18n:report # coverage / placeholder / length / glossary per locale
pnpm exec vitest run tests/unit/i18nPlaceholders.test.ts tests/unit/i18n/localesRegistry.test.tsThe registry-integrity test (tests/unit/i18n/localesRegistry.test.ts) fails if the registry and the
locales/ folders disagree β your new entry + folder must both exist.
- Run the app, switch to
<code>: bundle loads,<html lang>/<dir>correct, glyphs render (no tofu), no console errors, selector shows flag + exonym + Beta badge. - Update the README language list (Beta tag). Counts auto-sync via
scripts/sync-readme-metrics.mjs. - Open a focused, coherent PR and run the active review loops to quiescence (CodeAnt and
DeepSource when DeepSource is enabled), then use the normal protected squash-merge path. Any
protection bypass requires fresh, incident-specific maintainer authorization. See
docs/CODEANT-REVIEW-LOOP.md.
New locales land as Beta. Raising them toward Production is the Beta-to-Production playbook (glossary expansion, help strategy, status tier) β run it once the English source is stable.