@iamalond/nestjs-i18next 1.3.0 → 1.3.1

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
@@ -10,7 +10,7 @@
10
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
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
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>
13
+ <a href='https://img.shields.io/github/last-commit/iamAlond/nestjs-i18next'><img src="https://img.shields.io/github/last-commit/iamAlond/nestjs-i18next" alt="Last commit" /></a>
14
14
  </p>
15
15
 
16
16
  ## About the Project
@@ -30,16 +30,20 @@ The project was inspired by the popular `nestjs-i18n` module, but with a major e
30
30
 
31
31
  ## Installation
32
32
 
33
+ **Node.js 18.0.0 or newer is required.**
34
+
33
35
  ```bash
34
- npm install nestjs-i18next
35
- yarn add nestjs-i18next
36
- pnpm add nestjs-i18next
36
+ $ npm install @iamalond/nestjs-i18next
37
+ $ yarn add @iamalond/nestjs-i18next
38
+ $ pnpm add @iamalond/nestjs-i18next
37
39
  ```
38
40
 
39
41
  ## Usage
40
42
 
43
+ After installation, we can import the `I18nextModule` into the root `AppModule`:
44
+
41
45
  ```typescript
42
- import { I18NextModule } from '@iamalond/nestjs-i18next';
46
+ import { I18nextModule } from '@iamalond/nestjs-i18next';
43
47
 
44
48
  @Module({
45
49
  imports: [
@@ -59,14 +63,115 @@ import { I18NextModule } from '@iamalond/nestjs-i18next';
59
63
  export class AppModule {}
60
64
  ```
61
65
 
66
+ Then use `I18nextService` in your service's constructor:
67
+
68
+ ```typescript
69
+ import { Injectable } from '@nestjs/common';
70
+ import { I18nextService } from '@iamalond/nestjs-i18next';
71
+ /**
72
+ * Import the types from the path you specified in 'generatedTypesPath'
73
+ * (e.g., src/i18n/index.d.ts)
74
+ */
75
+ import { I18nTranslations } from './i18n';
76
+
77
+ @Injectable()
78
+ export class AppService {
79
+ // Pass I18nTranslations to the service for strict typing
80
+ constructor(private readonly i18n: I18nextService<I18nTranslations>) {}
81
+
82
+ getHello(): string {
83
+ // 1. Simple translation (autocomplete for keys, when start typing!)
84
+ const simple = this.i18n.t('common.hello');
85
+
86
+ // 2. Translation with arguments (Typescript will REQUIRE 'args' if variables exist)
87
+ const withArgs = this.i18n.t('common.welcome', {
88
+ args: { name: 'Matvey iamAlond' },
89
+ lang: 'en'
90
+ });
91
+
92
+ const apples = this.i18n.t('common.apples', {
93
+ args: { count: 5 }
94
+ });
95
+
96
+ // 3. You can also use the 'translate' alias
97
+ return this.i18n.translate('core.errors.notFound', {
98
+ defaultValue: 'Not Found',
99
+ debug: true
100
+ });
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### Dynamic Translation Keys
106
+
107
+ If you need to use a dynamic key (e.g., based on an HTTP status code), use the I18nPath type. This type is also automatically generated in your generatedTypesPath and contains all valid translation paths.
108
+
109
+ ```typescript
110
+ import { I18nPath, I18nTranslations } from './i18n';
111
+
112
+ @Injectable()
113
+ export class ErrorsService {
114
+ constructor(private readonly i18n: I18nextService<I18nTranslations>) {}
115
+
116
+ handleError(statusCode: number) {
117
+ /**
118
+ * Use 'as I18nPath' to tell TypeScript that this string
119
+ * is a valid translation key.
120
+ */
121
+ return this.i18n.t(`errors.status.${statusCode}` as I18nPath, {
122
+ defaultValue: 'An unexpected error occurred'
123
+ });
124
+ }
125
+ }
126
+ ```
127
+
62
128
  ## File Structure
63
129
 
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:
130
+ 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`. You can also combine both of these approaches.
65
131
 
