@oracle/oraclejet-icu-l10n 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/Bundler.d.ts +46 -0
  2. package/Bundler.js +312 -0
  3. package/PatternCompiler.d.ts +83 -0
  4. package/PatternCompiler.js +253 -0
  5. package/README.md +321 -0
  6. package/l10nBundleBuilder.js +88 -0
  7. package/package.json +30 -0
  8. package/test/bundleTypeTest.ts +40 -0
  9. package/test/cliTest.js +48 -0
  10. package/test/jest.config.js +9 -0
  11. package/test/l10nTest.js +492 -0
  12. package/test/resources/CustomMessageType.d.ts +6 -0
  13. package/test/resources/custom-hooks-no-def.js +3 -0
  14. package/test/resources/custom-hooks.js +8 -0
  15. package/test/resources/escape-util.js +3 -0
  16. package/test/resources/nls/app-strings-legacy-keys.json +5 -0
  17. package/test/resources/nls/app-strings-x.json +4 -0
  18. package/test/resources/nls/app-strings.json +48 -0
  19. package/test/resources/nls/azb/app-strings.json +3 -0
  20. package/test/resources/nls/azb-AZ/app-strings.json +3 -0
  21. package/test/resources/nls/ro-MD/app-strings.json +3 -0
  22. package/test/resources/nls/ru/app-strings.json +4 -0
  23. package/test/resources/nls/ru-RU/app-strings-x.json +3 -0
  24. package/test/resources/nls/ru-RU/app-strings.json +3 -0
  25. package/test/resources/nls/zh/app-strings.json +3 -0
  26. package/test/resources/nls/zh-Hans/app-strings.json +2 -0
  27. package/test/resources/nls/zh-Hans/extra-app-strings-x.json +3 -0
  28. package/test/resources/nls/zh-Hans-CN/app-strings.json +2 -0
  29. package/test/resources/nls-bad-metadata/app-strings-bad-root.json +11 -0
  30. package/test/resources/nls-extra-keys/README.md +3 -0
  31. package/test/resources/nls-extra-keys/app-bundle-bad.json +4 -0
  32. package/test/resources/nls-extra-keys/en/app-bundle-bad.json +3 -0
  33. package/tsconfig.json +7 -0
