@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (73) hide show
  1. package/dist/cjs/cli/index.js +1 -1
  2. package/dist/cjs/cli/localisedRoutes.js +3 -2
  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 +8 -1
  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/shared/detection.js +118 -2
  13. package/dist/esm/cli/index.mjs +1 -1
  14. package/dist/esm/cli/localisedRoutes.mjs +3 -2
  15. package/dist/esm/runtime/context.mjs +2 -10
  16. package/dist/esm/runtime/contexts.mjs +5 -0
  17. package/dist/esm/runtime/core.mjs +3 -2
  18. package/dist/esm/runtime/i18n/detection/middleware.mjs +1 -1
  19. package/dist/esm/runtime/i18n/detection/middleware.node.mjs +10 -1
  20. package/dist/esm/runtime/i18n/detection/priority.mjs +1 -1
  21. package/dist/esm/runtime/navigation.mjs +7 -0
  22. package/dist/esm/runtime/routerAdapter.mjs +2 -4
  23. package/dist/esm/runtime/sharedContexts.mjs +1 -0
  24. package/dist/esm/shared/detection.mjs +85 -1
  25. package/dist/esm-node/cli/index.mjs +1 -1
  26. package/dist/esm-node/cli/localisedRoutes.mjs +3 -2
  27. package/dist/esm-node/runtime/context.mjs +2 -10
  28. package/dist/esm-node/runtime/contexts.mjs +6 -0
  29. package/dist/esm-node/runtime/core.mjs +3 -2
  30. package/dist/esm-node/runtime/i18n/detection/middleware.mjs +1 -1
  31. package/dist/esm-node/runtime/i18n/detection/middleware.node.mjs +10 -1
  32. package/dist/esm-node/runtime/i18n/detection/priority.mjs +1 -1
  33. package/dist/esm-node/runtime/navigation.mjs +8 -0
  34. package/dist/esm-node/runtime/routerAdapter.mjs +2 -4
  35. package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
  36. package/dist/esm-node/shared/detection.mjs +85 -1
  37. package/dist/types/cli/localisedRoutes.d.ts +3 -2
  38. package/dist/types/runtime/context.d.ts +2 -2
  39. package/dist/types/runtime/contexts.d.ts +6 -0
  40. package/dist/types/runtime/core.d.ts +1 -0
  41. package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
  42. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -5
  43. package/dist/types/runtime/navigation.d.ts +10 -0
  44. package/dist/types/runtime/sharedContexts.d.ts +1 -0
  45. package/dist/types/shared/detection.d.ts +11 -1
  46. package/package.json +43 -25
  47. package/src/cli/index.ts +1 -0
  48. package/src/cli/localisedRoutes.ts +11 -1
  49. package/src/runtime/context.tsx +6 -31
  50. package/src/runtime/contexts.ts +16 -0
  51. package/src/runtime/core.tsx +12 -1
  52. package/src/runtime/i18n/detection/middleware.node.ts +25 -7
  53. package/src/runtime/i18n/detection/middleware.ts +2 -0
  54. package/src/runtime/i18n/detection/priority.ts +2 -0
  55. package/src/runtime/navigation.tsx +20 -0
  56. package/src/runtime/routerAdapter.tsx +2 -11
  57. package/src/runtime/sharedContexts.ts +8 -0
  58. package/src/shared/detection.ts +34 -10
  59. package/rslib.config.mts +0 -4
  60. package/rstest.config.mts +0 -50
  61. package/tests/backendDefaults.test.ts +0 -30
  62. package/tests/defaultUrlStrategy.test.ts +0 -107
  63. package/tests/federatedI18nBoundaryResolver.test.tsx +0 -65
  64. package/tests/i18nUtils.test.ts +0 -60
  65. package/tests/link.test.tsx +0 -139
  66. package/tests/localisedRouteGeneration.test.ts +0 -159
  67. package/tests/localisedUrls.test.ts +0 -193
  68. package/tests/mappedUrlStrategyDerivation.test.ts +0 -62
  69. package/tests/reactI18nextRuntimeBoundary.test.ts +0 -117
  70. package/tests/redirectPolicy.test.ts +0 -107
  71. package/tests/routerAdapter.test.tsx +0 -221
  72. package/tests/urlStrategySerialization.test.ts +0 -122
  73. package/tsconfig.json +0 -15
@@ -1 +1,85 @@
1
- import "../runtime/i18n/detection/config.mjs";
1
+ import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
2
+ function detectLanguageFromRequest(req, languages, detectionOptions) {
3
+ try {
4
+ const mergedDetection = detectionOptions ? mergeDetectionOptions(detectionOptions) : DEFAULT_I18NEXT_DETECTION_OPTIONS;
5
+ const order = (mergedDetection.order || []).filter((item)=>![
6
+ 'path',
7
+ 'localStorage',
8
+ 'navigator',
9
+ 'htmlTag',
10
+ 'subdomain'
11
+ ].includes(item));
12
+ const detectionOrder = order.length > 0 ? order : [
13
+ 'querystring',
14
+ 'cookie',
15
+ 'header'
16
+ ];
17
+ const getHeader = (name)=>{
18
+ const headers = req.headers;
19
+ if ('function' == typeof headers?.get) return headers.get(name);
20
+ const value = headers[name.toLowerCase()];
21
+ if (Array.isArray(value)) return value[0] ?? null;
22
+ return value ?? null;
23
+ };
24
+ for (const method of detectionOrder){
25
+ let detectedLang = null;
26
+ switch(method){
27
+ case 'querystring':
28
+ {
29
+ const lookupKey = mergedDetection.lookupQuerystring || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring || 'lng';
30
+ const host = getHeader('host') || 'localhost';
31
+ const url = new URL(req.url, `http://${host}`);
32
+ detectedLang = url.searchParams.get(lookupKey);
33
+ break;
34
+ }
35
+ case 'cookie':
36
+ {
37
+ const lookupKey = mergedDetection.lookupCookie || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie || 'i18next';
38
+ const cookieHeader = getHeader('Cookie');
39
+ if (cookieHeader) {
40
+ const cookies = cookieHeader.split(';').reduce((acc, item)=>{
41
+ const [key, value] = item.trim().split('=');
42
+ if (key && value) acc[key] = value;
43
+ return acc;
44
+ }, {});
45
+ detectedLang = cookies[lookupKey] || null;
46
+ }
47
+ break;
48
+ }
49
+ case 'header':
50
+ {
51
+ const lookupKey = mergedDetection.lookupHeader || DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader || 'accept-language';
52
+ const acceptLanguage = getHeader(lookupKey);
53
+ if (acceptLanguage) {
54
+ const languagesList = acceptLanguage.split(',').map((lang)=>{
55
+ const [code, q] = lang.trim().split(';');
56
+ return {
57
+ code,
58
+ baseCode: code.split('-')[0],
59
+ quality: q ? parseFloat(q.split('=')[1]) : 1.0
60
+ };
61
+ }).sort((a, b)=>b.quality - a.quality);
62
+ for (const lang of languagesList){
63
+ if (0 === languages.length || languages.includes(lang.code)) {
64
+ detectedLang = lang.code;
65
+ break;
66
+ }
67
+ if (languages.includes(lang.baseCode)) {
68
+ detectedLang = lang.baseCode;
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ break;
74
+ }
75
+ }
76
+ if (detectedLang && (0 === languages.length || languages.includes(detectedLang))) return detectedLang;
77
+ if (detectedLang) {
78
+ const baseLang = detectedLang.split('-')[0];
79
+ if (languages.includes(baseLang)) return baseLang;
80
+ }
81
+ }
82
+ } catch (error) {}
83
+ return null;
84
+ }
85
+ export { detectLanguageFromRequest };
@@ -52,7 +52,7 @@ const i18nPlugin = (options = {})=>({
52
52
  });
53
53
  api.modifyFileSystemRoutes(({ entrypoint, routes })=>({
54
54
  entrypoint,
55
- routes: applyLocalisedRoutes(routes, localeDetection ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection) : void 0)
55
+ routes: applyLocalisedRoutes(routes, localeDetection ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection) : void 0, entrypoint)
56
56
  }));
57
57
  api._internalServerPlugins(({ plugins })=>{
58
58
  const { serverRoutes, metaName } = api.getAppContext();
@@ -1,9 +1,10 @@
1
1
  import "node:module";
2
2
  import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
3
- const applyLocalisedRoutes = (routes, localeDetection)=>{
3
+ import { getEntrypointRoutesOwner } from "@modern-js/runtime/cli";
4
+ const applyLocalisedRoutes = (routes, localeDetection, entrypoint)=>{
4
5
  const { localePathRedirect, languages = [], localisedUrls } = localeDetection ?? {};
5
6
  const resolved = resolveLocalisedUrlsConfig(localisedUrls);
6
7
  if (!localePathRedirect || !languages.length || !resolved.enabled) return routes;
7
- return applyLocalisedUrlsToRoutes(routes, languages, resolved.map);
8
+ return applyLocalisedUrlsToRoutes(routes, languages, resolved.map, entrypoint && '@modern-js/plugin-tanstack' === getEntrypointRoutesOwner(entrypoint) ? 'canonical' : 'physical');
8
9
  };
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,6 +1,7 @@
1
1
  import "node:module";
2
2
  import { resolveMappedUrlStrategy } from "../shared/mappedUrlStrategy.mjs";
3
3
  import { asI18nUrlStrategy } from "../shared/urlStrategy.mjs";
4
+ import { I18nRouterNavigationProvider, useIntegratedRouterAdapter } from "./navigation.mjs";
4
5
  import { setupI18nBeforeRender } from "./pluginSetup.mjs";
5
6
  import { createI18nRootWrapper } from "./providerComposition.mjs";
6
7
  import { resolveReactI18nextIntegration } from "./reactI18next.mjs";
@@ -40,7 +41,7 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
40
41
  fallbackLanguage,
41
42
  ignoreRedirectRoutes,
42
43
  urlStrategy: resolvedUrlStrategy,
43
- NavigationProvider,
44
+ NavigationProvider: NavigationProvider ?? I18nRouterNavigationProvider,
44
45
  LanguageSynchronization,
45
46
  getLatestI18nInstance: ()=>latestI18nInstance,
46
47
  getI18nextProvider: ()=>I18nextProvider
@@ -53,4 +54,4 @@ export { I18nLink } from "./I18nLink.mjs";
53
54
  export { Link } from "./Link.mjs";
54
55
  export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
55
56
  export { splitUrlTarget } from "./utils.mjs";
56
- 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";
@@ -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 };
@@ -1,4 +1,5 @@
1
- import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
1
+ import { getEntrypointRoutesOwner } from '@modern-js/runtime/cli';
2
+ import type { Entrypoint, NestedRouteForCli, PageRoute } from '@modern-js/types';
2
3
  import type { BaseLocaleDetectionOptions } from '../shared/type.js';
3
4
  type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
4
5
  /**
@@ -14,5 +15,5 @@ type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
14
15
  * with `localePathRedirect` and `languages` but no map keeps plain
15
16
  * locale-prefix behaviour.
16
17
  */
17
- export declare const applyLocalisedRoutes: (routes: FileSystemRoutes, localeDetection: BaseLocaleDetectionOptions | undefined) => FileSystemRoutes;
18
+ export declare const applyLocalisedRoutes: (routes: FileSystemRoutes, localeDetection: BaseLocaleDetectionOptions | undefined, entrypoint?: Entrypoint & Parameters<typeof getEntrypointRoutesOwner>[0]) => FileSystemRoutes;
18
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>;
@@ -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 +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;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@bleedingdev/modern-js-plugin-i18n",
3
3
  "description": "A Progressive React Framework for modern web development.",
4
- "homepage": "https://github.com/BleedingDev/ultramodern.js#readme",
4
+ "homepage": "https://github.com/ultramodernjs/ultramodern.js#readme",
5
5
  "bugs": {
6
- "url": "https://github.com/BleedingDev/ultramodern.js/issues"
6
+ "url": "https://github.com/ultramodernjs/ultramodern.js/issues"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/BleedingDev/ultramodern.js.git",
10
+ "url": "git+https://github.com/ultramodernjs/ultramodern.js.git",
11
11
  "directory": "packages/runtime/plugin-i18n"
12
12
  },
13
13
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.9.0-ultramodern.9",
20
+ "version": "3.9.3-ultramodern.2",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -55,6 +55,13 @@
55
55
  },
56
56
  "default": "./dist/esm/runtime/context.mjs"
57
57
  },
58
+ "./runtime/contexts": {
59
+ "types": "./dist/types/runtime/contexts.d.ts",
60
+ "node": {
61
+ "module": "./dist/esm/runtime/contexts.mjs"
62
+ },
63
+ "default": "./dist/esm/runtime/contexts.mjs"
64
+ },
58
65
  "./runtime/no-react-i18next": {
59
66
  "types": "./dist/types/runtime/no-react-i18next.d.ts",
60
67
  "node": {
@@ -104,6 +111,9 @@
104
111
  "runtime/consumer": [
105
112
  "./dist/types/runtime/context.d.ts"
106
113
  ],
114
+ "runtime/contexts": [
115
+ "./dist/types/runtime/contexts.d.ts"
116
+ ],
107
117
  "runtime/no-react-i18next": [
108
118
  "./dist/types/runtime/no-react-i18next.d.ts"
109
119
  ],
@@ -122,23 +132,23 @@
122
132
  }
123
133
  },
124
134
  "dependencies": {
125
- "@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.9",
126
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.9",
127
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.9",
128
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.9",
129
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.9",
130
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.9",
131
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.9",
135
+ "@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.3-ultramodern.2",
136
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.3-ultramodern.2",
137
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.3-ultramodern.2",
138
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.3-ultramodern.2",
139
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.3-ultramodern.2",
140
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.3-ultramodern.2",
141
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.3-ultramodern.2",
132
142
  "@swc/helpers": "^0.5.23",
133
143
  "i18next-browser-languagedetector": "^8.2.1",
134
144
  "i18next-chained-backend": "^5.0.6",
135
- "i18next-fs-backend": "^2.6.7",
145
+ "i18next-fs-backend": "^2.6.8",
136
146
  "i18next-http-backend": "^4.0.2",
137
- "i18next-http-middleware": "^3.9.8"
147
+ "i18next-http-middleware": "^3.9.9"
138
148
  },
139
149
  "peerDependencies": {
140
- "@modern-js/runtime": "3.9.0-ultramodern.9",
141
- "i18next": ">=25.7.4",
150
+ "@modern-js/runtime": "3.9.3-ultramodern.2",
151
+ "i18next": ">=25.10.10",
142
152
  "react": "^19.2.8",
143
153
  "react-dom": "^19.2.8",
144
154
  "react-i18next": ">=15.7.4"
@@ -152,26 +162,34 @@
152
162
  }
153
163
  },
154
164
  "devDependencies": {
155
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.9",
156
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.9",
157
- "@rslib/core": "1.0.0",
158
- "@types/node": "^26.4.1",
159
- "@typescript/native-preview": "7.0.0-dev.20260707.2",
165
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.3-ultramodern.2",
166
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.3-ultramodern.2",
167
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.3-ultramodern.2",
168
+ "@rslib/core": "1.0.3",
169
+ "@scripts/rstest-config": "2.66.0",
170
+ "@types/node": "^26.6.3",
160
171
  "esbuild": "^0.28.2",
161
172
  "i18next": "26.4.2",
162
- "react": "^19.2.8",
163
- "react-dom": "^19.2.8",
164
- "react-i18next": "17.0.13",
173
+ "react": "^19.3.0",
174
+ "react-dom": "^19.3.0",
175
+ "react-i18next": "17.0.15",
165
176
  "ts-node": "^10.9.2",
166
- "typescript": "^7.0.2"
177
+ "typescript": "7.0.2"
167
178
  },
168
179
  "sideEffects": false,
169
180
  "publishConfig": {
170
181
  "access": "public"
171
182
  },
183
+ "files": [
184
+ "dist",
185
+ "src"
186
+ ],
172
187
  "scripts": {
173
188
  "dev": "rslib build --watch",
174
189
  "build": "rslib build",
175
- "test": "rstest --passWithNoTests"
190
+ "test": "rstest"
191
+ },
192
+ "ultramodern": {
193
+ "sourceName": "@modern-js/plugin-i18n"
176
194
  }
177
195
  }
package/src/cli/index.ts CHANGED
@@ -174,6 +174,7 @@ export const i18nPlugin = (
174
174
  localeDetection
175
175
  ? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
176
176
  : undefined,
177
+ entrypoint,
177
178
  ) as typeof routes,
178
179
  }));
179
180
 
@@ -2,7 +2,12 @@ import {
2
2
  applyLocalisedUrlsToRoutes,
3
3
  resolveLocalisedUrlsConfig,
4
4
  } from '@modern-js/i18n-runtime-extensions';
5
- import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
5
+ import { getEntrypointRoutesOwner } from '@modern-js/runtime/cli';
6
+ import type {
7
+ Entrypoint,
8
+ NestedRouteForCli,
9
+ PageRoute,
10
+ } from '@modern-js/types';
6
11
  import type { BaseLocaleDetectionOptions } from '../shared/type';
7
12
 
8
13
  type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
@@ -23,6 +28,7 @@ type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
23
28
  export const applyLocalisedRoutes = (
24
29
  routes: FileSystemRoutes,
25
30
  localeDetection: BaseLocaleDetectionOptions | undefined,
31
+ entrypoint?: Entrypoint & Parameters<typeof getEntrypointRoutesOwner>[0],
26
32
  ): FileSystemRoutes => {
27
33
  const {
28
34
  localePathRedirect,
@@ -39,5 +45,9 @@ export const applyLocalisedRoutes = (
39
45
  routes,
40
46
  languages,
41
47
  resolved.map,
48
+ entrypoint &&
49
+ getEntrypointRoutesOwner(entrypoint) === '@modern-js/plugin-tanstack'
50
+ ? 'canonical'
51
+ : 'physical',
42
52
  ) as FileSystemRoutes;
43
53
  };