Skip to content

Commit

Permalink
Support t() with components passed as interpolation variables (#7)
Browse files Browse the repository at this point in the history
* Add failing test of t() with component as interpolation variables

* Merge branch 'main' into component-as-interpolation

* Add and use function that supports nested interpolated

* Safe operator

* Switch to switch statement. Update comments

* Fix a bug and add tests

* add changeset

* Remove JSX

* Modify rollup config

* Update method description

* Update .gitignore

* Remove dynamic react import

* Update comments and rename var

* Update src/utils.js

Co-authored-by: Trish Rempel <[email protected]>

---------

Co-authored-by: Robert Perry <[email protected]>
Co-authored-by: Robert Perry <[email protected]>
  • Loading branch information
3 people authored May 24, 2023
1 parent 26d90d9 commit 3d725cb
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-pumpkins-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/i18next-shopify': minor
---

Support t() with components passed as interpolation variables
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ node_modules
npm-debug.log
npm-debug.log.*
yarn-error.log
i18nextShopify.min.js
i18nextShopify.js
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ShopifyFormat {
? options.count || options.ordinal
: options[interpolation_key];
if (value !== undefined) {
interpolated = interpolated.replace(match, value);
interpolated = utils.replaceValue(interpolated, match, value);
}
});
return interpolated;
Expand Down
60 changes: 60 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,63 @@ export function defaults(obj, ...args) {
});
return obj;
}

