@modern-js/plugin-i18n 2.69.7 → 3.0.0-alpha.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.
Files changed (151) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/cli/index.cjs +154 -0
  3. package/dist/cjs/runtime/I18nLink.cjs +68 -0
  4. package/dist/cjs/runtime/context.cjs +138 -0
  5. package/dist/cjs/runtime/hooks.cjs +189 -0
  6. package/dist/cjs/runtime/i18n/backend/config.cjs +39 -0
  7. package/dist/cjs/runtime/i18n/backend/defaults.cjs +56 -0
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.cjs +56 -0
  9. package/dist/cjs/runtime/i18n/backend/index.cjs +108 -0
  10. package/dist/cjs/runtime/i18n/backend/middleware.cjs +54 -0
  11. package/dist/cjs/runtime/i18n/backend/middleware.common.cjs +105 -0
  12. package/dist/cjs/runtime/i18n/backend/middleware.node.cjs +58 -0
  13. package/dist/cjs/runtime/i18n/backend/sdk-backend.cjs +171 -0
  14. package/dist/cjs/runtime/i18n/detection/config.cjs +63 -0
  15. package/dist/cjs/runtime/i18n/detection/index.cjs +309 -0
  16. package/dist/cjs/runtime/i18n/detection/middleware.cjs +185 -0
  17. package/dist/cjs/runtime/i18n/detection/middleware.node.cjs +74 -0
  18. package/dist/cjs/runtime/i18n/index.cjs +43 -0
  19. package/dist/cjs/runtime/i18n/instance.cjs +132 -0
  20. package/dist/cjs/runtime/i18n/utils.cjs +185 -0
  21. package/dist/cjs/runtime/index.cjs +172 -0
  22. package/dist/cjs/runtime/types.cjs +18 -0
  23. package/dist/cjs/runtime/utils.cjs +134 -0
  24. package/dist/cjs/server/index.cjs +178 -0
  25. package/dist/cjs/shared/deepMerge.cjs +54 -0
  26. package/dist/cjs/shared/detection.cjs +105 -0
  27. package/dist/cjs/shared/type.cjs +18 -0
  28. package/dist/cjs/shared/utils.cjs +78 -0
  29. package/dist/esm/cli/index.js +106 -0
  30. package/dist/esm/runtime/I18nLink.js +31 -0
  31. package/dist/esm/runtime/context.js +101 -0
  32. package/dist/esm/runtime/hooks.js +146 -0
  33. package/dist/esm/runtime/i18n/backend/config.js +5 -0
  34. package/dist/esm/runtime/i18n/backend/defaults.js +19 -0
  35. package/dist/esm/runtime/i18n/backend/defaults.node.js +19 -0
  36. package/dist/esm/runtime/i18n/backend/index.js +74 -0
  37. package/dist/esm/runtime/i18n/backend/middleware.common.js +61 -0
  38. package/dist/esm/runtime/i18n/backend/middleware.js +7 -0
  39. package/dist/esm/runtime/i18n/backend/middleware.node.js +8 -0
  40. package/dist/esm/runtime/i18n/backend/sdk-backend.js +137 -0
  41. package/dist/esm/runtime/i18n/detection/config.js +26 -0
  42. package/dist/esm/runtime/i18n/detection/index.js +260 -0
  43. package/dist/esm/runtime/i18n/detection/middleware.js +132 -0
  44. package/dist/esm/runtime/i18n/detection/middleware.node.js +31 -0
  45. package/dist/esm/runtime/i18n/index.js +3 -0
  46. package/dist/esm/runtime/i18n/instance.js +77 -0
  47. package/dist/esm/runtime/i18n/utils.js +136 -0
  48. package/dist/esm/runtime/index.js +129 -0
  49. package/dist/esm/runtime/types.js +0 -0
  50. package/dist/esm/runtime/utils.js +82 -0
  51. package/dist/esm/server/index.js +168 -0
  52. package/dist/esm/shared/deepMerge.js +20 -0
  53. package/dist/esm/shared/detection.js +71 -0
  54. package/dist/esm/shared/type.js +0 -0
  55. package/dist/esm/shared/utils.js +35 -0
  56. package/dist/esm-node/cli/index.js +106 -0
  57. package/dist/esm-node/runtime/I18nLink.js +31 -0
  58. package/dist/esm-node/runtime/context.js +101 -0
  59. package/dist/esm-node/runtime/hooks.js +146 -0
  60. package/dist/esm-node/runtime/i18n/backend/config.js +5 -0
  61. package/dist/esm-node/runtime/i18n/backend/defaults.js +19 -0
  62. package/dist/esm-node/runtime/i18n/backend/defaults.node.js +19 -0
  63. package/dist/esm-node/runtime/i18n/backend/index.js +74 -0
  64. package/dist/esm-node/runtime/i18n/backend/middleware.common.js +61 -0
  65. package/dist/esm-node/runtime/i18n/backend/middleware.js +7 -0
  66. package/dist/esm-node/runtime/i18n/backend/middleware.node.js +8 -0
  67. package/dist/esm-node/runtime/i18n/backend/sdk-backend.js +137 -0
  68. package/dist/esm-node/runtime/i18n/detection/config.js +26 -0
  69. package/dist/esm-node/runtime/i18n/detection/index.js +260 -0
  70. package/dist/esm-node/runtime/i18n/detection/middleware.js +132 -0
  71. package/dist/esm-node/runtime/i18n/detection/middleware.node.js +31 -0
  72. package/dist/esm-node/runtime/i18n/index.js +3 -0
  73. package/dist/esm-node/runtime/i18n/instance.js +77 -0
  74. package/dist/esm-node/runtime/i18n/utils.js +136 -0
  75. package/dist/esm-node/runtime/index.js +129 -0
  76. package/dist/esm-node/runtime/types.js +0 -0
  77. package/dist/esm-node/runtime/utils.js +82 -0
  78. package/dist/esm-node/server/index.js +168 -0
  79. package/dist/esm-node/shared/deepMerge.js +20 -0
  80. package/dist/esm-node/shared/detection.js +71 -0
  81. package/dist/esm-node/shared/type.js +0 -0
  82. package/dist/esm-node/shared/utils.js +35 -0
  83. package/dist/types/cli/index.d.ts +21 -0
  84. package/dist/types/runtime/I18nLink.d.ts +8 -0
  85. package/dist/types/runtime/context.d.ts +38 -0
  86. package/dist/types/runtime/hooks.d.ts +28 -0
  87. package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
  88. package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
  89. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
  90. package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
  91. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
  92. package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
  93. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
  94. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +52 -0
  95. package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
  96. package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
  97. package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
  98. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
  99. package/dist/types/runtime/i18n/index.d.ts +3 -0
  100. package/dist/types/runtime/i18n/instance.d.ts +93 -0
  101. package/dist/types/runtime/i18n/utils.d.ts +29 -0
  102. package/dist/types/runtime/index.d.ts +20 -0
  103. package/dist/types/runtime/types.d.ts +15 -0
  104. package/dist/types/runtime/utils.d.ts +33 -0
  105. package/dist/types/server/index.d.ts +8 -0
  106. package/dist/types/shared/deepMerge.d.ts +1 -0
  107. package/dist/types/shared/detection.d.ts +11 -0
  108. package/dist/types/shared/type.d.ts +156 -0
  109. package/dist/types/shared/utils.d.ts +5 -0
  110. package/package.json +100 -34
  111. package/rslib.config.mts +4 -0
  112. package/src/cli/index.ts +245 -0
  113. package/src/runtime/I18nLink.tsx +76 -0
  114. package/src/runtime/context.tsx +256 -0
  115. package/src/runtime/hooks.ts +274 -0
  116. package/src/runtime/i18n/backend/config.ts +10 -0
  117. package/src/runtime/i18n/backend/defaults.node.ts +31 -0
  118. package/src/runtime/i18n/backend/defaults.ts +37 -0
  119. package/src/runtime/i18n/backend/index.ts +181 -0
  120. package/src/runtime/i18n/backend/middleware.common.ts +116 -0
  121. package/src/runtime/i18n/backend/middleware.node.ts +32 -0
  122. package/src/runtime/i18n/backend/middleware.ts +28 -0
  123. package/src/runtime/i18n/backend/sdk-backend.ts +292 -0
  124. package/src/runtime/i18n/detection/config.ts +32 -0
  125. package/src/runtime/i18n/detection/index.ts +641 -0
  126. package/src/runtime/i18n/detection/middleware.node.ts +84 -0
  127. package/src/runtime/i18n/detection/middleware.ts +251 -0
  128. package/src/runtime/i18n/index.ts +8 -0
  129. package/src/runtime/i18n/instance.ts +227 -0
  130. package/src/runtime/i18n/utils.ts +333 -0
  131. package/src/runtime/index.tsx +281 -0
  132. package/src/runtime/types.ts +17 -0
  133. package/src/runtime/utils.ts +151 -0
  134. package/src/server/index.ts +336 -0
  135. package/src/shared/deepMerge.ts +38 -0
  136. package/src/shared/detection.ts +131 -0
  137. package/src/shared/type.ts +170 -0
  138. package/src/shared/utils.ts +82 -0
  139. package/tsconfig.json +12 -0
  140. package/dist/cjs/index.js +0 -73
  141. package/dist/cjs/languageDetector.js +0 -51
  142. package/dist/cjs/utils/index.js +0 -39
  143. package/dist/esm/index.js +0 -61
  144. package/dist/esm/languageDetector.js +0 -33
  145. package/dist/esm/utils/index.js +0 -16
  146. package/dist/esm-node/index.js +0 -49
  147. package/dist/esm-node/languageDetector.js +0 -26
  148. package/dist/esm-node/utils/index.js +0 -15
  149. package/dist/types/index.d.ts +0 -34
  150. package/dist/types/languageDetector.d.ts +0 -6
  151. package/dist/types/utils/index.d.ts +0 -5
