7
7
import * as p from 'path' ;
8
8
import { writeFileSync } from 'fs' ;
9
9
import { sync as mkdirpSync } from 'mkdirp' ;
10
+ import printICUMessage from './print-icu-message' ;
10
11
11
12
const COMPONENT_NAMES = [
12
13
'FormattedMessage' ,
13
14
'FormattedHTMLMessage' ,
14
15
] ;
15
16
16
17
const FUNCTION_NAMES = [
17
- 'defineMessage ' ,
18
+ 'defineMessages ' ,
18
19
] ;
19
20
20
21
const IMPORTED_NAMES = new Set ( [ ...COMPONENT_NAMES , ...FUNCTION_NAMES ] ) ;
21
22
const DESCRIPTOR_PROPS = new Set ( [ 'id' , 'description' , 'defaultMessage' ] ) ;
22
23
23
- export default function ( { Plugin} ) {
24
- function getModuleSourceName ( options ) {
25
- const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
26
- return reactIntlOptions . moduleSourceName || 'react-intl' ;
27
- }
28
-
29
- function getMessagesDir ( options ) {
30
- const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
31
- return reactIntlOptions . messagesDir ;
24
+ export default function ( { Plugin, types : t } ) {
25
+ function getReactIntlOptions ( options ) {
26
+ return options . extra [ 'react-intl' ] || { } ;
32
27
}
33
28
34
- function getMessageDescriptor ( propertiesMap ) {
35
- // Force property order on descriptors.
36
- let descriptor = [ ...DESCRIPTOR_PROPS ] . reduce ( ( descriptor , key ) => {
37
- descriptor [ key ] = undefined ;
38
- return descriptor ;
39
- } , { } ) ;
40
-
41
- for ( let [ key , value ] of propertiesMap ) {
42
- key = getMessageDescriptorKey ( key ) ;
43
-
44
- if ( DESCRIPTOR_PROPS . has ( key ) ) {
45
- // TODO: Should this be trimming values?
46
- descriptor [ key ] = getMessageDescriptorValue ( value ) . trim ( ) ;
47
- }
48
- }
49
-
50
- return descriptor ;
29
+ function getModuleSourceName ( options ) {
30
+ return getReactIntlOptions ( options ) . moduleSourceName || 'react-intl' ;
51
31
}
52
32
53
33
function getMessageDescriptorKey ( path ) {
@@ -84,9 +64,38 @@ export default function ({Plugin}) {
84
64
) ;
85
65
}
86
66
87
- function storeMessage ( descriptor , node , file ) {
88
- const { id} = descriptor ;
89
- const { messages} = file . get ( 'react-intl' ) ;
67
+ function createMessageDescriptor ( propPaths ) {
68
+ return propPaths . reduce ( ( hash , [ keyPath , valuePath ] ) => {
69
+ let key = getMessageDescriptorKey ( keyPath ) ;
70
+
71
+ if ( DESCRIPTOR_PROPS . has ( key ) ) {
72
+ let value = getMessageDescriptorValue ( valuePath ) . trim ( ) ;
73
+
74
+ if ( key === 'defaultMessage' ) {
75
+ try {
76
+ hash [ key ] = printICUMessage ( value ) ;
77
+ } catch ( e ) {
78
+ throw valuePath . errorWithNode (
79
+ `[React Intl] Message failed to parse: ${ e } ` +
80
+ 'See: http://formatjs.io/guides/message-syntax/'
81
+ ) ;
82
+ }
83
+ } else {
84
+ hash [ key ] = value ;
85
+ }
86
+ }
87
+
88
+ return hash ;
89
+ } , { } ) ;
90
+ }
91
+
92
+ function createPropNode ( key , value ) {
93
+ return t . property ( 'init' , t . literal ( key ) , t . literal ( value ) ) ;
94
+ }
95
+
96
+ function storeMessage ( { id, description, defaultMessage} , node , file ) {
97
+ const { enforceDescriptions} = getReactIntlOptions ( file . opts ) ;
98
+ const { messages} = file . get ( 'react-intl' ) ;
90
99
91
100
if ( ! id ) {
92
101
throw file . errorWithNode ( node ,
@@ -95,22 +104,35 @@ export default function ({Plugin}) {
95
104
}
96
105
97
106
if ( messages . has ( id ) ) {
98
- throw file . errorWithNode ( node ,
99
- `[React Intl] Duplicate message id: "${ id } "`
100
- ) ;
107
+ let existing = messages . get ( id ) ;
108
+
109
+ if ( description !== existing . description ||
110
+ defaultMessage !== existing . defaultMessage ) {
111
+
112
+ throw file . errorWithNode ( node ,
113
+ `[React Intl] Duplicate message id: "${ id } ", ` +
114
+ 'but the `description` and/or `defaultMessage` are different.'
115
+ ) ;
116
+ }
101
117
}
102
118
103
- if ( ! descriptor . defaultMessage ) {
119
+ if ( ! defaultMessage ) {
104
120
let { loc} = node ;
105
121
file . log . warn (
106
122
`[React Intl] Line ${ loc . start . line } : ` +
107
- `Message "${ id } " is missing a \`defaultMessage\` ` +
108
- `and will not be extracted.`
123
+ 'Message is missing a `defaultMessage` and will not be extracted.'
109
124
) ;
125
+
110
126
return ;
111
127
}
112
128
113
- messages . set ( id , descriptor ) ;
129
+ if ( enforceDescriptions && ! description ) {
130
+ throw file . errorWithNode ( node ,
131
+ '[React Intl] Message must have a `description`.'
132
+ ) ;
133
+ }
134
+
135
+ messages . set ( id , { id, description, defaultMessage} ) ;
114
136
}
115
137
116
138
function referencesImport ( path , mod , importedNames ) {
@@ -147,7 +169,7 @@ export default function ({Plugin}) {
147
169
148
170
exit ( node , parent , scope , file ) {
149
171
const { messages} = file . get ( 'react-intl' ) ;
150
- const messagesDir = getMessagesDir ( file . opts ) ;
172
+ const { messagesDir} = getReactIntlOptions ( file . opts ) ;
151
173
const { basename, filename} = file . opts ;
152
174
153
175
let descriptors = [ ...messages . values ( ) ] ;
@@ -170,46 +192,79 @@ export default function ({Plugin}) {
170
192
171
193
JSXOpeningElement ( node , parent , scope , file ) {
172
194
const moduleSourceName = getModuleSourceName ( file . opts ) ;
195
+
173
196
let name = this . get ( 'name' ) ;
174
197
175
198
if ( referencesImport ( name , moduleSourceName , COMPONENT_NAMES ) ) {
176
199
let attributes = this . get ( 'attributes' )
177
- . filter ( ( attr ) => attr . isJSXAttribute ( ) )
178
- . map ( ( attr ) => [ attr . get ( 'name' ) , attr . get ( 'value' ) ] ) ;
200
+ . filter ( ( attr ) => attr . isJSXAttribute ( ) ) ;
179
201
180
- let descriptor = getMessageDescriptor ( new Map ( attributes ) ) ;
202
+ let descriptor = createMessageDescriptor (
203
+ attributes . map ( ( attr ) => [
204
+ attr . get ( 'name' ) ,
205
+ attr . get ( 'value' ) ,
206
+ ] )
207
+ ) ;
181
208
182
209
// In order for a default message to be extracted when
183
210
// declaring a JSX element, it must be done with standard
184
211
// `key=value` attributes. But it's completely valid to
185
212
// write `<FormattedMessage {...descriptor} />`, because it
186
- // will be skipped here and extracted elsewhere.
187
- if ( descriptor . id ) {
213
+ // will be skipped here and extracted elsewhere. When
214
+ // _either_ an `id` or `defaultMessage` prop exists, the
215
+ // descriptor will be checked; this way mixing an object
216
+ // spread with props will fail.
217
+ if ( descriptor . id || descriptor . defaultMessage ) {
188
218
storeMessage ( descriptor , node , file ) ;
219
+
220
+ attributes
221
+ . filter ( ( attr ) => {
222
+ let keyPath = attr . get ( 'name' ) ;
223
+ let key = getMessageDescriptorKey ( keyPath ) ;
224
+ return key === 'description' ;
225
+ } )
226
+ . forEach ( ( attr ) => attr . dangerouslyRemove ( ) ) ;
189
227
}
190
228
}
191
229
} ,
192
230
193
231
CallExpression ( node , parent , scope , file ) {
194
232
const moduleSourceName = getModuleSourceName ( file . opts ) ;
195
233
196
- let callee = this . get ( 'callee' ) ;
197
-
198
- if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) ) {
199
- let messageArg = this . get ( 'arguments' ) [ 0 ] ;
200
- if ( ! ( messageArg && messageArg . isObjectExpression ( ) ) ) {
234
+ function processMessageObject ( messageObj ) {
235
+ if ( ! ( messageObj && messageObj . isObjectExpression ( ) ) ) {
201
236
throw file . errorWithNode ( node ,
202
237
`[React Intl] \`${ callee . node . name } ()\` must be ` +
203
238
`called with message descriptor defined via an ` +
204
239
`object expression.`
205
240
) ;
206
241
}
207
242
208
- let properties = messageArg . get ( 'properties' )
209
- . map ( ( prop ) => [ prop . get ( 'key' ) , prop . get ( 'value' ) ] ) ;
243
+ let properties = messageObj . get ( 'properties' ) ;
244
+
245
+ let descriptor = createMessageDescriptor (
246
+ properties . map ( ( prop ) => [
247
+ prop . get ( 'key' ) ,
248
+ prop . get ( 'value' ) ,
249
+ ] )
250
+ ) ;
210
251
211
- let descriptor = getMessageDescriptor ( new Map ( properties ) ) ;
212
252
storeMessage ( descriptor , node , file ) ;
253
+
254
+ messageObj . replaceWith ( t . objectExpression ( [
255
+ createPropNode ( 'id' , descriptor . id ) ,
256
+ createPropNode ( 'defaultMessage' , descriptor . defaultMessage ) ,
257
+ ] ) ) ;
258
+ }
259
+
260
+ let callee = this . get ( 'callee' ) ;
261
+
262
+ if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) ) {
263
+ let firstArg = this . get ( 'arguments' ) [ 0 ] ;
264
+
265
+ firstArg . get ( 'properties' )
266
+ . map ( ( prop ) => prop . get ( 'value' ) )
267
+ . forEach ( processMessageObject ) ;
213
268
}
214
269
} ,
215
270
} ,
0 commit comments