149 lines
3.5 KiB
TypeScript
149 lines
3.5 KiB
TypeScript
|
|
type MockLocale = {
|
||
|
|
languageCode?: string;
|
||
|
|
languageTag?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type I18nModule = typeof import('@/i18n');
|
||
|
|
|
||
|
|
async function flushMicrotasks() {
|
||
|
|
await Promise.resolve();
|
||
|
|
await Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadI18nModule(options?: {
|
||
|
|
locales?: MockLocale[];
|
||
|
|
platformOS?: 'android' | 'ios';
|
||
|
|
}) {
|
||
|
|
let currentLocales = options?.locales ?? [{ languageTag: 'zh-CN' }];
|
||
|
|
const platformOS = options?.platformOS ?? 'ios';
|
||
|
|
const remove = jest.fn();
|
||
|
|
const addEventListener = jest.fn(
|
||
|
|
(_type: string, listener: (state: string) => void) => ({
|
||
|
|
listener,
|
||
|
|
remove,
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
jest.resetModules();
|
||
|
|
|
||
|
|
jest.doMock('expo-localization', () => ({
|
||
|
|
getLocales: () => currentLocales,
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.doMock('react-native', () => {
|
||
|
|
return {
|
||
|
|
AppState: {
|
||
|
|
addEventListener,
|
||
|
|
},
|
||
|
|
Platform: {
|
||
|
|
OS: platformOS,
|
||
|
|
select: (config: Record<string, unknown>) =>
|
||
|
|
config[platformOS] ?? config.default,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
const module = require('@/i18n') as I18nModule;
|
||
|
|
|
||
|
|
return {
|
||
|
|
...module,
|
||
|
|
addEventListener,
|
||
|
|
remove,
|
||
|
|
setLocales: (nextLocales: MockLocale[]) => {
|
||
|
|
currentLocales = nextLocales;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('i18n device synchronization', () => {
|
||
|
|
let consoleInfoSpy: jest.SpyInstance;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
consoleInfoSpy.mockRestore();
|
||
|
|
jest.resetModules();
|
||
|
|
jest.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('resolves zh when the device locale starts with zh', async () => {
|
||
|
|
const { getDeviceLanguage } = loadI18nModule({
|
||
|
|
locales: [{ languageTag: 'zh-Hans-CN' }],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(getDeviceLanguage()).toBe('zh');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('falls back to zh when the device locale is missing', async () => {
|
||
|
|
const { getDeviceLanguage } = loadI18nModule({
|
||
|
|
locales: [{}],
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(getDeviceLanguage()).toBe('zh');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('syncLanguageWithDevice updates i18n when the current language differs', async () => {
|
||
|
|
const { default: i18n, syncLanguageWithDevice } = loadI18nModule({
|
||
|
|
locales: [{ languageTag: 'en-US' }],
|
||
|
|
});
|
||
|
|
|
||
|
|
await i18n.changeLanguage('zh');
|
||
|
|
await syncLanguageWithDevice();
|
||
|
|
|
||
|
|
expect(i18n.resolvedLanguage).toBe('en');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('startLocaleSync subscribes on Android and refreshes language when the app becomes active', async () => {
|
||
|
|
const {
|
||
|
|
addEventListener,
|
||
|
|
default: i18n,
|
||
|
|
setLocales,
|
||
|
|
startLocaleSync,
|
||
|
|
} = loadI18nModule({
|
||
|
|
locales: [{ languageTag: 'zh-CN' }],
|
||
|
|
platformOS: 'android',
|
||
|
|
});
|
||
|
|
|
||
|
|
await i18n.changeLanguage('zh');
|
||
|
|
|
||
|
|
const subscription = startLocaleSync();
|
||
|
|
|
||
|
|
expect(addEventListener).toHaveBeenCalledWith(
|
||
|
|
'change',
|
||
|
|
expect.any(Function),
|
||
|
|
);
|
||
|
|
|
||
|
|
const listener = addEventListener.mock.calls[0][1] as (
|
||
|
|
state: string,
|
||
|
|
) => void;
|
||
|
|
|
||
|
|
setLocales([{ languageTag: 'en-US' }]);
|
||
|
|
listener('background');
|
||
|
|
await flushMicrotasks();
|
||
|
|
|
||
|
|
expect(i18n.resolvedLanguage).toBe('zh');
|
||
|
|
|
||
|
|
listener('active');
|
||
|
|
await flushMicrotasks();
|
||
|
|
|
||
|
|
expect(i18n.resolvedLanguage).toBe('en');
|
||
|
|
subscription.remove();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('startLocaleSync is a no-op on iOS', async () => {
|
||
|
|
const { addEventListener, startLocaleSync } = loadI18nModule({
|
||
|
|
locales: [{ languageTag: 'en-US' }],
|
||
|
|
platformOS: 'ios',
|
||
|
|
});
|
||
|
|
|
||
|
|
const subscription = startLocaleSync();
|
||
|
|
|
||
|
|
expect(addEventListener).not.toHaveBeenCalled();
|
||
|
|
expect(subscription.remove).toEqual(expect.any(Function));
|
||
|
|
|
||
|
|
subscription.remove();
|
||
|
|
});
|
||
|
|
});
|