@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.12 → 3.2.0-ultramodern.120

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 (117) hide show
  1. package/README.md +221 -11
  2. package/dist/cjs/cli/index.js +32 -5
  3. package/dist/cjs/runtime/I18nLink.js +17 -28
  4. package/dist/cjs/runtime/Link.js +252 -0
  5. package/dist/cjs/runtime/canonicalRoutes.js +18 -0
  6. package/dist/cjs/runtime/context.js +41 -10
  7. package/dist/cjs/runtime/hooks.js +17 -10
  8. package/dist/cjs/runtime/i18n/backend/config.js +9 -5
  9. package/dist/cjs/runtime/i18n/backend/defaults.js +15 -10
  10. package/dist/cjs/runtime/i18n/backend/defaults.node.js +16 -11
  11. package/dist/cjs/runtime/i18n/backend/index.js +9 -5
  12. package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
  13. package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
  14. package/dist/cjs/runtime/i18n/backend/middleware.node.js +13 -9
  15. package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
  16. package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
  17. package/dist/cjs/runtime/i18n/detection/config.js +9 -5
  18. package/dist/cjs/runtime/i18n/detection/index.js +9 -5
  19. package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
  20. package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
  21. package/dist/cjs/runtime/i18n/index.js +9 -5
  22. package/dist/cjs/runtime/i18n/instance.js +17 -37
  23. package/dist/cjs/runtime/i18n/react-i18next.js +53 -0
  24. package/dist/cjs/runtime/i18n/utils.js +9 -17
  25. package/dist/cjs/runtime/index.js +50 -15
  26. package/dist/cjs/runtime/localizedPaths.js +105 -0
  27. package/dist/cjs/runtime/routerAdapter.js +167 -0
  28. package/dist/cjs/runtime/utils.js +87 -97
  29. package/dist/cjs/server/index.js +69 -13
  30. package/dist/cjs/shared/deepMerge.js +12 -8
  31. package/dist/cjs/shared/detection.js +9 -5
  32. package/dist/cjs/shared/localisedUrls.js +271 -0
  33. package/dist/cjs/shared/utils.js +15 -11
  34. package/dist/esm/cli/index.mjs +23 -0
  35. package/dist/esm/runtime/I18nLink.mjs +7 -22
  36. package/dist/esm/runtime/Link.mjs +209 -0
  37. package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
  38. package/dist/esm/runtime/context.mjs +34 -7
  39. package/dist/esm/runtime/hooks.mjs +9 -6
  40. package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
  41. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +2 -2
  42. package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
  43. package/dist/esm/runtime/i18n/instance.mjs +1 -19
  44. package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
  45. package/dist/esm/runtime/i18n/utils.mjs +0 -12
  46. package/dist/esm/runtime/index.mjs +23 -13
  47. package/dist/esm/runtime/localizedPaths.mjs +58 -0
  48. package/dist/esm/runtime/routerAdapter.mjs +129 -0
  49. package/dist/esm/runtime/utils.mjs +25 -30
  50. package/dist/esm/server/index.mjs +53 -7
  51. package/dist/esm/shared/localisedUrls.mjs +212 -0
  52. package/dist/esm-node/cli/index.mjs +23 -0
  53. package/dist/esm-node/runtime/I18nLink.mjs +7 -22
  54. package/dist/esm-node/runtime/Link.mjs +210 -0
  55. package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
  56. package/dist/esm-node/runtime/context.mjs +34 -7
  57. package/dist/esm-node/runtime/hooks.mjs +9 -6
  58. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
  59. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +2 -2
  60. package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
  61. package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
  62. package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
  63. package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
  64. package/dist/esm-node/runtime/index.mjs +23 -13
  65. package/dist/esm-node/runtime/localizedPaths.mjs +59 -0
  66. package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
  67. package/dist/esm-node/runtime/utils.mjs +25 -30
  68. package/dist/esm-node/server/index.mjs +53 -7
  69. package/dist/esm-node/shared/localisedUrls.mjs +213 -0
  70. package/dist/types/cli/index.d.ts +1 -0
  71. package/dist/types/runtime/I18nLink.d.ts +6 -0
  72. package/dist/types/runtime/Link.d.ts +56 -0
  73. package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
  74. package/dist/types/runtime/context.d.ts +3 -0
  75. package/dist/types/runtime/hooks.d.ts +4 -2
  76. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
  77. package/dist/types/runtime/i18n/instance.d.ts +4 -6
  78. package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
  79. package/dist/types/runtime/index.d.ts +6 -1
  80. package/dist/types/runtime/localizedPaths.d.ts +39 -0
  81. package/dist/types/runtime/routerAdapter.d.ts +26 -0
  82. package/dist/types/runtime/types.d.ts +1 -1
  83. package/dist/types/runtime/utils.d.ts +13 -9
  84. package/dist/types/server/index.d.ts +6 -0
  85. package/dist/types/shared/localisedUrls.d.ts +21 -0
  86. package/dist/types/shared/type.d.ts +12 -0
  87. package/package.json +24 -28
  88. package/rstest.config.mts +39 -0
  89. package/src/cli/index.ts +44 -1
  90. package/src/runtime/I18nLink.tsx +14 -51
  91. package/src/runtime/Link.tsx +414 -0
  92. package/src/runtime/canonicalRoutes.ts +93 -0
  93. package/src/runtime/context.tsx +45 -7
  94. package/src/runtime/hooks.ts +13 -4
  95. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  96. package/src/runtime/i18n/backend/defaults.ts +3 -1
  97. package/src/runtime/i18n/backend/middleware.node.ts +1 -1
  98. package/src/runtime/i18n/instance.ts +3 -30
  99. package/src/runtime/i18n/react-i18next.ts +25 -0
  100. package/src/runtime/i18n/utils.ts +4 -26
  101. package/src/runtime/index.tsx +47 -12
  102. package/src/runtime/localizedPaths.ts +118 -0
  103. package/src/runtime/routerAdapter.tsx +333 -0
  104. package/src/runtime/types.ts +1 -1
  105. package/src/runtime/utils.ts +44 -37
  106. package/src/server/index.ts +117 -10
  107. package/src/shared/localisedUrls.ts +453 -0
  108. package/src/shared/type.ts +12 -0
  109. package/tests/i18nUtils.test.ts +52 -0
  110. package/tests/link.test.tsx +475 -0
  111. package/tests/linkTypes.test.ts +28 -0
  112. package/tests/localisedUrls.test.ts +312 -0
  113. package/tests/routerAdapter.test.tsx +452 -0
  114. package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
  115. package/tests/type-fixture/tsconfig.json +15 -0
  116. package/dist/esm/rslib-runtime.mjs +0 -18
  117. package/dist/esm-node/rslib-runtime.mjs +0 -19
