Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.
- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
reactive to the signed-in profile locale. Every view, component, form and
the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
language (the panel has no user profile) and a header language picker.
Chrome, cards, login and API section titles translated; endpoint reference
descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
driven by AppSettings.locale. Nav, login, lock, dashboard, the full
Settings panel (incl. language picker) and format.dart status labels
translated; remaining detail screens fall back to English.
Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.
Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.8 KiB
Markdown
96 lines
4.8 KiB
Markdown
# Translations (i18n)
|
|
|
|
DriverVault's user interface is translatable. Each surface reads its text from
|
|
**per-language files** — nothing hardcodes English in the parts that have been
|
|
converted — so adding a language is a matter of dropping in a new file, not
|
|
editing screens.
|
|
|
|
Three languages ship today: **English (`en`)**, **Polish (`pl`)** and
|
|
**Danish (`da`)**. English is the base and the fallback: any key missing from
|
|
another language renders the English string, so a partial translation is always
|
|
safe to ship.
|
|
|
|
The language is the **language half of the user's BCP-47 locale**
|
|
(`locale` = `language-REGION`, e.g. `pl-PL`). The Settings **Language** picker
|
|
sets it; the **Region** half keeps steering date, number and currency formatting
|
|
independently, so the two can be mixed freely (English text with Polish number
|
|
formatting, say). The picker offers every European language because the choice
|
|
also drives date/number formatting — the ones without a translation file fall
|
|
back to English UI text and say so beneath the picker.
|
|
|
|
## Where the files live
|
|
|
|
| Surface | Language files | Loader | Language source |
|
|
|---|---|---|---|
|
|
| **Web App** (Vue) | `Web App/web/src/i18n/{en,pl,da}.json` | `Web App/web/src/i18n/index.js` | signed-in profile `locale` (reactive `prefs`) |
|
|
| **API Server panel** (Vue) | `API Server/panel/src/i18n/{en,pl,da}.json` | `API Server/panel/src/i18n/index.js` | `localStorage` (`dh-panel-lang`) — the panel has no user profile |
|
|
| **Phone App** (Flutter) | `Phone App/assets/i18n/{en,pl,da}.json` | `Phone App/lib/i18n.dart` | signed-in profile `locale` (via `AppSettings`) |
|
|
|
|
All three use the same JSON shape and the same `t()` contract, so a translator
|
|
learns one format.
|
|
|
|
## The `t()` contract
|
|
|
|
```js
|
|
t("settings.appearance.title") // simple lookup (dot path = JSON nesting)
|
|
t("forms.share.title", { name: car.name }) // {name} placeholder interpolation
|
|
t("dashboard.serviceRecords", { n: count }) // plural — see below
|
|
```
|
|
|
|
- **Keys** are dot paths matching the nesting in the JSON.
|
|
- **Placeholders** are named (`{name}`), never positional, so a translator can
|
|
reorder them to suit the target grammar.
|
|
- **Plurals** are an object keyed by CLDR category, selected for the active
|
|
language by `Intl.PluralRules` (web/panel) or `Intl.plural` (Flutter):
|
|
|
|
```json
|
|
"serviceRecords": {
|
|
"one": "{n} service record",
|
|
"other": "{n} service records"
|
|
}
|
|
```
|
|
|
|
This is why Polish works: it needs `one` / `few` / `many` where English has
|
|
only `one` / `other`, and a naive `n === 1` check would get "5 samochodów"
|
|
wrong. Polish files therefore carry all four forms.
|
|
|
|
- The web/panel loaders also expose `tSplit(key, name)` for the few strings that
|
|
wrap one value in its own markup (a monospace URL, a bolded car name). It
|
|
returns `{ before, after }` around the placeholder so the value keeps its
|
|
styling without splitting the sentence into word-order-assuming fragments or
|
|
putting a translated string on a `v-html` path.
|
|
|
|
## Adding a language
|
|
|
|
1. **Copy `en.json` to `<code>.json`** in each surface you want to cover
|
|
(`fr.json`, say) and translate the string values. Keep the keys and the
|
|
`{placeholders}` unchanged. For a language with more plural categories than
|
|
English, expand the plural objects (`one`/`few`/`many`/`other` as CLDR
|
|
requires for that language).
|
|
2. **Register it** in the loader's `MESSAGES` map / `translatedLanguages` list:
|
|
- Web: `Web App/web/src/i18n/index.js` — add to the `import`s and `MESSAGES`.
|
|
- Panel: `API Server/panel/src/i18n/index.js` — same.
|
|
- Phone: `Phone App/lib/i18n.dart` — add the code to `translatedLanguages`
|
|
(the file is loaded from `assets/i18n/` automatically; it's covered by the
|
|
`assets/i18n/` directory entry in `pubspec.yaml`).
|
|
3. The Settings picker already lists every European language, so the new one
|
|
becomes selectable immediately and the "not translated yet" hint disappears
|
|
for it. Untranslated keys still fall back to English.
|
|
|
|
No screen code changes are needed to add a language.
|
|
|
|
## Coverage
|
|
|
|
- **Web App** — fully translated (every view, component, form, and the status
|
|
labels in `lib/format.js`).
|
|
- **API Server panel** — UI chrome, cards, login, status, and the API section
|
|
titles are translated. The individual REST endpoint **descriptions** in the
|
|
API reference table are intentionally left in English as developer reference
|
|
documentation.
|
|
- **Phone App** — the i18n system plus the core flows are translated: navigation,
|
|
login, lock screen, dashboard, the full Settings panel (including the language
|
|
picker), and all status/badge wording in `lib/format.dart`. The remaining
|
|
detail screens (car detail, record form sheets, admin users, car form sheet,
|
|
attachment field) still render in English via the fallback until their strings
|
|
are extracted — the pattern to follow is identical to the screens already done.
|