-
Notifications
You must be signed in to change notification settings - Fork 178
/
JSFbtBuilder.js
350 lines (321 loc) · 11 KB
/
JSFbtBuilder.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict-local
* @format
* @oncall i18n_fbt_js
*/
/* eslint max-len: ["warn", 120] */
'use strict';
import type {AnyStringVariationArg} from './fbt-nodes/FbtArguments';
import type {EnumKey} from './FbtEnumRegistrar';
import type {GenderConstEnum} from './Gender';
import type {JSFBTMetaEntry} from './index';
const {
EnumStringVariationArg,
GenderStringVariationArg,
NumberStringVariationArg,
} = require('./fbt-nodes/FbtArguments');
const FbtElementNode = require('./fbt-nodes/FbtElementNode');
const FbtEnumNode = require('./fbt-nodes/FbtEnumNode');
const FbtImplicitParamNode = require('./fbt-nodes/FbtImplicitParamNode');
const FbtNameNode = require('./fbt-nodes/FbtNameNode');
const FbtParamNode = require('./fbt-nodes/FbtParamNode');
const FbtPluralNode = require('./fbt-nodes/FbtPluralNode');
const FbtPronounNode = require('./fbt-nodes/FbtPronounNode');
const {ShowCountKeys} = require('./FbtConstants');
const {varDump} = require('./FbtUtil');
const {
EXACTLY_ONE,
FbtVariationType,
GENDER_ANY,
NUMBER_ANY,
SUBJECT,
} = require('./translate/IntlVariations');
const invariant = require('invariant');
const nullthrows = require('nullthrows');
/**
* Helper class to assemble the JSFBT table data.
* It's responsible for:
* - producing all the combinations of string variations' candidate values,
* from a given list of string variation arguments.
* - generating metadata to describe the meaning of each level of the JSFBT table tree.
*/
class JSFbtBuilder {
/**
* Source code that matches the Babel nodes used in the provided `stringVariationArgs`
*/
+fileSource: string;
/**
* Map of fbt:enum at the current recursion level of `_getStringVariationCombinations()`
*/
+usedEnums: {[enumArgCode: string]: EnumKey};
/**
* Map of fbt:plural at the current recursion level of `_getStringVariationCombinations()`
*/
+usedPlurals: {
[pluralsArgCode: string]: typeof EXACTLY_ONE | typeof NUMBER_ANY,
};
/**
* Map of fbt:pronoun at the current recursion level of `_getStringVariationCombinations()`
*/
+usedPronouns: {
[pronounsArgCode: string]: GenderConstEnum | typeof GENDER_ANY,
};
/**
* Set this to `true` if we're extracting strings for React Native
*/
+reactNativeMode: boolean;
/**
* List of string variation arguments from a given fbt callsite
*/
+stringVariationArgs: $ReadOnlyArray<AnyStringVariationArg>;
constructor(
fileSource: string,
stringVariationArgs: $ReadOnlyArray<AnyStringVariationArg>,
reactNativeMode?: boolean,
): void {
this.fileSource = fileSource;
this.reactNativeMode = !!reactNativeMode;
this.stringVariationArgs = stringVariationArgs;
this.usedEnums = {};
this.usedPlurals = {};
this.usedPronouns = {};
}
/**
* Generates a list of metadata entries that describe the usage of each level
* of the JSFBT table tree
* @param compactStringVariationArgs Consolidated list of string variation arguments.
* See FbtFunctionCallProcessor#_compactStringVariationArgs()
*/
buildMetadata(
compactStringVariationArgs: $ReadOnlyArray<AnyStringVariationArg>,
): Array<?JSFBTMetaEntry> {
return compactStringVariationArgs.map(svArg => {
const {fbtNode} = svArg;
if (fbtNode instanceof FbtPluralNode) {
if (fbtNode.options.showCount !== ShowCountKeys.no) {
return {
token: nullthrows(fbtNode.options.name),
type: FbtVariationType.NUMBER,
singular: true,
};
} else {
return this.reactNativeMode ? {type: FbtVariationType.NUMBER} : null;
}
}
if (
fbtNode instanceof FbtElementNode ||
fbtNode instanceof FbtImplicitParamNode
) {
return {
token: SUBJECT,
type: FbtVariationType.GENDER,
};
}
if (fbtNode instanceof FbtPronounNode) {
return this.reactNativeMode ? {type: FbtVariationType.PRONOUN} : null;
}
if (svArg instanceof EnumStringVariationArg) {
invariant(
fbtNode instanceof FbtEnumNode,
'Expected fbtNode to be an instance of FbtEnumNode but got `%s` instead',
fbtNode.constructor.name || varDump(fbtNode),
);
// We ensure we have placeholders in our metadata because enums and
// pronouns don't have metadata and will add "levels" to our resulting
// table.
//
// Example for the code:
//
// fbt.enum(value, {
// groups: 'Groups',
// photos: 'Photos',
// videos: 'Videos',
// })
//
// Expected metadata entry:
// for non-RN -> `null`
// for RN -> `{range: ['groups', 'photos', 'videos']}`
return this.reactNativeMode
? // Enum range will later be used to extract enums from the payload for React Native
{range: Object.keys(fbtNode.options.range)}
: null;
}
if (
svArg instanceof GenderStringVariationArg ||
svArg instanceof NumberStringVariationArg
) {
invariant(
fbtNode instanceof FbtNameNode || fbtNode instanceof FbtParamNode,
'Expected fbtNode to be an instance of FbtNameNode or FbtParamNode but got `%s` instead',
fbtNode.constructor.name || varDump(fbtNode),
);
return svArg instanceof NumberStringVariationArg
? {
token: fbtNode.options.name,
type: FbtVariationType.NUMBER,
}
: {
token: fbtNode.options.name,
type: FbtVariationType.GENDER,
};
}
invariant(
false,
'Unsupported string variation argument: %s',
varDump(svArg),
);
});
}
/**
* Get all the string variation combinations derived from a list of string variation arguments.
*
* E.g. If we have a list of string variation arguments as:
*
* [genderSV, numberSV]
*
* Assuming genderSV produces candidate variation values as: male, female, unknown
* Assuming numberSV produces candidate variation values as: singular, plural
*
* The output would be:
*
* [
* [ genderSV(male), numberSV(singular) ],
* [ genderSV(male), numberSV(plural) ],
* [ genderSV(female), numberSV(singular) ],
* [ genderSV(female), numberSV(plural) ],
* [ genderSV(unknown), numberSV(singular) ],
* [ genderSV(unknown), numberSV(plural) ],
* ]
*
* Follows legacy behavior:
* - process each SV argument (FIFO),
* - for each SV argument of the same fbt construct (e.g. plural)
* (and not of the same variation type like Gender)
* - check if there's already an existing SV argument of the same JS code being used
* - if so, re-use the same variation value
* - else, "multiplex" new variation value
* Do this for plural, gender, enum
*/
getStringVariationCombinations(): $ReadOnlyArray<
$ReadOnlyArray<AnyStringVariationArg>,
> {
return this._getStringVariationCombinations();
}
_getStringVariationCombinations(
combos: Array<$ReadOnlyArray<AnyStringVariationArg>> = [],
curArgIndex: number = 0,
prevArgs: $ReadOnlyArray<AnyStringVariationArg> = [],
): Array<$ReadOnlyArray<AnyStringVariationArg>> {
invariant(
curArgIndex >= 0,
'curArgIndex value must greater or equal to 0, but we got `%s` instead',
curArgIndex,
);
if (this.stringVariationArgs.length === 0) {
return combos;
}
if (curArgIndex >= this.stringVariationArgs.length) {
combos.push(prevArgs);
return combos;
}
const curArg = this.stringVariationArgs[curArgIndex];
const {fbtNode} = curArg;
const {usedEnums, usedPlurals, usedPronouns} = this;
const recurse = <V>(
candidateValues: $ReadOnlyArray<V>,
beforeRecurse?: V => mixed,
isCollapsible: boolean = false,
): void =>
candidateValues.forEach(value => {
if (beforeRecurse) {
beforeRecurse(value);
}
this._getStringVariationCombinations(
combos,
curArgIndex + 1,
prevArgs.concat(
curArg.cloneWithValue(
// $FlowFixMe[incompatible-call] `value` should be compatible with cloneWithValue()
value,
isCollapsible,
),
),
);
});
if (fbtNode instanceof FbtEnumNode) {
invariant(
curArg instanceof EnumStringVariationArg,
'Expected EnumStringVariationArg but got: %s',
varDump(curArg),
);
const argCode = curArg.getArgCode(this.fileSource);
if (argCode in usedEnums) {
const enumKey = usedEnums[argCode];
invariant(
enumKey in fbtNode.options.range,
'%s not found in %s. Attempting to re-use incompatible enums',
enumKey,
varDump(fbtNode.options.range),
);
recurse([enumKey], undefined, true);
return combos;
}
recurse(curArg.candidateValues, value => (usedEnums[argCode] = value));
delete usedEnums[argCode];
} else if (fbtNode instanceof FbtPluralNode) {
invariant(
curArg instanceof NumberStringVariationArg,
'Expected NumberStringVariationArg but got: %s',
varDump(curArg),
);
const argCode = curArg.getArgCode(this.fileSource);
if (argCode in usedPlurals) {
// Constrain our plural value ('many'/'singular') BUT still add a
// single level. We don't currently prune runtime args like we do
// with enums, but we ought to...
// TODO(T41100260) Prune plurals better
recurse([usedPlurals[argCode]]);
return combos;
}
recurse(curArg.candidateValues, value => (usedPlurals[argCode] = value));
delete usedPlurals[argCode];
} else if (fbtNode instanceof FbtPronounNode) {
invariant(
curArg instanceof GenderStringVariationArg,
'Expected GenderStringVariationArg but got: %s',
varDump(curArg),
);
const argCode = curArg.getArgCode(this.fileSource);
if (argCode in usedPronouns) {
// Constrain our pronoun value BUT still add a
// single level. We don't currently prune runtime args like we do
// with enums, but we ought to...
// TODO(T82185334) Prune pronouns better
recurse([usedPronouns[argCode]]);
return combos;
}
recurse(curArg.candidateValues, value => (usedPronouns[argCode] = value));
delete usedPronouns[argCode];
} else if (
curArg instanceof NumberStringVariationArg ||
curArg instanceof GenderStringVariationArg
) {
recurse(
curArg.candidateValues,
undefined,
curArg instanceof GenderStringVariationArg &&
fbtNode instanceof FbtImplicitParamNode,
);
} else {
invariant(
false,
'Unsupported string variation argument: %s',
varDump(curArg),
);
}
return combos;
}
}
module.exports = JSFbtBuilder;