Skip to content

Commit

Permalink
feat: format warning with code and data to allow conditional logging (#…
Browse files Browse the repository at this point in the history
…1826)

Co-authored-by: rawpixel-vincent <[email protected]>
  • Loading branch information
delta-9 and rawpixel-vincent authored Dec 30, 2024
1 parent ff509ba commit 3cd025f
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 44 deletions.
36 changes: 36 additions & 0 deletions TransWithoutContext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,39 @@ export function Trans<
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
>(props: TransProps<Key, Ns, KPrefix, TContext, TOpt, E>): React.ReactElement;

export type ErrorCode =
| 'NO_I18NEXT_INSTANCE'
| 'NO_LANGUAGES'
| 'DEPRECATED_OPTION'
| 'TRANS_NULL_VALUE'
| 'TRANS_INVALID_OBJ'
| 'TRANS_INVALID_VAR'
| 'TRANS_INVALID_COMPONENTS';

export type ErrorMeta = {
code: ErrorCode;
i18nKey?: string;
[x: string]: any;
};

/**
* Use to type the logger arguments
* @example
* ```
* import type { ErrorArgs } from 'react-i18next';
*
* const logger = {
* // ....
* warn: function (...args: ErrorArgs) {
* if (args[1]?.code === 'TRANS_INVALID_OBJ') {
* const [msg, { i18nKey, ...rest }] = args;
* return log(i18nKey, msg, rest);
* }
* log(...args);
* }
* }
* i18n.use(logger).use(i18nReactPlugin).init({...});
* ```
*/
export type ErrorArgs = readonly [string, ErrorMeta | undefined, ...any[]];
4 changes: 2 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import type {
KeyPrefix,
} from 'i18next';
import * as React from 'react';
import { Trans, TransProps } from './TransWithoutContext.js';
import { Trans, TransProps, ErrorCode, ErrorArgs } from './TransWithoutContext.js';
export { initReactI18next } from './initReactI18next.js';

export const TransWithoutContext: typeof Trans;
export { Trans, TransProps };
export { Trans, TransProps, ErrorArgs, ErrorCode };

