@vocab/core 0.0.0-intl-dep-bump-202301323944

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 (37) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/dist/declarations/src/ValidationError.d.ts +5 -0
  3. package/dist/declarations/src/compile.d.ts +6 -0
  4. package/dist/declarations/src/config.d.ts +4 -0
  5. package/dist/declarations/src/generate-language.d.ts +5 -0
  6. package/dist/declarations/src/icu-handler.d.ts +2 -0
  7. package/dist/declarations/src/index.d.ts +6 -0
  8. package/dist/declarations/src/load-translations.d.ts +30 -0
  9. package/dist/declarations/src/logger.d.ts +3 -0
  10. package/dist/declarations/src/runtime.d.ts +3 -0
  11. package/dist/declarations/src/translation-file.d.ts +2 -0
  12. package/dist/declarations/src/utils.d.ts +26 -0
  13. package/dist/declarations/src/validate/index.d.ts +3 -0
  14. package/dist/vocab-core.cjs.d.ts +1 -0
  15. package/dist/vocab-core.cjs.dev.js +948 -0
  16. package/dist/vocab-core.cjs.js +7 -0
  17. package/dist/vocab-core.cjs.prod.js +948 -0
  18. package/dist/vocab-core.esm.js +921 -0
  19. package/icu-handler/dist/vocab-core-icu-handler.cjs.d.ts +1 -0
  20. package/icu-handler/dist/vocab-core-icu-handler.cjs.dev.js +34 -0
  21. package/icu-handler/dist/vocab-core-icu-handler.cjs.js +7 -0
  22. package/icu-handler/dist/vocab-core-icu-handler.cjs.prod.js +34 -0
  23. package/icu-handler/dist/vocab-core-icu-handler.esm.js +26 -0
  24. package/icu-handler/package.json +4 -0
  25. package/package.json +57 -0
  26. package/runtime/dist/vocab-core-runtime.cjs.d.ts +1 -0
  27. package/runtime/dist/vocab-core-runtime.cjs.dev.js +15 -0
  28. package/runtime/dist/vocab-core-runtime.cjs.js +7 -0
  29. package/runtime/dist/vocab-core-runtime.cjs.prod.js +15 -0
  30. package/runtime/dist/vocab-core-runtime.esm.js +10 -0
  31. package/runtime/package.json +4 -0
  32. package/translation-file/dist/vocab-core-translation-file.cjs.d.ts +1 -0
  33. package/translation-file/dist/vocab-core-translation-file.cjs.dev.js +43 -0
  34. package/translation-file/dist/vocab-core-translation-file.cjs.js +7 -0
  35. package/translation-file/dist/vocab-core-translation-file.cjs.prod.js +43 -0
  36. package/translation-file/dist/vocab-core-translation-file.esm.js +39 -0
  37. package/translation-file/package.json +4 -0
