diff --git a/frontend/resources/vue/App.vue b/frontend/resources/vue/App.vue index b7226bf1..ffde4109 100644 --- a/frontend/resources/vue/App.vue +++ b/frontend/resources/vue/App.vue @@ -188,7 +188,7 @@ import { connectEventStreamIfNeeded } from '../../js/websocket.js' import { DashboardSquare01Icon } from '@hugeicons/core-free-icons' import logoUrl from '../../OliveTinLogo.png' import { useI18n } from 'vue-i18n' -import { activateLocale, resolveBrowserLocale } from './i18n.js' +import { ensureLocaleMessages, resolveBrowserLocale } from './i18n.js' import { searchIndexItems, clearSearchIndex, indexSystemNavigation, indexSearchHints, indexRootDashboardEntries } from './stores/searchIndex.js' import { applyThemeStyles } from './utils/themeLoader.js' const { t } = useI18n() @@ -220,6 +220,7 @@ const browserLanguages = ref([]) const initialLanguagePreference = typeof window !== 'undefined' ? localStorage.getItem('olivetin-language') : null const languagePreference = ref(initialLanguagePreference || 'auto') const selectedLanguage = ref(languagePreference.value) +let latestLanguageChange = 0 const themeDialog = ref(null) const availableThemes = ref([]) @@ -499,14 +500,24 @@ async function changeLanguage () { return } - if (selectedLanguage.value === 'auto') { + const languageChange = ++latestLanguageChange + const requestedPreference = selectedLanguage.value + const requestedLocale = requestedPreference === 'auto' ? resolveBrowserLocale() : requestedPreference + + await ensureLocaleMessages(window.i18n, requestedLocale) + + if (languageChange !== latestLanguageChange) { + return + } + + window.i18n.locale.value = requestedLocale + + if (requestedPreference === 'auto') { localStorage.removeItem('olivetin-language') languagePreference.value = 'auto' - await activateLocale(window.i18n, resolveBrowserLocale()) } else { - await activateLocale(window.i18n, selectedLanguage.value) - localStorage.setItem('olivetin-language', selectedLanguage.value) - languagePreference.value = selectedLanguage.value + localStorage.setItem('olivetin-language', requestedPreference) + languagePreference.value = requestedPreference } // Update navigation with new translations diff --git a/frontend/resources/vue/i18n.js b/frontend/resources/vue/i18n.js index 5460e65f..b8f06ddf 100644 --- a/frontend/resources/vue/i18n.js +++ b/frontend/resources/vue/i18n.js @@ -46,10 +46,8 @@ export async function loadInitialMessages (locale) { return Object.fromEntries(loadedMessages) } -export async function activateLocale (i18n, locale) { +export async function ensureLocaleMessages (i18n, locale) { if (!i18n.availableLocales.includes(locale)) { i18n.setLocaleMessage(locale, await loadLocaleMessages(locale)) } - - i18n.locale.value = locale } diff --git a/frontend/resources/vue/utils/localeSelection.js b/frontend/resources/vue/utils/localeSelection.js index 4a84d326..b9ab36a6 100644 --- a/frontend/resources/vue/utils/localeSelection.js +++ b/frontend/resources/vue/utils/localeSelection.js @@ -7,11 +7,15 @@ export function selectBrowserLocale (availableLocales, browserLanguages) { return exact } - const language = lowerCandidate.split('-')[0] - const prefix = availableLocales.find(locale => locale.toLowerCase().startsWith(`${language}-`)) + const parts = lowerCandidate.split('-') - if (prefix) { - return prefix + for (let length = parts.length - 1; length > 0; length--) { + const prefix = `${parts.slice(0, length).join('-')}-` + const match = availableLocales.find(locale => locale.toLowerCase().startsWith(prefix)) + + if (match) { + return match + } } } diff --git a/frontend/resources/vue/utils/localeSelection.test.mjs b/frontend/resources/vue/utils/localeSelection.test.mjs index 558cce32..5e3ce379 100644 --- a/frontend/resources/vue/utils/localeSelection.test.mjs +++ b/frontend/resources/vue/utils/localeSelection.test.mjs @@ -13,6 +13,10 @@ test('selectBrowserLocale falls back to a matching language', () => { assert.equal(selectBrowserLocale(availableLocales, ['de-AT']), 'de-DE') }) +test('selectBrowserLocale preserves script subtags during fallback', () => { + assert.equal(selectBrowserLocale(availableLocales, ['zh-Hant-HK']), 'zh-Hant-TW') +}) + test('selectBrowserLocale uses English when no locale matches', () => { assert.equal(selectBrowserLocale(availableLocales, ['fr-FR']), 'en') })