@bleedingdev/modern-js-plugin-i18n 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10

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 (143) hide show
  1. package/dist/cjs/cli/index.js +10 -21
  2. package/dist/cjs/cli/localisedRoutes.js +46 -0
  3. package/dist/cjs/runtime/Link.js +19 -32
  4. package/dist/cjs/runtime/context.js +26 -61
  5. package/dist/cjs/runtime/contextHelpers.js +5 -5
  6. package/dist/cjs/runtime/core.js +27 -14
  7. package/dist/cjs/runtime/hooks.js +7 -8
  8. package/dist/cjs/runtime/localizedPaths.js +11 -9
  9. package/dist/cjs/runtime/navigation.js +48 -0
  10. package/dist/cjs/runtime/providerComposition.js +37 -13
  11. package/dist/cjs/runtime/routerAdapter.js +35 -140
  12. package/dist/cjs/runtime/utils.js +21 -19
  13. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  14. package/dist/cjs/server/index.js +11 -7
  15. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  16. package/dist/cjs/server/redirectPolicy.js +33 -24
  17. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  18. package/dist/cjs/shared/urlStrategy.js +48 -0
  19. package/dist/esm/cli/index.mjs +10 -21
  20. package/dist/esm/cli/localisedRoutes.mjs +8 -0
  21. package/dist/esm/runtime/Link.mjs +19 -32
  22. package/dist/esm/runtime/context.mjs +18 -62
  23. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  24. package/dist/esm/runtime/core.mjs +13 -6
  25. package/dist/esm/runtime/hooks.mjs +7 -8
  26. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  27. package/dist/esm/runtime/navigation.mjs +7 -0
  28. package/dist/esm/runtime/providerComposition.mjs +38 -14
  29. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  30. package/dist/esm/runtime/utils.mjs +19 -17
  31. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  32. package/dist/esm/server/index.mjs +11 -7
  33. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  34. package/dist/esm/server/redirectPolicy.mjs +33 -24
  35. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  36. package/dist/esm/shared/urlStrategy.mjs +7 -0
  37. package/dist/esm-node/cli/index.mjs +10 -21
  38. package/dist/esm-node/cli/localisedRoutes.mjs +9 -0
  39. package/dist/esm-node/runtime/Link.mjs +19 -32
  40. package/dist/esm-node/runtime/context.mjs +18 -62
  41. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  42. package/dist/esm-node/runtime/core.mjs +13 -6
  43. package/dist/esm-node/runtime/hooks.mjs +7 -8
  44. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  45. package/dist/esm-node/runtime/navigation.mjs +8 -0
  46. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  47. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  48. package/dist/esm-node/runtime/utils.mjs +19 -17
  49. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  50. package/dist/esm-node/server/index.mjs +11 -7
  51. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  52. package/dist/esm-node/server/redirectPolicy.mjs +33 -24
  53. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  54. package/dist/esm-node/shared/urlStrategy.mjs +8 -0
  55. package/dist/types/cli/index.d.ts +2 -2
  56. package/dist/types/cli/locales.d.ts +1 -1
  57. package/dist/types/cli/localisedRoutes.d.ts +18 -0
  58. package/dist/types/runtime/Link.d.ts +2 -2
  59. package/dist/types/runtime/context.d.ts +10 -13
  60. package/dist/types/runtime/contextHelpers.d.ts +3 -3
  61. package/dist/types/runtime/core.d.ts +23 -13
  62. package/dist/types/runtime/hooks.d.ts +5 -6
  63. package/dist/types/runtime/i18n/backend/config.d.ts +1 -1
  64. package/dist/types/runtime/i18n/backend/index.d.ts +2 -2
  65. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -2
  66. package/dist/types/runtime/i18n/backend/middleware.d.ts +2 -2
  67. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +2 -2
  68. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +1 -1
  69. package/dist/types/runtime/i18n/detection/cache.d.ts +1 -1
  70. package/dist/types/runtime/i18n/detection/config.d.ts +1 -1
  71. package/dist/types/runtime/i18n/detection/detector.d.ts +2 -2
  72. package/dist/types/runtime/i18n/detection/index.d.ts +4 -4
  73. package/dist/types/runtime/i18n/detection/initOptions.d.ts +1 -1
  74. package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
  75. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -1
  76. package/dist/types/runtime/i18n/detection/priority.d.ts +2 -2
  77. package/dist/types/runtime/i18n/detection/types.d.ts +1 -1
  78. package/dist/types/runtime/i18n/index.d.ts +3 -3
  79. package/dist/types/runtime/i18n/instance.d.ts +1 -1
  80. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -1
  81. package/dist/types/runtime/i18n/utils.d.ts +2 -2
  82. package/dist/types/runtime/index.d.ts +2 -2
  83. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  84. package/dist/types/runtime/navigation.d.ts +10 -0
  85. package/dist/types/runtime/no-react-i18next.d.ts +2 -2
  86. package/dist/types/runtime/pluginSetup.d.ts +3 -3
  87. package/dist/types/runtime/providerComposition.d.ts +15 -3
  88. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  89. package/dist/types/runtime/types.d.ts +1 -1
  90. package/dist/types/runtime/utils.d.ts +4 -12
  91. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  92. package/dist/types/server/detectorOptions.d.ts +1 -1
  93. package/dist/types/server/index.d.ts +3 -1
  94. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  95. package/dist/types/server/redirectPolicy.d.ts +5 -13
  96. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  97. package/dist/types/shared/type.d.ts +8 -12
  98. package/dist/types/shared/urlStrategy.d.ts +18 -0
  99. package/dist/types/shared/utils.d.ts +1 -1
  100. package/package.json +30 -18
  101. package/rstest.config.mts +6 -5
  102. package/src/cli/index.ts +42 -43
  103. package/src/cli/localisedRoutes.ts +43 -0
  104. package/src/runtime/Link.tsx +20 -40
  105. package/src/runtime/context.tsx +34 -133
  106. package/src/runtime/contextHelpers.ts +7 -5
  107. package/src/runtime/core.tsx +53 -6
  108. package/src/runtime/hooks.ts +14 -9
  109. package/src/runtime/i18n/instance.ts +1 -2
  110. package/src/runtime/localizedPaths.ts +24 -19
  111. package/src/runtime/navigation.tsx +20 -0
  112. package/src/runtime/providerComposition.tsx +71 -23
  113. package/src/runtime/routerAdapter.tsx +73 -338
  114. package/src/runtime/utils.ts +49 -31
  115. package/src/server/defaultUrlStrategy.ts +27 -0
  116. package/src/server/index.ts +39 -9
  117. package/src/server/mappedUrlStrategy.ts +10 -0
  118. package/src/server/redirectPolicy.ts +85 -70
  119. package/src/shared/mappedUrlStrategy.ts +59 -0
  120. package/src/shared/type.ts +7 -11
  121. package/src/shared/urlStrategy.ts +38 -0
  122. package/tests/backendDefaults.test.ts +2 -23
  123. package/tests/defaultUrlStrategy.test.ts +107 -0
  124. package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
  125. package/tests/i18nUtils.test.ts +31 -65
  126. package/tests/link.test.tsx +13 -511
  127. package/tests/localisedRouteGeneration.test.ts +159 -0
  128. package/tests/localisedUrls.test.ts +16 -208
  129. package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
  130. package/tests/navigation.test.tsx +365 -0
  131. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  132. package/tests/redirectPolicy.test.ts +89 -27
  133. package/tests/routerAdapter.test.tsx +98 -1171
  134. package/tests/urlStrategySerialization.test.ts +122 -0
  135. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  136. package/tests/federatedI18nBoundary.test.tsx +0 -132
  137. package/tests/linkTypes.test.ts +0 -59
  138. package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
  139. package/tests/peerCohort.test.ts +0 -25
  140. package/tests/type-fixture/fixture-globals.d.ts +0 -11
  141. package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
  142. package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
  143. package/tests/type-fixture/tsconfig.json +0 -16
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
13
+ };
14
+ })();
15
+ (()=>{
16
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
17
+ })();
18
+ (()=>{
19
+ __webpack_require__.r = (exports1)=>{
20
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
21
+ value: 'Module'
22
+ });
23
+ Object.defineProperty(exports1, '__esModule', {
24
+ value: true
25
+ });
26
+ };
27
+ })();
28
+ var __webpack_exports__ = {};
29
+ __webpack_require__.r(__webpack_exports__);
30
+ const asI18nUrlStrategy = (candidate)=>{
31
+ if (!candidate || 'object' != typeof candidate) return;
32
+ const strategy = candidate;
33
+ return 'function' == typeof strategy.localizePathname && 'function' == typeof strategy.canonicalPathname ? strategy : void 0;
34
+ };
35
+ const isUnusableI18nUrlStrategy = (candidate)=>Boolean(candidate) && 'object' == typeof candidate && void 0 === asI18nUrlStrategy(candidate);
36
+ __webpack_require__.d(__webpack_exports__, {}, {
37
+ asI18nUrlStrategy: asI18nUrlStrategy,
38
+ isUnusableI18nUrlStrategy: isUnusableI18nUrlStrategy
39
+ });
40
+ exports.asI18nUrlStrategy = __webpack_exports__.asI18nUrlStrategy;
41
+ exports.isUnusableI18nUrlStrategy = __webpack_exports__.isUnusableI18nUrlStrategy;
42
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
43
+ "asI18nUrlStrategy",
44
+ "isUnusableI18nUrlStrategy"
45
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
46
+ Object.defineProperty(exports, '__esModule', {
47
+ value: true
48
+ });
@@ -1,7 +1,9 @@
1
- import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
2
1
  import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
