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