@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/src/config.ts DELETED
@@ -1,144 +0,0 @@
1
- import { UserConfig } from '@vocab/types';
2
- import path from 'path';
3
-
4
- import chalk from 'chalk';
5
- import findUp from 'find-up';
6
- import Validator from 'fastest-validator';
7
- import { ValidationError } from './ValidationError';
8
- import { trace } from './logger';
9
-
10
- const validator = new Validator();
11
- const schema = {
12
- $$strict: true,
13
- devLanguage: {
14
- type: 'string',
15
- },
16
- languages: {
17
- type: 'array',
18
- items: {
19
- type: 'object',
20
- props: {
21
- name: { type: 'string' },
22
- extends: { type: 'string', optional: true },
23
- },
24
- },
25
- },
26
- translationsDirectorySuffix: { type: 'string', optional: true },
27
- projectRoot: { type: 'string', optional: true },
28
- ignore: {
29
- type: 'array',
30
- items: 'string',
31
- optional: true,
32
- },
33
- };
34
- const checkConfigFile = validator.compile(schema);
35
-
36
- const splitMap = (message: string, callback: (value: string) => string) =>
37
- message
38
- .split(' ,')
39
- .map((v) => callback(v))
40
- .join(' ,');
41
-
42
- export function validateConfig(c: UserConfig) {
43
- trace('Validating configuration file');
44
- // Note: checkConfigFile mutates the config file by applying defaults
45
- const isValid = checkConfigFile(c);
46
- if (isValid !== true) {
47
- throw new ValidationError(
48
- 'InvalidStructure',
49
- (Array.isArray(isValid) ? isValid : [])
50
- .map((v) => {
51
- if (v.type === 'objectStrict') {
52
- return `Invalid key(s) ${splitMap(
53
- v.actual,
54
- (m) => `"${chalk.cyan(m)}"`,
55
- )}. Expected one of ${splitMap(v.expected, chalk.green)}`;
56
- }
57
- if (v.field) {
58
- return v.message?.replace(v.field, chalk.cyan(v.field));
59
- }
60
- return v.message;
61
- })
62
- .join(' \n'),
63
- );
64
- }
65
-
66
- // Dev Language should exist in languages
67
- const languageStrings = c.languages.map((v) => v.name);
68
- if (!languageStrings.includes(c.devLanguage)) {
69
- throw new ValidationError(
70
- 'InvalidDevLanguage',
71
- `InvalidDevLanguage: The dev language "${chalk.bold.cyan(
72
- c.devLanguage,
73
- )}" was not found in languages ${languageStrings.join(', ')}.`,
74
- );
75
- }
76
-
77
- const foundLanguages: string[] = [];
78
- for (const lang of c.languages) {
79
- // Languages must only exist once
80
- if (foundLanguages.includes(lang.name)) {
81
- throw new ValidationError(
82
- 'DuplicateLanguage',
83
- `The language "${chalk.bold.cyan(
84
- lang.name,
85
- )}" was defined multiple times.`,
86
- );
87
- }
88
- foundLanguages.push(lang.name);
89
-
90
- // Any extends must be in languages
91
- if (lang.extends && !languageStrings.includes(lang.extends)) {
92
- throw new ValidationError(
93
- 'InvalidExtends',
94
- `The language "${chalk.bold.cyan(
95
- lang.name,
96
- )}"'s extends of ${chalk.bold.cyan(
97
- lang.extends,
98
- )} was not found in languages ${languageStrings.join(', ')}.`,
99
- );
100
- }
101
- }
102
- trace('Configuration file is valid');
103
- return true;
104
- }
105
-
106
- function createConfig(configFilePath: string) {
107
- const cwd = path.dirname(configFilePath);
108
-
109
- return {
110
- projectRoot: cwd,
111
- ...(require(configFilePath as string) as UserConfig),
112
- };
113
- }
114
-
115
- export async function resolveConfig(
116
- customConfigFilePath?: string,
117
- ): Promise<UserConfig | null> {
118
- const configFilePath = customConfigFilePath
119
- ? path.resolve(customConfigFilePath)
120
- : await findUp('vocab.config.js');
121
-
122
- if (configFilePath) {
123
- trace(`Resolved configuration file to ${configFilePath}`);
124
- return createConfig(configFilePath);
125
- }
126
- trace('No configuration file found');
127
- return null;
128
- }
129
-
130
- export function resolveConfigSync(
131
- customConfigFilePath?: string,
132
- ): UserConfig | null {
133
- const configFilePath = customConfigFilePath
134
- ? path.resolve(customConfigFilePath)
135
- : findUp.sync('vocab.config.js');
136
-
137
- if (configFilePath) {
138
- trace(`Resolved configuration file to ${configFilePath}`);
139
- return createConfig(configFilePath);
140
- }
141
- trace('No configuration file found');
142
-
143
- return null;
144
- }
@@ -1,35 +0,0 @@
1
- import { ParsedICUMessages, TranslationMessagesByKey } from '@vocab/types';
2
- import IntlMessageFormat from 'intl-messageformat';
3
-
4
- type ICUMessagesByLocale = {
5
- [locale: string]: ParsedICUMessages<any>;
6
- };
7
-
8
- const moduleCache = new WeakMap<
9
- TranslationMessagesByKey,
10
- ICUMessagesByLocale
11
- >();
12
-
13
- export const getParsedICUMessages = (
14
- m: TranslationMessagesByKey,
15
- locale: string,
16
- ): ParsedICUMessages<any> => {
17
- const moduleCachedResult = moduleCache.get(m);
18
-
19
- if (moduleCachedResult && moduleCachedResult[locale]) {
20
- return moduleCachedResult[locale];
21
- }
22
-
23
- const parsedICUMessages: ParsedICUMessages<any> = {};
24
-
25
- for (const translation of Object.keys(m)) {
26
- const intlMessageFormat = new IntlMessageFormat(m[translation], locale);
27
- parsedICUMessages[translation] = {
28
- format: (params: any) => intlMessageFormat.format(params),
29
- };
30
- }
31
-
32
- moduleCache.set(m, { ...moduleCachedResult, [locale]: parsedICUMessages });
33
-
34
- return parsedICUMessages;
35
- };
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- export { compile, watch } from './compile';
2
- export { validate } from './validate';
3
- export { resolveConfig, resolveConfigSync, validateConfig } from './config';
4
- export {
5
- getAltLanguages,
6
- getAltLanguageFilePath,
7
- getDevLanguageFileFromTsFile,
8
- } from './utils';
9
- export {
10
- getUniqueKey,
11
- loadAllTranslations,
12
- loadTranslation,
13
- } from './load-translations';
14
- export type { TranslationFile } from '@vocab/types';
@@ -1,165 +0,0 @@
1
- import {
2
- getFallbackLanguageOrder,
3
- getLanguageHierarchy,
4
- loadAltLanguageFile,
5
- mergeWithDevLanguageTranslation,
6
- } from './load-translations';
7
- import path from 'path';
8
-
9
- describe('mergeWithDevLanguage', () => {
10
- const key = 'Hello';
11
-
12
- describe('when the translation has a message for a key in the dev translation', () => {
13
- it('should have a message from the translation', () => {
14
- const thTranslation = { Hello: { message: 'Hello in Thai' } };
15
- const devTranslation = { Hello: { message: 'Hello' } };
16
-
17
- const mergedTranslations = mergeWithDevLanguageTranslation({
18
- translation: thTranslation,
19
- devTranslation,
20
- });
21
-
22
- expect(mergedTranslations[key]).toEqual({ message: 'Hello in Thai' });
23
- });
24
- });
25
-
26
- describe('when the translation does not have a message for a key in the dev translation', () => {
27
- it('should not have a message for the given key', () => {
28
- const translation = {};
29
- const devTranslation = { Hello: { message: 'Hello' } };
30
-
31
- const mergedTranslations = mergeWithDevLanguageTranslation({
32
- translation,
33
- devTranslation,
34
- });
35
-
36
- expect(mergedTranslations[key]).toEqual(undefined);
37
- });
38
- });
39
- });
40
-
41
- describe('getLanguageHierarchy', () => {
42
- it('should return a correct language hierarchy', () => {
43
- expect(
44
- getLanguageHierarchy({
45
- languages: [
46
- { name: 'en' },
47
- { name: 'th', extends: 'en' },
48
- { name: 'th-TH', extends: 'th' },
49
- { name: 'en-AU', extends: 'en' },
50
- ],
51
- }),
52
- ).toEqual(
53
- new Map([
54
- ['en', []],
55
- ['th', ['en']],
56
- ['th-TH', ['th', 'en']],
57
- ['en-AU', ['en']],
58
- ]),
59
- );
60
- });
61
- });
62
-
63
- describe('getFallbackLanguageOrder', () => {
64
- const languages = [
65
- { name: 'fr' },
66
- { name: 'en' },
67
- { name: 'th', extends: 'en' },
68
- { name: 'th-TH', extends: 'th' },
69
- ];
70
- const languageName = 'th-TH';
71
- const devLanguage = 'fr';
72
-
73
- describe('fallbacks = none', () => {
74
- it('should return just the requested language', () => {
75
- const fallbacks = 'none';
76
-
77
- expect(
78
- getFallbackLanguageOrder({
79
- languages,
80
- languageName,
81
- devLanguage,
82
- fallbacks,
83
- }),
84
- ).toEqual(['th-TH']);
85
- });
86
- });
87
-
88
- describe('fallbacks = valid', () => {
89
- it('should return just the requested language', () => {
90
- const fallbacks = 'valid';
91
-
92
- expect(
93
- getFallbackLanguageOrder({
94
- languages,
95
- languageName,
96
- devLanguage,
97
- fallbacks,
98
- }),
99
- ).toEqual(['en', 'th', 'th-TH']);
100
- });
101
- });
102
-
103
- describe('fallbacks = all', () => {
104
- it('should return just the requested language', () => {
105
- const fallbacks = 'all';
106
-
107
- expect(
108
- getFallbackLanguageOrder({
109
- languages,
110
- languageName,
111
- devLanguage,
112
- fallbacks,
113
- }),
114
- ).toEqual(['fr', 'en', 'th', 'th-TH']);
115
- });
116
- });
117
- });
118
-
119
- describe('loadAltLanguageFile', () => {
120
- describe('when a language extends a language that also extends another language', () => {
121
- it('should resolve translation message correctly according to the fallback hierarchy', () => {
122
- const filePath = path.join(
123
- __dirname,
124
- 'test-translations/translations.json',
125
- );
126
- const devTranslation = {
127
- Hello: { message: 'Hello in French' },
128
- Goodbye: {
129
- message: 'Goodbye in French',
130
- },
131
- Welcome: {
132
- message: 'Welcome in French',
133
- },
134
- 'Good morning': {
135
- message: 'Good morning in French',
136
- },
137
- };
138
-
139
- const thTHTranslations = loadAltLanguageFile(
140
- {
141
- filePath,
142
- languageName: 'th-TH',
143
- devTranslation,
144
- fallbacks: 'all',
145
- },
146
- {
147
- devLanguage: 'fr',
148
- languages: [
149
- { name: 'fr' },
150
- { name: 'en' },
151
- { name: 'th', extends: 'en' },
152
- { name: 'th-TH', extends: 'th' },
153
- ],
154
- },
155
- );
156
-
157
- expect(thTHTranslations).toEqual({
158
- Hello: { message: 'Hello in Thai' },
159
- Goodbye: { message: 'Goodbye' },
160
- Welcome: { message: 'Welcome in Thai-TH' },
161
- 'Good morning': { message: 'Good morning in French' },
162
- });
163
- });
164
- });
165
- });
@@ -1,351 +0,0 @@
1
- import path from 'path';
2
-
3
- import glob from 'fast-glob';
4
- import {
5
- TranslationsByKey,
6
- UserConfig,
7
- LoadedTranslation,
8
- LanguageTarget,
9
- LanguageName,
10
- } from '@vocab/types';
11
- import chalk from 'chalk';
12
-
13
- import { trace } from './logger';
14
- import {
15
- defaultTranslationDirSuffix,
16
- Fallback,
17
- getAltLanguageFilePath,
18
- getAltLanguages,
19
- getDevTranslationFileGlob,
20
- } from './utils';
21
-
22
- export function getUniqueKey(key: string, namespace: string) {
23
- return `${key}.${namespace}`;
24
- }
25
-
26
- export function mergeWithDevLanguageTranslation({
27
- translation,
28
- devTranslation,
29
- }: {
30
- translation: TranslationsByKey;
31
- devTranslation: TranslationsByKey;
32
- }) {
33
- // Only use keys from the dev translation
34
- const keys = Object.keys(devTranslation);
35
- const newLanguage: TranslationsByKey = {};
36
-
37
- for (const key of keys) {
38
- if (translation[key]) {
39
- newLanguage[key] = {
40
- message: translation[key].message,
41
- description: devTranslation[key].description,
42
- };
43
- }
44
- }
45
-
46
- return newLanguage;
47
- }
48
-
49
- function getLanguageFallbacks({
50
- languages,
51
- }: {
52
- languages: Array<LanguageTarget>;
53
- }) {
54
- const languageFallbackMap = new Map<LanguageName, LanguageName>();
55
-
56
- for (const lang of languages) {
57
- if (lang.extends) {
58
- languageFallbackMap.set(lang.name, lang.extends);
59
- }
60
- }
61
-
62
- return languageFallbackMap;
63
- }
64
-
65
- export function getLanguageHierarchy({
66
- languages,
67
- }: {
68
- languages: Array<LanguageTarget>;
69
- }) {
70
- const hierarchyMap = new Map<LanguageName, Array<LanguageName>>();
71
- const fallbacks = getLanguageFallbacks({ languages });
72
-
73
- for (const lang of languages) {
74
- const langHierarchy = [];
75
- let currLang = lang.extends;
76
-
77
- while (currLang) {
78
- langHierarchy.push(currLang);
79
-
80
- currLang = fallbacks.get(currLang);
81
- }
82
-
83
- hierarchyMap.set(lang.name, langHierarchy);
84
- }
85
-
86
- return hierarchyMap;
87
- }
88
-
89
- export function getFallbackLanguageOrder({
90
- languages,
91
- languageName,
92
- devLanguage,
93
- fallbacks,
94
- }: {
95
- languages: LanguageTarget[];
96
- languageName: string;
97
- devLanguage: string;
98
- fallbacks: Fallback;
99
- }) {
100
- const languageHierarchy = getLanguageHierarchy({ languages }).get(
101
- languageName,
102
- );
103
-
104
- if (!languageHierarchy) {
105
- throw new Error(`Missing language hierarchy for ${languageName}`);
106
- }
107
-
108
- const fallbackLanguageOrder: Array<string> = [languageName];
109
-
110
- if (fallbacks !== 'none') {
111
- fallbackLanguageOrder.unshift(...languageHierarchy.reverse());
112
-
113
- if (fallbacks === 'all' && fallbackLanguageOrder[0] !== devLanguage) {
114
- fallbackLanguageOrder.unshift(devLanguage);
115
- }
116
- }
117
-
118
- return fallbackLanguageOrder;
119
- }
120
-
121
- function getNamespaceByFilePath(
122
- relativePath: string,
123
- { translationsDirectorySuffix = defaultTranslationDirSuffix }: UserConfig,
124
- ) {
125
- let namespace = path
126
- .dirname(relativePath)
127
- .replace(/^src\//, '')
128
- .replace(/\//g, '_');
129
-
130
- if (namespace.endsWith(translationsDirectorySuffix)) {
131
- namespace = namespace.slice(0, -translationsDirectorySuffix.length);
132
- }
133
-
134
- return namespace;
135
- }
136
-
137
- function printValidationError(...params: unknown[]) {
138
- // eslint-disable-next-line no-console
139
- console.error(chalk.red('Error loading translation:'), ...params);
140
- }
141
-
142
- function getTranslationsFromFile(
143
- translations: unknown,
144
- { isAltLanguage, filePath }: { isAltLanguage: boolean; filePath: string },
145
- ): { $namespace: unknown; keys: TranslationsByKey } {
146
- if (!translations || typeof translations !== 'object') {
147
- throw new Error(
148
- `Unable to read translation file ${filePath}. Translations must be an object`,
149
- );
150
- }
151
- const { $namespace, ...keys } = translations as TranslationsByKey;
152
- if (isAltLanguage && $namespace) {
153
- printValidationError(
154
- `Found $namespace in alt language file in ${filePath}. $namespace is only used in the dev language and will be ignored.`,
155
- );
156
- }
157
- if (!isAltLanguage && $namespace && typeof $namespace !== 'string') {
158
- printValidationError(
159
- `Found non-string $namespace in language file in ${filePath}. $namespace must be a string.`,
160
- );
161
- }
162
- const validKeys: TranslationsByKey = {};
163
- for (const [translationKey, translation] of Object.entries(keys)) {
164
- if (typeof translation === 'string') {
165
- printValidationError(
166
- `Found string for a translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`,
167
- );
168
- continue;
169
- }
170
- if (!translation) {
171
- printValidationError(
172
- `Found empty translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`,
173
- );
174
- continue;
175
- }
176
- if (!translation.message || typeof translation.message !== 'string') {
177
- printValidationError(
178
- `No message found for translation "${translationKey}" in ${filePath}. Translation must be an object of the format {mesage: string}.`,
179
- );
180
- continue;
181
- }
182
- validKeys[translationKey] = translation;
183
- }
184
- return { $namespace, keys: validKeys };
185
- }
186
-
187
- export function loadAltLanguageFile(
188
- {
189
- filePath,
190
- languageName,
191
- devTranslation,
192
- fallbacks,
193
- }: {
194
- filePath: string;
195
- languageName: string;
196
- devTranslation: TranslationsByKey;
197
- fallbacks: Fallback;
198
- },
199
- { devLanguage, languages }: UserConfig,
200
- ): TranslationsByKey {
201
- const altLanguageTranslation = {};
202
-
203
- const fallbackLanguageOrder = getFallbackLanguageOrder({
204
- languages,
205
- languageName,
206
- devLanguage,
207
- fallbacks,
208
- });
209
-
210
- trace(
211
- `Loading alt language file with precedence: ${fallbackLanguageOrder
212
- .slice()
213
- .reverse()
214
- .join(' -> ')}`,
215
- );
216
-
217
- for (const fallbackLanguage of fallbackLanguageOrder) {
218
- if (fallbackLanguage !== devLanguage) {
219
- try {
220
- const altFilePath = getAltLanguageFilePath(filePath, fallbackLanguage);
221
- delete require.cache[altFilePath];
222
-
223
- const translationFile = require(altFilePath);
224
- const { keys: fallbackLanguageTranslation } = getTranslationsFromFile(
225
- translationFile,
226
- {
227
- filePath: altFilePath,
228
- isAltLanguage: true,
229
- },
230
- );
231
- Object.assign(
232
- altLanguageTranslation,
233
- mergeWithDevLanguageTranslation({
234
- translation: fallbackLanguageTranslation,
235
- devTranslation,
236
- }),
237
- );
238
- } catch (e) {
239
- trace(`Missing alt language file ${getAltLanguageFilePath(
240
- filePath,
241
- fallbackLanguage,
242
- )}
243
- `);
244
- }
245
- } else {
246
- Object.assign(altLanguageTranslation, devTranslation);
247
- }
248
- }
249
-
250
- return altLanguageTranslation;
251
- }
252
-
253
- export function loadTranslation(
254
- {
255
- filePath,
256
- fallbacks,
257
- }: {
258
- filePath: string;
259
- fallbacks: Fallback;
260
- },
261
- userConfig: UserConfig,
262
- ): LoadedTranslation {
263
- trace(
264
- `Loading translation file in "${fallbacks}" fallback mode: "${filePath}"`,
265
- );
266
-
267
- const languageSet: Record<
268
- string,
269
- Record<string, { message: string; description?: string | undefined }>
270
- > = {};
271
-
272
- delete require.cache[filePath];
273
- const translationContent = require(filePath);
274
- const relativePath = path.relative(
275
- userConfig.projectRoot || process.cwd(),
276
- filePath,
277
- );
278
- const { $namespace, keys: devTranslation } = getTranslationsFromFile(
279
- translationContent,
280
- {
281
- filePath,
282
- isAltLanguage: false,
283
- },
284
- );
285
- const namespace: string =
286
- typeof $namespace === 'string'
287
- ? $namespace
288
- : getNamespaceByFilePath(relativePath, userConfig);
289
-
290
- trace(`Found file ${filePath}. Using namespace ${namespace}`);
291
-
292
- languageSet[userConfig.devLanguage] = devTranslation;
293
- const altLanguages = getAltLanguages(userConfig);
294
- for (const languageName of altLanguages) {
295
- languageSet[languageName] = loadAltLanguageFile(
296
- {
297
- filePath,
298
- languageName,
299
- devTranslation,
300
- fallbacks,
301
- },
302
- userConfig,
303
- );
304
- }
305
-
306
- return {
307
- filePath,
308
- keys: Object.keys(devTranslation),
309
- namespace,
310
- relativePath,
311
- languages: languageSet,
312
- };
313
- }
314
-
315
- export async function loadAllTranslations(
316
- {
317
- fallbacks,
318
- includeNodeModules,
319
- }: { fallbacks: Fallback; includeNodeModules: boolean },
320
- config: UserConfig,
321
- ): Promise<Array<LoadedTranslation>> {
322
- const { projectRoot, ignore = [] } = config;
323
-
324
- const translationFiles = await glob(getDevTranslationFileGlob(config), {
325
- ignore: includeNodeModules ? ignore : [...ignore, '**/node_modules/**'],
326
- absolute: true,
327
- cwd: projectRoot,
328
- });
329
-
330
- trace(`Found ${translationFiles.length} translation files`);
331
-
332
- const result = await Promise.all(
333
- translationFiles.map((filePath) =>
334
- loadTranslation({ filePath, fallbacks }, config),
335
- ),
336
- );
337
- const keys = new Set();
338
- for (const loadedTranslation of result) {
339
- for (const key of loadedTranslation.keys) {
340
- const uniqueKey = getUniqueKey(key, loadedTranslation.namespace);
341
- if (keys.has(uniqueKey)) {
342
- trace(`Duplicate keys found`);
343
- throw new Error(
344
- `Duplicate keys found. Key with namespace ${loadedTranslation.namespace} and key ${key} was found multiple times.`,
345
- );
346
- }
347
- keys.add(uniqueKey);
348
- }
349
- }
350
- return result;
351
- }