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 9b194b1 commit 9f9deb6
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 23 deletions.
59 changes: 44 additions & 15 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) => {
export const nodesToString = (children, i18nOptions, i18nKey, _parentWarnings) => {
if (!children) return '';
let stringNode = '';

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

const warnings = _parentWarnings || [];
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
childrenArray.forEach((child, childIndex) => {
if (isString(child)) {
Expand Down Expand Up @@ -72,11 +73,14 @@ export const nodesToString = (children, i18nOptions) => {
stringNode += `<${type}>${childChildren}</${type}>`;
} else {
// regular case mapping the inner children
const content = nodesToString(childChildren, i18nOptions);
const content = nodesToString(childChildren, i18nOptions, i18nKey, warnings);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
}
} else if (child === null) {
warn(`Trans: the passed in value is invalid - seems you passed in a null child.`);
warnings.push({
code: ERR_CODES.TRANS_NULL_VALUE,
message: `The passed in value is invalid - seems you passed in a null child.`,
});
} else if (isObject(child)) {
// e.g. lorem {{ value, format }} ipsum
const { format, ...clone } = child;
Expand All @@ -87,19 +91,28 @@ export const nodesToString = (children, i18nOptions) => {
stringNode += `{{${value}}}`;
} else {
// not a valid interpolation object (can only contain one value plus format)
warn(
`react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.`,
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,
);
});
}
} else {
warn(
`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}}.`,
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,
);
});
}
});

if (warnings.length && !_parentWarnings) {
warnOnce(`Trans:key:${i18nKey}: Interpolation errors - check the passed childrens.`, {
i18nKey,
warnings,
});
}

return stringNode;
};

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

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

// components could be either an array or an object
Expand All @@ -347,7 +360,15 @@ const generateComponents = (components, translation) => {

// if components is not an array or an object, warn the user
// and return null
warnOnce('<Trans /> component prop expects an object or an array');
warnOnce(`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 @@ -370,7 +391,15 @@ export function Trans({
const i18n = i18nFromProps || getI18n();

if (!i18n) {
warnOnce('You will need to pass in an i18next instance by using i18nextReactModule');
warnOnce('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 All @@ -382,7 +411,7 @@ export function Trans({
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];

const nodeAsString = nodesToString(children, reactI18nextOptions);
const nodeAsString = nodesToString(children, reactI18nextOptions, i18nKey);
const defaultValue =
defaults || nodeAsString || reactI18nextOptions.transEmptyNodeValue || i18nKey;
const { hashTransKey } = reactI18nextOptions;
Expand Down Expand Up @@ -413,7 +442,7 @@ export function Trans({
};
const translation = key ? t(key, combinedTOpts) : defaultValue;

const generatedComponents = generateComponents(components, translation);
const generatedComponents = generateComponents(components, translation, 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);
},
};
24 changes: 22 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,17 @@ export const useTranslation = (ns, props = {}) => {
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
if (!i18n) {
warnOnce('You will need to pass in an i18next instance by using initReactI18next');
warnOnce(
'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 @@ -51,7 +62,16 @@ export const useTranslation = (ns, props = {}) => {

if (i18n.options.react?.wait)
warnOnce(
'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
76 changes: 70 additions & 6 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
export const 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 defaultWarn = (...args) => {
if (console?.warn) {
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
console.warn(...args);
}
};

const alreadyWarned = {};
let warnFn = defaultWarn;
export const setWarnFn = (fn) => {
if (typeof fn === 'function' && fn !== warnFn) {
warnFn = fn;
} else {
defaultWarn('react-i18next:: setting a non a function as warn function');
}
};

export const warn = (...args) => {
if (isString(args[0])) {
args[0] = `react-i18next:: ${args[0]}`;
}
if (typeof warnFn === 'function') {
warnFn(...args);
} else {
defaultWarn(...args);
}
};

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');

export const warnOnce = (...args) => {
if (isString(args[0]) && alreadyWarned[args[0]]) return;
if (isString(args[0])) alreadyWarned[args[0]] = new Date();
warn(...args);
const hash = (isString(args[0]) && getHash(...args)) || false;
if (!isString(args[0]) || !alreadyWarned.data[hash]) {
if (hash) {
flagAsWarned(hash);
}
warn(...args);
}
};

// not needed right now
Expand Down

0 comments on commit 9f9deb6

Please sign in to comment.