2
+ import { logger } from "@modern-js/utils";
3
+ import { isUnusableI18nUrlStrategy } from "../shared/urlStrategy.mjs";
3
4
  import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
4
5
  import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
6
+ import { applyLocalisedRoutes } from "./localisedRoutes.mjs";
5
7
  import "../runtime/types.mjs";
6
8
  const i18nPlugin = (options = {})=>({
7
9
  name: '@modern-js/plugin-i18n',
@@ -31,6 +33,10 @@ const i18nPlugin = (options = {})=>({
31
33
  backend: backendOptions,
32
34
  ...extendedConfig
33
35
  };
36
+ if (isUnusableI18nUrlStrategy(config.urlStrategy)) {
37
+ delete config.urlStrategy;
38
+ 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.");
39
+ }
34
40
  const { reactI18next } = extendedConfig;
35
41
  const runtimePluginPath = customPlugin?.runtime?.path || (false === reactI18next ? `@${metaName}/plugin-i18n/runtime/no-react-i18next` : `@${metaName}/plugin-i18n/runtime`);
36
42
  plugins.push({
@@ -43,27 +49,10 @@ const i18nPlugin = (options = {})=>({
43
49
  plugins
44
50
  };
45
51
  });
46
- api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
47
- if (!localeDetection) return {
52
+ api.modifyFileSystemRoutes(({ entrypoint, routes })=>({
48
53
  entrypoint,
49
- routes
50
- };
51
- const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
52
- const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
53
- if (!localePathRedirect || 0 === languages.length) return {
54
- entrypoint,
55
- routes
56
- };
57
- const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
58
- if (!localisedUrlsConfig.enabled) return {
59
- entrypoint,
60
- routes
61
- };
62
- return {
63
- entrypoint,
64
- routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
65
- };
66
- });
54
+ routes: applyLocalisedRoutes(routes, localeDetection ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection) : void 0)
55
+ }));
67
56
  api._internalServerPlugins(({ plugins })=>{
68
57
  const { serverRoutes, metaName } = api.getAppContext();
69
58
  const normalizedConfig = api.getNormalizedConfig();
@@ -0,0 +1,8 @@
1
+ import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
2
+ const applyLocalisedRoutes = (routes, localeDetection)=>{
3
+ const { localePathRedirect, languages = [], localisedUrls } = localeDetection ?? {};
4
+ const resolved = resolveLocalisedUrlsConfig(localisedUrls);
5
+ if (!localePathRedirect || !languages.length || !resolved.enabled) return routes;
6
+ return applyLocalisedUrlsToRoutes(routes, languages, resolved.map);
7
+ };
8
+ export { applyLocalisedRoutes };
@@ -9,10 +9,10 @@ const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
9
9
  const Link = (props)=>{
10
10
  const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
11
11
  const adapter = useI18nRouterAdapter();
12
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
12
+ const { language, supportedLanguages, urlStrategy } = useModernI18n();
13
13
  const config = {
14
14
  languages: supportedLanguages,
15
- localisedUrls
15
+ urlStrategy
16
16
  };
17
17
  const isExternal = EXTERNAL_TARGET_RE.test(to);
18
18
  const isBareHash = to.startsWith('#');
@@ -22,7 +22,7 @@ const Link = (props)=>{
22
22
  const interpolated = interpolateRouteParams(pathname || '/', params);
23
23
  const firstSegment = interpolated.split('/').filter(Boolean)[0];
24
24
  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.`);
25
- const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, localisedUrls);
25
+ const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, urlStrategy);
26
26
  const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
27
27
  const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
28
28
  return {
@@ -42,7 +42,7 @@ const Link = (props)=>{
42
42
  isBareHash,
43
43
  language,
44
44
  supportedLanguages,
45
- localisedUrls
45
+ urlStrategy
46
46
  ]);
47
47
  const isActive = useMemo(()=>{
48
48
  if (!target || !adapter.location) return false;
@@ -57,7 +57,7 @@ const Link = (props)=>{
57
57
  adapter.location,
58
58
  activeOptions?.exact,
59
59
  supportedLanguages,
60
- localisedUrls
60
+ urlStrategy
61
61
  ]);
62
62
  const resolvedActiveProps = splitActiveProps(isActive, activeProps);
63
63
  const activeAttributes = isActive ? {
@@ -72,7 +72,7 @@ const Link = (props)=>{
72
72
  children: children
73
73
  });
74
74
  }
75
- const { Link: RouterLink, hasRouter, framework } = adapter;
75
+ const { Link: RouterLink, hasRouter } = adapter;
76
76
  if (!hasRouter || !RouterLink) {
77
77
  const { replace: _replace, ...anchorProps } = rest;
78
78
  const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
@@ -95,38 +95,25 @@ const Link = (props)=>{
95
95
  ...rest.style,
96
96
  ...activeStyle
97
97
  };
98
- if ('tanstack' === framework) {
99
- const tanstackPreload = void 0 !== preload ? preload : void 0 === prefetch ? void 0 : 'none' === prefetch ? false : prefetch;
100
- return /*#__PURE__*/ jsx(RouterLink, {
101
- to: target.localizedPathname,
102
- ...target.searchObject ? {
103
- search: target.searchObject
104
- } : {},
105
- ...target.hash ? {
106
- hash: target.hash
107
- } : {},
108
- ...void 0 === hashScrollIntoView ? {} : {
109
- hashScrollIntoView
110
- },
111
- ...void 0 === tanstackPreload ? {} : {
112
- preload: tanstackPreload
113
- },
114
- ...rest,
115
- ...activeRest,
116
- ...activeAttributes,
117
- className: mergedClassName,
118
- style: mergedStyle,
119
- children: children
120
- });
121
- }
122
- return /*#__PURE__*/ jsx(RouterLink, {
98
+ const routerProps = adapter.createLinkProps?.({
99
+ pathname: target.localizedPathname,
100
+ href: target.href,
101
+ search: target.searchObject,
102
+ hash: target.hash,
103
+ hashScrollIntoView,
104
+ prefetch,
105
+ preload
106
+ }) ?? {
123
107
  to: target.href,
124
108
  ...void 0 === prefetch ? {} : {
125
109
  prefetch
126
110
  },
127
111
  ...void 0 === preload ? {} : {
128
112
  preload
129
- },
113
+ }
114
+ };
115
+ return /*#__PURE__*/ jsx(RouterLink, {
116
+ ...routerProps,
130
117
  ...rest,
131
118
  ...activeRest,
132
119
  ...activeAttributes,
@@ -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 FederatedI18nBoundary = ({ children, defaultNamespace, fallbackLanguage, resources, supportedLanguages })=>{
26
- const parent = useContext(ModernI18nContext);
27
- const I18nextProvider = useContext(ReactI18nextProviderContext);
28
- if (!parent) throw new Error('FederatedI18nBoundary must be used within ModernI18nProvider');
29
- const languages = supportedLanguages ?? parent.languages ?? Object.keys(resources);
30
- const scopedInstance = useMemo(()=>{
31
- const parentInstance = getActualI18nextInstance(parent.i18nInstance);
32
- const clone = parentInstance.cloneInstance?.({
33
- defaultNS: defaultNamespace,
34
- fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
35
- forkResourceStore: true,
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, localisedUrls, updateLanguage, synchronizeLanguage } = context;
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
- localisedUrls,
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
- localisedUrls,
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
- localisedUrls,
93
+ urlStrategy,
139
94
  isLanguageSupported,
140
95
  isResourcesReady
141
96
  };
142
97
  };
143
- export { FederatedI18nBoundary, ModernI18nProvider, useModernI18n };
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, localisedUrls, languages, hasRouter, navigate, location } = options;
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 || [], localisedUrls);
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 || [], localisedUrls);
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
  }
@@ -1,3 +1,6 @@
1
+ import { resolveMappedUrlStrategy } from "../shared/mappedUrlStrategy.mjs";
2
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
3
+ import { I18nRouterNavigationProvider, useIntegratedRouterAdapter } from "./navigation.mjs";
1
4
  import { setupI18nBeforeRender } from "./pluginSetup.mjs";
2
5
  import { createI18nRootWrapper } from "./providerComposition.mjs";
3
6
  import { resolveReactI18nextIntegration } from "./reactI18next.mjs";
@@ -5,9 +8,10 @@ import "./types.mjs";
5
8
  const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
6
9
  name: '@modern-js/plugin-i18n',
7
10
  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, localisedUrls } = localeDetection || {};
11
+ const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
12
+ const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
10
13
  const { enabled: backendEnabled = false } = backend || {};
14
+ const resolvedUrlStrategy = asI18nUrlStrategy(urlStrategy) ?? resolveMappedUrlStrategy(localeDetection);
11
15
  let latestI18nInstance;
12
16
  let I18nextProvider;
13
17
  api.onBeforeRender(async (context)=>{
@@ -35,15 +39,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
35
39
  languages,
36
40
  fallbackLanguage,
37
41
  ignoreRedirectRoutes,
38
- localisedUrls,
42
+ urlStrategy: resolvedUrlStrategy,
43
+ NavigationProvider: NavigationProvider ?? I18nRouterNavigationProvider,
44
+ LanguageSynchronization,
39
45
  getLatestI18nInstance: ()=>latestI18nInstance,
40
46
  getI18nextProvider: ()=>I18nextProvider
41
47
  }));
42
48
  }
43
49
  });
50
+ export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
44
51
  export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
45
52
  export { I18nLink } from "./I18nLink.mjs";
46
53
  export { Link } from "./Link.mjs";
47
- export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
48
- export { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
49
- export { createI18nPlugin };
54
+ export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
55
+ export { splitUrlTarget } from "./utils.mjs";
56
+ export { I18nRouterNavigationProvider, createI18nPlugin, useIntegratedRouterAdapter };
@@ -18,7 +18,7 @@ function createMinimalI18nInstance(language) {
18
18
  };
19
19
  return minimalInstance;
20
20
  }
21
- function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage) {
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
- localisedUrls,
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, localisedUrls) {
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, localisedUrls);
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
- localisedUrls
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.localisedUrls);
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
- const resolvedPath = canonicalTargetPathname(pathname, config.languages, config.localisedUrls);
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, localisedUrls } = useModernI18n();
15
+ const { supportedLanguages, urlStrategy } = useModernI18n();
14
16
  return useMemo(()=>{
15
17
  const config = {
16
18
  languages: supportedLanguages,
17
- localisedUrls
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
- localisedUrls
27
+ urlStrategy
26
28
  ]);
27
29
  };
28
30
  const useLocalizedLocation = ()=>{
29
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
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
- localisedUrls
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
- localisedUrls,
51
+ urlStrategy,
50
52
  pathname,
51
53
  search,
52
54
  hash
@@ -0,0 +1,7 @@
1
+ import { createI18nRouterNavigation } from "@modern-js/i18n-runtime-extensions/router-navigation";
2
+ import { I18nNavigationProvider, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
3
+ const { I18nRouterNavigationProvider: I18nRouterNavigationProvider, useIntegratedRouterAdapter: useIntegratedRouterAdapter } = createI18nRouterNavigation({
4
+ I18nNavigationProvider: I18nNavigationProvider,
5
+ useNativeI18nRouterAdapter: useNativeI18nRouterAdapter
6
+ });
7
+ export { I18nRouterNavigationProvider, useIntegratedRouterAdapter };
@@ -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 createI18nRootWrapper = (options)=>(App)=>(props)=>{
11
- const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls, getLatestI18nInstance, getI18nextProvider } = options;
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
- const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, location?.pathname, prevLangRef, setLang);
37
- useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
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
- localisedUrls,
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(ModernI18nProvider, {
71
- i18nextProvider: I18nextProvider,
72
- value: contextValue,
73
- children: appContent
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 };