@vocab/core 1.6.4 → 1.6.5-fix-messageformat-import-20250923014407
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/dist/chunk-nOFOJqeH.js +30 -0
- package/dist/icu-handler-BSMqopEQ.js +29 -0
- package/dist/icu-handler-BSMqopEQ.js.map +1 -0
- package/dist/icu-handler-ckZzFRyo.mjs +22 -0
- package/dist/icu-handler-ckZzFRyo.mjs.map +1 -0
- package/dist/icu-handler.d.mts +7 -0
- package/dist/icu-handler.d.ts +7 -0
- package/dist/icu-handler.js +3 -0
- package/dist/icu-handler.mjs +3 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +679 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +660 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime.d.mts +8 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +13 -0
- package/dist/runtime.js.map +1 -0
- package/dist/runtime.mjs +12 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/translation-file-BEIJ1-3N.js +34 -0
- package/dist/translation-file-BEIJ1-3N.js.map +1 -0
- package/dist/translation-file-CAOIsAU9.d.mts +7 -0
- package/dist/translation-file-DQOiWtfV.d.ts +7 -0
- package/dist/translation-file-Dn19n2oY.mjs +28 -0
- package/dist/translation-file-Dn19n2oY.mjs.map +1 -0
- package/dist/translation-file.d.mts +3 -0
- package/dist/translation-file.d.ts +3 -0
- package/dist/translation-file.js +3 -0
- package/dist/translation-file.mjs +3 -0
- package/dist/types-DFxEF4pq.d.mts +146 -0
- package/dist/types-Di9uIscO.d.ts +146 -0
- package/package.json +15 -25
- package/README.md +0 -827
- package/dist/declarations/src/compile.d.ts +0 -6
- package/dist/declarations/src/config.d.ts +0 -4
- package/dist/declarations/src/icu-handler.d.ts +0 -2
- package/dist/declarations/src/index.d.ts +0 -6
- package/dist/declarations/src/load-translations.d.ts +0 -32
- package/dist/declarations/src/runtime.d.ts +0 -3
- package/dist/declarations/src/translation-file.d.ts +0 -2
- package/dist/declarations/src/types.d.ts +0 -143
- package/dist/declarations/src/utils.d.ts +0 -26
- package/dist/declarations/src/validate/index.d.ts +0 -3
- package/dist/vocab-core.cjs.d.ts +0 -2
- package/dist/vocab-core.cjs.dev.js +0 -893
- package/dist/vocab-core.cjs.js +0 -7
- package/dist/vocab-core.cjs.prod.js +0 -893
- package/dist/vocab-core.esm.js +0 -866
- package/icu-handler/dist/vocab-core-icu-handler.cjs.d.ts +0 -2
- package/icu-handler/dist/vocab-core-icu-handler.cjs.dev.js +0 -31
- package/icu-handler/dist/vocab-core-icu-handler.cjs.js +0 -7
- package/icu-handler/dist/vocab-core-icu-handler.cjs.prod.js +0 -31
- package/icu-handler/dist/vocab-core-icu-handler.esm.js +0 -23
- package/icu-handler/package.json +0 -4
- package/runtime/dist/vocab-core-runtime.cjs.d.ts +0 -2
- package/runtime/dist/vocab-core-runtime.cjs.dev.js +0 -15
- package/runtime/dist/vocab-core-runtime.cjs.js +0 -7
- package/runtime/dist/vocab-core-runtime.cjs.prod.js +0 -15
- package/runtime/dist/vocab-core-runtime.esm.js +0 -10
- package/runtime/package.json +0 -4
- package/translation-file/dist/vocab-core-translation-file.cjs.d.ts +0 -2
- package/translation-file/dist/vocab-core-translation-file.cjs.dev.js +0 -35
- package/translation-file/dist/vocab-core-translation-file.cjs.js +0 -7
- package/translation-file/dist/vocab-core-translation-file.cjs.prod.js +0 -35
- package/translation-file/dist/vocab-core-translation-file.esm.js +0 -31
- package/translation-file/package.json +0 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-nOFOJqeH.js');
|
|
2
|
+
let intl_messageformat = require("intl-messageformat");
|
|
3
|
+
intl_messageformat = require_chunk.__toESM(intl_messageformat);
|
|
4
|
+
let fs = require("fs");
|
|
5
|
+
fs = require_chunk.__toESM(fs);
|
|
6
|
+
let path = require("path");
|
|
7
|
+
path = require_chunk.__toESM(path);
|
|
8
|
+
let __formatjs_icu_messageformat_parser = require("@formatjs/icu-messageformat-parser");
|
|
9
|
+
__formatjs_icu_messageformat_parser = require_chunk.__toESM(__formatjs_icu_messageformat_parser);
|
|
10
|
+
let prettier = require("prettier");
|
|
11
|
+
prettier = require_chunk.__toESM(prettier);
|
|
12
|
+
let chokidar = require("chokidar");
|
|
13
|
+
chokidar = require_chunk.__toESM(chokidar);
|
|
14
|
+
let picocolors = require("picocolors");
|
|
15
|
+
picocolors = require_chunk.__toESM(picocolors);
|
|
16
|
+
let debug = require("debug");
|
|
17
|
+
debug = require_chunk.__toESM(debug);
|
|
18
|
+
let fast_glob = require("fast-glob");
|
|
19
|
+
fast_glob = require_chunk.__toESM(fast_glob);
|
|
20
|
+
let __formatjs_icu_messageformat_parser_printer_js = require("@formatjs/icu-messageformat-parser/printer.js");
|
|
21
|
+
__formatjs_icu_messageformat_parser_printer_js = require_chunk.__toESM(__formatjs_icu_messageformat_parser_printer_js);
|
|
22
|
+
let find_up = require("find-up");
|
|
23
|
+
find_up = require_chunk.__toESM(find_up);
|
|
24
|
+
let fastest_validator = require("fastest-validator");
|
|
25
|
+
fastest_validator = require_chunk.__toESM(fastest_validator);
|
|
26
|
+
|
|
27
|
+
//#region src/logger.ts
|
|
28
|
+
const trace = (0, debug.default)(`vocab:core`);
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/utils.ts
|
|
32
|
+
const defaultTranslationDirSuffix = ".vocab";
|
|
33
|
+
const devTranslationFileName = "translations.json";
|
|
34
|
+
const globAnyPathWithOptionalPrefix = "**/?(*)";
|
|
35
|
+
function isDevLanguageFile(filePath) {
|
|
36
|
+
return filePath.endsWith(`/${devTranslationFileName}`) || filePath === devTranslationFileName;
|
|
37
|
+
}
|
|
38
|
+
function isAltLanguageFile(filePath) {
|
|
39
|
+
return filePath.endsWith(".translations.json");
|
|
40
|
+
}
|
|
41
|
+
function isTranslationDirectory(filePath, { translationsDirectorySuffix = defaultTranslationDirSuffix }) {
|
|
42
|
+
return filePath.endsWith(translationsDirectorySuffix);
|
|
43
|
+
}
|
|
44
|
+
function getTranslationFolderGlob({ translationsDirectorySuffix = defaultTranslationDirSuffix }) {
|
|
45
|
+
const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}`;
|
|
46
|
+
trace("getTranslationFolderGlob", result);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function getDevTranslationFileGlob({ translationsDirectorySuffix = defaultTranslationDirSuffix }) {
|
|
50
|
+
const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}/${devTranslationFileName}`;
|
|
51
|
+
trace("getDevTranslationFileGlob", result);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function getAltTranslationFileGlob(config) {
|
|
55
|
+
const altLanguages = getAltLanguages(config);
|
|
56
|
+
const langMatch = altLanguages.length === 1 ? altLanguages[0] : `{${altLanguages.join(",")}}`;
|
|
57
|
+
const { translationsDirectorySuffix = defaultTranslationDirSuffix } = config;
|
|
58
|
+
const result = `**/*${translationsDirectorySuffix}/${langMatch}.translations.json`;
|
|
59
|
+
trace("getAltTranslationFileGlob", result);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function getAltLanguages({ devLanguage, languages }) {
|
|
63
|
+
return languages.map((v) => v.name).filter((lang) => lang !== devLanguage);
|
|
64
|
+
}
|
|
65
|
+
function getDevLanguageFileFromTsFile(tsFilePath) {
|
|
66
|
+
const directory = path.default.dirname(tsFilePath);
|
|
67
|
+
const result = path.default.normalize(path.default.join(directory, devTranslationFileName));
|
|
68
|
+
trace(`Returning dev language path ${result} for path ${tsFilePath}`);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function getDevLanguageFileFromAltLanguageFile(altLanguageFilePath) {
|
|
72
|
+
const directory = path.default.dirname(altLanguageFilePath);
|
|
73
|
+
const result = path.default.normalize(path.default.join(directory, devTranslationFileName));
|
|
74
|
+
trace(`Returning dev language path ${result} for path ${altLanguageFilePath}`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
function getTSFileFromDevLanguageFile(devLanguageFilePath) {
|
|
78
|
+
const directory = path.default.dirname(devLanguageFilePath);
|
|
79
|
+
const result = path.default.normalize(path.default.join(directory, "index.ts"));
|
|
80
|
+
trace(`Returning TS path ${result} for path ${devLanguageFilePath}`);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function getAltLanguageFilePath(devLanguageFilePath, language) {
|
|
84
|
+
const directory = path.default.dirname(devLanguageFilePath);
|
|
85
|
+
const result = path.default.normalize(path.default.join(directory, `${language}.translations.json`));
|
|
86
|
+
trace(`Returning alt language path ${result} for path ${devLanguageFilePath}`);
|
|
87
|
+
return path.default.normalize(result);
|
|
88
|
+
}
|
|
89
|
+
function mapValues(obj, func) {
|
|
90
|
+
const newObj = {};
|
|
91
|
+
const keys = Object.keys(obj);
|
|
92
|
+
for (const key of keys) newObj[key] = func(obj[key]);
|
|
93
|
+
return newObj;
|
|
94
|
+
}
|
|
95
|
+
function getTranslationMessages(translations) {
|
|
96
|
+
return mapValues(translations, (v) => v.message);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/generate-language.ts
|
|
101
|
+
function generateLanguageFromTranslations({ baseTranslations, generator }) {
|
|
102
|
+
if (!generator.transformElement && !generator.transformMessage) return baseTranslations;
|
|
103
|
+
const translationKeys = Object.keys(baseTranslations);
|
|
104
|
+
const generatedTranslations = {};
|
|
105
|
+
for (const translationKey of translationKeys) {
|
|
106
|
+
const translation = baseTranslations[translationKey];
|
|
107
|
+
let transformedMessage = translation.message;
|
|
108
|
+
if (generator.transformElement) {
|
|
109
|
+
const transformedAst = new intl_messageformat.IntlMessageFormat(translation.message).getAst().map(transformMessageFormatElement(generator.transformElement));
|
|
110
|
+
transformedMessage = (0, __formatjs_icu_messageformat_parser_printer_js.printAST)(transformedAst);
|
|
111
|
+
}
|
|
112
|
+
if (generator.transformMessage) transformedMessage = generator.transformMessage(transformedMessage);
|
|
113
|
+
generatedTranslations[translationKey] = { message: transformedMessage };
|
|
114
|
+
}
|
|
115
|
+
return generatedTranslations;
|
|
116
|
+
}
|
|
117
|
+
function transformMessageFormatElement(transformElement) {
|
|
118
|
+
return (messageFormatElement) => {
|
|
119
|
+
const transformedMessageFormatElement = { ...messageFormatElement };
|
|
120
|
+
switch (transformedMessageFormatElement.type) {
|
|
121
|
+
case __formatjs_icu_messageformat_parser.TYPE.literal:
|
|
122
|
+
transformedMessageFormatElement.value = transformElement(transformedMessageFormatElement.value);
|
|
123
|
+
break;
|
|
124
|
+
case __formatjs_icu_messageformat_parser.TYPE.select:
|
|
125
|
+
case __formatjs_icu_messageformat_parser.TYPE.plural:
|
|
126
|
+
const transformedOptions = { ...transformedMessageFormatElement.options };
|
|
127
|
+
for (const key of Object.keys(transformedOptions)) transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
|
|
128
|
+
break;
|
|
129
|
+
case __formatjs_icu_messageformat_parser.TYPE.tag:
|
|
130
|
+
transformedMessageFormatElement.children = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
|
|
131
|
+
break;
|
|
132
|
+
default: break;
|
|
133
|
+
}
|
|
134
|
+
return transformedMessageFormatElement;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/load-translations.ts
|
|
140
|
+
function getUniqueKey(key, namespace) {
|
|
141
|
+
return `${key}.${namespace}`;
|
|
142
|
+
}
|
|
143
|
+
function mergeWithDevLanguageTranslation({ translation, devTranslation }) {
|
|
144
|
+
const keys = Object.keys(devTranslation);
|
|
145
|
+
const newLanguage = {};
|
|
146
|
+
for (const key of keys) if (translation[key]) newLanguage[key] = {
|
|
147
|
+
message: translation[key].message,
|
|
148
|
+
description: devTranslation[key].description
|
|
149
|
+
};
|
|
150
|
+
return newLanguage;
|
|
151
|
+
}
|
|
152
|
+
function getLanguageFallbacks({ languages }) {
|
|
153
|
+
const languageFallbackMap = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const lang of languages) if (lang.extends) languageFallbackMap.set(lang.name, lang.extends);
|
|
155
|
+
return languageFallbackMap;
|
|
156
|
+
}
|
|
157
|
+
function getLanguageHierarchy({ languages }) {
|
|
158
|
+
const hierarchyMap = /* @__PURE__ */ new Map();
|
|
159
|
+
const fallbacks = getLanguageFallbacks({ languages });
|
|
160
|
+
for (const lang of languages) {
|
|
161
|
+
const langHierarchy = [];
|
|
162
|
+
let currLang = lang.extends;
|
|
163
|
+
while (currLang) {
|
|
164
|
+
langHierarchy.push(currLang);
|
|
165
|
+
currLang = fallbacks.get(currLang);
|
|
166
|
+
}
|
|
167
|
+
hierarchyMap.set(lang.name, langHierarchy);
|
|
168
|
+
}
|
|
169
|
+
return hierarchyMap;
|
|
170
|
+
}
|
|
171
|
+
function getFallbackLanguageOrder({ languages, languageName, devLanguage, fallbacks }) {
|
|
172
|
+
const languageHierarchy = getLanguageHierarchy({ languages }).get(languageName);
|
|
173
|
+
if (!languageHierarchy) throw new Error(`Missing language hierarchy for ${languageName}`);
|
|
174
|
+
const fallbackLanguageOrder = [languageName];
|
|
175
|
+
if (fallbacks !== "none") {
|
|
176
|
+
fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
|
|
177
|
+
if (fallbacks === "all" && fallbackLanguageOrder[0] !== devLanguage) fallbackLanguageOrder.unshift(devLanguage);
|
|
178
|
+
}
|
|
179
|
+
return fallbackLanguageOrder;
|
|
180
|
+
}
|
|
181
|
+
function getNamespaceByFilePath(relativePath, { translationsDirectorySuffix = defaultTranslationDirSuffix }) {
|
|
182
|
+
let namespace = path.default.dirname(relativePath).replace(/^src\//, "").replace(/\//g, "_");
|
|
183
|
+
if (namespace.endsWith(translationsDirectorySuffix)) namespace = namespace.slice(0, -translationsDirectorySuffix.length);
|
|
184
|
+
return namespace;
|
|
185
|
+
}
|
|
186
|
+
function printValidationError(...params) {
|
|
187
|
+
console.error(picocolors.default.red("Error loading translation:"), ...params);
|
|
188
|
+
}
|
|
189
|
+
function getTranslationsFromFile(translationFileContents, { isAltLanguage, filePath, withTags }) {
|
|
190
|
+
if (!translationFileContents || typeof translationFileContents !== "object") throw new Error(`Unable to read translation file ${filePath}. Translations must be an object.`);
|
|
191
|
+
const { $namespace, _meta,...keys } = translationFileContents;
|
|
192
|
+
if (isAltLanguage && $namespace) printValidationError(`Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`);
|
|
193
|
+
if (!isAltLanguage && $namespace && typeof $namespace !== "string") printValidationError(`Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`);
|
|
194
|
+
if (isAltLanguage && _meta?.tags) printValidationError(`Found _meta.tags in alt language file in ${filePath}. _meta.tags is only used in the dev language and will be ignored.`);
|
|
195
|
+
const includeTags = !isAltLanguage && withTags;
|
|
196
|
+
const validKeys = {};
|
|
197
|
+
for (const [translationKey, { tags,...translation }] of Object.entries(keys)) {
|
|
198
|
+
if (typeof translation === "string") {
|
|
199
|
+
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!translation) {
|
|
203
|
+
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!translation.message || typeof translation.message !== "string") {
|
|
207
|
+
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
validKeys[translationKey] = {
|
|
211
|
+
...translation,
|
|
212
|
+
tags: includeTags ? tags : void 0
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const metadata = { tags: includeTags ? _meta?.tags : void 0 };
|
|
216
|
+
return {
|
|
217
|
+
$namespace,
|
|
218
|
+
keys: validKeys,
|
|
219
|
+
metadata
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function loadAltLanguageFile({ filePath, languageName, devTranslation, fallbacks }, { devLanguage, languages }) {
|
|
223
|
+
const altLanguageTranslation = {};
|
|
224
|
+
const fallbackLanguageOrder = getFallbackLanguageOrder({
|
|
225
|
+
languages,
|
|
226
|
+
languageName,
|
|
227
|
+
devLanguage,
|
|
228
|
+
fallbacks
|
|
229
|
+
});
|
|
230
|
+
trace(`Loading alt language file with precedence: ${fallbackLanguageOrder.slice().reverse().join(" -> ")}`);
|
|
231
|
+
for (const fallbackLanguage of fallbackLanguageOrder) if (fallbackLanguage !== devLanguage) try {
|
|
232
|
+
const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
|
|
233
|
+
delete require.cache[altFilePath];
|
|
234
|
+
const translationFile = require(altFilePath);
|
|
235
|
+
const { keys: fallbackLanguageTranslation } = getTranslationsFromFile(translationFile, {
|
|
236
|
+
filePath: altFilePath,
|
|
237
|
+
isAltLanguage: true
|
|
238
|
+
});
|
|
239
|
+
Object.assign(altLanguageTranslation, mergeWithDevLanguageTranslation({
|
|
240
|
+
translation: fallbackLanguageTranslation,
|
|
241
|
+
devTranslation
|
|
242
|
+
}));
|
|
243
|
+
} catch {
|
|
244
|
+
trace(`Missing alt language file ${getAltLanguageFilePath(filePath, fallbackLanguage)}
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
else Object.assign(altLanguageTranslation, devTranslation);
|
|
248
|
+
return altLanguageTranslation;
|
|
249
|
+
}
|
|
250
|
+
function stripTagsFromTranslations(translations) {
|
|
251
|
+
return Object.fromEntries(Object.entries(translations).map(([key, { tags,...rest }]) => [key, rest]));
|
|
252
|
+
}
|
|
253
|
+
function loadTranslation({ filePath, fallbacks, withTags }, userConfig) {
|
|
254
|
+
trace(`Loading translation file in "${fallbacks}" fallback mode: "${filePath}"`);
|
|
255
|
+
const languageSet = {};
|
|
256
|
+
delete require.cache[filePath];
|
|
257
|
+
const translationContent = require(filePath);
|
|
258
|
+
const relativePath = path.default.relative(userConfig.projectRoot || process.cwd(), filePath);
|
|
259
|
+
const { $namespace, keys: devTranslation, metadata } = getTranslationsFromFile(translationContent, {
|
|
260
|
+
filePath,
|
|
261
|
+
isAltLanguage: false,
|
|
262
|
+
withTags
|
|
263
|
+
});
|
|
264
|
+
const namespace = typeof $namespace === "string" ? $namespace : getNamespaceByFilePath(relativePath, userConfig);
|
|
265
|
+
trace(`Found file ${filePath}. Using namespace ${namespace}`);
|
|
266
|
+
languageSet[userConfig.devLanguage] = devTranslation;
|
|
267
|
+
const devTranslationNoTags = withTags ? stripTagsFromTranslations(devTranslation) : devTranslation;
|
|
268
|
+
const altLanguages = getAltLanguages(userConfig);
|
|
269
|
+
for (const languageName of altLanguages) languageSet[languageName] = loadAltLanguageFile({
|
|
270
|
+
filePath,
|
|
271
|
+
languageName,
|
|
272
|
+
devTranslation: devTranslationNoTags,
|
|
273
|
+
fallbacks
|
|
274
|
+
}, userConfig);
|
|
275
|
+
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
276
|
+
const { name: generatedLanguageName, generator } = generatedLanguage;
|
|
277
|
+
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage;
|
|
278
|
+
const baseTranslations = languageSet[baseLanguage];
|
|
279
|
+
languageSet[generatedLanguageName] = generateLanguageFromTranslations({
|
|
280
|
+
baseTranslations,
|
|
281
|
+
generator
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
filePath,
|
|
286
|
+
keys: Object.keys(devTranslation),
|
|
287
|
+
namespace,
|
|
288
|
+
relativePath,
|
|
289
|
+
languages: languageSet,
|
|
290
|
+
metadata
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function loadAllTranslations({ fallbacks, includeNodeModules, withTags }, config) {
|
|
294
|
+
const { projectRoot, ignore = [] } = config;
|
|
295
|
+
const translationFiles = await (0, fast_glob.default)(getDevTranslationFileGlob(config), {
|
|
296
|
+
ignore: includeNodeModules ? ignore : [...ignore, "**/node_modules/**"],
|
|
297
|
+
absolute: true,
|
|
298
|
+
cwd: projectRoot
|
|
299
|
+
});
|
|
300
|
+
trace(`Found ${translationFiles.length} translation files`);
|
|
301
|
+
const loadedTranslations = [];
|
|
302
|
+
const keys = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const translationFile of translationFiles) {
|
|
304
|
+
const loadedTranslation = loadTranslation({
|
|
305
|
+
filePath: translationFile,
|
|
306
|
+
fallbacks,
|
|
307
|
+
withTags
|
|
308
|
+
}, config);
|
|
309
|
+
loadedTranslations.push(loadedTranslation);
|
|
310
|
+
for (const key of loadedTranslation.keys) {
|
|
311
|
+
const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
312
|
+
if (keys.has(uniqueKey)) {
|
|
313
|
+
trace(`Duplicate keys found`);
|
|
314
|
+
throw new Error(`Duplicate keys found. Key with namespace ${loadedTranslation.namespace} and key ${key} was found multiple times.`);
|
|
315
|
+
}
|
|
316
|
+
keys.add(uniqueKey);
|
|
317
|
+
const globalKey = loadedTranslation.languages[config.devLanguage][key].globalKey;
|
|
318
|
+
if (globalKey) {
|
|
319
|
+
if (keys.has(globalKey)) throw new Error(`Duplicate keys found. Key with global key ${globalKey} and key ${key} was found multiple times`);
|
|
320
|
+
keys.add(globalKey);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return loadedTranslations;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/compile.ts
|
|
329
|
+
function extractHasTags(ast) {
|
|
330
|
+
return ast.some((element) => {
|
|
331
|
+
if ((0, __formatjs_icu_messageformat_parser.isSelectElement)(element) || (0, __formatjs_icu_messageformat_parser.isPluralElement)(element)) return Object.values(element.options).map((o) => o.value).some((child) => extractHasTags(child));
|
|
332
|
+
return (0, __formatjs_icu_messageformat_parser.isTagElement)(element);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function extractParamTypes(ast, currentParams) {
|
|
336
|
+
let params = { ...currentParams };
|
|
337
|
+
let vocabTypesImports = /* @__PURE__ */ new Set();
|
|
338
|
+
for (const element of ast) if ((0, __formatjs_icu_messageformat_parser.isArgumentElement)(element)) {
|
|
339
|
+
if (!(element.value in params)) params[element.value] = "string";
|
|
340
|
+
} else if ((0, __formatjs_icu_messageformat_parser.isNumberElement)(element)) params[element.value] = "number";
|
|
341
|
+
else if ((0, __formatjs_icu_messageformat_parser.isPluralElement)(element)) {
|
|
342
|
+
params[element.value] = "number";
|
|
343
|
+
const children = Object.values(element.options).map((o) => o.value);
|
|
344
|
+
for (const child of children) {
|
|
345
|
+
const [newParams, subImports] = extractParamTypes(child, params);
|
|
346
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
347
|
+
params = newParams;
|
|
348
|
+
}
|
|
349
|
+
} else if ((0, __formatjs_icu_messageformat_parser.isDateElement)(element) || (0, __formatjs_icu_messageformat_parser.isTimeElement)(element)) params[element.value] = "Date | number";
|
|
350
|
+
else if ((0, __formatjs_icu_messageformat_parser.isTagElement)(element)) {
|
|
351
|
+
params[element.value] = "FormatXMLElementFn<T>";
|
|
352
|
+
vocabTypesImports.add("FormatXMLElementFn");
|
|
353
|
+
const [newParams, subImports] = extractParamTypes(element.children, params);
|
|
354
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
355
|
+
params = newParams;
|
|
356
|
+
} else if ((0, __formatjs_icu_messageformat_parser.isSelectElement)(element)) {
|
|
357
|
+
const nonOtherOptionsUnion = Object.keys(element.options).filter((o) => o !== "other").map((o) => `'${o}'`).join(" | ");
|
|
358
|
+
params[element.value] = `StringWithSuggestions<${nonOtherOptionsUnion}>`;
|
|
359
|
+
vocabTypesImports.add("StringWithSuggestions");
|
|
360
|
+
const children = Object.values(element.options).map((o) => o.value);
|
|
361
|
+
for (const child of children) {
|
|
362
|
+
const [newParams, subImports] = extractParamTypes(child, params);
|
|
363
|
+
vocabTypesImports = new Set([...vocabTypesImports, ...subImports]);
|
|
364
|
+
params = newParams;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return [params, vocabTypesImports];
|
|
368
|
+
}
|
|
369
|
+
function serialiseObjectToType(v) {
|
|
370
|
+
let result = "";
|
|
371
|
+
for (const [key, value] of Object.entries(v)) result += `${JSON.stringify(key)}: ${value && typeof value === "object" ? serialiseObjectToType(value) : value},`;
|
|
372
|
+
return `{ ${result} }`;
|
|
373
|
+
}
|
|
374
|
+
const serializeTypeImports = (imports, moduleName) => {
|
|
375
|
+
if (imports.size === 0) return "";
|
|
376
|
+
return `import type { ${Array.from(imports).join(", ")} } from '${moduleName}';`;
|
|
377
|
+
};
|
|
378
|
+
function serialiseTranslationRuntime(value, imports, loadedTranslation) {
|
|
379
|
+
trace("Serialising translations:", loadedTranslation);
|
|
380
|
+
const translationsType = {};
|
|
381
|
+
for (const [key, { params, message, hasTags }] of value.entries()) {
|
|
382
|
+
let translationFunctionString = `() => ${message}`;
|
|
383
|
+
if (Object.keys(params).length > 0) {
|
|
384
|
+
const formatGeneric = hasTags ? "<T = string>" : "";
|
|
385
|
+
const formatReturn = hasTags && imports.has("FormatXMLElementFn") ? "ReturnType<FormatXMLElementFn<T>>" : "string";
|
|
386
|
+
translationFunctionString = `${formatGeneric}(values: ${serialiseObjectToType(params)}) => ${formatReturn}`;
|
|
387
|
+
}
|
|
388
|
+
translationsType[key] = translationFunctionString;
|
|
389
|
+
}
|
|
390
|
+
const languagesUnionAsString = Object.keys(loadedTranslation.languages).map((l) => `'${l}'`).join(" | ");
|
|
391
|
+
const languageEntries = Object.entries(loadedTranslation.languages).map(([languageName, translations]) => `${JSON.stringify(languageName)}: createLanguage(${JSON.stringify(getTranslationMessages(translations))})`).join(",");
|
|
392
|
+
return `
|
|
393
|
+
// This file is automatically generated by Vocab.
|
|
394
|
+
// To make changes update translation.json files directly.
|
|
395
|
+
|
|
396
|
+
${serializeTypeImports(imports, "@vocab/core")}
|
|
397
|
+
import { createLanguage, createTranslationFile } from '@vocab/core/runtime';
|
|
398
|
+
|
|
399
|
+
const translations = createTranslationFile<
|
|
400
|
+
${languagesUnionAsString},
|
|
401
|
+
${serialiseObjectToType(translationsType)}
|
|
402
|
+
>({
|
|
403
|
+
${languageEntries}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export default translations;
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
async function generateRuntime(loadedTranslation) {
|
|
410
|
+
const { languages: loadedLanguages, filePath } = loadedTranslation;
|
|
411
|
+
trace("Generating types for", filePath);
|
|
412
|
+
const translationTypes = /* @__PURE__ */ new Map();
|
|
413
|
+
let imports = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const key of loadedTranslation.keys) {
|
|
415
|
+
let params = {};
|
|
416
|
+
const messages = /* @__PURE__ */ new Set();
|
|
417
|
+
let hasTags = false;
|
|
418
|
+
for (const translatedLanguage of Object.values(loadedLanguages)) if (translatedLanguage[key]) {
|
|
419
|
+
const ast = (0, __formatjs_icu_messageformat_parser.parse)(translatedLanguage[key].message);
|
|
420
|
+
hasTags = hasTags || extractHasTags(ast);
|
|
421
|
+
const [parsedParams, vocabTypesImports] = extractParamTypes(ast, params);
|
|
422
|
+
imports = new Set([...imports, ...vocabTypesImports]);
|
|
423
|
+
params = parsedParams;
|
|
424
|
+
messages.add(JSON.stringify(translatedLanguage[key].message));
|
|
425
|
+
}
|
|
426
|
+
translationTypes.set(key, {
|
|
427
|
+
params,
|
|
428
|
+
hasTags,
|
|
429
|
+
message: Array.from(messages).join(" | ")
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const prettierConfig = await prettier.default.resolveConfig(filePath);
|
|
433
|
+
const serializedTranslationType = serialiseTranslationRuntime(translationTypes, imports, loadedTranslation);
|
|
434
|
+
const declaration = await prettier.default.format(serializedTranslationType, {
|
|
435
|
+
...prettierConfig,
|
|
436
|
+
parser: "typescript"
|
|
437
|
+
});
|
|
438
|
+
const outputFilePath = getTSFileFromDevLanguageFile(filePath);
|
|
439
|
+
trace(`Writing translation types to ${outputFilePath}`);
|
|
440
|
+
await writeIfChanged(outputFilePath, declaration);
|
|
441
|
+
}
|
|
442
|
+
function watch(config) {
|
|
443
|
+
const cwd = config.projectRoot || process.cwd();
|
|
444
|
+
const watcher = chokidar.default.watch([
|
|
445
|
+
getDevTranslationFileGlob(config),
|
|
446
|
+
getAltTranslationFileGlob(config),
|
|
447
|
+
getTranslationFolderGlob(config)
|
|
448
|
+
], {
|
|
449
|
+
cwd,
|
|
450
|
+
ignored: config.ignore ? [...config.ignore, "**/node_modules/**"] : ["**/node_modules/**"],
|
|
451
|
+
ignoreInitial: true
|
|
452
|
+
});
|
|
453
|
+
const onTranslationChange = async (relativePath) => {
|
|
454
|
+
trace(`Detected change for file ${relativePath}`);
|
|
455
|
+
let targetFile;
|
|
456
|
+
if (isDevLanguageFile(relativePath)) targetFile = path.default.resolve(cwd, relativePath);
|
|
457
|
+
else if (isAltLanguageFile(relativePath)) targetFile = getDevLanguageFileFromAltLanguageFile(path.default.resolve(cwd, relativePath));
|
|
458
|
+
if (targetFile) try {
|
|
459
|
+
const loadedTranslation = loadTranslation({
|
|
460
|
+
filePath: targetFile,
|
|
461
|
+
fallbacks: "all"
|
|
462
|
+
}, config);
|
|
463
|
+
await generateRuntime(loadedTranslation);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
console.log("Failed to generate types for", relativePath);
|
|
466
|
+
console.error(e);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const onNewDirectory = async (relativePath) => {
|
|
470
|
+
trace("Detected new directory", relativePath);
|
|
471
|
+
if (!isTranslationDirectory(relativePath, config)) {
|
|
472
|
+
trace("Ignoring non-translation directory:", relativePath);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const newFilePath = path.default.join(relativePath, devTranslationFileName);
|
|
476
|
+
if (!(0, fs.existsSync)(newFilePath)) {
|
|
477
|
+
await fs.promises.writeFile(newFilePath, JSON.stringify({}, null, 2));
|
|
478
|
+
trace("Created new empty translation file:", newFilePath);
|
|
479
|
+
} else trace(`New directory already contains translation file. Skipping creation. Existing file ${newFilePath}`);
|
|
480
|
+
};
|
|
481
|
+
watcher.on("addDir", onNewDirectory);
|
|
482
|
+
watcher.on("add", onTranslationChange).on("change", onTranslationChange);
|
|
483
|
+
return () => watcher.close();
|
|
484
|
+
}
|
|
485
|
+
async function compile({ watch: shouldWatch = false } = {}, config) {
|
|
486
|
+
const translations = await loadAllTranslations({
|
|
487
|
+
fallbacks: "all",
|
|
488
|
+
includeNodeModules: false
|
|
489
|
+
}, config);
|
|
490
|
+
for (const loadedTranslation of translations) await generateRuntime(loadedTranslation);
|
|
491
|
+
if (shouldWatch) {
|
|
492
|
+
trace("Listening for changes to files...");
|
|
493
|
+
return watch(config);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function writeIfChanged(filepath, contents) {
|
|
497
|
+
let hasChanged = true;
|
|
498
|
+
try {
|
|
499
|
+
hasChanged = await fs.promises.readFile(filepath, { encoding: "utf-8" }) !== contents;
|
|
500
|
+
} catch {}
|
|
501
|
+
if (hasChanged) await fs.promises.writeFile(filepath, contents, { encoding: "utf-8" });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/validate/index.ts
|
|
506
|
+
function findMissingKeys(loadedTranslation, devLanguageName, altLanguages) {
|
|
507
|
+
const devLanguage = loadedTranslation.languages[devLanguageName];
|
|
508
|
+
if (!devLanguage) throw new Error(`Failed to load dev language: ${loadedTranslation.filePath}`);
|
|
509
|
+
const result = {};
|
|
510
|
+
let valid = true;
|
|
511
|
+
const requiredKeys = Object.keys(devLanguage);
|
|
512
|
+
if (requiredKeys.length > 0) for (const altLanguageName of altLanguages) {
|
|
513
|
+
const altLanguage = loadedTranslation.languages[altLanguageName] ?? {};
|
|
514
|
+
for (const key of requiredKeys) if (typeof altLanguage[key]?.message !== "string") {
|
|
515
|
+
if (!result[altLanguageName]) result[altLanguageName] = [];
|
|
516
|
+
result[altLanguageName].push(key);
|
|
517
|
+
valid = false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return [valid, result];
|
|
521
|
+
}
|
|
522
|
+
async function validate(config) {
|
|
523
|
+
const allTranslations = await loadAllTranslations({
|
|
524
|
+
fallbacks: "valid",
|
|
525
|
+
includeNodeModules: true
|
|
526
|
+
}, config);
|
|
527
|
+
let valid = true;
|
|
528
|
+
for (const loadedTranslation of allTranslations) {
|
|
529
|
+
const [translationValid, result] = findMissingKeys(loadedTranslation, config.devLanguage, getAltLanguages(config));
|
|
530
|
+
if (!translationValid) {
|
|
531
|
+
valid = false;
|
|
532
|
+
console.log(picocolors.default.red(`Incomplete translations: "${picocolors.default.bold(loadedTranslation.relativePath)}"`));
|
|
533
|
+
for (const lang of Object.keys(result)) {
|
|
534
|
+
const missingKeys = result[lang];
|
|
535
|
+
console.log(picocolors.default.yellow(lang), "->", missingKeys.map((v) => `"${v}"`).join(", "));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return valid;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/ValidationError.ts
|
|
544
|
+
var ValidationError = class extends Error {
|
|
545
|
+
code;
|
|
546
|
+
rawMessage;
|
|
547
|
+
constructor(code, message) {
|
|
548
|
+
super(`Invalid vocab.config.js: ${code} - ${message}`);
|
|
549
|
+
this.code = code;
|
|
550
|
+
this.rawMessage = message;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/config.ts
|
|
556
|
+
const boldCyan = (s) => picocolors.default.bold(picocolors.default.cyan(s));
|
|
557
|
+
const checkConfigFile = new fastest_validator.default().compile({
|
|
558
|
+
$$strict: true,
|
|
559
|
+
devLanguage: { type: "string" },
|
|
560
|
+
languages: {
|
|
561
|
+
type: "array",
|
|
562
|
+
items: {
|
|
563
|
+
type: "object",
|
|
564
|
+
props: {
|
|
565
|
+
name: { type: "string" },
|
|
566
|
+
extends: {
|
|
567
|
+
type: "string",
|
|
568
|
+
optional: true
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
generatedLanguages: {
|
|
574
|
+
type: "array",
|
|
575
|
+
items: {
|
|
576
|
+
type: "object",
|
|
577
|
+
props: {
|
|
578
|
+
name: { type: "string" },
|
|
579
|
+
extends: {
|
|
580
|
+
type: "string",
|
|
581
|
+
optional: true
|
|
582
|
+
},
|
|
583
|
+
generator: {
|
|
584
|
+
type: "object",
|
|
585
|
+
props: {
|
|
586
|
+
transformElement: {
|
|
587
|
+
type: "function",
|
|
588
|
+
optional: true
|
|
589
|
+
},
|
|
590
|
+
transformMessage: {
|
|
591
|
+
type: "function",
|
|
592
|
+
optional: true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
optional: true
|
|
599
|
+
},
|
|
600
|
+
translationsDirectorySuffix: {
|
|
601
|
+
type: "string",
|
|
602
|
+
optional: true
|
|
603
|
+
},
|
|
604
|
+
projectRoot: {
|
|
605
|
+
type: "string",
|
|
606
|
+
optional: true
|
|
607
|
+
},
|
|
608
|
+
ignore: {
|
|
609
|
+
type: "array",
|
|
610
|
+
items: "string",
|
|
611
|
+
optional: true
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const splitMap = (message, callback) => message.split(" ,").map((v) => callback(v)).join(" ,");
|
|
615
|
+
function validateConfig(c) {
|
|
616
|
+
trace("Validating configuration file");
|
|
617
|
+
const isValid = checkConfigFile(c);
|
|
618
|
+
if (isValid !== true) throw new ValidationError("InvalidStructure", (Array.isArray(isValid) ? isValid : []).map((v) => {
|
|
619
|
+
if (v.type === "objectStrict") return `Invalid key(s) ${splitMap(v.actual, (m) => `"${picocolors.default.cyan(m)}"`)}. Expected one of ${splitMap(v.expected, picocolors.default.green)}`;
|
|
620
|
+
if (v.field) return v.message?.replace(v.field, picocolors.default.cyan(v.field));
|
|
621
|
+
return v.message;
|
|
622
|
+
}).join(" \n"));
|
|
623
|
+
const languageStrings = c.languages.map((v) => v.name);
|
|
624
|
+
if (!languageStrings.includes(c.devLanguage)) throw new ValidationError("InvalidDevLanguage", `The dev language "${boldCyan(c.devLanguage)}" was not found in languages ${languageStrings.join(", ")}.`);
|
|
625
|
+
const foundLanguages = [];
|
|
626
|
+
for (const lang of c.languages) {
|
|
627
|
+
if (foundLanguages.includes(lang.name)) throw new ValidationError("DuplicateLanguage", `The language "${boldCyan(lang.name)}" was defined multiple times.`);
|
|
628
|
+
foundLanguages.push(lang.name);
|
|
629
|
+
if (lang.extends && !languageStrings.includes(lang.extends)) throw new ValidationError("InvalidExtends", `The language "${boldCyan(lang.name)}"'s extends of ${boldCyan(lang.extends)} was not found in languages ${languageStrings.join(", ")}.`);
|
|
630
|
+
}
|
|
631
|
+
const foundGeneratedLanguages = [];
|
|
632
|
+
for (const generatedLang of c.generatedLanguages || []) {
|
|
633
|
+
if (foundGeneratedLanguages.includes(generatedLang.name)) throw new ValidationError("DuplicateGeneratedLanguage", `The generated language "${boldCyan(generatedLang.name)}" was defined multiple times.`);
|
|
634
|
+
foundGeneratedLanguages.push(generatedLang.name);
|
|
635
|
+
if (languageStrings.includes(generatedLang.name)) throw new ValidationError("InvalidGeneratedLanguage", `The generated language "${boldCyan(generatedLang.name)}" is already defined as a language.`);
|
|
636
|
+
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) throw new ValidationError("InvalidExtends", `The generated language "${boldCyan(generatedLang.name)}"'s extends of ${boldCyan(generatedLang.extends)} was not found in languages ${languageStrings.join(", ")}.`);
|
|
637
|
+
}
|
|
638
|
+
trace("Configuration file is valid");
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
function createConfig(configFilePath) {
|
|
642
|
+
return {
|
|
643
|
+
projectRoot: path.default.dirname(configFilePath),
|
|
644
|
+
...require(configFilePath)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async function resolveConfig(customConfigFilePath) {
|
|
648
|
+
const configFilePath = customConfigFilePath ? path.default.resolve(customConfigFilePath) : await (0, find_up.default)(["vocab.config.js", "vocab.config.cjs"]);
|
|
649
|
+
if (configFilePath) {
|
|
650
|
+
trace(`Resolved configuration file to ${configFilePath}`);
|
|
651
|
+
return createConfig(configFilePath);
|
|
652
|
+
}
|
|
653
|
+
trace("No configuration file found");
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
function resolveConfigSync(customConfigFilePath) {
|
|
657
|
+
const configFilePath = customConfigFilePath ? path.default.resolve(customConfigFilePath) : find_up.default.sync(["vocab.config.js", "vocab.config.cjs"]);
|
|
658
|
+
if (configFilePath) {
|
|
659
|
+
trace(`Resolved configuration file to ${configFilePath}`);
|
|
660
|
+
return createConfig(configFilePath);
|
|
661
|
+
}
|
|
662
|
+
trace("No configuration file found");
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
//#endregion
|
|
667
|
+
exports.compile = compile;
|
|
668
|
+
exports.getAltLanguageFilePath = getAltLanguageFilePath;
|
|
669
|
+
exports.getAltLanguages = getAltLanguages;
|
|
670
|
+
exports.getDevLanguageFileFromTsFile = getDevLanguageFileFromTsFile;
|
|
671
|
+
exports.getUniqueKey = getUniqueKey;
|
|
672
|
+
exports.loadAllTranslations = loadAllTranslations;
|
|
673
|
+
exports.loadTranslation = loadTranslation;
|
|
674
|
+
exports.resolveConfig = resolveConfig;
|
|
675
|
+
exports.resolveConfigSync = resolveConfigSync;
|
|
676
|
+
exports.validate = validate;
|
|
677
|
+
exports.validateConfig = validateConfig;
|
|
678
|
+
exports.watch = watch;
|
|
679
|
+
//# sourceMappingURL=index.js.map
|