Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ef51838

Browse files
committed
Split Localized into LocalizedElement and LocalizedText
1 parent 36d09ae commit ef51838

File tree

4 files changed

+263
-174
lines changed

4 files changed

+263
-174
lines changed

‎fluent-react/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ export { ReactLocalization} from "./localization";
2121
export { LocalizationProvider } from "./provider";
2222
export { withLocalization, WithLocalizationProps } from "./with_localization";
2323
export { Localized, LocalizedProps } from "./localized";
24+
export { LocalizedElement, LocalizedElementProps } from "./localized_element";
25+
export { LocalizedText, LocalizedTextProps } from "./localized_text";
2426
export { MarkupParser } from "./markup";
2527
export { useLocalization } from "./use_localization";

‎fluent-react/src/localized.ts

Lines changed: 12 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
1-
import {
2-
Fragment,
3-
ReactElement,
4-
ReactNode,
5-
cloneElement,
6-
createElement,
7-
isValidElement,
8-
useContext
9-
} from "react";
1+
import { ReactElement, ReactNode, createElement } from "react";
102
import PropTypes from "prop-types";
11-
import voidElementTags from "../vendor/voidElementTags";
12-
import { FluentContext } from "./context";
133
import { FluentVariable } from "@fluent/bundle";
14-
15-
// Match the opening angle bracket (<) in HTML tags, and HTML entities like
16-
// &amp;, &#0038;, &#x0026;.
17-
const reMarkup = /<|&#?\w+;/;
4+
import { LocalizedElement } from "./localized_element";
5+
import { LocalizedText } from "./localized_text";
186

