UNPKG

react-i18next

Version:

Internationalization for react done right. Using the i18next i18n ecosystem.

309 lines 11.9 kB
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react'; import HTML from 'html-parse-stringify'; import { isObject, isString, warn, warnOnce } from './utils.js'; import { getDefaults } from './defaults.js'; import { getI18n } from './i18nInstance.js'; const hasChildren = (node, checkLength) => { if (!node) return false; const base = node.props?.children ?? node.children; if (checkLength) return base.length > 0; return !!base; }; const getChildren = node => { if (!node) return []; const children = node.props?.children ?? node.children; return node.props?.i18nIsDynamicList ? getAsArray(children) : children; }; const hasValidReactChildren = children => Array.isArray(children) && children.every(isValidElement); const getAsArray = data => Array.isArray(data) ? data : [data]; const mergeProps = (source, target) => { const newTarget = { ...target }; newTarget.props = Object.assign(source.props, target.props); return newTarget; }; export const nodesToString = (children, i18nOptions, i18n, i18nKey) => { if (!children) return ''; let stringNode = ''; const childrenArray = getAsArray(children); const keepArray = i18nOptions?.transSupportBasicHtmlNodes ? i18nOptions.transKeepBasicHtmlNodesFor ?? [] : []; childrenArray.forEach((child, childIndex) => { if (isString(child)) { stringNode += `${child}`; return; } if (isValidElement(child)) { const { props, type } = child; const childPropsCount = Object.keys(props).length; const shouldKeepChild = keepArray.indexOf(type) > -1; const childChildren = props.children; if (!childChildren && shouldKeepChild && !childPropsCount) { stringNode += `<${type}/>`; return; } if (!childChildren && (!shouldKeepChild || childPropsCount) || props.i18nIsDynamicList) { stringNode += `<${childIndex}></${childIndex}>`; return; } if (shouldKeepChild && childPropsCount === 1 && isString(childChildren)) { stringNode += `<${type}>${childChildren}</${type}>`; return; } 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)) { const { format, ...clone } = child; const keys = Object.keys(clone); if (keys.length === 1) { const value = format ? `${keys[0]}, ${format}` : keys[0]; stringNode += `{{${value}}}`; return; } warn(i18n, '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; }; const renderNodes = (children, targetString, i18n, i18nOptions, combinedTOpts, shouldUnescape) => { if (targetString === '') return []; const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || []; const emptyChildrenButNeedsHandling = targetString && new RegExp(keepArray.map(keep => `<${keep}`).join('|')).test(targetString); if (!children && !emptyChildrenButNeedsHandling && !shouldUnescape) return [targetString]; const data = {}; const getData = childs => { const childrenArray = getAsArray(childs); childrenArray.forEach(child => { if (isString(child)) return; if (hasChildren(child)) getData(getChildren(child));else if (isObject(child) && !isValidElement(child)) Object.assign(data, child); }); }; getData(children); const ast = HTML.parse(`<0>${targetString}</0>`); const opts = { ...data, ...combinedTOpts }; const renderInner = (child, node, rootReactNode) => { const childs = getChildren(child); const mappedChildren = mapAST(childs, node.children, rootReactNode); return hasValidReactChildren(childs) && mappedChildren.length === 0 || child.props?.i18nIsDynamicList ? childs : mappedChildren; }; const pushTranslatedJSX = (child, inner, mem, i, isVoid) => { if (child.dummy) { child.children = inner; mem.push(cloneElement(child, { key: i }, isVoid ? undefined : inner)); } else { mem.push(...Children.map([child], c => { const props = { ...c.props }; delete props.i18nIsDynamicList; return createElement(c.type, { ...props, key: i, ref: c.ref }, isVoid ? null : inner); })); } }; const mapAST = (reactNode, astNode, rootReactNode) => { const reactNodes = getAsArray(reactNode); const astNodes = getAsArray(astNode); return astNodes.reduce((mem, node, i) => { const translationContent = node.children?.[0]?.content && i18n.services.interpolator.interpolate(node.children[0].content, opts, i18n.language); if (node.type === 'tag') { let tmp = reactNodes[parseInt(node.name, 10)]; if (rootReactNode.length === 1 && !tmp) tmp = rootReactNode[0][node.name]; if (!tmp) tmp = {}; const child = Object.keys(node.attrs).length !== 0 ? mergeProps({ props: node.attrs }, tmp) : tmp; const isElement = isValidElement(child); const isValidTranslationWithChildren = isElement && hasChildren(node, true) && !node.voidElement; const isEmptyTransWithHTML = emptyChildrenButNeedsHandling && isObject(child) && child.dummy && !isElement; const isKnownComponent = isObject(children) && Object.hasOwnProperty.call(children, node.name); if (isString(child)) { const value = i18n.services.interpolator.interpolate(child, opts, i18n.language); mem.push(value); } else if (hasChildren(child) || isValidTranslationWithChildren) { const inner = renderInner(child, node, rootReactNode); pushTranslatedJSX(child, inner, mem, i); } else if (isEmptyTransWithHTML) { const inner = mapAST(reactNodes, node.children, rootReactNode); pushTranslatedJSX(child, inner, mem, i); } else if (Number.isNaN(parseFloat(node.name))) { if (isKnownComponent) { const inner = renderInner(child, node, rootReactNode); pushTranslatedJSX(child, inner, mem, i, node.voidElement); } else if (i18nOptions.transSupportBasicHtmlNodes && keepArray.indexOf(node.name) > -1) { if (node.voidElement) { mem.push(createElement(node.name, { key: `${node.name}-${i}` })); } else { const inner = mapAST(reactNodes, node.children, rootReactNode); mem.push(createElement(node.name, { key: `${node.name}-${i}` }, inner)); } } else if (node.voidElement) { mem.push(`<${node.name} />`); } else { const inner = mapAST(reactNodes, node.children, rootReactNode); mem.push(`<${node.name}>${inner}</${node.name}>`); } } else if (isObject(child) && !isElement) { const content = node.children[0] ? translationContent : null; if (content) mem.push(content); } else { pushTranslatedJSX(child, translationContent, mem, i, node.children.length !== 1 || !translationContent); } } else if (node.type === 'text') { const wrapTextNodes = i18nOptions.transWrapTextNodes; const content = shouldUnescape ? i18nOptions.unescape(i18n.services.interpolator.interpolate(node.content, opts, i18n.language)) : i18n.services.interpolator.interpolate(node.content, opts, i18n.language); if (wrapTextNodes) { mem.push(createElement(wrapTextNodes, { key: `${node.name}-${i}` }, content)); } else { mem.push(content); } } return mem; }, []); }; const result = mapAST([{ dummy: true, children: children || [] }], ast, getAsArray(children || [])); return getChildren(result[0]); }; const fixComponentProps = (component, index, translation) => { const componentKey = component.key || index; const comp = cloneElement(component, { key: componentKey }); if (!comp.props || !comp.props.children || translation.indexOf(`${index}/>`) < 0 && translation.indexOf(`${index} />`) < 0) { return comp; } function Componentized() { return createElement(Fragment, null, comp); } return createElement(Componentized, { key: componentKey }); }; const generateArrayComponents = (components, translation) => components.map((c, index) => fixComponentProps(c, index, translation)); const generateObjectComponents = (components, translation) => { const componentMap = {}; Object.keys(components).forEach(c => { Object.assign(componentMap, { [c]: fixComponentProps(components[c], c, translation) }); }); return componentMap; }; const generateComponents = (components, translation, i18n, i18nKey) => { if (!components) return null; if (Array.isArray(components)) { return generateArrayComponents(components, translation); } if (isObject(components)) { return generateObjectComponents(components, translation); } warnOnce(i18n, 'TRANS_INVALID_COMPONENTS', `<Trans /> "components" prop expects an object or array`, { i18nKey }); return null; }; export function Trans({ children, count, parent, i18nKey, context, tOptions = {}, values, defaults, components, ns, i18n: i18nFromProps, t: tFromProps, shouldUnescape, ...additionalProps }) { const i18n = i18nFromProps || getI18n(); if (!i18n) { warnOnce(i18n, 'NO_I18NEXT_INSTANCE', `Trans: You need to pass in an i18next instance using i18nextReactModule`, { i18nKey }); return children; } const t = tFromProps || i18n.t.bind(i18n) || (k => k); const reactI18nextOptions = { ...getDefaults(), ...i18n.options?.react }; let namespaces = ns || t.ns || i18n.options?.defaultNS; namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation']; const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey); const defaultValue = defaults || nodeAsString || reactI18nextOptions.transEmptyNodeValue || i18nKey; const { hashTransKey } = reactI18nextOptions; const key = i18nKey || (hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue); if (i18n.options?.interpolation?.defaultVariables) { values = values && Object.keys(values).length > 0 ? { ...values, ...i18n.options.interpolation.defaultVariables } : { ...i18n.options.interpolation.defaultVariables }; } const interpolationOverride = values || count !== undefined && !i18n.options?.interpolation?.alwaysFormat || !children ? tOptions.interpolation : { interpolation: { ...tOptions.interpolation, prefix: '#$?', suffix: '?$#' } }; const combinedTOpts = { ...tOptions, context: context || tOptions.context, count, ...values, ...interpolationOverride, defaultValue, ns: namespaces }; const translation = key ? t(key, combinedTOpts) : defaultValue; const generatedComponents = generateComponents(components, translation, i18n, i18nKey); const content = renderNodes(generatedComponents || children, translation, i18n, reactI18nextOptions, combinedTOpts, shouldUnescape); const useAsParent = parent ?? reactI18nextOptions.defaultTransParent; return useAsParent ? createElement(useAsParent, additionalProps, content) : content; }