@@ -0,0 +1,312 @@
1
+ import type { NestedRouteForCli } from '@modern-js/types';
2
+ import { describe, expect, test } from '@rstest/core';
3
+ import {
4
+ collectApiPrefixes,
5
+ i18nServerPlugin,
6
+ matchesApiPrefix,
7
+ } from '../src/server';
8
+ import {
9
+ applyLocalisedUrlsToRoutes,
10
+ resolveLocalisedPath,
11
+ validateLocalisedUrls,
12
+ } from '../src/shared/localisedUrls';
13
+
14
+ const createRoute = (
15
+ path: string,
16
+ children?: NestedRouteForCli[],
17
+ ): NestedRouteForCli => ({
18
+ id: path,
19
+ path,
20
+ type: 'nested',
21
+ origin: 'file-system',
22
+ routeType: children ? 'layout' : 'page',
23
+ _component: `${path}.tsx`,
24
+ children,
25
+ });
26
+
27
+ const createRequestContext = (pathname: string) =>
28
+ ({
29
+ req: {
30
+ url: `http://localhost${pathname}`,
31
+ },
32
+ }) as any;
33
+
34
+ describe('localisedUrls', () => {
35
+ test('requires every localisable route path to define every language', () => {
36
+ const routes = [createRoute(':lang', [createRoute('terms-of-service')])];
37
+
38
+ expect(() =>
39
+ validateLocalisedUrls(routes, ['en', 'cs'], {
40
+ '/terms-of-service': {
41
+ en: '/terms-of-service',
42
+ },
43
+ }),
44
+ ).toThrow('missing languages: cs');
45
+ });
46
+
47
+ test('expands route paths to localised aliases', () => {
48
+ const routes = [
49
+ createRoute(':lang', [
50
+ createRoute('terms-of-service'),
51
+ createRoute('products', [createRoute(':slug')]),
52
+ ]),
53
+ ];
54
+
55
+ const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
56
+ '/terms-of-service': {
57
+ en: '/terms-of-service',
58
+ cs: '/podminky-pouzivani',
59
+ },
60
+ '/products': {
61
+ en: '/products',
62
+ cs: '/produkty',
63
+ },
64
+ '/products/:slug': {
65
+ en: '/products/:slug',
66
+ cs: '/produkty/:slug',
67
+ },
68
+ });
69
+
70
+ const localeRoute = localisedRoutes[0] as NestedRouteForCli;
71
+ expect(localeRoute.children?.map(route => route.path)).toEqual([
72
+ 'terms-of-service',
73
+ 'podminky-pouzivani',
74
+ 'products',
75
+ 'produkty',
76
+ ]);
77
+
78
+ const productRoutes = localeRoute.children?.filter(
79
+ route => route.path === 'products' || route.path === 'produkty',
80
+ );
81
+ expect(productRoutes?.[0].children?.map(route => route.path)).toEqual([
82
+ ':slug',
83
+ ]);
84
+ expect(productRoutes?.[1].children?.map(route => route.path)).toEqual([
85
+ ':slug',
86
+ ]);
87
+ });
88
+
89
+ test('expands flat locale-prefixed route paths with canonical keys', () => {
90
+ const routes = [
91
+ createRoute('/:lang/about'),
92
+ createRoute('/:lang/products/:slug'),
93
+ ];
94
+
95
+ const localisedRoutes = applyLocalisedUrlsToRoutes(routes, ['en', 'cs'], {
96
+ '/about': {
97
+ en: '/about',
98
+ cs: '/o-nas',
99
+ },
100
+ '/products/:slug': {
101
+ en: '/products/:slug',
102
+ cs: '/produkty/:slug',
103
+ },
104
+ });
105
+
106
+ expect(localisedRoutes.map(route => route.path)).toEqual([
107
+ ':lang/about',
108
+ ':lang/o-nas',
109
+ ':lang/products/:slug',
110
+ ':lang/produkty/:slug',
111
+ ]);
112
+ });
113
+
114
+ test('resolves current localized path to the target language', () => {
115
+ const localisedUrls = {
116
+ '/terms-of-service': {
117
+ en: '/terms-of-service',
118
+ cs: '/podminky-pouzivani',
119
+ },
120
+ '/products/:slug': {
121
+ en: '/products/:slug',
122
+ cs: '/produkty/:slug',
123
+ },
124
+ };
125
+
126
+ expect(
127
+ resolveLocalisedPath(
128
+ '/terms-of-service',
129
+ 'cs',
130
+ ['en', 'cs'],
131
+ localisedUrls,
132
+ ),
133
+ ).toBe('/podminky-pouzivani');
134
+ expect(
135
+ resolveLocalisedPath('/produkty/cervena-bota', 'en', ['en', 'cs'], {
136
+ ...localisedUrls,
137
+ '/products/:slug': {
138
+ en: '/products/:slug',
139
+ cs: '/produkty/:slug',
140
+ },
141
+ }),
142
+ ).toBe('/products/cervena-bota');
143
+ });
144
+
145
+ test('resolves optional route params', () => {
146
+ const localisedUrls = {
147
+ '/products/:slug?': {
148
+ en: '/products/:slug?',
149
+ cs: '/produkty/:slug?',
150
+ },
151
+ };
152
+
153
+ expect(
154
+ resolveLocalisedPath('/products', 'cs', ['en', 'cs'], localisedUrls),
155
+ ).toBe('/produkty');
156
+ expect(
157
+ resolveLocalisedPath(
158
+ '/produkty/cervena-bota',
159
+ 'en',
160
+ ['en', 'cs'],
161
+ localisedUrls,
162
+ ),
163
+ ).toBe('/products/cervena-bota');
164
+ });
165
+
166
+ test('resolves nested optional route params with translated ancestors', () => {
167
+ const localisedUrls = {
168
+ '/checkout': {
169
+ en: '/checkout',
170
+ cs: '/pokladna',
171
+ },
172
+ '/checkout/thank-you': {
173
+ en: '/checkout/thank-you',
174
+ cs: '/pokladna/dekujeme',
175
+ },
176
+ '/checkout/thank-you/:orderId?': {
177
+ en: '/checkout/thank-you/:orderId?',
178
+ cs: '/pokladna/dekujeme/:orderId?',
179
+ },
180
+ };
181
+
182
+ expect(
183
+ resolveLocalisedPath(
184
+ '/checkout/thank-you',
185
+ 'cs',
186
+ ['en', 'cs'],
187
+ localisedUrls,
188
+ ),
189
+ ).toBe('/pokladna/dekujeme');
190
+ expect(
191
+ resolveLocalisedPath(
192
+ '/pokladna/dekujeme/ABC-123',
193
+ 'en',
194
+ ['en', 'cs'],
195
+ localisedUrls,
196
+ ),
197
+ ).toBe('/checkout/thank-you/ABC-123');
198
+ });
199
+ });
200
+
201
+ describe('i18n server API prefix skips', () => {
202
+ test('collects API route prefixes and normalized BFF config prefixes', () => {
203
+ expect(
204
+ collectApiPrefixes(
205
+ [
206
+ { entryName: 'main', isApi: false, urlPath: '/' },
207
+ { isApi: true, urlPath: '/bff-api' },
208
+ { isApi: true, urlPath: '/rpc/*' },
209
+ { isApi: true, urlPath: '/' },
210
+ { isApi: true },
211
+ ],
212
+ ['bff-api/', '/internal-api'],
213
+ ),
214
+ ).toEqual(['/bff-api', '/rpc', '/internal-api']);
215
+ });
216
+
217
+ test('matches API prefixes by exact path or slash-delimited segment', () => {
218
+ const prefixes = ['/bff-api'];
219
+
220
+ expect(matchesApiPrefix('/bff-api', prefixes)).toBe(true);
221
+ expect(matchesApiPrefix('/bff-api/ping', prefixes)).toBe(true);
222
+ expect(matchesApiPrefix('/bff-api-v2', prefixes)).toBe(false);
223
+ expect(matchesApiPrefix('/bff-api-v2/ping', prefixes)).toBe(false);
224
+ });
225
+
226
+ test('skips language detector and redirect middleware for API routes', async () => {
227
+ const middlewares: any[] = [];
228
+ const routes = [
229
+ { entryName: 'main', entryPath: '', urlPath: '/' },
230
+ { entryPath: '', isApi: true, urlPath: '/bff-api' },
231
+ ];
232
+ let prepare: (() => void) | undefined;
233
+
234
+ i18nServerPlugin({
235
+ localeDetection: {
236
+ fallbackLanguage: 'en',
237
+ languages: ['en', 'cs'],
238
+ localePathRedirect: true,
239
+ },
240
+ staticRoutePrefixes: [],
241
+ }).setup({
242
+ getServerConfig: () => ({}),
243
+ getServerContext: () => ({ middlewares, routes }),
244
+ onPrepare: fn => {
245
+ prepare = fn;
246
+ },
247
+ } as any);
248
+
249
+ prepare?.();
250
+
251
+ const detectorMiddleware = middlewares.find(
252
+ middleware => middleware.name === 'i18n-language-detector',
253
+ );
254
+ const redirectMiddleware = middlewares.find(
255
+ middleware => middleware.name === 'i18n-server-middleware',
256
+ );
257
+
258
+ expect(detectorMiddleware).toBeDefined();
259
+ expect(redirectMiddleware).toBeDefined();
260
+
261
+ for (const middleware of [detectorMiddleware, redirectMiddleware]) {
262
+ let nextCalls = 0;
263
+ const response = await middleware.handler(
264
+ createRequestContext('/bff-api/ping'),
265
+ async () => {
266
+ nextCalls++;
267
+ },
268
+ );
269
+
270
+ expect(response).toBeUndefined();
271
+ expect(nextCalls).toBe(1);
272
+ }
273
+ });
274
+
275
+ test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
276
+ const middlewares: any[] = [];
277
+ const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
278
+ let prepare: (() => void) | undefined;
279
+
280
+ i18nServerPlugin({
281
+ localeDetection: {
282
+ fallbackLanguage: 'en',
283
+ languages: ['en', 'cs'],
284
+ localePathRedirect: true,
285
+ },
286
+ staticRoutePrefixes: [],
287
+ }).setup({
288
+ getServerConfig: () => ({ bff: {} }),
289
+ getServerContext: () => ({ middlewares, routes }),
290
+ onPrepare: fn => {
291
+ prepare = fn;
292
+ },
293
+ } as any);
294
+
295
+ prepare?.();
296
+
297
+ const redirectMiddleware = middlewares.find(
298
+ middleware => middleware.name === 'i18n-server-middleware',
299
+ );
300
+
301
+ let nextCalls = 0;
302
+ const response = await redirectMiddleware.handler(
303
+ createRequestContext('/api/ping'),
304
+ async () => {
305
+ nextCalls++;
306
+ },
307
+ );
308
+
309
+ expect(response).toBeUndefined();
310
+ expect(nextCalls).toBe(1);
311
+ });
312
+ });