diff --git a/fluent-react/src/localization.ts b/fluent-react/src/localization.ts index e9e1250d..34bc120e 100644 --- a/fluent-react/src/localization.ts +++ b/fluent-react/src/localization.ts @@ -6,6 +6,7 @@ import { createElement, isValidElement, cloneElement, + ReactNode, } from "react"; import { CachedSyncIterable } from "cached-iterable"; import { createParseMarkup, MarkupParser } from "./markup.js"; @@ -101,6 +102,7 @@ export class ReactLocalization { vars?: Record; elems?: Record; attrs?: Record; + nestedElems?: boolean; } = {} ): ReactElement { const bundle = this.getBundle(id); @@ -186,9 +188,8 @@ export class ReactLocalization { return cloneElement(sourceElement, localizedProps, messageValue); } - let elemsLower: Map; + const elemsLower: Map = new Map(); if (args.elems) { - elemsLower = new Map(); for (let [name, elem] of Object.entries(args.elems)) { // Ignore elems which are not valid React elements. if (!isValidElement(elem)) { @@ -201,39 +202,55 @@ export class ReactLocalization { // If the message contains markup, parse it and try to match the children // found in the translation with the args passed to this function. const translationNodes = this.parseMarkup(messageValue); - const translatedChildren = translationNodes.map( - ({ nodeName, textContent }) => { - if (nodeName === "#text") { - return textContent; - } + const translatedChildren = translateChildren( + translationNodes, + elemsLower, + args.nestedElems + ); - const childName = nodeName.toLowerCase(); - const sourceChild = elemsLower?.get(childName); + return cloneElement(sourceElement, localizedProps, ...translatedChildren); + } +} - // If the child is not expected just take its textContent. - if (!sourceChild) { - return textContent; - } +function translateChildren( + translationNodes: Node[], + elemsLower: Map, + recursive: boolean | undefined +): ReactNode[] { + return translationNodes.map(({ nodeName, textContent, childNodes }) => { + if (nodeName === "#text") { + return textContent; + } - // If the element passed in the elems prop is a known void element, - // explicitly dismiss any textContent which might have accidentally been - // defined in the translation to prevent the "void element tags must not - // have children" error. - if ( - typeof sourceChild.type === "string" && - sourceChild.type in voidElementTags - ) { - return sourceChild; - } + const childName = nodeName.toLowerCase(); + const sourceChild = elemsLower?.get(childName); - // TODO Protect contents of elements wrapped in - // https://github.com/projectfluent/fluent.js/issues/184 - // TODO Control localizable attributes on elements passed as props - // https://github.com/projectfluent/fluent.js/issues/185 - return cloneElement(sourceChild, undefined, textContent); - } - ); + let translatedChildren = recursive + ? translateChildren([...childNodes], elemsLower, true) + : [textContent]; - return cloneElement(sourceElement, localizedProps, ...translatedChildren); - } + // If the child is not expected just take its content. + if (!sourceChild) { + return recursive + ? createElement(Fragment, null, ...translatedChildren) + : textContent; + } + + // If the element passed in the elems prop is a known void element, + // explicitly dismiss any textContent which might have accidentally been + // defined in the translation to prevent the "void element tags must not + // have children" error. + if ( + typeof sourceChild.type === "string" && + sourceChild.type in voidElementTags + ) { + return sourceChild; + } + + // TODO Protect contents of elements wrapped in + // https://github.com/projectfluent/fluent.js/issues/184 + // TODO Control localizable attributes on elements passed as props + // https://github.com/projectfluent/fluent.js/issues/185 + return cloneElement(sourceChild, undefined, ...translatedChildren); + }); } diff --git a/fluent-react/src/localized.ts b/fluent-react/src/localized.ts index 813b212d..dcf86cf5 100644 --- a/fluent-react/src/localized.ts +++ b/fluent-react/src/localized.ts @@ -13,6 +13,7 @@ export interface LocalizedProps { children?: ReactNode | Array; vars?: Record; elems?: Record; + nestedElems?: boolean; } /** @@ -41,7 +42,7 @@ export interface LocalizedProps { * ``` */ export function Localized(props: LocalizedProps): ReactElement { - const { id, attrs, vars, elems, children } = props; + const { id, attrs, vars, elems, nestedElems, children } = props; const l10n = useContext(FluentContext); if (!l10n) { @@ -74,7 +75,7 @@ export function Localized(props: LocalizedProps): ReactElement { return React.createElement(React.Fragment, null, string); } - return l10n.getElement(source, id, { attrs, vars, elems }); + return l10n.getElement(source, id, { attrs, vars, elems, nestedElems }); } export default Localized;