@flusys/nestjs-localization
v4.1.1
Published
Localization module for multi-language support
Downloads
363
Maintainers
Readme
@flusys/nestjs-localization
Multi-language backend for NestJS — database-driven translations,
{{variable}}interpolation, RTL support, missing-key detection, verification workflow, and module-scoped partial loading.
Table of Contents
- Overview
- Features
- Compatibility
- Installation
- Quick Start
- Module Registration
- Configuration Reference
- Feature Toggles
- API Endpoints
- Entities
- Translation Workflow
- Variable Interpolation
- Fallback Chain
- RTL Support
- Caching
- Exported Services
- Integration with API Responses
- Troubleshooting
- License
Overview
@flusys/nestjs-localization manages the backend side of multi-language support. Translations are stored in the database and served to the frontend via API. The package provides a full admin workflow: create languages, define translation keys (grouped by module), add translations per language, verify them, and detect missing ones.
All API responses in the FLUSYS ecosystem return a messageKey field (e.g., 'auth.login_success'). The frontend looks up this key in the translations returned by this package.
Features
- Language management — Create languages with ISO 639-1 codes, native names, and RTL/LTR direction
- Translation keys — Module-scoped key registry with variable declarations and readonly protection
- Translations — Per-language values with a verification workflow (draft → verified)
- Variable interpolation —
{{variableName}}syntax in messages withmessageVariablesfield in API responses - Missing key detection — Find which keys have no translation for a given language
- Bulk operations — Atomic bulk update of multiple translations in one transaction
- Default language — Configurable fallback language (e.g.,
'en') - Module scoping — Keys grouped by module for partial frontend loading
- Caching — Optional HybridCache for translation lookups with configurable TTL
- Multi-tenant — Per-tenant DataSource isolation
Compatibility
| Package | Version |
|---------|---------|
| @flusys/nestjs-core | ^4.0.0 |
| @flusys/nestjs-shared | ^4.0.0 |
| @nestjs/core | ^11.0.0 |
| typeorm | ^0.3.0 |
| Node.js | >= 18.x |
Installation
npm install @flusys/nestjs-localization @flusys/nestjs-shared @flusys/nestjs-coreQuick Start
Minimal Setup (Single Database)
import { Module } from '@nestjs/common';
import { LocalizationModule } from '@flusys/nestjs-localization';
@Module({
imports: [
LocalizationModule.forRoot({
global: true,
includeController: true,
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: false,
},
config: {
defaultDatabaseConfig: {
type: 'postgres',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT ?? 5432),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
defaultLanguageCode: 'en',
enableCaching: false,
cacheTtlSeconds: 300,
},
}),
],
})
export class AppModule {}After startup, create languages and translation keys via the API. The frontend fetches translations on init and uses the messageKey returned by each API response to look up the localized message.
Module Registration
forRoot (Sync)
LocalizationModule.forRoot({
global?: boolean;
includeController?: boolean; // Default: true
bootstrapAppConfig?: {
databaseMode: 'single' | 'multi-tenant';
enableCompanyFeature: boolean; // No company variants — shared data only
};
config: ILocalizationModuleConfig;
})forRootAsync (Factory)
import { ConfigService } from '@nestjs/config';
LocalizationModule.forRootAsync({
global: true,
includeController: true,
bootstrapAppConfig: {
databaseMode: 'single',
enableCompanyFeature: false,
},
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
defaultDatabaseConfig: {
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
},
defaultLanguageCode: configService.get('DEFAULT_LANGUAGE', 'en'),
enableCaching: configService.get<boolean>('LOCALIZATION_CACHE', false),
cacheTtlSeconds: 300,
}),
inject: [ConfigService],
})Multi-Tenant Setup
LocalizationModule.forRootAsync({
global: true,
bootstrapAppConfig: {
databaseMode: 'multi-tenant',
enableCompanyFeature: false,
},
useFactory: (): ILocalizationModuleConfig => ({
tenantDefaultDatabaseConfig: {
type: 'postgres',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
tenants: [
{ id: 'tenant1', database: 'tenant1_db' },
{ id: 'tenant2', database: 'tenant2_db' },
],
defaultLanguageCode: 'en',
}),
})Exported services:
LocalizationConfigServiceLocalizationDataSourceProviderLanguageServiceTranslationKeyServiceTranslationService
Configuration Reference
interface ILocalizationModuleConfig extends IDataSourceServiceOptions {
/** Default language code for fallback (default: 'en') */
defaultLanguageCode?: string;
/** Enable translation caching (default: false) */
enableCaching?: boolean;
/** Cache TTL in seconds (default: 300) */
cacheTtlSeconds?: number;
}Feature Toggles
| Feature | Config | Default | Effect |
|---------|--------|---------|--------|
| Caching | enableCaching: true | false | Translations cached in HybridCache with cacheTtlSeconds TTL |
| Multi-tenant | databaseMode: 'multi-tenant' | 'single' | Per-tenant DataSource connections |
Note: Localization entities do not have company variants (
enableCompanyFeatureis ignored). All translation data is shared across companies.
API Endpoints
All endpoints use POST. Authentication is required unless noted.
Languages — POST /localization/language/*
| Endpoint | Permission | Description |
|----------|-----------|-------------|
| POST /localization/language/insert | language.create | Create a language |
| POST /localization/language/get-all | Public | List all active languages |
| POST /localization/language/get/:id | language.read | Get language by ID |
| POST /localization/language/update | language.update | Update language |
| POST /localization/language/delete | language.delete | Delete language |
| POST /localization/language/set-default | language.update | Set as default language |
Create language:
POST /localization/language/insert
{
"name": "English",
"nativeName": "English",
"code": "en",
"direction": "ltr",
"isActive": true,
"isDefault": true
}POST /localization/language/insert
{
"name": "Arabic",
"nativeName": "العربية",
"code": "ar",
"direction": "rtl",
"isActive": true
}Translation Keys — POST /localization/translation-key/*
| Endpoint | Permission | Description |
|----------|-----------|-------------|
| POST /localization/translation-key/insert | translation-key.create | Register a new translation key |
| POST /localization/translation-key/get-all | translation-key.read | List all keys |
| POST /localization/translation-key/get/:id | translation-key.read | Get key by ID |
| POST /localization/translation-key/get-by-module | translation-key.read | Get all keys for a module |
| POST /localization/translation-key/update | translation-key.update | Update key (not readonly keys) |
| POST /localization/translation-key/delete | translation-key.delete | Delete key (not readonly) |
| POST /localization/translation-key/get-missing | translation-key.read | Get keys with no translation for a language |
Register a key:
POST /localization/translation-key/insert
{
"key": "auth.login_success",
"module": "auth",
"description": "Shown after successful login",
"defaultValue": "Login successful",
"variables": [],
"isReadonly": true
}Key with variables:
{
"key": "user.welcome",
"module": "auth",
"description": "Welcome message with user name",
"defaultValue": "Welcome, {{name}}!",
"variables": ["name"],
"isReadonly": false
}Translations — POST /localization/translation/*
| Endpoint | Permission | Description |
|----------|-----------|-------------|
| POST /localization/translation/insert | translation.create | Add a translation value |
| POST /localization/translation/get-all | translation.read | List all translations |
| POST /localization/translation/get-by-language | Public | Get all translations for a language |
| POST /localization/translation/get-by-language-module | Public | Get translations for a language + module |
| POST /localization/translation/update | translation.update | Update a translation value |
| POST /localization/translation/bulk-update | translation.update | Bulk update translations (atomic) |
| POST /localization/translation/delete | translation.delete | Delete a translation |
| POST /localization/translation/verify | translation.update | Mark a translation as verified |
| POST /localization/translation/unverify | translation.update | Mark a translation as unverified |
Add a translation:
POST /localization/translation/insert
{
"keyId": "uuid-of-translation-key",
"languageId": "uuid-of-language",
"value": "Connexion réussie"
}Bulk update (atomic transaction):
POST /localization/translation/bulk-update
{
"languageId": "uuid-of-french",
"translations": [
{ "keyId": "uuid-1", "value": "Connexion réussie" },
{ "keyId": "uuid-2", "value": "Bienvenue, {{name}}!" },
{ "keyId": "uuid-3", "value": "Mot de passe incorrect" }
]
}Entities
| Entity | Table | Description |
|--------|-------|-------------|
| Language | localization_language | Language with code, direction, default flag |
| TranslationKey | localization_translation_key | Key registry with module, variables, readonly flag |
| Translation | localization_translation | Per-language translation values with verification status |
Localization entities have no company variants. All translation data is shared globally.
import { LocalizationModule } from '@flusys/nestjs-localization';
TypeOrmModule.forRoot({
entities: [
...LocalizationModule.getEntities(),
// other entities
],
})Translation Workflow
1. Create Language → POST /localization/language/insert
2. Register Key → POST /localization/translation-key/insert
3. Add Translation → POST /localization/translation/insert
4. Verify Translation → POST /localization/translation/verify
5. Detect Missing → POST /localization/translation-key/get-missing
6. Bulk Update → POST /localization/translation/bulk-updateTranslation statuses:
- Unverified (default) — Draft translation, may not be production-ready
- Verified — Approved by a translator or admin
Variable Interpolation
Translation values use {{variableName}} syntax. The messageVariables field in API responses carries the variable values that should be interpolated by the frontend:
Server response:
{
"success": true,
"message": "Welcome, John!",
"messageKey": "user.welcome",
"messageVariables": { "name": "John" }
}Frontend interpolation:
// Fetch translation for 'user.welcome': "Bienvenue, {{name}}!"
const template = translations['user.welcome'];
const message = template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? key);
// Result: "Bienvenue, John!"Fallback Chain
When a translation is not found for the requested language, the resolution falls back in this order:
Requested language → Default language (e.g., 'en') → key.defaultValue → key itselfRTL Support
Each language has a direction field: 'ltr' (left-to-right) or 'rtl' (right-to-left).
The frontend uses this to switch layout direction:
// Angular
const lang = await this.http.post('/localization/language/get-all', {}).toPromise();
const current = lang.data.find(l => l.code === userLanguageCode);
document.dir = current.direction; // 'ltr' or 'rtl'Caching
When enableCaching: true, translation lookups are cached with the key pattern loc:{languageCode}:{module}. Cache is invalidated when translations are updated via the bulk-update or individual update endpoints.
config: {
enableCaching: true,
cacheTtlSeconds: 600, // 10 minutes
}Requires HybridCache (Redis or in-memory) to be configured via CacheModule from @flusys/nestjs-shared.
Exported Services
| Service | Description |
|---------|-------------|
| LanguageService | Language CRUD, default language management |
| TranslationKeyService | Key CRUD, missing-key detection, module filtering |
| TranslationService | Translation CRUD, bulk update, verification |
| LocalizationConfigService | Runtime config (default language, caching settings) |
| LocalizationDataSourceProvider | Dynamic DataSource per request |
Integration with API Responses
All FLUSYS response DTOs include messageKey and optionally messageVariables. The frontend uses these to display localized messages:
Backend (any service):
return {
success: true,
message: 'Welcome, John!',
messageKey: 'user.welcome',
messageVariables: { name: 'John' },
data: userDto,
};Frontend flow:
- On app init:
GET /localization/translation/get-by-languagefor the user's language - Cache translations in a map:
{ 'user.welcome': 'Bienvenue, {{name}}!' } - On every API response: look up
messageKey, interpolatemessageVariables - Display the localized message
Troubleshooting
Language not found for a valid code
Use the language's UUID (id), not the ISO code, in most endpoints. Use get-by-language with the code only for the public translation endpoints.
Translation not returned for a module
Check the module field on the TranslationKey. The get-by-language-module endpoint filters strictly by module name. Ensure it matches exactly (case-sensitive).
Missing keys not detected
get-missing returns keys that have no Translation record for the given language. If you created a translation but left value empty, it is still counted as "present" (not missing). Delete or update the empty translation to see it in the missing list.
Caching returning stale translations
When enableCaching: true, call POST /localization/translation/bulk-update (which invalidates the cache) instead of individual updates if you need immediate cache refresh.
No metadata for entity
Register entities:
entities: [...LocalizationModule.getEntities()]License
MIT © FLUSYS
Part of the FLUSYS framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
