@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,12 +1,6 @@
1
1
  import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
2
2
  import type { ComponentType, FC, ReactNode } from 'react';
3
- import {
4
- createContext,
5
- useCallback,
6
- useContext,
7
- useEffect,
8
- useMemo,
9
- } from 'react';
3
+ import { useCallback, useContext, useEffect, useMemo } from 'react';
10
4
  import type { I18nUrlStrategy } from '../shared/urlStrategy';
11
5
  import {
12
6
  changeModernI18nLanguage,
@@ -19,6 +13,10 @@ import type { I18nInstance } from './i18n';
19
13
  import type { Resources } from './i18n/instance';
20
14
 
21
15
  import { useI18nRouterAdapter } from './routerAdapter';
16
+ import {
17
+ ModernI18nContext,
18
+ ReactI18nextProviderContext,
19
+ } from './sharedContexts';
22
20
 
23
21
  export { getActualI18nextInstance } from './i18n/instance';
24
22
 
@@ -36,30 +34,7 @@ export interface ModernI18nContextValue {
36
34
  synchronizeLanguage?: (newLang: string) => void;
37
35
  }
38
36
 
39
- const modernI18nContextKey = Symbol.for(
40
- '@modern-js/plugin-i18n/runtime/ModernI18nContext',
41
- );
42
- const reactI18nextProviderContextKey = Symbol.for(
43
- '@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext',
44
- );
45
-
46
- type GlobalContextStore<T> = typeof globalThis & {
47
- [key: symbol]: ReturnType<typeof createContext<T>> | undefined;
48
- };
49
-
50
- const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
51
- const globalStore = globalThis as GlobalContextStore<T>;
52
- globalStore[key] ??= createContext<T>(defaultValue);
53
- return globalStore[key];
54
- };
55
-
56
- export const ModernI18nContext =
57
- getGlobalContext<ModernI18nContextValue | null>(modernI18nContextKey, null);
58
- export const ReactI18nextProviderContext =
59
- getGlobalContext<ComponentType<any> | null>(
60
- reactI18nextProviderContextKey,
61
- null,
62
- );
37
+ export { ModernI18nContext, ReactI18nextProviderContext };
63
38
 
