diff --git a/apps/desktop/src/i18n/context.test.tsx b/apps/desktop/src/i18n/context.test.tsx
index 092b252f66..f0b9b13fac 100644
--- a/apps/desktop/src/i18n/context.test.tsx
+++ b/apps/desktop/src/i18n/context.test.tsx
@@ -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(
+
+
+
+ )
+
+ 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()
+ })
})
diff --git a/apps/desktop/src/i18n/context.tsx b/apps/desktop/src/i18n/context.tsx
index 45fa502592..8d49ea6c2b 100644
--- a/apps/desktop/src/i18n/context.tsx
+++ b/apps/desktop/src/i18n/context.tsx
@@ -99,6 +99,9 @@ export function I18nProvider({ children, configClient = defaultConfigClient, ini
const [configLoadError, setConfigLoadError] = useState(null)
const [saveError, setSaveError] = useState(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)