Skip to content

Commit 57f58fe

Browse files
feat: add meta with codes on warnings to allow conditional logging
1 parent ff509ba commit 57f58fe

File tree

4 files changed

+156
-39
lines changed

4 files changed

+156
-39
lines changed

src/TransWithoutContext.js

+81-33
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, i18n, i18nKey) => {
32+
export const nodesToString = (children, i18nOptions, i18n, i18nKey, _parentWarnings) => {
3333
if (!children) return '';
3434
let stringNode = '';
3535

@@ -39,13 +39,17 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
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,55 +59,79 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
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, i18n, i18nKey);
76-
stringNode += `<${childIndex}>${content}</${childIndex}>`;
80+
return;
7781
}
78-
} else if (child === null) {
79-
warn(i18n, `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, i18n, 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-
i18n,
92-
`react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
93-
child,
94-
i18nKey,
95-
);
106+
return;
96107
}
97-
} else {
98-
warn(
99-
i18n,
100-
`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.`,
101113
child,
102-
i18nKey,
103-
);
114+
});
115+
116+
return;
104117
}
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+
});
105125
});
106126

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

@@ -336,7 +364,7 @@ const generateObjectComponents = (components, translation) => {
336364
return componentMap;
337365
};
338366

339-
const generateComponents = (components, translation, i18n) => {
367+
const generateComponents = (components, translation, i18n, i18nKey) => {
340368
if (!components) return null;
341369

342370
// components could be either an array or an object
@@ -351,7 +379,15 @@ const generateComponents = (components, translation, i18n) => {
351379

352380
// if components is not an array or an object, warn the user
353381
// and return null
354-
warnOnce(i18n, '<Trans /> component prop expects an object or an array');
382+
warnOnce(i18n, `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+
});
355391
return null;
356392
};
357393

@@ -374,7 +410,19 @@ export function Trans({
374410
const i18n = i18nFromProps || getI18n();
375411

376412
if (!i18n) {
377-
warnOnce(i18n, 'You will need to pass in an i18next instance by using i18nextReactModule');
413+
warnOnce(
414+
i18n,
415+
'Trans: You will need to pass in an i18next instance by using i18nextReactModule',
416+
{
417+
i18nKey,
418+
warnings: [
419+
{
420+
code: ERR_CODES.NO_I18NEXT_INSTANCE,
421+
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
422+
},
423+
],
424+
},
425+
);
378426
return children;
379427
}
380428

@@ -417,7 +465,7 @@ export function Trans({
417465
};
418466
const translation = key ? t(key, combinedTOpts) : defaultValue;
419467

420-
const generatedComponents = generateComponents(components, translation, i18n);
468+
const generatedComponents = generateComponents(components, translation, i18n, i18nKey);
421469

422470
const content = renderNodes(
423471
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

+23-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,18 @@ 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(i18n, 'You will need to pass in an i18next instance by using initReactI18next');
39+
warnOnce(
40+
i18n,
41+
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
42+
{
43+
warnings: [
44+
{
45+
code: ERR_CODES.NO_I18NEXT_INSTANCE,
46+
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
47+
},
48+
],
49+
},
50+
);
3951
const notReadyT = (k, optsOrDefaultValue) => {
4052
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
4153
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
@@ -52,7 +64,16 @@ export const useTranslation = (ns, props = {}) => {
5264
if (i18n.options.react?.wait)
5365
warnOnce(
5466
i18n,
55-
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
67+
'useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
68+
{
69+
warnings: [
70+
{
71+
code: ERR_CODES.DEPRECATED_WAIT_OPTION,
72+
message:
73+
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
74+
},
75+
],
76+
},
5677
);
5778

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

src/utils.js

+48-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,56 @@ export const warn = (i18n, ...args) => {
99
console.warn(...args);
1010
}
1111
};
12+
export const ERR_CODES = {
13+
NO_I18NEXT_INSTANCE: 'NO_I18NEXT_INSTANCE',
14+
DEPRECATED_WAIT_OPTION: 'DEPRECATED_WAIT_OPTION',
15+
TRANS_NULL_VALUE: 'TRANS_NULL_VALUE',
16+
TRANS_INVALID_OBJ: 'TRANS_INVALID_OBJ',
17+
TRANS_INVALID_VAR: 'TRANS_INVALID_VAR',
18+
TRANS_INVALID_COMPONENTS: 'TRANS_INVALID_COMPONENTS',
19+
};
20+
21+
const alreadyWarned = {
22+
data: {},
23+
history: [],
24+
maxSize: 150,
25+
};
26+
const flagAsWarned = (hash) => {
27+
if (alreadyWarned.data[hash]) {
28+
return;
29+
}
30+
alreadyWarned.data[hash] = true;
31+
alreadyWarned.history.push(hash);
32+
if (alreadyWarned.history.length > alreadyWarned.maxSize) {
33+
const removeHashes = alreadyWarned.history.splice(0, 25);
34+
for (let i = 0; i < removeHashes.length; i += 1) {
35+
delete alreadyWarned.data[removeHashes[i]];
36+
}
37+
}
38+
};
39+
const getHash = (...args) =>
40+
/* eslint-disable-next-line no-bitwise */ (
41+
args
42+
.filter((a) => isString(a))
43+
.join('::::')
44+
.split('')
45+
.reduce((acc, b) => {
46+
/* eslint-disable-next-line no-bitwise, no-param-reassign */
47+
acc = (acc << 5) - acc + b.charCodeAt(0);
48+
return acc;
49+
}, 0) >>> 0
50+
)
51+
.toString(36)
52+
.padStart(6, '0');
1253

13-
const alreadyWarned = {};
1454
export const warnOnce = (i18n, ...args) => {
15-
if (isString(args[0]) && alreadyWarned[args[0]]) return;
16-
if (isString(args[0])) alreadyWarned[args[0]] = new Date();
17-
warn(i18n, ...args);
55+
const hash = (isString(args[0]) && getHash(...args)) || false;
56+
if (!isString(args[0]) || !alreadyWarned.data[hash]) {
57+
if (hash) {
58+
flagAsWarned(hash);
59+
}
60+
warn(i18n, ...args);
61+
}
1862
};
1963

2064
// not needed right now

0 commit comments

Comments
 (0)