npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fluojs/i18n

v3.0.2

Published

Framework-agnostic internationalization core surface for fluo applications.

Downloads

381

Readme

@fluojs/i18n

Framework-agnostic internationalization core surface for fluo applications.

Table of Contents

Installation

npm install @fluojs/i18n

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.

When to Use

Use this package when you need a stable fluo-native package boundary for i18n work:

  • application-level module registration through I18nModule.forRoot(...)
  • a framework-agnostic I18nService for explicit-locale translation lookup
  • I18nService.create(...) for standalone non-module usage
  • locale-scoped message catalogs, deterministic fallback resolution, interpolation, and missing-message hooks
  • optional ICU MessageFormat plural/select formatting through @fluojs/i18n/icu
  • standard Intl formatting helpers with explicit locales
  • explicit HTTP RequestContext locale helpers through @fluojs/i18n/http
  • opt-in non-HTTP locale adapters for WebSocket, gRPC, CLI, local storage, and server-request abstractions through @fluojs/i18n/adapters
  • opt-in @fluojs/validation issue localization through @fluojs/i18n/validation
  • provider-backed remote catalog loading and opt-in cache wrappers through @fluojs/i18n/loaders/remote
  • opt-in catalog key declaration generation and typed translation helper declarations through @fluojs/i18n/typegen
  • shared option, catalog, locale, translation-key, and error types

@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.

Quick Start

import { Module } from '@fluojs/core';
import { I18nModule } from '@fluojs/i18n';

@Module({
  imports: [
    I18nModule.forRoot({
      defaultLocale: 'en',
      supportedLocales: ['en', 'ko'],
    }),
  ],
})
class AppModule {}

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.

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.

Catalog Aggregation and Fallback Migration

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:

