-
Notifications
You must be signed in to change notification settings - Fork 0
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.
-
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 flatkey → stringdictionary, 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.
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.
-
settings.localeis 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 once —
const tr = translator(settings.locale)— and callstr('key')ortr('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 upSTRINGS[locale][key], falls back toSTRINGS[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) anddialectSummary()insrc/lib/ai.js(the negotiated-dialect line in Settings) take the sametr— the sentences a user reads when reviewing an AI proposal are translated too, not just the surrounding chrome. Internally,dialectSummary()is also called from insideai.js's own retry loop for a debug string embedded in a thrown error, where notris 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,buildContextinsrc/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.
A couple of things use the raw locale code directly, not just translated strings:
-
Date formatting in the file bar (
saved: …) usestoLocaleStringwithen-USorde-DEdepending on the selected locale, so timestamps read naturally in either language. -
Table sorting compares text and enums with
localeComparein 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.
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.