@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.5 → 3.5.0-ultramodern.75

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 (222) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/cli/locales.js +15 -15
  3. package/dist/cjs/runtime/Link.js +8 -89
  4. package/dist/cjs/runtime/context.js +78 -91
  5. package/dist/cjs/runtime/contextHelpers.js +143 -0
  6. package/dist/cjs/runtime/core.js +32 -132
  7. package/dist/cjs/runtime/hooks.js +54 -21
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  9. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  10. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  11. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  12. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  13. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  14. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  15. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  16. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  17. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  18. package/dist/cjs/runtime/i18n/instance.js +34 -10
  19. package/dist/cjs/runtime/linkHelpers.js +148 -0
  20. package/dist/cjs/runtime/pluginSetup.js +102 -0
  21. package/dist/cjs/runtime/providerComposition.js +115 -0
  22. package/dist/cjs/runtime/reactI18next.js +43 -0
  23. package/dist/cjs/runtime/routerAdapter.js +16 -8
  24. package/dist/cjs/runtime/utils.js +1 -9
  25. package/dist/cjs/server/apiPrefix.js +72 -0
  26. package/dist/cjs/server/detectorOptions.js +97 -0
  27. package/dist/cjs/server/index.js +17 -144
  28. package/dist/cjs/server/redirectPolicy.js +96 -0
  29. package/dist/cjs/shared/detection.js +2 -104
  30. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  31. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  32. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  33. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  34. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  35. package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
  36. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  37. package/dist/cjs/shared/localisedUrls/routes.js +130 -0
  38. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  39. package/dist/cjs/shared/localisedUrls.js +28 -294
  40. package/dist/cjs/shared/utils.js +2 -8
  41. package/dist/esm/cli/index.mjs +2 -1
  42. package/dist/esm/cli/locales.mjs +13 -13
  43. package/dist/esm/runtime/Link.mjs +1 -82
  44. package/dist/esm/runtime/context.mjs +76 -92
  45. package/dist/esm/runtime/contextHelpers.mjs +87 -0
  46. package/dist/esm/runtime/core.mjs +30 -133
  47. package/dist/esm/runtime/hooks.mjs +56 -23
  48. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  49. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  50. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  51. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  52. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  54. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  55. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  56. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  57. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  58. package/dist/esm/runtime/i18n/instance.mjs +35 -8
  59. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  60. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  61. package/dist/esm/runtime/providerComposition.mjs +77 -0
  62. package/dist/esm/runtime/reactI18next.mjs +5 -0
  63. package/dist/esm/runtime/routerAdapter.mjs +16 -8
  64. package/dist/esm/runtime/utils.mjs +2 -10
  65. package/dist/esm/server/apiPrefix.mjs +31 -0
  66. package/dist/esm/server/detectorOptions.mjs +59 -0
  67. package/dist/esm/server/index.mjs +8 -135
  68. package/dist/esm/server/redirectPolicy.mjs +46 -0
  69. package/dist/esm/shared/detection.mjs +1 -71
  70. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  71. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  72. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  73. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  74. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  75. package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
  76. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  77. package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
  78. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  79. package/dist/esm/shared/localisedUrls.mjs +1 -283
  80. package/dist/esm/shared/utils.mjs +1 -1
  81. package/dist/esm-node/cli/index.mjs +2 -1
  82. package/dist/esm-node/cli/locales.mjs +13 -13
  83. package/dist/esm-node/runtime/Link.mjs +1 -82
  84. package/dist/esm-node/runtime/context.mjs +76 -92
  85. package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
  86. package/dist/esm-node/runtime/core.mjs +30 -133
  87. package/dist/esm-node/runtime/hooks.mjs +56 -23
  88. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  89. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  90. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  91. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  92. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  93. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  94. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  95. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  96. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  97. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  98. package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
  99. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  100. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  101. package/dist/esm-node/runtime/providerComposition.mjs +78 -0
  102. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  103. package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
  104. package/dist/esm-node/runtime/utils.mjs +2 -10
  105. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  106. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  107. package/dist/esm-node/server/index.mjs +8 -135
  108. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  109. package/dist/esm-node/shared/detection.mjs +1 -71
  110. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  111. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  112. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  113. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  114. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  115. package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
  116. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  117. package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
  118. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  119. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  120. package/dist/esm-node/shared/utils.mjs +1 -1
  121. package/dist/types/cli/locales.d.ts +1 -1
  122. package/dist/types/runtime/Link.d.ts +3 -7
  123. package/dist/types/runtime/context.d.ts +21 -5
  124. package/dist/types/runtime/contextHelpers.d.ts +29 -0
  125. package/dist/types/runtime/core.d.ts +4 -8
  126. package/dist/types/runtime/hooks.d.ts +3 -7
  127. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  128. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  129. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  130. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  131. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  132. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  133. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  134. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  135. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  136. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  137. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  138. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  139. package/dist/types/runtime/i18n/instance.d.ts +31 -27
  140. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  141. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  142. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  143. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  144. package/dist/types/runtime/providerComposition.d.ts +16 -0
  145. package/dist/types/runtime/reactI18next.d.ts +7 -0
  146. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  147. package/dist/types/runtime/utils.d.ts +7 -2
  148. package/dist/types/server/apiPrefix.d.ts +7 -0
  149. package/dist/types/server/detectorOptions.d.ts +21 -0
  150. package/dist/types/server/index.d.ts +1 -6
  151. package/dist/types/server/redirectPolicy.d.ts +22 -0
  152. package/dist/types/shared/detection.d.ts +1 -11
  153. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  154. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  155. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  156. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  157. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  158. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  159. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  160. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  161. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  162. package/dist/types/shared/localisedUrls.d.ts +2 -36
  163. package/dist/types/shared/utils.d.ts +0 -2
  164. package/package.json +17 -20
  165. package/rstest.config.mts +8 -1
  166. package/src/cli/index.ts +5 -4
  167. package/src/cli/locales.ts +3 -3
  168. package/src/runtime/Link.tsx +11 -138
  169. package/src/runtime/context.tsx +170 -207
  170. package/src/runtime/contextHelpers.ts +242 -0
  171. package/src/runtime/core.tsx +48 -275
  172. package/src/runtime/hooks.ts +86 -24
  173. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  174. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  175. package/src/runtime/i18n/detection/cache.ts +189 -0
  176. package/src/runtime/i18n/detection/detector.ts +166 -0
  177. package/src/runtime/i18n/detection/index.ts +10 -641
  178. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  179. package/src/runtime/i18n/detection/language.ts +64 -0
  180. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  181. package/src/runtime/i18n/detection/middleware.ts +15 -7
  182. package/src/runtime/i18n/detection/path.ts +41 -0
  183. package/src/runtime/i18n/detection/priority.ts +82 -0
  184. package/src/runtime/i18n/detection/ssr.ts +47 -0
  185. package/src/runtime/i18n/detection/types.ts +41 -0
  186. package/src/runtime/i18n/instance.ts +98 -34
  187. package/src/runtime/i18n/react-i18next.ts +1 -6
  188. package/src/runtime/i18n/utils.ts +18 -9
  189. package/src/runtime/linkHelpers.ts +174 -0
  190. package/src/runtime/pluginSetup.ts +189 -0
  191. package/src/runtime/providerComposition.tsx +148 -0
  192. package/src/runtime/reactI18next.ts +20 -0
  193. package/src/runtime/routerAdapter.tsx +36 -15
  194. package/src/runtime/utils.ts +13 -35
  195. package/src/server/apiPrefix.ts +67 -0
  196. package/src/server/detectorOptions.ts +105 -0
  197. package/src/server/index.ts +14 -319
  198. package/src/server/redirectPolicy.ts +127 -0
  199. package/src/shared/detection.ts +1 -1
  200. package/src/shared/localisedUrls/config.ts +18 -0
  201. package/src/shared/localisedUrls/index.ts +24 -0
  202. package/src/shared/localisedUrls/normalise.ts +67 -0
  203. package/src/shared/localisedUrls/pathname.ts +60 -0
  204. package/src/shared/localisedUrls/patterns.ts +160 -0
  205. package/src/shared/localisedUrls/redirect.ts +93 -0
  206. package/src/shared/localisedUrls/resolve.ts +135 -0
  207. package/src/shared/localisedUrls/routes.ts +234 -0
  208. package/src/shared/localisedUrls/types.ts +18 -0
  209. package/src/shared/localisedUrls.ts +26 -623
  210. package/src/shared/utils.ts +2 -2
  211. package/tests/federatedI18nBoundary.client.test.tsx +112 -0
  212. package/tests/federatedI18nBoundary.test.tsx +120 -0
  213. package/tests/fixtures/localised-routes.golden.json +156 -0
  214. package/tests/link.test.tsx +111 -0
  215. package/tests/linkTypes.test.ts +15 -4
  216. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  217. package/tests/localisedUrls.test.ts +148 -2
  218. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  219. package/tests/redirectPolicy.test.ts +45 -0
  220. package/tests/routerAdapter.test.tsx +603 -0
  221. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  222. package/tests/type-fixture/tsconfig.json +1 -0
