@hazeljs/i18n 0.2.0-beta.57 → 0.2.0-beta.58
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/decorators/lang.decorator.d.ts +15 -5
- package/dist/decorators/lang.decorator.d.ts.map +1 -1
- package/dist/decorators/lang.decorator.js +19 -6
- package/dist/decorators/lang.decorator.test.js +3 -3
- package/dist/i18n.middleware.d.ts +4 -0
- package/dist/i18n.middleware.d.ts.map +1 -1
- package/dist/i18n.middleware.js +6 -0
- package/dist/i18n.module.d.ts +1 -1
- package/dist/i18n.module.d.ts.map +1 -1
- package/dist/i18n.module.js +6 -6
- package/dist/i18n.module.test.js +22 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/translation.loader.d.ts +6 -2
- package/dist/translation.loader.d.ts.map +1 -1
- package/dist/translation.loader.js +11 -7
- package/dist/translation.loader.test.js +48 -44
- package/package.json +2 -2
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
+
/**
|
|
3
|
+
* Internal query key used to pass the resolved locale through the HazelJS
|
|
4
|
+
* request context so the router can inject it via @Lang().
|
|
5
|
+
*
|
|
6
|
+
* The proxy handler in main.ts writes this key into context.query before
|
|
7
|
+
* route matching; @Lang() reads it back using the standard 'query' injection
|
|
8
|
+
* type that the router already handles.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LANG_QUERY_KEY = "__hazel_i18n_locale__";
|
|
2
11
|
/**
|
|
3
12
|
* Parameter decorator that injects the detected locale string into a
|
|
4
13
|
* controller method parameter.
|
|
5
14
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
15
|
+
* Requires LocaleMiddleware (via addProxyHandler) to run before routing so
|
|
16
|
+
* the locale is resolved and stored in context.query[LANG_QUERY_KEY].
|
|
17
|
+
* When the middleware is not applied, the parameter will be undefined.
|
|
8
18
|
*
|
|
9
19
|
* @example
|
|
10
20
|
* ```ts
|
|
11
21
|
* @Get('/hello')
|
|
12
22
|
* greet(@Lang() locale: string) {
|
|
13
|
-
* return this.i18n.t('welcome', { locale });
|
|
23
|
+
* return this.i18n.t('welcome', { locale, vars: { name: 'World' } });
|
|
14
24
|
* }
|
|
15
25
|
* ```
|
|
16
26
|
*/
|
|
17
27
|
export declare function Lang(): ParameterDecorator;
|
|
18
28
|
/**
|
|
19
|
-
* Reads the locale injected by
|
|
20
|
-
*
|
|
29
|
+
* Reads the locale injected by LocaleMiddleware from the raw request object.
|
|
30
|
+
* Useful outside of controller parameters (e.g. in exception filters).
|
|
21
31
|
*/
|
|
22
32
|
export declare function extractLang(req: Record<string, unknown>): string | undefined;
|
|
23
33
|
//# sourceMappingURL=lang.decorator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lang.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/lang.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAK1B
|
|
1
|
+
{"version":3,"file":"lang.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/lang.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAK1B;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,0BAA0B,CAAC;AAEtD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,IAAI,IAAI,kBAAkB,CAiBzC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAE5E"}
|
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LANG_QUERY_KEY = void 0;
|
|
3
4
|
exports.Lang = Lang;
|
|
4
5
|
exports.extractLang = extractLang;
|
|
5
6
|
require("reflect-metadata");
|
|
6
7
|
const i18n_middleware_1 = require("../i18n.middleware");
|
|
7
8
|
const INJECT_METADATA_KEY = 'hazel:inject';
|
|
9
|
+
/**
|
|
10
|
+
* Internal query key used to pass the resolved locale through the HazelJS
|
|
11
|
+
* request context so the router can inject it via @Lang().
|
|
12
|
+
*
|
|
13
|
+
* The proxy handler in main.ts writes this key into context.query before
|
|
14
|
+
* route matching; @Lang() reads it back using the standard 'query' injection
|
|
15
|
+
* type that the router already handles.
|
|
16
|
+
*/
|
|
17
|
+
exports.LANG_QUERY_KEY = '__hazel_i18n_locale__';
|
|
8
18
|
/**
|
|
9
19
|
* Parameter decorator that injects the detected locale string into a
|
|
10
20
|
* controller method parameter.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
22
|
+
* Requires LocaleMiddleware (via addProxyHandler) to run before routing so
|
|
23
|
+
* the locale is resolved and stored in context.query[LANG_QUERY_KEY].
|
|
24
|
+
* When the middleware is not applied, the parameter will be undefined.
|
|
14
25
|
*
|
|
15
26
|
* @example
|
|
16
27
|
* ```ts
|
|
17
28
|
* @Get('/hello')
|
|
18
29
|
* greet(@Lang() locale: string) {
|
|
19
|
-
* return this.i18n.t('welcome', { locale });
|
|
30
|
+
* return this.i18n.t('welcome', { locale, vars: { name: 'World' } });
|
|
20
31
|
* }
|
|
21
32
|
* ```
|
|
22
33
|
*/
|
|
@@ -27,13 +38,15 @@ function Lang() {
|
|
|
27
38
|
}
|
|
28
39
|
const constructor = target.constructor;
|
|
29
40
|
const injections = Reflect.getMetadata(INJECT_METADATA_KEY, constructor, propertyKey) ?? [];
|
|
30
|
-
|
|
41
|
+
// Use the router's built-in 'query' injection — the proxy handler seeds
|
|
42
|
+
// context.query[LANG_QUERY_KEY] with the detected locale before routing.
|
|
43
|
+
injections[parameterIndex] = { type: 'query', name: exports.LANG_QUERY_KEY };
|
|
31
44
|
Reflect.defineMetadata(INJECT_METADATA_KEY, injections, constructor, propertyKey);
|
|
32
45
|
};
|
|
33
46
|
}
|
|
34
47
|
/**
|
|
35
|
-
* Reads the locale injected by
|
|
36
|
-
*
|
|
48
|
+
* Reads the locale injected by LocaleMiddleware from the raw request object.
|
|
49
|
+
* Useful outside of controller parameters (e.g. in exception filters).
|
|
37
50
|
*/
|
|
38
51
|
function extractLang(req) {
|
|
39
52
|
return req[i18n_middleware_1.LOCALE_KEY];
|
|
@@ -19,7 +19,7 @@ describe('Lang() decorator', () => {
|
|
|
19
19
|
decorator(target, 'greet', 0);
|
|
20
20
|
const metadata = Reflect.getMetadata(INJECT_METADATA_KEY, TestController, 'greet');
|
|
21
21
|
expect(metadata).toBeDefined();
|
|
22
|
-
expect(metadata[0]).toEqual({ type: '
|
|
22
|
+
expect(metadata[0]).toEqual({ type: 'query', name: lang_decorator_1.LANG_QUERY_KEY });
|
|
23
23
|
});
|
|
24
24
|
it('stores metadata at the correct parameter index', () => {
|
|
25
25
|
class TwoParamController {
|
|
@@ -28,7 +28,7 @@ describe('Lang() decorator', () => {
|
|
|
28
28
|
const decorator = (0, lang_decorator_1.Lang)();
|
|
29
29
|
decorator(TwoParamController.prototype, 'doSomething', 1);
|
|
30
30
|
const metadata = Reflect.getMetadata(INJECT_METADATA_KEY, TwoParamController, 'doSomething');
|
|
31
|
-
expect(metadata[1]).toEqual({ type: '
|
|
31
|
+
expect(metadata[1]).toEqual({ type: 'query', name: lang_decorator_1.LANG_QUERY_KEY });
|
|
32
32
|
expect(metadata[0]).toBeUndefined();
|
|
33
33
|
});
|
|
34
34
|
it('merges with existing metadata at other indices', () => {
|
|
@@ -41,7 +41,7 @@ describe('Lang() decorator', () => {
|
|
|
41
41
|
decorator(MultiController.prototype, 'action', 2);
|
|
42
42
|
const metadata = Reflect.getMetadata(INJECT_METADATA_KEY, MultiController, 'action');
|
|
43
43
|
expect(metadata[0]).toEqual({ type: 'body' });
|
|
44
|
-
expect(metadata[2]).toEqual({ type: '
|
|
44
|
+
expect(metadata[2]).toEqual({ type: 'query', name: lang_decorator_1.LANG_QUERY_KEY });
|
|
45
45
|
});
|
|
46
46
|
it('throws when propertyKey is undefined (constructor usage)', () => {
|
|
47
47
|
const decorator = (0, lang_decorator_1.Lang)();
|
|
@@ -24,7 +24,11 @@ export declare class LocaleMiddleware {
|
|
|
24
24
|
/**
|
|
25
25
|
* Run through each configured strategy in priority order and return the
|
|
26
26
|
* first valid locale found, or the default locale as a fallback.
|
|
27
|
+
*
|
|
28
|
+
* Exposed publicly so proxy handlers (e.g. addProxyHandler in main.ts) can
|
|
29
|
+
* call locale detection without going through the full middleware pipeline.
|
|
27
30
|
*/
|
|
31
|
+
resolveLocale(req: Request): string;
|
|
28
32
|
private detect;
|
|
29
33
|
/**
|
|
30
34
|
* Parse the Cookie header and extract the value for the given name.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.middleware.d.ts","sourceRoot":"","sources":["../src/i18n.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,mBAAmB;IAEzD,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI;IAQ3D
|
|
1
|
+
{"version":3,"file":"i18n.middleware.d.ts","sourceRoot":"","sources":["../src/i18n.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,UAAU,qBAAqB,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,mBAAmB;IAEzD,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI;IAQ3D;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM;IAInC,OAAO,CAAC,MAAM;IAwBd;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;;OAGG;IACH,OAAO,CAAC,OAAO;IAIf;;OAEG;IACH,MAAM,CAAC,MAAM,CACX,OAAO,EAAE,mBAAmB,GAC3B,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI;CAI3D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAErE"}
|
package/dist/i18n.middleware.js
CHANGED
|
@@ -33,7 +33,13 @@ class LocaleMiddleware {
|
|
|
33
33
|
/**
|
|
34
34
|
* Run through each configured strategy in priority order and return the
|
|
35
35
|
* first valid locale found, or the default locale as a fallback.
|
|
36
|
+
*
|
|
37
|
+
* Exposed publicly so proxy handlers (e.g. addProxyHandler in main.ts) can
|
|
38
|
+
* call locale detection without going through the full middleware pipeline.
|
|
36
39
|
*/
|
|
40
|
+
resolveLocale(req) {
|
|
41
|
+
return this.detect(req);
|
|
42
|
+
}
|
|
37
43
|
detect(req) {
|
|
38
44
|
for (const strategy of this.options.detection) {
|
|
39
45
|
switch (strategy) {
|
package/dist/i18n.module.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export declare class I18nModule {
|
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
59
|
static forRootAsync(options: {
|
|
60
|
-
useFactory: (...args:
|
|
60
|
+
useFactory: (...args: any[]) => I18nOptions;
|
|
61
61
|
inject?: unknown[];
|
|
62
62
|
}): {
|
|
63
63
|
module: typeof I18nModule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.module.d.ts","sourceRoot":"","sources":["../src/i18n.module.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAuB,MAAM,SAAS,CAAC;AAuB3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAIa,UAAU;IACrB;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG;QACzC,MAAM,EAAE,OAAO,UAAU,CAAC;QAC1B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,MAAM,GAAG,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC;YAC/D,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;YAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,CAAC,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC,CAAC;QAC7D,MAAM,EAAE,OAAO,CAAC;KACjB;IAmCD;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;
|
|
1
|
+
{"version":3,"file":"i18n.module.d.ts","sourceRoot":"","sources":["../src/i18n.module.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAuB,MAAM,SAAS,CAAC;AAuB3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAIa,UAAU;IACrB;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,WAAgB,GAAG;QACzC,MAAM,EAAE,OAAO,UAAU,CAAC;QAC1B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,MAAM,GAAG,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC;YAC/D,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;YAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,CAAC,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC,CAAC;QAC7D,MAAM,EAAE,OAAO,CAAC;KACjB;IAmCD;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAE3B,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW,CAAC;QAC5C,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;KACpB,GAAG;QACF,MAAM,EAAE,OAAO,UAAU,CAAC;QAC1B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,MAAM,GAAG,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC;YAC/D,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;YAC5C,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;SACpB,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,CAAC,OAAO,WAAW,GAAG,OAAO,gBAAgB,CAAC,CAAC;QAC7D,MAAM,EAAE,OAAO,CAAC;KACjB;CAoCF"}
|
package/dist/i18n.module.js
CHANGED
|
@@ -71,9 +71,9 @@ let I18nModule = I18nModule_1 = class I18nModule {
|
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
provide: i18n_service_1.I18nService,
|
|
74
|
-
useFactory:
|
|
74
|
+
useFactory: (...args) => {
|
|
75
75
|
const opts = args[0];
|
|
76
|
-
const store =
|
|
76
|
+
const store = translation_loader_1.TranslationLoader.load(opts.translationsPath);
|
|
77
77
|
const service = new i18n_service_1.I18nService();
|
|
78
78
|
service.initialize(store, opts);
|
|
79
79
|
return service;
|
|
@@ -114,17 +114,17 @@ let I18nModule = I18nModule_1 = class I18nModule {
|
|
|
114
114
|
providers: [
|
|
115
115
|
{
|
|
116
116
|
provide: I18N_OPTIONS_TOKEN,
|
|
117
|
-
useFactory:
|
|
118
|
-
const userOptions =
|
|
117
|
+
useFactory: (...args) => {
|
|
118
|
+
const userOptions = options.useFactory(...args);
|
|
119
119
|
return resolveOptions(userOptions);
|
|
120
120
|
},
|
|
121
121
|
inject: options.inject ?? [],
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
provide: i18n_service_1.I18nService,
|
|
125
|
-
useFactory:
|
|
125
|
+
useFactory: (...args) => {
|
|
126
126
|
const opts = args[0];
|
|
127
|
-
const store =
|
|
127
|
+
const store = translation_loader_1.TranslationLoader.load(opts.translationsPath);
|
|
128
128
|
const service = new i18n_service_1.I18nService();
|
|
129
129
|
service.initialize(store, opts);
|
|
130
130
|
return service;
|
package/dist/i18n.module.test.js
CHANGED
|
@@ -7,7 +7,7 @@ jest.mock('@hazeljs/core', () => ({
|
|
|
7
7
|
}));
|
|
8
8
|
jest.mock('./translation.loader', () => ({
|
|
9
9
|
TranslationLoader: {
|
|
10
|
-
load: jest.fn().
|
|
10
|
+
load: jest.fn().mockReturnValue(new Map()),
|
|
11
11
|
},
|
|
12
12
|
}));
|
|
13
13
|
const i18n_module_1 = require("./i18n.module");
|
|
@@ -18,7 +18,7 @@ const mockLoad = translation_loader_1.TranslationLoader.load;
|
|
|
18
18
|
describe('I18nModule', () => {
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
jest.clearAllMocks();
|
|
21
|
-
mockLoad.
|
|
21
|
+
mockLoad.mockReturnValue(new Map());
|
|
22
22
|
});
|
|
23
23
|
describe('forRoot()', () => {
|
|
24
24
|
it('returns module reference', () => {
|
|
@@ -57,14 +57,14 @@ describe('I18nModule', () => {
|
|
|
57
57
|
const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
|
|
58
58
|
expect(optionsProvider?.useValue?.fallbackLocale).toBe('en');
|
|
59
59
|
});
|
|
60
|
-
it('I18nService factory creates and initializes service',
|
|
60
|
+
it('I18nService factory creates and initializes service', () => {
|
|
61
61
|
const store = new Map([['en', { hello: 'Hello' }]]);
|
|
62
|
-
mockLoad.
|
|
62
|
+
mockLoad.mockReturnValue(store);
|
|
63
63
|
const result = i18n_module_1.I18nModule.forRoot({ translationsPath: './trans' });
|
|
64
64
|
const serviceProvider = result.providers.find((p) => p.provide === i18n_service_1.I18nService);
|
|
65
65
|
const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
|
|
66
66
|
const opts = optionsProvider?.useValue;
|
|
67
|
-
const service =
|
|
67
|
+
const service = serviceProvider?.useFactory?.(opts);
|
|
68
68
|
expect(service).toBeInstanceOf(i18n_service_1.I18nService);
|
|
69
69
|
expect(mockLoad).toHaveBeenCalled();
|
|
70
70
|
});
|
|
@@ -92,39 +92,39 @@ describe('I18nModule', () => {
|
|
|
92
92
|
});
|
|
93
93
|
describe('forRootAsync()', () => {
|
|
94
94
|
it('returns module reference', () => {
|
|
95
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
95
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
96
96
|
expect(result.module).toBe(i18n_module_1.I18nModule);
|
|
97
97
|
});
|
|
98
98
|
it('exports I18nService and LocaleMiddleware', () => {
|
|
99
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
99
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
100
100
|
expect(result.exports).toContain(i18n_service_1.I18nService);
|
|
101
101
|
expect(result.exports).toContain(i18n_middleware_1.LocaleMiddleware);
|
|
102
102
|
});
|
|
103
103
|
it('sets global to true', () => {
|
|
104
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
104
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
105
105
|
expect(result.global).toBe(true);
|
|
106
106
|
});
|
|
107
107
|
it('provides 3 providers', () => {
|
|
108
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
108
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
109
109
|
expect(result.providers).toHaveLength(3);
|
|
110
110
|
});
|
|
111
|
-
it('OPTIONS factory resolves and applies defaults',
|
|
111
|
+
it('OPTIONS factory resolves and applies defaults', () => {
|
|
112
112
|
const result = i18n_module_1.I18nModule.forRootAsync({
|
|
113
|
-
useFactory:
|
|
113
|
+
useFactory: () => ({ defaultLocale: 'ja' }),
|
|
114
114
|
});
|
|
115
115
|
const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
|
|
116
|
-
const resolved =
|
|
116
|
+
const resolved = optionsProvider?.useFactory?.();
|
|
117
117
|
expect(resolved.defaultLocale).toBe('ja');
|
|
118
118
|
});
|
|
119
|
-
it('passes inject args through to useFactory',
|
|
120
|
-
const factory = jest.fn().
|
|
119
|
+
it('passes inject args through to useFactory', () => {
|
|
120
|
+
const factory = jest.fn().mockReturnValue({ defaultLocale: 'ko' });
|
|
121
121
|
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: factory, inject: ['CONFIG'] });
|
|
122
122
|
const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
|
|
123
|
-
|
|
123
|
+
optionsProvider?.useFactory?.('some-config-value');
|
|
124
124
|
expect(factory).toHaveBeenCalledWith('some-config-value');
|
|
125
125
|
});
|
|
126
|
-
it('I18nService factory creates and initializes service',
|
|
127
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
126
|
+
it('I18nService factory creates and initializes service', () => {
|
|
127
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
128
128
|
const serviceProvider = result.providers.find((p) => p.provide === i18n_service_1.I18nService);
|
|
129
129
|
const resolvedOpts = {
|
|
130
130
|
defaultLocale: 'en',
|
|
@@ -135,11 +135,11 @@ describe('I18nModule', () => {
|
|
|
135
135
|
cookieName: 'locale',
|
|
136
136
|
isGlobal: true,
|
|
137
137
|
};
|
|
138
|
-
const service =
|
|
138
|
+
const service = serviceProvider?.useFactory?.(resolvedOpts);
|
|
139
139
|
expect(service).toBeInstanceOf(i18n_service_1.I18nService);
|
|
140
140
|
});
|
|
141
|
-
it('LocaleMiddleware factory creates middleware',
|
|
142
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
141
|
+
it('LocaleMiddleware factory creates middleware', () => {
|
|
142
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
143
143
|
const mwProvider = result.providers.find((p) => p.provide === i18n_middleware_1.LocaleMiddleware);
|
|
144
144
|
const resolvedOpts = {
|
|
145
145
|
defaultLocale: 'en',
|
|
@@ -150,11 +150,11 @@ describe('I18nModule', () => {
|
|
|
150
150
|
cookieName: 'locale',
|
|
151
151
|
isGlobal: true,
|
|
152
152
|
};
|
|
153
|
-
const mw =
|
|
153
|
+
const mw = mwProvider?.useFactory?.(resolvedOpts);
|
|
154
154
|
expect(mw).toBeInstanceOf(i18n_middleware_1.LocaleMiddleware);
|
|
155
155
|
});
|
|
156
156
|
it('uses empty array for inject when not specified', () => {
|
|
157
|
-
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory:
|
|
157
|
+
const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: () => ({}) });
|
|
158
158
|
const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
|
|
159
159
|
expect(optionsProvider?.inject).toEqual([]);
|
|
160
160
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export { I18nService, I18nFormatter } from './i18n.service';
|
|
|
22
22
|
export { LocaleMiddleware, getLocaleFromRequest, LOCALE_KEY } from './i18n.middleware';
|
|
23
23
|
export { I18nInterceptor } from './i18n.interceptor';
|
|
24
24
|
export { TranslationLoader } from './translation.loader';
|
|
25
|
-
export { Lang, extractLang } from './decorators/lang.decorator';
|
|
25
|
+
export { Lang, extractLang, LANG_QUERY_KEY } from './decorators/lang.decorator';
|
|
26
26
|
export type { I18nOptions, ResolvedI18nOptions, TranslateOptions, TranslationMap, TranslationValue, LocaleStore, LocaleDetectionStrategy, } from './types';
|
|
27
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAChF,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,uBAAuB,GACxB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.extractLang = exports.Lang = exports.TranslationLoader = exports.I18nInterceptor = exports.LOCALE_KEY = exports.getLocaleFromRequest = exports.LocaleMiddleware = exports.I18nFormatter = exports.I18nService = exports.I18nModule = void 0;
|
|
22
|
+
exports.LANG_QUERY_KEY = exports.extractLang = exports.Lang = exports.TranslationLoader = exports.I18nInterceptor = exports.LOCALE_KEY = exports.getLocaleFromRequest = exports.LocaleMiddleware = exports.I18nFormatter = exports.I18nService = exports.I18nModule = void 0;
|
|
23
23
|
var i18n_module_1 = require("./i18n.module");
|
|
24
24
|
Object.defineProperty(exports, "I18nModule", { enumerable: true, get: function () { return i18n_module_1.I18nModule; } });
|
|
25
25
|
var i18n_service_1 = require("./i18n.service");
|
|
@@ -36,3 +36,4 @@ Object.defineProperty(exports, "TranslationLoader", { enumerable: true, get: fun
|
|
|
36
36
|
var lang_decorator_1 = require("./decorators/lang.decorator");
|
|
37
37
|
Object.defineProperty(exports, "Lang", { enumerable: true, get: function () { return lang_decorator_1.Lang; } });
|
|
38
38
|
Object.defineProperty(exports, "extractLang", { enumerable: true, get: function () { return lang_decorator_1.extractLang; } });
|
|
39
|
+
Object.defineProperty(exports, "LANG_QUERY_KEY", { enumerable: true, get: function () { return lang_decorator_1.LANG_QUERY_KEY; } });
|
|
@@ -5,12 +5,16 @@ import { LocaleStore } from './types';
|
|
|
5
5
|
* Each file must be named <locale>.json (e.g. en.json, fr.json, zh-TW.json).
|
|
6
6
|
* Nested objects in the JSON are kept as-is; key lookup via dot-notation is
|
|
7
7
|
* resolved at translation time inside I18nService.
|
|
8
|
+
*
|
|
9
|
+
* Synchronous I/O is intentional: HazelJS's DI container resolves providers
|
|
10
|
+
* synchronously, so async factories would result in a Promise being stored
|
|
11
|
+
* as the service instance rather than the resolved value.
|
|
8
12
|
*/
|
|
9
13
|
export declare class TranslationLoader {
|
|
10
14
|
/**
|
|
11
|
-
* Read all *.json files from the given directory.
|
|
15
|
+
* Read all *.json files from the given directory synchronously.
|
|
12
16
|
* Returns a Map keyed by locale code (the filename without extension).
|
|
13
17
|
*/
|
|
14
|
-
static load(translationsPath: string):
|
|
18
|
+
static load(translationsPath: string): LocaleStore;
|
|
15
19
|
}
|
|
16
20
|
//# sourceMappingURL=translation.loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation.loader.d.ts","sourceRoot":"","sources":["../src/translation.loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtD
|
|
1
|
+
{"version":3,"file":"translation.loader.d.ts","sourceRoot":"","sources":["../src/translation.loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,qBAAa,iBAAiB;IAC5B;;;OAGG;IACH,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,WAAW;CAiCnD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TranslationLoader = void 0;
|
|
4
|
-
const
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
/**
|
|
7
7
|
* Loads JSON translation files from a directory and returns a locale store.
|
|
@@ -9,17 +9,21 @@ const path_1 = require("path");
|
|
|
9
9
|
* Each file must be named <locale>.json (e.g. en.json, fr.json, zh-TW.json).
|
|
10
10
|
* Nested objects in the JSON are kept as-is; key lookup via dot-notation is
|
|
11
11
|
* resolved at translation time inside I18nService.
|
|
12
|
+
*
|
|
13
|
+
* Synchronous I/O is intentional: HazelJS's DI container resolves providers
|
|
14
|
+
* synchronously, so async factories would result in a Promise being stored
|
|
15
|
+
* as the service instance rather than the resolved value.
|
|
12
16
|
*/
|
|
13
17
|
class TranslationLoader {
|
|
14
18
|
/**
|
|
15
|
-
* Read all *.json files from the given directory.
|
|
19
|
+
* Read all *.json files from the given directory synchronously.
|
|
16
20
|
* Returns a Map keyed by locale code (the filename without extension).
|
|
17
21
|
*/
|
|
18
|
-
static
|
|
22
|
+
static load(translationsPath) {
|
|
19
23
|
const store = new Map();
|
|
20
24
|
let entries;
|
|
21
25
|
try {
|
|
22
|
-
entries =
|
|
26
|
+
entries = (0, fs_1.readdirSync)(translationsPath);
|
|
23
27
|
}
|
|
24
28
|
catch {
|
|
25
29
|
// Directory does not exist — return an empty store so the service starts
|
|
@@ -27,11 +31,11 @@ class TranslationLoader {
|
|
|
27
31
|
return store;
|
|
28
32
|
}
|
|
29
33
|
const jsonFiles = entries.filter((file) => (0, path_1.extname)(file) === '.json');
|
|
30
|
-
|
|
34
|
+
for (const file of jsonFiles) {
|
|
31
35
|
const locale = (0, path_1.basename)(file, '.json');
|
|
32
36
|
const filePath = (0, path_1.join)(translationsPath, file);
|
|
33
37
|
try {
|
|
34
|
-
const raw =
|
|
38
|
+
const raw = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
35
39
|
const translations = JSON.parse(raw);
|
|
36
40
|
store.set(locale, translations);
|
|
37
41
|
}
|
|
@@ -40,7 +44,7 @@ class TranslationLoader {
|
|
|
40
44
|
const message = err instanceof Error ? err.message : String(err);
|
|
41
45
|
process.stderr.write(`[@hazeljs/i18n] Failed to load translation file "${filePath}": ${message}\n`);
|
|
42
46
|
}
|
|
43
|
-
}
|
|
47
|
+
}
|
|
44
48
|
return store;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -1,82 +1,86 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const translation_loader_1 = require("./translation.loader");
|
|
4
|
-
jest.mock('fs
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
jest.mock('fs', () => ({
|
|
5
|
+
readdirSync: jest.fn(),
|
|
6
|
+
readFileSync: jest.fn(),
|
|
7
7
|
}));
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const mockReaddirSync = fs_1.readdirSync;
|
|
10
|
+
const mockReadFileSync = fs_1.readFileSync;
|
|
11
11
|
describe('TranslationLoader', () => {
|
|
12
12
|
beforeEach(() => {
|
|
13
13
|
jest.clearAllMocks();
|
|
14
14
|
});
|
|
15
15
|
describe('load()', () => {
|
|
16
|
-
it('returns an empty store when directory does not exist',
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
it('returns an empty store when directory does not exist', () => {
|
|
17
|
+
mockReaddirSync.mockImplementation(() => {
|
|
18
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
19
|
+
});
|
|
20
|
+
const store = translation_loader_1.TranslationLoader.load('/nonexistent/path');
|
|
19
21
|
expect(store.size).toBe(0);
|
|
20
22
|
});
|
|
21
|
-
it('returns an empty store when directory is empty',
|
|
22
|
-
|
|
23
|
-
const store =
|
|
23
|
+
it('returns an empty store when directory is empty', () => {
|
|
24
|
+
mockReaddirSync.mockReturnValue([]);
|
|
25
|
+
const store = translation_loader_1.TranslationLoader.load('/empty/dir');
|
|
24
26
|
expect(store.size).toBe(0);
|
|
25
27
|
});
|
|
26
|
-
it('ignores non-JSON files',
|
|
27
|
-
|
|
28
|
-
const store =
|
|
28
|
+
it('ignores non-JSON files', () => {
|
|
29
|
+
mockReaddirSync.mockReturnValue(['README.md', 'notes.txt']);
|
|
30
|
+
const store = translation_loader_1.TranslationLoader.load('/some/dir');
|
|
29
31
|
expect(store.size).toBe(0);
|
|
30
|
-
expect(
|
|
32
|
+
expect(mockReadFileSync).not.toHaveBeenCalled();
|
|
31
33
|
});
|
|
32
|
-
it('loads a single JSON file and stores translations by locale',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const store =
|
|
34
|
+
it('loads a single JSON file and stores translations by locale', () => {
|
|
35
|
+
mockReaddirSync.mockReturnValue(['en.json']);
|
|
36
|
+
mockReadFileSync.mockReturnValue('{"hello":"Hello","bye":"Goodbye"}');
|
|
37
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
36
38
|
expect(store.has('en')).toBe(true);
|
|
37
39
|
expect(store.get('en')).toEqual({ hello: 'Hello', bye: 'Goodbye' });
|
|
38
40
|
});
|
|
39
|
-
it('loads multiple JSON files into separate locale entries',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
const store =
|
|
41
|
+
it('loads multiple JSON files into separate locale entries', () => {
|
|
42
|
+
mockReaddirSync.mockReturnValue(['en.json', 'fr.json']);
|
|
43
|
+
mockReadFileSync
|
|
44
|
+
.mockReturnValueOnce('{"hello":"Hello"}')
|
|
45
|
+
.mockReturnValueOnce('{"hello":"Bonjour"}');
|
|
46
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
45
47
|
expect(store.size).toBe(2);
|
|
46
48
|
expect(store.get('en')).toEqual({ hello: 'Hello' });
|
|
47
49
|
expect(store.get('fr')).toEqual({ hello: 'Bonjour' });
|
|
48
50
|
});
|
|
49
|
-
it('handles locale codes with hyphens (zh-TW.json)',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const store =
|
|
51
|
+
it('handles locale codes with hyphens (zh-TW.json)', () => {
|
|
52
|
+
mockReaddirSync.mockReturnValue(['zh-TW.json']);
|
|
53
|
+
mockReadFileSync.mockReturnValue('{"greeting":"你好"}');
|
|
54
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
53
55
|
expect(store.has('zh-TW')).toBe(true);
|
|
54
56
|
});
|
|
55
|
-
it('skips malformed JSON files and writes to stderr',
|
|
57
|
+
it('skips malformed JSON files and writes to stderr', () => {
|
|
56
58
|
const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
const store =
|
|
59
|
+
mockReaddirSync.mockReturnValue(['en.json', 'bad.json']);
|
|
60
|
+
mockReadFileSync
|
|
61
|
+
.mockReturnValueOnce('{"ok":true}')
|
|
62
|
+
.mockReturnValueOnce('{ invalid json');
|
|
63
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
62
64
|
expect(store.has('en')).toBe(true);
|
|
63
65
|
expect(store.has('bad')).toBe(false);
|
|
64
66
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('[@hazeljs/i18n]'));
|
|
65
67
|
stderrSpy.mockRestore();
|
|
66
68
|
});
|
|
67
|
-
it('writes non-Error exception to stderr as string',
|
|
69
|
+
it('writes non-Error exception to stderr as string', () => {
|
|
68
70
|
const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
mockReaddirSync.mockReturnValue(['broken.json']);
|
|
72
|
+
mockReadFileSync.mockImplementation(() => {
|
|
73
|
+
throw 'string error';
|
|
74
|
+
});
|
|
75
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
72
76
|
expect(store.has('broken')).toBe(false);
|
|
73
77
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('string error'));
|
|
74
78
|
stderrSpy.mockRestore();
|
|
75
79
|
});
|
|
76
|
-
it('loads nested translation objects',
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const store =
|
|
80
|
+
it('loads nested translation objects', () => {
|
|
81
|
+
mockReaddirSync.mockReturnValue(['en.json']);
|
|
82
|
+
mockReadFileSync.mockReturnValue(JSON.stringify({ errors: { notFound: 'Not found', invalid: 'Invalid' } }));
|
|
83
|
+
const store = translation_loader_1.TranslationLoader.load('/translations');
|
|
80
84
|
expect(store.get('en')).toEqual({
|
|
81
85
|
errors: { notFound: 'Not found', invalid: 'Invalid' },
|
|
82
86
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/i18n",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.58",
|
|
4
4
|
"description": "Internationalization (i18n) module for HazelJS framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "ce3821b373043284ac4847315371efb137dc860d"
|
|
51
51
|
}
|