Skip to content

Commit

Permalink
Merge pull request #101 from stasm/fluent-react-overlays
Browse files Browse the repository at this point in the history
DOM overlays for fluent-react
  • Loading branch information
stasm authored Dec 12, 2017
2 parents 5f99827 + 3036df2 commit 4967fff
Show file tree
Hide file tree
Showing 21 changed files with 496 additions and 413 deletions.
20 changes: 20 additions & 0 deletions fluent-react/examples/html-markup/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "fluent-react-example-html-markup",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "1.0.17"
},
"dependencies": {
"fluent": "file:../../../fluent",
"fluent-intl-polyfill": "file:../../../fluent-intl-polyfill",
"fluent-langneg": "file:../../../fluent-langneg",
"fluent-react": "file:../../../fluent-react",
"react": "^16.1.1",
"react-dom": "^16.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
}
}
19 changes: 19 additions & 0 deletions fluent-react/examples/html-markup/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML Markup in translations - a fluent-react example</title>
<style>
button.text {
background: none;
border: none;
padding: 0;
color: blue;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
29 changes: 29 additions & 0 deletions fluent-react/examples/html-markup/src/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { Localized, withLocalization } from 'fluent-react';

function App(props) {
function showAlert(id) {
const { getString } = props;
alert(getString(id));
}

return (
<div>
<Localized id="sign-in-or-cancel"
signin={<button onClick={() => showAlert('clicked-sign-in')}></button>}
cancel={<button className="text" onClick={() => showAlert('clicked-cancel')}></button>}
>
<p>{'<signin>Sign in</signin> or <cancel>cancel</cancel>.'}</p>
</Localized>

<Localized id="agree-prompt"
input={<input type="text" />}
button={<button onClick={() => showAlert('agree-alert')}></button>}
>
<p>{'My name is <input/> and <button>I agree</button>.'}</p>
</Localized>
</div>
);
}

export default withLocalization(App);
13 changes: 13 additions & 0 deletions fluent-react/examples/html-markup/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { LocalizationProvider } from 'fluent-react';

import { generateMessages } from './l10n';
import App from './App';

ReactDOM.render(
<LocalizationProvider messages={generateMessages(navigator.languages)}>
<App />
</LocalizationProvider>,
document.getElementById('root')
);
37 changes: 37 additions & 0 deletions fluent-react/examples/html-markup/src/l10n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'fluent-intl-polyfill';
import { MessageContext } from 'fluent';
import { negotiateLanguages } from 'fluent-langneg';

const MESSAGES_ALL = {
'pl': `
sign-in-or-cancel = <signin>Zaloguj</signin> albo <cancel>anuluj</cancel>.
clicked-sign-in = Brawo!
clicked-cancel = OK, nieważne.
agree-prompt = Nazywam się <input/> i <button>zgadzam się</button>.
agree-alert = Fajnie, zgadzamy się.
`,
'en-US': `
sign-in-or-cancel = <signin>Sign in</signin> or <cancel>cancel</cancel>.
clicked-sign-in = You are now signed in.
clicked-cancel = OK, nevermind.
agree-prompt = My name is <input/> and <button>I agree</button>.
agree-alert = Cool, agreed.
`,
};

export function* generateMessages(userLocales) {
// Choose locales that are best for the user.
const currentLocales = negotiateLanguages(
userLocales,
['en-US', 'pl'],
{ defaultLocale: 'en-US' }
);

for (const locale of currentLocales) {
const cx = new MessageContext(locale);
cx.addMessages(MESSAGES_ALL[locale]);
yield cx;
}
}
7 changes: 2 additions & 5 deletions fluent-react/src/localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ export default class ReactLocalization {
}

formatCompound(mcx, msg, args) {
const rawParts = mcx.formatToParts(msg, args) || [];

// Format the parts using the current `MessageContext` instance.
const parts = rawParts.map(part => part.valueOf(mcx));
const value = mcx.format(msg, args);

if (msg.attrs) {
var attrs = {};
Expand All @@ -62,7 +59,7 @@ export default class ReactLocalization {
}
}

return { parts, attrs };
return { value, attrs };
}

/*
Expand Down
84 changes: 41 additions & 43 deletions fluent-react/src/localized.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,26 @@
import { isValidElement, cloneElement, Component, Children } from 'react';
import PropTypes from 'prop-types';
import { MessageArgument } from 'fluent/compat';

import { isReactLocalization } from './localization';
import { parseMarkup } from './markup';

/*
* A Fluent argument type for React elements.
*
* When `MessageContext`'s `formatToParts` method is used, any interpolations
* which are valid `MessageArgument` instances are returned unformatted. The
* parts can then be `valueOf`'ed and concatenated to create the final
* translation.
*
* With `ElementArgument` it becomes possible to pass React elements as
* arguments to translations. This may be useful for passing links or buttons,
* or in general: elements with logic which should be defined in the app.
*/
class ElementArgument extends MessageArgument {
valueOf() {
return this.value;
}
}

/*
* Prepare props passed to `Localized` for formetting.
*
* Filter props which are not intended for formatting and turn arguments which
* are React elements into `ElementArgument` instances.
*
* Prepare props passed to `Localized` for formatting.
*/
function toArguments(props) {
const args = {};

for (const propname of Object.keys(props)) {
if (!propname.startsWith('$')) {
continue;
}

const arg = props[propname];
const name = propname.substr(1);

if (isValidElement(arg)) {
args[name] = new ElementArgument(arg);
} else {
args[name] = arg;
const elems = {};

for (const [propname, propval] of Object.entries(props)) {
if (propname.startsWith('$')) {
const name = propname.substr(1);
args[name] = propval;
} else if (isValidElement(propval)) {
elems[propname] = propval;
}
}

return args;
return [args, elems];
}

/*
Expand Down Expand Up @@ -116,12 +89,37 @@ export default class Localized extends Component {
}

const msg = mcx.getMessage(id);
const args = toArguments(this.props);
const { parts, attrs } = l10n.formatCompound(mcx, msg, args);
const [args, elems] = toArguments(this.props);
const { value, attrs } = l10n.formatCompound(mcx, msg, args);

if (value === null || !value.includes('<')) {
return cloneElement(elem, attrs, value);
}

// The formatted parts can be passed to `cloneElements` as arguments. They
// will be used as children of the cloned element.
return cloneElement(elem, attrs, ...parts);
const translationNodes = Array.from(parseMarkup(value).childNodes);
const translatedChildren = translationNodes.map(childNode => {
if (childNode.nodeType === childNode.TEXT_NODE) {
return childNode.textContent;
}

// If the child is not expected just take its textContent.
if (!elems.hasOwnProperty(childNode.localName)) {
return childNode.textContent;
}

return cloneElement(
elems[childNode.localName],
// XXX Explicitly ignore any attributes defined in the translation.
null,
// XXX React breaks if we try to pass non-null children to void elements
// (like <input>). At the same time, textContent of such elements is an
// empty string, so we explicitly pass null instead.
// See https://github.com/projectfluent/fluent.js/issues/105.
childNode.textContent || null
);
});

return cloneElement(elem, attrs, ...translatedChildren);
}
}

Expand Down
8 changes: 8 additions & 0 deletions fluent-react/src/markup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-env browser */

const TEMPLATE = document.createElement('template');

export function parseMarkup(str) {
TEMPLATE.innerHTML = str;
return TEMPLATE.content;
}
12 changes: 9 additions & 3 deletions fluent-react/test/localized_change_test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import assert from 'assert';
import sinon from 'sinon';
import { mount } from 'enzyme';
import MessageContext from './message_context_stub';
import { MessageContext } from '../../fluent/src';
import ReactLocalization from '../src/localization';
import { Localized } from '../src/index';

Expand All @@ -11,6 +10,10 @@ suite('Localized - change messages', function() {
const mcx1 = new MessageContext();
const l10n = new ReactLocalization([mcx1]);

mcx1.addMessages(`
foo = FOO
`);

const wrapper = mount(
<Localized id="foo">
<div />
Expand All @@ -23,7 +26,10 @@ suite('Localized - change messages', function() {
));

const mcx2 = new MessageContext();
sinon.stub(mcx2, 'getMessage').returns('BAR');
mcx2.addMessages(`
foo = BAR
`);

l10n.setMessages([mcx2]);

assert.ok(wrapper.contains(
Expand Down
20 changes: 16 additions & 4 deletions fluent-react/test/localized_fallback_test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import assert from 'assert';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import MessageContext from './message_context_stub';
import { MessageContext } from '../../fluent/src';
import ReactLocalization from '../src/localization';
import { Localized } from '../src/index';

Expand All @@ -11,6 +10,10 @@ suite('Localized - fallback', function() {
const mcx1 = new MessageContext();
const l10n = new ReactLocalization([mcx1]);

mcx1.addMessages(`
foo = FOO
`);

const wrapper = shallow(
<Localized id="foo">
<div>Bar</div>
Expand All @@ -25,10 +28,16 @@ suite('Localized - fallback', function() {

test('message id in the second context', function() {
const mcx1 = new MessageContext();
sinon.stub(mcx1, 'hasMessage').returns(false);
const mcx2 = new MessageContext();
const l10n = new ReactLocalization([mcx1, mcx2]);

mcx1.addMessages(`
not-foo = NOT FOO
`);
mcx2.addMessages(`
foo = FOO
`);

const wrapper = shallow(
<Localized id="foo">
<div>Bar</div>
Expand All @@ -43,9 +52,12 @@ suite('Localized - fallback', function() {

test('missing message', function() {
const mcx1 = new MessageContext();
sinon.stub(mcx1, 'hasMessage').returns(false);
const l10n = new ReactLocalization([mcx1]);

mcx1.addMessages(`
not-foo = NOT FOO
`);

const wrapper = shallow(
<Localized id="foo">
<div>Bar</div>
Expand Down
Loading

0 comments on commit 4967fff

Please sign in to comment.