@objectstack/service-i18n
v6.9.0
Published
I18n Service for ObjectStack — implements II18nService with file-based locale loading
Maintainers
Readme
@objectstack/service-i18n
I18n Service for ObjectStack — implements II18nService with file-based locale loading and translation management.
Features
- Multi-Language Support: Manage translations for unlimited languages
- File-Based Locales: Load translations from JSON/YAML files
- Namespace Support: Organize translations by domain (e.g.,
common,errors,ui) - Interpolation: Dynamic variable replacement in translations
- Pluralization: Language-specific plural rules
- Fallback Chain: Graceful fallback from dialect → base language → default
- Type-Safe: TypeScript support with type-safe translation keys
- Hot Reload: Reload translations without restarting (development)
Installation
pnpm add @objectstack/service-i18nBasic Usage
import { defineStack } from '@objectstack/spec';
import { ServiceI18n } from '@objectstack/service-i18n';
const stack = defineStack({
services: [
ServiceI18n.configure({
defaultLocale: 'en-US',
supportedLocales: ['en-US', 'es-ES', 'fr-FR', 'de-DE'],
loadPath: './locales/{{lng}}/{{ns}}.json',
}),
],
});Configuration
interface I18nServiceConfig {
/** Default locale (e.g., 'en-US') */
defaultLocale: string;
/** List of supported locales */
supportedLocales: string[];
/** Path template for locale files */
loadPath: string;
/** Fallback locale when translation is missing */
fallbackLocale?: string;
/** Enable hot reload in development */
hotReload?: boolean;
}Directory Structure
locales/
├── en-US/
│ ├── common.json
│ ├── errors.json
│ └── ui.json
├── es-ES/
│ ├── common.json
│ ├── errors.json
│ └── ui.json
└── fr-FR/
├── common.json
├── errors.json
└── ui.jsonExample locales/en-US/common.json:
{
"welcome": "Welcome to ObjectStack",
"greeting": "Hello, {{name}}!",
"item_count": "You have {{count}} item",
"item_count_plural": "You have {{count}} items",
"save_button": "Save",
"cancel_button": "Cancel"
}Service API
// Get i18n service
const i18n = kernel.getService<II18nService>('i18n');Basic Translation
// Simple translation
const text = await i18n.t('common:welcome');
// "Welcome to ObjectStack"
// With interpolation
const greeting = await i18n.t('common:greeting', { name: 'Alice' });
// "Hello, Alice!"
// With pluralization
const count1 = await i18n.t('common:item_count', { count: 1 });
// "You have 1 item"
const count5 = await i18n.t('common:item_count', { count: 5 });
// "You have 5 items"Change Locale
// Set locale for current context
await i18n.setLocale('es-ES');
// Get current locale
const locale = i18n.getLocale();
// "es-ES"
// Translate in specific locale (without changing context)
const text = await i18n.t('common:welcome', { locale: 'fr-FR' });Namespaces
// Load translation from 'errors' namespace
const errorMsg = await i18n.t('errors:not_found');
// Load multiple namespaces
await i18n.loadNamespaces(['common', 'ui', 'errors']);
// Check if namespace is loaded
const isLoaded = i18n.isNamespaceLoaded('common');Locale Management
// Get all supported locales
const locales = i18n.getSupportedLocales();
// ['en-US', 'es-ES', 'fr-FR', 'de-DE']
// Check if locale is supported
const isSupported = i18n.isLocaleSupported('ja-JP');
// false
// Get locale metadata
const metadata = i18n.getLocaleMetadata('en-US');
// {
// name: 'English (United States)',
// nativeName: 'English (United States)',
// direction: 'ltr',
// pluralRules: 'en'
// }Advanced Features
Nested Keys
{
"user": {
"profile": {
"title": "User Profile",
"edit": "Edit Profile"
}
}
}await i18n.t('common:user.profile.title');
// "User Profile"Arrays
{
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
}const days = await i18n.t('common:days', { returnObjects: true });
// ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]Context-Based Translations
{
"friend": "A friend",
"friend_male": "A boyfriend",
"friend_female": "A girlfriend"
}await i18n.t('common:friend', { context: 'male' });
// "A boyfriend"
await i18n.t('common:friend', { context: 'female' });
// "A girlfriend"Formatting
// Date formatting
const formatted = await i18n.formatDate(new Date(), {
locale: 'es-ES',
format: 'long',
});
// "15 de enero de 2024"
// Number formatting
const price = await i18n.formatNumber(1234.56, {
style: 'currency',
currency: 'EUR',
locale: 'fr-FR',
});
// "1 234,56 €"
// Relative time
const relative = await i18n.formatRelative(new Date('2024-01-01'), {
locale: 'en-US',
});
// "3 months ago"Dynamic Loading
// Add a new locale dynamically
await i18n.addLocale('ja-JP', {
loadPath: './locales/ja-JP/{{ns}}.json',
});
// Remove a locale
await i18n.removeLocale('ja-JP');
// Reload translations (useful in development)
await i18n.reload();Integration with Metadata
Translate metadata labels automatically:
import { ObjectSchema, Field } from '@objectstack/spec/data';
const contact = ObjectSchema.create({
name: 'contact',
label: 'i18n:objects.contact.label', // References translation key
fields: {
name: Field.text({
label: 'i18n:fields.contact.name',
}),
},
});
// Translation file: locales/en-US/metadata.json
{
"objects": {
"contact": {
"label": "Contact",
"label_plural": "Contacts"
}
},
"fields": {
"contact": {
"name": "Full Name"
}
}
}REST API Endpoints
GET /api/v1/i18n/locales # Get supported locales
GET /api/v1/i18n/translations/:locale # Get all translations for locale
POST /api/v1/i18n/translate # Translate keys (batch)Client Integration
React Hook Example
import { useTranslation } from '@objectstack/client-react';
function MyComponent() {
const { t, locale, setLocale } = useTranslation();
return (
<div>
<h1>{t('common:welcome')}</h1>
<button onClick={() => setLocale('es-ES')}>
Español
</button>
</div>
);
}Best Practices
- Use Namespaces: Organize translations by domain (common, ui, errors, metadata)
- Consistent Keys: Use dot notation for nested keys (e.g.,
user.profile.title) - Provide Context: Use context for gender, formality, or pluralization variants
- Fallback Values: Always provide fallback translations in default locale
- Avoid Hardcoding: Never hardcode user-facing text; use translation keys
- Professional Translation: Use professional translators for production
- Version Control: Store translation files in version control
Locale Coverage Detection
// Get coverage statistics
const coverage = await i18n.getCoverage();
// {
// 'en-US': { total: 245, missing: 0, percentage: 100 },
// 'es-ES': { total: 245, missing: 12, percentage: 95.1 },
// 'fr-FR': { total: 245, missing: 45, percentage: 81.6 }
// }
// Get missing keys for a locale
const missing = await i18n.getMissingKeys('es-ES');
// ['errors.validation.email', 'ui.dashboard.title', ...]Performance Considerations
- Lazy Loading: Namespaces are loaded on demand
- Caching: Translations are cached in memory
- Hot Reload: Only enable in development
- Bundle Size: Load only required locales on client
Contract Implementation
Implements II18nService from @objectstack/spec/contracts:
interface II18nService {
t(key: string, options?: TranslationOptions): Promise<string>;
setLocale(locale: string): Promise<void>;
getLocale(): string;
getSupportedLocales(): string[];
loadNamespaces(namespaces: string[]): Promise<void>;
formatDate(date: Date, options?: FormatOptions): Promise<string>;
formatNumber(value: number, options?: FormatOptions): Promise<string>;
}License
Apache-2.0. See LICENSING.md.
