Skip to content

fix(i18n): translate route titles once locale messages are loaded - #4231

Merged
gantoine merged 3 commits into
rommapp:masterfrom
sdornan:claude/settings-translation-title-bug-871d77
Aug 19, 2026
Merged

fix(i18n): translate route titles once locale messages are loaded#4231
gantoine merged 3 commits into
rommapp:masterfrom
sdornan:claude/settings-translation-title-bug-871d77

Conversation

@sdornan

@sdornan sdornan commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Opening a route in a new tab (or reloading one directly) showed the raw i18n key as the browser tab title — e.g. settings.scan-settings instead of "Scan settings". Most visible on the settings pages, since their views don't set a title of their own.

Two causes, both on the tab-title path:

  1. Locale messages weren't loaded when the router guard ran. loadLocales() used import.meta.glob without eager and filled the messages object inside async callbacks that nobody awaited, so namespaces landed a few ticks after createI18n. On a fresh page load the beforeEach guard ran first, and t("settings.scan-settings") returned the key. In-app navigation looked fine because the chunks had landed by then.
  2. Route definitions translated at module-eval time. Every meta.title was built with i18n.global.t(...) while the route table was constructed — before any messages existed — and the guard then translated the result a second time.

Changes:

  • locales/index.ts — adds a memoized loadLocale() that fetches a language's namespaces and registers them as one bundle, plus a localesReady promise covering the two bundles the first paint can need (the fallback and the stored language). Other languages load on demand when switched to, so boot doesn't block on all 19. Namespace loads are fail-soft, so one stale chunk after a redeploy can't block boot.
  • main.ts — awaits localesReady alongside initializeData() before app.use(router), so the first guard run always has messages.
  • plugins/router.tsmeta.title now holds the plain i18n key, a single applyRouteTitle() helper does the translating, and a watcher on the locale loads the incoming language before re-translating the current route's title. The two literal titles are gone: the controller-debug tool uses the settings.controller-debug key that already existed, and the root route drops its redundant "RomM" (the guard falls back to that anyway).

That last watcher fixes a second, latent bug: the user's stored language is applied when RomM.vue mounts, after the first navigation, so even with messages loaded a non-English user's new tab would have shown the English title. A fresh load of /scan-settings with ja_JP now correctly reads スキャン設定.

No translation strings were added or changed: every key involved already exists in all 19 locales (including settings.controller-debug, which now gives that page a translated tab title instead of hardcoded English).

Verification

Reproduced against the unpatched code in a browser: a fresh load of /scan-settings set document.title to settings.scan-settings. After the fix, fresh loads of /scan-settings, /user-interface, /administration and /metadata-sources all show real titles, router pushes across the remaining settings routes are correct, and switching language live updates the title.

On-demand loading verified too: a fresh load with ja_JP stored fetched only the en_US and ja_JP bundles, and switching to fr_FR (never loaded that session) pulled it in and updated the title to Paramètres de scan.

Note: the browser checks ran against a small stub backend (no RomM backend available locally), so the settings pages rendered with stub data — the guard logic exercised is identical.

npm run typecheck, npm run test (737 passed), check_i18n_locales.py, check_i18n_sorted.py, prettier and eslint all pass. Trunk isn't installed in this environment, so its underlying JS linters were run directly.

Checklist

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR
  • I've added unit tests that cover the changes

AI assistance disclosure

This change was written with AI assistance (Claude Code). The AI diagnosed the bug, wrote the fix and the regression test, and ran the verification described above; the result was reviewed by me before submitting.

Opening a route in a new tab showed the raw i18n key (e.g.
"settings.scan-settings") as the tab title. Locale namespaces load as
separate chunks and nothing awaited them, so the router guard resolved
the title before any messages existed. Route definitions compounded it
by calling `t()` at module-eval time, baking the key into `meta.title`
and leaving the guard to translate it a second time.

Bootstrap now awaits a `localesReady` promise before installing the
router, `meta.title` holds the plain key, and a locale watcher
re-translates the current title — the stored language is applied when
the app mounts, after the first navigation guard has already run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 22:06
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR defers initial router navigation until locale namespaces have loaded and stores untranslated i18n keys in route metadata, translating them when navigation resolves. It also reapplies route-owned titles when the active locale changes and adds coverage ensuring route titles resolve from loaded messages.

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified in the changed paths.

Locale messages are available before the first navigation guard translates route metadata, and later locale changes reapply titles only for routes whose titles are router-owned.

Important Files Changed

Filename Overview
frontend/src/locales/index.ts Introduces an awaitable, fail-soft locale-loading barrier and registers successfully loaded namespace bundles before routing begins.
frontend/src/main.ts Awaits locale loading alongside startup data before installing the router.
frontend/src/plugins/router.ts Converts translated route metadata to i18n keys, centralizes title application, and refreshes route-owned titles after locale changes.
frontend/src/plugins/router.test.ts Verifies nonliteral route titles remain i18n keys and resolve after locale messages load.

Reviews (1): Last reviewed commit: "fix(i18n): translate route titles once l..." | Re-trigger Greptile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes initial browser tab titles showing raw vue-i18n keys on fresh loads by ensuring locale messages are available before the first router guard runs, and by storing route title metadata as i18n keys (translated at navigation time) instead of eagerly translating at module evaluation time.

Changes:

  • Added a localesReady bootstrap promise that registers locale namespaces via setLocaleMessage().
  • Awaited localesReady during app initialization before installing the router, and centralized title translation in the router via applyRouteTitle() plus a locale-change watcher.
  • Added a regression test asserting route meta.title values are i18n keys that resolve against loaded messages.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
frontend/src/plugins/router.ts Stores meta.title as i18n keys and applies translated document.title per navigation, with a locale watcher to re-apply on language change.
frontend/src/plugins/router.test.ts Adds a unit test ensuring route titles are stored as i18n keys and translate after locales load.
frontend/src/main.ts Awaits locale message readiness before installing the router to prevent the first guard from translating without messages.
frontend/src/locales/index.ts Reworks locale loading to async-register all namespaced messages and exports localesReady for boot gating.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontend/src/plugins/router.ts
Comment thread frontend/src/locales/index.ts Outdated
Comment thread frontend/src/locales/index.ts Outdated
sdornan and others added 2 commits August 18, 2026 17:20
Review follow-ups:

- Only the fallback and stored languages are awaited at boot; the rest
  load when switched to, so startup no longer blocks on every locale
  bundle. The locale watcher awaits the incoming language before
  re-applying the route title.
- `settings.controller-debug` already exists, so the last literal
  route title becomes a key, and the root route drops its redundant
  "RomM" title (the guard already falls back to it). Every `meta.title`
  is now an i18n key, which the test no longer has to exempt.
- Drop the em-dash from the new comment per the repo writing rule, and
  the trailing space in the locale load error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`useI18n()` already resolves to the global scope while no SFC declares an
`<i18n>` block, but the components that switch the app's language now say
so, per the vue-i18n scope guide. An `<i18n>` block added to either SFC
would otherwise flip the write to component-local and silently stop the
language switch from reaching the rest of the app.

The v1 language selector is left alone: it writes the locale the same
way, but v1 is frozen and this is a defensive change, not a bug fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gantoine
gantoine merged commit 2ed4861 into rommapp:master Aug 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants