@vocab/core 1.0.3 → 1.1.1

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.
@@ -1,11 +1,13 @@
1
1
  import { existsSync, promises } from 'fs';
2
2
  import path from 'path';
3
- import { parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
3
+ import { TYPE, parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
4
4
  import prettier from 'prettier';
5
5
  import chokidar from 'chokidar';
6
6
  import chalk from 'chalk';
7
7
  import debug from 'debug';
8
8
  import glob from 'fast-glob';
9
+ import IntlMessageFormat from 'intl-messageformat';
10
+ import { printAST } from '@formatjs/icu-messageformat-parser/printer';
9
11
  import findUp from 'find-up';
10
12
  import Validator from 'fastest-validator';
11
13
 
@@ -93,11 +95,78 @@ function getTranslationMessages(translations) {
93
95
  return mapValues(translations, v => v.message);
94
96
  }
95
97
 
98
+ function generateLanguageFromTranslations({
99
+ baseTranslations,
100
+ generator
101
+ }) {
102
+ if (!generator.transformElement && !generator.transformMessage) {
103
+ return baseTranslations;
104
+ }
105
+
106
+ const translationKeys = Object.keys(baseTranslations);
107
+ const generatedTranslations = {};
108
+
109
+ for (const translationKey of translationKeys) {
110
+ const translation = baseTranslations[translationKey];
111
+ let transformedMessage = translation.message;
112
+
113
+ if (generator.transformElement) {
114
+ const messageAst = new IntlMessageFormat(translation.message).getAst();
115
+ const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement));
116
+ transformedMessage = printAST(transformedAst);
117
+ }
118
+
119
+ if (generator.transformMessage) {
120
+ transformedMessage = generator.transformMessage(transformedMessage);
121
+ }
122
+
123
+ generatedTranslations[translationKey] = {
124
+ message: transformedMessage
125
+ };
126
+ }
127
+
128
+ return generatedTranslations;
129
+ }
130
+
131
+ function transformMessageFormatElement(transformElement) {
132
+ return messageFormatElement => {
133
+ const transformedMessageFormatElement = { ...messageFormatElement
134
+ };
135
+
136
+ switch (transformedMessageFormatElement.type) {
137
+ case TYPE.literal:
138
+ const transformedValue = transformElement(transformedMessageFormatElement.value);
139
+ transformedMessageFormatElement.value = transformedValue;
140
+ break;
141
+
142
+ case TYPE.select:
143
+ case TYPE.plural:
144
+ const transformedOptions = { ...transformedMessageFormatElement.options
145
+ };
146
+
147
+ for (const key of Object.keys(transformedOptions)) {
148
+ transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
149
+ }
150
+
151
+ break;
152
+
153
+ case TYPE.tag:
154
+ const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
155
+ transformedMessageFormatElement.children = transformedChildren;
156
+ break;
157
+ }
158
+
159
+ return transformedMessageFormatElement;
160
+ };
161
+ }
162
+
96
163
  function getUniqueKey(key, namespace) {
97
164
  return `${key}.${namespace}`;
98
165
  }
