Skip to content

Commit 914ebad

Browse files
feat: add option to override warn logger
1 parent 9b194b1 commit 914ebad

File tree

4 files changed

+174
-38
lines changed

4 files changed

+174
-38
lines changed

src/TransWithoutContext.js

+78-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react';
22
import HTML from 'html-parse-stringify';
3-
import { isObject, isString, warn, warnOnce } from './utils.js';
3+
import { ERR_CODES, isObject, isString, warnOnce } from './utils.js';
44
import { getDefaults } from './defaults.js';
55
import { getI18n } from './i18nInstance.js';
66

@@ -29,7 +29,7 @@ const mergeProps = (source, target) => {
2929
return newTarget;
3030
};
3131

32-
export const nodesToString = (children, i18nOptions) => {
32+
export const nodesToString = (children, i18nOptions, i18nKey, _parentWarnings) => {
3333
if (!children) return '';
3434
let stringNode = '';
3535

@@ -39,13 +39,17 @@ export const nodesToString = (children, i18nOptions) => {
3939
? (i18nOptions.transKeepBasicHtmlNodesFor ?? [])
4040
: [];
4141

42+
const warnings = Array.isArray(_parentWarnings) ? _parentWarnings : [];
4243
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
4344
childrenArray.forEach((child, childIndex) => {
4445
if (isString(child)) {
4546
// actual e.g. lorem
4647
// expected e.g. lorem
4748
stringNode += `${child}`;
48-
} else if (isValidElement(child)) {
49+
return;
50+
}
51+
52+
if (isValidElement(child)) {
4953
const { props, type } = child;
5054
const childPropsCount = Object.keys(props).length;
5155
const shouldKeepChild = keepArray.indexOf(type) > -1;
@@ -55,51 +59,79 @@ export const nodesToString = (children, i18nOptions) => {
5559
// actual e.g. lorem <br/> ipsum
5660
// expected e.g. lorem <br/> ipsum
5761
stringNode += `<${type}/>`;
58-
} else if (
59-
(!childChildren && (!shouldKeepChild || childPropsCount)) ||
60-
props.i18nIsDynamicList
61-
) {
62+
return;
63+
}
64+
65+
if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) {
6266
// actual e.g. lorem <hr className="test" /> ipsum
6367
// expected e.g. lorem <0></0> ipsum
6468
// or
6569
// we got a dynamic list like
6670
// e.g. <ul i18nIsDynamicList>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>
6771
// expected e.g. "<0></0>", not e.g. "<0><0>a</0><1>b</1></0>"
6872
stringNode += `<${childIndex}></${childIndex}>`;
69-
} else if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
73+
return;
74+
}
75+
76+
if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
7077
// actual e.g. dolor <strong>bold</strong> amet
7178
// expected e.g. dolor <strong>bold</strong> amet
7279
stringNode += `<${type}>${childChildren}</${type}>`;
73-
} else {
74-
// regular case mapping the inner children
75-
const content = nodesToString(childChildren, i18nOptions);
76-
stringNode += `<${childIndex}>${content}</${childIndex}>`;
80+
return;
7781
}
78-
} else if (child === null) {
79-
warn(`Trans: the passed in value is invalid - seems you passed in a null child.`);
80-
} else if (isObject(child)) {
82+
83+
// regular case mapping the inner children
84+
const content = nodesToString(childChildren, i18nOptions, i18nKey, warnings);
85+
stringNode += `<${childIndex}>${content}</${childIndex}>`;
86+
87+
return;
88+
}
89+
90+
if (child === null) {
91+
warnings.push({
92+
code: ERR_CODES.TRANS_NULL_VALUE,
93+
message: `The passed in value is invalid - seems you passed in a null child.`,
94+
});
95+
return;
96+
}
97+
98+
if (isObject(child)) {
8199
// e.g. lorem {{ value, format }} ipsum
82100
const { format, ...clone } = child;
83101
const keys = Object.keys(clone);
84102

85103
if (keys.length === 1) {
86104
const value = format ? `${keys[0]}, ${format}` : keys[0];
87105
stringNode += `{{${value}}}`;
88-
} else {
89-
// not a valid interpolation object (can only contain one value plus format)
90-
warn(
91-
`react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
92-
child,
93-
);
106+
return;
94107
}
95-
} else {
96-
warn(
97-
`Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.`,
108+
109+
// not a valid interpolation object (can only contain one value plus format)
110+
warnings.push({
111+
code: ERR_CODES.TRANS_INVALID_OBJ,
112+
message: `The passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
98113
child,
99-
);
114+
});
115+
116+
return;
100117
}
118+
119+
// e.g. lorem {number} ipsum
120+
warnings.push({
121+
code: ERR_CODES.TRANS_INVALID_VAR,
122+
message: `Passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.`,
123+
child,
124+
});
101125
});
102126

