diff --git a/src/stores/__tests__/locale-store.test.ts b/src/stores/__tests__/locale-store.test.ts new file mode 100644 index 00000000..c9fdb6ba --- /dev/null +++ b/src/stores/__tests__/locale-store.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, vi } from 'vitest'; + +vi.mock('react-native', () => ({ + I18nManager: { isRTL: false, allowRTL: () => {}, forceRTL: () => {} }, +})); +vi.mock('expo-localization', () => ({ getLocales: () => [{ languageTag: 'en-US', languageCode: 'en' }] })); +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { + getItem: vi.fn(async () => JSON.stringify({ override: 'de' })), + setItem: vi.fn(async () => undefined), + }, +})); + +import { useLocaleStore } from '../locale-store'; + +describe('locale store', () => { + it('hands out a new t once the saved language loads, so screens re-render in it', async () => { + const before = useLocaleStore.getState().t; + expect(before('sidebar.settings', 'Settings')).toBe('Settings'); + + await useLocaleStore.getState().hydrate(); + + const after = useLocaleStore.getState().t; + expect(after).not.toBe(before); + expect(after('sidebar.settings', 'Settings')).toBe('Einstellungen'); + }); +}); diff --git a/src/stores/locale-store.ts b/src/stores/locale-store.ts index 3a4e8646..8c83f834 100644 --- a/src/stores/locale-store.ts +++ b/src/stores/locale-store.ts @@ -48,6 +48,13 @@ function applyLayoutDirection(locale: LocaleCode): boolean { return manager.isRTL !== rtl; } +// A new `t` per locale: most components select only `t`, and a function +// that never changed identity left them in the device language after the +// saved one loaded on a cold start (tab labels, the folder title). +function translatorFor(locale: LocaleCode): TranslateFn { + return (key, fallback, params) => translate(locale, key, fallback, params); +} + export const useLocaleStore = create((set, get) => ({ locale: detectDeviceLocale(), override: null, @@ -62,7 +69,7 @@ export const useLocaleStore = create((set, get) => ({ const override = stored && isSupportedLocale(stored) ? stored : null; const locale = override ?? detectDeviceLocale(); const directionChangePending = applyLayoutDirection(locale); - set({ override, locale, hydrated: true, directionChangePending }); + set({ override, locale, hydrated: true, directionChangePending, t: translatorFor(locale) }); } catch { set({ hydrated: true }); } @@ -71,11 +78,11 @@ export const useLocaleStore = create((set, get) => ({ setOverride: (override) => { const locale = override ?? detectDeviceLocale(); const directionChangePending = applyLayoutDirection(locale); - set({ override, locale, directionChangePending }); + set({ override, locale, directionChangePending, t: translatorFor(locale) }); void AsyncStorage.setItem(STORAGE_KEY, JSON.stringify({ override })).catch(() => {}); }, - t: (key, fallback, params) => translate(get().locale, key, fallback, params), + t: translatorFor(detectDeviceLocale()), })); // Non-hook accessor for code outside React (background tasks, stores).