@vocab/core 1.2.0 → 1.2.2
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/LICENSE +21 -0
- package/dist/declarations/src/ValidationError.d.ts +5 -5
- package/dist/declarations/src/compile.d.ts +6 -6
- package/dist/declarations/src/config.d.ts +4 -4
- package/dist/declarations/src/generate-language.d.ts +5 -5
- package/dist/declarations/src/icu-handler.d.ts +2 -2
- package/dist/declarations/src/index.d.ts +6 -6
- package/dist/declarations/src/load-translations.d.ts +32 -32
- package/dist/declarations/src/logger.d.ts +3 -3
- package/dist/declarations/src/runtime.d.ts +3 -3
- package/dist/declarations/src/translation-file.d.ts +2 -2
- package/dist/declarations/src/utils.d.ts +26 -26
- package/dist/declarations/src/validate/index.d.ts +3 -3
- package/dist/vocab-core.cjs.dev.js +92 -189
- package/dist/vocab-core.cjs.prod.js +92 -189
- package/dist/vocab-core.esm.js +58 -155
- package/icu-handler/dist/vocab-core-icu-handler.cjs.dev.js +3 -6
- package/icu-handler/dist/vocab-core-icu-handler.cjs.prod.js +3 -6
- package/icu-handler/dist/vocab-core-icu-handler.esm.js +2 -5
- package/package.json +2 -2
- package/translation-file/dist/vocab-core-translation-file.cjs.dev.js +0 -8
- package/translation-file/dist/vocab-core-translation-file.cjs.prod.js +0 -8
- package/translation-file/dist/vocab-core-translation-file.esm.js +0 -8
package/dist/vocab-core.esm.js
CHANGED
|
@@ -84,11 +84,9 @@ function getAltLanguageFilePath(devLanguageFilePath, language) {
|
|
|
84
84
|
function mapValues(obj, func) {
|
|
85
85
|
const newObj = {};
|
|
86
86
|
const keys = Object.keys(obj);
|
|
87
|
-
|
|
88
87
|
for (const key of keys) {
|
|
89
88
|
newObj[key] = func(obj[key]);
|
|
90
89
|
}
|
|
91
|
-
|
|
92
90
|
return newObj;
|
|
93
91
|
}
|
|
94
92
|
function getTranslationMessages(translations) {
|
|
@@ -102,60 +100,49 @@ function generateLanguageFromTranslations({
|
|
|
102
100
|
if (!generator.transformElement && !generator.transformMessage) {
|
|
103
101
|
return baseTranslations;
|
|
104
102
|
}
|
|
105
|
-
|
|
106
103
|
const translationKeys = Object.keys(baseTranslations);
|
|
107
104
|
const generatedTranslations = {};
|
|
108
|
-
|
|
109
105
|
for (const translationKey of translationKeys) {
|
|
110
106
|
const translation = baseTranslations[translationKey];
|
|
111
107
|
let transformedMessage = translation.message;
|
|
112
|
-
|
|
113
108
|
if (generator.transformElement) {
|
|
114
109
|
const messageAst = new IntlMessageFormat(translation.message).getAst();
|
|
115
110
|
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement));
|
|
116
111
|
transformedMessage = printAST(transformedAst);
|
|
117
112
|
}
|
|
118
|
-
|
|
119
113
|
if (generator.transformMessage) {
|
|
120
114
|
transformedMessage = generator.transformMessage(transformedMessage);
|
|
121
115
|
}
|
|
122
|
-
|
|
123
116
|
generatedTranslations[translationKey] = {
|
|
124
117
|
message: transformedMessage
|
|
125
118
|
};
|
|
126
119
|
}
|
|
127
|
-
|
|
128
120
|
return generatedTranslations;
|
|
129
121
|
}
|
|
130
|
-
|
|
131
122
|
function transformMessageFormatElement(transformElement) {
|
|
132
123
|
return messageFormatElement => {
|
|
133
|
-
const transformedMessageFormatElement = {
|
|
124
|
+
const transformedMessageFormatElement = {
|
|
125
|
+
...messageFormatElement
|
|
134
126
|
};
|
|
135
|
-
|
|
136
127
|
switch (transformedMessageFormatElement.type) {
|
|
137
128
|
case TYPE.literal:
|
|
138
129
|
const transformedValue = transformElement(transformedMessageFormatElement.value);
|
|
139
130
|
transformedMessageFormatElement.value = transformedValue;
|
|
140
131
|
break;
|
|
141
|
-
|
|
142
132
|
case TYPE.select:
|
|
143
133
|
case TYPE.plural:
|
|
144
|
-
const transformedOptions = {
|
|
134
|
+
const transformedOptions = {
|
|
135
|
+
...transformedMessageFormatElement.options
|
|
145
136
|
};
|
|
146
|
-
|
|
147
137
|
for (const key of Object.keys(transformedOptions)) {
|
|
148
138
|
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
|
|
149
139
|
}
|
|
150
|
-
|
|
151
140
|
break;
|
|
152
|
-
|
|
153
141
|
case TYPE.tag:
|
|
154
142
|
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
|
|
155
143
|
transformedMessageFormatElement.children = transformedChildren;
|
|
156
144
|
break;
|
|
157
145
|
}
|
|
158
|
-
|
|
159
146
|
return transformedMessageFormatElement;
|
|
160
147
|
};
|
|
161
148
|
}
|
|
@@ -170,7 +157,6 @@ function mergeWithDevLanguageTranslation({
|
|
|
170
157
|
// Only use keys from the dev translation
|
|
171
158
|
const keys = Object.keys(devTranslation);
|
|
172
159
|
const newLanguage = {};
|
|
173
|
-
|
|
174
160
|
for (const key of keys) {
|
|
175
161
|
if (translation[key]) {
|
|
176
162
|
newLanguage[key] = {
|
|
@@ -179,24 +165,19 @@ function mergeWithDevLanguageTranslation({
|
|
|
179
165
|
};
|
|
180
166
|
}
|
|
181
167
|
}
|
|
182
|
-
|
|
183
168
|
return newLanguage;
|
|
184
169
|
}
|
|
185
|
-
|
|
186
170
|
function getLanguageFallbacks({
|
|
187
171
|
languages
|
|
188
172
|
}) {
|
|
189
173
|
const languageFallbackMap = new Map();
|
|
190
|
-
|
|
191
174
|
for (const lang of languages) {
|
|
192
175
|
if (lang.extends) {
|
|
193
176
|
languageFallbackMap.set(lang.name, lang.extends);
|
|
194
177
|
}
|
|
195
178
|
}
|
|
196
|
-
|
|
197
179
|
return languageFallbackMap;
|
|
198
180
|
}
|
|
199
|
-
|
|
200
181
|
function getLanguageHierarchy({
|
|
201
182
|
languages
|
|
202
183
|
}) {
|
|
@@ -204,19 +185,15 @@ function getLanguageHierarchy({
|
|
|
204
185
|
const fallbacks = getLanguageFallbacks({
|
|
205
186
|
languages
|
|
206
187
|
});
|
|
207
|
-
|
|
208
188
|
for (const lang of languages) {
|
|
209
189
|
const langHierarchy = [];
|
|
210
190
|
let currLang = lang.extends;
|
|
211
|
-
|
|
212
191
|
while (currLang) {
|
|
213
192
|
langHierarchy.push(currLang);
|
|
214
193
|
currLang = fallbacks.get(currLang);
|
|
215
194
|
}
|
|
216
|
-
|
|
217
195
|
hierarchyMap.set(lang.name, langHierarchy);
|
|
218
196
|
}
|
|
219
|
-
|
|
220
197
|
return hierarchyMap;
|
|
221
198
|
}
|
|
222
199
|
function getFallbackLanguageOrder({
|
|
@@ -228,41 +205,31 @@ function getFallbackLanguageOrder({
|
|
|
228
205
|
const languageHierarchy = getLanguageHierarchy({
|
|
229
206
|
languages
|
|
230
207
|
}).get(languageName);
|
|
231
|
-
|
|
232
208
|
if (!languageHierarchy) {
|
|
233
209
|
throw new Error(`Missing language hierarchy for ${languageName}`);
|
|
234
210
|
}
|
|
235
|
-
|
|
236
211
|
const fallbackLanguageOrder = [languageName];
|
|
237
|
-
|
|
238
212
|
if (fallbacks !== 'none') {
|
|
239
213
|
fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
|
|
240
|
-
|
|
241
214
|
if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {
|
|
242
215
|
fallbackLanguageOrder.unshift(devLanguage);
|
|
243
216
|
}
|
|
244
217
|
}
|
|
245
|
-
|
|
246
218
|
return fallbackLanguageOrder;
|
|
247
219
|
}
|
|
248
|
-
|
|
249
220
|
function getNamespaceByFilePath(relativePath, {
|
|
250
221
|
translationsDirectorySuffix = defaultTranslationDirSuffix
|
|
251
222
|
}) {
|
|
252
223
|
let namespace = path.dirname(relativePath).replace(/^src\//, '').replace(/\//g, '_');
|
|
253
|
-
|
|
254
224
|
if (namespace.endsWith(translationsDirectorySuffix)) {
|
|
255
225
|
namespace = namespace.slice(0, -translationsDirectorySuffix.length);
|
|
256
226
|
}
|
|
257
|
-
|
|
258
227
|
return namespace;
|
|
259
228
|
}
|
|
260
|
-
|
|
261
229
|
function printValidationError(...params) {
|
|
262
230
|
// eslint-disable-next-line no-console
|
|
263
231
|
console.error(chalk.red('Error loading translation:'), ...params);
|
|
264
232
|
}
|
|
265
|
-
|
|
266
233
|
function getTranslationsFromFile(translationFileContents, {
|
|
267
234
|
isAltLanguage,
|
|
268
235
|
filePath,
|
|
@@ -271,29 +238,24 @@ function getTranslationsFromFile(translationFileContents, {
|
|
|
271
238
|
if (!translationFileContents || typeof translationFileContents !== 'object') {
|
|
272
239
|
throw new Error(`Unable to read translation file ${filePath}. Translations must be an object.`);
|
|
273
240
|
}
|
|
274
|
-
|
|
275
241
|
const {
|
|
276
242
|
$namespace,
|
|
277
243
|
_meta,
|
|
278
244
|
...keys
|
|
279
245
|
} = translationFileContents;
|
|
280
|
-
|
|
281
246
|
if (isAltLanguage && $namespace) {
|
|
282
247
|
printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
|
|
283
248
|
}
|
|
284
|
-
|
|
285
249
|
if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
|
|
286
250
|
printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
|
|
287
251
|
}
|
|
288
|
-
|
|
289
252
|
if (isAltLanguage && _meta !== null && _meta !== void 0 && _meta.tags) {
|
|
290
253
|
printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
|
|
291
|
-
}
|
|
292
|
-
|
|
254
|
+
}
|
|
293
255
|
|
|
256
|
+
// Never return tags if we're fetching translations for an alt language
|
|
294
257
|
const includeTags = !isAltLanguage && withTags;
|
|
295
258
|
const validKeys = {};
|
|
296
|
-
|
|
297
259
|
for (const [translationKey, {
|
|
298
260
|
tags,
|
|
299
261
|
...translation
|
|
@@ -302,22 +264,19 @@ function getTranslationsFromFile(translationFileContents, {
|
|
|
302
264
|
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
303
265
|
continue;
|
|
304
266
|
}
|
|
305
|
-
|
|
306
267
|
if (!translation) {
|
|
307
268
|
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
308
269
|
continue;
|
|
309
270
|
}
|
|
310
|
-
|
|
311
271
|
if (!translation.message || typeof translation.message !== 'string') {
|
|
312
272
|
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
313
273
|
continue;
|
|
314
274
|
}
|
|
315
|
-
|
|
316
|
-
|
|
275
|
+
validKeys[translationKey] = {
|
|
276
|
+
...translation,
|
|
317
277
|
tags: includeTags ? tags : undefined
|
|
318
278
|
};
|
|
319
279
|
}
|
|
320
|
-
|
|
321
280
|
const metadata = {
|
|
322
281
|
tags: includeTags ? _meta === null || _meta === void 0 ? void 0 : _meta.tags : undefined
|
|
323
282
|
};
|
|
@@ -327,7 +286,6 @@ function getTranslationsFromFile(translationFileContents, {
|
|
|
327
286
|
metadata
|
|
328
287
|
};
|
|
329
288
|
}
|
|
330
|
-
|
|
331
289
|
function loadAltLanguageFile({
|
|
332
290
|
filePath,
|
|
333
291
|
languageName,
|
|
@@ -345,15 +303,12 @@ function loadAltLanguageFile({
|
|
|
345
303
|
fallbacks
|
|
346
304
|
});
|
|
347
305
|
trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
|
|
348
|
-
|
|
349
306
|
for (const fallbackLanguage of fallbackLanguageOrder) {
|
|
350
307
|
if (fallbackLanguage !== devLanguage) {
|
|
351
308
|
try {
|
|
352
309
|
const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
|
|
353
310
|
delete require.cache[altFilePath];
|
|
354
|
-
|
|
355
311
|
const translationFile = require(altFilePath);
|
|
356
|
-
|
|
357
312
|
const {
|
|
358
313
|
keys: fallbackLanguageTranslation
|
|
359
314
|
} = getTranslationsFromFile(translationFile, {
|
|
@@ -372,17 +327,14 @@ function loadAltLanguageFile({
|
|
|
372
327
|
Object.assign(altLanguageTranslation, devTranslation);
|
|
373
328
|
}
|
|
374
329
|
}
|
|
375
|
-
|
|
376
330
|
return altLanguageTranslation;
|
|
377
331
|
}
|
|
378
|
-
|
|
379
332
|
function stripTagsFromTranslations(translations) {
|
|
380
333
|
return Object.fromEntries(Object.entries(translations).map(([key, {
|
|
381
334
|
tags,
|
|
382
335
|
...rest
|
|
383
336
|
}]) => [key, rest]));
|
|
384
337
|
}
|
|
385
|
-
|
|
386
338
|
function loadTranslation({
|
|
387
339
|
filePath,
|
|
388
340
|
fallbacks,
|
|
@@ -391,9 +343,7 @@ function loadTranslation({
|
|
|
391
343
|
trace(`Loading translation file in "${fallbacks}" fallback mode: "${filePath}"`);
|
|
392
344
|
const languageSet = {};
|
|
393
345
|
delete require.cache[filePath];
|
|
394
|
-
|
|
395
346
|
const translationContent = require(filePath);
|
|
396
|
-
|
|
397
347
|
const relativePath = path.relative(userConfig.projectRoot || process.cwd(), filePath);
|
|
398
348
|
const {
|
|
399
349
|
$namespace,
|
|
@@ -409,7 +359,6 @@ function loadTranslation({
|
|
|
409
359
|
languageSet[userConfig.devLanguage] = devTranslation;
|
|
410
360
|
const devTranslationNoTags = withTags ? stripTagsFromTranslations(devTranslation) : devTranslation;
|
|
411
361
|
const altLanguages = getAltLanguages(userConfig);
|
|
412
|
-
|
|
413
362
|
for (const languageName of altLanguages) {
|
|
414
363
|
languageSet[languageName] = loadAltLanguageFile({
|
|
415
364
|
filePath,
|
|
@@ -418,7 +367,6 @@ function loadTranslation({
|
|
|
418
367
|
fallbacks
|
|
419
368
|
}, userConfig);
|
|
420
369
|
}
|
|
421
|
-
|
|
422
370
|
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
423
371
|
const {
|
|
424
372
|
name: generatedLanguageName,
|
|
@@ -431,7 +379,6 @@ function loadTranslation({
|
|
|
431
379
|
generator
|
|
432
380
|
});
|
|
433
381
|
}
|
|
434
|
-
|
|
435
382
|
return {
|
|
436
383
|
filePath,
|
|
437
384
|
keys: Object.keys(devTranslation),
|
|
@@ -462,42 +409,33 @@ async function loadAllTranslations({
|
|
|
462
409
|
withTags
|
|
463
410
|
}, config)));
|
|
464
411
|
const keys = new Set();
|
|
465
|
-
|
|
466
412
|
for (const loadedTranslation of result) {
|
|
467
413
|
for (const key of loadedTranslation.keys) {
|
|
468
414
|
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
469
|
-
|
|
470
415
|
if (keys.has(uniqueKey)) {
|
|
471
416
|
trace(`Duplicate keys found`);
|
|
472
417
|
throw new Error(`Duplicate keys found. Key with namespace ${loadedTranslation.namespace} and key ${key} was found multiple times.`);
|
|
473
418
|
}
|
|
474
|
-
|
|
475
419
|
keys.add(uniqueKey);
|
|
476
420
|
}
|
|
477
421
|
}
|
|
478
|
-
|
|
479
422
|
return result;
|
|
480
423
|
}
|
|
481
424
|
|
|
482
425
|
const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
|
|
483
|
-
|
|
484
426
|
const encodeBackslash = v => v.replace(/\\/g, '\\\\');
|
|
485
|
-
|
|
486
427
|
function extractHasTags(ast) {
|
|
487
428
|
return ast.some(element => {
|
|
488
429
|
if (isSelectElement(element)) {
|
|
489
430
|
const children = Object.values(element.options).map(o => o.value);
|
|
490
431
|
return children.some(child => extractHasTags(child));
|
|
491
432
|
}
|
|
492
|
-
|
|
493
433
|
return isTagElement(element);
|
|
494
434
|
});
|
|
495
435
|
}
|
|
496
|
-
|
|
497
436
|
function extractParamTypes(ast) {
|
|
498
437
|
let params = {};
|
|
499
|
-
let
|
|
500
|
-
|
|
438
|
+
let vocabTypesImports = new Set();
|
|
501
439
|
for (const element of ast) {
|
|
502
440
|
if (isArgumentElement(element)) {
|
|
503
441
|
params[element.value] = 'string';
|
|
@@ -505,36 +443,49 @@ function extractParamTypes(ast) {
|
|
|
505
443
|
params[element.value] = 'number';
|
|
506
444
|
} else if (isPluralElement(element)) {
|
|
507
445
|
params[element.value] = 'number';
|
|
446
|
+
const children = Object.values(element.options).map(o => o.value);
|
|
447
|
+
for (const child of children) {
|
|
448
|
+
const [subParams, subImports] = extractParamTypes(child);
|
|
449
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
450
|
+
params = {
|
|
451
|
+
...params,
|
|
452
|
+
...subParams
|
|
453
|
+
};
|
|
454
|
+
}
|
|
508
455
|
} else if (isDateElement(element) || isTimeElement(element)) {
|
|
509
456
|
params[element.value] = 'Date | number';
|
|
510
457
|
} else if (isTagElement(element)) {
|
|
511
458
|
params[element.value] = 'FormatXMLElementFn<T>';
|
|
512
|
-
|
|
459
|
+
vocabTypesImports.add('FormatXMLElementFn');
|
|
513
460
|
const [subParams, subImports] = extractParamTypes(element.children);
|
|
514
|
-
|
|
515
|
-
params = {
|
|
461
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
462
|
+
params = {
|
|
463
|
+
...params,
|
|
516
464
|
...subParams
|
|
517
465
|
};
|
|
518
466
|
} else if (isSelectElement(element)) {
|
|
519
|
-
|
|
520
|
-
const children = Object.values(element.options).map(o => o.value);
|
|
467
|
+
const options = Object.keys(element.options);
|
|
521
468
|
|
|
469
|
+
// `other` will always be an option as the parser enforces this by default
|
|
470
|
+
const nonOtherOptions = options.filter(o => o !== 'other');
|
|
471
|
+
const nonOtherOptionsUnion = nonOtherOptions.map(o => `'${o}'`).join(' | ');
|
|
472
|
+
params[element.value] = `StringWithSuggestions<${nonOtherOptionsUnion}>`;
|
|
473
|
+
vocabTypesImports.add('StringWithSuggestions');
|
|
474
|
+
const children = Object.values(element.options).map(o => o.value);
|
|
522
475
|
for (const child of children) {
|
|
523
476
|
const [subParams, subImports] = extractParamTypes(child);
|
|
524
|
-
|
|
525
|
-
params = {
|
|
477
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
478
|
+
params = {
|
|
479
|
+
...params,
|
|
526
480
|
...subParams
|
|
527
481
|
};
|
|
528
482
|
}
|
|
529
483
|
}
|
|
530
484
|
}
|
|
531
|
-
|
|
532
|
-
return [params, imports];
|
|
485
|
+
return [params, vocabTypesImports];
|
|
533
486
|
}
|
|
534
|
-
|
|
535
487
|
function serialiseObjectToType(v) {
|
|
536
488
|
let result = '';
|
|
537
|
-
|
|
538
489
|
for (const [key, value] of Object.entries(v)) {
|
|
539
490
|
if (value && typeof value === 'object') {
|
|
540
491
|
result += `'${encodeWithinSingleQuotes(key)}': ${serialiseObjectToType(value)},`;
|
|
@@ -542,44 +493,40 @@ function serialiseObjectToType(v) {
|
|
|
542
493
|
result += `'${encodeWithinSingleQuotes(key)}': ${value},`;
|
|
543
494
|
}
|
|
544
495
|
}
|
|
545
|
-
|
|
546
496
|
return `{ ${result} }`;
|
|
547
497
|
}
|
|
548
|
-
|
|
549
498
|
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
|
|
550
|
-
|
|
499
|
+
const serializeModuleImports = (imports, moduleName) => {
|
|
500
|
+
const importNames = Array.from(imports);
|
|
501
|
+
return `import { ${Array.from(importNames).join(', ')} } from '${moduleName}'`;
|
|
502
|
+
};
|
|
551
503
|
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
|
|
552
504
|
trace('Serialising translations:', loadedTranslation);
|
|
553
505
|
const translationsType = {};
|
|
554
|
-
|
|
555
506
|
for (const [key, {
|
|
556
507
|
params,
|
|
557
508
|
message,
|
|
558
509
|
hasTags
|
|
559
510
|
}] of value.entries()) {
|
|
560
511
|
let translationFunctionString = `() => ${message}`;
|
|
561
|
-
|
|
562
512
|
if (Object.keys(params).length > 0) {
|
|
563
513
|
const formatGeneric = hasTags ? '<T = string>' : '';
|
|
564
514
|
const formatReturn = hasTags ? 'string | T | Array<string | T>' : 'string';
|
|
565
515
|
translationFunctionString = `${formatGeneric}(values: ${serialiseObjectToType(params)}) => ${formatReturn}`;
|
|
566
516
|
}
|
|
567
|
-
|
|
568
517
|
translationsType[encodeBackslash(key)] = translationFunctionString;
|
|
569
518
|
}
|
|
570
|
-
|
|
571
519
|
const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');
|
|
572
520
|
const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');
|
|
573
521
|
return `${banner}
|
|
574
522
|
|
|
575
|
-
${
|
|
523
|
+
${serializeModuleImports(imports, '@vocab/types')}
|
|
576
524
|
import { createLanguage, createTranslationFile } from '@vocab/core/runtime';
|
|
577
525
|
|
|
578
526
|
const translations = createTranslationFile<${languagesUnionAsString}, ${serialiseObjectToType(translationsType)}>({${content}});
|
|
579
527
|
|
|
580
528
|
export default translations;`;
|
|
581
529
|
}
|
|
582
|
-
|
|
583
530
|
async function generateRuntime(loadedTranslation) {
|
|
584
531
|
const {
|
|
585
532
|
languages: loadedLanguages,
|
|
@@ -588,25 +535,23 @@ async function generateRuntime(loadedTranslation) {
|
|
|
588
535
|
trace('Generating types for', filePath);
|
|
589
536
|
const translationTypes = new Map();
|
|
590
537
|
let imports = new Set();
|
|
591
|
-
|
|
592
538
|
for (const key of loadedTranslation.keys) {
|
|
593
539
|
let params = {};
|
|
594
540
|
const messages = new Set();
|
|
595
541
|
let hasTags = false;
|
|
596
|
-
|
|
597
542
|
for (const translatedLanguage of Object.values(loadedLanguages)) {
|
|
598
543
|
if (translatedLanguage[key]) {
|
|
599
544
|
const ast = parse(translatedLanguage[key].message);
|
|
600
545
|
hasTags = hasTags || extractHasTags(ast);
|
|
601
|
-
const [parsedParams,
|
|
602
|
-
imports = new Set([...imports, ...
|
|
603
|
-
params = {
|
|
546
|
+
const [parsedParams, vocabTypesImports] = extractParamTypes(ast);
|
|
547
|
+
imports = new Set([...imports, ...vocabTypesImports]);
|
|
548
|
+
params = {
|
|
549
|
+
...params,
|
|
604
550
|
...parsedParams
|
|
605
551
|
};
|
|
606
552
|
messages.add(`'${encodeWithinSingleQuotes(translatedLanguage[key].message)}'`);
|
|
607
553
|
}
|
|
608
554
|
}
|
|
609
|
-
|
|
610
555
|
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';
|
|
611
556
|
translationTypes.set(key, {
|
|
612
557
|
params,
|
|
@@ -615,10 +560,10 @@ async function generateRuntime(loadedTranslation) {
|
|
|
615
560
|
returnType
|
|
616
561
|
});
|
|
617
562
|
}
|
|
618
|
-
|
|
619
563
|
const prettierConfig = await prettier.resolveConfig(filePath);
|
|
620
564
|
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
|
|
621
|
-
const declaration = prettier.format(serializedTranslationType, {
|
|
565
|
+
const declaration = prettier.format(serializedTranslationType, {
|
|
566
|
+
...prettierConfig,
|
|
622
567
|
parser: 'typescript'
|
|
623
568
|
});
|
|
624
569
|
const outputFilePath = getTSFileFromDevLanguageFile(filePath);
|
|
@@ -632,17 +577,14 @@ function watch(config) {
|
|
|
632
577
|
ignored: config.ignore ? [...config.ignore, '**/node_modules/**'] : ['**/node_modules/**'],
|
|
633
578
|
ignoreInitial: true
|
|
634
579
|
});
|
|
635
|
-
|
|
636
580
|
const onTranslationChange = async relativePath => {
|
|
637
581
|
trace(`Detected change for file ${relativePath}`);
|
|
638
582
|
let targetFile;
|
|
639
|
-
|
|
640
583
|
if (isDevLanguageFile(relativePath)) {
|
|
641
584
|
targetFile = path.resolve(cwd, relativePath);
|
|
642
585
|
} else if (isAltLanguageFile(relativePath)) {
|
|
643
586
|
targetFile = getDevLanguageFileFromAltLanguageFile(path.resolve(cwd, relativePath));
|
|
644
587
|
}
|
|
645
|
-
|
|
646
588
|
if (targetFile) {
|
|
647
589
|
try {
|
|
648
590
|
const loadedTranslation = loadTranslation({
|
|
@@ -652,23 +594,19 @@ function watch(config) {
|
|
|
652
594
|
await generateRuntime(loadedTranslation);
|
|
653
595
|
} catch (e) {
|
|
654
596
|
// eslint-disable-next-line no-console
|
|
655
|
-
console.log('Failed to generate types for', relativePath);
|
|
656
|
-
|
|
597
|
+
console.log('Failed to generate types for', relativePath);
|
|
598
|
+
// eslint-disable-next-line no-console
|
|
657
599
|
console.error(e);
|
|
658
600
|
}
|
|
659
601
|
}
|
|
660
602
|
};
|
|
661
|
-
|
|
662
603
|
const onNewDirectory = async relativePath => {
|
|
663
604
|
trace('Detected new directory', relativePath);
|
|
664
|
-
|
|
665
605
|
if (!isTranslationDirectory(relativePath, config)) {
|
|
666
606
|
trace('Ignoring non-translation directory:', relativePath);
|
|
667
607
|
return;
|
|
668
608
|
}
|
|
669
|
-
|
|
670
609
|
const newFilePath = path.join(relativePath, devTranslationFileName);
|
|
671
|
-
|
|
672
610
|
if (!existsSync(newFilePath)) {
|
|
673
611
|
await promises.writeFile(newFilePath, JSON.stringify({}, null, 2));
|
|
674
612
|
trace('Created new empty translation file:', newFilePath);
|
|
@@ -676,7 +614,6 @@ function watch(config) {
|
|
|
676
614
|
trace(`New directory already contains translation file. Skipping creation. Existing file ${newFilePath}`);
|
|
677
615
|
}
|
|
678
616
|
};
|
|
679
|
-
|
|
680
617
|
watcher.on('addDir', onNewDirectory);
|
|
681
618
|
watcher.on('add', onTranslationChange).on('change', onTranslationChange);
|
|
682
619
|
return () => watcher.close();
|
|
@@ -688,28 +625,24 @@ async function compile({
|
|
|
688
625
|
fallbacks: 'all',
|
|
689
626
|
includeNodeModules: false
|
|
690
627
|
}, config);
|
|
691
|
-
|
|
692
628
|
for (const loadedTranslation of translations) {
|
|
693
629
|
await generateRuntime(loadedTranslation);
|
|
694
630
|
}
|
|
695
|
-
|
|
696
631
|
if (shouldWatch) {
|
|
697
632
|
trace('Listening for changes to files...');
|
|
698
633
|
return watch(config);
|
|
699
634
|
}
|
|
700
635
|
}
|
|
701
|
-
|
|
702
636
|
async function writeIfChanged(filepath, contents) {
|
|
703
637
|
let hasChanged = true;
|
|
704
|
-
|
|
705
638
|
try {
|
|
706
639
|
const existingContents = await promises.readFile(filepath, {
|
|
707
640
|
encoding: 'utf-8'
|
|
708
641
|
});
|
|
709
642
|
hasChanged = existingContents !== contents;
|
|
710
|
-
} catch (e) {
|
|
643
|
+
} catch (e) {
|
|
644
|
+
// ignore error, likely a file doesn't exist error so we want to write anyway
|
|
711
645
|
}
|
|
712
|
-
|
|
713
646
|
if (hasChanged) {
|
|
714
647
|
await promises.writeFile(filepath, contents, {
|
|
715
648
|
encoding: 'utf-8'
|
|
@@ -720,36 +653,28 @@ async function writeIfChanged(filepath, contents) {
|
|
|
720
653
|
/* eslint-disable no-console */
|
|
721
654
|
function findMissingKeys(loadedTranslation, devLanguageName, altLanguages) {
|
|
722
655
|
const devLanguage = loadedTranslation.languages[devLanguageName];
|
|
723
|
-
|
|
724
656
|
if (!devLanguage) {
|
|
725
657
|
throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
|
|
726
658
|
}
|
|
727
|
-
|
|
728
659
|
const result = {};
|
|
729
660
|
let valid = true;
|
|
730
661
|
const requiredKeys = Object.keys(devLanguage);
|
|
731
|
-
|
|
732
662
|
if (requiredKeys.length > 0) {
|
|
733
663
|
for (const altLanguageName of altLanguages) {
|
|
734
664
|
var _loadedTranslation$la;
|
|
735
|
-
|
|
736
665
|
const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
|
|
737
|
-
|
|
738
666
|
for (const key of requiredKeys) {
|
|
739
667
|
var _altLanguage$key;
|
|
740
|
-
|
|
741
668
|
if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {
|
|
742
669
|
if (!result[altLanguageName]) {
|
|
743
670
|
result[altLanguageName] = [];
|
|
744
671
|
}
|
|
745
|
-
|
|
746
672
|
result[altLanguageName].push(key);
|
|
747
673
|
valid = false;
|
|
748
674
|
}
|
|
749
675
|
}
|
|
750
676
|
}
|
|
751
677
|
}
|
|
752
|
-
|
|
753
678
|
return [valid, result];
|
|
754
679
|
}
|
|
755
680
|
async function validate(config) {
|
|
@@ -758,21 +683,17 @@ async function validate(config) {
|
|
|
758
683
|
includeNodeModules: true
|
|
759
684
|
}, config);
|
|
760
685
|
let valid = true;
|
|
761
|
-
|
|
762
686
|
for (const loadedTranslation of allTranslations) {
|
|
763
687
|
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
|
|
764
|
-
|
|
765
688
|
if (!translationValid) {
|
|
766
689
|
valid = false;
|
|
767
690
|
console.log(chalk.red`Incomplete translations: "${chalk.bold(loadedTranslation.relativePath)}"`);
|
|
768
|
-
|
|
769
691
|
for (const lang of Object.keys(result)) {
|
|
770
692
|
const missingKeys = result[lang];
|
|
771
693
|
console.log(chalk.yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
|
|
772
694
|
}
|
|
773
695
|
}
|
|
774
696
|
}
|
|
775
|
-
|
|
776
697
|
return valid;
|
|
777
698
|
}
|
|
778
699
|
|
|
@@ -782,7 +703,6 @@ class ValidationError extends Error {
|
|
|
782
703
|
this.code = code;
|
|
783
704
|
this.rawMessage = message;
|
|
784
705
|
}
|
|
785
|
-
|
|
786
706
|
}
|
|
787
707
|
|
|
788
708
|
const validator = new Validator();
|
|
@@ -850,75 +770,63 @@ const schema = {
|
|
|
850
770
|
}
|
|
851
771
|
};
|
|
852
772
|
const checkConfigFile = validator.compile(schema);
|
|
853
|
-
|
|
854
773
|
const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
|
|
855
|
-
|
|
856
774
|
function validateConfig(c) {
|
|
857
|
-
trace('Validating configuration file');
|
|
858
|
-
|
|
775
|
+
trace('Validating configuration file');
|
|
776
|
+
// Note: checkConfigFile mutates the config file by applying defaults
|
|
859
777
|
const isValid = checkConfigFile(c);
|
|
860
|
-
|
|
861
778
|
if (isValid !== true) {
|
|
862
779
|
throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {
|
|
863
780
|
if (v.type === 'objectStrict') {
|
|
864
781
|
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk.cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk.green)}`;
|
|
865
782
|
}
|
|
866
|
-
|
|
867
783
|
if (v.field) {
|
|
868
784
|
var _v$message;
|
|
869
|
-
|
|
870
785
|
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk.cyan(v.field));
|
|
871
786
|
}
|
|
872
|
-
|
|
873
787
|
return v.message;
|
|
874
788
|
}).join(' \n'));
|
|
875
789
|
}
|
|
790
|
+
const languageStrings = c.languages.map(v => v.name);
|
|
876
791
|
|
|
877
|
-
|
|
878
|
-
|
|
792
|
+
// Dev Language should exist in languages
|
|
879
793
|
if (!languageStrings.includes(c.devLanguage)) {
|
|
880
794
|
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
|
|
881
795
|
}
|
|
882
|
-
|
|
883
796
|
const foundLanguages = [];
|
|
884
|
-
|
|
885
797
|
for (const lang of c.languages) {
|
|
886
798
|
// Languages must only exist once
|
|
887
799
|
if (foundLanguages.includes(lang.name)) {
|
|
888
800
|
throw new ValidationError('DuplicateLanguage', `The language "${chalk.bold.cyan(lang.name)}" was defined multiple times.`);
|
|
889
801
|
}
|
|
802
|
+
foundLanguages.push(lang.name);
|
|
890
803
|
|
|
891
|
-
|
|
892
|
-
|
|
804
|
+
// Any extends must be in languages
|
|
893
805
|
if (lang.extends && !languageStrings.includes(lang.extends)) {
|
|
894
806
|
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(', ')}.`);
|
|
895
807
|
}
|
|
896
808
|
}
|
|
897
|
-
|
|
898
809
|
const foundGeneratedLanguages = [];
|
|
899
|
-
|
|
900
810
|
for (const generatedLang of c.generatedLanguages || []) {
|
|
901
811
|
// Generated languages must only exist once
|
|
902
812
|
if (foundGeneratedLanguages.includes(generatedLang.name)) {
|
|
903
813
|
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" was defined multiple times.`);
|
|
904
814
|
}
|
|
815
|
+
foundGeneratedLanguages.push(generatedLang.name);
|
|
905
816
|
|
|
906
|
-
|
|
907
|
-
|
|
817
|
+
// Generated language names must not conflict with language names
|
|
908
818
|
if (languageStrings.includes(generatedLang.name)) {
|
|
909
819
|
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" is already defined as a language.`);
|
|
910
|
-
}
|
|
911
|
-
|
|
820
|
+
}
|
|
912
821
|
|
|
822
|
+
// Any extends must be in languages
|
|
913
823
|
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
|
|
914
824
|
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(', ')}.`);
|
|
915
825
|
}
|
|
916
826
|
}
|
|
917
|
-
|
|
918
827
|
trace('Configuration file is valid');
|
|
919
828
|
return true;
|
|
920
829
|
}
|
|
921
|
-
|
|
922
830
|
function createConfig(configFilePath) {
|
|
923
831
|
const cwd = path.dirname(configFilePath);
|
|
924
832
|
return {
|
|
@@ -926,26 +834,21 @@ function createConfig(configFilePath) {
|
|
|
926
834
|
...require(configFilePath)
|
|
927
835
|
};
|
|
928
836
|
}
|
|
929
|
-
|
|
930
837
|
async function resolveConfig(customConfigFilePath) {
|
|
931
838
|
const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : await findUp('vocab.config.js');
|
|
932
|
-
|
|
933
839
|
if (configFilePath) {
|
|
934
840
|
trace(`Resolved configuration file to ${configFilePath}`);
|
|
935
841
|
return createConfig(configFilePath);
|
|
936
842
|
}
|
|
937
|
-
|
|
938
843
|
trace('No configuration file found');
|
|
939
844
|
return null;
|
|
940
845
|
}
|
|
941
846
|
function resolveConfigSync(customConfigFilePath) {
|
|
942
847
|
const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : findUp.sync('vocab.config.js');
|
|
943
|
-
|
|
944
848
|
if (configFilePath) {
|
|
945
849
|
trace(`Resolved configuration file to ${configFilePath}`);
|
|
946
850
|
return createConfig(configFilePath);
|
|
947
851
|
}
|
|
948
|
-
|
|
949
852
|
trace('No configuration file found');
|
|
950
853
|
return null;
|
|
951
854
|
}
|