fix: FireFox regional setting (#30377)

* fix: keep en-US as the last i18n fallback when a stored locale has no bundle

Settings labels rendered as raw keys such as settings.admin.connections.title
instead of "Connections" from the second page load on, while every other
label looked fine.

On the first visit the language detector saves whatever the browser reports
into localStorage, including bare codes like "en" that have no locale bundle.
The same value is saved by ?lang=en or DEFAULT_LOCALE=en. On the next load
that stored value became the only fallback language, so the missing bundle
left nothing to fall back to. Plain keys still looked right because the key
is the English text; only the settings.* keys have a distinct value.

en-US now stays at the end of the fallback list whenever a stored locale is
passed in. The stored locale keeps precedence, real locales are unaffected,
and the en-US bundle was already loaded for every language for the settings
merge, so there is no extra request.

Fixes #30348

* fix: match browser language codes to a locale bundle

Firefox reports German, Dutch and Polish as bare codes (de, nl, pl), both Chrome and Firefox report Japanese as ja, and Chrome in Latin America reports es-419. No bundle is keyed by any of these. The page layout used to match them on the first visit, but since 67ac1a4e9 (0.11.4) awaits the i18n init, the detector has already cached the raw code by the time that check runs, so it never does. Those users get an English interface, a language dropdown with nothing selected and English date formats, and the cached code keeps them there.

The detector now maps a reported code onto a bundle before anything else sees it. A code we ship stays as it is, matched case-insensitively. Otherwise the language's xx-XX bundle is used when there is one, so de and de-AT become de-DE, es-419 becomes es-ES, and fr becomes fr-FR even though fr-CA comes first in the list. A bare code with no xx-XX bundle takes the first one for its language, so ja becomes ja-JP. Any other regional code passes through untouched, so zh-HK is not sent to Simplified zh-CN, and no locale that works today changes.

Restoring the layout check would leave every browser that cached a raw code under 0.11.4 stuck. Matching in the detector migrates them on their next load, because the matched code is what gets cached. {{USER_LANGUAGE}} reports that matched code too, so es-419 users now send es-ES.

Verified against the real bundles on first and repeat loads, including the untouched cases fr-CA, pt-BR, zh-TW, zh-HK, ar-BH, en-GB and uz-Latn-UZ.
This commit is contained in:
Classic298
2026-09-22 16:46:58 -04:00
committed by GitHub
parent 1a74a9f46c
commit d603b57995
+17 -5
View File
@@ -6,6 +6,7 @@ import { writable } from 'svelte/store';
import type { I18nOverrides } from '$lib/utils/translationDictionary';
import { assembleSettingsTranslations } from './settings-translations';
import languages from './locales/languages.json';
let overrides: I18nOverrides = {};
@@ -71,6 +72,19 @@ const createIsLoadingStore = (i18n: i18nType) => {
return isLoading;
};
const languageCodes = languages.map(({ code }) => code);
const toBundleCode = (code: string) => {
const ownBundle = languageCodes.find((bundle) => bundle.toLowerCase() === code.toLowerCase());
if (ownBundle) return ownBundle;
const baseLanguage = code.split('-')[0].toLowerCase();
const primaryBundle = `${baseLanguage}-${baseLanguage.toUpperCase()}`;
if (languageCodes.includes(primaryBundle)) return primaryBundle;
// the first sibling of a regional code can be another script (zh-HK would get zh-CN)
if (code.includes('-')) return code;
return languageCodes.find((bundle) => bundle.startsWith(`${baseLanguage}-`)) ?? code;
};
export const initI18n = (defaultLocale?: string, value: I18nOverrides = {}) => {
overrides = value;
const detectionOrder = defaultLocale
@@ -87,7 +101,8 @@ export const initI18n = (defaultLocale?: string, value: I18nOverrides = {}) => {
order: detectionOrder,
caches: ['localStorage'],
lookupQuerystring: 'lang',
lookupLocalStorage: 'locale'
lookupLocalStorage: 'locale',
convertDetectedLanguage: toBundleCode
},
fallbackLng: {
fr: ['fr-FR'],
@@ -106,10 +121,7 @@ export const initI18n = (defaultLocale?: string, value: I18nOverrides = {}) => {
const i18n = createI18nStore(i18next);
const isLoadingStore = createIsLoadingStore(i18next);
export const getLanguages = async () => {
const languages = (await import(`./locales/languages.json`)).default;
return languages;
};
export const getLanguages = async () => languages;
export const changeLanguage = (lang: string) => {
document.documentElement.setAttribute('lang', lang);
return i18next.changeLanguage(lang);