@flusys/nestjs-localization 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -1,152 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flusys/nestjs-localization
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
> Multi-language backend for NestJS — database-driven translations, `{{variable}}` interpolation, RTL support, missing-key detection, verification workflow, and module-scoped partial loading.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/nestjs-localization)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nestjs.com/)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Overview](#overview)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Compatibility](#compatibility)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Module Registration](#module-registration)
|
|
21
|
+
- [forRoot (Sync)](#forroot-sync)
|
|
22
|
+
- [forRootAsync (Factory)](#forrootasync-factory)
|
|
23
|
+
- [Configuration Reference](#configuration-reference)
|
|
24
|
+
- [Feature Toggles](#feature-toggles)
|
|
25
|
+
- [API Endpoints](#api-endpoints)
|
|
26
|
+
- [Entities](#entities)
|
|
27
|
+
- [Translation Workflow](#translation-workflow)
|
|
28
|
+
- [Variable Interpolation](#variable-interpolation)
|
|
29
|
+
- [Fallback Chain](#fallback-chain)
|
|
30
|
+
- [RTL Support](#rtl-support)
|
|
31
|
+
- [Caching](#caching)
|
|
32
|
+
- [Exported Services](#exported-services)
|
|
33
|
+
- [Integration with API Responses](#integration-with-api-responses)
|
|
34
|
+
- [Troubleshooting](#troubleshooting)
|
|
35
|
+
- [License](#license)
|
|
6
36
|
|
|
7
37
|
---
|
|
8
38
|
|
|
9
39
|
## Overview
|
|
10
40
|
|
|
11
|
-
`@flusys/nestjs-localization` provides a
|
|
41
|
+
`@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.
|
|
12
42
|
|
|
13
|
-
|
|
14
|
-
- **Translation Keys** - Module-based key organization with variables
|
|
15
|
-
- **Translations** - Per-language translations with verification status
|
|
16
|
-
- **Caching** - Optional hybrid caching for performance
|
|
17
|
-
- **Dynamic DataSource** - Request-scoped repository pattern
|
|
18
|
-
- **Variable Interpolation** - `{{variable}}` syntax support
|
|
19
|
-
- **Bulk Operations** - Batch update translations
|
|
20
|
-
- **Missing Translation Detection** - Find untranslated keys
|
|
43
|
+
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.
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
---
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Language management** — Create languages with ISO 639-1 codes, native names, and RTL/LTR direction
|
|
50
|
+
- **Translation keys** — Module-scoped key registry with variable declarations and readonly protection
|
|
51
|
+
- **Translations** — Per-language values with a verification workflow (draft → verified)
|
|
52
|
+
- **Variable interpolation** — `{{variableName}}` syntax in messages with `messageVariables` field in API responses
|
|
53
|
+
- **Missing key detection** — Find which keys have no translation for a given language
|
|
54
|
+
- **Bulk operations** — Atomic bulk update of multiple translations in one transaction
|
|
55
|
+
- **Default language** — Configurable fallback language (e.g., `'en'`)
|
|
56
|
+
- **Module scoping** — Keys grouped by module for partial frontend loading
|
|
57
|
+
- **Caching** — Optional HybridCache for translation lookups with configurable TTL
|
|
58
|
+
- **Multi-tenant** — Per-tenant DataSource isolation
|
|
31
59
|
|
|
32
60
|
---
|
|
33
61
|
|
|
34
|
-
##
|
|
62
|
+
## Compatibility
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
│ ├── translation-key.interface.ts # ITranslationKey
|
|
44
|
-
│ ├── translation.interface.ts # ITranslation, ITranslationsMap
|
|
45
|
-
│ ├── localization-module-options.interface.ts # Module config
|
|
46
|
-
│ └── index.ts
|
|
47
|
-
├── entities/
|
|
48
|
-
│ ├── language.entity.ts # Language entity
|
|
49
|
-
│ ├── translation-key.entity.ts # TranslationKey entity
|
|
50
|
-
│ ├── translation.entity.ts # Translation entity
|
|
51
|
-
│ └── index.ts
|
|
52
|
-
├── dtos/
|
|
53
|
-
│ ├── language.dto.ts # Language DTOs
|
|
54
|
-
│ ├── translation-key.dto.ts # TranslationKey DTOs
|
|
55
|
-
│ ├── translation.dto.ts # Translation DTOs
|
|
56
|
-
│ └── index.ts
|
|
57
|
-
├── services/
|
|
58
|
-
│ ├── localization-config.service.ts # Config management
|
|
59
|
-
│ ├── localization-datasource.provider.ts # DataSource provider
|
|
60
|
-
│ ├── localization-helper.service.ts # Interpolation utilities
|
|
61
|
-
│ ├── language.service.ts # Language CRUD
|
|
62
|
-
│ ├── translation-key.service.ts # TranslationKey CRUD
|
|
63
|
-
│ ├── translation.service.ts # Translation CRUD + queries
|
|
64
|
-
│ └── index.ts
|
|
65
|
-
├── controllers/
|
|
66
|
-
│ ├── language.controller.ts # Language endpoints
|
|
67
|
-
│ ├── translation-key.controller.ts # TranslationKey endpoints
|
|
68
|
-
│ ├── translation.controller.ts # Translation endpoints
|
|
69
|
-
│ └── index.ts
|
|
70
|
-
├── modules/
|
|
71
|
-
│ └── localization.module.ts # Module definition
|
|
72
|
-
└── index.ts
|
|
73
|
-
```
|
|
64
|
+
| Package | Version |
|
|
65
|
+
|---------|---------|
|
|
66
|
+
| `@flusys/nestjs-core` | `^4.0.0` |
|
|
67
|
+
| `@flusys/nestjs-shared` | `^4.0.0` |
|
|
68
|
+
| `@nestjs/core` | `^11.0.0` |
|
|
69
|
+
| `typeorm` | `^0.3.0` |
|
|
70
|
+
| Node.js | `>= 18.x` |
|
|
74
71
|
|
|
75
72
|
---
|
|
76
73
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|--------|------|----------|-------------|
|
|
83
|
-
| `id` | uuid | No | Primary key |
|
|
84
|
-
| `code` | varchar(10) | No | ISO 639-1 code (e.g., 'en', 'ar') |
|
|
85
|
-
| `name` | varchar(100) | No | English name |
|
|
86
|
-
| `native_name` | varchar(100) | Yes | Native name (e.g., 'العربية') |
|
|
87
|
-
| `direction` | varchar(3) | Yes | 'ltr' or 'rtl' |
|
|
88
|
-
| `is_active` | boolean | No | Active status (default: true) |
|
|
89
|
-
| `is_default` | boolean | No | Default language flag |
|
|
90
|
-
| `serial` | int | Yes | Sort order |
|
|
91
|
-
| `created_at` | timestamp | No | Creation timestamp |
|
|
92
|
-
| `updated_at` | timestamp | No | Update timestamp |
|
|
93
|
-
| `deleted_at` | timestamp | Yes | Soft delete timestamp |
|
|
94
|
-
|
|
95
|
-
**Indexes:**
|
|
96
|
-
- `UNIQUE (code)`
|
|
97
|
-
- `INDEX (is_active)`
|
|
98
|
-
- `INDEX (is_default)`
|
|
99
|
-
|
|
100
|
-
### TranslationKey Table
|
|
101
|
-
|
|
102
|
-
| Column | Type | Nullable | Description |
|
|
103
|
-
|--------|------|----------|-------------|
|
|
104
|
-
| `id` | uuid | No | Primary key |
|
|
105
|
-
| `module` | varchar(100) | No | Module grouping (e.g., 'auth') |
|
|
106
|
-
| `key` | varchar(255) | No | Translation key |
|
|
107
|
-
| `default_message` | varchar(500) | No | Default/fallback message |
|
|
108
|
-
| `description` | text | Yes | Description for translators |
|
|
109
|
-
| `variables` | simple-array | Yes | Required variables |
|
|
110
|
-
| `is_active` | boolean | No | Active status |
|
|
111
|
-
| `serial` | int | Yes | Sort order |
|
|
112
|
-
| `created_at` | timestamp | No | Creation timestamp |
|
|
113
|
-
| `updated_at` | timestamp | No | Update timestamp |
|
|
114
|
-
| `deleted_at` | timestamp | Yes | Soft delete timestamp |
|
|
115
|
-
|
|
116
|
-
**Indexes:**
|
|
117
|
-
- `UNIQUE (key)`
|
|
118
|
-
- `INDEX (module)`
|
|
119
|
-
- `INDEX (module, key)`
|
|
120
|
-
- `INDEX (is_active)`
|
|
121
|
-
|
|
122
|
-
### Translation Table
|
|
123
|
-
|
|
124
|
-
| Column | Type | Nullable | Description |
|
|
125
|
-
|--------|------|----------|-------------|
|
|
126
|
-
| `id` | uuid | No | Primary key |
|
|
127
|
-
| `language_id` | uuid | No | FK to language |
|
|
128
|
-
| `translation_key_id` | uuid | No | FK to translation_key |
|
|
129
|
-
| `value` | text | No | Translated value |
|
|
130
|
-
| `is_verified` | boolean | No | Verified by translator |
|
|
131
|
-
| `is_active` | boolean | No | Active status |
|
|
132
|
-
| `created_at` | timestamp | No | Creation timestamp |
|
|
133
|
-
| `updated_at` | timestamp | No | Update timestamp |
|
|
134
|
-
| `deleted_at` | timestamp | Yes | Soft delete timestamp |
|
|
135
|
-
|
|
136
|
-
**Indexes:**
|
|
137
|
-
- `UNIQUE (language_id, translation_key_id)`
|
|
138
|
-
- `INDEX (language_id)`
|
|
139
|
-
- `INDEX (translation_key_id)`
|
|
140
|
-
- `INDEX (is_active)`
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install @flusys/nestjs-localization @flusys/nestjs-shared @flusys/nestjs-core
|
|
78
|
+
```
|
|
141
79
|
|
|
142
80
|
---
|
|
143
81
|
|
|
144
|
-
##
|
|
82
|
+
## Quick Start
|
|
145
83
|
|
|
146
|
-
###
|
|
84
|
+
### Minimal Setup (Single Database)
|
|
147
85
|
|
|
148
86
|
```typescript
|
|
149
|
-
|
|
87
|
+
import { Module } from '@nestjs/common';
|
|
150
88
|
import { LocalizationModule } from '@flusys/nestjs-localization';
|
|
151
89
|
|
|
152
90
|
@Module({
|
|
@@ -155,11 +93,20 @@ import { LocalizationModule } from '@flusys/nestjs-localization';
|
|
|
155
93
|
global: true,
|
|
156
94
|
includeController: true,
|
|
157
95
|
bootstrapAppConfig: {
|
|
158
|
-
|
|
96
|
+
databaseMode: 'single',
|
|
97
|
+
enableCompanyFeature: false,
|
|
159
98
|
},
|
|
160
99
|
config: {
|
|
100
|
+
defaultDatabaseConfig: {
|
|
101
|
+
type: 'postgres',
|
|
102
|
+
host: process.env.DB_HOST,
|
|
103
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
104
|
+
username: process.env.DB_USER,
|
|
105
|
+
password: process.env.DB_PASSWORD,
|
|
106
|
+
database: process.env.DB_NAME,
|
|
107
|
+
},
|
|
161
108
|
defaultLanguageCode: 'en',
|
|
162
|
-
enableCaching:
|
|
109
|
+
enableCaching: false,
|
|
163
110
|
cacheTtlSeconds: 300,
|
|
164
111
|
},
|
|
165
112
|
}),
|
|
@@ -168,703 +115,421 @@ import { LocalizationModule } from '@flusys/nestjs-localization';
|
|
|
168
115
|
export class AppModule {}
|
|
169
116
|
```
|
|
170
117
|
|
|
171
|
-
|
|
118
|
+
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.
|
|
172
119
|
|
|
173
|
-
|
|
174
|
-
// app.module.ts
|
|
175
|
-
import { LocalizationModule } from '@flusys/nestjs-localization';
|
|
176
|
-
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
120
|
+
---
|
|
177
121
|
|
|
178
|
-
|
|
179
|
-
imports: [
|
|
180
|
-
LocalizationModule.forRootAsync({
|
|
181
|
-
global: true,
|
|
182
|
-
includeController: true,
|
|
183
|
-
imports: [ConfigModule],
|
|
184
|
-
bootstrapAppConfig: {
|
|
185
|
-
dataSource: AppDataSource,
|
|
186
|
-
},
|
|
187
|
-
useFactory: (configService: ConfigService) => ({
|
|
188
|
-
defaultLanguageCode: configService.get('DEFAULT_LANGUAGE', 'en'),
|
|
189
|
-
enableCaching: configService.get('ENABLE_CACHE', true),
|
|
190
|
-
cacheTtlSeconds: configService.get('CACHE_TTL', 300),
|
|
191
|
-
}),
|
|
192
|
-
inject: [ConfigService],
|
|
193
|
-
}),
|
|
194
|
-
],
|
|
195
|
-
})
|
|
196
|
-
export class AppModule {}
|
|
197
|
-
```
|
|
122
|
+
## Module Registration
|
|
198
123
|
|
|
199
|
-
###
|
|
124
|
+
### forRoot (Sync)
|
|
200
125
|
|
|
201
126
|
```typescript
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
bootstrapAppConfig: IBootstrapAppConfig;
|
|
212
|
-
config?: ILocalizationModuleConfig;
|
|
213
|
-
}
|
|
127
|
+
LocalizationModule.forRoot({
|
|
128
|
+
global?: boolean;
|
|
129
|
+
includeController?: boolean; // Default: true
|
|
130
|
+
bootstrapAppConfig?: {
|
|
131
|
+
databaseMode: 'single' | 'multi-tenant';
|
|
132
|
+
enableCompanyFeature: boolean; // No company variants — shared data only
|
|
133
|
+
};
|
|
134
|
+
config: ILocalizationModuleConfig;
|
|
135
|
+
})
|
|
214
136
|
```
|
|
215
137
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
## Entities
|
|
219
|
-
|
|
220
|
-
### Language Entity
|
|
138
|
+
### forRootAsync (Factory)
|
|
221
139
|
|
|
222
140
|
```typescript
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
141
|
+
import { ConfigService } from '@nestjs/config';
|
|
142
|
+
|
|
143
|
+
LocalizationModule.forRootAsync({
|
|
144
|
+
global: true,
|
|
145
|
+
includeController: true,
|
|
146
|
+
bootstrapAppConfig: {
|
|
147
|
+
databaseMode: 'single',
|
|
148
|
+
enableCompanyFeature: false,
|
|
149
|
+
},
|
|
150
|
+
imports: [ConfigModule],
|
|
151
|
+
useFactory: (configService: ConfigService) => ({
|
|
152
|
+
defaultDatabaseConfig: {
|
|
153
|
+
type: 'postgres',
|
|
154
|
+
host: configService.get('DB_HOST'),
|
|
155
|
+
port: configService.get<number>('DB_PORT'),
|
|
156
|
+
username: configService.get('DB_USER'),
|
|
157
|
+
password: configService.get('DB_PASSWORD'),
|
|
158
|
+
database: configService.get('DB_NAME'),
|
|
159
|
+
},
|
|
160
|
+
defaultLanguageCode: configService.get('DEFAULT_LANGUAGE', 'en'),
|
|
161
|
+
enableCaching: configService.get<boolean>('LOCALIZATION_CACHE', false),
|
|
162
|
+
cacheTtlSeconds: 300,
|
|
163
|
+
}),
|
|
164
|
+
inject: [ConfigService],
|
|
165
|
+
})
|
|
249
166
|
```
|
|
250
167
|
|
|
251
|
-
###
|
|
168
|
+
### Multi-Tenant Setup
|
|
252
169
|
|
|
253
170
|
```typescript
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
171
|
+
LocalizationModule.forRootAsync({
|
|
172
|
+
global: true,
|
|
173
|
+
bootstrapAppConfig: {
|
|
174
|
+
databaseMode: 'multi-tenant',
|
|
175
|
+
enableCompanyFeature: false,
|
|
176
|
+
},
|
|
177
|
+
useFactory: (): ILocalizationModuleConfig => ({
|
|
178
|
+
tenantDefaultDatabaseConfig: {
|
|
179
|
+
type: 'postgres',
|
|
180
|
+
host: process.env.DB_HOST,
|
|
181
|
+
port: Number(process.env.DB_PORT),
|
|
182
|
+
username: process.env.DB_USER,
|
|
183
|
+
password: process.env.DB_PASSWORD,
|
|
184
|
+
},
|
|
185
|
+
tenants: [
|
|
186
|
+
{ id: 'tenant1', database: 'tenant1_db' },
|
|
187
|
+
{ id: 'tenant2', database: 'tenant2_db' },
|
|
188
|
+
],
|
|
189
|
+
defaultLanguageCode: 'en',
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
```
|
|
261
193
|
|
|
262
|
-
|
|
263
|
-
|
|
194
|
+
**Exported services:**
|
|
195
|
+
- `LocalizationConfigService`
|
|
196
|
+
- `LocalizationDataSourceProvider`
|
|
197
|
+
- `LanguageService`
|
|
198
|
+
- `TranslationKeyService`
|
|
199
|
+
- `TranslationService`
|
|
264
200
|
|
|
265
|
-
|
|
266
|
-
description!: string | null;
|
|
201
|
+
---
|
|
267
202
|
|
|
268
|
-
|
|
269
|
-
variables!: string[] | null;
|
|
203
|
+
## Configuration Reference
|
|
270
204
|
|
|
271
|
-
|
|
272
|
-
|
|
205
|
+
```typescript
|
|
206
|
+
interface ILocalizationModuleConfig extends IDataSourceServiceOptions {
|
|
207
|
+
/** Default language code for fallback (default: 'en') */
|
|
208
|
+
defaultLanguageCode?: string;
|
|
273
209
|
|
|
274
|
-
|
|
275
|
-
|
|
210
|
+
/** Enable translation caching (default: false) */
|
|
211
|
+
enableCaching?: boolean;
|
|
276
212
|
|
|
277
|
-
|
|
278
|
-
|
|
213
|
+
/** Cache TTL in seconds (default: 300) */
|
|
214
|
+
cacheTtlSeconds?: number;
|
|
279
215
|
}
|
|
280
216
|
```
|
|
281
217
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
@Entity({ name: 'translation' })
|
|
286
|
-
export class Translation extends Identity {
|
|
287
|
-
@Column({ type: 'uuid', name: 'language_id' })
|
|
288
|
-
languageId!: string;
|
|
289
|
-
|
|
290
|
-
@Column({ type: 'uuid', name: 'translation_key_id' })
|
|
291
|
-
translationKeyId!: string;
|
|
292
|
-
|
|
293
|
-
@Column({ type: 'text' })
|
|
294
|
-
value!: string;
|
|
295
|
-
|
|
296
|
-
@Column({ type: 'boolean', default: false, name: 'is_verified' })
|
|
297
|
-
isVerified!: boolean;
|
|
218
|
+
---
|
|
298
219
|
|
|
299
|
-
|
|
300
|
-
isActive!: boolean;
|
|
220
|
+
## Feature Toggles
|
|
301
221
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
222
|
+
| Feature | Config | Default | Effect |
|
|
223
|
+
|---------|--------|---------|--------|
|
|
224
|
+
| Caching | `enableCaching: true` | `false` | Translations cached in HybridCache with `cacheTtlSeconds` TTL |
|
|
225
|
+
| Multi-tenant | `databaseMode: 'multi-tenant'` | `'single'` | Per-tenant DataSource connections |
|
|
305
226
|
|
|
306
|
-
|
|
307
|
-
@JoinColumn({ name: 'translation_key_id' })
|
|
308
|
-
translationKey?: TranslationKey;
|
|
309
|
-
}
|
|
310
|
-
```
|
|
227
|
+
> **Note:** Localization entities do **not** have company variants (`enableCompanyFeature` is ignored). All translation data is shared across companies.
|
|
311
228
|
|
|
312
229
|
---
|
|
313
230
|
|
|
314
|
-
##
|
|
231
|
+
## API Endpoints
|
|
315
232
|
|
|
316
|
-
|
|
233
|
+
All endpoints use **POST**. Authentication is required unless noted.
|
|
317
234
|
|
|
318
|
-
|
|
235
|
+
### Languages — `POST /localization/language/*`
|
|
319
236
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
237
|
+
| Endpoint | Permission | Description |
|
|
238
|
+
|----------|-----------|-------------|
|
|
239
|
+
| `POST /localization/language/insert` | `language.create` | Create a language |
|
|
240
|
+
| `POST /localization/language/get-all` | Public | List all active languages |
|
|
241
|
+
| `POST /localization/language/get/:id` | `language.read` | Get language by ID |
|
|
242
|
+
| `POST /localization/language/update` | `language.update` | Update language |
|
|
243
|
+
| `POST /localization/language/delete` | `language.delete` | Delete language |
|
|
244
|
+
| `POST /localization/language/set-default` | `language.update` | Set as default language |
|
|
328
245
|
|
|
329
|
-
|
|
330
|
-
async getDefaultLanguage(): Promise<Language | null>;
|
|
246
|
+
**Create language:**
|
|
331
247
|
|
|
332
|
-
|
|
333
|
-
|
|
248
|
+
```json
|
|
249
|
+
POST /localization/language/insert
|
|
250
|
+
{
|
|
251
|
+
"name": "English",
|
|
252
|
+
"nativeName": "English",
|
|
253
|
+
"code": "en",
|
|
254
|
+
"direction": "ltr",
|
|
255
|
+
"isActive": true,
|
|
256
|
+
"isDefault": true
|
|
334
257
|
}
|
|
335
258
|
```
|
|
336
259
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// Get keys by module
|
|
347
|
-
async getByModule(module: string): Promise<TranslationKey[]>;
|
|
348
|
-
|
|
349
|
-
// Get all unique modules
|
|
350
|
-
async getModules(): Promise<string[]>;
|
|
260
|
+
```json
|
|
261
|
+
POST /localization/language/insert
|
|
262
|
+
{
|
|
263
|
+
"name": "Arabic",
|
|
264
|
+
"nativeName": "العربية",
|
|
265
|
+
"code": "ar",
|
|
266
|
+
"direction": "rtl",
|
|
267
|
+
"isActive": true
|
|
351
268
|
}
|
|
352
269
|
```
|
|
353
270
|
|
|
354
|
-
###
|
|
355
|
-
|
|
356
|
-
Translation operations with queries:
|
|
271
|
+
### Translation Keys — `POST /localization/translation-key/*`
|
|
357
272
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
273
|
+
| Endpoint | Permission | Description |
|
|
274
|
+
|----------|-----------|-------------|
|
|
275
|
+
| `POST /localization/translation-key/insert` | `translation-key.create` | Register a new translation key |
|
|
276
|
+
| `POST /localization/translation-key/get-all` | `translation-key.read` | List all keys |
|
|
277
|
+
| `POST /localization/translation-key/get/:id` | `translation-key.read` | Get key by ID |
|
|
278
|
+
| `POST /localization/translation-key/get-by-module` | `translation-key.read` | Get all keys for a module |
|
|
279
|
+
| `POST /localization/translation-key/update` | `translation-key.update` | Update key (not readonly keys) |
|
|
280
|
+
| `POST /localization/translation-key/delete` | `translation-key.delete` | Delete key (not readonly) |
|
|
281
|
+
| `POST /localization/translation-key/get-missing` | `translation-key.read` | Get keys with no translation for a language |
|
|
362
282
|
|
|
363
|
-
|
|
364
|
-
async getByLanguageCode(languageCode: string): Promise<ITranslationsMap>;
|
|
283
|
+
**Register a key:**
|
|
365
284
|
|
|
366
|
-
|
|
367
|
-
|
|
285
|
+
```json
|
|
286
|
+
POST /localization/translation-key/insert
|
|
287
|
+
{
|
|
288
|
+
"key": "auth.login_success",
|
|
289
|
+
"module": "auth",
|
|
290
|
+
"description": "Shown after successful login",
|
|
291
|
+
"defaultValue": "Login successful",
|
|
292
|
+
"variables": [],
|
|
293
|
+
"isReadonly": true
|
|
294
|
+
}
|
|
295
|
+
```
|
|
368
296
|
|
|
369
|
-
|
|
370
|
-
async bulkUpdate(updates: BulkUpdateTranslationDto[]): Promise<Translation[]>;
|
|
297
|
+
**Key with variables:**
|
|
371
298
|
|
|
372
|
-
|
|
373
|
-
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"key": "user.welcome",
|
|
302
|
+
"module": "auth",
|
|
303
|
+
"description": "Welcome message with user name",
|
|
304
|
+
"defaultValue": "Welcome, {{name}}!",
|
|
305
|
+
"variables": ["name"],
|
|
306
|
+
"isReadonly": false
|
|
374
307
|
}
|
|
375
308
|
```
|
|
376
309
|
|
|
377
|
-
###
|
|
310
|
+
### Translations — `POST /localization/translation/*`
|
|
378
311
|
|
|
379
|
-
|
|
312
|
+
| Endpoint | Permission | Description |
|
|
313
|
+
|----------|-----------|-------------|
|
|
314
|
+
| `POST /localization/translation/insert` | `translation.create` | Add a translation value |
|
|
315
|
+
| `POST /localization/translation/get-all` | `translation.read` | List all translations |
|
|
316
|
+
| `POST /localization/translation/get-by-language` | Public | Get all translations for a language |
|
|
317
|
+
| `POST /localization/translation/get-by-language-module` | Public | Get translations for a language + module |
|
|
318
|
+
| `POST /localization/translation/update` | `translation.update` | Update a translation value |
|
|
319
|
+
| `POST /localization/translation/bulk-update` | `translation.update` | Bulk update translations (atomic) |
|
|
320
|
+
| `POST /localization/translation/delete` | `translation.delete` | Delete a translation |
|
|
321
|
+
| `POST /localization/translation/verify` | `translation.update` | Mark a translation as verified |
|
|
322
|
+
| `POST /localization/translation/unverify` | `translation.update` | Mark a translation as unverified |
|
|
380
323
|
|
|
381
|
-
|
|
382
|
-
@Injectable()
|
|
383
|
-
export class LocalizationHelperService {
|
|
384
|
-
// Replace {{variable}} placeholders with values
|
|
385
|
-
interpolate(template: string, variables?: Record<string, string | number>): string;
|
|
324
|
+
**Add a translation:**
|
|
386
325
|
|
|
387
|
-
|
|
388
|
-
|
|
326
|
+
```json
|
|
327
|
+
POST /localization/translation/insert
|
|
328
|
+
{
|
|
329
|
+
"keyId": "uuid-of-translation-key",
|
|
330
|
+
"languageId": "uuid-of-language",
|
|
331
|
+
"value": "Connexion réussie"
|
|
389
332
|
}
|
|
390
333
|
```
|
|
391
334
|
|
|
392
|
-
**
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
const helper = inject(LocalizationHelperService);
|
|
396
|
-
|
|
397
|
-
const result = helper.interpolate('Hello, {{name}}!', { name: 'John' });
|
|
398
|
-
// Output: "Hello, John!"
|
|
335
|
+
**Bulk update (atomic transaction):**
|
|
399
336
|
|
|
400
|
-
|
|
401
|
-
|
|
337
|
+
```json
|
|
338
|
+
POST /localization/translation/bulk-update
|
|
339
|
+
{
|
|
340
|
+
"languageId": "uuid-of-french",
|
|
341
|
+
"translations": [
|
|
342
|
+
{ "keyId": "uuid-1", "value": "Connexion réussie" },
|
|
343
|
+
{ "keyId": "uuid-2", "value": "Bienvenue, {{name}}!" },
|
|
344
|
+
{ "keyId": "uuid-3", "value": "Mot de passe incorrect" }
|
|
345
|
+
]
|
|
346
|
+
}
|
|
402
347
|
```
|
|
403
348
|
|
|
404
349
|
---
|
|
405
350
|
|
|
406
|
-
##
|
|
407
|
-
|
|
408
|
-
All endpoints follow **POST-only RPC** style (not REST).
|
|
409
|
-
|
|
410
|
-
### Language Controller
|
|
411
|
-
|
|
412
|
-
| Endpoint | Auth | Permission | Description |
|
|
413
|
-
|----------|------|------------|-------------|
|
|
414
|
-
| `POST /localization/language/insert` | JWT | `language.create` | Create language |
|
|
415
|
-
| `POST /localization/language/update` | JWT | `language.update` | Update language |
|
|
416
|
-
| `POST /localization/language/delete` | JWT | `language.delete` | Delete language |
|
|
417
|
-
| `POST /localization/language/get-by-id` | JWT | `language.read` | Get by ID |
|
|
418
|
-
| `POST /localization/language/get-all` | JWT | `language.read` | Get all with pagination |
|
|
419
|
-
| `POST /localization/language/get-active` | None | - | Get active languages |
|
|
420
|
-
| `POST /localization/language/get-default` | None | - | Get default language |
|
|
421
|
-
| `POST /localization/language/set-default` | JWT | `language.update` | Set default language |
|
|
422
|
-
|
|
423
|
-
### TranslationKey Controller
|
|
424
|
-
|
|
425
|
-
| Endpoint | Auth | Permission | Description |
|
|
426
|
-
|----------|------|------------|-------------|
|
|
427
|
-
| `POST /localization/translation-key/insert` | JWT | `translation-key.create` | Create key |
|
|
428
|
-
| `POST /localization/translation-key/update` | JWT | `translation-key.update` | Update key |
|
|
429
|
-
| `POST /localization/translation-key/delete` | JWT | `translation-key.delete` | Delete key |
|
|
430
|
-
| `POST /localization/translation-key/get-by-id` | JWT | `translation-key.read` | Get by ID |
|
|
431
|
-
| `POST /localization/translation-key/get-all` | JWT | `translation-key.read` | Get all with pagination |
|
|
432
|
-
| `POST /localization/translation-key/get-by-module` | JWT | `translation-key.read` | Get keys by module |
|
|
433
|
-
| `POST /localization/translation-key/get-modules` | JWT | `translation-key.read` | Get all unique module names |
|
|
434
|
-
|
|
435
|
-
### Translation Controller
|
|
436
|
-
|
|
437
|
-
| Endpoint | Auth | Permission | Description |
|
|
438
|
-
|----------|------|------------|-------------|
|
|
439
|
-
| `POST /localization/translation/insert` | JWT | `translation.create` | Create translation |
|
|
440
|
-
| `POST /localization/translation/update` | JWT | `translation.update` | Update translation |
|
|
441
|
-
| `POST /localization/translation/delete` | JWT | `translation.delete` | Delete translation |
|
|
442
|
-
| `POST /localization/translation/get-by-id` | JWT | `translation.read` | Get by ID |
|
|
443
|
-
| `POST /localization/translation/get-all` | JWT | `translation.read` | Get all with pagination |
|
|
444
|
-
| `POST /localization/translation/get-by-language` | None | - | Get translations by language code |
|
|
445
|
-
| `POST /localization/translation/get-by-module` | None | - | Get translations by module + language |
|
|
446
|
-
| `POST /localization/translation/update-many-values` | JWT | `translation.update` | Bulk update translations |
|
|
447
|
-
| `POST /localization/translation/get-missing` | JWT | `translation.read` | Get missing translations |
|
|
448
|
-
|
|
449
|
-
---
|
|
351
|
+
## Entities
|
|
450
352
|
|
|
451
|
-
|
|
353
|
+
| Entity | Table | Description |
|
|
354
|
+
|--------|-------|-------------|
|
|
355
|
+
| `Language` | `localization_language` | Language with code, direction, default flag |
|
|
356
|
+
| `TranslationKey` | `localization_translation_key` | Key registry with module, variables, readonly flag |
|
|
357
|
+
| `Translation` | `localization_translation` | Per-language translation values with verification status |
|
|
452
358
|
|
|
453
|
-
|
|
359
|
+
> Localization entities have **no company variants**. All translation data is shared globally.
|
|
454
360
|
|
|
455
361
|
```typescript
|
|
456
|
-
|
|
457
|
-
interface CreateLanguageDto {
|
|
458
|
-
code: string; // Required, ISO 639-1
|
|
459
|
-
name: string; // Required
|
|
460
|
-
nativeName?: string;
|
|
461
|
-
direction?: 'ltr' | 'rtl';
|
|
462
|
-
isActive?: boolean;
|
|
463
|
-
isDefault?: boolean;
|
|
464
|
-
serial?: number;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Update
|
|
468
|
-
interface UpdateLanguageDto extends Partial<CreateLanguageDto> {
|
|
469
|
-
id: string; // Required
|
|
470
|
-
}
|
|
362
|
+
import { LocalizationModule } from '@flusys/nestjs-localization';
|
|
471
363
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
direction: 'ltr' | 'rtl' | null;
|
|
479
|
-
isActive: boolean;
|
|
480
|
-
isDefault: boolean;
|
|
481
|
-
serial: number | null;
|
|
482
|
-
createdAt: Date;
|
|
483
|
-
updatedAt: Date;
|
|
484
|
-
}
|
|
364
|
+
TypeOrmModule.forRoot({
|
|
365
|
+
entities: [
|
|
366
|
+
...LocalizationModule.getEntities(),
|
|
367
|
+
// other entities
|
|
368
|
+
],
|
|
369
|
+
})
|
|
485
370
|
```
|
|
486
371
|
|
|
487
|
-
|
|
372
|
+
---
|
|
488
373
|
|
|
489
|
-
|
|
490
|
-
// Create
|
|
491
|
-
interface CreateTranslationKeyDto {
|
|
492
|
-
module: string; // Required
|
|
493
|
-
key: string; // Required
|
|
494
|
-
defaultMessage: string; // Required
|
|
495
|
-
description?: string;
|
|
496
|
-
variables?: string[];
|
|
497
|
-
isActive?: boolean;
|
|
498
|
-
serial?: number;
|
|
499
|
-
}
|
|
374
|
+
## Translation Workflow
|
|
500
375
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
376
|
+
```
|
|
377
|
+
1. Create Language → POST /localization/language/insert
|
|
378
|
+
2. Register Key → POST /localization/translation-key/insert
|
|
379
|
+
3. Add Translation → POST /localization/translation/insert
|
|
380
|
+
4. Verify Translation → POST /localization/translation/verify
|
|
381
|
+
5. Detect Missing → POST /localization/translation-key/get-missing
|
|
382
|
+
6. Bulk Update → POST /localization/translation/bulk-update
|
|
505
383
|
```
|
|
506
384
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// Create
|
|
511
|
-
interface CreateTranslationDto {
|
|
512
|
-
languageId: string; // Required
|
|
513
|
-
translationKeyId: string; // Required
|
|
514
|
-
value: string; // Required
|
|
515
|
-
isVerified?: boolean;
|
|
516
|
-
isActive?: boolean;
|
|
517
|
-
}
|
|
385
|
+
Translation statuses:
|
|
386
|
+
- **Unverified** (default) — Draft translation, may not be production-ready
|
|
387
|
+
- **Verified** — Approved by a translator or admin
|
|
518
388
|
|
|
519
|
-
|
|
520
|
-
interface UpdateTranslationDto extends Partial<CreateTranslationDto> {
|
|
521
|
-
id: string; // Required
|
|
522
|
-
}
|
|
389
|
+
---
|
|
523
390
|
|
|
524
|
-
|
|
525
|
-
interface BulkUpdateTranslationDto {
|
|
526
|
-
id: string;
|
|
527
|
-
value: string;
|
|
528
|
-
isVerified?: boolean;
|
|
529
|
-
}
|
|
391
|
+
## Variable Interpolation
|
|
530
392
|
|
|
531
|
-
|
|
532
|
-
translations: BulkUpdateTranslationDto[];
|
|
533
|
-
}
|
|
393
|
+
Translation values use `{{variableName}}` syntax. The `messageVariables` field in API responses carries the variable values that should be interpolated by the frontend:
|
|
534
394
|
|
|
535
|
-
|
|
536
|
-
interface GetTranslationsByLanguageDto {
|
|
537
|
-
languageCode: string;
|
|
538
|
-
}
|
|
395
|
+
**Server response:**
|
|
539
396
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
397
|
+
```json
|
|
398
|
+
{
|
|
399
|
+
"success": true,
|
|
400
|
+
"message": "Welcome, John!",
|
|
401
|
+
"messageKey": "user.welcome",
|
|
402
|
+
"messageVariables": { "name": "John" }
|
|
543
403
|
}
|
|
544
404
|
```
|
|
545
405
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
## Interfaces
|
|
549
|
-
|
|
550
|
-
### ILanguage
|
|
406
|
+
**Frontend interpolation:**
|
|
551
407
|
|
|
552
408
|
```typescript
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
nativeName: string | null;
|
|
558
|
-
direction: 'ltr' | 'rtl' | null;
|
|
559
|
-
isActive: boolean;
|
|
560
|
-
isDefault: boolean;
|
|
561
|
-
serial: number | null;
|
|
562
|
-
}
|
|
409
|
+
// Fetch translation for 'user.welcome': "Bienvenue, {{name}}!"
|
|
410
|
+
const template = translations['user.welcome'];
|
|
411
|
+
const message = template.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? key);
|
|
412
|
+
// Result: "Bienvenue, John!"
|
|
563
413
|
```
|
|
564
414
|
|
|
565
|
-
|
|
415
|
+
---
|
|
566
416
|
|
|
567
|
-
|
|
568
|
-
interface ITranslationKey {
|
|
569
|
-
id: string;
|
|
570
|
-
module: string;
|
|
571
|
-
key: string;
|
|
572
|
-
defaultMessage: string;
|
|
573
|
-
description: string | null;
|
|
574
|
-
variables: string[] | null;
|
|
575
|
-
isActive: boolean;
|
|
576
|
-
serial: number | null;
|
|
577
|
-
}
|
|
578
|
-
```
|
|
417
|
+
## Fallback Chain
|
|
579
418
|
|
|
580
|
-
|
|
419
|
+
When a translation is not found for the requested language, the resolution falls back in this order:
|
|
581
420
|
|
|
582
|
-
```typescript
|
|
583
|
-
interface ITranslation {
|
|
584
|
-
id: string;
|
|
585
|
-
languageId: string;
|
|
586
|
-
translationKeyId: string;
|
|
587
|
-
value: string;
|
|
588
|
-
isVerified: boolean;
|
|
589
|
-
isActive: boolean;
|
|
590
|
-
}
|
|
591
421
|
```
|
|
592
|
-
|
|
593
|
-
### ITranslationsMap
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
type ITranslationsMap = Record<string, string>;
|
|
597
|
-
|
|
598
|
-
// Example
|
|
599
|
-
{
|
|
600
|
-
'shared.save': 'Save',
|
|
601
|
-
'shared.cancel': 'Cancel',
|
|
602
|
-
'auth.login.title': 'Login',
|
|
603
|
-
}
|
|
422
|
+
Requested language → Default language (e.g., 'en') → key.defaultValue → key itself
|
|
604
423
|
```
|
|
605
424
|
|
|
606
425
|
---
|
|
607
426
|
|
|
608
|
-
##
|
|
609
|
-
|
|
610
|
-
| Resource | Permission | Description |
|
|
611
|
-
|----------|------------|-------------|
|
|
612
|
-
| Language | `language.create` | Create languages |
|
|
613
|
-
| Language | `language.read` | Read languages |
|
|
614
|
-
| Language | `language.update` | Update languages |
|
|
615
|
-
| Language | `language.delete` | Delete languages |
|
|
616
|
-
| TranslationKey | `translation-key.create` | Create keys |
|
|
617
|
-
| TranslationKey | `translation-key.read` | Read keys |
|
|
618
|
-
| TranslationKey | `translation-key.update` | Update keys |
|
|
619
|
-
| TranslationKey | `translation-key.delete` | Delete keys |
|
|
620
|
-
| Translation | `translation.create` | Create translations |
|
|
621
|
-
| Translation | `translation.read` | Read translations |
|
|
622
|
-
| Translation | `translation.update` | Update translations |
|
|
623
|
-
| Translation | `translation.delete` | Delete translations |
|
|
427
|
+
## RTL Support
|
|
624
428
|
|
|
625
|
-
|
|
429
|
+
Each language has a `direction` field: `'ltr'` (left-to-right) or `'rtl'` (right-to-left).
|
|
626
430
|
|
|
627
|
-
|
|
431
|
+
The frontend uses this to switch layout direction:
|
|
628
432
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
POST /localization/translation/get-by-language
|
|
635
|
-
{ languageCode: 'en' }
|
|
636
|
-
↓
|
|
637
|
-
TranslationController.getByLanguage()
|
|
638
|
-
↓
|
|
639
|
-
TranslationService.getByLanguageCode('en')
|
|
640
|
-
↓
|
|
641
|
-
Find language by code (active only)
|
|
642
|
-
↓
|
|
643
|
-
Query translations with JOIN on translation_key
|
|
644
|
-
↓
|
|
645
|
-
Build ITranslationsMap { key: value }
|
|
646
|
-
↓
|
|
647
|
-
Return SingleResponseDto<ITranslationsMap>
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### Admin Translation Update
|
|
651
|
-
|
|
652
|
-
```
|
|
653
|
-
Admin updates translations
|
|
654
|
-
↓
|
|
655
|
-
POST /localization/translation/update-many-values
|
|
656
|
-
{ translations: [{ id, value, isVerified }] }
|
|
657
|
-
↓
|
|
658
|
-
JWT Auth + permission check (translation.update)
|
|
659
|
-
↓
|
|
660
|
-
TranslationController.updateManyValues()
|
|
661
|
-
↓
|
|
662
|
-
TranslationService.bulkUpdate(translations)
|
|
663
|
-
↓
|
|
664
|
-
Update each translation in database
|
|
665
|
-
↓
|
|
666
|
-
Return BulkResponseDto with updated count
|
|
433
|
+
```typescript
|
|
434
|
+
// Angular
|
|
435
|
+
const lang = await this.http.post('/localization/language/get-all', {}).toPromise();
|
|
436
|
+
const current = lang.data.find(l => l.code === userLanguageCode);
|
|
437
|
+
document.dir = current.direction; // 'ltr' or 'rtl'
|
|
667
438
|
```
|
|
668
439
|
|
|
669
440
|
---
|
|
670
441
|
|
|
671
|
-
##
|
|
672
|
-
|
|
673
|
-
### Inject Services
|
|
674
|
-
|
|
675
|
-
```typescript
|
|
676
|
-
import { TranslationService, LanguageService } from '@flusys/nestjs-localization';
|
|
677
|
-
|
|
678
|
-
@Injectable()
|
|
679
|
-
export class MyService {
|
|
680
|
-
constructor(
|
|
681
|
-
private readonly translationService: TranslationService,
|
|
682
|
-
private readonly languageService: LanguageService,
|
|
683
|
-
) {}
|
|
684
|
-
|
|
685
|
-
async getTranslationsForUser(languageCode: string) {
|
|
686
|
-
return this.translationService.getByLanguageCode(languageCode);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async getActiveLanguages() {
|
|
690
|
-
return this.languageService.getActiveLanguages();
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
```
|
|
442
|
+
## Caching
|
|
694
443
|
|
|
695
|
-
|
|
444
|
+
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.
|
|
696
445
|
|
|
697
446
|
```typescript
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
export class NotificationService {
|
|
702
|
-
constructor(private readonly helper: LocalizationHelperService) {}
|
|
703
|
-
|
|
704
|
-
formatMessage(template: string, user: User) {
|
|
705
|
-
return this.helper.interpolate(template, {
|
|
706
|
-
name: user.name,
|
|
707
|
-
count: user.notifications.length,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
447
|
+
config: {
|
|
448
|
+
enableCaching: true,
|
|
449
|
+
cacheTtlSeconds: 600, // 10 minutes
|
|
710
450
|
}
|
|
711
451
|
```
|
|
712
452
|
|
|
713
|
-
|
|
453
|
+
Requires `HybridCache` (Redis or in-memory) to be configured via `CacheModule` from `@flusys/nestjs-shared`.
|
|
714
454
|
|
|
715
|
-
|
|
716
|
-
// seed/languages.seed.ts
|
|
717
|
-
const languages = [
|
|
718
|
-
{ code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', isDefault: true, isActive: true },
|
|
719
|
-
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', isActive: true },
|
|
720
|
-
{ code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr', isActive: true },
|
|
721
|
-
];
|
|
722
|
-
|
|
723
|
-
// Use LanguageService to seed
|
|
724
|
-
for (const lang of languages) {
|
|
725
|
-
await languageService.insert(lang);
|
|
726
|
-
}
|
|
727
|
-
```
|
|
455
|
+
---
|
|
728
456
|
|
|
729
|
-
|
|
457
|
+
## Exported Services
|
|
730
458
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
```
|
|
459
|
+
| Service | Description |
|
|
460
|
+
|---------|-------------|
|
|
461
|
+
| `LanguageService` | Language CRUD, default language management |
|
|
462
|
+
| `TranslationKeyService` | Key CRUD, missing-key detection, module filtering |
|
|
463
|
+
| `TranslationService` | Translation CRUD, bulk update, verification |
|
|
464
|
+
| `LocalizationConfigService` | Runtime config (default language, caching settings) |
|
|
465
|
+
| `LocalizationDataSourceProvider` | Dynamic DataSource per request |
|
|
739
466
|
|
|
740
467
|
---
|
|
741
468
|
|
|
742
|
-
##
|
|
469
|
+
## Integration with API Responses
|
|
743
470
|
|
|
744
|
-
|
|
471
|
+
All FLUSYS response DTOs include `messageKey` and optionally `messageVariables`. The frontend uses these to display localized messages:
|
|
745
472
|
|
|
746
|
-
|
|
747
|
-
- Cache key format: `localization:translations:{languageCode}`
|
|
748
|
-
- TTL configured via `cacheTtlSeconds`
|
|
749
|
-
- Cache invalidated on translation updates
|
|
750
|
-
|
|
751
|
-
---
|
|
473
|
+
**Backend (any service):**
|
|
752
474
|
|
|
753
|
-
|
|
475
|
+
```typescript
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
message: 'Welcome, John!',
|
|
479
|
+
messageKey: 'user.welcome',
|
|
480
|
+
messageVariables: { name: 'John' },
|
|
481
|
+
data: userDto,
|
|
482
|
+
};
|
|
483
|
+
```
|
|
754
484
|
|
|
755
|
-
|
|
485
|
+
**Frontend flow:**
|
|
756
486
|
|
|
757
|
-
|
|
487
|
+
1. On app init: `GET /localization/translation/get-by-language` for the user's language
|
|
488
|
+
2. Cache translations in a map: `{ 'user.welcome': 'Bienvenue, {{name}}!' }`
|
|
489
|
+
3. On every API response: look up `messageKey`, interpolate `messageVariables`
|
|
490
|
+
4. Display the localized message
|
|
758
491
|
|
|
759
|
-
|
|
492
|
+
---
|
|
760
493
|
|
|
761
|
-
|
|
762
|
-
App Init
|
|
763
|
-
↓
|
|
764
|
-
POST /localization/language/get-active → Load available languages
|
|
765
|
-
↓
|
|
766
|
-
User navigates to protected route
|
|
767
|
-
↓
|
|
768
|
-
POST /localization/translation/get-by-module
|
|
769
|
-
{ module: 'shared', languageCode: 'en' }
|
|
770
|
-
↓
|
|
771
|
-
POST /localization/translation/get-by-module
|
|
772
|
-
{ module: 'layout', languageCode: 'en' }
|
|
773
|
-
↓
|
|
774
|
-
User navigates to /storage
|
|
775
|
-
↓
|
|
776
|
-
POST /localization/translation/get-by-module
|
|
777
|
-
{ module: 'storage', languageCode: 'en' }
|
|
778
|
-
```
|
|
494
|
+
## Troubleshooting
|
|
779
495
|
|
|
780
|
-
|
|
496
|
+
**`Language not found` for a valid code**
|
|
781
497
|
|
|
782
|
-
|
|
783
|
-
> ```
|
|
784
|
-
> ✅ event.manager.event.create.success
|
|
785
|
-
> ❌ eventManager.eventCreateSuccess
|
|
786
|
-
> ```
|
|
498
|
+
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.
|
|
787
499
|
|
|
788
|
-
|
|
789
|
-
|--------|-------------|-------------|
|
|
790
|
-
| `shared` | `shared.*` | Common UI labels, actions, validation messages |
|
|
791
|
-
| `layout` | `layout.*` | Layout, navigation, topbar labels |
|
|
792
|
-
| `auth` | `auth.*` | Login, register, password, user management |
|
|
793
|
-
| `iam` | `iam.*` | Roles, permissions, actions |
|
|
794
|
-
| `storage` | `storage.*` | File upload, folders, storage config |
|
|
795
|
-
| `formBuilder` | `formBuilder.*` | Form builder UI |
|
|
796
|
-
| `email` | `email.*` | Email templates, config |
|
|
797
|
-
| `notification` | `notification.*` | Notifications |
|
|
798
|
-
| `eventManager` | `event.manager.*` | Calendar, events |
|
|
799
|
-
| `localization` | `localization.*` | Language & translation admin UI |
|
|
500
|
+
---
|
|
800
501
|
|
|
801
|
-
|
|
502
|
+
**Translation not returned for a module**
|
|
802
503
|
|
|
803
|
-
|
|
804
|
-
2. **Route Navigation:** Frontend calls `POST /localization/translation/get-by-module` for each feature
|
|
805
|
-
3. **Language Switch:** Frontend reloads translations for already-loaded modules
|
|
504
|
+
Check the `module` field on the `TranslationKey`. The `get-by-language-module` endpoint filters strictly by module name. Ensure it matches exactly (case-sensitive).
|
|
806
505
|
|
|
807
|
-
|
|
506
|
+
---
|
|
808
507
|
|
|
809
|
-
|
|
810
|
-
2. Frontend uses hardcoded fallback messages only
|
|
811
|
-
3. Backend localization module is not utilized (can still be included for admin use)
|
|
508
|
+
**Missing keys not detected**
|
|
812
509
|
|
|
813
|
-
|
|
510
|
+
`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.
|
|
814
511
|
|
|
815
|
-
|
|
512
|
+
---
|
|
816
513
|
|
|
817
|
-
|
|
818
|
-
// GET Active Languages
|
|
819
|
-
{
|
|
820
|
-
success: true,
|
|
821
|
-
data: [
|
|
822
|
-
{ id: '...', code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', isDefault: true },
|
|
823
|
-
{ id: '...', code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', isDefault: false }
|
|
824
|
-
]
|
|
825
|
-
}
|
|
514
|
+
**Caching returning stale translations**
|
|
826
515
|
|
|
827
|
-
|
|
828
|
-
{
|
|
829
|
-
success: true,
|
|
830
|
-
data: {
|
|
831
|
-
'storage.folders.title': 'Folders',
|
|
832
|
-
'storage.upload.success': 'File uploaded successfully',
|
|
833
|
-
'storage.delete.confirm': 'Are you sure you want to delete this file?'
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
```
|
|
516
|
+
When `enableCaching: true`, call `POST /localization/translation/bulk-update` (which invalidates the cache) instead of individual updates if you need immediate cache refresh.
|
|
837
517
|
|
|
838
518
|
---
|
|
839
519
|
|
|
840
|
-
|
|
520
|
+
**`No metadata for entity`**
|
|
841
521
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
| **Default Messages** | Always provide meaningful defaults |
|
|
847
|
-
| **Document Variables** | Use `variables` array to document required placeholders |
|
|
848
|
-
| **Description Field** | Add context for translators |
|
|
849
|
-
| **Soft Delete** | Use soft delete for translations (recoverable) |
|
|
850
|
-
| **Bulk Operations** | Use bulk update for efficiency |
|
|
522
|
+
Register entities:
|
|
523
|
+
```typescript
|
|
524
|
+
entities: [...LocalizationModule.getEntities()]
|
|
525
|
+
```
|
|
851
526
|
|
|
852
527
|
---
|
|
853
528
|
|
|
854
|
-
##
|
|
529
|
+
## License
|
|
855
530
|
|
|
856
|
-
|
|
857
|
-
|----------|---------|
|
|
858
|
-
| **Entities** | `Language`, `TranslationKey`, `Translation` |
|
|
859
|
-
| **Interfaces** | `ILanguage`, `ITranslationKey`, `ITranslation`, `ITranslationsMap`, `ILocalizationModuleConfig`, `LocalizationModuleOptions` |
|
|
860
|
-
| **DTOs** | `CreateLanguageDto`, `UpdateLanguageDto`, `LanguageResponseDto`, `CreateTranslationKeyDto`, `UpdateTranslationKeyDto`, `TranslationKeyResponseDto`, `CreateTranslationDto`, `UpdateTranslationDto`, `TranslationResponseDto`, `BulkUpdateTranslationsDto`, `GetTranslationsByLanguageDto`, `GetTranslationsByModuleDto` |
|
|
861
|
-
| **Services** | `LanguageService`, `TranslationKeyService`, `TranslationService`, `LocalizationHelperService`, `LocalizationConfigService`, `LocalizationDataSourceProvider` |
|
|
862
|
-
| **Controllers** | `LanguageController`, `TranslationKeyController`, `TranslationController` |
|
|
863
|
-
| **Module** | `LocalizationModule` |
|
|
864
|
-
| **Constants** | `LOCALIZATION_MODULE_OPTIONS` |
|
|
531
|
+
MIT © FLUSYS
|
|
865
532
|
|
|
866
533
|
---
|
|
867
534
|
|
|
868
|
-
**
|
|
869
|
-
**Version:** 3.0.2
|
|
870
|
-
**NestJS Version:** 11
|
|
535
|
+
> Part of the **FLUSYS** framework — a full-stack monorepo powering Angular 21 + NestJS 11 applications.
|
|
@@ -4,12 +4,17 @@ import { CreateLanguageDto, LanguageResponseDto, UpdateLanguageDto } from '../dt
|
|
|
4
4
|
import { ILanguage } from '../interfaces';
|
|
5
5
|
import { LanguageService } from '../services';
|
|
6
6
|
declare const LanguageController_base: abstract new (service: LanguageService) => {
|
|
7
|
+
readonly enabledEndpoints: import("@flusys/nestjs-shared/classes").ApiEndpoint[] | "all";
|
|
7
8
|
service: LanguageService;
|
|
9
|
+
isEnabled(endpoint: import("@flusys/nestjs-shared/classes").ApiEndpoint): boolean;
|
|
8
10
|
insert(addDto: CreateLanguageDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<LanguageResponseDto>>;
|
|
9
11
|
insertMany(addDto: CreateLanguageDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<LanguageResponseDto>>;
|
|
10
12
|
getById(id: string, body: import("@flusys/nestjs-shared/dtos").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<LanguageResponseDto>>;
|
|
13
|
+
getByIds(body: import("@flusys/nestjs-shared/dtos").GetByIdsDto, user: ILoggedUserInfo | null): Promise<ListResponseDto<LanguageResponseDto>>;
|
|
11
14
|
update(updateDto: UpdateLanguageDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<LanguageResponseDto>>;
|
|
12
15
|
updateMany(updateDtos: UpdateLanguageDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<LanguageResponseDto>>;
|
|
16
|
+
bulkUpsert(dtos: (CreateLanguageDto | UpdateLanguageDto)[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<LanguageResponseDto>>;
|
|
17
|
+
getByFilter(filter: Record<string, any>, user: ILoggedUserInfo | null): Promise<SingleResponseDto<LanguageResponseDto>>;
|
|
13
18
|
getAll(filterAndPaginationDto: import("@flusys/nestjs-shared/dtos").FilterAndPaginationDto, user: ILoggedUserInfo | null, search?: string): Promise<ListResponseDto<LanguageResponseDto>>;
|
|
14
19
|
delete(deleteDto: import("@flusys/nestjs-shared/dtos").DeleteDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").MessageResponseDto>;
|
|
15
20
|
};
|
|
@@ -4,12 +4,17 @@ import { CreateTranslationKeyDto, GetByModuleDto, TranslationKeyResponseDto, Upd
|
|
|
4
4
|
import { ITranslationKey } from '../interfaces';
|
|
5
5
|
import { TranslationKeyService } from '../services';
|
|
6
6
|
declare const TranslationKeyController_base: abstract new (service: TranslationKeyService) => {
|
|
7
|
+
readonly enabledEndpoints: import("@flusys/nestjs-shared/classes").ApiEndpoint[] | "all";
|
|
7
8
|
service: TranslationKeyService;
|
|
9
|
+
isEnabled(endpoint: import("@flusys/nestjs-shared/classes").ApiEndpoint): boolean;
|
|
8
10
|
insert(addDto: CreateTranslationKeyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationKeyResponseDto>>;
|
|
9
11
|
insertMany(addDto: CreateTranslationKeyDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<TranslationKeyResponseDto>>;
|
|
10
12
|
getById(id: string, body: import("@flusys/nestjs-shared/dtos").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationKeyResponseDto>>;
|
|
13
|
+
getByIds(body: import("@flusys/nestjs-shared/dtos").GetByIdsDto, user: ILoggedUserInfo | null): Promise<ListResponseDto<TranslationKeyResponseDto>>;
|
|
11
14
|
update(updateDto: UpdateTranslationKeyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationKeyResponseDto>>;
|
|
12
15
|
updateMany(updateDtos: UpdateTranslationKeyDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<TranslationKeyResponseDto>>;
|
|
16
|
+
bulkUpsert(dtos: (CreateTranslationKeyDto | UpdateTranslationKeyDto)[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").BulkResponseDto<TranslationKeyResponseDto>>;
|
|
17
|
+
getByFilter(filter: Record<string, any>, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationKeyResponseDto>>;
|
|
13
18
|
getAll(filterAndPaginationDto: import("@flusys/nestjs-shared/dtos").FilterAndPaginationDto, user: ILoggedUserInfo | null, search?: string): Promise<ListResponseDto<TranslationKeyResponseDto>>;
|
|
14
19
|
delete(deleteDto: import("@flusys/nestjs-shared/dtos").DeleteDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").MessageResponseDto>;
|
|
15
20
|
};
|
|
@@ -4,12 +4,17 @@ import { BulkUpdateTranslationsDto, CreateTranslationDto, GetTranslationsByLangu
|
|
|
4
4
|
import { ITranslation, ITranslationKey, ITranslationsMap } from '../interfaces';
|
|
5
5
|
import { TranslationService } from '../services';
|
|
6
6
|
declare const TranslationController_base: abstract new (service: TranslationService) => {
|
|
7
|
+
readonly enabledEndpoints: import("@flusys/nestjs-shared/classes").ApiEndpoint[] | "all";
|
|
7
8
|
service: TranslationService;
|
|
9
|
+
isEnabled(endpoint: import("@flusys/nestjs-shared/classes").ApiEndpoint): boolean;
|
|
8
10
|
insert(addDto: CreateTranslationDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationResponseDto>>;
|
|
9
11
|
insertMany(addDto: CreateTranslationDto[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<TranslationResponseDto>>;
|
|
10
12
|
getById(id: string, body: import("@flusys/nestjs-shared/dtos").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationResponseDto>>;
|
|
13
|
+
getByIds(body: import("@flusys/nestjs-shared/dtos").GetByIdsDto, user: ILoggedUserInfo | null): Promise<ListResponseDto<TranslationResponseDto>>;
|
|
11
14
|
update(updateDto: UpdateTranslationDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationResponseDto>>;
|
|
12
15
|
updateMany(updateDtos: UpdateTranslationDto[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<TranslationResponseDto>>;
|
|
16
|
+
bulkUpsert(dtos: (CreateTranslationDto | UpdateTranslationDto)[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<TranslationResponseDto>>;
|
|
17
|
+
getByFilter(filter: Record<string, any>, user: ILoggedUserInfo | null): Promise<SingleResponseDto<TranslationResponseDto>>;
|
|
13
18
|
getAll(filterAndPaginationDto: import("@flusys/nestjs-shared/dtos").FilterAndPaginationDto, user: ILoggedUserInfo | null, search?: string): Promise<ListResponseDto<TranslationResponseDto>>;
|
|
14
19
|
delete(deleteDto: import("@flusys/nestjs-shared/dtos").DeleteDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared/dtos").MessageResponseDto>;
|
|
15
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-localization",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Localization module for multi-language support",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"express": "^4.18.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@flusys/nestjs-core": "4.0
|
|
79
|
-
"@flusys/nestjs-shared": "4.0
|
|
78
|
+
"@flusys/nestjs-core": "4.1.0",
|
|
79
|
+
"@flusys/nestjs-shared": "4.1.0"
|
|
80
80
|
}
|
|
81
81
|
}
|