package/Bundler.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Compile the message bundle in ICU format. This function starts with the "root"
3
+ * message bundle and attempts to discover bundles for all available locales by
4
+ * traversing the root bundle's directory and looking for locale directory names.
5
+ * Any file found under locale directories whose names match the root bundle will
6
+ * also be compiled.
7
+ * When creating the output file in the given targetDir, the locale directory
8
+ * structure from the source will be recreated.
9
+ * @param {object} props The properties with which to build the bundle
10
+ * @param {string} props.rootDir The path to the root message bundle
11
+ * @param {string} props.bundleName The name of the bundle file
12
+ * @param {string} props.locale The locale of the root message bundle
13
+ * @param {string} props.outDir The target directory where the compiled root message
14
+ * @param {string=} props.module The type of module to produce -- 'esm', 'amd', or 'legacy-amd'
15
+ * @param {string=} props.exportType The type of export to produce -- 'default' or 'named'
16
+ * @param {boolean=} props.override Indicates the bundle is an override, and only the root
17
+ * locale and those explicitly stated in [--supportedLocales] will be built.
18
+ * @param {string[]=} props.additionalLocales An array of additional locales to build
19
+ * @param {string=} hooks A path to the custom hooks file
20
+ */
21
+ export function build({
22
+ rootDir,
23
+ bundleName,
24
+ locale,
25
+ outDir,
26
+ module,
27
+ exportType,
28
+ override,
29
+ additionalLocales,
30
+ hooks
31
+ }: {
32
+ rootDir: string;
33
+ bundleName: string;
34
+ locale: string;
35
+ outDir: string;
36
+ module?: string | undefined;
37
+ exportType?: string | undefined;
38
+ override?: boolean | undefined;
39
+ additionalLocales?: string[] | undefined;
40
+ }): void;
41
+ /**
42
+ * Test if a given directory name (base name only) is an NLS directory.
43
+ * @param {string} name The directory name
44
+ * @return True if NLS directory, false otherwise
45
+ */
46
+ export function isNlsDir(name: string): boolean;
package/Bundler.js ADDED
@@ -0,0 +1,312 @@
1
+ const fsx = require('fs-extra');
2
+ const glob = require('glob');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const PatternCompiler = require('./PatternCompiler');
6
+ const ts = require('typescript');
7
+ const pkg = require('./package.json');
8
+
9
+ /**
10
+ * Traverse the message bundle and compile all strings.
11
+ * @param {PatternCompiler} compiler The instance of the PatternCompiler
12
+ * @param {Object} bundle The message bundle
13
+ * @return {Object} A message bundle whose strings are wrapped by functions
14
+ * generated from the PatternCompiler.
15
+ * @private
16
+ */
17
+ function traverse(compiler, bundle) {
18
+ const metadata = {};
19
+ // sort so that metadata '@' keys are processed first
20
+ const output = Object.keys(bundle).sort().reduce((accum, key) => {
21
+ if (key.match(/^@/)) {
22
+ metadata[key.substring(1)] = getMetadataParamTypes(bundle[key]);
23
+ } else {
24
+ const value = bundle[key];
25
+ if (typeof value !== 'string') {
26
+ throw Error(`"${key}" value must be a string`);
27
+ }
28
+ return {
29
+ ...accum,
30
+ [key]: compiler.compile(value, metadata[key])
31
+ };
32
+ }
33
+ return accum;
34
+ }, {});
35
+ return output;
36
+ }
37
+
38
+ function getMetadataParamTypes(metadata) {
39
+ return Object.keys(metadata.placeholders || {}).reduce((accum, key) => (
40
+ {
41
+ ...accum,
42
+ [key]: metadata.placeholders?.[key].type
43
+ }
44
+ ), {});
45
+ }
46
+
47
+ /**
48
+ * Get the bundle as an object from the given filepath
49
+ * @param {string} filepath
50
+ * @return {object} The bundle from the file
51
+ * @private
52
+ */
53
+ function getBundle(filepath) {
54
+ let bundle;
55
+ if (fsx.existsSync(filepath)) {
56
+ const bundleContents = fsx.readFileSync(filepath);
57
+ bundle = JSON.parse(bundleContents);
58
+ }
59
+ return bundle;
60
+ }
61
+
62
+ /**
63
+ * Process the given bundle for the given locale and write the compiled contents
64
+ * to the given file path
65
+ * @param {string} rootBundleFile The path to the root bundle JSON file
66
+ * @param {string} bundleId The id of the bundle
67
+ * @param {object} bundle The message bundle
68
+ * @param {string} locale The locale of the bundle
69
+ * @param {string} targetFile The path to the target file where the contents will
70
+ * be written.
71
+ * @param {string} exportType The type of export to produce, either 'default' or 'named'
72
+ * @param {boolean?} withBundleType Whether to export the bundle type definition
73
+ * @private
74
+ */
75
+ function convertBundle(
76
+ rootBundleFile,
77
+ bundleId,
78
+ bundle,
79
+ locale,
80
+ targetFile,
81
+ exportType,
82
+ withBundleType
83
+ ) {
84
+ const compiler = new PatternCompiler(locale);
85
+ let processed;
86
+ try {
87
+ processed = traverse(compiler, bundle);
88
+ } catch (ex) {
89
+ throw Error(`While processing ${locale}/${bundleId}:\n${ex.message}`);
90
+ }
91
+ // apply hooks and type imports
92
+ const convertor = customHooks.convertor;
93
+ const typeImport = customHooks.typeImport
94
+ ? Object.keys(customHooks.typeImport).map((k) => customHooks.typeImport[k])[0]
95
+ : '';
96
+ const otherImports = customHooks.otherImports ? [customHooks.otherImports] : [];
97
+ const valueType = typeImport ? Object.keys(customHooks.typeImport)[0] : '';
98
+ const translations = Object.keys(processed).map((messageKey) => {
99
+ const entry = processed[messageKey];
100
+ const translation = entry.formatter || '""';
101
+ // Param types for TS
102
+ const params = Object.keys(entry.paramTypes).map(
103
+ (paramKey) => `${paramKey}:${entry.paramTypes[paramKey]}`
104
+ );
105
+ const paramString = params.length ? `p: {${params.join(',')}}` : '';
106
+ const paramVar = params.length ? 'p' : undefined;
107
+ const output = convertor
108
+ ? `convert({bundleId:"${bundleId}",id:"${messageKey}",params:${paramVar},translation:${translation}})`
109
+ : translation;
110
+ return [messageKey, `(${paramString})${valueType && ':' + valueType} => ${output}`];
111
+ });
112
+ const targetDir = path.dirname(targetFile);
113
+ fsx.ensureDirSync(targetDir);
114
+ fsx.writeFileSync(
115
+ targetFile,
116
+ generateContent(
117
+ rootBundleFile,
118
+ otherImports.concat(typeImport),
119
+ translations,
120
+ withBundleType,
121
+ convertor,
122
+ exportType
123
+ )
124
+ );
125
+ }
126
+
127
+ function generateContent(
128
+ rootBundleFile,
129
+ imports,
130
+ translations,
131
+ withBundleType,
132
+ convertor,
133
+ exportType
134
+ ) {
135
+ return `// This file is auto-generated by ${pkg.name} from ${rootBundleFile}
136
+ // and should not be edited by hand. Update the JSON file and rerun the build to regenerate this content.
137
+ ${imports.join('\n')}
138
+
139
+ ${
140
+ (convertor &&
141
+ `
142
+ type ParamsType = {
143
+ bundleId: string,
144
+ id: string,
145
+ params: { [key:string]: any } | undefined,
146
+ translation: string
147
+ };
148
+
149
+ const convert = ${convertor}`) ||
150
+ ''
151
+ }
152
+ const bundle = {
153
+ ${translations.map((t) => ` "${t[0]}": ${t[1]}`).join(',\n')}
154
+ };
155
+ ${
156
+ (exportType === 'default' && `export default bundle;`) ||
157
+ `${translations.map((t) => `export const ${t[0]} = bundle.${t[0]}`).join(';\n')}`
158
+ }
159
+ ${withBundleType ? 'export type BundleType = typeof bundle;' : ''}
160
+ `;
161
+ }
162
+
163
+ /**
164
+ * Test if a given directory name (base name only) is an NLS directory.
165
+ * @param {string} name The directory name
166
+ * @return True if NLS directory, false otherwise
167
+ */
168
+ function isNlsDir(name) {
169
+ return (name.match(/^[a-z]{2,3}$/i) || name.match(/^[a-z]{2,3}-.+/)) && !name.match(/\.\w+$/);
170
+ }
171
+
172
+ function transpile(files, module) {
173
+ const moduleMap = {
174
+ amd: ts.ModuleKind.AMD,
175
+ 'legacy-amd': ts.ModuleKind.AMD,
176
+ esm: ts.ModuleKind.ES2020
177
+ };
178
+ const program = ts.createProgram(files, {
179
+ module: moduleMap[module],
180
+ target: 'ES2021',
181
+ strict: true
182
+ });
183
+ program.emit();
184
+ }
185
+
186
+ function convertToLegacyAmd(files) {
187
+ // Replace default export with top-level keys
188
+ const amdDefaultExport = 'exports.default = bundle;';
189
+ files.forEach((file) => {
190
+ const contents = fsx.readFileSync(file).toString();
191
+ if (contents.indexOf(amdDefaultExport) === -1) {
192
+ throw Error(`No default export found in ${file}`);
193
+ }
194
+ fsx.writeFileSync(file, contents.replace(amdDefaultExport, 'Object.assign(exports, bundle);'));
195
+ });
196
+ }
197
+
198
+ let customHooks = {};
199
+
200
+ /**
201
+ * Compile the message bundle in ICU format. This function starts with the "root"
202
+ * message bundle and attempts to discover bundles for all available locales by
203
+ * traversing the root bundle's directory and looking for locale directory names.
204
+ * Any file found under locale directories whose names match the root bundle will
205
+ * also be compiled.
206
+ * When creating the output file in the given targetDir, the locale directory
207
+ * structure from the source will be recreated.
208
+ * @param {object} props The properties with which to build the bundle
209
+ * @param {string} props.rootDir The path to the root message bundle
210
+ * @param {string} props.bundleName The name of the bundle file
211
+ * @param {string} props.locale The locale of the root message bundle
212
+ * @param {string} props.outDir The target directory where the compiled root message
213
+ * @param {string=} props.module The type of module to produce -- 'esm', 'amd', or 'legacy-amd'
214
+ * @param {string=} props.exportType The type of export to produce -- 'default' or 'named'
215
+ * @param {boolean=} props.override Indicates the bundle is an override, and only the root
216
+ * locale and those explicitly stated in [--supportedLocales] will be built.
217
+ * @param {string[]=} props.additionalLocales An array of additional locales to build
218
+ * @param {string=} hooks A path to the custom hooks file
219
+ */
220
+ function build({
221
+ rootDir,
222
+ bundleName,
223
+ locale,
224
+ outDir,
225
+ module,
226
+ exportType = 'default',
227
+ override,
228
+ additionalLocales = [],
229
+ hooks
230
+ }) {
231
+ const jsonRegEx = /\.json$/;
232
+ if (!jsonRegEx.test(bundleName)) {
233
+ throw Error(`${bundleName} must be a JSON file`);
234
+ }
235
+ const isLegacyAmd = module === 'legacy-amd';
236
+ const targetBundleName = bundleName.replace(jsonRegEx, '.ts');
237
+ // Merge in supported locales to build. If physical dirs don't exist, they'll
238
+ // inherit the root translations
239
+ const supportedLocales = [
240
+ ...new Set(
241
+ // for override bundles, only look in root and additionalLocales folders
242
+ (override ? [] : fsx.readdirSync(rootDir).filter(isNlsDir)).concat(additionalLocales).sort()
243
+ )
244
+ ];
245
+
246
+ customHooks = hooks ? require(hooks) : {};
247
+
248
+ // Traverse root bundle
249
+ const rootBundleFile = path.relative(process.cwd(), path.join(rootDir, bundleName));
250
+ const rootBundle = getBundle(rootBundleFile);
251
+ let rootBundleKeys;
252
+ if (rootBundle) {
253
+ convertBundle(
254
+ rootBundleFile,
255
+ bundleName,
256
+ rootBundle,
257
+ locale,
258
+ path.join(outDir, targetBundleName),
259
+ isLegacyAmd ? 'default' : exportType,
260
+ true
261
+ );
262
+ rootBundleKeys = new Set(Object.keys(rootBundle));
263
+ }
264
+
265
+ supportedLocales.forEach((locale) => {
266
+ // Combine all levels into single bundle, starting with rootBundle
267
+ const combinedBundle = Object.assign({}, rootBundle);
268
+ const localeParts = locale.split('-');
269
+ for (let i = 0, len = localeParts.length; i < len; i++) {
270
+ // Build segment from region (0) to index
271
+ let segment = localeParts.slice(0, i + 1).join('-');
272
+ const perBundlePath = path.join(rootDir, segment, bundleName);
273
+ const perBundle = getBundle(perBundlePath);
274
+ Object.assign(combinedBundle, perBundle);
275
+ // combinedBundle should not have keys not in rootBundle
276
+ if (rootBundleKeys) {
277
+ const extraKeys = Object.keys(combinedBundle).filter((k) => !rootBundleKeys.has(k));
278
+ if (extraKeys.length) {
279
+ throw Error(`${perBundlePath} has keys not found in ${rootBundleFile}: ${extraKeys}`);
280
+ }
281
+ }
282
+ }
283
+
284
+ convertBundle(
285
+ rootBundleFile,
286
+ bundleName,
287
+ combinedBundle,
288
+ locale,
289
+ path.join(outDir, locale, targetBundleName),
290
+ isLegacyAmd ? 'default' : exportType
291
+ );
292
+ });
293
+
294
+ !override && fsx.writeFileSync(
295
+ path.join(outDir, 'supportedLocales.ts'),
296
+ `export default ${JSON.stringify(supportedLocales)};\n`
297
+ );
298
+
299
+ if (module) {
300
+ const transpileFiles = glob.sync(`${path.resolve(outDir)}/**/*.ts`);
301
+ transpile(transpileFiles, module);
302
+
303
+ if (module === 'legacy-amd') {
304
+ convertToLegacyAmd(glob.sync(`${outDir}/**/${bundleName.replace(jsonRegEx, '')}.js`));
305
+ transpileFiles.forEach(fsx.removeSync);
306
+ }
307
+ }
308
+ }
309
+ module.exports = {
310
+ build,
311
+ isNlsDir
312
+ };
@@ -0,0 +1,83 @@
1
+ export = PatternCompiler;
2
+ declare class PatternCompiler {
3
+ constructor(locale: any);
4
+ _locale: any;
5
+ compile(pattern: any): {
6
+ paramTypes: any;
7
+ formatter: any;
8
+ };
9
+ _processParts(ast: any, paramAccumulator: any, currentPluralNode: any): any;
10
+ _getNumberFormat(node: any, paramAccumulator: any): string;
11
+ _getDateTimeFormat(node: any, type: any, paramAccumulator: any): string;
12
+ _getSelectFormat(node: any, paramAccumulator: any): string;
13
+ _getPluralFormat(node: any, paramAccumulator: any): string;
14
+ _getPoundFormat(node: any, paramAccumulator: any): string;
15
+ _getSelectOptions(
16
+ node: any,
17
+ paramAccumulator: any,
18
+ pluralNode: any
19
+ ): {
20
+ opts: any[];
21
+ other: string;
22
+ exactPlurals: any[];
23
+ };
24
+ _getParameterExpression(node: any, paramAccumulator: any, type: any): string;
25
+ _stringifyOptions(obj: any): any;
26
+ _getFormatOpts(): {
27
+ number: {
28
+ currency: {
29
+ style: string;
30
+ };
31
+ percent: {
32
+ style: string;
33
+ };
34
+ };
35
+ date: {
36
+ short: {
37
+ month: string;
38
+ day: string;
39
+ year: string;
40
+ };
41
+ medium: {
42
+ month: string;
43
+ day: string;
44
+ year: string;
45
+ };
46
+ long: {
47
+ month: string;
48
+ day: string;
49
+ year: string;
50
+ };
51
+ full: {
52
+ weekday: string;
53
+ month: string;
54
+ day: string;
55
+ year: string;
56
+ };
57
+ };
58
+ time: {
59
+ short: {
60
+ hour: string;
61
+ minute: string;
62
+ };
63
+ medium: {
64
+ hour: string;
65
+ minute: string;
66
+ second: string;
67
+ };
68
+ long: {
69
+ hour: string;
70
+ minute: string;
71
+ second: string;
72
+ timeZoneName: string;
73
+ };
74
+ full: {
75
+ hour: string;
76
+ minute: string;
77
+ second: string;
78
+ timeZoneName: string;
79
+ };
80
+ };
81
+ };
82
+ _getLanguage(): any;
83
+ }
@@ -0,0 +1,253 @@
1
+ const Parser = require('@formatjs/icu-messageformat-parser');
2
+
3
+ module.exports = class PatternCompiler {
4
+ constructor(locale) {
5
+ this._locale = locale;
6
+ }
7
+
8
+ compile(pattern, paramTypes = {}) {
9
+ // set ignioreTag flag, so that < and > characters are treated as regular characters that do not
10
+ // require escaping
11
+ const ast = Parser.parse(pattern, {ignoreTag: true});
12
+ const paramAccumulator = Object.create(null);
13
+ const combinedParts = this._processParts(ast, paramAccumulator);
14
+ return {paramTypes: combineParams(paramAccumulator, paramTypes), formatter: combinedParts};
15
+ }
16
+
17
+ _processParts(ast, paramAccumulator, currentPluralNode) {
18
+ if (!ast) {
19
+ return undefined;
20
+ }
21
+ const acc = [];
22
+ return ast.reduce(
23
+ (acc, node) => {
24
+ switch (node.type) {
25
+ case 0: // raw text
26
+ acc.push(`${JSON.stringify(node.value)}`);
27
+ break;
28
+ case 1: // argument
29
+ acc.push(this._getParameterExpression(node, paramAccumulator));
30
+ break;
31
+ case 2: // number
32
+ acc.push(this._getNumberFormat(node, paramAccumulator));
33
+ break;
34
+ case 3: // date
35
+ acc.push(this._getDateTimeFormat(node, 'date', paramAccumulator));
36
+ break;
37
+ case 4: // time
38
+ acc.push(this._getDateTimeFormat(node, 'time', paramAccumulator));
39
+ break;
40
+ case 5: // select
41
+ acc.push(this._getSelectFormat(node, paramAccumulator));
42
+ break;
43
+ case 6: // plural
44
+ acc.push(this._getPluralFormat(node, paramAccumulator));
45
+ break;
46
+ case 7: // pound
47
+ acc.push(this._getPoundFormat(currentPluralNode, paramAccumulator));
48
+ break;
49
+ default:
50
+ throw new Error('Unsupported token type: ' + node.type);
51
+
52
+ }
53
+
54
+ return acc;
55
+
56
+ },
57
+ acc
58
+ ).join('+');
59
+ }
60
+
61
+ _getNumberFormat(node, paramAccumulator) {
62
+ let opts;
63
+ const style = node.style;
64
+ if (typeof style === 'string') {
65
+ opts = this._getFormatOpts().number[style];
66
+ } else if (Parser.isNumberSkeleton(style)) {
67
+ opts = style.parsedOptions;
68
+ }
69
+ return `new Intl.NumberFormat("${this._locale}",${this._stringifyOptions(opts)}).
70
+ format(${this._getParameterExpression(node, paramAccumulator, 'number')})`;
71
+ }
72
+
73
+ _getDateTimeFormat(node, type, paramAccumulator) {
74
+ let opts;
75
+ const style = node.style;
76
+ if (typeof style === 'string') {
77
+ opts = this._getFormatOpts()[type][style];
78
+ } else if (Parser.isDateTimeSkeleton(style)) {
79
+ opts = style.parsedOptions;
80
+ }
81
+ return `new Intl.DateTimeFormat("${this._locale}",${this._stringifyOptions(opts)}).
82
+ format(new Date(${this._getParameterExpression(node, paramAccumulator, 'string')}))`;
83
+ }
84
+
85
+ _getSelectFormat(node, paramAccumulator) {
86
+ const {opts, other} = this._getSelectOptions(node, paramAccumulator);
87
+ // The generated code will look like the following:
88
+ // {male: "he said", female: "she said"}[param]||{"they said"}
89
+ return `({${opts.join(',')}}[${this._getParameterExpression(node, paramAccumulator)}]${other})`;
90
+ }
91
+
92
+ _getPluralFormat(node, paramAccumulator) {
93
+ const {opts, other, exactPlurals} = this._getSelectOptions(node, paramAccumulator, node);
94
+ const offset = node.offset !== 0 ? `-(${node.offset})` : '';
95
+ const num = this._getParameterExpression(node, paramAccumulator, 'number');
96
+
97
+ const pluralMatch = opts.length === 0 ? 'undefined' :
98
+ `({${opts.join(',')}} as Partial<Record<Intl.LDMLPluralRule, string>>)[new Intl.PluralRules('${this._locale}').select(${num}${offset})]`;
99
+ const exactMatch =
100
+ exactPlurals.length > 0 ? `{${exactPlurals.join(',')}}[${num}]||` : '';
101
+
102
+ // The generated code will look like the following:
103
+ // {0: "message for zero"}[pluralNum]||
104
+ // {one: "one message", many: "many message"}[new Intl.PluralRules(loc).select(pluralNum-offset)]||
105
+ // {"default message"}
106
+ return `(${exactMatch}${pluralMatch}${other})`;
107
+ }
108
+
109
+ _getPoundFormat(node, paramAccumulator) {
110
+ if (!node) {
111
+ throw new Error("Unexpected pound roken outside of the plural rule!");
112
+ }
113
+ const offset = node.offset !== 0 ? `-${node.offset}` : '';
114
+ const num = this._getParameterExpression(node, paramAccumulator, 'number');
115
+ return `new Intl.NumberFormat("${this._locale}").format(${num}${offset})`;
116
+ }
117
+
118
+ _getSelectOptions(node, paramAccumulator, pluralNode) {
119
+ const options = node.options;
120
+ const opts = []; // regular options
121
+ let other = ''; // default option
122
+ const exactPlurals = []; // exact plural values
123
+ Object.keys(options).forEach(key => {
124
+ const opt = options[key];
125
+ if (key === 'other') {
126
+ other = `||${this._processParts(opt.value, paramAccumulator, pluralNode)}`;
127
+ } else {
128
+ if (pluralNode && key.length > 1 && key.startsWith('=')) {
129
+ exactPlurals.push(`${key.substring(1)}:${this._processParts(opt.value, paramAccumulator, pluralNode)}`);
130
+ } else {
131
+ opts.push(`"${key}":${this._processParts(opt.value, paramAccumulator, pluralNode)}`);
132
+ }
133
+ }
134
+ });
135
+
136
+ return {opts, other, exactPlurals};
137
+ }
138
+
139
+ _getParameterExpression(node, paramAccumulator, type) {
140
+ const paramName = node.value;
141
+ paramAccumulator[paramName] = type || 'string';
142
+ // no need to escape parameter name since it is supposed to be a valid identifier or number
143
+ return `p["${paramName}"]`;
144
+ }
145
+
146
+ _stringifyOptions(obj) {
147
+ if (obj === undefined)
148
+ return obj;
149
+ const inner = Object.keys(obj)
150
+ .map(key => `${key}:${JSON.stringify(obj[key])}`)
151
+ .join(',');
152
+ return `{${inner}}`;
153
+ }
154
+
155
+ _getFormatOpts() {
156
+ return {
157
+ number: {
158
+ currency: {
159
+ style: 'currency',
160
+ },
161
+
162
+ percent: {
163
+ style: 'percent',
164
+ },
165
+ },
166
+
167
+ date: {
168
+ short: {
169
+ month: 'numeric',
170
+ day: 'numeric',
171
+ year: '2-digit',
172
+ },
173
+
174
+ medium: {
175
+ month: 'short',
176
+ day: 'numeric',
177
+ year: 'numeric',
178
+ },
179
+
180
+ long: {
181
+ month: 'long',
182
+ day: 'numeric',
183
+ year: 'numeric',
184
+ },
185
+
186
+ full: {
187
+ weekday: 'long',
188
+ month: 'long',
189
+ day: 'numeric',
190
+ year: 'numeric',
191
+ },
192
+ },
193
+
194
+ time: {
195
+ short: {
196
+ hour: 'numeric',
197
+ minute: 'numeric',
198
+ },
199
+
200
+ medium: {
201
+ hour: 'numeric',
202
+ minute: 'numeric',
203
+ second: 'numeric',
204
+ },
205
+
206
+ long: {
207
+ hour: 'numeric',
208
+ minute: 'numeric',
209
+ second: 'numeric',
210
+ timeZoneName: 'short',
211
+ },
212
+
213
+ full: {
214
+ hour: 'numeric',
215
+ minute: 'numeric',
216
+ second: 'numeric',
217
+ timeZoneName: 'short',
218
+ },
219
+ },
220
+ };
221
+ }
222
+ _getLanguage() {
223
+ return this._locale.split('-')[0];
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Combine params from newParams into origParams, but throw exception if existing type differs
229
+ * @param {*} origParams
230
+ * @param {*} newParams
231
+ * @return A combined object of params
232
+ */
233
+ function combineParams(origParams, newParams) {
234
+ const typeMap = {
235
+ 'date': 'string',
236
+ 'number': 'number',
237
+ 'plural': 'number',
238
+ 'select': 'string',
239
+ 'text': 'string'
240
+ }
241
+ return Object.keys(newParams).reduce((accum, key) => {
242
+ const newType = typeMap[newParams[key]] || newParams[key];
243
+ if (key in accum && newType !== undefined && accum[key] !== newType) {
244
+ throw Error(`Parameter '${key}' defined as type '${accum[key]}' in usage, but ${newType} in metadata`);
245
+ }
246
+ return {
247
+ ...accum,
248
+ [key]: newType
249
+ }
250
+ }, origParams);
251
+ }
252
+
253
+