@vocab/core 1.0.4 → 1.1.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.
- package/CHANGELOG.md +14 -0
- package/README.md +148 -1
- package/dist/declarations/src/generate-language.d.ts +5 -0
- package/dist/vocab-core.cjs.dev.js +136 -7
- package/dist/vocab-core.cjs.prod.js +136 -7
- package/dist/vocab-core.esm.js +136 -8
- package/package.json +8 -2
- package/src/ValidationError.ts +0 -9
- package/src/compile.ts +0 -329
- package/src/config.test.ts +0 -39
- package/src/config.ts +0 -144
- package/src/icu-handler.ts +0 -35
- package/src/index.ts +0 -14
- package/src/load-translations.test.ts +0 -165
- package/src/load-translations.ts +0 -351
- package/src/logger.ts +0 -9
- package/src/runtime.test.ts +0 -153
- package/src/runtime.ts +0 -12
- package/src/test-translations/en.translations.json +0 -9
- package/src/test-translations/th-TH.translations.json +0 -1
- package/src/test-translations/th.translations.json +0 -1
- package/src/translation-file.ts +0 -54
- package/src/utils.test.ts +0 -78
- package/src/utils.ts +0 -143
- package/src/validate/index.test.ts +0 -64
- package/src/validate/index.ts +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @vocab/core
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`87333d7`](https://github.com/seek-oss/vocab/commit/87333d79c4a883b07d7d8f2c272b16e2243c49bd) [#80](https://github.com/seek-oss/vocab/pull/80) Thanks [@askoufis](https://github.com/askoufis)! - Enable the creation of generated languages via the `generatedLanguages` config.
|
|
8
|
+
See [the docs] for more information and examples.
|
|
9
|
+
|
|
10
|
+
[the docs]: https://github.com/seek-oss/vocab#generated-languages
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [[`87333d7`](https://github.com/seek-oss/vocab/commit/87333d79c4a883b07d7d8f2c272b16e2243c49bd)]:
|
|
15
|
+
- @vocab/types@1.1.0
|
|
16
|
+
|
|
3
17
|
## 1.0.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -184,6 +184,14 @@ Configuration can either be passed into the Node API directly or be gathered fro
|
|
|
184
184
|
**vocab.config.js**
|
|
185
185
|
|
|
186
186
|
```js
|
|
187
|
+
function capitalize(element) {
|
|
188
|
+
return element.toUpperCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pad(message) {
|
|
192
|
+
return '[' + message + ']';
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
module.exports = {
|
|
188
196
|
devLanguage: 'en',
|
|
189
197
|
languages: [
|
|
@@ -192,11 +200,25 @@ module.exports = {
|
|
|
192
200
|
{ name: 'en-US', extends: 'en' },
|
|
193
201
|
{ name: 'fr-FR' }
|
|
194
202
|
],
|
|
203
|
+
/**
|
|
204
|
+
* An array of languages to generate based off translations for existing languages
|
|
205
|
+
* Default: []
|
|
206
|
+
*/
|
|
207
|
+
generatedLanguages: [
|
|
208
|
+
{
|
|
209
|
+
name: 'generatedLangauge',
|
|
210
|
+
extends: 'en',
|
|
211
|
+
generator: {
|
|
212
|
+
transformElement: capitalize,
|
|
213
|
+
transformMessage: pad
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
],
|
|
195
217
|
/**
|
|
196
218
|
* The root directory to compile and validate translations
|
|
197
219
|
* Default: Current working directory
|
|
198
220
|
*/
|
|
199
|
-
projectRoot: './example/'
|
|
221
|
+
projectRoot: './example/',
|
|
200
222
|
/**
|
|
201
223
|
* A custom suffix to name vocab translation directories
|
|
202
224
|
* Default: '.vocab'
|
|
@@ -209,6 +231,131 @@ module.exports = {
|
|
|
209
231
|
};
|
|
210
232
|
```
|
|
211
233
|
|
|
234
|
+
## Generated languages
|
|
235
|
+
|
|
236
|
+
Vocab supports the creation of generated languages via the `generatedLanguages` config.
|
|
237
|
+
|
|
238
|
+
Generated languages are created by running a message `generator` over every translation message in an existing translation.
|
|
239
|
+
A `generator` may contain a `transformElement` function, a `transformMessage` function, or both.
|
|
240
|
+
Both of these functions accept a single string parameter and return a string.
|
|
241
|
+
|
|
242
|
+
`transformElement` is applied to string literal values contained within `MessageFormatElement`s.
|
|
243
|
+
A `MessageFormatElement` is an object representing a node in the AST of a compiled translation message.
|
|
244
|
+
Simply put, any text that would end up being translated by a translator, i.e. anything that is not part of the [ICU Message syntax], will be passed to `transformElement`.
|
|
245
|
+
An example of a use case for this function would be adding [diacritics] to every letter in order to stress your UI from a vertical line-height perspective.
|
|
246
|
+
|
|
247
|
+
`transformMessage` receives the entire translation message _after_ `transformElement` has been applied to its individual elements.
|
|
248
|
+
An example of a use case for this function would be adding padding text to the start/end of your messages in order to easily identify which text in your app has not been extracted into a `translations.json` file.
|
|
249
|
+
|
|
250
|
+
By default, a generated language's messages will be based off the `devLanguage`'s messages, but this can be overridden by providing an `extends` value that references another language.
|
|
251
|
+
|
|
252
|
+
**vocab.config.js**
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
function capitalize(message) {
|
|
256
|
+
return message.toUpperCase();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pad(message) {
|
|
260
|
+
return '[' + message + ']';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
devLanguage: 'en',
|
|
265
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
266
|
+
generatedLanguages: [
|
|
267
|
+
{
|
|
268
|
+
name: 'generatedLanguage',
|
|
269
|
+
extends: 'en',
|
|
270
|
+
generator: {
|
|
271
|
+
transformElement: capitalize,
|
|
272
|
+
transformMessage: pad
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Generated languages are consumed the same way as regular languages.
|
|
280
|
+
Any Vocab API that accepts a `language` parameter will work with a generated language as well as a regular language.
|
|
281
|
+
|
|
282
|
+
**App.tsx**
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const App = () => (
|
|
286
|
+
<VocabProvider language="generatedLanguage">
|
|
287
|
+
...
|
|
288
|
+
</VocabProvider>
|
|
289
|
+
);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
[icu message syntax]: https://formatjs.io/docs/intl-messageformat/#message-syntax
|
|
293
|
+
[diacritics]: https://en.wikipedia.org/wiki/Diacritic
|
|
294
|
+
|
|
295
|
+
## Pseudo-localization
|
|
296
|
+
|
|
297
|
+
The `@vocab/pseudo-localize` package exports low-level functions that can be used for pseudo-localization of translation messages.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import {
|
|
301
|
+
extendVowels,
|
|
302
|
+
padString,
|
|
303
|
+
pseudoLocalize,
|
|
304
|
+
substituteCharacters
|
|
305
|
+
} from '@vocab/pseudo-localize';
|
|
306
|
+
|
|
307
|
+
const message = 'Hello';
|
|
308
|
+
|
|
309
|
+
// [Hello]
|
|
310
|
+
const paddedMessage = padString(message);
|
|
311
|
+
|
|
312
|
+
// Ḩẽƚƚö
|
|
313
|
+
const substitutedMessage = substituteCharacters(message);
|
|
314
|
+
|
|
315
|
+
// Heelloo
|
|
316
|
+
const extendedMessage = extendVowels(message);
|
|
317
|
+
|
|
318
|
+
// Extend the message and then substitute characters
|
|
319
|
+
// Ḩẽẽƚƚöö
|
|
320
|
+
const pseudoLocalizedMessage = pseudoLocalize(message);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Pseudo-localization is a transformation that can be applied to a translation message.
|
|
324
|
+
Vocab's implementation of this transformation contains the following elements:
|
|
325
|
+
|
|
326
|
+
- _Start and end markers (`padString`):_ All strings are encapsulated in `[` and `]`. If a developer doesn’t see these characters they know the string has been clipped by an inflexible UI element.
|
|
327
|
+
- _Transformation of ASCII characters to extended character equivalents (`substituteCharacters`):_ Stresses the UI from a vertical line-height perspective, tests font and encoding support, and weeds out strings that haven’t been externalized correctly (they will not have the pseudo-localization applied to them).
|
|
328
|
+
- _Padding text (`extendVowels`):_ Simulates translation-induced expansion. Vocab's implementation of this involves repeating vowels (and `y`) to simulate a 40% expansion in the message's length.
|
|
329
|
+
|
|
330
|
+
This Netflix technology [blog post][blog post] inspired Vocab's implementation of this
|
|
331
|
+
functionality.
|
|
332
|
+
|
|
333
|
+
### Generating a pseudo-localized language using Vocab
|
|
334
|
+
|
|
335
|
+
Vocab can generate a pseudo-localized language via the [`generatedLanguages` config][generated languages config], either via the webpack plugin or your `vocab.config.js` file.
|
|
336
|
+
`@vocab/pseudo-localize` exports a `generator` that can be used directly in your config.
|
|
337
|
+
|
|
338
|
+
**vocab.config.js**
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
const { generator } = require('@vocab/pseudo-localize');
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
devLanguage: 'en',
|
|
345
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
346
|
+
generatedLanguages: [
|
|
347
|
+
{
|
|
348
|
+
name: 'pseudo',
|
|
349
|
+
extends: 'en',
|
|
350
|
+
generator
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
[blog post]: https://netflixtechblog.com/pseudo-localization-netflix-12fff76fbcbe
|
|
357
|
+
[generated languages config]: #generated-languages
|
|
358
|
+
|
|
212
359
|
## Use without React
|
|
213
360
|
|
|
214
361
|
If you need to use Vocab outside of React, you can access the returned Vocab file directly. You'll then be responsible for when to load translations and how to update on translation load.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { MessageGenerator, TranslationsByKey } from '@vocab/types';
|
|
2
|
+
export declare function generateLanguageFromTranslations({ baseTranslations, generator, }: {
|
|
3
|
+
baseTranslations: TranslationsByKey<string>;
|
|
4
|
+
generator: MessageGenerator;
|
|
5
|
+
}): TranslationsByKey<string>;
|
|
@@ -10,6 +10,8 @@ var chokidar = require('chokidar');
|
|
|
10
10
|
var chalk = require('chalk');
|
|
11
11
|
var debug = require('debug');
|
|
12
12
|
var glob = require('fast-glob');
|
|
13
|
+
var IntlMessageFormat = require('intl-messageformat');
|
|
14
|
+
var printer = require('@formatjs/icu-messageformat-parser/printer');
|
|
13
15
|
var findUp = require('find-up');
|
|
14
16
|
var Validator = require('fastest-validator');
|
|
15
17
|
|
|
@@ -21,6 +23,7 @@ var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
|
|
|
21
23
|
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
|
|
22
24
|
var debug__default = /*#__PURE__*/_interopDefault(debug);
|
|
23
25
|
var glob__default = /*#__PURE__*/_interopDefault(glob);
|
|
26
|
+
var IntlMessageFormat__default = /*#__PURE__*/_interopDefault(IntlMessageFormat);
|
|
24
27
|
var findUp__default = /*#__PURE__*/_interopDefault(findUp);
|
|
25
28
|
var Validator__default = /*#__PURE__*/_interopDefault(Validator);
|
|
26
29
|
|
|
@@ -108,6 +111,71 @@ function getTranslationMessages(translations) {
|
|
|
108
111
|
return mapValues(translations, v => v.message);
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function generateLanguageFromTranslations({
|
|
115
|
+
baseTranslations,
|
|
116
|
+
generator
|
|
117
|
+
}) {
|
|
118
|
+
if (!generator.transformElement && !generator.transformMessage) {
|
|
119
|
+
return baseTranslations;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const translationKeys = Object.keys(baseTranslations);
|
|
123
|
+
const generatedTranslations = {};
|
|
124
|
+
|
|
125
|
+
for (const translationKey of translationKeys) {
|
|
126
|
+
const translation = baseTranslations[translationKey];
|
|
127
|
+
let transformedMessage = translation.message;
|
|
128
|
+
|
|
129
|
+
if (generator.transformElement) {
|
|
130
|
+
const messageAst = new IntlMessageFormat__default['default'](translation.message).getAst();
|
|
131
|
+
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement));
|
|
132
|
+
transformedMessage = printer.printAST(transformedAst);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (generator.transformMessage) {
|
|
136
|
+
transformedMessage = generator.transformMessage(transformedMessage);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
generatedTranslations[translationKey] = {
|
|
140
|
+
message: transformedMessage
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return generatedTranslations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function transformMessageFormatElement(transformElement) {
|
|
148
|
+
return messageFormatElement => {
|
|
149
|
+
const transformedMessageFormatElement = { ...messageFormatElement
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
switch (transformedMessageFormatElement.type) {
|
|
153
|
+
case icuMessageformatParser.TYPE.literal:
|
|
154
|
+
const transformedValue = transformElement(transformedMessageFormatElement.value);
|
|
155
|
+
transformedMessageFormatElement.value = transformedValue;
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case icuMessageformatParser.TYPE.select:
|
|
159
|
+
case icuMessageformatParser.TYPE.plural:
|
|
160
|
+
const transformedOptions = { ...transformedMessageFormatElement.options
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
for (const key of Object.keys(transformedOptions)) {
|
|
164
|
+
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case icuMessageformatParser.TYPE.tag:
|
|
170
|
+
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
|
|
171
|
+
transformedMessageFormatElement.children = transformedChildren;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return transformedMessageFormatElement;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
111
179
|
function getUniqueKey(key, namespace) {
|
|
112
180
|
return `${key}.${namespace}`;
|
|
113
181
|
}
|
|
@@ -236,17 +304,17 @@ function getTranslationsFromFile(translations, {
|
|
|
236
304
|
|
|
237
305
|
for (const [translationKey, translation] of Object.entries(keys)) {
|
|
238
306
|
if (typeof translation === 'string') {
|
|
239
|
-
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
307
|
+
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
240
308
|
continue;
|
|
241
309
|
}
|
|
242
310
|
|
|
243
311
|
if (!translation) {
|
|
244
|
-
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
312
|
+
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
245
313
|
continue;
|
|
246
314
|
}
|
|
247
315
|
|
|
248
316
|
if (!translation.message || typeof translation.message !== 'string') {
|
|
249
|
-
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
317
|
+
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
250
318
|
continue;
|
|
251
319
|
}
|
|
252
320
|
|
|
@@ -338,6 +406,19 @@ function loadTranslation({
|
|
|
338
406
|
}, userConfig);
|
|
339
407
|
}
|
|
340
408
|
|
|
409
|
+
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
410
|
+
const {
|
|
411
|
+
name: generatedLanguageName,
|
|
412
|
+
generator
|
|
413
|
+
} = generatedLanguage;
|
|
414
|
+
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage;
|
|
415
|
+
const baseTranslations = languageSet[baseLanguage];
|
|
416
|
+
languageSet[generatedLanguageName] = generateLanguageFromTranslations({
|
|
417
|
+
baseTranslations,
|
|
418
|
+
generator
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
341
422
|
return {
|
|
342
423
|
filePath,
|
|
343
424
|
keys: Object.keys(devTranslation),
|
|
@@ -709,6 +790,35 @@ const schema = {
|
|
|
709
790
|
}
|
|
710
791
|
}
|
|
711
792
|
},
|
|
793
|
+
generatedLanguages: {
|
|
794
|
+
type: 'array',
|
|
795
|
+
items: {
|
|
796
|
+
type: 'object',
|
|
797
|
+
props: {
|
|
798
|
+
name: {
|
|
799
|
+
type: 'string'
|
|
800
|
+
},
|
|
801
|
+
extends: {
|
|
802
|
+
type: 'string',
|
|
803
|
+
optional: true
|
|
804
|
+
},
|
|
805
|
+
generator: {
|
|
806
|
+
type: 'object',
|
|
807
|
+
props: {
|
|
808
|
+
transformElement: {
|
|
809
|
+
type: 'function',
|
|
810
|
+
optional: true
|
|
811
|
+
},
|
|
812
|
+
transformMessage: {
|
|
813
|
+
type: 'function',
|
|
814
|
+
optional: true
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
optional: true
|
|
821
|
+
},
|
|
712
822
|
translationsDirectorySuffix: {
|
|
713
823
|
type: 'string',
|
|
714
824
|
optional: true
|
|
@@ -746,13 +856,12 @@ function validateConfig(c) {
|
|
|
746
856
|
|
|
747
857
|
return v.message;
|
|
748
858
|
}).join(' \n'));
|
|
749
|
-
}
|
|
750
|
-
|
|
859
|
+
}
|
|
751
860
|
|
|
752
|
-
const languageStrings = c.languages.map(v => v.name);
|
|
861
|
+
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
|
|
753
862
|
|
|
754
863
|
if (!languageStrings.includes(c.devLanguage)) {
|
|
755
|
-
throw new ValidationError('InvalidDevLanguage', `
|
|
864
|
+
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
|
|
756
865
|
}
|
|
757
866
|
|
|
758
867
|
const foundLanguages = [];
|
|
@@ -770,6 +879,26 @@ function validateConfig(c) {
|
|
|
770
879
|
}
|
|
771
880
|
}
|
|
772
881
|
|
|
882
|
+
const foundGeneratedLanguages = [];
|
|
883
|
+
|
|
884
|
+
for (const generatedLang of c.generatedLanguages || []) {
|
|
885
|
+
// Generated languages must only exist once
|
|
886
|
+
if (foundGeneratedLanguages.includes(generatedLang.name)) {
|
|
887
|
+
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" was defined multiple times.`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
|
|
891
|
+
|
|
892
|
+
if (languageStrings.includes(generatedLang.name)) {
|
|
893
|
+
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" is already defined as a language.`);
|
|
894
|
+
} // Any extends must be in languages
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
|
|
898
|
+
throw new ValidationError('InvalidExtends', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}"'s extends of ${chalk__default['default'].bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
773
902
|
trace('Configuration file is valid');
|
|
774
903
|
return true;
|
|
775
904
|
}
|
|
@@ -10,6 +10,8 @@ var chokidar = require('chokidar');
|
|
|
10
10
|
var chalk = require('chalk');
|
|
11
11
|
var debug = require('debug');
|
|
12
12
|
var glob = require('fast-glob');
|
|
13
|
+
var IntlMessageFormat = require('intl-messageformat');
|
|
14
|
+
var printer = require('@formatjs/icu-messageformat-parser/printer');
|
|
13
15
|
var findUp = require('find-up');
|
|
14
16
|
var Validator = require('fastest-validator');
|
|
15
17
|
|
|
@@ -21,6 +23,7 @@ var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
|
|
|
21
23
|
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
|
|
22
24
|
var debug__default = /*#__PURE__*/_interopDefault(debug);
|
|
23
25
|
var glob__default = /*#__PURE__*/_interopDefault(glob);
|
|
26
|
+
var IntlMessageFormat__default = /*#__PURE__*/_interopDefault(IntlMessageFormat);
|
|
24
27
|
var findUp__default = /*#__PURE__*/_interopDefault(findUp);
|
|
25
28
|
var Validator__default = /*#__PURE__*/_interopDefault(Validator);
|
|
26
29
|
|
|
@@ -108,6 +111,71 @@ function getTranslationMessages(translations) {
|
|
|
108
111
|
return mapValues(translations, v => v.message);
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function generateLanguageFromTranslations({
|
|
115
|
+
baseTranslations,
|
|
116
|
+
generator
|
|
117
|
+
}) {
|
|
118
|
+
if (!generator.transformElement && !generator.transformMessage) {
|
|
119
|
+
return baseTranslations;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const translationKeys = Object.keys(baseTranslations);
|
|
123
|
+
const generatedTranslations = {};
|
|
124
|
+
|
|
125
|
+
for (const translationKey of translationKeys) {
|
|
126
|
+
const translation = baseTranslations[translationKey];
|
|
127
|
+
let transformedMessage = translation.message;
|
|
128
|
+
|
|
129
|
+
if (generator.transformElement) {
|
|
130
|
+
const messageAst = new IntlMessageFormat__default['default'](translation.message).getAst();
|
|
131
|
+
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement));
|
|
132
|
+
transformedMessage = printer.printAST(transformedAst);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (generator.transformMessage) {
|
|
136
|
+
transformedMessage = generator.transformMessage(transformedMessage);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
generatedTranslations[translationKey] = {
|
|
140
|
+
message: transformedMessage
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return generatedTranslations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function transformMessageFormatElement(transformElement) {
|
|
148
|
+
return messageFormatElement => {
|
|
149
|
+
const transformedMessageFormatElement = { ...messageFormatElement
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
switch (transformedMessageFormatElement.type) {
|
|
153
|
+
case icuMessageformatParser.TYPE.literal:
|
|
154
|
+
const transformedValue = transformElement(transformedMessageFormatElement.value);
|
|
155
|
+
transformedMessageFormatElement.value = transformedValue;
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case icuMessageformatParser.TYPE.select:
|
|
159
|
+
case icuMessageformatParser.TYPE.plural:
|
|
160
|
+
const transformedOptions = { ...transformedMessageFormatElement.options
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
for (const key of Object.keys(transformedOptions)) {
|
|
164
|
+
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case icuMessageformatParser.TYPE.tag:
|
|
170
|
+
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
|
|
171
|
+
transformedMessageFormatElement.children = transformedChildren;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return transformedMessageFormatElement;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
111
179
|
function getUniqueKey(key, namespace) {
|
|
112
180
|
return `${key}.${namespace}`;
|
|
113
181
|
}
|
|
@@ -236,17 +304,17 @@ function getTranslationsFromFile(translations, {
|
|
|
236
304
|
|
|
237
305
|
for (const [translationKey, translation] of Object.entries(keys)) {
|
|
238
306
|
if (typeof translation === 'string') {
|
|
239
|
-
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
307
|
+
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
240
308
|
continue;
|
|
241
309
|
}
|
|
242
310
|
|
|
243
311
|
if (!translation) {
|
|
244
|
-
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
312
|
+
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
245
313
|
continue;
|
|
246
314
|
}
|
|
247
315
|
|
|
248
316
|
if (!translation.message || typeof translation.message !== 'string') {
|
|
249
|
-
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
317
|
+
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
250
318
|
continue;
|
|
251
319
|
}
|
|
252
320
|
|
|
@@ -338,6 +406,19 @@ function loadTranslation({
|
|
|
338
406
|
}, userConfig);
|
|
339
407
|
}
|
|
340
408
|
|
|
409
|
+
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
410
|
+
const {
|
|
411
|
+
name: generatedLanguageName,
|
|
412
|
+
generator
|
|
413
|
+
} = generatedLanguage;
|
|
414
|
+
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage;
|
|
415
|
+
const baseTranslations = languageSet[baseLanguage];
|
|
416
|
+
languageSet[generatedLanguageName] = generateLanguageFromTranslations({
|
|
417
|
+
baseTranslations,
|
|
418
|
+
generator
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
341
422
|
return {
|
|
342
423
|
filePath,
|
|
343
424
|
keys: Object.keys(devTranslation),
|
|
@@ -709,6 +790,35 @@ const schema = {
|
|
|
709
790
|
}
|
|
710
791
|
}
|
|
711
792
|
},
|
|
793
|
+
generatedLanguages: {
|
|
794
|
+
type: 'array',
|
|
795
|
+
items: {
|
|
796
|
+
type: 'object',
|
|
797
|
+
props: {
|
|
798
|
+
name: {
|
|
799
|
+
type: 'string'
|
|
800
|
+
},
|
|
801
|
+
extends: {
|
|
802
|
+
type: 'string',
|
|
803
|
+
optional: true
|
|
804
|
+
},
|
|
805
|
+
generator: {
|
|
806
|
+
type: 'object',
|
|
807
|
+
props: {
|
|
808
|
+
transformElement: {
|
|
809
|
+
type: 'function',
|
|
810
|
+
optional: true
|
|
811
|
+
},
|
|
812
|
+
transformMessage: {
|
|
813
|
+
type: 'function',
|
|
814
|
+
optional: true
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
optional: true
|
|
821
|
+
},
|
|
712
822
|
translationsDirectorySuffix: {
|
|
713
823
|
type: 'string',
|
|
714
824
|
optional: true
|
|
@@ -746,13 +856,12 @@ function validateConfig(c) {
|
|
|
746
856
|
|
|
747
857
|
return v.message;
|
|
748
858
|
}).join(' \n'));
|
|
749
|
-
}
|
|
750
|
-
|
|
859
|
+
}
|
|
751
860
|
|
|
752
|
-
const languageStrings = c.languages.map(v => v.name);
|
|
861
|
+
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
|
|
753
862
|
|
|
754
863
|
if (!languageStrings.includes(c.devLanguage)) {
|
|
755
|
-
throw new ValidationError('InvalidDevLanguage', `
|
|
864
|
+
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk__default['default'].bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
|
|
756
865
|
}
|
|
757
866
|
|
|
758
867
|
const foundLanguages = [];
|
|
@@ -770,6 +879,26 @@ function validateConfig(c) {
|
|
|
770
879
|
}
|
|
771
880
|
}
|
|
772
881
|
|
|
882
|
+
const foundGeneratedLanguages = [];
|
|
883
|
+
|
|
884
|
+
for (const generatedLang of c.generatedLanguages || []) {
|
|
885
|
+
// Generated languages must only exist once
|
|
886
|
+
if (foundGeneratedLanguages.includes(generatedLang.name)) {
|
|
887
|
+
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" was defined multiple times.`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
|
|
891
|
+
|
|
892
|
+
if (languageStrings.includes(generatedLang.name)) {
|
|
893
|
+
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}" is already defined as a language.`);
|
|
894
|
+
} // Any extends must be in languages
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
|
|
898
|
+
throw new ValidationError('InvalidExtends', `The generated language "${chalk__default['default'].bold.cyan(generatedLang.name)}"'s extends of ${chalk__default['default'].bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
773
902
|
trace('Configuration file is valid');
|
|
774
903
|
return true;
|
|
775
904
|
}
|