Skip to content

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-i18next is already configured with fallbackLng: "en", so partial resources work out of the box.

Files You Need to Edit

When adding a new language, update these files:

  1. src/types/index.ts — add the locale code to the SupportedAppLocale union type
  2. src/i18n/locale.ts — add locale metadata (English name, native name, text direction)
  3. src/i18n/messages.ts — add the translation block (can be partial or empty)
  4. 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:

  • fr for French
  • de for German
  • zh-CN for Simplified Chinese
  • ko for 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

  1. Run the app locally (npm run dev or pnpm dev)
  2. Open Settings > Language & Region (or the onboarding screen on first run)
  3. Select your new language from the dropdown
  4. Check that translated sections show your strings and untranslated sections fall back to English
  5. Run the test suite: npx vitest run src/i18n/locale.test.ts
  6. Run the type checker: npx tsc --noEmit

Gotchas & Tips

  • Use BCP-47 hyphenated codes. Write zh-CN, not zh_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 to SUPPORTED_APP_LOCALE_METADATA is enough for the data-driven fallback to recognize it.

Quick Checklist

  • [ ] Added locale code to SupportedAppLocale in src/types/index.ts
  • [ ] Added metadata to SUPPORTED_APP_LOCALE_METADATA in src/i18n/locale.ts (with correct dir)
  • [ ] Added translation block (complete, partial, or {}) to translationCatalog in src/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