/**
* Replaces all occurrences of the specified text. Returns a new value with the replacements made.
* This function supports replacing text with React elements and replacing values within
* nested React elements/arrays.
*
* @param {string|object|Array} interpolated - The value to replace occurrences of the specified text in.
* @param {string|RegExp} pattern - The text or regular expression to search for in the interpolated value.
* @param {string|object|Array} replacement - The value to replace occurrences of the specified text with.
* @returns {string|object|Array} A new value with the specified text replaced.
*/
export function replaceValue(interpolated, pattern, replacement) {
switch (typeof interpolated) {
case 'string': {
const split = interpolated.split(pattern);
// Check if interpolated includes pattern
// && if String.prototype.replace wouldn't work because replacement is an object like a React element.
if (split.length !== 1 && typeof replacement === 'object') {
// Return array w/ the replacement

// React elements within arrays need a key prop
if (!replacement.key) {
// eslint-disable-next-line no-param-reassign
replacement = {...replacement, key: pattern.toString()};
}

return [split[0], replacement, split[1]].flat();
}

// interpolated and replacement are primitives
return interpolated.replace(pattern, replacement);
}

case 'object':
if (Array.isArray(interpolated)) {
return interpolated
.map((item) => replaceValue(item, pattern, replacement))
.flat();
}

// Check if the interpolated object may be a React element w/ children.
if (interpolated?.props?.children) {
const newChildren = replaceValue(
interpolated.props.children,
pattern,
replacement,
);

if (newChildren !== interpolated.props.children) {
return {
...interpolated,
props: {...interpolated.props, children: newChildren},
};
}
}
}

// The interpolated element is something else, just return it
return interpolated;
}
22 changes: 20 additions & 2 deletions test/__snapshots__/shopify_format_with_react_i18next.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`with react-i18next (Trans with interpolation) handles interpolation of React components 1`] = `
exports[`with react-i18next (Trans with interpolation) handles interpolation of React components using Trans component with explicit component tags 1`] = `
<div>
Hello
<strong
Expand All @@ -18,7 +18,7 @@ exports[`with react-i18next (Trans with interpolation) handles interpolation of
</div>
`;

exports[`with react-i18next (Trans with interpolation) handles interpolation of React components using explicit components 1`] = `
exports[`with react-i18next (Trans with interpolation) handles interpolation of React components using Trans component with numbered component tags 1`] = `
<div>
Hello
<strong
Expand All @@ -35,3 +35,21 @@ exports[`with react-i18next (Trans with interpolation) handles interpolation of
.
</div>
`;

exports[`with react-i18next (Trans with interpolation) handles interpolation of React components using t() function with React components passed as interpolation variables 1`] = `
<div>
Hello
<strong
title="This is your name"
>
Joe
</strong>
, you have 1 unread message.
<a
href="/msgs"
>
Go to message.
</a>
</div>
`;
20 changes: 20 additions & 0 deletions test/__snapshots__/utils.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Utils replaceValue replaces a string with React elements 1`] = `
[
"Hello, ",
<span>
John
</span>,
"!",
]
`;

exports[`Utils replaceValue replaces a string within nested React elements 1`] = `
<React.Fragment>
Hello there
<span>
John
</span>
</React.Fragment>
`;
41 changes: 39 additions & 2 deletions test/shopify_format_with_react_i18next.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,22 @@ describe('with react-i18next (Trans with interpolation)', () => {
other:
'Hello <Name>{{name}}</Name>, you have {{count}} unread messages. <MessagesLink>Go to messages</MessagesLink>.',
},
userMessagesUnreadSimple: {
one: 'Hello {{name}}, you have {{count}} unread message. {{messageLink}}',
other:
'Hello {{name}}, you have {{count}} unread messages. {{messageLink}}',
},
messageLinkText: {
one: 'Go to message.',
other: 'Go to messages.',
},
},
},
},
});
});

it('handles interpolation of React components', () => {
it('handles interpolation of React components using Trans component with numbered component tags', () => {
const {result} = renderHook(() => useTranslation('translation'));
const {t} = result.current;

Expand All @@ -410,7 +419,7 @@ describe('with react-i18next (Trans with interpolation)', () => {
expect(container).toMatchSnapshot();
});

it('handles interpolation of React components using explicit components', () => {
it('handles interpolation of React components using Trans component with explicit component tags', () => {
const {result} = renderHook(() => useTranslation('translation'));
const {t} = result.current;

Expand Down Expand Up @@ -439,4 +448,32 @@ describe('with react-i18next (Trans with interpolation)', () => {
);
expect(container).toMatchSnapshot();
});

it('handles interpolation of React components using t() function with React components passed as interpolation variables', () => {
const {result} = renderHook(() => useTranslation('translation'));
const {t} = result.current;

const MyComponent = () => {
const count = 1;
const name = 'Joe';

return (
<>
{t('userMessagesUnreadSimple', {
count,
name: <strong title={t('nameTitle')}>{name}</strong>,
messageLink: (
<Link to="/msgs">{t('messageLinkText', {count})}</Link>
),
})}
</>
);
};

const {container} = render(<MyComponent />);
expect(container).toHaveTextContent(
'Hello Joe, you have 1 unread message. Go to message.',
);
expect(container).toMatchSnapshot();
});
});
51 changes: 51 additions & 0 deletions test/utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import {replaceValue} from '../src/utils';

describe('Utils', () => {
describe('replaceValue', () => {
it('replaces a string with a single level of interpolation', () => {
expect(replaceValue('Hello, {{name}}!', '{{name}}', 'John')).toBe(
'Hello, John!',
);
});

it('replaces a string within an array with a single level of interpolation', () => {
expect(
replaceValue(['Blah! ', 'Hello, {{name}}!'], '{{name}}', 'John'),
).toStrictEqual(['Blah! ', 'Hello, John!']);
});

it('replaces a string with React elements', () => {
const span = React.createElement('span', {}, 'John');

expect(
replaceValue('Hello, {{name}}!', '{{name}}', span),
).toMatchSnapshot();
});

it('returns the original string when there is no match with given React elements', () => {
const span = React.createElement('span', {}, 'John');

expect(replaceValue('Hello, {{name}}!', '{{no_match}}', span)).toBe(
'Hello, {{name}}!',
);
});

it('replaces a string within nested React elements', () => {
const span = React.createElement('span', {key: '1'}, '{{name}}');
const fragment = React.createElement(React.Fragment, {}, [
'Hello there ',
span,
]);

expect(replaceValue(fragment, '{{name}}', 'John')).toMatchSnapshot();
});

it('replaces a string with a regular expression', () => {
expect(replaceValue('Hello, {{name}}!', /{{name}}/, 'John')).toBe(
'Hello, John!',
);
});
});
});

0 comments on commit 3d725cb

Please sign in to comment.