package/package.json CHANGED
@@ -2,11 +2,12 @@
2
2
  "name": "@modern-js/plugin-i18n",
3
3
  "description": "A Progressive React Framework for modern web development.",
4
4
  "homepage": "https://modernjs.dev",
5
+ "type": "module",
5
6
  "bugs": "https://github.com/web-infra-dev/modern.js/issues",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/web-infra-dev/modern.js",
9
- "directory": "packages/cli/plugin-i18n"
10
+ "directory": "packages/runtime/plugin-i18n"
10
11
  },
11
12
  "license": "MIT",
12
13
  "keywords": [
@@ -15,47 +16,112 @@
15
16
  "modern",
16
17
  "modern.js"
17
18
  ],
18
- "version": "2.69.7",
19
- "jsnext:source": "./src/index.ts",
20
- "types": "./dist/types/index.d.ts",
21
- "main": "./dist/cjs/index.js",
22
- "module": "./dist/esm/index.js",
23
- "typesVersions": {
24
- "*": {
25
- "language-detector": [
26
- "./dist/types/languageDetector.d.ts"
27
- ]
28
- }
19
+ "version": "3.0.0-alpha.1",
20
+ "engines": {
21
+ "node": ">=20"
29
22
  },
23
+ "jsnext:source": "./src/cli/index.ts",
24
+ "types": "./dist/types/cli/index.d.ts",
25
+ "main": "./dist/cjs/cli/index.cjs",
26
+ "module": "./dist/esm/cli/index.js",
30
27
  "exports": {
31
28
  ".": {
32
- "node": {
33
- "import": "./dist/esm-node/index.js",
34
- "require": "./dist/cjs/index.js"
35
- },
36
- "types": "./dist/types/index.d.ts",
37
- "default": "./dist/esm/index.js"
29
+ "types": "./dist/types/cli/index.d.ts",
30
+ "jsnext:source": "./src/cli/index.ts",
31
+ "default": "./dist/cjs/cli/index.cjs"
38
32
  },
39
- "./language-detector": {
40
- "node": {
41
- "import": "./dist/esm-node/languageDetector.js",
42
- "require": "./dist/cjs/languageDetector.js"
43
- },
44
- "types": "./dist/types/languageDetector.d.ts",
45
- "default": "./dist/esm/languageDetector.js"
33
+ "./package.json": "./package.json",
34
+ "./cli": {
35
+ "types": "./dist/types/cli/index.d.ts",
36
+ "jsnext:source": "./src/cli/index.ts",
37
+ "default": "./dist/cjs/cli/index.cjs"
38
+ },
39
+ "./runtime": {
40
+ "types": "./dist/types/runtime/index.d.ts",
41
+ "jsnext:source": "./src/runtime/index.ts",
42
+ "import": "./dist/esm/runtime/index.js",
43
+ "node": "./dist/esm-node/runtime/index.js",
44
+ "default": "./dist/esm/runtime/index.js"
45
+ },
46
+ "./server": {
47
+ "types": "./dist/types/server/index.d.ts",
48
+ "jsnext:source": "./src/server/index.ts",
49
+ "import": "./dist/esm/server/index.js",
50
+ "node": "./dist/esm-node/server/index.js",
51
+ "default": "./dist/esm/server/index.js"
52
+ },
53
+ "./i18n": {
54
+ "types": "./dist/types/runtime/i18n/index.d.ts",
55
+ "jsnext:source": "./src/runtime/i18n/index.ts",
56
+ "import": "./dist/esm/runtime/i18n/index.js",
57
+ "node": "./dist/esm-node/runtime/i18n/index.js",
58
+ "default": "./dist/esm/runtime/i18n/index.js"
59
+ }
60
+ },
61
+ "typesVersions": {
62
+ "*": {
63
+ ".": [
64
+ "./dist/types/cli/index.d.ts"
65
+ ],
66
+ "cli": [
67
+ "./dist/types/cli/index.d.ts"
68
+ ],
69
+ "runtime": [
70
+ "./dist/types/runtime/index.d.ts"
71
+ ],
72
+ "server": [
73
+ "./dist/types/server/index.d.ts"
74
+ ],
75
+ "i18n": [
76
+ "./dist/types/runtime/i18n/index.d.ts"
77
+ ]
46
78
  }
47
79
  },
48
80
  "dependencies": {
49
81
  "@swc/helpers": "^0.5.17",
50
- "@modern-js/utils": "2.69.7"
82
+ "i18next-browser-languagedetector": "^8.2.0",
83
+ "i18next-http-middleware": "^3.9.2",
84
+ "i18next-fs-backend": "^2.6.1",
85
+ "i18next-http-backend": "^3.0.2",
86
+ "i18next-chained-backend": "^4.6.3",
87
+ "@modern-js/plugin": "3.0.0-alpha.1",
88
+ "@modern-js/server-core": "3.0.0-alpha.1",
89
+ "@modern-js/runtime-utils": "3.0.0-alpha.1",
90
+ "@modern-js/types": "3.0.0-alpha.1",
91
+ "@modern-js/server-runtime": "3.0.0-alpha.1",
92
+ "@modern-js/utils": "3.0.0-alpha.1"
93
+ },
94
+ "peerDependencies": {
95
+ "react": ">=17.0.2",
96
+ "react-dom": ">=17.0.2",
97
+ "i18next": ">=25.7.3",
98
+ "react-i18next": ">=15.7.4",
99
+ "@modern-js/runtime": "^3.0.0-alpha.1"
100
+ },
101
+ "peerDependenciesMeta": {
102
+ "i18next": {
103
+ "optional": true
104
+ },
105
+ "react-i18next": {
106
+ "optional": true
107
+ }
51
108
  },
52
109
  "devDependencies": {
53
- "@types/jest": "^29",
54
- "@types/node": "^18",
55
- "jest": "^29",
110
+ "i18next": "25.7.3",
111
+ "react-i18next": "15.7.4",
112
+ "@rslib/core": "0.19.1",
113
+ "@types/jest": "^29.5.14",
114
+ "@types/node": "^20",
115
+ "jest": "^29.7.0",
116
+ "react": "^19.2.3",
117
+ "react-dom": "^19.2.3",
118
+ "ts-jest": "^29.4.6",
119
+ "ts-node": "^10.9.2",
56
120
  "typescript": "^5",
57
- "@scripts/build": "2.66.0",
58
- "@scripts/jest-config": "2.66.0"
121
+ "@modern-js/runtime": "3.0.0-alpha.1",
122
+ "@modern-js/app-tools": "3.0.0-alpha.1",
123
+ "@scripts/jest-config": "2.66.0",
124
+ "@modern-js/rslib": "2.68.10"
59
125
  },
60
126
  "sideEffects": false,
61
127
  "publishConfig": {
@@ -63,8 +129,8 @@
63
129
  "access": "public"
64
130
  },
65
131
  "scripts": {
66
- "new": "modern-lib new",
67
- "build": "modern-lib build",
68
- "test": "jest --passWithNoTests"
132
+ "dev": "rslib build --watch",
133
+ "build": "rslib build",
134
+ "test": "jest"
69
135
  }
70
136
  }
@@ -0,0 +1,4 @@
1
+ import { rslibConfig } from '@modern-js/rslib';
2
+ import { defineConfig } from '@rslib/core';
3
+
4
+ export default defineConfig(rslibConfig);
@@ -0,0 +1,245 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import type { AppTools, CliPlugin } from '@modern-js/app-tools';
4
+ import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
5
+ import type { Entrypoint } from '@modern-js/types';
6
+ import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
7
+ import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
8
+
9
+ export type TransformRuntimeConfigFn = (
10
+ extendedConfig: Record<string, any>,
11
+ entrypoint: Entrypoint,
12
+ ) => Record<string, any>;
13
+
14
+ /**
15
+ * Check if a directory exists and contains JSON files
16
+ */
17
+ function hasJsonFiles(dirPath: string): boolean {
18
+ try {
19
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
20
+ return false;
21
+ }
22
+ const entries = fs.readdirSync(dirPath);
23
+ // Check if there are any JSON files in the directory or subdirectories
24
+ for (const entry of entries) {
25
+ const entryPath = path.join(dirPath, entry);
26
+ const stat = fs.statSync(entryPath);
27
+ if (stat.isFile() && entry.endsWith('.json')) {
28
+ return true;
29
+ }
30
+ if (stat.isDirectory()) {
31
+ // Recursively check subdirectories (e.g., locales/en/, locales/zh/)
32
+ if (hasJsonFiles(entryPath)) {
33
+ return true;
34
+ }
35
+ }
36
+ }
37
+ return false;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Auto-detect if locales directory exists with JSON files
45
+ * Checks both project root and config/public directory
46
+ */
47
+ function detectLocalesDirectory(
48
+ appDirectory: string,
49
+ normalizedConfig?: any,
50
+ ): boolean {
51
+ // Check project root directory
52
+ const rootLocalesPath = path.join(appDirectory, 'locales');
53
+ if (hasJsonFiles(rootLocalesPath)) {
54
+ return true;
55
+ }
56
+
57
+ // Check config/public directory
58
+ const configPublicPath = path.join(
59
+ appDirectory,
60
+ 'config',
61
+ 'public',
62
+ 'locales',
63
+ );
64
+ if (hasJsonFiles(configPublicPath)) {
65
+ return true;
66
+ }
67
+
68
+ // Check publicDir if configured
69
+ const publicDir = normalizedConfig?.server?.publicDir;
70
+ if (publicDir) {
71
+ const publicDirPath = Array.isArray(publicDir) ? publicDir[0] : publicDir;
72
+ const localesPath = path.isAbsolute(publicDirPath)
73
+ ? path.join(publicDirPath, 'locales')
74
+ : path.join(appDirectory, publicDirPath, 'locales');
75
+ if (hasJsonFiles(localesPath)) {
76
+ return true;
77
+ }
78
+ }
79
+
80
+ return false;
81
+ }
82
+
83
+ export interface I18nPluginOptions {
84
+ localeDetection?: LocaleDetectionOptions;
85
+ backend?: BackendOptions;
86
+ transformRuntimeConfig?: TransformRuntimeConfigFn;
87
+ customPlugin?: {
88
+ runtime?: {
89
+ name?: string;
90
+ path?: string;
91
+ };
92
+ server?: {
93
+ name?: string;
94
+ };
95
+ };
96
+ [key: string]: any;
97
+ }
98
+
99
+ export const i18nPlugin = (
100
+ options: I18nPluginOptions = {},
101
+ ): CliPlugin<AppTools> => ({
102
+ name: '@modern-js/plugin-i18n',
103
+ setup: api => {
104
+ const {
105
+ localeDetection,
106
+ backend,
107
+ transformRuntimeConfig,
108
+ customPlugin,
109
+ ...restOptions
110
+ } = options;
111
+
112
+ api._internalRuntimePlugins(({ entrypoint, plugins }) => {
113
+ const localeDetectionOptions = localeDetection
114
+ ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
115
+ : undefined;
116
+
117
+ // Auto-detect locales directory and enable backend if:
118
+ // 1. User didn't explicitly set backend.enabled to false
119
+ // 2. Locales directory exists with JSON files
120
+ // 3. If user configured loadPath or addPath, auto-enable backend without detection
121
+ let backendOptions: BackendOptions | undefined;
122
+ const { appDirectory } = api.getAppContext();
123
+ const normalizedConfig = api.getNormalizedConfig();
124
+
125
+ if (backend) {
126
+ const entryBackendOptions = getBackendOptions(
127
+ entrypoint.entryName,
128
+ backend,
129
+ );
130
+ // If user explicitly set enabled to false, don't auto-detect
131
+ if (entryBackendOptions?.enabled === false) {
132
+ backendOptions = entryBackendOptions;
133
+ } else {
134
+ // If user configured loadPath or addPath, auto-enable backend
135
+ // No need to detect locales directory since user has specified the path
136
+ if (entryBackendOptions?.loadPath || entryBackendOptions?.addPath) {
137
+ backendOptions = {
138
+ ...entryBackendOptions,
139
+ enabled: true,
140
+ };
141
+ } else if (entryBackendOptions?.enabled !== true) {
142
+ // Auto-detect if enabled is not explicitly true and no loadPath/addPath configured
143
+ const hasLocales = detectLocalesDirectory(
144
+ appDirectory,
145
+ normalizedConfig,
146
+ );
147
+
148
+ if (hasLocales) {
149
+ // Auto-enable backend if locales directory is detected
150
+ backendOptions = {
151
+ ...entryBackendOptions,
152
+ enabled: true,
153
+ };
154
+ } else {
155
+ backendOptions = entryBackendOptions;
156
+ }
157
+ } else {
158
+ backendOptions = entryBackendOptions;
159
+ }
160
+ }
161
+ } else {
162
+ // No backend config provided, try auto-detection
163
+ const hasLocales = detectLocalesDirectory(
164
+ appDirectory,
165
+ normalizedConfig,
166
+ );
167
+
168
+ if (hasLocales) {
169
+ // Auto-enable backend if locales directory is detected
170
+ backendOptions = getBackendOptions(entrypoint.entryName, {
171
+ enabled: true,
172
+ });
173
+ }
174
+ }
175
+
176
+ const { metaName } = api.getAppContext();
177
+
178
+ // Transform extended config if transform function is provided
179
+ let extendedConfig = restOptions;
180
+ if (transformRuntimeConfig) {
181
+ extendedConfig = transformRuntimeConfig(
182
+ restOptions,
183
+ entrypoint as Entrypoint,
184
+ );
185
+ }
186
+
187
+ // Build final config with base config and transformed extended config
188
+ const config = {
189
+ entryName: entrypoint.entryName,
190
+ localeDetection: localeDetectionOptions,
191
+ backend: backendOptions,
192
+ ...extendedConfig,
193
+ };
194
+
195
+ plugins.push({
196
+ name: customPlugin?.runtime?.name || 'i18n',
197
+ path: customPlugin?.runtime?.path || `@${metaName}/plugin-i18n/runtime`,
198
+ config,
199
+ });
200
+ return {
201
+ entrypoint,
202
+ plugins,
203
+ };
204
+ });
205
+
206
+ api._internalServerPlugins(({ plugins }) => {
207
+ const { serverRoutes, metaName } = api.getAppContext();
208
+ const normalizedConfig = api.getNormalizedConfig();
209
+
210
+ let staticRoutePrefixes: string[] = [];
211
+ if (serverRoutes && Array.isArray(serverRoutes)) {
212
+ // Get static route prefixes from 'public' directories
213
+ // 'public' routes are handled by static plugin
214
+ staticRoutePrefixes = serverRoutes
215
+ .filter(
216
+ route => !route.entryName && route.entryPath.startsWith('public'),
217
+ )
218
+ .map(route => route.urlPath)
219
+ .filter(Boolean);
220
+ }
221
+
222
+ // Also include publicDir configuration paths
223
+ // publicDir files are copied to dist/{dir}/ and should be treated as static resources
224
+ const publicDirPrefixes = getPublicDirRoutePrefixes(
225
+ normalizedConfig?.server?.publicDir,
226
+ );
227
+ publicDirPrefixes.forEach(prefix => {
228
+ if (!staticRoutePrefixes.includes(prefix)) {
229
+ staticRoutePrefixes.push(prefix);
230
+ }
231
+ });
232
+
233
+ plugins.push({
234
+ name: customPlugin?.server?.name || `@${metaName}/plugin-i18n/server`,
235
+ options: {
236
+ localeDetection,
237
+ staticRoutePrefixes,
238
+ },
239
+ });
240
+ return { plugins };
241
+ });
242
+ },
243
+ });
244
+
245
+ export default i18nPlugin;
@@ -0,0 +1,76 @@
1
+ import { Link, useInRouterContext, useParams } from '@modern-js/runtime/router';
2
+ import type React from 'react';
3
+ import { useModernI18n } from './context';
4
+ import { buildLocalizedUrl } from './utils';
5
+
6
+ export interface I18nLinkProps {
7
+ to: string;
8
+ children: React.ReactNode;
9
+ [key: string]: any; // Allow other props to be passed through
10
+ }
11
+
12
+ /**
13
+ * I18nLink component that automatically adds language prefix to navigation links.
14
+ * This component should be used within a :lang dynamic route context.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * // When current language is 'en' and to="/about"
19
+ * // The actual link will be "/en/about"
20
+ * <I18nLink to="/about">About</I18nLink>
21
+ *
22
+ * // When current language is 'zh' and to="/"
23
+ * // The actual link will be "/zh"
24
+ * <I18nLink to="/">Home</I18nLink>
25
+ * ```
26
+ */
27
+ // Use static imports to avoid breaking router tree-shaking. Detect router context via useInRouterContext.
28
+ const useRouterHooks = () => {
29
+ const inRouter = useInRouterContext();
30
+ return {
31
+ Link: inRouter ? Link : null,
32
+ params: inRouter ? useParams() : ({} as any),
33
+ hasRouter: inRouter,
34
+ };
35
+ };
36
+
37
+ export const I18nLink: React.FC<I18nLinkProps> = ({
38
+ to,
39
+ children,
40
+ ...props
41
+ }) => {
42
+ const { Link, params, hasRouter } = useRouterHooks();
43
+ const { language, supportedLanguages } = useModernI18n();
44
+
45
+ // Get the current language from context (which reflects the actual current language)
46
+ // URL params might be stale after language changes, so we prioritize the context language
47
+ const currentLang = language;
48
+
49
+ // Build the localized URL by adding language prefix
50
+ const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages);
51
+
52
+ // In development mode, warn if used outside of :lang route context
53
+ if (process.env.NODE_ENV === 'development' && hasRouter && !params.lang) {
54
+ console.warn(
55
+ 'I18nLink is being used outside of a :lang dynamic route context. ' +
56
+ 'This may cause unexpected behavior. Please ensure I18nLink is used within a route that has a :lang parameter.',
57
+ );
58
+ }
59
+
60
+ // If router is not available, render as a regular anchor tag
61
+ if (!hasRouter || !Link) {
62
+ return (
63
+ <a href={localizedTo} {...props}>
64
+ {children}
65
+ </a>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <Link to={localizedTo} {...props}>
71
+ {children}
72
+ </Link>
73
+ );
74
+ };
75
+
76
+ export default I18nLink;