Skip to content

Commit

Permalink
feat: add option to override warn logger
Browse files Browse the repository at this point in the history
  • Loading branch information
rawpixel-vincent committed Dec 29, 2024
1 parent ff509ba commit e478b8d
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 39 deletions.
114 changes: 81 additions & 33 deletions src/TransWithoutContext.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react';
import HTML from 'html-parse-stringify';
import { isObject, isString, warn, warnOnce } from './utils.js';
import { ERR_CODES, isObject, isString, warnOnce } from './utils.js';
import { getDefaults } from './defaults.js';
import { getI18n } from './i18nInstance.js';

Expand Down Expand Up @@ -29,7 +29,7 @@ const mergeProps = (source, target) => {
return newTarget;
};

export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
export const nodesToString = (children, i18nOptions, i18n, i18nKey, _parentWarnings) => {
if (!children) return '';
let stringNode = '';

Expand All @@ -39,13 +39,17 @@ export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
? (i18nOptions.transKeepBasicHtmlNodesFor ?? [])
: [];

const warnings = Array.isArray(_parentWarnings) ? _parentWarnings : [];
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
childrenArray.forEach((child, childIndex) => {
if (isString(child)) {
// 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,55 +59,79 @@ 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, warnings);
stringNode += `<${childIndex}>${content}</${childIndex}>`;

return;
}

if (child === null) {
warnings.push({
code: ERR_CODES.TRANS_NULL_VALUE,
message: `The passed in value is invalid - seems you passed in a null child.`,
});
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}}.`,

// not a valid interpolation object (can only contain one value plus format)
warnings.push({
code: ERR_CODES.TRANS_INVALID_OBJ,
message: `The passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
child,
i18nKey,
);
});

return;
}

// e.g. lorem {number} ipsum
warnings.push({
code: ERR_CODES.TRANS_INVALID_VAR,
message: `Passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.`,
child,
});
});

// only warn once for a key and group warnings if there are more than 1.
if (warnings.length && !_parentWarnings) {
warnOnce(i18n, `Trans:key:${i18nKey}: Interpolation errors - check the passed childrens.`, {
i18nKey,
warnings,
});
}

return stringNode;
};

Expand Down Expand Up @@ -336,7 +364,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 +379,15 @@ 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:key:${i18nKey}: "components" prop expects an object or an array`, {
i18nKey,
warnings: [
{
code: ERR_CODES.TRANS_INVALID_COMPONENTS,
message: `<Trans /> "components" prop expects an object or an array, received ${typeof components}`,
},
],
});
return null;
};

Expand All @@ -374,7 +410,19 @@ 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,
'Trans: You will need to pass in an i18next instance by using i18nextReactModule',
{
i18nKey,
warnings: [
{
code: ERR_CODES.NO_I18NEXT_INSTANCE,
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
},
],
},
);
return children;
}

Expand Down Expand Up @@ -417,7 +465,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
4 changes: 4 additions & 0 deletions src/initReactI18next.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { setDefaults } from './defaults.js';
import { setI18n } from './i18nInstance.js';
import { setWarnFn } from './utils.js';

export const initReactI18next = {
type: '3rdParty',

init(instance) {
setDefaults(instance.options.react);
if (typeof instance.options.react.warn === 'function') {
setWarnFn(instance.options.react.warn);
}
setI18n(instance);
},
};
25 changes: 23 additions & 2 deletions src/useTranslation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
hasLoadedNamespace,
isString,
isObject,
ERR_CODES,
} from './utils.js';

const usePrevious = (value, ignore) => {
Expand Down Expand Up @@ -35,7 +36,18 @@ 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,
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
{
warnings: [
{
code: ERR_CODES.NO_I18NEXT_INSTANCE,
message: 'You will need to pass in an i18next instance by using i18nextReactModule',
},
],
},
);
const notReadyT = (k, optsOrDefaultValue) => {
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
Expand All @@ -52,7 +64,16 @@ 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.',
'useTranslation: It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.',
{
warnings: [
{
code: ERR_CODES.DEPRECATED_WAIT_OPTION,
message:
'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
52 changes: 48 additions & 4 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,56 @@ export const warn = (i18n, ...args) => {
console.warn(...args);
}
};
export const ERR_CODES = {
NO_I18NEXT_INSTANCE: 'NO_I18NEXT_INSTANCE',
DEPRECATED_WAIT_OPTION: 'DEPRECATED_WAIT_OPTION',
TRANS_NULL_VALUE: 'TRANS_NULL_VALUE',
TRANS_INVALID_OBJ: 'TRANS_INVALID_OBJ',
TRANS_INVALID_VAR: 'TRANS_INVALID_VAR',
TRANS_INVALID_COMPONENTS: 'TRANS_INVALID_COMPONENTS',
};

const alreadyWarned = {
data: {},
history: [],
maxSize: 150,
};
const flagAsWarned = (hash) => {
if (alreadyWarned.data[hash]) {
return;
}
alreadyWarned.data[hash] = true;
alreadyWarned.history.push(hash);
if (alreadyWarned.history.length > alreadyWarned.maxSize) {
const removeHashes = alreadyWarned.history.splice(0, 25);
for (let i = 0; i < removeHashes.length; i += 1) {
delete alreadyWarned.data[removeHashes[i]];
}
}
};
const getHash = (...args) =>
/* eslint-disable-next-line no-bitwise */ (
args
.filter((a) => isString(a))
.join('::::')
.split('')
.reduce((acc, b) => {
/* eslint-disable-next-line no-bitwise, no-param-reassign */
acc = (acc << 5) - acc + b.charCodeAt(0);
return acc;
}, 0) >>> 0
)
.toString(36)
.padStart(6, '0');

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);
const hash = (isString(args[0]) && getHash(...args)) || false;
if (!isString(args[0]) || !alreadyWarned.data[hash]) {
if (hash) {
flagAsWarned(hash);
}
warn(i18n, ...args);
}
};

// not needed right now
Expand Down

0 comments on commit e478b8d

Please sign in to comment.