@fluojs/i18n 1.0.0-beta.1

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +537 -0
  3. package/README.md +537 -0
  4. package/dist/adapters.d.ts +180 -0
  5. package/dist/adapters.d.ts.map +1 -0
  6. package/dist/adapters.js +266 -0
  7. package/dist/errors.d.ts +17 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +19 -0
  10. package/dist/http.d.ts +120 -0
  11. package/dist/http.d.ts.map +1 -0
  12. package/dist/http.js +179 -0
  13. package/dist/icu.d.ts +59 -0
  14. package/dist/icu.d.ts.map +1 -0
  15. package/dist/icu.js +142 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/loaders/fs.d.ts +43 -0
  20. package/dist/loaders/fs.d.ts.map +1 -0
  21. package/dist/loaders/fs.js +79 -0
  22. package/dist/loaders/remote.d.ts +146 -0
  23. package/dist/loaders/remote.d.ts.map +1 -0
  24. package/dist/loaders/remote.js +268 -0
  25. package/dist/loaders/shared.d.ts +54 -0
  26. package/dist/loaders/shared.d.ts.map +1 -0
  27. package/dist/loaders/shared.js +89 -0
  28. package/dist/locale-resolution.d.ts +86 -0
  29. package/dist/locale-resolution.d.ts.map +1 -0
  30. package/dist/locale-resolution.js +201 -0
  31. package/dist/module.d.ts +22 -0
  32. package/dist/module.d.ts.map +1 -0
  33. package/dist/module.js +60 -0
  34. package/dist/options.d.ts +9 -0
  35. package/dist/options.d.ts.map +1 -0
  36. package/dist/options.js +169 -0
  37. package/dist/service.d.ts +104 -0
  38. package/dist/service.d.ts.map +1 -0
  39. package/dist/service.js +348 -0
  40. package/dist/typegen.d.ts +60 -0
  41. package/dist/typegen.d.ts.map +1 -0
  42. package/dist/typegen.js +215 -0
  43. package/dist/types.d.ts +154 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +1 -0
  46. package/dist/validation.d.ts +74 -0
  47. package/dist/validation.d.ts.map +1 -0
  48. package/dist/validation.js +123 -0
  49. package/package.json +97 -0
package/README.md ADDED
@@ -0,0 +1,537 @@
1
+ # @fluojs/i18n
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Framework-agnostic internationalization core surface for fluo applications.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Core Translation](#core-translation)
13
+ - [Formatting](#formatting)
14
+ - [ICU MessageFormat](#icu-messageformat)
15
+ - [HTTP Locale Context Adapter](#http-locale-context-adapter)
16
+ - [Non-HTTP Locale Adapters](#non-http-locale-adapters)
17
+ - [Validation Error Localization](#validation-error-localization)
18
+ - [Node Filesystem Loader](#node-filesystem-loader)
19
+ - [Remote Catalog Loader](#remote-catalog-loader)
20
+ - [Catalog Type Generation](#catalog-type-generation)
21
+ - [Public API](#public-api)
22
+ - [Ecosystem Bridge Evaluation](#ecosystem-bridge-evaluation)
23
+ - [Post-MVP Roadmap](#post-mvp-roadmap)
24
+ - [Related Packages](#related-packages)
25
+ - [Example Sources](#example-sources)
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @fluojs/i18n
31
+ ```
32
+
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
+
35
+ ## When to Use
36
+
37
+ Use this package when you need a stable fluo-native package boundary for i18n work:
38
+
39
+ - application-level module registration through `I18nModule.forRoot(...)`
40
+ - a framework-agnostic `I18nService` for explicit-locale translation lookup
41
+ - a standalone `createI18n(...)` entry point for non-module usage
42
+ - locale-scoped message catalogs, deterministic fallback resolution, interpolation, and missing-message hooks
43
+ - optional ICU MessageFormat plural/select formatting through `@fluojs/i18n/icu`
44
+ - standard `Intl` formatting helpers with explicit locales
45
+ - explicit HTTP `RequestContext` locale helpers through `@fluojs/i18n/http`
46
+ - opt-in non-HTTP locale adapters for WebSocket, gRPC, CLI, local storage, and server-request abstractions through `@fluojs/i18n/adapters`
47
+ - opt-in `@fluojs/validation` issue localization through `@fluojs/i18n/validation`
48
+ - provider-backed remote catalog loading and opt-in cache wrappers through `@fluojs/i18n/loaders/remote`
49
+ - opt-in catalog key declaration generation and typed translation helper declarations through `@fluojs/i18n/typegen`
50
+ - shared option, catalog, locale, translation-key, and error types
51
+
52
+ `@fluojs/i18n` is intentionally not coupled to NestJS i18n, i18next, or next-intl. Its root entry point provides a standard-first alternative that stays close to the TC39 `Intl` baseline, while ICU MessageFormat support is isolated behind the dedicated `@fluojs/i18n/icu` subpath.
53
+
54
+ ## Quick Start
55
+
56
+ ```ts
57
+ import { Module } from '@fluojs/core';
58
+ import { I18nModule } from '@fluojs/i18n';
59
+
60
+ @Module({
61
+ imports: [
62
+ I18nModule.forRoot({
63
+ defaultLocale: 'en',
64
+ supportedLocales: ['en', 'ko'],
65
+ }),
66
+ ],
67
+ })
68
+ class AppModule {}
69
+ ```
70
+
71
+ ## Core Translation
72
+
73
+ The `I18nService` provides deterministic translation lookup.
74
+
75
+ ```ts
76
+ import { createI18n } from '@fluojs/i18n';
77
+
78
+ const i18n = createI18n({
79
+ defaultLocale: 'en',
80
+ supportedLocales: ['en', 'ko'],
81
+ fallbackLocales: { ko: ['en'] },
82
+ catalogs: {
83
+ en: { app: { title: 'Hello {{ name }}' } },
84
+ ko: { app: { title: '안녕하세요 {{ name }}' } },
85
+ },
86
+ });
87
+
88
+ // Translation with interpolation
89
+ const title = i18n.translate('app.title', {
90
+ locale: 'ko',
91
+ values: { name: 'fluo' },
92
+ });
93
+ ```
94
+
95
+ ### Fallback Behavior
96
+
97
+ Translation lookup follows a strict order:
98
+
99
+ 1. The explicit per-call locale.
100
+ 2. The configured fallback for that locale (either from the `fallbackLocales` map or a global fallback array).
101
+ 3. The configured `defaultLocale`.
102
+ 4. The per-call `defaultValue`.
103
+ 5. The configured `missingMessage` hook.
104
+
105
+ If no message is found, an `I18nError` is thrown with code `I18N_MISSING_MESSAGE`.
106
+
107
+ ## Formatting
108
+
109
+ Formatting helpers delegate directly to the host standard `Intl` implementation. Locale is explicit on every formatting call, and named formatter options are captured as immutable service-owned snapshots.
110
+
111
+ ```ts
112
+ import { createI18n } from '@fluojs/i18n';
113
+
114
+ const i18n = createI18n({
115
+ defaultLocale: 'en-US',
116
+ formats: {
117
+ dateTime: {
118
+ invoice: { dateStyle: 'medium', timeZone: 'UTC' },
119
+ },
120
+ },
121
+ });
122
+
123
+ i18n.formatDateTime(new Date(), {
124
+ format: 'invoice',
125
+ locale: 'en-US',
126
+ });
127
+
128
+ i18n.formatCurrency(12900, {
129
+ currency: 'KRW',
130
+ locale: 'ko-KR',
131
+ });
132
+ ```
133
+
134
+ ## ICU MessageFormat
135
+
136
+ ICU MessageFormat support lives under `@fluojs/i18n/icu` so the root `@fluojs/i18n` entry point keeps its framework-agnostic simple interpolation contract. The ICU service first resolves messages through the core `I18nService`, preserving locale fallback, per-call `defaultValue`, missing-message hooks, and `{{ name }}` interpolation for compatible primitive values. It then formats the resolved message with ICU plural, select, and nested MessageFormat rules.
137
+
138
+ ```ts
139
+ import { createIcuI18n } from '@fluojs/i18n/icu';
140
+
141
+ const i18n = createIcuI18n({
142
+ defaultLocale: 'en',
143
+ supportedLocales: ['en', 'ko'],
144
+ fallbackLocales: { ko: ['en'] },
145
+ catalogs: {
146
+ en: {
147
+ inbox: 'Hello {{ name }}. {count, plural, =0 {No messages} one {One message} other {# messages}}.',
148
+ invite:
149
+ '{gender, select, female {{host} invited {count, plural, one {one guest} other {# guests}}} other {{host} invited {count, plural, one {one guest} other {# guests}}}}',
150
+ },
151
+ },
152
+ });
153
+
154
+ i18n.translate('inbox', {
155
+ locale: 'ko',
156
+ values: { count: 3, name: 'Mina' },
157
+ });
158
+ // "Hello Mina. 3 messages."
159
+ ```
160
+
161
+ Invalid ICU patterns, missing ICU values, and non-string rich formatting results are reported as `I18nError` with code `I18N_INVALID_MESSAGE_FORMAT`. The subpath relies on the host `Intl.NumberFormat`, `Intl.DateTimeFormat`, and `Intl.PluralRules` implementations used by `intl-messageformat`.
162
+
163
+ ## HTTP Locale Context Adapter
164
+
165
+ HTTP request locale helpers live only under the `@fluojs/i18n/http` subpath so the root `@fluojs/i18n` entry point remains framework-agnostic and does not import `@fluojs/http`.
166
+
167
+ ```ts
168
+ import { createI18n } from '@fluojs/i18n';
169
+ import { createAcceptLanguageLocaleResolver, getHttpLocale, resolveHttpLocale } from '@fluojs/i18n/http';
170
+ import type { RequestContext } from '@fluojs/http';
171
+
172
+ const i18n = createI18n({
173
+ defaultLocale: 'en',
174
+ supportedLocales: ['en', 'ko'],
175
+ catalogs: {
176
+ en: { app: { title: 'Welcome' } },
177
+ },
178
+ });
179
+
180
+ const acceptLanguage = createAcceptLanguageLocaleResolver();
181
+
182
+ async function bindRequestLocale(ctx: RequestContext) {
183
+ return resolveHttpLocale(ctx, {
184
+ defaultLocale: 'en',
185
+ supportedLocales: ['en', 'ko'],
186
+ resolvers: [acceptLanguage],
187
+ });
188
+ }
189
+
190
+ function handler(ctx: RequestContext) {
191
+ const locale = getHttpLocale(ctx)?.locale ?? 'en';
192
+ // Use the service with the resolved locale
193
+ return i18n.translate('app.title', { locale, defaultValue: 'Welcome' });
194
+ }
195
+ ```
196
+
197
+ The adapter is intentionally explicit:
198
+
199
+ - `setHttpLocale(ctx, locale, metadata)` stores locale metadata on the current `RequestContext` using `createContextKey(...)`.
200
+ - `getHttpLocale(ctx)` reads the metadata without falling back to globals.
201
+ - `parseAcceptLanguage(header)` parses valid `Accept-Language` ranges by q-value and ignores invalid or q=0 entries.
202
+ - `createAcceptLanguageLocaleResolver(...)` selects the first supported locale from the request header.
203
+ - `createAcceptLanguageLocalePolicyResolver(...)` is opt-in and can normalize regional ranges such as `en-US` to supported `en` or select a wildcard fallback only after explicit supported ranges are exhausted.
204
+ - `resolveHttpLocale(ctx, options)` runs application-provided resolvers in order, ignores invalid or unsupported resolver output, and stores `defaultLocale` with source `default` when nothing matches.
205
+
206
+ Wildcard `*` ranges are parsed but do not automatically select a locale. Applications that want wildcard-specific behavior can add a resolver before or after the provided `Accept-Language` resolver.
207
+
208
+ For example, this resolver keeps explicit user ranges first, treats `*` as fallback-only, and only selects the first configured supported locale when no explicit range matches:
209
+
210
+ ```ts
211
+ const acceptLanguagePolicy = createAcceptLanguageLocalePolicyResolver({
212
+ wildcardLocale: 'firstSupportedLocale',
213
+ });
214
+ ```
215
+
216
+ ## Non-HTTP Locale Adapters
217
+
218
+ Non-HTTP locale helpers live under the `@fluojs/i18n/adapters` subpath. They provide resolver-order locale selection for WebSocket handshakes, gRPC metadata, CLI option objects, local storage wrappers, server sessions, and request-like abstractions without coupling the root package to browser globals, Node process state, or framework-specific transport packages.
219
+
220
+ ```ts
221
+ import {
222
+ bindLocale,
223
+ createHeaderLocaleResolver,
224
+ createQueryLocaleResolver,
225
+ createWeakMapLocaleStore,
226
+ getAdapterLocale,
227
+ } from '@fluojs/i18n/adapters';
228
+
229
+ interface SocketContext {
230
+ readonly handshake: {
231
+ readonly headers: Readonly<Record<string, string | undefined>>;
232
+ readonly query: Readonly<Record<string, string | undefined>>;
233
+ };
234
+ }
235
+
236
+ const socketLocales = createWeakMapLocaleStore<SocketContext>();
237
+
238
+ const queryLocale = createQueryLocaleResolver<SocketContext>({
239
+ getQueryValue: (socket) => socket.handshake.query.locale,
240
+ source: 'socket-query',
241
+ });
242
+ const headerLocale = createHeaderLocaleResolver<SocketContext>({
243
+ getHeader: (socket) => socket.handshake.headers['accept-language'],
244
+ source: 'socket-accept-language',
245
+ });
246
+
247
+ function bindSocketLocale(socket: SocketContext) {
248
+ return bindLocale(socket, {
249
+ defaultLocale: 'en',
250
+ supportedLocales: ['en', 'ko'],
251
+ resolvers: [queryLocale, headerLocale],
252
+ store: socketLocales,
253
+ });
254
+ }
255
+
256
+ function handleSocketMessage(socket: SocketContext) {
257
+ const locale = getAdapterLocale(socketLocales, socket)?.locale ?? 'en';
258
+ return locale;
259
+ }
260
+ ```
261
+
262
+ The generic adapter contract is intentionally explicit:
263
+
264
+ - `resolveLocale(context, options)` runs application-provided resolvers in order, ignores empty, invalid, and unsupported resolver output, and returns `defaultLocale` with source `default` when nothing matches.
265
+ - `bindLocale(context, { store, ...options })` resolves a locale and stores immutable metadata in an application-provided `LocaleAdapterStore`.
266
+ - `createWeakMapLocaleStore()` provides per-object metadata storage for socket, call, session, or request objects without mutating those objects.
267
+ - `createHeaderLocaleResolver(...)` parses `Accept-Language`-style values with the same q-value and wildcard behavior as the HTTP adapter.
268
+ - `createHeaderLocalePolicyResolver(...)` provides the same opt-in regional-locale normalization and wildcard fallback policy without importing HTTP types.
269
+ - `createQueryLocaleResolver(...)`, `createCookieLocaleResolver(...)`, and `createStorageLocaleResolver(...)` read locale candidates from caller-owned abstractions and never access browser globals or framework internals.
270
+
271
+ Applications choose the context shape and accessor functions. For example, a gRPC integration can read metadata through `getHeader`, a CLI integration can read a parsed `--locale` option through `getQueryValue` or `getStoredLocale`, and a browser application can pass a safe wrapper around `localStorage` through `getStoredLocale`.
272
+
273
+ ## Validation Error Localization
274
+
275
+ Validation issue localization lives under `@fluojs/i18n/validation` so the root `@fluojs/i18n` entry point stays framework-agnostic and does not change `@fluojs/validation` behavior by default. Applications opt in after validation fails by translating `ValidationIssue.message` snapshots explicitly.
276
+
277
+ ```ts
278
+ import { createI18n } from '@fluojs/i18n';
279
+ import { localizeDtoValidationError } from '@fluojs/i18n/validation';
280
+ import { DefaultValidator, DtoValidationError } from '@fluojs/validation';
281
+
282
+ const i18n = createI18n({
283
+ defaultLocale: 'en',
284
+ supportedLocales: ['en', 'ko'],
285
+ fallbackLocales: { ko: ['en'] },
286
+ catalogs: {
287
+ en: { validation: { email: { EMAIL: '{{ field }} must be a valid email address.' } } },
288
+ ko: { validation: { email: { EMAIL: '{{ field }}에는 올바른 이메일 주소가 필요합니다.' } } },
289
+ },
290
+ });
291
+
292
+ try {
293
+ await new DefaultValidator().materialize(input, CreateUserDto);
294
+ } catch (error) {
295
+ if (error instanceof DtoValidationError) {
296
+ throw localizeDtoValidationError(i18n, error, { locale: 'ko' });
297
+ }
298
+ throw error;
299
+ }
300
+ ```
301
+
302
+ The default key candidates are most-specific to least-specific: `source.field.code`, `field.code`, `source.code`, then `code`. The default namespace is `validation`, and callers can provide `keyPrefix`, `namespace`, or a custom `keyBuilder` to match their catalog layout. Translation values include `code`, `field`, `source`, and the original `message`. Missing translations preserve the original validation message unless `fallbackToIssueMessage: false` is set, in which case an `I18nError` with code `I18N_MISSING_MESSAGE` is thrown.
303
+
304
+ This integration is intentionally not an HTTP adapter. Request locale resolution can happen through `@fluojs/i18n/http`, CLI configuration, WebSocket session state, or any other application boundary, then the chosen locale is passed explicitly to the validation localization helper.
305
+
306
+ ## Node Filesystem Loader
307
+
308
+ Node applications can opt into a JSON filesystem loader from the dedicated subpath:
309
+
310
+ ```ts
311
+ import { createFileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';
312
+
313
+ const loader = createFileSystemI18nLoader({
314
+ rootDir: new URL('./locales', import.meta.url).pathname,
315
+ });
316
+
317
+ const common = await loader.load('en', 'common');
318
+ ```
319
+
320
+ The loader reads `${rootDir}/${locale}/${namespace}.json` and returns an immutable `I18nMessageTree`. Namespaces may use safe relative path segments such as `admin/common`; locale and namespace values are validated before disk reads, `.`/`..`, absolute paths, empty segments, extension-bearing names such as `common.json`, and traversal attempts are rejected with `I18N_INVALID_LOADER_OPTIONS`. Missing files throw `I18N_MISSING_CATALOG`; malformed JSON or invalid message tree shapes throw `I18N_INVALID_CATALOG`.
321
+
322
+ This subpath imports Node built-ins and is not exported from `@fluojs/i18n` root. Do not import it in Bun, Deno, Cloudflare Workers, browser, or other non-Node runtime-portable bundles unless your bundler explicitly targets Node.js.
323
+
324
+ ## Remote Catalog Loader
325
+
326
+ Remote catalog loading lives under a dedicated provider-backed subpath so applications can connect HTTP APIs, object stores, databases, or other asynchronous catalog sources without adding runtime-specific dependencies to the root entry point:
327
+
328
+ ```ts
329
+ import { createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
330
+
331
+ const loader = createRemoteI18nLoader({
332
+ timeoutMs: 5_000,
333
+ provider: async ({ locale, namespace, signal }) => {
334
+ const response = await fetch(`https://catalog.example/${locale}/${namespace}.json`, { signal });
335
+ if (response.status === 404) {
336
+ return undefined;
337
+ }
338
+ return response.text();
339
+ },
340
+ });
341
+
342
+ const common = await loader.load('en', 'common');
343
+ ```
344
+
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.
346
+
347
+ 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
+
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:
350
+
351
+ ```ts
352
+ import { createCachedRemoteI18nLoader, createRemoteI18nLoader } from '@fluojs/i18n/loaders/remote';
353
+
354
+ const uncachedLoader = createRemoteI18nLoader({ provider: fetchCatalog });
355
+ const cachedLoader = createCachedRemoteI18nLoader({
356
+ loader: uncachedLoader,
357
+ ttlMs: 60_000,
358
+ version: 'catalog-2026-05-11',
359
+ });
360
+
361
+ cachedLoader.invalidate('en', 'common');
362
+ ```
363
+
364
+ Like the filesystem loader, locale and namespace values are validated before the provider is called. Namespaces may use safe relative path segments such as `admin/common`; `.`/`..`, absolute paths, empty segments, extension-bearing names such as `common.json`, and traversal attempts are rejected with `I18N_INVALID_LOADER_OPTIONS`.
365
+
366
+ ## Catalog Type Generation
367
+
368
+ Catalog type generation lives under the Node-oriented `@fluojs/i18n/typegen` tooling subpath. It does not narrow `I18nService.translate(key: string, ...)`; applications can opt into generated helper types where they want type-safe translation key variables or typed translation facades.
369
+
370
+ ```ts
371
+ import { generateI18nCatalogTypesFromDirectory } from '@fluojs/i18n/typegen';
372
+
373
+ const declarations = await generateI18nCatalogTypesFromDirectory({
374
+ rootDir: new URL('./locales', import.meta.url).pathname,
375
+ });
376
+ ```
377
+
378
+ The directory helper scans `${rootDir}/${locale}/**/*.json`, validates each JSON file as an `I18nMessageTree`, and emits deterministic TypeScript declaration text. Filesystem namespace paths are preserved the same way loaders receive them: `locales/en/admin/common.json` contributes namespace `admin/common`, and nested leaves become fully qualified keys such as `admin/common.dashboard.title`. This matches `I18nService.translate('dashboard.title', { namespace: 'admin/common', ... })`, which prefixes the namespace exactly before lookup.
379
+
380
+ For custom pipelines or remote catalogs, generate from in-memory message trees:
381
+
382
+ ```ts
383
+ import { generateI18nCatalogTypes } from '@fluojs/i18n/typegen';
384
+
385
+ const declarations = generateI18nCatalogTypes([
386
+ {
387
+ locale: 'en',
388
+ namespace: 'admin/common',
389
+ messages: {
390
+ dashboard: {
391
+ title: 'Dashboard',
392
+ },
393
+ },
394
+ },
395
+ ]);
396
+ ```
397
+
398
+ The generated declaration text includes fully qualified key unions, namespace unions, namespace-to-leaf-key maps, and opt-in typed facade types. For example, `admin/common.dashboard.title` is available as a fully qualified key, while the same message can be represented as namespace `admin/common` plus leaf key `dashboard.title` through `I18nCatalogNamespaceKey<"admin/common">`.
399
+
400
+ ```ts
401
+ import type { I18nCatalogTypedService } from './generated-i18n-catalog.d.ts';
402
+
403
+ const typedI18n = {
404
+ translate: i18n.translate.bind(i18n),
405
+ translateInNamespace: (namespace, key, options) => i18n.translate(key, { ...options, namespace }),
406
+ } satisfies I18nCatalogTypedService;
407
+
408
+ typedI18n.translate('admin/common.dashboard.title', { locale: 'en' });
409
+ typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en' });
410
+ ```
411
+
412
+ 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
+
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`.
415
+
416
+ ## Public API
417
+
418
+ ### Core (@fluojs/i18n)
419
+
420
+ | Export | Description |
421
+ |---|---|
422
+ | `I18nModule` | Module facade for registering the core i18n service surface. |
423
+ | `I18nService` | Core service that owns detached options/catalog snapshots and resolves translations. |
424
+ | `createI18n(options)` | Creates a standalone `I18nService` without module registration. |
425
+ | `I18nError` | Base i18n package error with a stable error code. |
426
+
427
+ **Types:** `I18nModuleOptions`, `I18nMessageCatalogs`, `I18nMessageTree`, `I18nTranslateOptions`, `I18nInterpolationValues`, `I18nMissingMessageHandler`, `I18nMissingMessageContext`, `I18nLocale`, `I18nTranslationKey`, `I18nErrorCode`, `I18nFallbackLocales`, `I18nFormatOptions`, `I18nFormatterOptions`, `I18nDateTimeFormatOptions`, `I18nNumberFormatOptions`, `I18nCurrencyFormatOptions`, `I18nListFormatOptions`, `I18nRelativeTimeFormatOptions`, `I18nNamedDateTimeFormats`, `I18nNamedNumberFormats`, `I18nNamedListFormats`, `I18nNamedRelativeTimeFormats`.
428
+
429
+ ### HTTP Adapter (@fluojs/i18n/http)
430
+
431
+ | Export | Description |
432
+ |---|---|
433
+ | `resolveHttpLocale` | Resolves and stores locale metadata on the `RequestContext`. |
434
+ | `getHttpLocale` | Retrieves locale metadata from the `RequestContext`. |
435
+ | `setHttpLocale` | Manually stores locale metadata on the `RequestContext`. |
436
+ | `createAcceptLanguageLocaleResolver` | Creates a resolver for the `Accept-Language` header. |
437
+ | `createAcceptLanguageLocalePolicyResolver` | Creates an opt-in `Accept-Language` policy resolver for regional normalization and wildcard fallback handling. |
438
+ | `parseAcceptLanguage` | Utility to parse `Accept-Language` header into q-value preferences. |
439
+ | `HTTP_LOCALE_CONTEXT_KEY` | Context key used to store locale metadata on `RequestContext`. |
440
+
441
+ **Types:** `HttpLocaleContext`, `HttpLocaleResolver`, `HttpLocaleResolverInput`, `HttpLocaleResolverResult`, `ResolveHttpLocaleOptions`, `AcceptLanguageLocaleResolverOptions`, `AcceptLanguageLocalePolicyResolverOptions`, `AcceptLanguagePreference`.
442
+
443
+ ### Non-HTTP Adapters (@fluojs/i18n/adapters)
444
+
445
+ | Export | Description |
446
+ |---|---|
447
+ | `resolveLocale` | Resolves locale metadata from an explicit non-HTTP resolver chain. |
448
+ | `bindLocale` | Resolves and stores locale metadata in a caller-provided adapter store. |
449
+ | `setAdapterLocale` | Manually stores locale metadata in a caller-provided adapter store. |
450
+ | `getAdapterLocale` | Retrieves locale metadata from a caller-provided adapter store. |
451
+ | `createWeakMapLocaleStore` | Creates per-object metadata storage without mutating transport contexts. |
452
+ | `createHeaderLocaleResolver` | Creates an `Accept-Language`-style resolver for caller-owned header abstractions. |
453
+ | `createHeaderLocalePolicyResolver` | Creates an opt-in header policy resolver for regional normalization and wildcard fallback handling. |
454
+ | `createQueryLocaleResolver` | Creates a resolver for query, CLI option, or request parameter abstractions. |
455
+ | `createCookieLocaleResolver` | Creates a resolver for caller-owned cookie abstractions. |
456
+ | `createStorageLocaleResolver` | Creates a resolver for local storage, server session, socket data, or CLI config abstractions. |
457
+
458
+ **Types:** `LocaleAdapterContext`, `LocaleAdapterResolver`, `LocaleAdapterResolverInput`, `LocaleAdapterResolverResult`, `LocaleAdapterStore`, `ResolveLocaleOptions`, `BindLocaleOptions`, `HeaderLocaleResolverOptions`, `HeaderLocalePolicyResolverOptions`, `QueryLocaleResolverOptions`, `CookieLocaleResolverOptions`, `StorageLocaleResolverOptions`.
459
+
460
+ ### Validation Integration (@fluojs/i18n/validation)
461
+
462
+ | Export | Description |
463
+ |---|---|
464
+ | `createValidationIssueTranslationKeys(issue, keyPrefix?)` | Builds default translation key candidates from validation issue source, field path, and code. |
465
+ | `localizeValidationIssue(i18n, issue, options, index?)` | Returns a validation issue snapshot with a localized message when a candidate key resolves. |
466
+ | `localizeValidationIssues(i18n, issues, options)` | Localizes an issue list without mutating the original issues. |
467
+ | `localizeDtoValidationError(i18n, error, options)` | Creates a new `DtoValidationError` with localized issue messages. |
468
+
469
+ **Types:** `LocalizeValidationIssuesOptions`, `ValidationIssueTranslationKeyBuilder`, `ValidationIssueTranslationKeyContext`.
470
+
471
+ ### ICU MessageFormat (@fluojs/i18n/icu)
472
+
473
+ | Export | Description |
474
+ |---|---|
475
+ | `createIcuI18n(options)` | Creates a standalone ICU MessageFormat service while preserving core lookup semantics. |
476
+ | `IcuI18nService` | Service that resolves messages through `I18nService` before ICU formatting. |
477
+
478
+ **Types:** `I18nIcuTranslateOptions`, `I18nIcuValue`, `I18nIcuValues`.
479
+
480
+ ### Filesystem Loader (@fluojs/i18n/loaders/fs)
481
+
482
+ | Export | Description |
483
+ |---|---|
484
+ | `createFileSystemI18nLoader` | Creates a Node.js JSON filesystem loader. |
485
+ | `FileSystemI18nLoader` | Class implementation of the filesystem loader. |
486
+
487
+ **Types:** `I18nLoader`, `I18nLoaderLoadOptions`, `FileSystemI18nLoaderOptions`.
488
+
489
+ ### Remote Loader (@fluojs/i18n/loaders/remote)
490
+
491
+ | Export | Description |
492
+ |---|---|
493
+ | `createRemoteI18nLoader` | Creates a provider-backed remote catalog loader. |
494
+ | `RemoteI18nLoader` | Class implementation of the remote catalog loader. |
495
+ | `createCachedRemoteI18nLoader` | Creates an opt-in in-memory cache wrapper around a remote catalog loader. |
496
+ | `CachedRemoteI18nLoader` | Cache wrapper implementation with explicit `invalidate(...)` and `clear()` controls. |
497
+
498
+ **Types:** `I18nLoader`, `I18nLoaderLoadOptions`, `RemoteI18nCatalogProvider`, `RemoteI18nCatalogRequest`, `RemoteI18nLoaderOptions`, `CachedI18nLoader`, `CachedI18nLoaderKeyInput`, `CachedI18nLoaderOptions`.
499
+
500
+ ### Catalog Type Generation (@fluojs/i18n/typegen)
501
+
502
+ | Export | Description |
503
+ |---|---|
504
+ | `generateI18nCatalogTypes(inputs, options?)` | Generates deterministic TypeScript key declarations from in-memory catalog trees. |
505
+ | `generateI18nCatalogTypesFromDirectory(options)` | Reads locale/namespace JSON catalogs from disk and generates key declarations. |
506
+
507
+ **Types:** `I18nCatalogTypegenInput`, `I18nCatalogTypegenOptions`, `I18nCatalogTypegenDirectoryOptions`. Generated declaration defaults include `I18nCatalogKey`, `I18nCatalogNamespace`, `I18nCatalogKeyByNamespace`, `I18nCatalogNamespaceKey`, `I18nCatalogTypedTranslateOptions`, `I18nCatalogTypedTranslate`, and `I18nCatalogTypedService`.
508
+
509
+ ## Ecosystem Bridge Evaluation
510
+
511
+ The current bridge decision is documentation-first: NestJS i18n parity, i18next interop, next-intl catalog sharing, and request-locale/validation convenience glue should be handled through migration guidance and existing opt-in subpaths before adding runtime helpers. See [i18n ecosystem bridge decision record](../../docs/reference/i18n-ecosystem-bridges.md) for the classification matrix and the acceptance criteria required before any future bridge helper can become a first-party subpath.
512
+
513
+ This preserves the root package guarantee that `@fluojs/i18n` is not coupled to NestJS i18n, i18next, next-intl, React/Next.js runtime assumptions, or HTTP-only validation localization.
514
+
515
+ ## Post-MVP Roadmap
516
+
517
+ The core locale-resolution roadmap item for WebSocket, gRPC, CLI, local storage, and request-style abstractions is now available through `@fluojs/i18n/adapters`. Future transport work should stay opt-in and subpath-scoped unless a dedicated framework package owns the integration.
518
+
519
+ ## Related Packages
520
+
521
+
522
+ - **`@fluojs/core`**: Provides module metadata and shared framework errors used by this package.
523
+ - **`@fluojs/config`**: The closest package layout model for module registration and option snapshotting conventions.
524
+ - **`@fluojs/validation`**: Provides the opt-in validation issue contract consumed by `@fluojs/i18n/validation`.
525
+
526
+ ## Example Sources
527
+
528
+ - `packages/i18n/src/module.ts`
529
+ - `packages/i18n/src/service.ts`
530
+ - `packages/i18n/src/icu.ts`
531
+ - `packages/i18n/src/loaders/fs.ts`
532
+ - `packages/i18n/src/http.ts`
533
+ - `packages/i18n/src/adapters.ts`
534
+ - `packages/i18n/src/validation.ts`
535
+ - `packages/i18n/src/index.test.ts`
536
+ - `packages/i18n/src/loaders/remote.ts`
537
+ - `packages/i18n/src/typegen.ts`