@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6
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 +0 -22
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +26 -61
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/core.js +17 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +35 -140
- package/dist/cjs/runtime/utils.js +17 -19
- package/dist/cjs/server/index.js +10 -7
- package/dist/cjs/server/redirectPolicy.js +36 -31
- package/dist/cjs/shared/urlStrategy.js +18 -0
- package/dist/esm/cli/index.mjs +0 -22
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +18 -62
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/core.mjs +8 -5
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +30 -141
- package/dist/esm/runtime/utils.mjs +15 -17
- package/dist/esm/server/index.mjs +10 -7
- package/dist/esm/server/redirectPolicy.mjs +29 -24
- package/dist/esm/shared/urlStrategy.mjs +0 -0
- package/dist/esm-node/cli/index.mjs +0 -22
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +18 -62
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/core.mjs +8 -5
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
- package/dist/esm-node/runtime/utils.mjs +15 -17
- package/dist/esm-node/server/index.mjs +10 -7
- package/dist/esm-node/server/redirectPolicy.mjs +29 -24
- package/dist/esm-node/shared/urlStrategy.mjs +1 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/core.d.ts +11 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/type.d.ts +0 -14
- package/dist/types/shared/urlStrategy.d.ts +7 -0
- package/package.json +21 -11
- package/rstest.config.mts +2 -8
- package/src/cli/index.ts +1 -43
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +34 -133
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/core.tsx +26 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +73 -338
- package/src/runtime/utils.ts +40 -31
- package/src/server/index.ts +44 -9
- package/src/server/redirectPolicy.ts +70 -70
- package/src/shared/type.ts +0 -14
- package/src/shared/urlStrategy.ts +11 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/linkTypes.test.ts +12 -3
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
- package/tests/localisedUrls.test.ts +7 -216
- package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
- package/tests/redirectPolicy.test.ts +39 -30
- package/tests/routerAdapter.test.tsx +95 -1055
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/peerCohort.test.ts +0 -25
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
|
|
2
3
|
import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
|
|
3
4
|
import { changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
|
|
4
|
-
import { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
5
5
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
6
|
const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
|
|
7
7
|
const reactI18nextProviderContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext');
|
|
@@ -22,68 +22,23 @@ const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
|
|
|
22
22
|
children: content
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
initImmediate: false,
|
|
37
|
-
lng: parent.language,
|
|
38
|
-
ns: [
|
|
39
|
-
defaultNamespace
|
|
40
|
-
],
|
|
41
|
-
resources,
|
|
42
|
-
supportedLngs: languages
|
|
43
|
-
});
|
|
44
|
-
if (!clone) throw new Error('FederatedI18nBoundary requires an i18n instance with cloneInstance support');
|
|
45
|
-
const resourceStore = clone.store;
|
|
46
|
-
const parentResourceStore = parentInstance.store;
|
|
47
|
-
const sharesNestedResourceState = Object.entries(resourceStore?.data ?? {}).some(([language, namespaces])=>{
|
|
48
|
-
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
49
|
-
return namespaces === parentNamespaces || Object.entries(namespaces).some(([namespace, resource])=>null !== resource && 'object' == typeof resource && resource === parentNamespaces?.[namespace]);
|
|
50
|
-
});
|
|
51
|
-
if (clone === parentInstance || resourceStore === parentResourceStore || resourceStore?.data === parentResourceStore?.data || sharesNestedResourceState) throw new Error('FederatedI18nBoundary cloneInstance did not isolate the host resource store');
|
|
52
|
-
if (!resourceStore?.addResourceBundle || !clone.removeResourceBundle || !resourceStore.data) throw new Error('FederatedI18nBoundary requires an isolated mutable i18n resource store');
|
|
53
|
-
for (const [language, namespaces] of Object.entries(resourceStore.data))for (const namespace of Object.keys(namespaces))clone.removeResourceBundle(language, namespace);
|
|
54
|
-
for (const [language, namespaces] of Object.entries(resources))for (const [namespace, resource] of Object.entries(namespaces))resourceStore.addResourceBundle(language, namespace, resource, true, true);
|
|
55
|
-
return clone;
|
|
56
|
-
}, [
|
|
57
|
-
defaultNamespace,
|
|
58
|
-
fallbackLanguage,
|
|
59
|
-
languages,
|
|
60
|
-
parent.i18nInstance,
|
|
61
|
-
parent.language,
|
|
62
|
-
resources
|
|
63
|
-
]);
|
|
64
|
-
const value = useMemo(()=>({
|
|
65
|
-
...parent,
|
|
66
|
-
i18nInstance: scopedInstance,
|
|
67
|
-
language: parent.language,
|
|
68
|
-
languages
|
|
69
|
-
}), [
|
|
70
|
-
languages,
|
|
71
|
-
parent,
|
|
72
|
-
scopedInstance
|
|
73
|
-
]);
|
|
74
|
-
const scopedContent = /*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
75
|
-
value: value,
|
|
76
|
-
children: children
|
|
25
|
+
const MissingFederatedI18nBoundary = ()=>{
|
|
26
|
+
throw new Error('FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin');
|
|
27
|
+
};
|
|
28
|
+
const FederatedI18nBoundary = (props)=>{
|
|
29
|
+
const resolveComponent = useContext(RuntimeComponentResolverContext);
|
|
30
|
+
const Boundary = resolveComponent?.(MissingFederatedI18nBoundary, {
|
|
31
|
+
name: 'i18n.FederatedI18nBoundary'
|
|
32
|
+
}) ?? MissingFederatedI18nBoundary;
|
|
33
|
+
if (Boundary === FederatedI18nBoundary) throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
|
|
34
|
+
return /*#__PURE__*/ jsx(Boundary, {
|
|
35
|
+
...props
|
|
77
36
|
});
|
|
78
|
-
return I18nextProvider ? /*#__PURE__*/ jsx(I18nextProvider, {
|
|
79
|
-
i18n: scopedInstance,
|
|
80
|
-
children: scopedContent
|
|
81
|
-
}) : scopedContent;
|
|
82
37
|
};
|
|
83
38
|
const useModernI18n = ()=>{
|
|
84
39
|
const context = useContext(ModernI18nContext);
|
|
85
40
|
if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
|
|
86
|
-
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
41
|
+
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, urlStrategy, updateLanguage, synchronizeLanguage } = context;
|
|
87
42
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
88
43
|
const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
|
|
89
44
|
languages,
|
|
@@ -102,7 +57,7 @@ const useModernI18n = ()=>{
|
|
|
102
57
|
updateLanguage,
|
|
103
58
|
localePathRedirect,
|
|
104
59
|
ignoreRedirectRoutes,
|
|
105
|
-
|
|
60
|
+
urlStrategy,
|
|
106
61
|
languages,
|
|
107
62
|
hasRouter,
|
|
108
63
|
navigate,
|
|
@@ -112,7 +67,7 @@ const useModernI18n = ()=>{
|
|
|
112
67
|
updateLanguage,
|
|
113
68
|
localePathRedirect,
|
|
114
69
|
ignoreRedirectRoutes,
|
|
115
|
-
|
|
70
|
+
urlStrategy,
|
|
116
71
|
languages,
|
|
117
72
|
hasRouter,
|
|
118
73
|
navigate,
|
|
@@ -135,9 +90,10 @@ const useModernI18n = ()=>{
|
|
|
135
90
|
t,
|
|
136
91
|
i18nInstance: i18nInstance,
|
|
137
92
|
supportedLanguages: languages || [],
|
|
138
|
-
|
|
93
|
+
urlStrategy,
|
|
139
94
|
isLanguageSupported,
|
|
140
95
|
isResourcesReady
|
|
141
96
|
};
|
|
142
97
|
};
|
|
143
|
-
export {
|
|
98
|
+
export { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
99
|
+
export { FederatedI18nBoundary, ModernI18nContext, ModernI18nProvider, ReactI18nextProviderContext, useModernI18n };
|
|
@@ -17,7 +17,7 @@ async function changeI18nInstanceLanguage(i18nInstance, language) {
|
|
|
17
17
|
else await i18nInstance.changeLanguage?.(language);
|
|
18
18
|
}
|
|
19
19
|
async function changeModernI18nLanguage(newLang, options) {
|
|
20
|
-
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes,
|
|
20
|
+
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, urlStrategy, languages, hasRouter, navigate, location } = options;
|
|
21
21
|
try {
|
|
22
22
|
if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
|
|
23
23
|
await changeI18nInstanceLanguage(i18nInstance, newLang);
|
|
@@ -28,8 +28,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
28
28
|
const relativePath = currentPath.replace(entryPath, '');
|
|
29
29
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
30
30
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
31
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
32
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
31
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
32
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
33
33
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
34
34
|
await navigate(newUrl, {
|
|
35
35
|
replace: true
|
|
@@ -41,8 +41,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
41
41
|
const relativePath = currentPath.replace(entryPath, '');
|
|
42
42
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
43
43
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
44
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
45
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
44
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
45
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
46
46
|
const newUrl = entryPath + newPath + window.location.search + window.location.hash;
|
|
47
47
|
window.history.pushState(null, '', newUrl);
|
|
48
48
|
}
|
|
@@ -5,8 +5,8 @@ import "./types.mjs";
|
|
|
5
5
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
6
6
|
name: '@modern-js/plugin-i18n',
|
|
7
7
|
setup: (api)=>{
|
|
8
|
-
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
9
|
-
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes
|
|
8
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
|
|
9
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
|
|
10
10
|
const { enabled: backendEnabled = false } = backend || {};
|
|
11
11
|
let latestI18nInstance;
|
|
12
12
|
let I18nextProvider;
|
|
@@ -35,15 +35,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
35
35
|
languages,
|
|
36
36
|
fallbackLanguage,
|
|
37
37
|
ignoreRedirectRoutes,
|
|
38
|
-
|
|
38
|
+
urlStrategy,
|
|
39
|
+
NavigationProvider,
|
|
40
|
+
LanguageSynchronization,
|
|
39
41
|
getLatestI18nInstance: ()=>latestI18nInstance,
|
|
40
42
|
getI18nextProvider: ()=>I18nextProvider
|
|
41
43
|
}));
|
|
42
44
|
}
|
|
43
45
|
});
|
|
46
|
+
export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
44
47
|
export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
|
|
45
48
|
export { I18nLink } from "./I18nLink.mjs";
|
|
46
49
|
export { Link } from "./Link.mjs";
|
|
47
|
-
export {
|
|
48
|
-
export {
|
|
50
|
+
export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
51
|
+
export { splitUrlTarget } from "./utils.mjs";
|
|
49
52
|
export { createI18nPlugin };
|
|
@@ -18,7 +18,7 @@ function createMinimalI18nInstance(language) {
|
|
|
18
18
|
};
|
|
19
19
|
return minimalInstance;
|
|
20
20
|
}
|
|
21
|
-
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
21
|
+
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy) {
|
|
22
22
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
23
23
|
return {
|
|
24
24
|
language: lang,
|
|
@@ -27,9 +27,8 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
|
|
|
27
27
|
languages,
|
|
28
28
|
localePathRedirect,
|
|
29
29
|
ignoreRedirectRoutes,
|
|
30
|
-
|
|
31
|
-
updateLanguage: setLang
|
|
32
|
-
synchronizeLanguage
|
|
30
|
+
urlStrategy,
|
|
31
|
+
updateLanguage: setLang
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
34
|
function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
@@ -78,7 +77,7 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
|
78
77
|
setForceUpdate
|
|
79
78
|
]);
|
|
80
79
|
}
|
|
81
|
-
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes,
|
|
80
|
+
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy) {
|
|
82
81
|
const urlRef = useRef('');
|
|
83
82
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
84
83
|
useEffect(()=>{
|
|
@@ -95,10 +94,10 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
95
94
|
const currentHash = hasRouter && location ? location.hash : window.location.hash;
|
|
96
95
|
const entryPath = getEntryPath();
|
|
97
96
|
const relativePath = currentPathname.replace(entryPath, '');
|
|
98
|
-
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) return;
|
|
97
|
+
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes, urlStrategy)) return;
|
|
99
98
|
const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
|
|
100
99
|
const targetLanguage = pathDetection.language || i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
101
|
-
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages,
|
|
100
|
+
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, urlStrategy);
|
|
102
101
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
103
102
|
urlRef.current = currentPathname;
|
|
104
103
|
if (newUrl !== currentPathname + currentSearch + currentHash) if (hasRouter && navigate && location) navigate(newUrl, {
|
|
@@ -114,7 +113,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
114
113
|
languages,
|
|
115
114
|
fallbackLanguage,
|
|
116
115
|
ignoreRedirectRoutes,
|
|
117
|
-
|
|
116
|
+
urlStrategy
|
|
118
117
|
]);
|
|
119
118
|
}
|
|
120
119
|
function useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang) {
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { canonicalTargetPathname } from "@modern-js/i18n-runtime-extensions";
|
|
2
1
|
import { useMemo } from "react";
|
|
3
2
|
import { useModernI18n } from "./context.mjs";
|
|
4
3
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
5
4
|
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
6
|
-
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.
|
|
5
|
+
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.urlStrategy);
|
|
7
6
|
const canonicalPath = (target, config)=>{
|
|
8
7
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
9
|
-
|
|
8
|
+
if (config.urlStrategy) return `${config.urlStrategy.canonicalPathname(pathname, config.languages)}${search}${hash}`;
|
|
9
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
10
|
+
if (segments[0] && config.languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
|
|
11
|
+
const resolvedPath = `/${segments.join('/')}`;
|
|
10
12
|
return `${resolvedPath}${search}${hash}`;
|
|
11
13
|
};
|
|
12
14
|
const useLocalizedPaths = ()=>{
|
|
13
|
-
const { supportedLanguages,
|
|
15
|
+
const { supportedLanguages, urlStrategy } = useModernI18n();
|
|
14
16
|
return useMemo(()=>{
|
|
15
17
|
const config = {
|
|
16
18
|
languages: supportedLanguages,
|
|
17
|
-
|
|
19
|
+
urlStrategy
|
|
18
20
|
};
|
|
19
21
|
return {
|
|
20
22
|
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
@@ -22,11 +24,11 @@ const useLocalizedPaths = ()=>{
|
|
|
22
24
|
};
|
|
23
25
|
}, [
|
|
24
26
|
supportedLanguages,
|
|
25
|
-
|
|
27
|
+
urlStrategy
|
|
26
28
|
]);
|
|
27
29
|
};
|
|
28
30
|
const useLocalizedLocation = ()=>{
|
|
29
|
-
const { language, supportedLanguages,
|
|
31
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
30
32
|
const { location } = useI18nRouterAdapter();
|
|
31
33
|
const pathname = location?.pathname ?? '/';
|
|
32
34
|
const search = location?.search ?? '';
|
|
@@ -34,7 +36,7 @@ const useLocalizedLocation = ()=>{
|
|
|
34
36
|
return useMemo(()=>{
|
|
35
37
|
const config = {
|
|
36
38
|
languages: supportedLanguages,
|
|
37
|
-
|
|
39
|
+
urlStrategy
|
|
38
40
|
};
|
|
39
41
|
const alternates = {};
|
|
40
42
|
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
@@ -46,7 +48,7 @@ const useLocalizedLocation = ()=>{
|
|
|
46
48
|
}, [
|
|
47
49
|
language,
|
|
48
50
|
supportedLanguages,
|
|
49
|
-
|
|
51
|
+
urlStrategy,
|
|
50
52
|
pathname,
|
|
51
53
|
search,
|
|
52
54
|
hash
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useLanguageSync } from "@modern-js/i18n-runtime-extensions";
|
|
3
2
|
import { RuntimeContext } from "@modern-js/runtime";
|
|
4
3
|
import { Helmet } from "@modern-js/runtime/head";
|
|
5
4
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
5
|
import { ModernI18nProvider } from "./context.mjs";
|
|
7
|
-
import { createContextValue, useClientSideRedirect, useSdkResourcesLoader } from "./hooks.mjs";
|
|
6
|
+
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
8
7
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
9
8
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const NativeLanguageSynchronization = ({ i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang, children })=>{
|
|
10
|
+
const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang);
|
|
11
|
+
return children(synchronizeLanguage);
|
|
12
|
+
};
|
|
13
|
+
const createI18nRootWrapper = (options)=>(App)=>{
|
|
14
|
+
const LanguageSynchronization = options.LanguageSynchronization ?? NativeLanguageSynchronization;
|
|
15
|
+
const I18nRoot = (props)=>{
|
|
16
|
+
const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy, getLatestI18nInstance, getI18nextProvider } = options;
|
|
12
17
|
const runtimeContext = useContext(RuntimeContext);
|
|
13
18
|
const i18nInstance = runtimeContext.i18nInstance || getLatestI18nInstance();
|
|
14
19
|
const initialLang = useMemo(()=>i18nInstance?.language || fallbackLanguage, [
|
|
@@ -33,18 +38,16 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
33
38
|
lang
|
|
34
39
|
]);
|
|
35
40
|
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage), [
|
|
41
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy);
|
|
42
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy), [
|
|
39
43
|
lang,
|
|
40
44
|
i18nInstance,
|
|
41
45
|
entryName,
|
|
42
46
|
languages,
|
|
43
47
|
localePathRedirect,
|
|
44
48
|
ignoreRedirectRoutes,
|
|
45
|
-
|
|
46
|
-
forceUpdate
|
|
47
|
-
synchronizeLanguage
|
|
49
|
+
urlStrategy,
|
|
50
|
+
forceUpdate
|
|
48
51
|
]);
|
|
49
52
|
const I18nextProvider = getI18nextProvider();
|
|
50
53
|
const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
|
|
@@ -67,12 +70,33 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
67
70
|
lang
|
|
68
71
|
}
|
|
69
72
|
}),
|
|
70
|
-
/*#__PURE__*/ jsx(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
/*#__PURE__*/ jsx(LanguageSynchronization, {
|
|
74
|
+
i18nInstance: i18nInstance,
|
|
75
|
+
localePathRedirect: localePathRedirect,
|
|
76
|
+
languages: languages,
|
|
77
|
+
pathname: location?.pathname,
|
|
78
|
+
prevLangRef: prevLangRef,
|
|
79
|
+
setLang: setLang,
|
|
80
|
+
children: (synchronizeLanguage)=>/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
81
|
+
i18nextProvider: I18nextProvider,
|
|
82
|
+
value: {
|
|
83
|
+
...contextValue,
|
|
84
|
+
synchronizeLanguage
|
|
85
|
+
},
|
|
86
|
+
children: appContent
|
|
87
|
+
})
|
|
74
88
|
})
|
|
75
89
|
]
|
|
76
90
|
});
|
|
77
91
|
};
|
|
92
|
+
const { NavigationProvider } = options;
|
|
93
|
+
if (!NavigationProvider) return I18nRoot;
|
|
94
|
+
return function(props) {
|
|
95
|
+
return /*#__PURE__*/ jsx(NavigationProvider, {
|
|
96
|
+
children: /*#__PURE__*/ jsx(I18nRoot, {
|
|
97
|
+
...props
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
};
|
|
78
102
|
export { createI18nRootWrapper };
|
|
@@ -1,150 +1,39 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
1
2
|
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
2
|
-
import { InternalRuntimeContext
|
|
3
|
-
import {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
pathname: locationValue.pathname,
|
|
14
|
-
search: normalizeUrlPart('string' == typeof locationValue.search ? locationValue.search : locationValue.searchStr, '?'),
|
|
15
|
-
hash: normalizeUrlPart(locationValue.hash, '#')
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
const getWindowLocation = ()=>{
|
|
19
|
-
if (!isBrowser()) return null;
|
|
20
|
-
return {
|
|
21
|
-
pathname: window.location.pathname,
|
|
22
|
-
search: window.location.search,
|
|
23
|
-
hash: window.location.hash
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
27
|
-
const framework = getRouterRuntimeState(internalContext)?.framework || getRouterRuntimeState(runtimeContext)?.framework;
|
|
28
|
-
if (framework) return framework;
|
|
29
|
-
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) return 'tanstack';
|
|
30
|
-
if (internalContext.router?.useLocation || internalContext.router?.useHref || runtimeContext.router?.useLocation || runtimeContext.router?.useHref) return 'react-router';
|
|
31
|
-
if (inReactRouter) return 'react-router';
|
|
32
|
-
};
|
|
33
|
-
const getRouterInstance = (internalContext, contextRouter)=>{
|
|
34
|
-
if (contextRouter) return contextRouter;
|
|
35
|
-
const router = getRouterRuntimeState(internalContext)?.instance;
|
|
36
|
-
if (!router || 'object' != typeof router) return null;
|
|
37
|
-
return router;
|
|
38
|
-
};
|
|
39
|
-
const getRouterStateLocation = (internalContext, contextRouter)=>{
|
|
40
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
41
|
-
return normalizeLocation(router?.stores?.location?.get?.()) || normalizeLocation(router?.state?.location);
|
|
42
|
-
};
|
|
43
|
-
const getRouterParams = (internalContext, contextRouter)=>{
|
|
44
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
45
|
-
const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
|
|
46
|
-
if (!Array.isArray(matches)) return {};
|
|
47
|
-
return matches.reduce((params, match)=>{
|
|
48
|
-
if (match?.params) Object.assign(params, match.params);
|
|
49
|
-
return params;
|
|
50
|
-
}, {});
|
|
51
|
-
};
|
|
52
|
-
const getRouterSnapshot = (internalContext, contextRouter)=>{
|
|
53
|
-
const location = getRouterStateLocation(internalContext, contextRouter);
|
|
54
|
-
const params = getRouterParams(internalContext, contextRouter);
|
|
55
|
-
return JSON.stringify([
|
|
56
|
-
location?.pathname ?? '',
|
|
57
|
-
location?.search ?? '',
|
|
58
|
-
location?.hash ?? '',
|
|
59
|
-
Object.entries(params).sort(([left], [right])=>left.localeCompare(right))
|
|
60
|
-
]);
|
|
61
|
-
};
|
|
62
|
-
const useI18nRouterAdapter = ()=>{
|
|
3
|
+
import { InternalRuntimeContext } from "@modern-js/runtime/context";
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
const navigationContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/I18nNavigationContext');
|
|
6
|
+
const contextStore = globalThis;
|
|
7
|
+
const I18nNavigationContext = contextStore[navigationContextKey] ??= /*#__PURE__*/ createContext(null);
|
|
8
|
+
const I18nNavigationProvider = ({ children, value })=>/*#__PURE__*/ jsx(I18nNavigationContext.Provider, {
|
|
9
|
+
value: value,
|
|
10
|
+
children: children
|
|
11
|
+
});
|
|
12
|
+
const useNativeI18nRouterAdapter = ()=>{
|
|
63
13
|
const runtimeContext = useContext(RuntimeContext);
|
|
64
14
|
const internalContext = useContext(InternalRuntimeContext);
|
|
65
15
|
const routerApi = internalContext.router || runtimeContext.router;
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
|
|
76
|
-
const subscribeToRouter = useCallback((update)=>{
|
|
77
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
78
|
-
if (!router) return ()=>void 0;
|
|
79
|
-
const unsubscribers = [];
|
|
80
|
-
if ('react-router' === framework && !inReactRouter && 'function' == typeof router.subscribe) {
|
|
81
|
-
const subscribe = router.subscribe;
|
|
82
|
-
const unsubscribe = subscribe.call(router, update);
|
|
83
|
-
if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
|
|
84
|
-
}
|
|
85
|
-
if ('tanstack' === framework && 'function' == typeof router.stores?.location?.subscribe) {
|
|
86
|
-
const unsubscribe = router.stores.location.subscribe(update);
|
|
87
|
-
if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
|
|
88
|
-
}
|
|
89
|
-
if ('tanstack' === framework && 'function' == typeof router.subscribe) {
|
|
90
|
-
const subscribe = router.subscribe;
|
|
91
|
-
for (const eventType of [
|
|
92
|
-
'onBeforeNavigate',
|
|
93
|
-
'onBeforeLoad'
|
|
94
|
-
]){
|
|
95
|
-
const unsubscribe = subscribe.call(router, eventType, update);
|
|
96
|
-
if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return ()=>{
|
|
100
|
-
for (const unsubscribe of unsubscribers)unsubscribe();
|
|
101
|
-
};
|
|
102
|
-
}, [
|
|
103
|
-
contextRouter,
|
|
104
|
-
framework,
|
|
105
|
-
inReactRouter,
|
|
106
|
-
internalContext
|
|
107
|
-
]);
|
|
108
|
-
const getSnapshot = useCallback(()=>getRouterSnapshot(internalContext, contextRouter), [
|
|
109
|
-
contextRouter,
|
|
110
|
-
internalContext
|
|
111
|
-
]);
|
|
112
|
-
useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
|
|
113
|
-
const navigate = useCallback((href, options)=>{
|
|
114
|
-
const router = getRouterInstance(internalContext, contextRouter);
|
|
115
|
-
const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
|
|
116
|
-
if ('tanstack' === activeFramework) {
|
|
117
|
-
if ('function' == typeof router?.navigate) return router.navigate({
|
|
118
|
-
to: href,
|
|
119
|
-
replace: options?.replace,
|
|
120
|
-
...options?.state === void 0 ? {} : {
|
|
121
|
-
state: options.state
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
throw new Error('TanStack router instance is not available.');
|
|
125
|
-
}
|
|
126
|
-
if (reactRouterNavigate) return reactRouterNavigate(href, options);
|
|
127
|
-
if ('react-router' === activeFramework) {
|
|
128
|
-
if ('function' == typeof router?.navigate) return router.navigate(href, options);
|
|
129
|
-
throw new Error('React Router instance is not available.');
|
|
130
|
-
}
|
|
131
|
-
}, [
|
|
132
|
-
contextRouter,
|
|
133
|
-
internalContext,
|
|
134
|
-
inReactRouter,
|
|
135
|
-
reactRouterNavigate,
|
|
136
|
-
runtimeContext
|
|
137
|
-
]);
|
|
138
|
-
const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
|
|
139
|
-
const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
|
|
140
|
-
const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? routerApi?.Link ?? null : null;
|
|
16
|
+
const hasRouter = routerApi?.useInRouterContext?.() ?? false;
|
|
17
|
+
const navigate = hasRouter ? routerApi?.useNavigate?.() ?? null : null;
|
|
18
|
+
const routerLocation = hasRouter ? routerApi?.useLocation?.() ?? null : null;
|
|
19
|
+
const params = hasRouter ? routerApi?.useParams?.() ?? {} : {};
|
|
20
|
+
const location = routerLocation ?? (isBrowser() ? {
|
|
21
|
+
pathname: window.location.pathname,
|
|
22
|
+
search: window.location.search,
|
|
23
|
+
hash: window.location.hash
|
|
24
|
+
} : null);
|
|
141
25
|
return {
|
|
142
|
-
framework,
|
|
26
|
+
framework: hasRouter ? 'react-router' : void 0,
|
|
143
27
|
hasRouter,
|
|
28
|
+
navigate,
|
|
144
29
|
location,
|
|
145
|
-
|
|
146
|
-
Link
|
|
147
|
-
params
|
|
30
|
+
params,
|
|
31
|
+
Link: hasRouter ? routerApi?.Link ?? null : null
|
|
148
32
|
};
|
|
149
33
|
};
|
|
150
|
-
|
|
34
|
+
const useI18nRouterAdapter = ()=>{
|
|
35
|
+
const provided = useContext(I18nNavigationContext);
|
|
36
|
+
const native = useNativeI18nRouterAdapter();
|
|
37
|
+
return provided ?? native;
|
|
38
|
+
};
|
|
39
|
+
export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { localiseTargetPathname, shouldSkipLocaleRedirect } from "@modern-js/i18n-runtime-extensions";
|
|
2
1
|
import { isBrowser } from "@modern-js/runtime";
|
|
3
2
|
import { getGlobalBasename } from "@modern-js/runtime/context";
|
|
3
|
+
import { splitUrlTarget } from "@modern-js/runtime-utils/url";
|
|
4
4
|
const getPathname = (context)=>{
|
|
5
5
|
if (isBrowser()) return window.location.pathname;
|
|
6
6
|
return context.ssrContext?.request?.pathname || '/';
|
|
@@ -16,22 +16,13 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
|
|
|
16
16
|
if (languages.includes(firstSegment)) return firstSegment;
|
|
17
17
|
return fallbackLanguage;
|
|
18
18
|
};
|
|
19
|
-
const
|
|
20
|
-
const hashIndex = target.indexOf('#');
|
|
21
|
-
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
22
|
-
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
23
|
-
const searchIndex = beforeHash.indexOf('?');
|
|
24
|
-
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
25
|
-
const pathname = searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
26
|
-
return {
|
|
27
|
-
pathname,
|
|
28
|
-
search,
|
|
29
|
-
hash
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
|
|
19
|
+
const buildLocalizedUrl = (target, language, languages, urlStrategy)=>{
|
|
33
20
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
34
|
-
|
|
21
|
+
if (urlStrategy) return `${urlStrategy.localizePathname(pathname, language, languages)}${search}${hash}`;
|
|
22
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
23
|
+
if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
|
|
24
|
+
else segments.unshift(language);
|
|
25
|
+
const localizedPathname = `/${segments.join('/')}`;
|
|
35
26
|
return `${localizedPathname}${search}${hash}`;
|
|
36
27
|
};
|
|
37
28
|
const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
@@ -51,5 +42,12 @@ const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
|
51
42
|
detected: false
|
|
52
43
|
};
|
|
53
44
|
};
|
|
54
|
-
const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes
|
|
45
|
+
const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes, urlStrategy)=>{
|
|
46
|
+
if (urlStrategy?.shouldSkipRedirect?.(pathname, languages)) return true;
|
|
47
|
+
if (!ignoreRedirectRoutes) return false;
|
|
48
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
49
|
+
if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments.shift();
|
|
50
|
+
const normalizedPath = `/${segments.join('/')}`;
|
|
51
|
+
return 'function' == typeof ignoreRedirectRoutes ? ignoreRedirectRoutes(normalizedPath) : ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
52
|
+
};
|
|
55
53
|
export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect, splitUrlTarget };
|