@fluojs/i18n 1.0.3 → 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)
@@ -30,7 +31,7 @@ fluo 애플리케이션을 위한 프레임워크 비종속 국제화 코어 표
30
31
  npm install @fluojs/i18n
31
32
  ```
32
33
 
33
- Root entry point는 `@fluojs/core`에만 의존합니다. Optional subpath는 integration dependency를 optional peer로 유지합니다. `@fluojs/i18n/icu`를 opt-in하면 `intl-messageformat`, `@fluojs/i18n/http`를 opt-in하면 `@fluojs/http`, `@fluojs/i18n/validation`을 opt-in하면 `@fluojs/validation`을 함께 설치하세요. 기존 subpath 사용자는 이 dependency boundary 변경이 포함된 릴리스로 업그레이드하기 전에 해당 peer dependency를 application 또는 package manifest에 추가해야 합니다.
34
+ Root entry point는 `@fluojs/core`에만 의존하고 Node.js engine floor를 선언하지 않으며 framework-agnostic bundle에서 안전하게 import할 수 있습니다. Optional subpath는 integration dependency를 optional peer로 유지합니다. `@fluojs/i18n/icu`를 opt-in하면 `intl-messageformat`, `@fluojs/i18n/http`를 opt-in하면 `@fluojs/http`, `@fluojs/i18n/validation`을 opt-in하면 `@fluojs/validation`을 함께 설치하세요. 기존 subpath 사용자는 이 dependency boundary 변경이 포함된 릴리스로 업그레이드하기 전에 해당 peer dependency를 application 또는 package manifest에 추가해야 합니다.
34
35
 
35
36
  ## 사용 시점
36
37
 
@@ -68,6 +69,73 @@ import { I18nModule } from '@fluojs/i18n';
68
69
  class AppModule {}
69
70
  ```
70
71
 
72
+ `I18nModule.forRoot(...)`는 기본적으로 `I18nService`를 global provider로 export하므로 root package를 한 번 import한 뒤 sibling module에서도 shared service를 inject할 수 있습니다. Service를 i18n module을 import한 module 안에만 보이게 하려면 `global: false`를 전달하세요.
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
+
71
139
  ## 코어 번역
72
140
 
73
141
  `I18nService`는 결정론적인 번역 조회를 제공합니다.
