@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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 +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
|
|
3
2
|
import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
|
|
3
|
+
import { logger } from "@modern-js/utils";
|
|
4
|
+
import { isUnusableI18nUrlStrategy } from "../shared/urlStrategy.mjs";
|
|
4
5
|
import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
5
6
|
import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
|
|
7
|
+
import { applyLocalisedRoutes } from "./localisedRoutes.mjs";
|
|
6
8
|
import "../runtime/types.mjs";
|
|
7
9
|
const i18nPlugin = (options = {})=>({
|
|
8
10
|
name: '@modern-js/plugin-i18n',
|
|
@@ -32,6 +34,10 @@ const i18nPlugin = (options = {})=>({
|
|
|
32
34
|
backend: backendOptions,
|
|
33
35
|
...extendedConfig
|
|
34
36
|
};
|
|
37
|
+
if (isUnusableI18nUrlStrategy(config.urlStrategy)) {
|
|
38
|
+
delete config.urlStrategy;
|
|
39
|
+
logger.warn("[i18n] `urlStrategy` cannot be passed through `modern.config.ts`: the generated runtime registration carries plugin options as JSON, which drops its methods. Configure `localeDetection.localisedUrls` instead — the runtime and the server both derive the URL policy from it — or register a runtime plugin that supplies the strategy directly.");
|
|
40
|
+
}
|
|
35
41
|
const { reactI18next } = extendedConfig;
|
|
36
42
|
const runtimePluginPath = customPlugin?.runtime?.path || (false === reactI18next ? `@${metaName}/plugin-i18n/runtime/no-react-i18next` : `@${metaName}/plugin-i18n/runtime`);
|
|
37
43
|
plugins.push({
|
|
@@ -44,27 +50,10 @@ const i18nPlugin = (options = {})=>({
|
|
|
44
50
|
plugins
|
|
45
51
|
};
|
|
46
52
|
});
|
|
47
|
-
api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
|
|
48
|
-
if (!localeDetection) return {
|
|
53
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes })=>({
|
|
49
54
|
entrypoint,
|
|
50
|
-
routes
|
|
51
|
-
};
|
|
52
|
-
const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
|
|
53
|
-
const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
|
|
54
|
-
if (!localePathRedirect || 0 === languages.length) return {
|
|
55
|
-
entrypoint,
|
|
56
|
-
routes
|
|
57
|
-
};
|
|
58
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
59
|
-
if (!localisedUrlsConfig.enabled) return {
|
|
60
|
-
entrypoint,
|
|
61
|
-
routes
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
entrypoint,
|
|
65
|
-
routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
|
|
66
|
-
};
|
|
67
|
-
});
|
|
55
|
+
routes: applyLocalisedRoutes(routes, localeDetection ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection) : void 0, entrypoint)
|
|
56
|
+
}));
|
|
68
57
|
api._internalServerPlugins(({ plugins })=>{
|
|
69
58
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
70
59
|
const normalizedConfig = api.getNormalizedConfig();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
|
|
3
|
+
import { getEntrypointRoutesOwner } from "@modern-js/runtime/cli";
|
|
4
|
+
const applyLocalisedRoutes = (routes, localeDetection, entrypoint)=>{
|
|
5
|
+
const { localePathRedirect, languages = [], localisedUrls } = localeDetection ?? {};
|
|
6
|
+
const resolved = resolveLocalisedUrlsConfig(localisedUrls);
|
|
7
|
+
if (!localePathRedirect || !languages.length || !resolved.enabled) return routes;
|
|
8
|
+
return applyLocalisedUrlsToRoutes(routes, languages, resolved.map, entrypoint && '@modern-js/plugin-tanstack' === getEntrypointRoutesOwner(entrypoint) ? 'canonical' : 'physical');
|
|
9
|
+
};
|
|
10
|
+
export { applyLocalisedRoutes };
|
|
@@ -10,10 +10,10 @@ const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
|
10
10
|
const Link = (props)=>{
|
|
11
11
|
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
|
|
12
12
|
const adapter = useI18nRouterAdapter();
|
|
13
|
-
const { language, supportedLanguages,
|
|
13
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
14
14
|
const config = {
|
|
15
15
|
languages: supportedLanguages,
|
|
16
|
-
|
|
16
|
+
urlStrategy
|
|
17
17
|
};
|
|
18
18
|
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
19
19
|
const isBareHash = to.startsWith('#');
|
|
@@ -23,7 +23,7 @@ const Link = (props)=>{
|
|
|
23
23
|
const interpolated = interpolateRouteParams(pathname || '/', params);
|
|
24
24
|
const firstSegment = interpolated.split('/').filter(Boolean)[0];
|
|
25
25
|
if (firstSegment && supportedLanguages.includes(firstSegment)) warnOnce(`lang-prefix:${to}`, `[plugin-i18n] <Link to="${to}"> starts with a language prefix. Write language-agnostic canonical paths; the Link localizes them automatically.`);
|
|
26
|
-
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages,
|
|
26
|
+
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, urlStrategy);
|
|
27
27
|
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
28
28
|
const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
|
|
29
29
|
return {
|
|
@@ -43,7 +43,7 @@ const Link = (props)=>{
|
|
|
43
43
|
isBareHash,
|
|
44
44
|
language,
|
|
45
45
|
supportedLanguages,
|
|
46
|
-
|
|
46
|
+
urlStrategy
|
|
47
47
|
]);
|
|
48
48
|
const isActive = useMemo(()=>{
|
|
49
49
|
if (!target || !adapter.location) return false;
|
|
@@ -58,7 +58,7 @@ const Link = (props)=>{
|
|
|
58
58
|
adapter.location,
|
|
59
59
|
activeOptions?.exact,
|
|
60
60
|
supportedLanguages,
|
|
61
|
-
|
|
61
|
+
urlStrategy
|
|
62
62
|
]);
|
|
63
63
|
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
64
64
|
const activeAttributes = isActive ? {
|
|
@@ -73,7 +73,7 @@ const Link = (props)=>{
|
|
|
73
73
|
children: children
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
const { Link: RouterLink, hasRouter
|
|
76
|
+
const { Link: RouterLink, hasRouter } = adapter;
|
|
77
77
|
if (!hasRouter || !RouterLink) {
|
|
78
78
|
const { replace: _replace, ...anchorProps } = rest;
|
|
79
79
|
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
@@ -96,38 +96,25 @@ const Link = (props)=>{
|
|
|
96
96
|
...rest.style,
|
|
97
97
|
...activeStyle
|
|
98
98
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} : {},
|
|
109
|
-
...void 0 === hashScrollIntoView ? {} : {
|
|
110
|
-
hashScrollIntoView
|
|
111
|
-
},
|
|
112
|
-
...void 0 === tanstackPreload ? {} : {
|
|
113
|
-
preload: tanstackPreload
|
|
114
|
-
},
|
|
115
|
-
...rest,
|
|
116
|
-
...activeRest,
|
|
117
|
-
...activeAttributes,
|
|
118
|
-
className: mergedClassName,
|
|
119
|
-
style: mergedStyle,
|
|
120
|
-
children: children
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return /*#__PURE__*/ jsx(RouterLink, {
|
|
99
|
+
const routerProps = adapter.createLinkProps?.({
|
|
100
|
+
pathname: target.localizedPathname,
|
|
101
|
+
href: target.href,
|
|
102
|
+
search: target.searchObject,
|
|
103
|
+
hash: target.hash,
|
|
104
|
+
hashScrollIntoView,
|
|
105
|
+
prefetch,
|
|
106
|
+
preload
|
|
107
|
+
}) ?? {
|
|
124
108
|
to: target.href,
|
|
125
109
|
...void 0 === prefetch ? {} : {
|
|
126
110
|
prefetch
|
|
127
111
|
},
|
|
128
112
|
...void 0 === preload ? {} : {
|
|
129
113
|
preload
|
|
130
|
-
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
117
|
+
...routerProps,
|
|
131
118
|
...rest,
|
|
132
119
|
...activeRest,
|
|
133
120
|
...activeAttributes,
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
|
|
4
|
+
import { useCallback, useContext, useEffect, useMemo } from "react";
|
|
4
5
|
import { changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
|
|
5
|
-
import { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
6
6
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
|
-
|
|
8
|
-
const reactI18nextProviderContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext');
|
|
9
|
-
const getGlobalContext = (key, defaultValue)=>{
|
|
10
|
-
const globalStore = globalThis;
|
|
11
|
-
globalStore[key] ??= /*#__PURE__*/ createContext(defaultValue);
|
|
12
|
-
return globalStore[key];
|
|
13
|
-
};
|
|
14
|
-
const ModernI18nContext = getGlobalContext(modernI18nContextKey, null);
|
|
15
|
-
const ReactI18nextProviderContext = getGlobalContext(reactI18nextProviderContextKey, null);
|
|
7
|
+
import { ModernI18nContext, ReactI18nextProviderContext } from "./sharedContexts.mjs";
|
|
16
8
|
const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
|
|
17
9
|
const content = /*#__PURE__*/ jsx(ModernI18nContext.Provider, {
|
|
18
10
|
value: value,
|
|
@@ -23,68 +15,23 @@ const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
|
|
|
23
15
|
children: content
|
|
24
16
|
});
|
|
25
17
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
initImmediate: false,
|
|
38
|
-
lng: parent.language,
|
|
39
|
-
ns: [
|
|
40
|
-
defaultNamespace
|
|
41
|
-
],
|
|
42
|
-
resources,
|
|
43
|
-
supportedLngs: languages
|
|
44
|
-
});
|
|
45
|
-
if (!clone) throw new Error('FederatedI18nBoundary requires an i18n instance with cloneInstance support');
|
|
46
|
-
const resourceStore = clone.store;
|
|
47
|
-
const parentResourceStore = parentInstance.store;
|
|
48
|
-
const sharesNestedResourceState = Object.entries(resourceStore?.data ?? {}).some(([language, namespaces])=>{
|
|
49
|
-
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
50
|
-
return namespaces === parentNamespaces || Object.entries(namespaces).some(([namespace, resource])=>null !== resource && 'object' == typeof resource && resource === parentNamespaces?.[namespace]);
|
|
51
|
-
});
|
|
52
|
-
if (clone === parentInstance || resourceStore === parentResourceStore || resourceStore?.data === parentResourceStore?.data || sharesNestedResourceState) throw new Error('FederatedI18nBoundary cloneInstance did not isolate the host resource store');
|
|
53
|
-
if (!resourceStore?.addResourceBundle || !clone.removeResourceBundle || !resourceStore.data) throw new Error('FederatedI18nBoundary requires an isolated mutable i18n resource store');
|
|
54
|
-
for (const [language, namespaces] of Object.entries(resourceStore.data))for (const namespace of Object.keys(namespaces))clone.removeResourceBundle(language, namespace);
|
|
55
|
-
for (const [language, namespaces] of Object.entries(resources))for (const [namespace, resource] of Object.entries(namespaces))resourceStore.addResourceBundle(language, namespace, resource, true, true);
|
|
56
|
-
return clone;
|
|
57
|
-
}, [
|
|
58
|
-
defaultNamespace,
|
|
59
|
-
fallbackLanguage,
|
|
60
|
-
languages,
|
|
61
|
-
parent.i18nInstance,
|
|
62
|
-
parent.language,
|
|
63
|
-
resources
|
|
64
|
-
]);
|
|
65
|
-
const value = useMemo(()=>({
|
|
66
|
-
...parent,
|
|
67
|
-
i18nInstance: scopedInstance,
|
|
68
|
-
language: parent.language,
|
|
69
|
-
languages
|
|
70
|
-
}), [
|
|
71
|
-
languages,
|
|
72
|
-
parent,
|
|
73
|
-
scopedInstance
|
|
74
|
-
]);
|
|
75
|
-
const scopedContent = /*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
76
|
-
value: value,
|
|
77
|
-
children: children
|
|
18
|
+
const MissingFederatedI18nBoundary = ()=>{
|
|
19
|
+
throw new Error('FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin');
|
|
20
|
+
};
|
|
21
|
+
const FederatedI18nBoundary = (props)=>{
|
|
22
|
+
const resolveComponent = useContext(RuntimeComponentResolverContext);
|
|
23
|
+
const Boundary = resolveComponent?.(MissingFederatedI18nBoundary, {
|
|
24
|
+
name: 'i18n.FederatedI18nBoundary'
|
|
25
|
+
}) ?? MissingFederatedI18nBoundary;
|
|
26
|
+
if (Boundary === FederatedI18nBoundary) throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
|
|
27
|
+
return /*#__PURE__*/ jsx(Boundary, {
|
|
28
|
+
...props
|
|
78
29
|
});
|
|
79
|
-
return I18nextProvider ? /*#__PURE__*/ jsx(I18nextProvider, {
|
|
80
|
-
i18n: scopedInstance,
|
|
81
|
-
children: scopedContent
|
|
82
|
-
}) : scopedContent;
|
|
83
30
|
};
|
|
84
31
|
const useModernI18n = ()=>{
|
|
85
32
|
const context = useContext(ModernI18nContext);
|
|
86
33
|
if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
|
|
87
|
-
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
34
|
+
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, urlStrategy, updateLanguage, synchronizeLanguage } = context;
|
|
88
35
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
89
36
|
const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
|
|
90
37
|
languages,
|
|
@@ -103,7 +50,7 @@ const useModernI18n = ()=>{
|
|
|
103
50
|
updateLanguage,
|
|
104
51
|
localePathRedirect,
|
|
105
52
|
ignoreRedirectRoutes,
|
|
106
|
-
|
|
53
|
+
urlStrategy,
|
|
107
54
|
languages,
|
|
108
55
|
hasRouter,
|
|
109
56
|
navigate,
|
|
@@ -113,7 +60,7 @@ const useModernI18n = ()=>{
|
|
|
113
60
|
updateLanguage,
|
|
114
61
|
localePathRedirect,
|
|
115
62
|
ignoreRedirectRoutes,
|
|
116
|
-
|
|
63
|
+
urlStrategy,
|
|
117
64
|
languages,
|
|
118
65
|
hasRouter,
|
|
119
66
|
navigate,
|
|
@@ -136,9 +83,10 @@ const useModernI18n = ()=>{
|
|
|
136
83
|
t,
|
|
137
84
|
i18nInstance: i18nInstance,
|
|
138
85
|
supportedLanguages: languages || [],
|
|
139
|
-
|
|
86
|
+
urlStrategy,
|
|
140
87
|
isLanguageSupported,
|
|
141
88
|
isResourcesReady
|
|
142
89
|
};
|
|
143
90
|
};
|
|
144
|
-
export {
|
|
91
|
+
export { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
92
|
+
export { FederatedI18nBoundary, ModernI18nContext, ModernI18nProvider, ReactI18nextProviderContext, useModernI18n };
|
|
@@ -18,7 +18,7 @@ async function changeI18nInstanceLanguage(i18nInstance, language) {
|
|
|
18
18
|
else await i18nInstance.changeLanguage?.(language);
|
|
19
19
|
}
|
|
20
20
|
async function changeModernI18nLanguage(newLang, options) {
|
|
21
|
-
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes,
|
|
21
|
+
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, urlStrategy, languages, hasRouter, navigate, location } = options;
|
|
22
22
|
try {
|
|
23
23
|
if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
|
|
24
24
|
await changeI18nInstanceLanguage(i18nInstance, newLang);
|
|
@@ -29,8 +29,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
29
29
|
const relativePath = currentPath.replace(entryPath, '');
|
|
30
30
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
31
31
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
32
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
33
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
32
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
33
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
34
34
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
35
35
|
await navigate(newUrl, {
|
|
36
36
|
replace: true
|
|
@@ -42,8 +42,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
42
42
|
const relativePath = currentPath.replace(entryPath, '');
|
|
43
43
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
44
44
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
45
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
46
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
45
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
46
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
47
47
|
const newUrl = entryPath + newPath + window.location.search + window.location.hash;
|
|
48
48
|
window.history.pushState(null, '', newUrl);
|
|
49
49
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
const ModernI18nContext = createContext(null);
|
|
4
|
+
const ReactI18nextProviderContext = createContext(null);
|
|
5
|
+
const I18nNavigationContext = createContext(null);
|
|
6
|
+
export { I18nNavigationContext, ModernI18nContext, ReactI18nextProviderContext };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import { resolveMappedUrlStrategy } from "../shared/mappedUrlStrategy.mjs";
|
|
3
|
+
import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
|
|
4
|
+
import { I18nRouterNavigationProvider, useIntegratedRouterAdapter } from "./navigation.mjs";
|
|
2
5
|
import { setupI18nBeforeRender } from "./pluginSetup.mjs";
|
|
3
6
|
import { createI18nRootWrapper } from "./providerComposition.mjs";
|
|
4
7
|
import { resolveReactI18nextIntegration } from "./reactI18next.mjs";
|
|
@@ -6,9 +9,10 @@ import "./types.mjs";
|
|
|
6
9
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
7
10
|
name: '@modern-js/plugin-i18n',
|
|
8
11
|
setup: (api)=>{
|
|
9
|
-
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
10
|
-
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes
|
|
12
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
|
|
13
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
|
|
11
14
|
const { enabled: backendEnabled = false } = backend || {};
|
|
15
|
+
const resolvedUrlStrategy = asI18nUrlStrategy(urlStrategy) ?? resolveMappedUrlStrategy(localeDetection);
|
|
12
16
|
let latestI18nInstance;
|
|
13
17
|
let I18nextProvider;
|
|
14
18
|
api.onBeforeRender(async (context)=>{
|
|
@@ -36,15 +40,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
36
40
|
languages,
|
|
37
41
|
fallbackLanguage,
|
|
38
42
|
ignoreRedirectRoutes,
|
|
39
|
-
|
|
43
|
+
urlStrategy: resolvedUrlStrategy,
|
|
44
|
+
NavigationProvider: NavigationProvider ?? I18nRouterNavigationProvider,
|
|
45
|
+
LanguageSynchronization,
|
|
40
46
|
getLatestI18nInstance: ()=>latestI18nInstance,
|
|
41
47
|
getI18nextProvider: ()=>I18nextProvider
|
|
42
48
|
}));
|
|
43
49
|
}
|
|
44
50
|
});
|
|
51
|
+
export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
45
52
|
export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
|
|
46
53
|
export { I18nLink } from "./I18nLink.mjs";
|
|
47
54
|
export { Link } from "./Link.mjs";
|
|
48
|
-
export {
|
|
49
|
-
export {
|
|
50
|
-
export { createI18nPlugin };
|
|
55
|
+
export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
56
|
+
export { splitUrlTarget } from "./utils.mjs";
|
|
57
|
+
export { I18nRouterNavigationProvider, createI18nPlugin, useIntegratedRouterAdapter };
|
|
@@ -19,7 +19,7 @@ function createMinimalI18nInstance(language) {
|
|
|
19
19
|
};
|
|
20
20
|
return minimalInstance;
|
|
21
21
|
}
|
|
22
|
-
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
22
|
+
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy) {
|
|
23
23
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
24
24
|
return {
|
|
25
25
|
language: lang,
|
|
@@ -28,9 +28,8 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
|
|
|
28
28
|
languages,
|
|
29
29
|
localePathRedirect,
|
|
30
30
|
ignoreRedirectRoutes,
|
|
31
|
-
|
|
32
|
-
updateLanguage: setLang
|
|
33
|
-
synchronizeLanguage
|
|
31
|
+
urlStrategy,
|
|
32
|
+
updateLanguage: setLang
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
@@ -79,7 +78,7 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
|
79
78
|
setForceUpdate
|
|
80
79
|
]);
|
|
81
80
|
}
|
|
82
|
-
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes,
|
|
81
|
+
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy) {
|
|
83
82
|
const urlRef = useRef('');
|
|
84
83
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
85
84
|
useEffect(()=>{
|
|
@@ -96,10 +95,10 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
96
95
|
const currentHash = hasRouter && location ? location.hash : window.location.hash;
|
|
97
96
|
const entryPath = getEntryPath();
|
|
98
97
|
const relativePath = currentPathname.replace(entryPath, '');
|
|
99
|
-
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) return;
|
|
98
|
+
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes, urlStrategy)) return;
|
|
100
99
|
const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
|
|
101
100
|
const targetLanguage = pathDetection.language || i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
102
|
-
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages,
|
|
101
|
+
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, urlStrategy);
|
|
103
102
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
104
103
|
urlRef.current = currentPathname;
|
|
105
104
|
if (newUrl !== currentPathname + currentSearch + currentHash) if (hasRouter && navigate && location) navigate(newUrl, {
|
|
@@ -115,7 +114,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
115
114
|
languages,
|
|
116
115
|
fallbackLanguage,
|
|
117
116
|
ignoreRedirectRoutes,
|
|
118
|
-
|
|
117
|
+
urlStrategy
|
|
119
118
|
]);
|
|
120
119
|
}
|
|
121
120
|
function useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang) {
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import { canonicalTargetPathname } from "@modern-js/i18n-runtime-extensions";
|
|
3
2
|
import { useMemo } from "react";
|
|
4
3
|
import { useModernI18n } from "./context.mjs";
|
|
5
4
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
5
|
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
7
|
-
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.
|
|
6
|
+
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.urlStrategy);
|
|
8
7
|
const canonicalPath = (target, config)=>{
|
|
9
8
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
10
|
-
|
|
9
|
+
if (config.urlStrategy) return `${config.urlStrategy.canonicalPathname(pathname, config.languages)}${search}${hash}`;
|
|
10
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
11
|
+
if (segments[0] && config.languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
|
|
12
|
+
const resolvedPath = `/${segments.join('/')}`;
|
|
11
13
|
return `${resolvedPath}${search}${hash}`;
|
|
12
14
|
};
|
|
13
15
|
const useLocalizedPaths = ()=>{
|
|
14
|
-
const { supportedLanguages,
|
|
16
|
+
const { supportedLanguages, urlStrategy } = useModernI18n();
|
|
15
17
|
return useMemo(()=>{
|
|
16
18
|
const config = {
|
|
17
19
|
languages: supportedLanguages,
|
|
18
|
-
|
|
20
|
+
urlStrategy
|
|
19
21
|
};
|
|
20
22
|
return {
|
|
21
23
|
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
@@ -23,11 +25,11 @@ const useLocalizedPaths = ()=>{
|
|
|
23
25
|
};
|
|
24
26
|
}, [
|
|
25
27
|
supportedLanguages,
|
|
26
|
-
|
|
28
|
+
urlStrategy
|
|
27
29
|
]);
|
|
28
30
|
};
|
|
29
31
|
const useLocalizedLocation = ()=>{
|
|
30
|
-
const { language, supportedLanguages,
|
|
32
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
31
33
|
const { location } = useI18nRouterAdapter();
|
|
32
34
|
const pathname = location?.pathname ?? '/';
|
|
33
35
|
const search = location?.search ?? '';
|
|
@@ -35,7 +37,7 @@ const useLocalizedLocation = ()=>{
|
|
|
35
37
|
return useMemo(()=>{
|
|
36
38
|
const config = {
|
|
37
39
|
languages: supportedLanguages,
|
|
38
|
-
|
|
40
|
+
urlStrategy
|
|
39
41
|
};
|
|
40
42
|
const alternates = {};
|
|
41
43
|
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
@@ -47,7 +49,7 @@ const useLocalizedLocation = ()=>{
|
|
|
47
49
|
}, [
|
|
48
50
|
language,
|
|
49
51
|
supportedLanguages,
|
|
50
|
-
|
|
52
|
+
urlStrategy,
|
|
51
53
|
pathname,
|
|
52
54
|
search,
|
|
53
55
|
hash
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createI18nRouterNavigation } from "@modern-js/i18n-runtime-extensions/router-navigation";
|
|
3
|
+
import { I18nNavigationProvider, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
4
|
+
const { I18nRouterNavigationProvider: I18nRouterNavigationProvider, useIntegratedRouterAdapter: useIntegratedRouterAdapter } = createI18nRouterNavigation({
|
|
5
|
+
I18nNavigationProvider: I18nNavigationProvider,
|
|
6
|
+
useNativeI18nRouterAdapter: useNativeI18nRouterAdapter
|
|
7
|
+
});
|
|
8
|
+
export { I18nRouterNavigationProvider, useIntegratedRouterAdapter };
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useLanguageSync } from "@modern-js/i18n-runtime-extensions";
|
|
4
3
|
import { RuntimeContext } from "@modern-js/runtime";
|
|
5
4
|
import { Helmet } from "@modern-js/runtime/head";
|
|
6
5
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
6
|
import { ModernI18nProvider } from "./context.mjs";
|
|
8
|
-
import { createContextValue, useClientSideRedirect, useSdkResourcesLoader } from "./hooks.mjs";
|
|
7
|
+
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
9
8
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
10
9
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
11
|
-
const
|
|
12
|
-
|
|
10
|
+
const NativeLanguageSynchronization = ({ i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang, children })=>{
|
|
11
|
+
const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang);
|
|
12
|
+
return children(synchronizeLanguage);
|
|
13
|
+
};
|
|
14
|
+
const createI18nRootWrapper = (options)=>(App)=>{
|
|
15
|
+
const LanguageSynchronization = options.LanguageSynchronization ?? NativeLanguageSynchronization;
|
|
16
|
+
const I18nRoot = (props)=>{
|
|
17
|
+
const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy, getLatestI18nInstance, getI18nextProvider } = options;
|
|
13
18
|
const runtimeContext = useContext(RuntimeContext);
|
|
14
19
|
const i18nInstance = runtimeContext.i18nInstance || getLatestI18nInstance();
|
|
15
20
|
const initialLang = useMemo(()=>i18nInstance?.language || fallbackLanguage, [
|
|
@@ -34,18 +39,16 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
34
39
|
lang
|
|
35
40
|
]);
|
|
36
41
|
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage), [
|
|
42
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy);
|
|
43
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy), [
|
|
40
44
|
lang,
|
|
41
45
|
i18nInstance,
|
|
42
46
|
entryName,
|
|
43
47
|
languages,
|
|
44
48
|
localePathRedirect,
|
|
45
49
|
ignoreRedirectRoutes,
|
|
46
|
-
|
|
47
|
-
forceUpdate
|
|
48
|
-
synchronizeLanguage
|
|
50
|
+
urlStrategy,
|
|
51
|
+
forceUpdate
|
|
49
52
|
]);
|
|
50
53
|
const I18nextProvider = getI18nextProvider();
|
|
51
54
|
const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
|
|
@@ -68,12 +71,33 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
68
71
|
lang
|
|
69
72
|
}
|
|
70
73
|
}),
|
|
71
|
-
/*#__PURE__*/ jsx(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
/*#__PURE__*/ jsx(LanguageSynchronization, {
|
|
75
|
+
i18nInstance: i18nInstance,
|
|
76
|
+
localePathRedirect: localePathRedirect,
|
|
77
|
+
languages: languages,
|
|
78
|
+
pathname: location?.pathname,
|
|
79
|
+
prevLangRef: prevLangRef,
|
|
80
|
+
setLang: setLang,
|
|
81
|
+
children: (synchronizeLanguage)=>/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
82
|
+
i18nextProvider: I18nextProvider,
|
|
83
|
+
value: {
|
|
84
|
+
...contextValue,
|
|
85
|
+
synchronizeLanguage
|
|
86
|
+
},
|
|
87
|
+
children: appContent
|
|
88
|
+
})
|
|
75
89
|
})
|
|
76
90
|
]
|
|
77
91
|
});
|
|
78
92
|
};
|
|
93
|
+
const { NavigationProvider } = options;
|
|
94
|
+
if (!NavigationProvider) return I18nRoot;
|
|
95
|
+
return function(props) {
|
|
96
|
+
return /*#__PURE__*/ jsx(NavigationProvider, {
|
|
97
|
+
children: /*#__PURE__*/ jsx(I18nRoot, {
|
|
98
|
+
...props
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
};
|
|
79
103
|
export { createI18nRootWrapper };
|