Skip to content

feat: UI localization with vue-i18n and a per-user language setting - #9

Merged
helldweller merged 1 commit into
mainfrom
feat/i18n
Oct 26, 2026
Merged

helldweller merged 1 commit into
mainfrom
feat/i18n

Conversation

@helldweller

Copy link
Copy Markdown
Owner

What

First localization pass: infrastructure, a language switch in the profile, Russian for the application shell.

  • vue-i18n 11 with natural keys: the English string is the key ($t("Report incident"), :label="$t('Search')"). English needs no message file, an untranslated key renders as itself. Russian lives in src/locales/ru.json (flat map, 125 entries). A custom messageResolver looks the whole key up before treating dots as a nested path, so "Loading... Please wait" resolves while Vuetify's $vuetify.* messages keep working.
  • Vuetify uses createVueI18nAdapter, so one switch changes both application and built-in strings (pagination, dialogs). Relative dates in filters.js use the active locale.
  • Language switch in the profile menu (avatar → Language) as a v-btn-toggle. A v-select was tried first: its dropdown renders under the profile menu overlay and clicks land on the menu behind it.
  • Persistence: localStorage.locale plus a new dispatch_user_settings.locale column (migration c6baea6d5ae8, UserSettingsBase.locale: str | None, max 16 chars, null = follow the browser). /auth/me already returns settings; userSettings.load() applies the saved locale.
  • Translated now: toolbar, profile menu, navigation drawer (route titles and setting groups), login/register, incident and case tables (headers moved from data() to computed / t() so they re-render on switch). The rest of the ~1100 unique strings follows in batches by section.
  • Tests: src/tests/setup.js installs i18n globally for every mount; i18n.spec.js covers the resolver, fallback, Vuetify messages and the switch. Vitest 320/320, eslint, typecheck, ruff and vite build pass.
  • Docs: "Localization" section in the contributing guide.

Verified on a dispatch-docker stand

Backend files and migration copied into the running container, dispatch database upgrade, built dist swapped in. Headless run: toggle in the profile menu switches navigation, headers and Vuetify footer to Russian, PUT /auth/me/settings stores locale: "ru", a reload keeps Russian, GET /auth/me/settings returns it; a 20-character locale is rejected with 422; null resets.

Out of scope

Backend notifications and e-mails (English only), moment-formatted absolute dates, remaining components.

🤖 Generated with Claude Code

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>
@helldweller helldweller added the enhancement New feature or request label Oct 26, 2026
@helldweller
helldweller requested a balanced review from Copilot October 26, 2026 03:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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.js with a custom messageResolver for dotted natural keys, detectLocale/setLocale, Vuetify adapter) plus ru.json (125 keys) and the vue-i18n dependency.
  • Localizes shell components (AppToolbar, AppDrawer, incident/case Table.vue, Login, Register) and makes table headers reactive to locale; relative dates in filters.js use the active locale.
  • Persists the language: new backend locale field (UserSettingsBase, model column) + Alembic migration c6baea6d5ae8, applied on /auth/me load and saved via updateUserSettings.
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

@helldweller
helldweller merged commit 20fcfbf into main Oct 26, 2026
11 of 12 checks passed
@helldweller
helldweller deleted the feat/i18n branch October 26, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants