@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.8 → 3.9.3-ultramodern.1

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 (106) hide show
  1. package/dist/cjs/cli/index.js +11 -0
  2. package/dist/cjs/cli/localisedRoutes.js +47 -0
  3. package/dist/cjs/runtime/context.js +6 -14
  4. package/dist/cjs/runtime/contexts.js +49 -0
  5. package/dist/cjs/runtime/core.js +12 -2
  6. package/dist/cjs/runtime/i18n/detection/middleware.js +1 -1
  7. package/dist/cjs/runtime/i18n/detection/middleware.node.js +10 -1
  8. package/dist/cjs/runtime/i18n/detection/priority.js +1 -1
  9. package/dist/cjs/runtime/navigation.js +48 -0
  10. package/dist/cjs/runtime/routerAdapter.js +3 -5
  11. package/dist/cjs/runtime/sharedContexts.js +43 -0
  12. package/dist/cjs/runtime/utils.js +6 -2
  13. package/dist/cjs/server/defaultUrlStrategy.js +48 -0
  14. package/dist/cjs/server/index.js +3 -2
  15. package/dist/cjs/server/mappedUrlStrategy.js +43 -0
  16. package/dist/cjs/server/redirectPolicy.js +13 -9
  17. package/dist/cjs/shared/detection.js +118 -2
  18. package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
  19. package/dist/cjs/shared/urlStrategy.js +31 -1
  20. package/dist/esm/cli/index.mjs +11 -0
  21. package/dist/esm/cli/localisedRoutes.mjs +9 -0
  22. package/dist/esm/runtime/context.mjs +2 -10
  23. package/dist/esm/runtime/contexts.mjs +5 -0
  24. package/dist/esm/runtime/core.mjs +7 -3
  25. package/dist/esm/runtime/i18n/detection/middleware.mjs +1 -1
  26. package/dist/esm/runtime/i18n/detection/middleware.node.mjs +10 -1
  27. package/dist/esm/runtime/i18n/detection/priority.mjs +1 -1
  28. package/dist/esm/runtime/navigation.mjs +7 -0
  29. package/dist/esm/runtime/routerAdapter.mjs +2 -4
  30. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  31. package/dist/esm/runtime/utils.mjs +6 -2
  32. package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
  33. package/dist/esm/server/index.mjs +3 -2
  34. package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
  35. package/dist/esm/server/redirectPolicy.mjs +6 -2
  36. package/dist/esm/shared/detection.mjs +85 -1
  37. package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
  38. package/dist/esm/shared/urlStrategy.mjs +7 -0
  39. package/dist/esm-node/cli/index.mjs +11 -0
  40. package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
  41. package/dist/esm-node/runtime/context.mjs +2 -10
  42. package/dist/esm-node/runtime/contexts.mjs +6 -0
  43. package/dist/esm-node/runtime/core.mjs +7 -3
  44. package/dist/esm-node/runtime/i18n/detection/middleware.mjs +1 -1
  45. package/dist/esm-node/runtime/i18n/detection/middleware.node.mjs +10 -1
  46. package/dist/esm-node/runtime/i18n/detection/priority.mjs +1 -1
  47. package/dist/esm-node/runtime/navigation.mjs +8 -0
  48. package/dist/esm-node/runtime/routerAdapter.mjs +2 -4
  49. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  50. package/dist/esm-node/runtime/utils.mjs +6 -2
  51. package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
  52. package/dist/esm-node/server/index.mjs +3 -2
  53. package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
  54. package/dist/esm-node/server/redirectPolicy.mjs +6 -2
  55. package/dist/esm-node/shared/detection.mjs +85 -1
  56. package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
  57. package/dist/esm-node/shared/urlStrategy.mjs +7 -0
  58. package/dist/types/cli/localisedRoutes.d.ts +19 -0
  59. package/dist/types/runtime/context.d.ts +2 -2
  60. package/dist/types/runtime/contexts.d.ts +6 -0
  61. package/dist/types/runtime/core.d.ts +2 -1
  62. package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
  63. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -5
  64. package/dist/types/runtime/navigation.d.ts +10 -0
  65. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  66. package/dist/types/runtime/utils.d.ts +1 -1
  67. package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
  68. package/dist/types/server/index.d.ts +1 -1
  69. package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
  70. package/dist/types/server/redirectPolicy.d.ts +1 -1
  71. package/dist/types/shared/detection.d.ts +11 -1
  72. package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
  73. package/dist/types/shared/type.d.ts +10 -0
  74. package/dist/types/shared/urlStrategy.d.ts +11 -0
  75. package/package.json +43 -24
  76. package/src/cli/index.ts +43 -1
  77. package/src/cli/localisedRoutes.ts +53 -0
  78. package/src/runtime/context.tsx +6 -31
  79. package/src/runtime/contexts.ts +16 -0
  80. package/src/runtime/core.tsx +30 -3
  81. package/src/runtime/i18n/detection/middleware.node.ts +25 -7
  82. package/src/runtime/i18n/detection/middleware.ts +2 -0
  83. package/src/runtime/i18n/detection/priority.ts +2 -0
  84. package/src/runtime/navigation.tsx +20 -0
  85. package/src/runtime/routerAdapter.tsx +2 -11
  86. package/src/runtime/sharedContexts.ts +8 -0
  87. package/src/runtime/utils.ts +13 -4
  88. package/src/server/defaultUrlStrategy.ts +27 -0
  89. package/src/server/index.ts +7 -12
  90. package/src/server/mappedUrlStrategy.ts +10 -0
  91. package/src/server/redirectPolicy.ts +19 -4
  92. package/src/shared/detection.ts +34 -10
  93. package/src/shared/mappedUrlStrategy.ts +59 -0
  94. package/src/shared/type.ts +10 -0
  95. package/src/shared/urlStrategy.ts +27 -0
  96. package/rslib.config.mts +0 -4
  97. package/rstest.config.mts +0 -46
  98. package/tests/backendDefaults.test.ts +0 -30
  99. package/tests/federatedI18nBoundaryResolver.test.tsx +0 -65
  100. package/tests/i18nUtils.test.ts +0 -60
  101. package/tests/link.test.tsx +0 -139
  102. package/tests/localisedUrls.test.ts +0 -176
  103. package/tests/reactI18nextRuntimeBoundary.test.ts +0 -117
  104. package/tests/redirectPolicy.test.ts +0 -54
  105. package/tests/routerAdapter.test.tsx +0 -221
  106. package/tsconfig.json +0 -15
@@ -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 };
@@ -1,18 +1,10 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
4
- import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
4
+ import { useCallback, useContext, useEffect, useMemo } from "react";
5
5
  import { changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
6
6
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
- const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
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,
@@ -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";
@@ -9,6 +12,7 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
9
12
  const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
10
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,8 +40,8 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
36
40
  languages,
37
41
  fallbackLanguage,
38
42
  ignoreRedirectRoutes,
39
- urlStrategy,
40
- NavigationProvider,
43
+ urlStrategy: resolvedUrlStrategy,
44
+ NavigationProvider: NavigationProvider ?? I18nRouterNavigationProvider,
41
45
  LanguageSynchronization,
42
46
  getLatestI18nInstance: ()=>latestI18nInstance,
43
47
  getI18nextProvider: ()=>I18nextProvider
@@ -50,4 +54,4 @@ export { I18nLink } from "./I18nLink.mjs";
50
54
  export { Link } from "./Link.mjs";
51
55
  export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
52
56
  export { splitUrlTarget } from "./utils.mjs";
53
- export { createI18nPlugin };
57
+ export { I18nRouterNavigationProvider, createI18nPlugin, useIntegratedRouterAdapter };
@@ -12,7 +12,7 @@ const useI18nextLanguageDetector = (i18nInstance)=>{
12
12
  }
13
13
  return i18nInstance;
14
14
  };
15
- const readLanguageFromStorage = (detectionOptions)=>{
15
+ const readLanguageFromStorage = (detectionOptions, _request, _languages)=>{
16
16
  try {
17
17
  const options = detectionOptions || {};
18
18
  const order = options.order || [
@@ -1,7 +1,16 @@
1
1
  import "node:module";
2
2
  import { LanguageDetector } from "i18next-http-middleware";
3
+ import { detectLanguageFromRequest } from "../../../shared/detection.mjs";
4
+ const isDetectionRequest = (request)=>{
5
+ if (!request || 'object' != typeof request) return false;
6
+ const { url, headers } = request;
7
+ return 'string' == typeof url && !!headers && 'object' == typeof headers;
8
+ };
3
9
  const cacheUserLanguage = (_i18nInstance, _language, _detectionOptions)=>{};
4
- const readLanguageFromStorage = (_detectionOptions)=>{};
10
+ const readLanguageFromStorage = (detectionOptions, request, languages = [])=>{
11
+ if (!isDetectionRequest(request)) return;
12
+ return detectLanguageFromRequest(request, languages, detectionOptions) ?? void 0;
13
+ };
5
14
  const useI18nextLanguageDetector = (i18nInstance)=>{
6
15
  if (!i18nInstance.isInitialized) return i18nInstance.use(LanguageDetector);
7
16
  return i18nInstance;
@@ -10,7 +10,7 @@ const detectLanguageWithPriority = async (i18nInstance, options)=>{
10
10
  let detectedLanguage;
11
11
  detectedLanguage = detectLanguageFromPathPriority(pathname, languages, localePathRedirect);
12
12
  if (!detectedLanguage) detectedLanguage = detectLanguageFromSSR(languages);
13
- if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions)) : await detectLanguageFromI18nextDetector(i18nInstance, {
13
+ if (!detectedLanguage && i18nextDetector) detectedLanguage = isI18nWrapperInstance(i18nInstance) ? readLanguageFromStorage(mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions), ssrContext?.request, languages) : await detectLanguageFromI18nextDetector(i18nInstance, {
14
14
  languages,
15
15
  fallbackLanguage,
16
16
  localePathRedirect,
@@ -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,11 +1,9 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { I18nNavigationContext } from "@modern-js/plugin-i18n/runtime/contexts";
3
4
  import { RuntimeContext, isBrowser } from "@modern-js/runtime";
4
5
  import { InternalRuntimeContext } from "@modern-js/runtime/context";
5
- import { createContext, useContext } from "react";
6
- const navigationContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/I18nNavigationContext');
7
- const contextStore = globalThis;
8
- const I18nNavigationContext = contextStore[navigationContextKey] ??= /*#__PURE__*/ createContext(null);
6
+ import { useContext } from "react";
9
7
  const I18nNavigationProvider = ({ children, value })=>/*#__PURE__*/ jsx(I18nNavigationContext.Provider, {
10
8
  value: value,
11
9
  children: children
@@ -0,0 +1,2 @@
1
+ import "node:module";
2
+ export { ModernI18nContext, ReactI18nextProviderContext } from "@modern-js/plugin-i18n/runtime/contexts";
@@ -2,6 +2,8 @@ import "node:module";
2
2
  import { isBrowser } from "@modern-js/runtime";
3
3
  import { getGlobalBasename } from "@modern-js/runtime/context";
4
4
  import { splitUrlTarget } from "@modern-js/runtime-utils/url";
5
+ import { isDefaultLocaleRedirectSkipPath } from "../shared/mappedUrlStrategy.mjs";
6
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
5
7
  const getPathname = (context)=>{
6
8
  if (isBrowser()) return window.location.pathname;
7
9
  return context.ssrContext?.request?.pathname || '/';
@@ -19,7 +21,8 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
19
21
  };
20
22
  const buildLocalizedUrl = (target, language, languages, urlStrategy)=>{
21
23
  const { pathname, search, hash } = splitUrlTarget(target);
22
- if (urlStrategy) return `${urlStrategy.localizePathname(pathname, language, languages)}${search}${hash}`;
24
+ const strategy = asI18nUrlStrategy(urlStrategy);
25
+ if (strategy) return `${strategy.localizePathname(pathname, language, languages)}${search}${hash}`;
23
26
  const segments = pathname.split('/').filter(Boolean);
24
27
  if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
25
28
  else segments.unshift(language);
@@ -44,7 +47,8 @@ const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
44
47
  };
45
48
  };
46
49
  const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes, urlStrategy)=>{
47
- if (urlStrategy?.shouldSkipRedirect?.(pathname, languages)) return true;
50
+ if (isDefaultLocaleRedirectSkipPath(pathname, languages)) return true;
51
+ if (asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(pathname, languages)) return true;
48
52
  if (!ignoreRedirectRoutes) return false;
49
53
  const segments = pathname.split('/').filter(Boolean);
50
54
  if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments.shift();
@@ -0,0 +1,8 @@
1
+ import "node:module";
2
+ import { createMappedUrlStrategy } from "./mappedUrlStrategy.mjs";
3
+ const createDefaultUrlStrategy = (localisedUrls)=>createMappedUrlStrategy(localisedUrls);
4
+ const resolveDefaultUrlStrategy = (localisedUrls)=>{
5
+ if (!localisedUrls || 'object' != typeof localisedUrls || 0 === Object.keys(localisedUrls).length) return;
6
+ return createDefaultUrlStrategy(localisedUrls);
7
+ };
8
+ export { createDefaultUrlStrategy, resolveDefaultUrlStrategy };
@@ -1,6 +1,8 @@
1
1
  import "node:module";
2
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
2
3
  import { getLocaleDetectionOptions } from "../shared/utils.mjs";
3
4
  import { collectApiPrefixes, matchesApiPrefix } from "./apiPrefix.mjs";
5
+ import { resolveDefaultUrlStrategy } from "./defaultUrlStrategy.mjs";
4
6
  import { convertToHonoLanguageDetectorOptions } from "./detectorOptions.mjs";
5
7
  import { buildLocalizedUrl, createLocaleRedirectResponse, getLanguageFromPath, isStaticResourceRequest, shouldIgnoreRedirect } from "./redirectPolicy.mjs";
6
8
  import * as __rspack_external__modern_js_server_core_hono_0af3baa7 from "@modern-js/server-core/hono";
@@ -27,8 +29,7 @@ const i18nServerPlugin = (options)=>({
27
29
  const entryLocaleDetection = getLocaleDetectionOptions(entryName, options.localeDetection);
28
30
  const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = entryLocaleDetection;
29
31
  const legacyLocalisedUrls = Reflect.get(entryLocaleDetection, 'localisedUrls');
30
- const urlStrategy = options.resolveUrlStrategy?.(entryName);
31
- if (legacyLocalisedUrls && 'object' == typeof legacyLocalisedUrls && Object.keys(legacyLocalisedUrls).length > 0 && !urlStrategy) throw new Error('Mapped locale URLs require a URL strategy. Use the i18n integration server plugin or supply resolveUrlStrategy.');
32
+ const urlStrategy = asI18nUrlStrategy(options.resolveUrlStrategy?.(entryName)) ?? resolveDefaultUrlStrategy(legacyLocalisedUrls);
32
33
  const staticRoutePrefixes = options.staticRoutePrefixes;
33
34
  const originUrlPath = route.urlPath;
34
35
  const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
@@ -0,0 +1,2 @@
1
+ import "node:module";
2
+ export { createMappedUrlStrategy, resolveMappedUrlStrategy } from "../shared/mappedUrlStrategy.mjs";
@@ -1,4 +1,6 @@
1
1
  import "node:module";
2
+ import { isDefaultLocaleRedirectSkipPath } from "../shared/mappedUrlStrategy.mjs";
3
+ import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
2
4
  const stripUrlPathPrefix = (pathname, urlPath)=>{
3
5
  const basePath = urlPath.replace('/*', '');
4
6
  if (!basePath || '/' === basePath) return pathname;
@@ -6,7 +8,8 @@ const stripUrlPathPrefix = (pathname, urlPath)=>{
6
8
  };
7
9
  const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages = [])=>{
8
10
  const remainingPath = stripUrlPathPrefix(pathname, urlPath);
9
- if (urlStrategy?.shouldSkipRedirect?.(remainingPath, languages)) return true;
11
+ if (isDefaultLocaleRedirectSkipPath(remainingPath, languages)) return true;
12
+ if (asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(remainingPath, languages)) return true;
10
13
  if (!ignoreRedirectRoutes) return false;
11
14
  const segments = remainingPath.split('/').filter(Boolean);
12
15
  if (segments[0] && languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
@@ -38,7 +41,8 @@ const buildLocalizedUrl = (req, urlPath, language, languages, urlStrategy)=>{
38
41
  const segments = remainingPath.split('/').filter(Boolean);
39
42
  if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
40
43
  else segments.unshift(language);
41
- const pathname = urlStrategy ? urlStrategy.localizePathname(remainingPath, language, languages) : `/${segments.join('/')}`;
44
+ const strategy = asI18nUrlStrategy(urlStrategy);
45
+ const pathname = strategy ? strategy.localizePathname(remainingPath, language, languages) : `/${segments.join('/')}`;
42
46
  return `${'/' === basePath ? '' : basePath}${pathname}${url.search}${url.hash}`;
43
47
  };
44
48
  const createLocaleRedirectResponse = (location)=>new Response(null, {
@@ -1,2 +1,86 @@
1
1
  import "node:module";
2
- import "../runtime/i18n/detection/config.mjs";
2
+ import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
3
+ function detectLanguageFromRequest(req, languages, detectionOptions) {
4
+ try {
5
+ const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
6
+ const order = (mergedDetection.order || []).filter((item)=>![
7
+ 'path',
8
+ 'localStorage',
9
+ 'navigator',
10
+ 'htmlTag',
11
+ 'subdomain'
12
+ ].includes(item));
13
+ const detectionOrder = order.length > 0 ? order : [
14
+ 'querystring',
15
+ 'cookie',
16
+ 'header'
17
+ ];
18
+ const getHeader = (name)=>{
19
+ const headers = req.headers;
20
+ if ('function' == typeof headers?.get) return headers.get(name);
21
+ const value = headers[name.toLowerCase()];
22
+ if (Array.isArray(value)) return value[0] ?? null;
23
+ return value ?? null;
24
+ };
25
+ for (const method of detectionOrder){
26
+ let detectedLang = null;
27
+ switch(method){
28
+ case 'querystring':
29
+ {
30
+ const lookupKey = mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng';
31
+ const host = getHeader('host') || 'localhost';
32
+ const url = new URL(req.url, `http://${host}`);
33
+ detectedLang = url.searchParams.get(lookupKey);
34
+ break;
35
+ }
36
+ case 'cookie':
37
+ {
38
+ const lookupKey = mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next';
39
+ const cookieHeader = getHeader('Cookie');
40
+ if (cookieHeader) {
41
+ const cookies = cookieHeader.split(';').reduce((acc, item)=>{
42
+ const [key, value] = item.trim().split('=');
43
+ if (key && value) acc[key] = value;
44
+ return acc;
45
+ }, {});
46
+ detectedLang = cookies[lookupKey] || null;
47
+ }
48
+ break;
49
+ }
50
+ case 'header':
51
+ {
52
+ const lookupKey = mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language';
53
+ const acceptLanguage = getHeader(lookupKey);
54
+ if (acceptLanguage) {
55
+ const languagesList = acceptLanguage.split(',').map((lang)=>{
56
+ const [code, q] = lang.trim().split(';');
57
+ return {
58
+ code,
59
+ baseCode: code.split('-')[0],
60
+ quality: q ? parseFloat(q.split('=')[1]) : 1.0
61
+ };
62
+ }).sort((a, b)=>b.quality - a.quality);
63
+ for (const lang of languagesList){
64
+ if (0 === languages.length || languages.includes(lang.code)) {
65
+ detectedLang = lang.code;
66
+ break;
67
+ }
68
+ if (languages.includes(lang.baseCode)) {
69
+ detectedLang = lang.baseCode;
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ break;
75
+ }
76
+ }
77
+ if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
78
+ if (detectedLang) {
79
+ const baseLang = detectedLang.split('-')[0];
80
+ if (languages.includes(baseLang)) return baseLang;
81
+ }
82
+ }
83
+ } catch (error) {}
84
+ return null;
85
+ }
86
+ export { detectLanguageFromRequest };
@@ -0,0 +1,20 @@
1
+ import "node:module";
2
+ import { canonicalTargetPathname, isDefaultLocaleRedirectSkipPath, localiseTargetPathname } from "@modern-js/i18n-runtime-extensions";
3
+ const createMappedUrlStrategy = (localisedUrls)=>({
4
+ localizePathname: (pathname, language, languages)=>localiseTargetPathname(pathname, language, [
5
+ ...languages
6
+ ], localisedUrls),
7
+ canonicalPathname: (pathname, languages)=>canonicalTargetPathname(pathname, [
8
+ ...languages
9
+ ], localisedUrls),
10
+ shouldSkipRedirect: (pathname, languages)=>isDefaultLocaleRedirectSkipPath(pathname, [
11
+ ...languages
12
+ ])
13
+ });
14
+ const resolveMappedUrlStrategy = (localeDetection)=>{
15
+ if (!localeDetection || 'object' != typeof localeDetection) return;
16
+ const localisedUrls = Reflect.get(localeDetection, 'localisedUrls');
17
+ if (!localisedUrls || 'object' != typeof localisedUrls || 0 === Object.keys(localisedUrls).length) return;
18
+ return createMappedUrlStrategy(localisedUrls);
19
+ };
20
+ export { createMappedUrlStrategy, isDefaultLocaleRedirectSkipPath, resolveMappedUrlStrategy };
@@ -1 +1,8 @@
1
1
  import "node:module";
2
+ const asI18nUrlStrategy = (candidate)=>{
3
+ if (!candidate || 'object' != typeof candidate) return;
4
+ const strategy = candidate;
5
+ return 'function' == typeof strategy.localizePathname && 'function' == typeof strategy.canonicalPathname ? strategy : void 0;
6
+ };
7
+ const isUnusableI18nUrlStrategy = (candidate)=>Boolean(candidate) && 'object' == typeof candidate && void 0 === asI18nUrlStrategy(candidate);
8
+ export { asI18nUrlStrategy, isUnusableI18nUrlStrategy };
@@ -0,0 +1,19 @@
1
+ import { getEntrypointRoutesOwner } from '@modern-js/runtime/cli';
2
+ import type { Entrypoint, NestedRouteForCli, PageRoute } from '@modern-js/types';
3
+ import type { BaseLocaleDetectionOptions } from '../shared/type.js';
4
+ type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
5
+ /**
6
+ * Expand file-system routes from `localeDetection.localisedUrls`.
7
+ *
8
+ * Fork-owned: the localised-URL engine lives in
9
+ * `@modern-js/i18n-runtime-extensions`, which upstream-owned files must not
10
+ * import. `cli/index.ts` calls this through a relative import so the seam, not
11
+ * the edge, is what appears in the upstream-shaped file.
12
+ *
13
+ * Returns the routes unchanged unless locale-path redirects are on, languages
14
+ * are declared, and a non-empty map is configured — an upstream-style config
15
+ * with `localePathRedirect` and `languages` but no map keeps plain
16
+ * locale-prefix behaviour.
17
+ */
18
+ export declare const applyLocalisedRoutes: (routes: FileSystemRoutes, localeDetection: BaseLocaleDetectionOptions | undefined, entrypoint?: Entrypoint & Parameters<typeof getEntrypointRoutesOwner>[0]) => FileSystemRoutes;
19
+ export {};
@@ -2,6 +2,7 @@ import type { ComponentType, FC, ReactNode } from 'react';
2
2
  import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
3
3
  import type { I18nInstance } from './i18n/index.js';
4
4
  import type { Resources } from './i18n/instance.js';
5
+ import { ModernI18nContext, ReactI18nextProviderContext } from './sharedContexts.js';
5
6
  export { getActualI18nextInstance } from './i18n/instance.js';
6
7
  export interface ModernI18nContextValue {
7
8
  language: string;
@@ -14,8 +15,7 @@ export interface ModernI18nContextValue {
14
15
  updateLanguage?: (newLang: string) => void;
15
16
  synchronizeLanguage?: (newLang: string) => void;
16
17
  }
17
- export declare const ModernI18nContext: import("react").Context<ModernI18nContextValue | null>;
18
- export declare const ReactI18nextProviderContext: import("react").Context<ComponentType<any> | null>;
18
+ export { ModernI18nContext, ReactI18nextProviderContext };
19
19
  export interface ModernI18nProviderProps {
20
20
  children: ReactNode;
21
21
  i18nextProvider?: ComponentType<any> | null;
@@ -0,0 +1,6 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { ModernI18nContextValue } from './context.js';
3
+ import type { I18nRouterAdapter } from './routerAdapter.js';
4
+ export declare const ModernI18nContext: import("react").Context<ModernI18nContextValue | null>;
5
+ export declare const ReactI18nextProviderContext: import("react").Context<ComponentType<any> | null>;
6
+ export declare const I18nNavigationContext: import("react").Context<I18nRouterAdapter | null>;
@@ -1,7 +1,7 @@
1
1
  import type { RuntimePlugin } from '@modern-js/runtime';
2
2
  import type React from 'react';
3
3
  import type { BaseBackendOptions, BaseLocaleDetectionOptions } from '../shared/type.js';
4
- import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
4
+ import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
5
5
  import type { I18nInitOptions, I18nInstance } from './i18n/index.js';
6
6
  import { type I18nLanguageSynchronizationProps } from './providerComposition.js';
7
7
  export type { I18nLanguageSynchronizationProps } from './providerComposition.js';
@@ -10,6 +10,7 @@ import './types.js';
10
10
  export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type.js';
11
11
  export type { I18nUrlStrategy } from '../shared/urlStrategy.js';
12
12
  export type { I18nInitOptions, I18nInstance, Resources, TranslateFn, } from './i18n/instance.js';
13
+ export { I18nRouterNavigationProvider, useIntegratedRouterAdapter, } from './navigation.js';
13
14
  export type { LoadReactI18nextIntegration, ReactI18nextIntegration, } from './reactI18next.js';
14
15
  export { I18nNavigationProvider, type I18nRouterAdapter, type I18nRouterLinkTarget, useI18nRouterAdapter, useNativeI18nRouterAdapter, } from './routerAdapter.js';
15
16
  export interface I18nPluginOptions {
@@ -9,7 +9,7 @@ export declare const useI18nextLanguageDetector: (i18nInstance: I18nInstance) =>
9
9
  * Read language directly from localStorage/cookie
10
10
  * Fallback when detector is not available in services
11
11
  */
12
- export declare const readLanguageFromStorage: (detectionOptions?: LanguageDetectorOptions) => string | undefined;
12
+ export declare const readLanguageFromStorage: (detectionOptions?: LanguageDetectorOptions, _request?: unknown, _languages?: string[]) => string | undefined;
13
13
  /**
14
14
  * Detect language using i18next-browser-languagedetector
15
15
  * For initialized instances without detector in services, manually create a detector instance
@@ -1,10 +1,6 @@
1
1
  import type { I18nInstance, LanguageDetectorOptions } from '../instance.js';
2
2
  export declare const cacheUserLanguage: (_i18nInstance: I18nInstance, _language: string, _detectionOptions?: unknown) => void;
3
- /**
4
- * Read language directly from storage (localStorage/cookie)
5
- * Not available in Node.js environment, returns undefined
6
- */
7
- export declare const readLanguageFromStorage: (_detectionOptions?: LanguageDetectorOptions) => string | undefined;
3
+ export declare const readLanguageFromStorage: (detectionOptions?: LanguageDetectorOptions, request?: unknown, languages?: string[]) => string | undefined;
8
4
  /**
9
5
  * Register LanguageDetector plugin to i18n instance
10
6
  * Must be called before init() to properly register the detector
@@ -0,0 +1,10 @@
1
+ import { type I18nRouterAdapter } from './routerAdapter.js';
2
+ /**
3
+ * The seam, and only the seam. The selected-router navigation adapter is a
4
+ * fork subsystem, so it lives in the fork-owned
5
+ * `@modern-js/i18n-runtime-extensions` and is attached here through the
6
+ * `NavigationProvider` extension point this package already exposes. The
7
+ * factory takes this package's own primitives as arguments, so the fork
8
+ * package never imports back into it and the dependency stays one-way.
9
+ */
10
+ export declare const I18nRouterNavigationProvider: ({ children, }: React.PropsWithChildren) => React.JSX.Element, useIntegratedRouterAdapter: () => I18nRouterAdapter;
@@ -0,0 +1 @@
1
+ export { ModernI18nContext, ReactI18nextProviderContext, } from './contexts.js';
@@ -1,4 +1,4 @@
1
- import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
1
+ import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
2
2
  export { splitUrlTarget } from '@modern-js/runtime-utils/url';
3
3
  export declare const getPathname: (context: {
4
4
  ssrContext?: {
@@ -0,0 +1,11 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
+ import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
3
+ /**
4
+ * Mapped locale URLs work without any extra plugin. `localeDetection.localisedUrls`
5
+ * alone is enough: the server derives the URL policy from the map it is given.
6
+ * An explicit `resolveUrlStrategy` still wins, so an integration can replace the
7
+ * policy wholesale.
8
+ */
9
+ export declare const createDefaultUrlStrategy: (localisedUrls?: LocalisedUrlsOption) => I18nUrlStrategy;
10
+ /** A non-empty map is the only configuration that needs a derived policy. */
11
+ export declare const resolveDefaultUrlStrategy: (localisedUrls: unknown) => I18nUrlStrategy | undefined;
@@ -1,6 +1,6 @@
1
1
  import type { ServerPlugin } from '@modern-js/server-runtime';
2
2
  import type { LocaleDetectionOptions } from '../shared/type.js';
3
- import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
3
+ import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
4
4
  export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
5
5
  export interface I18nPluginOptions {
6
6
  localeDetection: LocaleDetectionOptions;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The mapped-URL policy moved to `../shared` so the client runtime can derive
3
+ * the same policy the server does — SSR and a hydrated `<Link>` have to agree
4
+ * on the mapping or client navigation targets a path the server will not serve.
5
+ * Re-exported here to keep the server-side import path stable.
6
+ */
7
+ export { createMappedUrlStrategy, resolveMappedUrlStrategy, } from '../shared/mappedUrlStrategy.js';
@@ -1,4 +1,4 @@
1
- import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
1
+ import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
2
2
  interface LocaleRedirectRequest {
3
3
  url: string;
4
4
  header?: () => {
@@ -1 +1,11 @@
1
- export {};
1
+ import type { LanguageDetectorOptions } from '../runtime/i18n/instance.js';
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: Headers | {
9
+ get: (name: string) => string | null;
10
+ } | Record<string, string | string[] | undefined>;
11
+ }, languages: string[], detectionOptions?: LanguageDetectorOptions): string | null;
@@ -0,0 +1,27 @@
1
+ import { type LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
+ import type { I18nUrlStrategy } from './urlStrategy.js';
3
+ /**
4
+ * The fork's native locale-redirect exclusions (federation artifacts, static
5
+ * and upload prefixes). Re-exported from this fork-owned seam so the native
6
+ * runtime and server redirect paths apply them without importing the
7
+ * extensions package themselves.
8
+ */
9
+ export { isDefaultLocaleRedirectSkipPath } from '@modern-js/i18n-runtime-extensions';
10
+ /**
11
+ * Build the URL policy from a mapped-locale-URL configuration.
12
+ *
13
+ * `localeDetection.localisedUrls` is plain data, so unlike a `urlStrategy`
14
+ * object it survives the JSON boundary between `modern.config.ts` and the
15
+ * generated runtime registration. It is therefore the one configuration both
16
+ * sides can rely on for mapped URLs.
17
+ *
18
+ * This is shared rather than server-only on purpose. The server and the client
19
+ * have to agree on the mapping or a hydrated `<Link>` navigates to a path the
20
+ * server will not serve: SSR renders `/cs/obchodni-podminky` while a
21
+ * client-side navigation asks for `/cs/terms-of-service`. The helpers below are
22
+ * pure pathname functions from `@modern-js/i18n-runtime-extensions`, so the
23
+ * client cost is a few hundred bytes of string work and no i18n engine.
24
+ */
25
+ export declare const createMappedUrlStrategy: (localisedUrls?: LocalisedUrlsOption) => I18nUrlStrategy;
26
+ /** Only a non-empty map needs a derived policy. */
27
+ export declare const resolveMappedUrlStrategy: (localeDetection: unknown) => I18nUrlStrategy | undefined;
@@ -6,6 +6,16 @@ export interface BaseLocaleDetectionOptions {
6
6
  fallbackLanguage?: string;
7
7
  detection?: LanguageDetectorOptions;
8
8
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
9
+ /**
10
+ * Canonical-path -> per-language path map, e.g.
11
+ * `{ '/products': { en: '/products', cs: '/produkty' } }`.
12
+ *
13
+ * Plain data on purpose: the generated runtime registration carries plugin
14
+ * options as JSON, so a map survives the trip where a `urlStrategy` object
15
+ * would lose its methods. The CLI expands file-system routes from it, and
16
+ * the server derives its URL strategy from it, with no extra plugin.
17
+ */
18
+ localisedUrls?: boolean | Record<string, Record<string, string>>;
9
19
  }
10
20
  export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
11
21
  localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
@@ -5,3 +5,14 @@ export interface I18nUrlStrategy {
5
5
  /** Adds exclusions; it cannot enable redirects excluded by native rules. */
6
6
  shouldSkipRedirect?(pathname: string, languages: readonly string[]): boolean;
7
7
  }
8
+ /**
9
+ * A URL strategy reaches the runtime through the generated runtime
10
+ * registration, which carries plugin options as JSON. Functions do not survive
11
+ * that, so a configured strategy can arrive as an empty object — which is
12
+ * truthy and would make every call site throw. Anything that is not a complete
13
+ * strategy is treated as absent, and the caller falls back to the built-in
14
+ * language-prefix behaviour.
15
+ */
16
+ export declare const asI18nUrlStrategy: (candidate: unknown) => I18nUrlStrategy | undefined;
17
+ /** `true` when a value was configured as a strategy but cannot be used as one. */
18
+ export declare const isUnusableI18nUrlStrategy: (candidate: unknown) => boolean;