@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,139 +0,0 @@
1
- import type { I18nInstance } from '../src/runtime/i18n';
2
- import { detectLanguageWithPriority } from '../src/runtime/i18n/detection';
3
- import { interpolateRouteParams } from '../src/runtime/Link';
4
- import { canonicalPath, localizePath } from '../src/runtime/localizedPaths';
5
- import { buildLocalizedUrl, splitUrlTarget } from '../src/runtime/utils';
6
-
7
- const languages = ['en', 'cs'];
8
-
9
- function createI18nInstance(language = 'en'): I18nInstance {
10
- return {
11
- language,
12
- isInitialized: true,
13
- init: () => Promise.resolve(undefined),
14
- use: () => {},
15
- t: (key: string | string[]) => (Array.isArray(key) ? key[0] : key),
16
- createInstance: () => createI18nInstance(language),
17
- services: {},
18
- options: {},
19
- };
20
- }
21
-
22
- describe('splitUrlTarget public compatibility export', () => {
23
- test('retains the shared suffix splitter', () => {
24
- expect(splitUrlTarget('/a?q=1#x')).toEqual({
25
- pathname: '/a',
26
- search: '?q=1',
27
- hash: '#x',
28
- });
29
- });
30
- });
31
-
32
- describe('buildLocalizedUrl suffix handling', () => {
33
- test('hash-only target keeps the hash and drops the trailing slash', () => {
34
- expect(buildLocalizedUrl('/#work-with-me', 'en', languages)).toBe(
35
- '/en#work-with-me',
36
- );
37
- });
38
-
39
- test('query-only target keeps the query', () => {
40
- expect(buildLocalizedUrl('/products?tag=x', 'en', languages)).toBe(
41
- '/en/products?tag=x',
42
- );
43
- });
44
-
45
- test('root path', () => {
46
- expect(buildLocalizedUrl('/', 'cs', languages)).toBe('/cs');
47
- });
48
- });
49
-
50
- describe('interpolateRouteParams', () => {
51
- test('interpolates $param and :param segments', () => {
52
- expect(interpolateRouteParams('/talks/$slug', { slug: 'ai slop' })).toBe(
53
- '/talks/ai%20slop',
54
- );
55
- expect(interpolateRouteParams('/talks/:slug', { slug: 'x' })).toBe(
56
- '/talks/x',
57
- );
58
- });
59
-
60
- test('drops missing optional segments', () => {
61
- expect(interpolateRouteParams('/opt/{-$slug}', {})).toBe('/opt');
62
- expect(interpolateRouteParams('/opt/:slug?', {})).toBe('/opt');
63
- expect(interpolateRouteParams('/opt/{-$slug}', { slug: 'v' })).toBe(
64
- '/opt/v',
65
- );
66
- });
67
-
68
- test('expands splat params', () => {
69
- expect(interpolateRouteParams('/files/$', { _splat: 'a/b' })).toBe(
70
- '/files/a/b',
71
- );
72
- expect(interpolateRouteParams('/files/*', { '*': 'a' })).toBe('/files/a');
73
- });
74
- });
75
-
76
- describe('native URL strategy helpers', () => {
77
- test('preserves suffixes when adding or stripping only a language prefix', () => {
78
- expect(localizePath('/products?q=1#x', 'cs', { languages })).toBe(
79
- '/cs/products?q=1#x',
80
- );
81
- expect(canonicalPath('/CS/products?q=1#x', { languages })).toBe(
82
- '/products?q=1#x',
83
- );
84
- });
85
- });
86
-
87
- describe('language detection priority', () => {
88
- test('path locale overrides stale SSR data', async () => {
89
- const previousSsrData = (window as any)._SSR_DATA;
90
- (window as any)._SSR_DATA = { data: { i18nData: { lng: 'en' } } };
91
-
92
- try {
93
- await expect(
94
- detectLanguageWithPriority(createI18nInstance('en'), {
95
- languages,
96
- fallbackLanguage: 'en',
97
- localePathRedirect: true,
98
- i18nextDetector: false,
99
- detection: {},
100
- userInitOptions: {},
101
- pathname: '/cs/produkty',
102
- ssrContext: undefined,
103
- }),
104
- ).resolves.toEqual({ detectedLanguage: 'cs', finalLanguage: 'cs' });
105
- } finally {
106
- if (previousSsrData === undefined) {
107
- delete (window as any)._SSR_DATA;
108
- } else {
109
- (window as any)._SSR_DATA = previousSsrData;
110
- }
111
- }
112
- });
113
-
114
- test('regional SSR language resolves to supported base language', async () => {
115
- const previousSsrData = (window as any)._SSR_DATA;
116
- (window as any)._SSR_DATA = { data: { i18nData: { lng: 'en-US' } } };
117
-
118
- try {
119
- await expect(
120
- detectLanguageWithPriority(createI18nInstance('cs'), {
121
- languages,
122
- fallbackLanguage: 'cs',
123
- localePathRedirect: true,
124
- i18nextDetector: false,
125
- detection: {},
126
- userInitOptions: {},
127
- pathname: '/products',
128
- ssrContext: undefined,
129
- }),
130
- ).resolves.toEqual({ detectedLanguage: 'en', finalLanguage: 'en' });
131
- } finally {
132
- if (previousSsrData === undefined) {
133
- delete (window as any)._SSR_DATA;
134
- } else {
135
- (window as any)._SSR_DATA = previousSsrData;
136
- }
137
- }
138
- });
139
- });
@@ -1,159 +0,0 @@
1
- import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
- import { createAsyncHook, createPluginManager } from '@modern-js/plugin';
3
- import {
4
- createContext,
5
- initAppContext,
6
- initPluginAPI,
7
- } from '@modern-js/plugin/cli';
8
- import type { NestedRouteForCli } from '@modern-js/types';
9
- import { describe, expect, test } from '@rstest/core';
10
- import { type I18nPluginOptions, i18nPlugin } from '../src/cli';
11
-
12
- /**
13
- * Localised route generation must reach a bare `appTools()` consumer.
14
- *
15
- * The fork used to register it only from `@modern-js/i18n-integration`, which
16
- * `ultramodernAppTools()` composes but plain `appTools()` does not — so an app
17
- * declaring `localeDetection.localisedUrls` got a 404 on every mapped path
18
- * under SSR. These cases deliberately register *only* the native plugin, with
19
- * no integration plugin in the manager, so a regression back to the opt-in
20
- * wiring fails here.
21
- */
22
-
23
- const createRoute = (
24
- path: string,
25
- children?: NestedRouteForCli[],
26
- ): NestedRouteForCli => ({
27
- id: path,
28
- path,
29
- type: 'nested',
30
- origin: 'file-system',
31
- routeType: children ? 'layout' : 'page',
32
- _component: `${path}.tsx`,
33
- children,
34
- });
35
-
36
- async function createNativeOnlyHarness(options: I18nPluginOptions = {}) {
37
- const manager = createPluginManager();
38
- const routeHost: CliPlugin<AppTools> = {
39
- name: 'test-route-host',
40
- registryHooks: { modifyFileSystemRoutes: createAsyncHook<any>() },
41
- setup() {
42
- // Only supplies the hook registry the app-tools router owns.
43
- },
44
- };
45
-
46
- manager.addPlugins([i18nPlugin(options), routeHost]);
47
- const plugins = manager.getPlugins() as CliPlugin<AppTools>[];
48
- const context = await createContext<AppTools>({
49
- appContext: initAppContext({
50
- packageName: 'plugin-i18n-native-only-test',
51
- configFile: false,
52
- command: 'build',
53
- appDirectory: process.cwd(),
54
- metaName: 'modern-js',
55
- plugins,
56
- }),
57
- config: {},
58
- normalizedConfig: {} as any,
59
- });
60
- const api = initPluginAPI<AppTools>({ context, pluginManager: manager });
61
- for (const plugin of plugins) {
62
- await plugin.setup?.(api);
63
- }
64
- return api.getHooks().modifyFileSystemRoutes.call;
65
- }
66
-
67
- const entrypoint = { entryName: 'main' } as any;
68
-
69
- const localisedUrls = {
70
- '/about': { en: '/about', cs: '/o-nas' },
71
- '/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' },
72
- };
73
-
74
- describe('native localised route generation', () => {
75
- test('expands mapped locale paths without the integration plugin', async () => {
76
- const modifyRoutes = await createNativeOnlyHarness({
77
- localeDetection: {
78
- localePathRedirect: true,
79
- languages: ['en', 'cs'],
80
- localisedUrls,
81
- },
82
- });
83
-
84
- const result = await modifyRoutes({
85
- entrypoint,
86
- routes: [
87
- createRoute(':lang', [
88
- createRoute('about'),
89
- createRoute('products/:slug'),
90
- ]),
91
- ],
92
- });
93
-
94
- const paths = result.routes[0].children?.map(
95
- (route: NestedRouteForCli) => route.path,
96
- );
97
- expect(paths).toContain('about');
98
- expect(paths).toContain('o-nas');
99
- expect(paths).toContain('products/:slug');
100
- expect(paths).toContain('produkty/:slug');
101
- });
102
-
103
- test('leaves routes untouched when no map is declared', async () => {
104
- const modifyRoutes = await createNativeOnlyHarness({
105
- localeDetection: {
106
- localePathRedirect: true,
107
- languages: ['en', 'cs'],
108
- },
109
- });
110
-
111
- const input = [createRoute(':lang', [createRoute('about')])];
112
- const result = await modifyRoutes({ entrypoint, routes: input });
113
-
114
- expect(
115
- result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
116
- ).toEqual(['about']);
117
- });
118
-
119
- test('leaves routes untouched without localePathRedirect', async () => {
120
- const modifyRoutes = await createNativeOnlyHarness({
121
- localeDetection: {
122
- languages: ['en', 'cs'],
123
- localisedUrls,
124
- },
125
- });
126
-
127
- const input = [createRoute(':lang', [createRoute('about')])];
128
- const result = await modifyRoutes({ entrypoint, routes: input });
129
-
130
- expect(
131
- result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
132
- ).toEqual(['about']);
133
- });
134
-
135
- test('resolves the map from a per-entry locale detection override', async () => {
136
- const modifyRoutes = await createNativeOnlyHarness({
137
- localeDetection: {
138
- localePathRedirect: true,
139
- languages: ['en'],
140
- localeDetectionByEntry: {
141
- main: {
142
- localePathRedirect: true,
143
- languages: ['en', 'cs'],
144
- localisedUrls,
145
- },
146
- },
147
- },
148
- });
149
-
150
- const result = await modifyRoutes({
151
- entrypoint,
152
- routes: [createRoute(':lang', [createRoute('about')])],
153
- });
154
-
155
- expect(
156
- result.routes[0].children?.map((r: NestedRouteForCli) => r.path),
157
- ).toContain('o-nas');
158
- });
159
- });
@@ -1,193 +0,0 @@
1
- import { describe, expect, test } from '@rstest/core';
2
- import { i18nPlugin as i18nCliPlugin } from '../src/cli';
3
- import {
4
- collectApiPrefixes,
5
- i18nServerPlugin,
6
- matchesApiPrefix,
7
- } from '../src/server';
8
-
9
- const createRequestContext = (pathname: string) =>
10
- ({
11
- req: {
12
- url: `http://localhost${pathname}`,
13
- },
14
- }) as any;
15
-
16
- describe('cli modifyFileSystemRoutes', () => {
17
- test('uses the no-react runtime entry when reactI18next is disabled', () => {
18
- let runtimePlugin:
19
- | {
20
- path: string;
21
- config: Record<string, unknown>;
22
- }
23
- | undefined;
24
-
25
- i18nCliPlugin({ reactI18next: false }).setup({
26
- _internalRuntimePlugins: (fn: any) => {
27
- const plugins: Array<{
28
- path: string;
29
- config: Record<string, unknown>;
30
- }> = [];
31
- fn({
32
- entrypoint: { entryName: 'main' },
33
- plugins,
34
- });
35
- runtimePlugin = plugins[0];
36
- },
37
- modifyFileSystemRoutes: () => {},
38
- _internalServerPlugins: () => {},
39
- getAppContext: () => ({
40
- appDirectory: process.cwd(),
41
- metaName: 'modern-js',
42
- }),
43
- getNormalizedConfig: () => ({}),
44
- } as any);
45
-
46
- expect(runtimePlugin?.path).toBe(
47
- '@modern-js/plugin-i18n/runtime/no-react-i18next',
48
- );
49
- expect(runtimePlugin?.config.reactI18next).toBe(false);
50
- });
51
-
52
- test('upstream-style configs without a map keep routes untouched', async () => {
53
- // The hook is registered unconditionally now that route expansion is a
54
- // native feature; what matters is that a config with no `localisedUrls`
55
- // map gets its routes back byte-for-byte instead of a locale-expanded tree.
56
- let handler:
57
- | ((args: { entrypoint: unknown; routes: unknown[] }) => Promise<{
58
- routes: unknown[];
59
- }>)
60
- | undefined;
61
-
62
- i18nCliPlugin({
63
- localeDetection: { localePathRedirect: true, languages: ['en', 'cs'] },
64
- }).setup({
65
- _internalRuntimePlugins: () => {},
66
- _internalServerPlugins: () => {},
67
- modifyFileSystemRoutes: (fn: any) => {
68
- handler = fn;
69
- },
70
- } as any);
71
-
72
- const routes = [{ id: 'about', path: 'about', type: 'nested' }];
73
- const result = await handler?.({
74
- entrypoint: { entryName: 'main' },
75
- routes,
76
- });
77
-
78
- expect(result?.routes).toEqual(routes);
79
- });
80
- });
81
-
82
- describe('i18n server API prefix skips', () => {
83
- test('collects API route prefixes and normalized BFF config prefixes', () => {
84
- expect(
85
- collectApiPrefixes(
86
- [
87
- { entryName: 'main', isApi: false, urlPath: '/' },
88
- { isApi: true, urlPath: '/bff-api' },
89
- { isApi: true, urlPath: '/rpc/*' },
90
- { isApi: true, urlPath: '/' },
91
- { isApi: true },
92
- ],
93
- ['bff-api/', '/internal-api'],
94
- ),
95
- ).toEqual(['/bff-api', '/rpc', '/internal-api']);
96
- });
97
-
98
- test('matches API prefixes by exact path or slash-delimited segment', () => {
99
- const prefixes = ['/bff-api'];
100
-
101
- expect(matchesApiPrefix('/bff-api', prefixes)).toBe(true);
102
- expect(matchesApiPrefix('/bff-api/ping', prefixes)).toBe(true);
103
- expect(matchesApiPrefix('/bff-api-v2', prefixes)).toBe(false);
104
- expect(matchesApiPrefix('/bff-api-v2/ping', prefixes)).toBe(false);
105
- });
106
-
107
- test('skips language detector and redirect middleware for API routes', async () => {
108
- const middlewares: any[] = [];
109
- const routes = [
110
- { entryName: 'main', entryPath: '', urlPath: '/' },
111
- { entryPath: '', isApi: true, urlPath: '/bff-api' },
112
- ];
113
- let prepare: (() => void) | undefined;
114
-
115
- i18nServerPlugin({
116
- localeDetection: {
117
- fallbackLanguage: 'en',
118
- languages: ['en', 'cs'],
119
- localePathRedirect: true,
120
- },
121
- staticRoutePrefixes: [],
122
- }).setup({
123
- getServerConfig: () => ({}),
124
- getServerContext: () => ({ middlewares, routes }),
125
- onPrepare: fn => {
126
- prepare = fn;
127
- },
128
- } as any);
129
-
130
- prepare?.();
131
-
132
- const detectorMiddleware = middlewares.find(
133
- middleware => middleware.name === 'i18n-language-detector',
134
- );
135
- const redirectMiddleware = middlewares.find(
136
- middleware => middleware.name === 'i18n-server-middleware',
137
- );
138
-
139
- expect(detectorMiddleware).toBeDefined();
140
- expect(redirectMiddleware).toBeDefined();
141
-
142
- for (const middleware of [detectorMiddleware, redirectMiddleware]) {
143
- let nextCalls = 0;
144
- const response = await middleware.handler(
145
- createRequestContext('/bff-api/ping'),
146
- async () => {
147
- nextCalls++;
148
- },
149
- );
150
-
151
- expect(response).toBeUndefined();
152
- expect(nextCalls).toBe(1);
153
- }
154
- });
155
-
156
- test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
157
- const middlewares: any[] = [];
158
- const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
159
- let prepare: (() => void) | undefined;
160
-
161
- i18nServerPlugin({
162
- localeDetection: {
163
- fallbackLanguage: 'en',
164
- languages: ['en', 'cs'],
165
- localePathRedirect: true,
166
- },
167
- staticRoutePrefixes: [],
168
- }).setup({
169
- getServerConfig: () => ({ bff: {} }),
170
- getServerContext: () => ({ middlewares, routes }),
171
- onPrepare: fn => {
172
- prepare = fn;
173
- },
174
- } as any);
175
-
176
- prepare?.();
177
-
178
- const redirectMiddleware = middlewares.find(
179
- middleware => middleware.name === 'i18n-server-middleware',
180
- );
181
-
182
- let nextCalls = 0;
183
- const response = await redirectMiddleware.handler(
184
- createRequestContext('/api/ping'),
185
- async () => {
186
- nextCalls++;
187
- },
188
- );
189
-
190
- expect(response).toBeUndefined();
191
- expect(nextCalls).toBe(1);
192
- });
193
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, test } from '@rstest/core';
2
- import { resolveMappedUrlStrategy } from '../src/shared/mappedUrlStrategy';
3
-
4
- /**
5
- * Mapped locale URLs must work for a bare `appTools()` consumer that only
6
- * declares `localeDetection.localisedUrls`. The map is plain data, so it
7
- * survives the JSON boundary a `urlStrategy` object does not; the runtime
8
- * derives its policy from it, and the server derives the same policy from the
9
- * same shared module.
10
- */
11
-
12
- const localisedUrls = {
13
- '/terms-of-service': { en: '/terms-of-service', cs: '/obchodni-podminky' },
14
- '/products/:slug': { en: '/products/:slug', cs: '/produkty/:slug' },
15
- };
16
- const languages = ['en', 'cs'];
17
-
18
- describe('mapped url strategy derivation', () => {
19
- test('derives a localizing strategy from localisedUrls', () => {
20
- const strategy = resolveMappedUrlStrategy({ localisedUrls });
21
-
22
- expect(strategy).toBeDefined();
23
- expect(
24
- strategy?.localizePathname?.('/terms-of-service', 'cs', languages),
25
- ).toBe('/cs/obchodni-podminky');
26
- expect(
27
- strategy?.localizePathname?.('/terms-of-service', 'en', languages),
28
- ).toBe('/en/terms-of-service');
29
- expect(
30
- strategy?.canonicalPathname?.('/cs/obchodni-podminky', languages),
31
- ).toBe('/terms-of-service');
32
- });
33
-
34
- test('round-trips a parameterised mapped route', () => {
35
- const strategy = resolveMappedUrlStrategy({ localisedUrls });
36
- const localized = strategy?.localizePathname?.(
37
- '/products/widget',
38
- 'cs',
39
- languages,
40
- );
41
-
42
- expect(localized).toBe('/cs/produkty/widget');
43
- expect(strategy?.canonicalPathname?.(localized!, languages)).toBe(
44
- '/products/widget',
45
- );
46
- });
47
-
48
- test('leaves unmapped paths on the plain language prefix', () => {
49
- const strategy = resolveMappedUrlStrategy({ localisedUrls });
50
-
51
- expect(strategy?.localizePathname?.('/unmapped', 'cs', languages)).toBe(
52
- '/cs/unmapped',
53
- );
54
- });
55
-
56
- test('derives nothing without a usable map', () => {
57
- expect(resolveMappedUrlStrategy(undefined)).toBeUndefined();
58
- expect(resolveMappedUrlStrategy({})).toBeUndefined();
59
- expect(resolveMappedUrlStrategy({ localisedUrls: {} })).toBeUndefined();
60
- expect(resolveMappedUrlStrategy({ localisedUrls: true })).toBeUndefined();
61
- });
62
- });
@@ -1,117 +0,0 @@
1
- import { mkdtemp, rm } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- import { describe, expect, test } from '@rstest/core';
6
- import { build } from 'esbuild';
7
- import i18next from 'i18next';
8
- import { createElement } from 'react';
9
- import { renderToStaticMarkup } from 'react-dom/server';
10
-
11
- const runtimeRoot = resolve(__dirname, '../src/runtime');
12
-
13
- describe('react-i18next runtime boundary', () => {
14
- test('bundles the disabled runtime entry when react-i18next is unavailable', async () => {
15
- await expect(
16
- build({
17
- bundle: true,
18
- entryPoints: [resolve(runtimeRoot, 'no-react-i18next.tsx')],
19
- format: 'esm',
20
- packages: 'external',
21
- platform: 'node',
22
- plugins: [
23
- {
24
- name: 'reject-react-i18next',
25
- setup(buildApi) {
26
- buildApi.onResolve({ filter: /^react-i18next$/ }, () => {
27
- throw new Error(
28
- 'disabled runtime entry reached optional react-i18next',
29
- );
30
- });
31
- },
32
- },
33
- ],
34
- write: false,
35
- }),
36
- ).resolves.toBeDefined();
37
- });
38
-
39
- test('gets router capabilities from the selected runtime provider', async () => {
40
- await expect(
41
- build({
42
- bundle: true,
43
- entryPoints: [resolve(runtimeRoot, 'routerAdapter.tsx')],
44
- format: 'esm',
45
- packages: 'external',
46
- platform: 'neutral',
47
- plugins: [
48
- {
49
- name: 'reject-direct-router-provider',
50
- setup(buildApi) {
51
- buildApi.onResolve(
52
- { filter: /^@modern-js\/runtime\/router$/ },
53
- () => {
54
- throw new Error(
55
- 'The i18n adapter loaded the React Router provider directly.',
56
- );
57
- },
58
- );
59
- },
60
- },
61
- ],
62
- write: false,
63
- }),
64
- ).resolves.toBeDefined();
65
- });
66
-
67
- test('shares Modern i18n context across independently bundled runtime copies', async () => {
68
- const tempDir = await mkdtemp(
69
- resolve(__dirname, '.modern-i18n-runtime-boundary-'),
70
- );
71
- try {
72
- const contextEntry = resolve(runtimeRoot, 'context.tsx');
73
- const copies = await Promise.all(
74
- ['copy-a.mjs', 'copy-b.mjs'].map(async filename => {
75
- const outfile = resolve(tempDir, filename);
76
- await build({
77
- bundle: true,
78
- entryPoints: [contextEntry],
79
- format: 'esm',
80
- jsx: 'automatic',
81
- outfile,
82
- packages: 'external',
83
- platform: 'node',
84
- });
85
- return import(pathToFileURL(outfile).href);
86
- }),
87
- );
88
- const [copyA, copyB] = copies;
89
- const instance = i18next.createInstance();
90
- await instance.init({
91
- initImmediate: false,
92
- lng: 'cs',
93
- resources: { cs: { translation: { language: 'Jazyk' } } },
94
- });
95
- const Consumer = () =>
96
- createElement('span', null, copyB.useModernI18n().language);
97
-
98
- const html = renderToStaticMarkup(
99
- createElement(
100
- copyA.ModernI18nProvider,
101
- {
102
- value: {
103
- i18nInstance: instance,
104
- language: 'cs',
105
- languages: ['en', 'cs'],
106
- },
107
- },
108
- createElement(Consumer),
109
- ),
110
- );
111
-
112
- expect(html).toBe('<span>cs</span>');
113
- } finally {
114
- await rm(tempDir, { force: true, recursive: true });
115
- }
116
- });
117
- });