Skip to content

Interface Languages

Michael Dohmen edited this page Aug 24, 2026 · 4 revisions

Interface Languages

The interface ships in English (default) and German, selectable in Settings → Appearance → Language. The choice travels with the file, exactly like the colour scheme — open a file someone else saved in German, and it opens in German.

Two layers that deliberately don't mix

  • App chrome — buttons, dialogs, toasts, the AI proposal review ("Updated A-123 — Status: open → done"). This is what the language toggle controls. It lives in one file, src/i18n.js: a flat key → string dictionary, one block per language.
  • Schema content — field labels, enum values, seed data in src/domain.js. Whatever language you wrote them in is what stays on screen, regardless of the interface language. A German-only tool keeps its German column headers ("Fälligkeit", "Zuständig") even with the toggle set to English — translating live business data isn't a UI toggle's job, and mixing the two would make the schema layer depend on a locale it doesn't know about.

If you switch the interface to German on the shipped action-items example, you'll see this split directly: "Übersicht", "Überfällig", "Einstellungen" everywhere — but the table columns (Title, Owner, Due, Status) and the status pills (open, in progress, waiting, done) stay in English, because that's the language src/domain.js was written in for this particular tool.

Adding a language

Since the whole mechanism sits in one file with every string already translated once, adding a language is small enough to hand an AI assistant as a single instruction:

Add Italian as an interface language.

Everything it needs is right there: src/i18n.js documents the pattern in its header comment, LOCALES and LOCALE_LABELS list what Settings offers, and all 467 keys in STRINGS.en already have a STRINGS.de counterpart to translate from. Concretely, the change is:

// src/i18n.js
export const LOCALES = ['en', 'de', 'it']
export const LOCALE_LABELS = { en: 'English', de: 'Deutsch', it: 'Italiano' }

const STRINGS = {
  en: { /* … */ },
  de: { /* … */ },
  it: {
    'app.settings': 'Impostazioni',
    'common.apply': 'Applica',
    // … one line per key, same shape as `en` — plain strings and small
    // functions like `(n) => `${n} record${n === 1 ? '' : 's'}`` for the
    // handful of keys that take arguments (counts, names, plurals).
  },
}

Nothing else in the codebase changes. Settings picks up the new option from LOCALES automatically, every component already calls the generic tr('some.key', ...args), and a key missing from a work-in-progress translation falls back to English rather than breaking — so a partial translation is safe to ship and finish later.

How it's wired

  • settings.locale is a normal setting: stored with the file, defaults to 'en', rendered as a segmented control next to colour scheme and row height.
  • Every component builds a bound translator onceconst tr = translator(settings.locale) — and calls tr('key') or tr('key', ...args) for the handful of keys that interpolate a value (a count, a filename, a field label).
  • The fallback chain: t(locale, key, ...args) looks up STRINGS[locale][key], falls back to STRINGS[DEFAULT_LOCALE][key] (English), and finally to the raw key string itself if even that's missing — so a typo in a key name degrades to visible-but-harmless rather than a crash.
  • Pluralization and interpolation are handled by making the dictionary value a small function instead of a plain string, e.g. 'filebar.records': (n) => \${n} ${plural(n, 'record', 'records')}`` — no separate templating syntax to learn, just JavaScript.
  • src/lib/actions.js (validating and describing AI-proposed changes) and dialectSummary() in src/lib/ai.js (the negotiated-dialect line in Settings) take the same tr — the sentences a user reads when reviewing an AI proposal are translated too, not just the surrounding chrome. Internally, dialectSummary() is also called from inside ai.js's own retry loop for a debug string embedded in a thrown error, where no tr is available — it defaults to English there via a default parameter, rather than requiring every internal call site to thread a translator through for a string nobody but a developer will ever read.
  • What stays English on purpose: the instructions and schema description sent to the AI model (buildInstructions, buildContext in src/lib/ai.js). Models are most reliable in English regardless of the interface language, and the user never reads that text directly — only the model does. See AI Assistant.

Locale-aware details beyond string lookup

A couple of things use the raw locale code directly, not just translated strings:

  • Date formatting in the file bar (saved: …) uses toLocaleString with en-US or de-DE depending on the selected locale, so timestamps read naturally in either language.
  • Table sorting compares text and enums with localeCompare in the current locale, so alphabetical sort respects German collation (umlauts sorting where a German speaker expects them) when German is selected; numbers and dates have type-specific orders of their own — see Sorting.

Accessibility

Switching languages also updates document.documentElement.lang, so screen readers and browser tools (spellcheck, translation prompts) treat the page as being in the language it's actually displayed in.

Clone this wiki locally