99
-
100
- function mergeWithDevLanguage(translation, devTranslation) {
166
+ function mergeWithDevLanguageTranslation({
167
+ translation,
168
+ devTranslation
169
+ }) {
101
170
  // Only use keys from the dev translation
102
171
  const keys = Object.keys(devTranslation);
103
172
  const newLanguage = {};
@@ -128,7 +197,7 @@ function getLanguageFallbacks({
128
197
  return languageFallbackMap;
129
198
  }
130
199
 
131
- function getLanguageHierarcy({
200
+ function getLanguageHierarchy({
132
201
  languages
133
202
  }) {
134
203
  const hierarchyMap = new Map();
@@ -137,19 +206,45 @@ function getLanguageHierarcy({
137
206
  });
138
207
 
139
208
  for (const lang of languages) {
140
- const langHierachy = [];
209
+ const langHierarchy = [];
141
210
  let currLang = lang.extends;
142
211
 
143
212
  while (currLang) {
144
- langHierachy.push(currLang);
213
+ langHierarchy.push(currLang);
145
214
  currLang = fallbacks.get(currLang);
146
215
  }
147
216
 
148
- hierarchyMap.set(lang.name, langHierachy);
217
+ hierarchyMap.set(lang.name, langHierarchy);
149
218
  }
150
219
 
151
220
  return hierarchyMap;
152
221
  }
222
+ function getFallbackLanguageOrder({
223
+ languages,
224
+ languageName,
225
+ devLanguage,
226
+ fallbacks
227
+ }) {
228
+ const languageHierarchy = getLanguageHierarchy({
229
+ languages
230
+ }).get(languageName);
231
+
232
+ if (!languageHierarchy) {
233
+ throw new Error(`Missing language hierarchy for ${languageName}`);
234
+ }
235
+
236
+ const fallbackLanguageOrder = [languageName];
237
+
238
+ if (fallbacks !== 'none') {
239
+ fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
240
+
241
+ if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {
242
+ fallbackLanguageOrder.unshift(devLanguage);
243
+ }
244
+ }
245
+
246
+ return fallbackLanguageOrder;
247
+ }
153
248
 
154
249
  function getNamespaceByFilePath(relativePath, {
155
250
  translationsDirectorySuffix = defaultTranslationDirSuffix
@@ -193,17 +288,17 @@ function getTranslationsFromFile(translations, {
193
288
 
194
289
  for (const [translationKey, translation] of Object.entries(keys)) {
195
290
  if (typeof translation === 'string') {
196
- printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`);
291
+ printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
197
292
  continue;
198
293
  }
199
294
 
200
295
  if (!translation) {
201
- printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`);
296
+ printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
202
297
  continue;
203
298
  }
204
299
 
205
300
  if (!translation.message || typeof translation.message !== 'string') {
206
- printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`);
301
+ printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
207
302
  continue;
208
303
  }
209
304
 
@@ -225,54 +320,44 @@ function loadAltLanguageFile({
225
320
  devLanguage,
226
321
  languages
227
322
  }) {
228
- const result = {};
229
- const languageHierarchy = getLanguageHierarcy({
230
- languages
231
- }).get(languageName);
232
-
233
- if (!languageHierarchy) {
234
- throw new Error(`Missing language hierarchy for ${languageName}`);
235
- }
236
-
237
- const fallbackLanguages = [languageName];
238
-
239
- if (fallbacks !== 'none') {
240
- fallbackLanguages.unshift(...languageHierarchy);
241
-
242
- if (fallbacks === 'all' && fallbackLanguages[0] !== devLanguage) {
243
- fallbackLanguages.unshift(devLanguage);
244
- }
245
- }
246
-
247
- trace(`Loading alt language file with precendence: ${fallbackLanguages.slice().reverse().join(' -> ')}`);
323
+ const altLanguageTranslation = {};
324
+ const fallbackLanguageOrder = getFallbackLanguageOrder({
325
+ languages,
326
+ languageName,
327
+ devLanguage,
328
+ fallbacks
329
+ });
330
+ trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
248
331
 
249
- for (const fallbackLang of fallbackLanguages) {
250
- if (fallbackLang !== devLanguage) {
332
+ for (const fallbackLanguage of fallbackLanguageOrder) {
333
+ if (fallbackLanguage !== devLanguage) {
251
334
  try {
252
- const altFilePath = getAltLanguageFilePath(filePath, fallbackLang);
335
+ const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
253
336
  delete require.cache[altFilePath];
254
337
 
255
338
  const translationFile = require(altFilePath);
256
339
 
257
340
  const {
258
- keys
341
+ keys: fallbackLanguageTranslation
259
342
  } = getTranslationsFromFile(translationFile, {
260
343
  filePath: altFilePath,
261
344
  isAltLanguage: true
262
345
  });
263
- Object.assign(result, mergeWithDevLanguage(keys, devTranslation));
346
+ Object.assign(altLanguageTranslation, mergeWithDevLanguageTranslation({
347
+ translation: fallbackLanguageTranslation,
348
+ devTranslation
349
+ }));
264
350
  } catch (e) {
265
- trace(`Missing alt language file ${getAltLanguageFilePath(filePath, fallbackLang)}
351
+ trace(`Missing alt language file ${getAltLanguageFilePath(filePath, fallbackLanguage)}
266
352
  `);
267
353
  }
268
354
  } else {
269
- Object.assign(result, devTranslation);
355
+ Object.assign(altLanguageTranslation, devTranslation);
270
356
  }
271
357
  }
272
358
 
273
- return result;
359
+ return altLanguageTranslation;
274
360
  }
275
-
276
361
  function loadTranslation({
277
362
  filePath,
278
363
  fallbacks
@@ -305,6 +390,19 @@ function loadTranslation({
305
390
  }, userConfig);
306
391
  }
307
392
 
393
+ for (const generatedLanguage of userConfig.generatedLanguages || []) {
394
+ const {
395
+ name: generatedLanguageName,
396
+ generator
397
+ } = generatedLanguage;
398
+ const baseLanguage = generatedLanguage.extends || userConfig.devLanguage;
399
+ const baseTranslations = languageSet[baseLanguage];
400
+ languageSet[generatedLanguageName] = generateLanguageFromTranslations({
401
+ baseTranslations,
402
+ generator
403
+ });
404
+ }
405
+
308
406
  return {
309
407
  filePath,
310
408
  keys: Object.keys(devTranslation),
@@ -588,7 +686,7 @@ async function writeIfChanged(filepath, contents) {
588
686
  }
589
687
 
590
688
  /* eslint-disable no-console */
591
- function findMissingKeys(loadedTranslation, devLanguageName, altLangauges) {
689
+ function findMissingKeys(loadedTranslation, devLanguageName, altLanguages) {
592
690
  const devLanguage = loadedTranslation.languages[devLanguageName];
593
691
 
594
692
  if (!devLanguage) {
@@ -600,7 +698,7 @@ function findMissingKeys(loadedTranslation, devLanguageName, altLangauges) {
600
698
  const requiredKeys = Object.keys(devLanguage);
601
699
 
602
700
  if (requiredKeys.length > 0) {
603
- for (const altLanguageName of altLangauges) {
701
+ for (const altLanguageName of altLanguages) {
604
702
  var _loadedTranslation$la;
605
703
 
606
704
  const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
@@ -676,6 +774,35 @@ const schema = {
676
774
  }
677
775
  }
678
776
  },
777
+ generatedLanguages: {
778
+ type: 'array',
779
+ items: {
780
+ type: 'object',
781
+ props: {
782
+ name: {
783
+ type: 'string'
784
+ },
785
+ extends: {
786
+ type: 'string',
787
+ optional: true
788
+ },
789
+ generator: {
790
+ type: 'object',
791
+ props: {
792
+ transformElement: {
793
+ type: 'function',
794
+ optional: true
795
+ },
796
+ transformMessage: {
797
+ type: 'function',
798
+ optional: true
799
+ }
800
+ }
801
+ }
802
+ }
803
+ },
804
+ optional: true
805
+ },
679
806
  translationsDirectorySuffix: {
680
807
  type: 'string',
681
808
  optional: true
@@ -713,13 +840,12 @@ function validateConfig(c) {
713
840
 
714
841
  return v.message;
715
842
  }).join(' \n'));
716
- } // Dev Language should exist in languages
717
-
843
+ }
718
844
 
719
- const languageStrings = c.languages.map(v => v.name);
845
+ const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
720
846
 
721
847
  if (!languageStrings.includes(c.devLanguage)) {
722
- throw new ValidationError('InvalidDevLanguage', `InvalidDevLanguage: The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
848
+ throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
723
849
  }
724
850
 
725
851
  const foundLanguages = [];
@@ -737,6 +863,26 @@ function validateConfig(c) {
737
863
  }
738
864
  }
739
865
 
866
+ const foundGeneratedLanguages = [];
867
+
868
+ for (const generatedLang of c.generatedLanguages || []) {
869
+ // Generated languages must only exist once
870
+ if (foundGeneratedLanguages.includes(generatedLang.name)) {
871
+ throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" was defined multiple times.`);
872
+ }
873
+
874
+ foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
875
+
876
+ if (languageStrings.includes(generatedLang.name)) {
877
+ throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" is already defined as a language.`);
878
+ } // Any extends must be in languages
879
+
880
+
881
+ if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
882
+ throw new ValidationError('InvalidExtends', `The generated language "${chalk.bold.cyan(generatedLang.name)}"'s extends of ${chalk.bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
883
+ }
884
+ }
885
+
740
886
  trace('Configuration file is valid');
741
887
  return true;
742
888
  }
package/package.json CHANGED
@@ -1,8 +1,27 @@
1
1
  {
2
2
  "name": "@vocab/core",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "main": "dist/vocab-core.cjs.js",
5
5
  "module": "dist/vocab-core.esm.js",
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ ".": {
9
+ "module": "./dist/vocab-core.esm.js",
10
+ "default": "./dist/vocab-core.cjs.js"
11
+ },
12
+ "./icu-handler": {
13
+ "module": "./icu-handler/dist/vocab-core-icu-handler.esm.js",
14
+ "default": "./icu-handler/dist/vocab-core-icu-handler.cjs.js"
15
+ },
16
+ "./runtime": {
17
+ "module": "./runtime/dist/vocab-core-runtime.esm.js",
18
+ "default": "./runtime/dist/vocab-core-runtime.cjs.js"
19
+ },
20
+ "./translation-file": {
21
+ "module": "./translation-file/dist/vocab-core-translation-file.esm.js",
22
+ "default": "./translation-file/dist/vocab-core-translation-file.cjs.js"
23
+ }
24
+ },
6
25
  "author": "SEEK",
7
26
  "license": "MIT",
8
27
  "preconstruct": {
@@ -13,9 +32,15 @@
13
32
  "translation-file.ts"
14
33
  ]
15
34
  },
35
+ "files": [
36
+ "dist",
37
+ "runtime",
38
+ "icu-handler",
39
+ "translation-file"
40
+ ],
16
41
  "dependencies": {
17
42
  "@formatjs/icu-messageformat-parser": "^2.0.10",
18
- "@vocab/types": "^1.0.1",
43
+ "@vocab/types": "^1.1.0",
19
44
  "chalk": "^4.1.0",
20
45
  "chokidar": "^3.4.3",
21
46
  "debug": "^4.3.1",
@@ -1,9 +0,0 @@
1
- export class ValidationError extends Error {
2
- code: string;
3
- rawMessage: string;
4
- constructor(code: string, message: string) {
5
- super(`Invalid vocab.config.js: ${code} - ${message}`);
6
- this.code = code;
7
- this.rawMessage = message;
8
- }
9
- }