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
9 changes: 9 additions & 0 deletions src/utils/arrayDifference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This function is an equivalent of _.difference, it takes two arrays and returns the difference between them.
* It returns an array of items that are in the first array but not in the second array.
*/
function arrayDifference<TItem>(array1: TItem[], array2: TItem[]): TItem[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document this function? What exactly is it doing? Is it returning the any values that are either in array1 or array2 but not in both? Or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function documented, let me know what you think! Regarding the other question, it's because some object keys don't fit the @typescript-eslint/naming-convention rule, that expects them to be camelCase, UPPER_CASE or PascalCase.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks!

return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c)));
}

export default arrayDifference;
File renamed without changes.
69 changes: 39 additions & 30 deletions tests/unit/TranslateTest.js → tests/unit/TranslateTest.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {AnnotationError} from '@actions/core';
import _ from 'underscore';
import CONFIG from '../../src/CONFIG';
import CONST from '../../src/CONST';
import * as translations from '../../src/languages/translations';
import * as Localize from '../../src/libs/Localize';

const originalTranslations = _.clone(translations);
translations.default = {
/* eslint-disable @typescript-eslint/naming-convention */
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import * as translations from '@src/languages/translations';
import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types';
import * as Localize from '@src/libs/Localize';
import asMutable from '@src/types/utils/asMutable';
import arrayDifference from '@src/utils/arrayDifference';

const originalTranslations = {...translations};

asMutable(translations).default = {
[CONST.LOCALES.EN]: translations.flattenObject({
testKey1: 'English',
testKey2: 'Test Word 2',
Expand All @@ -24,80 +27,86 @@ translations.default = {

describe('translate', () => {
it('Test present key in full locale', () => {
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1')).toBe('Spanish ES');
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')).toBe('Spanish Word 2');
expect(Localize.translate(CONST.LOCALES.ES, 'testKey2')).toBe('Spanish Word 2');
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')).toBe('Test Word 3');
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3' as TranslationPaths)).toBe('Test Word 3');
});

test('Test when key is not found in default', () => {
expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toThrow(Error);
expect(() => Localize.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;
CONFIG.IS_IN_PRODUCTION = true;
expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toBe('testKey4');
CONFIG.IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION;
asMutable(CONFIG).IS_IN_PRODUCTION = true;
expect(Localize.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';
expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction', {testVariable})).toBe(expectedValue);
// @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);
});
});

describe('Translation Keys', () => {
function traverseKeyPath(source, path, keyPaths) {
const pathArray = keyPaths || [];
function traverseKeyPath(source: TranslationFlatObject, path?: string, keyPaths?: string[]): string[] {
const pathArray = keyPaths ?? [];
const keyPath = path ? `${path}.` : '';
_.each(_.keys(source), (key) => {
if (_.isObject(source[key]) && !_.isFunction(source[key])) {
(Object.keys(source) as Array<keyof TranslationFlatObject>).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;
}

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

_.each(languages, (ln) => {
const languageKeys = traverseKeyPath(originalTranslations.default[ln]);
languages.forEach((ln) => {
const languageKeys = traverseKeyPath(originalTranslations.default[ln as keyof typeof originalTranslations.default]);

it(`Does ${ln} locale have all the keys`, () => {
const hasAllKeys = _.difference(mainLanguageKeys, languageKeys);
const hasAllKeys = arrayDifference(mainLanguageKeys, languageKeys);
if (hasAllKeys.length) {
console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
Error(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`);
}
expect(hasAllKeys).toEqual([]);
});

it(`Does ${ln} locale have unused keys`, () => {
const hasAllKeys = _.difference(languageKeys, mainLanguageKeys);
const hasAllKeys = arrayDifference(languageKeys, mainLanguageKeys);
if (hasAllKeys.length) {
console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
Error(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`);
}
expect(hasAllKeys).toEqual([]);
});
});
});

type ReportContentArgs = {content: string};

describe('flattenObject', () => {
it('It should work correctly', () => {
const func = ({content}) => `This is the content: ${content}`;
const func = ({content}: ReportContentArgs) => `This is the content: ${content}`;
const simpleObject = {
common: {
yes: 'Yes',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* @jest-environment node
*/
import * as core from '@actions/core';

/* eslint-disable @typescript-eslint/naming-convention */
import * as fns from 'date-fns';
import {vol} from 'memfs';
import path from 'path';
Expand All @@ -19,7 +20,9 @@ const mockGetPullRequestsMergedBetween = jest.fn();

beforeAll(() => {
// Mock core module
core.getInput = mockGetInput;
jest.mock('@actions/core', () => ({
getInput: mockGetInput,
}));

// Mock octokit module
const moctokit = {
Expand Down Expand Up @@ -49,8 +52,8 @@ beforeAll(() => {
list: jest.fn().mockResolvedValue([]),
},
},
paginate: jest.fn().mockImplementation((objectMethod) => objectMethod().then(({data}) => data)),
};
paginate: jest.fn().mockImplementation((objectMethod: () => Promise<{data: unknown}>) => objectMethod().then(({data}) => data)),
} as typeof GithubUtils.octokit;
GithubUtils.internalOctokit = moctokit;

// Mock GitUtils
Expand Down Expand Up @@ -295,7 +298,7 @@ describe('createOrUpdateStagingDeployCash', () => {
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
issue_number: openStagingDeployCashBefore.number,
// eslint-disable-next-line max-len
// eslint-disable-next-line max-len, @typescript-eslint/naming-convention
html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`,
// eslint-disable-next-line max-len
body:
Expand Down Expand Up @@ -371,7 +374,7 @@ describe('createOrUpdateStagingDeployCash', () => {
owner: CONST.GITHUB_OWNER,
repo: CONST.APP_REPO,
issue_number: openStagingDeployCashBefore.number,
// eslint-disable-next-line max-len
// eslint-disable-next-line max-len, @typescript-eslint/naming-convention
html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`,
// eslint-disable-next-line max-len
body:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import Onyx from 'react-native-onyx';
import CONFIG from '../../src/CONFIG';
import enhanceParameters from '../../src/libs/Network/enhanceParameters';
Expand Down