@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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/dist/cjs/cli/index.js +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from '@rstest/core';
|
|
2
|
+
import { buildLocalizedUrl, shouldIgnoreRedirect } from '../src/runtime/utils';
|
|
3
|
+
import { resolveMappedUrlStrategy } from '../src/server/mappedUrlStrategy';
|
|
4
|
+
import {
|
|
5
|
+
buildLocalizedUrl as buildServerLocalizedUrl,
|
|
6
|
+
shouldIgnoreRedirect as shouldIgnoreServerRedirect,
|
|
7
|
+
} from '../src/server/redirectPolicy';
|
|
8
|
+
import {
|
|
9
|
+
asI18nUrlStrategy,
|
|
10
|
+
isUnusableI18nUrlStrategy,
|
|
11
|
+
} from '../src/shared/urlStrategy';
|
|
12
|
+
|
|
13
|
+
// What a configured `urlStrategy` looks like after the generated runtime
|
|
14
|
+
// registration has carried it through JSON: an object with no methods left.
|
|
15
|
+
const serializedStrategy = {} as any;
|
|
16
|
+
|
|
17
|
+
const workingStrategy = {
|
|
18
|
+
localizePathname: (pathname: string, language: string) =>
|
|
19
|
+
`/${language}${pathname}`,
|
|
20
|
+
canonicalPathname: (pathname: string) => pathname,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createRequest = (pathname: string) =>
|
|
24
|
+
({ url: `https://example.test${pathname}` }) as any;
|
|
25
|
+
|
|
26
|
+
describe('URL strategy that lost its methods in transit', () => {
|
|
27
|
+
test('is recognised as unusable rather than accepted', () => {
|
|
28
|
+
expect(asI18nUrlStrategy(serializedStrategy)).toBeUndefined();
|
|
29
|
+
expect(isUnusableI18nUrlStrategy(serializedStrategy)).toBe(true);
|
|
30
|
+
expect(isUnusableI18nUrlStrategy(undefined)).toBe(false);
|
|
31
|
+
expect(asI18nUrlStrategy(workingStrategy)).toBe(workingStrategy);
|
|
32
|
+
expect(isUnusableI18nUrlStrategy(workingStrategy)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a partial strategy is unusable too', () => {
|
|
36
|
+
expect(asI18nUrlStrategy({ localizePathname: () => '/x' })).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('the client link builder falls back instead of throwing', () => {
|
|
40
|
+
expect(() =>
|
|
41
|
+
buildLocalizedUrl('/about', 'cs', ['en', 'cs'], serializedStrategy),
|
|
42
|
+
).not.toThrow();
|
|
43
|
+
expect(
|
|
44
|
+
buildLocalizedUrl('/about', 'cs', ['en', 'cs'], serializedStrategy),
|
|
45
|
+
).toBe('/cs/about');
|
|
46
|
+
expect(
|
|
47
|
+
buildLocalizedUrl('/about', 'cs', ['en', 'cs'], workingStrategy),
|
|
48
|
+
).toBe('/cs/about');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('the client redirect check falls back instead of throwing', () => {
|
|
52
|
+
expect(() =>
|
|
53
|
+
shouldIgnoreRedirect(
|
|
54
|
+
'/about',
|
|
55
|
+
['en', 'cs'],
|
|
56
|
+
undefined,
|
|
57
|
+
serializedStrategy,
|
|
58
|
+
),
|
|
59
|
+
).not.toThrow();
|
|
60
|
+
expect(
|
|
61
|
+
shouldIgnoreRedirect(
|
|
62
|
+
'/about',
|
|
63
|
+
['en', 'cs'],
|
|
64
|
+
undefined,
|
|
65
|
+
serializedStrategy,
|
|
66
|
+
),
|
|
67
|
+
).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('the server redirect builder falls back instead of throwing', () => {
|
|
71
|
+
expect(() =>
|
|
72
|
+
buildServerLocalizedUrl(
|
|
73
|
+
createRequest('/about'),
|
|
74
|
+
'/*',
|
|
75
|
+
'cs',
|
|
76
|
+
['en', 'cs'],
|
|
77
|
+
serializedStrategy,
|
|
78
|
+
),
|
|
79
|
+
).not.toThrow();
|
|
80
|
+
expect(
|
|
81
|
+
buildServerLocalizedUrl(
|
|
82
|
+
createRequest('/about'),
|
|
83
|
+
'/*',
|
|
84
|
+
'cs',
|
|
85
|
+
['en', 'cs'],
|
|
86
|
+
serializedStrategy,
|
|
87
|
+
),
|
|
88
|
+
).toBe('/cs/about');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the server redirect skip check falls back instead of throwing', () => {
|
|
92
|
+
expect(() =>
|
|
93
|
+
shouldIgnoreServerRedirect(
|
|
94
|
+
'/about',
|
|
95
|
+
'/*',
|
|
96
|
+
undefined,
|
|
97
|
+
serializedStrategy,
|
|
98
|
+
['en', 'cs'],
|
|
99
|
+
),
|
|
100
|
+
).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('serializable mapped locale URLs', () => {
|
|
105
|
+
test('produce a working strategy on the server', () => {
|
|
106
|
+
const strategy = resolveMappedUrlStrategy({
|
|
107
|
+
languages: ['en', 'cs'],
|
|
108
|
+
localisedUrls: { '/about': { en: '/about', cs: '/o-nas' } },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(strategy).toBeDefined();
|
|
112
|
+
expect(strategy?.localizePathname('/about', 'cs', ['en', 'cs'])).toBe(
|
|
113
|
+
'/cs/o-nas',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('are absent when no map is configured', () => {
|
|
118
|
+
expect(resolveMappedUrlStrategy(undefined)).toBeUndefined();
|
|
119
|
+
expect(resolveMappedUrlStrategy({ languages: ['en'] })).toBeUndefined();
|
|
120
|
+
expect(resolveMappedUrlStrategy({ localisedUrls: {} })).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
"jsx": "preserve",
|
|
7
7
|
// FORK: `baseUrl` removed. TypeScript 7 / tsgo reject it outright (TS5102),
|
|
8
8
|
// so this package's own tsconfig could not be typechecked at all. `paths`
|
|
9
|
-
//
|
|
9
|
+
// needs no baseUrl.
|
|
10
10
|
"isolatedModules": true,
|
|
11
|
-
"paths": {
|
|
11
|
+
"paths": {
|
|
12
|
+
"@modern-js/plugin-i18n/runtime/contexts": ["./src/runtime/contexts.ts"]
|
|
13
|
+
},
|
|
12
14
|
"rootDir": "./src"
|
|
13
15
|
},
|
|
14
16
|
"include": ["src", "types"]
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { expect, test } from '@rstest/core';
|
|
2
|
-
import i18next, { type i18n } from 'i18next';
|
|
3
|
-
import { act, useState } from 'react';
|
|
4
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
5
|
-
import { renderToString } from 'react-dom/server';
|
|
6
|
-
import { I18nextProvider, useTranslation } from 'react-i18next';
|
|
7
|
-
import { ModernI18nProvider } from '../src/runtime/context';
|
|
8
|
-
import { FederatedI18nBoundary, useModernI18n } from '../src/runtime/core';
|
|
9
|
-
|
|
10
|
-
const remoteResources = {
|
|
11
|
-
cs: {
|
|
12
|
-
inventory: {
|
|
13
|
-
'inventory.widgetBody': 'C1 skladová plocha vlastněná MicroVerticalem.',
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
en: {
|
|
17
|
-
inventory: {
|
|
18
|
-
'inventory.widgetBody':
|
|
19
|
-
'C1 inventory surface owned by the MicroVertical.',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function InventoryCopy() {
|
|
25
|
-
const modernI18n = useModernI18n<i18n>();
|
|
26
|
-
const reactI18n = useTranslation('inventory');
|
|
27
|
-
const reactI18nInstance = reactI18n.i18n as i18n & { __original?: i18n };
|
|
28
|
-
const sharesScopedInstance =
|
|
29
|
-
reactI18nInstance === modernI18n.i18nInstance ||
|
|
30
|
-
reactI18nInstance.__original === modernI18n.i18nInstance;
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<p data-testid="inventory-copy">
|
|
34
|
-
{modernI18n.t('inventory.widgetBody')}|
|
|
35
|
-
{reactI18n.t('inventory.widgetBody')}|{String(sharesScopedInstance)}
|
|
36
|
-
</p>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function Host({ hostI18n }: { hostI18n: i18n }) {
|
|
41
|
-
const [language, setLanguage] = useState('en');
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<ModernI18nProvider
|
|
45
|
-
i18nextProvider={I18nextProvider}
|
|
46
|
-
value={{
|
|
47
|
-
i18nInstance: hostI18n,
|
|
48
|
-
language,
|
|
49
|
-
languages: ['en', 'cs'],
|
|
50
|
-
}}
|
|
51
|
-
>
|
|
52
|
-
<button
|
|
53
|
-
onClick={async () => {
|
|
54
|
-
await hostI18n.changeLanguage('cs');
|
|
55
|
-
setLanguage('cs');
|
|
56
|
-
}}
|
|
57
|
-
type="button"
|
|
58
|
-
>
|
|
59
|
-
cs
|
|
60
|
-
</button>
|
|
61
|
-
<FederatedI18nBoundary
|
|
62
|
-
defaultNamespace="inventory"
|
|
63
|
-
fallbackLanguage="en"
|
|
64
|
-
resources={remoteResources}
|
|
65
|
-
supportedLanguages={['en', 'cs']}
|
|
66
|
-
>
|
|
67
|
-
<InventoryCopy />
|
|
68
|
-
</FederatedI18nBoundary>
|
|
69
|
-
</ModernI18nProvider>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function createHostI18n() {
|
|
74
|
-
const instance = i18next.createInstance();
|
|
75
|
-
await instance.init({
|
|
76
|
-
defaultNS: 'inventory',
|
|
77
|
-
fallbackLng: 'en',
|
|
78
|
-
initImmediate: false,
|
|
79
|
-
lng: 'en',
|
|
80
|
-
resources: {
|
|
81
|
-
cs: {
|
|
82
|
-
inventory: {
|
|
83
|
-
'inventory.widgetBody': 'stale host Czech copy',
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
en: {
|
|
87
|
-
inventory: {
|
|
88
|
-
'inventory.widgetBody': 'stale host English copy',
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
supportedLngs: ['en', 'cs'],
|
|
93
|
-
});
|
|
94
|
-
return instance;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
test('hydrates remote-owned copy and follows the host language', async () => {
|
|
98
|
-
const serverI18n = await createHostI18n();
|
|
99
|
-
const container = document.createElement('div');
|
|
100
|
-
container.innerHTML = renderToString(<Host hostI18n={serverI18n} />);
|
|
101
|
-
document.body.appendChild(container);
|
|
102
|
-
|
|
103
|
-
const clientI18n = await createHostI18n();
|
|
104
|
-
const hydrationErrors: unknown[] = [];
|
|
105
|
-
const root = hydrateRoot(container, <Host hostI18n={clientI18n} />, {
|
|
106
|
-
onRecoverableError: error => hydrationErrors.push(error),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
await act(async () => undefined);
|
|
110
|
-
expect(
|
|
111
|
-
container.querySelector('[data-testid="inventory-copy"]')?.textContent,
|
|
112
|
-
).toBe(
|
|
113
|
-
'C1 inventory surface owned by the MicroVertical.|C1 inventory surface owned by the MicroVertical.|true',
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
await act(async () => {
|
|
117
|
-
(container.querySelector('button') as HTMLButtonElement).click();
|
|
118
|
-
});
|
|
119
|
-
expect(
|
|
120
|
-
container.querySelector('[data-testid="inventory-copy"]')?.textContent,
|
|
121
|
-
).toBe(
|
|
122
|
-
'C1 skladová plocha vlastněná MicroVerticalem.|C1 skladová plocha vlastněná MicroVerticalem.|true',
|
|
123
|
-
);
|
|
124
|
-
expect(clientI18n.t('inventory.widgetBody')).toBe('stale host Czech copy');
|
|
125
|
-
expect(hydrationErrors).toEqual([]);
|
|
126
|
-
|
|
127
|
-
await act(async () => root.unmount());
|
|
128
|
-
container.remove();
|
|
129
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@rstest/core';
|
|
2
|
-
import i18next, { type i18n } from 'i18next';
|
|
3
|
-
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
-
import { I18nextProvider, useTranslation } from 'react-i18next';
|
|
5
|
-
import { ModernI18nProvider } from '../src/runtime/context';
|
|
6
|
-
import { FederatedI18nBoundary, useModernI18n } from '../src/runtime/core';
|
|
7
|
-
|
|
8
|
-
const C0_COPY = 'C0 copy bundled by the shell';
|
|
9
|
-
const C1_COPY =
|
|
10
|
-
'C1 operational independence: inventory UI and localization moved together.';
|
|
11
|
-
const HOST_ONLY_INVENTORY_COPY = 'inventory copy owned only by the shell';
|
|
12
|
-
const HOST_ONLY_SHELL_COPY = 'private shell copy';
|
|
13
|
-
|
|
14
|
-
function InventoryCopy() {
|
|
15
|
-
const modernI18n = useModernI18n<i18n>();
|
|
16
|
-
const reactI18n = useTranslation('inventory');
|
|
17
|
-
const reactI18nInstance = reactI18n.i18n as i18n & { __original?: i18n };
|
|
18
|
-
const sharesScopedInstance =
|
|
19
|
-
reactI18nInstance === modernI18n.i18nInstance ||
|
|
20
|
-
reactI18nInstance.__original === modernI18n.i18nInstance;
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<p>
|
|
24
|
-
{modernI18n.t('inventory.widgetBody')}|
|
|
25
|
-
{reactI18n.t('inventory.widgetBody')}|{String(sharesScopedInstance)}|
|
|
26
|
-
{modernI18n.t('inventory.hostOnly')}|{modernI18n.t('shell:private')}|
|
|
27
|
-
{reactI18n.t('inventory.hostOnly')}|{reactI18n.t('shell:private')}
|
|
28
|
-
</p>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('FederatedI18nBoundary', () => {
|
|
33
|
-
test('renders remote-owned resources instead of stale host resources', async () => {
|
|
34
|
-
const hostI18n = i18next.createInstance();
|
|
35
|
-
await hostI18n.init({
|
|
36
|
-
defaultNS: 'inventory',
|
|
37
|
-
fallbackLng: 'en',
|
|
38
|
-
initImmediate: false,
|
|
39
|
-
lng: 'en',
|
|
40
|
-
ns: ['inventory'],
|
|
41
|
-
resources: {
|
|
42
|
-
en: {
|
|
43
|
-
inventory: {
|
|
44
|
-
'inventory.hostOnly': HOST_ONLY_INVENTORY_COPY,
|
|
45
|
-
'inventory.widgetBody': C0_COPY,
|
|
46
|
-
},
|
|
47
|
-
shell: {
|
|
48
|
-
private: HOST_ONLY_SHELL_COPY,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
supportedLngs: ['en', 'cs'],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const html = renderToStaticMarkup(
|
|
56
|
-
<ModernI18nProvider
|
|
57
|
-
i18nextProvider={I18nextProvider}
|
|
58
|
-
value={{
|
|
59
|
-
i18nInstance: hostI18n,
|
|
60
|
-
language: 'en',
|
|
61
|
-
languages: ['en', 'cs'],
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
<FederatedI18nBoundary
|
|
65
|
-
defaultNamespace="inventory"
|
|
66
|
-
resources={{
|
|
67
|
-
en: {
|
|
68
|
-
inventory: {
|
|
69
|
-
'inventory.widgetBody': C1_COPY,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
<InventoryCopy />
|
|
75
|
-
</FederatedI18nBoundary>
|
|
76
|
-
</ModernI18nProvider>,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
expect(html).toContain(C1_COPY);
|
|
80
|
-
expect(html).not.toContain(C0_COPY);
|
|
81
|
-
expect(html).toContain(`${C1_COPY}|${C1_COPY}|true`);
|
|
82
|
-
expect(html).not.toContain(HOST_ONLY_INVENTORY_COPY);
|
|
83
|
-
expect(html).not.toContain(HOST_ONLY_SHELL_COPY);
|
|
84
|
-
expect(hostI18n.t('inventory.widgetBody')).toBe(C0_COPY);
|
|
85
|
-
expect(hostI18n.t('inventory.hostOnly')).toBe(HOST_ONLY_INVENTORY_COPY);
|
|
86
|
-
expect(hostI18n.t('shell:private')).toBe(HOST_ONLY_SHELL_COPY);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('fails before mutation when cloneInstance does not isolate the host store', async () => {
|
|
90
|
-
const hostI18n = i18next.createInstance();
|
|
91
|
-
await hostI18n.init({
|
|
92
|
-
defaultNS: 'inventory',
|
|
93
|
-
initImmediate: false,
|
|
94
|
-
lng: 'en',
|
|
95
|
-
resources: {
|
|
96
|
-
en: {
|
|
97
|
-
inventory: {
|
|
98
|
-
'inventory.widgetBody': C0_COPY,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
hostI18n.cloneInstance = () => hostI18n;
|
|
104
|
-
|
|
105
|
-
expect(() =>
|
|
106
|
-
renderToStaticMarkup(
|
|
107
|
-
<ModernI18nProvider
|
|
108
|
-
i18nextProvider={I18nextProvider}
|
|
109
|
-
value={{
|
|
110
|
-
i18nInstance: hostI18n,
|
|
111
|
-
language: 'en',
|
|
112
|
-
languages: ['en'],
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
<FederatedI18nBoundary
|
|
116
|
-
defaultNamespace="inventory"
|
|
117
|
-
resources={{
|
|
118
|
-
en: {
|
|
119
|
-
inventory: {
|
|
120
|
-
'inventory.widgetBody': C1_COPY,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
<InventoryCopy />
|
|
126
|
-
</FederatedI18nBoundary>
|
|
127
|
-
</ModernI18nProvider>,
|
|
128
|
-
),
|
|
129
|
-
).toThrow(/did not isolate the host resource store/u);
|
|
130
|
-
expect(hostI18n.t('inventory.widgetBody')).toBe(C0_COPY);
|
|
131
|
-
});
|
|
132
|
-
});
|
package/tests/linkTypes.test.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
const fixtureDir = path.resolve(__dirname, 'type-fixture');
|
|
5
|
-
|
|
6
|
-
function resolveTsgoBin() {
|
|
7
|
-
const pkgPath = require.resolve('@typescript/native-preview/package.json');
|
|
8
|
-
const pkgDir = path.dirname(pkgPath);
|
|
9
|
-
const pkg = require(pkgPath) as {
|
|
10
|
-
bin?:
|
|
11
|
-
| string
|
|
12
|
-
| {
|
|
13
|
-
tsgo?: string;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
const binEntry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.tsgo;
|
|
17
|
-
return path.resolve(pkgDir, binEntry ?? 'bin/tsgo.js');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const tsgoBin = resolveTsgoBin();
|
|
21
|
-
const useShell = process.platform === 'win32';
|
|
22
|
-
const execOptions = { cwd: path.dirname(__dirname), shell: useShell };
|
|
23
|
-
execFileSync('pnpm', ['build'], execOptions);
|
|
24
|
-
|
|
25
|
-
describe('Link type-level tests', () => {
|
|
26
|
-
test('fixture type-checks correctly: valid uses compile, invalid uses are rejected', () => {
|
|
27
|
-
try {
|
|
28
|
-
execFileSync(
|
|
29
|
-
process.execPath,
|
|
30
|
-
[tsgoBin, '--noEmit', '-p', 'tsconfig.json'],
|
|
31
|
-
{
|
|
32
|
-
cwd: fixtureDir,
|
|
33
|
-
stdio: 'pipe',
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
} catch (e: any) {
|
|
37
|
-
const stdout = e?.stdout ? String(e.stdout) : '';
|
|
38
|
-
const stderr = e?.stderr ? String(e.stderr) : '';
|
|
39
|
-
throw new Error(`TypeScript type-check failed:\n${stdout}\n${stderr}`);
|
|
40
|
-
}
|
|
41
|
-
}, 60_000);
|
|
42
|
-
|
|
43
|
-
test('consumer declarations avoid toolchain internals', () => {
|
|
44
|
-
const output = execFileSync(process.execPath, [
|
|
45
|
-
tsgoBin,
|
|
46
|
-
'--ignoreConfig',
|
|
47
|
-
'--listFilesOnly',
|
|
48
|
-
'--module',
|
|
49
|
-
'preserve',
|
|
50
|
-
'--moduleResolution',
|
|
51
|
-
'bundler',
|
|
52
|
-
path.resolve(__dirname, '../dist/types/runtime/context.d.ts'),
|
|
53
|
-
]);
|
|
54
|
-
const files = output.toString().replaceAll('\\', '/');
|
|
55
|
-
expect(files).not.toMatch(
|
|
56
|
-
/\/packages\/(cli|runtime\/plugin-runtime\/dist\/types|solutions\/app-tools|toolkit\/(types|utils))\//u,
|
|
57
|
-
);
|
|
58
|
-
}, 60_000);
|
|
59
|
-
});
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { LocalisedUrlsMap } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
-
import { describe, expect, test } from '@rstest/core';
|
|
3
|
-
import {
|
|
4
|
-
interpolateRouteParams,
|
|
5
|
-
normalizeSearch,
|
|
6
|
-
} from '../src/runtime/linkHelpers';
|
|
7
|
-
import { canonicalPath, localizePath } from '../src/runtime/localizedPaths';
|
|
8
|
-
import { buildLocalizedUrl } from '../src/runtime/utils';
|
|
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
|
-
});
|
package/tests/peerCohort.test.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'node:module';
|
|
2
|
-
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
const pluginManifest = require('../package.json') as {
|
|
5
|
-
peerDependencies: Record<string, string>;
|
|
6
|
-
peerDependenciesMeta: Record<string, { optional?: boolean }>;
|
|
7
|
-
};
|
|
8
|
-
const runtimeManifest = require('../../plugin-runtime/package.json') as {
|
|
9
|
-
peerDependencies: Record<string, string>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
describe('plugin-i18n peer cohort', () => {
|
|
13
|
-
test('matches the required runtime React cohort without overstating i18n floors', () => {
|
|
14
|
-
expect(pluginManifest.peerDependencies).toMatchObject({
|
|
15
|
-
i18next: '>=25.7.4',
|
|
16
|
-
react: runtimeManifest.peerDependencies.react,
|
|
17
|
-
'react-dom': runtimeManifest.peerDependencies['react-dom'],
|
|
18
|
-
'react-i18next': '>=15.7.4',
|
|
19
|
-
});
|
|
20
|
-
expect(pluginManifest.peerDependenciesMeta).toMatchObject({
|
|
21
|
-
i18next: { optional: true },
|
|
22
|
-
'react-i18next': { optional: true },
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type I18nInstance,
|
|
3
|
-
useModernI18n,
|
|
4
|
-
} from '@modern-js/plugin-i18n/runtime';
|
|
5
|
-
import i18next, { type i18n } from 'i18next';
|
|
6
|
-
|
|
7
|
-
// WRITE direction — this is what had zero coverage and was broken before the
|
|
8
|
-
// top-level index signature was removed from `I18nInstance`. Eight integration
|
|
9
|
-
// fixtures (tests/integration/i18n/*/src/modern.runtime.tsx) write exactly
|
|
10
|
-
// this and none of them is ever typechecked; this file is the guard.
|
|
11
|
-
export const w1: I18nInstance = i18next.createInstance();
|
|
12
|
-
export const w2: I18nInstance = i18next;
|
|
13
|
-
|
|
14
|
-
// READ direction.
|
|
15
|
-
declare const inst: I18nInstance;
|
|
16
|
-
export const s: string = inst.t('k');
|
|
17
|
-
export const b: boolean = inst.exists!('k');
|
|
18
|
-
export const langs: readonly string[] = inst.languages!;
|
|
19
|
-
export const fixed: string = inst.getFixedT!('en')('k');
|
|
20
|
-
|
|
21
|
-
// W2 generic, both with and without an explicit type argument.
|
|
22
|
-
type StarlingLike = i18n & { setLang?: (l: string) => void | Promise<void> };
|
|
23
|
-
export function useTyped() {
|
|
24
|
-
const r = useModernI18n<StarlingLike>();
|
|
25
|
-
return r.i18nInstance.t('k') satisfies string;
|
|
26
|
-
}
|
|
27
|
-
export function useDefault() {
|
|
28
|
-
return useModernI18n().i18nInstance.t('k') satisfies string;
|
|
29
|
-
}
|