Skip to content

Commit

Permalink
fix(i18nHelpers): sw-922 i18n element wrapping (#1080)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdcabrera authored Mar 14, 2023
1 parent 09c67be commit c8e074f
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 71 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ This project makes use of reserved DOM attributes and string identifiers used by
- To use add the `testId` to your locale string function call use
- `t('locale.string.id', { testId: true })`. In this example, this would populate `locale.string.id` as the testId.
- or `t('locale.string.id', { testId: 'custom-id-coordinated-with-QE' })`
- or `t('locale.string.id', { testId: <div data-test="custom-element-wrapper-and-id" /> })`
### Reserved Files
#### Spandx Config
Expand Down
25 changes: 24 additions & 1 deletion src/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2701,6 +2701,7 @@ Default props.
* [~splitContext(value, settings)](#i18n.module_i18nHelpers..splitContext) ⇒ <code>string</code> \| <code>Array.&lt;string&gt;</code>
* [~parseContext(translateKey, context, settings)](#i18n.module_i18nHelpers..parseContext) ⇒ <code>Object</code>
* [~parseTranslateKey(translateKey)](#i18n.module_i18nHelpers..parseTranslateKey) ⇒ <code>\*</code>
* [~setI18nTestElement(params)](#i18n.module_i18nHelpers..setI18nTestElement) ⇒ <code>null</code> \| <code>React.ReactNode</code>
* [~translate(translateKey, values, components, settings)](#i18n.module_i18nHelpers..translate) ⇒ <code>string</code> \| <code>React.ReactNode</code>
* [~translateComponent(Component, settings)](#i18n.module_i18nHelpers..translateComponent) ⇒ <code>React.ReactNode</code>

Expand Down Expand Up @@ -2799,6 +2800,28 @@ Parse a translation key. If an array, filter for defined strings.
</tr> </tbody>
</table>

<a name="i18n.module_i18nHelpers..setI18nTestElement"></a>

### i18nHelpers~setI18nTestElement(params) ⇒ <code>null</code> \| <code>React.ReactNode</code>
Return a test element wrapper;

**Kind**: inner method of [<code>i18nHelpers</code>](#i18n.module_i18nHelpers)
<table>
<thead>
<tr>
<th>Param</th><th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>params</td><td><code>object</code></td>
</tr><tr>
<td>params.defaultTestId</td><td><code>string</code> | <code>Array</code></td>
</tr><tr>
<td>params.testId</td><td><code>string</code></td>
</tr> </tbody>
</table>

<a name="i18n.module_i18nHelpers..translate"></a>

### i18nHelpers~translate(translateKey, values, components, settings) ⇒ <code>string</code> \| <code>React.ReactNode</code>
Expand All @@ -2819,7 +2842,7 @@ See, https://react.i18next.com/
</tr><tr>
<td>values</td><td><code>string</code> | <code>object</code> | <code>Array</code></td><td><code>null</code></td><td><ul>
<li>A default string if the key can&#39;t be found.<ul>
<li>An object with i18next settings. i.e. &quot;{ context: Array|string, testId: boolean|string }&quot;</li>
<li>An object with i18next settings. i.e. &quot;{ context: Array|string, testId: boolean|string|React.ReactNode }&quot;</li>
<li>An array of objects (key/value) pairs used to replace string tokens. i.e. &quot;[{ hello: &#39;world&#39; }]&quot;</li>
</ul>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,55 @@ exports[`I18nHelpers should attempt to perform translate with a node: translated
exports[`I18nHelpers should attempt to place a test identifier around copy: test id 1`] = `
{
"basic": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="lorem.ipsum"
/>,
"basicNode": <TestElementNode>
<dolor-sit
data-test="dolor-sit"
/>
</TestElementNode>,
"basicString": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="dolor-sit"
/>,
"emptyContext": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="lorem.ipsum"
/>,
"emptyContextNode": <TestElementNode>
<dolor-sit
data-test="dolor-sit"
/>
</TestElementNode>,
"emptyContextString": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="dolor-sit"
/>,
"emptyPartialContext": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="lorem.ipsum"
/>,
"emptyPartialContextNode": <TestElementNode>
<dolor-sit
data-test="dolor-sit"
/>
</TestElementNode>,
"emptyPartialContextString": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="dolor-sit"
/>,
"stringContextNested": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="lorem.ipsum"
/>,
"stringContextNestedNode": <TestElementNode>
<dolor-sit
data-test="dolor-sit"
/>
</TestElementNode>,
"stringContextNestedString": <span
className="curiosity-translate__test-id"
class="curiosity-translate__test-id"
data-test="dolor-sit"
/>,
}
Expand Down
146 changes: 99 additions & 47 deletions src/components/i18n/__tests__/i18nHelpers.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
import PropTypes from 'prop-types';
import { i18nHelpers, EMPTY_CONTEXT, translate, translateComponent } from '../i18nHelpers';

Expand Down Expand Up @@ -60,61 +61,112 @@ describe('I18nHelpers', () => {
it('should attempt to place a test identifier around copy', () => {
const mockI18next = { store: jest.fn(), t: jest.fn() };

const basic = translate('lorem.ipsum', { testId: true }, undefined, { i18next: mockI18next, isDebug: false });
const basicString = translate('lorem.ipsum', { testId: 'dolor-sit' }, undefined, {
i18next: mockI18next,
isDebug: false
});
const emptyContext = translate('lorem.ipsum', { context: EMPTY_CONTEXT, testId: true }, undefined, {
i18next: mockI18next,
isDebug: false
});
const emptyContextString = translate('lorem.ipsum', { context: EMPTY_CONTEXT, testId: 'dolor-sit' }, undefined, {
i18next: mockI18next,
isDebug: false
});
const emptyPartialContext = translate(
'lorem.ipsum',
{ context: ['hello', EMPTY_CONTEXT], testId: true },
undefined,
{ i18next: mockI18next, isDebug: false }
const basic = mount(
translate('lorem.ipsum', { testId: true }, undefined, { i18next: mockI18next, isDebug: false })
);
const emptyPartialContextString = translate(
'lorem.ipsum',
{ context: ['hello', EMPTY_CONTEXT], testId: 'dolor-sit' },
undefined,
{ i18next: mockI18next, isDebug: false }
const basicString = mount(
translate('lorem.ipsum', { testId: 'dolor-sit' }, undefined, {
i18next: mockI18next,
isDebug: false
})
);
const basicNode = mount(
translate('lorem.ipsum', { testId: <dolor-sit data-test="dolor-sit" /> }, undefined, {
i18next: mockI18next,
isDebug: false
})
);
const emptyContext = mount(
translate('lorem.ipsum', { context: EMPTY_CONTEXT, testId: true }, undefined, {
i18next: mockI18next,
isDebug: false
})
);
const emptyContextString = mount(
translate('lorem.ipsum', { context: EMPTY_CONTEXT, testId: 'dolor-sit' }, undefined, {
i18next: mockI18next,
isDebug: false
})
);
const emptyContextNode = mount(
translate('lorem.ipsum', { context: EMPTY_CONTEXT, testId: <dolor-sit data-test="dolor-sit" /> }, undefined, {
i18next: mockI18next,
isDebug: false
})
);
const stringContextNested = translate(
'lorem.ipsum',
{
context: 'hello_world_lorem_ipsum_dolor_sit',
const emptyPartialContext = mount(
translate('lorem.ipsum', { context: ['hello', EMPTY_CONTEXT], testId: true }, undefined, {
i18next: mockI18next,
testId: true
},
undefined,
{ i18next: mockI18next, isDebug: false }
isDebug: false
})
);
const stringContextNestedString = translate(
'lorem.ipsum',
{
context: 'hello_world_lorem_ipsum_dolor_sit',
const emptyPartialContextString = mount(
translate('lorem.ipsum', { context: ['hello', EMPTY_CONTEXT], testId: 'dolor-sit' }, undefined, {
i18next: mockI18next,
testId: 'dolor-sit'
},
undefined,
{ i18next: mockI18next, isDebug: false }
isDebug: false
})
);
const emptyPartialContextNode = mount(
translate(
'lorem.ipsum',
{ context: ['hello', EMPTY_CONTEXT], testId: <dolor-sit data-test="dolor-sit" /> },
undefined,
{
i18next: mockI18next,
isDebug: false
}
)
);
const stringContextNested = mount(
translate(
'lorem.ipsum',
{
context: 'hello_world_lorem_ipsum_dolor_sit',
i18next: mockI18next,
testId: true
},
undefined,
{ i18next: mockI18next, isDebug: false }
)
);
const stringContextNestedString = mount(
translate(
'lorem.ipsum',
{
context: 'hello_world_lorem_ipsum_dolor_sit',
i18next: mockI18next,
testId: 'dolor-sit'
},
undefined,
{ i18next: mockI18next, isDebug: false }
)
);
const stringContextNestedNode = mount(
translate(
'lorem.ipsum',
{
context: 'hello_world_lorem_ipsum_dolor_sit',
i18next: mockI18next,
testId: <dolor-sit data-test="dolor-sit" />
},
undefined,
{ i18next: mockI18next, isDebug: false }
)
);

expect({
basic,
basicString,
emptyContext,
emptyContextString,
emptyPartialContext,
emptyPartialContextString,
stringContextNested,
stringContextNestedString
basic: basic.render(),
basicString: basicString.render(),
basicNode,
emptyContext: emptyContext.render(),
emptyContextString: emptyContextString.render(),
emptyContextNode,
emptyPartialContext: emptyPartialContext.render(),
emptyPartialContextString: emptyPartialContextString.render(),
emptyPartialContextNode,
stringContextNested: stringContextNested.render(),
stringContextNestedString: stringContextNestedString.render(),
stringContextNestedNode
}).toMatchSnapshot('test id');
});
});
74 changes: 59 additions & 15 deletions src/components/i18n/i18nHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,56 @@ const parseTranslateKey = translateKey => {
return updatedTranslateKey;
};

/**
* Return a test element wrapper;
*
* @param {object} params
* @param {string|Array} params.defaultTestId
* @param {string} params.testId
* @returns {null|React.ReactNode}
*/
const setI18nTestElement = ({ defaultTestId, testId }) => {
if (typeof testId === 'boolean' && defaultTestId) {
const updatedDataTest = (Array.isArray(defaultTestId) && defaultTestId[0]) || defaultTestId;
// eslint-disable-next-line
return function TestElementBool({ children }) {
return (
<span key={updatedDataTest} className="curiosity-translate__test-id" data-test={updatedDataTest}>
{children}
</span>
);
};
}

if (typeof testId === 'string' && testId.length > 0) {
// eslint-disable-next-line
return function TestElementString({ children }) {
return (
<span key={testId} className="curiosity-translate__test-id" data-test={testId}>
{children}
</span>
);
};
}

if (React.isValidElement(testId)) {
// eslint-disable-next-line
return function TestElementNode({ children }) {
return React.cloneElement(testId, {}, children);
};
}

return null;
};

/**
* 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. i.e. "{ context: Array|string, testId: boolean|string }"
* - An object with i18next settings. i.e. "{ context: Array|string, testId: boolean|string|React.ReactNode }"
* - An array of objects (key/value) pairs used to replace string tokens. 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} settings
Expand All @@ -153,6 +195,7 @@ const translate = (
const updatedValues = values || {};
const baseUpdatedTranslateKey = aliasParseTranslateKey(translateKey);
let updatedTranslateKey = baseUpdatedTranslateKey;
let TestElement;

if (updatedValues?.context) {
const { context: parsedContext, translateKey: parsedAgainTranslateKey } = aliasParseContext(
Expand All @@ -167,26 +210,27 @@ const translate = (
return aliasNoopTranslate(updatedTranslateKey, updatedValues, components);
}

if (components) {
return (
(aliasI18next.store && (
<Trans i18nKey={updatedTranslateKey} values={updatedValues} components={components} />
)) || <React.Fragment>t({updatedTranslateKey})</React.Fragment>
);
if (updatedValues?.testId) {
TestElement = setI18nTestElement({ defaultTestId: baseUpdatedTranslateKey, testId: updatedValues.testId });
}

if (aliasI18next.store) {
if (updatedValues?.testId) {
const updatedTestId =
(typeof updatedValues?.testId === 'string' && updatedValues?.testId.length > 0 && updatedValues?.testId) ||
baseUpdatedTranslateKey;
if (components && aliasI18next.store) {
if (TestElement) {
return (
<span className="curiosity-translate__test-id" data-test={updatedTestId}>
{aliasI18next.t(updatedTranslateKey, updatedValues)}
</span>
<TestElement>
<Trans i18nKey={updatedTranslateKey} values={updatedValues} components={components} />
</TestElement>
);
}

return <Trans i18nKey={updatedTranslateKey} values={updatedValues} components={components} />;
}

if (aliasI18next.store) {
if (TestElement) {
return <TestElement>{aliasI18next.t(updatedTranslateKey, updatedValues)}</TestElement>;
}

return aliasI18next.t(updatedTranslateKey, updatedValues);
}

Expand Down

0 comments on commit c8e074f

Please sign in to comment.