@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/logger.ts DELETED
@@ -1,9 +0,0 @@
1
- import chalk from 'chalk';
2
- import debug from 'debug';
3
-
4
- export const trace = debug(`vocab:core`);
5
-
6
- export const log = (...params: unknown[]) => {
7
- // eslint-disable-next-line no-console
8
- console.log(chalk.yellow('Vocab'), ...params);
9
- };
@@ -1,153 +0,0 @@
1
- import { FormatXMLElementFn } from 'intl-messageformat';
2
- import { createTranslationFile, createLanguage } from './runtime';
3
-
4
- const createDemoTranslationFile = () =>
5
- createTranslationFile<
6
- 'en' | 'fr',
7
- {
8
- vocabPublishDate: <T = string>(values: {
9
- publishDate: Date | number;
10
- }) => string | T | Array<string | T>;
11
- }
12
- >({
13
- en: createLanguage({
14
- vocabPublishDate: 'Vocab was published on {publishDate, date, small}',
15
- }),
16
- fr: createLanguage({
17
- vocabPublishDate: 'Vocab a été publié le {publishDate, date, medium}',
18
- }),
19
- });
20
- const createDemoTranslationFileWithTag = () =>
21
- createTranslationFile<
22
- 'en' | 'fr',
23
- {
24
- vocabPublishDate: <T = string>(values: {
25
- link: FormatXMLElementFn<T>;
26
- strong: FormatXMLElementFn<T>;
27
- }) => string | T | Array<string | T>;
28
- }
29
- >({
30
- en: createLanguage({
31
- vocabPublishDate: '<link><strong>Vocab</strong> is awesome</link>!',
32
- }),
33
- fr: createLanguage({
34
- vocabPublishDate: '<link><strong>Vocab</strong> est génial</link>!',
35
- }),
36
- });
37
-
38
- describe('createTranslationFile', () => {
39
- it('should return translations as a promise', async () => {
40
- const translations = createDemoTranslationFile();
41
- const translationModule = await translations.getMessages('en');
42
- expect(
43
- translationModule?.vocabPublishDate.format({
44
- publishDate: 1605847714000,
45
- }),
46
- ).toBe('Vocab was published on 11/20/2020');
47
- });
48
- it('should return TranslationModules with language as locale', () => {
49
- const translations = createDemoTranslationFile();
50
- const translationModule = translations.getLoadedMessages('en');
51
- expect(
52
- translationModule?.vocabPublishDate.format({
53
- publishDate: 1605847714000,
54
- }),
55
- ).toBe('Vocab was published on 11/20/2020');
56
- });
57
-
58
- // Support for alternative ICU locales in Node not available in current CI environment
59
- // Disabling test for now until `full-icu` can be added. See https://nodejs.org/api/intl.html#intl_options_for_building_node_js
60
- // eslint-disable-next-line jest/no-disabled-tests
61
- it.skip('should return TranslationModules with en-AU locale', () => {
62
- const translations = createDemoTranslationFile();
63
- const translationModule = translations.getLoadedMessages('en', 'en-AU');
64
- expect(
65
- translationModule?.vocabPublishDate.format({
66
- publishDate: 1605847714000,
67
- }),
68
- ).toBe('Vocab was published on 20/11/2020');
69
- });
70
- it('should return an array when tags return objects', () => {
71
- const translations = createDemoTranslationFileWithTag();
72
- const translationModule = translations.getLoadedMessages('en', 'en-US');
73
- interface TagResult {
74
- type: string;
75
- children: unknown;
76
- }
77
- type ExpectedResultType = string | TagResult | Array<string | TagResult>;
78
- if (!translationModule) {
79
- throw new Error('no translationModule');
80
- }
81
- const result = translationModule.vocabPublishDate.format<TagResult>({
82
- strong: (children) => ({
83
- type: 'strong',
84
- children,
85
- }),
86
- link: (children) => ({
87
- type: 'link',
88
- children,
89
- }),
90
- });
91
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
- const _unused: ExpectedResultType = result;
93
- expect(result).toEqual(expect.any(Array));
94
- expect(result).toEqual([
95
- {
96
- children: [{ children: ['Vocab'], type: 'strong' }, ' is awesome'],
97
- type: 'link',
98
- },
99
- '!',
100
- ]);
101
- });
102
- it('should return a string when all tags return strings', () => {
103
- const translations = createDemoTranslationFileWithTag();
104
- const translationModule = translations.getLoadedMessages('en', 'en-US');
105
- type ExpectedResultType = string | Array<string>;
106
- if (!translationModule) {
107
- throw new Error('no translationModule');
108
- }
109
- const result = translationModule.vocabPublishDate.format({
110
- strong: (children) => `*${children}*`,
111
- link: (children) => `[${children}]()`,
112
- });
113
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
114
- const _unused: ExpectedResultType = result;
115
- expect(typeof result).toBe('string');
116
- expect(result).toBe('[*Vocab* is awesome]()!');
117
- });
118
- it('should return TranslationModules with en-US locale', () => {
119
- const translations = createDemoTranslationFile();
120
- const translationModule = translations.getLoadedMessages('en', 'en-US');
121
- if (!translationModule) {
122
- throw new Error('no translationModule');
123
- }
124
- const result = translationModule.vocabPublishDate.format({
125
- publishDate: 1605847714000,
126
- });
127
- expect(result).toBe('Vocab was published on 11/20/2020');
128
- });
129
- it('should require parameters to be passed in', () => {
130
- const translations = createDemoTranslationFile();
131
- const translationModule = translations.getLoadedMessages('en');
132
- expect(() => {
133
- // @ts-expect-error Incorrect params parameter
134
- const result = translationModule?.vocabPublishDate.format({});
135
- return result;
136
- }).toThrowError(
137
- expect.objectContaining({
138
- message: expect.stringContaining('not provided'),
139
- }),
140
- );
141
- expect(() => {
142
- const result = translationModule?.vocabPublishDate.format({
143
- // @ts-expect-error Incorrect params parameter
144
- unrelated: 'message',
145
- });
146
- return result;
147
- }).toThrowError(
148
- expect.objectContaining({
149
- message: expect.stringContaining('not provided'),
150
- }),
151
- );
152
- });
153
- });
package/src/runtime.ts DELETED
@@ -1,12 +0,0 @@
1
- import { TranslationModule, TranslationMessagesByKey } from '@vocab/types';
2
-
3
- import { getParsedICUMessages } from './icu-handler';
4
-
5
- export { createTranslationFile } from './translation-file';
6
-
7
- export const createLanguage = (
8
- module: TranslationMessagesByKey,
9
- ): TranslationModule<any> => ({
10
- getValue: (locale) => getParsedICUMessages(module, locale),
11
- load: () => Promise.resolve(),
12
- });
@@ -1,9 +0,0 @@
1
- {
2
- "Hello": { "message": "Hello" },
3
- "Goodbye": {
4
- "message": "Goodbye"
5
- },
6
- "Welcome": {
7
- "message": "Welcome"
8
- }
9
- }
@@ -1 +0,0 @@
1
- { "Welcome": { "message": "Welcome in Thai-TH" } }
@@ -1 +0,0 @@
1
- { "Hello": { "message": "Hello in Thai" } }
@@ -1,54 +0,0 @@
1
- import {
2
- TranslationModule,
3
- TranslationModuleByLanguage,
4
- ParsedICUMessages,
5
- LanguageName,
6
- ParsedFormatFnByKey,
7
- TranslationFile,
8
- } from '@vocab/types';
9
-
10
- export function createTranslationFile<
11
- Language extends LanguageName,
12
- FormatFnByKey extends ParsedFormatFnByKey,
13
- >(
14
- translationsByLanguage: TranslationModuleByLanguage<Language, FormatFnByKey>,
15
- ): TranslationFile<Language, FormatFnByKey> {
16
- function getByLanguage(language: Language): TranslationModule<FormatFnByKey> {
17
- const translationModule = translationsByLanguage[language];
18
- if (!translationModule) {
19
- throw new Error(
20
- `Attempted to retrieve translations for unknown language "${language}"`,
21
- );
22
- }
23
- return translationModule;
24
- }
25
-
26
- return {
27
- getLoadedMessages(
28
- language: Language,
29
- locale?: string,
30
- ): ParsedICUMessages<FormatFnByKey> | null {
31
- const translationModule = getByLanguage(language);
32
- return translationModule.getValue(locale || language) || null;
33
- },
34
- getMessages(
35
- language: Language,
36
- locale?: string,
37
- ): Promise<ParsedICUMessages<FormatFnByKey>> {
38
- const translationModule = getByLanguage(language);
39
- return translationModule.load().then(() => {
40
- const result = translationModule.getValue(locale || language);
41
- if (!result) {
42
- throw new Error(
43
- `Unable to find translations for ${language} after attempting to load. Module may have failed to load or an internal error may have occurred.`,
44
- );
45
- }
46
- return result;
47
- });
48
- },
49
- load(language: Language): Promise<void> {
50
- const translationModule = getByLanguage(language);
51
- return translationModule.load();
52
- },
53
- };
54
- }
package/src/utils.test.ts DELETED
@@ -1,78 +0,0 @@
1
- import {
2
- getDevLanguageFileFromTsFile,
3
- getAltLanguageFilePath,
4
- getTSFileFromDevLanguageFile,
5
- getDevLanguageFileFromAltLanguageFile,
6
- isDevLanguageFile,
7
- isAltLanguageFile,
8
- } from './utils';
9
-
10
- describe('getDevLanguageFileFromTsFile', () => {
11
- it('should find a translation.json file', () => {
12
- expect(getDevLanguageFileFromTsFile('/my/foobar/index.ts')).toBe(
13
- '/my/foobar/translations.json',
14
- );
15
- });
16
- });
17
-
18
- describe('getAltLanguageFilePath', () => {
19
- it('should find a translation.json file', () => {
20
- expect(getAltLanguageFilePath('/my/awesome/translations.json', 'fr')).toBe(
21
- '/my/awesome/fr.translations.json',
22
- );
23
- });
24
- });
25
-
26
- describe('getTSFileFromDevLanguageFile', () => {
27
- it('should find a translation.ts file', () => {
28
- expect(getTSFileFromDevLanguageFile('/my/foobar/translations.json')).toBe(
29
- '/my/foobar/index.ts',
30
- );
31
- });
32
- });
33
-
34
- describe('getDevLanguageFileFromAltLanguageFile', () => {
35
- it('should find a translation.json file', () => {
36
- expect(
37
- getDevLanguageFileFromAltLanguageFile(
38
- '/my/awesome/__translations__/fr.translations.json',
39
- ),
40
- ).toBe('/my/awesome/__translations__/translations.json');
41
- });
42
- });
43
-
44
- describe('isDevLanguageFile', () => {
45
- it('should match dev language filename', () => {
46
- expect(
47
- isDevLanguageFile('/my/awesome/__translations__/translations.json'),
48
- ).toBe(true);
49
- });
50
-
51
- it('should match relative dev language filename', () => {
52
- expect(isDevLanguageFile('translations.json')).toBe(true);
53
- });
54
-
55
- it('should not match alt language filename', () => {
56
- expect(
57
- isDevLanguageFile('/my/awesome/__translations__/fr.translations.json'),
58
- ).toBe(false);
59
- });
60
- });
61
-
62
- describe('isAltLanguageFile', () => {
63
- it('should match alt language filename', () => {
64
- expect(
65
- isAltLanguageFile('/my/awesome/__translations__/fr.translations.json'),
66
- ).toBe(true);
67
- });
68
-
69
- it('should match relative alt language filename', () => {
70
- expect(isAltLanguageFile('fr.translations.json')).toBe(true);
71
- });
72
-
73
- it('should not match alt language filename', () => {
74
- expect(
75
- isAltLanguageFile('/my/awesome/__translations__/translations.json'),
76
- ).toBe(false);
77
- });
78
- });
package/src/utils.ts DELETED
@@ -1,143 +0,0 @@
1
- import path from 'path';
2
-
3
- import type {
4
- LanguageName,
5
- LanguageTarget,
6
- TranslationsByKey,
7
- TranslationMessagesByKey,
8
- UserConfig,
9
- } from '@vocab/types';
10
- import { trace } from './logger';
11
-
12
- export const defaultTranslationDirSuffix = '.vocab';
13
- export const devTranslationFileName = 'translations.json';
14
-
15
- export type Fallback = 'none' | 'valid' | 'all';
16
-
17
- const globAnyPathWithOptionalPrefix = '**/?(*)';
18
-
19
- export function isDevLanguageFile(filePath: string) {
20
- return (
21
- filePath.endsWith(`/${devTranslationFileName}`) ||
22
- filePath === devTranslationFileName
23
- );
24
- }
25
- export function isAltLanguageFile(filePath: string) {
26
- return filePath.endsWith('.translations.json');
27
- }
28
- export function isTranslationDirectory(
29
- filePath: string,
30
- {
31
- translationsDirectorySuffix = defaultTranslationDirSuffix,
32
- }: {
33
- translationsDirectorySuffix?: string;
34
- },
35
- ) {
36
- return filePath.endsWith(translationsDirectorySuffix);
37
- }
38
-
39
- export function getTranslationFolderGlob({
40
- translationsDirectorySuffix = defaultTranslationDirSuffix,
41
- }: {
42
- translationsDirectorySuffix?: string;
43
- }) {
44
- const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}`;
45
-
46
- trace('getTranslationFolderGlob', result);
47
-
48
- return result;
49
- }
50
-
51
- export function getDevTranslationFileGlob({
52
- translationsDirectorySuffix = defaultTranslationDirSuffix,
53
- }: {
54
- translationsDirectorySuffix?: string;
55
- }) {
56
- const result = `${globAnyPathWithOptionalPrefix}${translationsDirectorySuffix}/${devTranslationFileName}`;
57
-
58
- trace('getDevTranslationFileGlob', result);
59
-
60
- return result;
61
- }
62
-
63
- export function getAltTranslationFileGlob(config: UserConfig) {
64
- const altLanguages = getAltLanguages(config);
65
- const langMatch =
66
- altLanguages.length === 1 ? altLanguages[0] : `{${altLanguages.join(',')}}`;
67
-
68
- const { translationsDirectorySuffix = defaultTranslationDirSuffix } = config;
69
- const result = `**/*${translationsDirectorySuffix}/${langMatch}.translations.json`;
70
-
71
- trace('getAltTranslationFileGlob', result);
72
-
73
- return result;
74
- }
75
-
76
- export function getAltLanguages({
77
- devLanguage,
78
- languages,
79
- }: {
80
- devLanguage: LanguageName;
81
- languages: Array<LanguageTarget>;
82
- }) {
83
- return languages.map((v) => v.name).filter((lang) => lang !== devLanguage);
84
- }
85
-
86
- export function getDevLanguageFileFromTsFile(tsFilePath: string) {
87
- const directory = path.dirname(tsFilePath);
88
- const result = path.normalize(path.join(directory, devTranslationFileName));
89
-
90
- trace(`Returning dev language path ${result} for path ${tsFilePath}`);
91
- return result;
92
- }
93
-
94
- export function getDevLanguageFileFromAltLanguageFile(
95
- altLanguageFilePath: string,
96
- ) {
97
- const directory = path.dirname(altLanguageFilePath);
98
- const result = path.normalize(path.join(directory, devTranslationFileName));
99
- trace(
100
- `Returning dev language path ${result} for path ${altLanguageFilePath}`,
101
- );
102
- return result;
103
- }
104
-
105
- export function getTSFileFromDevLanguageFile(devLanguageFilePath: string) {
106
- const directory = path.dirname(devLanguageFilePath);
107
- const result = path.normalize(path.join(directory, 'index.ts'));
108
-
109
- trace(`Returning TS path ${result} for path ${devLanguageFilePath}`);
110
- return result;
111
- }
112
-
113
- export function getAltLanguageFilePath(
114
- devLanguageFilePath: string,
115
- language: string,
116
- ) {
117
- const directory = path.dirname(devLanguageFilePath);
118
- const result = path.normalize(
119
- path.join(directory, `${language}.translations.json`),
120
- );
121
- trace(
122
- `Returning alt language path ${result} for path ${devLanguageFilePath}`,
123
- );
124
- return path.normalize(result);
125
- }
126
-
127
- export function mapValues<Key extends string, OriginalValue, ReturnValue>(
128
- obj: Record<Key, OriginalValue>,
129
- func: (val: OriginalValue) => ReturnValue,
130
- ): TranslationMessagesByKey<Key> {
131
- const newObj: any = {};
132
- const keys = Object.keys(obj) as Key[];
133
- for (const key of keys) {
134
- newObj[key] = func(obj[key]);
135
- }
136
- return newObj;
137
- }
138
-
139
- export function getTranslationMessages<Key extends string>(
140
- translations: TranslationsByKey<Key>,
141
- ): TranslationMessagesByKey<Key> {
142
- return mapValues(translations, (v) => v.message);
143
- }
@@ -1,64 +0,0 @@
1
- import { LoadedTranslation, LanguageName } from '@vocab/types';
2
- import { findMissingKeys } from './index';
3
-
4
- interface TestCase {
5
- loadedTranslation: LoadedTranslation;
6
- devLanguage: LanguageName;
7
- altLanguages: Array<LanguageName>;
8
- valid: boolean;
9
- missingKeys?: Record<LanguageName, Array<string>>;
10
- }
11
-
12
- const testCases: Array<TestCase> = [
13
- {
14
- loadedTranslation: {
15
- filePath: 'some-file.json',
16
- namespace: 'some-file',
17
- keys: ['key1', 'key2'],
18
- relativePath: 'some-file.json',
19
- languages: {
20
- en: { key1: { message: 'Hi' } },
21
- th: { key1: { message: 'Bye' } },
22
- },
23
- },
24
- devLanguage: 'en',
25
- altLanguages: ['th'],
26
- valid: true,
27
- },
28
- {
29
- loadedTranslation: {
30
- filePath: 'some-file.json',
31
- relativePath: 'some-file.json',
32
- namespace: 'some-file-2',
33
- keys: ['key1'],
34
- languages: {
35
- en: { key1: { message: 'Hi' } },
36
- th: {},
37
- },
38
- },
39
- devLanguage: 'en',
40
- altLanguages: ['th'],
41
- valid: false,
42
- missingKeys: {
43
- th: ['key1'],
44
- },
45
- },
46
- ];
47
-
48
- test.each(testCases)(
49
- 'validate',
50
- ({ loadedTranslation, devLanguage, altLanguages, valid, missingKeys }) => {
51
- const result = findMissingKeys(
52
- loadedTranslation,
53
- devLanguage,
54
- altLanguages,
55
- );
56
-
57
- expect(result[0]).toBe(valid);
58
-
59
- if (missingKeys) {
60
- // eslint-disable-next-line jest/no-conditional-expect
61
- expect(result[1]).toMatchObject(missingKeys);
62
- }
63
- },
64
- );
@@ -1,81 +0,0 @@
1
- /* eslint-disable no-console */
2
- import { UserConfig, LoadedTranslation, LanguageName } from '@vocab/types';
3
- import chalk from 'chalk';
4
-
5
- import { loadAllTranslations } from '../load-translations';
6
- import { getAltLanguages } from '../utils';
7
-
8
- export function findMissingKeys(
9
- loadedTranslation: LoadedTranslation,
10
- devLanguageName: LanguageName,
11
- altLanguages: Array<LanguageName>,
12
- ) {
13
- const devLanguage = loadedTranslation.languages[devLanguageName];
14
-
15
- if (!devLanguage) {
16
- throw new Error(
17
- `Failed to load dev language: ${loadedTranslation.filePath}`,
18
- );
19
- }
20
-
21
- const result: Record<LanguageName, Array<string>> = {};
22
- let valid = true;
23
-
24
- const requiredKeys = Object.keys(devLanguage);
25
-
26
- if (requiredKeys.length > 0) {
27
- for (const altLanguageName of altLanguages) {
28
- const altLanguage = loadedTranslation.languages[altLanguageName] ?? {};
29
-
30
- for (const key of requiredKeys) {
31
- if (typeof altLanguage[key]?.message !== 'string') {
32
- if (!result[altLanguageName]) {
33
- result[altLanguageName] = [];
34
- }
35
-
36
- result[altLanguageName].push(key);
37
- valid = false;
38
- }
39
- }
40
- }
41
- }
42
- return [valid, result] as const;
43
- }
44
-
45
- export async function validate(config: UserConfig) {
46
- const allTranslations = await loadAllTranslations(
47
- { fallbacks: 'valid', includeNodeModules: true },
48
- config,
49
- );
50
-
51
- let valid = true;
52
-
53
- for (const loadedTranslation of allTranslations) {
54
- const [translationValid, result] = findMissingKeys(
55
- loadedTranslation,
56
- config.devLanguage,
57
- getAltLanguages(config),
58
- );
59
-
60
- if (!translationValid) {
61
- valid = false;
62
- console.log(
63
- chalk.red`Incomplete translations: "${chalk.bold(
64
- loadedTranslation.relativePath,
65
- )}"`,
66
- );
67
-
68
- for (const lang of Object.keys(result)) {
69
- const missingKeys = result[lang];
70
-
71
- console.log(
72
- chalk.yellow(lang),
73
- '->',
74
- missingKeys.map((v) => `"${v}"`).join(', '),
75
- );
76
- }
77
- }
78
- }
79
-
80
- return valid;
81
- }