@octanejs/i18next 0.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/LICENSE +22 -0
- package/README.md +151 -0
- package/package.json +73 -0
- package/src/I18nextProvider.js +9 -0
- package/src/IcuTrans.js +104 -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 +216 -0
- package/src/IcuTransUtils/tokenizer.js +78 -0
- package/src/IcuTransWithoutContext.js +147 -0
- package/src/Trans.js +46 -0
- package/src/TransWithoutContext.d.ts +160 -0
- package/src/TransWithoutContext.js +687 -0
- package/src/Translation.js +15 -0
- package/src/context.js +61 -0
- package/src/defaults.js +21 -0
- package/src/helpers.d.ts +3 -0
- package/src/i18nInstance.js +7 -0
- package/src/index.d.ts +315 -0
- package/src/index.js +26 -0
- package/src/initReactI18next.d.ts +3 -0
- package/src/initReactI18next.js +11 -0
- package/src/internal.js +42 -0
- package/src/unescape.js +31 -0
- package/src/useSSR.js +55 -0
- package/src/useTranslation.js +285 -0
- package/src/utils.js +93 -0
- package/src/withSSR.js +22 -0
- package/src/withTranslation.js +38 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// Ported from react-i18next@17.0.9 (8b4a9ea). The parser/reconstruction logic is
|
|
2
|
+
// kept byte-close; React element helpers map to octane's descriptor helpers.
|
|
3
|
+
import {
|
|
4
|
+
Fragment,
|
|
5
|
+
isValidElement,
|
|
6
|
+
cloneElement,
|
|
7
|
+
createElement,
|
|
8
|
+
Children,
|
|
9
|
+
isChildrenBlock,
|
|
10
|
+
positionalChildren,
|
|
11
|
+
} from 'octane';
|
|
12
|
+
import { keyFromSelector } from 'i18next';
|
|
13
|
+
import HTML from 'html-parse-stringify';
|
|
14
|
+
import { isObject, isString, warn, warnOnce } from './utils.js';
|
|
15
|
+
import { getDefaults } from './defaults.js';
|
|
16
|
+
import { getI18n } from './i18nInstance.js';
|
|
17
|
+
import { unescape } from './unescape.js';
|
|
18
|
+
|
|
19
|
+
const hasChildren = (node, checkLength) => {
|
|
20
|
+
if (!node) return false;
|
|
21
|
+
const base = node.props?.children ?? node.children;
|
|
22
|
+
if (checkLength) return base.length > 0;
|
|
23
|
+
return !!base;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getChildren = (node) => {
|
|
27
|
+
if (!node) return [];
|
|
28
|
+
const children = node.props?.children ?? node.children;
|
|
29
|
+
return node.props?.i18nIsDynamicList ? getAsArray(children) : children;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const hasValidReactChildren = (children) =>
|
|
33
|
+
Array.isArray(children) && children.every(isValidElement);
|
|
34
|
+
|
|
35
|
+
const getAsArray = (data) => (Array.isArray(data) ? data : [data]);
|
|
36
|
+
|
|
37
|
+
const mergeProps = (source, target) => {
|
|
38
|
+
const newTarget = { ...target };
|
|
39
|
+
// translation props (source.props) should override component props (target.props)
|
|
40
|
+
newTarget.props = { ...target.props, ...source.props };
|
|
41
|
+
return newTarget;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getValuesFromChildren = (children) => {
|
|
45
|
+
const values = {};
|
|
46
|
+
if (!children) return values;
|
|
47
|
+
const getData = (childs) => {
|
|
48
|
+
const childrenArray = getAsArray(childs);
|
|
49
|
+
childrenArray.forEach((child) => {
|
|
50
|
+
if (isString(child)) return;
|
|
51
|
+
if (hasChildren(child)) getData(getChildren(child));
|
|
52
|
+
else if (isObject(child) && !isValidElement(child)) Object.assign(values, child);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
getData(children);
|
|
56
|
+
return values;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
|
|
60
|
+
if (!children) return '';
|
|
61
|
+
let stringNode = '';
|
|
62
|
+
|
|
63
|
+
// do not use `React.Children.toArray`, will fail at object children
|
|
64
|
+
const childrenArray = getAsArray(children);
|
|
65
|
+
const keepArray = i18nOptions?.transSupportBasicHtmlNodes
|
|
66
|
+
? (i18nOptions.transKeepBasicHtmlNodesFor ?? [])
|
|
67
|
+
: [];
|
|
68
|
+
|
|
69
|
+
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
|
|
70
|
+
childrenArray.forEach((child, childIndex) => {
|
|
71
|
+
if (isString(child)) {
|
|
72
|
+
// actual e.g. lorem
|
|
73
|
+
// expected e.g. lorem
|
|
74
|
+
stringNode += `${child}`;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (isValidElement(child)) {
|
|
78
|
+
const { props, type } = child;
|
|
79
|
+
const childPropsCount = Object.keys(props).length;
|
|
80
|
+
const shouldKeepChild = keepArray.indexOf(type) > -1;
|
|
81
|
+
const childChildren = props.children;
|
|
82
|
+
|
|
83
|
+
if (!childChildren && shouldKeepChild && !childPropsCount) {
|
|
84
|
+
// actual e.g. lorem <br/> ipsum
|
|
85
|
+
// expected e.g. lorem <br/> ipsum
|
|
86
|
+
stringNode += `<${type}/>`;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) {
|
|
90
|
+
// actual e.g. lorem <hr className="test" /> ipsum
|
|
91
|
+
// expected e.g. lorem <0></0> ipsum
|
|
92
|
+
// or
|
|
93
|
+
// we got a dynamic list like
|
|
94
|
+
// e.g. <ul i18nIsDynamicList>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>
|
|
95
|
+
// expected e.g. "<0></0>", not e.g. "<0><0>a</0><1>b</1></0>"
|
|
96
|
+
stringNode += `<${childIndex}></${childIndex}>`;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (shouldKeepChild && childPropsCount <= 1) {
|
|
100
|
+
// actual e.g. dolor <strong>bold</strong> amet
|
|
101
|
+
// expected e.g. dolor <strong>bold</strong> amet
|
|
102
|
+
const cnt = isString(childChildren)
|
|
103
|
+
? childChildren
|
|
104
|
+
: nodesToString(childChildren, i18nOptions, i18n, i18nKey);
|
|
105
|
+
stringNode += `<${type}>${cnt}</${type}>`;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// regular case mapping the inner children
|
|
109
|
+
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
|
|
110
|
+
stringNode += `<${childIndex}>${content}</${childIndex}>`;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (child === null) {
|
|
114
|
+
warn(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, { i18nKey });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (isObject(child)) {
|
|
118
|
+
// e.g. lorem {{ value, format }} ipsum
|
|
119
|
+
const { format, ...clone } = child;
|
|
120
|
+
const keys = Object.keys(clone);
|
|
121
|
+
|
|
122
|
+
if (keys.length === 1) {
|
|
123
|
+
const value = format ? `${keys[0]}, ${format}` : keys[0];
|
|
124
|
+
stringNode += `{{${value}}}`;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
warn(
|
|
128
|
+
i18n,
|
|
129
|
+
'TRANS_INVALID_OBJ',
|
|
130
|
+
`Invalid child - Object should only have keys {{ value, format }} (format is optional).`,
|
|
131
|
+
{ i18nKey, child },
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
warn(
|
|
136
|
+
i18n,
|
|
137
|
+
'TRANS_INVALID_VAR',
|
|
138
|
+
`Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`,
|
|
139
|
+
{ i18nKey, child },
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return stringNode;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Escape literal < characters that are not part of valid tags
|
|
148
|
+
* Valid tags are: numbered tags like <0>, </0> or named tags from keepArray/knownComponents
|
|
149
|
+
* @param {string} str - The string to escape
|
|
150
|
+
* @param {Array<string>} keepArray - Array of HTML tag names to keep
|
|
151
|
+
* @param {Object} knownComponentsMap - Map of known component names
|
|
152
|
+
* @returns {string} String with literal < characters escaped
|
|
153
|
+
*/
|
|
154
|
+
const escapeLiteralLessThan = (str, keepArray = [], knownComponentsMap = {}) => {
|
|
155
|
+
if (!str) return str;
|
|
156
|
+
|
|
157
|
+
// Build a list of valid tag names (numbered indices and known component names)
|
|
158
|
+
const knownNames = Object.keys(knownComponentsMap);
|
|
159
|
+
const allValidNames = [...keepArray, ...knownNames];
|
|
160
|
+
|
|
161
|
+
// Pattern to match:
|
|
162
|
+
// 1. Opening tags: <number> or <name> where name is in allValidNames
|
|
163
|
+
// 2. Closing tags: </number> or </name> where name is in allValidNames
|
|
164
|
+
// 3. Self-closing tags: <name/> or <name /> where name is in keepArray
|
|
165
|
+
// Everything else starting with < should be escaped
|
|
166
|
+
|
|
167
|
+
let result = '';
|
|
168
|
+
let i = 0;
|
|
169
|
+
|
|
170
|
+
while (i < str.length) {
|
|
171
|
+
if (str[i] === '<') {
|
|
172
|
+
// Check if this is a valid tag
|
|
173
|
+
let isValidTag = false;
|
|
174
|
+
|
|
175
|
+
// Check for closing tag: </number> or </name>
|
|
176
|
+
const closingMatch = str.slice(i).match(/^<\/(\d+|[a-zA-Z][a-zA-Z0-9_-]*)>/);
|
|
177
|
+
if (closingMatch) {
|
|
178
|
+
const tagName = closingMatch[1];
|
|
179
|
+
// Valid if it's a number or in our valid names list
|
|
180
|
+
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
|
|
181
|
+
isValidTag = true;
|
|
182
|
+
result += closingMatch[0];
|
|
183
|
+
i += closingMatch[0].length;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for opening tag: <number> or <name> or <name/> or <name />
|
|
188
|
+
// Also handle tags with attributes: <0 href="..."> or <name class="...">
|
|
189
|
+
if (!isValidTag) {
|
|
190
|
+
// Match: <tagName [attributes] [/]>
|
|
191
|
+
// Attributes pattern: name="value" or name='value' or name (boolean)
|
|
192
|
+
const openingMatch = str
|
|
193
|
+
.slice(i)
|
|
194
|
+
.match(
|
|
195
|
+
/^<(\d+|[a-zA-Z][a-zA-Z0-9_-]*)(\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*(\/)?>/,
|
|
196
|
+
);
|
|
197
|
+
if (openingMatch) {
|
|
198
|
+
const tagName = openingMatch[1];
|
|
199
|
+
// Valid if it's a number or in our valid names list
|
|
200
|
+
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
|
|
201
|
+
isValidTag = true;
|
|
202
|
+
result += openingMatch[0];
|
|
203
|
+
i += openingMatch[0].length;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// If not a valid tag, escape the <
|
|
209
|
+
if (!isValidTag) {
|
|
210
|
+
result += '<';
|
|
211
|
+
i += 1;
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
result += str[i];
|
|
215
|
+
i += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const renderNodes = (
|
|
223
|
+
children,
|
|
224
|
+
knownComponentsMap,
|
|
225
|
+
targetString,
|
|
226
|
+
i18n,
|
|
227
|
+
i18nOptions,
|
|
228
|
+
combinedTOpts,
|
|
229
|
+
shouldUnescape,
|
|
230
|
+
) => {
|
|
231
|
+
if (targetString === '') return [];
|
|
232
|
+
|
|
233
|
+
// check if contains tags we need to replace from html string to react nodes
|
|
234
|
+
const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || [];
|
|
235
|
+
const emptyChildrenButNeedsHandling =
|
|
236
|
+
targetString && new RegExp(keepArray.map((keep) => `<${keep}`).join('|')).test(targetString);
|
|
237
|
+
|
|
238
|
+
// no need to replace tags in the targetstring
|
|
239
|
+
if (!children && !knownComponentsMap && !emptyChildrenButNeedsHandling && !shouldUnescape)
|
|
240
|
+
return [targetString];
|
|
241
|
+
|
|
242
|
+
// v2 -> interpolates upfront no need for "some <0>{{var}}</0>"" -> will be just "some {{var}}" in translation file
|
|
243
|
+
const data = knownComponentsMap ?? {};
|
|
244
|
+
|
|
245
|
+
const getData = (childs) => {
|
|
246
|
+
const childrenArray = getAsArray(childs);
|
|
247
|
+
|
|
248
|
+
childrenArray.forEach((child) => {
|
|
249
|
+
if (isString(child)) return;
|
|
250
|
+
if (hasChildren(child)) getData(getChildren(child));
|
|
251
|
+
else if (isObject(child) && !isValidElement(child)) Object.assign(data, child);
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
getData(children);
|
|
256
|
+
|
|
257
|
+
// Escape literal < characters that are not part of valid tags before parsing
|
|
258
|
+
const escapedString = escapeLiteralLessThan(targetString, keepArray, data);
|
|
259
|
+
|
|
260
|
+
// parse ast from string with additional wrapper tag
|
|
261
|
+
// -> avoids issues in parser removing prepending text nodes
|
|
262
|
+
const ast = HTML.parse(`<0>${escapedString}</0>`);
|
|
263
|
+
const opts = { ...data, ...combinedTOpts };
|
|
264
|
+
|
|
265
|
+
const renderInner = (child, node, rootReactNode) => {
|
|
266
|
+
const childs = getChildren(child);
|
|
267
|
+
const mappedChildren = mapAST(childs, node.children, rootReactNode);
|
|
268
|
+
// `mappedChildren` will always be empty if using the `i18nIsDynamicList` prop,
|
|
269
|
+
// but the children might not necessarily be react components
|
|
270
|
+
return (hasValidReactChildren(childs) && mappedChildren.length === 0) ||
|
|
271
|
+
child.props?.i18nIsDynamicList
|
|
272
|
+
? childs
|
|
273
|
+
: mappedChildren;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const pushTranslatedJSX = (child, inner, mem, i, isVoid) => {
|
|
277
|
+
if (child.dummy) {
|
|
278
|
+
// This parser-only wrapper is intentionally not an element descriptor.
|
|
279
|
+
// React's cloneElement tolerates that internal shape; octane validates
|
|
280
|
+
// descriptors strictly, so retain the wrapper and unwrap it below.
|
|
281
|
+
child.children = isVoid ? undefined : inner;
|
|
282
|
+
mem.push(child);
|
|
283
|
+
} else {
|
|
284
|
+
mem.push(
|
|
285
|
+
...Children.map([child], (c) => {
|
|
286
|
+
// Fragments only accept key/children (#1914), and elements carrying the internal
|
|
287
|
+
// i18nIsDynamicList prop must not forward it to the DOM (#1915). cloneElement
|
|
288
|
+
// cannot remove props from the merged result, so in both cases we rebuild the
|
|
289
|
+
// props via createElement. Fragments can't have refs, and i18nIsDynamicList is
|
|
290
|
+
// typically used on list wrappers rather than ref'd elements, so the common
|
|
291
|
+
// ref-forwarding path (#1887) still goes through cloneElement below.
|
|
292
|
+
if (c.type === Fragment || c.props?.i18nIsDynamicList !== undefined) {
|
|
293
|
+
const freshProps = { key: i };
|
|
294
|
+
if (c && c.props) {
|
|
295
|
+
Object.keys(c.props).forEach((k) => {
|
|
296
|
+
if (k === 'children' || k === 'i18nIsDynamicList') return;
|
|
297
|
+
// On React >= 19 `ref` is a regular prop and flows through here.
|
|
298
|
+
freshProps[k] = c.props[k];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return createElement(c.type, freshProps, isVoid ? null : inner);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Use cloneElement so React preserves/forwards refs internally without us
|
|
305
|
+
// ever accessing element.ref or c.props.ref (avoids the React 19 warning). (#1887)
|
|
306
|
+
const override = { key: i };
|
|
307
|
+
if (c && c.props) {
|
|
308
|
+
Object.keys(c.props).forEach((k) => {
|
|
309
|
+
if (k === 'ref' || k === 'children') return;
|
|
310
|
+
override[k] = c.props[k];
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return cloneElement(c, override, isVoid ? null : inner);
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// reactNode (the jsx root element or child)
|
|
320
|
+
// astNode (the translation string as html ast)
|
|
321
|
+
// rootReactNode (the most outer jsx children array or trans components prop)
|
|
322
|
+
const mapAST = (reactNode, astNode, rootReactNode) => {
|
|
323
|
+
const reactNodes = getAsArray(reactNode);
|
|
324
|
+
const astNodes = getAsArray(astNode);
|
|
325
|
+
|
|
326
|
+
// Track keep-tag occurrences at this level so we can match the n-th `<p>` AST
|
|
327
|
+
// node to the n-th original React element with type === 'p' (#1919).
|
|
328
|
+
const keepTagOccurrence = {};
|
|
329
|
+
|
|
330
|
+
return astNodes.reduce((mem, node, i) => {
|
|
331
|
+
const translationContent =
|
|
332
|
+
node.children?.[0]?.content &&
|
|
333
|
+
i18n.services.interpolator.interpolate(node.children[0].content, opts, i18n.language);
|
|
334
|
+
|
|
335
|
+
if (node.type === 'tag') {
|
|
336
|
+
// regular array (components or children)
|
|
337
|
+
let tmp = reactNodes[parseInt(node.name, 10)];
|
|
338
|
+
if (!tmp && knownComponentsMap) tmp = knownComponentsMap[node.name];
|
|
339
|
+
|
|
340
|
+
// trans components is an object
|
|
341
|
+
if (rootReactNode.length === 1 && !tmp) tmp = rootReactNode[0][node.name];
|
|
342
|
+
|
|
343
|
+
// neither
|
|
344
|
+
if (!tmp) tmp = {};
|
|
345
|
+
|
|
346
|
+
// should fix #1893
|
|
347
|
+
const props = { ...node.attrs };
|
|
348
|
+
if (shouldUnescape) {
|
|
349
|
+
Object.keys(props).forEach((p) => {
|
|
350
|
+
const val = props[p];
|
|
351
|
+
if (isString(val)) {
|
|
352
|
+
props[p] = unescape(val);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const child = Object.keys(props).length !== 0 ? mergeProps({ props }, tmp) : tmp;
|
|
358
|
+
|
|
359
|
+
const isElement = isValidElement(child);
|
|
360
|
+
|
|
361
|
+
const isValidTranslationWithChildren =
|
|
362
|
+
isElement && hasChildren(node, true) && !node.voidElement;
|
|
363
|
+
|
|
364
|
+
const isEmptyTransWithHTML =
|
|
365
|
+
emptyChildrenButNeedsHandling && isObject(child) && child.dummy && !isElement;
|
|
366
|
+
|
|
367
|
+
const isKnownComponent =
|
|
368
|
+
isObject(knownComponentsMap) && Object.hasOwnProperty.call(knownComponentsMap, node.name);
|
|
369
|
+
|
|
370
|
+
if (isString(child)) {
|
|
371
|
+
const value = i18n.services.interpolator.interpolate(child, opts, i18n.language);
|
|
372
|
+
mem.push(value);
|
|
373
|
+
} else if (
|
|
374
|
+
hasChildren(child) || // the jsx element has children -> loop
|
|
375
|
+
isValidTranslationWithChildren // valid jsx element with no children but the translation has -> loop
|
|
376
|
+
) {
|
|
377
|
+
const inner = renderInner(child, node, rootReactNode);
|
|
378
|
+
pushTranslatedJSX(child, inner, mem, i);
|
|
379
|
+
} else if (isEmptyTransWithHTML) {
|
|
380
|
+
// we have a empty Trans node (the dummy element) with a targetstring that contains html tags needing
|
|
381
|
+
// conversion to react nodes
|
|
382
|
+
// so we just need to map the inner stuff
|
|
383
|
+
const inner = mapAST(
|
|
384
|
+
reactNodes /* wrong but we need something */,
|
|
385
|
+
node.children,
|
|
386
|
+
rootReactNode,
|
|
387
|
+
);
|
|
388
|
+
pushTranslatedJSX(child, inner, mem, i);
|
|
389
|
+
} else if (Number.isNaN(parseFloat(node.name))) {
|
|
390
|
+
if (isKnownComponent) {
|
|
391
|
+
const inner = renderInner(child, node, rootReactNode);
|
|
392
|
+
pushTranslatedJSX(child, inner, mem, i, node.voidElement);
|
|
393
|
+
} else if (i18nOptions.transSupportBasicHtmlNodes && keepArray.indexOf(node.name) > -1) {
|
|
394
|
+
if (node.voidElement) {
|
|
395
|
+
mem.push(createElement(node.name, { key: `${node.name}-${i}` }));
|
|
396
|
+
} else {
|
|
397
|
+
// Find the matching React element by tag name (positional among
|
|
398
|
+
// same-named keep-tags at this level) so its children become the
|
|
399
|
+
// scope for any indexed <N> placeholders nested inside. Without
|
|
400
|
+
// this, `<N>` would be looked up in the parent scope. (#1919)
|
|
401
|
+
const occurrence = keepTagOccurrence[node.name] || 0;
|
|
402
|
+
keepTagOccurrence[node.name] = occurrence + 1;
|
|
403
|
+
let matched;
|
|
404
|
+
let seen = 0;
|
|
405
|
+
for (let r = 0; r < reactNodes.length; r += 1) {
|
|
406
|
+
const rn = reactNodes[r];
|
|
407
|
+
if (isValidElement(rn) && rn.type === node.name) {
|
|
408
|
+
if (seen === occurrence) {
|
|
409
|
+
matched = rn;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
seen += 1;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const innerScope = matched ? getAsArray(getChildren(matched)) : reactNodes;
|
|
416
|
+
const inner = mapAST(innerScope, node.children, rootReactNode);
|
|
417
|
+
|
|
418
|
+
mem.push(createElement(node.name, { key: `${node.name}-${i}` }, inner));
|
|
419
|
+
}
|
|
420
|
+
} else if (node.voidElement) {
|
|
421
|
+
mem.push(`<${node.name} />`);
|
|
422
|
+
} else {
|
|
423
|
+
const inner = mapAST(
|
|
424
|
+
reactNodes /* wrong but we need something */,
|
|
425
|
+
node.children,
|
|
426
|
+
rootReactNode,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
mem.push(`<${node.name}>${inner}</${node.name}>`);
|
|
430
|
+
}
|
|
431
|
+
} else if (isObject(child) && !isElement) {
|
|
432
|
+
const content = node.children[0] ? translationContent : null;
|
|
433
|
+
|
|
434
|
+
// v1
|
|
435
|
+
// as interpolation was done already we just have a regular content node
|
|
436
|
+
// in the translation AST while having an object in reactNodes
|
|
437
|
+
// -> push the content no need to interpolate again
|
|
438
|
+
if (content) mem.push(content);
|
|
439
|
+
} else {
|
|
440
|
+
// If component does not have children, but translation - has
|
|
441
|
+
// with this in component could be components={[<span class='make-beautiful'/>]} and in translation - 'some text <0>some highlighted message</0>'
|
|
442
|
+
pushTranslatedJSX(
|
|
443
|
+
child,
|
|
444
|
+
translationContent,
|
|
445
|
+
mem,
|
|
446
|
+
i,
|
|
447
|
+
node.children.length !== 1 || !translationContent,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
} else if (node.type === 'text') {
|
|
451
|
+
const wrapTextNodes = i18nOptions.transWrapTextNodes;
|
|
452
|
+
const unescapeFn =
|
|
453
|
+
typeof i18nOptions.unescape === 'function'
|
|
454
|
+
? i18nOptions.unescape
|
|
455
|
+
: getDefaults().unescape;
|
|
456
|
+
const content = shouldUnescape
|
|
457
|
+
? unescapeFn(i18n.services.interpolator.interpolate(node.content, opts, i18n.language))
|
|
458
|
+
: i18n.services.interpolator.interpolate(node.content, opts, i18n.language);
|
|
459
|
+
if (wrapTextNodes) {
|
|
460
|
+
mem.push(createElement(wrapTextNodes, { key: `${node.name}-${i}` }, content));
|
|
461
|
+
} else {
|
|
462
|
+
mem.push(content);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return mem;
|
|
466
|
+
}, []);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// call mapAST with having react nodes nested into additional node like
|
|
470
|
+
// we did for the string ast from translation
|
|
471
|
+
// return the children of that extra node to get expected result
|
|
472
|
+
const result = mapAST(
|
|
473
|
+
[{ dummy: true, children: children || [] }],
|
|
474
|
+
ast,
|
|
475
|
+
getAsArray(children || []),
|
|
476
|
+
);
|
|
477
|
+
return getChildren(result[0]);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const fixComponentProps = (component, index, translation) => {
|
|
481
|
+
const componentKey = component.key || index;
|
|
482
|
+
const comp = cloneElement(component, { key: componentKey });
|
|
483
|
+
if (
|
|
484
|
+
!comp.props ||
|
|
485
|
+
!comp.props.children ||
|
|
486
|
+
(translation.indexOf(`${index}/>`) < 0 && translation.indexOf(`${index} />`) < 0)
|
|
487
|
+
) {
|
|
488
|
+
return comp;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function Componentized() {
|
|
492
|
+
return comp;
|
|
493
|
+
}
|
|
494
|
+
// <Componentized />
|
|
495
|
+
return createElement(Componentized, { key: componentKey });
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const generateArrayComponents = (components, translation) =>
|
|
499
|
+
components.map((c, index) => fixComponentProps(c, index, translation));
|
|
500
|
+
|
|
501
|
+
const generateObjectComponents = (components, translation) => {
|
|
502
|
+
const componentMap = {};
|
|
503
|
+
|
|
504
|
+
Object.keys(components).forEach((c) => {
|
|
505
|
+
Object.assign(componentMap, {
|
|
506
|
+
[c]: fixComponentProps(components[c], c, translation),
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return componentMap;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const generateComponents = (components, translation, i18n, i18nKey) => {
|
|
514
|
+
if (!components) return null;
|
|
515
|
+
|
|
516
|
+
// components could be either an array or an object
|
|
517
|
+
|
|
518
|
+
if (Array.isArray(components)) {
|
|
519
|
+
return generateArrayComponents(components, translation);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (isObject(components)) {
|
|
523
|
+
return generateObjectComponents(components, translation);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// if components is not an array or an object, warn the user
|
|
527
|
+
// and return null
|
|
528
|
+
warnOnce(
|
|
529
|
+
i18n,
|
|
530
|
+
'TRANS_INVALID_COMPONENTS',
|
|
531
|
+
`<Trans /> "components" prop expects an object or array`,
|
|
532
|
+
{ i18nKey },
|
|
533
|
+
);
|
|
534
|
+
return null;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// A component map is an object like: { Button: <button> }, but not an object like { 1: <button> }
|
|
538
|
+
const isComponentsMap = (object) => {
|
|
539
|
+
if (!isObject(object)) return false;
|
|
540
|
+
if (Array.isArray(object)) return false;
|
|
541
|
+
return Object.keys(object).reduce(
|
|
542
|
+
(acc, key) => acc && Number.isNaN(Number.parseFloat(key)),
|
|
543
|
+
true,
|
|
544
|
+
);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
export function Trans({
|
|
548
|
+
children,
|
|
549
|
+
count,
|
|
550
|
+
parent,
|
|
551
|
+
i18nKey,
|
|
552
|
+
context,
|
|
553
|
+
tOptions = {},
|
|
554
|
+
values,
|
|
555
|
+
defaults,
|
|
556
|
+
components,
|
|
557
|
+
ns,
|
|
558
|
+
i18n: i18nFromProps,
|
|
559
|
+
t: tFromProps,
|
|
560
|
+
shouldUnescape,
|
|
561
|
+
...additionalProps
|
|
562
|
+
}) {
|
|
563
|
+
const i18n = i18nFromProps || getI18n();
|
|
564
|
+
|
|
565
|
+
// Natural `.tsrx` component children compile to an imperative render body,
|
|
566
|
+
// which cannot be inspected to derive a default string or component map.
|
|
567
|
+
// Prop-position JSX (`children={<>…</>}`) compiles to descriptor values and
|
|
568
|
+
// follows the complete upstream path below. Fail safe here: preserve the
|
|
569
|
+
// authored fallback UI and make the required adaptation explicit.
|
|
570
|
+
if (isChildrenBlock(children)) {
|
|
571
|
+
warnOnce(
|
|
572
|
+
i18n,
|
|
573
|
+
'OCTANE_TRANS_BLOCK_CHILDREN',
|
|
574
|
+
'Trans cannot inspect natural .tsrx block children. Pass descriptor children with `children={<>…</>}`, or use `defaults` plus `components`.',
|
|
575
|
+
{ i18nKey },
|
|
576
|
+
);
|
|
577
|
+
return children;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!i18n) {
|
|
581
|
+
warnOnce(
|
|
582
|
+
i18n,
|
|
583
|
+
'NO_I18NEXT_INSTANCE',
|
|
584
|
+
`Trans: You need to pass in an i18next instance using i18nextReactModule`,
|
|
585
|
+
{ i18nKey },
|
|
586
|
+
);
|
|
587
|
+
return children;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const t = tFromProps || i18n.t.bind(i18n) || ((k) => k);
|
|
591
|
+
|
|
592
|
+
const reactI18nextOptions = { ...getDefaults(), ...i18n.options?.react };
|
|
593
|
+
|
|
594
|
+
// prepare having a namespace
|
|
595
|
+
let namespaces = ns || t.ns || i18n.options?.defaultNS;
|
|
596
|
+
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
|
|
597
|
+
|
|
598
|
+
const { transDefaultProps } = reactI18nextOptions;
|
|
599
|
+
const mergedTOptions = transDefaultProps?.tOptions
|
|
600
|
+
? { ...transDefaultProps.tOptions, ...tOptions }
|
|
601
|
+
: tOptions;
|
|
602
|
+
|
|
603
|
+
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
|
|
604
|
+
|
|
605
|
+
const mergedValues = transDefaultProps?.values
|
|
606
|
+
? { ...transDefaultProps.values, ...values }
|
|
607
|
+
: values;
|
|
608
|
+
|
|
609
|
+
const mergedComponents = transDefaultProps?.components
|
|
610
|
+
? { ...transDefaultProps.components, ...components }
|
|
611
|
+
: components;
|
|
612
|
+
|
|
613
|
+
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
|
|
614
|
+
const defaultValue =
|
|
615
|
+
defaults ||
|
|
616
|
+
mergedTOptions?.defaultValue ||
|
|
617
|
+
nodeAsString ||
|
|
618
|
+
reactI18nextOptions.transEmptyNodeValue ||
|
|
619
|
+
(typeof i18nKey === 'function' ? keyFromSelector(i18nKey) : i18nKey);
|
|
620
|
+
const { hashTransKey } = reactI18nextOptions;
|
|
621
|
+
const key =
|
|
622
|
+
i18nKey ||
|
|
623
|
+
(hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue);
|
|
624
|
+
if (i18n.options?.interpolation?.defaultVariables) {
|
|
625
|
+
// eslint-disable-next-line no-param-reassign
|
|
626
|
+
values =
|
|
627
|
+
mergedValues && Object.keys(mergedValues).length > 0
|
|
628
|
+
? { ...mergedValues, ...i18n.options.interpolation.defaultVariables }
|
|
629
|
+
: { ...i18n.options.interpolation.defaultVariables };
|
|
630
|
+
} else {
|
|
631
|
+
// eslint-disable-next-line no-param-reassign
|
|
632
|
+
values = mergedValues;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const valuesFromChildren = getValuesFromChildren(children);
|
|
636
|
+
|
|
637
|
+
if (valuesFromChildren && typeof valuesFromChildren.count === 'number' && count === undefined) {
|
|
638
|
+
// eslint-disable-next-line no-param-reassign
|
|
639
|
+
count = valuesFromChildren.count;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const interpolationOverride =
|
|
643
|
+
values ||
|
|
644
|
+
(count !== undefined && !i18n.options?.interpolation?.alwaysFormat) || // https://github.com/i18next/react-i18next/issues/1719 + https://github.com/i18next/react-i18next/issues/1801
|
|
645
|
+
!children // if !children gets problems in future, undo that fix: https://github.com/i18next/react-i18next/issues/1729 by removing !children from this condition
|
|
646
|
+
? mergedTOptions.interpolation
|
|
647
|
+
: { interpolation: { ...mergedTOptions.interpolation, prefix: '#$?', suffix: '?$#' } };
|
|
648
|
+
const combinedTOpts = {
|
|
649
|
+
...mergedTOptions,
|
|
650
|
+
context: context || mergedTOptions.context, // Add `context` from the props or fallback to the value from `tOptions`
|
|
651
|
+
count,
|
|
652
|
+
...values,
|
|
653
|
+
...interpolationOverride,
|
|
654
|
+
defaultValue,
|
|
655
|
+
ns: namespaces,
|
|
656
|
+
};
|
|
657
|
+
let translation = key ? t(key, combinedTOpts) : defaultValue;
|
|
658
|
+
if (translation === key && defaultValue) translation = defaultValue;
|
|
659
|
+
|
|
660
|
+
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
|
|
661
|
+
let indexedChildren = generatedComponents || children;
|
|
662
|
+
let componentsMap = null;
|
|
663
|
+
if (isComponentsMap(generatedComponents)) {
|
|
664
|
+
componentsMap = generatedComponents;
|
|
665
|
+
indexedChildren = children;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const content = renderNodes(
|
|
669
|
+
indexedChildren,
|
|
670
|
+
componentsMap,
|
|
671
|
+
translation,
|
|
672
|
+
i18n,
|
|
673
|
+
reactI18nextOptions,
|
|
674
|
+
combinedTOpts,
|
|
675
|
+
mergedShouldUnescape,
|
|
676
|
+
);
|
|
677
|
+
// Translation AST siblings are a fixed positional sequence, like a JSX
|
|
678
|
+
// fragment. Mark the array so octane does not issue the runtime-list key
|
|
679
|
+
// warning for text nodes that cannot carry keys.
|
|
680
|
+
positionalChildren(content);
|
|
681
|
+
|
|
682
|
+
// allows user to pass `null` to `parent`
|
|
683
|
+
// and override `defaultTransParent` if is present
|
|
684
|
+
const useAsParent = parent ?? reactI18nextOptions.defaultTransParent;
|
|
685
|
+
|
|
686
|
+
return useAsParent ? createElement(useAsParent, additionalProps, content) : content;
|
|
687
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useTranslation } from './useTranslation.js';
|
|
2
|
+
import { S } from './internal.js';
|
|
3
|
+
|
|
4
|
+
export const Translation = ({ ns, children, ...options }) => {
|
|
5
|
+
const [t, i18n, ready] = useTranslation(ns, options, S('Translation:useTranslation'));
|
|
6
|
+
|
|
7
|
+
return children(
|
|
8
|
+
t,
|
|
9
|
+
{
|
|
10
|
+
i18n,
|
|
11
|
+
lng: i18n?.language,
|
|
12
|
+
},
|
|
13
|
+
ready,
|
|
14
|
+
);
|
|
15
|
+
};
|