The LeafWiki application UI (buttons, labels, navigation, dialogs, settings, system messages) is translatable. Wiki page content is not affected by this — it is always shown exactly as written.
This document explains the setup and the exact steps to add a new language. Related issue: #789.
- The frontend uses
i18nextviareact-i18next. - Strings live in per-feature namespace catalogs:
ui/leafwiki-ui/src/locales/<lang>/<namespace>.json(for exampleen/editor.json,en/settings.json). There are 19 namespace files today. ui/leafwiki-ui/src/lib/i18n.tsglobssrc/locales/*/*.jsonat build time. A new language folder is picked up automatically — there is no list of languages to register in the frontend, and the language switcher in Settings → Account → Preferences is generated from what it finds.- English (
en/) is the fallback language. Any key missing in another language falls back to English at runtime. ui/leafwiki-ui/src/lib/i18n.test.tsenforces key parity: every namespace file must exist for every language with an identical set of keys. The test suite fails on any missing or extra key.- The name shown for a language in the switcher comes from that language's own
common.json→language.selfName(e.g."Deutsch","Español").
Components read strings with useTranslation('<namespace>'); code outside React
uses i18next.t(key, { ns: '<namespace>' }).
| Code | Language | Notes |
|---|---|---|
en |
English | default / fallback |
de |
Deutsch | |
es |
Español |
Example: adding French (fr).
Copy the full English set as a starting point and translate every value. Keep every key exactly as in English — do not rename, add, or drop keys.
cd ui/leafwiki-ui/src/locales
cp -r en frThen translate the values in fr/*.json. In fr/common.json, set the
language's own name:
"language": {
"selfName": "Français"
}Leaving a value in English is acceptable as an interim state (English is the fallback anyway), but the key must be present in every file or the parity test fails.
internal/usersettings/language.go has an allow-list that user-settings
validation checks when a user saves their language choice. The frontend
discovers the locale by glob, so the language appears in the switcher
immediately — but without this step, saving the choice fails validation and
the UI snaps back with an error (this is exactly what happened for Spanish in
#1526).
var allowedLanguages = map[string]bool{
DefaultLanguage: true,
"de": true,
"es": true,
"fr": true, // add this
}Update the expectations in internal/usersettings/language_test.go to include
the new code.
ui/leafwiki-ui/src/lib/dateLocale.ts keeps date-fns (relative times like
"3 days ago") and Intl.DateTimeFormat in sync with the UI language. It has
two small maps that must gain an entry, otherwise dates stay English for the
new language:
import { de, enUS, fr, type Locale } from 'date-fns/locale'
const DATE_FNS_LOCALES: Record<string, Locale> = {
en: enUS,
de,
fr, // add this
}
const BCP47_TAGS: Record<string, string> = {
en: 'en-US',
de: 'de-DE',
fr: 'fr-FR', // add this
}# frontend: key parity + language discovery
cd ui/leafwiki-ui
npm run test
npm run format
# backend: allow-list + user-settings validation
cd ../..
go test ./internal/usersettings/...Operators can point new users at the language with
--default-language=fr / LEAFWIKI_DEFAULT_LANGUAGE=fr. Once the locale folder
ships, this value is picked up automatically; an unknown value is ignored with a
console warning.
- Never hardcode a user-facing string in React/TS — always go through a key.
- Add or change the key in
ui/leafwiki-ui/src/locales/en/<namespace>.jsonfirst, then make the same change in every otherui/leafwiki-ui/src/locales/<lang>/<namespace>.json. - A brand-new namespace only needs
en/<namespace>.jsonplus a matching file under every other language folder —i18n.tsglobs them, there is nothing else to register. npm run testinui/leafwiki-uiconfirms parity still holds.