fix(desktop): a late startup locale read never overrides the user's in-session pick
With the startup config read now retried (previous commits, #76158), a retry can resolve after the user has already chosen a language in Settings and would repaint the stale on-disk value over it. Mark the explicit pick in a ref and let every retry outcome (success or exhausted fallback) defer to it. Idea and guard from #105470 (webtecnica), folded onto the #76158 base. Co-authored-by: webtecnica <webtecnica@gmail.com>
This commit is contained in:
@@ -310,4 +310,41 @@ describe('I18nProvider', () => {
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('a late startup read never overrides a language the user picked mid-retry', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const getConfig = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('backend not ready yet'))
|
||||
.mockResolvedValue({ display: { language: 'en' } })
|
||||
|
||||
const configClient: I18nConfigClient = {
|
||||
getConfig,
|
||||
saveConfig: vi.fn().mockResolvedValue({ ok: true })
|
||||
}
|
||||
|
||||
render(
|
||||
<I18nProvider configClient={configClient}>
|
||||
<LanguageProbe target="ja" />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
await act(async () => {})
|
||||
expect(screen.getByTestId('locale').textContent).toBe('en')
|
||||
|
||||
// User picks Japanese while the startup retry is still pending.
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'switch' }))
|
||||
})
|
||||
expect(screen.getByTestId('locale').textContent).toBe('ja')
|
||||
|
||||
// The retry resolves with the stale on-disk value; the explicit pick wins.
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(3_000)
|
||||
})
|
||||
expect(screen.getByTestId('locale').textContent).toBe('ja')
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -99,6 +99,9 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
|
||||
const [configLoadError, setConfigLoadError] = useState<Error | null>(null)
|
||||
const [saveError, setSaveError] = useState<Error | null>(null)
|
||||
const localeRef = useRef(locale)
|
||||
// Set once the user picks a language through setLocale: a startup read that
|
||||
// resolves (or fails) after that must never overwrite an explicit choice.
|
||||
const userLocaleRef = useRef(false)
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- legitimate non-atom ref write (see eslint rule comment)
|
||||
useEffect(() => {
|
||||
@@ -132,12 +135,12 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
|
||||
return configClient
|
||||
.getConfig()
|
||||
.then(config => {
|
||||
if (!cancelled) {
|
||||
if (!cancelled && !userLocaleRef.current) {
|
||||
setLocaleState(normalizeLocale(getConfigDisplayLanguage(config)))
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (cancelled) {
|
||||
if (cancelled || userLocaleRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -173,6 +176,7 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
|
||||
async (next: Locale) => {
|
||||
const previousLocale = localeRef.current
|
||||
|
||||
userLocaleRef.current = true
|
||||
setSaveError(null)
|
||||
setLocaleState(next)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user