@fluojs/i18n 2.0.0 → 3.0.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/README.ko.md CHANGED
@@ -9,6 +9,7 @@ fluo 애플리케이션을 위한 프레임워크 비종속 국제화 코어 표
9
9
  - [설치](#설치)
10
10
  - [사용 시점](#사용-시점)
11
11
  - [빠른 시작](#빠른-시작)
12
+ - [Catalog Aggregation 및 Fallback Migration](#catalog-aggregation-및-fallback-migration)
12
13
  - [코어 번역](#코어-번역)
13
14
  - [포맷팅](#포맷팅)
14
15
  - [ICU MessageFormat](#icu-messageformat)
@@ -70,6 +71,71 @@ class AppModule {}
70
71
 
71
72
  `I18nModule.forRoot(...)`는 기본적으로 `I18nService`를 global provider로 export하므로 root package를 한 번 import한 뒤 sibling module에서도 shared service를 inject할 수 있습니다. Service를 i18n module을 import한 module 안에만 보이게 하려면 `global: false`를 전달하세요.
72
73
 
74
+ `I18nModule.forRoot(...)`는 module graph가 정의될 때 최종 option을 capture하는 동기 registration입니다. 비동기 catalog 또는 configuration loading은 `I18nModule.forRoot(...)` 전에 application-owned bootstrap boundary에서 완료하고, resolve된 catalog와 option을 해당 registration call에 전달하세요. 이 방식은 framework-agnostic root contract를 유지하며 NestJS dynamic-module runtime bridge나 `forRootAsync(...)` compatibility surface를 제공하지 않습니다.
75
+
76
+ ## Catalog Aggregation 및 Fallback Migration
77
+
78
+ NestJS i18n에서 migration할 때는 동기 `I18nModule.forRoot(...)` 호출 전에 필요한 모든 locale과 namespace를 load하세요. NestJS loader configuration을 module registration으로 가져오지 않습니다.
79
+
80
+ ```ts
81
+ import { Module } from '@fluojs/core';
82
+ import { I18nModule } from '@fluojs/i18n';
83
+ import { createFileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';
84
+
85
+ const locales = ['en', 'ko'] as const;
86
+ const namespaces = ['common', 'validation'] as const;
87
+ const catalogLoader = createFileSystemI18nLoader({
88
+ rootDir: new URL('./locales', import.meta.url).pathname,
89
+ });
90
+
91
+ const catalogEntries = await Promise.all(
92
+ locales.map(async (locale) => {
93
+ const namespaceEntries = await Promise.all(
94
+ namespaces.map(async (namespace) => [
95
+ namespace,
96
+ await catalogLoader.load(locale, namespace),
97
+ ] as const),
98
+ );
99
+
100
+ return [locale, Object.fromEntries(namespaceEntries)] as const;
101
+ }),
102
+ );
103
+ const catalogs = Object.fromEntries(catalogEntries);
104
+
105
+ @Module({
106
+ imports: [
107
+ I18nModule.forRoot({
108
+ defaultLocale: 'en',
109
+ supportedLocales: locales,
110
+ fallbackLocales: { ko: ['en'] },
111
+ catalogs,
112
+ }),
113
+ ],
114
+ })
115
+ class AppModule {}
116
+ ```
117
+
118
+ Aggregate는 namespace boundary를 보존합니다. `locales/ko/common.json`은 namespace tree를 shallow merge하지 말고 `i18n.translate('title', { locale: 'ko', namespace: 'common' })`로 조회하세요. Catalog file이 없으면 aggregation은 `I18N_MISSING_CATALOG`으로 reject됩니다. `fallbackLocales`는 catalog가 존재한 뒤 메시지를 조회할 때만 적용됩니다.
119
+
120
+ NestJS i18n fallback intent는 명시적으로 변환합니다.
121
+
122
+ ```ts
123
+ // NestJS i18n
124
+ I18nModule.forRoot({
125
+ fallbackLanguage: 'en',
126
+ fallbacks: { ko: 'en' },
127
+ });
128
+
129
+ // fluo
130
+ I18nModule.forRoot({
131
+ defaultLocale: 'en',
132
+ fallbackLocales: { ko: ['en'] },
133
+ catalogs,
134
+ });
135
+ ```
136
+
137
+ 이 방식은 lookup order를 보존합니다. 명시적 locale, 구성된 fallback chain, `defaultLocale`, 호출별 `defaultValue`, `missingMessage` 순서입니다. Registration 전에 비동기 aggregation을 완료해도 이 순서는 바뀌지 않으며 `forRootAsync(...)`도 추가되지 않습니다.
138
+
73
139
  ## 코어 번역
74
140
 
75
141
  `I18nService`는 결정론적인 번역 조회를 제공합니다.
@@ -344,11 +410,11 @@ const loader = createRemoteI18nLoader({
344
410
  const common = await loader.load('en', 'common');
345
411
  ```
346
412
 
347
- Provider는 validated `locale`, `namespace`, 그리고 loader timeout과 optional per-call cancellation을 결합한 `AbortSignal`을 받습니다. Provider는 raw object message tree 또는 JSON string을 반환할 수 있습니다. `undefined`와 `null`은 missing catalog로 취급되어 `I18N_MISSING_CATALOG`를 throw합니다. Malformed JSON과 invalid message tree shape는 `I18N_INVALID_CATALOG`, provider failure는 `I18N_LOADER_FAILED`, timeout은 `I18N_LOADER_TIMEOUT`, caller cancellation은 `I18N_LOADER_ABORTED`로 보고됩니다. 반환된 catalog는 항상 detached immutable `I18nMessageTree` snapshot입니다.
413
+ Provider는 validated `locale`, `namespace`, 그리고 loader timeout과 optional per-call cancellation을 결합한 `AbortSignal`을 받습니다. `timeoutMs`는 runtime timer가 정확하게 표현할 수 있는 최대 지연값인 `2_147_483_647` 이하의 양의 정수여야 합니다. Provider는 raw object message tree 또는 JSON string을 반환할 수 있습니다. `undefined`와 `null`은 missing catalog로 취급되어 `I18N_MISSING_CATALOG`를 throw합니다. Malformed JSON과 invalid message tree shape는 `I18N_INVALID_CATALOG`, provider throw한 `I18nError` instance는 변경 없이 다시 throw되며 그 외 provider failure는 `I18N_LOADER_FAILED`, timeout은 `I18N_LOADER_TIMEOUT`, caller cancellation은 `I18N_LOADER_ABORTED`로 보고됩니다. 반환된 catalog는 항상 detached immutable `I18nMessageTree` snapshot입니다.
348
414
 
349
415
  Remote loader는 기본적으로 cache하지 않습니다. 모든 `load(locale, namespace)` 호출은 provider를 호출하고 그 provider result를 snapshot합니다. Memory, HTTP, CDN, database 또는 stale-while-revalidate caching이 필요한 애플리케이션은 cache invalidation이 application boundary에서 명시적으로 유지되도록 provider 내부 또는 provider wrapper에서 구현해야 합니다.
350
416
 
351
- First-party in-memory policy가 필요한 애플리케이션은 loader를 명시적으로 wrap할 수 있습니다. Cache entry는 caller가 custom key를 제공하지 않는 한 `(locale, namespace, version)`으로 keying되며, `invalidate(...)` / `clear()`가 invalidation을 application-owned 상태로 유지합니다.
417
+ First-party in-memory policy가 필요한 애플리케이션은 loader를 명시적으로 wrap할 수 있습니다. Cache entry는 caller가 custom key를 제공하지 않는 한 `(locale, namespace, version)`으로 keying되고 successful load 이후에만 TTL을 시작하며, `invalidate(...)` / `clear()`가 invalidation을 application-owned 상태로 유지합니다.
352
418
 
353
419
  ```ts
354
420
  import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
@@ -413,7 +479,7 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
413
479
 
414
480
  이 helper declaration은 type-only이며 application-owned입니다. Runtime wrapper를 추가하지 않고, framework bridge를 import하지 않으며, 넓은 runtime `I18nService.translate(key: string, options)` signature도 바꾸지 않습니다.
415
481
 
416
- 두 helper 모두 locale 간 key를 deduplicate하고 stable diff를 위해 output을 sort하며, invalid catalog shape는 `I18N_INVALID_CATALOG`, unsafe locale 또는 namespace path는 `I18N_INVALID_LOADER_OPTIONS`로 거부합니다.
482
+ 두 helper 모두 locale 간 key를 deduplicate하고 stable diff를 위해 output을 sort하며, invalid catalog shape는 `I18N_INVALID_CATALOG`, unsafe locale 또는 namespace path는 `I18N_INVALID_LOADER_OPTIONS`로 거부합니다. Custom output name은 유효하고 서로 다른 TypeScript identifier여야 하며, invalid 또는 충돌하는 name은 `I18N_INVALID_OPTIONS`로 거부합니다.
417
483
 
418
484
  ## 공개 API
419
485
 
package/README.md CHANGED
@@ -9,6 +9,7 @@ Framework-agnostic internationalization core surface for fluo applications.
9
9
  - [Installation](#installation)
10
10
  - [When to Use](#when-to-use)
11
11
  - [Quick Start](#quick-start)
12
+ - [Catalog Aggregation and Fallback Migration](#catalog-aggregation-and-fallback-migration)
12
13
  - [Core Translation](#core-translation)
13
14
  - [Formatting](#formatting)
14
15
  - [ICU MessageFormat](#icu-messageformat)
@@ -70,6 +71,71 @@ class AppModule {}
70
71
 
71
72
  `I18nModule.forRoot(...)` exports `I18nService` as a global provider by default so sibling modules can inject the shared service after the root package is imported once. Pass `global: false` when the service should stay visible only to the module that imports the i18n module.
72
73
 
74
+ `I18nModule.forRoot(...)` is synchronous and captures final options when the module graph is defined. Finish asynchronous catalog or configuration loading at the application-owned bootstrap boundary before `I18nModule.forRoot(...)`, then pass the resolved catalogs and options into that registration call. This preserves the framework-agnostic root contract; it does not provide a NestJS dynamic-module runtime bridge or a `forRootAsync(...)` compatibility surface.
75
+
76
+ ## Catalog Aggregation and Fallback Migration
77
+
78
+ When migrating from NestJS i18n, load every required locale and namespace before the synchronous `I18nModule.forRoot(...)` call. Do not carry a NestJS loader configuration into module registration:
79
+
80
+ ```ts
81
+ import { Module } from '@fluojs/core';
82
+ import { I18nModule } from '@fluojs/i18n';
83
+ import { createFileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';
84
+
85
+ const locales = ['en', 'ko'] as const;
86
+ const namespaces = ['common', 'validation'] as const;
87
+ const catalogLoader = createFileSystemI18nLoader({
88
+ rootDir: new URL('./locales', import.meta.url).pathname,
89
+ });
90
+
91
+ const catalogEntries = await Promise.all(
92
+ locales.map(async (locale) => {
93
+ const namespaceEntries = await Promise.all(
94
+ namespaces.map(async (namespace) => [
95
+ namespace,
96
+ await catalogLoader.load(locale, namespace),
97
+ ] as const),
98
+ );
99
+
100
+ return [locale, Object.fromEntries(namespaceEntries)] as const;
101
+ }),
102
+ );
103
+ const catalogs = Object.fromEntries(catalogEntries);
104
+
105
+ @Module({
106
+ imports: [
107
+ I18nModule.forRoot({
108
+ defaultLocale: 'en',
109
+ supportedLocales: locales,
110
+ fallbackLocales: { ko: ['en'] },
111
+ catalogs,
112
+ }),
113
+ ],
114
+ })
115
+ class AppModule {}
116
+ ```
117
+
118
+ The aggregate preserves namespace boundaries: use `i18n.translate('title', { locale: 'ko', namespace: 'common' })` for `locales/ko/common.json`, rather than shallow-merging namespace trees. A missing catalog file rejects aggregation with `I18N_MISSING_CATALOG`; `fallbackLocales` applies only to message lookup after catalogs exist.
119
+
120
+ Convert NestJS i18n fallback intent explicitly:
121
+
122
+ ```ts
123
+ // NestJS i18n
124
+ I18nModule.forRoot({
125
+ fallbackLanguage: 'en',
126
+ fallbacks: { ko: 'en' },
127
+ });
128
+
129
+ // fluo
130
+ I18nModule.forRoot({
131
+ defaultLocale: 'en',
132
+ fallbackLocales: { ko: ['en'] },
133
+ catalogs,
134
+ });
135
+ ```
136
+
137
+ This preserves the lookup order: explicit locale, configured fallback chain, `defaultLocale`, per-call `defaultValue`, then `missingMessage`. Completing asynchronous aggregation before registration does not change that order or add `forRootAsync(...)`.
138
+
73
139
  ## Core Translation
74
140
 
75
141
  The `I18nService` provides deterministic translation lookup.
@@ -344,11 +410,11 @@ const loader = createRemoteI18nLoader({
344
410
  const common = await loader.load('en', 'common');
345
411
  ```
346
412
 
347
- The provider receives the validated `locale`, `namespace`, and an `AbortSignal` that combines the loader timeout with optional per-call cancellation. Providers may return a raw object message tree or a JSON string. `undefined` and `null` are treated as missing catalogs and throw `I18N_MISSING_CATALOG`; malformed JSON and invalid message tree shapes throw `I18N_INVALID_CATALOG`; provider failures are wrapped as `I18N_LOADER_FAILED`; timeouts throw `I18N_LOADER_TIMEOUT`; caller cancellation throws `I18N_LOADER_ABORTED`. Returned catalogs are always detached immutable `I18nMessageTree` snapshots.
413
+ The provider receives the validated `locale`, `namespace`, and an `AbortSignal` that combines the loader timeout with optional per-call cancellation. `timeoutMs` must be a positive integer no greater than `2_147_483_647`, the largest delay that the runtime timer can represent truthfully. Providers may return a raw object message tree or a JSON string. `undefined` and `null` are treated as missing catalogs and throw `I18N_MISSING_CATALOG`; malformed JSON and invalid message tree shapes throw `I18N_INVALID_CATALOG`; provider-thrown `I18nError` instances are rethrown unchanged, while other provider failures are wrapped as `I18N_LOADER_FAILED`; timeouts throw `I18N_LOADER_TIMEOUT`; caller cancellation throws `I18N_LOADER_ABORTED`. Returned catalogs are always detached immutable `I18nMessageTree` snapshots.
348
414
 
349
415
  The remote loader never caches by default: every `load(locale, namespace)` call invokes the provider and snapshots that provider result. Applications that need memory, HTTP, CDN, database, or stale-while-revalidate caching should implement it inside the provider or in a wrapper around the provider so cache invalidation remains explicit at the application boundary.
350
416
 
351
- Applications that want a first-party in-memory policy can wrap the loader explicitly. Cache entries are keyed by `(locale, namespace, version)` unless the caller provides a custom key, and `invalidate(...)` / `clear()` keep invalidation application-owned:
417
+ Applications that want a first-party in-memory policy can wrap the loader explicitly. Cache entries are keyed by `(locale, namespace, version)` unless the caller provides a custom key, begin their TTL only after a successful load, and keep invalidation application-owned through `invalidate(...)` / `clear()`:
352
418
 
353
419
  ```ts
354
420
  import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
@@ -413,7 +479,7 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
413
479
 
414
480
  These helper declarations are type-only and application-owned. They do not add runtime wrappers, do not import framework bridges, and do not change the broad runtime `I18nService.translate(key: string, options)` signature.
415
481
 
416
- Both helpers deduplicate keys across locales, sort output for stable diffs, reject invalid catalog shapes with `I18N_INVALID_CATALOG`, and reject unsafe locale or namespace paths with `I18N_INVALID_LOADER_OPTIONS`.
482
+ Both helpers deduplicate keys across locales, sort output for stable diffs, reject invalid catalog shapes with `I18N_INVALID_CATALOG`, and reject unsafe locale or namespace paths with `I18N_INVALID_LOADER_OPTIONS`. Custom output names must be valid, distinct TypeScript identifiers; invalid or colliding names reject with `I18N_INVALID_OPTIONS`.
417
483
 
418
484
  ## Public API
419
485
 
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
package/dist/adapters.js CHANGED
@@ -1,4 +1,5 @@
1
- import { isSupportedLocale, isValidLocale, normalizeLocaleResolverResult, parseLocalePreferences, resolveSupportedLocale, selectLocaleFromAcceptLanguagePolicy } from './locale-resolution.js';
1
+ import { isSupportedLocale, isValidLocale, parseLocalePreferences, resolveSupportedLocale, selectLocaleFromAcceptLanguagePolicy } from './locale-resolution.js';
2
+ import { resolveLocaleResolverChain } from './resolver-chain.js';
2
3
 
3
4
  /**
4
5
  * Locale metadata resolved for a non-HTTP transport context.
@@ -102,30 +103,11 @@ export function getAdapterLocale(store, context) {
102
103
  * @throws {TypeError} When the configured default locale is invalid or unsupported.
103
104
  */
104
105
  export function resolveLocale(context, options) {
105
- if (!isValidLocale(options.defaultLocale)) {
106
- throw new TypeError('defaultLocale must be a syntactically valid locale string.');
107
- }
108
- if (!isSupportedLocale(options.defaultLocale, options.supportedLocales)) {
109
- throw new TypeError('defaultLocale must be listed in supportedLocales when supportedLocales is provided.');
110
- }
111
- for (const resolver of options.resolvers ?? []) {
112
- const result = normalizeLocaleResolverResult(resolver({
113
- context,
114
- defaultLocale: options.defaultLocale,
115
- supportedLocales: options.supportedLocales
116
- }));
117
- if (result === undefined || !isValidLocale(result.locale) || !isSupportedLocale(result.locale, options.supportedLocales)) {
118
- continue;
119
- }
120
- return Object.freeze({
121
- locale: result.locale,
122
- source: result.source
123
- });
124
- }
125
- return Object.freeze({
126
- locale: options.defaultLocale,
127
- source: 'default'
128
- });
106
+ return resolveLocaleResolverChain({
107
+ context,
108
+ defaultLocale: options.defaultLocale,
109
+ supportedLocales: options.supportedLocales
110
+ }, options);
129
111
  }
130
112
 
131
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO9E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA0BjF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBxG"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO9E;AAmCD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAEjF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBxG"}
package/dist/catalog.js CHANGED
@@ -12,19 +12,15 @@ export function isPlainObject(value) {
12
12
  const prototype = Object.getPrototypeOf(value);
13
13
  return prototype === Object.prototype || prototype === null;
14
14
  }
15
-
16
- /**
17
- * Creates a detached immutable message tree from untrusted catalog-like input.
18
- *
19
- * @param value Catalog-like value to validate and snapshot.
20
- * @param path Diagnostic path used in validation errors.
21
- * @returns A frozen message tree detached from caller-owned data.
22
- */
23
- export function snapshotMessageTree(value, path) {
15
+ function snapshotMessageTreeAtPath(value, path, ancestors) {
24
16
  if (!isPlainObject(value)) {
25
17
  throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
26
18
  }
27
- const snapshot = {};
19
+ if (ancestors.has(value)) {
20
+ throw new I18nError(`${path} contains a cyclic message tree.`, 'I18N_INVALID_CATALOG');
21
+ }
22
+ ancestors.add(value);
23
+ const snapshot = Object.create(null);
28
24
  for (const [key, entry] of Object.entries(value)) {
29
25
  if (key.trim() === '') {
30
26
  throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
@@ -34,14 +30,26 @@ export function snapshotMessageTree(value, path) {
34
30
  continue;
35
31
  }
36
32
  if (isPlainObject(entry)) {
37
- snapshot[key] = snapshotMessageTree(entry, `${path}.${key}`);
33
+ snapshot[key] = snapshotMessageTreeAtPath(entry, `${path}.${key}`, ancestors);
38
34
  continue;
39
35
  }
40
36
  throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
41
37
  }
38
+ ancestors.delete(value);
42
39
  return Object.freeze(snapshot);
43
40
  }
44
41
 
42
+ /**
43
+ * Creates a detached immutable message tree from untrusted catalog-like input.
44
+ *
45
+ * @param value Catalog-like value to validate and snapshot.
46
+ * @param path Diagnostic path used in validation errors.
47
+ * @returns A frozen message tree detached from caller-owned data.
48
+ */
49
+ export function snapshotMessageTree(value, path) {
50
+ return snapshotMessageTreeAtPath(value, path, new Set());
51
+ }
52
+
45
53
  /**
46
54
  * Resolves a direct or dot-path message from an immutable i18n message tree.
47
55
  *
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,iCAAiC,EAOvC,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,gGAAgG;IAChG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,gGAAgG;IAChG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,2EAA2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACpD;AAED;;GAEG;AACH,MAAM,WAAW,mCAAmC;IAClD,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,yCAA0C,SAAQ,mCAAmC,EAAE,iCAAiC;CAAG;AAE5I;;GAEG;AACH,eAAO,MAAM,uBAAuB,sDAA+D,CAAC;AAepG;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,UAAU,EAClB,QAAQ,GAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAM,GAC/C,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,GAAG,SAAS,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,wBAAwB,EAAE,CAEvH;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,GAAE,mCAAwC,GAChD,kBAAkB,CAsBpB;AAED;;;;;GAKG;AACH,wBAAgB,wCAAwC,CACtD,OAAO,GAAE,yCAA8C,GACtD,kBAAkB,CAkBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAwB/G"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,iCAAiC,EAIvC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,gGAAgG;IAChG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC;AAE7E;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,gGAAgG;IAChG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,2EAA2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACpD;AAED;;GAEG;AACH,MAAM,WAAW,mCAAmC;IAClD,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,yCAA0C,SAAQ,mCAAmC,EAAE,iCAAiC;CAAG;AAE5I;;GAEG;AACH,eAAO,MAAM,uBAAuB,sDAA+D,CAAC;AAepG;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,UAAU,EAClB,QAAQ,GAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAM,GAC/C,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,GAAG,SAAS,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,wBAAwB,EAAE,CAEvH;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,GAAE,mCAAwC,GAChD,kBAAkB,CAsBpB;AAED;;;;;GAKG;AACH,wBAAgB,wCAAwC,CACtD,OAAO,GAAE,yCAA8C,GACtD,kBAAkB,CAkBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAQ/G"}
package/dist/http.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createContextKey, getContextValue, setContextValue } from '@fluojs/http';
2
- import { isSupportedLocale, isValidLocale, normalizeLocaleResolverResult, parseLocalePreferences, resolveSupportedLocale, selectLocaleFromAcceptLanguagePolicy } from './locale-resolution.js';
2
+ import { parseLocalePreferences, resolveSupportedLocale, selectLocaleFromAcceptLanguagePolicy } from './locale-resolution.js';
3
+ import { resolveLocaleResolverChain } from './resolver-chain.js';
3
4
 
4
5
  /**
5
6
  * Locale metadata stored on a fluo HTTP request context.
@@ -147,34 +148,13 @@ export function createAcceptLanguageLocalePolicyResolver(options = {}) {
147
148
  * @throws {TypeError} When the configured default locale is invalid or unsupported.
148
149
  */
149
150
  export function resolveHttpLocale(context, options) {
150
- if (!isValidLocale(options.defaultLocale)) {
151
- throw new TypeError('defaultLocale must be a syntactically valid locale string.');
152
- }
153
- if (!isSupportedLocale(options.defaultLocale, options.supportedLocales)) {
154
- throw new TypeError('defaultLocale must be listed in supportedLocales when supportedLocales is provided.');
155
- }
156
- for (const resolver of options.resolvers ?? []) {
157
- const result = normalizeLocaleResolverResult(resolver({
158
- context,
159
- defaultLocale: options.defaultLocale,
160
- supportedLocales: options.supportedLocales
161
- }));
162
- if (result === undefined || !isValidLocale(result.locale) || !isSupportedLocale(result.locale, options.supportedLocales)) {
163
- continue;
164
- }
165
- setHttpLocale(context, result.locale, {
166
- source: result.source
167
- });
168
- return getHttpLocale(context) ?? {
169
- locale: result.locale,
170
- source: result.source
171
- };
172
- }
173
- setHttpLocale(context, options.defaultLocale, {
174
- source: 'default'
151
+ const resolved = resolveLocaleResolverChain({
152
+ context,
153
+ defaultLocale: options.defaultLocale,
154
+ supportedLocales: options.supportedLocales
155
+ }, options);
156
+ setHttpLocale(context, resolved.locale, {
157
+ source: resolved.source
175
158
  });
176
- return getHttpLocale(context) ?? {
177
- locale: options.defaultLocale,
178
- source: 'default'
179
- };
159
+ return getHttpLocale(context) ?? resolved;
180
160
  }
package/dist/icu.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"icu.d.ts","sourceRoot":"","sources":["../src/icu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAuC,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE/G;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;AAExF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC;IACnF,kGAAkG;IAClG,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AAqDD;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IAEtC;;;;OAIG;gBACS,OAAO,GAAE,iBAAiB,GAAG,WAAgB;IAIzD;;;;OAIG;IACH,cAAc,IAAI,WAAW;IAI7B;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;CAsBjE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAiB,GAAG,WAAgB,GAAG,cAAc,CAE3F"}
1
+ {"version":3,"file":"icu.d.ts","sourceRoot":"","sources":["../src/icu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,WAAW,EAA4C,MAAM,cAAc,CAAC;AACrF,OAAO,KAAK,EAAuC,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE/G;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;AAExF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC;IACnF,kGAAkG;IAClG,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AA4CD;;;;;;GAMG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IAEtC;;;;OAIG;gBACS,OAAO,GAAE,iBAAiB,GAAG,WAAgB;IAIzD;;;;OAIG;IACH,cAAc,IAAI,WAAW;IAI7B;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;CAsBjE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAiB,GAAG,WAAgB,GAAG,cAAc,CAE3F"}
package/dist/icu.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { FormatError, IntlMessageFormat } from 'intl-messageformat';
2
- import { resolveCatalogMessage } from './catalog.js';
3
2
  import { I18nError } from './errors.js';
4
- import { I18nService, createI18n } from './service.js';
3
+ import { I18nService, createI18n, resolveI18nMessageProvenance } from './service.js';
5
4
 
6
5
  /**
7
6
  * Primitive values accepted by the ICU MessageFormat subpath.
@@ -39,14 +38,7 @@ function toMessageFormatValues(values) {
39
38
  return Object.fromEntries(Object.entries(values));
40
39
  }
41
40
  function resolveMessageLocale(service, key, options) {
42
- const resolvedKey = options.namespace === undefined ? key : `${options.namespace}.${key}`;
43
- const snapshot = service.snapshotOptions();
44
- for (const locale of service.resolveLocales(options.locale)) {
45
- if (resolveCatalogMessage(snapshot.catalogs?.[locale], resolvedKey) !== undefined) {
46
- return locale;
47
- }
48
- }
49
- return options.locale;
41
+ return resolveI18nMessageProvenance(service, key, options.locale, options.namespace)?.locale ?? options.locale;
50
42
  }
51
43
  function normalizeMessageFormatError(error, key) {
52
44
  if (error instanceof FormatError || error instanceof SyntaxError || error instanceof Error) {
@@ -1 +1 @@
1
- {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/loaders/remote.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../../src/loaders/remote.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { I18nError } from '../errors.js';
2
2
  import { isPlainObject, snapshotLoaderMessageTree, validateLoaderLocale, validateLoaderNamespace } from './shared.js';
3
3
  const DEFAULT_TIMEOUT_MS = 10_000;
4
+ const MAX_TIMEOUT_MS = 2_147_483_647;
4
5
 
5
6
  /**
6
7
  * Request metadata passed to a remote catalog provider.
@@ -30,8 +31,8 @@ function validateTimeout(timeoutMs) {
30
31
  if (timeoutMs === undefined) {
31
32
  return DEFAULT_TIMEOUT_MS;
32
33
  }
33
- if (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
34
- throw new I18nError('Remote i18n loader timeoutMs must be a positive integer when provided.', 'I18N_INVALID_LOADER_OPTIONS');
34
+ if (typeof timeoutMs !== 'number' || !Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS) {
35
+ throw new I18nError(`Remote i18n loader timeoutMs must be a positive integer no greater than ${MAX_TIMEOUT_MS} when provided.`, 'I18N_INVALID_LOADER_OPTIONS');
35
36
  }
36
37
  return timeoutMs;
37
38
  }
@@ -137,8 +138,10 @@ export class RemoteI18nLoader {
137
138
  const controller = new AbortController();
138
139
  const unlinkCallerAbort = linkCallerAbort(options.signal, controller);
139
140
  const timeout = setTimeout(() => controller.abort(createTimeoutError()), this.timeoutMs);
141
+ let rejectOnAbort;
140
142
  const abortRace = new Promise((_resolve, reject) => {
141
- controller.signal.addEventListener('abort', () => reject(controller.signal.reason instanceof I18nError ? controller.signal.reason : createAbortError()), {
143
+ rejectOnAbort = () => reject(controller.signal.reason instanceof I18nError ? controller.signal.reason : createAbortError());
144
+ controller.signal.addEventListener('abort', rejectOnAbort, {
142
145
  once: true
143
146
  });
144
147
  });
@@ -159,6 +162,9 @@ export class RemoteI18nLoader {
159
162
  } finally {
160
163
  clearTimeout(timeout);
161
164
  unlinkCallerAbort();
165
+ if (rejectOnAbort !== undefined) {
166
+ controller.signal.removeEventListener('abort', rejectOnAbort);
167
+ }
162
168
  }
163
169
  }
164
170
  }
@@ -218,7 +224,7 @@ export class CachedRemoteI18nLoader {
218
224
  const catalog = await this.loader.load(locale, namespace, options);
219
225
  this.cache.set(cacheKey, {
220
226
  catalog,
221
- expiresAt: currentTime + this.ttlMs
227
+ expiresAt: this.now() + this.ttlMs
222
228
  });
223
229
  return catalog;
224
230
  }
@@ -0,0 +1,7 @@
1
+ import type { I18nLocale, I18nMessageCatalogs } from './types.js';
2
+ export interface I18nMessageProvenance {
3
+ readonly locale: I18nLocale;
4
+ readonly message: string;
5
+ }
6
+ export declare function resolveMessageProvenance(catalogs: I18nMessageCatalogs | undefined, locales: readonly I18nLocale[], key: string): I18nMessageProvenance | undefined;
7
+ //# sourceMappingURL=message-provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-provenance.d.ts","sourceRoot":"","sources":["../src/message-provenance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,GAAG,EAAE,MAAM,GACV,qBAAqB,GAAG,SAAS,CAUnC"}
@@ -0,0 +1,13 @@
1
+ import { resolveCatalogMessage } from './catalog.js';
2
+ export function resolveMessageProvenance(catalogs, locales, key) {
3
+ for (const locale of locales) {
4
+ const message = resolveCatalogMessage(catalogs?.[locale], key);
5
+ if (message !== undefined) {
6
+ return Object.freeze({
7
+ locale,
8
+ message
9
+ });
10
+ }
11
+ }
12
+ return undefined;
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { I18nLocale } from './types.js';
2
+ export interface LocaleResolverChainResult {
3
+ readonly locale: I18nLocale;
4
+ readonly source?: string;
5
+ }
6
+ export interface LocaleResolverChainOptions<TInput> {
7
+ readonly defaultLocale: I18nLocale;
8
+ readonly supportedLocales?: readonly I18nLocale[];
9
+ readonly resolvers?: readonly ((input: TInput) => unknown)[];
10
+ }
11
+ export declare function resolveLocaleResolverChain<TInput>(input: TInput, options: LocaleResolverChainOptions<TInput>): LocaleResolverChainResult;
12
+ //# sourceMappingURL=resolver-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver-chain.d.ts","sourceRoot":"","sources":["../src/resolver-chain.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B,CAAC,MAAM;IAChD,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC;IACnC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,EAAE,CAAC;CAC9D;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAC/C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,0BAA0B,CAAC,MAAM,CAAC,GAC1C,yBAAyB,CAkB3B"}
@@ -0,0 +1,22 @@
1
+ import { isSupportedLocale, isValidLocale, normalizeLocaleResolverResult } from './locale-resolution.js';
2
+ export function resolveLocaleResolverChain(input, options) {
3
+ if (!isValidLocale(options.defaultLocale)) {
4
+ throw new TypeError('defaultLocale must be a syntactically valid locale string.');
5
+ }
6
+ if (!isSupportedLocale(options.defaultLocale, options.supportedLocales)) {
7
+ throw new TypeError('defaultLocale must be listed in supportedLocales when supportedLocales is provided.');
8
+ }
9
+ for (const resolver of options.resolvers ?? []) {
10
+ const result = normalizeLocaleResolverResult(resolver(input));
11
+ if (result !== undefined && isValidLocale(result.locale) && isSupportedLocale(result.locale, options.supportedLocales)) {
12
+ return Object.freeze({
13
+ locale: result.locale,
14
+ source: result.source
15
+ });
16
+ }
17
+ }
18
+ return Object.freeze({
19
+ locale: options.defaultLocale,
20
+ source: 'default'
21
+ });
22
+ }
package/dist/service.d.ts CHANGED
@@ -94,6 +94,7 @@ export declare class I18nService {
94
94
  */
95
95
  translate(key: string, options: I18nTranslateOptions): string;
96
96
  }
97
+ export declare function resolveI18nMessageProvenance(service: I18nService, key: string, locale: I18nLocale, namespace: string | undefined): import("./message-provenance.js").I18nMessageProvenance | undefined;
97
98
  /**
98
99
  * Creates a standalone i18n service without registering a fluo module.
99
100
  *
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA2GpB;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;OAIG;gBACS,OAAO,GAAE,iBAAsB;IAI3C;;;;OAIG;IACH,eAAe,IAAI,iBAAiB;IAIpC;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;IA2BzD,OAAO,CAAC,mBAAmB;IAkB3B;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAWhF;;;;;;;OAOG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWrE;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAiBzE;;;;;;;OAOG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWtE;;;;;;;OAOG;IACH,UAAU,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAY7E;;;;;;;;OAQG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,GAAG,MAAM;IAWpH;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM;CAoC9D;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,WAAW,CAEvE"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACrB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA6GpB;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;OAIG;gBACS,OAAO,GAAE,iBAAsB;IAK3C;;;;OAIG;IACH,eAAe,IAAI,iBAAiB;IAIpC;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;IA2BzD,OAAO,CAAC,mBAAmB;IAkB3B;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAWhF;;;;;;;OAOG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWrE;;;;;;;OAOG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,GAAG,MAAM;IAiBzE;;;;;;;OAOG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM;IAWtE;;;;;;;OAOG;IACH,UAAU,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM;IAY7E;;;;;;;;OAQG;IACH,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,OAAO,EAAE,6BAA6B,GAAG,MAAM;IAWpH;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM;CAkC9D;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,WAAW,EACpB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,GAAG,SAAS,uEAU9B;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,WAAW,CAEvE"}
package/dist/service.js CHANGED
@@ -1,6 +1,8 @@
1
- import { isPlainObject, resolveCatalogMessage } from './catalog.js';
1
+ import { isPlainObject } from './catalog.js';
2
+ import { resolveMessageProvenance } from './message-provenance.js';
2
3
  import { snapshotI18nModuleOptions } from './options.js';
3
4
  import { I18nError } from './errors.js';
5
+ const serviceOptions = new WeakMap();
4
6
  function normalizeTranslationKey(key, namespace) {
5
7
  if (typeof key !== 'string') {
6
8
  throw new I18nError('Translation key must be a string.', 'I18N_INVALID_OPTIONS');
@@ -103,6 +105,7 @@ export class I18nService {
103
105
  */
104
106
  constructor(options = {}) {
105
107
  this.options = snapshotI18nModuleOptions(options);
108
+ serviceOptions.set(this, this.options);
106
109
  }
107
110
 
108
111
  /**
@@ -289,11 +292,9 @@ export class I18nService {
289
292
  assertDefaultValue(options.defaultValue);
290
293
  const resolvedKey = normalizeTranslationKey(key, options.namespace);
291
294
  const locales = this.resolveLocales(options.locale);
292
- for (const locale of locales) {
293
- const message = resolveCatalogMessage(this.options.catalogs?.[locale], resolvedKey);
294
- if (message !== undefined) {
295
- return interpolate(message, options.values);
296
- }
295
+ const message = resolveMessageProvenance(this.options.catalogs, locales, resolvedKey);
296
+ if (message !== undefined) {
297
+ return interpolate(message.message, options.values);
297
298
  }
298
299
  if (options.defaultValue !== undefined) {
299
300
  return interpolate(options.defaultValue, options.values);
@@ -310,6 +311,14 @@ export class I18nService {
310
311
  throw new I18nError(`Missing i18n message: ${resolvedKey}`, 'I18N_MISSING_MESSAGE');
311
312
  }
312
313
  }
314
+ export function resolveI18nMessageProvenance(service, key, locale, namespace) {
315
+ const options = serviceOptions.get(service);
316
+ if (options === undefined) {
317
+ return undefined;
318
+ }
319
+ const resolvedKey = normalizeTranslationKey(key, namespace);
320
+ return resolveMessageProvenance(options.catalogs, service.resolveLocales(locale), resolvedKey);
321
+ }
313
322
 
314
323
  /**
315
324
  * Creates a standalone i18n service without registering a fluo module.
@@ -1 +1 @@
1
- {"version":3,"file":"typegen.d.ts","sourceRoot":"","sources":["../src/typegen.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkBlF;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,mGAAmG;IACnG,QAAQ,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,8EAA8E;IAC9E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IAChD,uEAAuE;IACvE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,wEAAwE;IACxE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,kCAAmC,SAAQ,yBAAyB;IACnF,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uGAAuG;IACvG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AA+JD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,SAAS,uBAAuB,EAAE,EAC1C,OAAO,GAAE,yBAA8B,GACtC,MAAM,CA8ER;AAED;;;;;;GAMG;AACH,wBAAsB,qCAAqC,CAAC,OAAO,EAAE,kCAAkC,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBxH"}
1
+ {"version":3,"file":"typegen.d.ts","sourceRoot":"","sources":["../src/typegen.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkBlF;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,mGAAmG;IACnG,QAAQ,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,8EAA8E;IAC9E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,8EAA8E;IAC9E,QAAQ,CAAC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IAChD,uEAAuE;IACvE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,wEAAwE;IACxE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,kCAAmC,SAAQ,yBAAyB;IACnF,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uGAAuG;IACvG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AA+JD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,SAAS,uBAAuB,EAAE,EAC1C,OAAO,GAAE,yBAA8B,GACtC,MAAM,CA0FR;AAED;;;;;;GAMG;AACH,wBAAsB,qCAAqC,CAAC,OAAO,EAAE,kCAAkC,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBxH"}
package/dist/typegen.js CHANGED
@@ -154,6 +154,10 @@ export function generateI18nCatalogTypes(inputs, options = {}) {
154
154
  const typedTranslateTypeName = assertIdentifier(options.typedTranslateTypeName, DEFAULT_TYPED_TRANSLATE_TYPE_NAME, 'Catalog typegen typedTranslateTypeName');
155
155
  const typedTranslateOptionsTypeName = assertIdentifier(options.typedTranslateOptionsTypeName, DEFAULT_TYPED_TRANSLATE_OPTIONS_TYPE_NAME, 'Catalog typegen typedTranslateOptionsTypeName');
156
156
  const typedServiceTypeName = assertIdentifier(options.typedServiceTypeName, DEFAULT_TYPED_SERVICE_TYPE_NAME, 'Catalog typegen typedServiceTypeName');
157
+ const generatedIdentifiers = [keyTypeName, namespaceTypeName, keyByNamespaceTypeName, namespaceKeyTypeName, typedTranslateOptionsTypeName, typedTranslateTypeName, typedServiceTypeName];
158
+ if (new Set(generatedIdentifiers).size !== generatedIdentifiers.length) {
159
+ throw new I18nError('Catalog typegen output identifiers must be unique.', 'I18N_INVALID_OPTIONS');
160
+ }
157
161
  const keys = new Set();
158
162
  const namespaces = new Set();
159
163
  const keysByNamespace = new Map();
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "localization",
9
9
  "translations"
10
10
  ],
11
- "version": "2.0.0",
11
+ "version": "3.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -60,12 +60,12 @@
60
60
  "dist"
61
61
  ],
62
62
  "dependencies": {
63
- "@fluojs/core": "^1.1.0"
63
+ "@fluojs/core": "^2.0.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "intl-messageformat": "^11.2.4",
67
- "@fluojs/http": "^2.0.1",
68
- "@fluojs/validation": "^1.0.6"
67
+ "@fluojs/http": "^3.0.0",
68
+ "@fluojs/validation": "^2.0.0"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "@fluojs/http": {
@@ -80,10 +80,10 @@
80
80
  },
81
81
  "devDependencies": {
82
82
  "intl-messageformat": "^11.2.4",
83
- "vitest": "^3.2.4",
84
- "@fluojs/testing": "^2.0.0",
85
- "@fluojs/http": "^2.0.1",
86
- "@fluojs/validation": "^1.0.6"
83
+ "vitest": "^4.1.11",
84
+ "@fluojs/testing": "^3.0.0",
85
+ "@fluojs/http": "^3.0.0",
86
+ "@fluojs/validation": "^2.0.0"
87
87
  },
88
88
  "scripts": {
89
89
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",