diff --git a/app/i18n/index.test.ts b/app/i18n/index.test.ts new file mode 100644 index 000000000..029a54bde --- /dev/null +++ b/app/i18n/index.test.ts @@ -0,0 +1,204 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import moment from 'moment'; +import {getLocales} from 'react-native-localize'; + +import en from '@assets/i18n/en.json'; +import * as logUtils from '@utils/log'; + +import { + getLocaleFromLanguage, + resetMomentLocale, + getTranslations, + getLocalizedMessage, + DEFAULT_LOCALE, +} from './index'; + +jest.mock('react-native-localize', () => ({ + getLocales: jest.fn().mockReturnValue([{languageTag: 'en'}]), +})); + +jest.mock('@utils/log', () => ({ + logError: jest.fn(), +})); + +jest.mock('@assets/i18n/en.json', () => ({test: 'Test', hello: 'Hello'})); +jest.mock('@assets/i18n/bg.json', () => ({test: 'Тест', hello: 'Здравейте'})); +jest.mock('@assets/i18n/de.json', () => ({test: 'Test', hello: 'Hallo'})); +jest.mock('@assets/i18n/en_AU.json', () => ({test: 'Test', hello: 'G\'day'})); +jest.mock('@assets/i18n/es.json', () => ({test: 'Prueba', hello: 'Hola'})); +jest.mock('@assets/i18n/fa.json', () => ({test: 'تست', hello: 'سلام'})); +jest.mock('@assets/i18n/fr.json', () => ({test: 'Test', hello: 'Bonjour'})); +jest.mock('@assets/i18n/hu.json', () => ({test: 'Teszt', hello: 'Helló'})); +jest.mock('@assets/i18n/it.json', () => ({test: 'Test', hello: 'Ciao'})); +jest.mock('@assets/i18n/ja.json', () => ({test: 'テスト', hello: 'こんにちは'})); +jest.mock('@assets/i18n/ko.json', () => ({test: '테스트', hello: '안녕하세요'})); +jest.mock('@assets/i18n/nl.json', () => ({test: 'Test', hello: 'Hallo'})); +jest.mock('@assets/i18n/pl.json', () => ({test: 'Test', hello: 'Cześć'})); +jest.mock('@assets/i18n/pt-BR.json', () => ({test: 'Teste', hello: 'Olá'})); +jest.mock('@assets/i18n/ro.json', () => ({test: 'Test', hello: 'Bună'})); +jest.mock('@assets/i18n/ru.json', () => ({test: 'Тест', hello: 'Привет'})); +jest.mock('@assets/i18n/sv.json', () => ({test: 'Test', hello: 'Hej'})); +jest.mock('@assets/i18n/tr.json', () => ({test: 'Test', hello: 'Merhaba'})); +jest.mock('@assets/i18n/uk.json', () => ({test: 'Тест', hello: 'Привіт'})); +jest.mock('@assets/i18n/vi.json', () => ({test: 'Kiểm tra', hello: 'Xin chào'})); +jest.mock('@assets/i18n/zh-CN.json', () => ({test: '测试', hello: '你好'})); +jest.mock('@assets/i18n/zh-TW.json', () => ({test: '測試', hello: '你好'})); + +describe('i18n', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(getLocales).mockReturnValue([{ + languageTag: 'en', + languageCode: '', + countryCode: '', + isRTL: false, + }]); + }); + + describe('getLocaleFromLanguage', () => { + it('returns correct locale for full language code', () => { + expect(getLocaleFromLanguage('es-ES')).toBe('es'); + expect(getLocaleFromLanguage('pt-BR')).toBe('pt-BR'); + expect(getLocaleFromLanguage('zh-TW')).toBe('zh-TW'); + expect(getLocaleFromLanguage('zh-CN')).toBe('zh-CN'); + expect(getLocaleFromLanguage('en-AU')).toBe('en-AU'); + }); + + it('returns correct locale for language code', () => { + expect(getLocaleFromLanguage('es')).toBe('es'); + expect(getLocaleFromLanguage('en')).toBe('en'); + expect(getLocaleFromLanguage('fr')).toBe('fr'); + expect(getLocaleFromLanguage('de')).toBe('de'); + expect(getLocaleFromLanguage('ja')).toBe('ja'); + }); + + it('returns default locale for unsupported language', () => { + expect(getLocaleFromLanguage('xx')).toBe('en'); + expect(getLocaleFromLanguage('')).toBe('en'); + expect(getLocaleFromLanguage('not-valid')).toBe('en'); + }); + }); + + describe('resetMomentLocale', () => { + let spy: jest.SpyInstance; + + beforeEach(() => { + spy = jest.spyOn(moment, 'locale'); + }); + + afterEach(() => { + spy.mockRestore(); + }); + + it('sets moment locale correctly for various languages', () => { + resetMomentLocale('es-ES'); + expect(spy).toHaveBeenCalledWith('es'); + + resetMomentLocale('en'); + expect(spy).toHaveBeenCalledWith('en'); + + resetMomentLocale('zh-TW'); + expect(spy).toHaveBeenCalledWith('zh'); + + resetMomentLocale('pt-BR'); + expect(spy).toHaveBeenCalledWith('pt'); + }); + + it('uses default locale when no locale provided', () => { + resetMomentLocale(); + expect(spy).toHaveBeenCalledWith(DEFAULT_LOCALE.split('-')[0]); + }); + + it('handles invalid locales gracefully', () => { + resetMomentLocale('invalid-locale'); + expect(spy).toHaveBeenCalledWith('invalid'); + + resetMomentLocale(''); + expect(spy).toHaveBeenCalledWith(DEFAULT_LOCALE.split('-')[0]); + }); + }); + + describe('getTranslations', () => { + beforeEach(() => { + (logUtils.logError as jest.Mock).mockClear(); + }); + + it('returns correct translations for all supported locales', () => { + const testCases = [ + {locale: 'bg', hello: 'Здравейте', test: 'Тест'}, + {locale: 'de', hello: 'Hallo', test: 'Test'}, + {locale: 'en', hello: 'Hello', test: 'Test'}, + {locale: 'en-AU', hello: 'G\'day', test: 'Test'}, + {locale: 'es', hello: 'Hola', test: 'Prueba'}, + {locale: 'fa', hello: 'سلام', test: 'تست'}, + {locale: 'fr', hello: 'Bonjour', test: 'Test'}, + {locale: 'hu', hello: 'Helló', test: 'Teszt'}, + {locale: 'it', hello: 'Ciao', test: 'Test'}, + {locale: 'ja', hello: 'こんにちは', test: 'テスト'}, + {locale: 'ko', hello: '안녕하세요', test: '테스트'}, + {locale: 'nl', hello: 'Hallo', test: 'Test'}, + {locale: 'pl', hello: 'Cześć', test: 'Test'}, + {locale: 'pt-BR', hello: 'Olá', test: 'Teste'}, + {locale: 'ro', hello: 'Bună', test: 'Test'}, + {locale: 'ru', hello: 'Привет', test: 'Тест'}, + {locale: 'sv', hello: 'Hej', test: 'Test'}, + {locale: 'tr', hello: 'Merhaba', test: 'Test'}, + {locale: 'uk', hello: 'Привіт', test: 'Тест'}, + {locale: 'vi', hello: 'Xin chào', test: 'Kiểm tra'}, + {locale: 'zh-CN', hello: '你好', test: '测试'}, + {locale: 'zh-TW', hello: '你好', test: '測試'}, + ]; + + testCases.forEach(({locale, hello, test: testWord}) => { + const translations = getTranslations(locale); + expect(translations.hello).toBe(hello); + expect(translations.test).toBe(testWord); + }); + }); + + it('returns english translations for unsupported locale', () => { + const translations = getTranslations('xx'); + expect(translations).toEqual(en); + expect(logUtils.logError).not.toHaveBeenCalled(); + }); + + it('loads correct polyfills for Chinese locales', () => { + const zhCNTranslations = getTranslations('zh-CN'); + expect(zhCNTranslations.hello).toBe('你好'); + expect(zhCNTranslations.test).toBe('测试'); + + const zhTWTranslations = getTranslations('zh-TW'); + expect(zhTWTranslations.hello).toBe('你好'); + expect(zhTWTranslations.test).toBe('測試'); + }); + }); + + describe('getLocalizedMessage', () => { + it('returns correct message for existing key in various languages', () => { + expect(getLocalizedMessage('en', 'test')).toBe('Test'); + expect(getLocalizedMessage('es', 'test')).toBe('Prueba'); + expect(getLocalizedMessage('en', 'hello')).toBe('Hello'); + expect(getLocalizedMessage('es', 'hello')).toBe('Hola'); + }); + + it('returns default message when key not found', () => { + expect(getLocalizedMessage('en', 'nonexistent', 'Default')).toBe('Default'); + expect(getLocalizedMessage('es', 'nonexistent', 'Por defecto')).toBe('Por defecto'); + }); + + it('returns empty string when no translation or default', () => { + expect(getLocalizedMessage('en', 'nonexistent')).toBe(''); + expect(getLocalizedMessage('es', 'nonexistent')).toBe(''); + }); + + it('handles invalid inputs gracefully', () => { + expect(getLocalizedMessage('', 'test')).toBe('Test'); + expect(getLocalizedMessage('invalid-locale', 'test')).toBe('Test'); + expect(getLocalizedMessage('en', '')).toBe(''); + expect(getLocalizedMessage('en', undefined as unknown as string)).toBe(''); + expect(getLocalizedMessage('en', null as unknown as string)).toBe(''); + }); + }); +}); diff --git a/app/i18n/index.ts b/app/i18n/index.ts index a04eb1930..c5aad9124 100644 --- a/app/i18n/index.ts +++ b/app/i18n/index.ts @@ -10,8 +10,8 @@ import {logError} from '@utils/log'; import availableLanguages from './languages'; -const deviceLocale = getLocales()[0].languageTag; const PRIMARY_LOCALE = 'en'; +const deviceLocale = getLocales()[0]?.languageTag || PRIMARY_LOCALE; export const DEFAULT_LOCALE = getLocaleFromLanguage(deviceLocale); function loadTranslation(locale?: string): {[x: string]: string} {