@modern-js/plugin-i18n 2.69.5 → 3.0.0-alpha.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.
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 +162 -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 +119 -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 +119 -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 +19 -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 +275 -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
@@ -0,0 +1,119 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { isBrowser, useRuntimeContext } from "@modern-js/runtime";
3
+ import { merge } from "@modern-js/runtime-utils/merge";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { ModernI18nProvider, useModernI18n } from "./context.js";
6
+ import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.js";
7
+ import { getI18nInstance } from "./i18n/index.js";
8
+ import { mergeBackendOptions } from "./i18n/backend/index.js";
9
+ import { useI18nextBackend } from "./i18n/backend/middleware.js";
10
+ import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.js";
11
+ import { useI18nextLanguageDetector } from "./i18n/detection/middleware.js";
12
+ import { getI18nextInstanceForProvider, getI18nextProvider, getInitReactI18next } from "./i18n/instance.js";
13
+ import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.js";
14
+ import { getPathname } from "./utils.js";
15
+ import "./types.js";
16
+ import { I18nLink } from "./I18nLink.js";
17
+ const i18nPlugin = (options)=>({
18
+ name: '@modern-js/plugin-i18n',
19
+ setup: (api)=>{
20
+ const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend } = options;
21
+ const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
22
+ const { enabled: backendEnabled = false } = backend || {};
23
+ let I18nextProvider;
24
+ api.onBeforeRender(async (context)=>{
25
+ let i18nInstance = await getI18nInstance(userI18nInstance);
26
+ const { i18n: otherConfig } = api.getRuntimeConfig();
27
+ const { initOptions: otherInitOptions } = otherConfig || {};
28
+ const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
29
+ const initReactI18next = await getInitReactI18next();
30
+ I18nextProvider = await getI18nextProvider();
31
+ if (initReactI18next) i18nInstance.use(initReactI18next);
32
+ const pathname = getPathname(context);
33
+ if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
34
+ const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
35
+ const mergedBackend = mergeBackendOptions(backend, userInitOptions);
36
+ const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
37
+ if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
38
+ const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
39
+ languages,
40
+ fallbackLanguage,
41
+ localePathRedirect,
42
+ i18nextDetector,
43
+ detection,
44
+ userInitOptions,
45
+ mergedBackend,
46
+ pathname,
47
+ ssrContext: context.ssrContext
48
+ });
49
+ await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
50
+ if (!isBrowser() && i18nInstance.cloneInstance) {
51
+ i18nInstance = i18nInstance.cloneInstance();
52
+ await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
53
+ }
54
+ if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
55
+ if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
56
+ context.i18nInstance = i18nInstance;
57
+ context.changeLanguage = async (newLang)=>{
58
+ await changeI18nLanguage(i18nInstance, newLang, {
59
+ detectionOptions: mergedDetection
60
+ });
61
+ };
62
+ });
63
+ api.wrapRoot((App)=>(props)=>{
64
+ const runtimeContext = useRuntimeContext();
65
+ const i18nInstance = runtimeContext.i18nInstance;
66
+ const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
67
+ i18nInstance?.language,
68
+ localeDetection?.fallbackLanguage
69
+ ]);
70
+ const [lang, setLang] = useState(initialLang);
71
+ const [forceUpdate, setForceUpdate] = useState(0);
72
+ const prevLangRef = useRef(lang);
73
+ const runtimeContextRef = useRef(runtimeContext);
74
+ runtimeContextRef.current = runtimeContext;
75
+ useEffect(()=>{
76
+ if (i18nInstance?.language) {
77
+ const translator = i18nInstance.translator;
78
+ if (translator) translator.language = i18nInstance.language;
79
+ }
80
+ }, [
81
+ i18nInstance?.language
82
+ ]);
83
+ useEffect(()=>{
84
+ prevLangRef.current = lang;
85
+ }, [
86
+ lang
87
+ ]);
88
+ useSdkResourcesLoader(i18nInstance, setForceUpdate);
89
+ useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
90
+ useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes);
91
+ const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang), [
92
+ lang,
93
+ i18nInstance,
94
+ entryName,
95
+ languages,
96
+ localePathRedirect,
97
+ ignoreRedirectRoutes,
98
+ forceUpdate
99
+ ]);
100
+ const appContent = /*#__PURE__*/ jsx(ModernI18nProvider, {
101
+ value: contextValue,
102
+ children: /*#__PURE__*/ jsx(App, {
103
+ ...props
104
+ })
105
+ });
106
+ if (!i18nInstance) return appContent;
107
+ if (I18nextProvider) {
108
+ const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
109
+ return /*#__PURE__*/ jsx(I18nextProvider, {
110
+ i18n: i18nextInstanceForProvider,
111
+ children: appContent
112
+ });
113
+ }
114
+ return appContent;
115
+ });
116
+ }
117
+ });
118
+ const runtime = i18nPlugin;
119
+ export { I18nLink, runtime as default, i18nPlugin, useModernI18n };
File without changes
@@ -0,0 +1,82 @@
1
+ import * as __rspack_external__modern_js_runtime_router_2dfd0c78 from "@modern-js/runtime/router";
2
+ import { isBrowser } from "@modern-js/runtime";
3
+ import { getGlobalBasename } from "@modern-js/runtime/context";
4
+ var __webpack_modules__ = {
5
+ "@modern-js/runtime/router" (module) {
6
+ module.exports = __rspack_external__modern_js_runtime_router_2dfd0c78;
7
+ }
8
+ };
9
+ var __webpack_module_cache__ = {};
10
+ function __webpack_require__(moduleId) {
11
+ var cachedModule = __webpack_module_cache__[moduleId];
12
+ if (void 0 !== cachedModule) return cachedModule.exports;
13
+ var module = __webpack_module_cache__[moduleId] = {
14
+ exports: {}
15
+ };
16
+ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
17
+ return module.exports;
18
+ }
19
+ const getPathname = (context)=>{
20
+ if (isBrowser()) return window.location.pathname;
21
+ return context.ssrContext?.request?.pathname || '/';
22
+ };
23
+ const getEntryPath = ()=>{
24
+ const basename = getGlobalBasename();
25
+ if (basename) return '/' === basename ? '' : basename;
26
+ return '';
27
+ };
28
+ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
29
+ const segments = pathname.split('/').filter(Boolean);
30
+ const firstSegment = segments[0];
31
+ if (languages.includes(firstSegment)) return firstSegment;
32
+ return fallbackLanguage;
33
+ };
34
+ const buildLocalizedUrl = (pathname, language, languages)=>{
35
+ const segments = pathname.split('/').filter(Boolean);
36
+ if (segments.length > 0 && languages.includes(segments[0])) segments[0] = language;
37
+ else segments.unshift(language);
38
+ return `/${segments.join('/')}`;
39
+ };
40
+ const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
41
+ if (!localePathRedirect) return {
42
+ detected: false
43
+ };
44
+ const relativePath = pathname.replace(getEntryPath(), '');
45
+ const segments = relativePath.split('/').filter(Boolean);
46
+ const firstSegment = segments[0];
47
+ if (firstSegment && languages.includes(firstSegment)) return {
48
+ detected: true,
49
+ language: firstSegment
50
+ };
51
+ return {
52
+ detected: false
53
+ };
54
+ };
55
+ const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes)=>{
56
+ if (!ignoreRedirectRoutes) return false;
57
+ const segments = pathname.split('/').filter(Boolean);
58
+ let pathWithoutLang = pathname;
59
+ if (segments.length > 0 && languages.includes(segments[0])) pathWithoutLang = `/${segments.slice(1).join('/')}`;
60
+ const normalizedPath = pathWithoutLang.startsWith('/') ? pathWithoutLang : `/${pathWithoutLang}`;
61
+ if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
62
+ return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
63
+ };
64
+ const useRouterHooks = ()=>{
65
+ try {
66
+ const { useLocation, useNavigate, useParams } = __webpack_require__("@modern-js/runtime/router");
67
+ return {
68
+ navigate: useNavigate(),
69
+ location: useLocation(),
70
+ params: useParams(),
71
+ hasRouter: true
72
+ };
73
+ } catch (error) {
74
+ return {
75
+ navigate: null,
76
+ location: null,
77
+ params: {},
78
+ hasRouter: false
79
+ };
80
+ }
81
+ };
82
+ export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect, useRouterHooks };
@@ -0,0 +1,168 @@
1
+ import { languageDetector } from "@modern-js/server-core/hono";
2
+ import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.js";
3
+ import { getLocaleDetectionOptions } from "../shared/utils.js";
4
+ var __webpack_require__ = {};
5
+ (()=>{
6
+ __webpack_require__.d = (exports, definition)=>{
7
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) Object.defineProperty(exports, key, {
8
+ enumerable: true,
9
+ get: definition[key]
10
+ });
11
+ };
12
+ })();
13
+ (()=>{
14
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
15
+ })();
16
+ (()=>{
17
+ __webpack_require__.r = (exports)=>{
18
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports, Symbol.toStringTag, {
19
+ value: 'Module'
20
+ });
21
+ Object.defineProperty(exports, '__esModule', {
22
+ value: true
23
+ });
24
+ };
25
+ })();
26
+ var hono_namespaceObject = {};
27
+ __webpack_require__.r(hono_namespaceObject);
28
+ __webpack_require__.d(hono_namespaceObject, {
29
+ languageDetector: ()=>languageDetector
30
+ });
31
+ const { languageDetector: server_languageDetector } = hono_namespaceObject;
32
+ const convertToHonoLanguageDetectorOptions = (languages, fallbackLanguage, detectionOptions)=>{
33
+ const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
34
+ const order = (mergedDetection.order || []).filter((item)=>![
35
+ 'path',
36
+ 'localStorage',
37
+ 'navigator',
38
+ 'htmlTag',
39
+ 'subdomain'
40
+ ].includes(item));
41
+ const detectionOrder = order.length > 0 ? order : [
42
+ 'querystring',
43
+ 'cookie',
44
+ 'header'
45
+ ];
46
+ const honoOrder = detectionOrder.map((item)=>{
47
+ if ('querystring' === item) return 'querystring';
48
+ if ('cookie' === item) return 'cookie';
49
+ if ('header' === item) return 'header';
50
+ return item;
51
+ });
52
+ const caches = false === mergedDetection.caches ? false : Array.isArray(mergedDetection.caches) && !mergedDetection.caches.includes('cookie') ? false : [
53
+ 'cookie'
54
+ ];
55
+ return {
56
+ supportedLanguages: languages.length > 0 ? languages : [
57
+ fallbackLanguage
58
+ ],
59
+ fallbackLanguage,
60
+ order: honoOrder,
61
+ lookupQueryString: mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng',
62
+ lookupCookie: mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next',
63
+ lookupFromHeaderKey: mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language',
64
+ ...void 0 !== caches && {
65
+ caches
66
+ },
67
+ ignoreCase: true
68
+ };
69
+ };
70
+ const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes)=>{
71
+ if (!ignoreRedirectRoutes) return false;
72
+ const basePath = urlPath.replace('/*', '');
73
+ const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
74
+ const normalizedPath = remainingPath.startsWith('/') ? remainingPath : `/${remainingPath}`;
75
+ if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
76
+ return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
77
+ };
78
+ const isStaticResourceRequest = (pathname, staticRoutePrefixes, languages = [])=>{
79
+ if (staticRoutePrefixes.some((prefix)=>pathname.startsWith(`${prefix}/`) || pathname === prefix)) return true;
80
+ const standardStaticPrefixes = [
81
+ '/static/',
82
+ '/upload/'
83
+ ];
84
+ if (standardStaticPrefixes.some((prefix)=>pathname.startsWith(prefix))) return true;
85
+ const pathSegments = pathname.split('/').filter(Boolean);
86
+ if (pathSegments.length > 0 && languages.includes(pathSegments[0])) {
87
+ const pathWithoutLang = '/' + pathSegments.slice(1).join('/');
88
+ if (standardStaticPrefixes.some((prefix)=>pathWithoutLang.startsWith(prefix)) || staticRoutePrefixes.some((prefix)=>pathWithoutLang.startsWith(`${prefix}/`) || pathWithoutLang === prefix)) return true;
89
+ }
90
+ return false;
91
+ };
92
+ const getLanguageFromPath = (req, urlPath, languages)=>{
93
+ const url = new URL(req.url, `http://${req.header().host}`);
94
+ const pathname = url.pathname;
95
+ const basePath = urlPath.replace('/*', '');
96
+ const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
97
+ const segments = remainingPath.split('/').filter(Boolean);
98
+ const firstSegment = segments[0];
99
+ if (languages.includes(firstSegment)) return firstSegment;
100
+ return null;
101
+ };
102
+ const buildLocalizedUrl = (req, urlPath, language, languages)=>{
103
+ const url = new URL(req.url);
104
+ const pathname = url.pathname;
105
+ const basePath = urlPath.replace('/*', '');
106
+ const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
107
+ const segments = remainingPath.split('/').filter(Boolean);
108
+ if (segments.length > 0 && languages.includes(segments[0])) segments[0] = language;
109
+ else segments.unshift(language);
110
+ const newPathname = `/${segments.join('/')}`;
111
+ const suffix = `${url.search}${url.hash}`;
112
+ const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
113
+ return localizedUrl;
114
+ };
115
+ const i18nServerPlugin = (options)=>({
116
+ name: '@modern-js/plugin-i18n/server',
117
+ setup: (api)=>{
118
+ api.onPrepare(()=>{
119
+ const { middlewares, routes } = api.getServerContext();
120
+ routes.map((route)=>{
121
+ const { entryName } = route;
122
+ if (!entryName) return;
123
+ if (!options.localeDetection) return;
124
+ const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = getLocaleDetectionOptions(entryName, options.localeDetection);
125
+ const staticRoutePrefixes = options.staticRoutePrefixes;
126
+ const originUrlPath = route.urlPath;
127
+ const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
128
+ if (localePathRedirect) {
129
+ if (i18nextDetector) {
130
+ const detectorOptions = convertToHonoLanguageDetectorOptions(languages, fallbackLanguage, detection);
131
+ const detectorHandler = server_languageDetector(detectorOptions);
132
+ middlewares.push({
133
+ name: 'i18n-language-detector',
134
+ path: urlPath,
135
+ handler: async (c, next)=>{
136
+ const url = new URL(c.req.url);
137
+ const pathname = url.pathname;
138
+ if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
139
+ return detectorHandler(c, next);
140
+ }
141
+ });
142
+ }
143
+ middlewares.push({
144
+ name: 'i18n-server-middleware',
145
+ path: urlPath,
146
+ handler: async (c, next)=>{
147
+ const url = new URL(c.req.url);
148
+ const pathname = url.pathname;
149
+ if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
150
+ if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)) return await next();
151
+ const language = getLanguageFromPath(c.req, urlPath, languages);
152
+ if (!language) {
153
+ let detectedLanguage = null;
154
+ if (i18nextDetector) detectedLanguage = c.get('language') || null;
155
+ const targetLanguage = detectedLanguage || fallbackLanguage;
156
+ const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages);
157
+ return c.redirect(localizedUrl);
158
+ }
159
+ await next();
160
+ }
161
+ });
162
+ }
163
+ });
164
+ });
165
+ }
166
+ });
167
+ const server = i18nServerPlugin;
168
+ export { server as default, i18nServerPlugin };
@@ -0,0 +1,20 @@
1
+ function isPlainObject(value) {
2
+ return null !== value && 'object' == typeof value && !Array.isArray(value) && !(value instanceof Date);
3
+ }
4
+ function deepMerge(defaultOptions, userOptions) {
5
+ if (!userOptions) return defaultOptions;
6
+ const merged = {
7
+ ...defaultOptions
8
+ };
9
+ for(const key in userOptions){
10
+ const userValue = userOptions[key];
11
+ if (void 0 === userValue) continue;
12
+ const defaultValue = merged[key];
13
+ const isUserValueObject = isPlainObject(userValue);
14
+ const isDefaultValueObject = isPlainObject(defaultValue);
15
+ if (isUserValueObject && isDefaultValueObject) merged[key] = deepMerge(defaultValue, userValue);
16
+ else merged[key] = userValue;
17
+ }
18
+ return merged;
19
+ }
20
+ export { deepMerge };
@@ -0,0 +1,71 @@
1
+ import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.js";
2
+ function detectLanguageFromRequest(req, languages, detectionOptions) {
3
+ try {
4
+ const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
5
+ const order = (mergedDetection.order || []).filter((item)=>![
6
+ 'path',
7
+ 'localStorage',
8
+ 'navigator',
9
+ 'htmlTag',
10
+ 'subdomain'
11
+ ].includes(item));
12
+ const detectionOrder = order.length > 0 ? order : [
13
+ 'querystring',
14
+ 'cookie',
15
+ 'header'
16
+ ];
17
+ const getHeader = (name)=>{
18
+ req.headers, Headers;
19
+ return req.headers.get(name);
20
+ };
21
+ for (const method of detectionOrder){
22
+ let detectedLang = null;
23
+ switch(method){
24
+ case 'querystring':
25
+ {
26
+ const lookupKey = mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng';
27
+ const host = getHeader('host') || 'localhost';
28
+ const url = new URL(req.url, `http://${host}`);
29
+ detectedLang = url.searchParams.get(lookupKey);
30
+ break;
31
+ }
32
+ case 'cookie':
33
+ {
34
+ const lookupKey = mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next';
35
+ const cookieHeader = getHeader('Cookie');
36
+ if (cookieHeader) {
37
+ const cookies = cookieHeader.split(';').reduce((acc, item)=>{
38
+ const [key, value] = item.trim().split('=');
39
+ if (key && value) acc[key] = value;
40
+ return acc;
41
+ }, {});
42
+ detectedLang = cookies[lookupKey] || null;
43
+ }
44
+ break;
45
+ }
46
+ case 'header':
47
+ {
48
+ const lookupKey = mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language';
49
+ const acceptLanguage = getHeader(lookupKey);
50
+ if (acceptLanguage) {
51
+ const languagesList = acceptLanguage.split(',').map((lang)=>{
52
+ const [code, q] = lang.trim().split(';');
53
+ return {
54
+ code: code.split('-')[0],
55
+ quality: q ? parseFloat(q.split('=')[1]) : 1.0
56
+ };
57
+ }).sort((a, b)=>b.quality - a.quality);
58
+ for (const lang of languagesList)if (0 === languages.length || languages.includes(lang.code)) {
59
+ detectedLang = lang.code;
60
+ break;
61
+ }
62
+ }
63
+ break;
64
+ }
65
+ }
66
+ if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
67
+ }
68
+ } catch (error) {}
69
+ return null;
70
+ }
71
+ export { detectLanguageFromRequest };
File without changes
@@ -0,0 +1,35 @@
1
+ function getEntryConfig(entryName, config, entryKey) {
2
+ const entryConfigMap = config[entryKey];
3
+ return entryConfigMap?.[entryName];
4
+ }
5
+ function removeEntryConfigKey(config, entryKey) {
6
+ const { [entryKey]: _, ...rest } = config;
7
+ return rest;
8
+ }
9
+ function getLocaleDetectionOptions(entryName, localeDetection) {
10
+ const fullConfig = localeDetection;
11
+ const entryConfig = getEntryConfig(entryName, fullConfig, 'localeDetectionByEntry');
12
+ if (entryConfig) {
13
+ const globalConfig = removeEntryConfigKey(fullConfig, 'localeDetectionByEntry');
14
+ return {
15
+ ...globalConfig,
16
+ ...entryConfig
17
+ };
18
+ }
19
+ if ('localeDetectionByEntry' in fullConfig) return removeEntryConfigKey(fullConfig, 'localeDetectionByEntry');
20
+ return localeDetection;
21
+ }
22
+ function getBackendOptions(entryName, backend) {
23
+ const fullConfig = backend;
24
+ const entryConfig = getEntryConfig(entryName, fullConfig, 'backendOptionsByEntry');
25
+ if (entryConfig) {
26
+ const globalConfig = removeEntryConfigKey(fullConfig, 'backendOptionsByEntry');
27
+ return {
28
+ ...globalConfig,
29
+ ...entryConfig
30
+ };
31
+ }
32
+ if ('backendOptionsByEntry' in fullConfig) return removeEntryConfigKey(fullConfig, 'backendOptionsByEntry');
33
+ return backend;
34
+ }
35
+ export { getBackendOptions, getEntryConfig, getLocaleDetectionOptions, removeEntryConfigKey };
@@ -0,0 +1,21 @@
1
+ import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
+ import type { Entrypoint } from '@modern-js/types';
3
+ import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
4
+ export type TransformRuntimeConfigFn = (extendedConfig: Record<string, any>, entrypoint: Entrypoint) => Record<string, any>;
5
+ export interface I18nPluginOptions {
6
+ localeDetection?: LocaleDetectionOptions;
7
+ backend?: BackendOptions;
8
+ transformRuntimeConfig?: TransformRuntimeConfigFn;
9
+ customPlugin?: {
10
+ runtime?: {
11
+ name?: string;
12
+ path?: string;
13
+ };
14
+ server?: {
15
+ name?: string;
16
+ };
17
+ };
18
+ [key: string]: any;
19
+ }
20
+ export declare const i18nPlugin: (options?: I18nPluginOptions) => CliPlugin<AppTools>;
21
+ export default i18nPlugin;
@@ -0,0 +1,8 @@
1
+ import type React from 'react';
2
+ export interface I18nLinkProps {
3
+ to: string;
4
+ children: React.ReactNode;
5
+ [key: string]: any;
6
+ }
7
+ export declare const I18nLink: React.FC<I18nLinkProps>;
8
+ export default I18nLink;
@@ -0,0 +1,38 @@
1
+ import type { FC, ReactNode } from 'react';
2
+ import type { I18nInstance } from './i18n';
3
+ export interface ModernI18nContextValue {
4
+ language: string;
5
+ i18nInstance: I18nInstance;
6
+ entryName?: string;
7
+ languages?: string[];
8
+ localePathRedirect?: boolean;
9
+ ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
10
+ updateLanguage?: (newLang: string) => void;
11
+ }
12
+ export interface ModernI18nProviderProps {
13
+ children: ReactNode;
14
+ value: ModernI18nContextValue;
15
+ }
16
+ export declare const ModernI18nProvider: FC<ModernI18nProviderProps>;
17
+ export interface UseModernI18nReturn {
18
+ language: string;
19
+ changeLanguage: (newLang: string) => Promise<void>;
20
+ i18nInstance: I18nInstance;
21
+ supportedLanguages: string[];
22
+ isLanguageSupported: (lang: string) => boolean;
23
+ isResourcesReady: boolean;
24
+ }
25
+ /**
26
+ * Hook for accessing i18n functionality in Modern.js applications.
27
+ *
28
+ * This hook provides:
29
+ * - Current language from URL params or i18n context
30
+ * - changeLanguage function that updates both i18n instance and URL
31
+ * - Direct access to the i18n instance
32
+ * - List of supported languages
33
+ * - Helper function to check if a language is supported
34
+ *
35
+ * @param options - Optional configuration to override context settings
36
+ * @returns Object containing i18n functionality and utilities
37
+ */
38
+ export declare const useModernI18n: () => UseModernI18nReturn;
@@ -0,0 +1,28 @@
1
+ import type { TRuntimeContext } from '@modern-js/runtime';
2
+ import type React from 'react';
3
+ import type { I18nInstance } from './i18n';
4
+ interface RuntimeContextWithI18n extends TRuntimeContext {
5
+ i18nInstance?: I18nInstance;
6
+ }
7
+ export declare function createContextValue(lang: string, i18nInstance: I18nInstance | undefined, entryName: string | undefined, languages: string[], localePathRedirect: boolean, ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined, setLang: (lang: string) => void): {
8
+ language: string;
9
+ i18nInstance: I18nInstance;
10
+ entryName: string | undefined;
11
+ languages: string[];
12
+ localePathRedirect: boolean;
13
+ ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined;
14
+ updateLanguage: (lang: string) => void;
15
+ };
16
+ export declare function useSdkResourcesLoader(i18nInstance: I18nInstance | undefined, setForceUpdate: React.Dispatch<React.SetStateAction<number>>): void;
17
+ /**
18
+ * Hook to handle client-side redirect for locale path redirect in static deployments
19
+ * This ensures that when users access paths without language prefix, they are redirected
20
+ * to the localized version of the path
21
+ *
22
+ * Note: This hook only runs in CSR (Client-Side Rendering) scenarios.
23
+ * In SSR/SSG scenarios, server-side middleware handles redirects, so this hook is skipped.
24
+ * We use process.env.MODERN_TARGET to ensure this code is only included in browser bundles.
25
+ */
26
+ export declare function useClientSideRedirect(i18nInstance: I18nInstance | undefined, localePathRedirect: boolean, languages: string[], fallbackLanguage: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)): void;
27
+ export declare function useLanguageSync(i18nInstance: I18nInstance | undefined, localePathRedirect: boolean, languages: string[], runtimeContextRef: React.MutableRefObject<RuntimeContextWithI18n>, prevLangRef: React.MutableRefObject<string>, setLang: (lang: string) => void): void;
28
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { BackendOptions } from '../instance';
2
+ export declare function mergeBackendOptions(defaultOptions: BackendOptions, cliOptions?: BackendOptions, userOptions?: BackendOptions): BackendOptions;
@@ -0,0 +1,13 @@
1
+ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
2
+ loadPath: string;
3
+ addPath: string;
4
+ };
5
+ declare global {
6
+ interface Window {
7
+ __assetPrefix__?: string;
8
+ }
9
+ }
10
+ export declare function convertBackendOptions<T extends {
11
+ loadPath?: string;
12
+ addPath?: string;
13
+ }>(options: T): T;
@@ -0,0 +1,8 @@
1
+ export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
2
+ loadPath: string;
3
+ addPath: string;
4
+ };
5
+ export declare function convertBackendOptions<T extends {
6
+ loadPath?: string;
7
+ addPath?: string;
8
+ }>(options: T): T;
@@ -0,0 +1,3 @@
1
+ import type { BaseBackendOptions } from '../../../shared/type';
2
+ import type { BackendOptions, I18nInitOptions } from '../instance';
3
+ export declare const mergeBackendOptions: (backend?: BaseBackendOptions, userInitOptions?: I18nInitOptions) => BackendOptions | undefined;
@@ -0,0 +1,14 @@
1
+ import type { BaseBackendOptions, ChainedBackendConfig } from '../../../shared/type';
2
+ import type { I18nInstance } from '../instance';
3
+ type BackendConfigWithChained = BaseBackendOptions & Partial<ChainedBackendConfig>;
4
+ /**
5
+ * Common logic for using i18next backend
6
+ * This function handles the backend selection and chained backend configuration
7
+ *
8
+ * @param i18nInstance - The i18n instance to configure
9
+ * @param BackendWithSave - The wrapped backend class with save method (required for chained backend refresh logic)
10
+ * @param BackendBase - The base backend class (for non-chained use)
11
+ * @param backend - Optional backend configuration
12
+ */
13
+ export declare function useI18nextBackendCommon(i18nInstance: I18nInstance, BackendWithSave: new (...args: any[]) => any, BackendBase: new (...args: any[]) => any, backend?: BackendConfigWithChained): void;
14
+ export {};