i18n: keep the page's locale in a vue-i18n instance

The application views install one vue-i18n instance that holds the active
locale and the catalogs. src/i18n/index.js stays free of Vue: bindLocale()
keeps its locale in the instance's ref, so t(), tn() and the Intl formatters
read it from there and a view that translates while rendering renders again
when the locale changes. A tag written straight to the ref is normalized.
Message formatting, catalogs and call sites are unchanged, and switching
language still reloads the page.

The instance outlives the views: unmounting one no longer disposes it. The
browser build defines vue-i18n's feature flags and drops its message
compiler, which Core does not use.
This commit is contained in:
Sameh Khamis committed 2026-11-01 15:25:55 -08:00
1 parent 22d4cf8ee7
commit 2c72a038d5
12 files changed
+330 -44

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import { createI18n } from 'vue-i18n';
import { bindLocale, getCatalog, getLocale } from '../i18n/index.js';
import { DEFAULT_LOCALE, LOCALES } from '../i18n/locale.js';
let pageI18n = null;
// Catalogs are flat and keyed by English text, which holds dots.
const resolveFlat = (messages, key) =>
Object.hasOwn(messages, key) ? messages[key] : null;
/**
* The page's vue-i18n instance, installed in every application view. It
* holds the active locale and the catalogs; Core's t(), tn() and the Intl
* formatters read the locale from it, so a view that calls them while
* rendering re-renders when the locale changes. Views translate through
* t()/tn() rather than vue-i18n's own t: English is the key, `context` picks
* an entry, and plural forms follow Intl.PluralRules (docs/I18N.md).
* @returns {import('vue-i18n').I18n}
*/
export function applicationI18n() {
if (pageI18n) return pageI18n;
const messages = {};
for (const locale of LOCALES)
if (locale !== DEFAULT_LOCALE) messages[locale] = getCatalog(locale);
pageI18n = createI18n({
legacy: false,
globalInjection: false,
locale: getLocale(),
fallbackLocale: DEFAULT_LOCALE,
messages,
messageResolver: resolveFlat,
missingWarn: false,
fallbackWarn: false,
});
// vue-i18n disposes its instance when an app it is installed in unmounts;
// the page's outlives every view, so an unmounted panel keeps it.
const install = pageI18n.install;
pageI18n.install = (app, ...options) => {
const unmount = app.unmount;
const installed = install.call(pageI18n, app, ...options);
app.unmount = unmount;
return installed;
};
bindLocale(pageI18n.global.locale);
return pageI18n;
}