197
export interface LocalizedProps {
208
id: string;
@@ -24,171 +12,21 @@ export interface LocalizedProps {
2412
elems?: Record<string, ReactElement>;
2513
}
2614
/*
27-
* The `Localized` class renders its child with translated props and children.
28-
*
29-
* <Localized id="hello-world">
30-
* <p>{'Hello, world!'}</p>
31-
* </Localized>
32-
*
33-
* The `id` prop should be the unique identifier of the translation. Any
34-
* attributes found in the translation will be applied to the wrapped element.
35-
*
36-
* Arguments to the translation can be passed as `$`-prefixed props on
37-
* `Localized`.
38-
*
39-
* <Localized id="hello-world" $username={name}>
40-
* <p>{'Hello, { $username }!'}</p>
41-
* </Localized>
42-
*
43-
* It's recommended that the contents of the wrapped component be a string
44-
* expression. The string will be used as the ultimate fallback if no
45-
* translation is available. It also makes it easy to grep for strings in the
46-
* source code.
15+
* The `Localized` component redirects to `LocalizedElement` or
16+
* `LocalizedText`, depending on props.children.
4717
*/
4818
export function Localized(props: LocalizedProps): ReactElement {
49-
const { id, attrs, vars, elems, children: child = null } = props;
50-
const l10n = useContext(FluentContext);
51-
52-
// Validate that the child element isn't an array
53-
if (Array.isArray(child)) {
54-
throw new Error("<Localized/> expected to receive a single " +
55-
"React node child");
19+
if (!props.children || typeof props.children === "string") {
20+
// Redirect to LocalizedText for string children: <Localized>Fallback
21+
// copy</Localized>, and empty calls: <Localized />.
22+
return createElement(LocalizedText, props);
5623
}
5724

58-
if (!l10n) {
59-
// Use the wrapped component as fallback.
60-
return createElement(Fragment, null, child);
61-
}
62-
63-
const bundle = l10n.getBundle(id);
64-
65-
if (bundle === null) {
66-
// Use the wrapped component as fallback.
67-
return createElement(Fragment, null, child);
68-
}
69-
70-
// l10n.getBundle makes the bundle.hasMessage check which ensures that
71-
// bundle.getMessage returns an existing message.
72-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
73-
const msg = bundle.getMessage(id)!;
74-
let errors: Array<Error> = [];
75-
76-
// Check if the child inside <Localized> is a valid element -- if not, then
77-
// it's either null or a simple fallback string. No need to localize the
78-
// attributes.
79-
if (!isValidElement(child)) {
80-
if (msg.value) {
81-
// Replace the fallback string with the message value;
82-
let value = bundle.formatPattern(msg.value, vars, errors);
83-
for (let error of errors) {
84-
l10n.reportError(error);
85-
}
86-
return createElement(Fragment, null, value);
87-
}
88-
89-
return createElement(Fragment, null, child);
90-
}
91-
92-
let localizedProps: Record<string, string> | undefined;
93-
94-
// The default is to forbid all message attributes. If the attrs prop exists
95-
// on the Localized instance, only set message attributes which have been
96-
// explicitly allowed by the developer.
97-
if (attrs && msg.attributes) {
98-
localizedProps = {};
99-
errors = [];
100-
for (const [name, allowed] of Object.entries(attrs)) {
101-
if (allowed && name in msg.attributes) {
102-
localizedProps[name] = bundle.formatPattern(
103-
msg.attributes[name], vars, errors);
104-
}
105-
}
106-
for (let error of errors) {
107-
l10n.reportError(error);
108-
}
109-
}
110-
111-
// If the wrapped component is a known void element, explicitly dismiss the
112-
// message value and do not pass it to cloneElement in order to avoid the
113-
// "void element tags must neither have `children` nor use
114-
// `dangerouslySetInnerHTML`" error.
115-
if (child.type in voidElementTags) {
116-
return cloneElement(child, localizedProps);
117-
}
118-
119-
// If the message has a null value, we're only interested in its attributes.
120-
// Do not pass the null value to cloneElement as it would nuke all children
121-
// of the wrapped component.
122-
if (msg.value === null) {
123-
return cloneElement(child, localizedProps);
124-
}
125-
126-
errors = [];
127-
const messageValue = bundle.formatPattern(msg.value, vars, errors);
128-
for (let error of errors) {
129-
l10n.reportError(error);
130-
}
131-
132-
// If the message value doesn't contain any markup nor any HTML entities,
133-
// insert it as the only child of the wrapped component.
134-
if (!reMarkup.test(messageValue) || l10n.parseMarkup === null) {
135-
return cloneElement(child, localizedProps, messageValue);
136-
}
137-
138-
let elemsLower: Record<string, ReactElement>;
139-
if (elems) {
140-
elemsLower = {};
141-
for (let [name, elem] of Object.entries(elems)) {
142-
elemsLower[name.toLowerCase()] = elem;
143-
}
144-
}
145-
146-
147-
// If the message contains markup, parse it and try to match the children
148-
// found in the translation with the props passed to this Localized.
149-
const translationNodes = l10n.parseMarkup(messageValue);
150-
const translatedChildren = translationNodes.map(childNode => {
151-
if (childNode.nodeName === "#text") {
152-
return childNode.textContent;
153-
}
154-
155-
const childName = childNode.nodeName.toLowerCase();
156-
157-
// If the child is not expected just take its textContent.
158-
if (
159-
!elemsLower ||
160-
!Object.prototype.hasOwnProperty.call(elemsLower, childName)
161-
) {
162-
return childNode.textContent;
163-
}
164-
165-
const sourceChild = elemsLower[childName];
166-
167-
// Ignore elems which are not valid React elements.
168-
if (!isValidElement(sourceChild)) {
169-
return childNode.textContent;
170-
}
171-
172-
// If the element passed in the elems prop is a known void element,
173-
// explicitly dismiss any textContent which might have accidentally been
174-
// defined in the translation to prevent the "void element tags must not
175-
// have children" error.
176-
if (sourceChild.type in voidElementTags) {
177-
return sourceChild;
178-
}
179-
180-
// TODO Protect contents of elements wrapped in <Localized>
181-
// https://github.com/projectfluent/fluent.js/issues/184
182-
// TODO Control localizable attributes on elements passed as props
183-
// https://github.com/projectfluent/fluent.js/issues/185
184-
return cloneElement(sourceChild, undefined, childNode.textContent);
185-
});
186-
187-
return cloneElement(child, localizedProps, ...translatedChildren);
25+
// Redirect to LocalizedElement for element children. Only a single element
26+
// child is supported; LocalizedElement enforces this requirement.
27+
return createElement(LocalizedElement, props);
18828
}
18929

190-
export default Localized;
191-
19230
Localized.propTypes = {
19331
children: PropTypes.node
19432
};

‎fluent-react/src/localized_element.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import {
2+
Fragment,
3+
ReactElement,
4+
ReactNode,
5+
cloneElement,
6+
createElement,
7+
isValidElement,
8+
useContext
9+
} from "react";
10+
import PropTypes from "prop-types";
11+
import voidElementTags from "../vendor/voidElementTags";
12+
import { FluentContext } from "./context";
13+
import { FluentVariable } from "@fluent/bundle";
14+
15+
// Match the opening angle bracket (<) in HTML tags, and HTML entities like
16+
// &amp;, &#0038;, &#x0026;.
17+
const reMarkup = /<|&#?\w+;/;
18+
19+
export interface LocalizedElementProps {
20+
id: string;
21+
attrs?: Record<string, boolean>;
22+
children?: ReactNode;
23+
vars?: Record<string, FluentVariable>;
24+
elems?: Record<string, ReactElement>;
25+
}
26+
/*
27+
* The `LocalizedElement` component renders its child with translated contents
28+
* and props.
29+
*
30+
* <Localized id="hello-world">
31+
* <p>Hello, world!</p>
32+
* </Localized>
33+
*
34+
* Arguments to the translation can be passed as an object in the `vars` prop.
35+
*
36+
* <LocalizedElement id="hello-world" vars={{userName: name}}>
37+
* <p>{'Hello, {$userName}!'}</p>
38+
* </LocalizedElement>
39+
*
40+
* The props of the wrapped child can be localized using Fluent attributes
41+
* found on the requested message, provided they are explicitly allowed by the
42+
* `attrs` prop.
43+
*
44+
* <LocalizedElement id="hello-world" attrs={{title: true}}>
45+
* <p>Hello, world!</p>
46+
* </LocalizedElement>
47+
*/
48+
export function LocalizedElement(props: LocalizedElementProps): ReactElement {
49+
const { id, attrs, vars, elems, children: child = null } = props;
50+
51+
// Check if the child inside <LocalizedElement> is a valid element.
52+
if (!isValidElement(child)) {
53+
throw new Error("<LocalizedElement/> expected to receive a single " +
54+
"React element child");
55+
}
56+
57+
const l10n = useContext(FluentContext);
58+
if (!l10n) {
59+
// Use the wrapped component as fallback.
60+
return createElement(Fragment, null, child);
61+
}
62+
63+
const bundle = l10n.getBundle(id);
64+
if (bundle === null) {
65+
// Use the wrapped component as fallback.
66+
return createElement(Fragment, null, child);
67+
}
68+
69+
let errors: Array<Error> = [];
70+
71+
// l10n.getBundle makes the bundle.hasMessage check which ensures that
72+
// bundle.getMessage returns an existing message.
73+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
74+
const msg = bundle.getMessage(id)!;
75+
76+
let localizedProps: Record<string, string> | undefined;
77+
78+
// The default is to forbid all message attributes. If the attrs prop exists
79+
// on the Localized instance, only set message attributes which have been
80+
// explicitly allowed by the developer.
81+
if (attrs && msg.attributes) {
82+
localizedProps = {};
83+
errors = [];
84+
for (const [name, allowed] of Object.entries(attrs)) {
85+
if (allowed && name in msg.attributes) {
86+
localizedProps[name] = bundle.formatPattern(
87+
msg.attributes[name], vars, errors);
88+
}
89+
}
90+
for (let error of errors) {
91+
l10n.reportError(error);
92+
}
93+
}
94+
95+
// If the wrapped component is a known void element, explicitly dismiss the
96+
// message value and do not pass it to cloneElement in order to avoid the
97+
// "void element tags must neither have `children` nor use
98+
// `dangerouslySetInnerHTML`" error.
99+
if (child.type in voidElementTags) {
100+
return cloneElement(child, localizedProps);
101+
}
102+
103+
// If the message has a null value, we're only interested in its attributes.
104+
// Do not pass the null value to cloneElement as it would nuke all children
105+
// of the wrapped component.
106+
if (msg.value === null) {
107+
return cloneElement(child, localizedProps);
108+
}
109+
110+
errors = [];
111+
const messageValue = bundle.formatPattern(msg.value, vars, errors);
112+
for (let error of errors) {
113+
l10n.reportError(error);
114+
}
115+
116+
// If the message value doesn't contain any markup nor any HTML entities,
117+
// insert it as the only child of the wrapped component.
118+
if (!reMarkup.test(messageValue) || l10n.parseMarkup === null) {
119+
return cloneElement(child, localizedProps, messageValue);
120+
}
121+
122+
let elemsLower: Record<string, ReactElement>;
123+
if (elems) {
124+
elemsLower = {};
125+
for (let [name, elem] of Object.entries(elems)) {
126+
elemsLower[name.toLowerCase()] = elem;
127+
}
128+
}
129+
130+
131+
// If the message contains markup, parse it and try to match the children
132+
// found in the translation with the props passed to this Localized.
133+
const translationNodes = l10n.parseMarkup(messageValue);
134+
const translatedChildren = translationNodes.map(childNode => {
135+
if (childNode.nodeName === "#text") {
136+
return childNode.textContent;
137+
}
138+
139+
const childName = childNode.nodeName.toLowerCase();
140+
141+
// If the child is not expected just take its textContent.
142+
if (
143+
!elemsLower ||
144+
!Object.prototype.hasOwnProperty.call(elemsLower, childName)
145+
) {
146+
return childNode.textContent;
147+
}
148+
149+
const sourceChild = elemsLower[childName];
150+
151+
// Ignore elems which are not valid React elements.
152+
if (!isValidElement(sourceChild)) {
153+
return childNode.textContent;
154+
}
155+
156+
// If the element passed in the elems prop is a known void element,
157+
// explicitly dismiss any textContent which might have accidentally been
158+
// defined in the translation to prevent the "void element tags must not
159+
// have children" error.
160+
if (sourceChild.type in voidElementTags) {
161+
return sourceChild;
162+
}
163+
164+
// TODO Protect contents of elements wrapped in <Localized>
165+
// https://github.com/projectfluent/fluent.js/issues/184
166+
// TODO Control localizable attributes on elements passed as props
167+
// https://github.com/projectfluent/fluent.js/issues/185
168+
return cloneElement(sourceChild, undefined, childNode.textContent);
169+
});
170+
171+
return cloneElement(child, localizedProps, ...translatedChildren);
172+
}
173+
174+
LocalizedElement.propTypes = {
175+
children: PropTypes.element
176+
};

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /