Repository navigation
feat: UI localization with vue-i18n and a per-user language setting - #9
Conversation
Adds vue-i18n with the English string as the message key, so templates
read $t("Report incident") and English needs no message file; Russian
lives in src/locales/ru.json. Vuetify's own strings follow the same
switch through its vue-i18n adapter, and relative dates use the active
locale. A custom messageResolver looks the whole key up before walking
dots as a path, so keys like "Loading... Please wait" resolve while
Vuetify's $vuetify.* messages keep working.
The language is chosen in the profile menu (avatar > Language) with a
button toggle instead of a select: a select's dropdown renders under the
profile menu overlay. The choice is kept in localStorage and in the new
dispatch_user_settings.locale column (migration c6baea6d5ae8) and applied
when /auth/me loads; without a saved choice the UI follows the browser.
This pass translates the shell: toolbar, profile menu, navigation (route
titles and groups), login/register, incident and case tables. Table
headers moved from data() to computed so they re-render on switch.
src/tests/setup.js installs i18n for every mount; i18n.spec.js covers the
resolver and the switch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It combines a database migration, a new runtime dependency, and cross-cutting i18n infrastructure that warrant final human sign-off even though no concrete defects were found.
No open issues 🎉
1 file not reviewed
src/dispatch/static/dispatch/package-lock.json: Generated file
What changed in this PR
This PR introduces the first pass of UI localization for the Dispatch frontend using vue-i18n 11, with English as the natural key and Russian as the first translated locale. It wires vue-i18n into Vuetify via createVueI18nAdapter so one switch changes both app and built-in strings, adds a language toggle to the profile menu, and persists the choice in both localStorage and a new per-user dispatch_user_settings.locale column. The initial translated surface is the application shell (toolbar, drawer, login/register, incident/case tables).
Changes:
- Adds i18n infrastructure (
src/i18n/index.jswith a custommessageResolverfor dotted natural keys,detectLocale/setLocale, Vuetify adapter) plusru.json(125 keys) and thevue-i18ndependency. - Localizes shell components (
AppToolbar,AppDrawer, incident/caseTable.vue,Login,Register) and makes table headers reactive to locale; relative dates infilters.jsuse the active locale. - Persists the language: new backend
localefield (UserSettingsBase, model column) + Alembic migrationc6baea6d5ae8, applied on/auth/meload and saved viaupdateUserSettings.
| File | Description |
|---|---|
src/.../src/i18n/index.js |
New i18n instance, locale detection/persistence, custom message resolver. |
src/.../src/vuetify/index.js |
Wires Vuetify locale to vue-i18n via createVueI18nAdapter. |
src/.../src/main.js |
Registers i18n plugin and applies detected locale before Vuetify. |
src/.../src/locales/ru.json |
Russian translations (flat English→Russian map, 125 keys). |
src/.../src/components/AppToolbar.vue |
Localized strings and adds the language v-btn-toggle that persists the choice. |
src/.../src/components/AppDrawer.vue |
Localizes route titles and navigation group headers. |
src/.../src/incident/Table.vue |
Localizes labels; moves headers from data() to a computed so they re-render on switch. |
src/.../src/case/Table.vue |
Localizes labels; uses useI18n().t() for headers. |
src/.../src/auth/Login.vue, Register.vue |
Localizes form labels/buttons (also fixes "a account" → "an account"). |
src/.../src/auth/userSettings.js |
Applies the saved locale after loading user info. |
src/.../src/filters.js |
Uses the active locale for relative-date formatting. |
src/.../src/tests/setup.js, tests/i18n.spec.js |
Installs i18n globally for mounts; tests resolver/fallback/switch. |
src/.../package.json, package-lock.json |
Adds vue-i18n ^11.4.16 and transitive @intlify/*. |
src/dispatch/auth/models.py |
Adds locale column and locale to UserSettingsBase (max 16 chars). |
src/dispatch/database/.../c6baea6d5ae8.py |
Migration adding nullable locale column. |
docs/.../contributing/index.mdx |
Documents the localization workflow. |
Review effort: Balanced
What
First localization pass: infrastructure, a language switch in the profile, Russian for the application shell.
$t("Report incident"),:label="$t('Search')"). English needs no message file, an untranslated key renders as itself. Russian lives insrc/locales/ru.json(flat map, 125 entries). A custommessageResolverlooks the whole key up before treating dots as a nested path, so"Loading... Please wait"resolves while Vuetify's$vuetify.*messages keep working.createVueI18nAdapter, so one switch changes both application and built-in strings (pagination, dialogs). Relative dates infilters.jsuse the active locale.v-btn-toggle. Av-selectwas tried first: its dropdown renders under the profile menu overlay and clicks land on the menu behind it.localStorage.localeplus a newdispatch_user_settings.localecolumn (migrationc6baea6d5ae8,UserSettingsBase.locale: str | None, max 16 chars,null= follow the browser)./auth/mealready returns settings;userSettings.load()applies the saved locale.data()tocomputed/t()so they re-render on switch). The rest of the ~1100 unique strings follows in batches by section.src/tests/setup.jsinstalls i18n globally for every mount;i18n.spec.jscovers the resolver, fallback, Vuetify messages and the switch. Vitest 320/320, eslint, typecheck, ruff andvite buildpass.Verified on a dispatch-docker stand
Backend files and migration copied into the running container,
dispatch database upgrade, builtdistswapped in. Headless run: toggle in the profile menu switches navigation, headers and Vuetify footer to Russian,PUT /auth/me/settingsstoreslocale: "ru", a reload keeps Russian,GET /auth/me/settingsreturns it; a 20-character locale is rejected with 422;nullresets.Out of scope
Backend notifications and e-mails (English only),
moment-formatted absolute dates, remaining components.🤖 Generated with Claude Code