Skip to content

Commit

Permalink
perf(i18n): breakout i18n into helpers (#985)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdcabrera committed Nov 3, 2022
1 parent 0f68906 commit 52e6ba2
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 137 deletions.
16 changes: 0 additions & 16 deletions src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`I18n Component should attempt to perform a component translate: translated component 1`] = `"<div>t(lorem.ipsum, hello world)</div>"`;

exports[`I18n Component should attempt to perform a string replace: translate 1`] = `
{
"emptyContext": "t(lorem.ipsum, {"context":" "})",
"emptyPartialContext": "t(lorem.ipsum, {"context":"hello_ "})",
"localeKey": "t(lorem.ipsum)",
"multiContext": "t(lorem.ipsum, {"context":"hello_world"})",
"multiContextWithEmptyValue": "t(lorem.ipsum, {"context":"hello_world"})",
"multiKey": "t([lorem.ipsum,lorem.fallback])",
"placeholder": "t(lorem.ipsum, hello world)",
}
`;

exports[`I18n Component should attempt to perform translate with a node: translated node 1`] = `"<div>t(lorem.ipsum, {&quot;hello&quot;:&quot;world&quot;}, [object Object])</div>"`;

exports[`I18n Component should generate a predictable locale key output snapshot: key output 1`] = `
[
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`I18nHelpers should attempt to perform a component translate: translated component 1`] = `"<div>t(lorem.ipsum, hello world)</div>"`;

exports[`I18nHelpers should attempt to perform a string replace: translate 1`] = `
{
"emptyContext": "t(lorem.ipsum, {"context":" "})",
"emptyPartialContext": "t(lorem.ipsum, {"context":"hello_ "})",
"localeKey": "t(lorem.ipsum)",
"multiContext": "t(lorem.ipsum, {"context":"hello_world"})",
"multiContextWithEmptyValue": "t(lorem.ipsum, {"context":"hello_world"})",
"multiKey": "t([lorem.ipsum,lorem.fallback])",
"placeholder": "t(lorem.ipsum, hello world)",
}
`;

exports[`I18nHelpers should attempt to perform translate with a node: translated node 1`] = `"<div>t(lorem.ipsum, {&quot;hello&quot;:&quot;world&quot;}, [object Object])</div>"`;

exports[`I18nHelpers should have specific functions: i18nHelpers 1`] = `
{
"EMPTY_CONTEXT": "LOCALE_EMPTY_CONTEXT",
"translate": [Function],
"translateComponent": [Function],
}
`;
52 changes: 1 addition & 51 deletions src/components/i18n/__tests__/i18n.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { readFileSync } from 'fs';
import glob from 'glob';
import React from 'react';
import PropTypes from 'prop-types';
import { shallow } from 'enzyme';
import _get from 'lodash/get';
import enLocales from '../../../../public/locales/en-US.json';
import { EMPTY_CONTEXT, I18n, translate, translateComponent } from '../i18n';
import { I18n } from '../i18n';

/**
* Get translation keys.
Expand Down Expand Up @@ -68,54 +66,6 @@ describe('I18n Component', () => {
expect(component.html()).toMatchSnapshot('children');
});

it('should attempt to perform translate with a node', () => {
const ExampleComponent = () => <div>{translate('lorem.ipsum', { hello: 'world' }, [<span id="test" />])}</div>;

ExampleComponent.propTypes = {};
ExampleComponent.defaultProps = {};

const component = shallow(<ExampleComponent />);

expect(component.html()).toMatchSnapshot('translated node');
});

it('should attempt to perform a component translate', () => {
const ExampleComponent = ({ t }) => <div>{t('lorem.ipsum', 'hello world')}</div>;

ExampleComponent.propTypes = {
t: PropTypes.func
};

ExampleComponent.defaultProps = {
t: translate
};

const TranslatedComponent = translateComponent(ExampleComponent);
const component = shallow(<TranslatedComponent />);

expect(component.html()).toMatchSnapshot('translated component');
});

it('should attempt to perform a string replace', () => {
const emptyContext = translate('lorem.ipsum', { context: EMPTY_CONTEXT });
const emptyPartialContext = translate('lorem.ipsum', { context: ['hello', EMPTY_CONTEXT] });
const localeKey = translate('lorem.ipsum');
const placeholder = translate('lorem.ipsum', 'hello world');
const multiContext = translate('lorem.ipsum', { context: ['hello', 'world'] });
const multiContextWithEmptyValue = translate('lorem.ipsum', { context: ['hello', undefined, null, '', 'world'] });
const multiKey = translate(['lorem.ipsum', undefined, null, '', 'lorem.fallback']);

expect({
emptyContext,
emptyPartialContext,
localeKey,
placeholder,
multiContext,
multiContextWithEmptyValue,
multiKey
}).toMatchSnapshot('translate');
});

it('should generate a predictable locale key output snapshot', () => {
expect(getKeys).toMatchSnapshot('key output');
});
Expand Down
58 changes: 58 additions & 0 deletions src/components/i18n/__tests__/i18nHelpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { shallow } from 'enzyme';
import PropTypes from 'prop-types';
import { i18nHelpers, EMPTY_CONTEXT, translate, translateComponent } from '../i18nHelpers';

describe('I18nHelpers', () => {
it('should have specific functions', () => {
expect(i18nHelpers).toMatchSnapshot('i18nHelpers');
});

it('should attempt to perform translate with a node', () => {
const ExampleComponent = () => <div>{translate('lorem.ipsum', { hello: 'world' }, [<span id="test" />])}</div>;

ExampleComponent.propTypes = {};
ExampleComponent.defaultProps = {};

const component = shallow(<ExampleComponent />);

expect(component.html()).toMatchSnapshot('translated node');
});

it('should attempt to perform a component translate', () => {
const ExampleComponent = ({ t }) => <div>{t('lorem.ipsum', 'hello world')}</div>;

ExampleComponent.propTypes = {
t: PropTypes.func
};

ExampleComponent.defaultProps = {
t: translate
};

const TranslatedComponent = translateComponent(ExampleComponent);
const component = shallow(<TranslatedComponent />);

expect(component.html()).toMatchSnapshot('translated component');
});

it('should attempt to perform a string replace', () => {
const emptyContext = translate('lorem.ipsum', { context: EMPTY_CONTEXT });
const emptyPartialContext = translate('lorem.ipsum', { context: ['hello', EMPTY_CONTEXT] });
const localeKey = translate('lorem.ipsum');
const placeholder = translate('lorem.ipsum', 'hello world');
const multiContext = translate('lorem.ipsum', { context: ['hello', 'world'] });
const multiContextWithEmptyValue = translate('lorem.ipsum', { context: ['hello', undefined, null, '', 'world'] });
const multiKey = translate(['lorem.ipsum', undefined, null, '', 'lorem.fallback']);

expect({
emptyContext,
emptyPartialContext,
localeKey,
placeholder,
multiContext,
multiContextWithEmptyValue,
multiKey
}).toMatchSnapshot('translate');
});
});
72 changes: 2 additions & 70 deletions src/components/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,10 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import i18next from 'i18next';
import XHR from 'i18next-http-backend';
import { initReactI18next, Trans } from 'react-i18next';
import { initReactI18next } from 'react-i18next';
import { useMount } from 'react-use';
import { helpers } from '../../common/helpers';

/**
* Check to help provide an empty context.
*
* @type {string}
*/
const EMPTY_CONTEXT = 'LOCALE_EMPTY_CONTEXT';

/**
* Apply a string towards a key. Optional replacement values and component/nodes.
* See, https://react.i18next.com/
*
* @param {string|Array} translateKey A key reference, or an array of a primary key with fallback keys.
* @param {string|object|Array} values A default string if the key can't be found. An object with i18next settings. Or an array of objects (key/value) pairs used to replace string tokes. i.e. "[{ hello: 'world' }]"
* @param {Array} components An array of HTML/React nodes used to replace string tokens. i.e. "[<span />, <React.Fragment />]"
* @param {object} options
* @param {string} options.emptyContextValue Check to allow an empty context value.
* @returns {string|React.ReactNode}
*/
const translate = (translateKey, values = null, components, { emptyContextValue = EMPTY_CONTEXT } = {}) => {
const updatedValues = values;
let updatedTranslateKey = translateKey;

if (Array.isArray(updatedTranslateKey)) {
updatedTranslateKey = updatedTranslateKey.filter(value => typeof value === 'string' && value.length > 0);
}

if (Array.isArray(updatedValues?.context)) {
updatedValues.context = updatedValues.context
.map(value => (value === emptyContextValue && ' ') || value)
.filter(value => typeof value === 'string' && value.length > 0)
.join('_');
} else if (updatedValues?.context === emptyContextValue) {
updatedValues.context = ' ';
}

if (helpers.TEST_MODE) {
return helpers.noopTranslate(updatedTranslateKey, updatedValues, components);
}

if (components) {
return (
(i18next.store && <Trans i18nKey={updatedTranslateKey} values={updatedValues} components={components} />) || (
<React.Fragment>t({updatedTranslateKey})</React.Fragment>
)
);
}

return (i18next.store && i18next.t(updatedTranslateKey, updatedValues)) || `t([${updatedTranslateKey}])`;
};

/**
* Apply string replacements against a component, HOC.
*
* @param {React.ReactNode} Component
* @returns {React.ReactNode}
*/
const translateComponent = Component => {
const withTranslation = ({ ...props }) => (
<Component
{...props}
t={(i18next.store && translate) || helpers.noopTranslate}
i18n={(i18next.store && i18next) || helpers.noop}
/>
);

withTranslation.displayName = 'withTranslation';
return withTranslation;
};
import { EMPTY_CONTEXT, translate, translateComponent } from './i18nHelpers';

/**
* Load I18n.
Expand Down
81 changes: 81 additions & 0 deletions src/components/i18n/i18nHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import i18next from 'i18next';
import { Trans } from 'react-i18next';
import { helpers } from '../../common/helpers';

/**
* Check to help provide an empty context.
*
* @type {string}
*/
const EMPTY_CONTEXT = 'LOCALE_EMPTY_CONTEXT';

/**
* Apply a string towards a key. Optional replacement values and component/nodes.
* See, https://react.i18next.com/
*
* @param {string|Array} translateKey A key reference, or an array of a primary key with fallback keys.
* @param {string|object|Array} values A default string if the key can't be found. An object with i18next settings. Or an array of objects (key/value) pairs used to replace string tokes. i.e. "[{ hello: 'world' }]"
* @param {Array} components An array of HTML/React nodes used to replace string tokens. i.e. "[<span />, <React.Fragment />]"
* @param {object} options
* @param {string} options.emptyContextValue Check to allow an empty context value.
* @returns {string|React.ReactNode}
*/
const translate = (translateKey, values = null, components, { emptyContextValue = EMPTY_CONTEXT } = {}) => {
const updatedValues = values;
let updatedTranslateKey = translateKey;

if (Array.isArray(updatedTranslateKey)) {
updatedTranslateKey = updatedTranslateKey.filter(value => typeof value === 'string' && value.length > 0);
}

if (Array.isArray(updatedValues?.context)) {
updatedValues.context = updatedValues.context
.map(value => (value === emptyContextValue && ' ') || value)
.filter(value => typeof value === 'string' && value.length > 0)
.join('_');
} else if (updatedValues?.context === emptyContextValue) {
updatedValues.context = ' ';
}

if (helpers.TEST_MODE) {
return helpers.noopTranslate(updatedTranslateKey, updatedValues, components);
}

if (components) {
return (
(i18next.store && <Trans i18nKey={updatedTranslateKey} values={updatedValues} components={components} />) || (
<React.Fragment>t({updatedTranslateKey})</React.Fragment>
)
);
}

return (i18next.store && i18next.t(updatedTranslateKey, updatedValues)) || `t([${updatedTranslateKey}])`;
};

/**
* Apply string replacements against a component, HOC.
*
* @param {React.ReactNode} Component
* @returns {React.ReactNode}
*/
const translateComponent = Component => {
const withTranslation = ({ ...props }) => (
<Component
{...props}
t={(i18next.store && translate) || helpers.noopTranslate}
i18n={(i18next.store && i18next) || helpers.noop}
/>
);

withTranslation.displayName = 'withTranslation';
return withTranslation;
};

const i18nHelpers = {
EMPTY_CONTEXT,
translate,
translateComponent
};

export { i18nHelpers as default, i18nHelpers, EMPTY_CONTEXT, translate, translateComponent };

0 comments on commit 52e6ba2

Please sign in to comment.