@@ -1,14 +1,9 @@
1
1
  import type { ServerPlugin } from '@modern-js/server-runtime';
2
2
  import type { LocaleDetectionOptions } from '../shared/type';
3
+ export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
3
4
  export interface I18nPluginOptions {
4
5
  localeDetection: LocaleDetectionOptions;
5
6
  staticRoutePrefixes: string[];
6
7
  }
7
- type ApiPrefixInput = string | string[] | undefined;
8
- export declare const collectApiPrefixes: (routes: Array<{
9
- isApi?: boolean;
10
- urlPath?: string;
11
- }>, bffPrefix?: ApiPrefixInput) => string[];
12
- export declare const matchesApiPrefix: (pathname: string, apiPrefixes: string[]) => boolean;
13
8
  export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
14
9
  export default i18nServerPlugin;
@@ -0,0 +1,22 @@
1
+ import type { LocaleDetectionOptions } from '../shared/type';
2
+ interface LocaleRedirectRequest {
3
+ url: string;
4
+ header: () => {
5
+ host?: string;
6
+ };
7
+ }
8
+ /**
9
+ * Check if pathname should ignore automatic locale redirect.
10
+ */
11
+ export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
12
+ /**
13
+ * Check if pathname is a static or federation resource request.
14
+ *
15
+ * This includes configured staticRoutePrefixes, the shared default skip policy
16
+ * from ADR-0002, and language-prefixed variants such as /en/static/app.js.
17
+ */
18
+ export declare const isStaticResourceRequest: (pathname: string, staticRoutePrefixes: string[], languages?: string[]) => boolean;
19
+ export declare const getLanguageFromPath: (req: LocaleRedirectRequest, urlPath: string, languages: string[]) => string | null;
20
+ export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[], localisedUrls?: LocaleDetectionOptions['localisedUrls']) => string;
21
+ export declare const createLocaleRedirectResponse: (location: string) => Response;
22
+ export {};
@@ -1,11 +1 @@
1
- import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
2
- /**
3
- * Detect language from request using the same detection logic as i18next
4
- * This ensures consistency between server-side and client-side detection
5
- */
6
- export declare function detectLanguageFromRequest(req: {
7
- url: string;
8
- headers: {
9
- get: (name: string) => string | null;
10
- } | Headers;
11
- }, languages: string[], detectionOptions?: LanguageDetectorOptions): string | null;
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { LocalisedUrlsOption, ResolvedLocalisedUrlsConfig } from './types';
2
+ /**
3
+ * Localised URLs are strictly opt-in: only an explicit, non-empty map enables
4
+ * route expansion and validation. `true`, `false`, an empty map and absence
5
+ * all resolve to disabled, so upstream-style configs (`localePathRedirect` +
6
+ * `languages` without a map) keep plain locale-prefix behavior instead of
7
+ * failing the build for every route missing from a map they never wrote.
8
+ */
9
+ export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
@@ -0,0 +1,9 @@
1
+ export { resolveLocalisedUrlsConfig } from './config';
2
+ export { normalisePathname, normalisePathPattern } from './normalise';
3
+ export { canonicalTargetPathname, localiseTargetPathname, stripLanguagePrefix, } from './pathname';
4
+ export { buildPathFromPattern, matchPathPattern } from './patterns';
5
+ export type { LocaleRedirectSkipRule } from './redirect';
6
+ export { DEFAULT_LOCALE_REDIRECT_SKIP_RULES, isDefaultLocaleRedirectSkipPath, matchesPathPrefix, shouldSkipLocaleRedirect, } from './redirect';
7
+ export { resolveCanonicalLocalisedPath, resolveLocalisedPath } from './resolve';
8
+ export { applyLocalisedUrlsToRoutes, validateLocalisedUrls } from './routes';
9
+ export type { LocalisedRoute, LocalisedUrlPathMap, LocalisedUrlsMap, LocalisedUrlsOption, ResolvedLocalisedUrlsConfig, } from './types';
@@ -0,0 +1,10 @@
1
+ export declare const normalisePathPattern: (path: string) => string;
2
+ /**
3
+ * Normalise a concrete request pathname: slash cleanup only. Unlike
4
+ * {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
5
+ * `:x` params — pathnames are values, not patterns.
6
+ */
7
+ export declare const normalisePathname: (pathname: string) => string;
8
+ export declare const normaliseRoutePath: (path: string) => string;
9
+ export declare const stripLeadingLocaleParam: (path?: string) => string | undefined;
10
+ export declare const getLeadingLocaleParam: (path?: string) => string | null;
@@ -0,0 +1,4 @@
1
+ import type { LocalisedUrlsOption } from './types';
2
+ export declare const stripLanguagePrefix: (pathname: string, languages: string[]) => string;
3
+ export declare const localiseTargetPathname: (pathname: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
4
+ export declare const canonicalTargetPathname: (pathname: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
@@ -0,0 +1,5 @@
1
+ export declare const sortPatternsBySpecificity: <T extends {
2
+ pattern: string;
3
+ }>(patterns: T[]) => T[];
4
+ export declare const matchPathPattern: (pathname: string, pattern: string) => Record<string, string> | null;
5
+ export declare const buildPathFromPattern: (pattern: string, params: Record<string, string>) => string;
@@ -0,0 +1,11 @@
1
+ export type LocaleRedirectSkipRule = {
2
+ type: 'exact';
3
+ path: string;
4
+ } | {
5
+ type: 'prefix';
6
+ path: string;
7
+ };
8
+ export declare const DEFAULT_LOCALE_REDIRECT_SKIP_RULES: readonly LocaleRedirectSkipRule[];
9
+ export declare const matchesPathPrefix: (pathname: string, prefix: string) => boolean;
10
+ export declare const isDefaultLocaleRedirectSkipPath: (pathname: string, languages?: string[]) => boolean;
11
+ export declare const shouldSkipLocaleRedirect: (pathname: string, languages?: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
@@ -0,0 +1,7 @@
1
+ import type { LocalisedUrlsMap } from './types';
2
+ export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
3
+ /**
4
+ * Reverse-map language-specific pathnames back to language-agnostic
5
+ * LocalisedUrlsMap keys.
6
+ */
7
+ export declare const resolveCanonicalLocalisedPath: (pathname: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
@@ -0,0 +1,3 @@
1
+ import type { LocalisedRoute, LocalisedUrlsMap } from './types';
2
+ export declare const validateLocalisedUrls: (routes: LocalisedRoute[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
3
+ export declare const applyLocalisedUrlsToRoutes: (routes: LocalisedRoute[], languages: string[], localisedUrls: LocalisedUrlsMap) => LocalisedRoute[];
@@ -0,0 +1,14 @@
1
+ export type LocalisedRoute = {
2
+ type: 'nested' | 'page';
3
+ path?: string;
4
+ id?: string;
5
+ children?: LocalisedRoute[];
6
+ [key: string]: any;
7
+ };
8
+ export type LocalisedUrlPathMap = Record<string, string>;
9
+ export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
10
+ export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
11
+ export interface ResolvedLocalisedUrlsConfig {
12
+ enabled: boolean;
13
+ map: LocalisedUrlsMap;
14
+ }
@@ -1,36 +1,2 @@
1
- import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
2
- export type LocalisedUrlPathMap = Record<string, string>;
3
- export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
4
- export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
5
- export interface ResolvedLocalisedUrlsConfig {
6
- enabled: boolean;
7
- map: LocalisedUrlsMap;
8
- }
9
- export declare const normalisePathPattern: (path: string) => string;
10
- /**
11
- * Normalise a concrete request pathname: slash cleanup only. Unlike
12
- * {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
13
- * `:x` params — pathnames are values, not patterns.
14
- */
15
- export declare const normalisePathname: (pathname: string) => string;
16
- /**
17
- * Localised URLs are strictly opt-in: only an explicit, non-empty map enables
18
- * route expansion and validation. `true`, `false`, an empty map and absence
19
- * all resolve to disabled, so upstream-style configs (`localePathRedirect` +
20
- * `languages` without a map) keep plain locale-prefix behavior instead of
21
- * failing the build for every route missing from a map they never wrote.
22
- */
23
- export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
24
- export declare const validateLocalisedUrls: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
25
- export declare const applyLocalisedUrlsToRoutes: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => (NestedRouteForCli | PageRoute)[];
26
- export declare const matchPathPattern: (pathname: string, pattern: string) => Record<string, string> | null;
27
- export declare const buildPathFromPattern: (pattern: string, params: Record<string, string>) => string;
28
- export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
29
- /**
30
- * Reverse-map a language-specific pathname (without language prefix) back to
31
- * the canonical, language-agnostic path: localized slug patterns are matched
32
- * against every language variant and rebuilt from the canonical map key.
33
- */
34
- export declare const resolveCanonicalLocalisedPath: (pathname: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
35
- export declare const localiseTargetPathname: (pathname: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
36
- export declare const canonicalTargetPathname: (pathname: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
1
+ export type { LocaleRedirectSkipRule, LocalisedRoute, LocalisedUrlPathMap, LocalisedUrlsMap, LocalisedUrlsOption, ResolvedLocalisedUrlsConfig, } from './localisedUrls/index';
2
+ export { applyLocalisedUrlsToRoutes, buildPathFromPattern, canonicalTargetPathname, DEFAULT_LOCALE_REDIRECT_SKIP_RULES, isDefaultLocaleRedirectSkipPath, localiseTargetPathname, matchesPathPrefix, matchPathPattern, normalisePathname, normalisePathPattern, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, shouldSkipLocaleRedirect, stripLanguagePrefix, validateLocalisedUrls, } from './localisedUrls/index';
@@ -1,5 +1,3 @@
1
1
  import type { BaseBackendOptions, BaseLocaleDetectionOptions } from './type';
2
- export declare function getEntryConfig<T extends Record<string, any>>(entryName: string, config: T, entryKey: string): T | undefined;
3
- export declare function removeEntryConfigKey<T extends Record<string, any>>(config: T, entryKey: string): Omit<T, typeof entryKey>;
4
2
  export declare function getLocaleDetectionOptions(entryName: string, localeDetection: BaseLocaleDetectionOptions): BaseLocaleDetectionOptions;
5
3
  export declare function getBackendOptions(entryName: string, backend: BaseBackendOptions): BaseBackendOptions;
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.5.0-ultramodern.5",
20
+ "version": "3.5.0-ultramodern.75",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -91,25 +91,24 @@
91
91
  }
92
92
  },
93
93
  "dependencies": {
94
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.75",
95
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.75",
96
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.75",
97
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.75",
98
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.75",
94
99
  "@swc/helpers": "^0.5.23",
95
100
  "i18next-browser-languagedetector": "^8.2.1",
96
101
  "i18next-chained-backend": "^5.0.5",
97
102
  "i18next-fs-backend": "^2.6.6",
98
103
  "i18next-http-backend": "^4.0.0",
99
- "i18next-http-middleware": "^3.9.7",
100
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.5",
101
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.5",
102
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.5",
103
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.5",
104
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.5",
105
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.5"
104
+ "i18next-http-middleware": "^3.9.7"
106
105
  },
107
106
  "peerDependencies": {
108
- "@modern-js/runtime": "3.5.0-ultramodern.5",
107
+ "@modern-js/runtime": "3.5.0-ultramodern.75",
109
108
  "i18next": ">=26.3.1",
110
109
  "react": "^19.2.7",
111
110
  "react-dom": "^19.2.7",
112
- "react-i18next": "^17.0.8"
111
+ "react-i18next": "^17.0.9"
113
112
  },
114
113
  "peerDependenciesMeta": {
115
114
  "i18next": {
@@ -120,21 +119,19 @@
120
119
  }
121
120
  },
122
121
  "devDependencies": {
123
- "@rslib/core": "0.23.1",
124
- "@types/node": "^26.0.1",
125
- "@typescript/native-preview": "7.0.0-dev.20260628.1",
126
- "i18next": "26.3.3",
122
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.75",
123
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.75",
124
+ "@rslib/core": "0.23.2",
125
+ "@types/node": "^26.1.1",
126
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
127
+ "i18next": "26.3.6",
127
128
  "react": "^19.2.7",
128
129
  "react-dom": "^19.2.7",
129
- "react-i18next": "17.0.8",
130
- "ts-node": "^10.9.2",
131
- "typescript": "^6.0.3",
132
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.5",
133
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.5"
130
+ "react-i18next": "17.0.9",
131
+ "typescript": "^7.0.2"
134
132
  },
135
133
  "sideEffects": false,
136
134
  "publishConfig": {
137
- "registry": "https://registry.npmjs.org/",
138
135
  "access": "public"
139
136
  },
140
137
  "scripts": {
package/rstest.config.mts CHANGED
@@ -29,16 +29,23 @@ export default {
29
29
  include: [
30
30
  'tests/i18nUtils.test.ts',
31
31
  'tests/localisedUrls.test.ts',
32
+ 'tests/localisedUrlRewriteMatrix.fork.test.ts',
32
33
  'tests/linkTypes.test.ts',
33
34
  'tests/backendDefaults.test.ts',
35
+ 'tests/redirectPolicy.test.ts',
34
36
  'tests/reactI18nextRuntimeBoundary.test.ts',
37
+ 'tests/federatedI18nBoundary.test.tsx',
35
38
  ],
36
39
  extends: commonConfig,
37
40
  }),
38
41
  withTestPreset({
39
42
  name: 'plugin-i18n-client',
40
43
  testEnvironment: 'happy-dom',
41
- include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
44
+ include: [
45
+ 'tests/routerAdapter.test.tsx',
46
+ 'tests/link.test.tsx',
47
+ 'tests/federatedI18nBoundary.client.test.tsx',
48
+ ],
42
49
  extends: commonConfig,
43
50
  }),
44
51
  ],
package/src/cli/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
4
  import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
3
5
  import type {
@@ -5,8 +7,6 @@ import type {
5
7
  NestedRouteForCli,
6
8
  PageRoute,
7
9
  } from '@modern-js/types';
8
- import fs from 'fs';
9
- import path from 'path';
10
10
  import {
11
11
  applyLocalisedUrlsToRoutes,
12
12
  resolveLocalisedUrlsConfig,
@@ -126,9 +126,10 @@ export const i18nPlugin = (
126
126
  backend: backendOptions,
127
127
  ...extendedConfig,
128
128
  };
129
+ const { reactI18next } = extendedConfig as { reactI18next?: boolean };
129
130
  const runtimePluginPath =
130
131
  customPlugin?.runtime?.path ||
131
- (config.reactI18next === false
132
+ (reactI18next === false
132
133
  ? `@${metaName}/plugin-i18n/runtime/no-react-i18next`
133
134
  : `@${metaName}/plugin-i18n/runtime`);
134
135
 
@@ -173,7 +174,7 @@ export const i18nPlugin = (
173
174
  routes as (NestedRouteForCli | PageRoute)[],
174
175
  languages,
175
176
  localisedUrlsConfig.map,
176
- ),
177
+ ) as (NestedRouteForCli | PageRoute)[],
177
178
  };
178
179
  });
179
180
 
@@ -1,10 +1,10 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import {
2
4
  getPublicDirRoutePrefixes,
3
5
  normalizePublicDir,
4
6
  normalizePublicDirPath,
5
7
  } from '@modern-js/server-core';
6
- import fs from 'fs';
7
- import path from 'path';
8
8
  import type { BackendOptions } from '../shared/type';
9
9
 
10
10
  interface NormalizedConfigForLocales {
@@ -13,7 +13,7 @@ interface NormalizedConfigForLocales {
13
13
  };
14
14
  }
15
15
 
16
- export interface DetectedLocalesDirectory {
16
+ interface DetectedLocalesDirectory {
17
17
  loadPath: string;
18
18
  addPath: string;
19
19
  serverLoadPath: string;
@@ -6,96 +6,22 @@ import type {
6
6
  ValidateLinkTo,
7
7
  } from './canonicalRoutes';
8
8
  import { useModernI18n } from './context';
9
+ import {
10
+ interpolateRouteParams,
11
+ type LinkParams,
12
+ mergeClassNames,
13
+ normalizeSearch,
14
+ splitActiveProps,
15
+ warnOnce,
16
+ } from './linkHelpers';
9
17
  import { canonicalPath, type LocalizedPathsConfig } from './localizedPaths';
10
18
  import { useI18nRouterAdapter } from './routerAdapter';
11
19
  import { buildLocalizedUrl, splitUrlTarget } from './utils';
12
20
 
13
- const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
14
-
15
- const warnedTargets = new Set<string>();
16
-
17
- const warnOnce = (key: string, message: string) => {
18
- if (process.env.NODE_ENV !== 'development' || warnedTargets.has(key)) {
19
- return;
20
- }
21
- warnedTargets.add(key);
22
- console.warn(message);
23
- };
24
-
25
- export type LinkParams = Record<string, string | number | undefined>;
26
-
27
- /**
28
- * Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
29
- * (`$` / `*`) segments with concrete values before localization, so
30
- * pattern-mapped slugs localize correctly.
31
- */
32
- export const interpolateRouteParams = (
33
- pathname: string,
34
- params?: LinkParams,
35
- ): string => {
36
- if (!/[$:*{]/.test(pathname)) {
37
- return pathname;
38
- }
39
-
40
- const resolveParam = (name: string): string | undefined => {
41
- const value = params?.[name];
42
- return value === undefined ? undefined : String(value);
43
- };
44
-
45
- const segments = pathname
46
- .split('/')
47
- .map(segment => {
48
- if (!segment) {
49
- return segment;
50
- }
51
-
52
- if (segment.startsWith('{-$') && segment.endsWith('}')) {
53
- const value = resolveParam(segment.slice(3, -1));
54
- return value === undefined ? null : encodeURIComponent(value);
55
- }
56
-
57
- if (segment === '$' || segment === '*') {
58
- const value = resolveParam('_splat') ?? resolveParam('*');
59
- return value === undefined
60
- ? null
61
- : value.split('/').map(encodeURIComponent).join('/');
62
- }
63
-
64
- if (segment.startsWith('$')) {
65
- const value = resolveParam(segment.slice(1));
66
- if (value === undefined) {
67
- warnOnce(
68
- `missing-param:${pathname}:${segment}`,
69
- `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`,
70
- );
71
- return segment;
72
- }
73
- return encodeURIComponent(value);
74
- }
75
-
76
- if (segment.startsWith(':')) {
77
- const optional = segment.endsWith('?');
78
- const name = segment.slice(1, optional ? -1 : undefined);
79
- const value = resolveParam(name);
80
- if (value === undefined) {
81
- if (optional) {
82
- return null;
83
- }
84
- warnOnce(
85
- `missing-param:${pathname}:${segment}`,
86
- `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`,
87
- );
88
- return segment;
89
- }
90
- return encodeURIComponent(value);
91
- }
21
+ export type { LinkParams };
22
+ export { interpolateRouteParams };
92
23
 
93
- return segment;
94
- })
95
- .filter(segment => segment !== null);
96
-
97
- return segments.join('/') || '/';
98
- };
24
+ const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
99
25
 
100
26
  export interface LinkActiveOptions {
101
27
  /**
@@ -142,59 +68,6 @@ export type LinkProps<TTo extends string = string> = LinkBaseProps & {
142
68
  } & ValidateLinkTo<TTo> &
143
69
  LinkParamsProp<LinkTargetPathname<TTo>>;
144
70
 
145
- const normalizeSearch = (
146
- search: string | Record<string, unknown> | undefined,
147
- searchFromTo: string,
148
- ): {
149
- searchString: string;
150
- searchObject: Record<string, string> | undefined;
151
- } => {
152
- if (search && typeof search === 'object') {
153
- const entries = Object.entries(search).filter(
154
- ([, value]) => value !== undefined && value !== null,
155
- );
156
- const searchObject = Object.fromEntries(
157
- entries.map(([key, value]) => [key, String(value)]),
158
- );
159
- const params = new URLSearchParams(searchObject);
160
- const serialized = params.toString();
161
- return {
162
- searchString: serialized ? `?${serialized}` : '',
163
- searchObject,
164
- };
165
- }
166
-
167
- const raw = typeof search === 'string' && search ? search : searchFromTo;
168
- if (!raw) {
169
- return { searchString: '', searchObject: undefined };
170
- }
171
-
172
- const searchString = raw.startsWith('?') ? raw : `?${raw}`;
173
- const searchObject: Record<string, string> = {};
174
- new URLSearchParams(searchString).forEach((value, key) => {
175
- searchObject[key] = value;
176
- });
177
-
178
- return { searchString, searchObject };
179
- };
180
-
181
- const splitActiveProps = (
182
- active: boolean,
183
- activeProps?: LinkBaseProps['activeProps'],
184
- ) => {
185
- if (!active || !activeProps) {
186
- return {};
187
- }
188
- return activeProps;
189
- };
190
-
191
- const mergeClassNames = (...values: Array<unknown>): string | undefined => {
192
- const classNames = values.filter(
193
- (value): value is string => typeof value === 'string' && value.length > 0,
194
- );
195
- return classNames.length > 0 ? classNames.join(' ') : undefined;
196
- };
197
-
198
71
  /**
199
72
  * The standard UltraModern link: a vanilla link in every respect except that
200
73
  * it localizes canonical, language-agnostic paths automatically.