-
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 ~230 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 uses
localeComparewith the current locale, so alphabetical sort respects German collation (umlauts sorting where a German speaker expects them) when German is selected.
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.