i18n
Contract
@modularityjs/i18n provides server-side translation with pluggable sources, priority-based resolution, locale fallback, and parameter interpolation.
abstract class TranslationSource {
abstract readonly name: string;
abstract readonly priority: number;
abstract get(locale: string, key: string): string | undefined;
}
class TranslationService {
translate(
locale: string,
key: string,
params?: Record<string, unknown>,
): string;
}Sources are registered in TranslationSourcePool and sorted by priority (descending). The first source returning a value for the given locale + key wins. If no source has a translation, the fallback locale is tried. If still missing, the key itself is returned.
Setup
import { I18nModule } from '@modularityjs/i18n';
import { I18nJsonModule } from '@modularityjs/i18n-json';
const modules = [
I18nModule.forRoot({ defaultLocale: 'en', fallbackLocale: 'en' }),
I18nJsonModule.forRoot({
directory: 'src/translations',
locales: ['en', 'de', 'es'],
}),
];Configuration:
| Option | Default | Description |
|---|---|---|
defaultLocale | 'en' | Default locale when none specified |
fallbackLocale | 'en' | Locale to try when the requested locale has no translation |
Drivers
JSON (@modularityjs/i18n-json)
Loads translations from JSON files in a directory. File naming: {locale}.json.
src/translations/
en.json
de.json
es.jsonFiles support flat keys or nested objects (flattened with dots):
{
"errors": {
"required": "{{field}} is required",
"not_found": "{{entity}} not found"
},
"auth": {
"login_required": "Please sign in to continue"
}
}Accessed as errors.required, auth.login_required, etc.
Config:
| Option | Default | Description |
|---|---|---|
directory | (required) | Directory of {locale}.json files |
locales | [] | Locales to preload at boot. Missing files (ENOENT) are skipped silently; other read errors and JSON parse failures emit a process.emitWarning and don't fail boot |
Usage
@Injectable()
class NotificationService {
constructor(
@Inject(TranslationService) private readonly t: TranslationService,
) {}
sendWelcome(user: { locale: string; name: string }) {
const subject = this.t.translate(user.locale, 'email.welcome_subject');
const body = this.t.translate(user.locale, 'email.welcome_body', {
name: user.name,
});
// ...
}
}Parameter interpolation
Use syntax in translation strings:
{ "greeting": "Hello, {{name}}! You have {{count}} messages." }t.translate('en', 'greeting', { name: 'Alice', count: 5 });
// "Hello, Alice! You have 5 messages."Unmatched placeholders are preserved: stays in the output.
Locale fallback
When a key is missing in the requested locale, the service falls back to fallbackLocale:
// en.json: { "greeting": "Hello" }
// de.json: { }t.translate('de', 'greeting'); // "Hello" (fell back to 'en')If the key is missing in both the requested and fallback locale, the key itself is returned: "greeting".
HTTP Bridge (@modularityjs/http-i18n)
Resolves the locale from each HTTP request and attaches it to request.locale.
import { HttpI18nModule } from '@modularityjs/http-i18n';
const modules = [
I18nModule.forRoot({ defaultLocale: 'en', fallbackLocale: 'en' }),
I18nJsonModule.forRoot({
directory: 'src/translations',
locales: ['en', 'de', 'es'],
}),
HttpI18nModule,
];By default, locale is extracted from the Accept-Language header (en-US,en;q=0.9 → en). Customize:
HttpI18nModule.forRoot({
localeExtractor: (request) => {
// Read from query param, cookie, or user preference
return (request as any).auth?.attributes?.locale ?? 'en';
},
});Access in controllers via getRequestLocale from @modularityjs/http-i18n — prefer the helper over reading request.locale directly, so the storage key can change without touching consumers:
import { getRequestLocale } from '@modularityjs/http-i18n';
@Get('/greeting')
greet(@Request() req: HttpRequest) {
const locale = getRequestLocale(req) ?? 'en';
return { message: this.t.translate(locale, 'greeting.hello') };
}Custom Sources
Extend TranslationSource to add custom backends (database, remote API, etc.):
@Injectable()
class DatabaseTranslationSource extends TranslationSource {
readonly name = 'database';
readonly priority = 100; // higher than JSON (0)
get(locale: string, key: string): string | undefined {
return this.db.getTranslation(locale, key);
}
}Register in TranslationSourcePool:
@Module({
name: 'db-translations',
imports: [I18nModule],
providers: [DatabaseTranslationSource],
pools: [
{
pool: TranslationSourcePool,
key: 'database',
useClass: DatabaseTranslationSource,
},
],
})
class DbTranslationsModule {}Higher-priority sources override lower-priority ones — database translations (100) override bundled JSON (0).