127+
// only warn once for a key and group warnings if there are more than 1.
128+
if (warnings.length && !_parentWarnings) {
129+
warnOnce(`Trans:key:${i18nKey}: Interpolation errors - check the passed childrens.`, {
130+
i18nKey,
131+
warnings,
132+
});
133+
}
134+
103135
return stringNode;
104136
};
105137

@@ -332,7 +364,7 @@ const generateObjectComponents = (components, translation) => {
332364
return componentMap;
333365
};
334366

335-
const generateComponents = (components, translation) => {
367+
const generateComponents = (components, translation, i18nKey) => {
336368
if (!components) return null;
337369

338370
// components could be either an array or an object
@@ -347,7 +379,15 @@ const generateComponents = (components, translation) => {
347379

348380
// if components is not an array or an object, warn the user
349381
// and return null
350-
warnOnce('<Trans /> component prop expects an object or an array');
382+
warnOnce(`Trans:key:${i18nKey} "components" prop expects an object or an array`, {
383+
i18nKey,
384+
warnings: [
385+
{
386+
code: ERR_CODES.TRANS_INVALID_COMPONENTS,
387+
message: `<Trans /> "components" prop expects an object or an array, received ${typeof components}`,
388+
},
389+
],
390+
});
351391
return null;
352392
};
353393

@@ -370,7 +410,15 @@ export function Trans({
370410
const i18n = i18nFromProps || getI18n();
371411

372412
if (!i18n) {
373-
warnOnce('You will need to pass in an i18next instance by using i18nextReactModule');
413+
warnOnce('Trans: You will need to pass in an i18next instance by using i18nextReactModule', {
414+
i18nKey,
415+
warnings: [
416+
{
417+
code: ERR_CODES.NO_I18NEXT_INSTANCE,
418+
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
419+
},
420+
],
421+
});
374422
return children;
375423
}
376424

@@ -382,7 +430,7 @@ export function Trans({
382430
let namespaces = ns || t.ns || i18n.options?.defaultNS;
383431
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
384432

385-
const nodeAsString = nodesToString(children, reactI18nextOptions);
433+
const nodeAsString = nodesToString(children, reactI18nextOptions, i18nKey);
386434
const defaultValue =
387435
defaults || nodeAsString || reactI18nextOptions.transEmptyNodeValue || i18nKey;
388436
const { hashTransKey } = reactI18nextOptions;
@@ -413,7 +461,7 @@ export function Trans({
413461
};
414462
const translation = key ? t(key, combinedTOpts) : defaultValue;
415463

416-
const generatedComponents = generateComponents(components, translation);
464+
const generatedComponents = generateComponents(components, translation, i18nKey);
417465

418466
const content = renderNodes(
419467
generatedComponents || children,

src/initReactI18next.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { setDefaults } from './defaults.js';
22
import { setI18n } from './i18nInstance.js';
3+
import { setWarnFn } from './utils.js';
34

45
export const initReactI18next = {
56
type: '3rdParty',
67

78
init(instance) {
89
setDefaults(instance.options.react);
10+
if (typeof instance.options.react.warn === 'function') {
11+
setWarnFn(instance.options.react.warn);
12+
}
913
setI18n(instance);
1014
},
1115
};

src/useTranslation.js

+22-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
hasLoadedNamespace,
88
isString,
99
isObject,
10+
ERR_CODES,
1011
} from './utils.js';
1112

1213
const usePrevious = (value, ignore) => {
@@ -35,7 +36,17 @@ export const useTranslation = (ns, props = {}) => {
3536
const i18n = i18nFromProps || i18nFromContext || getI18n();
3637
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
3738
if (!i18n) {
38-
warnOnce('You will need to pass in an i18next instance by using initReactI18next');
39+
warnOnce(
40+
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
41+
{
42+
warnings: [
43+
{
44+
code: ERR_CODES.NO_I18NEXT_INSTANCE,
45+
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
46+
},
47+
],
48+
},
49+
);
3950
const notReadyT = (k, optsOrDefaultValue) => {
4051
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
4152
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
@@ -51,7 +62,16 @@ export const useTranslation = (ns, props = {}) => {
5162

5263
if (i18n.options.react?.wait)
5364
warnOnce(
54-
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
65+
'useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
66+
{
67+
warnings: [
68+
{
69+
code: ERR_CODES.DEPRECATED_WAIT_OPTION,
70+
message:
71+
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
72+
},
73+
],
74+
},
5575
);
5676

5777
const i18nOptions = { ...getDefaults(), ...i18n.options.react, ...props };

src/utils.js

+70-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,79 @@
1-
export const warn = (...args) => {
1+
export const ERR_CODES = {
2+
NO_I18NEXT_INSTANCE: 'NO_I18NEXT_INSTANCE',
3+
DEPRECATED_WAIT_OPTION: 'DEPRECATED_WAIT_OPTION',
4+
TRANS_NULL_VALUE: 'TRANS_NULL_VALUE',
5+
TRANS_INVALID_OBJ: 'TRANS_INVALID_OBJ',
6+
TRANS_INVALID_VAR: 'TRANS_INVALID_VAR',
7+
TRANS_INVALID_COMPONENTS: 'TRANS_INVALID_COMPONENTS',
8+
};
9+
10+
const defaultWarn = (...args) => {
211
if (console?.warn) {
3-
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
412
console.warn(...args);
513
}
614
};
715

8-
const alreadyWarned = {};
16+
let warnFn = defaultWarn;
17+
export const setWarnFn = (fn) => {
18+
if (typeof fn === 'function' && fn !== warnFn) {
19+
warnFn = fn;
20+
} else {
21+
defaultWarn('react-i18next:: setting a non a function as warn function');
22+
}
23+
};
24+
25+
export const warn = (...args) => {
26+
if (isString(args[0])) {
27+
args[0] = `react-i18next:: ${args[0]}`;
28+
}
29+
if (typeof warnFn === 'function') {
30+
warnFn(...args);
31+
} else {
32+
defaultWarn(...args);
33+
}
34+
};
35+
36+
const alreadyWarned = {
37+
data: {},
38+
history: [],
39+
maxSize: 150,
40+
};
41+
const flagAsWarned = (hash) => {
42+
if (alreadyWarned.data[hash]) {
43+
return;
44+
}
45+
alreadyWarned.data[hash] = true;
46+
alreadyWarned.history.push(hash);
47+
if (alreadyWarned.history.length > alreadyWarned.maxSize) {
48+
const removeHashes = alreadyWarned.history.splice(0, 25);
49+
for (let i = 0; i < removeHashes.length; i += 1) {
50+
delete alreadyWarned.data[removeHashes[i]];
51+
}
52+
}
53+
};
54+
const getHash = (...args) =>
55+
/* eslint-disable-next-line no-bitwise */ (
56+
args
57+
.filter((a) => isString(a))
58+
.join('::::')
59+
.split('')
60+
.reduce((acc, b) => {
61+
/* eslint-disable-next-line no-bitwise, no-param-reassign */
62+
acc = (acc << 5) - acc + b.charCodeAt(0);
63+
return acc;
64+
}, 0) >>> 0
65+
)
66+
.toString(36)
67+
.padStart(6, '0');
68+
969
export const warnOnce = (...args) => {
10-
if (isString(args[0]) && alreadyWarned[args[0]]) return;
11-
if (isString(args[0])) alreadyWarned[args[0]] = new Date();
12-
warn(...args);
70+
const hash = (isString(args[0]) && getHash(...args)) || false;
71+
if (!isString(args[0]) || !alreadyWarned.data[hash]) {
72+
if (hash) {
73+
flagAsWarned(hash);
74+
}
75+
warn(...args);
76+
}
1377
};
1478

1579
// not needed right now

0 commit comments

Comments
 (0)