forked from Automattic/xgettext-js
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathxgettext.js
280 lines (242 loc) · 8.51 KB
/
xgettext.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
var _ = require( 'lodash' ),
parser = require( 'hermes-parser' ),
walk = require( 'estree-walker' ).walk;
/**
* XGettext will parse a given input string for any instances of i18n function
* calls, returning an array of objects for all translatable strings
* discovered.
*
* @param {Object} options Options to use when p.arsing the input. Refer to
* XGettext.defaultOptions for available options and a
* description for each
*/
var XGettext = module.exports = function( options ) {
if ( 'object' !== typeof options ) {
options = {};
}
this.options = _.extend( {}, XGettext.defaultOptions, options );
this.options.keywords = this._normalizeKeywords( this.options.keywords );
this.options.keywordFunctions = Object.keys( this.options.keywords );
this.options.parseOptions = _.extend( { locations: true }, this.options.parseOptions );
};
XGettext.defaultOptions = {
/**
* A key-value pair of keyword function names to be mapped into their
* desired string value. Transform functions are passed a match including
* three keys: `keyword` (the matched keyword), `arguments` (a
* CallExpression arguments array), and `comment` if one exists. It is
* expected that this function will return a string or an array of strings.
* Alternatively, define value as number to return value in that argument
* position on a 1-based index.
*
* @type {Object}
* @see https://github.com/babel/babylon/blob/master/ast/spec.md
*/
keywords: {
_: 1,
},
/**
* Optionally match translator comments to be included with translatable
* strings.
*
* If undesired, set as `undefined`. A comment will be matched if it is
* prefixed by this option and occurs either on the same or previous line
* as the matched keyword.
*
* @type {String,undefined}
*/
commentPrefix: 'translators:',
/**
* Options for the parser. Babylon has some extra ones.
*
* @type {Object}
* @see https://www.npmjs.com/package/@babel/parser
*/
parseOptions: {},
};
/**
* Returns an array of objects for all strings matched by the keywords defined
* in the `keyword` option property.
*
* Each object in the array contains a `string` key where the value is
* determined by the corresponding keyword mapping function. An object may also
* contain a `comment` key if the `commentPrefix` option is provided and a
* comment is associated with the matched keyword.
*
* @param {String} input String from which to find matches
* @return {Array} An array containing objects for each matched
* occurrance of a keyword function
*/
XGettext.prototype.getMatches = function( input ) {
var parsedInput, matches, transformedMatches;
// Parse input as AST and matching comments
parsedInput = this._parseInput( input );
// Find matches (i.e. where keyword functions are used)
matches = this._discoverMatches( parsedInput );
// Use configured keyword transforms to parse string value
transformedMatches = _( matches ).sortBy( [ 'line', 'column' ] ).map( function( match ) {
return this._transformMatch( match );
}.bind( this ) ).flatten().value();
return transformedMatches;
};
/**
* Returns an object containing keyword functions where number values are
* replaced with a function returning the nth argument on a 1-based index.
*
* @private
* @see https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html
*
* @param {Object} keywords Original keywords object configuration
* @return {Object} An object containing keyword functions where
* number values are replaced with a function
* returning the nth argument on a 1-based index
*/
XGettext.prototype._normalizeKeywords = function( keywords ) {
var normalizedKeywords = {};
for ( var fn in keywords ) {
normalizedKeywords[ fn ] = this._normalizeKeyword( keywords[ fn ] );
}
return normalizedKeywords;
};
/**
* If passed a number, returns a function which returns the nth argument on a
* 1-based index. Otherwise, returns the passed argument.
*
* @param {(Number|Function)} keyword A number or function to be normalized
* @return {Function} A function to be used in place of the
* passed argument
*/
XGettext.prototype._normalizeKeyword = function( keyword ) {
if ( 'number' === typeof keyword ) {
return ( function( argnum ) {
var argumentPosition = argnum - 1;
return function( match ) {
if ( match.arguments.length > argumentPosition &&
typeof match.arguments[ argumentPosition ].value === 'string' ) {
return match.arguments[ argumentPosition ].value;
}
};
}( keyword ) );
}
return keyword;
};
/**
* Returns an object containing as AST representation of the input (as `ast`)
* and any matching comments discovered during parsing (as `comments`)
*
* @private
* @param {String} input String from which to find matches
* @return {Array} An object containing as AST representation of the
* input (as `ast`) and any matching comments discovered
* during parsing (as `comments`)
*/
XGettext.prototype._parseInput = function( input ) {
var comments = [],
parseOptions = this.options.parseOptions,
ast;
ast = parser.parse( input, parseOptions );
if ( typeof this.options.commentPrefix !== 'undefined' ) {
// Optionally locate translator comments
var rxCommentMatch = new RegExp( '^\\s*' + this.options.commentPrefix, 'i' );
ast.comments.forEach( function( comment ) {
var text = comment.value;
var isTranslatorComment = rxCommentMatch.test( text );
if ( isTranslatorComment ) {
comments.push( {
value: text.replace( rxCommentMatch, '' ).trim(),
line: comment.loc.start.line,
} );
}
} );
}
return {
comments: comments,
ast: ast,
};
};
/**
* Returns an array of objects representing all matched keywords, including the
* matched keyword (as `keyword`), the CallExpression arguments array (as
* `arguments`), and potentially any comment associated with the match (as
* `comment`)
*
* @private
*
* @param {Object} parsedInput Parse results
* @return {Array} An array of objects representing all matched
* keywords
*/
XGettext.prototype._discoverMatches = function( parsedInput ) {
var keywordFunctions = this.options.keywordFunctions,
matches = [];
walk( parsedInput.ast, {
enter: function( node ) {
if ( node.type !== 'CallExpression' ) {
return;
}
// Pull the resultingFunction out of (0, resultingFunction)()
var callee = node.callee;
while ( 'SequenceExpression' === callee.type ) {
callee = _.last( callee.expressions );
}
var functionName = ( callee.property ) ? callee.property.name : callee.name;
// Validate is named function
if ( ! functionName ) {
return;
}
// Validate desired function name
if ( keywordFunctions.indexOf( functionName ) === -1 ) {
return;
}
// Build discovered match
var match = {
arguments: node.arguments,
keyword: functionName,
line: node.loc.start.line,
column: node.loc.start.column,
};
// Find translator comment
_.each( parsedInput.comments, function( translatorComment ) {
if ( node.loc.start.line === translatorComment.line ||
node.loc.start.line - 1 === translatorComment.line ) {
match.comment = translatorComment.value;
}
} );
matches.push( match );
},
} );
return matches;
};
/**
* Returns an object representing a single transformed matched keyword,
* including the transformed keyword string value (as `string`), and
* potentially any comment associated with the match (as `comment`)
*
* @private
*
* @param {Object} match Match object
* @return {Object} An object representing a single transformed matched
* keyword
*/
XGettext.prototype._transformMatch = function( match ) {
var strings = this.options.keywords[ match.keyword ]( match );
// If transformed result is object, immediately return
if ( _.isPlainObject( strings ) ) {
return strings;
}
// Cast strings to single-element array to enable mapping
if ( ! ( strings instanceof Array ) ) {
strings = [ strings ];
}
// Remove falsey string values
strings = strings.filter( Boolean );
// Transform string back to object with comment
strings = _.map( strings, function( string ) {
var transformed = { string: string, line: match.line, column: match.column };
if ( typeof match.comment !== 'undefined' ) {
transformed.comment = match.comment;
}
return transformed;
} );
return strings;
};