From 4cb41435ffe422fbabd3426006fcff4f59a7d148 Mon Sep 17 00:00:00 2001 From: "denys.oblohin" Date: Tue, 27 Aug 2024 18:36:21 +0300 Subject: [PATCH] added tests --- src/v3/src/util/languageUtils.ts | 18 +-- src/v3/src/util/locUtil.test.ts | 219 +++++++++++++++++++++++-------- 2 files changed, 173 insertions(+), 64 deletions(-) diff --git a/src/v3/src/util/languageUtils.ts b/src/v3/src/util/languageUtils.ts index 2b56d50ff8..9d09d92d87 100644 --- a/src/v3/src/util/languageUtils.ts +++ b/src/v3/src/util/languageUtils.ts @@ -24,9 +24,9 @@ export const initDefaultLanguage = () => { // Load translations for default language from Bundles to i18next const languageCode = Bundles.currentLanguage ?? config.defaultLanguage; const isDefaultLanguage = !Bundles.currentLanguage; - if (isDefaultLanguage && !i18next?.hasResourceBundle(languageCode, 'login')) { - i18next?.addResourceBundle(languageCode, 'login', Bundles.login); - i18next?.addResourceBundle(languageCode, 'country', Bundles.country); + if (isDefaultLanguage && !i18next.hasResourceBundle(languageCode, 'login')) { + i18next.addResourceBundle(languageCode, 'login', Bundles.login); + i18next.addResourceBundle(languageCode, 'country', Bundles.country); } }; @@ -53,9 +53,9 @@ export const loadLanguage = async (widgetProps: WidgetProps): Promise => { }, supportedLanguages, omitDefaultKeys); // Load translations from Bundles to i18next and change language - i18next?.addResourceBundle(languageCode, 'login', Bundles.login); - i18next?.addResourceBundle(languageCode, 'country', Bundles.country); - i18next?.changeLanguage(languageCode); + i18next.addResourceBundle(languageCode, 'login', Bundles.login); + i18next.addResourceBundle(languageCode, 'country', Bundles.country); + i18next.changeLanguage(languageCode); }; export const unloadLanguage = (languageCode: LanguageCode) => { @@ -64,10 +64,10 @@ export const unloadLanguage = (languageCode: LanguageCode) => { // For dev environment with HMR don't clear translations. // Otherwise during HMR widget language will be reset to default one. // `Bundles.remove()` also doesn't reset bundles to default language. - i18next?.removeResourceBundle(languageCode, 'login'); - i18next?.removeResourceBundle(languageCode, 'country'); + i18next.removeResourceBundle(languageCode, 'login'); + i18next.removeResourceBundle(languageCode, 'country'); } - i18next?.changeLanguage(undefined); + i18next.changeLanguage(undefined); }; export const getOdysseyTranslationOverrides = (): Partial => ( diff --git a/src/v3/src/util/locUtil.test.ts b/src/v3/src/util/locUtil.test.ts index a612442dab..6e6abc625d 100644 --- a/src/v3/src/util/locUtil.test.ts +++ b/src/v3/src/util/locUtil.test.ts @@ -10,69 +10,178 @@ * See the License for the specific language governing permissions and limitations under the License. */ -jest.unmock('./locUtil'); +import type { loc as origLoc } from './locUtil'; +import * as OrigLanguageUtils from './languageUtils'; -const MockedBundle: Record = { - 'some.basic.key': 'This is a key without params', - 'some.key.with.$1.token': 'This is a key with a token <$1>This is some text', - 'some.key.with.plain.html': 'This is a key with a token This is some text', - 'some.key.with.multiple.tokens': 'This is some test string with multiple tokens: <$1> <$2> here is a test string ', -}; - -jest.mock('./i18next', () => ({ - i18next: { - t: jest.fn().mockImplementation( - (origKey, params) => { - const bundleAndKey = origKey.split(':'); - let bundle; - let key = origKey; - if (bundleAndKey.length === 2) { - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - ([bundle, key] = bundleAndKey); - if (bundle === 'login' && MockedBundle[key]) { - return MockedBundle[key].replace('{0}', params?.[0]); - } - } - return ''; - }, - ), - }, -})); +jest.unmock('./locUtil'); -// eslint-disable-next-line import/first -import { loc } from './locUtil'; +let loc: typeof origLoc; +let languageUtils: typeof OrigLanguageUtils; describe('locUtil Tests', () => { - it('should return simple translated string', () => { - const localizedText = loc('some.basic.key', 'login'); - expect(localizedText).toBe('This is a key without params'); - }); + describe('Token replacement', () => { + const MockedBundle: Record = { + 'some.basic.key': 'This is a key without params', + 'some.key.with.$1.token': 'This is a key with a token <$1>This is some text', + 'some.key.with.plain.html': 'This is a key with a token This is some text', + 'some.key.with.multiple.tokens': 'This is some test string with multiple tokens: <$1> <$2> here is a test string ', + }; - it('should not perform replacement when tokens are not found in the string', () => { - const localizedText = loc('some.key.with.plain.html', 'login', undefined, { $1: { element: 'span' } }); - expect(localizedText).toBe('This is a key with a token This is some text'); - }); + beforeEach(() => { + jest.resetModules(); + jest.unmock('util/Bundles'); + jest.mock('./i18next', () => ({ + i18next: { + t: jest.fn().mockImplementation( + (origKey, params) => { + const bundleAndKey = origKey.split(':'); + let bundle; + let key = origKey; + if (bundleAndKey.length === 2) { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + ([bundle, key] = bundleAndKey); + if (bundle === 'login' && MockedBundle[key]) { + return MockedBundle[key].replace('{0}', params?.[0]); + } + } + return ''; + }, + ), + }, + })); - it('should perform replacement when tokens are found in the string', () => { - const localizedText = loc('some.key.with.$1.token', 'login', undefined, { $1: { element: 'span' } }); - expect(localizedText).toBe('This is a key with a token This is some text'); - }); + loc = jest.requireActual('./locUtil').loc; + }); - it('should perform replacement with provided attributes when tokens are found in the string', () => { - const localizedText = loc('some.key.with.$1.token', 'login', undefined, { $1: { element: 'span', attributes: { class: 'strong' } } }); - expect(localizedText).toBe('This is a key with a token This is some text'); - }); + it('should return simple translated string', () => { + const localizedText = loc('some.basic.key', 'login'); + expect(localizedText).toBe('This is a key without params'); + }); + + it('should not perform replacement when tokens are not found in the string', () => { + const localizedText = loc('some.key.with.plain.html', 'login', undefined, { $1: { element: 'span' } }); + expect(localizedText).toBe('This is a key with a token This is some text'); + }); + + it('should perform replacement when tokens are found in the string', () => { + const localizedText = loc('some.key.with.$1.token', 'login', undefined, { $1: { element: 'span' } }); + expect(localizedText).toBe('This is a key with a token This is some text'); + }); + + it('should perform replacement with provided attributes when tokens are found in the string', () => { + const localizedText = loc('some.key.with.$1.token', 'login', undefined, { $1: { element: 'span', attributes: { class: 'strong' } } }); + expect(localizedText).toBe('This is a key with a token This is some text'); + }); - it('should perform replacement when translation string contains multiple tokens', () => { - const localizedText = loc( - 'some.key.with.multiple.tokens', - 'login', - undefined, - { - $1: { element: 'a', attributes: { href: '#' } }, - $2: { element: 'span', attributes: { class: 'strong' } }, + it('should perform replacement when translation string contains multiple tokens', () => { + const localizedText = loc( + 'some.key.with.multiple.tokens', + 'login', + undefined, + { + $1: { element: 'a', attributes: { href: '#' } }, + $2: { element: 'span', attributes: { class: 'strong' } }, + }, + ); + expect(localizedText).toBe('This is some test string with multiple tokens: here is a test string '); + }); + }); + + // https://www.i18next.com/translation-function/plurals + describe('Pluralization', () => { + const MockedLogin: Record> = { + en: { + 'item_one': 'one item', + 'item_other': '{0} items', + 'apple_one': 'one apple', + 'apple_other': '{0} apples', + 'pear_one': 'one pear', + 'pear_other': '{0} pears', + }, + ro: { + 'item_one': 'un articol', + 'item_few': '{0} articole', + 'item_other': '{0} de articole', + // 'apple_one': 'un măr', // missing translation + 'apple_few': '{0} mere', + 'apple_other': '{0} de mere', + 'apple': '{0} de mere', // will be used as fallback + 'pear_few': '{0} pere', // no fallback }, - ); - expect(localizedText).toBe('This is some test string with multiple tokens: here is a test string '); + }; + + beforeEach(() => { + jest.resetModules(); + jest.unmock('./i18next'); + jest.mock('util/Bundles', () => { + const Bundles: { + currentLanguage: string | undefined, + login: Record, + country: Record, + loadLanguage: () => Promise, + } = { + currentLanguage: undefined, + login: MockedLogin.en, + country: {}, + loadLanguage: jest.fn().mockImplementation( + // eslint-disable-next-line no-unused-vars + async (language: string, overrides: any, assets: Record, + supportedLanguages: string[], omitDefaultKeys?: (key: string) => boolean + ) => { + Bundles.currentLanguage = language; + Bundles.login = MockedLogin[language] ?? {}; + Bundles.country = {}; + }, + ), + }; + return Bundles; + }); + + languageUtils = jest.requireActual('./languageUtils'); + loc = jest.requireActual('./locUtil').loc; + }); + + it('can localize singular/plural in English', () => { + languageUtils.initDefaultLanguage(); + const localizedTextOne = loc('item', 'login', [1]); + expect(localizedTextOne).toBe('one item'); + const localizedTextTwo = loc('item', 'login', [2]); + expect(localizedTextTwo).toBe('2 items'); + }); + + it('can localize multiple plurals in other languages', async () => { + languageUtils.initDefaultLanguage(); + await languageUtils.loadLanguage({ language: 'ro' }); + const localizedText1 = loc('item', 'login', [1]); + expect(localizedText1).toBe('un articol'); + const localizedText2 = loc('item', 'login', [2]); + expect(localizedText2).toBe('2 articole'); + const localizedText20 = loc('item', 'login', [20]); + expect(localizedText20).toBe('20 de articole'); + }); + + it('can fallback to default form if some plural form translation is missing', async () => { + languageUtils.initDefaultLanguage(); + await languageUtils.loadLanguage({ language: 'ro' }); + const localizedText2 = loc('apple', 'login', [2]); + expect(localizedText2).toBe('2 mere'); + // fallback + const localizedText1 = loc('apple', 'login', [1]); + expect(localizedText1).toBe('1 de mere'); + }); + + it('will fallback to English if some plural form translation is missing and there is no fallback', async () => { + languageUtils.initDefaultLanguage(); + await languageUtils.loadLanguage({ language: 'ro' }); + const localizedText2 = loc('pear', 'login', [2]); + expect(localizedText2).toBe('2 pere'); + // fallback to English + const localizedText1 = loc('pear', 'login', [1]); + expect(localizedText1).toBe('one pear'); + }); + + // todo: unload lang + + // todo: L10N_ERROR }); });