@iamalond/nestjs-i18next 1.3.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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/nestjs-i18next.d.ts +24 -0
- package/dist/nestjs-i18next.js +2 -0
- package/dist/nestjs-i18next.json-loader.d.ts +18 -0
- package/dist/nestjs-i18next.json-loader.js +157 -0
- package/dist/nestjs-i18next.module-definition.d.ts +4 -0
- package/dist/nestjs-i18next.module-definition.js +13 -0
- package/dist/nestjs-i18next.module.d.ts +7 -0
- package/dist/nestjs-i18next.module.js +31 -0
- package/dist/nestjs-i18next.service.d.ts +25 -0
- package/dist/nestjs-i18next.service.js +129 -0
- package/package.json +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matvey iamAlond
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>
|
|
3
|
+
<a href="#"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo"></a>
|
|
4
|
+
</h1>
|
|
5
|
+
🌍 Internalize your <a href="https://nestjs.com">NestJS</a> application using the nestjs-i18next module<b>
|
|
6
|
+
<br/><br/>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href='https://img.shields.io/npm/v/@iamalond/nestjs-i18next'><img src="https://img.shields.io/npm/v/@iamalond/nestjs-i18next" alt="NPM Version" /></a>
|
|
11
|
+
<a href='https://img.shields.io/npm/l/@iamalond/nestjs-i18next'><img src="https://img.shields.io/npm/l/@iamalond/nestjs-i18next" alt="NPM License" /></a>
|
|
12
|
+
<a href='https://img.shields.io/npm/dm/@iamalond/nestjs-i18next'><img src="https://img.shields.io/npm/dm/@iamalond/nestjs-i18next" alt="NPM Downloads" /></a>
|
|
13
|
+
<a href='https://img.shields.io/github/last-commit/iamalond/@iamalond/nestjs-i18next'><img src="https://img.shields.io/github/last-commit/iamalond/@iamalond/nestjs-i18next" alt="Last commit" /></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## About the Project
|
|
17
|
+
|
|
18
|
+
**nestjs-i18next** is an internationalization module for NestJS applications.
|
|
19
|
+
|
|
20
|
+
The project was inspired by the popular `nestjs-i18n` module, but with a major enhancement: **full type-safety for translation arguments**. No more `any` in your translations — your keys and variables are strictly typed based on your JSON files.
|
|
21
|
+
|
|
22
|
+
### Key Features:
|
|
23
|
+
|
|
24
|
+
- 🛡️ **Strict Typing**: Automatic type generation for paths and arguments.
|
|
25
|
+
- 🧩 **ICU Format**: Support for variables and complex pluralization (similar to the `i18next-icu` plugin).
|
|
26
|
+
- 🔄 **Live Reload**: Watch for translation file changes without restarting the server.
|
|
27
|
+
- 📂 **Recursive Namespaces**: Support for nested subfolders to organize your translations cleanly.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install nestjs-i18next
|
|
35
|
+
yarn add nestjs-i18next
|
|
36
|
+
pnpm add nestjs-i18next
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { I18NextModule } from '@iamalond/nestjs-i18next';
|
|
43
|
+
|
|
44
|
+
@Module({
|
|
45
|
+
imports: [
|
|
46
|
+
I18nextModule.forRoot({
|
|
47
|
+
fallbackLanguage: 'en',
|
|
48
|
+
throwOnMissingKey: true,
|
|
49
|
+
logging: true,
|
|
50
|
+
generatedTypesPath: path.join(process.cwd(), 'src/i18n/index.d.ts'),
|
|
51
|
+
loadingOptions: {
|
|
52
|
+
path: path.join(process.cwd(), 'src/i18n'),
|
|
53
|
+
subfolders: true, // recursive loading
|
|
54
|
+
watch: true
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
export class AppModule {}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## File Structure
|
|
63
|
+
|
|
64
|
+
By default, the module uses the locale/ns.json structure. With subfolders: true, you can use recursive nesting. Keys will be resolved as namespace.subfolder.key:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
src/i18n/
|
|
68
|
+
├── en/
|
|
69
|
+
│ ├── common.json
|
|
70
|
+
│ └── auth/
|
|
71
|
+
│ └── errors.json <-- key will be "auth.errors.invalid_password"
|
|
72
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./nestjs-i18next"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./nestjs-i18next.module"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./nestjs-i18next.module-definition"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./nestjs-i18next.service"), exports);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type I18nextModuleOptions = {
|
|
2
|
+
fallbackLanguage: string;
|
|
3
|
+
loadingOptions: {
|
|
4
|
+
path: string;
|
|
5
|
+
subfolders?: boolean;
|
|
6
|
+
watch?: boolean;
|
|
7
|
+
};
|
|
8
|
+
logging?: boolean;
|
|
9
|
+
throwOnMissingKey?: boolean;
|
|
10
|
+
generatedTypesPath?: string;
|
|
11
|
+
};
|
|
12
|
+
export type TranslateOptions<P, T> = {
|
|
13
|
+
lang?: string;
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
} & (Extract<T, {
|
|
17
|
+
key: P;
|
|
18
|
+
}> extends {
|
|
19
|
+
args: infer A;
|
|
20
|
+
} ? {
|
|
21
|
+
args: A;
|
|
22
|
+
} : {
|
|
23
|
+
args?: never;
|
|
24
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { I18nextModuleOptions } from './nestjs-i18next';
|
|
3
|
+
export declare class I18nextJsonLoader implements OnModuleInit, OnModuleDestroy {
|
|
4
|
+
protected readonly options: I18nextModuleOptions;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private watcher;
|
|
7
|
+
private watchTimeout;
|
|
8
|
+
constructor(options: I18nextModuleOptions);
|
|
9
|
+
onModuleInit(): void;
|
|
10
|
+
onModuleDestroy(): void;
|
|
11
|
+
private onRefreshCb?;
|
|
12
|
+
onChange(cb: () => void): void;
|
|
13
|
+
private startWatcher;
|
|
14
|
+
parseTransitions(): Record<string, any> | undefined;
|
|
15
|
+
private readDirectory;
|
|
16
|
+
private generateTypes;
|
|
17
|
+
private generateUnionType;
|
|
18
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var I18nextJsonLoader_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.I18nextJsonLoader = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
8
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
|
+
const nestjs_i18next_module_definition_1 = require("./nestjs-i18next.module-definition");
|
|
10
|
+
let I18nextJsonLoader = I18nextJsonLoader_1 = class I18nextJsonLoader {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.logger = new common_1.Logger(I18nextJsonLoader_1.name);
|
|
14
|
+
this.watcher = null;
|
|
15
|
+
this.watchTimeout = null;
|
|
16
|
+
}
|
|
17
|
+
onModuleInit() {
|
|
18
|
+
if (this.options.loadingOptions.watch) {
|
|
19
|
+
this.startWatcher();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
onModuleDestroy() {
|
|
23
|
+
this.watcher?.close();
|
|
24
|
+
if (this.watchTimeout)
|
|
25
|
+
clearTimeout(this.watchTimeout);
|
|
26
|
+
}
|
|
27
|
+
onChange(cb) {
|
|
28
|
+
this.logger.debug('Watch file changes');
|
|
29
|
+
this.onRefreshCb = cb;
|
|
30
|
+
}
|
|
31
|
+
startWatcher() {
|
|
32
|
+
const targetPath = path_1.default.resolve(process.cwd(), this.options.loadingOptions.path);
|
|
33
|
+
try {
|
|
34
|
+
this.watcher = fs_1.default.watch(targetPath, { recursive: true }, (eventType, filename) => {
|
|
35
|
+
if (filename && filename.endsWith('.json')) {
|
|
36
|
+
if (this.watchTimeout)
|
|
37
|
+
clearTimeout(this.watchTimeout);
|
|
38
|
+
this.watchTimeout = setTimeout(() => {
|
|
39
|
+
if (this.options.logging) {
|
|
40
|
+
this.logger.log(`Translation file changed. Checking translation changes...`);
|
|
41
|
+
}
|
|
42
|
+
if (this.onRefreshCb)
|
|
43
|
+
this.onRefreshCb();
|
|
44
|
+
this.watchTimeout = null;
|
|
45
|
+
}, 100);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
this.logger.error(`Could not start watcher: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
parseTransitions() {
|
|
54
|
+
const translations = {};
|
|
55
|
+
if (!fs_1.default.existsSync(this.options.loadingOptions.path)) {
|
|
56
|
+
this.logger.error(`Translations directory not found: ${this.options.loadingOptions.path}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const languages = fs_1.default
|
|
60
|
+
.readdirSync(this.options.loadingOptions.path)
|
|
61
|
+
.filter(f => fs_1.default.statSync(path_1.default.join(this.options.loadingOptions.path, f)).isDirectory());
|
|
62
|
+
for (const lang of languages) {
|
|
63
|
+
const langPath = path_1.default.join(this.options.loadingOptions.path, lang);
|
|
64
|
+
translations[lang] = this.readDirectory(langPath);
|
|
65
|
+
}
|
|
66
|
+
if (this.options.generatedTypesPath) {
|
|
67
|
+
this.generateTypes(translations);
|
|
68
|
+
}
|
|
69
|
+
return translations;
|
|
70
|
+
}
|
|
71
|
+
readDirectory(dirPath) {
|
|
72
|
+
const result = {};
|
|
73
|
+
const files = fs_1.default.readdirSync(dirPath);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const fullPath = path_1.default.join(dirPath, file);
|
|
76
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
77
|
+
if (stat.isDirectory() && this.options.loadingOptions.subfolders) {
|
|
78
|
+
result[file] = this.readDirectory(fullPath);
|
|
79
|
+
}
|
|
80
|
+
else if (stat.isFile() && file.endsWith('.json')) {
|
|
81
|
+
const ns = path_1.default.parse(file).name;
|
|
82
|
+
try {
|
|
83
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
84
|
+
result[ns] = JSON.parse(content);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
this.logger.warn(`Failed to parse JSON at ${fullPath}, skipping this file`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
generateTypes(translations) {
|
|
94
|
+
const outputPath = this.options.generatedTypesPath;
|
|
95
|
+
if (!outputPath)
|
|
96
|
+
return;
|
|
97
|
+
this.logger.log(`Checking translations changes for generated types...`);
|
|
98
|
+
const schema = translations[this.options.fallbackLanguage];
|
|
99
|
+
const fileContent = `/* STOP!!! DO NOT EDIT, file generated by nestjs-i18next */
|
|
100
|
+
|
|
101
|
+
/* eslint-disable */
|
|
102
|
+
/* prettier-ignore */
|
|
103
|
+
export type I18nTranslations = ${this.generateUnionType(schema)};
|
|
104
|
+
|
|
105
|
+
/* prettier-ignore */
|
|
106
|
+
export type I18nPath = I18nTranslations['key'];
|
|
107
|
+
|
|
108
|
+
/* prettier-ignore */
|
|
109
|
+
export type ExtractArgs<P extends I18nPath> = Extract<I18nTranslations, { key: P }> extends { args: infer A }
|
|
110
|
+
? A
|
|
111
|
+
: never;
|
|
112
|
+
`;
|
|
113
|
+
fs_1.default.mkdirSync(path_1.default.dirname(outputPath), {
|
|
114
|
+
recursive: true
|
|
115
|
+
});
|
|
116
|
+
let currentFileContent = null;
|
|
117
|
+
try {
|
|
118
|
+
currentFileContent = fs_1.default.readFileSync(outputPath, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
this.logger.error(err);
|
|
122
|
+
}
|
|
123
|
+
if (currentFileContent != fileContent) {
|
|
124
|
+
fs_1.default.writeFileSync(outputPath, fileContent);
|
|
125
|
+
this.logger
|
|
126
|
+
.log(`Translations types generated in: ${outputPath}. Please do not edit this file manually.
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.logger.log('No translations changes detected');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
generateUnionType(obj, prefix = '') {
|
|
134
|
+
const entries = Object.entries(obj);
|
|
135
|
+
return entries
|
|
136
|
+
.map(([key, value]) => {
|
|
137
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
const simpleVars = [...value.matchAll(/{{(\w+)}}/g)].map(m => m[1]);
|
|
140
|
+
const icuVars = [...value.matchAll(/{(\w+),\s*plural/g)].map(m => m[1]);
|
|
141
|
+
const allVars = [...new Set([...simpleVars, ...icuVars])];
|
|
142
|
+
if (allVars.length === 0) {
|
|
143
|
+
return `{ key: "${fullKey}" }`;
|
|
144
|
+
}
|
|
145
|
+
const args = allVars.map(v => `"${v}": any`).join(', ');
|
|
146
|
+
return `{ key: "${fullKey}", args: { ${args} } }`;
|
|
147
|
+
}
|
|
148
|
+
return this.generateUnionType(value, fullKey);
|
|
149
|
+
})
|
|
150
|
+
.join('\n | ');
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
exports.I18nextJsonLoader = I18nextJsonLoader;
|
|
154
|
+
exports.I18nextJsonLoader = I18nextJsonLoader = I18nextJsonLoader_1 = tslib_1.__decorate([
|
|
155
|
+
tslib_1.__param(0, (0, common_1.Inject)(nestjs_i18next_module_definition_1.MODULE_OPTIONS_TOKEN)),
|
|
156
|
+
tslib_1.__metadata("design:paramtypes", [Object])
|
|
157
|
+
], I18nextJsonLoader);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { I18nextModuleOptions } from './nestjs-i18next';
|
|
2
|
+
export declare const ConfigurableModuleClass: import("@nestjs/common").ConfigurableModuleCls<I18nextModuleOptions, "forRoot", "createLibraryOptions", {
|
|
3
|
+
isGlobal: boolean;
|
|
4
|
+
}>, MODULE_OPTIONS_TOKEN: string | symbol;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _a;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.MODULE_OPTIONS_TOKEN = exports.ConfigurableModuleClass = void 0;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
_a = new common_1.ConfigurableModuleBuilder()
|
|
7
|
+
.setClassMethodName('forRoot')
|
|
8
|
+
.setFactoryMethodName('createLibraryOptions')
|
|
9
|
+
.setExtras({ isGlobal: true }, (definition, extras) => ({
|
|
10
|
+
...definition,
|
|
11
|
+
global: extras.isGlobal
|
|
12
|
+
}))
|
|
13
|
+
.build(), exports.ConfigurableModuleClass = _a.ConfigurableModuleClass, exports.MODULE_OPTIONS_TOKEN = _a.MODULE_OPTIONS_TOKEN;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ConfigurableModuleClass } from './nestjs-i18next.module-definition';
|
|
3
|
+
export declare class I18nextModule extends ConfigurableModuleClass implements OnModuleInit {
|
|
4
|
+
private readonly logger;
|
|
5
|
+
private readonly options;
|
|
6
|
+
onModuleInit(): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var I18nextModule_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.I18nextModule = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const nestjs_i18next_json_loader_1 = require("./nestjs-i18next.json-loader");
|
|
8
|
+
const nestjs_i18next_module_definition_1 = require("./nestjs-i18next.module-definition");
|
|
9
|
+
const nestjs_i18next_service_1 = require("./nestjs-i18next.service");
|
|
10
|
+
let I18nextModule = I18nextModule_1 = class I18nextModule extends nestjs_i18next_module_definition_1.ConfigurableModuleClass {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.logger = new common_1.Logger(I18nextModule_1.name);
|
|
14
|
+
}
|
|
15
|
+
onModuleInit() {
|
|
16
|
+
if (this.options.logging) {
|
|
17
|
+
this.logger.log(`I18nextModule initialized with options: ${JSON.stringify(this.options)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
exports.I18nextModule = I18nextModule;
|
|
22
|
+
tslib_1.__decorate([
|
|
23
|
+
(0, common_1.Inject)(nestjs_i18next_module_definition_1.MODULE_OPTIONS_TOKEN),
|
|
24
|
+
tslib_1.__metadata("design:type", Object)
|
|
25
|
+
], I18nextModule.prototype, "options", void 0);
|
|
26
|
+
exports.I18nextModule = I18nextModule = I18nextModule_1 = tslib_1.__decorate([
|
|
27
|
+
(0, common_1.Module)({
|
|
28
|
+
providers: [nestjs_i18next_service_1.I18nextService, nestjs_i18next_json_loader_1.I18nextJsonLoader],
|
|
29
|
+
exports: [nestjs_i18next_service_1.I18nextService]
|
|
30
|
+
})
|
|
31
|
+
], I18nextModule);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { I18nextModuleOptions, TranslateOptions } from './nestjs-i18next';
|
|
4
|
+
import { I18nextJsonLoader } from './nestjs-i18next.json-loader';
|
|
5
|
+
export declare class I18nextService<T extends {
|
|
6
|
+
key: string;
|
|
7
|
+
}> implements OnModuleInit {
|
|
8
|
+
protected readonly options: I18nextModuleOptions;
|
|
9
|
+
private readonly jsonLoader;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
private readonly translations$;
|
|
12
|
+
constructor(options: I18nextModuleOptions, jsonLoader: I18nextJsonLoader);
|
|
13
|
+
onModuleInit(): void;
|
|
14
|
+
refresh(): void;
|
|
15
|
+
get state$(): Observable<Record<string, any>>;
|
|
16
|
+
private get currentTranslations();
|
|
17
|
+
getSupportedLanguages(): string[];
|
|
18
|
+
getTranslations(): Record<string, any>;
|
|
19
|
+
getTranslationsForNamespace(lang: string): any;
|
|
20
|
+
translate<P extends T['key']>(key: P, options: TranslateOptions<P, T>): string;
|
|
21
|
+
t<P extends T['key']>(key: P, options: TranslateOptions<P, T>): string;
|
|
22
|
+
private getValueByKey;
|
|
23
|
+
private replaceArgs;
|
|
24
|
+
private handleMissingKey;
|
|
25
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var I18nextService_1;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.I18nextService = void 0;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const common_1 = require("@nestjs/common");
|
|
7
|
+
const rxjs_1 = require("rxjs");
|
|
8
|
+
const nestjs_i18next_json_loader_1 = require("./nestjs-i18next.json-loader");
|
|
9
|
+
const nestjs_i18next_module_definition_1 = require("./nestjs-i18next.module-definition");
|
|
10
|
+
let I18nextService = I18nextService_1 = class I18nextService {
|
|
11
|
+
constructor(options, jsonLoader) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.jsonLoader = jsonLoader;
|
|
14
|
+
this.logger = new common_1.Logger(I18nextService_1.name);
|
|
15
|
+
this.translations$ = new rxjs_1.BehaviorSubject({});
|
|
16
|
+
}
|
|
17
|
+
onModuleInit() {
|
|
18
|
+
this.jsonLoader.onChange(() => {
|
|
19
|
+
this.refresh();
|
|
20
|
+
});
|
|
21
|
+
this.refresh();
|
|
22
|
+
}
|
|
23
|
+
refresh() {
|
|
24
|
+
const loaded = this.jsonLoader.parseTransitions();
|
|
25
|
+
if (loaded) {
|
|
26
|
+
this.translations$.next(loaded);
|
|
27
|
+
if (this.options.logging) {
|
|
28
|
+
this.logger.log(`Translations updated. Loaded languages: ${Object.keys(loaded).length}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get state$() {
|
|
33
|
+
return this.translations$
|
|
34
|
+
.asObservable()
|
|
35
|
+
.pipe((0, rxjs_1.distinctUntilChanged)((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)));
|
|
36
|
+
}
|
|
37
|
+
get currentTranslations() {
|
|
38
|
+
return this.translations$.getValue();
|
|
39
|
+
}
|
|
40
|
+
getSupportedLanguages() {
|
|
41
|
+
return Object.keys(this.currentTranslations);
|
|
42
|
+
}
|
|
43
|
+
getTranslations() {
|
|
44
|
+
return this.currentTranslations;
|
|
45
|
+
}
|
|
46
|
+
getTranslationsForNamespace(lang) {
|
|
47
|
+
return this.currentTranslations[lang];
|
|
48
|
+
}
|
|
49
|
+
translate(key, options) {
|
|
50
|
+
return this.t(key, options);
|
|
51
|
+
}
|
|
52
|
+
t(key, options) {
|
|
53
|
+
const { lang, debug, defaultValue } = options || {};
|
|
54
|
+
const fallback = this.options.fallbackLanguage;
|
|
55
|
+
let result = this.getValueByKey(key, lang ?? fallback);
|
|
56
|
+
if (result === undefined && lang && lang !== fallback) {
|
|
57
|
+
result = this.getValueByKey(key, fallback);
|
|
58
|
+
}
|
|
59
|
+
if (result === undefined) {
|
|
60
|
+
if (defaultValue)
|
|
61
|
+
return defaultValue;
|
|
62
|
+
return this.handleMissingKey(key, lang ?? fallback);
|
|
63
|
+
}
|
|
64
|
+
if (debug) {
|
|
65
|
+
return key;
|
|
66
|
+
}
|
|
67
|
+
if (typeof result === 'string') {
|
|
68
|
+
const args = options?.args;
|
|
69
|
+
return args ? this.replaceArgs(result, args, lang ?? fallback) : result;
|
|
70
|
+
}
|
|
71
|
+
return String(result);
|
|
72
|
+
}
|
|
73
|
+
getValueByKey(key, lang) {
|
|
74
|
+
const translations = this.currentTranslations[lang];
|
|
75
|
+
if (!translations)
|
|
76
|
+
return undefined;
|
|
77
|
+
const keys = key.split('.');
|
|
78
|
+
let current = translations;
|
|
79
|
+
for (const k of keys) {
|
|
80
|
+
current = current?.[k];
|
|
81
|
+
if (current === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
replaceArgs(text, args, lang) {
|
|
87
|
+
// 1. Find ICU plural: {key, plural, =0 {..} one {..} few {..} many {..} other {..}}
|
|
88
|
+
const icuRegex = /{(\w+),\s*plural,\s*([^{}]*({[^{}]*}[^{}]*)*)}/g;
|
|
89
|
+
const processedText = text.replace(icuRegex, (match, propName, rulesString) => {
|
|
90
|
+
const value = args[propName];
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return match;
|
|
93
|
+
// Extract args
|
|
94
|
+
const ruleRegex = /(=?\w+)\s*{([^}]+)}/g;
|
|
95
|
+
const rules = {};
|
|
96
|
+
let m;
|
|
97
|
+
while ((m = ruleRegex.exec(rulesString)) !== null) {
|
|
98
|
+
rules[m[1]] = m[2];
|
|
99
|
+
}
|
|
100
|
+
// Select values (=0, =1)
|
|
101
|
+
if (rules[`=${value}`])
|
|
102
|
+
return rules[`=${value}`];
|
|
103
|
+
// Use categories (one, few, many, other)
|
|
104
|
+
const pluralCategory = new Intl.PluralRules(lang).select(value);
|
|
105
|
+
const result = rules[pluralCategory] || rules['other'] || '';
|
|
106
|
+
// Use # in plurals for value
|
|
107
|
+
return result.replace(/#/g, String(value));
|
|
108
|
+
});
|
|
109
|
+
// 2. Args {{key}} as args.key
|
|
110
|
+
return processedText.replace(/{{(\w+)}}/g, (match, key) => {
|
|
111
|
+
return args[key] !== undefined ? String(args[key]) : match;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
handleMissingKey(key, lang) {
|
|
115
|
+
if (this.options.throwOnMissingKey) {
|
|
116
|
+
throw new Error(`Translation key "${key}" not found for language "${lang}"`);
|
|
117
|
+
}
|
|
118
|
+
if (this.options.logging) {
|
|
119
|
+
this.logger.warn(`Missing key: "${key}" in language: "${lang}"`);
|
|
120
|
+
}
|
|
121
|
+
return key;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
exports.I18nextService = I18nextService;
|
|
125
|
+
exports.I18nextService = I18nextService = I18nextService_1 = tslib_1.__decorate([
|
|
126
|
+
(0, common_1.Injectable)(),
|
|
127
|
+
tslib_1.__param(0, (0, common_1.Inject)(nestjs_i18next_module_definition_1.MODULE_OPTIONS_TOKEN)),
|
|
128
|
+
tslib_1.__metadata("design:paramtypes", [Object, nestjs_i18next_json_loader_1.I18nextJsonLoader])
|
|
129
|
+
], I18nextService);
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iamalond/nestjs-i18next",
|
|
3
|
+
"description": "🌍 i18next module for NestJS",
|
|
4
|
+
"version": "1.3.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "rimraf -rf dist && tsc -p tsconfig.build.json",
|
|
7
|
+
"prepublish:npm": "npm run build",
|
|
8
|
+
"publish:npm": "release-it",
|
|
9
|
+
"commit": "cz",
|
|
10
|
+
"prepublish:dev": "npm run build",
|
|
11
|
+
"publish:dev": "npm publish --access public --tag dev",
|
|
12
|
+
"prepare": "husky",
|
|
13
|
+
"format": "prettier --write \"{src,apps,libs,test}/**/*.ts\"",
|
|
14
|
+
"lint": "eslint --ignore-pattern .gitignore \"{src,apps,libs,test}/**/*.ts\"",
|
|
15
|
+
"lint:fix": "npm run lint -- --fix",
|
|
16
|
+
"test": "jest",
|
|
17
|
+
"test:watch": "jest --watch",
|
|
18
|
+
"test:cov": "jest --coverage",
|
|
19
|
+
"test:ci": "jest --ci --passWithNoTests --coverage"
|
|
20
|
+
},
|
|
21
|
+
"lint-staged": {
|
|
22
|
+
"*.ts": [
|
|
23
|
+
"npm run format",
|
|
24
|
+
"npm run lint:fix"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"directories": {
|
|
28
|
+
"lib": "src",
|
|
29
|
+
"test": "test"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"keywords": [
|
|
43
|
+
"nestjs",
|
|
44
|
+
"i18n",
|
|
45
|
+
"i18next",
|
|
46
|
+
"internationalization",
|
|
47
|
+
"internationalisation",
|
|
48
|
+
"internationalize",
|
|
49
|
+
"internationalise",
|
|
50
|
+
"international",
|
|
51
|
+
"internationalize",
|
|
52
|
+
"locale"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/iamAlond/nestjs-i18next.git"
|
|
61
|
+
},
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/iamAlond/nestjs-i18next"
|
|
64
|
+
},
|
|
65
|
+
"author": "Matvey iamAlond",
|
|
66
|
+
"contributors": [
|
|
67
|
+
"Matvey iamAlond"
|
|
68
|
+
],
|
|
69
|
+
"config": {
|
|
70
|
+
"commitizen": {
|
|
71
|
+
"path": "cz-conventional-changelog"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"tslib": "^2.8.1"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@commitlint/cli": "20.5.0",
|
|
79
|
+
"@commitlint/config-angular": "20.5.0",
|
|
80
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
81
|
+
"@eslint/js": "^10.0.1",
|
|
82
|
+
"@nestjs/common": "11.1.17",
|
|
83
|
+
"@nestjs/core": "11.1.17",
|
|
84
|
+
"@nestjs/testing": "^11.1.17",
|
|
85
|
+
"@release-it/conventional-changelog": "^10.0.6",
|
|
86
|
+
"@types/jest": "^30.0.0",
|
|
87
|
+
"@types/node": "25.5.0",
|
|
88
|
+
"commitizen": "^4.3.1",
|
|
89
|
+
"cz-conventional-changelog": "^3.3.0",
|
|
90
|
+
"eslint": "^10.1.0",
|
|
91
|
+
"eslint-config-prettier": "10.1.8",
|
|
92
|
+
"eslint-plugin-import": "^2.32.0",
|
|
93
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
94
|
+
"globals": "^17.4.0",
|
|
95
|
+
"husky": "9.1.7",
|
|
96
|
+
"jest": "^30.3.0",
|
|
97
|
+
"lint-staged": "16.4.0",
|
|
98
|
+
"prettier": "3.8.1",
|
|
99
|
+
"reflect-metadata": "0.2.2",
|
|
100
|
+
"release-it": "19.2.4",
|
|
101
|
+
"rimraf": "6.1.3",
|
|
102
|
+
"rxjs": "7.8.2",
|
|
103
|
+
"ts-jest": "^29.4.9",
|
|
104
|
+
"ts-node": "10.9.2",
|
|
105
|
+
"typescript": "6.0.2",
|
|
106
|
+
"typescript-eslint": "^8.58.0"
|
|
107
|
+
},
|
|
108
|
+
"peerDependencies": {
|
|
109
|
+
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
110
|
+
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
|
|
111
|
+
"reflect-metadata": "^0.2.1",
|
|
112
|
+
"rxjs": "^7.2.0"
|
|
113
|
+
}
|
|
114
|
+
}
|