@vocab/core 1.0.4 → 1.1.0
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/CHANGELOG.md +14 -0
- package/README.md +148 -1
- package/dist/declarations/src/generate-language.d.ts +5 -0
- package/dist/vocab-core.cjs.dev.js +136 -7
- package/dist/vocab-core.cjs.prod.js +136 -7
- package/dist/vocab-core.esm.js +136 -8
- package/package.json +8 -2
- package/src/ValidationError.ts +0 -9
- package/src/compile.ts +0 -329
- package/src/config.test.ts +0 -39
- package/src/config.ts +0 -144
- package/src/icu-handler.ts +0 -35
- package/src/index.ts +0 -14
- package/src/load-translations.test.ts +0 -165
- package/src/load-translations.ts +0 -351
- package/src/logger.ts +0 -9
- package/src/runtime.test.ts +0 -153
- package/src/runtime.ts +0 -12
- package/src/test-translations/en.translations.json +0 -9
- package/src/test-translations/th-TH.translations.json +0 -1
- package/src/test-translations/th.translations.json +0 -1
- package/src/translation-file.ts +0 -54
- package/src/utils.test.ts +0 -78
- package/src/utils.ts +0 -143
- package/src/validate/index.test.ts +0 -64
- package/src/validate/index.ts +0 -81
package/dist/vocab-core.esm.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { existsSync, promises } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
|
|
3
|
+
import { TYPE, parse, isSelectElement, isTagElement, isArgumentElement, isNumberElement, isPluralElement, isDateElement, isTimeElement } from '@formatjs/icu-messageformat-parser';
|
|
4
4
|
import prettier from 'prettier';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import debug from 'debug';
|
|
8
8
|
import glob from 'fast-glob';
|
|
9
|
+
import IntlMessageFormat from 'intl-messageformat';
|
|
10
|
+
import { printAST } from '@formatjs/icu-messageformat-parser/printer';
|
|
9
11
|
import findUp from 'find-up';
|
|
10
12
|
import Validator from 'fastest-validator';
|
|
11
13
|
|
|
@@ -93,6 +95,71 @@ function getTranslationMessages(translations) {
|
|
|
93
95
|
return mapValues(translations, v => v.message);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function generateLanguageFromTranslations({
|
|
99
|
+
baseTranslations,
|
|
100
|
+
generator
|
|
101
|
+
}) {
|
|
102
|
+
if (!generator.transformElement && !generator.transformMessage) {
|
|
103
|
+
return baseTranslations;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const translationKeys = Object.keys(baseTranslations);
|
|
107
|
+
const generatedTranslations = {};
|
|
108
|
+
|
|
109
|
+
for (const translationKey of translationKeys) {
|
|
110
|
+
const translation = baseTranslations[translationKey];
|
|
111
|
+
let transformedMessage = translation.message;
|
|
112
|
+
|
|
113
|
+
if (generator.transformElement) {
|
|
114
|
+
const messageAst = new IntlMessageFormat(translation.message).getAst();
|
|
115
|
+
const transformedAst = messageAst.map(transformMessageFormatElement(generator.transformElement));
|
|
116
|
+
transformedMessage = printAST(transformedAst);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (generator.transformMessage) {
|
|
120
|
+
transformedMessage = generator.transformMessage(transformedMessage);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
generatedTranslations[translationKey] = {
|
|
124
|
+
message: transformedMessage
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return generatedTranslations;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function transformMessageFormatElement(transformElement) {
|
|
132
|
+
return messageFormatElement => {
|
|
133
|
+
const transformedMessageFormatElement = { ...messageFormatElement
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
switch (transformedMessageFormatElement.type) {
|
|
137
|
+
case TYPE.literal:
|
|
138
|
+
const transformedValue = transformElement(transformedMessageFormatElement.value);
|
|
139
|
+
transformedMessageFormatElement.value = transformedValue;
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case TYPE.select:
|
|
143
|
+
case TYPE.plural:
|
|
144
|
+
const transformedOptions = { ...transformedMessageFormatElement.options
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const key of Object.keys(transformedOptions)) {
|
|
148
|
+
transformedOptions[key].value = transformedOptions[key].value.map(transformMessageFormatElement(transformElement));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case TYPE.tag:
|
|
154
|
+
const transformedChildren = transformedMessageFormatElement.children.map(transformMessageFormatElement(transformElement));
|
|
155
|
+
transformedMessageFormatElement.children = transformedChildren;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return transformedMessageFormatElement;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
96
163
|
function getUniqueKey(key, namespace) {
|
|
97
164
|
return `${key}.${namespace}`;
|
|
98
165
|
}
|
|
@@ -221,17 +288,17 @@ function getTranslationsFromFile(translations, {
|
|
|
221
288
|
|
|
222
289
|
for (const [translationKey, translation] of Object.entries(keys)) {
|
|
223
290
|
if (typeof translation === 'string') {
|
|
224
|
-
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
291
|
+
printValidationError(`Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
225
292
|
continue;
|
|
226
293
|
}
|
|
227
294
|
|
|
228
295
|
if (!translation) {
|
|
229
|
-
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
296
|
+
printValidationError(`Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
230
297
|
continue;
|
|
231
298
|
}
|
|
232
299
|
|
|
233
300
|
if (!translation.message || typeof translation.message !== 'string') {
|
|
234
|
-
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {
|
|
301
|
+
printValidationError(`No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {message: string}.`);
|
|
235
302
|
continue;
|
|
236
303
|
}
|
|
237
304
|
|
|
@@ -323,6 +390,19 @@ function loadTranslation({
|
|
|
323
390
|
}, userConfig);
|
|
324
391
|
}
|
|
325
392
|
|
|
393
|
+
for (const generatedLanguage of userConfig.generatedLanguages || []) {
|
|
394
|
+
const {
|
|
395
|
+
name: generatedLanguageName,
|
|
396
|
+
generator
|
|
397
|
+
} = generatedLanguage;
|
|
398
|
+
const baseLanguage = generatedLanguage.extends || userConfig.devLanguage;
|
|
399
|
+
const baseTranslations = languageSet[baseLanguage];
|
|
400
|
+
languageSet[generatedLanguageName] = generateLanguageFromTranslations({
|
|
401
|
+
baseTranslations,
|
|
402
|
+
generator
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
326
406
|
return {
|
|
327
407
|
filePath,
|
|
328
408
|
keys: Object.keys(devTranslation),
|
|
@@ -694,6 +774,35 @@ const schema = {
|
|
|
694
774
|
}
|
|
695
775
|
}
|
|
696
776
|
},
|
|
777
|
+
generatedLanguages: {
|
|
778
|
+
type: 'array',
|
|
779
|
+
items: {
|
|
780
|
+
type: 'object',
|
|
781
|
+
props: {
|
|
782
|
+
name: {
|
|
783
|
+
type: 'string'
|
|
784
|
+
},
|
|
785
|
+
extends: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
optional: true
|
|
788
|
+
},
|
|
789
|
+
generator: {
|
|
790
|
+
type: 'object',
|
|
791
|
+
props: {
|
|
792
|
+
transformElement: {
|
|
793
|
+
type: 'function',
|
|
794
|
+
optional: true
|
|
795
|
+
},
|
|
796
|
+
transformMessage: {
|
|
797
|
+
type: 'function',
|
|
798
|
+
optional: true
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
optional: true
|
|
805
|
+
},
|
|
697
806
|
translationsDirectorySuffix: {
|
|
698
807
|
type: 'string',
|
|
699
808
|
optional: true
|
|
@@ -731,13 +840,12 @@ function validateConfig(c) {
|
|
|
731
840
|
|
|
732
841
|
return v.message;
|
|
733
842
|
}).join(' \n'));
|
|
734
|
-
}
|
|
735
|
-
|
|
843
|
+
}
|
|
736
844
|
|
|
737
|
-
const languageStrings = c.languages.map(v => v.name);
|
|
845
|
+
const languageStrings = c.languages.map(v => v.name); // Dev Language should exist in languages
|
|
738
846
|
|
|
739
847
|
if (!languageStrings.includes(c.devLanguage)) {
|
|
740
|
-
throw new ValidationError('InvalidDevLanguage', `
|
|
848
|
+
throw new ValidationError('InvalidDevLanguage', `The dev language "${chalk.bold.cyan(c.devLanguage)}" was not found in languages ${languageStrings.join(', ')}.`);
|
|
741
849
|
}
|
|
742
850
|
|
|
743
851
|
const foundLanguages = [];
|
|
@@ -755,6 +863,26 @@ function validateConfig(c) {
|
|
|
755
863
|
}
|
|
756
864
|
}
|
|
757
865
|
|
|
866
|
+
const foundGeneratedLanguages = [];
|
|
867
|
+
|
|
868
|
+
for (const generatedLang of c.generatedLanguages || []) {
|
|
869
|
+
// Generated languages must only exist once
|
|
870
|
+
if (foundGeneratedLanguages.includes(generatedLang.name)) {
|
|
871
|
+
throw new ValidationError('DuplicateGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" was defined multiple times.`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
foundGeneratedLanguages.push(generatedLang.name); // Generated language names must not conflict with language names
|
|
875
|
+
|
|
876
|
+
if (languageStrings.includes(generatedLang.name)) {
|
|
877
|
+
throw new ValidationError('InvalidGeneratedLanguage', `The generated language "${chalk.bold.cyan(generatedLang.name)}" is already defined as a language.`);
|
|
878
|
+
} // Any extends must be in languages
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
if (generatedLang.extends && !languageStrings.includes(generatedLang.extends)) {
|
|
882
|
+
throw new ValidationError('InvalidExtends', `The generated language "${chalk.bold.cyan(generatedLang.name)}"'s extends of ${chalk.bold.cyan(generatedLang.extends)} was not found in languages ${languageStrings.join(', ')}.`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
758
886
|
trace('Configuration file is valid');
|
|
759
887
|
return true;
|
|
760
888
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocab/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "dist/vocab-core.cjs.js",
|
|
5
5
|
"module": "dist/vocab-core.esm.js",
|
|
6
6
|
"author": "SEEK",
|
|
@@ -13,9 +13,15 @@
|
|
|
13
13
|
"translation-file.ts"
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"runtime",
|
|
19
|
+
"icu-handler",
|
|
20
|
+
"translation-file"
|
|
21
|
+
],
|
|
16
22
|
"dependencies": {
|
|
17
23
|
"@formatjs/icu-messageformat-parser": "^2.0.10",
|
|
18
|
-
"@vocab/types": "^1.0
|
|
24
|
+
"@vocab/types": "^1.1.0",
|
|
19
25
|
"chalk": "^4.1.0",
|
|
20
26
|
"chokidar": "^3.4.3",
|
|
21
27
|
"debug": "^4.3.1",
|
package/src/ValidationError.ts
DELETED
package/src/compile.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { promises as fs, existsSync } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import { LoadedTranslation, UserConfig } from '@vocab/types';
|
|
5
|
-
import {
|
|
6
|
-
isArgumentElement,
|
|
7
|
-
isDateElement,
|
|
8
|
-
isNumberElement,
|
|
9
|
-
isPluralElement,
|
|
10
|
-
isSelectElement,
|
|
11
|
-
isTagElement,
|
|
12
|
-
isTimeElement,
|
|
13
|
-
MessageFormatElement,
|
|
14
|
-
parse,
|
|
15
|
-
} from '@formatjs/icu-messageformat-parser';
|
|
16
|
-
import prettier from 'prettier';
|
|
17
|
-
import chokidar from 'chokidar';
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
getTranslationMessages,
|
|
21
|
-
getDevTranslationFileGlob,
|
|
22
|
-
getTSFileFromDevLanguageFile,
|
|
23
|
-
getDevLanguageFileFromAltLanguageFile,
|
|
24
|
-
getAltTranslationFileGlob,
|
|
25
|
-
isDevLanguageFile,
|
|
26
|
-
isAltLanguageFile,
|
|
27
|
-
getTranslationFolderGlob,
|
|
28
|
-
devTranslationFileName,
|
|
29
|
-
isTranslationDirectory,
|
|
30
|
-
} from './utils';
|
|
31
|
-
import { trace } from './logger';
|
|
32
|
-
import { loadAllTranslations, loadTranslation } from './load-translations';
|
|
33
|
-
|
|
34
|
-
type ICUParams = { [key: string]: string };
|
|
35
|
-
|
|
36
|
-
interface TranslationTypeInfo {
|
|
37
|
-
params: ICUParams;
|
|
38
|
-
message: string;
|
|
39
|
-
returnType: string;
|
|
40
|
-
hasTags: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const encodeWithinSingleQuotes = (v: string) => v.replace(/'/g, "\\'");
|
|
44
|
-
|
|
45
|
-
const encodeBackslash = (v: string) => v.replace(/\\/g, '\\\\');
|
|
46
|
-
|
|
47
|
-
function extractHasTags(ast: MessageFormatElement[]): boolean {
|
|
48
|
-
return ast.some((element) => {
|
|
49
|
-
if (isSelectElement(element)) {
|
|
50
|
-
const children = Object.values(element.options).map((o) => o.value);
|
|
51
|
-
return children.some((child) => extractHasTags(child));
|
|
52
|
-
}
|
|
53
|
-
return isTagElement(element);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function extractParamTypes(
|
|
58
|
-
ast: MessageFormatElement[],
|
|
59
|
-
): [params: ICUParams, imports: Set<string>] {
|
|
60
|
-
let params: ICUParams = {};
|
|
61
|
-
let imports = new Set<string>();
|
|
62
|
-
|
|
63
|
-
for (const element of ast) {
|
|
64
|
-
if (isArgumentElement(element)) {
|
|
65
|
-
params[element.value] = 'string';
|
|
66
|
-
} else if (isNumberElement(element)) {
|
|
67
|
-
params[element.value] = 'number';
|
|
68
|
-
} else if (isPluralElement(element)) {
|
|
69
|
-
params[element.value] = 'number';
|
|
70
|
-
} else if (isDateElement(element) || isTimeElement(element)) {
|
|
71
|
-
params[element.value] = 'Date | number';
|
|
72
|
-
} else if (isTagElement(element)) {
|
|
73
|
-
params[element.value] = 'FormatXMLElementFn<T>';
|
|
74
|
-
imports.add(`import { FormatXMLElementFn } from '@vocab/types';`);
|
|
75
|
-
|
|
76
|
-
const [subParams, subImports] = extractParamTypes(element.children);
|
|
77
|
-
|
|
78
|
-
imports = new Set([...imports, ...subImports]);
|
|
79
|
-
params = { ...params, ...subParams };
|
|
80
|
-
} else if (isSelectElement(element)) {
|
|
81
|
-
params[element.value] = Object.keys(element.options)
|
|
82
|
-
.map((o) => `'${o}'`)
|
|
83
|
-
.join(' | ');
|
|
84
|
-
|
|
85
|
-
const children = Object.values(element.options).map((o) => o.value);
|
|
86
|
-
|
|
87
|
-
for (const child of children) {
|
|
88
|
-
const [subParams, subImports] = extractParamTypes(child);
|
|
89
|
-
|
|
90
|
-
imports = new Set([...imports, ...subImports]);
|
|
91
|
-
params = { ...params, ...subParams };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return [params, imports];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function serialiseObjectToType(v: any) {
|
|
100
|
-
let result = '';
|
|
101
|
-
|
|
102
|
-
for (const [key, value] of Object.entries(v)) {
|
|
103
|
-
if (value && typeof value === 'object') {
|
|
104
|
-
result += `'${encodeWithinSingleQuotes(key)}': ${serialiseObjectToType(
|
|
105
|
-
value,
|
|
106
|
-
)},`;
|
|
107
|
-
} else {
|
|
108
|
-
result += `'${encodeWithinSingleQuotes(key)}': ${value},`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return `{ ${result} }`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const banner = `// This file is automatically generated by Vocab.\n// To make changes update translation.json files directly.`;
|
|
116
|
-
|
|
117
|
-
function serialiseTranslationRuntime(
|
|
118
|
-
value: Map<string, TranslationTypeInfo>,
|
|
119
|
-
imports: Set<string>,
|
|
120
|
-
loadedTranslation: LoadedTranslation,
|
|
121
|
-
) {
|
|
122
|
-
trace('Serialising translations:', loadedTranslation);
|
|
123
|
-
const translationsType: any = {};
|
|
124
|
-
|
|
125
|
-
for (const [key, { params, message, hasTags }] of value.entries()) {
|
|
126
|
-
let translationFunctionString = `() => ${message}`;
|
|
127
|
-
|
|
128
|
-
if (Object.keys(params).length > 0) {
|
|
129
|
-
const formatGeneric = hasTags ? '<T = string>' : '';
|
|
130
|
-
const formatReturn = hasTags
|
|
131
|
-
? 'string | T | Array<string | T>'
|
|
132
|
-
: 'string';
|
|
133
|
-
translationFunctionString = `${formatGeneric}(values: ${serialiseObjectToType(
|
|
134
|
-
params,
|
|
135
|
-
)}) => ${formatReturn}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
translationsType[encodeBackslash(key)] = translationFunctionString;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const content = Object.entries(loadedTranslation.languages)
|
|
142
|
-
.map(
|
|
143
|
-
([languageName, translations]) =>
|
|
144
|
-
`'${encodeWithinSingleQuotes(
|
|
145
|
-
languageName,
|
|
146
|
-
)}': createLanguage(${JSON.stringify(
|
|
147
|
-
getTranslationMessages(translations),
|
|
148
|
-
)})`,
|
|
149
|
-
)
|
|
150
|
-
.join(',');
|
|
151
|
-
|
|
152
|
-
const languagesUnionAsString = Object.keys(loadedTranslation.languages)
|
|
153
|
-
.map((l) => `'${l}'`)
|
|
154
|
-
.join(' | ');
|
|
155
|
-
|
|
156
|
-
return `${banner}
|
|
157
|
-
|
|
158
|
-
${Array.from(imports).join('\n')}
|
|
159
|
-
import { createLanguage, createTranslationFile } from '@vocab/core/runtime';
|
|
160
|
-
|
|
161
|
-
const translations = createTranslationFile<${languagesUnionAsString}, ${serialiseObjectToType(
|
|
162
|
-
translationsType,
|
|
163
|
-
)}>({${content}});
|
|
164
|
-
|
|
165
|
-
export default translations;`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export async function generateRuntime(loadedTranslation: LoadedTranslation) {
|
|
169
|
-
const { languages: loadedLanguages, filePath } = loadedTranslation;
|
|
170
|
-
|
|
171
|
-
trace('Generating types for', loadedTranslation.filePath);
|
|
172
|
-
const translationTypes = new Map<string, TranslationTypeInfo>();
|
|
173
|
-
|
|
174
|
-
let imports = new Set<string>();
|
|
175
|
-
|
|
176
|
-
for (const key of loadedTranslation.keys) {
|
|
177
|
-
let params: ICUParams = {};
|
|
178
|
-
const messages = new Set();
|
|
179
|
-
let hasTags = false;
|
|
180
|
-
|
|
181
|
-
for (const translatedLanguage of Object.values(loadedLanguages)) {
|
|
182
|
-
if (translatedLanguage[key]) {
|
|
183
|
-
const ast = parse(translatedLanguage[key].message);
|
|
184
|
-
|
|
185
|
-
hasTags = hasTags || extractHasTags(ast);
|
|
186
|
-
|
|
187
|
-
const [parsedParams, parsedImports] = extractParamTypes(ast);
|
|
188
|
-
imports = new Set([...imports, ...parsedImports]);
|
|
189
|
-
|
|
190
|
-
params = {
|
|
191
|
-
...params,
|
|
192
|
-
...parsedParams,
|
|
193
|
-
};
|
|
194
|
-
messages.add(
|
|
195
|
-
`'${encodeWithinSingleQuotes(translatedLanguage[key].message)}'`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const returnType = hasTags ? 'NonNullable<ReactNode>' : 'string';
|
|
201
|
-
|
|
202
|
-
translationTypes.set(key, {
|
|
203
|
-
params,
|
|
204
|
-
hasTags,
|
|
205
|
-
message: Array.from(messages).join(' | '),
|
|
206
|
-
returnType,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const prettierConfig = await prettier.resolveConfig(filePath);
|
|
211
|
-
const serializedTranslationType = serialiseTranslationRuntime(
|
|
212
|
-
translationTypes,
|
|
213
|
-
imports,
|
|
214
|
-
loadedTranslation,
|
|
215
|
-
);
|
|
216
|
-
const declaration = prettier.format(serializedTranslationType, {
|
|
217
|
-
...prettierConfig,
|
|
218
|
-
parser: 'typescript',
|
|
219
|
-
});
|
|
220
|
-
const outputFilePath = getTSFileFromDevLanguageFile(filePath);
|
|
221
|
-
trace(`Writing translation types to ${outputFilePath}`);
|
|
222
|
-
await writeIfChanged(outputFilePath, declaration);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function watch(config: UserConfig) {
|
|
226
|
-
const cwd = config.projectRoot || process.cwd();
|
|
227
|
-
|
|
228
|
-
const watcher = chokidar.watch(
|
|
229
|
-
[
|
|
230
|
-
getDevTranslationFileGlob(config),
|
|
231
|
-
getAltTranslationFileGlob(config),
|
|
232
|
-
getTranslationFolderGlob(config),
|
|
233
|
-
],
|
|
234
|
-
{
|
|
235
|
-
cwd,
|
|
236
|
-
ignored: config.ignore
|
|
237
|
-
? [...config.ignore, '**/node_modules/**']
|
|
238
|
-
: ['**/node_modules/**'],
|
|
239
|
-
ignoreInitial: true,
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const onTranslationChange = async (relativePath: string) => {
|
|
244
|
-
trace(`Detected change for file ${relativePath}`);
|
|
245
|
-
|
|
246
|
-
let targetFile;
|
|
247
|
-
|
|
248
|
-
if (isDevLanguageFile(relativePath)) {
|
|
249
|
-
targetFile = path.resolve(cwd, relativePath);
|
|
250
|
-
} else if (isAltLanguageFile(relativePath)) {
|
|
251
|
-
targetFile = getDevLanguageFileFromAltLanguageFile(
|
|
252
|
-
path.resolve(cwd, relativePath),
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (targetFile) {
|
|
257
|
-
try {
|
|
258
|
-
const loadedTranslation = await loadTranslation(
|
|
259
|
-
{ filePath: targetFile, fallbacks: 'all' },
|
|
260
|
-
config,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
await generateRuntime(loadedTranslation);
|
|
264
|
-
} catch (e) {
|
|
265
|
-
// eslint-disable-next-line no-console
|
|
266
|
-
console.log('Failed to generate types for', relativePath);
|
|
267
|
-
// eslint-disable-next-line no-console
|
|
268
|
-
console.error(e);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const onNewDirectory = async (relativePath: string) => {
|
|
274
|
-
trace('Detected new directory', relativePath);
|
|
275
|
-
if (!isTranslationDirectory(relativePath, config)) {
|
|
276
|
-
trace('Ignoring non-translation directory:', relativePath);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const newFilePath = path.join(relativePath, devTranslationFileName);
|
|
280
|
-
if (!existsSync(newFilePath)) {
|
|
281
|
-
await fs.writeFile(newFilePath, JSON.stringify({}, null, 2));
|
|
282
|
-
trace('Created new empty translation file:', newFilePath);
|
|
283
|
-
} else {
|
|
284
|
-
trace(
|
|
285
|
-
`New directory already contains translation file. Skipping creation. Existing file ${newFilePath}`,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
watcher.on('addDir', onNewDirectory);
|
|
291
|
-
watcher.on('add', onTranslationChange).on('change', onTranslationChange);
|
|
292
|
-
|
|
293
|
-
return () => watcher.close();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function compile(
|
|
297
|
-
{ watch: shouldWatch = false } = {},
|
|
298
|
-
config: UserConfig,
|
|
299
|
-
) {
|
|
300
|
-
const translations = await loadAllTranslations(
|
|
301
|
-
{ fallbacks: 'all', includeNodeModules: false },
|
|
302
|
-
config,
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
for (const loadedTranslation of translations) {
|
|
306
|
-
await generateRuntime(loadedTranslation);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (shouldWatch) {
|
|
310
|
-
trace('Listening for changes to files...');
|
|
311
|
-
return watch(config);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function writeIfChanged(filepath: string, contents: string) {
|
|
316
|
-
let hasChanged = true;
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const existingContents = await fs.readFile(filepath, { encoding: 'utf-8' });
|
|
320
|
-
|
|
321
|
-
hasChanged = existingContents !== contents;
|
|
322
|
-
} catch (e) {
|
|
323
|
-
// ignore error, likely a file doesn't exist error so we want to write anyway
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (hasChanged) {
|
|
327
|
-
await fs.writeFile(filepath, contents, { encoding: 'utf-8' });
|
|
328
|
-
}
|
|
329
|
-
}
|
package/src/config.test.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { validateConfig } from './config';
|
|
2
|
-
|
|
3
|
-
describe('validateConfig', () => {
|
|
4
|
-
it('should allow a valid config', () => {
|
|
5
|
-
const config = { devLanguage: 'en', languages: [{ name: 'en' }] };
|
|
6
|
-
expect(() => validateConfig(config)).not.toThrow();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('should throw an error on no config', () => {
|
|
10
|
-
// @ts-expect-error For Science!!!
|
|
11
|
-
expect(() => validateConfig({})).toThrowError(
|
|
12
|
-
expect.objectContaining({ code: 'InvalidStructure' }),
|
|
13
|
-
);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("should throw an error when the devLanguage isn't defined in languages", () => {
|
|
17
|
-
expect(() =>
|
|
18
|
-
validateConfig({ devLanguage: 'en', languages: [{ name: 'th' }] }),
|
|
19
|
-
).toThrowError(expect.objectContaining({ code: 'InvalidDevLanguage' }));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should throw an error when there are duplicate languages', () => {
|
|
23
|
-
expect(() =>
|
|
24
|
-
validateConfig({
|
|
25
|
-
devLanguage: 'en',
|
|
26
|
-
languages: [{ name: 'en' }, { name: 'en' }],
|
|
27
|
-
}),
|
|
28
|
-
).toThrowError(expect.objectContaining({ code: 'DuplicateLanguage' }));
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should throw an error when extending a missing language', () => {
|
|
32
|
-
expect(() =>
|
|
33
|
-
validateConfig({
|
|
34
|
-
devLanguage: 'en',
|
|
35
|
-
languages: [{ name: 'en' }, { name: 'en-US', extends: 'en-AU' }],
|
|
36
|
-
}),
|
|
37
|
-
).toThrowError(expect.objectContaining({ code: 'InvalidExtends' }));
|
|
38
|
-
});
|
|
39
|
-
});
|