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 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,3 @@
1
+ import type { I18nextToolkitConfig } from './options.js';
2
+ export declare function extractTranslationKeys(files: string[], locales: string[], i18nextToolkitConfig?: I18nextToolkitConfig): Promise<Record<string, string[]>>;
3
+ //# sourceMappingURL=i18nextCLIExtractor.d.ts.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
@@ -0,0 +1,3 @@
1
+ export type { PluginI18nextExtractorOptions } from './options.js';
2
+ export * from './pluginI18nextExtractor.js';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './pluginI18nextExtractor.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=options.js.map
@@ -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
@@ -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
+ }