Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions __mocks__/react-native-localize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// eslint-disable-next-line import/no-import-module-exports
import mockRNLocalize from 'react-native-localize/mock';
// use a provided translation, or return undefined to test your fallback
const findBestAvailableLanguage = () => ({
languageTag: 'en',
isRTL: false,
});

module.exports = mockRNLocalize;
// eslint-disable-next-line import/prefer-default-export
export {findBestAvailableLanguage};
3 changes: 0 additions & 3 deletions src/languages/es-ES.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/languages/translations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import en from './en';
import es from './es';
import esES from './es-ES';
import type {FlatTranslationsObject, TranslationDeepObject} from './types';

/**
Expand Down Expand Up @@ -45,6 +44,4 @@ export function flattenObject<TTranslations>(obj: TranslationDeepObject<TTransla
export default {
en: flattenObject(en),
es: flattenObject(es),
// eslint-disable-next-line @typescript-eslint/naming-convention
'es-ES': flattenObject(esES),
};
34 changes: 6 additions & 28 deletions src/libs/Localize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ function init() {
* phrase and stores the translated value in the cache and returns
* the translated value.
*/
function getTranslatedPhrase<TKey extends TranslationPaths>(
language: 'en' | 'es' | 'es-ES',
phraseKey: TKey,
fallbackLanguage: 'en' | 'es' | null,
...parameters: TranslationParameters<TKey>
): string | null {
function getTranslatedPhrase<TKey extends TranslationPaths>(language: 'en' | 'es', phraseKey: TKey, ...parameters: TranslationParameters<TKey>): string | null {
Comment thread
shubham1206agra marked this conversation as resolved.
const translatedPhrase = translations?.[language]?.[phraseKey];

if (translatedPhrase) {
Expand Down Expand Up @@ -108,30 +103,15 @@ function getTranslatedPhrase<TKey extends TranslationPaths>(
return translatedPhrase;
}

if (!fallbackLanguage) {
return null;
}

// Phrase is not found in full locale, search it in fallback language e.g. es
const fallbackTranslatedPhrase = getTranslatedPhrase(fallbackLanguage, phraseKey, null, ...parameters);

if (fallbackTranslatedPhrase) {
return fallbackTranslatedPhrase;
}

if (fallbackLanguage !== CONST.LOCALES.DEFAULT) {
Log.alert(`${phraseKey} was not found in the ${fallbackLanguage} locale`);
}

// Phrase is not translated, search it in default language (en)
return getTranslatedPhrase(CONST.LOCALES.DEFAULT, phraseKey, null, ...parameters);
Log.alert(`${phraseKey} was not found in the ${language} locale`);
return null;
}

const memoizedGetTranslatedPhrase = memoize(getTranslatedPhrase, {
maxArgs: 2,
equality: 'shallow',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
skipCache: (params) => !isEmptyObject(params.at(3)),
skipCache: (params) => !isEmptyObject(params.at(2)),
});

/**
Expand All @@ -142,11 +122,9 @@ const memoizedGetTranslatedPhrase = memoize(getTranslatedPhrase, {
*/
function translate<TPath extends TranslationPaths>(desiredLanguage: 'en' | 'es' | 'es-ES' | 'es_ES', path: TPath, ...parameters: TranslationParameters<TPath>): string {
// Search phrase in full locale e.g. es-ES
const language = desiredLanguage === CONST.LOCALES.ES_ES_ONFIDO ? CONST.LOCALES.ES_ES : desiredLanguage;
// Phrase is not found in full locale, search it in fallback language e.g. es
const languageAbbreviation = desiredLanguage.substring(0, 2) as 'en' | 'es';
const language = ([CONST.LOCALES.ES_ES_ONFIDO, CONST.LOCALES.ES_ES] as string[]).includes(desiredLanguage) ? CONST.LOCALES.ES : (desiredLanguage as 'en' | 'es');

const translatedPhrase = memoizedGetTranslatedPhrase(language, path, languageAbbreviation, ...parameters);
const translatedPhrase = memoizedGetTranslatedPhrase(language, path, ...parameters);
if (translatedPhrase !== null && translatedPhrase !== undefined) {
return translatedPhrase;
}
Expand Down
49 changes: 14 additions & 35 deletions tests/unit/TranslateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import * as translations from '@src/languages/translations';
import type {FlatTranslationsObject, TranslationDeepObject, TranslationPaths} from '@src/languages/types';
import * as Localize from '@src/libs/Localize';
import {translate} from '@src/libs/Localize';
import asMutable from '@src/types/utils/asMutable';
import arrayDifference from '@src/utils/arrayDifference';

Expand All @@ -13,7 +13,6 @@ asMutable(translations).default = {
[CONST.LOCALES.EN]: translations.flattenObject({
testKey1: 'English',
testKey2: 'Test Word 2',
testKey3: 'Test Word 3',
testKeyGroup: {
testFunction: ({testVariable}: {testVariable: string}) => `With variable ${testVariable}`,
},
Expand All @@ -35,84 +34,64 @@ asMutable(translations).default = {
}),
},
}),
[CONST.LOCALES.ES_ES]: translations.flattenObject({testKey1: 'Spanish ES'}),
};

describe('translate', () => {
it('Test present key in full locale', () => {
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1' as TranslationPaths)).toBe('Spanish ES');
});

it('Test when key is not found in full locale, but present in language', () => {
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
expect(Localize.translate(CONST.LOCALES.ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
});

it('Test when key is not found in full locale and language, but present in default', () => {
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3' as TranslationPaths)).toBe('Test Word 3');
expect(translate(CONST.LOCALES.ES_ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
expect(translate(CONST.LOCALES.ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2');
});

test('Test when key is not found in default', () => {
expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toThrow(Error);
expect(() => translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toThrow(Error);
});

test('Test when key is not found in default (Production Mode)', () => {
const ORIGINAL_IS_IN_PRODUCTION = CONFIG.IS_IN_PRODUCTION;
asMutable(CONFIG).IS_IN_PRODUCTION = true;
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4');
expect(translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4');
asMutable(CONFIG).IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION;
});

it('Test when translation value is a function', () => {
const expectedValue = 'With variable Test Variable';
const testVariable = 'Test Variable';
// @ts-expect-error - TranslationPaths doesn't include testKeyGroup.testFunction as a valid key
expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction' as TranslationPaths, {testVariable})).toBe(expectedValue);
expect(translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction' as TranslationPaths, {testVariable})).toBe(expectedValue);
});

it('Test when count value passed to function but output is string', () => {
const expectedValue = 'Count value is 10';
const count = 10;
// @ts-expect-error - TranslationPaths doesn't include pluralizationGroup.countWithoutPluralRules as a valid key
expect(Localize.translate(CONST.LOCALES.EN, 'pluralizationGroup.countWithoutPluralRules' as TranslationPaths, {count})).toBe(expectedValue);
expect(translate(CONST.LOCALES.EN, 'pluralizationGroup.countWithoutPluralRules' as TranslationPaths, {count})).toBe(expectedValue);
});

it('Test when count value 2 passed to function but there is no rule for the key two', () => {
const expectedValue = 'Other 2 files are being downloaded.';
const count = 2;
// @ts-expect-error - TranslationPaths doesn't include pluralizationGroup.countWithNoCorrespondingRule as a valid key
expect(Localize.translate(CONST.LOCALES.EN, 'pluralizationGroup.countWithNoCorrespondingRule' as TranslationPaths, {count})).toBe(expectedValue);
expect(translate(CONST.LOCALES.EN, 'pluralizationGroup.countWithNoCorrespondingRule' as TranslationPaths, {count})).toBe(expectedValue);
});

it('Test when count value 0, 1, 100 passed to function', () => {
// @ts-expect-error - TranslationPaths doesn't include pluralizationGroup.couthWithCorrespondingRule as a valid key
expect(Localize.translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 0})).toBe('0 artículos');
expect(translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 0})).toBe('0 artículos');

// @ts-expect-error - TranslationPaths doesn't include pluralizationGroup.couthWithCorrespondingRule as a valid key
expect(Localize.translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 1})).toBe('Un artículo');
expect(translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 1})).toBe('Un artículo');

// @ts-expect-error - TranslationPaths doesn't include pluralizationGroup.couthWithCorrespondingRule as a valid key
expect(Localize.translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 100})).toBe('100 artículos');
expect(translate(CONST.LOCALES.ES, 'pluralizationGroup.couthWithCorrespondingRule' as TranslationPaths, {count: 100})).toBe('100 artículos');
});
});

describe('Translation Keys', () => {
function traverseKeyPath(source: FlatTranslationsObject, path?: string, keyPaths?: string[]): string[] {
const pathArray = keyPaths ?? [];
const keyPath = path ? `${path}.` : '';
(Object.keys(source) as TranslationPaths[]).forEach((key) => {
if (typeof source[key] === 'object' && typeof source[key] !== 'function') {
// @ts-expect-error - We are modifying the translations object for testing purposes
traverseKeyPath(source[key], keyPath + key, pathArray);
} else {
pathArray.push(keyPath + key);
}
});

return pathArray;
function traverseKeyPath(source: FlatTranslationsObject): string[] {
return Object.keys(source);
}

const excludeLanguages = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES];
const excludeLanguages = [CONST.LOCALES.EN];
const languages = Object.keys(originalTranslations.default).filter((ln) => !excludeLanguages.some((excludeLanguage) => excludeLanguage === ln));
const mainLanguage = originalTranslations.default.en;
const mainLanguageKeys = traverseKeyPath(mainLanguage);
Expand Down