@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.33 → 3.5.0-ultramodern.35

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.
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.5.0-ultramodern.33",
20
+ "version": "3.5.0-ultramodern.35",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -97,15 +97,15 @@
97
97
  "i18next-fs-backend": "^2.6.6",
98
98
  "i18next-http-backend": "^4.0.0",
99
99
  "i18next-http-middleware": "^3.9.7",
100
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.33",
101
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.33",
102
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.33",
103
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.33",
104
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.33",
105
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.33"
100
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.5.0-ultramodern.35",
101
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.35",
102
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.35",
103
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.35",
104
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.35",
105
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.5.0-ultramodern.35"
106
106
  },
107
107
  "peerDependencies": {
108
- "@modern-js/runtime": "3.5.0-ultramodern.33",
108
+ "@modern-js/runtime": "3.5.0-ultramodern.35",
109
109
  "i18next": ">=26.3.1",
110
110
  "react": "^19.2.7",
111
111
  "react-dom": "^19.2.7",
@@ -129,8 +129,8 @@
129
129
  "react-i18next": "17.0.8",
130
130
  "ts-node": "^10.9.2",
131
131
  "typescript": "^6.0.3",
132
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.33",
133
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.33"
132
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.35",
133
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.35"
134
134
  },
135
135
  "sideEffects": false,
136
136
  "publishConfig": {
package/rstest.config.mts CHANGED
@@ -29,6 +29,7 @@ export default {
29
29
  include: [
30
30
  'tests/i18nUtils.test.ts',
31
31
  'tests/localisedUrls.test.ts',
32
+ 'tests/localisedUrlRewriteMatrix.fork.test.ts',
32
33
  'tests/linkTypes.test.ts',
33
34
  'tests/backendDefaults.test.ts',
34
35
  'tests/reactI18nextRuntimeBoundary.test.ts',
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from '@rstest/core';
2
+ import {
3
+ interpolateRouteParams,
4
+ normalizeSearch,
5
+ } from '../src/runtime/linkHelpers';
6
+ import { canonicalPath, localizePath } from '../src/runtime/localizedPaths';
7
+ import { buildLocalizedUrl } from '../src/runtime/utils';
8
+ import type { LocalisedUrlsMap } from '../src/shared/localisedUrls';
9
+
10
+ const languages = ['en', 'cs'];
11
+
12
+ const localisedUrls = {
13
+ '/products/:slug': {
14
+ en: '/products/:slug',
15
+ cs: '/produkty/:slug',
16
+ },
17
+ '/files/*': {
18
+ en: '/files/*',
19
+ cs: '/soubory/*',
20
+ },
21
+ '/app/products/:slug': {
22
+ en: '/app/products/:slug',
23
+ cs: '/app/produkty/:slug',
24
+ },
25
+ } satisfies LocalisedUrlsMap;
26
+
27
+ const pathsConfig = {
28
+ languages,
29
+ localisedUrls,
30
+ };
31
+
32
+ describe('fork-owned localised URL rewrite matrix', () => {
33
+ const rewriteScenarios = [
34
+ {
35
+ name: 'adds the target locale prefix and localises canonical segments',
36
+ target: '/products/red-shoe',
37
+ language: 'cs',
38
+ expected: '/cs/produkty/red-shoe',
39
+ },
40
+ {
41
+ name: 'strips a case-insensitive source locale before relocalising',
42
+ target: '/CS/produkty/red%20shoe?tag=boots#details',
43
+ language: 'en',
44
+ expected: '/en/products/red%20shoe?tag=boots#details',
45
+ },
46
+ {
47
+ name: 'preserves repeated query keys and the fragment',
48
+ target: '/products/red-shoe?tag=boots&tag=sale#details',
49
+ language: 'cs',
50
+ expected: '/cs/produkty/red-shoe?tag=boots&tag=sale#details',
51
+ },
52
+ {
53
+ name: 'keeps a base-path segment inside the localised route map',
54
+ target: '/app/products/red-shoe?ref=nav#details',
55
+ language: 'cs',
56
+ expected: '/cs/app/produkty/red-shoe?ref=nav#details',
57
+ },
58
+ {
59
+ name: 'keeps the default locale explicitly prefixed',
60
+ target: '/products/red-shoe',
61
+ language: 'en',
62
+ expected: '/en/products/red-shoe',
63
+ },
64
+ {
65
+ name: 'does not double-prefix an already-localised URL',
66
+ target: '/cs/produkty/red-shoe?tag=boots#details',
67
+ language: 'cs',
68
+ expected: '/cs/produkty/red-shoe?tag=boots#details',
69
+ },
70
+ ];
71
+
72
+ for (const scenario of rewriteScenarios) {
73
+ test(scenario.name, () => {
74
+ expect(
75
+ buildLocalizedUrl(
76
+ scenario.target,
77
+ scenario.language,
78
+ languages,
79
+ localisedUrls,
80
+ ),
81
+ ).toBe(scenario.expected);
82
+ });
83
+ }
84
+
85
+ test('canonical and localized path helpers strip and add prefixes exactly', () => {
86
+ expect(
87
+ canonicalPath('/CS/produkty/red-shoe?tag=boots#details', pathsConfig),
88
+ ).toBe('/products/red-shoe?tag=boots#details');
89
+ expect(
90
+ localizePath('/products/red-shoe?tag=boots#details', 'cs', pathsConfig),
91
+ ).toBe('/cs/produkty/red-shoe?tag=boots#details');
92
+ });
93
+
94
+ test('splat params preserve separators and percent-encode each segment', () => {
95
+ const target = interpolateRouteParams('/files/*', {
96
+ '*': 'resume drafts/Q1 deck.pdf',
97
+ });
98
+
99
+ expect(target).toBe('/files/resume%20drafts/Q1%20deck.pdf');
100
+ expect(buildLocalizedUrl(target, 'cs', languages, localisedUrls)).toBe(
101
+ '/cs/soubory/resume%20drafts/Q1%20deck.pdf',
102
+ );
103
+ });
104
+
105
+ const searchScenarios: Array<{
106
+ name: string;
107
+ search: Parameters<typeof normalizeSearch>[0];
108
+ searchFromTo: string;
109
+ expected: ReturnType<typeof normalizeSearch>;
110
+ }> = [
111
+ {
112
+ name: 'object array values are preserved',
113
+ search: { tag: ['boots', 'sale'], page: 2 },
114
+ searchFromTo: '?ignored=1',
115
+ expected: {
116
+ searchString: '?tag=boots&tag=sale&page=2',
117
+ searchObject: { tag: ['boots', 'sale'], page: '2' },
118
+ },
119
+ },
120
+ {
121
+ name: 'target query arrays are preserved',
122
+ search: undefined,
123
+ searchFromTo: '?tag=boots&tag=sale&page=2',
124
+ expected: {
125
+ searchString: '?tag=boots&tag=sale&page=2',
126
+ searchObject: { tag: ['boots', 'sale'], page: '2' },
127
+ },
128
+ },
129
+ {
130
+ name: 'empty search clears the target query',
131
+ search: '',
132
+ searchFromTo: '?tag=boots&tag=sale',
133
+ expected: {
134
+ searchString: '',
135
+ searchObject: undefined,
136
+ },
137
+ },
138
+ ];
139
+
140
+ for (const scenario of searchScenarios) {
141
+ test(`normalizes search: ${scenario.name}`, () => {
142
+ expect(normalizeSearch(scenario.search, scenario.searchFromTo)).toEqual(
143
+ scenario.expected,
144
+ );
145
+ });
146
+ }
147
+ });