Skip to content

Commit 8ee8463

Browse files
feat: format warning with code and data to allow conditional logging
1 parent ff509ba commit 8ee8463

File tree

5 files changed

+105
-44
lines changed

5 files changed

+105
-44
lines changed

TransWithoutContext.d.ts

+36
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,39 @@ export function Trans<
3535
TOpt extends TOptions & { context?: TContext } = { context: TContext },
3636
E = React.HTMLProps<HTMLDivElement>,
3737
>(props: TransProps<Key, Ns, KPrefix, TContext, TOpt, E>): React.ReactElement;
38+
39+
export type ErrorCode =
40+
| 'NO_I18NEXT_INSTANCE'
41+
| 'NO_LANGUAGES'
42+
| 'DEPRECATED_OPTION'
43+
| 'TRANS_NULL_VALUE'
44+
| 'TRANS_INVALID_OBJ'
45+
| 'TRANS_INVALID_VAR'
46+
| 'TRANS_INVALID_COMPONENTS';
47+
48+
export type ErrorMeta = {
49+
code: ErrorCode;
50+
i18nKey?: string;
51+
[x: string]: any;
52+
};
53+
54+
/**
55+
* Use to type the logger arguments
56+
* @example
57+
* ```
58+
* import type { ErrorArgs } from 'react-i18next';
59+
*
60+
* const logger = {
61+
* // ....
62+
* warn: function (...args: ErrorArgs) {
63+
* if (args[1]?.code === 'TRANS_INVALID_OBJ') {
64+
* const [msg, { i18nKey, ...rest }] = args;
65+
* return log(i18nKey, msg, rest);
66+
* }
67+
* log(...args);
68+
* }
69+
* }
70+
* i18n.use(logger).use(i18nReactPlugin).init({...});
71+
* ```
72+
*/
73+
export type ErrorArgs = readonly [string, ErrorMeta | undefined, ...any[]];

index.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import type {
1010
KeyPrefix,
1111
} from 'i18next';
1212
import * as React from 'react';
13-
import { Trans, TransProps } from './TransWithoutContext.js';
13+
import type { Trans, TransProps, ErrorCode, ErrorArgs } from './TransWithoutContext.js';
1414
export { initReactI18next } from './initReactI18next.js';
1515

1616
export const TransWithoutContext: typeof Trans;
17-
export { Trans, TransProps };
17+
export { Trans, TransProps, ErrorArgs, ErrorCode };
1818

1919
export function setDefaults(options: ReactOptions): void;
2020
export function getDefaults(): ReactOptions;

src/TransWithoutContext.js

+45-29
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
4545
// actual e.g. lorem
4646
// expected e.g. lorem
4747
stringNode += `${child}`;
48-
} else if (isValidElement(child)) {
48+
return;
49+
}
50+
if (isValidElement(child)) {
4951
const { props, type } = child;
5052
const childPropsCount = Object.keys(props).length;
5153
const shouldKeepChild = keepArray.indexOf(type) > -1;
@@ -55,53 +57,57 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
5557
// actual e.g. lorem <br/> ipsum
5658
// expected e.g. lorem <br/> ipsum
5759
stringNode += `<${type}/>`;
58-
} else if (
59-
(!childChildren && (!shouldKeepChild || childPropsCount)) ||
60-
props.i18nIsDynamicList
61-
) {
60+
return;
61+
}
62+
if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) {
6263
// actual e.g. lorem <hr className="test" /> ipsum
6364
// expected e.g. lorem <0></0> ipsum
6465
// or
6566
// we got a dynamic list like
6667
// e.g. <ul i18nIsDynamicList>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>
6768
// expected e.g. "<0></0>", not e.g. "<0><0>a</0><1>b</1></0>"
6869
stringNode += `<${childIndex}></${childIndex}>`;
69-
} else if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
70+
return;
71+
}
72+
if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
7073
// actual e.g. dolor <strong>bold</strong> amet
7174
// expected e.g. dolor <strong>bold</strong> amet
7275
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}>`;
76+
return;
7777
}
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)) {
78+
// regular case mapping the inner children
79+
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
80+
stringNode += `<${childIndex}>${content}</${childIndex}>`;
81+
return;
82+
}
83+
if (child === null) {
84+
warn(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, { i18nKey });
85+
return;
86+
}
87+
if (isObject(child)) {
8188
// e.g. lorem {{ value, format }} ipsum
8289
const { format, ...clone } = child;
8390
const keys = Object.keys(clone);
8491

8592
if (keys.length === 1) {
8693
const value = format ? `${keys[0]}, ${format}` : keys[0];
8794
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-
);
95+
return;
9696
}
97-
} else {
9897
warn(
9998
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}}.`,
101-
child,
102-
i18nKey,
99+
'TRANS_INVALID_OBJ',
100+
`Invalid child - Object should only have keys {{ value, format }} (format is optional).`,
101+
{ i18nKey, child },
103102
);
103+
return;
104104
}
105+
warn(
106+
i18n,
107+
'TRANS_INVALID_VAR',
108+
`Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`,
109+
{ i18nKey, child },
110+
);
105111
});
106112

