From 6715562a33f7a1fdf1be4e797342a49d1cc0aa36 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 24 Jul 2019 18:34:24 +0200 Subject: [PATCH 01/14] initial version with React's new Context API --- fluent-react/package.json | 1 + fluent-react/src/context.js | 3 + fluent-react/src/index.js | 2 - fluent-react/src/localization.js | 92 ----------- fluent-react/src/localized.js | 228 ++++++++++++-------------- fluent-react/src/provider.js | 83 ++++++---- fluent-react/src/with_localization.js | 31 ---- 7 files changed, 153 insertions(+), 287 deletions(-) create mode 100644 fluent-react/src/context.js delete mode 100644 fluent-react/src/localization.js diff --git a/fluent-react/package.json b/fluent-react/package.json index 32e1be8a3..f01aba069 100644 --- a/fluent-react/package.json +++ b/fluent-react/package.json @@ -62,6 +62,7 @@ "jsdom": "^11.12.0", "react": "^16.2.0", "react-dom": "^16.2.0", + "react-test-renderer": "^16.8.6", "sinon": "^4.2.2" } } diff --git a/fluent-react/src/context.js b/fluent-react/src/context.js new file mode 100644 index 000000000..e6287e6f4 --- /dev/null +++ b/fluent-react/src/context.js @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export default createContext({getBundle: null, parseMarkup: null}); diff --git a/fluent-react/src/index.js b/fluent-react/src/index.js index 0c5ade489..a891e4f0c 100644 --- a/fluent-react/src/index.js +++ b/fluent-react/src/index.js @@ -20,5 +20,3 @@ export { default as LocalizationProvider } from "./provider"; export { default as withLocalization } from "./with_localization"; export { default as Localized } from "./localized"; -export { default as ReactLocalization, isReactLocalization } - from "./localization"; diff --git a/fluent-react/src/localization.js b/fluent-react/src/localization.js deleted file mode 100644 index bcb0ce748..000000000 --- a/fluent-react/src/localization.js +++ /dev/null @@ -1,92 +0,0 @@ -import { mapBundleSync } from "@fluent/sequence"; -import { CachedSyncIterable } from "cached-iterable"; - -/* - * `ReactLocalization` handles translation formatting and fallback. - * - * The current negotiated fallback chain of languages is stored in the - * `ReactLocalization` instance in form of an iterable of `FluentBundle` - * instances. This iterable is used to find the best existing translation for - * a given identifier. - * - * `Localized` components must subscribe to the changes of the - * `ReactLocalization`'s fallback chain. When the fallback chain changes (the - * `bundles` iterable is set anew), all subscribed compontent must relocalize. - * - * The `ReactLocalization` class instances are exposed to `Localized` elements - * via the `LocalizationProvider` component. - */ -export default class ReactLocalization { - constructor(bundles) { - this.bundles = CachedSyncIterable.from(bundles); - this.subs = new Set(); - } - - /* - * Subscribe a `Localized` component to changes of `bundles`. - */ - subscribe(comp) { - this.subs.add(comp); - } - - /* - * Unsubscribe a `Localized` component from `bundles` changes. - */ - unsubscribe(comp) { - this.subs.delete(comp); - } - - /* - * Set a new `bundles` iterable and trigger the retranslation. - */ - setBundles(bundles) { - this.bundles = CachedSyncIterable.from(bundles); - - // Update all subscribed Localized components. - this.subs.forEach(comp => comp.relocalize()); - } - - getBundle(id) { - return mapBundleSync(this.bundles, id); - } - - /* - * Find a translation by `id` and format it to a string using `args`. - */ - getString(id, args, fallback) { - const bundle = this.getBundle(id); - if (bundle) { - const msg = bundle.getMessage(id); - if (msg && msg.value) { - let errors = []; - let value = bundle.formatPattern(msg.value, args, errors); - for (let error of errors) { - this.reportError(error); - } - return value; - } - } - - return fallback || id; - } - - // XXX Control this via a prop passed to the LocalizationProvider. - // See https://github.com/projectfluent/fluent.js/issues/411. - reportError(error) { - /* global console */ - // eslint-disable-next-line no-console - console.warn(`[@fluent/react] ${error.name}: ${error.message}`); - } -} - -export function isReactLocalization(props, propName) { - const prop = props[propName]; - - if (prop instanceof ReactLocalization) { - return null; - } - - return new Error( - `The ${propName} context field must be an instance of ReactLocalization.` - ); -} diff --git a/fluent-react/src/localized.js b/fluent-react/src/localized.js index e971f7ee0..0070d1618 100644 --- a/fluent-react/src/localized.js +++ b/fluent-react/src/localized.js @@ -1,6 +1,6 @@ -import { isValidElement, cloneElement, Component } from "react"; +import { isValidElement, cloneElement, useContext } from "react"; import PropTypes from "prop-types"; -import { isReactLocalization } from "./localization"; +import FluentContext from "./context"; import VOID_ELEMENTS from "../vendor/voidElementTags"; // Match the opening angle bracket (<) in HTML tags, and HTML entities like @@ -51,158 +51,134 @@ function toArguments(props) { * translation is available. It also makes it easy to grep for strings in the * source code. */ -export default class Localized extends Component { - componentDidMount() { - const { l10n } = this.context; - - if (l10n) { - l10n.subscribe(this); - } +function Localized(props) { + const { id, attrs, children: elem = null } = props; + const { l10n, parseMarkup } = useContext(FluentContext); + + if (l10n == null) { + throw new Error( + " needs to have a up in the tree" + ); } - componentWillUnmount() { - const { l10n } = this.context; - - if (l10n) { - l10n.unsubscribe(this); - } + // Validate that the child element isn't an array + if (Array.isArray(child)) { + throw new Error(" expected to receive a single " + + "React node child"); } - /* - * Rerender this component in a new language. - */ - relocalize() { - // When the `ReactLocalization`'s fallback chain changes, update the - // component. - this.forceUpdate(); + if (!l10n) { + // Use the wrapped component as fallback. + return child; } - render() { - const { l10n, parseMarkup } = this.context; - const { id, attrs, children: child = null } = this.props; - - // Validate that the child element isn't an array - if (Array.isArray(child)) { - throw new Error(" expected to receive a single " + - "React node child"); - } - - if (!l10n) { - // Use the wrapped component as fallback. - return child; - } + const bundle = l10n.getBundle(id); - const bundle = l10n.getBundle(id); - - if (bundle === null) { - // Use the wrapped component as fallback. - return child; - } - - const msg = bundle.getMessage(id); - const [args, elems] = toArguments(this.props); - let errors = []; - - // Check if the child inside is a valid element -- if not, then - // it's either null or a simple fallback string. No need to localize the - // attributes. - if (!isValidElement(child)) { - if (msg.value) { - // Replace the fallback string with the message value; - let value = bundle.formatPattern(msg.value, args, errors); - for (let error of errors) { - l10n.reportError(error); - } - return value; - } - - return child; - } + if (bundle === null) { + // Use the wrapped component as fallback. + return child; + } - let localizedProps; - - // The default is to forbid all message attributes. If the attrs prop exists - // on the Localized instance, only set message attributes which have been - // explicitly allowed by the developer. - if (attrs && msg.attributes) { - localizedProps = {}; - errors = []; - for (const [name, allowed] of Object.entries(attrs)) { - if (allowed && name in msg.attributes) { - localizedProps[name] = bundle.formatPattern( - msg.attributes[name], args, errors); - } - } + const msg = bundle.getMessage(id); + const [args, elems] = toArguments(this.props); + let errors = []; + + // Check if the child inside is a valid element -- if not, then + // it's either null or a simple fallback string. No need to localize the + // attributes. + if (!isValidElement(child)) { + if (msg.value) { + // Replace the fallback string with the message value; + let value = bundle.formatPattern(msg.value, args, errors); for (let error of errors) { l10n.reportError(error); } + return value; } - // 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 (child.type in VOID_ELEMENTS) { - return cloneElement(child, localizedProps); - } + return child; + } - // 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 (msg.value === null) { - return cloneElement(child, localizedProps); - } + let localizedProps; + // The default is to forbid all message attributes. If the attrs prop exists + // on the Localized instance, only set message attributes which have been + // explicitly allowed by the developer. + if (attrs && msg.attributes) { + localizedProps = {}; errors = []; - const messageValue = bundle.formatPattern(msg.value, args, errors); + for (const [name, allowed] of Object.entries(attrs)) { + if (allowed && name in msg.attributes) { + localizedProps[name] = bundle.formatPattern( + msg.attributes[name], args, errors); + } + } for (let error of errors) { l10n.reportError(error); } + } - // 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(child, localizedProps, messageValue); - } + // 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 (child.type in VOID_ELEMENTS) { + return cloneElement(child, localizedProps); + } - // 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 = parseMarkup(messageValue); - const translatedChildren = translationNodes.map(childNode => { - if (childNode.nodeType === childNode.TEXT_NODE) { - return childNode.textContent; - } + // 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 (msg.value === null) { + return cloneElement(child, localizedProps); + } - // If the child is not expected just take its textContent. - if (!elems.hasOwnProperty(childNode.localName)) { - return childNode.textContent; - } + errors = []; + const messageValue = bundle.formatPattern(msg.value, args, errors); + for (let error of errors) { + l10n.reportError(error); + } - const sourceChild = elems[childNode.localName]; + // 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(child, localizedProps, messageValue); + } - // 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; - } + // 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 = parseMarkup(messageValue); + const translatedChildren = translationNodes.map(childNode => { + if (childNode.nodeType === childNode.TEXT_NODE) { + return childNode.textContent; + } - // 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); - }); + // If the child is not expected just take its textContent. + if (!elems.hasOwnProperty(childNode.localName)) { + return childNode.textContent; + } - return cloneElement(child, localizedProps, ...translatedChildren); - } + 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(child, localizedProps, ...translatedChildren); } -Localized.contextTypes = { - l10n: isReactLocalization, - parseMarkup: PropTypes.func, -}; +export default Localized; Localized.propTypes = { children: PropTypes.node diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index ab7a826cb..59e7839e1 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -1,6 +1,8 @@ -import { Component, Children } from "react"; +import { CachedSyncIterable } from "cached-iterable"; +import { createElement, useMemo } from "react"; import PropTypes from "prop-types"; -import ReactLocalization, { isReactLocalization} from "./localization"; +import { mapBundleSync } from "fluent-sequence"; +import FluentContext from "./context"; import createParseMarkup from "./markup"; /* @@ -21,47 +23,56 @@ import createParseMarkup from "./markup"; * `ReactLocalization` to format translations. If a translation is missing in * one instance, `ReactLocalization` will fall back to the next one. */ -export default class LocalizationProvider extends Component { - constructor(props) { - super(props); - const {bundles, parseMarkup} = props; - - if (bundles === undefined) { - throw new Error("LocalizationProvider must receive the bundles prop."); - } - - if (!bundles[Symbol.iterator]) { - throw new Error("The bundles prop must be an iterable."); - } - - this.l10n = new ReactLocalization(bundles); - this.parseMarkup = parseMarkup || createParseMarkup(); +export default function LocalizationProvider(props) { + if (props.bundles === undefined) { + throw new Error("LocalizationProvider must receive the bundles prop."); } - getChildContext() { - return { - l10n: this.l10n, - parseMarkup: this.parseMarkup, - }; + if (!props.bundles[Symbol.iterator]) { + throw new Error("The bundles prop must be an iterable."); } - componentWillReceiveProps(next) { - const { bundles } = next; + const bundles = useMemo( + () => CachedSyncIterable.from(props.bundles), + [props.bundles] + ); + const parseMarkup = useMemo( + () => props.parseMarkup || createParseMarkup(), + [props.parseMarkup] + ); + const value = useMemo( + () => ({ + l10n: { + getBundle: id => mapBundleSync(bundles, id), + getString(id, args, fallback) { + const bundle = mapBundleSync(bundles, id); - if (bundles !== this.props.bundles) { - this.l10n.setBundles(bundles); - } - } + if (bundle) { + const msg = bundle.getMessage(id); + if (msg && msg.value) { + let errors = []; + let value = bundle.formatPattern(msg.value, args, errors); + for (let error of errors) { + this.reportError(error); + } + return value; + } + } - render() { - return Children.only(this.props.children); - } -} + return fallback || id; + } + }, + parseMarkup + }), + [bundles, parseMarkup] + ); -LocalizationProvider.childContextTypes = { - l10n: isReactLocalization, - parseMarkup: PropTypes.func, -}; + return createElement( + FluentContext.Provider, + {value}, + props.children + ); +} LocalizationProvider.propTypes = { children: PropTypes.element.isRequired, diff --git a/fluent-react/src/with_localization.js b/fluent-react/src/with_localization.js index 0a210aab0..d0504c880 100644 --- a/fluent-react/src/with_localization.js +++ b/fluent-react/src/with_localization.js @@ -1,34 +1,7 @@ import { createElement, Component } from "react"; -import { isReactLocalization } from "./localization"; - export default function withLocalization(Inner) { class WithLocalization extends Component { - componentDidMount() { - const { l10n } = this.context; - - if (l10n) { - l10n.subscribe(this); - } - } - - componentWillUnmount() { - const { l10n } = this.context; - - if (l10n) { - l10n.unsubscribe(this); - } - } - - /* - * Rerender this component in a new language. - */ - relocalize() { - // When the `ReactLocalization`'s fallback chain changes, update the - // component. - this.forceUpdate(); - } - /* * Find a translation by `id` and format it to a string using `args`. */ @@ -56,10 +29,6 @@ export default function withLocalization(Inner) { WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`; - WithLocalization.contextTypes = { - l10n: isReactLocalization - }; - return WithLocalization; } From 55db0be64c8754c516fb90b50d4e7d7536879af1 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 31 Jul 2019 19:57:58 +0200 Subject: [PATCH 02/14] use jest for snapshot testing localized --- fluent-react/babel.config.js | 15 + fluent-react/package.json | 18 +- fluent-react/src/localized.js | 4 +- fluent-react/src/provider.js | 48 +- fluent-react/test/exports_test.js | 5 - fluent-react/test/index.js | 18 - fluent-react/test/localized_change.test.js | 36 + fluent-react/test/localized_change_test.js | 40 - fluent-react/test/localized_fallback.test.js | 74 ++ fluent-react/test/localized_fallback_test.js | 72 -- fluent-react/test/localized_overlay.test.js | 860 +++++++++++++++++++ fluent-react/test/localized_overlay_test.js | 748 ---------------- fluent-react/test/localized_render.test.js | 505 +++++++++++ fluent-react/test/localized_render_test.js | 523 ----------- fluent-react/test/localized_valid.test.js | 86 ++ fluent-react/test/localized_valid_test.js | 79 -- fluent-react/test/localized_void.test.js | 70 ++ 17 files changed, 1686 insertions(+), 1515 deletions(-) create mode 100644 fluent-react/babel.config.js delete mode 100644 fluent-react/test/index.js create mode 100644 fluent-react/test/localized_change.test.js delete mode 100644 fluent-react/test/localized_change_test.js create mode 100644 fluent-react/test/localized_fallback.test.js delete mode 100644 fluent-react/test/localized_fallback_test.js create mode 100644 fluent-react/test/localized_overlay.test.js delete mode 100644 fluent-react/test/localized_overlay_test.js create mode 100644 fluent-react/test/localized_render.test.js delete mode 100644 fluent-react/test/localized_render_test.js create mode 100644 fluent-react/test/localized_valid.test.js delete mode 100644 fluent-react/test/localized_valid_test.js create mode 100644 fluent-react/test/localized_void.test.js diff --git a/fluent-react/babel.config.js b/fluent-react/babel.config.js new file mode 100644 index 000000000..f8de65760 --- /dev/null +++ b/fluent-react/babel.config.js @@ -0,0 +1,15 @@ +module.exports = { + "presets": [ + "@babel/preset-react", + ["@babel/preset-env", { + "targets": "node >= 8.9.0" + }] + ], + "plugins": [ + ["babel-plugin-transform-rename-import", { + "original": "fluent", + "replacement": "fluent/compat" + }], + "@babel/plugin-proposal-async-generator-functions" + ], +}; diff --git a/fluent-react/package.json b/fluent-react/package.json index f01aba069..18b38493a 100644 --- a/fluent-react/package.json +++ b/fluent-react/package.json @@ -55,14 +55,20 @@ "react": "^0.14.9 || ^15.0.0 || ^16.0.0" }, "devDependencies": { - "@babel/preset-react": "^7.0.0-beta.47", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/preset-env": "^7.5.5", + "@babel/preset-react": "7.0.0", + "babel-jest": "^24.8.0", "babel-plugin-transform-rename-import": "^2.2.0", - "enzyme": "^3.3.0", - "enzyme-adapter-react-16": "^1.1.1", - "jsdom": "^11.12.0", + "jest": "^24.8.0", + "prettier": "^1.18.2", "react": "^16.2.0", "react-dom": "^16.2.0", - "react-test-renderer": "^16.8.6", - "sinon": "^4.2.2" + "react-test-renderer": "^16.8.6" + }, + "jest": { + "transformIgnorePatterns": [ + "node_modules/(?!(@fluent/sequence)/)" + ] } } diff --git a/fluent-react/src/localized.js b/fluent-react/src/localized.js index 0070d1618..236211d7d 100644 --- a/fluent-react/src/localized.js +++ b/fluent-react/src/localized.js @@ -52,7 +52,7 @@ function toArguments(props) { * source code. */ function Localized(props) { - const { id, attrs, children: elem = null } = props; + const { id, attrs, children: child = null } = props; const { l10n, parseMarkup } = useContext(FluentContext); if (l10n == null) { @@ -80,7 +80,7 @@ function Localized(props) { } const msg = bundle.getMessage(id); - const [args, elems] = toArguments(this.props); + const [args, elems] = toArguments(props); let errors = []; // Check if the child inside is a valid element -- if not, then diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index 59e7839e1..5cc0c6057 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -1,7 +1,7 @@ import { CachedSyncIterable } from "cached-iterable"; import { createElement, useMemo } from "react"; import PropTypes from "prop-types"; -import { mapBundleSync } from "fluent-sequence"; +import { mapBundleSync } from "@fluent/sequence"; import FluentContext from "./context"; import createParseMarkup from "./markup"; @@ -32,20 +32,18 @@ export default function LocalizationProvider(props) { throw new Error("The bundles prop must be an iterable."); } - const bundles = useMemo( - () => CachedSyncIterable.from(props.bundles), - [props.bundles] - ); - const parseMarkup = useMemo( - () => props.parseMarkup || createParseMarkup(), - [props.parseMarkup] - ); + const bundles = useMemo(() => CachedSyncIterable.from(props.bundles), [ + props.bundles + ]); + const parseMarkup = useMemo(() => props.parseMarkup || createParseMarkup(), [ + props.parseMarkup + ]); const value = useMemo( - () => ({ - l10n: { + () => { + const l10n = { getBundle: id => mapBundleSync(bundles, id), getString(id, args, fallback) { - const bundle = mapBundleSync(bundles, id); + const bundle = l10n.getBundle(id); if (bundle) { const msg = bundle.getMessage(id); @@ -53,31 +51,37 @@ export default function LocalizationProvider(props) { let errors = []; let value = bundle.formatPattern(msg.value, args, errors); for (let error of errors) { - this.reportError(error); + l10n.reportError(error); } return value; } } return fallback || id; + }, + // XXX Control this via a prop passed to the LocalizationProvider. + // See https://github.com/projectfluent/fluent.js/issues/411. + reportError(error) { + /* global console */ + // eslint-disable-next-line no-console + console.warn(`[@fluent/react] ${error.name}: ${error.message}`); } - }, - parseMarkup - }), + }; + return { + l10n, + parseMarkup + }; + }, [bundles, parseMarkup] ); - return createElement( - FluentContext.Provider, - {value}, - props.children - ); + return createElement(FluentContext.Provider, { value }, props.children); } LocalizationProvider.propTypes = { children: PropTypes.element.isRequired, bundles: isIterable, - parseMarkup: PropTypes.func, + parseMarkup: PropTypes.func }; function isIterable(props, propName, componentName) { diff --git a/fluent-react/test/exports_test.js b/fluent-react/test/exports_test.js index 60da23d9f..cf35f5714 100644 --- a/fluent-react/test/exports_test.js +++ b/fluent-react/test/exports_test.js @@ -3,7 +3,6 @@ import * as FluentReact from '../src/index'; import LocalizationProvider from '../src/provider'; import Localized from '../src/localized'; import withLocalization from '../src/with_localization'; -import ReactLocalization, { isReactLocalization } from '../src/localization'; suite('Exports', () => { test('LocalizationProvider', () => { @@ -21,8 +20,4 @@ suite('Exports', () => { test('ReactLocalization', () => { assert.strictEqual(FluentReact.ReactLocalization, ReactLocalization); }); - - test('isReactLocalization', () => { - assert.strictEqual(FluentReact.isReactLocalization, isReactLocalization); - }); }); diff --git a/fluent-react/test/index.js b/fluent-react/test/index.js deleted file mode 100644 index f6ef1b10a..000000000 --- a/fluent-react/test/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const jsdom = require('jsdom'); -const { JSDOM } = jsdom; -const { window } = new JSDOM('', { - url: 'http://localhost', -}); - -for (const [key, value] of Object.entries(window)) { - if (!(key in global)) { - global[key] = value; - } -} - -const Enzyme = require('enzyme'); -const Adapter = require('enzyme-adapter-react-16'); - -Enzyme.configure({ adapter: new Adapter() }); diff --git a/fluent-react/test/localized_change.test.js b/fluent-react/test/localized_change.test.js new file mode 100644 index 000000000..401b1061a --- /dev/null +++ b/fluent-react/test/localized_change.test.js @@ -0,0 +1,36 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import { LocalizationProvider, Localized } from "../src/index"; + +test("relocalizes", () => { + const Root = ({ bundle }) => ( + + +
+ + + ); + + const bundle1 = new FluentBundle(); + bundle1.addResource(new FluentResource(` +foo = FOO +`)); + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ FOO +
+ `); + + const bundle2 = new FluentBundle(); + bundle2.addResource(new FluentResource(` +foo = BAR +`)); + renderer.update(); + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BAR +
+ `); +}); diff --git a/fluent-react/test/localized_change_test.js b/fluent-react/test/localized_change_test.js deleted file mode 100644 index 875a8bce9..000000000 --- a/fluent-react/test/localized_change_test.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import { Localized } from '../src/index'; - -suite('Localized - change bundles', function() { - test('relocalizing', function() { - const bundle1 = new FluentBundle(); - const l10n = new ReactLocalization([bundle1]); - - bundle1.addResource(new FluentResource(` -foo = FOO -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
FOO
- )); - - const bundle2 = new FluentBundle(); - bundle2.addResource(new FluentResource(` -foo = BAR -`)); - - l10n.setBundles([bundle2]); - - wrapper.update(); - assert.ok(wrapper.contains( -
BAR
- )); - }); -}); diff --git a/fluent-react/test/localized_fallback.test.js b/fluent-react/test/localized_fallback.test.js new file mode 100644 index 000000000..66bd0e11c --- /dev/null +++ b/fluent-react/test/localized_fallback.test.js @@ -0,0 +1,74 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import { LocalizationProvider, Localized } from "../src/index"; + +test("uses message from 1st bundle", () => { + const bundle1 = new FluentBundle(); + + bundle1.addResource(new FluentResource(` +foo = FOO +`)); + + const renderer = TestRenderer.create( + + +
Bar
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ FOO +
+ `); +}); + +test("uses message from the 2nd bundle", function() { + const bundle1 = new FluentBundle(); + const bundle2 = new FluentBundle(); + + bundle1.addResource(new FluentResource(` +not-foo = NOT FOO +`)); + bundle2.addResource(new FluentResource(` +foo = FOO +`)); + + const renderer = TestRenderer.create( + + +
Bar
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ FOO +
+ `); +}); + +test("falls back back for missing message", function() { + const bundle1 = new FluentBundle(); + + bundle1.addResource(new FluentResource(` +not-foo = NOT FOO +`)); + + const renderer = TestRenderer.create( + + +
Bar
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Bar +
+ `); +}); diff --git a/fluent-react/test/localized_fallback_test.js b/fluent-react/test/localized_fallback_test.js deleted file mode 100644 index 3ae439c3b..000000000 --- a/fluent-react/test/localized_fallback_test.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import { Localized } from '../src/index'; - -suite('Localized - fallback', function() { - test('message id in the first context', function() { - const bundle1 = new FluentBundle(); - const l10n = new ReactLocalization([bundle1]); - - bundle1.addResource(new FluentResource(` -foo = FOO -`)); - - const wrapper = shallow( - -
Bar
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
FOO
- )); - }); - - test('message id in the second context', function() { - const bundle1 = new FluentBundle(); - const bundle2 = new FluentBundle(); - const l10n = new ReactLocalization([bundle1, bundle2]); - - bundle1.addResource(new FluentResource(` -not-foo = NOT FOO -`)); - bundle2.addResource(new FluentResource(` -foo = FOO -`)); - - const wrapper = shallow( - -
Bar
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
FOO
- )); - }); - - test('missing message', function() { - const bundle1 = new FluentBundle(); - const l10n = new ReactLocalization([bundle1]); - - bundle1.addResource(new FluentResource(` -not-foo = NOT FOO -`)); - - const wrapper = shallow( - -
Bar
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
Bar
- )); - }); -}); diff --git a/fluent-react/test/localized_overlay.test.js b/fluent-react/test/localized_overlay.test.js new file mode 100644 index 000000000..665b7dc28 --- /dev/null +++ b/fluent-react/test/localized_overlay.test.js @@ -0,0 +1,860 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import createParseMarkup from "../src/markup"; +import { LocalizationProvider, Localized } from "../src/index"; + +describe("Localized - overlay", () => { + test("< in text", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +true = 0 < 3 is true. +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ 0 < 3 is true. +
+ `); + }); + + test("& in text", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +megaman = Jumping & Shooting +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Jumping & Shooting +
+ `); + }); + + test("HTML entity", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +two = First · Second +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ First · Second +
+ `); + }); + + test("one element is matched", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Click ! +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Click + + ! +
+ `); + }); + + test("an element of different case is matched", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Click ! +`)); + + // The Button prop is capitalized whereas the }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Click + + ! +
+ `); + }); + + test("two elements are matched", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Sign in or cancel. +`)); + + const renderer = TestRenderer.create( + + } + cancel={} + > +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ + or + + . +
+ `); + }); + + test("unexpected child is reduced to text", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Sign in or cancel. +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ + or + cancel + . +
+ `); + }); + + test("element not found in the translation is removed", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Sign in. +`)); + + const renderer = TestRenderer.create( + + } + cancel={} + > +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ + . +
+ `); + }); + + test("attributes on translated children are ignored", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Click ! +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Click + + ! +
+ `); + }); + + test("nested children are ignored", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Click ! +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Click + + ! +
+ `); + }); + + test("non-React element prop is used in markup", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Sign in. +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Sign in + . +
+ `); + }); +}); + +describe("Localized - overlay of void elements", () => { + let parseMarkup = createParseMarkup(); + + test("void prop name, void prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("void prop name, void prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("void prop name, void prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + // The opening tag is parsed as an HTMLInputElement and the closing + // is ignored. "Foo" is then parsed as a regular text node. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + Foo AFTER +
+ `); + }); + + test("void prop name, non-empty prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + + + AFTER +
+ `); + }); + + test("void prop name, non-empty prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + + + AFTER +
+ `); + }); + + test("void prop name, non-empty prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + // The opening tag is parsed as an HTMLInputElement and the closing + // is ignored. "Foo" is then parsed as a regular text node. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + + + Foo AFTER +
+ `); + }); + + test("non-void prop name, void prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + // XXX HTML parser breaks self-closing elements + // https://github.com/projectfluent/fluent.js/issues/188 + // is parsed as an unclosed element. Everything that follows + // it becomes its children and is ignored because the passed as a + // prop is known to be void. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + +
+ `); + }); + + test("non-void prop name, void prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("non-void prop name, void prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("non-void prop name, non-empty prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + // XXX HTML parser breaks self-closing elements + // https://github.com/projectfluent/fluent.js/issues/188 + // is parsed as an unclosed element. Everything that follows + // it becomes its children and is inserted into the passed as a prop. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER + +
+ `); + }); + + test("non-void prop name, non-empty prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded
}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + + + AFTER +
+ `); + }); + + test("non-void prop name, non-empty prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + Foo + + AFTER +
+ `); + }); + + test("custom prop name, void prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + // XXX HTML parser breaks self-closing elements + // https://github.com/projectfluent/fluent.js/issues/188 + // is parsed as an unclosed custom element. + // Everything that follows it becomes its children which are ignored because + // the passed as a prop is known to be void. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + +
+ `); + }); + + test("custom prop name, void prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("custom prop name, void prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + }> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER +
+ `); + }); + + test("custom prop name, non-empty prop value, void translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + // XXX HTML parser breaks self-closing elements + // https://github.com/projectfluent/fluent.js/issues/188 + // is parsed as an unclosed custom element. + // Everything that follows it becomes its children which are inserted into + // the passed as a prop. + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + AFTER + +
+ `); + }); + + test("custom prop name, non-empty prop value, empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded
}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + + + AFTER +
+ `); + }); + + test("custom prop name, non-empty prop value, non-empty translation", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = BEFORE Foo AFTER +`)); + + const renderer = TestRenderer.create( + + Hardcoded}> +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ BEFORE + + Foo + + AFTER +
+ `); + }); +}); + +describe("Localized - custom parseMarkup", () => { + test("is called if defined in the context", () => { + let parseMarkupCalls = []; + + function parseMarkup(str) { + parseMarkupCalls.push(str); + return createParseMarkup()(str); + } + + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +# We must use an HTML tag to trigger the overlay logic. +foo = test custom markup parser +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(parseMarkupCalls).toEqual(["test custom markup parser"]); + }); + + test("custom sanitization logic", () => { + function parseMarkup(str) { + return [ + { + TEXT_NODE: 3, + nodeType: 3, + textContent: str.toUpperCase() + } + ]; + } + + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +# We must use an HTML tag to trigger the overlay logic. +foo = test custom markup parser +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ TEST <EM>CUSTOM MARKUP PARSER</EM> +
+ `); + }); +}); diff --git a/fluent-react/test/localized_overlay_test.js b/fluent-react/test/localized_overlay_test.js deleted file mode 100644 index 262de6aa9..000000000 --- a/fluent-react/test/localized_overlay_test.js +++ /dev/null @@ -1,748 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import createParseMarkup from '../src/markup'; -import { Localized } from '../src/index'; - -suite('Localized - overlay', function() {; - let parseMarkup = createParseMarkup(); - - test('< in text', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -true = 0 < 3 is true. -`)); - - const wrapper = shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- 0 {'<'} 3 is true. -
- )); - }); - - test('& in text', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -megaman = Jumping & Shooting -`)); - - const wrapper = shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Jumping & Shooting -
- )); - }); - - test('HTML entity', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -two = First · Second -`)); - - const wrapper = shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- First · Second -
- )); - }); - - test('one element is matched', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Click ! -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Click ! -
- )); - }); - - test('an element of different case is matched', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Click ! -`)); - - // The Button prop is capitalized whereas the }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Click ! -
- )); - }); - - test('two elements are matched', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Sign in or cancel. -`)); - - const wrapper = shallow( - } - cancel={} - > -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- or . -
- )); - }); - - test('unexpected child is reduced to text', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Sign in or cancel. -`)); - - const wrapper = shallow( - } - > -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- or cancel. -
- )); - }); - - test('element not found in the translation is removed', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Sign in. -`)); - - const wrapper = shallow( - } - cancel={} - > -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- . -
- )); - }); - - test('attributes on translated children are ignored', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Click ! -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Click ! -
- )); - }); - - test('nested children are ignored', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Click ! -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Click ! -
- )); - }); - - test('non-React element prop is used in markup', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Sign in. -`)); - - const wrapper = shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- Sign in. -
- )); - }); - -}); - -suite('Localized - overlay of void elements', function() {; - let parseMarkup = createParseMarkup(); - - test('void prop name, void prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('void prop name, void prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('void prop name, void prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - // The opening tag is parsed as an HTMLInputElement and the closing - // is ignored. "Foo" is then parsed as a regular text node. - assert.ok(wrapper.contains( -
- BEFORE Foo AFTER -
- )); - }); - - test('void prop name, non-empty prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE {""} AFTER -
- )); - }); - - test('void prop name, non-empty prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE {""} AFTER -
- )); - }); - - test('void prop name, non-empty prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - // The opening tag is parsed as an HTMLInputElement and the closing - // is ignored. "Foo" is then parsed as a regular text node. - assert.ok(wrapper.contains( -
- BEFORE {""}Foo AFTER -
- )); - }); - - test('non-void prop name, void prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - // XXX HTML parser breaks self-closing elements - // https://github.com/projectfluent/fluent.js/issues/188 - // is parsed as an unclosed element. Everything that follows - // it becomes its children and is ignored because the passed as a - // prop is known to be void. - assert.ok(wrapper.contains( -
- BEFORE -
- )); - }); - - test('non-void prop name, void prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('non-void prop name, void prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('non-void prop name, non-empty prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - // XXX HTML parser breaks self-closing elements - // https://github.com/projectfluent/fluent.js/issues/188 - // is parsed as an unclosed element. Everything that follows - // it becomes its children and is inserted into the passed as a prop. - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('non-void prop name, non-empty prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded
}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE {""} AFTER -
- )); - }); - - test('non-void prop name, non-empty prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE Foo AFTER -
- )); - }); - - test('custom prop name, void prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - // XXX HTML parser breaks self-closing elements - // https://github.com/projectfluent/fluent.js/issues/188 - // is parsed as an unclosed custom element. - // Everything that follows it becomes its children which are ignored because - // the passed as a prop is known to be void. - assert.ok(wrapper.contains( -
- BEFORE -
- )); - }); - - test('custom prop name, void prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('custom prop name, void prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - }> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('custom prop name, non-empty prop value, void translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - // XXX HTML parser breaks self-closing elements - // https://github.com/projectfluent/fluent.js/issues/188 - // is parsed as an unclosed custom element. - // Everything that follows it becomes its children which are inserted into - // the passed as a prop. - assert.ok(wrapper.contains( -
- BEFORE AFTER -
- )); - }); - - test('custom prop name, non-empty prop value, empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE AFTER -`)); - - const wrapper = shallow( - Hardcoded
}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE {""} AFTER -
- )); - }); - - test('custom prop name, non-empty prop value, non-empty translation', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = BEFORE Foo AFTER -`)); - - const wrapper = shallow( - Hardcoded}> -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- BEFORE Foo AFTER -
- )); - }); -}); - -suite('Localized - custom parseMarkup', function() {; - test('is called if defined in the context', function() { - let parseMarkupCalls = []; - function parseMarkup(str) { - parseMarkupCalls.push(str); - return createParseMarkup()(str); - } - - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -# We must use an HTML tag to trigger the overlay logic. -foo = test custom markup parser -`)); - - shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.deepEqual(parseMarkupCalls, ['test custom markup parser']); - }); - - test('custom sanitization logic', function() { - function parseMarkup(str) { - return [ - { - TEXT_NODE: 3, - nodeType: 3, - textContent: str.toUpperCase() - } - ]; - } - - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -# We must use an HTML tag to trigger the overlay logic. -foo = test custom markup parser -`)); - - const wrapper = shallow( - -
- , - { context: { l10n, parseMarkup } } - ); - - assert.ok(wrapper.contains( -
- TEST <EM>CUSTOM MARKUP PARSER</EM> -
- )); - }); -}); diff --git a/fluent-react/test/localized_render.test.js b/fluent-react/test/localized_render.test.js new file mode 100644 index 000000000..9623100ce --- /dev/null +++ b/fluent-react/test/localized_render.test.js @@ -0,0 +1,505 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import { LocalizationProvider, Localized } from "../src/index"; + +describe("Localized - rendering", () => { + test("render the value", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = FOO +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ FOO +
+ `); + }); + + test("render an allowed attribute", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .attr = ATTR +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("only render allowed attributes", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .attr1 = ATTR 1 + .attr2 = ATTR 2 +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("filter out forbidden attributes", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .attr = ATTR +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("filter all attributes if attrs not given", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .attr = ATTR +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("preserve existing attributes when setting new ones", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .attr = ATTR +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("overwrite existing attributes if allowed", () => { + const bundle = new FluentBundle(); + + bundle.addResource( + new FluentResource(` +foo = + .existing = ATTR +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("protect existing attributes if setting is forbidden", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = + .existing = ATTR +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("protect existing attributes by default", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = + .existing = ATTR +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ `); + }); + + test("preserve children when translation value is null", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = + .title = TITLE +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` + + `); + }); + + test("$arg is passed to format the value", () => { + const bundle = new FluentBundle("en", { useIsolating: false }); + const format = jest.spyOn(bundle, "formatPattern"); + + bundle.addResource(new FluentResource(` +foo = { $arg } +`)); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ ARG +
+ `); + + expect(format).toHaveBeenCalledWith(expect.anything(), { arg: "ARG" }, expect.anything()); + }); + + test("$arg is passed to format the attributes", () => { + const bundle = new FluentBundle(); + const format = jest.spyOn(bundle, "formatPattern"); + + bundle.addResource( + new FluentResource(` +foo = { $arg } + .title = { $arg } +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ ARG +
+ `); + + // The value. + expect(format).toHaveBeenNthCalledWith(1, expect.anything(), { + arg: "ARG" + }, expect.anything()); + // The attribute. + expect(format).toHaveBeenNthCalledWith(2, expect.anything(), { + arg: "ARG" + }, expect.anything()); + }); + + test("render with a fragment and no message preserves the fragment", () => { + const bundle = new FluentBundle(); + + const renderer = TestRenderer.create( + + + +
Fragment content
+
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Fragment content +
+ `); + }); + + test("A missing $arg does not break rendering", () => { + const bundle = new FluentBundle("en", { useIsolating: false }); + + bundle.addResource( + new FluentResource(` +foo = { $arg } + .title = { $arg } +`) + ); + + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ {$arg} +
+ `); + }); + + test("render with a fragment and no message value preserves the fragment", () => { + const bundle = new FluentBundle(); + bundle.addResource(new FluentResource(` +foo = + .attr = Attribute +`)); + + const renderer = TestRenderer.create( + + + +
Fragment content
+
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+ Fragment content +
+ `); + }); + + test("render with a fragment renders the message into the fragment", () => { + const bundle = new FluentBundle(); + bundle.addResource(new FluentResource(` +foo = Test message +`)); + + const renderer = TestRenderer.create( + + + +
Fragment content
+
+
+
+ ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"Test message"`); + }); + + test("render with an empty fragment and no message preserves the fragment", () => { + const bundle = new FluentBundle(); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`null`); + }); + + test("render with an empty fragment and no message value preserves the fragment", () => { + const bundle = new FluentBundle(); + bundle.addResource(new FluentResource(` +foo = + .attr = Attribute +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`null`); + }); + + test("render with an empty fragment renders the message into the fragment", () => { + const bundle = new FluentBundle(); + bundle.addResource(new FluentResource(` +foo = Test message +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"Test message"`); + }); + + test("render with a string fallback and no message returns the fallback", () => { + const bundle = new FluentBundle(); + + const renderer = TestRenderer.create( + + String fallback + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"String fallback"`); + }); + + test("render with a string fallback returns the message", () => { + const bundle = new FluentBundle(); + bundle.addResource(new FluentResource(` +foo = Test message +`)); + + const renderer = TestRenderer.create( + + String fallback + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"Test message"`); + }); + + test("render without a fallback returns the message", () => { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = Message +`)); + + const renderer = TestRenderer.create( + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"Message"`); + }); + + test("render without a fallback and no message returns nothing", () => { + const bundle = new FluentBundle(); + + const renderer = TestRenderer.create( + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`null`); + }); +}); diff --git a/fluent-react/test/localized_render_test.js b/fluent-react/test/localized_render_test.js deleted file mode 100644 index 266baf2ee..000000000 --- a/fluent-react/test/localized_render_test.js +++ /dev/null @@ -1,523 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import { Localized } from '../src/index'; - -suite('Localized - rendering', function() { - test('render the value', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = FOO -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
FOO
- )); - }); - - test('render an allowed attribute', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('only render allowed attributes', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr1 = ATTR 1 - .attr2 = ATTR 2 -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('filter out forbidden attributes', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('filter all attributes if attrs not given', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('preserve existing attributes when setting new ones', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('overwrite existing attributes if allowed', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .existing = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('protect existing attributes if setting is forbidden', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .existing = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('protect existing attributes by default', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .existing = ATTR -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
- )); - }); - - test('preserve children when translation value is null', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .title = TITLE -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( - - )); - }); - - - test('$arg is passed to format the value', function() { - const bundle = new FluentBundle("en", {useIsolating: false}); - const formatPattern = sinon.spy(bundle, 'formatPattern'); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = { $arg } -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - const { args } = formatPattern.getCall(0); - assert.deepEqual(args[1], { arg: 'ARG' }); - - assert.ok(wrapper.contains( -
ARG
- )); - }); - - test('$arg is passed to format the attributes', function() { - const bundle = new FluentBundle(); - const formatPattern = sinon.spy(bundle, 'formatPattern'); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = { $arg } - .title = { $arg } -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - // The value. - assert.deepEqual(formatPattern.getCall(0).args[1], { arg: 'ARG' }); - // The attribute. - assert.deepEqual(formatPattern.getCall(1).args[1], { arg: 'ARG' }); - - assert.ok(wrapper.contains( -
ARG
- )); - }); - - test('A missing $arg does not break rendering', function() { - const bundle = new FluentBundle("en", {useIsolating: false}); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = { $arg } - .title = { $arg } -`)); - - const wrapper = shallow( - -
- , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( -
{"{$arg}"}
- )); - }); - - test('render with a fragment and no message preserves the fragment', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - const wrapper = shallow( - - -
Fragment content
-
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - -
Fragment content
-
- )); - }); - - test('render with a fragment and no message value preserves the fragment', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = - .attr = Attribute -`)); - - const wrapper = shallow( - - -
Fragment content
-
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - -
Fragment content
-
- )); - }); - - test('render with a fragment renders the message into the fragment', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = Test message -`)); - - const wrapper = shallow( - - -
Fragment content
-
-
, - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - - Test message - - )); - }); - - test('render with an empty fragment and no message preserves the fragment', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - - )); - }); - - test('render with an empty fragment and no message value preserves the fragment', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = - .attr = Attribute -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - - )); - }); - - test('render with an empty fragment renders the message into the fragment', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = Test message -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.equals( - - Test message - - )); - }); - - test('render with a string fallback and no message returns the fallback', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - const wrapper = shallow( - - String fallback - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), 'String fallback'); - }); - - test('render with a string fallback and no message value preserves the fallback', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = - .attr = Attribute -`)); - - const wrapper = shallow( - - String fallback - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), 'String fallback'); - }); - - test('render with a string fallback returns the message', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - bundle.addResource(new FluentResource(` -foo = Test message -`)); - - const wrapper = shallow( - - String fallback - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), 'Test message'); - }); - - test('render without a fallback and no message returns nothing', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - const wrapper = shallow( - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), ''); - }); - - test('render without a fallback and no message value returns nothing', - function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .attr = Attribute -`)); - - const wrapper = shallow( - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), ''); - }); - - test('render without a fallback returns the message', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = Message -`)); - - const wrapper = shallow( - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), 'Message'); - }); - -}); diff --git a/fluent-react/test/localized_valid.test.js b/fluent-react/test/localized_valid.test.js new file mode 100644 index 000000000..4cf4757f1 --- /dev/null +++ b/fluent-react/test/localized_valid.test.js @@ -0,0 +1,86 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { LocalizationProvider, Localized } from "../src/index"; + +describe("Localized - validation", () => { + let consoleError = console.error; + + beforeAll(() => { + console.error = () => {}; + }); + + afterAll(() => { + console.error = consoleError; + }); + + test("inside of a LocalizationProvider", () => { + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("outside of a LocalizationProvider", () => { + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("with a manually set context", () => { + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("without a child", () => { + const renderer = TestRenderer.create( + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`null`); + }); + + test("with multiple children", () => { + expect(() => { + TestRenderer.create( + + +
+
+ + + ) + }).toThrow(/single/) + }); + + test("without id", () => { + const renderer = TestRenderer.create( + + +
+ + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/fluent-react/test/localized_valid_test.js b/fluent-react/test/localized_valid_test.js deleted file mode 100644 index e9cc62af6..000000000 --- a/fluent-react/test/localized_valid_test.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { LocalizationProvider, Localized } from '../src/index'; -import ReactLocalization from '../src/localization'; - -suite('Localized - validation', function() { - suiteSetup(function() { - this.error = console.error; - console.error = () => {}; - }); - - suiteTeardown(function() { - console.error = this.error; - }); - - test('inside of a LocalizationProvider', function() { - const wrapper = shallow( - - -
- - - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('outside of a LocalizationProvider', function() { - const wrapper = shallow( - -
- - ); - assert.strictEqual(wrapper.find('div').length, 1); - }); - - test('with a manually set context', function() { - const wrapper = shallow( - -
- , - { context: { l10n: new ReactLocalization([]) } } - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('without a child', function() { - const wrapper = shallow( - , - { context: { l10n: new ReactLocalization([]) } } - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('with multiple children', function() { - function render() { - shallow( - -
-
- , - { context: { l10n: new ReactLocalization([]) } } - ); - } - assert.throws(render, /a single React node child/); - }); - - test('without id', function() { - const wrapper = shallow( - -
- , - { context: { l10n: new ReactLocalization([]) } } - ); - assert.ok(wrapper.contains( -
- )); - }); -}); diff --git a/fluent-react/test/localized_void.test.js b/fluent-react/test/localized_void.test.js new file mode 100644 index 000000000..92f1845b1 --- /dev/null +++ b/fluent-react/test/localized_void.test.js @@ -0,0 +1,70 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import { LocalizationProvider, Localized } from "../src/index"; + +describe("Localized - void elements", function() { + test("do not render the value in void elements", function() { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = FOO +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(``); + }); + + test("render attributes in void elements", function() { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = + .title = TITLE +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` + + `); + }); + + test("render attributes but not value in void elements", function() { + const bundle = new FluentBundle(); + + bundle.addResource(new FluentResource(` +foo = FOO + .title = TITLE +`)); + + const renderer = TestRenderer.create( + + + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` + + `); + }); +}); From 0816b85182815495ae36e8fc428bcc3060e9e131 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Tue, 6 Aug 2019 20:29:29 +0200 Subject: [PATCH 03/14] use new context API in withLocalization and update its tests --- fluent-react/src/provider.js | 6 + fluent-react/src/with_localization.js | 45 +++---- fluent-react/test/exports.test.js | 18 +++ fluent-react/test/exports_test.js | 23 ---- fluent-react/test/localized_void_test.js | 70 ----------- fluent-react/test/provider_valid.test.js | 46 +++++++ fluent-react/test/provider_valid_test.js | 63 ---------- fluent-react/test/with_localization.test.js | 128 ++++++++++++++++++++ fluent-react/test/with_localization_test.js | 126 ------------------- 9 files changed, 216 insertions(+), 309 deletions(-) create mode 100644 fluent-react/test/exports.test.js delete mode 100644 fluent-react/test/exports_test.js delete mode 100644 fluent-react/test/localized_void_test.js create mode 100644 fluent-react/test/provider_valid.test.js delete mode 100644 fluent-react/test/provider_valid_test.js create mode 100644 fluent-react/test/with_localization.test.js delete mode 100644 fluent-react/test/with_localization_test.js diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index 5cc0c6057..3aa7ad7f4 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -87,6 +87,12 @@ LocalizationProvider.propTypes = { function isIterable(props, propName, componentName) { const prop = props[propName]; + if (!prop) { + return new Error( + `The ${propName} prop supplied to ${componentName} is required.` + ) + } + if (Symbol.iterator in Object(prop)) { return null; } diff --git a/fluent-react/src/with_localization.js b/fluent-react/src/with_localization.js index d0504c880..c1cd50e64 100644 --- a/fluent-react/src/with_localization.js +++ b/fluent-react/src/with_localization.js @@ -1,35 +1,26 @@ -import { createElement, Component } from "react"; +import { createElement, useContext } from "react"; +import FluentContext from "./context"; export default function withLocalization(Inner) { - class WithLocalization extends Component { - /* - * Find a translation by `id` and format it to a string using `args`. - */ - getString(id, args, fallback) { - const { l10n } = this.context; - - if (!l10n) { - return fallback || id; - } - - return l10n.getString(id, args, fallback); - } - - render() { - return createElement( - Inner, - Object.assign( - // getString needs to be re-bound on updates to trigger a re-render - { getString: (...args) => this.getString(...args) }, - this.props - ) - ); - } + function WithDisplay(props) { + const { l10n } = useContext(FluentContext); + return createElement( + Inner, + // getString needs to be re-bound on updates to trigger a re-render + { + getString: (id, args, fallback) => ( + l10n + ? l10n.getString(id, args, fallback) + : fallback || id + ), + ...props + }, + ); } - WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`; + WithDisplay.displayName = `WithLocalization(${displayName(Inner)})`; - return WithLocalization; + return WithDisplay; } function displayName(component) { diff --git a/fluent-react/test/exports.test.js b/fluent-react/test/exports.test.js new file mode 100644 index 000000000..095f5c8a0 --- /dev/null +++ b/fluent-react/test/exports.test.js @@ -0,0 +1,18 @@ +import * as FluentReact from '../src/index'; +import LocalizationProvider from '../src/provider'; +import Localized from '../src/localized'; +import withLocalization from '../src/with_localization'; + +describe('Exports', () => { + test('LocalizationProvider', () => { + expect(FluentReact.LocalizationProvider).toBe(LocalizationProvider); + }); + + test('Localized', () => { + expect(FluentReact.Localized).toBe(Localized); + }); + + test('withLocalization', () => { + expect(FluentReact.withLocalization).toBe(withLocalization); + }); +}); diff --git a/fluent-react/test/exports_test.js b/fluent-react/test/exports_test.js deleted file mode 100644 index cf35f5714..000000000 --- a/fluent-react/test/exports_test.js +++ /dev/null @@ -1,23 +0,0 @@ -import assert from 'assert'; -import * as FluentReact from '../src/index'; -import LocalizationProvider from '../src/provider'; -import Localized from '../src/localized'; -import withLocalization from '../src/with_localization'; - -suite('Exports', () => { - test('LocalizationProvider', () => { - assert.strictEqual(FluentReact.LocalizationProvider, LocalizationProvider); - }); - - test('Localized', () => { - assert.strictEqual(FluentReact.Localized, Localized); - }); - - test('withLocalization', () => { - assert.strictEqual(FluentReact.withLocalization, withLocalization); - }); - - test('ReactLocalization', () => { - assert.strictEqual(FluentReact.ReactLocalization, ReactLocalization); - }); -}); diff --git a/fluent-react/test/localized_void_test.js b/fluent-react/test/localized_void_test.js deleted file mode 100644 index ce1727f5c..000000000 --- a/fluent-react/test/localized_void_test.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import { Localized } from '../src/index'; - -suite('Localized - void elements', function() { - test('do not render the value in void elements', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = FOO -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( - - )); - }); - - test('render attributes in void elements', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = - .title = TITLE -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( - - )); - }); - - test('render attributes but not value in void elements', function() { - const bundle = new FluentBundle(); - const l10n = new ReactLocalization([bundle]); - - bundle.addResource(new FluentResource(` -foo = FOO - .title = TITLE -`)); - - const wrapper = shallow( - - - , - { context: { l10n } } - ); - - assert.ok(wrapper.contains( - - )); - }); -}); diff --git a/fluent-react/test/provider_valid.test.js b/fluent-react/test/provider_valid.test.js new file mode 100644 index 000000000..70f579955 --- /dev/null +++ b/fluent-react/test/provider_valid.test.js @@ -0,0 +1,46 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { LocalizationProvider } from "../src/index"; + +describe("LocalizationProvider - validation", () => { + let consoleError = console.error; + + beforeAll(() => { + console.error = (message) => { + if (/(Failed prop type)/.test(message)) { + throw new Error(message); + } + }; + }); + + afterAll(() => { + console.error = consoleError; + }); + + test("valid use", () => { + const renderer = TestRenderer.create( + +
+ + ); + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("without a child", () => { + expect(() => { + TestRenderer.create(); + }).toThrow(/required/); + }); + + test("without bundles", () => { + expect(() => { + TestRenderer.create(); + }).toThrow(/is required/); + }); + + test("without iterable bundles", () => { + expect(() => { + TestRenderer.create(); + }).toThrow(/must be an iterable/); + }); +}); diff --git a/fluent-react/test/provider_valid_test.js b/fluent-react/test/provider_valid_test.js deleted file mode 100644 index a8323cea0..000000000 --- a/fluent-react/test/provider_valid_test.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { LocalizationProvider } from '../src/index'; - -suite('LocalizationProvider - validation', function() { - suiteSetup(function() { - this.error = console.error; - console.error = () => {}; - }); - - suiteTeardown(function() { - console.error = this.error; - }); - - test('valid use', function() { - const wrapper = shallow( - -
- - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('without a child', function() { - function render() { - shallow( - - ); - } - assert.throws(render, /a single React element child/); - }); - - test('with multiple children', function() { - function render() { - shallow( - -
-
- - ); - } - assert.throws(render, /a single React element child/); - }); - - test('without bundles', function() { - function render() { - shallow( - - ); - } - assert.throws(render, /must receive the bundles prop/); - }); - - test('without iterable bundles', function() { - function render() { - shallow( - - ); - } - assert.throws(render, /must be an iterable/); - }); -}); diff --git a/fluent-react/test/with_localization.test.js b/fluent-react/test/with_localization.test.js new file mode 100644 index 000000000..e740f8901 --- /dev/null +++ b/fluent-react/test/with_localization.test.js @@ -0,0 +1,128 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { FluentBundle, FluentResource } from "../../fluent-bundle/src"; +import { LocalizationProvider, withLocalization } from "../src"; + +function DummyComponent() { + return
; +} + +describe("withLocalization", () => { + test("render inside of a LocalizationProvider", () => { + const EnhancedComponent = withLocalization(DummyComponent); + + const renderer = TestRenderer.create( + + + + ); + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("render outside of a LocalizationProvider", () => { + const EnhancedComponent = withLocalization(DummyComponent); + + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchInlineSnapshot(`
`); + }); + + test("getString with access to the l10n context", () => { + const bundle = new FluentBundle("en", { useIsolating: false }); + const EnhancedComponent = withLocalization(DummyComponent); + + bundle.addResource( + new FluentResource(` +foo = FOO +bar = BAR {$arg} +`) + ); + + const renderer = TestRenderer.create( + + + + ); + + const { getString } = renderer.root.findByType(DummyComponent).props; + // Returns the translation. + expect(getString("foo", {})).toBe("FOO"); + expect(getString("bar", { arg: "ARG" })).toBe("BAR ARG"); + // Doesn't throw on formatting errors. + expect(getString("bar", {})).toBe("BAR {$arg}"); + }); + + test("getString with access to the l10n context, with fallback value", () => { + const bundle = new FluentBundle("en", { useIsolating: false }); + const EnhancedComponent = withLocalization(DummyComponent); + + bundle.addResource( + new FluentResource(` +foo = FOO +bar = BAR {$arg} +`) + ); + + const renderer = TestRenderer.create( + + + + ); + + const { getString } = renderer.root.findByType(DummyComponent).props; + // Returns the translation, even if fallback value provided. + expect(getString("foo", {}, "fallback")).toBe("FOO"); + // Returns the fallback. + expect(getString("missing", {}, "fallback")).toBe("fallback"); + expect(getString("bar", { arg: "ARG" })).toBe("BAR ARG"); + // Doesn't throw on formatting errors. + expect(getString("bar", {})).toBe("BAR {$arg}"); + }); + + test("getString without access to the l10n context", () => { + const EnhancedComponent = withLocalization(DummyComponent); + const renderer = TestRenderer.create(); + + const { getString } = renderer.root.findByType(DummyComponent).props; + // Returns the id if no fallback. + expect(getString("foo", { arg: 1 })).toBe("foo"); + }); + + test("getString without access to the l10n context, with fallback value", () => { + const EnhancedComponent = withLocalization(DummyComponent); + const renderer = TestRenderer.create(); + + const { getString } = renderer.root.findByType(DummyComponent).props; + // Returns the fallback if provided. + expect(getString("foo", { arg: 1 }, "fallback message")).toBe( + "fallback message" + ); + }); + + test("getString with access to the l10n context, with message changes", () => { + const initialBundle = new FluentBundle(); + const EnhancedComponent = withLocalization(({ getString }) => + getString("foo") + ); + + initialBundle.addResource(new FluentResource("foo = FOO")); + + const renderer = TestRenderer.create( + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"FOO"`); + + const newBundle = new FluentBundle(); + newBundle.addResource(new FluentResource("foo = BAR")); + + renderer.update( + + + + ); + + expect(renderer.toJSON()).toMatchInlineSnapshot(`"BAR"`); + }); +}); diff --git a/fluent-react/test/with_localization_test.js b/fluent-react/test/with_localization_test.js deleted file mode 100644 index a280fd3f7..000000000 --- a/fluent-react/test/with_localization_test.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import { mount, shallow } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import ReactLocalization from '../src/localization'; -import { withLocalization, LocalizationProvider } from '../src'; - -function DummyComponent() { - return
; -} - -suite('withLocalization', function() { - test('render inside of a LocalizationProvider', function() { - const EnhancedComponent = withLocalization(DummyComponent); - - const wrapper = shallow( - - - - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('render outside of a LocalizationProvider', function() { - const EnhancedComponent = withLocalization(DummyComponent); - - const wrapper = shallow( - , - ); - assert.strictEqual(wrapper.length, 1); - }); - - test('getString with access to the l10n context', function() { - const bundle = new FluentBundle("en", {useIsolating: false}); - const l10n = new ReactLocalization([bundle]); - const EnhancedComponent = withLocalization(DummyComponent); - - bundle.addResource(new FluentResource(` -foo = FOO -bar = BAR {$arg} -`)); - - const wrapper = shallow( - , - { context: { l10n } } - ); - - const getString = wrapper.prop('getString'); - // Returns the translation. - assert.strictEqual(getString('foo', {}), 'FOO'); - assert.strictEqual(getString('bar', {arg: 'ARG'}), 'BAR ARG'); - // Doesn't throw on formatting errors. - assert.strictEqual(getString('bar', {}), 'BAR {$arg}'); - }); - - test('getString with access to the l10n context, with fallback value', function() { - const bundle = new FluentBundle("en", {useIsolating: false}); - const l10n = new ReactLocalization([bundle]); - const EnhancedComponent = withLocalization(DummyComponent); - - bundle.addResource(new FluentResource(` -foo = FOO -bar = BAR {$arg} -`)); - - const wrapper = shallow( - , - { context: { l10n } } - ); - - const getString = wrapper.prop('getString'); - // Returns the translation, even if fallback value provided. - assert.strictEqual(getString('foo', {}, 'fallback'), 'FOO'); - // Returns the fallback. - assert.strictEqual(getString('missing', {}, 'fallback'), 'fallback'); - assert.strictEqual(getString('bar', {arg: 'ARG'}), 'BAR ARG'); - // Doesn't throw on formatting errors. - assert.strictEqual(getString('bar', {}), 'BAR {$arg}'); - }); - - test('getString without access to the l10n context', function() { - const EnhancedComponent = withLocalization(DummyComponent); - - const wrapper = shallow( - - ); - - const getString = wrapper.prop('getString'); - // Returns the id if no fallback. - assert.strictEqual(getString('foo', {arg: 1}), 'foo'); - }); - - test('getString without access to the l10n context, with fallback value', function() { - const EnhancedComponent = withLocalization(DummyComponent); - - const wrapper = shallow( - - ); - - const getString = wrapper.prop('getString'); - // Returns the fallback if provided. - assert.strictEqual(getString('foo', {arg: 1}, 'fallback message'), 'fallback message'); - }); - - test('getString with access to the l10n context, with message changes', function() { - const initialBundle = new FluentBundle(); - const l10n = new ReactLocalization([initialBundle]); - const EnhancedComponent = withLocalization(({ getString }) => getString('foo')); - - initialBundle.addResource(new FluentResource('foo = FOO')); - - const wrapper = mount( - , - { context: { l10n } } - ); - - assert.strictEqual(wrapper.text(), 'FOO'); - - const newBundle = new FluentBundle(); - newBundle.addResource(new FluentResource('foo = BAR')); - l10n.setBundles([newBundle]); - - wrapper.update(); - assert.strictEqual(wrapper.text(), 'BAR'); - }) -}); From a1d186aeca685e9e05689ed90c23ae4a8afc34d4 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 7 Aug 2019 12:36:04 +0200 Subject: [PATCH 04/14] fix linting issues --- fluent-react/src/localized.js | 2 +- fluent-react/src/provider.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fluent-react/src/localized.js b/fluent-react/src/localized.js index 236211d7d..c8ddd7ac0 100644 --- a/fluent-react/src/localized.js +++ b/fluent-react/src/localized.js @@ -55,7 +55,7 @@ function Localized(props) { const { id, attrs, children: child = null } = props; const { l10n, parseMarkup } = useContext(FluentContext); - if (l10n == null) { + if (l10n === null) { throw new Error( " needs to have a up in the tree" ); diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index 3aa7ad7f4..a30c6da3f 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -38,7 +38,7 @@ export default function LocalizationProvider(props) { const parseMarkup = useMemo(() => props.parseMarkup || createParseMarkup(), [ props.parseMarkup ]); - const value = useMemo( + const contextValue = useMemo( () => { const l10n = { getBundle: id => mapBundleSync(bundles, id), @@ -75,7 +75,11 @@ export default function LocalizationProvider(props) { [bundles, parseMarkup] ); - return createElement(FluentContext.Provider, { value }, props.children); + return createElement( + FluentContext.Provider, + { value: contextValue }, + props.children + ); } LocalizationProvider.propTypes = { @@ -90,7 +94,7 @@ function isIterable(props, propName, componentName) { if (!prop) { return new Error( `The ${propName} prop supplied to ${componentName} is required.` - ) + ); } if (Symbol.iterator in Object(prop)) { From a603ffa7e673116659f99f9b7116598919d75c71 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Mon, 12 Aug 2019 14:19:56 +0200 Subject: [PATCH 05/14] Remove tests not covering fluent-react's API --- fluent-react/test/provider_change_test.js | 49 -------------------- fluent-react/test/provider_context_test.js | 54 ---------------------- 2 files changed, 103 deletions(-) delete mode 100644 fluent-react/test/provider_change_test.js delete mode 100644 fluent-react/test/provider_context_test.js diff --git a/fluent-react/test/provider_change_test.js b/fluent-react/test/provider_change_test.js deleted file mode 100644 index ce4cc6852..000000000 --- a/fluent-react/test/provider_change_test.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import assert from 'assert'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import { LocalizationProvider } from '../src/index'; - -suite('LocalizationProvider - changing props', function() { - test('does not change the ReactLocalization', function() { - const wrapper = shallow( - -
- - ); - - const oldL10n = wrapper.instance().l10n; - wrapper.setProps({ bundles: [] }); - const newL10n = wrapper.instance().l10n; - - assert.strictEqual(oldL10n, newL10n); - }); - - test('calls the ReactLocalization\'s setBundles method', function() { - const wrapper = shallow( - -
- - ); - - const spy = sinon.spy(wrapper.instance().l10n, 'setBundles'); - const newMessages = []; - wrapper.setProps({ bundles: newMessages }); - const { args } = spy.getCall(0); - assert.deepEqual(args, [newMessages]); - }); - - test('changes the ReactLocalization\'s bundles bundles', function() { - const wrapper = shallow( - -
- - ); - - const oldContexts = wrapper.instance().l10n.bundles; - wrapper.setProps({ bundles: [] }); - const newContexts = wrapper.instance().l10n.bundles; - - assert.notEqual(oldContexts, newContexts); - }); -}); diff --git a/fluent-react/test/provider_context_test.js b/fluent-react/test/provider_context_test.js deleted file mode 100644 index 76e8c475e..000000000 --- a/fluent-react/test/provider_context_test.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from "prop-types"; -import assert from 'assert'; -import { render } from 'enzyme'; -import { FluentBundle, FluentResource } from '../../fluent-bundle/src'; -import { LocalizationProvider, isReactLocalization } from '../src/index'; - - -suite('LocalizationProvider - context', function() { - test('exposes localization', function() { - const bundle = new FluentBundle(); - bundle.addResource(new FluentResource("foo = Foo")); - - class Testing extends Component { - render() { - return

{this.context.l10n.getString("foo")}

; - } - } - - Testing.contextTypes = { - l10n: isReactLocalization - }; - - const wrapper = render( - - - - ); - - assert.ok(wrapper.is("p")); - assert.ok(wrapper.text() === "Foo"); - }); - - test('exposes custom parseMarkup', function() { - class Testing extends Component { - render() { - return

{this.context.parseMarkup()}

; - } - } - - Testing.contextTypes = { - parseMarkup: PropTypes.func - }; - - const wrapper = render( - "Test"}> - - - ); - - assert.ok(wrapper.is("p")); - assert.ok(wrapper.text() === "Test"); - }); -}); From b23bf2839019901d2cbfd1c5700b0adb4cacdf2e Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Mon, 12 Aug 2019 15:02:26 +0200 Subject: [PATCH 06/14] Move test task into each separate makefile --- common.mk | 14 -------------- fluent-bundle/makefile | 7 +++++++ fluent-dedent/makefile | 6 ++++++ fluent-dom/makefile | 7 +++++++ fluent-gecko/makefile | 6 ++++++ fluent-langneg/makefile | 6 ++++++ fluent-react/makefile | 3 +++ fluent-sequence/makefile | 6 ++++++ fluent-syntax/makefile | 6 ++++++ 9 files changed, 47 insertions(+), 14 deletions(-) diff --git a/common.mk b/common.mk index 83b83541c..6b104d351 100644 --- a/common.mk +++ b/common.mk @@ -19,20 +19,6 @@ lint: @eslint --config $(ROOT)/eslint_test.json --max-warnings 0 test/ @echo -e " $(OK) $@" -test: -ifneq (,$(wildcard ./test/index.js)) - @nyc --reporter=text --reporter=html mocha \ - --recursive --ui tdd \ - --require $(ROOT)/mocha_setup \ - --require ./test/index \ - test/**/*_test.js -else - @nyc --reporter=text --reporter=html mocha \ - --recursive --ui tdd \ - --require $(ROOT)/mocha_setup \ - test/**/*_test.js -endif - html: ifneq (,$(wildcard ./.esdoc.json)) @esdoc diff --git a/fluent-bundle/makefile b/fluent-bundle/makefile index a5364caec..5445423b3 100644 --- a/fluent-bundle/makefile +++ b/fluent-bundle/makefile @@ -5,6 +5,13 @@ include ../common.mk build: index.js compat.js +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + --require ./test/index \ + test/**/*_test.js + index.js: $(SOURCES) @rollup $(CURDIR)/src/index.js \ --config $(ROOT)/bundle_config.js \ diff --git a/fluent-dedent/makefile b/fluent-dedent/makefile index c42097ba0..ee62c3995 100644 --- a/fluent-dedent/makefile +++ b/fluent-dedent/makefile @@ -5,6 +5,12 @@ include ../common.mk build: index.js compat.js +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + test/**/*_test.js + index.js: $(SOURCES) @rollup $(CURDIR)/src/index.js \ --config $(ROOT)/bundle_config.js \ diff --git a/fluent-dom/makefile b/fluent-dom/makefile index e37ac1258..59948f400 100644 --- a/fluent-dom/makefile +++ b/fluent-dom/makefile @@ -4,6 +4,13 @@ DEPS := cached-iterable:CachedIterable include ../common.mk +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + --require ./test/index \ + test/**/*_test.js + build: index.js compat.js index.js: $(SOURCES) diff --git a/fluent-gecko/makefile b/fluent-gecko/makefile index ed32d184d..7f9750608 100644 --- a/fluent-gecko/makefile +++ b/fluent-gecko/makefile @@ -6,6 +6,12 @@ include ../common.mk version = $(1)@$(shell node -e "\ console.log(require('../$(1)/package.json').version)") +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + test/**/*_test.js + build: Fluent.jsm FluentSyntax.jsm Localization.jsm DOMLocalization.jsm l10n.js fluent-react.js Fluent.jsm: $(SOURCES) diff --git a/fluent-langneg/makefile b/fluent-langneg/makefile index 7112e1880..e3c7c4e6c 100644 --- a/fluent-langneg/makefile +++ b/fluent-langneg/makefile @@ -3,6 +3,12 @@ GLOBAL := FluentLangNeg include ../common.mk +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + test/**/*_test.js + build: index.js compat.js index.js: $(SOURCES) diff --git a/fluent-react/makefile b/fluent-react/makefile index 73384fb3b..27dd0a251 100644 --- a/fluent-react/makefile +++ b/fluent-react/makefile @@ -6,6 +6,9 @@ include ../common.mk build: index.js compat.js +test: + ./node_modules/.bin/jest --collect-coverage + index.js: $(SOURCES) @rollup $(CURDIR)/src/index.js \ --config $(ROOT)/bundle_config.js \ diff --git a/fluent-sequence/makefile b/fluent-sequence/makefile index 0cb5255e0..913abf09d 100644 --- a/fluent-sequence/makefile +++ b/fluent-sequence/makefile @@ -3,6 +3,12 @@ GLOBAL := FluentSequence include ../common.mk +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + test/**/*_test.js + build: index.js compat.js index.js: $(SOURCES) diff --git a/fluent-syntax/makefile b/fluent-syntax/makefile index 3dc75639b..c07b8d37b 100644 --- a/fluent-syntax/makefile +++ b/fluent-syntax/makefile @@ -3,6 +3,12 @@ GLOBAL := FluentSyntax include ../common.mk +test: + @nyc --reporter=text --reporter=html mocha \ + --recursive --ui tdd \ + --require $(ROOT)/mocha_setup \ + test/**/*_test.js + build: index.js compat.js index.js: $(SOURCES) From ecb5bde238b2d67103c1d05f55c669395bc39fc4 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 11 Dec 2019 18:00:50 +0100 Subject: [PATCH 07/14] Memoize the whole Provider component instead of each prop It simplifies the code a lot and might also make it faster, as memoization during render comes at the cost of additional array and function scope allocation. This also adds a test to check that the Provider is not re-rendered when the bundles prop does not change. --- fluent-react/src/provider.js | 74 +++++++++++------------- fluent-react/test/provider_valid.test.js | 12 +++- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index a30c6da3f..dc1b11f3c 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -1,5 +1,5 @@ import { CachedSyncIterable } from "cached-iterable"; -import { createElement, useMemo } from "react"; +import { createElement, memo } from "react"; import PropTypes from "prop-types"; import { mapBundleSync } from "@fluent/sequence"; import FluentContext from "./context"; @@ -23,7 +23,7 @@ import createParseMarkup from "./markup"; * `ReactLocalization` to format translations. If a translation is missing in * one instance, `ReactLocalization` will fall back to the next one. */ -export default function LocalizationProvider(props) { +function LocalizationProvider(props) { if (props.bundles === undefined) { throw new Error("LocalizationProvider must receive the bundles prop."); } @@ -32,52 +32,42 @@ export default function LocalizationProvider(props) { throw new Error("The bundles prop must be an iterable."); } - const bundles = useMemo(() => CachedSyncIterable.from(props.bundles), [ - props.bundles - ]); - const parseMarkup = useMemo(() => props.parseMarkup || createParseMarkup(), [ - props.parseMarkup - ]); - const contextValue = useMemo( - () => { - const l10n = { - getBundle: id => mapBundleSync(bundles, id), - getString(id, args, fallback) { - const bundle = l10n.getBundle(id); + const bundles = CachedSyncIterable.from(props.bundles); + const parseMarkup = props.parseMarkup || createParseMarkup(); + const l10n = { + getBundle: id => mapBundleSync(bundles, id), + getString(id, args, fallback) { + const bundle = l10n.getBundle(id); - if (bundle) { - const msg = bundle.getMessage(id); - if (msg && msg.value) { - let errors = []; - let value = bundle.formatPattern(msg.value, args, errors); - for (let error of errors) { - l10n.reportError(error); - } - return value; - } + if (bundle) { + const msg = bundle.getMessage(id); + if (msg && msg.value) { + let errors = []; + let value = bundle.formatPattern(msg.value, args, errors); + for (let error of errors) { + l10n.reportError(error); } - - return fallback || id; - }, - // XXX Control this via a prop passed to the LocalizationProvider. - // See https://github.com/projectfluent/fluent.js/issues/411. - reportError(error) { - /* global console */ - // eslint-disable-next-line no-console - console.warn(`[@fluent/react] ${error.name}: ${error.message}`); + return value; } - }; - return { - l10n, - parseMarkup - }; + } + + return fallback || id; }, - [bundles, parseMarkup] - ); + // XXX Control this via a prop passed to the LocalizationProvider. + // See https://github.com/projectfluent/fluent.js/issues/411. + reportError(error) { + /* global console */ + // eslint-disable-next-line no-console + console.warn(`[@fluent/react] ${error.name}: ${error.message}`); + } + }; return createElement( FluentContext.Provider, - { value: contextValue }, + { value: { + l10n, + parseMarkup + } }, props.children ); } @@ -105,3 +95,5 @@ function isIterable(props, propName, componentName) { `The ${propName} prop supplied to ${componentName} must be an iterable.` ); } + +export default memo(LocalizationProvider); diff --git a/fluent-react/test/provider_valid.test.js b/fluent-react/test/provider_valid.test.js index 70f579955..ccce959e5 100644 --- a/fluent-react/test/provider_valid.test.js +++ b/fluent-react/test/provider_valid.test.js @@ -1,5 +1,5 @@ import React from "react"; -import TestRenderer from "react-test-renderer"; +import TestRenderer, {act} from "react-test-renderer"; import { LocalizationProvider } from "../src/index"; describe("LocalizationProvider - validation", () => { @@ -43,4 +43,14 @@ describe("LocalizationProvider - validation", () => { TestRenderer.create(); }).toThrow(/must be an iterable/); }); + + test("is memoized (no re-render) when props are the same", () => { + const bundles = []; + const spy = jest.spyOn(bundles, Symbol.iterator); + let renderer = TestRenderer.create(); + act(() => { + renderer = renderer.update(); + }); + expect(spy).toHaveBeenCalledTimes(1); + }); }); From 8f618ef268f49e076bf7a7ce21f96c1e5dd89c82 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 11 Dec 2019 18:02:58 +0100 Subject: [PATCH 08/14] Update fluent-react/makefile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Staś Małolepszy --- fluent-react/makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluent-react/makefile b/fluent-react/makefile index 27dd0a251..8e2b65da6 100644 --- a/fluent-react/makefile +++ b/fluent-react/makefile @@ -7,7 +7,7 @@ include ../common.mk build: index.js compat.js test: - ./node_modules/.bin/jest --collect-coverage + jest --collect-coverage index.js: $(SOURCES) @rollup $(CURDIR)/src/index.js \ From a67a0b76be7e29fcac0722386fc2fa87a116ea05 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 11 Dec 2019 18:05:42 +0100 Subject: [PATCH 09/14] Add comment explaining that we need babel.config.js for Jest --- fluent-react/babel.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/fluent-react/babel.config.js b/fluent-react/babel.config.js index f8de65760..94802389b 100644 --- a/fluent-react/babel.config.js +++ b/fluent-react/babel.config.js @@ -1,3 +1,4 @@ +// Jest requires us to specify the transforms it needs to run the tests module.exports = { "presets": [ "@babel/preset-react", From 8021604a7d3e0dc335cad7ebcd52a7c94e2a6de0 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 11 Dec 2019 18:08:29 +0100 Subject: [PATCH 10/14] Remove error throwing behavior for Localized without a Provider --- fluent-react/src/localized.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fluent-react/src/localized.js b/fluent-react/src/localized.js index c8ddd7ac0..9fc02cf40 100644 --- a/fluent-react/src/localized.js +++ b/fluent-react/src/localized.js @@ -55,12 +55,6 @@ function Localized(props) { const { id, attrs, children: child = null } = props; const { l10n, parseMarkup } = useContext(FluentContext); - if (l10n === null) { - throw new Error( - " needs to have a up in the tree" - ); - } - // Validate that the child element isn't an array if (Array.isArray(child)) { throw new Error(" expected to receive a single " + From 61a621926d1cc739dc58779463334caf9f7c5e7f Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 11 Dec 2019 18:10:12 +0100 Subject: [PATCH 11/14] Fix erroneous WithLocalization renaming --- fluent-react/src/with_localization.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fluent-react/src/with_localization.js b/fluent-react/src/with_localization.js index c1cd50e64..31b836c09 100644 --- a/fluent-react/src/with_localization.js +++ b/fluent-react/src/with_localization.js @@ -2,7 +2,7 @@ import { createElement, useContext } from "react"; import FluentContext from "./context"; export default function withLocalization(Inner) { - function WithDisplay(props) { + function WithLocalization(props) { const { l10n } = useContext(FluentContext); return createElement( Inner, @@ -18,9 +18,9 @@ export default function withLocalization(Inner) { ); } - WithDisplay.displayName = `WithLocalization(${displayName(Inner)})`; + WithLocalization.displayName = `WithLocalization(${displayName(Inner)})`; - return WithDisplay; + return WithLocalization; } function displayName(component) { From ea35184efae98950a352de19e62570303800d544 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Wed, 11 Dec 2019 18:34:39 +0100 Subject: [PATCH 12/14] Bring ReactLocalization back to encapsulate bundles and related methods --- fluent-react/babel.config.js | 3 +- fluent-react/package.json | 1 + fluent-react/src/context.js | 3 +- fluent-react/src/localization.js | 47 ++++++++++++++++++++++++++++++++ fluent-react/src/provider.js | 37 ++----------------------- fluent-react/test/.babelrc | 5 ---- 6 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 fluent-react/src/localization.js delete mode 100644 fluent-react/test/.babelrc diff --git a/fluent-react/babel.config.js b/fluent-react/babel.config.js index 94802389b..7ff70d250 100644 --- a/fluent-react/babel.config.js +++ b/fluent-react/babel.config.js @@ -11,6 +11,7 @@ module.exports = { "original": "fluent", "replacement": "fluent/compat" }], - "@babel/plugin-proposal-async-generator-functions" + "@babel/plugin-proposal-async-generator-functions", + "@babel/plugin-proposal-class-properties" ], }; diff --git a/fluent-react/package.json b/fluent-react/package.json index 18b38493a..becddcd6a 100644 --- a/fluent-react/package.json +++ b/fluent-react/package.json @@ -56,6 +56,7 @@ }, "devDependencies": { "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/preset-env": "^7.5.5", "@babel/preset-react": "7.0.0", "babel-jest": "^24.8.0", diff --git a/fluent-react/src/context.js b/fluent-react/src/context.js index e6287e6f4..ed4516646 100644 --- a/fluent-react/src/context.js +++ b/fluent-react/src/context.js @@ -1,3 +1,4 @@ import { createContext } from "react"; +import ReactLocalization from "./localization"; -export default createContext({getBundle: null, parseMarkup: null}); +export default createContext(new ReactLocalization([])); diff --git a/fluent-react/src/localization.js b/fluent-react/src/localization.js new file mode 100644 index 000000000..b4bfe8ae2 --- /dev/null +++ b/fluent-react/src/localization.js @@ -0,0 +1,47 @@ +import { mapBundleSync } from "@fluent/sequence"; +import { CachedSyncIterable } from "cached-iterable"; + +/* + * `ReactLocalization` handles translation formatting and fallback. + * + * The current negotiated fallback chain of languages is stored in the + * `ReactLocalization` instance in form of an iterable of `FluentBundle` + * instances. This iterable is used to find the best existing translation for + * a given identifier. + * + * The `ReactLocalization` class instances are exposed to `Localized` elements + * via the `LocalizationProvider` component. + */ +export default class ReactLocalization { + constructor(bundles) { + this.bundles = CachedSyncIterable.from(bundles); + } + + getBundle = id => mapBundleSync(this.bundles, id); + + getString(id, args, fallback) { + const bundle = this.getBundle(id); + + if (bundle) { + const msg = bundle.getMessage(id); + if (msg && msg.value) { + let errors = []; + let value = bundle.formatPattern(msg.value, args, errors); + for (let error of errors) { + this.reportError(error); + } + return value; + } + } + + return fallback || id; + } + + // XXX Control this via a prop passed to the LocalizationProvider. + // See https://github.com/projectfluent/fluent.js/issues/411. + reportError(error) { + /* global console */ + // eslint-disable-next-line no-console + console.warn(`[@fluent/react] ${error.name}: ${error.message}`); + } +} diff --git a/fluent-react/src/provider.js b/fluent-react/src/provider.js index dc1b11f3c..cb5e45714 100644 --- a/fluent-react/src/provider.js +++ b/fluent-react/src/provider.js @@ -1,8 +1,7 @@ -import { CachedSyncIterable } from "cached-iterable"; import { createElement, memo } from "react"; import PropTypes from "prop-types"; -import { mapBundleSync } from "@fluent/sequence"; import FluentContext from "./context"; +import ReactLocalization from "./localization"; import createParseMarkup from "./markup"; /* @@ -32,41 +31,11 @@ function LocalizationProvider(props) { throw new Error("The bundles prop must be an iterable."); } - const bundles = CachedSyncIterable.from(props.bundles); - const parseMarkup = props.parseMarkup || createParseMarkup(); - const l10n = { - getBundle: id => mapBundleSync(bundles, id), - getString(id, args, fallback) { - const bundle = l10n.getBundle(id); - - if (bundle) { - const msg = bundle.getMessage(id); - if (msg && msg.value) { - let errors = []; - let value = bundle.formatPattern(msg.value, args, errors); - for (let error of errors) { - l10n.reportError(error); - } - return value; - } - } - - return fallback || id; - }, - // XXX Control this via a prop passed to the LocalizationProvider. - // See https://github.com/projectfluent/fluent.js/issues/411. - reportError(error) { - /* global console */ - // eslint-disable-next-line no-console - console.warn(`[@fluent/react] ${error.name}: ${error.message}`); - } - }; - return createElement( FluentContext.Provider, { value: { - l10n, - parseMarkup + l10n: new ReactLocalization(props.bundles, props.parseMarkup), + parseMarkup: props.parseMarkup || createParseMarkup() } }, props.children ); diff --git a/fluent-react/test/.babelrc b/fluent-react/test/.babelrc deleted file mode 100644 index 30e6bceb6..000000000 --- a/fluent-react/test/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "presets": [ - "@babel/preset-react" - ] -} From 0a83a197115ae1b199ea465ff186d4bd6a00a958 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Fri, 13 Dec 2019 12:13:51 +0100 Subject: [PATCH 13/14] Remove babel class properties transform --- fluent-react/babel.config.js | 3 +-- fluent-react/package.json | 1 - fluent-react/src/localization.js | 4 +++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fluent-react/babel.config.js b/fluent-react/babel.config.js index 7ff70d250..94802389b 100644 --- a/fluent-react/babel.config.js +++ b/fluent-react/babel.config.js @@ -11,7 +11,6 @@ module.exports = { "original": "fluent", "replacement": "fluent/compat" }], - "@babel/plugin-proposal-async-generator-functions", - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-async-generator-functions" ], }; diff --git a/fluent-react/package.json b/fluent-react/package.json index becddcd6a..18b38493a 100644 --- a/fluent-react/package.json +++ b/fluent-react/package.json @@ -56,7 +56,6 @@ }, "devDependencies": { "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/preset-env": "^7.5.5", "@babel/preset-react": "7.0.0", "babel-jest": "^24.8.0", diff --git a/fluent-react/src/localization.js b/fluent-react/src/localization.js index b4bfe8ae2..7817b3c9f 100644 --- a/fluent-react/src/localization.js +++ b/fluent-react/src/localization.js @@ -17,7 +17,9 @@ export default class ReactLocalization { this.bundles = CachedSyncIterable.from(bundles); } - getBundle = id => mapBundleSync(this.bundles, id); + getBundle(id) { + return mapBundleSync(this.bundles, id); + } getString(id, args, fallback) { const bundle = this.getBundle(id); From d02a9c5455a20b8e25de11fd3f7de37c5282ce69 Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Fri, 13 Dec 2019 12:30:29 +0100 Subject: [PATCH 14/14] Correctly initialize context --- fluent-react/src/context.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fluent-react/src/context.js b/fluent-react/src/context.js index ed4516646..b7f6a09a3 100644 --- a/fluent-react/src/context.js +++ b/fluent-react/src/context.js @@ -1,4 +1,7 @@ import { createContext } from "react"; import ReactLocalization from "./localization"; -export default createContext(new ReactLocalization([])); +export default createContext({ + l10n: new ReactLocalization([]), + parseMarkup: null +});