66
132
  ```bash
67
133
  src/i18n/
68
134
  ├── en/
69
135
  │ ├── common.json
70
- │ └── auth/
71
- │ └── errors.json <-- key will be "auth.errors.invalid_password"
136
+ │ └── core/
137
+ │ └── errors.json <-- key will be "core.errors.invalid_password"
138
+ └── ru/
139
+ ├── common.json
140
+ └── ...
141
+ ```
142
+
143
+ ### Examples
144
+
145
+ To support the examples above, your translation directory should look like this:
146
+
147
+ `src/i18n/en/common.json`
148
+
149
+ ```json
150
+ {
151
+ "hello": "Hello!",
152
+ "welcome": "Welcome, {{name}}!",
153
+ "apples": "{count, plural, =0 {No apples} one {# apple} =3 {three apples} other {# apples}}"
154
+ }
72
155
  ```
156
+
157
+ `src/i18n/en/core/errors.json`
158
+
159
+ ```json
160
+ {
161
+ "invalid_password": "Invalid password",
162
+ "notFound": "Resource not found",
163
+ "status": {
164
+ "400": "Bad request",
165
+ "401": "Unauthorized",
166
+ "403": "Forbidden",
167
+ "404": "Not found",
168
+ "500": "Internal server error"
169
+ }
170
+ }
171
+ ```
172
+
173
+ ## Key Resolution Rules
174
+
175
+ - Simple file: common.json -> key starts with common (e.g., common.hello).
176
+ - Subfolders: core/errors.json -> key starts with core.errors (e.g., core.errors.status.404).
177
+ - Arguments: Any string containing {{var}} or ICU structures like {count, plural, ...} will automatically require args in your TypeScript code.
@@ -9,16 +9,16 @@ export type I18nextModuleOptions = {
9
9
  throwOnMissingKey?: boolean;
10
10
  generatedTypesPath?: string;
11
11
  };
12
- export type TranslateOptions<P, T> = {
12
+ type BaseOptions = {
13
13
  lang?: string;
14
14
  defaultValue?: string;
15
15
  debug?: boolean;
16
- } & (Extract<T, {
16
+ };
17
+ export type TranslateOptions<P, T> = Extract<T, {
17
18
  key: P;
18
19
  }> extends {
19
20
  args: infer A;
20
- } ? {
21
+ } ? BaseOptions & {
21
22
  args: A;
22
- } : {
23
- args?: never;
24
- });
23
+ } : BaseOptions | undefined;
24
+ export {};
@@ -104,11 +104,6 @@ export type I18nTranslations = ${this.generateUnionType(schema)};
104
104
 
105
105
  /* prettier-ignore */
106
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
107
  `;
113
108
  fs_1.default.mkdirSync(path_1.default.dirname(outputPath), {
114
109
  recursive: true
@@ -17,8 +17,16 @@ export declare class I18nextService<T extends {
17
17
  getSupportedLanguages(): string[];
18
18
  getTranslations(): Record<string, any>;
19
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;
20
+ translate<P extends T['key']>(key: P, ...params: Extract<T, {
21
+ key: P;
22
+ }> extends {
23
+ args: any;
24
+ } ? [options: TranslateOptions<P, T>] : [options?: TranslateOptions<P, T>]): string;
25
+ t<P extends T['key']>(key: P, ...params: Extract<T, {
26
+ key: P;
27
+ }> extends {
28
+ args: any;
29
+ } ? [options: TranslateOptions<P, T>] : [options?: TranslateOptions<P, T>]): string;
22
30
  private getValueByKey;
23
31
  private replaceArgs;
24
32
  private handleMissingKey;
@@ -46,11 +46,11 @@ let I18nextService = I18nextService_1 = class I18nextService {
46
46
  getTranslationsForNamespace(lang) {
47
47
  return this.currentTranslations[lang];
48
48
  }
49
- translate(key, options) {
50
- return this.t(key, options);
49
+ translate(key, ...params) {
50
+ return this.t(key, ...params);
51
51
  }
52
- t(key, options) {
53
- const { lang, debug, defaultValue } = options || {};
52
+ t(key, ...params) {
53
+ const { lang, debug, defaultValue, ...options } = params[0] || {};
54
54
  const fallback = this.options.fallbackLanguage;
55
55
  let result = this.getValueByKey(key, lang ?? fallback);
56
56
  if (result === undefined && lang && lang !== fallback) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iamalond/nestjs-i18next",
3
3
  "description": "🌍 i18next module for NestJS",
4
- "version": "1.3.0",
4
+ "version": "1.3.1",
5
5
  "scripts": {
6
6
  "build": "rimraf -rf dist && tsc -p tsconfig.build.json",
7
7
  "prepublish:npm": "npm run build",