@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/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # JET ICU Message Format Parser
2
+
3
+ This utility parses message bundles written in ICU format and converts them to
4
+ formatter functions to be called by the application to get localized strings.
5
+
6
+ ## Message Bundle Restrictions
7
+
8
+ - Message bundle files must be JSON, and only top-level properties are supported
9
+ (no nesting). The string values must follow the ICU format described at
10
+ http://userguide.icu-project.org/formatparse/messages
11
+
12
+ - Any keys which start with "@" will be ignored, and will not be written to the
13
+ output file(s).
14
+
15
+ ### Ex. app-strings.json
16
+
17
+ ```json
18
+ {
19
+ "greeting": "Hello {name}",
20
+ "invitation": "{gender_of_host, select, female {{num_guests, plural, ..."
21
+ }
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ Run the `l10nBundleBuilder.js` script with arguments:
27
+
28
+ ```sh
29
+ $ l10nBundleBuilder.js <root-message-bundle.json> <root-bundle-locale> <output-dir>
30
+ ```
31
+
32
+ ### Example, running with a root bundle in en-US locale
33
+
34
+ ```sh
35
+ $ l10nBundleBuilder.js --rootDir=resources/nls --bundleName=app-strings.json --locale=en-US --outDir=dist
36
+ ```
37
+
38
+ ## Output
39
+
40
+ The parser reads the given root bundle and converts the message to a TS module
41
+ containing a single default export object. Within the object, each message
42
+ name will be the key to the formatter function.
43
+
44
+ ### Ex. app-strings.ts
45
+
46
+ ```javascript
47
+ const bundle = {
48
+ "greeting": function(p) { ... },
49
+ "invitation": function(p) { ... }
50
+ };
51
+ export default bundle;
52
+ ```
53
+
54
+ The AMD output would be
55
+
56
+ ```javascript
57
+ define(["require", "exports"], function (require, exports) {
58
+ const bundle = {
59
+ "greeting": function(p) { ... },
60
+ "invitation": function(p) { ... }
61
+ };
62
+ exports.default = bundle;
63
+ });
64
+ ```
65
+
66
+ In addition to the root bundle, the parser will traverse all directories at the
67
+ same level, looking for other NLS directories which contain message bundle files
68
+ whose name matches that of the root bundle. If found, it combines all messages
69
+ starting from the root bundle up to the most specific bundle, with the most specific
70
+ ones taking precendence.
71
+
72
+ ## Custom Hooks
73
+
74
+ Custom hooks allows an external script to alter the output of the generated bundle.
75
+ For example, if you want each bundle key to return a custom type rather than
76
+ just the plain translation string, define a custom hook JS file and pass it using
77
+ the `--hooks` switch:
78
+
79
+ ```sh
80
+ $ l10nBundleBuilder.js --rootDir=resources/nls --bundleName=app-strings.json --outDir=dist --hooks=./custom-hooks.js
81
+ ```
82
+
83
+ `custom-hooks.js`
84
+
85
+ ```javascript
86
+ module.exports = {
87
+ typeImport: {
88
+ CustomMessageType: '../../resources/CustomMessageType'
89
+ },
90
+ otherImports: {
91
+ escape: '../../str',
92
+ ext: '../../utils'
93
+ },
94
+ // remap some of the parameter names to different keys
95
+ convertor:
96
+ '(args: ParamsType): CustomMessageType => ({bundle: ext(args.bundleId), key: escape(args.id), params: args.params, value: args.translation})'
97
+ };
98
+ ```
99
+
100
+ The `typeImport` field defines the custom type import that will be added to the
101
+ Typescript file generated. This should use a single key:value mapping that contains
102
+ the type definition for your custom type begin returned.
103
+
104
+ You can optionally define `otherImports` to include other imports into the bundle.
105
+ This map can contain an unlimited number of imports. In the example above, the
106
+ functions `escape` and `ext` are imported because they're used by the convertor.
107
+
108
+ The `convertor` string defines the _contents_ of your convertor function that'll
109
+ be run in place of the default behavior of returning just the translation string.
110
+ The return of this function should match the custom type defined by `typeImport`.
111
+
112
+ > Note the use of the `ParamsType` as the argument to the convertor. This type
113
+ > is automatically added by the bundler to help your convertor understand the type
114
+ > of parameters that it can expect to be called with.
115
+ > `ParamsType` is defined as such, and is included in each bundle file
116
+ >
117
+ > ```javascript
118
+ > type ParamsType = {
119
+ > bundleId: string,
120
+ > id: string,
121
+ > params: { [key: string]: any } | undefined,
122
+ > translation: string
123
+ > };
124
+ > ```
125
+
126
+ `CustomMessageType.d.ts`
127
+
128
+ ```typescript
129
+ export declare type CustomMessageType = {
130
+ bundle: string;
131
+ key: string;
132
+ params: { [key: string]: any } | undefined;
133
+ value: string;
134
+ };
135
+ ```
136
+
137
+ This will produce a bundle with key/values similar to
138
+
139
+ ```javascript
140
+ "welcome": ():CustomMessageType => convert({bundleId:ext("app-strings.json"),id:escape("welcome"),params:undefined,translation:"Welcome"}),
141
+ ```
142
+
143
+ The `convertor` function takes `ParamsType` and returns a custom object type
144
+ conforming to `CustomMessageType`.
145
+
146
+ ## Options
147
+
148
+ ### **rootDir**
149
+
150
+ The root directory of your ICU bundles
151
+
152
+ > ex. `--rootDir=resources/nls`
153
+
154
+ ### **bundleName**
155
+
156
+ The bundle file name that should be processed
157
+
158
+ > ex. `--bundleName=app-strings.json`
159
+
160
+ ### **hooks**
161
+
162
+ A path to a file containing custom hooks
163
+
164
+ > ex. `--hooks=./hooks.js`
165
+
166
+ ### **locale**
167
+
168
+ The locale of the root bundle
169
+
170
+ > ex. `--locale=en-US`
171
+
172
+ ### **outDir**
173
+
174
+ The directory where the resource bundles will be written
175
+
176
+ > ex. `--outDir=src/resources/nls`
177
+
178
+ ### **module** _(optional)_
179
+
180
+ The type of module to produce for the bundles. Supported values are `esm`, `amd`, or `legacy-amd`.
181
+ If not specified, only the original Typescript source files will be produced, and
182
+ no transpilation is performed.
183
+
184
+ > ex. `--module=esm`
185
+
186
+ > For backwards compatibility with previous forms of the bundle in AMD, the `legacy-amd` format
187
+ > will produce a bundle with all keys at the root, similar to using named exports. There will also be no
188
+ > TypeScript or type definitions present for the bundle.
189
+ > This option overrides the `--exportType` argument.
190
+
191
+ ### **exportType** _(optional)_
192
+
193
+ The type of export to be used. Possible values `named` or `default`. If not given,
194
+ `default` is used.
195
+
196
+ > ex. `--exportType=named`
197
+ >
198
+ > Note that in v2.0.0 of the builder, default exports are used, causing TypeScript
199
+ > to place all of the keys into an object named "default" at the top level. If
200
+ > you want all of the bundle keys at the top-level, use `--exportType=named` to
201
+ > produce
202
+ >
203
+ > ```javascript
204
+ > export greeting = bundle.greeting;
205
+ > export invitation = bundle.invitation;
206
+ > ```
207
+ >
208
+ > and the resulting AMD output
209
+
210
+ ```javascript
211
+ define(['require', 'exports'], function (require, exports) {
212
+ exports.greeting = bundle.greeting;
213
+ exports.invitation = bundle.invitation;
214
+ });
215
+ ```
216
+
217
+ ### **override** _(optional)_
218
+
219
+ Indicates the bundle is an override, and only the root locale and those explicitly stated in
220
+ `--supportedLocales` will be built. All other NLS directories will not be processed.
221
+
222
+ > ex. `--optional --supportedLocales=en`
223
+
224
+ ### **supportedLocales** _(optional)_
225
+
226
+ A comma-separated list of additional locales to build. If a given locale doesn't
227
+ have a corresponding NLS directory underneath `rootDir`, then it will be built
228
+ using the root bundle.
229
+
230
+ > ex. `--supportedLocales=en,en-XB`
231
+
232
+ ## Changes in 2.0.0
233
+
234
+ ### AMD output
235
+
236
+ In 1.0.0, AMD bundles were constructed with
237
+
238
+ ```javascript
239
+ define({ ...bundle contents... })
240
+ ```
241
+
242
+ Default exports now follow ES module format, and contents are underneath the
243
+ `default` property
244
+
245
+ ```javascript
246
+ define(["require", "exports"], function (require, exports) {
247
+ "use strict";
248
+ exports.__esModule = true;
249
+ exports.default = {
250
+ ...bundle contents...
251
+ };
252
+ });
253
+ ```
254
+
255
+ ### Typescript
256
+
257
+ All bundles are now created in Typescript and transpiled to their target module
258
+ formats. The TS sources replace the `d.ts` files previously created in 1.0.0, and
259
+ an additional `BundleType` is exported. This type can be imported for type-safe
260
+ usage during design-time.
261
+
262
+ ```javascript
263
+ import type { BundleType as App1Bundle } from './resources/nls/app1bundle';
264
+ import type { BundleType as App2Bundle } from './resources/nls/app2bundle';
265
+ ```
266
+
267
+ Typescript [type imports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)
268
+ are only used during design-time, and removed from the output when transpiled.
269
+
270
+ ## Changes in 2.0.1
271
+
272
+ ### supportedLocales.ts
273
+
274
+ A new module file is created in `<outDir>/supportedLocales.ts` which exports an
275
+ array of all the supported locales built for the bundle. See `--supportedLocales`
276
+ option above. The array can be imported
277
+ with
278
+
279
+ ```javascript
280
+ import supportedLocales from './&lt;rootDir>/supportedLocales';
281
+ ```
282
+
283
+ ## Changes in 2.2.0
284
+
285
+ Custom hooks implemented. See [Custom Hooks](#custom-hooks) above.
286
+
287
+ ## Changes in 2.3.0
288
+
289
+ Add the option to declare `--exportType=named` to generate keys at the top level
290
+ of the exports instead of the `default` export type.
291
+
292
+ ## Changes in 16.0.0
293
+
294
+ ### Types in Metadata
295
+
296
+ Parameter types can be defined in metadata instead of just inferred from the root bundle.
297
+ Keys starting with the `@` character are used to specify metadata, and is associated with the key whose name follows it:
298
+
299
+ ```
300
+ "input_message_error": "Error: {MESSAGE}",
301
+ "@input_message_error": {
302
+ "placeholders": {
303
+ "MESSAGE": {
304
+ "type": "text",
305
+ "description": "translated error message"
306
+ }
307
+ },
308
+ "description": "Error with an embedded message"
309
+ }
310
+ ```
311
+
312
+ Metadata is optional. It should be supplied in the following cases:
313
+
314
+ 1. The usage of the text placeholder is going to be unclear to the translators, and supplying extra description in the metadata is going to help translators with translating the rest of the message.
315
+ 1. A particular placeholder is not being used in the message included in the root/development bundle, but the corresponding parameter should be settable for some other locales
316
+ 1. v2 translation build process will derive parameter type information from the message in the root/development bundle. Type information for any parameters used only in other locales will have to come from metadata.
317
+
318
+ ### Date Time String Format
319
+
320
+ Date values are to be passed as string parameters to the message bundle. They can be any valid string,
321
+ as defined by [Date time string format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
@@ -0,0 +1,88 @@
1
+ #! /usr/bin/env node
2
+ const path = require('path');
3
+
4
+ // Extract arguments
5
+ const argsMap = Array.from(process.argv)
6
+ .slice(2)
7
+ .reduce((acc, arg) => {
8
+ // Store --key=value as { key: value }
9
+ const pair = (arg.split(/^--/)[1] || '').split(/=/);
10
+ return {
11
+ ...acc,
12
+ [pair && pair[0]]: pair && pair[1]
13
+ };
14
+ }, {});
15
+ if ('rootDir' in argsMap && 'bundleName' in argsMap && 'locale' in argsMap && 'outDir' in argsMap) {
16
+ require('./Bundler').build({
17
+ ...argsMap,
18
+ override: 'override' in argsMap,
19
+ additionalLocales: argsMap.supportedLocales && argsMap.supportedLocales.split(',')
20
+ });
21
+ } else {
22
+ const procName = path.basename(process.argv[1]);
23
+ console.warn(
24
+ `Usage: ${procName} --rootDir=</path/to/bundle-dir> --bundleName=<message-bundle-name.json> --locale=<bundle-locale> --outDir=<output-dir> [--module=amd|esm|ts] [--export=named|default] [--hooks=<path-to-hooks-file>] [--suportedLocales=..,..]
25
+
26
+ Required:
27
+ --rootDir\tThe root directory where the bundle files are contained
28
+ --bundleName\tThe bundle's filename (basename, without the directory path)
29
+ --locale\tThe root bundle's locale
30
+ --outDir\tThe output directory where the built bundle will be written
31
+ Optional:
32
+ --module\tProduce bundles as 'esm', 'amd', or 'legacy-amd' modules
33
+ --exportType\tThe type of export, either 'named' or 'default' (default='default')
34
+ --hooks\tThe hooks file to use (see example)
35
+ --override\tIndicates the bundle is an override, and only the root locale and
36
+ \tthose explicitly stated in [--supportedLocales] will be built.
37
+ --supportedLocales\tA list of comma-separated additional locales to build. If
38
+ \ta locale is specified but doesn't have a directory and translation file
39
+ \tin the rootDir, it will be built using the root translations.
40
+ \tThis list will be added onto the full list of supportedLocales exported
41
+ \tfrom the root bundle TS file, which contains locales found in the rootDir.
42
+
43
+ Example for root bundle:
44
+ ${procName} --rootDir=resources/nls --bundleName=bundle-i18n.json --locale=en-US --outDir=dist --module=amd
45
+
46
+ Example for override bundle:
47
+ ${procName} --rootDir=resources/nls --bundleName=bundle-i18n-x.json --override --locale=en-US --outDir=dist --module=amd --supportedLocales=en
48
+
49
+ The rootDir should be the root directory where your root resource bundles and
50
+ NLS directories reside. This directory would contain entries such as:
51
+
52
+ - bundle-i18n.json
53
+ - de
54
+ - bundle-i18n.json
55
+ - bundle-i18n-x.json
56
+ - de-DE
57
+ - bundle-i18n.json
58
+ - bundle-i18n-x.json
59
+
60
+ Example for hooks:
61
+ ${procName} --hooks=./custom-hooks.js
62
+
63
+ custom-hooks.js:
64
+ module.exports = {
65
+ typeImport: { CustomMessageType: './types/custom-message-type' },
66
+ convertor: \`({ bundleId, id, params, translation }) => ({
67
+ bundleId,
68
+ id,
69
+ params,
70
+ translation
71
+ })\`
72
+ };
73
+
74
+ Produces:
75
+ import { CustomMessageType } from "./types/custom-message-type";
76
+
77
+ const bundle = {
78
+ "greeting": (params: ParamsType): CustomMessageType => ({
79
+ "bundleId": "my-bundle",
80
+ "key": "greeting",
81
+ "params": [],
82
+ "translation": "Hello!"
83
+ })
84
+ };
85
+ `
86
+ );
87
+ process.exit(1);
88
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@oracle/oraclejet-icu-l10n",
3
+ "version": "16.0.0",
4
+ "description": "JET ICU Message Format Parser",
5
+ "main": "l10nBundleBuilder.js",
6
+ "scripts": {
7
+ "clean": "rimraf test/built test_results",
8
+ "build-types": "tsc --declaration --emitDeclarationOnly --allowJs --skipLibCheck Bundler.js",
9
+ "test": "jest -c test/jest.config.js"
10
+ },
11
+ "author": "",
12
+ "license": "ISC",
13
+ "dependencies": {
14
+ "@formatjs/icu-messageformat-parser": "2.3.1",
15
+ "fs-extra": "10.1.0",
16
+ "glob": "7.2.0",
17
+ "typescript": "5.3.2"
18
+ },
19
+ "devDependencies": {
20
+ "@jest/globals": "29.6.2",
21
+ "@oracle/oraclejet-test-utils": "workspace:^",
22
+ "jest": "29.6.2",
23
+ "jest-environment-jsdom": "29.6.2",
24
+ "rimraf": "3.0.2",
25
+ "ts-jest": "29.1.0"
26
+ },
27
+ "bin": {
28
+ "l10nBundleBuilder": "./l10nBundleBuilder.js"
29
+ }
30
+ }
@@ -0,0 +1,40 @@
1
+ import * as fsx from 'fs-extra';
2
+
3
+ import { describe, expect, test } from '@jest/globals';
4
+
5
+ import { build } from '../Bundler';
6
+ import glob from 'glob';
7
+ import path from 'path';
8
+
9
+ describe('L10n builder TypeScript tests', () => {
10
+ const outputRoot = `${__dirname}/built`;
11
+ const rootDir = `${__dirname}/resources/nls`;
12
+
13
+ describe('TS-only output', () => {
14
+ const outDir = path.resolve(outputRoot, 'ts');
15
+ beforeAll(() => {
16
+ fsx.removeSync(outDir);
17
+ build({
18
+ rootDir,
19
+ bundleName: 'app-strings.json',
20
+ locale: 'en-US',
21
+ outDir
22
+ });
23
+ build({
24
+ rootDir,
25
+ bundleName: 'app-strings-x.json',
26
+ locale: 'en-US',
27
+ outDir
28
+ });
29
+ });
30
+
31
+ test('does not produce JS', () => {
32
+ expect(glob.sync(`${outDir}/**/*.js`).length).toEqual(0);
33
+ });
34
+
35
+ test('retrieves a value from the TS bundle', async () => {
36
+ const bundle = (await import(`${outDir}/app-strings`)).default;
37
+ expect(bundle.testPlural({ itemCount: 2 })).toEqual('You have 2 items');
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ const path = require('path');
3
+ const fsx = require('fs-extra');
4
+ const { spawnSync } = require('child_process');
5
+
6
+
7
+ describe('CLI', () => {
8
+ const node = process.argv[0];
9
+ const outputRoot = path.resolve(__dirname, 'built');
10
+ const builder = path.resolve(__dirname, '../l10nBundleBuilder.js');
11
+ const outputDir = path.resolve(outputRoot, 'cli');
12
+
13
+ function runBuilder(args) {
14
+ return spawnSync(node, [builder, ...args], { cwd: __dirname });
15
+ }
16
+
17
+ beforeAll(() => fsx.removeSync(outputDir));
18
+
19
+ const args = [
20
+ `--rootDir=${path.resolve(__dirname, 'resources/nls')}`,
21
+ '--bundleName=app-strings.json',
22
+ '--locale=en-US',
23
+ `--outDir=${outputDir}`
24
+ ];
25
+ const omitArg = (name) => (arg) => !arg.startsWith(`--${name}=`);
26
+
27
+ const runFailureTest = (...names) => names.forEach(name =>
28
+ it(`fails with no ${name}`, () => {
29
+ const build = runBuilder(args.filter(omitArg(name)));
30
+ expect(build.output[2].toString()).toMatch(/^Usage:/);
31
+ expect(build.status).toEqual(1);
32
+ })
33
+ );
34
+
35
+ it('fails with no arguments', () => {
36
+ const build = runBuilder([]);
37
+ expect(build.output[2].toString()).toMatch(/^Usage:/);
38
+ expect(build.status).toEqual(1);
39
+ });
40
+
41
+ runFailureTest('rootDir', 'bundleName', 'locale', 'outDir');
42
+
43
+ it('succeeds with required arguments', () => {
44
+ const build = runBuilder(args);
45
+ expect(build.output[2].toString()).toEqual('');
46
+ expect(build.status).toEqual(0);
47
+ })
48
+ });
@@ -0,0 +1,9 @@
1
+ const pkg = require('../package.json');
2
+ const workspaceConfig = require('@oracle/oraclejet-test-utils/workspace-jest-config');
3
+
4
+ module.exports = workspaceConfig({
5
+ preset: 'ts-jest',
6
+ displayName: pkg.name,
7
+ rootDir: __dirname,
8
+ testMatch: [ '**/*Test.js', '**/*Test.ts' ]
9
+ });