UNPKG

react-i18next

Version:

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

237 lines (209 loc) 9.23 kB
import React, { useContext } from 'react'; import HTML from 'html-parse-stringify2'; import { getI18n, getHasUsedI18nextProvider, I18nContext } from './context'; import { warn, warnOnce } from './utils'; function hasChildren(node) { return node && (node.children || (node.props && node.props.children)); } function getChildren(node) { if (!node) return []; return node && node.children ? node.children : node.props && node.props.children; } function hasValidReactChildren(children) { if (Object.prototype.toString.call(children) !== '[object Array]') return false; return children.every(child => React.isValidElement(child)); } export function nodesToString(mem, children, index, i18nOptions) { if (!children) return ''; if (Object.prototype.toString.call(children) !== '[object Array]') children = [children]; const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || []; children.forEach((child, i) => { // const isElement = React.isValidElement(child); // const elementKey = `${index !== 0 ? index + '-' : ''}${i}:${typeof child.type === 'function' ? child.type.name : child.type || 'var'}`; const elementKey = `${i}`; if (typeof child === 'string') { mem = `${mem}${child}`; } else if (hasChildren(child)) { const elementTag = keepArray.indexOf(child.type) > -1 && Object.keys(child.props).length === 1 && typeof hasChildren(child) === 'string' ? child.type : elementKey; if (child.props && child.props.i18nIsDynamicList) { // we got a dynamic list like "<ul>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>"" // the result should be "<0></0>" and not "<0><0>a</0><1>b</1></0>" mem = `${mem}<${elementTag}></${elementTag}>`; } else { // regular case mapping the inner children mem = `${mem}<${elementTag}>${nodesToString( '', getChildren(child), i + 1, i18nOptions, )}</${elementTag}>`; } } else if (React.isValidElement(child)) { if (keepArray.indexOf(child.type) > -1 && Object.keys(child.props).length === 0) { mem = `${mem}<${child.type}/>`; } else { mem = `${mem}<${elementKey}></${elementKey}>`; } } else if (typeof child === 'object') { const clone = { ...child }; const format = clone.format; delete clone.format; const keys = Object.keys(clone); if (format && keys.length === 1) { mem = `${mem}{{${keys[0]}, ${format}}}`; } else if (keys.length === 1) { mem = `${mem}{{${keys[0]}}}`; } 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.`, 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}}.`, child, ); } }); return mem; } function renderNodes(children, targetString, i18n, i18nOptions) { if (targetString === '') return []; // check if contains tags we need to replace from html string to react nodes const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || []; const emptyChildrenButNeedsHandling = targetString && new RegExp(keepArray.join('|')).test(targetString); // no need to replace tags in the targetstring if (!children && !emptyChildrenButNeedsHandling) return [targetString]; // v2 -> interpolates upfront no need for "some <0>{{var}}</0>"" -> will be just "some {{var}}" in translation file const data = {}; function getData(childs) { if (Object.prototype.toString.call(childs) !== '[object Array]') childs = [childs]; childs.forEach(child => { if (typeof child === 'string') return; if (hasChildren(child)) getData(getChildren(child)); else if (typeof child === 'object' && !React.isValidElement(child)) Object.assign(data, child); }); } getData(children); targetString = i18n.services.interpolator.interpolate(targetString, data, i18n.language); // parse ast from string with additional wrapper tag // -> avoids issues in parser removing prepending text nodes const ast = HTML.parse(`<0>${targetString}</0>`); function mapAST(reactNodes, astNodes) { if (Object.prototype.toString.call(reactNodes) !== '[object Array]') reactNodes = [reactNodes]; if (Object.prototype.toString.call(astNodes) !== '[object Array]') astNodes = [astNodes]; return astNodes.reduce((mem, node, i) => { const translationContent = node.children && node.children[0] && node.children[0].content; if (node.type === 'tag') { const child = reactNodes[parseInt(node.name, 10)] || {}; const isElement = React.isValidElement(child); if (typeof child === 'string') { mem.push(child); } else if (hasChildren(child)) { const childs = getChildren(child); const mappedChildren = mapAST(childs, node.children); const inner = hasValidReactChildren(childs) && mappedChildren.length === 0 ? childs : mappedChildren; if (child.dummy) child.children = inner; // needed on preact! mem.push(React.cloneElement(child, { ...child.props, key: i }, inner)); } else if ( emptyChildrenButNeedsHandling && typeof child === 'object' && child.dummy && !isElement ) { // we have a empty Trans node (the dummy element) with a targetstring that contains html tags needing // conversion to react nodes // so we just need to map the inner stuff const inner = mapAST(reactNodes /* wrong but we need something */, node.children); mem.push(React.cloneElement(child, { ...child.props, key: i }, inner)); } else if (isNaN(node.name) && i18nOptions.transSupportBasicHtmlNodes) { if (node.voidElement) { mem.push(React.createElement(node.name, { key: `${node.name}-${i}` })); } else { const inner = mapAST(reactNodes /* wrong but we need something */, node.children); mem.push(React.createElement(node.name, { key: `${node.name}-${i}` }, inner)); } } else if (typeof child === 'object' && !isElement) { const content = node.children[0] ? translationContent : null; // v1 // as interpolation was done already we just have a regular content node // in the translation AST while having an object in reactNodes // -> push the content no need to interpolate again if (content) mem.push(content); } else if (node.children.length === 1 && translationContent) { // If component does not have children, but translation - has // with this in component could be components={[<span class='make-beautiful'/>]} and in translation - 'some text <0>some highlighted message</0>' mem.push(React.cloneElement(child, { ...child.props, key: i }, translationContent)); } else { mem.push(child); } } else if (node.type === 'text') { mem.push(node.content); } return mem; }, []); } // call mapAST with having react nodes nested into additional node like // we did for the string ast from translation // return the children of that extra node to get expected result const result = mapAST([{ dummy: true, children }], ast); return getChildren(result[0]); } export function Trans({ children, count, parent, i18nKey, tOptions, values, defaults, components, ns, i18n: i18nFromProps, t: tFromProps, ...additionalProps }) { const { i18n: i18nFromContext } = getHasUsedI18nextProvider() ? useContext(I18nContext) : {}; const i18n = i18nFromProps || i18nFromContext || getI18n(); if (!i18n) { warnOnce('You will need pass in an i18next instance by using i18nextReactModule'); return children; } const t = tFromProps || i18n.t.bind(i18n); const reactI18nextOptions = (i18n.options && i18n.options.react) || {}; const useAsParent = parent !== undefined ? parent : reactI18nextOptions.defaultTransParent; const defaultValue = defaults || nodesToString('', children, 0, reactI18nextOptions) || reactI18nextOptions.transEmptyNodeValue; const hashTransKey = reactI18nextOptions.hashTransKey; const key = i18nKey || (hashTransKey ? hashTransKey(defaultValue) : defaultValue); const interpolationOverride = values ? {} : { interpolation: { prefix: '#$?', suffix: '?$#' } }; const translation = key ? t(key, { ...tOptions, ...values, ...interpolationOverride, defaultValue, count, ns, }) : defaultValue; if (!useAsParent) return renderNodes(components || children, translation, i18n, reactI18nextOptions); return React.createElement( useAsParent, additionalProps, renderNodes(components || children, translation, i18n, reactI18nextOptions), ); }