Skip to content

Commit b298dfa

Browse files
authored
Merge pull request #380 from stasm/formatPattern
FluentBundle.formatPattern
2 parents 4dd134b + 384dc56 commit b298dfa

File tree

128 files changed

+4118
-3003
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+4118
-3003
lines changed

fluent-dom/src/localization.js

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ export default class Localization {
7171
/**
7272
* Format translations into {value, attributes} objects.
7373
*
74-
* The fallback logic is the same as in `formatValues` but the argument type
75-
* is stricter (an array of arrays) and it returns {value, attributes}
76-
* objects which are suitable for the translation of DOM elements.
74+
* The fallback logic is the same as in `formatValues` but it returns {value,
75+
* attributes} objects which are suitable for the translation of DOM
76+
* elements.
7777
*
7878
* docL10n.formatMessages([
7979
* {id: 'hello', args: { who: 'Mary' }},
@@ -101,8 +101,8 @@ export default class Localization {
101101
/**
102102
* Retrieve translations corresponding to the passed keys.
103103
*
104-
* A generalized version of `DOMLocalization.formatValue`. Keys can
105-
* either be simple string identifiers or `[id, args]` arrays.
104+
* A generalized version of `DOMLocalization.formatValue`. Keys must
105+
* be `{id, args}` objects.
106106
*
107107
* docL10n.formatValues([
108108
* {id: 'hello', args: { who: 'Mary' }},
@@ -164,26 +164,26 @@ export default class Localization {
164164
}
165165

166166
/**
167-
* Format the value of a message into a string.
167+
* Format the value of a message into a string or `null`.
168168
*
169169
* This function is passed as a method to `keysFromBundle` and resolve
170170
* a value of a single L10n Entity using provided `FluentBundle`.
171171
*
172-
* If the function fails to retrieve the entity, it will return an ID of it.
173-
* If formatting fails, it will return a partially resolved entity.
174-
*
175-
* In both cases, an error is being added to the errors array.
172+
* If the message doesn't have a value, return `null`.
176173
*
177174
* @param {FluentBundle} bundle
178-
* @param {Array<Error>} errors
179-
* @param {string} id
180-
* @param {Object} args
181-
* @returns {string}
175+
* @param {Array<Error>} errors
176+
* @param {Object} message
177+
* @param {Object} args
178+
* @returns {string|null}
182179
* @private
183180
*/
184-
function valueFromBundle(bundle, errors, id, args) {
185-
const msg = bundle.getMessage(id);
186-
return bundle.format(msg, args, errors);
181+
function valueFromBundle(bundle, errors, message, args) {
182+
if (message.value) {
183+
return bundle.formatPattern(message.value, args, errors);
184+
}
185+
186+
return null;
187187
}
188188

189189
/**
@@ -195,34 +195,29 @@ function valueFromBundle(bundle, errors, id, args) {
195195
* The function will return an object with a value and attributes of the
196196
* entity.
197197
*
198-
* If the function fails to retrieve the entity, the value is set to the ID of
199-
* an entity, and attributes to `null`. If formatting fails, it will return
200-
* a partially resolved value and attributes.
201-
*
202-
* In both cases, an error is being added to the errors array.
203-
*
204198
* @param {FluentBundle} bundle
205-
* @param {Array<Error>} errors
206-
* @param {String} id
207-
* @param {Object} args
199+
* @param {Array<Error>} errors
200+
* @param {Object} message
201+
* @param {Object} args
208202
* @returns {Object}
209203
* @private
210204
*/
211-
function messageFromBundle(bundle, errors, id, args) {
212-
const msg = bundle.getMessage(id);
213-
205+
function messageFromBundle(bundle, errors, message, args) {
214206
const formatted = {
215-
value: bundle.format(msg, args, errors),
207+
value: null,
216208
attributes: null,
217209
};
218210

219-
if (msg.attrs) {
220-
formatted.attributes = [];
221-
for (const [name, attr] of Object.entries(msg.attrs)) {
222-
const value = bundle.format(attr, args, errors);
223-
if (value !== null) {
224-
formatted.attributes.push({name, value});
225-
}
211+
if (message.value) {
212+
formatted.value = bundle.formatPattern(message.value, args, errors);
213+
}
214+
215+
let attrNames = Object.keys(message.attributes);
216+
if (attrNames.length > 0) {
217+
formatted.attributes = new Array(attrNames.length);
218+
for (let [i, name] of attrNames.entries()) {
219+
let value = bundle.formatPattern(message.attributes[name], args, errors);
220+
formatted.attributes[i] = {name, value};
226221
}
227222
}
228223

@@ -270,9 +265,10 @@ function keysFromBundle(method, bundle, keys, translations) {
270265
return;
271266
}
272267

273-
if (bundle.hasMessage(id)) {
268+
let message = bundle.getMessage(id);
269+
if (message) {
274270
messageErrors.length = 0;
275-
translations[i] = method(bundle, messageErrors, id, args);
271+
translations[i] = method(bundle, messageErrors, message, args);
276272
// XXX: Report resolver errors
277273
} else {
278274
missingIds.add(id);

fluent-react/src/localization.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ export default class ReactLocalization {
5555
*/
5656
getString(id, args, fallback) {
5757
const bundle = this.getBundle(id);
58-
59-
if (bundle === null) {
60-
return fallback || id;
58+
if (bundle) {
59+
const msg = bundle.getMessage(id);
60+
if (msg && msg.value) {
61+
return bundle.formatPattern(msg.value, args);
62+
}
6163
}
6264

63-
const msg = bundle.getMessage(id);
64-
return bundle.format(msg, args);
65+
return fallback || id;
6566
}
6667
}
6768

fluent-react/src/localized.js

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,45 +79,50 @@ export default class Localized extends Component {
7979

8080
render() {
8181
const { l10n, parseMarkup } = this.context;
82-
const { id, attrs, children: elem = null } = this.props;
82+
const { id, attrs, children: child = null } = this.props;
8383

8484
// Validate that the child element isn't an array
85-
if (Array.isArray(elem)) {
85+
if (Array.isArray(child)) {
8686
throw new Error("<Localized/> expected to receive a single " +
8787
"React node child");
8888
}
8989

9090
if (!l10n) {
9191
// Use the wrapped component as fallback.
92-
return elem;
92+
return child;
9393
}
9494

9595
const bundle = l10n.getBundle(id);
9696

9797
if (bundle === null) {
9898
// Use the wrapped component as fallback.
99-
return elem;
99+
return child;
100100
}
101101

102102
const msg = bundle.getMessage(id);
103103
const [args, elems] = toArguments(this.props);
104-
const messageValue = bundle.format(msg, args);
105104

106-
// Check if the fallback is a valid element -- if not then it's not
107-
// markup (e.g. nothing or a fallback string) so just use the
108-
// formatted message value
109-
if (!isValidElement(elem)) {
110-
return messageValue;
105+
// Check if the child inside <Localized> is a valid element -- if not, then
106+
// it's either null or a simple fallback string. No need to localize the
107+
// attributes.
108+
if (!isValidElement(child)) {
109+
if (msg.value) {
110+
// Replace the fallback string with the message value;
111+
return bundle.formatPattern(msg.value, args);
112+
}
113+
114+
return child;
111115
}
112116

113117
// The default is to forbid all message attributes. If the attrs prop exists
114118
// on the Localized instance, only set message attributes which have been
115119
// explicitly allowed by the developer.
116-
if (attrs && msg.attrs) {
120+
if (attrs && msg.attributes) {
117121
var localizedProps = {};
118122
for (const [name, allowed] of Object.entries(attrs)) {
119-
if (allowed && msg.attrs.hasOwnProperty(name)) {
120-
localizedProps[name] = bundle.format(msg.attrs[name], args);
123+
if (allowed && name in msg.attributes) {
124+
localizedProps[name] = bundle.formatPattern(
125+
msg.attributes[name], args);
121126
}
122127
}
123128
}
@@ -126,21 +131,23 @@ export default class Localized extends Component {
126131
// message value and do not pass it to cloneElement in order to avoid the
127132
// "void element tags must neither have `children` nor use
128133
// `dangerouslySetInnerHTML`" error.
129-
if (elem.type in VOID_ELEMENTS) {
130-
return cloneElement(elem, localizedProps);
134+
if (child.type in VOID_ELEMENTS) {
135+
return cloneElement(child, localizedProps);
131136
}
132137

133138
// If the message has a null value, we're only interested in its attributes.
134139
// Do not pass the null value to cloneElement as it would nuke all children
135140
// of the wrapped component.
136-
if (messageValue === null) {
137-
return cloneElement(elem, localizedProps);
141+
if (msg.value === null) {
142+
return cloneElement(child, localizedProps);
138143
}
139144

145+
const messageValue = bundle.formatPattern(msg.value, args);
146+
140147
// If the message value doesn't contain any markup nor any HTML entities,
141148
// insert it as the only child of the wrapped component.
142149
if (!reMarkup.test(messageValue)) {
143-
return cloneElement(elem, localizedProps, messageValue);
150+
return cloneElement(child, localizedProps, messageValue);
144151
}
145152

146153
// If the message contains markup, parse it and try to match the children
@@ -173,7 +180,7 @@ export default class Localized extends Component {
173180
return cloneElement(sourceChild, null, childNode.textContent);
174181
});
175182

176-
return cloneElement(elem, localizedProps, ...translatedChildren);
183+
return cloneElement(child, localizedProps, ...translatedChildren);
177184
}
178185
}
179186

fluent-react/test/localized_render_test.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ foo =
224224

225225
test('$arg is passed to format the value', function() {
226226
const bundle = new FluentBundle();
227-
const format = sinon.spy(bundle, 'format');
227+
const formatPattern = sinon.spy(bundle, 'formatPattern');
228228
const l10n = new ReactLocalization([bundle]);
229229

230230
bundle.addMessages(`
@@ -238,13 +238,13 @@ foo = { $arg }
238238
{ context: { l10n } }
239239
);
240240

241-
const { args } = format.getCall(0);
241+
const { args } = formatPattern.getCall(0);
242242
assert.deepEqual(args[1], { arg: 'ARG' });
243243
});
244244

245245
test('$arg is passed to format the attributes', function() {
246246
const bundle = new FluentBundle();
247-
const format = sinon.spy(bundle, 'format');
247+
const formatPattern = sinon.spy(bundle, 'formatPattern');
248248
const l10n = new ReactLocalization([bundle]);
249249

250250
bundle.addMessages(`
@@ -259,7 +259,7 @@ foo = { $arg }
259259
{ context: { l10n } }
260260
);
261261

262-
const { args } = format.getCall(0);
262+
const { args } = formatPattern.getCall(0);
263263
assert.deepEqual(args[1], { arg: 'ARG' });
264264
});
265265

@@ -406,6 +406,25 @@ foo = Test message
406406
assert.equal(wrapper.text(), 'String fallback');
407407
});
408408

409+
test('render with a string fallback and no message value preserves the fallback',
410+
function() {
411+
const mcx = new FluentBundle();
412+
const l10n = new ReactLocalization([mcx]);
413+
mcx.addMessages(`
414+
foo =
415+
.attr = Attribute
416+
`)
417+
418+
const wrapper = shallow(
419+
<Localized id="foo">
420+
String fallback
421+
</Localized>,
422+
{ context: { l10n } }
423+
);
424+
425+
assert.equal(wrapper.text(), 'String fallback');
426+
});
427+
409428
test('render with a string fallback returns the message', function() {
410429
const mcx = new FluentBundle();
411430
const l10n = new ReactLocalization([mcx]);
@@ -423,32 +442,51 @@ foo = Test message
423442
assert.equal(wrapper.text(), 'Test message');
424443
});
425444

426-
test('render without a fallback returns the message', function() {
445+
test('render without a fallback and no message returns nothing',
446+
function() {
447+
const mcx = new FluentBundle();
448+
const l10n = new ReactLocalization([mcx]);
449+
450+
const wrapper = shallow(
451+
<Localized id="foo" />,
452+
{ context: { l10n } }
453+
);
454+
455+
assert.equal(wrapper.text(), '');
456+
});
457+
458+
test('render without a fallback and no message value returns nothing',
459+
function() {
427460
const mcx = new FluentBundle();
428461
const l10n = new ReactLocalization([mcx]);
429462

430463
mcx.addMessages(`
431-
foo = Message
464+
foo =
465+
.attr = Attribute
432466
`)
433467

434468
const wrapper = shallow(
435469
<Localized id="foo" />,
436470
{ context: { l10n } }
437471
);
438472

439-
assert.equal(wrapper.text(), 'Message');
473+
assert.equal(wrapper.text(), '');
440474
});
441475

442-
test('render without a fallback and no message returns nothing',
443-
function() {
476+
test('render without a fallback returns the message', function() {
444477
const mcx = new FluentBundle();
445478
const l10n = new ReactLocalization([mcx]);
446479

480+
mcx.addMessages(`
481+
foo = Message
482+
`)
483+
447484
const wrapper = shallow(
448485
<Localized id="foo" />,
449486
{ context: { l10n } }
450487
);
451488

452-
assert.equal(wrapper.text(), '');
489+
assert.equal(wrapper.text(), 'Message');
453490
});
491+
454492
});

fluent/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ if (errors.length) {
3434

3535
const welcome = bundle.getMessage('welcome');
3636

37-
bundle.format(welcome, { name: 'Anna' });
38-
// → 'Welcome, Anna, to Foo 3000!'
37+
if (welcome.value) {
38+
bundle.formatPattern(welcome.value, { name: 'Anna' });
39+
// → 'Welcome, Anna, to Foo 3000!'
40+
}
3941
```
4042

4143
The API reference is available at http://projectfluent.org/fluent.js/fluent.

0 commit comments

Comments
 (0)