64
39
  export interface ModernI18nProviderProps {
65
40
  children: ReactNode;
@@ -0,0 +1,16 @@
1
+ import type { ComponentType } from 'react';
2
+ import { createContext } from 'react';
3
+ import type { ModernI18nContextValue } from './context';
4
+ import type { I18nRouterAdapter } from './routerAdapter';
5
+
6
+ // Every other i18n runtime module imports these through
7
+ // `@modern-js/plugin-i18n/runtime/contexts` so Module Federation shares one
8
+ // copy of them. Their values stay provider-owned.
9
+ export const ModernI18nContext = createContext<ModernI18nContextValue | null>(
10
+ null,
11
+ );
12
+ export const ReactI18nextProviderContext =
13
+ createContext<ComponentType<any> | null>(null);
14
+ export const I18nNavigationContext = createContext<I18nRouterAdapter | null>(
15
+ null,
16
+ );
@@ -7,6 +7,7 @@ import type {
7
7
  } from '../shared/type';
8
8
  import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
9
9
  import type { I18nInitOptions, I18nInstance } from './i18n';
10
+ import { I18nRouterNavigationProvider } from './navigation';
10
11
  import {
11
12
  type RuntimeContextWithI18n,
12
13
  setupI18nBeforeRender,
@@ -32,6 +33,10 @@ export type {
32
33
  Resources,
33
34
  TranslateFn,
34
35
  } from './i18n/instance';
36
+ export {
37
+ I18nRouterNavigationProvider,
38
+ useIntegratedRouterAdapter,
39
+ } from './navigation';
35
40
  export type {
36
41
  LoadReactI18nextIntegration,
37
42
  ReactI18nextIntegration,
@@ -137,7 +142,13 @@ export const createI18nPlugin =
137
142
  fallbackLanguage,
138
143
  ignoreRedirectRoutes,
139
144
  urlStrategy: resolvedUrlStrategy,
140
- NavigationProvider,
145
+ // The selected router, not react-router, is what a localized `<Link>`
146
+ // has to navigate through, and the i18n language has to follow a
147
+ // client-side navigation to a mapped URL. Both need the actual router
148
+ // instance, so a bare `appTools()` consumer gets the integrated
149
+ // adapter by default; a composing runtime plugin can still replace it.
150
+ NavigationProvider:
151
+ NavigationProvider ?? I18nRouterNavigationProvider,
141
152
  LanguageSynchronization,
142
153
  getLatestI18nInstance: () => latestI18nInstance,
143
154
  getI18nextProvider: () => I18nextProvider,
@@ -1,4 +1,5 @@
1
1
  import { LanguageDetector } from 'i18next-http-middleware';
2
+ import { detectLanguageFromRequest } from '../../../shared/detection.js';
2
3
  import type { I18nInstance, LanguageDetectorOptions } from '../instance';
3
4
 
4
5
  type HttpDetectorInit = (
@@ -13,6 +14,20 @@ type HttpDetectorDetect = (
13
14
  detectionOrder?: unknown,
14
15
  ) => string | string[] | undefined;
15
16
 
17
+ type DetectionRequest = Parameters<typeof detectLanguageFromRequest>[0];
18
+
19
+ const isDetectionRequest = (request: unknown): request is DetectionRequest => {
20
+ if (!request || typeof request !== 'object') {
21
+ return false;
22
+ }
23
+
24
+ const { url, headers } = request as {
25
+ url?: unknown;
26
+ headers?: unknown;
27
+ };
28
+ return typeof url === 'string' && !!headers && typeof headers === 'object';
29
+ };
30
+
16
31
  export const cacheUserLanguage = (
17
32
  _i18nInstance: I18nInstance,
18
33
  _language: string,
@@ -21,15 +36,18 @@ export const cacheUserLanguage = (
21
36
  return;
22
37
  };
23
38
 
24
- /**
25
- * Read language directly from storage (localStorage/cookie)
26
- * Not available in Node.js environment, returns undefined
27
- */
28
39
  export const readLanguageFromStorage = (
29
- _detectionOptions?: LanguageDetectorOptions,
40
+ detectionOptions?: LanguageDetectorOptions,
41
+ request?: unknown,
42
+ languages: string[] = [],
30
43
  ): string | undefined => {
31
- // In Node.js environment, storage-based detection is not available
32
- return undefined;
44
+ if (!isDetectionRequest(request)) {
45
+ return undefined;
46
+ }
47
+
48
+ return (
49
+ detectLanguageFromRequest(request, languages, detectionOptions) ?? undefined
50
+ );
33
51
  };
34
52
  /**
35
53
  * Register LanguageDetector plugin to i18n instance
@@ -30,6 +30,8 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
30
30
  */
31
31
  export const readLanguageFromStorage = (
32
32
  detectionOptions?: LanguageDetectorOptions,
33
+ _request?: unknown,
34
+ _languages?: string[],
33
35
  ): string | undefined => {
34
36
  try {
35
37
  const options = detectionOptions || {};
@@ -59,6 +59,8 @@ export const detectLanguageWithPriority = async (
59
59
  localePathRedirect,
60
60
  userInitOptions,
61
61
  ),
62
+ ssrContext?.request,
63
+ languages,
62
64
  );
63
65
  } else {
64
66
  detectedLanguage = await detectLanguageFromI18nextDetector(i18nInstance, {
@@ -0,0 +1,20 @@
1
+ import { createI18nRouterNavigation } from '@modern-js/i18n-runtime-extensions/router-navigation';
2
+ import {
3
+ I18nNavigationProvider,
4
+ type I18nRouterAdapter,
5
+ useNativeI18nRouterAdapter,
6
+ } from './routerAdapter';
7
+
8
+ /**
9
+ * The seam, and only the seam. The selected-router navigation adapter is a
10
+ * fork subsystem, so it lives in the fork-owned
11
+ * `@modern-js/i18n-runtime-extensions` and is attached here through the
12
+ * `NavigationProvider` extension point this package already exposes. The
13
+ * factory takes this package's own primitives as arguments, so the fork
14
+ * package never imports back into it and the dependency stays one-way.
15
+ */
16
+ export const { I18nRouterNavigationProvider, useIntegratedRouterAdapter } =
17
+ createI18nRouterNavigation<I18nRouterAdapter>({
18
+ I18nNavigationProvider,
19
+ useNativeI18nRouterAdapter,
20
+ });
@@ -1,7 +1,8 @@
1
+ import { I18nNavigationContext } from '@modern-js/plugin-i18n/runtime/contexts';
1
2
  import { isBrowser, RuntimeContext } from '@modern-js/runtime';
2
3
  import { InternalRuntimeContext } from '@modern-js/runtime/context';
3
4
  import type React from 'react';
4
- import { createContext, useContext } from 'react';
5
+ import { useContext } from 'react';
5
6
 
6
7
  interface I18nRouterLocation {
7
8
  pathname: string;
@@ -44,16 +45,6 @@ export interface I18nRouterAdapter {
44
45
  };
45
46
  }
46
47
 
47
- const navigationContextKey = Symbol.for(
48
- '@modern-js/plugin-i18n/runtime/I18nNavigationContext',
49
- );
50
- // Only the React context identity is shared. Its values remain provider-owned.
51
- const contextStore = globalThis as typeof globalThis & {
52
- [navigationContextKey]?: React.Context<I18nRouterAdapter | null>;
53
- };
54
- const I18nNavigationContext = (contextStore[navigationContextKey] ??=
55
- createContext<I18nRouterAdapter | null>(null));
56
-
57
48
  export const I18nNavigationProvider = ({
58
49
  children,
59
50
  value,
@@ -0,0 +1,8 @@
1
+ // Upstream-owned runtime modules take the i18n contexts from this fork-owned
2
+ // module, which reaches them through the shared
3
+ // `@modern-js/plugin-i18n/runtime/contexts` request. That keeps the fork
4
+ // import edge out of the upstream-owned files.
5
+ export {
6
+ ModernI18nContext,
7
+ ReactI18nextProviderContext,
8
+ } from '@modern-js/plugin-i18n/runtime/contexts';
@@ -8,14 +8,13 @@ import type { LanguageDetectorOptions } from '../runtime/i18n/instance';
8
8
  * Detect language from request using the same detection logic as i18next
9
9
  * This ensures consistency between server-side and client-side detection
10
10
  */
11
- function detectLanguageFromRequest(
11
+ export function detectLanguageFromRequest(
12
12
  req: {
13
13
  url: string;
14
14
  headers:
15
- | {
16
- get: (name: string) => string | null;
17
- }
18
- | Headers;
15
+ | Headers
16
+ | { get: (name: string) => string | null }
17
+ | Record<string, string | string[] | undefined>;
19
18
  },
20
19
  languages: string[],
21
20
  detectionOptions?: LanguageDetectorOptions,
@@ -39,11 +38,21 @@ function detectLanguageFromRequest(
39
38
  order.length > 0 ? order : ['querystring', 'cookie', 'header'];
40
39
 
41
40
  // Helper to get header value
41
+ // req.headers is a Fetch API Headers instance when the caller passes the raw
42
+ // Request (e.g. ssrContext.request.raw), but modern.js's own SSR request object
43
+ // (ssrContext.request) carries headers as a plain, lower-cased key/value object.
42
44
  const getHeader = (name: string): string | null => {
43
- if (req.headers instanceof Headers) {
44
- return req.headers.get(name);
45
+ const headers = req.headers;
46
+ if (typeof (headers as Headers)?.get === 'function') {
47
+ return (headers as Headers).get(name);
48
+ }
49
+ const value = (headers as Record<string, string | string[] | undefined>)[
50
+ name.toLowerCase()
51
+ ];
52
+ if (Array.isArray(value)) {
53
+ return value[0] ?? null;
45
54
  }
46
- return req.headers.get(name);
55
+ return value ?? null;
47
56
  };
48
57
 
49
58
  // Try each detection method in order
@@ -94,7 +103,8 @@ function detectLanguageFromRequest(
94
103
  .map((lang: string) => {
95
104
  const [code, q] = lang.trim().split(';');
96
105
  return {
97
- code: code.split('-')[0], // Extract base language code
106
+ code,
107
+ baseCode: code.split('-')[0],
98
108
  quality: q ? parseFloat(q.split('=')[1]) : 1.0,
99
109
  };
100
110
  })
@@ -103,12 +113,17 @@ function detectLanguageFromRequest(
103
113
  b.quality - a.quality,
104
114
  );
105
115
 
106
- // Find first matching language
116
+ // Find first matching language, preferring an exact tag match
117
+ // (e.g. 'zh-Hant-TW') over the base language code (e.g. 'zh')
107
118
  for (const lang of languagesList) {
108
119
  if (languages.length === 0 || languages.includes(lang.code)) {
109
120
  detectedLang = lang.code;
110
121
  break;
111
122
  }
123
+ if (languages.includes(lang.baseCode)) {
124
+ detectedLang = lang.baseCode;
125
+ break;
126
+ }
112
127
  }
113
128
  }
114
129
  break;
@@ -122,6 +137,15 @@ function detectLanguageFromRequest(
122
137
  ) {
123
138
  return detectedLang;
124
139
  }
140
+
141
+ // Match query and cookie values like header values: exact tag first,
142
+ // then the supported base language (e.g. 'zh-CN' -> 'zh').
143
+ if (detectedLang) {
144
+ const baseLang = detectedLang.split('-')[0];
145
+ if (languages.includes(baseLang)) {
146
+ return baseLang;
147
+ }
148
+ }
125
149
  }
126
150
  } catch (error) {
127
151
  // Silently ignore errors
package/rslib.config.mts DELETED
@@ -1,4 +0,0 @@
1
- import { rslibConfig } from '@modern-js/rslib';
2
- import { defineConfig } from '@rslib/core';
3
-
4
- export default defineConfig(rslibConfig);
package/rstest.config.mts DELETED
@@ -1,50 +0,0 @@
1
- import { dirname, resolve } from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import type { ProjectConfig } from '@rstest/core';
4
- import { withTestPreset } from '@scripts/rstest-config';
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
-
8
- const commonConfig: ProjectConfig = {
9
- setupFiles: [resolve(__dirname, '../../../scripts/rstest-config/setup.ts')],
10
- globals: true,
11
- tools: {
12
- swc: {
13
- jsc: {
14
- transform: {
15
- react: {
16
- runtime: 'automatic',
17
- },
18
- },
19
- },
20
- },
21
- },
22
- };
23
-
24
- export default {
25
- projects: [
26
- withTestPreset({
27
- name: 'plugin-i18n-node',
28
- testEnvironment: 'node',
29
- include: [
30
- 'tests/i18nUtils.test.ts',
31
- 'tests/localisedUrls.test.ts',
32
- 'tests/localisedRouteGeneration.test.ts',
33
- 'tests/mappedUrlStrategyDerivation.test.ts',
34
- 'tests/defaultUrlStrategy.test.ts',
35
- 'tests/urlStrategySerialization.test.ts',
36
- 'tests/backendDefaults.test.ts',
37
- 'tests/redirectPolicy.test.ts',
38
- 'tests/reactI18nextRuntimeBoundary.test.ts',
39
- 'tests/federatedI18nBoundaryResolver.test.tsx',
40
- ],
41
- extends: commonConfig,
42
- }),
43
- withTestPreset({
44
- name: 'plugin-i18n-client',
45
- testEnvironment: 'happy-dom',
46
- include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
47
- extends: commonConfig,
48
- }),
49
- ],
50
- };
@@ -1,30 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { describe, expect, test } from '@rstest/core';
5
- import { resolveDefaultLocalesDir } from '../src/runtime/i18n/backend/defaults.node';
6
-
7
- const makeTempDir = (...dirs: string[]): string => {
8
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-defaults-'));
9
- for (const dir of dirs) {
10
- fs.mkdirSync(path.join(root, dir), { recursive: true });
11
- }
12
- return root;
13
- };
14
-
15
- describe('node backend default loadPath', () => {
16
- test('prefers the project-root ./locales convention (the detection root)', () => {
17
- const root = makeTempDir('locales', 'config/public/locales');
18
- expect(resolveDefaultLocalesDir(root)).toBe('./locales');
19
- });
20
-
21
- test('falls back to ./config/public/locales when only it exists', () => {
22
- const root = makeTempDir('config/public/locales');
23
- expect(resolveDefaultLocalesDir(root)).toBe('./config/public/locales');
24
- });
25
-
26
- test('defaults to ./locales when neither conventional directory exists', () => {
27
- const root = makeTempDir();
28
- expect(resolveDefaultLocalesDir(root)).toBe('./locales');
29
- });
30
- });
@@ -1,107 +0,0 @@
1
- import { describe, expect, test } from '@rstest/core';
2
- import { i18nServerPlugin } from '../src/server';
3
- import { resolveDefaultUrlStrategy } from '../src/server/defaultUrlStrategy';
4
-
5
- const localisedUrls = {
6
- '/about': { en: '/about', cs: '/o-nas' },
7
- };
8
-
9
- const createRequestContext = (pathname: string) =>
10
- ({
11
- req: { url: `http://localhost${pathname}` },
12
- }) as any;
13
-
14
- const bootServerPlugin = (options: Parameters<typeof i18nServerPlugin>[0]) => {
15
- const middlewares: any[] = [];
16
- const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
17
- let prepare: (() => void) | undefined;
18
-
19
- i18nServerPlugin(options).setup({
20
- getServerConfig: () => ({}),
21
- getServerContext: () => ({ middlewares, routes }),
22
- onPrepare: (fn: () => void) => {
23
- prepare = fn;
24
- },
25
- } as any);
26
-
27
- prepare?.();
28
- return middlewares;
29
- };
30
-
31
- describe('default URL strategy', () => {
32
- test('a mapped locale URL config alone produces a strategy', () => {
33
- const strategy = resolveDefaultUrlStrategy(localisedUrls);
34
-
35
- expect(strategy).toBeDefined();
36
- expect(strategy?.localizePathname('/about', 'cs', ['en', 'cs'])).toBe(
37
- '/cs/o-nas',
38
- );
39
- expect(strategy?.canonicalPathname('/cs/o-nas', ['en', 'cs'])).toBe(
40
- '/about',
41
- );
42
- });
43
-
44
- test('no map means no derived strategy', () => {
45
- expect(resolveDefaultUrlStrategy(undefined)).toBeUndefined();
46
- expect(resolveDefaultUrlStrategy({})).toBeUndefined();
47
- expect(resolveDefaultUrlStrategy('nope')).toBeUndefined();
48
- });
49
-
50
- test('`localisedUrls` boots the server without any extra plugin', () => {
51
- // Previously this threw "Mapped locale URLs require a URL strategy",
52
- // which stopped every existing consumer's server from starting.
53
- const middlewares = bootServerPlugin({
54
- localeDetection: {
55
- fallbackLanguage: 'en',
56
- languages: ['en', 'cs'],
57
- localePathRedirect: true,
58
- localisedUrls,
59
- } as any,
60
- staticRoutePrefixes: [],
61
- });
62
-
63
- expect(middlewares.map(middleware => middleware.name)).toContain(
64
- 'i18n-server-middleware',
65
- );
66
- });
67
-
68
- test('redirects a canonical path to its mapped locale URL', async () => {
69
- const middlewares = bootServerPlugin({
70
- localeDetection: {
71
- fallbackLanguage: 'en',
72
- languages: ['en', 'cs'],
73
- localePathRedirect: true,
74
- localisedUrls,
75
- } as any,
76
- staticRoutePrefixes: [],
77
- });
78
-
79
- const redirect = middlewares.find(
80
- middleware => middleware.name === 'i18n-server-middleware',
81
- );
82
- const context = createRequestContext('/about');
83
- context.get = () => 'cs';
84
- const response = await redirect.handler(context, async () => undefined);
85
-
86
- expect(response?.headers?.get('location')).toBe('/cs/o-nas');
87
- });
88
-
89
- test('an explicit strategy still wins over the derived one', () => {
90
- const explicit = {
91
- localizePathname: () => '/explicit',
92
- canonicalPathname: () => '/explicit',
93
- };
94
- const middlewares = bootServerPlugin({
95
- localeDetection: {
96
- fallbackLanguage: 'en',
97
- languages: ['en', 'cs'],
98
- localePathRedirect: true,
99
- localisedUrls,
100
- } as any,
101
- staticRoutePrefixes: [],
102
- resolveUrlStrategy: () => explicit,
103
- });
104
-
105
- expect(middlewares.length).toBeGreaterThan(0);
106
- });
107
- });
@@ -1,65 +0,0 @@
1
- import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
2
- import { describe, expect, test } from '@rstest/core';
3
- import { useState } from 'react';
4
- import { renderToStaticMarkup } from 'react-dom/server';
5
- import {
6
- FederatedI18nBoundary,
7
- type FederatedI18nBoundaryProps,
8
- } from '../src/runtime/context';
9
-
10
- const props: FederatedI18nBoundaryProps = {
11
- defaultNamespace: 'inventory',
12
- resources: {},
13
- children: <span>child content</span>,
14
- };
15
-
16
- describe('native federation resolver wrapper', () => {
17
- test('retains the actionable fallback when a resolver does not handle i18n', () => {
18
- expect(() =>
19
- renderToStaticMarkup(
20
- <RuntimeComponentResolverContext.Provider
21
- value={component => component}
22
- >
23
- <FederatedI18nBoundary {...props} />
24
- </RuntimeComponentResolverContext.Provider>,
25
- ),
26
- ).toThrow(/requires the @modern-js\/i18n-integration runtime plugin/);
27
- });
28
-
29
- test('rejects resolution back to the native wrapper', () => {
30
- expect(() =>
31
- renderToStaticMarkup(
32
- <RuntimeComponentResolverContext.Provider
33
- value={() => FederatedI18nBoundary}
34
- >
35
- <FederatedI18nBoundary {...props} />
36
- </RuntimeComponentResolverContext.Provider>,
37
- ),
38
- ).toThrow(/resolver returned its own wrapper/);
39
- });
40
-
41
- test('renders the resolved hook-bearing component with unchanged props', () => {
42
- let receivedProps: FederatedI18nBoundaryProps | undefined;
43
- function ResolvedBoundary(value: FederatedI18nBoundaryProps) {
44
- receivedProps = value;
45
- const [label] = useState('resolved');
46
- return (
47
- <section>
48
- {label}:{value.children}
49
- </section>
50
- );
51
- }
52
- const html = renderToStaticMarkup(
53
- <RuntimeComponentResolverContext.Provider
54
- value={(component, { name }) =>
55
- name === 'i18n.FederatedI18nBoundary' ? ResolvedBoundary : component
56
- }
57
- >
58
- <FederatedI18nBoundary {...props} />
59
- </RuntimeComponentResolverContext.Provider>,
60
- );
61
- expect(html).toBe('<section>resolved:<span>child content</span></section>');
62
- expect(receivedProps?.resources).toBe(props.resources);
63
- expect(receivedProps?.defaultNamespace).toBe('inventory');
64
- });
65
- });
@@ -1,60 +0,0 @@
1
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { describe, expect, test } from '@rstest/core';
5
- import {
6
- FsBackendWithSave,
7
- resolveFsBackendConstructor,
8
- } from '../src/runtime/i18n/backend/middleware.node';
9
-
10
- describe('i18n runtime utils', () => {
11
- test('normalizes node fs backend CJS and ESM namespace shapes', () => {
12
- class FakeBackend {}
13
-
14
- expect(resolveFsBackendConstructor(FakeBackend)).toBe(FakeBackend);
15
- expect(resolveFsBackendConstructor({ default: FakeBackend })).toBe(
16
- FakeBackend,
17
- );
18
- expect(resolveFsBackendConstructor({ 'module.exports': FakeBackend })).toBe(
19
- FakeBackend,
20
- );
21
- expect(
22
- resolveFsBackendConstructor({ default: { default: FakeBackend } }),
23
- ).toBe(FakeBackend);
24
- expect(
25
- resolveFsBackendConstructor({
26
- default: { 'module.exports': FakeBackend },
27
- }),
28
- ).toBe(FakeBackend);
29
- });
30
-
31
- test('loads translation resources through the node fs backend', async () => {
32
- const root = await mkdtemp(join(tmpdir(), 'i18n-backend-'));
33
-
34
- try {
35
- await mkdir(join(root, 'cs'), { recursive: true });
36
- await writeFile(
37
- join(root, 'cs', 'common.json'),
38
- JSON.stringify({ title: 'Ahoj' }),
39
- );
40
- const backend = new FsBackendWithSave(
41
- {},
42
- { loadPath: join(root, '{{lng}}', '{{ns}}.json') },
43
- {},
44
- );
45
- const resources = await new Promise<unknown>((resolve, reject) => {
46
- backend.read('cs', 'common', (error: Error | null, data: unknown) => {
47
- if (error) {
48
- reject(error);
49
- } else {
50
- resolve(data);
51
- }
52
- });
53
- });
54
-
55
- expect(resources).toEqual({ title: 'Ahoj' });
56
- } finally {
57
- await rm(root, { force: true, recursive: true });
58
- }
59
- });
60
- });