@hazeljs/i18n 0.2.0-beta.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -0
- package/README.md +292 -0
- package/dist/decorators/lang.decorator.d.ts +23 -0
- package/dist/decorators/lang.decorator.d.ts.map +1 -0
- package/dist/decorators/lang.decorator.js +40 -0
- package/dist/decorators/lang.decorator.test.d.ts +2 -0
- package/dist/decorators/lang.decorator.test.d.ts.map +1 -0
- package/dist/decorators/lang.decorator.test.js +61 -0
- package/dist/i18n.interceptor.d.ts +29 -0
- package/dist/i18n.interceptor.d.ts.map +1 -0
- package/dist/i18n.interceptor.js +52 -0
- package/dist/i18n.interceptor.test.d.ts +2 -0
- package/dist/i18n.interceptor.test.d.ts.map +1 -0
- package/dist/i18n.interceptor.test.js +126 -0
- package/dist/i18n.middleware.d.ts +55 -0
- package/dist/i18n.middleware.d.ts.map +1 -0
- package/dist/i18n.middleware.js +120 -0
- package/dist/i18n.middleware.test.d.ts +2 -0
- package/dist/i18n.middleware.test.d.ts.map +1 -0
- package/dist/i18n.middleware.test.js +239 -0
- package/dist/i18n.module.d.ts +73 -0
- package/dist/i18n.module.d.ts.map +1 -0
- package/dist/i18n.module.js +154 -0
- package/dist/i18n.module.test.d.ts +2 -0
- package/dist/i18n.module.test.d.ts.map +1 -0
- package/dist/i18n.module.test.js +162 -0
- package/dist/i18n.service.d.ts +112 -0
- package/dist/i18n.service.d.ts.map +1 -0
- package/dist/i18n.service.js +228 -0
- package/dist/i18n.service.test.d.ts +2 -0
- package/dist/i18n.service.test.d.ts.map +1 -0
- package/dist/i18n.service.test.js +297 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/translation.loader.d.ts +16 -0
- package/dist/translation.loader.d.ts.map +1 -0
- package/dist/translation.loader.js +47 -0
- package/dist/translation.loader.test.d.ts +2 -0
- package/dist/translation.loader.test.d.ts.map +1 -0
- package/dist/translation.loader.test.js +85 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation.loader.test.d.ts","sourceRoot":"","sources":["../src/translation.loader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const translation_loader_1 = require("./translation.loader");
|
|
4
|
+
jest.mock('fs/promises', () => ({
|
|
5
|
+
readdir: jest.fn(),
|
|
6
|
+
readFile: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const mockReaddir = promises_1.readdir;
|
|
10
|
+
const mockReadFile = promises_1.readFile;
|
|
11
|
+
describe('TranslationLoader', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe('load()', () => {
|
|
16
|
+
it('returns an empty store when directory does not exist', async () => {
|
|
17
|
+
mockReaddir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
|
18
|
+
const store = await translation_loader_1.TranslationLoader.load('/nonexistent/path');
|
|
19
|
+
expect(store.size).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
it('returns an empty store when directory is empty', async () => {
|
|
22
|
+
mockReaddir.mockResolvedValue([]);
|
|
23
|
+
const store = await translation_loader_1.TranslationLoader.load('/empty/dir');
|
|
24
|
+
expect(store.size).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
it('ignores non-JSON files', async () => {
|
|
27
|
+
mockReaddir.mockResolvedValue(['README.md', 'notes.txt']);
|
|
28
|
+
const store = await translation_loader_1.TranslationLoader.load('/some/dir');
|
|
29
|
+
expect(store.size).toBe(0);
|
|
30
|
+
expect(mockReadFile).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
it('loads a single JSON file and stores translations by locale', async () => {
|
|
33
|
+
mockReaddir.mockResolvedValue(['en.json']);
|
|
34
|
+
mockReadFile.mockResolvedValue('{"hello":"Hello","bye":"Goodbye"}');
|
|
35
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
36
|
+
expect(store.has('en')).toBe(true);
|
|
37
|
+
expect(store.get('en')).toEqual({ hello: 'Hello', bye: 'Goodbye' });
|
|
38
|
+
});
|
|
39
|
+
it('loads multiple JSON files into separate locale entries', async () => {
|
|
40
|
+
mockReaddir.mockResolvedValue(['en.json', 'fr.json']);
|
|
41
|
+
mockReadFile
|
|
42
|
+
.mockResolvedValueOnce('{"hello":"Hello"}')
|
|
43
|
+
.mockResolvedValueOnce('{"hello":"Bonjour"}');
|
|
44
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
45
|
+
expect(store.size).toBe(2);
|
|
46
|
+
expect(store.get('en')).toEqual({ hello: 'Hello' });
|
|
47
|
+
expect(store.get('fr')).toEqual({ hello: 'Bonjour' });
|
|
48
|
+
});
|
|
49
|
+
it('handles locale codes with hyphens (zh-TW.json)', async () => {
|
|
50
|
+
mockReaddir.mockResolvedValue(['zh-TW.json']);
|
|
51
|
+
mockReadFile.mockResolvedValue('{"greeting":"你好"}');
|
|
52
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
53
|
+
expect(store.has('zh-TW')).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('skips malformed JSON files and writes to stderr', async () => {
|
|
56
|
+
const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
57
|
+
mockReaddir.mockResolvedValue(['en.json', 'bad.json']);
|
|
58
|
+
mockReadFile
|
|
59
|
+
.mockResolvedValueOnce('{"ok":true}')
|
|
60
|
+
.mockResolvedValueOnce('{ invalid json');
|
|
61
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
62
|
+
expect(store.has('en')).toBe(true);
|
|
63
|
+
expect(store.has('bad')).toBe(false);
|
|
64
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('[@hazeljs/i18n]'));
|
|
65
|
+
stderrSpy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
it('writes non-Error exception to stderr as string', async () => {
|
|
68
|
+
const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
69
|
+
mockReaddir.mockResolvedValue(['broken.json']);
|
|
70
|
+
mockReadFile.mockRejectedValue('string error');
|
|
71
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
72
|
+
expect(store.has('broken')).toBe(false);
|
|
73
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('string error'));
|
|
74
|
+
stderrSpy.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
it('loads nested translation objects', async () => {
|
|
77
|
+
mockReaddir.mockResolvedValue(['en.json']);
|
|
78
|
+
mockReadFile.mockResolvedValue(JSON.stringify({ errors: { notFound: 'Not found', invalid: 'Invalid' } }));
|
|
79
|
+
const store = await translation_loader_1.TranslationLoader.load('/translations');
|
|
80
|
+
expect(store.get('en')).toEqual({
|
|
81
|
+
errors: { notFound: 'Not found', invalid: 'Invalid' },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported locale detection strategies, in order of priority.
|
|
3
|
+
*/
|
|
4
|
+
export type LocaleDetectionStrategy = 'query' | 'cookie' | 'header';
|
|
5
|
+
/**
|
|
6
|
+
* A flat or nested map of translation keys to values.
|
|
7
|
+
* Nested objects represent namespaced keys (e.g. "errors.notFound").
|
|
8
|
+
* Leaf values are translation strings, which may contain {placeholder} tokens
|
|
9
|
+
* and optional plural forms via a "one"/"other" sub-object.
|
|
10
|
+
*/
|
|
11
|
+
export type TranslationValue = string | {
|
|
12
|
+
one: string;
|
|
13
|
+
other: string;
|
|
14
|
+
[form: string]: string;
|
|
15
|
+
} | {
|
|
16
|
+
[key: string]: TranslationValue;
|
|
17
|
+
};
|
|
18
|
+
export type TranslationMap = Record<string, TranslationValue>;
|
|
19
|
+
/**
|
|
20
|
+
* In-memory store: locale → flat translation map.
|
|
21
|
+
*/
|
|
22
|
+
export type LocaleStore = Map<string, TranslationMap>;
|
|
23
|
+
/**
|
|
24
|
+
* Options for interpolating variables and selecting plural forms.
|
|
25
|
+
*/
|
|
26
|
+
export interface TranslateOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Used for plural rule selection.
|
|
29
|
+
*/
|
|
30
|
+
count?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Key-value pairs substituted into {placeholder} tokens.
|
|
33
|
+
*/
|
|
34
|
+
vars?: Record<string, string | number>;
|
|
35
|
+
/**
|
|
36
|
+
* Override the locale for this single call.
|
|
37
|
+
*/
|
|
38
|
+
locale?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configuration options for I18nModule.forRoot().
|
|
42
|
+
*/
|
|
43
|
+
export interface I18nOptions {
|
|
44
|
+
/**
|
|
45
|
+
* The locale used when no locale can be detected.
|
|
46
|
+
* @default 'en'
|
|
47
|
+
*/
|
|
48
|
+
defaultLocale?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The locale to fall back to when a key is missing in the requested locale.
|
|
51
|
+
* @default same as defaultLocale
|
|
52
|
+
*/
|
|
53
|
+
fallbackLocale?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Absolute or relative path to the directory containing JSON translation files.
|
|
56
|
+
* Files must be named <locale>.json (e.g. en.json, fr.json).
|
|
57
|
+
* @default './translations'
|
|
58
|
+
*/
|
|
59
|
+
translationsPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Ordered list of locale-detection strategies to apply per request.
|
|
62
|
+
* @default ['query', 'cookie', 'header']
|
|
63
|
+
*/
|
|
64
|
+
detection?: LocaleDetectionStrategy[];
|
|
65
|
+
/**
|
|
66
|
+
* Name of the query-string parameter to check.
|
|
67
|
+
* @default 'lang'
|
|
68
|
+
*/
|
|
69
|
+
queryParam?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Name of the cookie to check.
|
|
72
|
+
* @default 'locale'
|
|
73
|
+
*/
|
|
74
|
+
cookieName?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Whether to register this module globally so it does not need to be
|
|
77
|
+
* imported in every feature module.
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
isGlobal?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Fully resolved options with all defaults applied.
|
|
84
|
+
*/
|
|
85
|
+
export type ResolvedI18nOptions = Required<I18nOptions>;
|
|
86
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACtD;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAA;CAAE,CAAC;AAExC,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAEvC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;IAEtC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hazeljs/i18n",
|
|
3
|
+
"version": "0.2.0-beta.57",
|
|
4
|
+
"description": "Internationalization (i18n) module for HazelJS framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest --coverage",
|
|
13
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.17.50",
|
|
19
|
+
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
20
|
+
"@typescript-eslint/parser": "^8.18.2",
|
|
21
|
+
"eslint": "^8.56.0",
|
|
22
|
+
"jest": "^29.7.0",
|
|
23
|
+
"ts-jest": "^29.1.2",
|
|
24
|
+
"typescript": "^5.3.3"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/hazel-js/hazeljs.git",
|
|
32
|
+
"directory": "packages/i18n"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"hazeljs",
|
|
36
|
+
"i18n",
|
|
37
|
+
"internationalization",
|
|
38
|
+
"localization",
|
|
39
|
+
"translation"
|
|
40
|
+
],
|
|
41
|
+
"author": "Muhammad Arslan <muhammad.arslan@hazeljs.com>",
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/hazeljs/hazel-js/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://hazeljs.com",
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
49
|
+
},
|
|
50
|
+
"gitHead": "98c84683e8f487c660e344c2beda3a8dcaaff07f"
|
|
51
|
+
}
|