@@ -342,11 +410,11 @@ const loader = createRemoteI18nLoader({
342
410
  const common = await loader.load('en', 'common');
343
411
  ```
344
412
 
345
- 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입니다.
346
414
 
347
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에서 구현해야 합니다.
348
416
 
349
- 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 상태로 유지합니다.
350
418
 
351
419
  ```ts
352
420
  import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
@@ -411,7 +479,7 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
411
479
 
412
480
  이 helper declaration은 type-only이며 application-owned입니다. Runtime wrapper를 추가하지 않고, framework bridge를 import하지 않으며, 넓은 runtime `I18nService.translate(key: string, options)` signature도 바꾸지 않습니다.
413
481
 
414
- 두 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`로 거부합니다.
415
483
 
416
484
  ## 공개 API
417
485
 
@@ -419,10 +487,10 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
419
487
 
420
488
  | Export | 설명 |
421
489
  |---|---|
422
- | `I18nModule` | DI 등록을 위한 모듈입니다. |
490
+ | `I18nModule` | Core i18n service surface를 등록하는 모듈 facade입니다. Provider는 기본적으로 global이며 `global: false`로 module-local로 유지할 수 있습니다. |
423
491
  | `I18nService` | Detached option/catalog snapshot을 소유하고 translation을 resolve하며 explicit-locale `Intl` formatting helper(`formatDateTime`, `formatNumber`, `formatCurrency`, `formatPercent`, `formatList`, `formatRelativeTime`)를 제공하는 core service입니다. |
424
492
  | `createI18n` | 독립형 서비스를 생성하기 위한 헬퍼입니다. |
425
- | `I18nError` | 패키지 전용 에러 클래스입니다. |
493
+ | `I18nError` | 안정적인 에러 코드를 포함하는 기본 i18n 패키지 에러입니다. |
426
494
 
427
495
  **타입:** `I18nModuleOptions`, `I18nMessageCatalogs`, `I18nMessageTree`, `I18nTranslateOptions`, `I18nInterpolationValues`, `I18nMissingMessageHandler`, `I18nMissingMessageContext`, `I18nLocale`, `I18nTranslationKey`, `I18nErrorCode`, `I18nFallbackLocales`, `I18nFormatOptions`, `I18nFormatterOptions`, `I18nDateTimeFormatOptions`, `I18nNumberFormatOptions`, `I18nCurrencyFormatOptions`, `I18nListFormatOptions`, `I18nRelativeTimeFormatOptions`, `I18nNamedDateTimeFormats`, `I18nNamedNumberFormats`, `I18nNamedListFormats`, `I18nNamedRelativeTimeFormats`.
428
496
 
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)
@@ -30,7 +31,7 @@ Framework-agnostic internationalization core surface for fluo applications.
30
31
  npm install @fluojs/i18n
31
32
  ```
32
33
 
33
- The root entry point depends only on `@fluojs/core`. Optional subpaths keep their integration dependencies as optional peers: install `intl-messageformat` for `@fluojs/i18n/icu`, `@fluojs/http` for `@fluojs/i18n/http`, and `@fluojs/validation` for `@fluojs/i18n/validation` when you opt into those surfaces. Existing subpath users should add those peer dependencies to their application or package manifest before upgrading to the release that includes this dependency boundary change.
34
+ The root entry point depends only on `@fluojs/core`, does not declare a Node.js engine floor, and is safe to import in framework-agnostic bundles. Optional subpaths keep their integration dependencies as optional peers: install `intl-messageformat` for `@fluojs/i18n/icu`, `@fluojs/http` for `@fluojs/i18n/http`, and `@fluojs/validation` for `@fluojs/i18n/validation` when you opt into those surfaces. Existing subpath users should add those peer dependencies to their application or package manifest before upgrading to the release that includes this dependency boundary change.
34
35
 
35
36
  ## When to Use
36
37
 
@@ -68,6 +69,73 @@ import { I18nModule } from '@fluojs/i18n';
68
69
  class AppModule {}
69
70
  ```
70
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.
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
+
71
139
  ## Core Translation
72
140
 
73
141
  The `I18nService` provides deterministic translation lookup.
@@ -342,11 +410,11 @@ const loader = createRemoteI18nLoader({
342
410
  const common = await loader.load('en', 'common');
343
411
  ```
344
412
 
345
- 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.
346
414
 
347
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.
348
416
 
349
- 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()`:
350
418
 
351
419
  ```ts
352
420
  import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
@@ -411,7 +479,7 @@ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en'
411
479
 
412
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.
413
481
 
414
- 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`.
415
483
 
416
484
  ## Public API
417
485
 
@@ -419,7 +487,7 @@ Both helpers deduplicate keys across locales, sort output for stable diffs, reje
419
487
 
420
488
  | Export | Description |
421
489
  |---|---|
422
- | `I18nModule` | Module facade for registering the core i18n service surface. |
490
+ | `I18nModule` | Module facade for registering the core i18n service surface; providers are global by default and can be kept module-local with `global: false`. |
423
491
  | `I18nService` | Core service that owns detached options/catalog snapshots, resolves translations, and exposes explicit-locale `Intl` formatting helpers (`formatDateTime`, `formatNumber`, `formatCurrency`, `formatPercent`, `formatList`, `formatRelativeTime`). |
424
492
  | `createI18n(options)` | Creates a standalone `I18nService` without module registration. |
425
493
  | `I18nError` | Base i18n package error with a stable error code. |
@@ -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
  /**
@@ -0,0 +1,25 @@
1
+ import type { I18nMessageTree } from './types.js';
2
+ /**
3
+ * Checks whether a value is a plain object suitable for i18n option or catalog validation.
4
+ *
5
+ * @param value Candidate value to inspect.
6
+ * @returns `true` when the value is a plain object or null-prototype object.
7
+ */
8
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
9
+ /**
10
+ * Creates a detached immutable message tree from untrusted catalog-like input.
11
+ *
12
+ * @param value Catalog-like value to validate and snapshot.
13
+ * @param path Diagnostic path used in validation errors.
14
+ * @returns A frozen message tree detached from caller-owned data.
15
+ */
16
+ export declare function snapshotMessageTree(value: unknown, path: string): I18nMessageTree;
17
+ /**
18
+ * Resolves a direct or dot-path message from an immutable i18n message tree.
19
+ *
20
+ * @param tree Locale-scoped catalog tree to inspect.
21
+ * @param key Direct key or dot-path key to resolve.
22
+ * @returns The resolved message string, or `undefined` when the key is absent or not a string leaf.
23
+ */
24
+ export declare function resolveCatalogMessage(tree: I18nMessageTree | undefined, key: string): string | undefined;
25
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +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;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"}
@@ -0,0 +1,76 @@
1
+ import { I18nError } from './errors.js';
2
+ /**
3
+ * Checks whether a value is a plain object suitable for i18n option or catalog validation.
4
+ *
5
+ * @param value Candidate value to inspect.
6
+ * @returns `true` when the value is a plain object or null-prototype object.
7
+ */
8
+ export function isPlainObject(value) {
9
+ if (typeof value !== 'object' || value === null) {
10
+ return false;
11
+ }
12
+ const prototype = Object.getPrototypeOf(value);
13
+ return prototype === Object.prototype || prototype === null;
14
+ }
15
+ function snapshotMessageTreeAtPath(value, path, ancestors) {
16
+ if (!isPlainObject(value)) {
17
+ throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
18
+ }
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);
24
+ for (const [key, entry] of Object.entries(value)) {
25
+ if (key.trim() === '') {
26
+ throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
27
+ }
28
+ if (typeof entry === 'string') {
29
+ snapshot[key] = entry;
30
+ continue;
31
+ }
32
+ if (isPlainObject(entry)) {
33
+ snapshot[key] = snapshotMessageTreeAtPath(entry, `${path}.${key}`, ancestors);
34
+ continue;
35
+ }
36
+ throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
37
+ }
38
+ ancestors.delete(value);
39
+ return Object.freeze(snapshot);
40
+ }
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
+
53
+ /**
54
+ * Resolves a direct or dot-path message from an immutable i18n message tree.
55
+ *
56
+ * @param tree Locale-scoped catalog tree to inspect.
57
+ * @param key Direct key or dot-path key to resolve.
58
+ * @returns The resolved message string, or `undefined` when the key is absent or not a string leaf.
59
+ */
60
+ export function resolveCatalogMessage(tree, key) {
61
+ if (tree === undefined) {
62
+ return undefined;
63
+ }
64
+ if (Object.hasOwn(tree, key)) {
65
+ const direct = tree[key];
66
+ return typeof direct === 'string' ? direct : undefined;
67
+ }
68
+ let current = tree;
69
+ for (const part of key.split('.')) {
70
+ if (!isPlainObject(current) || !Object.hasOwn(current, part)) {
71
+ return undefined;
72
+ }
73
+ current = current[part];
74
+ }
75
+ return typeof current === 'string' ? current : undefined;
76
+ }
@@ -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;AAGlD,OAAO,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAwD,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEhI;;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;AAgFD;;;;;;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,6 +1,6 @@
1
1
  import { FormatError, IntlMessageFormat } from 'intl-messageformat';
2
2
  import { I18nError } from './errors.js';
3
- import { I18nService, createI18n } from './service.js';
3
+ import { I18nService, createI18n, resolveI18nMessageProvenance } from './service.js';
4
4
 
5
5
  /**
6
6
  * Primitive values accepted by the ICU MessageFormat subpath.
@@ -37,35 +37,8 @@ function toMessageFormatValues(values) {
37
37
  }
38
38
  return Object.fromEntries(Object.entries(values));
39
39
  }
40
- function hasOwn(value, key) {
41
- return typeof value === 'object' && value !== null && Object.hasOwn(value, key);
42
- }
43
- function resolveMessage(tree, key) {
44
- if (tree === undefined) {
45
- return undefined;
46
- }
47
- if (hasOwn(tree, key)) {
48
- const direct = tree[key];
49
- return typeof direct === 'string' ? direct : undefined;
50
- }
51
- let current = tree;
52
- for (const part of key.split('.')) {
53
- if (!hasOwn(current, part)) {
54
- return undefined;
55
- }
56
- current = current[part];
57
- }
58
- return typeof current === 'string' ? current : undefined;
59
- }
60
40
  function resolveMessageLocale(service, key, options) {
61
- const resolvedKey = options.namespace === undefined ? key : `${options.namespace}.${key}`;
62
- const snapshot = service.snapshotOptions();
63
- for (const locale of service.resolveLocales(options.locale)) {
64
- if (resolveMessage(snapshot.catalogs?.[locale], resolvedKey) !== undefined) {
65
- return locale;
66
- }
67
- }
68
- return options.locale;
41
+ return resolveI18nMessageProvenance(service, key, options.locale, options.namespace)?.locale ?? options.locale;
69
42
  }
70
43
  function normalizeMessageFormatError(error, key) {
71
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
  }
@@ -20,13 +20,7 @@ export interface I18nLoader {
20
20
  */
21
21
  load(locale: I18nLocale, namespace: I18nTranslationKey, options?: I18nLoaderLoadOptions): Promise<I18nMessageTree>;
22
22
  }
23
- /**
24
- * Checks whether a value is a plain object suitable for loader option or catalog validation.
25
- *
26
- * @param value Candidate value to inspect.
27
- * @returns `true` when the value is a plain object or null-prototype object.
28
- */
29
- export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
23
+ export { isPlainObject } from '../catalog.js';
30
24
  /**
31
25
  * Validates a loader locale before a backend is called.
32
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/loaders/shared.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKnF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACpH;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO9E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,CAIjG;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAelH;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA0BvF"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/loaders/shared.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKnF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACpH;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,UAAU,CAIjG;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAelH;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAEvF"}
@@ -1,4 +1,5 @@
1
1
  import { I18nError } from '../errors.js';
2
+ import { snapshotMessageTree } from '../catalog.js';
2
3
  const SAFE_LOCALE_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?(?:-[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?)*$/;
3
4
  const SAFE_NAMESPACE_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?$/;
4
5
 
@@ -10,19 +11,7 @@ const SAFE_NAMESPACE_SEGMENT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9]
10
11
  * Loader contract for asynchronous locale and namespace catalog sources.
11
12
  */
12
13
 
13
- /**
14
- * Checks whether a value is a plain object suitable for loader option or catalog validation.
15
- *
16
- * @param value Candidate value to inspect.
17
- * @returns `true` when the value is a plain object or null-prototype object.
18
- */
19
- export function isPlainObject(value) {
20
- if (typeof value !== 'object' || value === null) {
21
- return false;
22
- }
23
- const prototype = Object.getPrototypeOf(value);
24
- return prototype === Object.prototype || prototype === null;
25
- }
14
+ export { isPlainObject } from '../catalog.js';
26
15
 
27
16
  /**
28
17
  * Validates a loader locale before a backend is called.
@@ -67,23 +56,5 @@ export function validateLoaderNamespace(namespace, label) {
67
56
  * @returns A frozen message tree detached from caller-owned data.
68
57
  */
69
58
  export function snapshotLoaderMessageTree(value, path) {
70
- if (!isPlainObject(value)) {
71
- throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
72
- }
73
- const snapshot = {};
74
- for (const [key, entry] of Object.entries(value)) {
75
- if (key.trim() === '') {
76
- throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
77
- }
78
- if (typeof entry === 'string') {
79
- snapshot[key] = entry;
80
- continue;
81
- }
82
- if (isPlainObject(entry)) {
83
- snapshot[key] = snapshotLoaderMessageTree(entry, `${path}.${key}`);
84
- continue;
85
- }
86
- throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
87
- }
88
- return Object.freeze(snapshot);
59
+ return snapshotMessageTree(value, path);
89
60
  }
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgF,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAwMlI;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB,CAU5F"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgF,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmKlI;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB,CAU5F"}
package/dist/options.js CHANGED
@@ -1,11 +1,5 @@
1
1
  import { I18nError } from './errors.js';
2
- function isPlainObject(value) {
3
- if (typeof value !== 'object' || value === null) {
4
- return false;
5
- }
6
- const prototype = Object.getPrototypeOf(value);
7
- return prototype === Object.prototype || prototype === null;
8
- }
2
+ import { isPlainObject, snapshotMessageTree } from './catalog.js';
9
3
  function assertLocale(value, label) {
10
4
  if (typeof value !== 'string' || value.trim() === '') {
11
5
  throw new I18nError(`${label} must be a non-empty string.`, 'I18N_INVALID_LOCALE_CONFIG');
@@ -62,27 +56,6 @@ function snapshotFormats(formats) {
62
56
  relativeTime: snapshotNamedIntlFormats(formats.relativeTime, 'formats.relativeTime')
63
57
  });
64
58
  }
65
- function snapshotMessageTree(value, path) {
66
- if (!isPlainObject(value)) {
67
- throw new I18nError(`${path} must be a plain object message tree.`, 'I18N_INVALID_CATALOG');
68
- }
69
- const snapshot = {};
70
- for (const [key, entry] of Object.entries(value)) {
71
- if (key.trim() === '') {
72
- throw new I18nError(`${path} contains an empty message key segment.`, 'I18N_INVALID_CATALOG');
73
- }
74
- if (typeof entry === 'string') {
75
- snapshot[key] = entry;
76
- continue;
77
- }
78
- if (isPlainObject(entry)) {
79
- snapshot[key] = snapshotMessageTree(entry, `${path}.${key}`);
80
- continue;
81
- }
82
- throw new I18nError(`${path}.${key} must be a string or nested message tree.`, 'I18N_INVALID_CATALOG');
83
- }
84
- return Object.freeze(snapshot);
85
- }
86
59
  function snapshotCatalogs(catalogs, supportedLocales) {
87
60
  if (catalogs === undefined) {
88
61
  return undefined;
@@ -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":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACrB,UAAU,EAEV,iBAAiB,EACjB,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,EACrB,MAAM,YAAY,CAAC;AA+IpB;;;;;;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,15 +1,8 @@
1
+ import { isPlainObject } from './catalog.js';
2
+ import { resolveMessageProvenance } from './message-provenance.js';
1
3
  import { snapshotI18nModuleOptions } from './options.js';
2
4
  import { I18nError } from './errors.js';
3
- function hasOwn(value, key) {
4
- return typeof value === 'object' && value !== null && Object.hasOwn(value, key);
5
- }
6
- function isPlainObject(value) {
7
- if (typeof value !== 'object' || value === null) {
8
- return false;
9
- }
10
- const prototype = Object.getPrototypeOf(value);
11
- return prototype === Object.prototype || prototype === null;
12
- }
5
+ const serviceOptions = new WeakMap();
13
6
  function normalizeTranslationKey(key, namespace) {
14
7
  if (typeof key !== 'string') {
15
8
  throw new I18nError('Translation key must be a string.', 'I18N_INVALID_OPTIONS');
@@ -86,23 +79,6 @@ function interpolate(message, values) {
86
79
  return value === undefined || value === null ? '' : String(value);
87
80
  });
88
81
  }
89
- function resolveMessage(tree, key) {
90
- if (tree === undefined) {
91
- return undefined;
92
- }
93
- if (hasOwn(tree, key)) {
94
- const direct = tree[key];
95
- return typeof direct === 'string' ? direct : undefined;
96
- }
97
- let current = tree;
98
- for (const part of key.split('.')) {
99
- if (!hasOwn(current, part)) {
100
- return undefined;
101
- }
102
- current = current[part];
103
- }
104
- return typeof current === 'string' ? current : undefined;
105
- }
106
82
  function pushUnique(locales, locale) {
107
83
  if (locale !== undefined && !locales.includes(locale)) {
108
84
  locales.push(locale);
@@ -129,6 +105,7 @@ export class I18nService {
129
105
  */
130
106
  constructor(options = {}) {
131
107
  this.options = snapshotI18nModuleOptions(options);
108
+ serviceOptions.set(this, this.options);
132
109
  }
133
110
 
134
111
  /**
@@ -315,11 +292,9 @@ export class I18nService {
315
292
  assertDefaultValue(options.defaultValue);
316
293
  const resolvedKey = normalizeTranslationKey(key, options.namespace);
317
294
  const locales = this.resolveLocales(options.locale);
318
- for (const locale of locales) {
319
- const message = resolveMessage(this.options.catalogs?.[locale], resolvedKey);
320
- if (message !== undefined) {
321
- return interpolate(message, options.values);
322
- }
295
+ const message = resolveMessageProvenance(this.options.catalogs, locales, resolvedKey);
296
+ if (message !== undefined) {
297
+ return interpolate(message.message, options.values);
323
298
  }
324
299
  if (options.defaultValue !== undefined) {
325
300
  return interpolate(options.defaultValue, options.values);
@@ -336,6 +311,14 @@ export class I18nService {
336
311
  throw new I18nError(`Missing i18n message: ${resolvedKey}`, 'I18N_MISSING_MESSAGE');
337
312
  }
338
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
+ }
339
322
 
340
323
  /**
341
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": "1.0.3",
11
+ "version": "3.0.0",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -16,9 +16,6 @@
16
16
  "url": "https://github.com/fluojs/fluo.git",
17
17
  "directory": "packages/i18n"
18
18
  },
19
- "engines": {
20
- "node": ">=20.0.0"
21
- },
22
19
  "publishConfig": {
23
20
  "access": "public"
24
21
  },
@@ -63,12 +60,12 @@
63
60
  "dist"
64
61
  ],
65
62
  "dependencies": {
66
- "@fluojs/core": "^1.0.3"
63
+ "@fluojs/core": "^2.0.0"
67
64
  },
68
65
  "peerDependencies": {
69
66
  "intl-messageformat": "^11.2.4",
70
- "@fluojs/http": "^1.1.0",
71
- "@fluojs/validation": "^1.0.4"
67
+ "@fluojs/http": "^3.0.0",
68
+ "@fluojs/validation": "^2.0.0"
72
69
  },
73
70
  "peerDependenciesMeta": {
74
71
  "@fluojs/http": {
@@ -83,9 +80,10 @@
83
80
  },
84
81
  "devDependencies": {
85
82
  "intl-messageformat": "^11.2.4",
86
- "vitest": "^3.2.4",
87
- "@fluojs/http": "^1.1.0",
88
- "@fluojs/validation": "^1.0.4"
83
+ "vitest": "^4.1.11",
84
+ "@fluojs/testing": "^3.0.0",
85
+ "@fluojs/http": "^3.0.0",
86
+ "@fluojs/validation": "^2.0.0"
89
87
  },
90
88
  "scripts": {
91
89
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",