Skip to content

Commit be99228

Browse files
committed
Add configuration option to set default props for the Trans component #1895
1 parent 09b9d86 commit be99228

7 files changed

Lines changed: 198 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
### 16.5.0
2+
3+
- Add configuration option `transDefaultProps` to set default props for the Trans component (e.g. `tOptions`, `shouldUnescape`, `values`) [1895](https://github.com/i18next/react-i18next/issues/1895)
4+
15
### 16.4.1
26

3-
- fix(Trans): prevent double-escaping of interpolated values in component props (e.g. title). Unescape HTML entities before passing prop values to React to avoid rendered output like `"` / `'`. [#1893](https://github.com/i18next/react-i18next/issues/1893)
7+
- fix(Trans): prevent double-escaping of interpolated values in component props (e.g. title). Unescape HTML entities before passing prop values to React to avoid rendered output like `"` / `'`. [1893](https://github.com/i18next/react-i18next/issues/1893)
48

59
### 16.4.0
610

react-i18next.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,7 +2405,8 @@
24052405
transWrapTextNodes: '',
24062406
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
24072407
useSuspense: true,
2408-
unescape
2408+
unescape,
2409+
transDefaultProps: undefined
24092410
};
24102411
const setDefaults = (options = {}) => {
24112412
defaultOptions = {
@@ -2764,34 +2765,52 @@
27642765
};
27652766
let namespaces = ns || t.ns || i18n.options?.defaultNS;
27662767
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
2768+
const {
2769+
transDefaultProps
2770+
} = reactI18nextOptions;
2771+
const mergedTOptions = transDefaultProps?.tOptions ? {
2772+
...transDefaultProps.tOptions,
2773+
...tOptions
2774+
} : tOptions;
2775+
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
2776+
const mergedValues = transDefaultProps?.values ? {
2777+
...transDefaultProps.values,
2778+
...values
2779+
} : values;
2780+
const mergedComponents = transDefaultProps?.components ? {
2781+
...transDefaultProps.components,
2782+
...components
2783+
} : components;
27672784
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
2768-
const defaultValue = defaults || tOptions?.defaultValue || nodeAsString || reactI18nextOptions.transEmptyNodeValue || (typeof i18nKey === 'function' ? keysFromSelector(i18nKey) : i18nKey);
2785+
const defaultValue = defaults || mergedTOptions?.defaultValue || nodeAsString || reactI18nextOptions.transEmptyNodeValue || (typeof i18nKey === 'function' ? keysFromSelector(i18nKey) : i18nKey);
27692786
const {
27702787
hashTransKey
27712788
} = reactI18nextOptions;
27722789
const key = i18nKey || (hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue);
27732790
if (i18n.options?.interpolation?.defaultVariables) {
2774-
values = values && Object.keys(values).length > 0 ? {
2775-
...values,
2791+
values = mergedValues && Object.keys(mergedValues).length > 0 ? {
2792+
...mergedValues,
27762793
...i18n.options.interpolation.defaultVariables
27772794
} : {
27782795
...i18n.options.interpolation.defaultVariables
27792796
};
2797+
} else {
2798+
values = mergedValues;
27802799
}
27812800
const valuesFromChildren = getValuesFromChildren(children);
27822801
if (valuesFromChildren && typeof valuesFromChildren.count === 'number' && count === undefined) {
27832802
count = valuesFromChildren.count;
27842803
}
2785-
const interpolationOverride = values || count !== undefined && !i18n.options?.interpolation?.alwaysFormat || !children ? tOptions.interpolation : {
2804+
const interpolationOverride = values || count !== undefined && !i18n.options?.interpolation?.alwaysFormat || !children ? mergedTOptions.interpolation : {
27862805
interpolation: {
2787-
...tOptions.interpolation,
2806+
...mergedTOptions.interpolation,
27882807
prefix: '#$?',
27892808
suffix: '?$#'
27902809
}
27912810
};
27922811
const combinedTOpts = {
2793-
...tOptions,
2794-
context: context || tOptions.context,
2812+
...mergedTOptions,
2813+
context: context || mergedTOptions.context,
27952814
count,
27962815
...values,
27972816
...interpolationOverride,
@@ -2800,14 +2819,14 @@
28002819
};
28012820
let translation = key ? t(key, combinedTOpts) : defaultValue;
28022821
if (translation === key && defaultValue) translation = defaultValue;
2803-
const generatedComponents = generateComponents(components, translation, i18n, i18nKey);
2822+
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
28042823
let indexedChildren = generatedComponents || children;
28052824
let componentsMap = null;
28062825
if (isComponentsMap(generatedComponents)) {
28072826
componentsMap = generatedComponents;
28082827
indexedChildren = children;
28092828
}
2810-
const content = renderNodes(indexedChildren, componentsMap, translation, i18n, reactI18nextOptions, combinedTOpts, shouldUnescape);
2829+
const content = renderNodes(indexedChildren, componentsMap, translation, i18n, reactI18nextOptions, combinedTOpts, mergedShouldUnescape);
28112830
const useAsParent = parent ?? reactI18nextOptions.defaultTransParent;
28122831
return useAsParent ? React.createElement(useAsParent, additionalProps, content) : content;
28132832
}

react-i18next.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/TransWithoutContext.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,25 @@ export function Trans({
540540
let namespaces = ns || t.ns || i18n.options?.defaultNS;
541541
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
542542

543+
const { transDefaultProps } = reactI18nextOptions;
544+
const mergedTOptions = transDefaultProps?.tOptions
545+
? { ...transDefaultProps.tOptions, ...tOptions }
546+
: tOptions;
547+
548+
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
549+
550+
const mergedValues = transDefaultProps?.values
551+
? { ...transDefaultProps.values, ...values }
552+
: values;
553+
554+
const mergedComponents = transDefaultProps?.components
555+
? { ...transDefaultProps.components, ...components }
556+
: components;
557+
543558
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
544559
const defaultValue =
545560
defaults ||
546-
tOptions?.defaultValue ||
561+
mergedTOptions?.defaultValue ||
547562
nodeAsString ||
548563
reactI18nextOptions.transEmptyNodeValue ||
549564
(typeof i18nKey === 'function' ? keyFromSelector(i18nKey) : i18nKey);
@@ -554,9 +569,12 @@ export function Trans({
554569
if (i18n.options?.interpolation?.defaultVariables) {
555570
// eslint-disable-next-line no-param-reassign
556571
values =
557-
values && Object.keys(values).length > 0
558-
? { ...values, ...i18n.options.interpolation.defaultVariables }
572+
mergedValues && Object.keys(mergedValues).length > 0
573+
? { ...mergedValues, ...i18n.options.interpolation.defaultVariables }
559574
: { ...i18n.options.interpolation.defaultVariables };
575+
} else {
576+
// eslint-disable-next-line no-param-reassign
577+
values = mergedValues;
560578
}
561579

562580
const valuesFromChildren = getValuesFromChildren(children);
@@ -570,11 +588,11 @@ export function Trans({
570588
values ||
571589
(count !== undefined && !i18n.options?.interpolation?.alwaysFormat) || // https://github.com/i18next/react-i18next/issues/1719 + https://github.com/i18next/react-i18next/issues/1801
572590
!children // if !children gets problems in future, undo that fix: https://github.com/i18next/react-i18next/issues/1729 by removing !children from this condition
573-
? tOptions.interpolation
574-
: { interpolation: { ...tOptions.interpolation, prefix: '#$?', suffix: '?$#' } };
591+
? mergedTOptions.interpolation
592+
: { interpolation: { ...mergedTOptions.interpolation, prefix: '#$?', suffix: '?$#' } };
575593
const combinedTOpts = {
576-
...tOptions,
577-
context: context || tOptions.context, // Add `context` from the props or fallback to the value from `tOptions`
594+
...mergedTOptions,
595+
context: context || mergedTOptions.context, // Add `context` from the props or fallback to the value from `tOptions`
578596
count,
579597
...values,
580598
...interpolationOverride,
@@ -584,7 +602,7 @@ export function Trans({
584602
let translation = key ? t(key, combinedTOpts) : defaultValue;
585603
if (translation === key && defaultValue) translation = defaultValue;
586604

587-
const generatedComponents = generateComponents(components, translation, i18n, i18nKey);
605+
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
588606
let indexedChildren = generatedComponents || children;
589607
let componentsMap = null;
590608
if (isComponentsMap(generatedComponents)) {
@@ -599,7 +617,7 @@ export function Trans({
599617
i18n,
600618
reactI18nextOptions,
601619
combinedTOpts,
602-
shouldUnescape,
620+
mergedShouldUnescape,
603621
);
604622

605623
// allows user to pass `null` to `parent`

src/defaults.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let defaultOptions = {
1111
// hashTransKey: key => key // calculate a key for Trans component based on defaultValue
1212
useSuspense: true,
1313
unescape,
14+
transDefaultProps: undefined, // { tOptions: {}, shouldUnescape: false, values: {}, components: [] }
1415
};
1516

1617
export const setDefaults = (options = {}) => {

test/trans.render.spec.jsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,3 +1318,137 @@ describe('trans issue 1893 - double escaping in props', () => {
13181318
`);
13191319
});
13201320
});
1321+
1322+
describe('trans with default props', () => {
1323+
it('should use transDefaultProps for tOptions', () => {
1324+
const i18nInst = i18n.createInstance();
1325+
i18nInst.init({
1326+
lng: 'en',
1327+
resources: {
1328+
en: {
1329+
translation: {
1330+
key1: 'hello #name#',
1331+
},
1332+
},
1333+
},
1334+
react: {
1335+
transDefaultProps: {
1336+
tOptions: { interpolation: { prefix: '#', suffix: '#' } },
1337+
},
1338+
},
1339+
});
1340+
1341+
const { container } = render(
1342+
<Trans i18n={i18nInst} i18nKey="key1" values={{ name: 'world' }} />,
1343+
);
1344+
expect(container).toHaveTextContent('hello world');
1345+
});
1346+
1347+
it('should use transDefaultProps for shouldUnescape', () => {
1348+
const i18nInst = i18n.createInstance();
1349+
i18nInst.init({
1350+
lng: 'en',
1351+
resources: {
1352+
en: {
1353+
translation: {
1354+
key1: 'hello <Item title="{{name}}" />',
1355+
},
1356+
},
1357+
},
1358+
react: {
1359+
transDefaultProps: {
1360+
tOptions: { interpolation: { escapeValue: true } },
1361+
shouldUnescape: true,
1362+
},
1363+
},
1364+
});
1365+
1366+
function Item({ title }) {
1367+
return <span title={title}>{title}</span>;
1368+
}
1369+
1370+
render(
1371+
<Trans
1372+
i18n={i18nInst}
1373+
i18nKey="key1"
1374+
values={{ name: '"' }}
1375+
components={{ Item: <Item /> }}
1376+
/>,
1377+
);
1378+
const span = screen.getByTitle('"');
1379+
expect(span).toHaveAttribute('title', '"');
1380+
});
1381+
1382+
it('should override transDefaultProps', () => {
1383+
const i18nInst = i18n.createInstance();
1384+
i18nInst.init({
1385+
lng: 'en',
1386+
resources: {
1387+
en: {
1388+
translation: {
1389+
key1: 'hello {{name}}',
1390+
},
1391+
},
1392+
},
1393+
react: {
1394+
transDefaultProps: {
1395+
tOptions: { interpolation: { prefix: '#', suffix: '#' } },
1396+
},
1397+
},
1398+
});
1399+
1400+
const { container } = render(
1401+
<Trans
1402+
i18n={i18nInst}
1403+
i18nKey="key1"
1404+
values={{ name: 'world' }}
1405+
tOptions={{ interpolation: { prefix: '{{', suffix: '}}' } }}
1406+
/>,
1407+
);
1408+
expect(container).toHaveTextContent('hello world');
1409+
});
1410+
1411+
it('should use transDefaultProps for values', () => {
1412+
const i18nInst = i18n.createInstance();
1413+
i18nInst.init({
1414+
lng: 'en',
1415+
resources: {
1416+
en: {
1417+
translation: {
1418+
key1: 'hello {{name}}',
1419+
},
1420+
},
1421+
},
1422+
react: {
1423+
transDefaultProps: {
1424+
values: { name: 'world' },
1425+
},
1426+
},
1427+
});
1428+
1429+
const { container } = render(<Trans i18n={i18nInst} i18nKey="key1" />);
1430+
expect(container).toHaveTextContent('hello world');
1431+
});
1432+
1433+
it('should use transDefaultProps for components', () => {
1434+
const i18nInst = i18n.createInstance();
1435+
i18nInst.init({
1436+
lng: 'en',
1437+
resources: {
1438+
en: {
1439+
translation: {
1440+
key1: 'hello <Bold>world</Bold>',
1441+
},
1442+
},
1443+
},
1444+
react: {
1445+
transDefaultProps: {
1446+
components: { Bold: <strong /> },
1447+
},
1448+
},
1449+
});
1450+
1451+
render(<Trans i18n={i18nInst} i18nKey="key1" />);
1452+
expect(screen.getByText('world', { selector: 'strong' })).toHaveTextContent('world');
1453+
});
1454+
});

test/useTranslation.spec.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, vitest, expect, beforeAll, afterAll, afterEach } from 'vitest';
22
import React from 'react';
3-
import { renderHook, cleanup, render } from '@testing-library/react';
3+
import { renderHook, cleanup, render, act } from '@testing-library/react';
44
import { createInstance } from 'i18next';
55
import i18nInstance from './i18n';
66
import { useTranslation } from '../src/useTranslation';

0 commit comments

Comments
 (0)