107113
return stringNode;
@@ -336,7 +342,7 @@ const generateObjectComponents = (components, translation) => {
336342
return componentMap;
337343
};
338344

339-
const generateComponents = (components, translation, i18n) => {
345+
const generateComponents = (components, translation, i18n, i18nKey) => {
340346
if (!components) return null;
341347

342348
// components could be either an array or an object
@@ -351,7 +357,12 @@ const generateComponents = (components, translation, i18n) => {
351357

352358
// if components is not an array or an object, warn the user
353359
// and return null
354-
warnOnce(i18n, '<Trans /> component prop expects an object or an array');
360+
warnOnce(
361+
i18n,
362+
'TRANS_INVALID_COMPONENTS',
363+
`<Trans /> "components" prop expects an object or array`,
364+
{ i18nKey },
365+
);
355366
return null;
356367
};
357368

@@ -374,7 +385,12 @@ export function Trans({
374385
const i18n = i18nFromProps || getI18n();
375386

376387
if (!i18n) {
377-
warnOnce(i18n, 'You will need to pass in an i18next instance by using i18nextReactModule');
388+
warnOnce(
389+
i18n,
390+
'NO_I18NEXT_INSTANCE',
391+
`Trans: You need to pass in an i18next instance using i18nextReactModule`,
392+
{ i18nKey },
393+
);
378394
return children;
379395
}
380396

@@ -417,7 +433,7 @@ export function Trans({
417433
};
418434
const translation = key ? t(key, combinedTOpts) : defaultValue;
419435

420-
const generatedComponents = generateComponents(components, translation, i18n);
436+
const generatedComponents = generateComponents(components, translation, i18n, i18nKey);
421437

422438
const content = renderNodes(
423439
generatedComponents || children,

src/useTranslation.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export const useTranslation = (ns, props = {}) => {
3535
const i18n = i18nFromProps || i18nFromContext || getI18n();
3636
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
3737
if (!i18n) {
38-
warnOnce(i18n, 'You will need to pass in an i18next instance by using initReactI18next');
38+
warnOnce(
39+
i18n,
40+
'NO_I18NEXT_INSTANCE',
41+
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
42+
);
3943
const notReadyT = (k, optsOrDefaultValue) => {
4044
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
4145
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
@@ -52,7 +56,8 @@ export const useTranslation = (ns, props = {}) => {
5256
if (i18n.options.react?.wait)
5357
warnOnce(
5458
i18n,
55-
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
59+
'DEPRECATED_OPTION',
60+
'useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
5661
);
5762

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

src/utils.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
export const warn = (i18n, ...args) => {
1+
/** @type {(i18n:any,code:import('../TransWithoutContext').ErrorCode,msg?:string, rest?:{[key:string]: any})=>void} */
2+
export const warn = (i18n, code, msg, rest) => {
3+
const args = [msg, { code, ...(rest || {}) }];
24
if (i18n?.services?.logger?.forward) {
3-
i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
4-
} else if (i18n?.services?.logger?.warn) {
5-
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
5+
return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
6+
}
7+
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
8+
if (i18n?.services?.logger?.warn) {
69
i18n.services.logger.warn(...args);
710
} else if (console?.warn) {
8-
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
911
console.warn(...args);
1012
}
1113
};
12-
1314
const alreadyWarned = {};
14-
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);
15+
/** @type {typeof warn} */
16+
export const warnOnce = (i18n, code, msg, rest) => {
17+
if (isString(msg) && alreadyWarned[msg]) return;
18+
if (isString(msg)) alreadyWarned[msg] = new Date();
19+
warn(i18n, code, msg, rest);
1820
};
1921

2022
// not needed right now
@@ -60,7 +62,9 @@ export const loadLanguages = (i18n, lng, ns, cb) => {
6062

6163
export const hasLoadedNamespace = (ns, i18n, options = {}) => {
6264
if (!i18n.languages || !i18n.languages.length) {
63-
warnOnce(i18n, 'i18n.languages were undefined or empty', i18n.languages);
65+
warnOnce(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
66+
languages: i18n.languages,
67+
});
6468
return true;
6569
}
6670

0 commit comments

Comments
 (0)