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" ;
102import PropTypes from "prop-types" ;
11- import voidElementTags from "../vendor/voidElementTags" ;
12- import { FluentContext } from "./context" ;
133import { 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" ;
186
197export 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 */
4818export 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-
19230Localized . propTypes = {
19331 children : PropTypes . node
19432} ;
0 commit comments