fix: make lazy locale selection race-safe

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jamesread
2026-09-10 19:27:49 +01:00
co-authored by Cursor
parent 91f7764de2
commit 7dc27bf7d6
4 changed files with 30 additions and 13 deletions
+17 -6
View File
@@ -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
View File
@@ -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
}
@@ -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
}
}
}
@@ -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')
})