@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.
@@ -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 += '&lt;';
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
+ };