@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.6 → 3.5.0-ultramodern.75
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 +2 -1
- package/dist/cjs/cli/locales.js +15 -15
- package/dist/cjs/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +78 -91
- package/dist/cjs/runtime/contextHelpers.js +143 -0
- package/dist/cjs/runtime/core.js +32 -132
- package/dist/cjs/runtime/hooks.js +54 -21
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
- package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
- package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
- package/dist/cjs/runtime/i18n/detection/index.js +8 -263
- package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
- package/dist/cjs/runtime/i18n/detection/language.js +62 -0
- package/dist/cjs/runtime/i18n/detection/path.js +49 -0
- package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
- package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
- package/dist/cjs/runtime/i18n/detection/types.js +18 -0
- package/dist/cjs/runtime/i18n/instance.js +34 -10
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +115 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/runtime/routerAdapter.js +16 -8
- package/dist/cjs/runtime/utils.js +1 -9
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -144
- package/dist/cjs/server/redirectPolicy.js +96 -0
- package/dist/cjs/shared/detection.js +2 -104
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +91 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
- package/dist/cjs/shared/localisedUrls/routes.js +130 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +28 -294
- package/dist/cjs/shared/utils.js +2 -8
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/cli/locales.mjs +13 -13
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +76 -92
- package/dist/esm/runtime/contextHelpers.mjs +87 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/hooks.mjs +56 -23
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
- package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
- package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
- package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
- package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
- package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
- package/dist/esm/runtime/i18n/instance.mjs +35 -8
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +77 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/runtime/routerAdapter.mjs +16 -8
- package/dist/esm/runtime/utils.mjs +2 -10
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +8 -135
- package/dist/esm/server/redirectPolicy.mjs +46 -0
- package/dist/esm/shared/detection.mjs +1 -71
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +7 -0
- package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
- package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm/shared/utils.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/cli/locales.mjs +13 -13
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +76 -92
- package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/hooks.mjs +56 -23
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
- package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
- package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
- package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
- package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
- package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
- package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +78 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
- package/dist/esm-node/runtime/utils.mjs +2 -10
- package/dist/esm-node/server/apiPrefix.mjs +32 -0
- package/dist/esm-node/server/detectorOptions.mjs +60 -0
- package/dist/esm-node/server/index.mjs +8 -135
- package/dist/esm-node/server/redirectPolicy.mjs +47 -0
- package/dist/esm-node/shared/detection.mjs +1 -71
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
- package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
- package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/shared/utils.mjs +1 -1
- package/dist/types/cli/locales.d.ts +1 -1
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +21 -5
- package/dist/types/runtime/contextHelpers.d.ts +29 -0
- package/dist/types/runtime/core.d.ts +4 -8
- package/dist/types/runtime/hooks.d.ts +3 -7
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
- package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
- package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
- package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
- package/dist/types/runtime/i18n/instance.d.ts +31 -27
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- package/dist/types/runtime/i18n/utils.d.ts +14 -5
- package/dist/types/runtime/linkHelpers.d.ts +17 -0
- package/dist/types/runtime/pluginSetup.d.ts +32 -0
- package/dist/types/runtime/providerComposition.d.ts +16 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- package/dist/types/runtime/routerAdapter.d.ts +7 -6
- package/dist/types/runtime/utils.d.ts +7 -2
- package/dist/types/server/apiPrefix.d.ts +7 -0
- package/dist/types/server/detectorOptions.d.ts +21 -0
- package/dist/types/server/index.d.ts +1 -6
- package/dist/types/server/redirectPolicy.d.ts +22 -0
- package/dist/types/shared/detection.d.ts +1 -11
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +9 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
- package/dist/types/shared/localisedUrls/types.d.ts +14 -0
- package/dist/types/shared/localisedUrls.d.ts +2 -36
- package/dist/types/shared/utils.d.ts +0 -2
- package/package.json +17 -20
- package/rstest.config.mts +8 -1
- package/src/cli/index.ts +5 -4
- package/src/cli/locales.ts +3 -3
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +170 -207
- package/src/runtime/contextHelpers.ts +242 -0
- package/src/runtime/core.tsx +48 -275
- package/src/runtime/hooks.ts +86 -24
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/middleware.common.ts +5 -4
- package/src/runtime/i18n/detection/cache.ts +189 -0
- package/src/runtime/i18n/detection/detector.ts +166 -0
- package/src/runtime/i18n/detection/index.ts +10 -641
- package/src/runtime/i18n/detection/initOptions.ts +74 -0
- package/src/runtime/i18n/detection/language.ts +64 -0
- package/src/runtime/i18n/detection/middleware.node.ts +26 -7
- package/src/runtime/i18n/detection/middleware.ts +15 -7
- package/src/runtime/i18n/detection/path.ts +41 -0
- package/src/runtime/i18n/detection/priority.ts +82 -0
- package/src/runtime/i18n/detection/ssr.ts +47 -0
- package/src/runtime/i18n/detection/types.ts +41 -0
- package/src/runtime/i18n/instance.ts +98 -34
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/i18n/utils.ts +18 -9
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +148 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/runtime/routerAdapter.tsx +36 -15
- package/src/runtime/utils.ts +13 -35
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +14 -319
- package/src/server/redirectPolicy.ts +127 -0
- package/src/shared/detection.ts +1 -1
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +24 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +60 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/redirect.ts +93 -0
- package/src/shared/localisedUrls/resolve.ts +135 -0
- package/src/shared/localisedUrls/routes.ts +234 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +26 -623
- package/src/shared/utils.ts +2 -2
- package/tests/federatedI18nBoundary.client.test.tsx +112 -0
- package/tests/federatedI18nBoundary.test.tsx +120 -0
- package/tests/fixtures/localised-routes.golden.json +156 -0
- package/tests/link.test.tsx +111 -0
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
- package/tests/localisedUrls.test.ts +148 -2
- package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
- package/tests/redirectPolicy.test.ts +45 -0
- package/tests/routerAdapter.test.tsx +603 -0
- package/tests/type-fixture/fixture-globals.d.ts +11 -0
- package/tests/type-fixture/tsconfig.json +1 -0
|
@@ -3,15 +3,23 @@ import {
|
|
|
3
3
|
InternalRuntimeContext,
|
|
4
4
|
RuntimeContext,
|
|
5
5
|
} from '@modern-js/runtime/context';
|
|
6
|
+
import {
|
|
7
|
+
createMemoryRouter,
|
|
8
|
+
Link as ReactRouterLink,
|
|
9
|
+
RouterProvider,
|
|
10
|
+
} from '@modern-js/runtime/router';
|
|
11
|
+
import i18next from 'i18next';
|
|
6
12
|
import type React from 'react';
|
|
7
13
|
import type { ComponentType, PropsWithChildren } from 'react';
|
|
8
14
|
import { act, useState } from 'react';
|
|
9
15
|
import { createRoot, type Root } from 'react-dom/client';
|
|
16
|
+
import { useTranslation } from 'react-i18next';
|
|
10
17
|
import { i18nPlugin } from '../src/runtime';
|
|
11
18
|
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
12
19
|
import { I18nLink } from '../src/runtime/I18nLink';
|
|
13
20
|
import type { I18nInstance } from '../src/runtime/i18n';
|
|
14
21
|
import { getReactI18nextIntegration } from '../src/runtime/i18n/react-i18next';
|
|
22
|
+
import { createI18nRootWrapper } from '../src/runtime/providerComposition';
|
|
15
23
|
|
|
16
24
|
(
|
|
17
25
|
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
@@ -110,6 +118,173 @@ function collectI18nWrapRoot() {
|
|
|
110
118
|
return wrapRoot;
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
async function collectI18nRuntime(
|
|
122
|
+
i18nInstance: I18nInstance,
|
|
123
|
+
reactI18next = false,
|
|
124
|
+
) {
|
|
125
|
+
let onBeforeRender:
|
|
126
|
+
| ((
|
|
127
|
+
context: ReturnType<typeof createTanstackRuntimeContext>,
|
|
128
|
+
) => Promise<void>)
|
|
129
|
+
| undefined;
|
|
130
|
+
let wrapRoot: ((App: ComponentType<any>) => ComponentType<any>) | undefined;
|
|
131
|
+
|
|
132
|
+
i18nPlugin({
|
|
133
|
+
i18nInstance,
|
|
134
|
+
reactI18next,
|
|
135
|
+
localeDetection: {
|
|
136
|
+
fallbackLanguage: 'en',
|
|
137
|
+
i18nextDetector: false,
|
|
138
|
+
languages: ['en', 'cs'],
|
|
139
|
+
localePathRedirect: true,
|
|
140
|
+
},
|
|
141
|
+
}).setup?.({
|
|
142
|
+
getRuntimeConfig: () => ({}),
|
|
143
|
+
onBeforeRender: (
|
|
144
|
+
callback: (
|
|
145
|
+
context: ReturnType<typeof createTanstackRuntimeContext>,
|
|
146
|
+
) => Promise<void>,
|
|
147
|
+
) => {
|
|
148
|
+
onBeforeRender = callback;
|
|
149
|
+
},
|
|
150
|
+
wrapRoot: (callback: (App: ComponentType<any>) => ComponentType<any>) => {
|
|
151
|
+
wrapRoot = callback;
|
|
152
|
+
},
|
|
153
|
+
} as any);
|
|
154
|
+
|
|
155
|
+
if (!onBeforeRender || !wrapRoot) {
|
|
156
|
+
throw new Error('Expected i18n runtime plugin lifecycle registrations');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { onBeforeRender, wrapRoot };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function createEventEmittingDeferredI18nInstance() {
|
|
163
|
+
const instance = i18next.createInstance();
|
|
164
|
+
await instance.init({
|
|
165
|
+
lng: 'en',
|
|
166
|
+
fallbackLng: 'en',
|
|
167
|
+
resources: {
|
|
168
|
+
en: { translation: { languageSwitcher: 'Language' } },
|
|
169
|
+
cs: { translation: { languageSwitcher: 'Jazyk' } },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const originalHasLoadedNamespace = instance.hasLoadedNamespace;
|
|
173
|
+
instance.hasLoadedNamespace = function hasLoadedNamespace(
|
|
174
|
+
namespace,
|
|
175
|
+
options,
|
|
176
|
+
) {
|
|
177
|
+
if (this !== instance) {
|
|
178
|
+
throw new Error('i18next hasLoadedNamespace receiver was not preserved');
|
|
179
|
+
}
|
|
180
|
+
return originalHasLoadedNamespace.call(this, namespace, options);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const pending: Array<{
|
|
184
|
+
language: string;
|
|
185
|
+
promise: Promise<unknown>;
|
|
186
|
+
resolve: () => void;
|
|
187
|
+
}> = [];
|
|
188
|
+
const translator = (instance as unknown as { translator: I18nInstance })
|
|
189
|
+
.translator;
|
|
190
|
+
|
|
191
|
+
instance.changeLanguage = rstest.fn((language = 'en') => {
|
|
192
|
+
let resolveChange!: (value: unknown) => void;
|
|
193
|
+
const promise = new Promise<unknown>(resolve => {
|
|
194
|
+
resolveChange = resolve;
|
|
195
|
+
});
|
|
196
|
+
pending.push({
|
|
197
|
+
language,
|
|
198
|
+
promise,
|
|
199
|
+
resolve: () => {
|
|
200
|
+
instance.language = language;
|
|
201
|
+
translator.changeLanguage?.(language);
|
|
202
|
+
instance.emit('languageChanged', language);
|
|
203
|
+
resolveChange(instance.t.bind(instance));
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return promise;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return { instance, pending };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createMutableTanstackRouter(pathname = '/en') {
|
|
213
|
+
let location = {
|
|
214
|
+
pathname,
|
|
215
|
+
searchStr: '',
|
|
216
|
+
hash: '',
|
|
217
|
+
};
|
|
218
|
+
let matches = [{ params: { lang: pathname.slice(1) } }];
|
|
219
|
+
const listeners = new Set<() => void>();
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
navigate: rstest.fn(async () => undefined),
|
|
223
|
+
stores: {
|
|
224
|
+
location: {
|
|
225
|
+
get: () => location,
|
|
226
|
+
subscribe: (listener: () => void) => {
|
|
227
|
+
listeners.add(listener);
|
|
228
|
+
return () => listeners.delete(listener);
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
matches: {
|
|
232
|
+
get: () => matches,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
publishPathname(nextPathname: string) {
|
|
236
|
+
location = {
|
|
237
|
+
pathname: nextPathname,
|
|
238
|
+
searchStr: '',
|
|
239
|
+
hash: '',
|
|
240
|
+
};
|
|
241
|
+
matches = [{ params: { lang: nextPathname.slice(1) } }];
|
|
242
|
+
for (const listener of listeners) {
|
|
243
|
+
listener();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createDeferredI18nInstance() {
|
|
250
|
+
const resources = {
|
|
251
|
+
en: { languageSwitcher: 'Language' },
|
|
252
|
+
cs: { languageSwitcher: 'Jazyk' },
|
|
253
|
+
};
|
|
254
|
+
const pending: Array<{
|
|
255
|
+
language: keyof typeof resources;
|
|
256
|
+
promise: Promise<void>;
|
|
257
|
+
resolve: () => void;
|
|
258
|
+
reject: (error: Error) => void;
|
|
259
|
+
}> = [];
|
|
260
|
+
const instance = createI18nInstance('en');
|
|
261
|
+
delete instance.setLang;
|
|
262
|
+
instance.t = (key: string) =>
|
|
263
|
+
resources[instance.language as keyof typeof resources][
|
|
264
|
+
key as keyof (typeof resources)['en']
|
|
265
|
+
];
|
|
266
|
+
instance.changeLanguage = rstest.fn((language = 'en') => {
|
|
267
|
+
let resolveChange!: () => void;
|
|
268
|
+
let rejectChange!: (error: Error) => void;
|
|
269
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
270
|
+
resolveChange = resolve;
|
|
271
|
+
rejectChange = reject;
|
|
272
|
+
});
|
|
273
|
+
pending.push({
|
|
274
|
+
language: language as keyof typeof resources,
|
|
275
|
+
promise,
|
|
276
|
+
resolve: () => {
|
|
277
|
+
instance.language = language;
|
|
278
|
+
resolveChange();
|
|
279
|
+
},
|
|
280
|
+
reject: rejectChange,
|
|
281
|
+
});
|
|
282
|
+
return promise;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return { instance, pending };
|
|
286
|
+
}
|
|
287
|
+
|
|
113
288
|
function createTanstackRouter(pathname = '/en/terms-of-service', lang = 'en') {
|
|
114
289
|
const url = new URL(pathname, 'https://modernjs.test');
|
|
115
290
|
|
|
@@ -179,6 +354,30 @@ async function renderWithRuntime(
|
|
|
179
354
|
};
|
|
180
355
|
}
|
|
181
356
|
|
|
357
|
+
async function renderWithRuntimeContexts(
|
|
358
|
+
node: React.ReactNode,
|
|
359
|
+
runtimeContext: ReturnType<typeof createTanstackRuntimeContext>,
|
|
360
|
+
) {
|
|
361
|
+
const container = document.createElement('div');
|
|
362
|
+
document.body.appendChild(container);
|
|
363
|
+
const root = createRoot(container);
|
|
364
|
+
|
|
365
|
+
await act(async () => {
|
|
366
|
+
root.render(
|
|
367
|
+
<InternalRuntimeContext.Provider value={runtimeContext}>
|
|
368
|
+
<RuntimeContext.Provider value={runtimeContext}>
|
|
369
|
+
{node}
|
|
370
|
+
</RuntimeContext.Provider>
|
|
371
|
+
</InternalRuntimeContext.Provider>,
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
container,
|
|
377
|
+
root,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
182
381
|
function cleanup(rendered?: { container: HTMLElement; root: Root }) {
|
|
183
382
|
if (!rendered) {
|
|
184
383
|
return;
|
|
@@ -195,6 +394,8 @@ describe('i18n runtime wrapRoot', () => {
|
|
|
195
394
|
afterEach(() => {
|
|
196
395
|
cleanup(rendered);
|
|
197
396
|
rendered = undefined;
|
|
397
|
+
rstest.restoreAllMocks();
|
|
398
|
+
window.history.replaceState(null, '', '/');
|
|
198
399
|
});
|
|
199
400
|
|
|
200
401
|
test('renders children when no root App exists yet', async () => {
|
|
@@ -228,6 +429,291 @@ describe('i18n runtime wrapRoot', () => {
|
|
|
228
429
|
).toBe('root');
|
|
229
430
|
expect(rendered.container.textContent).toContain('router content');
|
|
230
431
|
});
|
|
432
|
+
|
|
433
|
+
test('keeps the optional i18next provider inside Modern i18n context', async () => {
|
|
434
|
+
const i18nInstance = createI18nInstance('cs');
|
|
435
|
+
const observedLanguages: string[] = [];
|
|
436
|
+
const I18nextProvider = ({
|
|
437
|
+
children,
|
|
438
|
+
i18n,
|
|
439
|
+
}: PropsWithChildren<{ i18n: I18nInstance }>) => {
|
|
440
|
+
const { language } = useModernI18n();
|
|
441
|
+
observedLanguages.push(`${language}:${i18n.language}`);
|
|
442
|
+
|
|
443
|
+
return <section data-testid="i18next-provider">{children}</section>;
|
|
444
|
+
};
|
|
445
|
+
const App = () => <main>router content</main>;
|
|
446
|
+
const I18nRoot = createI18nRootWrapper({
|
|
447
|
+
htmlLangAttr: false,
|
|
448
|
+
localePathRedirect: false,
|
|
449
|
+
languages: ['en', 'cs'],
|
|
450
|
+
fallbackLanguage: 'en',
|
|
451
|
+
getLatestI18nInstance: () => i18nInstance,
|
|
452
|
+
getI18nextProvider: () => I18nextProvider,
|
|
453
|
+
})(App);
|
|
454
|
+
|
|
455
|
+
rendered = await renderI18nRoot(<I18nRoot />);
|
|
456
|
+
|
|
457
|
+
expect(observedLanguages).toEqual(['cs:cs']);
|
|
458
|
+
expect(rendered.container.textContent).toContain('router content');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('commits route language and translated copy after the instance changes', async () => {
|
|
462
|
+
window.history.replaceState(null, '', '/en');
|
|
463
|
+
const router = createMutableTanstackRouter();
|
|
464
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
465
|
+
const { instance, pending } = createDeferredI18nInstance();
|
|
466
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
467
|
+
await onBeforeRender(runtimeContext);
|
|
468
|
+
|
|
469
|
+
const Translation = ({ id }: { id: string }) => {
|
|
470
|
+
const { language, t } = useModernI18n();
|
|
471
|
+
return (
|
|
472
|
+
<span data-testid={id}>
|
|
473
|
+
{language}:{t('languageSwitcher')}
|
|
474
|
+
</span>
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
const App = () => (
|
|
478
|
+
<main>
|
|
479
|
+
<Translation id="first" />
|
|
480
|
+
<Translation id="second" />
|
|
481
|
+
</main>
|
|
482
|
+
);
|
|
483
|
+
const I18nRoot = wrapRoot(App);
|
|
484
|
+
|
|
485
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
486
|
+
|
|
487
|
+
expect(rendered.container.textContent).toBe('en:Languageen:Language');
|
|
488
|
+
|
|
489
|
+
await act(async () => {
|
|
490
|
+
router.publishPathname('/cs');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(instance.changeLanguage).toHaveBeenCalledTimes(1);
|
|
494
|
+
expect(instance.changeLanguage).toHaveBeenCalledWith('cs');
|
|
495
|
+
expect(rendered.container.textContent).toBe('en:Languageen:Language');
|
|
496
|
+
|
|
497
|
+
await act(async () => {
|
|
498
|
+
pending[0].resolve();
|
|
499
|
+
await pending[0].promise;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(rendered.container.textContent).toBe('cs:Jazykcs:Jazyk');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('uses the wrapper language operation without changing twice', async () => {
|
|
506
|
+
window.history.replaceState(null, '', '/en');
|
|
507
|
+
const router = createMutableTanstackRouter();
|
|
508
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
509
|
+
const instance = createI18nInstance();
|
|
510
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
511
|
+
await onBeforeRender(runtimeContext);
|
|
512
|
+
|
|
513
|
+
const App = () => {
|
|
514
|
+
const { language } = useModernI18n();
|
|
515
|
+
return <main>{language}</main>;
|
|
516
|
+
};
|
|
517
|
+
const I18nRoot = wrapRoot(App);
|
|
518
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
519
|
+
|
|
520
|
+
await act(async () => {
|
|
521
|
+
router.publishPathname('/cs');
|
|
522
|
+
await Promise.resolve();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(instance.setLang).toHaveBeenCalledTimes(1);
|
|
526
|
+
expect(instance.setLang).toHaveBeenCalledWith('cs');
|
|
527
|
+
expect(instance.changeLanguage).not.toHaveBeenCalled();
|
|
528
|
+
expect(rendered.container.textContent).toBe('cs');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('keeps the latest route when language changes resolve out of order', async () => {
|
|
532
|
+
window.history.replaceState(null, '', '/en');
|
|
533
|
+
const router = createMutableTanstackRouter();
|
|
534
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
535
|
+
const { instance, pending } = createDeferredI18nInstance();
|
|
536
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
537
|
+
await onBeforeRender(runtimeContext);
|
|
538
|
+
|
|
539
|
+
const App = () => {
|
|
540
|
+
const { language, t } = useModernI18n();
|
|
541
|
+
return (
|
|
542
|
+
<main>
|
|
543
|
+
{language}:{t('languageSwitcher')}
|
|
544
|
+
</main>
|
|
545
|
+
);
|
|
546
|
+
};
|
|
547
|
+
const I18nRoot = wrapRoot(App);
|
|
548
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
549
|
+
|
|
550
|
+
await act(async () => {
|
|
551
|
+
router.publishPathname('/cs');
|
|
552
|
+
});
|
|
553
|
+
expect(pending).toHaveLength(1);
|
|
554
|
+
|
|
555
|
+
await act(async () => {
|
|
556
|
+
router.publishPathname('/en');
|
|
557
|
+
});
|
|
558
|
+
expect(rendered.container.textContent).toBe('en:Language');
|
|
559
|
+
|
|
560
|
+
await act(async () => {
|
|
561
|
+
pending[0].resolve();
|
|
562
|
+
await pending[0].promise;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(rendered.container.textContent).toBe('en:Language');
|
|
566
|
+
expect(pending).toHaveLength(2);
|
|
567
|
+
expect(pending[1].language).toBe('en');
|
|
568
|
+
|
|
569
|
+
await act(async () => {
|
|
570
|
+
pending[1].resolve();
|
|
571
|
+
await pending[1].promise;
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(rendered.container.textContent).toBe('en:Language');
|
|
575
|
+
expect(instance.language).toBe('en');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('does not block a replacement instance behind abandoned language work', async () => {
|
|
579
|
+
window.history.replaceState(null, '', '/en');
|
|
580
|
+
const router = createMutableTanstackRouter();
|
|
581
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
582
|
+
const oldI18n = createDeferredI18nInstance();
|
|
583
|
+
runtimeContext.i18nInstance = oldI18n.instance;
|
|
584
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
585
|
+
oldI18n.instance,
|
|
586
|
+
);
|
|
587
|
+
await onBeforeRender(runtimeContext);
|
|
588
|
+
|
|
589
|
+
const App = () => {
|
|
590
|
+
const { language } = useModernI18n();
|
|
591
|
+
return <main>{language}</main>;
|
|
592
|
+
};
|
|
593
|
+
const I18nRoot = wrapRoot(App);
|
|
594
|
+
rendered = await renderWithRuntimeContexts(<I18nRoot />, runtimeContext);
|
|
595
|
+
|
|
596
|
+
await act(async () => {
|
|
597
|
+
router.publishPathname('/cs');
|
|
598
|
+
});
|
|
599
|
+
expect(oldI18n.pending).toHaveLength(1);
|
|
600
|
+
|
|
601
|
+
const replacementI18n = createDeferredI18nInstance();
|
|
602
|
+
const replacementContext = createTanstackRuntimeContext(router);
|
|
603
|
+
replacementContext.i18nInstance = replacementI18n.instance;
|
|
604
|
+
await act(async () => {
|
|
605
|
+
rendered?.root.render(
|
|
606
|
+
<InternalRuntimeContext.Provider value={replacementContext}>
|
|
607
|
+
<RuntimeContext.Provider value={replacementContext}>
|
|
608
|
+
<I18nRoot />
|
|
609
|
+
</RuntimeContext.Provider>
|
|
610
|
+
</InternalRuntimeContext.Provider>,
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(replacementI18n.pending).toHaveLength(1);
|
|
615
|
+
expect(replacementI18n.pending[0].language).toBe('cs');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('does not expose an obsolete route language through react-i18next', async () => {
|
|
619
|
+
window.history.replaceState(null, '', '/en');
|
|
620
|
+
const router = createMutableTanstackRouter();
|
|
621
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
622
|
+
const { instance, pending } =
|
|
623
|
+
await createEventEmittingDeferredI18nInstance();
|
|
624
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
625
|
+
instance,
|
|
626
|
+
true,
|
|
627
|
+
);
|
|
628
|
+
await onBeforeRender(runtimeContext);
|
|
629
|
+
|
|
630
|
+
const App = () => {
|
|
631
|
+
const { t } = useTranslation();
|
|
632
|
+
return <main>{t('languageSwitcher')}</main>;
|
|
633
|
+
};
|
|
634
|
+
const I18nRoot = wrapRoot(App);
|
|
635
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
636
|
+
|
|
637
|
+
await act(async () => {
|
|
638
|
+
router.publishPathname('/cs');
|
|
639
|
+
});
|
|
640
|
+
expect(pending).toHaveLength(1);
|
|
641
|
+
|
|
642
|
+
await act(async () => {
|
|
643
|
+
router.publishPathname('/en');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await act(async () => {
|
|
647
|
+
pending[0].resolve();
|
|
648
|
+
await pending[0].promise;
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(rendered.container.textContent).toBe('Language');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('keeps committed copy after a failed change and recovers later', async () => {
|
|
655
|
+
window.history.replaceState(null, '', '/en');
|
|
656
|
+
const consoleError = rstest
|
|
657
|
+
.spyOn(console, 'error')
|
|
658
|
+
.mockImplementation(() => undefined);
|
|
659
|
+
const router = createMutableTanstackRouter();
|
|
660
|
+
const runtimeContext = createTanstackRuntimeContext(router);
|
|
661
|
+
const { instance, pending } = createDeferredI18nInstance();
|
|
662
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(instance);
|
|
663
|
+
await onBeforeRender(runtimeContext);
|
|
664
|
+
|
|
665
|
+
const RetryConsumer = () => {
|
|
666
|
+
useModernI18n();
|
|
667
|
+
return null;
|
|
668
|
+
};
|
|
669
|
+
const App = () => {
|
|
670
|
+
const [showRetryConsumer, setShowRetryConsumer] = useState(false);
|
|
671
|
+
const { language, t } = useModernI18n();
|
|
672
|
+
return (
|
|
673
|
+
<main>
|
|
674
|
+
{language}:{t('languageSwitcher')}
|
|
675
|
+
<button type="button" onClick={() => setShowRetryConsumer(true)}>
|
|
676
|
+
Retry
|
|
677
|
+
</button>
|
|
678
|
+
{showRetryConsumer && <RetryConsumer />}
|
|
679
|
+
</main>
|
|
680
|
+
);
|
|
681
|
+
};
|
|
682
|
+
const I18nRoot = wrapRoot(App);
|
|
683
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
684
|
+
|
|
685
|
+
await act(async () => {
|
|
686
|
+
router.publishPathname('/cs');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const languageError = new Error('failed to load Czech resources');
|
|
690
|
+
await act(async () => {
|
|
691
|
+
pending[0].reject(languageError);
|
|
692
|
+
await pending[0].promise.catch(() => undefined);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(rendered.container.textContent).toContain('en:Language');
|
|
696
|
+
expect(consoleError).toHaveBeenCalledTimes(1);
|
|
697
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
698
|
+
'Failed to synchronize i18n language "cs".',
|
|
699
|
+
languageError,
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
await act(async () => {
|
|
703
|
+
rendered?.container
|
|
704
|
+
.querySelector('button')
|
|
705
|
+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
706
|
+
});
|
|
707
|
+
expect(pending).toHaveLength(2);
|
|
708
|
+
|
|
709
|
+
await act(async () => {
|
|
710
|
+
pending[1].resolve();
|
|
711
|
+
await pending[1].promise;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
expect(rendered.container.textContent).toContain('cs:Jazyk');
|
|
715
|
+
expect(instance.language).toBe('cs');
|
|
716
|
+
});
|
|
231
717
|
});
|
|
232
718
|
|
|
233
719
|
describe('i18n react-i18next integration', () => {
|
|
@@ -399,6 +885,62 @@ describe('i18n router adapter', () => {
|
|
|
399
885
|
});
|
|
400
886
|
});
|
|
401
887
|
|
|
888
|
+
test('updates provider language when target locale is already in URL', async () => {
|
|
889
|
+
window.history.replaceState(null, '', '/cs/podminky-pouzivani');
|
|
890
|
+
|
|
891
|
+
const router = createTanstackRouter('/cs/podminky-pouzivani', 'cs');
|
|
892
|
+
const i18nInstance = createI18nInstance('en');
|
|
893
|
+
const updateLanguage = rstest.fn();
|
|
894
|
+
let changeLanguagePromise: Promise<void> | undefined;
|
|
895
|
+
|
|
896
|
+
const Harness = () => {
|
|
897
|
+
const { changeLanguage } = useModernI18n();
|
|
898
|
+
return (
|
|
899
|
+
<button
|
|
900
|
+
type="button"
|
|
901
|
+
onClick={() => {
|
|
902
|
+
changeLanguagePromise = changeLanguage('cs');
|
|
903
|
+
}}
|
|
904
|
+
>
|
|
905
|
+
Change language
|
|
906
|
+
</button>
|
|
907
|
+
);
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
rendered = await renderWithRuntime(
|
|
911
|
+
<ModernI18nProvider
|
|
912
|
+
value={{
|
|
913
|
+
language: 'en',
|
|
914
|
+
i18nInstance,
|
|
915
|
+
languages: ['en', 'cs'],
|
|
916
|
+
localePathRedirect: true,
|
|
917
|
+
localisedUrls,
|
|
918
|
+
updateLanguage,
|
|
919
|
+
}}
|
|
920
|
+
>
|
|
921
|
+
<Harness />
|
|
922
|
+
</ModernI18nProvider>,
|
|
923
|
+
createTanstackRuntimeContext(router),
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
const button = rendered.container.querySelector('button');
|
|
927
|
+
updateLanguage.mockClear();
|
|
928
|
+
|
|
929
|
+
await act(async () => {
|
|
930
|
+
button?.dispatchEvent(
|
|
931
|
+
new MouseEvent('click', {
|
|
932
|
+
bubbles: true,
|
|
933
|
+
cancelable: true,
|
|
934
|
+
button: 0,
|
|
935
|
+
}),
|
|
936
|
+
);
|
|
937
|
+
await changeLanguagePromise;
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(updateLanguage).toHaveBeenCalledWith('cs');
|
|
941
|
+
expect(router.navigate).not.toHaveBeenCalled();
|
|
942
|
+
});
|
|
943
|
+
|
|
402
944
|
test('keeps React Router positional replacement when changeLanguage updates the URL', async () => {
|
|
403
945
|
window.history.replaceState(null, '', '/en/terms-of-service');
|
|
404
946
|
|
|
@@ -454,6 +996,67 @@ describe('i18n router adapter', () => {
|
|
|
454
996
|
});
|
|
455
997
|
});
|
|
456
998
|
|
|
999
|
+
test('tracks React Router link and history navigation without Modern i18n consumers', async () => {
|
|
1000
|
+
window.history.replaceState(null, '', '/en');
|
|
1001
|
+
const i18nInstance = i18next.createInstance();
|
|
1002
|
+
await i18nInstance.init({
|
|
1003
|
+
lng: 'en',
|
|
1004
|
+
fallbackLng: 'en',
|
|
1005
|
+
resources: {
|
|
1006
|
+
en: { translation: { languageSwitcher: 'Language' } },
|
|
1007
|
+
cs: { translation: { languageSwitcher: 'Jazyk' } },
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const RouteContent = () => {
|
|
1012
|
+
const { t } = useTranslation();
|
|
1013
|
+
return (
|
|
1014
|
+
<main>
|
|
1015
|
+
<span data-testid="translation">{t('languageSwitcher')}</span>
|
|
1016
|
+
<ReactRouterLink to="/cs">Czech</ReactRouterLink>
|
|
1017
|
+
</main>
|
|
1018
|
+
);
|
|
1019
|
+
};
|
|
1020
|
+
const router = createMemoryRouter(
|
|
1021
|
+
[{ path: '/:lang', element: <RouteContent /> }],
|
|
1022
|
+
{ initialEntries: ['/en'] },
|
|
1023
|
+
);
|
|
1024
|
+
const originalSubscribe = router.subscribe;
|
|
1025
|
+
router.subscribe = function subscribe(listener) {
|
|
1026
|
+
if (this !== router) {
|
|
1027
|
+
throw new Error('React Router subscribe receiver was not preserved');
|
|
1028
|
+
}
|
|
1029
|
+
return originalSubscribe.call(this, listener);
|
|
1030
|
+
};
|
|
1031
|
+
const runtimeContext = createReactRouterRuntimeContext(router);
|
|
1032
|
+
const { onBeforeRender, wrapRoot } = await collectI18nRuntime(
|
|
1033
|
+
i18nInstance,
|
|
1034
|
+
true,
|
|
1035
|
+
);
|
|
1036
|
+
await onBeforeRender(runtimeContext);
|
|
1037
|
+
const I18nRoot = wrapRoot(() => <RouterProvider router={router} />);
|
|
1038
|
+
rendered = await renderWithRuntime(<I18nRoot />, runtimeContext);
|
|
1039
|
+
|
|
1040
|
+
await act(async () => {
|
|
1041
|
+
rendered?.container
|
|
1042
|
+
.querySelector('a')
|
|
1043
|
+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
1044
|
+
await Promise.resolve();
|
|
1045
|
+
});
|
|
1046
|
+
expect(
|
|
1047
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
1048
|
+
?.textContent,
|
|
1049
|
+
).toBe('Jazyk');
|
|
1050
|
+
|
|
1051
|
+
await act(async () => {
|
|
1052
|
+
await router.navigate(-1);
|
|
1053
|
+
});
|
|
1054
|
+
expect(
|
|
1055
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
1056
|
+
?.textContent,
|
|
1057
|
+
).toBe('Language');
|
|
1058
|
+
});
|
|
1059
|
+
|
|
457
1060
|
test('exposes a language-scoped t function for rendered copy', async () => {
|
|
458
1061
|
const i18nInstance = createI18nInstance('en');
|
|
459
1062
|
i18nInstance.t = (key: string) => `${i18nInstance.language}:${key}`;
|