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" ;
10
2
import PropTypes from "prop-types" ;
11
- import voidElementTags from "../vendor/voidElementTags" ;
12
- import { FluentContext } from "./context" ;
13
3
import { FluentVariable } from "@fluent/bundle" ;
14
-
15
- // Match the opening angle bracket (<) in HTML tags, and HTML entities like
16
- // &, &, &.
17
- const reMarkup = / < | & # ? \w + ; / ;
4
+ import { LocalizedElement } from "./localized_element" ;
5
+ import { LocalizedText } from "./localized_text" ;
18
6
19
7
export interface LocalizedProps {
20
8
id : string ;
@@ -24,171 +12,21 @@ export interface LocalizedProps {
24
12
elems ?: Record < string , ReactElement > ;
25
13
}
26
14
/*
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.
47
17
*/
48
18
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 ) ;
56
23
}
57
24
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 ) ;
188
28
}
189
29
190
- export default Localized ;
191
-
192
30
Localized . propTypes = {
193
31
children : PropTypes . node
194
32
} ;
0 commit comments