@@ -0,0 +1,921 @@
1
+ import { existsSync, promises } from 'fs';
2
+ import path from 'path';
3
+ import { TYPE, parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
4
+ import prettier from 'prettier';
5
+ import chokidar from 'chokidar';
6
+ import chalk from 'chalk';
7
+ import debug from 'debug';
8
+ import glob from 'fast-glob';
9
+ import IntlMessageFormat from 'intl-messageformat';
10
+ import { printAST } from '@formatjs/icu-messageformat-parser/printer';
11
+ import findUp from 'find-up';
12
+ import Validator from 'fastest-validator';
13
+
14
+ const trace = debug(`vocab:core`);
15
+
16
+ const defaultTranslationDirSuffix = '.vocab';
17
+ const devTranslationFileName = 'translations.json';
18
+ const globAnyPathWithOptionalPrefix = '**/?(*)';
19
+ function isDevLanguageFile(filePath) {
20
+ return filePath.endsWith(`/${devTranslationFileName}`) || filePath === devTranslationFileName;
21
+ }
22
+ function isAltLanguageFile(filePath) {
23
+ return filePath.endsWith('.translations.json');
24
+ }
25
+ function isTranslationDirectory(filePath, {
26
+ translationsDirectorySuffix = defaultTranslationDirSuffix
27
+ }) {
28
+ return filePath.endsWith(translationsDirectorySuffix);
29
+ }
30
+ function getTranslationFolderGlob({
31
+ translationsDirectorySuffix = defaultTranslationDirSuffix
32
+ }) {
33
+ const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}`;
34
+ trace('getTranslationFolderGlob', result);
35
+ return result;
36
+ }
37
+ function getDevTranslationFileGlob({
38
+ translationsDirectorySuffix = defaultTranslationDirSuffix
39
+ }) {
40
+ const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}/${devTranslationFileName}`;
41
+ trace('getDevTranslationFileGlob', result);
42
+ return result;
43
+ }
44
+ function getAltTranslationFileGlob(config) {
45
+ const altLanguages = getAltLanguages(config);
46
+ const langMatch = altLanguages.length === 1 ? altLanguages[0] : `{${altLanguages.join(',')}}`;
47
+ const {
48
+ translationsDirectorySuffix = defaultTranslationDirSuffix
49
+ } = config;
50
+ const result = `**/*${translationsDirectorySuffix}/${langMatch}.translations.json`;
51
+ trace('getAltTranslationFileGlob', result);
52
+ return result;
53
+ }
54
+ function getAltLanguages({
55
+ devLanguage,
56
+ languages
57
+ }) {
58
+ return languages.map(v => v.name).filter(lang => lang !== devLanguage);
59
+ }
60
+ function getDevLanguageFileFromTsFile(tsFilePath) {
61
+ const directory = path.dirname(tsFilePath);
62
+ const result = path.normalize(path.join(directory, devTranslationFileName));
63
+ trace(`Returning dev language path ${result} for path ${tsFilePath}`);
64
+ return result;
65
+ }
66
+ function getDevLanguageFileFromAltLanguageFile(altLanguageFilePath) {
67
+ const directory = path.dirname(altLanguageFilePath);
68
+ const result = path.normalize(path.join(directory, devTranslationFileName));
69
+ trace(`Returning dev language path ${result} for path ${altLanguageFilePath}`);
70
+ return result;
71
+ }
72
+ function getTSFileFromDevLanguageFile(devLanguageFilePath) {
73
+ const directory = path.dirname(devLanguageFilePath);
74
+ const result = path.normalize(path.join(directory, 'index.ts'));
75
+ trace(`Returning TS path ${result} for path ${devLanguageFilePath}`);
76
+ return result;
77
+ }
78
+ function getAltLanguageFilePath(devLanguageFilePath, language) {
79
+ const directory = path.dirname(devLanguageFilePath);
80
+ const result = path.normalize(path.join(directory, `${language}.translations.json`));
81
+ trace(`Returning alt language path ${result} for path ${devLanguageFilePath}`);
82
+ return path.normalize(result);
83
+ }
84
+ function mapValues(obj, func) {
85
+ const newObj = {};
86
+ const keys = Object.keys(obj);
87
+
88
+ for (const key of keys) {
89
+ newObj[key] = func(obj[key]);
90
+ }
91
+
92
+ return newObj;
93
+ }
94
+ function getTranslationMessages(translations) {
95
+ return mapValues(translations, v => v.message);
96
+ }
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
+
163
+ function getUniqueKey(key, namespace) {
164
+ return `${key}.${namespace}`;
165
+ }
166
+ function mergeWithDevLanguageTranslation({
167
+ translation,
168
+ devTranslation
169
+ }) {
170
+ // Only use keys from the dev translation
171
+ const keys = Object.keys(devTranslation);
172
+ const newLanguage = {};
173
+
174
+ for (const key of keys) {
175
+ if (translation[key]) {
176
+ newLanguage[key] = {
177
+ message: translation[key].message,
178
+ description: devTranslation[key].description
179
+ };
180
+ }
181
+ }
182
+
183
+ return newLanguage;
184
+ }
185
+
186
+ function getLanguageFallbacks({
187
+ languages
188
+ }) {
189
+ const languageFallbackMap = new Map();
190
+
191
+ for (const lang of languages) {
192
+ if (lang.extends) {
193
+ languageFallbackMap.set(lang.name, lang.extends);
194
+ }
195
+ }
196
+
197
+ return languageFallbackMap;
198
+ }
199
+
200
+ function getLanguageHierarchy({
201
+ languages
202
+ }) {
203
+ const hierarchyMap = new Map();
204
+ const fallbacks = getLanguageFallbacks({
205
+ languages
206
+ });
207
+
208
+ for (const lang of languages) {
209
+ const langHierarchy = [];
210
+ let currLang = lang.extends;
211
+
212
+ while (currLang) {
213
+ langHierarchy.push(currLang);
214
+ currLang = fallbacks.get(currLang);
215
+ }
216
+
217
+ hierarchyMap.set(lang.name, langHierarchy);
218
+ }
219
+
220
+ return hierarchyMap;
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
+ }
248
+
249
+ function getNamespaceByFilePath(relativePath, {
250
+ translationsDirectorySuffix = defaultTranslationDirSuffix
251
+ }) {
252
+ let namespace = path.dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
253
+
254
+ if (namespace.endsWith(translationsDirectorySuffix)) {
255
+ namespace = namespace.slice(0, -translationsDirectorySuffix.length);
256
+ }
257
+
258
+ return namespace;
259
+ }
260
+
261
+ function printValidationError(...params) {
262
+ // eslint-disable-next-line no-console
263
+ console.error(chalk.red('Error loading translation:'), ...params);
264
+ }
265
+
266
+ function getTranslationsFromFile(translations, {
267
+ isAltLanguage,
268
+ filePath
269
+ }) {
270
+ if (!translations || typeof translations !== 'object') {
271
+ throw new Error(`Unable to read translation file ${filePath}. Translations must be an object`);
272
+ }
273
+
274
+ const {
275
+ $namespace,
276
+ ...keys
277
+ } = translations;
278
+
279
+ if (isAltLanguage && $namespace) {
280
+ printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
281
+ }
282
+
283
+ if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
284
+ printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
285
+ }
286
+
287
+ const validKeys = {};
288
+
289
+ for (const [translationKey, translation] of Object.entries(keys)) {
290
+ if (typeof translation === 'string') {
291
+ printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
292
+ continue;
293
+ }
294
+
295
+ if (!translation) {
296
+ printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
297
+ continue;
298
+ }
299
+
300
+ if (!translation.message || typeof translation.message !== 'string') {
301
+ printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
302
+ continue;
303
+ }
304
+
305
+ validKeys[translationKey] = translation;
306
+ }
307
+
308
+ return {
309
+ $namespace,
310
+ keys: validKeys
311
+ };
312
+ }
313
+
314
+ function loadAltLanguageFile({
315
+ filePath,
316
+ languageName,
317
+ devTranslation,
318
+ fallbacks
319
+ }, {
320
+ devLanguage,
321
+ languages
322
+ }) {
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(' -> ')}`);
331
+
332
+ for (const fallbackLanguage of fallbackLanguageOrder) {
333
+ if (fallbackLanguage !== devLanguage) {
334
+ try {
335
+ const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
336
+ delete require.cache[altFilePath];
337
+
338
+ const translationFile = require(altFilePath);
339
+
340
+ const {
341
+ keys: fallbackLanguageTranslation
342
+ } = getTranslationsFromFile(translationFile, {
343
+ filePath: altFilePath,
344
+ isAltLanguage: true
345
+ });
346
+ Object.assign(altLanguageTranslation, mergeWithDevLanguageTranslation({
347
+ translation: fallbackLanguageTranslation,
348
+ devTranslation
349
+ }));
350
+ } catch (e) {
351
+ trace(`Missing alt language file ${getAltLanguageFilePath(filePath, fallbackLanguage)}
352
+ `);
353
+ }
354
+ } else {
355
+ Object.assign(altLanguageTranslation, devTranslation);
356
+ }
357
+ }
358
+
359
+ return altLanguageTranslation;
360
+ }
361
+ function loadTranslation({
362
+ filePath,
363
+ fallbacks
364
+ }, userConfig) {
365
+ trace(`Loading translation file in "${fallbacks}" fallback mode: "${filePath}"`);
366
+ const languageSet = {};
367
+ delete require.cache[filePath];
368
+
369
+ const translationContent = require(filePath);
370
+
371
+ const relativePath = path.relative(userConfig.projectRoot || process.cwd(), filePath);
372
+ const {
373
+ $namespace,
374
+ keys: devTranslation
375
+ } = getTranslationsFromFile(translationContent, {
376
+ filePath,
377
+ isAltLanguage: false
378
+ });
379
+ const namespace = typeof $namespace === 'string' ? $namespace : getNamespaceByFilePath(relativePath, userConfig);
380
+ trace(`Found file ${filePath}. Using namespace ${namespace}`);
381
+ languageSet[userConfig.devLanguage] = devTranslation;
382
+ const altLanguages = getAltLanguages(userConfig);
383
+
384
+ for (const languageName of altLanguages) {
385
+ languageSet[languageName] = loadAltLanguageFile({
386
+ filePath,
387
+ languageName,
388
+ devTranslation,
389
+ fallbacks
390
+ }, userConfig);
391
+ }
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
+
406
+ return {
407
+ filePath,
408
+ keys: Object.keys(devTranslation),
409
+ namespace,
410
+ relativePath,
411
+ languages: languageSet
412
+ };
413
+ }
414
+ async function loadAllTranslations({
415
+ fallbacks,
416
+ includeNodeModules
417
+ }, config) {
418
+ const {
419
+ projectRoot,
420
+ ignore = []
421
+ } = config;
422
+ const translationFiles = await glob(getDevTranslationFileGlob(config), {
423
+ ignore: includeNodeModules ? ignore : [...ignore, '**/node_modules/**'],
424
+ absolute: true,
425
+ cwd: projectRoot
426
+ });
427
+ trace(`Found ${translationFiles.length} translation files`);
428
+ const result = await Promise.all(translationFiles.map(filePath => loadTranslation({
429
+ filePath,
430
+ fallbacks
431
+ }, config)));
432
+ const keys = new Set();
433
+
434
+ for (const loadedTranslation of result) {
435
+ for (const key of loadedTranslation.keys) {
436
+ const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
437
+
438
+ if (keys.has(uniqueKey)) {
439
+ trace(`Duplicate keys found`);
440
+ throw new Error(`Duplicate keys found. Key with namespace ${loadedTranslation.namespace} and key ${key} was found multiple times.`);
441
+ }
442
+
443
+ keys.add(uniqueKey);
444
+ }
445
+ }
446
+
447
+ return result;
448
+ }
449
+
450
+ const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
451
+
452
+ const encodeBackslash = v => v.replace(/\\/g, '\\\\');
453
+
454
+ function extractHasTags(ast) {
455
+ return ast.some(element => {
456
+ if (isSelectElement(element)) {
457
+ const children = Object.values(element.options).map(o => o.value);
458
+ return children.some(child => extractHasTags(child));
459
+ }
460
+
461
+ return isTagElement(element);
462
+ });
463
+ }
464
+
465
+ function extractParamTypes(ast) {
466
+ let params = {};
467
+ let imports = new Set();
468
+
469
+ for (const element of ast) {
470
+ if (isArgumentElement(element)) {
471
+ params[element.value] = 'string';
472
+ } else if (isNumberElement(element)) {
473
+ params[element.value] = 'number';
474
+ } else if (isPluralElement(element)) {
475
+ params[element.value] = 'number';
476
+ } else if (isDateElement(element) || isTimeElement(element)) {
477
+ params[element.value] = 'Date | number';
478
+ } else if (isTagElement(element)) {
479
+ params[element.value] = 'FormatXMLElementFn<T>';
480
+ imports.add(`import { FormatXMLElementFn } from '@vocab/types';`);
481
+ const [subParams, subImports] = extractParamTypes(element.children);
482
+ imports = new Set([...imports, ...subImports]);
483
+ params = { ...params,
484
+ ...subParams
485
+ };
486
+ } else if (isSelectElement(element)) {
487
+ params[element.value] = Object.keys(element.options).map(o => `'${o}'`).join(' | ');
488
+ const children = Object.values(element.options).map(o => o.value);
489
+
490
+ for (const child of children) {
491
+ const [subParams, subImports] = extractParamTypes(child);
492
+ imports = new Set([...imports, ...subImports]);
493
+ params = { ...params,
494
+ ...subParams
495
+ };
496
+ }
497
+ }
498
+ }
499
+
500
+ return [params, imports];
501
+ }
502
+
503
+ function serialiseObjectToType(v) {
504
+ let result = '';
505
+
506
+ for (const [key, value] of Object.entries(v)) {
507
+ if (value && typeof value === 'object') {
508
+ result += `'${encodeWithinSingleQuotes(key)}': ${serialiseObjectToType(value)},`;
509
+ } else {
510
+ result += `'${encodeWithinSingleQuotes(key)}': ${value},`;
511
+ }
512
+ }
513
+
514
+ return `{ ${result} }`;
515
+ }
516
+
517
+ const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
518
+
519
+ function serialiseTranslationRuntime(value, imports, loadedTranslation) {
520
+ trace('Serialising translations:', loadedTranslation);
521
+ const translationsType = {};
522
+
523
+ for (const [key, {
524
+ params,
525
+ message,
526
+ hasTags
527
+ }] of value.entries()) {
528
+ let translationFunctionString = `() => ${message}`;
529
+
530
+ if (Object.keys(params).length > 0) {
531
+ const formatGeneric = hasTags ? '<T = string>' : '';
532
+ const formatReturn = hasTags ? 'string | T | Array<string | T>' : 'string';
533
+ translationFunctionString = `${formatGeneric}(values: ${serialiseObjectToType(params)}) => ${formatReturn}`;
534
+ }
535
+
536
+ translationsType[encodeBackslash(key)] = translationFunctionString;
537
+ }
538
+
539
+ const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');
540
+ const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');
541
+ return `${banner}
542
+
543
+ ${Array.from(imports).join('\n')}
544
+ import { createLanguage, createTranslationFile } from '@vocab/core/runtime';
545
+
546
+ const translations = createTranslationFile<${languagesUnionAsString}, ${serialiseObjectToType(translationsType)}>({${content}});
547
+
548
+ export default translations;`;
549
+ }
550
+
551
+ async function generateRuntime(loadedTranslation) {
552
+ const {
553
+ languages: loadedLanguages,
554
+ filePath
555
+ } = loadedTranslation;
556
+ trace('Generating types for', loadedTranslation.filePath);
557
+ const translationTypes = new Map();
558
+ let imports = new Set();
559
+
560
+ for (const key of loadedTranslation.keys) {
561
+ let params = {};
562
+ const messages = new Set();
563
+ let hasTags = false;
564
+
565
+ for (const translatedLanguage of Object.values(loadedLanguages)) {
566
+ if (translatedLanguage[key]) {
567
+ const ast = parse(translatedLanguage[key].message);
568
+ hasTags = hasTags || extractHasTags(ast);
569
+ const [parsedParams, parsedImports] = extractParamTypes(ast);
570
+ imports = new Set([...imports, ...parsedImports]);
571
+ params = { ...params,
572
+ ...parsedParams
573
+ };
574
+ messages.add(`'${encodeWithinSingleQuotes(translatedLanguage[key].message)}'`);
575
+ }
576
+ }
577
+
578
+ const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';
579
+ translationTypes.set(key, {
580
+ params,
581
+ hasTags,
582
+ message: Array.from(messages).join(' | '),
583
+ returnType
584
+ });
585
+ }
586
+
587
+ const prettierConfig = await prettier.resolveConfig(filePath);
588
+ const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
589
+ const declaration = prettier.format(serializedTranslationType, { ...prettierConfig,
590
+ parser: 'typescript'
591
+ });
592
+ const outputFilePath = getTSFileFromDevLanguageFile(filePath);
593
+ trace(`Writing translation types to ${outputFilePath}`);
594
+ await writeIfChanged(outputFilePath, declaration);
595
+ }
596
+ function watch(config) {
597
+ const cwd = config.projectRoot || process.cwd();
598
+ const watcher = chokidar.watch([getDevTranslationFileGlob(config), getAltTranslationFileGlob(config), getTranslationFolderGlob(config)], {
599
+ cwd,
600
+ ignored: config.ignore ? [...config.ignore, '**/node_modules/**'] : ['**/node_modules/**'],
601
+ ignoreInitial: true
602
+ });
603
+
604
+ const onTranslationChange = async relativePath => {
605
+ trace(`Detected change for file ${relativePath}`);
606
+ let targetFile;
607
+
608
+ if (isDevLanguageFile(relativePath)) {
609
+ targetFile = path.resolve(cwd, relativePath);
610
+ } else if (isAltLanguageFile(relativePath)) {
611
+ targetFile = getDevLanguageFileFromAltLanguageFile(path.resolve(cwd, relativePath));
612
+ }
613
+
614
+ if (targetFile) {
615
+ try {
616
+ const loadedTranslation = await loadTranslation({
617
+ filePath: targetFile,
618
+ fallbacks: 'all'
619
+ }, config);
620
+ await generateRuntime(loadedTranslation);
621
+ } catch (e) {
622
+ // eslint-disable-next-line no-console
623
+ console.log('Failed to generate types for', relativePath); // eslint-disable-next-line no-console
624
+
625
+ console.error(e);
626
+ }
627
+ }
628
+ };
629
+
630
+ const onNewDirectory = async relativePath => {
631
+ trace('Detected new directory', relativePath);
632
+
633
+ if (!isTranslationDirectory(relativePath, config)) {
634
+ trace('Ignoring non-translation directory:', relativePath);
635
+ return;
636
+ }
637
+
638
+ const newFilePath = path.join(relativePath, devTranslationFileName);
639
+
640
+ if (!existsSync(newFilePath)) {
641
+ await promises.writeFile(newFilePath, JSON.stringify({}, null, 2));
642
+ trace('Created new empty translation file:', newFilePath);
643
+ } else {
644
+ trace(`New directory already contains translation file. Skipping creation. Existing file ${newFilePath}`);
645
+ }
646
+ };
647
+
648
+ watcher.on('addDir', onNewDirectory);
649
+ watcher.on('add', onTranslationChange).on('change', onTranslationChange);
650
+ return () => watcher.close();
651
+ }
652
+ async function compile({
653
+ watch: shouldWatch = false
654
+ } = {}, config) {
655
+ const translations = await loadAllTranslations({
656
+ fallbacks: 'all',
657
+ includeNodeModules: false
658
+ }, config);
659
+
660
+ for (const loadedTranslation of translations) {
661
+ await generateRuntime(loadedTranslation);
662
+ }
663
+
664
+ if (shouldWatch) {
665
+ trace('Listening for changes to files...');
666
+ return watch(config);
667
+ }
668
+ }
669
+
670
+ async function writeIfChanged(filepath, contents) {
671
+ let hasChanged = true;
672
+
673
+ try {
674
+ const existingContents = await promises.readFile(filepath, {
675
+ encoding: 'utf-8'
676
+ });
677
+ hasChanged = existingContents !== contents;
678
+ } catch (e) {// ignore error, likely a file doesn't exist error so we want to write anyway
679
+ }
680
+
681
+ if (hasChanged) {
682
+ await promises.writeFile(filepath, contents, {
683
+ encoding: 'utf-8'
684
+ });
685
+ }
686
+ }
687
+
688
+ /* eslint-disable no-console */
689
+ function findMissingKeys(loadedTranslation, devLanguageName, altLanguages) {
690
+ const devLanguage = loadedTranslation.languages[devLanguageName];
691
+
692
+ if (!devLanguage) {
693
+ throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
694
+ }
695
+
696
+ const result = {};
697
+ let valid = true;
698
+ const requiredKeys = Object.keys(devLanguage);
699
+
700
+ if (requiredKeys.length > 0) {
701
+ for (const altLanguageName of altLanguages) {
702
+ var _loadedTranslation$la;
703
+
704
+ const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
705
+
706
+ for (const key of requiredKeys) {
707
+ var _altLanguage$key;
708
+
709
+ if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {
710
+ if (!result[altLanguageName]) {
711
+ result[altLanguageName] = [];
712
+ }
713
+
714
+ result[altLanguageName].push(key);
715
+ valid = false;
716
+ }
717
+ }
718
+ }
719
+ }
720
+
721
+ return [valid, result];
722
+ }
723
+ async function validate(config) {
724
+ const allTranslations = await loadAllTranslations({
725
+ fallbacks: 'valid',
726
+ includeNodeModules: true
727
+ }, config);
728
+ let valid = true;
729
+
730
+ for (const loadedTranslation of allTranslations) {
731
+ const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
732
+
733
+ if (!translationValid) {
734
+ valid = false;
735
+ console.log(chalk.red`Incomplete translations: "${chalk.bold(loadedTranslation.relativePath)}"`);
736
+
737
+ for (const lang of Object.keys(result)) {
738
+ const missingKeys = result[lang];
739
+ console.log(chalk.yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
740
+ }
741
+ }
742
+ }
743
+
744
+ return valid;
745
+ }
746
+
747
+ class ValidationError extends Error {
748
+ constructor(code, message) {
749
+ super(`Invalid vocab.config.js: ${code} - ${message}`);
750
+ this.code = code;
751
+ this.rawMessage = message;
752
+ }
753
+
754
+ }
755
+
756
+ const validator = new Validator();
757
+ const schema = {
758
+ $$strict: true,
759
+ devLanguage: {
760
+ type: 'string'
761
+ },
762
+ languages: {
763
+ type: 'array',
764
+ items: {
765
+ type: 'object',
766
+ props: {
767
+ name: {
768
+ type: 'string'
769
+ },
770
+ extends: {
771
+ type: 'string',
772
+ optional: true
773
+ }
774
+ }
775
+ }
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
+ },
806
+ translationsDirectorySuffix: {
807
+ type: 'string',
808
+ optional: true
809
+ },
810
+ projectRoot: {
811
+ type: 'string',
812
+ optional: true
813
+ },
814
+ ignore: {
815
+ type: 'array',
816
+ items: 'string',
817
+ optional: true
818
+ }
819
+ };
820
+ const checkConfigFile = validator.compile(schema);
821
+
822
+ const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
823
+
824
+ function validateConfig(c) {
825
+ trace('Validating configuration file'); // Note: checkConfigFile mutates the config file by applying defaults
826
+
827
+ const isValid = checkConfigFile(c);
828
+
829
+ if (isValid !== true) {
830
+ throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {
831
+ if (v.type === 'objectStrict') {
832
+ return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk.cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk.green)}`;
833
+ }
834
+
835
+ if (v.field) {
836
+ var _v$message;
837
+
838
+ return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk.cyan(v.field));
839
+ }
840
+
841
+ return v.message;
842
+ }).join(' \n'));
843
+ }
844
+
845
+ const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
846
+
847
+ if (!languageStrings.includes(c.devLanguage)) {
848
+ throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
849
+ }
850
+
851
+ const foundLanguages = [];
852
+
853
+ for (const lang of c.languages) {
854
+ // Languages must only exist once
855
+ if (foundLanguages.includes(lang.name)) {
856
+ throw new ValidationError('DuplicateLanguage', `The language "${chalk.bold.cyan(lang.name)}" was defined multiple times.`);
857
+ }
858
+
859
+ foundLanguages.push(lang.name); // Any extends must be in languages
860
+
861
+ if (lang.extends && !languageStrings.includes(lang.extends)) {
862
+ throw new ValidationError('InvalidExtends', `The language "${chalk.bold.cyan(lang.name)}"'s extends of ${chalk.bold.cyan(lang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
863
+ }
864
+ }
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
+
886
+ trace('Configuration file is valid');
887
+ return true;
888
+ }
889
+
890
+ function createConfig(configFilePath) {
891
+ const cwd = path.dirname(configFilePath);
892
+ return {
893
+ projectRoot: cwd,
894
+ ...require(configFilePath)
895
+ };
896
+ }
897
+
898
+ async function resolveConfig(customConfigFilePath) {
899
+ const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : await findUp('vocab.config.js');
900
+
901
+ if (configFilePath) {
902
+ trace(`Resolved configuration file to ${configFilePath}`);
903
+ return createConfig(configFilePath);
904
+ }
905
+
906
+ trace('No configuration file found');
907
+ return null;
908
+ }
909
+ function resolveConfigSync(customConfigFilePath) {
910
+ const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : findUp.sync('vocab.config.js');
911
+
912
+ if (configFilePath) {
913
+ trace(`Resolved configuration file to ${configFilePath}`);
914
+ return createConfig(configFilePath);
915
+ }
916
+
917
+ trace('No configuration file found');
918
+ return null;
919
+ }
920
+
921
+ export { compile, getAltLanguageFilePath, getAltLanguages, getDevLanguageFileFromTsFile, getUniqueKey, loadAllTranslations, loadTranslation, resolveConfig, resolveConfigSync, validate, validateConfig, watch };