import { Module } from '@fluojs/core';
import { I18nModule } from '@fluojs/i18n';
import { FileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';

const locales = ['en', 'ko'] as const;
const namespaces = ['common', 'validation'] as const;
const catalogLoader = FileSystemI18nLoader.create({
  rootDir: new URL('./locales', import.meta.url).pathname,
});

const catalogEntries = await Promise.all(
  locales.map(async (locale) => {
    const namespaceEntries = await Promise.all(
      namespaces.map(async (namespace) => [
        namespace,
        await catalogLoader.load(locale, namespace),
      ] as const),
    );

    return [locale, Object.fromEntries(namespaceEntries)] as const;
  }),
);
const catalogs = Object.fromEntries(catalogEntries);

@Module({
  imports: [
    I18nModule.forRoot({
      defaultLocale: 'en',
      supportedLocales: locales,
      fallbackLocales: { ko: ['en'] },
      catalogs,
    }),
  ],
})
class AppModule {}

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.

Convert NestJS i18n fallback intent explicitly:

// NestJS i18n
I18nModule.forRoot({
  fallbackLanguage: 'en',
  fallbacks: { ko: 'en' },
});

// fluo
I18nModule.forRoot({
  defaultLocale: 'en',
  fallbackLocales: { ko: ['en'] },
  catalogs,
});

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(...).

Core Translation

The I18nService provides deterministic translation lookup.

import { I18nService } from '@fluojs/i18n';

const i18n = I18nService.create({
  defaultLocale: 'en',
  supportedLocales: ['en', 'ko'],
  fallbackLocales: { ko: ['en'] },
  catalogs: {
    en: { app: { title: 'Hello {{ name }}' } },
    ko: { app: { title: '안녕하세요 {{ name }}' } },
  },
});

// Translation with interpolation
const title = i18n.translate('app.title', {
  locale: 'ko',
  values: { name: 'fluo' },
});

Fallback Behavior

Translation lookup follows a strict order:

  1. The explicit per-call locale.
  2. The configured fallback for that locale (either from the fallbackLocales map or a global fallback array).
  3. The configured defaultLocale.
  4. The per-call defaultValue.
  5. The configured missingMessage hook.

If no message is found, an I18nError is thrown with code I18N_MISSING_MESSAGE.

Formatting

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.

import { I18nService } from '@fluojs/i18n';

const i18n = I18nService.create({
  defaultLocale: 'en-US',
  formats: {
    dateTime: {
      invoice: { dateStyle: 'medium', timeZone: 'UTC' },
    },
  },
});

i18n.formatDateTime(new Date(), {
  format: 'invoice',
  locale: 'en-US',
});

i18n.formatCurrency(12900, {
  currency: 'KRW',
  locale: 'ko-KR',
});

ICU MessageFormat

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.

import { IcuI18nService } from '@fluojs/i18n/icu';

const i18n = IcuI18nService.create({
  defaultLocale: 'en',
  supportedLocales: ['en', 'ko'],
  fallbackLocales: { ko: ['en'] },
  catalogs: {
    en: {
      inbox: 'Hello {{ name }}. {count, plural, =0 {No messages} one {One message} other {# messages}}.',
      invite:
        '{gender, select, female {{host} invited {count, plural, one {one guest} other {# guests}}} other {{host} invited {count, plural, one {one guest} other {# guests}}}}',
    },
  },
});

i18n.translate('inbox', {
  locale: 'ko',
  values: { count: 3, name: 'Mina' },
});
// "Hello Mina. 3 messages."

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.

HTTP Locale Context Adapter

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.

import { I18nService } from '@fluojs/i18n';
import { createAcceptLanguageLocalePolicyResolver, getHttpLocale, resolveHttpLocale } from '@fluojs/i18n/http';
import type { RequestContext } from '@fluojs/http';

const i18n = I18nService.create({
  defaultLocale: 'en',
  supportedLocales: ['en', 'ko'],
  catalogs: {
    en: { app: { title: 'Welcome' } },
  },
});

const acceptLanguage = createAcceptLanguageLocalePolicyResolver({ normalizeToSupportedLocale: false });

async function bindRequestLocale(ctx: RequestContext) {
  return resolveHttpLocale(ctx, {
    defaultLocale: 'en',
    supportedLocales: ['en', 'ko'],
    resolvers: [acceptLanguage],
  });
}

function handler(ctx: RequestContext) {
  const locale = getHttpLocale(ctx)?.locale ?? 'en';
  // Use the service with the resolved locale
  return i18n.translate('app.title', { locale, defaultValue: 'Welcome' });
}

The adapter is intentionally explicit:

  • setHttpLocale(ctx, locale, metadata) stores locale metadata on the current RequestContext using createContextKey(...).
  • getHttpLocale(ctx) reads the metadata without falling back to globals.
  • parseAcceptLanguage(header) parses valid Accept-Language ranges by q-value and ignores invalid or q=0 entries.
  • createAcceptLanguageLocalePolicyResolver(...) selects a supported locale from Accept-Language headers with explicit normalization (normalizeToSupportedLocale, defaults to true) and optional wildcard policy (wildcardLocale).
  • resolveHttpLocale(ctx, options) runs application-provided resolvers in order, ignores invalid or unsupported resolver output, and stores defaultLocale with source default when nothing matches.

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.

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:

const acceptLanguagePolicy = createAcceptLanguageLocalePolicyResolver({
  wildcardLocale: 'firstSupportedLocale',
});

Non-HTTP Locale Adapters

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.

import {
  bindLocale,
  createHeaderLocalePolicyResolver,
  createQueryLocaleResolver,
  createWeakMapLocaleStore,
  getAdapterLocale,
} from '@fluojs/i18n/adapters';

interface SocketContext {
  readonly handshake: {
    readonly headers: Readonly<Record<string, string | undefined>>;
    readonly query: Readonly<Record<string, string | undefined>>;
  };
}

const socketLocales = createWeakMapLocaleStore<SocketContext>();

const queryLocale = createQueryLocaleResolver<SocketContext>({
  getQueryValue: (socket) => socket.handshake.query.locale,
  source: 'socket-query',
});
const headerLocale = createHeaderLocalePolicyResolver<SocketContext>({
  getHeader: (socket) => socket.handshake.headers['accept-language'],
  source: 'socket-accept-language',
});

function bindSocketLocale(socket: SocketContext) {
  return bindLocale(socket, {
    defaultLocale: 'en',
    supportedLocales: ['en', 'ko'],
    resolvers: [queryLocale, headerLocale],
    store: socketLocales,
  });
}

function handleSocketMessage(socket: SocketContext) {
  const locale = getAdapterLocale(socketLocales, socket)?.locale ?? 'en';
  return locale;
}

The generic adapter contract is intentionally explicit:

  • 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.
  • bindLocale(context, { store, ...options }) resolves a locale and stores immutable metadata in an application-provided LocaleAdapterStore.
  • createWeakMapLocaleStore() provides per-object metadata storage for socket, call, session, or request objects without mutating those objects.
  • createHeaderLocalePolicyResolver(...) provides an Accept-Language-style resolver with regional-locale normalization and wildcard fallback policy without importing HTTP types.
  • createQueryLocaleResolver(...), createCookieLocaleResolver(...), and createStorageLocaleResolver(...) read locale candidates from caller-owned abstractions and never access browser globals or framework internals.

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.

Validation Error Localization

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.

import { I18nService } from '@fluojs/i18n';
import { localizeDtoValidationError } from '@fluojs/i18n/validation';
import { DefaultValidator, DtoValidationError } from '@fluojs/validation';

const i18n = I18nService.create({
  defaultLocale: 'en',
  supportedLocales: ['en', 'ko'],
  fallbackLocales: { ko: ['en'] },
  catalogs: {
    en: { validation: { email: { EMAIL: '{{ field }} must be a valid email address.' } } },
    ko: { validation: { email: { EMAIL: '{{ field }}에는 올바른 이메일 주소가 필요합니다.' } } },
  },
});

try {
  await new DefaultValidator().materialize(input, CreateUserDto);
} catch (error) {
  if (error instanceof DtoValidationError) {
    throw localizeDtoValidationError(i18n, error, { locale: 'ko' });
  }
  throw error;
}

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.

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.

Node Filesystem Loader

Node applications can opt into a JSON filesystem loader from the dedicated subpath:

import { FileSystemI18nLoader } from '@fluojs/i18n/loaders/fs';

const loader = FileSystemI18nLoader.create({
  rootDir: new URL('./locales', import.meta.url).pathname,
});

const common = await loader.load('en', 'common');

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.

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.

Remote Catalog Loader

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:

import { RemoteI18nLoader } from '@fluojs/i18n/loaders/remote';

const loader = RemoteI18nLoader.create({
  timeoutMs: 5_000,
  provider: async ({ locale, namespace, signal }) => {
    const response = await fetch(`https://catalog.example/${locale}/${namespace}.json`, { signal });
    if (response.status === 404) {
      return undefined;
    }
    return response.text();
  },
});

const common = await loader.load('en', 'common');

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.

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.

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():

import { CachedRemoteI18nLoader, RemoteI18nLoader } from '@fluojs/i18n/loaders/remote';

const uncachedLoader = RemoteI18nLoader.create({ provider: fetchCatalog });
const cachedLoader = CachedRemoteI18nLoader.create({
  loader: uncachedLoader,
  ttlMs: 60_000,
  version: 'catalog-2026-05-11',
});

cachedLoader.invalidate('en', 'common');

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.

Catalog Type Generation

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.

import { generateI18nCatalogTypesFromDirectory } from '@fluojs/i18n/typegen';

const declarations = await generateI18nCatalogTypesFromDirectory({
  rootDir: new URL('./locales', import.meta.url).pathname,
});

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.

For custom pipelines or remote catalogs, generate from in-memory message trees:

import { generateI18nCatalogTypes } from '@fluojs/i18n/typegen';

const declarations = generateI18nCatalogTypes([
  {
    locale: 'en',
    namespace: 'admin/common',
    messages: {
      dashboard: {
        title: 'Dashboard',
      },
    },
  },
]);

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">.

import type { I18nCatalogTypedService } from './generated-i18n-catalog.d.ts';

const typedI18n = {
  translate: i18n.translate.bind(i18n),
  translateInNamespace: (namespace, key, options) => i18n.translate(key, { ...options, namespace }),
} satisfies I18nCatalogTypedService;

typedI18n.translate('admin/common.dashboard.title', { locale: 'en' });
typedI18n.translateInNamespace('admin/common', 'dashboard.title', { locale: 'en' });

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.

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.

Public API

Core (@fluojs/i18n)

| Export | Description | |---|---| | I18nModule | Module facade for registering the core i18n service surface; providers are global by default and can be kept module-local with global: false. | | I18nService | Core service that owns detached options/catalog snapshots, resolves translations, exposes static I18nService.create(options), and provides explicit-locale Intl formatting helpers (formatDateTime, formatNumber, formatCurrency, formatPercent, formatList, formatRelativeTime). | | I18nError | Base i18n package error with a stable error code. |

Types: I18nModuleOptions, I18nMessageCatalogs, I18nMessageTree, I18nTranslateOptions, I18nInterpolationValues, I18nMissingMessageHandler, I18nMissingMessageContext, I18nLocale, I18nTranslationKey, I18nErrorCode, I18nFallbackLocales, I18nFormatOptions, I18nFormatterOptions, I18nDateTimeFormatOptions, I18nNumberFormatOptions, I18nCurrencyFormatOptions, I18nListFormatOptions, I18nRelativeTimeFormatOptions, I18nNamedDateTimeFormats, I18nNamedNumberFormats, I18nNamedListFormats, I18nNamedRelativeTimeFormats.

HTTP Adapter (@fluojs/i18n/http)

| Export | Description | |---|---| | resolveHttpLocale | Resolves and stores locale metadata on the RequestContext. | | getHttpLocale | Retrieves locale metadata from the RequestContext. | | setHttpLocale | Manually stores locale metadata on the RequestContext. | | createAcceptLanguageLocalePolicyResolver | Creates a policy resolver for Accept-Language headers with regional normalization and wildcard fallback controls. | | parseAcceptLanguage | Utility to parse Accept-Language header into q-value preferences. | | HTTP_LOCALE_CONTEXT_KEY | Context key used to store locale metadata on RequestContext. |

Types: HttpLocaleContext, HttpLocaleResolver, HttpLocaleResolverInput, HttpLocaleResolverResult, ResolveHttpLocaleOptions, AcceptLanguageLocaleResolverOptions, AcceptLanguageLocalePolicyResolverOptions, AcceptLanguagePreference.

Non-HTTP Adapters (@fluojs/i18n/adapters)

| Export | Description | |---|---| | resolveLocale | Resolves locale metadata from an explicit non-HTTP resolver chain. | | bindLocale | Resolves and stores locale metadata in a caller-provided adapter store. | | setAdapterLocale | Manually stores locale metadata in a caller-provided adapter store. | | getAdapterLocale | Retrieves locale metadata from a caller-provided adapter store. | | createWeakMapLocaleStore | Creates per-object metadata storage without mutating transport contexts. | | createHeaderLocalePolicyResolver | Creates a header policy resolver for caller-owned header abstractions with regional normalization and wildcard fallback handling. | | createQueryLocaleResolver | Creates a resolver for query, CLI option, or request parameter abstractions. | | createCookieLocaleResolver | Creates a resolver for caller-owned cookie abstractions. | | createStorageLocaleResolver | Creates a resolver for local storage, server session, socket data, or CLI config abstractions. |

Types: LocaleAdapterContext, LocaleAdapterResolver, LocaleAdapterResolverInput, LocaleAdapterResolverResult, LocaleAdapterStore, ResolveLocaleOptions, BindLocaleOptions, HeaderLocaleResolverOptions, HeaderLocalePolicyResolverOptions, QueryLocaleResolverOptions, CookieLocaleResolverOptions, StorageLocaleResolverOptions.

Validation Integration (@fluojs/i18n/validation)

| Export | Description | |---|---| | createValidationIssueTranslationKeys(issue, keyPrefix?) | Builds default translation key candidates from validation issue source, field path, and code. | | localizeValidationIssue(i18n, issue, options, index?) | Returns a validation issue snapshot with a localized message when a candidate key resolves. | | localizeValidationIssues(i18n, issues, options) | Localizes an issue list without mutating the original issues. | | localizeDtoValidationError(i18n, error, options) | Creates a new DtoValidationError with localized issue messages. |

Types: LocalizeValidationIssuesOptions, ValidationIssueTranslationKeyBuilder, ValidationIssueTranslationKeyContext.

ICU MessageFormat (@fluojs/i18n/icu)

| Export | Description | |---|---| | IcuI18nService | Service that resolves messages through I18nService before ICU formatting; creates standalone instances via static IcuI18nService.create(options). |

Types: I18nIcuTranslateOptions, I18nIcuValue, I18nIcuValues.

Filesystem Loader (@fluojs/i18n/loaders/fs)

| Export | Description | |---|---| | FileSystemI18nLoader | Node.js JSON filesystem loader class with static FileSystemI18nLoader.create(options). |

Types: I18nLoader, I18nLoaderLoadOptions, FileSystemI18nLoaderOptions.

Remote Loader (@fluojs/i18n/loaders/remote)

| Export | Description | |---|---| | RemoteI18nLoader | Provider-backed remote catalog loader class with static RemoteI18nLoader.create(options). | | CachedRemoteI18nLoader | In-memory cache wrapper implementation with static CachedRemoteI18nLoader.create(options) and explicit invalidate(...) and clear() controls. |

Types: I18nLoader, I18nLoaderLoadOptions, RemoteI18nCatalogProvider, RemoteI18nCatalogRequest, RemoteI18nLoaderOptions, CachedI18nLoader, CachedI18nLoaderKeyInput, CachedI18nLoaderOptions.

Catalog Type Generation (@fluojs/i18n/typegen)

| Export | Description | |---|---| | generateI18nCatalogTypes(inputs, options?) | Generates deterministic TypeScript key declarations from in-memory catalog trees. | | generateI18nCatalogTypesFromDirectory(options) | Reads locale/namespace JSON catalogs from disk and generates key declarations. |

Types: I18nCatalogTypegenInput, I18nCatalogTypegenOptions, I18nCatalogTypegenDirectoryOptions. Generated declaration defaults include I18nCatalogKey, I18nCatalogNamespace, I18nCatalogKeyByNamespace, I18nCatalogNamespaceKey, I18nCatalogTypedTranslateOptions, I18nCatalogTypedTranslate, and I18nCatalogTypedService.

Ecosystem Bridge Evaluation

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 for the classification matrix and the acceptance criteria required before any future bridge helper can become a first-party subpath.

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.

Post-MVP Roadmap

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.

Related Packages

  • @fluojs/core: Provides module metadata and shared framework errors used by this package.
  • @fluojs/config: The closest package layout model for module registration and option snapshotting conventions.
  • @fluojs/validation: Provides the opt-in validation issue contract consumed by @fluojs/i18n/validation.

Error recognition

isI18nError(...) validates the versioned i18n owner contract before code-based handling. Remote loading, cancellation, and validation localization therefore preserve compatible duplicate-copy errors while rejecting plain lookalikes and incompatible versions.

Example Sources

  • packages/i18n/src/module.ts
  • packages/i18n/src/service.ts
  • packages/i18n/src/icu.ts
  • packages/i18n/src/loaders/fs.ts
  • packages/i18n/src/http.ts
  • packages/i18n/src/adapters.ts
  • packages/i18n/src/validation.ts
  • packages/i18n/src/index.test.ts
  • packages/i18n/src/loaders/remote.ts
  • packages/i18n/src/typegen.ts

Compatible service copies

I18nService supports ordinary class injection across compatible same-realm package copies; option tokens remain isolated. IcuI18nService also accepts a compatible foreign service only after verifying the non-enumerable versioned owner capability and the complete lookup and formatting surface. It keeps the foreign service's catalog snapshot and locale resolution, so a fallback message is formatted using its supplying locale while defaultValue and missing-message handling retain the requested locale.