@modern-js/plugin-i18n 3.3.0 → 3.5.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/dist/cjs/cli/index.js +7 -59
- package/dist/cjs/cli/locales.js +132 -0
- package/dist/cjs/runtime/i18n/backend/defaults.js +6 -2
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +47 -7
- package/dist/esm/cli/index.mjs +7 -48
- package/dist/esm/cli/locales.mjs +80 -0
- package/dist/esm/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +33 -2
- package/dist/esm/runtime/utils.mjs +2 -2
- package/dist/esm-node/cli/index.mjs +7 -48
- package/dist/esm-node/cli/locales.mjs +81 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +33 -2
- package/dist/types/cli/locales.d.ts +17 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +10 -7
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +10 -2
- package/dist/types/shared/type.d.ts +20 -0
- package/package.json +11 -11
- package/src/cli/index.ts +24 -100
- package/src/cli/locales.ts +186 -0
- package/src/runtime/i18n/backend/defaults.node.ts +74 -5
- package/src/runtime/i18n/backend/defaults.ts +20 -16
- package/src/shared/type.ts +20 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getPublicDirRoutePrefixes, normalizePublicDir, normalizePublicDirPath } from "@modern-js/server-core";
|
|
5
|
+
const LOCALES_RESOURCE_PATTERN = '{{lng}}/{{ns}}.json';
|
|
6
|
+
function hasJsonFiles(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) return false;
|
|
9
|
+
const entries = fs.readdirSync(dirPath);
|
|
10
|
+
for (const entry of entries){
|
|
11
|
+
const entryPath = path.join(dirPath, entry);
|
|
12
|
+
const stat = fs.statSync(entryPath);
|
|
13
|
+
if (stat.isFile() && entry.endsWith('.json')) return true;
|
|
14
|
+
if (stat.isDirectory() && hasJsonFiles(entryPath)) return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function toPosixPath(filePath) {
|
|
22
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
23
|
+
}
|
|
24
|
+
function getPublicDirOutputPath(publicDir) {
|
|
25
|
+
return normalizePublicDirPath(publicDir);
|
|
26
|
+
}
|
|
27
|
+
function buildDetectedLocalesDirectory(clientBasePath, serverBasePath, serverBasePathCandidates = [
|
|
28
|
+
serverBasePath
|
|
29
|
+
]) {
|
|
30
|
+
const normalizedClientBasePath = clientBasePath.startsWith('/') ? clientBasePath : `/${clientBasePath}`;
|
|
31
|
+
const normalizedServerBasePath = toPosixPath(serverBasePath).replace(/\/$/, '');
|
|
32
|
+
const normalizedServerBasePathCandidates = Array.from(new Set(serverBasePathCandidates.map((candidate)=>toPosixPath(candidate).replace(/\/$/, ''))));
|
|
33
|
+
const serverLoadPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
|
|
34
|
+
const serverAddPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
|
|
35
|
+
return {
|
|
36
|
+
loadPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
37
|
+
addPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
38
|
+
serverLoadPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
39
|
+
serverAddPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
40
|
+
serverLoadPaths,
|
|
41
|
+
serverAddPaths
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function detectLocalesDirectory(appDirectory, normalizedConfig) {
|
|
45
|
+
const publicDirs = normalizePublicDir(normalizedConfig?.server?.publicDir);
|
|
46
|
+
const publicDirPrefixes = getPublicDirRoutePrefixes(normalizedConfig?.server?.publicDir);
|
|
47
|
+
for(let index = 0; index < publicDirs.length; index++){
|
|
48
|
+
const publicDir = publicDirs[index];
|
|
49
|
+
if (path.isAbsolute(publicDir)) continue;
|
|
50
|
+
const publicDirPath = path.join(appDirectory, publicDir);
|
|
51
|
+
const publicDirPrefix = publicDirPrefixes[index] || `/${normalizePublicDirPath(publicDir)}`;
|
|
52
|
+
const publicDirOutputPath = getPublicDirOutputPath(publicDir);
|
|
53
|
+
if ('locales' === path.basename(normalizePublicDirPath(publicDir)) && hasJsonFiles(publicDirPath)) return buildDetectedLocalesDirectory(publicDirPrefix, `./${publicDirOutputPath}`);
|
|
54
|
+
const localesPath = path.join(publicDirPath, 'locales');
|
|
55
|
+
if (hasJsonFiles(localesPath)) return buildDetectedLocalesDirectory(`${publicDirPrefix}/locales`, `./${publicDirOutputPath}/locales`);
|
|
56
|
+
}
|
|
57
|
+
const configPublicPath = path.join(appDirectory, 'config', 'public', 'locales');
|
|
58
|
+
if (hasJsonFiles(configPublicPath)) return buildDetectedLocalesDirectory('/locales', './public/locales', [
|
|
59
|
+
'./config/public/locales',
|
|
60
|
+
'./public/locales'
|
|
61
|
+
]);
|
|
62
|
+
const rootLocalesPath = path.join(appDirectory, 'locales');
|
|
63
|
+
if (hasJsonFiles(rootLocalesPath)) return buildDetectedLocalesDirectory('/locales', './locales');
|
|
64
|
+
}
|
|
65
|
+
function applyDetectedBackendPaths(backendOptions, detectedLocales) {
|
|
66
|
+
if (!detectedLocales) return backendOptions;
|
|
67
|
+
const backendWithDetectedPaths = {
|
|
68
|
+
...backendOptions,
|
|
69
|
+
enabled: true,
|
|
70
|
+
loadPath: backendOptions.loadPath ?? detectedLocales.loadPath,
|
|
71
|
+
addPath: backendOptions.addPath ?? detectedLocales.addPath,
|
|
72
|
+
serverLoadPath: backendOptions.serverLoadPath ?? detectedLocales.serverLoadPath,
|
|
73
|
+
serverLoadPaths: backendOptions.serverLoadPath || backendOptions.serverLoadPaths ? backendOptions.serverLoadPaths : detectedLocales.serverLoadPaths,
|
|
74
|
+
serverAddPath: backendOptions.serverAddPath ?? detectedLocales.serverAddPath,
|
|
75
|
+
serverAddPaths: backendOptions.serverAddPath || backendOptions.serverAddPaths ? backendOptions.serverAddPaths : detectedLocales.serverAddPaths,
|
|
76
|
+
_detectedLoadPath: detectedLocales.loadPath,
|
|
77
|
+
_detectedAddPath: detectedLocales.addPath
|
|
78
|
+
};
|
|
79
|
+
return backendWithDetectedPaths;
|
|
80
|
+
}
|
|
81
|
+
export { applyDetectedBackendPaths, detectLocalesDirectory };
|
|
@@ -4,8 +4,6 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
|
4
4
|
addPath: '/locales/{{lng}}/{{ns}}.json'
|
|
5
5
|
};
|
|
6
6
|
function convertPath(path) {
|
|
7
|
-
if (!path) return path;
|
|
8
|
-
if (path.startsWith('/')) return `${window.__assetPrefix__ || ''}${path}`;
|
|
9
7
|
return path;
|
|
10
8
|
}
|
|
11
9
|
function convertBackendOptions(options) {
|
|
@@ -13,6 +11,12 @@ function convertBackendOptions(options) {
|
|
|
13
11
|
const converted = {
|
|
14
12
|
...options
|
|
15
13
|
};
|
|
14
|
+
delete converted.serverLoadPath;
|
|
15
|
+
delete converted.serverAddPath;
|
|
16
|
+
delete converted.serverLoadPaths;
|
|
17
|
+
delete converted.serverAddPaths;
|
|
18
|
+
delete converted._detectedLoadPath;
|
|
19
|
+
delete converted._detectedAddPath;
|
|
16
20
|
if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
|
|
17
21
|
if (converted.addPath) converted.addPath = convertPath(converted.addPath);
|
|
18
22
|
return converted;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
3
4
|
loadPath: './locales/{{lng}}/{{ns}}.json',
|
|
4
5
|
addPath: './locales/{{lng}}/{{ns}}.json'
|
|
@@ -8,13 +9,43 @@ function convertPath(path) {
|
|
|
8
9
|
if (path.startsWith('/')) return `.${path}`;
|
|
9
10
|
return path;
|
|
10
11
|
}
|
|
12
|
+
function shouldUseServerPath(currentPath, detectedPath) {
|
|
13
|
+
return !detectedPath || currentPath === detectedPath;
|
|
14
|
+
}
|
|
15
|
+
function getResourceBasePath(resourcePath) {
|
|
16
|
+
const markerIndex = resourcePath.indexOf('{{lng}}');
|
|
17
|
+
if (markerIndex < 0) return resourcePath;
|
|
18
|
+
return resourcePath.slice(0, markerIndex).replace(/[\\/]+$/, '');
|
|
19
|
+
}
|
|
20
|
+
function pathExists(resourcePath) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.existsSync(getResourceBasePath(resourcePath));
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getServerPath(pathCandidates, fallbackPath) {
|
|
28
|
+
const candidates = Array.from(new Set([
|
|
29
|
+
...pathCandidates || [],
|
|
30
|
+
fallbackPath
|
|
31
|
+
].filter(Boolean)));
|
|
32
|
+
return candidates.find(pathExists) || candidates[0];
|
|
33
|
+
}
|
|
11
34
|
function convertBackendOptions(options) {
|
|
12
35
|
if (!options) return options;
|
|
13
36
|
const converted = {
|
|
14
37
|
...options
|
|
15
38
|
};
|
|
16
|
-
if (converted.loadPath) converted.loadPath =
|
|
17
|
-
if (converted.
|
|
39
|
+
if ((converted.serverLoadPath || converted.serverLoadPaths) && shouldUseServerPath(converted.loadPath, converted._detectedLoadPath)) converted.loadPath = getServerPath(converted.serverLoadPaths, converted.serverLoadPath);
|
|
40
|
+
else if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
|
|
41
|
+
if ((converted.serverAddPath || converted.serverAddPaths) && shouldUseServerPath(converted.addPath, converted._detectedAddPath)) converted.addPath = getServerPath(converted.serverAddPaths, converted.serverAddPath);
|
|
42
|
+
else if (converted.addPath) converted.addPath = convertPath(converted.addPath);
|
|
43
|
+
delete converted.serverLoadPath;
|
|
44
|
+
delete converted.serverAddPath;
|
|
45
|
+
delete converted.serverLoadPaths;
|
|
46
|
+
delete converted.serverAddPaths;
|
|
47
|
+
delete converted._detectedLoadPath;
|
|
48
|
+
delete converted._detectedAddPath;
|
|
18
49
|
return converted;
|
|
19
50
|
}
|
|
20
51
|
export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BackendOptions } from '../shared/type';
|
|
2
|
+
interface NormalizedConfigForLocales {
|
|
3
|
+
server?: {
|
|
4
|
+
publicDir?: string | string[];
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export interface DetectedLocalesDirectory {
|
|
8
|
+
loadPath: string;
|
|
9
|
+
addPath: string;
|
|
10
|
+
serverLoadPath: string;
|
|
11
|
+
serverAddPath: string;
|
|
12
|
+
serverLoadPaths?: string[];
|
|
13
|
+
serverAddPaths?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function detectLocalesDirectory(appDirectory: string, normalizedConfig?: NormalizedConfigForLocales): DetectedLocalesDirectory | undefined;
|
|
16
|
+
export declare function applyDetectedBackendPaths(backendOptions: BackendOptions, detectedLocales: DetectedLocalesDirectory | undefined): BackendOptions;
|
|
17
|
+
export {};
|
|
@@ -2,12 +2,15 @@ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
|
|
|
2
2
|
loadPath: string;
|
|
3
3
|
addPath: string;
|
|
4
4
|
};
|
|
5
|
-
|
|
6
|
-
interface Window {
|
|
7
|
-
__assetPrefix__?: string;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
export declare function convertBackendOptions<T extends {
|
|
5
|
+
interface InternalBackendPathOptions {
|
|
11
6
|
loadPath?: string;
|
|
12
7
|
addPath?: string;
|
|
13
|
-
|
|
8
|
+
serverLoadPath?: string;
|
|
9
|
+
serverAddPath?: string;
|
|
10
|
+
serverLoadPaths?: string[];
|
|
11
|
+
serverAddPaths?: string[];
|
|
12
|
+
_detectedLoadPath?: string;
|
|
13
|
+
_detectedAddPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function convertBackendOptions<T extends InternalBackendPathOptions>(options: T): T;
|
|
16
|
+
export {};
|
|
@@ -2,7 +2,15 @@ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
|
|
|
2
2
|
loadPath: string;
|
|
3
3
|
addPath: string;
|
|
4
4
|
};
|
|
5
|
-
|
|
5
|
+
interface InternalBackendPathOptions {
|
|
6
6
|
loadPath?: string;
|
|
7
7
|
addPath?: string;
|
|
8
|
-
|
|
8
|
+
serverLoadPath?: string;
|
|
9
|
+
serverAddPath?: string;
|
|
10
|
+
serverLoadPaths?: string[];
|
|
11
|
+
serverAddPaths?: string[];
|
|
12
|
+
_detectedLoadPath?: string;
|
|
13
|
+
_detectedAddPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function convertBackendOptions<T extends InternalBackendPathOptions>(options: T): T;
|
|
16
|
+
export {};
|
|
@@ -58,6 +58,26 @@ export interface BaseBackendOptions {
|
|
|
58
58
|
enabled?: boolean;
|
|
59
59
|
loadPath?: string;
|
|
60
60
|
addPath?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Internal file-system path used by the Node.js FS backend.
|
|
63
|
+
* Browser HTTP backend keeps using `loadPath`.
|
|
64
|
+
*/
|
|
65
|
+
serverLoadPath?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Internal file-system path candidates used by the Node.js FS backend.
|
|
68
|
+
* The first existing path under current cwd will be used.
|
|
69
|
+
*/
|
|
70
|
+
serverLoadPaths?: string[];
|
|
71
|
+
/**
|
|
72
|
+
* Internal file-system path used by the Node.js FS backend.
|
|
73
|
+
* Browser HTTP backend keeps using `addPath`.
|
|
74
|
+
*/
|
|
75
|
+
serverAddPath?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Internal file-system path candidates used by the Node.js FS backend.
|
|
78
|
+
* The first existing path under current cwd will be used.
|
|
79
|
+
*/
|
|
80
|
+
serverAddPaths?: string[];
|
|
61
81
|
/**
|
|
62
82
|
* Cache hit mode for chained backend (only effective when both `loadPath` and `sdk` are provided)
|
|
63
83
|
*
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"modern",
|
|
16
16
|
"modern.js"
|
|
17
17
|
],
|
|
18
|
-
"version": "3.
|
|
18
|
+
"version": "3.5.0",
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
@@ -85,19 +85,19 @@
|
|
|
85
85
|
"i18next-fs-backend": "^2.6.3",
|
|
86
86
|
"i18next-http-backend": "^3.0.6",
|
|
87
87
|
"i18next-http-middleware": "^3.9.7",
|
|
88
|
-
"@modern-js/plugin": "3.
|
|
89
|
-
"@modern-js/runtime-utils": "3.
|
|
90
|
-
"@modern-js/server-core": "3.
|
|
91
|
-
"@modern-js/server-runtime": "3.
|
|
92
|
-
"@modern-js/types": "3.
|
|
93
|
-
"@modern-js/utils": "3.
|
|
88
|
+
"@modern-js/plugin": "3.5.0",
|
|
89
|
+
"@modern-js/runtime-utils": "3.5.0",
|
|
90
|
+
"@modern-js/server-core": "3.5.0",
|
|
91
|
+
"@modern-js/server-runtime": "3.5.0",
|
|
92
|
+
"@modern-js/types": "3.5.0",
|
|
93
|
+
"@modern-js/utils": "3.5.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=17.0.2",
|
|
97
97
|
"react-dom": ">=17.0.2",
|
|
98
98
|
"i18next": ">=25.7.4",
|
|
99
99
|
"react-i18next": ">=15.7.4",
|
|
100
|
-
"@modern-js/runtime": "^3.
|
|
100
|
+
"@modern-js/runtime": "^3.5.0"
|
|
101
101
|
},
|
|
102
102
|
"peerDependenciesMeta": {
|
|
103
103
|
"i18next": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@rslib/core": "0.
|
|
111
|
+
"@rslib/core": "0.23.0",
|
|
112
112
|
"@types/node": "^20",
|
|
113
113
|
"i18next": "25.7.4",
|
|
114
114
|
"react": "^19.2.7",
|
|
@@ -116,9 +116,9 @@
|
|
|
116
116
|
"react-i18next": "15.7.4",
|
|
117
117
|
"ts-node": "^10.9.2",
|
|
118
118
|
"typescript": "^5",
|
|
119
|
+
"@modern-js/app-tools": "3.5.0",
|
|
119
120
|
"@modern-js/rslib": "2.68.10",
|
|
120
|
-
"@modern-js/
|
|
121
|
-
"@modern-js/runtime": "3.3.0"
|
|
121
|
+
"@modern-js/runtime": "3.5.0"
|
|
122
122
|
},
|
|
123
123
|
"sideEffects": false,
|
|
124
124
|
"publishConfig": {
|
package/src/cli/index.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
1
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
4
2
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
5
3
|
import type { Entrypoint } from '@modern-js/types';
|
|
6
4
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
7
5
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
6
|
+
import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
|
|
8
7
|
import '../runtime/types';
|
|
9
8
|
|
|
10
9
|
export type TransformRuntimeConfigFn = (
|
|
@@ -12,75 +11,6 @@ export type TransformRuntimeConfigFn = (
|
|
|
12
11
|
entrypoint: Entrypoint,
|
|
13
12
|
) => Record<string, any>;
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
* Check if a directory exists and contains JSON files
|
|
17
|
-
*/
|
|
18
|
-
function hasJsonFiles(dirPath: string): boolean {
|
|
19
|
-
try {
|
|
20
|
-
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const entries = fs.readdirSync(dirPath);
|
|
24
|
-
// Check if there are any JSON files in the directory or subdirectories
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const entryPath = path.join(dirPath, entry);
|
|
27
|
-
const stat = fs.statSync(entryPath);
|
|
28
|
-
if (stat.isFile() && entry.endsWith('.json')) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
if (stat.isDirectory()) {
|
|
32
|
-
// Recursively check subdirectories (e.g., locales/en/, locales/zh/)
|
|
33
|
-
if (hasJsonFiles(entryPath)) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Auto-detect if locales directory exists with JSON files
|
|
46
|
-
* Checks both project root and config/public directory
|
|
47
|
-
*/
|
|
48
|
-
function detectLocalesDirectory(
|
|
49
|
-
appDirectory: string,
|
|
50
|
-
normalizedConfig?: any,
|
|
51
|
-
): boolean {
|
|
52
|
-
// Check project root directory
|
|
53
|
-
const rootLocalesPath = path.join(appDirectory, 'locales');
|
|
54
|
-
if (hasJsonFiles(rootLocalesPath)) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check config/public directory
|
|
59
|
-
const configPublicPath = path.join(
|
|
60
|
-
appDirectory,
|
|
61
|
-
'config',
|
|
62
|
-
'public',
|
|
63
|
-
'locales',
|
|
64
|
-
);
|
|
65
|
-
if (hasJsonFiles(configPublicPath)) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check publicDir if configured
|
|
70
|
-
const publicDir = normalizedConfig?.server?.publicDir;
|
|
71
|
-
if (publicDir) {
|
|
72
|
-
const publicDirPath = Array.isArray(publicDir) ? publicDir[0] : publicDir;
|
|
73
|
-
const localesPath = path.isAbsolute(publicDirPath)
|
|
74
|
-
? path.join(publicDirPath, 'locales')
|
|
75
|
-
: path.join(appDirectory, publicDirPath, 'locales');
|
|
76
|
-
if (hasJsonFiles(localesPath)) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
14
|
export interface I18nPluginOptions {
|
|
85
15
|
localeDetection?: LocaleDetectionOptions;
|
|
86
16
|
backend?: BackendOptions;
|
|
@@ -118,10 +48,14 @@ export const i18nPlugin = (
|
|
|
118
48
|
// Auto-detect locales directory and enable backend if:
|
|
119
49
|
// 1. User didn't explicitly set backend.enabled to false
|
|
120
50
|
// 2. Locales directory exists with JSON files
|
|
121
|
-
// 3. If user configured loadPath or addPath, auto-enable backend
|
|
51
|
+
// 3. If user configured loadPath or addPath, auto-enable backend
|
|
122
52
|
let backendOptions: BackendOptions | undefined;
|
|
123
53
|
const { appDirectory } = api.getAppContext();
|
|
124
54
|
const normalizedConfig = api.getNormalizedConfig();
|
|
55
|
+
const detectedLocales = detectLocalesDirectory(
|
|
56
|
+
appDirectory,
|
|
57
|
+
normalizedConfig,
|
|
58
|
+
);
|
|
125
59
|
|
|
126
60
|
if (backend) {
|
|
127
61
|
const entryBackendOptions = getBackendOptions(
|
|
@@ -132,45 +66,35 @@ export const i18nPlugin = (
|
|
|
132
66
|
if (entryBackendOptions?.enabled === false) {
|
|
133
67
|
backendOptions = entryBackendOptions;
|
|
134
68
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
69
|
+
if (detectedLocales) {
|
|
70
|
+
backendOptions = applyDetectedBackendPaths(
|
|
71
|
+
entryBackendOptions,
|
|
72
|
+
detectedLocales,
|
|
73
|
+
);
|
|
74
|
+
} else if (
|
|
75
|
+
entryBackendOptions?.loadPath ||
|
|
76
|
+
entryBackendOptions?.addPath
|
|
77
|
+
) {
|
|
78
|
+
// If user configured loadPath or addPath, auto-enable backend even
|
|
79
|
+
// when no local locales directory is detected.
|
|
138
80
|
backendOptions = {
|
|
139
81
|
...entryBackendOptions,
|
|
140
82
|
enabled: true,
|
|
141
83
|
};
|
|
142
|
-
} else if (entryBackendOptions?.enabled !== true) {
|
|
143
|
-
// Auto-detect if enabled is not explicitly true and no loadPath/addPath configured
|
|
144
|
-
const hasLocales = detectLocalesDirectory(
|
|
145
|
-
appDirectory,
|
|
146
|
-
normalizedConfig,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
if (hasLocales) {
|
|
150
|
-
// Auto-enable backend if locales directory is detected
|
|
151
|
-
backendOptions = {
|
|
152
|
-
...entryBackendOptions,
|
|
153
|
-
enabled: true,
|
|
154
|
-
};
|
|
155
|
-
} else {
|
|
156
|
-
backendOptions = entryBackendOptions;
|
|
157
|
-
}
|
|
158
84
|
} else {
|
|
159
85
|
backendOptions = entryBackendOptions;
|
|
160
86
|
}
|
|
161
87
|
}
|
|
162
88
|
} else {
|
|
163
89
|
// No backend config provided, try auto-detection
|
|
164
|
-
|
|
165
|
-
appDirectory,
|
|
166
|
-
normalizedConfig,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (hasLocales) {
|
|
90
|
+
if (detectedLocales) {
|
|
170
91
|
// Auto-enable backend if locales directory is detected
|
|
171
|
-
backendOptions =
|
|
172
|
-
|
|
173
|
-
|
|
92
|
+
backendOptions = applyDetectedBackendPaths(
|
|
93
|
+
getBackendOptions(entrypoint.entryName, {
|
|
94
|
+
enabled: true,
|
|
95
|
+
}),
|
|
96
|
+
detectedLocales,
|
|
97
|
+
);
|
|
174
98
|
}
|
|
175
99
|
}
|
|
176
100
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
getPublicDirRoutePrefixes,
|
|
5
|
+
normalizePublicDir,
|
|
6
|
+
normalizePublicDirPath,
|
|
7
|
+
} from '@modern-js/server-core';
|
|
8
|
+
import type { BackendOptions } from '../shared/type';
|
|
9
|
+
|
|
10
|
+
interface NormalizedConfigForLocales {
|
|
11
|
+
server?: {
|
|
12
|
+
publicDir?: string | string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DetectedLocalesDirectory {
|
|
17
|
+
loadPath: string;
|
|
18
|
+
addPath: string;
|
|
19
|
+
serverLoadPath: string;
|
|
20
|
+
serverAddPath: string;
|
|
21
|
+
serverLoadPaths?: string[];
|
|
22
|
+
serverAddPaths?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LOCALES_RESOURCE_PATTERN = '{{lng}}/{{ns}}.json';
|
|
26
|
+
|
|
27
|
+
function hasJsonFiles(dirPath: string): boolean {
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const entries = fs.readdirSync(dirPath);
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const entryPath = path.join(dirPath, entry);
|
|
35
|
+
const stat = fs.statSync(entryPath);
|
|
36
|
+
if (stat.isFile() && entry.endsWith('.json')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (stat.isDirectory() && hasJsonFiles(entryPath)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toPosixPath(filePath: string): string {
|
|
50
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getPublicDirOutputPath(publicDir: string): string {
|
|
54
|
+
return normalizePublicDirPath(publicDir);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildDetectedLocalesDirectory(
|
|
58
|
+
clientBasePath: string,
|
|
59
|
+
serverBasePath: string,
|
|
60
|
+
serverBasePathCandidates: string[] = [serverBasePath],
|
|
61
|
+
): DetectedLocalesDirectory {
|
|
62
|
+
const normalizedClientBasePath = clientBasePath.startsWith('/')
|
|
63
|
+
? clientBasePath
|
|
64
|
+
: `/${clientBasePath}`;
|
|
65
|
+
const normalizedServerBasePath = toPosixPath(serverBasePath).replace(
|
|
66
|
+
/\/$/,
|
|
67
|
+
'',
|
|
68
|
+
);
|
|
69
|
+
const normalizedServerBasePathCandidates = Array.from(
|
|
70
|
+
new Set(
|
|
71
|
+
serverBasePathCandidates.map(candidate =>
|
|
72
|
+
toPosixPath(candidate).replace(/\/$/, ''),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
const serverLoadPaths = normalizedServerBasePathCandidates.map(
|
|
77
|
+
candidate => `${candidate}/${LOCALES_RESOURCE_PATTERN}`,
|
|
78
|
+
);
|
|
79
|
+
const serverAddPaths = normalizedServerBasePathCandidates.map(
|
|
80
|
+
candidate => `${candidate}/${LOCALES_RESOURCE_PATTERN}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
loadPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
85
|
+
addPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
86
|
+
serverLoadPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
87
|
+
serverAddPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
88
|
+
serverLoadPaths,
|
|
89
|
+
serverAddPaths,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function detectLocalesDirectory(
|
|
94
|
+
appDirectory: string,
|
|
95
|
+
normalizedConfig?: NormalizedConfigForLocales,
|
|
96
|
+
): DetectedLocalesDirectory | undefined {
|
|
97
|
+
const publicDirs = normalizePublicDir(normalizedConfig?.server?.publicDir);
|
|
98
|
+
const publicDirPrefixes = getPublicDirRoutePrefixes(
|
|
99
|
+
normalizedConfig?.server?.publicDir,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < publicDirs.length; index++) {
|
|
103
|
+
const publicDir = publicDirs[index];
|
|
104
|
+
if (path.isAbsolute(publicDir)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const publicDirPath = path.join(appDirectory, publicDir);
|
|
109
|
+
const publicDirPrefix =
|
|
110
|
+
publicDirPrefixes[index] || `/${normalizePublicDirPath(publicDir)}`;
|
|
111
|
+
const publicDirOutputPath = getPublicDirOutputPath(publicDir);
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
path.basename(normalizePublicDirPath(publicDir)) === 'locales' &&
|
|
115
|
+
hasJsonFiles(publicDirPath)
|
|
116
|
+
) {
|
|
117
|
+
return buildDetectedLocalesDirectory(
|
|
118
|
+
publicDirPrefix,
|
|
119
|
+
`./${publicDirOutputPath}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const localesPath = path.join(publicDirPath, 'locales');
|
|
124
|
+
if (hasJsonFiles(localesPath)) {
|
|
125
|
+
return buildDetectedLocalesDirectory(
|
|
126
|
+
`${publicDirPrefix}/locales`,
|
|
127
|
+
`./${publicDirOutputPath}/locales`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const configPublicPath = path.join(
|
|
133
|
+
appDirectory,
|
|
134
|
+
'config',
|
|
135
|
+
'public',
|
|
136
|
+
'locales',
|
|
137
|
+
);
|
|
138
|
+
if (hasJsonFiles(configPublicPath)) {
|
|
139
|
+
return buildDetectedLocalesDirectory('/locales', './public/locales', [
|
|
140
|
+
'./config/public/locales',
|
|
141
|
+
'./public/locales',
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rootLocalesPath = path.join(appDirectory, 'locales');
|
|
146
|
+
if (hasJsonFiles(rootLocalesPath)) {
|
|
147
|
+
return buildDetectedLocalesDirectory('/locales', './locales');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function applyDetectedBackendPaths(
|
|
154
|
+
backendOptions: BackendOptions,
|
|
155
|
+
detectedLocales: DetectedLocalesDirectory | undefined,
|
|
156
|
+
): BackendOptions {
|
|
157
|
+
if (!detectedLocales) {
|
|
158
|
+
return backendOptions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const backendWithDetectedPaths: BackendOptions & {
|
|
162
|
+
_detectedLoadPath: string;
|
|
163
|
+
_detectedAddPath: string;
|
|
164
|
+
} = {
|
|
165
|
+
...backendOptions,
|
|
166
|
+
enabled: true,
|
|
167
|
+
loadPath: backendOptions.loadPath ?? detectedLocales.loadPath,
|
|
168
|
+
addPath: backendOptions.addPath ?? detectedLocales.addPath,
|
|
169
|
+
serverLoadPath:
|
|
170
|
+
backendOptions.serverLoadPath ?? detectedLocales.serverLoadPath,
|
|
171
|
+
serverLoadPaths:
|
|
172
|
+
backendOptions.serverLoadPath || backendOptions.serverLoadPaths
|
|
173
|
+
? backendOptions.serverLoadPaths
|
|
174
|
+
: detectedLocales.serverLoadPaths,
|
|
175
|
+
serverAddPath:
|
|
176
|
+
backendOptions.serverAddPath ?? detectedLocales.serverAddPath,
|
|
177
|
+
serverAddPaths:
|
|
178
|
+
backendOptions.serverAddPath || backendOptions.serverAddPaths
|
|
179
|
+
? backendOptions.serverAddPaths
|
|
180
|
+
: detectedLocales.serverAddPaths,
|
|
181
|
+
_detectedLoadPath: detectedLocales.loadPath,
|
|
182
|
+
_detectedAddPath: detectedLocales.addPath,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return backendWithDetectedPaths;
|
|
186
|
+
}
|