Skip to content

Commit ed3ac64

Browse files
authored
fluent-react: Filter props with <Localized attrs={{…}}> (#141)
The <Localized> component now requires the `attrs` prop to set any localized attributes as props on the wrapped component. `attrs` should be an object with attribute names as keys and booleans as values. ```jsx <Localized id="type-name" attrs={{placeholder: true}}> <input type="text" placeholder="Localizable placeholder" value={name} onChange={…} /> </Localized> ``` By default, if `attrs` is not passed, no attributes will be set. This is a breaking change compared to the previous behavior: in `fluent-react` 0.4.1 and before `<Localized>` would set _all_ attributes found in the translation.
1 parent 6805811 commit ed3ac64

File tree

4 files changed

+207
-13
lines changed

4 files changed

+207
-13
lines changed

fluent-react/CHANGELOG.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,59 @@
5050
the element passed as a prop is cloned with the translated text content
5151
taken from the `DocumentFragment` used as `children`.
5252
53+
- Filter props set by translations with <Localized attrs={{…}}>.
54+
55+
The `<Localized>` component now requires the `attrs` prop to set any
56+
localized attributes as props on the wrapped component. `attrs` should be
57+
an object with attribute names as keys and booleans as values.
58+
59+
```jsx
60+
<Localized id="type-name" attrs={{placeholder: true}}>
61+
<input
62+
type="text"
63+
placeholder="Localizable placeholder"
64+
value={name}
65+
onChange={…}
66+
/>
67+
</Localized>
68+
```
69+
70+
By default, if `attrs` is not passed, no attributes will be set. This is
71+
a breaking change compared to the previous behavior: in `fluent-react`
72+
0.4.1 and before `<Localized>` would set _all_ attributes found in the
73+
translation.
5374
54-
#### Migrating from `fluent-react` 0.4.1
75+
#### Migrating from `fluent-react` 0.4.1 to 0.6.0
76+
77+
If you're setting localized attributes as props of elements wrapped in
78+
`<Localized>`, in `fluent-react` 0.6.0 you'll need to also explicitly allow
79+
the props you're interested in using the `attrs` prop. This protects your
80+
components from accidentally gaining props they aren't expecting or from
81+
translations overwriting important props which shouldn't change.
82+
83+
```jsx
84+
// BEFORE (fluent-react 0.4.1)
85+
<Localized id="type-name">
86+
<input
87+
type="text"
88+
placeholder="Localizable placeholder"
89+
value={name}
90+
onChange={…}
91+
/>
92+
</Localized>
93+
```
94+
95+
```jsx
96+
// AFTER (fluent-react 0.6.0)
97+
<Localized id="type-name" attrs={{placeholder: true}}>
98+
<input
99+
type="text"
100+
placeholder="Localizable placeholder"
101+
value={name}
102+
onChange={…}
103+
/>
104+
</Localized>
105+
```
55106

56107
In `fluent-react` 0.4.1 it was possible to pass React elements as _external
57108
arguments_ to localization via the `$`-prefixed props, just like you'd pass

fluent-react/examples/text-input/src/App.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ export default class App extends Component {
3232
</Localized>
3333
}
3434

35-
<Localized id="type-name">
35+
<Localized id="type-name" attrs={{placeholder: true}}>
3636
<input
3737
type="text"
38-
placeholder="Your name"
38+
placeholder="Type your name"
3939
onChange={evt => this.handleNameChange(evt.target.value)}
4040
value={name}
4141
/>

fluent-react/src/localized.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default class Localized extends Component {
7676

7777
render() {
7878
const { l10n } = this.context;
79-
const { id, children } = this.props;
79+
const { id, attrs, children } = this.props;
8080
const elem = Children.only(children);
8181

8282
if (!l10n) {
@@ -93,13 +93,29 @@ export default class Localized extends Component {
9393

9494
const msg = mcx.getMessage(id);
9595
const [args, elems] = toArguments(this.props);
96-
const { value, attrs } = l10n.formatCompound(mcx, msg, args);
96+
const {
97+
value: messageValue,
98+
attrs: messageAttrs
99+
} = l10n.formatCompound(mcx, msg, args);
100+
101+
// The default is to forbid all message attributes. If the attrs prop exists
102+
// on the Localized instance, only set message attributes which have been
103+
// explicitly allowed by the developer.
104+
if (attrs && messageAttrs) {
105+
var localizedProps = {};
106+
107+
for (const [name, value] of Object.entries(messageAttrs)) {
108+
if (attrs[name]) {
109+
localizedProps[name] = value;
110+
}
111+
}
112+
}
97113

98-
if (value === null || !value.includes('<')) {
99-
return cloneElement(elem, attrs, value);
114+
if (messageValue === null || !messageValue.includes('<')) {
115+
return cloneElement(elem, localizedProps, messageValue);
100116
}
101117

102-
const translationNodes = Array.from(parseMarkup(value).childNodes);
118+
const translationNodes = Array.from(parseMarkup(messageValue).childNodes);
103119
const translatedChildren = translationNodes.map(childNode => {
104120
if (childNode.nodeType === childNode.TEXT_NODE) {
105121
return childNode.textContent;
@@ -122,7 +138,7 @@ export default class Localized extends Component {
122138
);
123139
});
124140

125-
return cloneElement(elem, attrs, ...translatedChildren);
141+
return cloneElement(elem, localizedProps, ...translatedChildren);
126142
}
127143
}
128144

fluent-react/test/localized_render_test.js

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ReactLocalization from '../src/localization';
77
import { Localized } from '../src/index';
88

99
suite('Localized - rendering', function() {
10-
test('rendering the value', function() {
10+
test('render the value', function() {
1111
const mcx = new MessageContext();
1212
const l10n = new ReactLocalization([mcx]);
1313

@@ -27,7 +27,7 @@ foo = FOO
2727
));
2828
});
2929

30-
test('rendering the attributes', function() {
30+
test('render an allowed attribute', function() {
3131
const mcx = new MessageContext();
3232
const l10n = new ReactLocalization([mcx]);
3333

@@ -37,7 +37,7 @@ foo
3737
`)
3838

3939
const wrapper = shallow(
40-
<Localized id="foo">
40+
<Localized id="foo" attrs={{attr: true}}>
4141
<div />
4242
</Localized>,
4343
{ context: { l10n } }
@@ -48,7 +48,50 @@ foo
4848
));
4949
});
5050

51-
test('preserves existing attributes', function() {
51+
test('only render allowed attributes', function() {
52+
const mcx = new MessageContext();
53+
const l10n = new ReactLocalization([mcx]);
54+
55+
mcx.addMessages(`
56+
foo
57+
.attr1 = ATTR 1
58+
.attr2 = ATTR 2
59+
`)
60+
61+
const wrapper = shallow(
62+
<Localized id="foo" attrs={{attr2: true}}>
63+
<div />
64+
</Localized>,
65+
{ context: { l10n } }
66+
);
67+
68+
assert.ok(wrapper.contains(
69+
<div attr2="ATTR 2" />
70+
));
71+
});
72+
73+
test('filter out forbidden attributes', function() {
74+
const mcx = new MessageContext();
75+
const l10n = new ReactLocalization([mcx]);
76+
77+
mcx.addMessages(`
78+
foo
79+
.attr = ATTR
80+
`)
81+
82+
const wrapper = shallow(
83+
<Localized id="foo" attrs={{attr: false}}>
84+
<div />
85+
</Localized>,
86+
{ context: { l10n } }
87+
);
88+
89+
assert.ok(wrapper.contains(
90+
<div />
91+
));
92+
});
93+
94+
test('filter all attributes if attrs not given', function() {
5295
const mcx = new MessageContext();
5396
const l10n = new ReactLocalization([mcx]);
5497

@@ -59,6 +102,27 @@ foo
59102

60103
const wrapper = shallow(
61104
<Localized id="foo">
105+
<div />
106+
</Localized>,
107+
{ context: { l10n } }
108+
);
109+
110+
assert.ok(wrapper.contains(
111+
<div />
112+
));
113+
});
114+
115+
test('preserve existing attributes when setting new ones', function() {
116+
const mcx = new MessageContext();
117+
const l10n = new ReactLocalization([mcx]);
118+
119+
mcx.addMessages(`
120+
foo
121+
.attr = ATTR
122+
`)
123+
124+
const wrapper = shallow(
125+
<Localized id="foo" attrs={{attr: true}}>
62126
<div existing={true} />
63127
</Localized>,
64128
{ context: { l10n } }
@@ -69,6 +133,69 @@ foo
69133
));
70134
});
71135

136+
test('overwrite existing attributes if allowed', function() {
137+
const mcx = new MessageContext();
138+
const l10n = new ReactLocalization([mcx]);
139+
140+
mcx.addMessages(`
141+
foo
142+
.existing = ATTR
143+
`)
144+
145+
const wrapper = shallow(
146+
<Localized id="foo" attrs={{existing: true}}>
147+
<div existing={true} />
148+
</Localized>,
149+
{ context: { l10n } }
150+
);
151+
152+
assert.ok(wrapper.contains(
153+
<div existing="ATTR" />
154+
));
155+
});
156+
157+
test('protect existing attributes if setting is forbidden', function() {
158+
const mcx = new MessageContext();
159+
const l10n = new ReactLocalization([mcx]);
160+
161+
mcx.addMessages(`
162+
foo
163+
.existing = ATTR
164+
`)
165+
166+
const wrapper = shallow(
167+
<Localized id="foo" attrs={{existing: false}}>
168+
<div existing={true} />
169+
</Localized>,
170+
{ context: { l10n } }
171+
);
172+
173+
assert.ok(wrapper.contains(
174+
<div existing={true} />
175+
));
176+
});
177+
178+
test('protect existing attributes by default', function() {
179+
const mcx = new MessageContext();
180+
const l10n = new ReactLocalization([mcx]);
181+
182+
mcx.addMessages(`
183+
foo
184+
.existing = ATTR
185+
`)
186+
187+
const wrapper = shallow(
188+
<Localized id="foo">
189+
<div existing={true} />
190+
</Localized>,
191+
{ context: { l10n } }
192+
);
193+
194+
assert.ok(wrapper.contains(
195+
<div existing={true} />
196+
));
197+
});
198+
72199
test('$arg is passed to format the value', function() {
73200
const mcx = new MessageContext();
74201
const format = sinon.spy(mcx, 'format');

0 commit comments

Comments
 (0)