diff --git a/TransWithoutContext.d.ts b/TransWithoutContext.d.ts index 0b028fc1..d8acd67f 100644 --- a/TransWithoutContext.d.ts +++ b/TransWithoutContext.d.ts @@ -35,3 +35,39 @@ export function Trans< TOpt extends TOptions & { context?: TContext } = { context: TContext }, E = React.HTMLProps, >(props: TransProps): 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[]]; diff --git a/index.d.ts b/index.d.ts index 5b8b778c..83b71376 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; diff --git a/src/TransWithoutContext.js b/src/TransWithoutContext.js index f47a99ef..03f6344f 100644 --- a/src/TransWithoutContext.js +++ b/src/TransWithoutContext.js @@ -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; @@ -55,10 +57,9 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => { // actual e.g. lorem
ipsum // expected e.g. lorem
ipsum stringNode += `<${type}/>`; - } else if ( - (!childChildren && (!shouldKeepChild || childPropsCount)) || - props.i18nIsDynamicList - ) { + return; + } + if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) { // actual e.g. lorem
ipsum // expected e.g. lorem <0> ipsum // or @@ -66,18 +67,24 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => { // e.g. // expected e.g. "<0>", not e.g. "<0><0>a<1>b" stringNode += `<${childIndex}>`; - } else if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) { + return; + } + if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) { // actual e.g. dolor bold amet // expected e.g. dolor bold amet stringNode += `<${type}>${childChildren}`; - } else { - // regular case mapping the inner children - const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey); - stringNode += `<${childIndex}>${content}`; + 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}`; + 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); @@ -85,23 +92,22 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => { 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; @@ -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 @@ -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, ' component prop expects an object or an array'); + warnOnce( + i18n, + 'TRANS_INVALID_COMPONENTS', + ` "components" prop expects an object or array`, + { i18nKey }, + ); return null; }; @@ -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; } @@ -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, diff --git a/src/useTranslation.js b/src/useTranslation.js index 8241e1c2..53c1935f 100644 --- a/src/useTranslation.js +++ b/src/useTranslation.js @@ -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)) @@ -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 }; diff --git a/src/utils.js b/src/utils.js index 0c96bd8f..01b95676 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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 @@ -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; }