i18n library with enum translation support
npm install @digitaldefiance/i18n-libA production-ready TypeScript internationalization library with component-based architecture, type-safe translations, and comprehensive error handling.
Part of Express Suite
✨ String Key Enum Registration - Register branded string key enums for direct translation without specifying component IDs.
Prerequisites: Requires branded enums from v4.0.4+ (see Branded Enums section)
New Features:
- registerStringKeyEnum(): Register branded enums for automatic component routing
- translateStringKey(): Translate keys directly - component ID resolved from branded enum
- safeTranslateStringKey(): Safe version returning placeholder on failure
- hasStringKeyEnum() / getStringKeyEnums(): Query registered enums
``typescript
import { createI18nStringKeysFromEnum, PluginI18nEngine, LanguageCodes } from '@digitaldefiance/i18n-lib';
// First, create a branded enum from your string keys
enum MyStringKeys {
Welcome = 'welcome',
Goodbye = 'goodbye',
}
const MyBrandedKeys = createI18nStringKeysFromEnum('my-component', MyStringKeys);
// Register your branded enum
engine.registerStringKeyEnum(MyBrandedKeys);
// Translate without component ID - it's resolved automatically
const text = engine.translateStringKey(MyBrandedKeys.Welcome, { name: 'Alice' });
`
- 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.key}}
- Alias resolution: {{EnumName.value}}
- Enum name resolution: {variable}
- Variable substitution: {currency}
- Context variables: , {timezone}, {language}
- Context Integration: Automatic injection of currency, timezone, and language 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
`bash`
npm install @digitaldefiance/i18n-libor
yarn add @digitaldefiance/i18n-lib
`typescript
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"
`
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
`typescript
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"
`
- ✅ 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
- 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
`typescript`
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';
---
Automatic plural form selection based on count with CLDR-compliant rules for 37 languages:
`typescript
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-aware translations with intelligent fallback:
`typescript
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"
`
Nested plural and gender forms:
`typescript
// 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'
}
};
`
`typescript
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'
});
`
`typescript
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);
}
`
The main engine class that manages translations, languages, and components.
`typescript
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);
`
Components group related translations together:
`typescript
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: { / ... / }
});
`
`typescript
// 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]"
`
`typescript
// 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
// 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 > constants
engine.t('{AppName}'); // Uses constant
engine.t('{currency}'); // Uses context
engine.t('{currency}', { currency: 'GBP' }); // Uses provided (overrides context)
// Mixed patterns
engine.t('{{auth.login}}: {username} ({currency})', { username: 'admin' });
`
`typescript
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():
`typescript
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);
`
Automatic injection of currency, timezone, and language from GlobalActiveContext:
`typescript
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"
// Override context with provided variables
engine.t('Price in {currency}', { currency: 'USD' }); // "Price in USD"
`
Manage application-wide constants for use in translations:
`typescript
// Merge constants (adds/updates specific keys, preserves others)
engine.mergeConstants({ Version: '2.0', NewKey: 'value' });
// Existing constants preserved, specified ones added/updated
// Update all constants (replaces entire constants object)
engine.updateConstants({ Site: 'NewSite', Version: '2.0' });
// All previous constants removed, only these remain
`
When to use:
- Constants: Application-wide values that rarely change (AppName, Version)
- Variables: Request-specific or dynamic values passed to translate()
- Context: User-specific values (currency, timezone, language)
`typescript
// 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();
`
Separate language for admin interfaces (useful for multi-tenant applications where admins need consistent UI language regardless of user's language):
`typescript
// 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
Pre-built translations for common UI elements in 8 languages:
`typescript
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.
Create isolated engines for different parts of your application (useful for micro-frontends, plugins, or multi-tenant systems):
`typescript
// 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)
Errors related to component/language registration:
`typescript
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' }
}
}
`
Base class for errors with translated messages:
`typescript
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
`
Adapt PluginI18nEngine to simpler TranslationEngine interface (useful when integrating with error classes or other components expecting a simplified translation interface):
`typescript
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' });
`
Built-in language codes following BCP 47 standard:
`typescript
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:
`typescript
// Define your custom language type
type MyLanguages = CoreLanguageCode | 'pt-BR' | 'it' | 'nl';
// Create engine with custom languages
const engine = PluginI18nEngine.createInstance
{ 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.
Static Methods
- createInstance - Create named instancegetInstance
- - 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 enums
- 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
- 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 definitions
`typescript
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');
});
});
`
Full TypeScript support with generic types:
`typescript
// Type-safe language codes
type MyLanguages = 'en-US' | 'fr' | 'es';
const engine = PluginI18nEngine.createInstance
// Type-safe string keys
enum MyStringKeys {
Welcome = 'welcome',
Goodbye = 'goodbye'
}
// Type-safe component registration
const registration: ComponentRegistration
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 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.
- 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
`typescript
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
`
`typescript
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);
`
`typescript`
// 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',
},
},
});
`typescript
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(', ')});`
}
`typescript
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'
`
`typescript
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
}
`
`typescript
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);
`
1. Use Namespaced Key Values: Prevent collisions with prefixed values
`typescript`
// ✅ 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
`typescript`
// ✅ 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:
`typescript`
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.
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
`typescript
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:
`typescript
// 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
`typescript
// 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
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
`typescript
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
`typescript
// 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:
`typescript
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:
`typescript
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:
`typescript
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:
`typescript
import type {
BrandedEnumTranslation,
RegisterableEnum,
TranslatableEnumValue,
} from '@digitaldefiance/i18n-lib';
// Type for branded enum translation maps
type MyTranslations = BrandedEnumTranslation
// Union type accepting both traditional and branded enums
function registerAnyEnum
enumObj: RegisterableEnum
translations: Record
): void {
// Works with both enum types
}
// Union type for translatable values
function translateAnyValue
value: TranslatableEnumValue
): string {
// Works with both traditional and branded enum values
}
`
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.
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.
`typescript
// 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')ComponentConfig
- Registers the Core i18n component automatically
- Registers each library component's and branded stringKeyEnum from the I18nComponentPackageGlobalActiveContext
- Registers your application component and its branded enum
- Initializes with the specified defaultLanguage (defaults to 'en-US')I18nSetupResult
- Returns an 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.
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.
`typescript
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:
`typescript
// 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.
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.
For advanced use cases where you need full control over engine creation, validation options, or custom registration order, you can use the manual approach:
Click to expand manual i18n-setup.ts example
`typescript
// 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
const appStrings: Record
[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
});
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
>();
globalContext.createContext(LanguageCodes.EN_US, LanguageCodes.EN_US, AppComponentId);
// Export helpers
export { i18nEngine };
export const translate = (
name: AppStringKeyValue,
variables?: Record
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
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.registerIfNotExists
- Core component first: Always register the Core component before other components — it provides error message translations used internally.
- : 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.translate
- Context-aware helpers: The and safeTranslate functions resolve the active language from GlobalActiveContext, respecting user vs. admin context.
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)
`typescript
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)
`typescript
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 importsI18nBuilder
2. Replace manual engine creation ( / I18nEngine.registerIfNotExists) with createI18nSetup()libraryComponents
3. Move library component registrations into the arrayregisterStringKeyEnum
4. Remove manual calls — the factory handles themGlobalActiveContext
5. Remove manual initialization — the factory handles itI18nSetupResult
6. Destructure the returned to get engine, translate, safeTranslate, etc.
#### Notes
- Existing createXxxComponentConfig() functions remain available for consumers that prefer the manual approachregisterIfNotExists
- The factory uses the same 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
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:
`typescript
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:
`typescript
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 |as const
|---|---|---|
| Input | Object literal with | Existing TypeScript enum |BrandedStringKeys
| Use case | New code, fresh components | Migrating existing enum-based code |
| Output | | BrandedStringKeys |createBrandedEnum
| Internal behavior | Calls directly | Filters reverse numeric mappings, then delegates to createI18nStringKeys |
#### 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:
`bashnpm
npm ls @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:
`jsonc
// npm (package.json)
"overrides": {
"@digitaldefiance/branded-enum": ""
} // yarn (package.json)
"resolutions": {
"@digitaldefiance/branded-enum": ""
}
`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 polyfillTroubleshooting
$3
Problem:
RegistryError: Component 'xyz' not foundSolutions:
1. Ensure component is registered before use:
engine.registerComponent({...})
2. Check component ID spelling matches exactly
3. Verify registration completed successfully (no errors thrown)$3
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)$3
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 PLURALIZATION_SUPPORT.md)
4. Check required plural forms for language: getRequiredPluralForms('ru')$3
Problem: ICU syntax appears as literal text
Solutions:
1. Use
formatICUMessage() function, not translate()
2. Check ICU syntax is valid: validateICUMessage(message)
3. Ensure variables are provided: formatICUMessage(msg, { count: 1 })
4. See docs/ICU_MESSAGEFORMAT.md for syntax$3
Problem: Memory usage grows over time
Solutions:
1. Call
PluginI18nEngine.removeInstance(key) when done
2. Use resetAll() in test cleanup
3. Reuse instances instead of creating new ones
4. Check for circular references in custom code$3
Problem: Type errors with language codes or string keys
Solutions:
1. Use generic types:
PluginI18nEngine.createInstance
2. Ensure as const on string key objects
3. Import types: import type { ComponentRegistration } from '@digitaldefiance/i18n-lib'
4. Check TypeScript version (4.5+ recommended)FAQ
$3
Use ICU MessageFormat when:
- You need complex pluralization (multiple forms)
- You need gender-specific translations
- You need number/date/time formatting
- You need nested conditional logic
Use simple templates when:
- You only need variable substitution
- You're referencing other components ({{Component.key}})
- You want simpler, more readable strings
$3
Options:
1. Use
safeTranslate() - returns [componentId.key] placeholder
2. Set fallback language in config
3. Use engine.validate() to find missing translations during development
4. Implement custom error handling with try/catch$3
Yes, but you lose type safety benefits:
`javascript
const { PluginI18nEngine, LanguageCodes } = require('@digitaldefiance/i18n-lib');
const engine = PluginI18nEngine.createInstance('app', languages);
`$3
Steps:
1. Add language definition to engine creation
2. Register components with translations for new language
3. If using plurals, check required forms:
getRequiredPluralForms('pt-BR')
4. See docs/ADDING_LANGUAGES.md for details$3
Branded enums (v4.0.4+):
- Runtime identification of component ownership
- Collision detection between components
- Automatic component routing
- Created with
createI18nStringKeys()Regular enums:
- Compile-time only (erased at runtime)
- No collision detection
- Must specify component ID in every translate call
$3
Key changes:
1.
CoreLanguage enum → LanguageCodes constants
2. Language IDs now use BCP 47 codes ('en-US' not 'English (US)')
3. register() → registerComponent() (more explicit)
4. See docs/MIGRATION_GUIDE.md for complete guide$3
Yes, use named instances:
`typescript
const adminEngine = PluginI18nEngine.createInstance('admin', adminLangs);
const userEngine = PluginI18nEngine.createInstance('user', userLangs);
`See Multiple Instances section for details.
$3
Pattern:
`typescript
import { PluginI18nEngine } from '@digitaldefiance/i18n-lib';describe('MyComponent', () => {
beforeEach(() => {
PluginI18nEngine.resetAll();
const engine = PluginI18nEngine.createInstance('test', languages);
engine.registerComponent(/ ... /);
});
afterEach(() => {
PluginI18nEngine.resetAll();
});
});
`See Testing section for more patterns.
License
MIT License - See LICENSE file for details
Contributing
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass (
npm test)
5. Submit a pull requestSupport
- Issues:
- Documentation: See docs/ directory
- Examples: See tests/ directory
ChangeLog
$3
Factory-Based i18n Setup & Browser-Safe Fallback
This release introduces
createI18nSetup(), a factory function that replaces ~200 lines of boilerplate per consumer with a single function call. It also adds a browser-safe fallback for translateStringKey when bundler-duplicated packages break Symbol-based identity checks.New Features:
-
createI18nSetup(): Factory function that handles engine creation, core/library/app component registration, branded enum registration, and GlobalActiveContext initialization in one call
- I18nComponentPackage interface: Bundles a ComponentConfig with its branded string key enum so the factory can auto-register both
- I18nSetupConfig / I18nSetupResult interfaces: Typed config input and result output for the factory
- createSuiteCoreComponentPackage(): New function in suite-core-lib returning an I18nComponentPackage
- createEciesComponentPackage(): New function in ecies-lib returning an I18nComponentPackage
- createNodeEciesComponentPackage(): New function in node-ecies-lib returning an I18nComponentPackage
- Browser-safe fallback: translateStringKey and safeTranslateStringKey now fall back to scanning registered components when the StringKeyEnumRegistry fails (e.g., due to bundler Symbol mismatch), with a lazily-built ValueComponentLookupCache that invalidates on new component registration
- Updated starter template: express-suite-starter scaffolding now uses createI18nSetup() for minimal boilerplateBackward Compatibility:
- All existing APIs (
createSuiteCoreComponentConfig, createEciesComponentConfig, I18nBuilder, manual registration) remain unchanged
- The factory uses the same registerIfNotExists pattern internally, so factory and manual consumers can safely share the same engine instance$3
String Key Enum Registration for Direct Translation
This release adds the ability to register branded string key enums with the I18nEngine for direct translation via
translateStringKey(). This enables automatic component ID resolution from branded enum values.New Features:
-
registerStringKeyEnum(): Register a branded string key enum for direct translation support
`typescript
import { createI18nStringKeysFromEnum } from '@digitaldefiance/i18n-lib';
const MyKeys = createI18nStringKeysFromEnum('my-component', MyStringKeyEnum);
engine.registerStringKeyEnum(MyKeys);
`-
translateStringKey(): Translate a branded string key value directly without specifying component ID
`typescript
// Before: engine.translate('my-component', MyKeys.Welcome, { name: 'Alice' });
// After:
engine.translateStringKey(MyKeys.Welcome, { name: 'Alice' });
`-
safeTranslateStringKey(): Safe version that returns a placeholder on failure instead of throwing-
hasStringKeyEnum(): Check if a branded enum is registered-
getStringKeyEnums(): Get all registered branded enumsNew Error Codes:
-
INVALID_STRING_KEY_ENUM - When a non-branded enum is passed to registerStringKeyEnum()
- STRING_KEY_NOT_REGISTERED - When translating a key from an unregistered enumBenefits:
- Cleaner translation calls without repeating component IDs
- Automatic component routing based on branded enum metadata
- Type-safe translations with full TypeScript support
- Idempotent registration (safe to call multiple times)
$3
Branded Enum Translation Support
This release adds comprehensive support for translating branded enums from
@digitaldefiance/branded-enum, enabling automatic name inference and type-safe enum value translations.New Features:
- Automatic Name Inference: When registering a branded enum, the name is automatically extracted from the enum's component ID
`typescript
const Status = createBrandedEnum('status', { Active: 'active', Inactive: 'inactive' });
engine.registerEnum(Status, translations); // Name 'status' inferred automatically
`- New Utility Functions:
-
isBrandedEnum() - Type guard to detect branded enums
- getBrandedEnumComponentId() - Extract component ID from branded enum
- getBrandedEnumId() - Get raw brand ID for debugging- Enhanced
registerEnum()`: Now accepts branded enums and returns the registered translations object- Property-Based Tests: Comprehensive property tests for branded enum translation ensuring:
- Registration idempotence
- Translation consistency across languages
- Proper error handling for missing translations
Documentation:
- Added "Branded Enum Translation" section to README
- Comprehensive JSDoc documentation for all new utilities
Enhanced Type Documentation and BrandedMasterStringsCollection
This r