react-i18next 16.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/amd/react-i18next.js +3028 -844
- package/dist/amd/react-i18next.min.js +1 -1
- package/dist/commonjs/IcuTrans.js +35 -0
- package/dist/commonjs/IcuTransUtils/TranslationParserError.js +18 -0
- package/dist/commonjs/IcuTransUtils/htmlEntityDecoder.js +218 -0
- package/dist/commonjs/IcuTransUtils/index.js +49 -0
- package/dist/commonjs/IcuTransUtils/renderTranslation.js +114 -0
- package/dist/commonjs/IcuTransUtils/tokenizer.js +58 -0
- package/dist/commonjs/IcuTransWithoutContext.js +56 -0
- package/dist/commonjs/TransWithoutContext.js +2 -1
- package/dist/es/IcuTrans.js +29 -0
- package/dist/es/IcuTransUtils/TranslationParserError.js +11 -0
- package/dist/es/IcuTransUtils/htmlEntityDecoder.js +211 -0
- package/dist/es/IcuTransUtils/index.js +4 -0
- package/dist/es/IcuTransUtils/renderTranslation.js +106 -0
- package/dist/es/IcuTransUtils/tokenizer.js +51 -0
- package/dist/es/IcuTransWithoutContext.js +49 -0
- package/dist/es/TransWithoutContext.js +2 -1
- package/dist/es/package.json +1 -1
- package/dist/umd/react-i18next.js +3031 -847
- package/dist/umd/react-i18next.min.js +1 -1
- package/icu.macro.js +170 -48
- package/package.json +6 -3
- package/react-i18next.js +3031 -847
- package/react-i18next.min.js +1 -1
- package/src/IcuTrans.js +103 -0
- package/src/IcuTransUtils/TranslationParserError.js +24 -0
- package/src/IcuTransUtils/htmlEntityDecoder.js +264 -0
- package/src/IcuTransUtils/index.js +4 -0
- package/src/IcuTransUtils/renderTranslation.js +215 -0
- package/src/IcuTransUtils/tokenizer.js +78 -0
- package/src/IcuTransWithoutContext.js +146 -0
- package/src/TransWithoutContext.js +5 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { TranslationParserError } from './TranslationParserError';
|
|
4
|
+
import { tokenize } from './tokenizer';
|
|
5
|
+
import { decodeHtmlEntities } from './htmlEntityDecoder';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a React element tree from a declaration node and its children
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} declaration - The component declaration (type + props)
|
|
11
|
+
* @param {Array<React.ReactNode>} children - Array of child nodes (text, numbers, React elements)
|
|
12
|
+
* @param {Array<Object>} [childDeclarations] - Optional array of child declarations to use for nested rendering
|
|
13
|
+
* @returns {React.ReactElement} A React element
|
|
14
|
+
*/
|
|
15
|
+
const renderDeclarationNode = (declaration, children, childDeclarations) => {
|
|
16
|
+
const { type, props = {} } = declaration;
|
|
17
|
+
|
|
18
|
+
// If props contain a children declaration AND we have childDeclarations to work with,
|
|
19
|
+
// we need to recursively render the content with those child declarations
|
|
20
|
+
if (props.children && Array.isArray(props.children) && childDeclarations) {
|
|
21
|
+
// The children array contains the parsed content from inside this tag
|
|
22
|
+
// We need to rebuild the translation string and re-parse it with child declarations
|
|
23
|
+
// For now, we'll use the children directly as they're already parsed
|
|
24
|
+
// This happens when renderTranslation is called recursively
|
|
25
|
+
|
|
26
|
+
// Remove children from props since we'll pass them as the third argument
|
|
27
|
+
// eslint-disable-next-line no-unused-vars
|
|
28
|
+
const { children: _childrenToRemove, ...propsWithoutChildren } = props;
|
|
29
|
+
|
|
30
|
+
return React.createElement(type, propsWithoutChildren, ...children);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Standard rendering with children from translation
|
|
34
|
+
if (children.length === 0) {
|
|
35
|
+
return React.createElement(type, props);
|
|
36
|
+
}
|
|
37
|
+
if (children.length === 1) {
|
|
38
|
+
return React.createElement(type, props, children[0]);
|
|
39
|
+
}
|
|
40
|
+
return React.createElement(type, props, ...children);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render translation string with declaration tree to create React elements
|
|
45
|
+
*
|
|
46
|
+
* This function parses an ICU format translation string and reconstructs
|
|
47
|
+
* a React element tree using the provided declaration tree. It replaces
|
|
48
|
+
* numbered tags (e.g., <0>, <1>) with the corresponding components from
|
|
49
|
+
* the declaration array and fills them with the translated text.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} translation - ICU format string (e.g., "<0>Click here</0>")
|
|
52
|
+
* @param {Array<Object>} [declarations=[]] - Array of component declarations matching tag numbers
|
|
53
|
+
* @returns {Array<React.ReactNode>} Array of React nodes (elements and text)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```jsx
|
|
57
|
+
* const result = renderTranslation(
|
|
58
|
+
* "<0>bonjour</0> monde",
|
|
59
|
+
* [{ type: 'strong', props: { className: 'bold' } }]
|
|
60
|
+
* );
|
|
61
|
+
* // Returns: [<strong className="bold">bonjour</strong>, " monde"]
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```jsx
|
|
66
|
+
* // With nested children in declaration
|
|
67
|
+
* const result = renderTranslation(
|
|
68
|
+
* "<0>Click <1>here</1></0>",
|
|
69
|
+
* [
|
|
70
|
+
* {
|
|
71
|
+
* type: 'div',
|
|
72
|
+
* props: {
|
|
73
|
+
* children: [{ type: 'span', props: {} }]
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* ]
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const renderTranslation = (translation, declarations = []) => {
|
|
81
|
+
if (!translation) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tokens = tokenize(translation);
|
|
86
|
+
const result = [];
|
|
87
|
+
const stack = [];
|
|
88
|
+
|
|
89
|
+
// Track tag numbers that should be treated as literal text (no declaration found)
|
|
90
|
+
const literalTagNumbers = new Set();
|
|
91
|
+
|
|
92
|
+
// Helper to get the current declarations array based on context
|
|
93
|
+
const getCurrentDeclarations = () => {
|
|
94
|
+
if (stack.length === 0) {
|
|
95
|
+
return declarations;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parentFrame = stack[stack.length - 1];
|
|
99
|
+
|
|
100
|
+
// If the parent declaration has children declarations, use those
|
|
101
|
+
if (
|
|
102
|
+
parentFrame.declaration.props?.children &&
|
|
103
|
+
Array.isArray(parentFrame.declaration.props.children)
|
|
104
|
+
) {
|
|
105
|
+
return parentFrame.declaration.props.children;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Otherwise, use the parent's declarations array
|
|
109
|
+
return parentFrame.declarations;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
tokens.forEach((token) => {
|
|
113
|
+
// eslint-disable-next-line default-case
|
|
114
|
+
switch (token.type) {
|
|
115
|
+
case 'Text':
|
|
116
|
+
{
|
|
117
|
+
const decoded = decodeHtmlEntities(token.value);
|
|
118
|
+
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
119
|
+
|
|
120
|
+
targetArray.push(decoded);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'TagOpen':
|
|
126
|
+
{
|
|
127
|
+
const { tagNumber } = token;
|
|
128
|
+
const currentDeclarations = getCurrentDeclarations();
|
|
129
|
+
const declaration = currentDeclarations[tagNumber];
|
|
130
|
+
|
|
131
|
+
if (!declaration) {
|
|
132
|
+
// No declaration found - treat this tag as literal text
|
|
133
|
+
literalTagNumbers.add(tagNumber);
|
|
134
|
+
|
|
135
|
+
const literalText = `<${tagNumber}>`;
|
|
136
|
+
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
137
|
+
|
|
138
|
+
targetArray.push(literalText);
|
|
139
|
+
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
stack.push({
|
|
144
|
+
tagNumber,
|
|
145
|
+
children: [],
|
|
146
|
+
position: token.position,
|
|
147
|
+
declaration,
|
|
148
|
+
declarations: currentDeclarations,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'TagClose':
|
|
155
|
+
{
|
|
156
|
+
const { tagNumber } = token;
|
|
157
|
+
|
|
158
|
+
// If this tag was treated as literal, output the closing tag as literal text
|
|
159
|
+
if (literalTagNumbers.has(tagNumber)) {
|
|
160
|
+
const literalText = `</${tagNumber}>`;
|
|
161
|
+
const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
162
|
+
|
|
163
|
+
literalTargetArray.push(literalText);
|
|
164
|
+
|
|
165
|
+
literalTagNumbers.delete(tagNumber);
|
|
166
|
+
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (stack.length === 0) {
|
|
171
|
+
throw new TranslationParserError(
|
|
172
|
+
`Unexpected closing tag </${tagNumber}> at position ${token.position}`,
|
|
173
|
+
token.position,
|
|
174
|
+
translation,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const frame = stack.pop();
|
|
179
|
+
|
|
180
|
+
if (frame.tagNumber !== tagNumber) {
|
|
181
|
+
throw new TranslationParserError(
|
|
182
|
+
`Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`,
|
|
183
|
+
token.position,
|
|
184
|
+
translation,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Render the element using the declaration and collected children
|
|
189
|
+
const element = renderDeclarationNode(
|
|
190
|
+
frame.declaration,
|
|
191
|
+
frame.children,
|
|
192
|
+
frame.declarations,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
196
|
+
|
|
197
|
+
elementTargetArray.push(element);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (stack.length > 0) {
|
|
205
|
+
const unclosed = stack[stack.length - 1];
|
|
206
|
+
|
|
207
|
+
throw new TranslationParserError(
|
|
208
|
+
`Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`,
|
|
209
|
+
unclosed.position,
|
|
210
|
+
translation,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize a translation string with numbered tags
|
|
3
|
+
* Note: Variables are already interpolated by the i18n system before we receive the string
|
|
4
|
+
*
|
|
5
|
+
* @param {string} translation - Translation string with numbered tags
|
|
6
|
+
* @returns {Array<Token>} Array of tokens
|
|
7
|
+
*/
|
|
8
|
+
export const tokenize = (translation) => {
|
|
9
|
+
const tokens = [];
|
|
10
|
+
|
|
11
|
+
let position = 0;
|
|
12
|
+
|
|
13
|
+
let currentText = '';
|
|
14
|
+
|
|
15
|
+
const flushText = () => {
|
|
16
|
+
if (currentText) {
|
|
17
|
+
tokens.push({
|
|
18
|
+
type: 'Text',
|
|
19
|
+
value: currentText,
|
|
20
|
+
position: position - currentText.length,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
currentText = '';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
while (position < translation.length) {
|
|
28
|
+
const char = translation[position];
|
|
29
|
+
|
|
30
|
+
// Check for opening tag: <0>, <1>, etc.
|
|
31
|
+
if (char === '<') {
|
|
32
|
+
const tagMatch = translation.slice(position).match(/^<(\d+)>/);
|
|
33
|
+
|
|
34
|
+
if (tagMatch) {
|
|
35
|
+
flushText();
|
|
36
|
+
|
|
37
|
+
tokens.push({
|
|
38
|
+
type: 'TagOpen',
|
|
39
|
+
value: tagMatch[0],
|
|
40
|
+
position,
|
|
41
|
+
tagNumber: parseInt(tagMatch[1], 10),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
position += tagMatch[0].length;
|
|
45
|
+
} else {
|
|
46
|
+
// Check for closing tag: </0>, </1>, etc.
|
|
47
|
+
const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
|
|
48
|
+
|
|
49
|
+
if (closeTagMatch) {
|
|
50
|
+
flushText();
|
|
51
|
+
|
|
52
|
+
tokens.push({
|
|
53
|
+
type: 'TagClose',
|
|
54
|
+
value: closeTagMatch[0],
|
|
55
|
+
position,
|
|
56
|
+
tagNumber: parseInt(closeTagMatch[1], 10),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
position += closeTagMatch[0].length;
|
|
60
|
+
} else {
|
|
61
|
+
// Regular text (including any { } characters that aren't our tags)
|
|
62
|
+
currentText += char;
|
|
63
|
+
|
|
64
|
+
position += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Regular text (including any { } characters that aren't our tags)
|
|
69
|
+
currentText += char;
|
|
70
|
+
|
|
71
|
+
position += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
flushText();
|
|
76
|
+
|
|
77
|
+
return tokens;
|
|
78
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { warn, warnOnce, isString } from './utils.js';
|
|
3
|
+
import { getI18n } from './i18nInstance.js';
|
|
4
|
+
import { renderTranslation } from './IcuTransUtils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* IcuTrans component for rendering ICU MessageFormat translations (without React Context)
|
|
8
|
+
*
|
|
9
|
+
* This is the core implementation without React hooks or context dependencies,
|
|
10
|
+
* making it suitable for use in any environment. It uses a declaration tree
|
|
11
|
+
* approach where components are defined as type + props blueprints, fetches
|
|
12
|
+
* the translated string via i18next, and reconstructs the React element tree
|
|
13
|
+
* by replacing numbered tags (<0>, <1>) with actual components.
|
|
14
|
+
*
|
|
15
|
+
* Key features:
|
|
16
|
+
* - No React hooks or context (can be used anywhere)
|
|
17
|
+
* - ICU MessageFormat compatible
|
|
18
|
+
* - Supports nested component declarations
|
|
19
|
+
* - Automatic HTML entity decoding
|
|
20
|
+
* - Graceful error handling with fallbacks
|
|
21
|
+
* - Merges default interpolation variables
|
|
22
|
+
*
|
|
23
|
+
* Note: Users should typically use the IcuTrans export which provides automatic
|
|
24
|
+
* context support. This component is exposed for advanced use cases where direct
|
|
25
|
+
* i18n instance control is needed, or for use outside of React Context.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} props - Component props
|
|
28
|
+
* @param {string} props.i18nKey - The i18n key to look up the translation
|
|
29
|
+
* @param {string} props.defaultTranslation - The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>")
|
|
30
|
+
* @param {Array<{type: string|React.ComponentType, props?: Object}>} props.content - Declaration tree describing React components and their props
|
|
31
|
+
* @param {string|string[]} [props.ns] - Optional namespace(s) for the translation. Falls back to t.ns, then i18n.options.defaultNS, then 'translation'
|
|
32
|
+
* @param {Object} [props.values={}] - Optional values for ICU variable interpolation (merged with i18n.options.interpolation.defaultVariables if present)
|
|
33
|
+
* @param {Object} [props.i18n] - i18next instance. If not provided, uses global instance from getI18n()
|
|
34
|
+
* @param {Function} [props.t] - Custom translation function. If not provided, uses i18n.t.bind(i18n)
|
|
35
|
+
* @returns {React.ReactElement} React fragment containing the rendered translation
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```jsx
|
|
39
|
+
* // Direct usage with i18n instance
|
|
40
|
+
* <IcuTransWithoutContext
|
|
41
|
+
* i18nKey="welcome.message"
|
|
42
|
+
* defaultTranslation="Welcome <0>back</0>!"
|
|
43
|
+
* content={[
|
|
44
|
+
* { type: 'strong', props: { className: 'highlight' } }
|
|
45
|
+
* ]}
|
|
46
|
+
* i18n={i18nInstance}
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```jsx
|
|
52
|
+
* // With nested declarations for list rendering
|
|
53
|
+
* <IcuTransWithoutContext
|
|
54
|
+
* i18nKey="features.list"
|
|
55
|
+
* defaultTranslation="Features: <0><0>Fast</0><1>Reliable</1><2>Secure</2></0>"
|
|
56
|
+
* content={[
|
|
57
|
+
* {
|
|
58
|
+
* type: 'ul',
|
|
59
|
+
* props: {
|
|
60
|
+
* children: [
|
|
61
|
+
* { type: 'li', props: {} },
|
|
62
|
+
* { type: 'li', props: {} },
|
|
63
|
+
* { type: 'li', props: {} }
|
|
64
|
+
* ]
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ]}
|
|
68
|
+
* i18n={i18nInstance}
|
|
69
|
+
* />
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```jsx
|
|
74
|
+
* // With values for ICU variable interpolation
|
|
75
|
+
* <IcuTransWithoutContext
|
|
76
|
+
* i18nKey="greeting"
|
|
77
|
+
* defaultTranslation="Hello <0>{name}</0>!"
|
|
78
|
+
* content={[{ type: 'strong', props: {} }]}
|
|
79
|
+
* values={{ name: 'Alice' }}
|
|
80
|
+
* i18n={i18nInstance}
|
|
81
|
+
* />
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function IcuTransWithoutContext({
|
|
85
|
+
i18nKey,
|
|
86
|
+
defaultTranslation,
|
|
87
|
+
content,
|
|
88
|
+
ns,
|
|
89
|
+
values = {},
|
|
90
|
+
i18n: i18nFromProps,
|
|
91
|
+
t: tFromProps,
|
|
92
|
+
}) {
|
|
93
|
+
const i18n = i18nFromProps || getI18n();
|
|
94
|
+
|
|
95
|
+
if (!i18n) {
|
|
96
|
+
warnOnce(
|
|
97
|
+
i18n,
|
|
98
|
+
'NO_I18NEXT_INSTANCE',
|
|
99
|
+
`IcuTrans: You need to pass in an i18next instance using i18nextReactModule`,
|
|
100
|
+
{ i18nKey },
|
|
101
|
+
);
|
|
102
|
+
return React.createElement(React.Fragment, {}, defaultTranslation);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const t = tFromProps || i18n.t?.bind(i18n) || ((k) => k);
|
|
106
|
+
|
|
107
|
+
// prepare having a namespace
|
|
108
|
+
let namespaces = ns || t.ns || i18n.options?.defaultNS;
|
|
109
|
+
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
|
|
110
|
+
|
|
111
|
+
// Merge default interpolation variables if they exist
|
|
112
|
+
let mergedValues = values;
|
|
113
|
+
if (i18n.options?.interpolation?.defaultVariables) {
|
|
114
|
+
mergedValues =
|
|
115
|
+
values && Object.keys(values).length > 0
|
|
116
|
+
? { ...values, ...i18n.options.interpolation.defaultVariables }
|
|
117
|
+
: { ...i18n.options.interpolation.defaultVariables };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get the translation, falling back to defaultTranslation
|
|
121
|
+
const translation = t(i18nKey, {
|
|
122
|
+
defaultValue: defaultTranslation,
|
|
123
|
+
...mergedValues,
|
|
124
|
+
ns: namespaces,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Render the translation with the declaration tree
|
|
128
|
+
try {
|
|
129
|
+
const rendered = renderTranslation(translation, content);
|
|
130
|
+
|
|
131
|
+
// Return as a React fragment to avoid extra wrapper
|
|
132
|
+
return React.createElement(React.Fragment, {}, ...rendered);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// If rendering fails, warn and fall back to the translation string
|
|
135
|
+
warn(
|
|
136
|
+
i18n,
|
|
137
|
+
'ICU_TRANS_RENDER_ERROR',
|
|
138
|
+
`IcuTrans component error for key "${i18nKey}": ${error.message}`,
|
|
139
|
+
{ i18nKey, error },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return React.createElement(React.Fragment, {}, translation);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
IcuTransWithoutContext.displayName = 'IcuTransWithoutContext';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react';
|
|
2
|
+
import { keyFromSelector } from 'i18next';
|
|
2
3
|
import HTML from 'html-parse-stringify';
|
|
3
4
|
import { isObject, isString, warn, warnOnce } from './utils.js';
|
|
4
5
|
import { getDefaults } from './defaults.js';
|
|
@@ -424,7 +425,10 @@ export function Trans({
|
|
|
424
425
|
|
|
425
426
|
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
|
|
426
427
|
const defaultValue =
|
|
427
|
-
defaults ||
|
|
428
|
+
defaults ||
|
|
429
|
+
nodeAsString ||
|
|
430
|
+
reactI18nextOptions.transEmptyNodeValue ||
|
|
431
|
+
(typeof i18nKey === 'function' ? keyFromSelector(i18nKey) : i18nKey);
|
|
428
432
|
const { hashTransKey } = reactI18nextOptions;
|
|
429
433
|
const key =
|
|
430
434
|
i18nKey ||
|