diff --git a/fluent-react/src/index.js b/fluent-react/src/index.js index 874aeaee7..253865ebe 100644 --- a/fluent-react/src/index.js +++ b/fluent-react/src/index.js @@ -20,5 +20,6 @@ export { default as LocalizationProvider } from "./provider"; export { default as withLocalization } from "./with_localization"; export { default as Localized } from "./localized"; +export { default as Overlay } from "./overlay"; export { default as ReactLocalization, isReactLocalization } from "./localization"; diff --git a/fluent-react/src/localized.js b/fluent-react/src/localized.js index b97666539..578f2cb29 100644 --- a/fluent-react/src/localized.js +++ b/fluent-react/src/localized.js @@ -1,13 +1,7 @@ -import { isValidElement, cloneElement, Component, Children } from "react"; +import { isValidElement, Component, Children, createElement } from "react"; import PropTypes from "prop-types"; - import { isReactLocalization } from "./localization"; -import { parseMarkup } from "./markup"; -import VOID_ELEMENTS from "../vendor/voidElementTags"; - -// Match the opening angle bracket (<) in HTML tags, and HTML entities like -// &, &, &. -const reMarkup = /<|&#?\w+;/; +import Overlay from "./overlay"; /* * Prepare props passed to `Localized` for formatting. @@ -116,58 +110,13 @@ export default class Localized extends Component { } } - // If the wrapped component is a known void element, explicitly dismiss the - // message value and do not pass it to cloneElement in order to avoid the - // "void element tags must neither have `children` nor use - // `dangerouslySetInnerHTML`" error. - if (elem.type in VOID_ELEMENTS) { - return cloneElement(elem, localizedProps); - } - - // If the message has a null value, we're only interested in its attributes. - // Do not pass the null value to cloneElement as it would nuke all children - // of the wrapped component. - if (messageValue === null) { - return cloneElement(elem, localizedProps); - } - - // If the message value doesn't contain any markup nor any HTML entities, - // insert it as the only child of the wrapped component. - if (!reMarkup.test(messageValue)) { - return cloneElement(elem, localizedProps, messageValue); - } - - // If the message contains markup, parse it and try to match the children - // found in the translation with the props passed to this Localized. - const translationNodes = Array.from(parseMarkup(messageValue).childNodes); - const translatedChildren = translationNodes.map(childNode => { - if (childNode.nodeType === childNode.TEXT_NODE) { - return childNode.textContent; - } - - // If the child is not expected just take its textContent. - if (!elems.hasOwnProperty(childNode.localName)) { - return childNode.textContent; - } - - const sourceChild = elems[childNode.localName]; - - // If the element passed as a prop to 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 (sourceChild.type in VOID_ELEMENTS) { - 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, null, childNode.textContent); - }); - - return cloneElement(elem, localizedProps, ...translatedChildren); + return createElement( + Overlay, { + value: messageValue, + attrs: localizedProps, + args: elems + }, elem + ); } } diff --git a/fluent-react/src/markup.js b/fluent-react/src/markup.js deleted file mode 100644 index ef0a9fc61..000000000 --- a/fluent-react/src/markup.js +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-env browser */ - -const TEMPLATE = document.createElement("template"); - -export function parseMarkup(str) { - TEMPLATE.innerHTML = str; - return TEMPLATE.content; -} diff --git a/fluent-react/src/overlay.js b/fluent-react/src/overlay.js new file mode 100644 index 000000000..a89174358 --- /dev/null +++ b/fluent-react/src/overlay.js @@ -0,0 +1,72 @@ +/* eslint-env browser */ + +import { cloneElement, Children } from "react"; +import VOID_ELEMENTS from "../vendor/voidElementTags"; + +// Match the opening angle bracket (<) in HTML tags, and HTML entities like +// &, &, &. +const reMarkup = /<|&#?\w+;/; + +const TEMPLATE = document.createElement("template"); + +export function parseMarkup(str) { + TEMPLATE.innerHTML = str; + return TEMPLATE.content; +} + +export default function Overlay({children, value, attrs, args}) { + const elem = Children.only(children); + + // If the wrapped component is a known void element, explicitly dismiss the + // message value and do not pass it to cloneElement in order to avoid the + // "void element tags must neither have `children` nor use + // `dangerouslySetInnerHTML`" error. + if (elem.type in VOID_ELEMENTS) { + return cloneElement(elem, attrs); + } + + // If the message has a null value, we're only interested in its attributes. + // Do not pass the null value to cloneElement as it would nuke all children + // of the wrapped component. + if (value === null) { + return cloneElement(elem, attrs); + } + + // If the message value doesn't contain any markup nor any HTML entities, + // insert it as the only child of the wrapped component. + if (!reMarkup.test(value)) { + return cloneElement(elem, attrs, value); + } + + // If the message contains markup, parse it and try to match the children + // found in the translation with the props passed to this Localized. + const translationNodes = Array.from(parseMarkup(value).childNodes); + const translatedChildren = translationNodes.map(childNode => { + if (childNode.nodeType === childNode.TEXT_NODE) { + return childNode.textContent; + } + + // If the child is not expected just take its textContent. + if (!args.hasOwnProperty(childNode.localName)) { + return childNode.textContent; + } + + const sourceChild = args[childNode.localName]; + + // If the element passed as a prop to 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 (sourceChild.type in VOID_ELEMENTS) { + 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, null, childNode.textContent); + }); + + return cloneElement(elem, attrs, ...translatedChildren); +} diff --git a/fluent-react/test/exports_test.js b/fluent-react/test/exports_test.js index 089ffa681..1eb784a9c 100644 --- a/fluent-react/test/exports_test.js +++ b/fluent-react/test/exports_test.js @@ -2,6 +2,7 @@ import assert from 'assert'; import * as FluentReact from '../src/index'; import LocalizationProvider from '../src/provider'; import Localized from '../src/localized'; +import Overlay from '../src/overlay'; import withLocalization from '../src/with_localization'; import ReactLocalization, { isReactLocalization } from '../src/localization'; @@ -14,6 +15,10 @@ suite('Exports', () => { assert.equal(FluentReact.Localized, Localized); }); + test('Overlay', () => { + assert.equal(FluentReact.Overlay, Overlay); + }); + test('withLocalization', () => { assert.equal(FluentReact.withLocalization, withLocalization); });