rsbuild-plugin-i18next-extractor 0.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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/I18nextExtractorWebpackPlugin.d.ts +14 -0
- package/dist/I18nextExtractorWebpackPlugin.js +114 -0
- package/dist/i18nextCLIExtractor.d.ts +3 -0
- package/dist/i18nextCLIExtractor.js +37 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/loader.d.ts +23 -0
- package/dist/loader.js +35 -0
- package/dist/options.d.ts +51 -0
- package/dist/options.js +2 -0
- package/dist/pluginI18nextExtractor.d.ts +28 -0
- package/dist/pluginI18nextExtractor.js +71 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.js +49 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rspack Contrib
|
|
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,160 @@
|
|
|
1
|
+
# rsbuild-plugin-i18next-extractor
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://npmjs.com/package/rsbuild-plugin-i18next-extractor">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/rsbuild-plugin-i18next-extractor?style=flat-square&colorA=564341&colorB=EDED91" alt="npm version" />
|
|
6
|
+
</a>
|
|
7
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="license" />
|
|
8
|
+
<a href="https://npmcharts.com/compare/rsbuild-plugin-i18next-extractor?minimal=true"><img src="https://img.shields.io/npm/dm/rsbuild-plugin-i18next-extractor.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="downloads" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
A Rsbuild plugin for extracting i18n translations using [i18next-cli](https://github.com/i18next/i18next-cli).
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
`i18next-cli` can extract i18n translations from your source code through [`extract.input`](https://github.com/i18next/i18next-cli?tab=readme-ov-file#1-initialize-configuration). However some i18n translations will be bundled together with your code even if they are not used.
|
|
16
|
+
|
|
17
|
+
This plugin uses the Rspack module graph to override the `extract.input` configuration with imported modules, generating i18n translations based on usage.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm add rsbuild-plugin-i18next-extractor --save-dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Install
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// rsbuild.config.ts
|
|
31
|
+
import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
|
|
32
|
+
import { defineConfig } from '@rsbuild/core';
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
plugins: [
|
|
36
|
+
pluginI18nextExtractor({
|
|
37
|
+
localesDir: './locales',
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Directory Structure
|
|
44
|
+
|
|
45
|
+
Your project should have a locales directory with JSON files:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
locales/
|
|
49
|
+
en.json
|
|
50
|
+
zh.json
|
|
51
|
+
ja.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**NOTE:** `rsbuild-plugin-i18next-extractor` only supports `*.json` files.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Using i18n in your code
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// ./src/i18n.js
|
|
61
|
+
import i18next from 'i18next';
|
|
62
|
+
import en from '../locales/en.json';
|
|
63
|
+
import zh from '../locales/zh.json';
|
|
64
|
+
|
|
65
|
+
const i18n = i18next.createInstance()
|
|
66
|
+
|
|
67
|
+
i18n.init({
|
|
68
|
+
lng: 'en',
|
|
69
|
+
resources: {
|
|
70
|
+
en: {
|
|
71
|
+
translation: en,
|
|
72
|
+
},
|
|
73
|
+
zh: {
|
|
74
|
+
translation: zh,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export { i18n }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
use `i18n.t('key')` to translate
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
// src/index.js
|
|
86
|
+
import { i18n } from './i18n';
|
|
87
|
+
|
|
88
|
+
console.log(i18n.t('hello'));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
### `localesDir`
|
|
94
|
+
|
|
95
|
+
- **Type:** `string`
|
|
96
|
+
- **Required:** Yes
|
|
97
|
+
|
|
98
|
+
The directory containing your locale JSON files.
|
|
99
|
+
|
|
100
|
+
Supports both relative and absolute paths:
|
|
101
|
+
- Relative path: Resolved relative to the project root directory (e.g., `'./locales'`, `'src/locales'`)
|
|
102
|
+
- Absolute path: Used as-is (e.g., `'/absolute/path/to/locales'`)
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
pluginI18nextExtractor({
|
|
106
|
+
localesDir: './locales',
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `i18nextToolkitConfig`
|
|
111
|
+
|
|
112
|
+
- **Type:** `I18nextToolkitConfig`
|
|
113
|
+
- **Required:** No
|
|
114
|
+
|
|
115
|
+
The configuration for i18next-cli toolkit. This allows you to customize how translation keys are extracted from your code.
|
|
116
|
+
|
|
117
|
+
See [i18next-cli configuration](https://github.com/i18next/i18next-cli) for available options.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
pluginI18nextExtractor({
|
|
121
|
+
localesDir: './locales',
|
|
122
|
+
i18nextToolkitConfig: {
|
|
123
|
+
extract: {
|
|
124
|
+
// Custom extraction configuration
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `onKeyNotFound`
|
|
131
|
+
|
|
132
|
+
- **Type:** `(key: string, locale: string, localeFilePath: string, entryName: string) => void`
|
|
133
|
+
- **Required:** No
|
|
134
|
+
|
|
135
|
+
Custom callback function invoked when a translation key is not found in the locale file.
|
|
136
|
+
|
|
137
|
+
By default, a warning is logged to the console with the missing key and file information.
|
|
138
|
+
|
|
139
|
+
**Parameters:**
|
|
140
|
+
- `key` - The translation key that was not found
|
|
141
|
+
- `locale` - The locale identifier (e.g., `'en'`, `'zh-CN'`)
|
|
142
|
+
- `localeFilePath` - The path to the locale file
|
|
143
|
+
- `entryName` - The name of the current entry being processed
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
pluginI18nextExtractor({
|
|
147
|
+
localesDir: './locales',
|
|
148
|
+
onKeyNotFound: (key, locale, localeFilePath, entryName) => {
|
|
149
|
+
console.error(`Missing key: ${key} in ${locale}`);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Credits
|
|
155
|
+
|
|
156
|
+
[rsbuild-plugin-tailwindcss](https://github.com/rspack-contrib/rsbuild-plugin-tailwindcss) - Inspiration for this plugin
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rspack } from '@rsbuild/core';
|
|
2
|
+
import type { PluginI18nextExtractorOptions } from './options.js';
|
|
3
|
+
interface I18nextExtractorWebpackPluginOptions extends PluginI18nextExtractorOptions {
|
|
4
|
+
logger: {
|
|
5
|
+
warn: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export declare class I18nextExtractorWebpackPlugin {
|
|
9
|
+
private options;
|
|
10
|
+
constructor(options: I18nextExtractorWebpackPluginOptions);
|
|
11
|
+
apply(compiler: Rspack.Compiler): void;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=I18nextExtractorWebpackPlugin.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getLocalesFromDirectory, getLocaleVariableName, resolveLocaleFilePath, } from './utils.js';
|
|
4
|
+
export class I18nextExtractorWebpackPlugin {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
apply(compiler) {
|
|
10
|
+
const { Compilation, sources } = compiler.webpack;
|
|
11
|
+
compiler.hooks.compilation.tap(this.constructor.name, (compilation) => {
|
|
12
|
+
const locales = getLocalesFromDirectory(compiler.context, this.options.localesDir);
|
|
13
|
+
if (locales.length === 0) {
|
|
14
|
+
throw new Error(`[rsbuild-plugin-i18next-extractor] There is no "*.json" in ${this.options.localesDir}. Please check your "localesDir" option.`);
|
|
15
|
+
}
|
|
16
|
+
compilation.hooks.processAssets.tapPromise({
|
|
17
|
+
name: this.constructor.name,
|
|
18
|
+
stage: Compilation.PROCESS_ASSETS_STAGE_DERIVED,
|
|
19
|
+
}, async () => {
|
|
20
|
+
// Process each entrypoint
|
|
21
|
+
await Promise.all([...compilation.entrypoints.entries()].map(async ([entryName, entrypoint]) => {
|
|
22
|
+
// get entry chunk but not split chunk
|
|
23
|
+
const jsFiles = entrypoint.chunks
|
|
24
|
+
.filter((chunk) => chunk.hasRuntime())
|
|
25
|
+
.flatMap((chunk) => Array.from(chunk.files))
|
|
26
|
+
.filter((file) => /\.(c|m)?js$/.test(file))
|
|
27
|
+
.map((file) => compilation.getAsset(file))
|
|
28
|
+
.filter((file) => !!file);
|
|
29
|
+
const asyncJsFiles = compilation.chunkGroups
|
|
30
|
+
.filter((cg) => !cg.isInitial())
|
|
31
|
+
.filter((cg) => cg.getParents().some((p) => p.name === entryName))
|
|
32
|
+
.flatMap((cg) => cg.getFiles())
|
|
33
|
+
.filter((file) => /\.(c|m)?js$/.test(file))
|
|
34
|
+
.map((file) => compilation.getAsset(file))
|
|
35
|
+
.filter((file) => !!file);
|
|
36
|
+
// Collect all the modules belong to current entry
|
|
37
|
+
const entryModules = new Set();
|
|
38
|
+
for (const chunk of entrypoint.chunks) {
|
|
39
|
+
const modules = compilation.chunkGraph.getChunkModulesIterable(chunk);
|
|
40
|
+
for (const m of modules) {
|
|
41
|
+
collectModules(m, entryModules);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Only extract keys in js(x)/ts(x) files
|
|
45
|
+
const files = Array.from(entryModules).filter((f) => /\.[jt]sx?$/.test(f));
|
|
46
|
+
// Load origin translations
|
|
47
|
+
const originTranslations = {};
|
|
48
|
+
for (const locale of locales) {
|
|
49
|
+
const localePath = resolveLocaleFilePath(this.options.localesDir, locale, compiler.context);
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(localePath, 'utf-8');
|
|
52
|
+
originTranslations[locale] = JSON.parse(content);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`[rsbuild-plugin-i18next-extractor] Failed to read locale file "${localePath}"`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const { extractTranslationKeys } = await import('./i18nextCLIExtractor.js');
|
|
59
|
+
const extractedTranslationKeys = await extractTranslationKeys(files, locales, this.options.i18nextToolkitConfig);
|
|
60
|
+
// Generate i18n resource definitions for each locale
|
|
61
|
+
const i18nTranslationDefinitions = [];
|
|
62
|
+
for (const locale of locales) {
|
|
63
|
+
const localeFilePath = resolveLocaleFilePath(this.options.localesDir, locale, compiler.context);
|
|
64
|
+
const extractedTranslations = pickTranslationsByKeys(originTranslations[locale], extractedTranslationKeys[locale], (key) => {
|
|
65
|
+
// Use custom callback if provided, otherwise use default warning
|
|
66
|
+
if (this.options.onKeyNotFound) {
|
|
67
|
+
this.options.onKeyNotFound(key, locale, localeFilePath, entryName);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.options.logger.warn(`[rsbuild-plugin-i18next-extractor] The key "${key}" is not found in "${path.relative(compiler.context, localeFilePath)}". Current entry is "${entryName}".`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
i18nTranslationDefinitions.push(`const ${getLocaleVariableName(locale)} = ${JSON.stringify(extractedTranslations)};`);
|
|
74
|
+
}
|
|
75
|
+
// Replace the placeholder with actual extracted translations
|
|
76
|
+
for (const jsFile of [...jsFiles, ...asyncJsFiles]) {
|
|
77
|
+
const assetName = jsFile.name;
|
|
78
|
+
compilation.updateAsset(assetName, (oldSource) => new sources.ConcatSource(i18nTranslationDefinitions.join('\n'), '\n', oldSource));
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function collectModules(m, entryModules) {
|
|
86
|
+
if (m.modules) {
|
|
87
|
+
for (const innerModule of m.modules) {
|
|
88
|
+
collectModules(innerModule, entryModules);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (m.resource) {
|
|
92
|
+
const resource = m.resource.split('?')[0];
|
|
93
|
+
if (resource) {
|
|
94
|
+
entryModules.add(resource);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Combine the origin translations and the extracted translation keys.
|
|
100
|
+
*/
|
|
101
|
+
function pickTranslationsByKeys(originTranslations, extractedKeys, onKeyNotFoundCallback) {
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const key of extractedKeys) {
|
|
104
|
+
if (originTranslations[key]) {
|
|
105
|
+
result[key] = originTranslations[key];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
onKeyNotFoundCallback(key);
|
|
109
|
+
result[key] = '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=I18nextExtractorWebpackPlugin.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* An extractor based i18next-cli is used with `rsbuild-plugin-i18next-extractor`.
|
|
5
|
+
*/
|
|
6
|
+
import { extract } from 'i18next-cli';
|
|
7
|
+
export async function extractTranslationKeys(files, locales, i18nextToolkitConfig) {
|
|
8
|
+
const extractResult = await extract({
|
|
9
|
+
...i18nextToolkitConfig,
|
|
10
|
+
locales,
|
|
11
|
+
extract: {
|
|
12
|
+
input: files,
|
|
13
|
+
output: 'node_modules/.rsbuild-plugin-i18next-extractor/locales/{{language}}/{{namespace}}.json',
|
|
14
|
+
...i18nextToolkitConfig?.extract,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
// Group keys by locale
|
|
18
|
+
const translationsKeysByLocale = {};
|
|
19
|
+
for (const result of extractResult) {
|
|
20
|
+
// Extract locale from path (support both Unix and Windows paths)
|
|
21
|
+
// Example paths:
|
|
22
|
+
// Unix: '/path/to/node_modules/.rsbuild-plugin-i18next-extractor/locales/zh-CN/translation.json'
|
|
23
|
+
// Windows: 'C:\\path\\to\\node_modules\\.rsbuild-plugin-i18next-extractor\\locales\\zh-CN\\translation.json'
|
|
24
|
+
const pathMatch = result.path?.match(/[/\\]locales[/\\]([^/\\]+)[/\\]/);
|
|
25
|
+
const locale = pathMatch?.[1];
|
|
26
|
+
if (locale && result.newTranslations) {
|
|
27
|
+
const keys = Object.keys(result.newTranslations);
|
|
28
|
+
if (!translationsKeysByLocale[locale]) {
|
|
29
|
+
translationsKeysByLocale[locale] = [];
|
|
30
|
+
}
|
|
31
|
+
// Add keys for this locale (avoiding duplicates)
|
|
32
|
+
translationsKeysByLocale[locale] = Array.from(new Set([...translationsKeysByLocale[locale], ...keys]));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return translationsKeysByLocale;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=i18nextCLIExtractor.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Rspack } from '@rsbuild/core';
|
|
2
|
+
interface LoaderOptions {
|
|
3
|
+
localesDir: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* This loader will replace the export of i18n translations JSON file
|
|
7
|
+
* with a variable placeholder.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* // Before:
|
|
12
|
+
* module.exports = { "hello": "World" }
|
|
13
|
+
*
|
|
14
|
+
* // After:
|
|
15
|
+
* module.exports = __I18N_<LOCALE>_EXTRACTED_TRANSLATIONS__
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The `__I18N_<LOCALE>_EXTRACTED_TRANSLATIONS__` will be replaced in Webpack/Rspack ProcessAssets hook
|
|
19
|
+
* with the actual extracted translations for that locale.
|
|
20
|
+
*/
|
|
21
|
+
export default function loader(this: Rspack.LoaderContext<LoaderOptions>, source: string): void;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=loader.d.ts.map
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { getLocaleVariableName } from './utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* This loader will replace the export of i18n translations JSON file
|
|
5
|
+
* with a variable placeholder.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```js
|
|
9
|
+
* // Before:
|
|
10
|
+
* module.exports = { "hello": "World" }
|
|
11
|
+
*
|
|
12
|
+
* // After:
|
|
13
|
+
* module.exports = __I18N_<LOCALE>_EXTRACTED_TRANSLATIONS__
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The `__I18N_<LOCALE>_EXTRACTED_TRANSLATIONS__` will be replaced in Webpack/Rspack ProcessAssets hook
|
|
17
|
+
* with the actual extracted translations for that locale.
|
|
18
|
+
*/
|
|
19
|
+
export default function loader(source) {
|
|
20
|
+
const { localesDir } = this.getOptions();
|
|
21
|
+
const absoluteLocalesDir = path.isAbsolute(localesDir)
|
|
22
|
+
? localesDir.replaceAll(path.sep, '/')
|
|
23
|
+
: path.posix.join(this.rootContext, localesDir);
|
|
24
|
+
const match = new RegExp(`${absoluteLocalesDir}/(.*)\\.json`).exec(this.resourcePath.replaceAll(path.sep, '/'));
|
|
25
|
+
if (!match || match.length < 2) {
|
|
26
|
+
this.callback(null, `module.exports = ${source}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const locale = match[1];
|
|
30
|
+
const replacement = getLocaleVariableName(locale);
|
|
31
|
+
// For JSON files, we need to export the placeholder
|
|
32
|
+
// Since JSON is treated as a module, we use module.exports
|
|
33
|
+
this.callback(null, `module.exports = ${replacement}`);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { I18nextToolkitConfig as RawI18nextToolkitConfig } from 'i18next-cli';
|
|
2
|
+
type ExtractConfigWithOptionalFields = Omit<RawI18nextToolkitConfig['extract'], 'input' | 'output'> & {
|
|
3
|
+
input?: RawI18nextToolkitConfig['extract']['input'];
|
|
4
|
+
output?: RawI18nextToolkitConfig['extract']['output'];
|
|
5
|
+
};
|
|
6
|
+
export type I18nextToolkitConfig = Omit<RawI18nextToolkitConfig, 'extract' | 'locales'> & {
|
|
7
|
+
extract?: ExtractConfigWithOptionalFields;
|
|
8
|
+
locales?: string[];
|
|
9
|
+
};
|
|
10
|
+
export interface PluginI18nextExtractorOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The directory which contains the raw locale translations.
|
|
13
|
+
*
|
|
14
|
+
* Supports both relative and absolute paths:
|
|
15
|
+
* - Relative path: Resolved relative to the project root directory (e.g., './locales', 'src/locales')
|
|
16
|
+
* - Absolute path: Used as-is (e.g., '/absolute/path/to/locales')
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Relative path
|
|
20
|
+
* { localesDir: './locales' }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Absolute path
|
|
24
|
+
* { localesDir: '/Users/username/project/locales' }
|
|
25
|
+
*/
|
|
26
|
+
localesDir: string;
|
|
27
|
+
/**
|
|
28
|
+
* The configuration of i18next-cli toolkit.
|
|
29
|
+
*/
|
|
30
|
+
i18nextToolkitConfig?: I18nextToolkitConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Custom callback function invoked when a translation key is not found in the locale file.
|
|
33
|
+
*
|
|
34
|
+
* By default, a warning is logged to the console with the missing key and file information.
|
|
35
|
+
*
|
|
36
|
+
* @param key - The translation key that was not found
|
|
37
|
+
* @param locale - The locale identifier (e.g., 'en', 'zh-CN')
|
|
38
|
+
* @param localeFilePath - The path to the locale file
|
|
39
|
+
* @param entryName - The name of the current entry being processed
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* {
|
|
43
|
+
* onKeyNotFound: (key, locale, localeFilePath, entryName) => {
|
|
44
|
+
* console.error(`Missing key: ${key} in ${locale}`);
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
onKeyNotFound?: (key: string, locale: string, localeFilePath: string, entryName: string) => void;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=options.d.ts.map
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RsbuildPlugin } from '@rsbuild/core';
|
|
2
|
+
import type { PluginI18nextExtractorOptions } from './options.js';
|
|
3
|
+
/**
|
|
4
|
+
* A Rsbuild plugin for extracting i18n translations using i18next-cli.
|
|
5
|
+
*
|
|
6
|
+
* This plugin automatically scans your source code for i18n translation keys
|
|
7
|
+
* and extracts only used keys from your locale files. This helps reduce
|
|
8
|
+
* bundle size by only including translations that are actually used in your code.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // rsbuild.config.ts
|
|
13
|
+
* import { defineConfig } from '@rsbuild/core'
|
|
14
|
+
* import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* plugins: [
|
|
18
|
+
* pluginI18nextExtractor({
|
|
19
|
+
* localesDir: './locales'
|
|
20
|
+
* })
|
|
21
|
+
* ],
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export declare function pluginI18nextExtractor(pluginOptions: PluginI18nextExtractorOptions): RsbuildPlugin;
|
|
28
|
+
//# sourceMappingURL=pluginI18nextExtractor.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { logger } from '@rsbuild/core';
|
|
3
|
+
import { I18nextExtractorWebpackPlugin } from './I18nextExtractorWebpackPlugin.js';
|
|
4
|
+
import { resolveLocalesDir } from './utils.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
/**
|
|
7
|
+
* A Rsbuild plugin for extracting i18n translations using i18next-cli.
|
|
8
|
+
*
|
|
9
|
+
* This plugin automatically scans your source code for i18n translation keys
|
|
10
|
+
* and extracts only used keys from your locale files. This helps reduce
|
|
11
|
+
* bundle size by only including translations that are actually used in your code.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // rsbuild.config.ts
|
|
16
|
+
* import { defineConfig } from '@rsbuild/core'
|
|
17
|
+
* import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({
|
|
20
|
+
* plugins: [
|
|
21
|
+
* pluginI18nextExtractor({
|
|
22
|
+
* localesDir: './locales'
|
|
23
|
+
* })
|
|
24
|
+
* ],
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export function pluginI18nextExtractor(pluginOptions) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'rsbuild:i18next-extractor',
|
|
33
|
+
setup(api) {
|
|
34
|
+
validateOptions(pluginOptions);
|
|
35
|
+
const { localesDir, i18nextToolkitConfig, onKeyNotFound } = pluginOptions;
|
|
36
|
+
api.modifyBundlerChain((chain) => {
|
|
37
|
+
// Add a rule to replace locale JSON imports with a placeholder
|
|
38
|
+
chain.module
|
|
39
|
+
.rule('i18next-extractor:i18n-translations-replacement')
|
|
40
|
+
.test(/\.json$/)
|
|
41
|
+
.type('javascript/auto')
|
|
42
|
+
.include.add(resolveLocalesDir(api.context.rootPath, localesDir))
|
|
43
|
+
.end()
|
|
44
|
+
.use('i18n-translations-replacement')
|
|
45
|
+
.loader(require.resolve('./loader.js'))
|
|
46
|
+
.options({ localesDir })
|
|
47
|
+
.end()
|
|
48
|
+
.end()
|
|
49
|
+
.end()
|
|
50
|
+
.plugin(I18nextExtractorWebpackPlugin.name)
|
|
51
|
+
.use(I18nextExtractorWebpackPlugin, [
|
|
52
|
+
{
|
|
53
|
+
localesDir,
|
|
54
|
+
i18nextToolkitConfig,
|
|
55
|
+
onKeyNotFound,
|
|
56
|
+
logger,
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
.end();
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function validateOptions(pluginOptions) {
|
|
65
|
+
if (!pluginOptions ||
|
|
66
|
+
!pluginOptions.localesDir ||
|
|
67
|
+
pluginOptions.localesDir.trim() === '') {
|
|
68
|
+
throw new Error('[rsbuild-plugin-i18next-extractor] The "localesDir" option is required.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=pluginI18nextExtractor.js.map
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a variable name for storing extracted translations of a given locale.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* getLocaleVariableName('zh-CN') // Returns: '__I18N_ZH_CN_EXTRACTED_TRANSLATIONS__'
|
|
6
|
+
*/
|
|
7
|
+
export declare function getLocaleVariableName(locale: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Get list of locales from the locales directory.
|
|
10
|
+
*
|
|
11
|
+
* Locales are determined by JSON files in the directory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLocalesFromDirectory(rootDir: string, localesDir: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the absolute path to the locales directory.
|
|
16
|
+
*
|
|
17
|
+
* If the locales directory is already absolute, returns it as-is.
|
|
18
|
+
* Otherwise, resolves it relative to the root directory.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveLocalesDir(rootDir: string, localesDir: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the absolute path to a specific locale's JSON file.
|
|
23
|
+
*
|
|
24
|
+
* @param localesDir - The locales directory (absolute or relative)
|
|
25
|
+
* @param locale - The locale identifier (e.g., 'zh-CN', 'en')
|
|
26
|
+
* @param rootDir - The root directory for resolving relative paths
|
|
27
|
+
* @returns The absolute path to the locale JSON file
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveLocaleFilePath(localesDir: string, locale: string, rootDir: string): string;
|
|
30
|
+
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Generate a variable name for storing extracted translations of a given locale.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* getLocaleVariableName('zh-CN') // Returns: '__I18N_ZH_CN_EXTRACTED_TRANSLATIONS__'
|
|
8
|
+
*/
|
|
9
|
+
export function getLocaleVariableName(locale) {
|
|
10
|
+
return `__I18N_${locale.toUpperCase().replaceAll('-', '_')}_EXTRACTED_TRANSLATIONS__`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get list of locales from the locales directory.
|
|
14
|
+
*
|
|
15
|
+
* Locales are determined by JSON files in the directory.
|
|
16
|
+
*/
|
|
17
|
+
export function getLocalesFromDirectory(rootDir, localesDir) {
|
|
18
|
+
const files = readdirSync(resolveLocalesDir(rootDir, localesDir), {
|
|
19
|
+
withFileTypes: true,
|
|
20
|
+
});
|
|
21
|
+
return files
|
|
22
|
+
.filter((file) => !file.isDirectory() && file.name.endsWith('.json'))
|
|
23
|
+
.map(({ name: filename }) => filename.replace('.json', ''));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the absolute path to the locales directory.
|
|
27
|
+
*
|
|
28
|
+
* If the locales directory is already absolute, returns it as-is.
|
|
29
|
+
* Otherwise, resolves it relative to the root directory.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveLocalesDir(rootDir, localesDir) {
|
|
32
|
+
return path.isAbsolute(localesDir)
|
|
33
|
+
? localesDir
|
|
34
|
+
: path.resolve(rootDir, localesDir);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the absolute path to a specific locale's JSON file.
|
|
38
|
+
*
|
|
39
|
+
* @param localesDir - The locales directory (absolute or relative)
|
|
40
|
+
* @param locale - The locale identifier (e.g., 'zh-CN', 'en')
|
|
41
|
+
* @param rootDir - The root directory for resolving relative paths
|
|
42
|
+
* @returns The absolute path to the locale JSON file
|
|
43
|
+
*/
|
|
44
|
+
export function resolveLocaleFilePath(localesDir, locale, rootDir) {
|
|
45
|
+
return path.join(path.isAbsolute(localesDir)
|
|
46
|
+
? localesDir
|
|
47
|
+
: path.resolve(rootDir, localesDir), `${locale}.json`);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rsbuild-plugin-i18next-extractor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Rsbuild plugin for extracting i18n translations using i18next-cli",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rsbuild",
|
|
7
|
+
"rspack",
|
|
8
|
+
"i18n",
|
|
9
|
+
"i18next",
|
|
10
|
+
"i18next-cli",
|
|
11
|
+
"extractor"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Hengchang Lu",
|
|
16
|
+
"email": "luhengchan228@gmail.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/rspack-contrib/rsbuild-plugin-i18next-extractor.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"!dist/**/*.map",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"type-check": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"lint": "biome check .",
|
|
43
|
+
"lint:write": "biome check . --write"
|
|
44
|
+
},
|
|
45
|
+
"simple-git-hooks": {
|
|
46
|
+
"pre-commit": "npm run lint:write"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"i18next-cli": "1.33.5",
|
|
50
|
+
"picocolors": "^1.1.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "2.3.9",
|
|
54
|
+
"@playwright/test": "^1.50.0",
|
|
55
|
+
"@rsbuild/core": "^1.6.15",
|
|
56
|
+
"@types/node": "^22.10.2",
|
|
57
|
+
"i18next": "25.7.3",
|
|
58
|
+
"simple-git-hooks": "^2.13.1",
|
|
59
|
+
"typescript": "^5.7.2",
|
|
60
|
+
"vitest": "^4.0.16"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@rsbuild/core": "^1.0.0"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
},
|
|
68
|
+
"packageManager": "pnpm@10.15.0"
|
|
69
|
+
}
|