🐛 fix(i18n): 修复系统语言跟随不实时更新

Refs #335
This commit is contained in:
Syngnat
2026-06-28 20:15:56 +08:00
parent 730d4bb1ab
commit fdb05fb8d3
2 changed files with 100 additions and 1 deletions

View File

@@ -543,4 +543,79 @@ describe('main browser mock', () => {
expect(dayjsLocaleMock).toHaveBeenCalledWith('ja');
expect(syncLanguageRuntimeMock.mock.calls.map(([language]) => language)).toEqual(['ja-JP']);
});
it('updates the resolved locale when the system browser language changes', async () => {
const windowListeners = new Map<string, Set<EventListener>>();
const windowMock = {
addEventListener: vi.fn((type: string, listener: EventListener) => {
const listeners = windowListeners.get(type) ?? new Set<EventListener>();
listeners.add(listener);
windowListeners.set(type, listeners);
}),
removeEventListener: vi.fn((type: string, listener: EventListener) => {
windowListeners.get(type)?.delete(listener);
}),
dispatchEvent: vi.fn((event: Event) => {
windowListeners.get(event.type)?.forEach((listener) => listener(event));
return true;
}),
};
vi.stubGlobal('window', windowMock);
let navigatorLanguages = ['en-US'];
const navigatorMock = {};
Object.defineProperty(navigatorMock, 'languages', {
configurable: true,
get: () => navigatorLanguages,
});
Object.defineProperty(navigatorMock, 'language', {
configurable: true,
get: () => navigatorLanguages[0] ?? '',
});
vi.stubGlobal('navigator', navigatorMock);
const setLanguagePreference = vi.fn();
vi.doMock('./store', () => ({
useStore: Object.assign(
<T,>(selector: (state: { languagePreference: string; setLanguagePreference: (nextPreference: string) => void }) => T): T =>
React.useSyncExternalStore(
() => () => {},
() => selector({ languagePreference: 'system', setLanguagePreference }),
() => selector({ languagePreference: 'system', setLanguagePreference }),
),
{
persist: {
hasHydrated: () => true,
onFinishHydration: () => () => {},
},
},
),
}));
let renderer: TestRenderer.ReactTestRenderer | null = null;
renderRootImpl = (node) => {
act(() => {
renderer = TestRenderer.create(node as React.ReactElement);
});
};
await importMain();
const { getCurrentLanguage } = await import('./i18n');
await act(async () => {});
expect(renderer).not.toBeNull();
expect(getCurrentLanguage()).toBe('en-US');
dayjsLocaleMock.mockClear();
syncLanguageRuntimeMock.mockClear();
await act(async () => {
navigatorLanguages = ['zh-CN'];
windowMock.dispatchEvent(new Event('languagechange'));
});
expect(getCurrentLanguage()).toBe('zh-CN');
expect(dayjsLocaleMock).toHaveBeenCalledWith('zh-cn');
expect(syncLanguageRuntimeMock.mock.calls.map(([language]) => language)).toEqual(['zh-CN']);
});
});

View File

@@ -598,6 +598,25 @@ const readBrowserLanguages = (): string[] => {
return navigator.language ? [navigator.language] : [];
};
const serializeBrowserLanguages = (languages: readonly string[]) => languages.join('\n');
const deserializeBrowserLanguages = (snapshot: string) =>
snapshot ? snapshot.split('\n').filter(Boolean) : [];
const getBrowserLanguageSnapshot = () => serializeBrowserLanguages(readBrowserLanguages());
const subscribeBrowserLanguageChange = (listener: () => void) => {
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
return () => {};
}
window.addEventListener('languagechange', listener);
return () => {
if (typeof window.removeEventListener === 'function') {
window.removeEventListener('languagechange', listener);
}
};
};
const subscribeStoreHydration = (listener: () => void) => {
if (useStore.persist.hasHydrated()) {
listener();
@@ -618,12 +637,17 @@ const Root = ({ rootComponent }: { rootComponent: React.ReactNode }) => {
);
const languagePreference = useStore((state) => state.languagePreference);
const setLanguagePreference = useStore((state) => state.setLanguagePreference);
const browserLanguageSnapshot = useSyncExternalStore(
subscribeBrowserLanguageChange,
getBrowserLanguageSnapshot,
getBrowserLanguageSnapshot,
);
if (!isStoreHydrated) {
return null;
}
const systemLanguages = readBrowserLanguages();
const systemLanguages = deserializeBrowserLanguages(browserLanguageSnapshot);
const resolvedLanguage = setCurrentLanguage(languagePreference, systemLanguages);
applyDayjsLocale(resolvedLanguage);