Skip to content

Commit 9f9deb6

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

File tree

4 files changed

+140
-23
lines changed

4 files changed

+140
-23
lines changed

src/TransWithoutContext.js

+44-15
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,6 +39,7 @@ export const nodesToString = (children, i18nOptions) => {
3939
? (i18nOptions.transKeepBasicHtmlNodesFor ?? [])
4040
: [];
4141

42+
const warnings = _parentWarnings || [];
4243
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
4344
childrenArray.forEach((child, childIndex) => {
4445
if (isString(child)) {
@@ -72,11 +73,14 @@ export const nodesToString = (children, i18nOptions) => {
7273
stringNode += `<${type}>${childChildren}</${type}>`;
7374
} else {
7475
// regular case mapping the inner children
75-
const content = nodesToString(childChildren, i18nOptions);
76+
const content = nodesToString(childChildren, i18nOptions, i18nKey, warnings);
7677
stringNode += `<${childIndex}>${content}</${childIndex}>`;
7778
}
7879
} else if (child === null) {
79-
warn(`Trans: the passed in value is invalid - seems you passed in a null child.`);
80+
warnings.push({
81+
code: ERR_CODES.TRANS_NULL_VALUE,
82+
message: `The passed in value is invalid - seems you passed in a null child.`,
83+
});
8084
} else if (isObject(child)) {
8185
// e.g. lorem {{ value, format }} ipsum
8286
const { format, ...clone } = child;
@@ -87,19 +91,28 @@ export const nodesToString = (children, i18nOptions) => {
8791
stringNode += `{{${value}}}`;
8892
} else {
8993
// 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.`,
94+
warnings.push({
95+
code: ERR_CODES.TRANS_INVALID_OBJ,
96+
message: `The passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
9297
child,
93-
);
98+
});
9499
}
95100
} 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}}.`,
101+
warnings.push({
102+
code: ERR_CODES.TRANS_INVALID_VAR,
103+
message: `Passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.`,
98104
child,
99-
);
105+
});
100106
}
101107
});
102108

109+
if (warnings.length && !_parentWarnings) {
110+
warnOnce(`Trans:key:${i18nKey}: Interpolation errors - check the passed childrens.`, {
111+
i18nKey,
112+
warnings,
113+
});
114+
}
115+
103116
return stringNode;
104117
};
105118

@@ -332,7 +345,7 @@ const generateObjectComponents = (components, translation) => {
332345
return componentMap;
333346
};
334347

335-
const generateComponents = (components, translation) => {
348+
const generateComponents = (components, translation, i18nKey) => {
336349
if (!components) return null;
337350

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

348361
// if components is not an array or an object, warn the user
349362
// and return null
350-
warnOnce('<Trans /> component prop expects an object or an array');
363+
warnOnce(`Trans:key:${i18nKey} "components" prop expects an object or an array`, {
364+
i18nKey,
365+
warnings: [
366+
{
367+
code: ERR_CODES.TRANS_INVALID_COMPONENTS,
368+
message: `<Trans /> "components" prop expects an object or an array, received ${typeof components}`,
369+
},
370+
],
371+
});
351372
return null;
352373
};
353374

@@ -370,7 +391,15 @@ export function Trans({
370391
const i18n = i18nFromProps || getI18n();
371392

372393
if (!i18n) {
373-
warnOnce('You will need to pass in an i18next instance by using i18nextReactModule');
394+
warnOnce('Trans: You will need to pass in an i18next instance by using i18nextReactModule', {
395+
i18nKey,
396+
warnings: [
397+
{
398+
code: ERR_CODES.NO_I18NEXT_INSTANCE,
399+
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
400+
},
401+
],
402+
});
374403
return children;
375404
}
376405

@@ -382,7 +411,7 @@ export function Trans({
382411
let namespaces = ns || t.ns || i18n.options?.defaultNS;
383412
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
384413

385-
const nodeAsString = nodesToString(children, reactI18nextOptions);
414+
const nodeAsString = nodesToString(children, reactI18nextOptions, i18nKey);
386415
const defaultValue =
387416
defaults || nodeAsString || reactI18nextOptions.transEmptyNodeValue || i18nKey;
388417
const { hashTransKey } = reactI18nextOptions;
@@ -413,7 +442,7 @@ export function Trans({
413442
};
414443
const translation = key ? t(key, combinedTOpts) : defaultValue;
415444

416-
const generatedComponents = generateComponents(components, translation);
445+
const generatedComponents = generateComponents(components, translation, i18nKey);
417446

418447
const content = renderNodes(
419448
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)