Adding a New Language to ButtonsCLI¶
This guide explains how to add a new language to the ButtonsCLI i18n (internationalization) system.
Overview¶
ButtonsCLI uses react-i18next with a static translation catalog. All user-visible strings in the renderer are routed through the t() function.
The translation catalog supports partial translations: only English must be complete. Every other locale may contain any subset of keys. Missing strings automatically fall back to English at runtime.
Architecture¶
- English (
en) is the fallback and must always contain every key. - Other locales may be partial. They only need the keys you have translated.
- The
getTranslationMessages()function deep-merges a locale's partial catalog onto the full English catalog at runtime. - An empty locale block (
{}) is valid — it means "this locale is selectable, but shows 100% English strings for now." react-i18nextis already configured withfallbackLng: "en", so partial resources work out of the box.
Files You Need to Edit¶
When adding a new language, update these files:
src/types/index.ts— add the locale code to theSupportedAppLocaleunion typesrc/i18n/locale.ts— add locale metadata (English name, native name, text direction)src/i18n/messages.ts— add the translation block (can be partial or empty)docs/ADDING_LANGUAGES.md— update the locale list in the table (optional but helpful)
You no longer need to touch a resolveLanguageFallback switch — it is now data-driven and picks up new locales automatically from the metadata array.
Step-by-Step Workflow¶
1. Choose a locale code¶
Use a standard BCP-47 hyphenated tag, for example:
frfor Frenchdefor Germanzh-CNfor Simplified Chinesekofor Korean
Use hyphens (-), not underscores. Underscores are normalized to hyphens during locale resolution, but new code should use the canonical form.
2. Update src/types/index.ts¶
Find the SupportedAppLocale union and add your new code:
export type SupportedAppLocale =
| "en"
| "es"
| "zh-CN"
| "fr"
// ... add your code here, e.g.:
| "ko";
3. Update src/i18n/locale.ts¶
Add an entry to the SUPPORTED_APP_LOCALE_METADATA array. Set dir to "rtl" only for languages that read right-to-left (Arabic, Hebrew, etc.):
const SUPPORTED_APP_LOCALE_METADATA: readonly AppLocaleMetadata[] = [
// ... existing entries ...
{
code: "ko",
englishLabel: "Korean",
nativeLabel: "한국어",
dir: "ltr",
},
] as const;
That's all. The data-driven resolveLanguageFallback() will automatically match ko-KR → ko without any switch-case changes.
4. Update src/i18n/messages.ts¶
Add your locale to the translationCatalog object. You can provide:
- A complete translation block (copy the English block and translate every string)
- A partial translation block (translate high-priority sections like
common,localization,onboarding— the rest falls back to English) - An empty block (
{}) to make the language immediately selectable with 100% English strings
Example partial block:
export const translationCatalog = {
// ... existing locales ...
ko: {
common: {
continue: "계속",
systemDefault: "시스템 기본값",
loading: "로딩 중...",
cancel: "취소",
save: "저장",
send: "보내기",
category: "카테고리",
replyEmail: "답장 이메일",
message: "메시지",
},
settings: {
localization: {
heading: "언어 및 지역",
languageLabel: "표시 언어",
followSystemLabel: "시스템 언어 사용",
chooseSpecificLanguageLabel: "특정 언어 선택",
currentLanguage: "현재 앱 언어: {{language}}",
detectedSystemLanguage: "감지된 시스템 언어: {{language}}",
},
},
},
};
All other sections (statusBar, tabBar, settings panels, AI Help, feedback, etc.) will fall back to English automatically.
Important: The file's type annotation only requires English to be complete. Partial blocks are type-checked for key names but not for completeness.
5. Update docs/ADDING_LANGUAGES.md (optional)¶
Update the locale list below so the docs stay current.
Currently Supported Locales¶
| Code | Language | Status |
|---|---|---|
en |
English | Complete |
es |
Spanish | Complete |
zh-CN |
Chinese Simplified | Partial |
fr |
French | Partial |
ja |
Japanese | Complete |
hi |
Hindi | Partial |
de |
German | Partial |
pt-BR |
Portuguese (Brazil) | Complete |
it |
Italian | Partial |
ru |
Russian | Partial |
ko |
Korean | Partial |
ar |
Arabic | Partial |
tr |
Turkish | Partial |
pl |
Polish | Partial |
nl |
Dutch | Partial |
sv |
Swedish | Partial |
da |
Danish | Partial |
fi |
Finnish | Partial |
no |
Norwegian | Partial |
zh-TW |
Chinese Traditional | Partial |
Testing the New Language¶
- Run the app locally (
npm run devorpnpm dev) - Open Settings > Language & Region (or the onboarding screen on first run)
- Select your new language from the dropdown
- Check that translated sections show your strings and untranslated sections fall back to English
- Run the test suite:
npx vitest run src/i18n/locale.test.ts - Run the type checker:
npx tsc --noEmit
Gotchas & Tips¶
- Use BCP-47 hyphenated codes. Write
zh-CN, notzh_CN. Underscore normalization happens at runtime, but canonical source code should use hyphens. - Do not rename or restructure keys. Key names are shared across all locales.
- Interpolation variables must stay unchanged. If English has
{{language}}, your translation must also contain{{language}}. - Partial blocks are valid. TypeScript will verify that keys you provide exist in the English catalog, but it will not require every key.
{}is valid. An empty locale is selectable and shows 100% English — useful for adding a language quickly and filling in translations later.- English is the fallback. If a key is missing from a non-English locale,
getTranslationMessages()merges it from English automatically. - The
resolveLanguageFallback()switch is gone. Adding a locale toSUPPORTED_APP_LOCALE_METADATAis enough for the data-driven fallback to recognize it.
Quick Checklist¶
- [ ] Added locale code to
SupportedAppLocaleinsrc/types/index.ts - [ ] Added metadata to
SUPPORTED_APP_LOCALE_METADATAinsrc/i18n/locale.ts(with correctdir) - [ ] Added translation block (complete, partial, or
{}) totranslationCataloginsrc/i18n/messages.ts - [ ] Added test assertions for new locale fallback in
src/i18n/locale.test.ts - [ ] TypeScript compiles without errors (
npx tsc --noEmit) - [ ] Tests pass (
npx vitest run src/i18n/locale.test.ts) - [ ] Tested the language in the app Settings and onboarding screen