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

@digitaldefiance/i18n-lib

v4.7.0

Published

i18n library with enum translation support

Readme

@digitaldefiance/i18n-lib

A production-ready TypeScript internationalization library with component-based architecture, type-safe translations, and comprehensive error handling.

Part of Express Suite

What's New in v4.7.0

Built-in Date/Time Template Variables{YEAR}, {MONTH}, {DAY}, and {NOW} are now automatically available in all translation paths without explicit variable passing.

  • {YEAR}, {MONTH}, {DAY} — current UTC date as strings (month/day zero-padded)
  • {NOW} — epoch milliseconds for ICU date/time formatting: {NOW, date, long} → locale-aware output
  • ICU date/time/number patterns now correctly route through the ICU formatting pipeline
engine.t('Copyright {YEAR}');                    // "Copyright 2026"
engine.t('Date: {YEAR}-{MONTH}-{DAY}');          // "Date: 2026-03-25"
engine.translate('app', 'posted');               // "Posted on March 25, 2026"
engine.translate('app', 'posted', {}, 'fr');     // "Publié le 25 mars 2026"

Features

  • Production-Grade Security: Comprehensive protection against common attacks
    • Prototype pollution prevention
    • ReDoS (Regular Expression Denial of Service) mitigation
    • XSS (Cross-Site Scripting) protection with HTML escaping
    • Input validation with configurable limits
    • Bounded resource usage (cache, recursion, input length)
  • ICU MessageFormat: Industry-standard message formatting with plural, select, date/time/number formatting
  • Component-Based Architecture: Register translation components with full type safety
  • 37 Supported Languages: CLDR-compliant plural rules for world's most complex languages
  • Pluralization Support: Automatic plural form selection based on count (one/few/many/other)
  • Gender Support: Gender-aware translations (male/female/neutral/other)
  • Advanced Number Formatting: Thousand separators, currency, percent with decimal precision
  • 8 Built-in Languages: English (US/UK), French, Spanish, German, Chinese, Japanese, Ukrainian
  • Advanced Template Processing:
    • Component references: {{Component.key}}
    • Alias resolution: {{Alias.key}}
    • Enum name resolution: {{EnumName.value}}
    • Variable substitution: {variable}
    • Context variables: {currency}, {timezone}, {language}, {YEAR}, {MONTH}, {DAY}, {NOW}
  • Context Integration: Automatic injection of currency, timezone, language, and UTC date/time from GlobalActiveContext
  • Smart Object Handling: CurrencyCode and Timezone objects automatically extract values
  • Multiple Instances: Create isolated i18n engines for different contexts
  • Fluent Builder: I18nBuilder for clean, chainable engine configuration
  • Core System Strings: Pre-built translations for common UI elements and errors
  • Type Safety: Full TypeScript support with generic types
  • Branded Enums: Runtime-identifiable string keys with collision detection and component routing
  • Error Handling: Comprehensive error classes with translation support and ICU formatting
  • 93.22% Test Coverage: 2,007 tests covering all features
  • Security Hardened: Comprehensive protection against prototype pollution, ReDoS, and XSS attacks

Installation

npm install @digitaldefiance/i18n-lib
# or
yarn add @digitaldefiance/i18n-lib

Quick Start

import { PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';

// Create engine with languages
const engine = PluginI18nEngine.createInstance('myapp', [
  { id: LanguageCodes.EN_US, name: 'English (US)', code: 'en-US', isDefault: true },
  { id: LanguageCodes.FR, name: 'Français', code: 'fr' }
]);

// Register component with translations
engine.registerComponent({
  component: {
    id: 'app',
    name: 'Application',
    stringKeys: ['welcome', 'goodbye']
  },
  strings: {
    [LanguageCodes.EN_US]: {
      welcome: 'Welcome to {appName}!',
      goodbye: 'Goodbye!'
    },
    [LanguageCodes.FR]: {
      welcome: 'Bienvenue sur {appName}!',
      goodbye: 'Au revoir!'
    }
  }
});

// Translate
console.log(engine.translate('app', 'welcome', { appName: 'MyApp' }));
// Output: "Welcome to MyApp!"

// Switch language
engine.setLanguage(LanguageCodes.FR);
console.log(engine.translate('app', 'welcome', { appName: 'MyApp' }));
// Output: "Bienvenue sur MyApp!"

// Pluralization (automatic form selection)
engine.registerComponent({
  component: {
    id: 'cart',
    name: 'Cart',
    stringKeys: ['items']
  },
  strings: {
    'en-US': {
      items: {
        one: '1 item',
        other: '{count} items'
      }
    }
  }
});

console.log(engine.translate('cart', 'items', { count: 1 }));
// Output: "1 item"
console.log(engine.translate('cart', 'items', { count: 5 }));
// Output: "5 items"

ICU MessageFormat

Industry-standard message formatting with powerful features. See docs/ICU_MESSAGEFORMAT.md for complete guide.

When to use ICU MessageFormat:

  • Complex pluralization with multiple forms
  • Gender-specific translations
  • Number/date/time formatting with locale awareness
  • Nested conditional logic (select within plural)

When to use simple templates:

  • Basic variable substitution
  • Component references ({{Component.key}})
  • Simple string interpolation

Quick Example

import { formatICUMessage } from '@digitaldefiance/i18n-lib';

// Simple variable
formatICUMessage('Hello {name}', { name: 'Alice' });
// → "Hello Alice"

// Plural
formatICUMessage('{count, plural, one {# item} other {# items}}', { count: 1 });
// → "1 item"

// Select
formatICUMessage('{gender, select, male {He} female {She} other {They}}', { gender: 'male' });
// → "He"

// Number formatting
formatICUMessage('{price, number, currency}', { price: 99.99 }, 'en-US');
// → "$99.99"

// Complex nested
formatICUMessage(
  '{gender, select, male {He has} female {She has}} {count, plural, one {# item} other {# items}}',
  { gender: 'female', count: 2 }
);
// → "She has 2 items"

Features

  • Full ICU Syntax: Variables, plural, select, selectordinal
  • Formatters: Number (integer, currency, percent), Date, Time
  • 37 Languages: CLDR plural rules for all supported languages
  • Nested Messages: Up to 4 levels deep
  • Performance: <1ms per format, message caching
  • Specification Compliant: Unicode ICU, CLDR, FormatJS compatible

Documentation

API

import { 
  formatICUMessage,      // One-line formatting
  isICUMessage,          // Detect ICU format
  parseICUMessage,       // Parse to AST
  compileICUMessage,     // Compile to function
  validateICUMessage,    // Validate syntax
  Runtime                // Advanced usage
} from '@digitaldefiance/i18n-lib';

Pluralization & Gender

Pluralization

Automatic plural form selection based on count with CLDR-compliant rules for 37 languages:

import { createPluralString, PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';

const engine = PluginI18nEngine.createInstance('app', [
  { id: LanguageCodes.EN_US, name: 'English', code: 'en-US', isDefault: true }
]);

// English (one/other)
engine.registerComponent({
  component: {
    id: 'shop',
    name: 'Shop',
    stringKeys: ['items']
  },
  strings: {
    'en-US': {
      items: createPluralString({
        one: '{count} item',
        other: '{count} items'
      })
    }
  }
});

// Russian (one/few/many)
engine.registerComponent({
  component: {
    id: 'shop',
    name: 'Shop',
    stringKeys: ['items']
  },
  strings: {
    'ru': {
      items: createPluralString({
        one: '{count} товар',
        few: '{count} товара',
        many: '{count} товаров'
      })
    }
  }
});

// Arabic (zero/one/two/few/many/other)
engine.registerComponent({
  component: {
    id: 'shop',
    name: 'Shop',
    stringKeys: ['items']
  },
  strings: {
    'ar': {
      items: createPluralString({
        zero: 'لا عناصر',
        one: 'عنصر واحد',
        two: 'عنصران',
        few: '{count} عناصر',
        many: '{count} عنصرًا',
        other: '{count} عنصر'
      })
    }
  }
});

// Automatic form selection
engine.translate('shop', 'items', { count: 1 });  // "1 item"
engine.translate('shop', 'items', { count: 5 });  // "5 items"
engine.translate('shop', 'items', { count: 21 }, 'ru'); // "21 товар"

Supported Languages (37 total):

  • Simple (other only): Japanese, Chinese, Korean, Turkish, Vietnamese, Thai, Indonesian, Malay
  • Two forms (one/other): English, German, Spanish, Italian, Portuguese, Dutch, Swedish, Norwegian, Danish, Finnish, Greek, Hebrew, Hindi
  • Three forms (one/few/many): Russian, Ukrainian, Romanian, Latvian
  • Four forms: Polish, Czech, Lithuanian, Slovenian, Scottish Gaelic
  • Five forms: Irish, Breton
  • Six forms: Arabic, Welsh

See PLURALIZATION_SUPPORT.md for complete language matrix.

Gender Support

Gender-aware translations with intelligent fallback:

import { createGenderedString, PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';

const engine = PluginI18nEngine.createInstance('app', [
  { id: LanguageCodes.EN_US, name: 'English', code: 'en-US', isDefault: true }
]);

engine.registerComponent({
  component: {
    id: 'profile',
    name: 'Profile',
    stringKeys: ['greeting']
  },
  strings: {
    'en-US': {
      greeting: createGenderedString({
        male: 'Welcome, Mr. {name}',
        female: 'Welcome, Ms. {name}',
        neutral: 'Welcome, {name}'
      })
    }
  }
});

engine.translate('profile', 'greeting', { name: 'Smith', gender: 'male' });
// Output: "Welcome, Mr. Smith"

Combined Plural + Gender

Nested plural and gender forms:

// Plural → Gender
const pluralGender = {
  one: {
    male: 'He has {count} item',
    female: 'She has {count} item'
  },
  other: {
    male: 'He has {count} items',
    female: 'She has {count} items'
  }
};

// Gender → Plural
const genderPlural = {
  male: {
    one: 'He has {count} item',
    other: 'He has {count} items'
  },
  female: {
    one: 'She has {count} item',
    other: 'She has {count} items'
  }
};

Helper Functions

import { 
  createPluralString, 
  createGenderedString, 
  getRequiredPluralForms 
} from '@digitaldefiance/i18n-lib';

// Get required forms for a language
const forms = getRequiredPluralForms('ru');
// Returns: ['one', 'few', 'many']

// Type-safe plural string creation
const plural = createPluralString({
  one: '1 item',
  other: '{count} items'
});

// Type-safe gender string creation
const gender = createGenderedString({
  male: 'He',
  female: 'She',
  neutral: 'They'
});

Validation

import { validatePluralForms } from '@digitaldefiance/i18n-lib';

// Validate plural forms for a language
const result = validatePluralForms(
  { one: 'item', other: 'items' },
  'en',
  'items',
  { strict: true, checkUnused: true, checkVariables: true }
);

if (!result.isValid) {
  console.error('Errors:', result.errors);
}
if (result.warnings.length > 0) {
  console.warn('Warnings:', result.warnings);
}

Core Concepts

PluginI18nEngine

The main engine class that manages translations, languages, and components.

import { PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';

// Recommended: Create named instance (supports multiple engines)
const engine = PluginI18nEngine.createInstance('myapp', languages);

// Alternative: Direct constructor (for single engine use cases)
const engine = new PluginI18nEngine(languages, config);

Component Registration

Components group related translations together:

engine.registerComponent({
  component: {
    id: 'auth',
    name: 'Authentication',
    stringKeys: ['login', 'logout', 'error']
  },
  strings: {
    [LanguageCodes.EN_US]: {
      login: 'Login',
      logout: 'Logout',
      error: 'Authentication failed'
    },
    [LanguageCodes.FR]: {
      login: 'Connexion',
      logout: 'Déconnexion',
      error: 'Échec de l\'authentification'
    }
  },
  aliases: ['authentication'] // Optional aliases
});

// Safe registration (won't error if already registered)
engine.registerComponentIfNotExists({
  component: { id: 'auth', /* ... */ },
  strings: { /* ... */ }
});

Translation

// Simple translation
const text = engine.translate('auth', 'login');

// With variables
const greeting = engine.translate('app', 'welcome', { name: 'John' });

// Specific language
const french = engine.translate('auth', 'login', {}, LanguageCodes.FR);

// Safe translation (returns fallback on error)
const safe = engine.safeTranslate('missing', 'key'); // Returns "[missing.key]"

Template Processing

// Component references: {{componentId.stringKey}}
engine.t('Click {{auth.login}} to continue');

// Alias resolution: {{alias.stringKey}}
engine.registerComponent({
  component: { id: 'authentication', /* ... */ },
  aliases: ['auth', 'AuthModule']
});
engine.t('{{auth.login}}'); // Resolves via alias

// Variables: {variableName}
engine.t('Hello, {username}!', { username: 'Alice' });

// Context variables (automatic injection)
engine.t('Price in {currency}'); // Uses context currency
engine.t('Time: {timezone}'); // Uses context timezone
engine.t('Language: {language}'); // Uses current language

// Date/time variables (automatic UTC injection)
engine.t('Copyright {YEAR}');            // "Copyright 2026"
engine.t('Date: {YEAR}-{MONTH}-{DAY}'); // "Date: 2026-03-25"
engine.t('Timestamp: {NOW}');            // "Timestamp: 1742918400000" (epoch ms)

// CurrencyCode and Timezone objects
const currency = new CurrencyCode('EUR');
const timezone = new Timezone('America/New_York');
engine.t('Price: {amount} {currency}', { amount: 100, currency });
// Output: "Price: 100 EUR"

// Variable priority: provided > context > date/time > constants
engine.t('{AppName}'); // Uses constant
engine.t('{currency}'); // Uses context
engine.t('{YEAR}');     // Uses built-in UTC date
engine.t('{currency}', { currency: 'GBP' }); // Uses provided (overrides context)
engine.t('{YEAR}', { YEAR: '1776' });         // Uses provided (overrides built-in)

// Mixed patterns
engine.t('{{auth.login}}: {username} ({currency})', { username: 'admin' });

Builder Pattern

import { I18nBuilder } from '@digitaldefiance/i18n-lib';

const engine = I18nBuilder.create()
  .withLanguages([
    { id: 'en-US', name: 'English', code: 'en-US', isDefault: true },
    { id: 'fr', name: 'French', code: 'fr' }
  ])
  .withDefaultLanguage('en-US')
  .withFallbackLanguage('en-US')
  .withConstants({
    AppName: 'MyApp',
    Version: '1.0.0'
  })
  .withValidation({
    requireCompleteStrings: false,
    allowPartialRegistration: true
  })
  .withInstanceKey('myapp')
  .withRegisterInstance(true)
  .withSetAsDefault(true)
  .build();

Builder with String Key Enum Registration

Register branded string key enums during engine construction for direct translation via translateStringKey():

import { I18nBuilder, createI18nStringKeysFromEnum } from '@digitaldefiance/i18n-lib';

// Create branded enum from your string keys
enum MyStringKeys {
  Welcome = 'welcome',
  Goodbye = 'goodbye',
}
const BrandedKeys = createI18nStringKeysFromEnum('my-component', MyStringKeys);

const engine = I18nBuilder.create()
  .withLanguages([
    { id: 'en-US', name: 'English', code: 'en-US', isDefault: true },
  ])
  .withStringKeyEnum(BrandedKeys)  // Register single enum
  // Or register multiple at once:
  // .withStringKeyEnums([BrandedKeys, OtherKeys])
  .build();

// Now you can translate directly without component ID
engine.translateStringKey(BrandedKeys.Welcome);

Context Integration

Automatic injection of currency, timezone, and language from GlobalActiveContext:

import { 
  GlobalActiveContext, 
  CurrencyCode, 
  Timezone,
  PluginI18nEngine,
  LanguageCodes
} from '@digitaldefiance/i18n-lib';

// Set context variables
const context = GlobalActiveContext.getInstance();
context.setCurrencyCode(new CurrencyCode('EUR'));
context.setUserTimezone(new Timezone('Europe/Paris'));
context.setUserLanguage('fr');

// Context variables automatically available in translations
engine.t('Price in {currency}'); // "Price in EUR"
engine.t('Timezone: {timezone}'); // "Timezone: Europe/Paris"
engine.t('Language: {language}'); // "Language: fr"

// UTC date/time variables (always available, no context setup needed)
engine.t('Copyright {YEAR}');            // "Copyright 2026"
engine.t('Date: {YEAR}-{MONTH}-{DAY}'); // "Date: 2026-03-25"

// Override context with provided variables
engine.t('Price in {currency}', { currency: 'USD' }); // "Price in USD"
engine.t('Year: {YEAR}', { YEAR: '1776' });            // "Year: 1776"

ICU Date/Time Formatting with {NOW}

The built-in {NOW} variable (epoch milliseconds) works with ICU MessageFormat date and time types for locale-aware formatting. This works in both I18nEngine and PluginI18nEngine.

// Register translations with ICU date/time patterns
engine.register({
  id: 'app',
  strings: {
    'en-US': {
      posted: 'Posted on {NOW, date, long}',
      updated: 'Last updated {NOW, date, medium} at {NOW, time, short}',
    },
    'fr': {
      posted: 'Publié le {NOW, date, long}',
      updated: 'Mis à jour le {NOW, date, medium} à {NOW, time, short}',
    },
  },
});

// NOW is injected automatically — no need to pass it
engine.translate('app', 'posted');
// en-US → "Posted on March 25, 2026"

engine.translate('app', 'posted', {}, 'fr');
// fr → "Publié le 25 mars 2026"

engine.translate('app', 'updated');
// en-US → "Last updated Mar 25, 2026 at 2:30 PM"

// Override with a specific timestamp
const launchDate = new Date('2024-07-04T12:00:00Z').getTime();
engine.translate('app', 'posted', { NOW: launchDate });
// → "Posted on July 4, 2024"

Available ICU date/time styles:

| Pattern | Style | en-US Example | |---|---|---| | {NOW, date, short} | Short date | 3/25/26 | | {NOW, date, medium} | Medium date | Mar 25, 2026 | | {NOW, date, long} | Long date | March 25, 2026 | | {NOW, date, full} | Full date | Wednesday, March 25, 2026 | | {NOW, time, short} | Short time | 2:30 PM | | {NOW, time, medium} | Medium time | 2:30:00 PM | | {NOW, time, long} | Long time | 2:30:00 PM UTC | | {NOW, time, full} | Full time | 2:30:00 PM Coordinated Universal Time |

All styles are locale-aware — the same pattern produces different output per language via Intl.DateTimeFormat. Both I18nEngine and PluginI18nEngine support ICU date/time formatting.

Constants Management

Constants are application-wide values available as template variables in all translations (e.g., {Site} in a translation string resolves to the registered value). The ConstantsRegistry provides structured, per-component registration with conflict detection and ownership tracking.

Type-Safe Constants with II18nConstants

All constants passed to the i18n system must satisfy the II18nConstants base interface. Library authors define component-specific interfaces extending it for compile-time safety:

import type { II18nConstants } from '@digitaldefiance/i18n-lib';

// Define a typed constants interface for your component
export interface IMyAppI18nConstants extends II18nConstants {
  Site: string;
  SiteTagline: string;
  ApiVersion: number;
}

// TypeScript enforces the shape at compile time
const constants: IMyAppI18nConstants = {
  Site: 'Acme Corp',
  SiteTagline: 'Building the future',
  ApiVersion: 2,
};

The library ships two helper types for working with typed constants:

  • ConstantKeys<T> — Extracts the string keys from a constants type
  • ConstantVariables<T> — Builds a Partial<Record<key, string | number>> for translation variable overrides
import type { ConstantKeys, ConstantVariables } from '@digitaldefiance/i18n-lib';

type Keys = ConstantKeys<IMyAppI18nConstants>;
// 'Site' | 'SiteTagline' | 'ApiVersion'

type Vars = ConstantVariables<IMyAppI18nConstants>;
// { Site?: string | number; SiteTagline?: string | number; ApiVersion?: string | number }

Suite Core ships ISuiteCoreI18nConstants (from @digitaldefiance/suite-core-lib) with the standard template variable keys used in its translation strings.

Registration Flow (via createI18nSetup)

Library packages declare default constants in their I18nComponentPackage. The app overrides them at runtime. The factory handles the ordering automatically:

import { createI18nSetup } from '@digitaldefiance/i18n-lib';
import { createSuiteCoreComponentPackage } from '@digitaldefiance/suite-core-lib';
import { AppStringKey } from './enumerations/app-string-key';
import { Strings } from './strings-collection';

const setup = createI18nSetup({
  componentId: 'my-app',
  stringKeyEnum: AppStringKey,
  strings: Strings,
  // App constants override library defaults — app always wins
  constants: { Site: 'My Real Site', SiteTagline: 'We do things' },
  // Library components register their own defaults
  libraryComponents: [createSuiteCoreComponentPackage()],
});

// The factory does this internally:
// 1. Library components register defaults via registerConstants()
// 2. App constants override via updateConstants() — app values win

Direct Engine API

// Register constants for a component (idempotent, conflict-detecting)
engine.registerConstants('suite-core', { Site: 'New Site', Version: '1.0' });

// Update/override constants (merges, updater wins ownership)
engine.updateConstants('my-app', { Site: 'My Real Site' });

// Replace all constants for a component (wipes old keys)
engine.replaceConstants({ Site: 'Completely New', Version: '2.0' });

// Merge into engine-level constants (legacy, preserved for compat)
engine.mergeConstants({ ExtraKey: 'value' });

// Query
engine.hasConstants('suite-core');           // true
engine.getConstants('suite-core');           // { Site: 'New Site', Version: '1.0' }
engine.getAllConstants();                     // [{ componentId, constants }, ...]
engine.resolveConstantOwner('Site');         // 'my-app' (last updater wins)

I18nComponentPackage Constants

Library authors can bundle default constants with their component package:

import type { I18nComponentPackage, II18nConstants } from '@digitaldefiance/i18n-lib';

export interface IMyLibI18nConstants extends II18nConstants {
  LibName: string;
  LibVersion: string;
}

export function createMyLibComponentPackage(): I18nComponentPackage {
  const constants: IMyLibI18nConstants = {
    LibName: 'My Library',
    LibVersion: '1.0.0',
  };
  return {
    config: createMyLibComponentConfig(),
    stringKeyEnum: MyLibStringKey,
    constants,
  };
}

These are registered automatically when passed via libraryComponents in createI18nSetup.

I18nSetupResult Helpers

The result from createI18nSetup exposes constants helpers:

const setup = createI18nSetup({ /* ... */ });

// Register constants for a new component after setup
setup.registerConstants('analytics', { TrackingId: 'UA-12345' });

// Override constants at runtime
setup.updateConstants('my-app', { Site: 'Updated Site Name' });

Conflict Detection

If two different components try to register the same key with different values, an error is thrown:

engine.registerConstants('lib-a', { Site: 'Alpha' });
engine.registerConstants('lib-b', { Site: 'Beta' });
// Throws: I18nError CONSTANT_CONFLICT — "Site" already registered by "lib-a" with a different value

Use updateConstants instead of registerConstants when you intentionally want to override.

Runtime Validation with validateConstantsCoverage()

Use validateConstantsCoverage() in tests to verify that all {variable} references in your translation templates have corresponding constant keys. This catches drift between templates and constants at test time:

import { validateConstantsCoverage } from '@digitaldefiance/i18n-lib';
import { SuiteCoreComponentStrings } from '@digitaldefiance/suite-core-lib';

const constants = { Site: 'Test', SiteTagline: 'Tagline', SiteDescription: 'Desc' };

const result = validateConstantsCoverage(SuiteCoreComponentStrings, constants, {
  ignoreVariables: ['count', 'name'], // runtime-only variables, not constants
});

expect(result.isValid).toBe(true);
expect(result.missingConstants).toEqual([]);   // no template vars without constants
expect(result.unusedConstants).toEqual([]);     // no constants without template refs
expect(result.referencedVariables).toContain('Site');

The result object contains:

  • isValidtrue if all template variables have matching constants
  • missingConstants — variable names referenced in templates but missing from constants
  • unusedConstants — constant keys registered but never referenced in any template
  • referencedVariables — all variable names found in templates

When to use:

  • Constants: Application-wide values that rarely change (Site, Version, SiteTagline)
  • Variables: Request-specific or dynamic values passed to translate()
  • Context: User-specific values (currency, timezone, language)
  • Date/Time: Built-in UTC values (YEAR, MONTH, DAY, NOW) — always available, no setup needed

Variable priority: provided variables > context > date/time built-ins > constants

Language Management

// Set current language
engine.setLanguage(LanguageCodes.FR);

// Get current language
const lang = engine.getCurrentLanguage();

// Check if language exists (recommended before setLanguage)
if (engine.hasLanguage(LanguageCodes.ES)) {
  engine.setLanguage(LanguageCodes.ES);
} else {
  console.warn('Spanish not available, using default');
}

// Get all languages
const languages = engine.getLanguages();

Admin Context

Separate language for admin interfaces (useful for multi-tenant applications where admins need consistent UI language regardless of user's language):

// Set admin language (e.g., always English for admin panel)
engine.setAdminLanguage(LanguageCodes.EN_US);

// Switch to admin context (uses admin language)
engine.switchToAdmin();
const adminText = engine.translate('app', 'dashboard'); // Uses EN_US

// Switch back to user context (uses user's language)
engine.switchToUser();
const userText = engine.translate('app', 'dashboard'); // Uses user's language

Use cases:

  • Admin panels in multi-language applications
  • Support interfaces that need consistent language
  • Internal tools accessed by multilingual teams

Core System Strings

Pre-built translations for common UI elements in 8 languages:

import { getCoreI18nEngine, CoreStringKey, CoreI18nComponentId } from '@digitaldefiance/i18n-lib';

const coreEngine = getCoreI18nEngine();

// Use core strings
const yes = coreEngine.translate(CoreI18nComponentId, CoreStringKey.Common_Yes);
const error = coreEngine.translate(CoreI18nComponentId, CoreStringKey.Error_NotFound);

Available core string categories:

  • Common (30+ strings): Yes, No, Cancel, OK, Save, Delete, Edit, Create, Update, Loading, Search, Filter, Sort, Export, Import, Settings, Help, About, Contact, Terms, Privacy, Logout, Profile, Dashboard, Home, Back, Next, Previous, Submit, Reset
  • Errors (25+ strings): InvalidInput, NetworkError, NotFound, AccessDenied, ValidationFailed, Unauthorized, Forbidden, ServerError, Timeout, BadRequest, Conflict, Gone, TooManyRequests, ServiceUnavailable
  • System (20+ strings): Welcome, Goodbye, PleaseWait, ProcessingRequest, OperationComplete, OperationFailed, Success, Warning, Info, Confirm, AreYouSure, UnsavedChanges, SessionExpired, MaintenanceMode

See CoreStringKey enum for complete list of available strings.

Multiple Instances

Create isolated engines for different parts of your application (useful for micro-frontends, plugins, or multi-tenant systems):

// Admin engine with admin-specific languages
const adminEngine = PluginI18nEngine.createInstance('admin', adminLanguages);

// User engine with user-facing languages
const userEngine = PluginI18nEngine.createInstance('user', userLanguages);

// Get instance by key
const admin = PluginI18nEngine.getInstance('admin');

// Check if instance exists
if (PluginI18nEngine.hasInstance('admin')) {
  // ...
}

// Remove instance (cleanup)
PluginI18nEngine.removeInstance('admin');

// Reset all instances (useful in tests)
PluginI18nEngine.resetAll(); // ⚠️ Removes ALL instances globally

Use cases:

  • Micro-frontends with independent i18n
  • Plugin systems with isolated translations
  • Multi-tenant applications with tenant-specific languages
  • Testing (create/destroy engines per test)

Error Handling

RegistryError

Errors related to component/language registration:

import { RegistryError, RegistryErrorType } from '@digitaldefiance/i18n-lib';

try {
  engine.translate('missing', 'key');
} catch (error) {
  if (error instanceof RegistryError) {
    console.log(error.type);     // RegistryErrorType.COMPONENT_NOT_FOUND
    console.log(error.message);  // "Component 'missing' not found"
    console.log(error.metadata); // { componentId: 'missing' }
  }
}

TranslatableError

Base class for errors with translated messages:

import { TranslatableError, CoreStringKey, CoreI18nComponentId } from '@digitaldefiance/i18n-lib';

class MyError extends TranslatableError {
  constructor(language?: string) {
    super(
      CoreI18nComponentId,
      CoreStringKey.Error_AccessDenied,
      {},
      language
    );
  }
}

throw new MyError(LanguageCodes.FR); // Throws with French error message

Translation Adapter

Adapt PluginI18nEngine to simpler TranslationEngine interface (useful when integrating with error classes or other components expecting a simplified translation interface):

import { createTranslationAdapter } from '@digitaldefiance/i18n-lib';

const adapter = createTranslationAdapter(engine, 'componentId');

// Use adapter where TranslationEngine is expected
// (e.g., error classes, third-party libraries)
const message = adapter.translate('key', { var: 'value' });

Language Codes

Built-in language codes following BCP 47 standard:

import { LanguageCodes } from '@digitaldefiance/i18n-lib';

LanguageCodes.EN_US  // 'en-US'
LanguageCodes.EN_GB  // 'en-GB'
LanguageCodes.FR     // 'fr'
LanguageCodes.ES     // 'es'
LanguageCodes.DE     // 'de'
LanguageCodes.ZH_CN  // 'zh-CN'
LanguageCodes.JA     // 'ja'
LanguageCodes.UK     // 'uk'

Adding custom language codes:

// Define your custom language type
type MyLanguages = CoreLanguageCode | 'pt-BR' | 'it' | 'nl';

// Create engine with custom languages
const engine = PluginI18nEngine.createInstance<MyLanguages>('app', [
  { id: LanguageCodes.EN_US, name: 'English', code: 'en-US', isDefault: true },
  { id: 'pt-BR', name: 'Portuguese (Brazil)', code: 'pt-BR' },
  { id: 'it', name: 'Italian', code: 'it' },
  { id: 'nl', name: 'Dutch', code: 'nl' },
]);

Note: The 8 built-in codes have pre-translated core strings. Custom languages require you to provide all translations.

API Reference

PluginI18nEngine

Static Methods

  • createInstance<TLanguage>(key: string, languages: LanguageDefinition[], config?: RegistryConfig) - Create named instance
  • getInstance<TLanguage>(key?: string) - Get instance by key
  • hasInstance(key?: string) - Check if instance exists
  • removeInstance(key?: string) - Remove instance
  • resetAll() - Reset all instances

Instance Methods

  • registerComponent(registration: ComponentRegistration) - Register component
  • translate(componentId: string, key: string, variables?, language?) - Translate string
  • safeTranslate(componentId: string, key: string, variables?, language?) - Safe translate with fallback
  • t(template: string, variables?, language?) - Process template string
  • setLanguage(language: TLanguage) - Set current language
  • setAdminLanguage(language: TLanguage) - Set admin language
  • getCurrentLanguage() - Get current language
  • getLanguages() - Get all languages
  • hasLanguage(language: TLanguage) - Check if language exists
  • switchToAdmin() - Switch to admin context
  • switchToUser() - Switch to user context
  • validate() - Validate all components
  • registerBrandedComponent(registration) - Register component with branded string keys
  • getCollisionReport() - Get map of key collisions across components
  • registerStringKeyEnum(enum) - Register a branded string key enum for direct translation
  • translateStringKey(key, variables?, language?) - Translate a branded string key directly
  • safeTranslateStringKey(key, variables?, language?) - Safe version returning placeholder on failure
  • hasStringKeyEnum(enum) - Check if a branded enum is registered
  • getStringKeyEnums() - Get all registered branded enums
  • registerConstants(componentId, constants) - Register constants for a component (idempotent, conflict-detecting)
  • updateConstants(componentId, constants) - Update/override constants for a component (merges, updater wins)
  • replaceConstants(constants) - Replace all engine-level constants
  • mergeConstants(constants) - Merge into engine-level constants
  • hasConstants(componentId) - Check if constants are registered for a component
  • getConstants(componentId) - Get constants for a specific component
  • getAllConstants() - Get all registered constants entries
  • resolveConstantOwner(key) - Resolve which component owns a constant key

Branded Enum Functions

  • createI18nStringKeys(componentId, keys) - Create a branded enum for i18n keys
  • createI18nStringKeysFromEnum(componentId, enum) - Convert legacy enum to branded enum
  • mergeI18nStringKeys(newId, ...enums) - Merge multiple branded enums
  • findStringKeySources(key) - Find components containing a key
  • resolveStringKeyComponent(key) - Resolve key to single component
  • getStringKeysByComponentId(id) - Get enum by component ID
  • getRegisteredI18nComponents() - List all registered components
  • getStringKeyValues(enum) - Get all values from enum
  • isValidStringKey(value, enum) - Type guard for key validation
  • checkStringKeyCollisions(...enums) - Check enums for collisions

Core Functions

  • getCoreI18nEngine() - Get core engine with system strings
  • createCoreI18nEngine(instanceKey?) - Create core engine instance
  • getCoreTranslation(stringKey, variables?, language?, instanceKey?) - Get core translation
  • safeCoreTranslation(stringKey, variables?, language?, instanceKey?) - Safe core translation
  • getCoreLanguageCodes() - Get array of core language codes
  • getCoreLanguageDefinitions() - Get core language definitions
  • getUtcDateVars() - Returns { YEAR, MONTH, DAY, NOW } with current UTC date values (strings for date parts, epoch ms number for NOW)

Testing

import { PluginI18nEngine } from '@digitaldefiance/i18n-lib';

describe('My Tests', () => {
  beforeEach(() => {
    PluginI18nEngine.resetAll();
  });

  afterEach(() => {
    PluginI18nEngine.resetAll();
  });

  it('should translate', () => {
    const engine = PluginI18nEngine.createInstance('test', languages);
    engine.registerComponent(registration);
    expect(engine.translate('app', 'hello')).toBe('Hello');
  });
});

TypeScript Support

Full TypeScript support with generic types:

// Type-safe language codes
type MyLanguages = 'en-US' | 'fr' | 'es';
const engine = PluginI18nEngine.createInstance<MyLanguages>('app', languages);

// Type-safe string keys
enum MyStringKeys {
  Welcome = 'welcome',
  Goodbye = 'goodbye'
}

// Type-safe component registration
const registration: ComponentRegistration<MyStringKeys, MyLanguages> = {
  component: {
    id: 'app',
    name: 'App',
    stringKeys: Object.values(MyStringKeys)
  },
  strings: {
    'en-US': {
      [MyStringKeys.Welcome]: 'Welcome',
      [MyStringKeys.Goodbye]: 'Goodbye'
    },
    'fr': {
      [MyStringKeys.Welcome]: 'Bienvenue',
      [MyStringKeys.Goodbye]: 'Au revoir'
    }
  }
};

Branded Enums

Branded enums enable runtime identification of string keys and collision detection between components. Unlike traditional TypeScript enums (erased at compile time), branded enums embed metadata for runtime component routing.

Why Branded Enums?

  • Runtime Identification: Determine which component a string key belongs to
  • Collision Detection: Detect key collisions between components automatically
  • Component Routing: Route translations to the correct handler when keys overlap
  • Zero Overhead: Values remain raw strings with embedded metadata

Creating Branded Enums

import { createI18nStringKeys, BrandedStringKeyValue } from '@digitaldefiance/i18n-lib';

// Create a branded enum for i18n keys
export const UserKeys = createI18nStringKeys('user-component', {
  Login: 'user.login',
  Logout: 'user.logout',
  Profile: 'user.profile',
} as const);

// Export the value type for type annotations
export type UserKeyValue = BrandedStringKeyValue<typeof UserKeys>;

Converting from Legacy Enums

import { createI18nStringKeysFromEnum } from '@digitaldefiance/i18n-lib';

// Legacy enum
enum LegacyUserKeys {
  Login = 'user.login',
  Logout = 'user.logout',
}

// Convert to branded enum
const BrandedUserKeys = createI18nStringKeysFromEnum('user-component', LegacyUserKeys);

Registering Branded Components

// Use registerBrandedComponent instead of registerComponent
engine.registerBrandedComponent({
  component: {
    id: 'user-component',
    name: 'User Component',
    brandedStringKeys: UserKeys,
  },
  strings: {
    [LanguageCodes.EN_US]: {
      [UserKeys.Login]: 'Log In',
      [UserKeys.Logout]: 'Log Out',
      [UserKeys.Profile]: 'My Profile',
    },
  },
});

Collision Detection

import { checkStringKeyCollisions } from '@digitaldefiance/i18n-lib';

// Check specific enums for collisions
const result = checkStringKeyCollisions(UserKeys, AdminKeys, CommonKeys);

if (result.hasCollisions) {
  console.warn('String key collisions detected:');
  result.collisions.forEach(c => {
    console.warn(`  "${c.value}" in: ${c.componentIds.join(', ')}`);
  });
}

// Or use the engine's collision report
const collisions = engine.getCollisionReport();
for (const [key, componentIds] of collisions) {
  console.warn(`Key "${key}" found in: ${componentIds.join(', ')}`);
}

Finding Key Sources

import { findStringKeySources, resolveStringKeyComponent } from '@digitaldefiance/i18n-lib';

// Find all components that have a specific key
const sources = findStringKeySources('user.login');
// Returns: ['i18n:user-component']

// Resolve to a single component (null if ambiguous)
const componentId = resolveStringKeyComponent('user.login');
// Returns: 'user-component'

Type Guards

import { isValidStringKey } from '@digitaldefiance/i18n-lib';

function handleKey(key: string) {
  if (isValidStringKey(key, UserKeys)) {
    // key is now typed as UserKeyValue
    return translateUserKey(key);
  }
  if (isValidStringKey(key, AdminKeys)) {
    // key is now typed as AdminKeyValue
    return translateAdminKey(key);
  }
  return key; // Unknown key
}

Merging Enums

import { mergeI18nStringKeys, getStringKeyValues } from '@digitaldefiance/i18n-lib';

// Create a combined key set for the entire app
const AllKeys = mergeI18nStringKeys('all-keys',
  CoreStringKeys,
  UserKeys,
  AdminKeys,
);

// Get all values from an enum
const allValues = getStringKeyValues(AllKeys);

Best Practices

  1. Use Namespaced Key Values: Prevent collisions with prefixed values

    // ✅ Good - namespaced values
    const Keys = createI18nStringKeys('user', {
      Welcome: 'user.welcome',
    } as const);
       
    // ❌ Bad - generic values may collide
    const Keys = createI18nStringKeys('user', {
      Welcome: 'welcome',
    } as const);
  2. Use Consistent Component IDs: Match IDs across enum creation and registration

  3. Always Use as const: Preserve literal types

    // ✅ Correct - literal types preserved
    const Keys = createI18nStringKeys('id', { A: 'a' } as const);
       
    // ❌ Wrong - types widened to string
    const Keys = createI18nStringKeys('id', { A: 'a' });
  4. Check for Collisions During Development:

    if (process.env.NODE_ENV === 'development') {
      const collisions = engine.getCollisionReport();
      if (collisions.size > 0) {
        console.warn('⚠️ String key collisions detected!');
      }
    }

For complete migration guide, see BRANDED_ENUM_MIGRATION.md.

String Key Enum Registration

Register branded string key enums with the engine for direct translation without specifying component IDs. This simplifies translation calls and enables automatic component routing.

Registering String Key Enums

import { createI18nStringKeysFromEnum, PluginI18nEngine } from '@digitaldefiance/i18n-lib';

// Create a branded enum from your string keys
enum MyStringKeys {
  Welcome = 'welcome',
  Goodbye = 'goodbye',
}

const BrandedKeys = createI18nStringKeysFromEnum('my-component', MyStringKeys);

// Create engine and register component
const engine = PluginI18nEngine.createInstance('myapp', languages);
engine.registerBrandedComponent({
  component: {
    id: 'my-component',
    name: 'My Component',
    brandedStringKeys: BrandedKeys,
  },
  strings: {
    [LanguageCodes.EN_US]: {
      [BrandedKeys.Welcome]: 'Welcome!',
      [BrandedKeys.Goodbye]: 'Goodbye!',
    },
  },
});

// Register the enum for direct translation
engine.registerStringKeyEnum(BrandedKeys);

Direct Translation with translateStringKey

Once registered, translate keys directly without specifying the component ID:

// Before: Required component ID
const text = engine.translate('my-component', BrandedKeys.Welcome, { name: 'Alice' });

// After: Component ID resolved automatically from branded enum
const text = engine.translateStringKey(BrandedKeys.Welcome, { name: 'Alice' });

// Safe version returns placeholder on failure instead of throwing
const safeText = engine.safeTranslateStringKey(BrandedKeys.Welcome, { name: 'Alice' });

Checking Registration Status

// Check if an enum is registered
if (engine.hasStringKeyEnum(BrandedKeys)) {
  console.log('BrandedKeys is registered');
}

// Get all registered enums
const registeredEnums = engine.getStringKeyEnums();

Benefits

  • Cleaner Code: No need to repeat component IDs in every translation call
  • Automatic Routing: Component ID resolved from branded enum metadata
  • Type Safety: Full TypeScript support with branded enum types
  • Idempotent: Safe to call registerStringKeyEnum() multiple times

Branded Enum Translation

The enum translation system supports branded enums from @digitaldefiance/branded-enum, enabling automatic name inference and type-safe enum value translations.

Why Use Branded Enums for Enum Translation?

  • Automatic Name Inference: No need to provide an explicit enum name - it's extracted from the branded enum's component ID
  • Type Safety: Full TypeScript support for enum values and translations
  • Consistent Naming: Enum names in error messages match your component IDs
  • Unified Pattern: Use the same branded enum pattern for both string keys and enum translations

Registering Branded Enums for Translation

import { createBrandedEnum } from '@digitaldefiance/branded-enum';
import { I18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';

// Create a branded enum
const Status = createBrandedEnum('status', {
  Active: 'active',
  Inactive: 'inactive',
  Pending: 'pending',
});

// Create engine
const engine = new I18nEngine([
  { id: LanguageCodes.EN_US, name: 'English', code: 'en-US', isDefault: true },
  { id: LanguageCodes.ES, name: 'Spanish', code: 'es' },
]);

// Register branded enum - name is automatically inferred as 'status'
engine.registerEnum(Status, {
  [LanguageCodes.EN_US]: {
    active: 'Active',
    inactive: 'Inactive',
    pending: 'Pending',
  },
  [LanguageCodes.ES]: {
    active: 'Activo',
    inactive: 'Inactivo',
    pending: 'Pendiente',
  },
});

// Translate enum values
engine.translateEnum(Status, Status.Active, LanguageCodes.EN_US); // 'Active'
engine.translateEnum(Status, Status.Active, LanguageCodes.ES);    // 'Activo'

Comparison: Traditional vs Branded Enum Registration

// Traditional enum - requires explicit name
enum TraditionalStatus {
  Active = 'active',
  Inactive = 'inactive',
}
engine.registerEnum(TraditionalStatus, translations, 'Status'); // Name required

// Branded enum - name inferred from component ID
const BrandedStatus = createBrandedEnum('status', {
  Active: 'active',
  Inactive: 'inactive',
});
engine.registerEnum(BrandedStatus, translations); // Name 'status' inferred automatically

Using with PluginI18nEngine

The PluginI18nEngine also supports branded enum translation with language validation:

import { PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';
import { createBrandedEnum } from '@digitaldefiance/branded-enum';

const Priority = createBrandedEnum('priority', {
  High: 'high',
  Medium: 'medium',
  Low: 'low',
});

const engine = PluginI18nEngine.createInstance('myapp', [
  { id: LanguageCodes.EN_US, name: 'English', code: 'en-US', isDefault: true },
  { id: LanguageCodes.FR, name: 'French', code: 'fr' },
]);

// Register with automatic name inference
engine.registerEnum(Priority, {
  [LanguageCodes.EN_US]: {
    high: 'High Priority',
    medium: 'Medium Priority',
    low: 'Low Priority',
  },
  [LanguageCodes.FR]: {
    high: 'Haute Priorité',
    medium: 'Priorité Moyenne',
    low: 'Basse Priorité',
  },
});

// Translate
engine.translateEnum(Priority, Priority.High); // Uses current language
engine.translateEnum(Priority, Priority.High, LanguageCodes.FR); // 'Haute Priorité'

Utility Functions

The library provides utility functions for working with branded enums:

import { 
  isBrandedEnum, 
  getBrandedEnumComponentId, 
  getBrandedEnumId 
} from '@digitaldefiance/i18n-lib';

// Check if an enum is branded
if (isBrandedEnum(Status)) {
  // TypeScript knows Status is a branded enum here
  const componentId = getBrandedEnumComponentId(Status);
  console.log(componentId); // 'status'
}

// Get the raw brand ID (includes 'i18n:' prefix for i18n string keys)
const rawId = getBrandedEnumId(Status); // 'status'

Error Messages with Branded Enums

When translation errors occur, the enum name in error messages is automatically derived from the branded enum's component ID:

const Status = createBrandedEnum('user-status', { Active: 'active' });
engine.registerEnum(Status, { en: { active: 'Active' } });

// If translation fails, error message includes the inferred name:
// "No translations found for enum: user-status"
// "Translation missing for enum value 'unknown' in language 'en'"

Type Definitions

New types are available for working with branded enum translations:

import type {
  BrandedEnumTranslation,
  RegisterableEnum,
  TranslatableEnumValue,
} from '@digitaldefiance/i18n-lib';

// Type for branded enum translation maps
type MyTranslations = BrandedEnumTranslation<typeof Status, 'en' | 'es'>;

// Union type accepting both traditional and branded enums
function registerAnyEnum<T extends string | number>(
  enumObj: RegisterableEnum<T>,
  translations: Record<string, Record<string, string>>,
): void {
  // Works with both enum types
}

// Union type for translatable values
function translateAnyValue<T extends string | number>(
  value: TranslatableEnumValue<T>,
): string {
  // Works with both traditional and branded enum values
}

Monorepo i18n-setup Guide

When building an application that consumes multiple Express Suite packages (e.g., suite-core-lib, ecies-lib), you need a single i18n-setup.ts file that initializes the engine, registers all components and their branded string key enums, sets up the global context, and exports translation helpers.

Recommended: Factory Approach (createI18nSetup)

The createI18nSetup() factory replaces ~200 lines of manual boilerplate with a single function call. It handles engine creation, core component registration, library component registration, branded enum registration, and context initialization automatically.

// i18n-setup.ts — Application-level i18n initialization (factory approach)

import {
  createI18nSetup,
  createI18nStringKeys,
  LanguageCodes,
} from '@digitaldefiance/i18n-lib';
import { createSuiteCoreComponentPackage } from '@digitaldefiance/suite-core-lib';
import { createEciesComponentPackage } from '@digitaldefiance/ecies-lib';

// 1. Define your application component
export const AppComponentId = 'MyApp';

export const AppStringKey = createI18nStringKeys(AppComponentId, {
  SiteTitle: 'siteTitle',
  SiteDescription: 'siteDescription',
  WelcomeMessage: 'welcomeMessage',
} as const);

const appStrings = {
  [LanguageCodes.EN_US]: {
    siteTitle: 'My Application',
    siteDescription: 'An Express Suite application',
    welcomeMessage: 'Welcome, {name}!',
  },
  [LanguageCodes.FR]: {
    siteTitle: 'Mon Application',
    siteDescription: 'Une application Express Suite',
    welcomeMessage: 'Bienvenue, {name} !',
  },
};

// 2. Create the i18n setup — one call does everything
const i18n = createI18nSetup({
  componentId: AppComponentId,
  stringKeyEnum: AppStringKey,
  strings: appStrings,
  aliases: ['AppStringKey'],
  libraryComponents: [
    createSuiteCoreComponentPackage(),
    createEciesComponentPackage(),
  ],
});

// 3. Export the public API
export const { engine: i18nEngine, translate, safeTranslate } = i18n;
export const i18nContext = i18n.context;

The factory:

  • Creates or reuses an I18nEngine instance (idempotent via instanceKey, defaults to 'default')
  • Registers the Core i18n component automatically
  • Registers each library component's ComponentConfig and branded stringKeyEnum from the I18nComponentPackage
  • Registers your application component and its branded enum
  • Initializes GlobalActiveContext with the specified defaultLanguage (defaults to 'en-US')
  • Returns an I18nSetupResult with engine, translate, safeTranslate, context, setLanguage, setAdminLanguage, setContext, getLanguage, getAdminLanguage, and reset

Calling createI18nSetup() multiple times with the same instanceKey reuses the existing engine — safe for monorepos where a subset library and a superset API both call the factory.

I18nComponentPackage Interface

Library authors bundle a ComponentConfig with its branded string key enum in a single I18nComponentPackage object. This lets the factory auto-register both the component and its enum in one step.

import type { AnyBrandedEnum } from '@digitaldefiance/branded-enum';
import type { ComponentConfig } from '@digitaldefiance/i18n-lib';

interface I18nComponentPackage {
  readonly config: ComponentConfig;
  readonly stringKeyEnum?: AnyBrandedEnum;
}

Each library exports a createXxxComponentPackage() function:

// In suite-core-lib
import { createSuiteCoreComponentPackage } from '@digitaldefiance/suite-core-lib';
const pkg = createSuiteCoreComponentPackage();
// pkg.config  → SuiteCore ComponentConfig
// pkg.stringKeyEnum → SuiteCoreStringKey branded enum

// In ecies-lib
import { createEciesComponentPackage } from '@digitaldefiance/ecies-lib';
const pkg = createEciesComponentPackage();

// In node-ecies-lib
import { createNodeEciesComponentPackage } from '@digitaldefiance/node-ecies-lib';
const pkg = createNodeEciesComponentPackage();

The existing createSuiteCoreComponentConfig() and createEciesComponentConfig() functions remain available for consumers that prefer the manual approach.

Browser-Safe Fallback

In browser environments, bundlers like Vite and webpack may create separate copies of @digitaldefiance/branded-enum, causing isBrandedEnum() to fail due to Symbol/WeakSet identity mismatch. This breaks registerStringKeyEnum() and translateStringKey().

The engine now includes a transparent fallback: when the StringKeyEnumRegistry fails to resolve a component ID for a known string key value, the engine scans all registered components' string keys to find the matching component. The result is cached in a ValueComponentLookupCache for subsequent lookups, and the cache is invalidated whenever a new component is registered.

This means consumers do not need manual workarounds (like safeRegisterStringKeyEnum or _componentLookup maps) for bundler Symbol mismatch issues. Both translateStringKey and safeTranslateStringKey use the fallback automatically.

Advanced: Manual Setup

For advanced use cases where you need full control over engine creation, validation options, or custom registration order, you can use the manual approach:

// i18n-setup.ts — Manual approach (advanced)

import {
  I18nBuilder,
  I18nEngine,
  LanguageCodes,
  GlobalActiveContext,
  getCoreLanguageDefinitions,
  createCoreComponentRegistration,
  createI18nStringKeys,
  type CoreLanguageCode,
  type IActiveContext,
  type ComponentConfig,
  type LanguageContextSpace,
} from '@digitaldefiance/i18n-lib';
import type { BrandedEnumValue } from '@digitaldefiance/branded-enum';
import {
  createSuiteCoreComponentConfig,
  SuiteCoreStringKey,
} from '@digitaldefiance/suite-core-lib';
import {
  createEciesComponentConfig,
  EciesStringKey,
} from '@digitaldefiance/ecies-lib';

export const AppComponentId = 'MyApp';

export const AppStringKey = createI18nStringKeys(AppComponentId, {
  SiteTitle: 'siteTitle',
  SiteDescription: 'siteDescription',
  WelcomeMessage: 'welcomeMessage',
} as const);

export type AppStringKeyValue = BrandedEnumValue<typeof AppStringKey>;

const appStrings: Record<string, Record<string, string>> = {
  [LanguageCodes.EN_US]: {
    siteTitle: 'My Application',
    siteDescription: 'An Express Suite application',
    welcomeMessage: 'Welcome, {name}!',
  },
  [LanguageCodes.FR]: {
    siteTitle: 'Mon Application',
    siteDescription: 'Une application Express Suite',
    welcomeMessage: 'Bienvenue, {name} !',
  },
};

function createAppComponentConfig(): ComponentConfig {
  return { id: AppComponentId, strings: appStrings, aliases: ['AppStringKey'] };
}

// Create or reuse engine
let i18nEngine: I18nEngine;
if (I18nEngine.hasInstance('default')) {
  i18nEngine = I18nEngine.getInstance('default');
} else {
  i18nEngine = I18nBuilder.create()
    .withLanguages(getCoreLanguageDefinitions())
    .withDefaultLanguage(LanguageCodes.EN_US)
    .withFallbackLanguage(LanguageCodes.EN_US)
    .withInstanceKey('default')
    .build();
}

// Register components
const coreReg = createCoreComponentRegistration();
i18nEngine.registerIfNotExists({
  id: coreReg.component.id,
  strings: coreReg.strings as Record<string, Record<string, string>>,
});
i18nEngine.registerIfNotExists(createSuiteCoreComponentConfig());
i18nEngine.registerIfNotExists(createEciesComponentConfig());
i18nEngine.registerIfNotExists(createAppComponentConfig());

// Register branded enums
if (!i18nEngine.hasStringKeyEnum(SuiteCoreStringKey)) {
  i18nEngine.registerStringKeyEnum(SuiteCoreStringKey);
}
if (!i18nEngine.hasStringKeyEnum(EciesStringKey)) {
  i18nEngine.registerStringKeyEnum(EciesStringKey);
}
if (!i18nEngine.hasStringKeyEnum(AppStringKey)) {
  i18nEngine.registerStringKeyEnum(AppStringKey);
}

// Initialize context
const globalContext = GlobalActiveContext.getInstance<
  CoreLanguageCode,
  IActiveContext<CoreLanguageCode>
>();
globalContext.createContext(LanguageCodes.EN_US, LanguageCodes.EN_US, AppComponentId);

// Export helpers
export { i18nEngine };

export const translate = (
  name: AppStringKeyValue,
  variables?: Record<string, string | number>,
  language?: CoreLanguageCode,
  context?: LanguageContextSpace,
): string => {
  const activeContext =
    context ?? globalContext.getContext(AppComponentId).currentContext;
  const lang =
    language ??
    (activeContext === 'admin'
      ? globalContext.getContext(AppComponentId).adminLanguage
      : globalContext.getContext(AppComponentId).language);
  return i18nEngine.translateStringKey(name, variables, lang);
};

export const safeTranslate = (
  name: AppStringKeyValue,
  variables?: Record<string, string | number>,
  language?: CoreLanguageCode,
  context?: LanguageContextSpace,
): string => {
  const activeContext =
    context ?? globalContext.getContext(AppComponentId).currentContext;
  const lang =
    language ??
    (activeContext === 'admin'
      ? globalContext.getContext(AppComponentId).adminLanguage
      : globalContext.getContext(AppComponentId).language);
  return i18nEngine.safeTranslateStringKey(name, variables, lang);
};

Key points for the manual approach:

  • Idempotent engine creation: I18nEngine.hasInstance('default') checks for an existing engine before building a new one.
  • Core component first: Always register the Core component before other components — it provides error message translations used internally.
  • registerIfNotExists: All component registrations use the idempotent variant so multiple packages can safely register without conflicts.
  • hasStringKeyEnum guard: Prevents duplicate enum registration when multiple setup files run in the same process.
  • Context-aware helpers: The translate and safeTranslate functions resolve the active language from GlobalActiveContext, respecting user vs. admin context.

Migration Guide

Converting an existing manual i18n-setup.ts to the factory approach is straightforward. There are no breaking changes — the factory produces the same engine state as the manual approach.

Before (manual)

import { I18nBuilder, I18nEngine, LanguageCodes, GlobalActiveContext, ... } from '@digitaldefiance/i18n-lib';
import { createSuiteCoreComponentConfig, SuiteCoreStringKey } from '@digitaldefiance/suite-core-lib';
import { createEciesComponentConfig, EciesStringKey } from '@digitaldefiance/ecies-lib';

// ~200 lines: engine creation, core registration, library registration,
// enum registration, context initialization, translate helpers...

After (factory)

import { createI18nSetup, createI18nStringKeys, LanguageCodes } from '@digitaldefiance/i18n-lib';
import { createSuiteCoreComponentPackage } from '@digitaldefiance/suite-core-lib';
import { createEciesComponentPackage } from '@digitaldefiance/ecies-lib';

const i18n = createI18nSetup({
  componentId: AppComponentId,
  stringKeyEnum: AppStringKey,
  strings: appStrings,
  libraryComponents: [
    createSuiteCoreComponentPackage(),
    createEciesComponentPackage(),
  ],
});

export const { engine: i18nEngine, translate, safeTranslate } = i18n;

Migration steps

  1. Replace createXxxComponentConfig imports with createXxxComponentPackage imports
  2. Replace manual engine creation (I18nBuilder / I18nEngine.registerIfNotExists) with createI18nSetup()
  3. Move library component registrations into the libraryComponents array
  4. Remove manual registerStringKeyEnum calls — the factory handles them
  5. Remove manual GlobalActiveContext initialization — the factory handles it
  6. Destructure the returned I18nSetupResult to get engine, translate, safeTranslate, etc.

Notes

  • Existing createXxxComponentConfig() functions remain available for consumers that prefer the manual approach
  • The factory uses the same registerIfNotExists pattern internally, so it is safe to mix factory and manual consumers sharing the same engine instance
  • There are no breaking changes or behavioral differences between the manual and factory approaches

createI18nStringKeys vs createI18nStringKeysFromEnum

Both functions produce identical BrandedStringKeys output. Choose based on your starting point.

createI18nStringKeys — preferred for new code

Creates a branded enum directly from an as const object literal:

import { createI18nStringKeys } from '@digitaldefiance/i18n-lib';

export const AppStringKey = createI18nStringKeys('my-app', {
  Welcome: 'welcome',
  Goodbye: 'goodbye',
} as const);

Use this when writing a new component from scratch. The as const assertion preserves literal types so each key is fully type-safe.

createI18nStringKeysFromEnum — useful for migration

Wraps an existing TypeScript enum into a branded enum:

import { createI18nStringKeysFromEnum } from '@digitaldefiance/i18n-lib';

// Existing traditional enum
enum LegacyStringKeys {
  Welcome = 'welcome',
  Goodbye = 'goodbye',
}

export const AppStringKey = createI18nStringKeysFromEnum(
  'my-app',
  LegacyStringKeys,
);

Use this when migrating code that already has a traditional enum. Internally, createI18nStringKeysFromEnum filters out TypeScript's reverse numeric mappings and then delegates to createI18nStringKeys.

Comparison

| Aspect | createI18nStringKeys | createI18nStringKeysFromEnum | |---|---|---| | Input | Object literal with as const | Existing TypeScript enum | | Use case | New code, fresh components | Migrating existing enum-based code | | Output | BrandedStringKeys<T> | BrandedStringKeys<T> | | Internal behavior | Calls createBrandedEnum directly | Filters reverse numeric mappings, then delegates to createI18nStringKeys |

Troubleshooting: Branded Enum Module Identity in Monorepos

Symptom

registerStringKeyEnum() throws or hasStringKeyEnum() returns false for a branded enum that was created with createI18nStringKeys or createI18nStringKeysFromEnum.

Root Cause

isBrandedEnum() returns false when the @digitaldefiance/branded-enum global registry holds a different module instance. This happens when multiple copies of the package are installed — each copy has its own WeakSet / Symbol registry, so enums created by one copy are not recognized by another.

Diagnosis

Check for duplicate installations:

# npm
npm ls @digitaldefiance/branded-enum

# yarn
yarn why @digitaldefiance/branded-enum

If you see more than one resolved version (or multiple paths), the registry is split.

Solutions

  1. Single version via resolutions/overrides — pin a single version in your root package.json:

    // npm (package.json)
    "overrides": {
      "@digitaldefiance/branded-enum": "<version>"
    }
    
    // yarn (package.json)
    "resolutions": {
      "@digitaldefiance/branded-enum": "<version>"
    }
  2. Bundler deduplication — if you use webpack, Rollup, or esbuild, ensure the @digitaldefiance/branded-enum module is resolved to a single path. For webpack, the resolve.alias or resolve.dedupe options can help.

  3. Consistent package resolution — in Nx or other monorepo tools, verify that all projects resolve the same physical copy. Running nx graph can help visualize dependency relationships.

Browser Support

  • Chrome/Edge: Latest 2 versions (minimum: Chrome 90, Edge 90)
  • Firefox: Latest 2 versions (minimum: Firefox 88)
  • Safari: Latest 2 versions (minimum: Safari 14)
  • Node.js: 18+

Polyfills: Not required for supported versions. For older browsers, you may need:

  • Intl.PluralRules polyfill
  • Intl.NumberFormat polyfill

Troubleshooting

Component Not Found Error

Problem: RegistryError: Component 'xyz' not found

Solutions:

  1. Ensure component is registered before use: engine.registerComponent({...})
  2. Check component ID spelling matches exactly
  3. Verify registration completed successfully (no errors thrown)

Translation Returns [componentId.key]

Problem: Translation returns placeholder instead of translated text

Solutions:

  1. Check string key exists in component registration
  2. Verify current language has translation for this key
  3. Use engine.validate() to find missing translations
  4. Check if using safeTranslate() (returns placeholder on error)

Plural Forms Not Working

Problem: Always shows same plural form regardless of count

Solutions:

  1. Ensure passing count variable: engine.translate('id', 'key', { count: 5 })
  2. Use createPluralString() helper to create plural object
  3. Verify language has correct plural rules (see PLURA