@vocab/core 1.1.2 → 1.2.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.
- package/LICENSE +21 -0
- package/README.md +66 -5
- 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 -30
- 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 +116 -191
- package/dist/vocab-core.cjs.prod.js +116 -191
- package/dist/vocab-core.esm.js +82 -157
- 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 +3 -3
- 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/CHANGELOG.md +0 -204
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,89 +205,87 @@ 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
|
-
function getTranslationsFromFile(translations, {
|
|
233
|
+
function getTranslationsFromFile(translationFileContents, {
|
|
267
234
|
isAltLanguage,
|
|
268
|
-
filePath
|
|
235
|
+
filePath,
|
|
236
|
+
withTags
|
|
269
237
|
}) {
|
|
270
|
-
if (!
|
|
271
|
-
throw new Error(`Unable to read translation file ${filePath}. Translations must be an object
|
|
238
|
+
if (!translationFileContents || typeof translationFileContents !== 'object') {
|
|
239
|
+
throw new Error(`Unable to read translation file ${filePath}. Translations must be an object.`);
|
|
272
240
|
}
|
|
273
|
-
|
|
274
241
|
const {
|
|
275
242
|
$namespace,
|
|
243
|
+
_meta,
|
|
276
244
|
...keys
|
|
277
|
-
} =
|
|
278
|
-
|
|
245
|
+
} = translationFileContents;
|
|
279
246
|
if (isAltLanguage && $namespace) {
|
|
280
247
|
printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
|
|
281
248
|
}
|
|
282
|
-
|
|
283
249
|
if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
|
|
284
250
|
printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
|
|
285
251
|
}
|
|
252
|
+
if (isAltLanguage && _meta !== null && _meta !== void 0 && _meta.tags) {
|
|
253
|
+
printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
|
|
254
|
+
}
|
|
286
255
|
|
|
256
|
+
// Never return tags if we're fetching translations for an alt language
|
|
257
|
+
const includeTags = !isAltLanguage && withTags;
|
|
287
258
|
const validKeys = {};
|
|
288
|
-
|
|
289
|
-
|
|
259
|
+
for (const [translationKey, {
|
|
260
|
+
tags,
|
|
261
|
+
...translation
|
|
262
|
+
}] of Object.entries(keys)) {
|
|
290
263
|
if (typeof translation === 'string') {
|
|
291
264
|
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
292
265
|
continue;
|
|
293
266
|
}
|
|
294
|
-
|
|
295
267
|
if (!translation) {
|
|
296
268
|
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
297
269
|
continue;
|
|
298
270
|
}
|
|
299
|
-
|
|
300
271
|
if (!translation.message || typeof translation.message !== 'string') {
|
|
301
272
|
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
302
273
|
continue;
|
|
303
274
|
}
|
|
304
|
-
|
|
305
|
-
|
|
275
|
+
validKeys[translationKey] = {
|
|
276
|
+
...translation,
|
|
277
|
+
tags: includeTags ? tags : undefined
|
|
278
|
+
};
|
|
306
279
|
}
|
|
307
|
-
|
|
280
|
+
const metadata = {
|
|
281
|
+
tags: includeTags ? _meta === null || _meta === void 0 ? void 0 : _meta.tags : undefined
|
|
282
|
+
};
|
|
308
283
|
return {
|
|
309
284
|
$namespace,
|
|
310
|
-
keys: validKeys
|
|
285
|
+
keys: validKeys,
|
|
286
|
+
metadata
|
|
311
287
|
};
|
|
312
288
|
}
|
|
313
|
-
|
|
314
289
|
function loadAltLanguageFile({
|
|
315
290
|
filePath,
|
|
316
291
|
languageName,
|
|
@@ -328,15 +303,12 @@ function loadAltLanguageFile({
|
|
|
328
303
|
fallbacks
|
|
329
304
|
});
|
|
330
305
|
trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(' -> ')}`);
|
|
331
|
-
|
|
332
306
|
for (const fallbackLanguage of fallbackLanguageOrder) {
|
|
333
307
|
if (fallbackLanguage !== devLanguage) {
|
|
334
308
|
try {
|
|
335
309
|
const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
|
|
336
310
|
delete require.cache[altFilePath];
|
|
337
|
-
|
|
338
311
|
const translationFile = require(altFilePath);
|
|
339
|
-
|
|
340
312
|
const {
|
|
341
313
|
keys: fallbackLanguageTranslation
|
|
342
314
|
} = getTranslationsFromFile(translationFile, {
|
|
@@ -355,41 +327,46 @@ function loadAltLanguageFile({
|
|
|
355
327
|
Object.assign(altLanguageTranslation, devTranslation);
|
|
356
328
|
}
|
|
357
329
|
}
|
|
358
|
-
|
|
359
330
|
return altLanguageTranslation;
|
|
360
331
|
}
|
|
332
|
+
function stripTagsFromTranslations(translations) {
|
|
333
|
+
return Object.fromEntries(Object.entries(translations).map(([key, {
|
|
334
|
+
tags,
|
|
335
|
+
...rest
|
|
336
|
+
}]) => [key, rest]));
|
|
337
|
+
}
|
|
361
338
|
function loadTranslation({
|
|
362
339
|
filePath,
|
|
363
|
-
fallbacks
|
|
340
|
+
fallbacks,
|
|
341
|
+
withTags
|
|
364
342
|
}, userConfig) {
|
|
365
343
|
trace(`Loading translation file in "${fallbacks}" fallback mode: "${filePath}"`);
|
|
366
344
|
const languageSet = {};
|
|
367
345
|
delete require.cache[filePath];
|
|
368
|
-
|
|
369
346
|
const translationContent = require(filePath);
|
|
370
|
-
|
|
371
347
|
const relativePath = path.relative(userConfig.projectRoot || process.cwd(), filePath);
|
|
372
348
|
const {
|
|
373
349
|
$namespace,
|
|
374
|
-
keys: devTranslation
|
|
350
|
+
keys: devTranslation,
|
|
351
|
+
metadata
|
|
375
352
|
} = getTranslationsFromFile(translationContent, {
|
|
376
353
|
filePath,
|
|
377
|
-
isAltLanguage: false
|
|
354
|
+
isAltLanguage: false,
|
|
355
|
+
withTags
|
|
378
356
|
});
|
|
379
357
|
const namespace = typeof $namespace === 'string' ? $namespace : getNamespaceByFilePath(relativePath, userConfig);
|
|
380
358
|
trace(`Found file ${filePath}. Using namespace ${namespace}`);
|
|
381
359
|
languageSet[userConfig.devLanguage] = devTranslation;
|
|
360
|
+
const devTranslationNoTags = withTags ? stripTagsFromTranslations(devTranslation) : devTranslation;
|
|
382
361
|
const altLanguages = getAltLanguages(userConfig);
|
|
383
|
-
|
|
384
362
|
for (const languageName of altLanguages) {
|
|
385
363
|
languageSet[languageName] = loadAltLanguageFile({
|
|
386
364
|
filePath,
|
|
387
365
|
languageName,
|
|
388
|
-
devTranslation,
|
|
366
|
+
devTranslation: devTranslationNoTags,
|
|
389
367
|
fallbacks
|
|
390
368
|
}, userConfig);
|
|
391
369
|
}
|
|
392
|
-
|
|
393
370
|
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
394
371
|
const {
|
|
395
372
|
name: generatedLanguageName,
|
|
@@ -402,18 +379,19 @@ function loadTranslation({
|
|
|
402
379
|
generator
|
|
403
380
|
});
|
|
404
381
|
}
|
|
405
|
-
|
|
406
382
|
return {
|
|
407
383
|
filePath,
|
|
408
384
|
keys: Object.keys(devTranslation),
|
|
409
385
|
namespace,
|
|
410
386
|
relativePath,
|
|
411
|
-
languages: languageSet
|
|
387
|
+
languages: languageSet,
|
|
388
|
+
metadata
|
|
412
389
|
};
|
|
413
390
|
}
|
|
414
391
|
async function loadAllTranslations({
|
|
415
392
|
fallbacks,
|
|
416
|
-
includeNodeModules
|
|
393
|
+
includeNodeModules,
|
|
394
|
+
withTags
|
|
417
395
|
}, config) {
|
|
418
396
|
const {
|
|
419
397
|
projectRoot,
|
|
@@ -427,45 +405,37 @@ async function loadAllTranslations({
|
|
|
427
405
|
trace(`Found ${translationFiles.length} translation files`);
|
|
428
406
|
const result = await Promise.all(translationFiles.map(filePath => loadTranslation({
|
|
429
407
|
filePath,
|
|
430
|
-
fallbacks
|
|
408
|
+
fallbacks,
|
|
409
|
+
withTags
|
|
431
410
|
}, config)));
|
|
432
411
|
const keys = new Set();
|
|
433
|
-
|
|
434
412
|
for (const loadedTranslation of result) {
|
|
435
413
|
for (const key of loadedTranslation.keys) {
|
|
436
414
|
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
437
|
-
|
|
438
415
|
if (keys.has(uniqueKey)) {
|
|
439
416
|
trace(`Duplicate keys found`);
|
|
440
417
|
throw new Error(`Duplicate keys found. Key with namespace ${loadedTranslation.namespace} and key ${key} was found multiple times.`);
|
|
441
418
|
}
|
|
442
|
-
|
|
443
419
|
keys.add(uniqueKey);
|
|
444
420
|
}
|
|
445
421
|
}
|
|
446
|
-
|
|
447
422
|
return result;
|
|
448
423
|
}
|
|
449
424
|
|
|
450
425
|
const encodeWithinSingleQuotes = v => v.replace(/'/g, "\\'");
|
|
451
|
-
|
|
452
426
|
const encodeBackslash = v => v.replace(/\\/g, '\\\\');
|
|
453
|
-
|
|
454
427
|
function extractHasTags(ast) {
|
|
455
428
|
return ast.some(element => {
|
|
456
429
|
if (isSelectElement(element)) {
|
|
457
430
|
const children = Object.values(element.options).map(o => o.value);
|
|
458
431
|
return children.some(child => extractHasTags(child));
|
|
459
432
|
}
|
|
460
|
-
|
|
461
433
|
return isTagElement(element);
|
|
462
434
|
});
|
|
463
435
|
}
|
|
464
|
-
|
|
465
436
|
function extractParamTypes(ast) {
|
|
466
437
|
let params = {};
|
|
467
438
|
let imports = new Set();
|
|
468
|
-
|
|
469
439
|
for (const element of ast) {
|
|
470
440
|
if (isArgumentElement(element)) {
|
|
471
441
|
params[element.value] = 'string';
|
|
@@ -473,6 +443,15 @@ function extractParamTypes(ast) {
|
|
|
473
443
|
params[element.value] = 'number';
|
|
474
444
|
} else if (isPluralElement(element)) {
|
|
475
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
|
+
imports = new Set([...imports, ...subImports]);
|
|
450
|
+
params = {
|
|
451
|
+
...params,
|
|
452
|
+
...subParams
|
|
453
|
+
};
|
|
454
|
+
}
|
|
476
455
|
} else if (isDateElement(element) || isTimeElement(element)) {
|
|
477
456
|
params[element.value] = 'Date | number';
|
|
478
457
|
} else if (isTagElement(element)) {
|
|
@@ -480,29 +459,27 @@ function extractParamTypes(ast) {
|
|
|
480
459
|
imports.add(`import { FormatXMLElementFn } from '@vocab/types';`);
|
|
481
460
|
const [subParams, subImports] = extractParamTypes(element.children);
|
|
482
461
|
imports = new Set([...imports, ...subImports]);
|
|
483
|
-
params = {
|
|
462
|
+
params = {
|
|
463
|
+
...params,
|
|
484
464
|
...subParams
|
|
485
465
|
};
|
|
486
466
|
} else if (isSelectElement(element)) {
|
|
487
467
|
params[element.value] = Object.keys(element.options).map(o => `'${o}'`).join(' | ');
|
|
488
468
|
const children = Object.values(element.options).map(o => o.value);
|
|
489
|
-
|
|
490
469
|
for (const child of children) {
|
|
491
470
|
const [subParams, subImports] = extractParamTypes(child);
|
|
492
471
|
imports = new Set([...imports, ...subImports]);
|
|
493
|
-
params = {
|
|
472
|
+
params = {
|
|
473
|
+
...params,
|
|
494
474
|
...subParams
|
|
495
475
|
};
|
|
496
476
|
}
|
|
497
477
|
}
|
|
498
478
|
}
|
|
499
|
-
|
|
500
479
|
return [params, imports];
|
|
501
480
|
}
|
|
502
|
-
|
|
503
481
|
function serialiseObjectToType(v) {
|
|
504
482
|
let result = '';
|
|
505
|
-
|
|
506
483
|
for (const [key, value] of Object.entries(v)) {
|
|
507
484
|
if (value && typeof value === 'object') {
|
|
508
485
|
result += `'${encodeWithinSingleQuotes(key)}': ${serialiseObjectToType(value)},`;
|
|
@@ -510,32 +487,25 @@ function serialiseObjectToType(v) {
|
|
|
510
487
|
result += `'${encodeWithinSingleQuotes(key)}': ${value},`;
|
|
511
488
|
}
|
|
512
489
|
}
|
|
513
|
-
|
|
514
490
|
return `{ ${result} }`;
|
|
515
491
|
}
|
|
516
|
-
|
|
517
492
|
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
|
|
518
|
-
|
|
519
493
|
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
|
|
520
494
|
trace('Serialising translations:', loadedTranslation);
|
|
521
495
|
const translationsType = {};
|
|
522
|
-
|
|
523
496
|
for (const [key, {
|
|
524
497
|
params,
|
|
525
498
|
message,
|
|
526
499
|
hasTags
|
|
527
500
|
}] of value.entries()) {
|
|
528
501
|
let translationFunctionString = `() => ${message}`;
|
|
529
|
-
|
|
530
502
|
if (Object.keys(params).length > 0) {
|
|
531
503
|
const formatGeneric = hasTags ? '<T = string>' : '';
|
|
532
504
|
const formatReturn = hasTags ? 'string | T | Array<string | T>' : 'string';
|
|
533
505
|
translationFunctionString = `${formatGeneric}(values: ${serialiseObjectToType(params)}) => ${formatReturn}`;
|
|
534
506
|
}
|
|
535
|
-
|
|
536
507
|
translationsType[encodeBackslash(key)] = translationFunctionString;
|
|
537
508
|
}
|
|
538
|
-
|
|
539
509
|
const content = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `'${encodeWithinSingleQuotes(languageName)}': createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(',');
|
|
540
510
|
const languagesUnionAsString = Object.keys(loadedTranslation.languages).map(l => `'${l}'`).join(' | ');
|
|
541
511
|
return `${banner}
|
|
@@ -547,34 +517,31 @@ function serialiseTranslationRuntime(value, imports, loadedTranslation) {
|
|
|
547
517
|
|
|
548
518
|
export default translations;`;
|
|
549
519
|
}
|
|
550
|
-
|
|
551
520
|
async function generateRuntime(loadedTranslation) {
|
|
552
521
|
const {
|
|
553
522
|
languages: loadedLanguages,
|
|
554
523
|
filePath
|
|
555
524
|
} = loadedTranslation;
|
|
556
|
-
trace('Generating types for',
|
|
525
|
+
trace('Generating types for', filePath);
|
|
557
526
|
const translationTypes = new Map();
|
|
558
527
|
let imports = new Set();
|
|
559
|
-
|
|
560
528
|
for (const key of loadedTranslation.keys) {
|
|
561
529
|
let params = {};
|
|
562
530
|
const messages = new Set();
|
|
563
531
|
let hasTags = false;
|
|
564
|
-
|
|
565
532
|
for (const translatedLanguage of Object.values(loadedLanguages)) {
|
|
566
533
|
if (translatedLanguage[key]) {
|
|
567
534
|
const ast = parse(translatedLanguage[key].message);
|
|
568
535
|
hasTags = hasTags || extractHasTags(ast);
|
|
569
536
|
const [parsedParams, parsedImports] = extractParamTypes(ast);
|
|
570
537
|
imports = new Set([...imports, ...parsedImports]);
|
|
571
|
-
params = {
|
|
538
|
+
params = {
|
|
539
|
+
...params,
|
|
572
540
|
...parsedParams
|
|
573
541
|
};
|
|
574
542
|
messages.add(`'${encodeWithinSingleQuotes(translatedLanguage[key].message)}'`);
|
|
575
543
|
}
|
|
576
544
|
}
|
|
577
|
-
|
|
578
545
|
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';
|
|
579
546
|
translationTypes.set(key, {
|
|
580
547
|
params,
|
|
@@ -583,10 +550,10 @@ async function generateRuntime(loadedTranslation) {
|
|
|
583
550
|
returnType
|
|
584
551
|
});
|
|
585
552
|
}
|
|
586
|
-
|
|
587
553
|
const prettierConfig = await prettier.resolveConfig(filePath);
|
|
588
554
|
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
|
|
589
|
-
const declaration = prettier.format(serializedTranslationType, {
|
|
555
|
+
const declaration = prettier.format(serializedTranslationType, {
|
|
556
|
+
...prettierConfig,
|
|
590
557
|
parser: 'typescript'
|
|
591
558
|
});
|
|
592
559
|
const outputFilePath = getTSFileFromDevLanguageFile(filePath);
|
|
@@ -600,43 +567,36 @@ function watch(config) {
|
|
|
600
567
|
ignored: config.ignore ? [...config.ignore, '**/node_modules/**'] : ['**/node_modules/**'],
|
|
601
568
|
ignoreInitial: true
|
|
602
569
|
});
|
|
603
|
-
|
|
604
570
|
const onTranslationChange = async relativePath => {
|
|
605
571
|
trace(`Detected change for file ${relativePath}`);
|
|
606
572
|
let targetFile;
|
|
607
|
-
|
|
608
573
|
if (isDevLanguageFile(relativePath)) {
|
|
609
574
|
targetFile = path.resolve(cwd, relativePath);
|
|
610
575
|
} else if (isAltLanguageFile(relativePath)) {
|
|
611
576
|
targetFile = getDevLanguageFileFromAltLanguageFile(path.resolve(cwd, relativePath));
|
|
612
577
|
}
|
|
613
|
-
|
|
614
578
|
if (targetFile) {
|
|
615
579
|
try {
|
|
616
|
-
const loadedTranslation =
|
|
580
|
+
const loadedTranslation = loadTranslation({
|
|
617
581
|
filePath: targetFile,
|
|
618
582
|
fallbacks: 'all'
|
|
619
583
|
}, config);
|
|
620
584
|
await generateRuntime(loadedTranslation);
|
|
621
585
|
} catch (e) {
|
|
622
586
|
// eslint-disable-next-line no-console
|
|
623
|
-
console.log('Failed to generate types for', relativePath);
|
|
624
|
-
|
|
587
|
+
console.log('Failed to generate types for', relativePath);
|
|
588
|
+
// eslint-disable-next-line no-console
|
|
625
589
|
console.error(e);
|
|
626
590
|
}
|
|
627
591
|
}
|
|
628
592
|
};
|
|
629
|
-
|
|
630
593
|
const onNewDirectory = async relativePath => {
|
|
631
594
|
trace('Detected new directory', relativePath);
|
|
632
|
-
|
|
633
595
|
if (!isTranslationDirectory(relativePath, config)) {
|
|
634
596
|
trace('Ignoring non-translation directory:', relativePath);
|
|
635
597
|
return;
|
|
636
598
|
}
|
|
637
|
-
|
|
638
599
|
const newFilePath = path.join(relativePath, devTranslationFileName);
|
|
639
|
-
|
|
640
600
|
if (!existsSync(newFilePath)) {
|
|
641
601
|
await promises.writeFile(newFilePath, JSON.stringify({}, null, 2));
|
|
642
602
|
trace('Created new empty translation file:', newFilePath);
|
|
@@ -644,7 +604,6 @@ function watch(config) {
|
|
|
644
604
|
trace(`New directory already contains translation file. Skipping creation. Existing file ${newFilePath}`);
|
|
645
605
|
}
|
|
646
606
|
};
|
|
647
|
-
|
|
648
607
|
watcher.on('addDir', onNewDirectory);
|
|
649
608
|
watcher.on('add', onTranslationChange).on('change', onTranslationChange);
|
|
650
609
|
return () => watcher.close();
|
|
@@ -656,28 +615,24 @@ async function compile({
|
|
|
656
615
|
fallbacks: 'all',
|
|
657
616
|
includeNodeModules: false
|
|
658
617
|
}, config);
|
|
659
|
-
|
|
660
618
|
for (const loadedTranslation of translations) {
|
|
661
619
|
await generateRuntime(loadedTranslation);
|
|
662
620
|
}
|
|
663
|
-
|
|
664
621
|
if (shouldWatch) {
|
|
665
622
|
trace('Listening for changes to files...');
|
|
666
623
|
return watch(config);
|
|
667
624
|
}
|
|
668
625
|
}
|
|
669
|
-
|
|
670
626
|
async function writeIfChanged(filepath, contents) {
|
|
671
627
|
let hasChanged = true;
|
|
672
|
-
|
|
673
628
|
try {
|
|
674
629
|
const existingContents = await promises.readFile(filepath, {
|
|
675
630
|
encoding: 'utf-8'
|
|
676
631
|
});
|
|
677
632
|
hasChanged = existingContents !== contents;
|
|
678
|
-
} catch (e) {
|
|
633
|
+
} catch (e) {
|
|
634
|
+
// ignore error, likely a file doesn't exist error so we want to write anyway
|
|
679
635
|
}
|
|
680
|
-
|
|
681
636
|
if (hasChanged) {
|
|
682
637
|
await promises.writeFile(filepath, contents, {
|
|
683
638
|
encoding: 'utf-8'
|
|
@@ -688,36 +643,28 @@ async function writeIfChanged(filepath, contents) {
|
|
|
688
643
|
/* eslint-disable no-console */
|
|
689
644
|
function findMissingKeys(loadedTranslation, devLanguageName, altLanguages) {
|
|
690
645
|
const devLanguage = loadedTranslation.languages[devLanguageName];
|
|
691
|
-
|
|
692
646
|
if (!devLanguage) {
|
|
693
647
|
throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
|
|
694
648
|
}
|
|
695
|
-
|
|
696
649
|
const result = {};
|
|
697
650
|
let valid = true;
|
|
698
651
|
const requiredKeys = Object.keys(devLanguage);
|
|
699
|
-
|
|
700
652
|
if (requiredKeys.length > 0) {
|
|
701
653
|
for (const altLanguageName of altLanguages) {
|
|
702
654
|
var _loadedTranslation$la;
|
|
703
|
-
|
|
704
655
|
const altLanguage = (_loadedTranslation$la = loadedTranslation.languages[altLanguageName]) !== null && _loadedTranslation$la !== void 0 ? _loadedTranslation$la : {};
|
|
705
|
-
|
|
706
656
|
for (const key of requiredKeys) {
|
|
707
657
|
var _altLanguage$key;
|
|
708
|
-
|
|
709
658
|
if (typeof ((_altLanguage$key = altLanguage[key]) === null || _altLanguage$key === void 0 ? void 0 : _altLanguage$key.message) !== 'string') {
|
|
710
659
|
if (!result[altLanguageName]) {
|
|
711
660
|
result[altLanguageName] = [];
|
|
712
661
|
}
|
|
713
|
-
|
|
714
662
|
result[altLanguageName].push(key);
|
|
715
663
|
valid = false;
|
|
716
664
|
}
|
|
717
665
|
}
|
|
718
666
|
}
|
|
719
667
|
}
|
|
720
|
-
|
|
721
668
|
return [valid, result];
|
|
722
669
|
}
|
|
723
670
|
async function validate(config) {
|
|
@@ -726,21 +673,17 @@ async function validate(config) {
|
|
|
726
673
|
includeNodeModules: true
|
|
727
674
|
}, config);
|
|
728
675
|
let valid = true;
|
|
729
|
-
|
|
730
676
|
for (const loadedTranslation of allTranslations) {
|
|
731
677
|
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
|
|
732
|
-
|
|
733
678
|
if (!translationValid) {
|
|
734
679
|
valid = false;
|
|
735
680
|
console.log(chalk.red`Incomplete translations: "${chalk.bold(loadedTranslation.relativePath)}"`);
|
|
736
|
-
|
|
737
681
|
for (const lang of Object.keys(result)) {
|
|
738
682
|
const missingKeys = result[lang];
|
|
739
683
|
console.log(chalk.yellow(lang), '->', missingKeys.map(v => `"${v}"`).join(', '));
|
|
740
684
|
}
|
|
741
685
|
}
|
|
742
686
|
}
|
|
743
|
-
|
|
744
687
|
return valid;
|
|
745
688
|
}
|
|
746
689
|
|
|
@@ -750,7 +693,6 @@ class ValidationError extends Error {
|
|
|
750
693
|
this.code = code;
|
|
751
694
|
this.rawMessage = message;
|
|
752
695
|
}
|
|
753
|
-
|
|
754
696
|
}
|
|
755
697
|
|
|
756
698
|
const validator = new Validator();
|
|
@@ -818,75 +760,63 @@ const schema = {
|
|
|
818
760
|
}
|
|
819
761
|
};
|
|
820
762
|
const checkConfigFile = validator.compile(schema);
|
|
821
|
-
|
|
822
763
|
const splitMap = (message, callback) => message.split(' ,').map(v => callback(v)).join(' ,');
|
|
823
|
-
|
|
824
764
|
function validateConfig(c) {
|
|
825
|
-
trace('Validating configuration file');
|
|
826
|
-
|
|
765
|
+
trace('Validating configuration file');
|
|
766
|
+
// Note: checkConfigFile mutates the config file by applying defaults
|
|
827
767
|
const isValid = checkConfigFile(c);
|
|
828
|
-
|
|
829
768
|
if (isValid !== true) {
|
|
830
769
|
throw new ValidationError('InvalidStructure', (Array.isArray(isValid) ? isValid : []).map(v => {
|
|
831
770
|
if (v.type === 'objectStrict') {
|
|
832
771
|
return `Invalid key(s) ${splitMap(v.actual, m => `"${chalk.cyan(m)}"`)}. Expected one of ${splitMap(v.expected, chalk.green)}`;
|
|
833
772
|
}
|
|
834
|
-
|
|
835
773
|
if (v.field) {
|
|
836
774
|
var _v$message;
|
|
837
|
-
|
|
838
775
|
return (_v$message = v.message) === null || _v$message === void 0 ? void 0 : _v$message.replace(v.field, chalk.cyan(v.field));
|
|
839
776
|
}
|
|
840
|
-
|
|
841
777
|
return v.message;
|
|
842
778
|
}).join(' \n'));
|
|
843
779
|
}
|
|
780
|
+
const languageStrings = c.languages.map(v => v.name);
|
|
844
781
|
|
|
845
|
-
|
|
846
|
-
|
|
782
|
+
// Dev Language should exist in languages
|
|
847
783
|
if (!languageStrings.includes(c.devLanguage)) {
|
|
848
784
|
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
|
|
849
785
|
}
|
|
850
|
-
|
|
851
786
|
const foundLanguages = [];
|
|
852
|
-
|
|
853
787
|
for (const lang of c.languages) {
|
|
854
788
|
// Languages must only exist once
|
|
855
789
|
if (foundLanguages.includes(lang.name)) {
|
|
856
790
|
throw new ValidationError('DuplicateLanguage', `The language "${chalk.bold.cyan(lang.name)}" was defined multiple times.`);
|
|
857
791
|
}
|
|
792
|
+
foundLanguages.push(lang.name);
|
|
858
793
|
|
|
859
|
-
|
|
860
|
-
|
|
794
|
+
// Any extends must be in languages
|
|
861
795
|
if (lang.extends && !languageStrings.includes(lang.extends)) {
|
|
862
796
|
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
797
|
}
|
|
864
798
|
}
|
|
865
|
-
|
|
866
799
|
const foundGeneratedLanguages = [];
|
|
867
|
-
|
|
868
800
|
for (const generatedLang of c.generatedLanguages || []) {
|
|
869
801
|
// Generated languages must only exist once
|
|
870
802
|
if (foundGeneratedLanguages.includes(generatedLang.name)) {
|
|
871
803
|
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" was defined multiple times.`);
|
|
872
804
|
}
|
|
805
|
+
foundGeneratedLanguages.push(generatedLang.name);
|
|
873
806
|
|
|
874
|
-
|
|
875
|
-
|
|
807
|
+
// Generated language names must not conflict with language names
|
|
876
808
|
if (languageStrings.includes(generatedLang.name)) {
|
|
877
809
|
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" is already defined as a language.`);
|
|
878
|
-
}
|
|
879
|
-
|
|
810
|
+
}
|
|
880
811
|
|
|
812
|
+
// Any extends must be in languages
|
|
881
813
|
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
|
|
882
814
|
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
815
|
}
|
|
884
816
|
}
|
|
885
|
-
|
|
886
817
|
trace('Configuration file is valid');
|
|
887
818
|
return true;
|
|
888
819
|
}
|
|
889
|
-
|
|
890
820
|
function createConfig(configFilePath) {
|
|
891
821
|
const cwd = path.dirname(configFilePath);
|
|
892
822
|
return {
|
|
@@ -894,26 +824,21 @@ function createConfig(configFilePath) {
|
|
|
894
824
|
...require(configFilePath)
|
|
895
825
|
};
|
|
896
826
|
}
|
|
897
|
-
|
|
898
827
|
async function resolveConfig(customConfigFilePath) {
|
|
899
828
|
const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : await findUp('vocab.config.js');
|
|
900
|
-
|
|
901
829
|
if (configFilePath) {
|
|
902
830
|
trace(`Resolved configuration file to ${configFilePath}`);
|
|
903
831
|
return createConfig(configFilePath);
|
|
904
832
|
}
|
|
905
|
-
|
|
906
833
|
trace('No configuration file found');
|
|
907
834
|
return null;
|
|
908
835
|
}
|
|
909
836
|
function resolveConfigSync(customConfigFilePath) {
|
|
910
837
|
const configFilePath = customConfigFilePath ? path.resolve(customConfigFilePath) : findUp.sync('vocab.config.js');
|
|
911
|
-
|
|
912
838
|
if (configFilePath) {
|
|
913
839
|
trace(`Resolved configuration file to ${configFilePath}`);
|
|
914
840
|
return createConfig(configFilePath);
|
|
915
841
|
}
|
|
916
|
-
|
|
917
842
|
trace('No configuration file found');
|
|
918
843
|
return null;
|
|
919
844
|
}
|