Skip to content

Commit b68d4d9

Browse files
committed
fluent-react: Report errors from Localized and getString
1 parent fb12ab6 commit b68d4d9

File tree

4 files changed

+82
-13
lines changed

4 files changed

+82
-13
lines changed

fluent-react/src/localization.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,25 @@ export default class ReactLocalization {
5858
if (bundle) {
5959
const msg = bundle.getMessage(id);
6060
if (msg && msg.value) {
61-
return bundle.formatPattern(msg.value, args);
61+
let errors = [];
62+
let value = bundle.formatPattern(msg.value, args, errors);
63+
for (let error of errors) {
64+
this.reportError(error);
65+
}
66+
return value;
6267
}
6368
}
6469

6570
return fallback || id;
6671
}
72+
73+
// XXX Control this via a prop passed to the LocalizationProvider.
74+
// See https://github.com/projectfluent/fluent.js/issues/411.
75+
reportError(error) {
76+
/* global console */
77+
// eslint-disable-next-line no-console
78+
console.warn(`[@fluent/react] ${error.name}: ${error.message}`);
79+
}
6780
}
6881

6982
export function isReactLocalization(props, propName) {

fluent-react/src/localized.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,30 +101,41 @@ export default class Localized extends Component {
101101

102102
const msg = bundle.getMessage(id);
103103
const [args, elems] = toArguments(this.props);
104+
let errors = [];
104105

105106
// Check if the child inside <Localized> is a valid element -- if not, then
106107
// it's either null or a simple fallback string. No need to localize the
107108
// attributes.
108109
if (!isValidElement(child)) {
109110
if (msg.value) {
110111
// Replace the fallback string with the message value;
111-
return bundle.formatPattern(msg.value, args);
112+
let value = bundle.formatPattern(msg.value, args, errors);
113+
for (let error of errors) {
114+
l10n.reportError(error);
115+
}
116+
return value;
112117
}
113118

114119
return child;
115120
}
116121

122+
let localizedProps;
123+
117124
// The default is to forbid all message attributes. If the attrs prop exists
118125
// on the Localized instance, only set message attributes which have been
119126
// explicitly allowed by the developer.
120127
if (attrs && msg.attributes) {
121-
var localizedProps = {};
128+
localizedProps = {};
129+
errors = [];
122130
for (const [name, allowed] of Object.entries(attrs)) {
123131
if (allowed && name in msg.attributes) {
124132
localizedProps[name] = bundle.formatPattern(
125-
msg.attributes[name], args);
133+
msg.attributes[name], args, errors);
126134
}
127135
}
136+
for (let error of errors) {
137+
l10n.reportError(error);
138+
}
128139
}
129140

130141
// If the wrapped component is a known void element, explicitly dismiss the
@@ -142,7 +153,11 @@ export default class Localized extends Component {
142153
return cloneElement(child, localizedProps);
143154
}
144155

145-
const messageValue = bundle.formatPattern(msg.value, args);
156+
errors = [];
157+
const messageValue = bundle.formatPattern(msg.value, args, errors);
158+
for (let error of errors) {
159+
l10n.reportError(error);
160+
}
146161

147162
// If the message value doesn't contain any markup nor any HTML entities,
148163
// insert it as the only child of the wrapped component.

fluent-react/test/localized_render_test.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ foo =
223223

224224

225225
test('$arg is passed to format the value', function() {
226-
const bundle = new FluentBundle();
226+
const bundle = new FluentBundle("en", {useIsolating: false});
227227
const formatPattern = sinon.spy(bundle, 'formatPattern');
228228
const l10n = new ReactLocalization([bundle]);
229229

@@ -240,6 +240,10 @@ foo = { $arg }
240240

241241
const { args } = formatPattern.getCall(0);
242242
assert.deepEqual(args[1], { arg: 'ARG' });
243+
244+
assert.ok(wrapper.contains(
245+
<div>ARG</div>
246+
));
243247
});
244248

245249
test('$arg is passed to format the attributes', function() {
@@ -249,18 +253,45 @@ foo = { $arg }
249253

250254
bundle.addResource(new FluentResource(`
251255
foo = { $arg }
252-
.attr = { $arg }
256+
.title = { $arg }
253257
`));
254258

255259
const wrapper = shallow(
256-
<Localized id="foo" $arg="ARG">
260+
<Localized id="foo" attrs={{title: true}} $arg="ARG">
257261
<div />
258262
</Localized>,
259263
{ context: { l10n } }
260264
);
261265

262-
const { args } = formatPattern.getCall(0);
263-
assert.deepEqual(args[1], { arg: 'ARG' });
266+
// The value.
267+
assert.deepEqual(formatPattern.getCall(0).args[1], { arg: 'ARG' });
268+
// The attribute.
269+
assert.deepEqual(formatPattern.getCall(1).args[1], { arg: 'ARG' });
270+
271+
assert.ok(wrapper.contains(
272+
<div title="ARG">ARG</div>
273+
));
274+
});
275+
276+
test('A missing $arg does not break rendering', function() {
277+
const bundle = new FluentBundle("en", {useIsolating: false});
278+
const l10n = new ReactLocalization([bundle]);
279+
280+
bundle.addResource(new FluentResource(`
281+
foo = { $arg }
282+
.title = { $arg }
283+
`));
284+
285+
const wrapper = shallow(
286+
<Localized id="foo" attrs={{title: true}}>
287+
<div />
288+
</Localized>,
289+
{ context: { l10n } }
290+
);
291+
292+
assert.ok(wrapper.contains(
293+
<div title="{$arg}">{"{$arg}"}</div>
294+
));
264295
});
265296

266297
test('render with a fragment and no message preserves the fragment',

fluent-react/test/with_localization_test.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ suite('withLocalization', function() {
3131
});
3232

3333
test('getString with access to the l10n context', function() {
34-
const bundle = new FluentBundle();
34+
const bundle = new FluentBundle("en", {useIsolating: false});
3535
const l10n = new ReactLocalization([bundle]);
3636
const EnhancedComponent = withLocalization(DummyComponent);
3737

3838
bundle.addResource(new FluentResource(`
3939
foo = FOO
40+
bar = BAR {$arg}
4041
`));
4142

4243
const wrapper = shallow(
@@ -47,15 +48,19 @@ foo = FOO
4748
const getString = wrapper.prop('getString');
4849
// Returns the translation.
4950
assert.strictEqual(getString('foo', {}), 'FOO');
51+
assert.strictEqual(getString('bar', {arg: 'ARG'}), 'BAR ARG');
52+
// Doesn't throw on formatting errors.
53+
assert.strictEqual(getString('bar', {}), 'BAR {$arg}');
5054
});
5155

5256
test('getString with access to the l10n context, with fallback value', function() {
53-
const bundle = new FluentBundle();
57+
const bundle = new FluentBundle("en", {useIsolating: false});
5458
const l10n = new ReactLocalization([bundle]);
5559
const EnhancedComponent = withLocalization(DummyComponent);
5660

5761
bundle.addResource(new FluentResource(`
5862
foo = FOO
63+
bar = BAR {$arg}
5964
`));
6065

6166
const wrapper = shallow(
@@ -65,7 +70,12 @@ foo = FOO
6570

6671
const getString = wrapper.prop('getString');
6772
// Returns the translation, even if fallback value provided.
68-
assert.strictEqual(getString('bar', {}, 'fallback'), 'fallback');
73+
assert.strictEqual(getString('foo', {}, 'fallback'), 'FOO');
74+
// Returns the fallback.
75+
assert.strictEqual(getString('missing', {}, 'fallback'), 'fallback');
76+
assert.strictEqual(getString('bar', {arg: 'ARG'}), 'BAR ARG');
77+
// Doesn't throw on formatting errors.
78+
assert.strictEqual(getString('bar', {}), 'BAR {$arg}');
6979
});
7080

7181
test('getString without access to the l10n context', function() {

0 commit comments

Comments
 (0)