Просмотр исходного кода

fix: make lazy locale selection race-safe

Co-authored-by: Cursor <cursoragent@cursor.com>
jamesread 1 день назад
Родитель
Сommit
7dc27bf7d6

+ 17 - 6
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

+ 1 - 3
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
 }

+ 8 - 4
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
+      }
     }
   }
 

+ 4 - 0
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')
 })