Skip to content

i18n

Contract

@modularityjs/i18n provides server-side translation with pluggable sources, priority-based resolution, locale fallback, and parameter interpolation.

typescript
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

typescript
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:

OptionDefaultDescription
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.json

Files support flat keys or nested objects (flattened with dots):

json
{
  "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:

OptionDefaultDescription
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

typescript
@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:

json
{ "greeting": "Hello, {{name}}! You have {{count}} messages." }
typescript
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:

json
// en.json: { "greeting": "Hello" }
// de.json: { }
typescript
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.

typescript
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.9en). Customize:

typescript
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:

typescript
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.):

typescript
@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:

typescript
@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).