@hazeljs/i18n 0.2.0-beta.57 → 0.2.0-beta.59

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.
@@ -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
- * The locale is set by LocaleMiddleware earlier in the request lifecycle.
7
- * When the middleware is not applied, the parameter will be `undefined`.
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 @Lang() from the raw request object.
20
- * Exposed for use in custom parameter resolvers.
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;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,IAAI,IAAI,kBAAkB,CAezC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAE5E"}
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
- * The locale is set by LocaleMiddleware earlier in the request lifecycle.
13
- * When the middleware is not applied, the parameter will be `undefined`.
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
- injections[parameterIndex] = { type: 'custom', key: i18n_middleware_1.LOCALE_KEY, source: 'request' };
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 @Lang() from the raw request object.
36
- * Exposed for use in custom parameter resolvers.
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: 'custom', key: i18n_middleware_1.LOCALE_KEY, source: 'request' });
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: 'custom', key: i18n_middleware_1.LOCALE_KEY, source: 'request' });
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: 'custom', key: i18n_middleware_1.LOCALE_KEY, source: 'request' });
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;;;OAGG;IACH,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"}
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"}
@@ -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) {
@@ -57,7 +57,7 @@ export declare class I18nModule {
57
57
  * ```
58
58
  */
59
59
  static forRootAsync(options: {
60
- useFactory: (...args: unknown[]) => Promise<I18nOptions> | I18nOptions;
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;QAC3B,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC;QACvE,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"}
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"}
@@ -71,9 +71,9 @@ let I18nModule = I18nModule_1 = class I18nModule {
71
71
  },
72
72
  {
73
73
  provide: i18n_service_1.I18nService,
74
- useFactory: async (...args) => {
74
+ useFactory: (...args) => {
75
75
  const opts = args[0];
76
- const store = await translation_loader_1.TranslationLoader.load(opts.translationsPath);
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: async (...args) => {
118
- const userOptions = await options.useFactory(...args);
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: async (...args) => {
125
+ useFactory: (...args) => {
126
126
  const opts = args[0];
127
- const store = await translation_loader_1.TranslationLoader.load(opts.translationsPath);
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;
@@ -7,7 +7,7 @@ jest.mock('@hazeljs/core', () => ({
7
7
  }));
8
8
  jest.mock('./translation.loader', () => ({
9
9
  TranslationLoader: {
10
- load: jest.fn().mockResolvedValue(new Map()),
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.mockResolvedValue(new Map());
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', async () => {
60
+ it('I18nService factory creates and initializes service', () => {
61
61
  const store = new Map([['en', { hello: 'Hello' }]]);
62
- mockLoad.mockResolvedValue(store);
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 = await serviceProvider?.useFactory?.(opts);
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: async () => ({}) });
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: async () => ({}) });
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: async () => ({}) });
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: async () => ({}) });
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', async () => {
111
+ it('OPTIONS factory resolves and applies defaults', () => {
112
112
  const result = i18n_module_1.I18nModule.forRootAsync({
113
- useFactory: async () => ({ defaultLocale: 'ja' }),
113
+ useFactory: () => ({ defaultLocale: 'ja' }),
114
114
  });
115
115
  const optionsProvider = result.providers.find((p) => p.provide === 'I18N_OPTIONS');
116
- const resolved = await optionsProvider?.useFactory?.();
116
+ const resolved = optionsProvider?.useFactory?.();
117
117
  expect(resolved.defaultLocale).toBe('ja');
118
118
  });
119
- it('passes inject args through to useFactory', async () => {
120
- const factory = jest.fn().mockResolvedValue({ defaultLocale: 'ko' });
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
- await optionsProvider?.useFactory?.('some-config-value');
123
+ optionsProvider?.useFactory?.('some-config-value');
124
124
  expect(factory).toHaveBeenCalledWith('some-config-value');
125
125
  });
126
- it('I18nService factory creates and initializes service', async () => {
127
- const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: async () => ({}) });
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 = await serviceProvider?.useFactory?.(resolvedOpts);
138
+ const service = serviceProvider?.useFactory?.(resolvedOpts);
139
139
  expect(service).toBeInstanceOf(i18n_service_1.I18nService);
140
140
  });
141
- it('LocaleMiddleware factory creates middleware', async () => {
142
- const result = i18n_module_1.I18nModule.forRootAsync({ useFactory: async () => ({}) });
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 = await mwProvider?.useFactory?.(resolvedOpts);
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: async () => ({}) });
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
@@ -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;AAChE,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,uBAAuB,GACxB,MAAM,SAAS,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): Promise<LocaleStore>;
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;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B;;;OAGG;WACU,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAmClE"}
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 promises_1 = require("fs/promises");
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 async load(translationsPath) {
22
+ static load(translationsPath) {
19
23
  const store = new Map();
20
24
  let entries;
21
25
  try {
22
- entries = await (0, promises_1.readdir)(translationsPath);
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
- await Promise.all(jsonFiles.map(async (file) => {
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 = await (0, promises_1.readFile)(filePath, 'utf-8');
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/promises', () => ({
5
- readdir: jest.fn(),
6
- readFile: jest.fn(),
4
+ jest.mock('fs', () => ({
5
+ readdirSync: jest.fn(),
6
+ readFileSync: jest.fn(),
7
7
  }));
8
- const promises_1 = require("fs/promises");
9
- const mockReaddir = promises_1.readdir;
10
- const mockReadFile = promises_1.readFile;
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', async () => {
17
- mockReaddir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
18
- const store = await translation_loader_1.TranslationLoader.load('/nonexistent/path');
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', async () => {
22
- mockReaddir.mockResolvedValue([]);
23
- const store = await translation_loader_1.TranslationLoader.load('/empty/dir');
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', async () => {
27
- mockReaddir.mockResolvedValue(['README.md', 'notes.txt']);
28
- const store = await translation_loader_1.TranslationLoader.load('/some/dir');
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(mockReadFile).not.toHaveBeenCalled();
32
+ expect(mockReadFileSync).not.toHaveBeenCalled();
31
33
  });
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');
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', 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');
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)', async () => {
50
- mockReaddir.mockResolvedValue(['zh-TW.json']);
51
- mockReadFile.mockResolvedValue('{"greeting":"你好"}');
52
- const store = await translation_loader_1.TranslationLoader.load('/translations');
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', async () => {
57
+ it('skips malformed JSON files and writes to stderr', () => {
56
58
  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');
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', async () => {
69
+ it('writes non-Error exception to stderr as string', () => {
68
70
  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');
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', 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
+ 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.57",
3
+ "version": "0.2.0-beta.59",
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": "98c84683e8f487c660e344c2beda3a8dcaaff07f"
50
+ "gitHead": "f6d8ee8162a40e2298ccce46d843269838bbe6ff"
51
51
  }