export function setDefaults(options: ReactOptions): void;
export function getDefaults(): ReactOptions;
Expand Down
74 changes: 45 additions & 29 deletions src/TransWithoutContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
// actual e.g. lorem
// expected e.g. lorem
stringNode += `${child}`;
} else if (isValidElement(child)) {
return;
}
if (isValidElement(child)) {
const { props, type } = child;
const childPropsCount = Object.keys(props).length;
const shouldKeepChild = keepArray.indexOf(type) > -1;
Expand All @@ -55,53 +57,57 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
// actual e.g. lorem <br/> ipsum
// expected e.g. lorem <br/> ipsum
stringNode += `<${type}/>`;
} else if (
(!childChildren && (!shouldKeepChild || childPropsCount)) ||
props.i18nIsDynamicList
) {
return;
}
if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) {
// actual e.g. lorem <hr className="test" /> ipsum
// expected e.g. lorem <0></0> ipsum
// or
// we got a dynamic list like
// e.g. <ul i18nIsDynamicList>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>
// expected e.g. "<0></0>", not e.g. "<0><0>a</0><1>b</1></0>"
stringNode += `<${childIndex}></${childIndex}>`;
} else if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
return;
}
if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) {
// actual e.g. dolor <strong>bold</strong> amet
// expected e.g. dolor <strong>bold</strong> amet
stringNode += `<${type}>${childChildren}</${type}>`;
} else {
// regular case mapping the inner children
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
return;
}
} else if (child === null) {
warn(i18n, `Trans: the passed in value is invalid - seems you passed in a null child.`);
} else if (isObject(child)) {
// regular case mapping the inner children
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
return;
}
if (child === null) {
warn(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, { i18nKey });
return;
}
if (isObject(child)) {
// e.g. lorem {{ value, format }} ipsum
const { format, ...clone } = child;
const keys = Object.keys(clone);

if (keys.length === 1) {
const value = format ? `${keys[0]}, ${format}` : keys[0];
stringNode += `{{${value}}}`;
} else {
// not a valid interpolation object (can only contain one value plus format)
warn(
i18n,
`react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
child,
i18nKey,
);
return;
}
} else {
warn(
i18n,
`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}}.`,
child,
i18nKey,
'TRANS_INVALID_OBJ',
`Invalid child - Object should only have keys {{ value, format }} (format is optional).`,
{ i18nKey, child },
);
return;
}
warn(
i18n,
'TRANS_INVALID_VAR',
`Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`,
{ i18nKey, child },
);
});

return stringNode;
Expand Down Expand Up @@ -336,7 +342,7 @@ const generateObjectComponents = (components, translation) => {
return componentMap;
};

const generateComponents = (components, translation, i18n) => {
const generateComponents = (components, translation, i18n, i18nKey) => {
if (!components) return null;

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

// if components is not an array or an object, warn the user
// and return null
warnOnce(i18n, '<Trans /> component prop expects an object or an array');
warnOnce(
i18n,
'TRANS_INVALID_COMPONENTS',
`<Trans /> "components" prop expects an object or array`,
{ i18nKey },
);
return null;
};

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

if (!i18n) {
warnOnce(i18n, 'You will need to pass in an i18next instance by using i18nextReactModule');
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
`Trans: You need to pass in an i18next instance using i18nextReactModule`,
{ i18nKey },
);
return children;
}

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

const generatedComponents = generateComponents(components, translation, i18n);
const generatedComponents = generateComponents(components, translation, i18n, i18nKey);

const content = renderNodes(
generatedComponents || children,
Expand Down
9 changes: 7 additions & 2 deletions src/useTranslation.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export const useTranslation = (ns, props = {}) => {
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
if (!i18n) {
warnOnce(i18n, 'You will need to pass in an i18next instance by using initReactI18next');
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
);
const notReadyT = (k, optsOrDefaultValue) => {
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
Expand All @@ -52,7 +56,8 @@ export const useTranslation = (ns, props = {}) => {
if (i18n.options.react?.wait)
warnOnce(
i18n,
'It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
'DEPRECATED_OPTION',
'useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
);

const i18nOptions = { ...getDefaults(), ...i18n.options.react, ...props };
Expand Down
26 changes: 15 additions & 11 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
export const warn = (i18n, ...args) => {
/** @type {(i18n:any,code:import('../TransWithoutContext').ErrorCode,msg?:string, rest?:{[key:string]: any})=>void} */
export const warn = (i18n, code, msg, rest) => {
const args = [msg, { code, ...(rest || {}) }];
if (i18n?.services?.logger?.forward) {
i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
} else if (i18n?.services?.logger?.warn) {
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
}
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
if (i18n?.services?.logger?.warn) {
i18n.services.logger.warn(...args);
} else if (console?.warn) {
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
console.warn(...args);
}
};

const alreadyWarned = {};
export const warnOnce = (i18n, ...args) => {
if (isString(args[0]) && alreadyWarned[args[0]]) return;
if (isString(args[0])) alreadyWarned[args[0]] = new Date();
warn(i18n, ...args);
/** @type {typeof warn} */
export const warnOnce = (i18n, code, msg, rest) => {
if (isString(msg) && alreadyWarned[msg]) return;
if (isString(msg)) alreadyWarned[msg] = new Date();
warn(i18n, code, msg, rest);
};

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

export const hasLoadedNamespace = (ns, i18n, options = {}) => {
if (!i18n.languages || !i18n.languages.length) {
warnOnce(i18n, 'i18n.languages were undefined or empty', i18n.languages);
warnOnce(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
languages: i18n.languages,
});
return true;
}

Expand Down

0 comments on commit 3cd025f

Please sign in to comment.