@digitaldefiance/i18n-lib
v4.7.0
Published
i18n library with enum translation support
Maintainers
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/numberpatterns 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}
- Component references:
- 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-libQuick 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
- docs/ICU_MESSAGEFORMAT.md - Complete guide with syntax reference and examples
- docs/ICU_COMPREHENSIVE_VALIDATION.md - Validation report with test coverage
- docs/ICU_PROJECT_COMPLETE.md - Implementation summary
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 typeConstantVariables<T>— Builds aPartial<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 winDirect 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 valueUse 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:
isValid—trueif all template variables have matching constantsmissingConstants— variable names referenced in templates but missing from constantsunusedConstants— constant keys registered but never referenced in any templatereferencedVariables— 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 languageUse 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 globallyUse 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 messageTranslation 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 instancegetInstance<TLanguage>(key?: string)- Get instance by keyhasInstance(key?: string)- Check if instance existsremoveInstance(key?: string)- Remove instanceresetAll()- Reset all instances
Instance Methods
registerComponent(registration: ComponentRegistration)- Register componenttranslate(componentId: string, key: string, variables?, language?)- Translate stringsafeTranslate(componentId: string, key: string, variables?, language?)- Safe translate with fallbackt(template: string, variables?, language?)- Process template stringsetLanguage(language: TLanguage)- Set current languagesetAdminLanguage(language: TLanguage)- Set admin languagegetCurrentLanguage()- Get current languagegetLanguages()- Get all languageshasLanguage(language: TLanguage)- Check if language existsswitchToAdmin()- Switch to admin contextswitchToUser()- Switch to user contextvalidate()- Validate all componentsregisterBrandedComponent(registration)- Register component with branded string keysgetCollisionReport()- Get map of key collisions across componentsregisterStringKeyEnum(enum)- Register a branded string key enum for direct translationtranslateStringKey(key, variables?, language?)- Translate a branded string key directlysafeTranslateStringKey(key, variables?, language?)- Safe version returning placeholder on failurehasStringKeyEnum(enum)- Check if a branded enum is registeredgetStringKeyEnums()- Get all registered branded enumsregisterConstants(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 constantsmergeConstants(constants)- Merge into engine-level constantshasConstants(componentId)- Check if constants are registered for a componentgetConstants(componentId)- Get constants for a specific componentgetAllConstants()- Get all registered constants entriesresolveConstantOwner(key)- Resolve which component owns a constant key
Branded Enum Functions
createI18nStringKeys(componentId, keys)- Create a branded enum for i18n keyscreateI18nStringKeysFromEnum(componentId, enum)- Convert legacy enum to branded enummergeI18nStringKeys(newId, ...enums)- Merge multiple branded enumsfindStringKeySources(key)- Find components containing a keyresolveStringKeyComponent(key)- Resolve key to single componentgetStringKeysByComponentId(id)- Get enum by component IDgetRegisteredI18nComponents()- List all registered componentsgetStringKeyValues(enum)- Get all values from enumisValidStringKey(value, enum)- Type guard for key validationcheckStringKeyCollisions(...enums)- Check enums for collisions
Core Functions
getCoreI18nEngine()- Get core engine with system stringscreateCoreI18nEngine(instanceKey?)- Create core engine instancegetCoreTranslation(stringKey, variables?, language?, instanceKey?)- Get core translationsafeCoreTranslation(stringKey, variables?, language?, instanceKey?)- Safe core translationgetCoreLanguageCodes()- Get array of core language codesgetCoreLanguageDefinitions()- Get core language definitionsgetUtcDateVars()- 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
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);Use Consistent Component IDs: Match IDs across enum creation and registration
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' });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 automaticallyUsing 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
I18nEngineinstance (idempotent viainstanceKey, defaults to'default') - Registers the Core i18n component automatically
- Registers each library component's
ComponentConfigand brandedstringKeyEnumfrom theI18nComponentPackage - Registers your application component and its branded enum
- Initializes
GlobalActiveContextwith the specifieddefaultLanguage(defaults to'en-US') - Returns an
I18nSetupResultwithengine,translate,safeTranslate,context,setLanguage,setAdminLanguage,setContext,getLanguage,getAdminLanguage, andreset
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.hasStringKeyEnumguard: Prevents duplicate enum registration when multiple setup files run in the same process.- Context-aware helpers: The
translateandsafeTranslatefunctions resolve the active language fromGlobalActiveContext, 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
- Replace
createXxxComponentConfigimports withcreateXxxComponentPackageimports - Replace manual engine creation (
I18nBuilder/I18nEngine.registerIfNotExists) withcreateI18nSetup() - Move library component registrations into the
libraryComponentsarray - Remove manual
registerStringKeyEnumcalls — the factory handles them - Remove manual
GlobalActiveContextinitialization — the factory handles it - Destructure the returned
I18nSetupResultto getengine,translate,safeTranslate, etc.
Notes
- Existing
createXxxComponentConfig()functions remain available for consumers that prefer the manual approach - The factory uses the same
registerIfNotExistspattern 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-enumIf you see more than one resolved version (or multiple paths), the registry is split.
Solutions
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>" }Bundler deduplication — if you use webpack, Rollup, or esbuild, ensure the
@digitaldefiance/branded-enummodule is resolved to a single path. For webpack, theresolve.aliasorresolve.dedupeoptions can help.Consistent package resolution — in Nx or other monorepo tools, verify that all projects resolve the same physical copy. Running
nx graphcan 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.PluralRulespolyfillIntl.NumberFormatpolyfill
Troubleshooting
Component Not Found Error
Problem: RegistryError: Component 'xyz' not found
Solutions:
- Ensure component is registered before use:
engine.registerComponent({...}) - Check component ID spelling matches exactly
- Verify registration completed successfully (no errors thrown)
Translation Returns [componentId.key]
Problem: Translation returns placeholder instead of translated text
Solutions:
- Check string key exists in component registration
- Verify current language has translation for this key
- Use
engine.validate()to find missing translations - Check if using
safeTranslate()(returns placeholder on error)
Plural Forms Not Working
Problem: Always shows same plural form regardless of count
Solutions:
- Ensure passing
countvariable:engine.translate('id', 'key', { count: 5 }) - Use
createPluralString()helper to create plural object - Verify language has correct plural rules (see PLURA
