@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.
- package/README.md +221 -11
- package/dist/cjs/cli/index.js +32 -5
- package/dist/cjs/runtime/I18nLink.js +17 -28
- package/dist/cjs/runtime/Link.js +252 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/context.js +41 -10
- package/dist/cjs/runtime/hooks.js +17 -10
- package/dist/cjs/runtime/i18n/backend/config.js +9 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +15 -10
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +16 -11
- package/dist/cjs/runtime/i18n/backend/index.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +13 -9
- package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
- package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
- package/dist/cjs/runtime/i18n/detection/config.js +9 -5
- package/dist/cjs/runtime/i18n/detection/index.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
- package/dist/cjs/runtime/i18n/index.js +9 -5
- package/dist/cjs/runtime/i18n/instance.js +17 -37
- package/dist/cjs/runtime/i18n/react-i18next.js +53 -0
- package/dist/cjs/runtime/i18n/utils.js +9 -17
- package/dist/cjs/runtime/index.js +50 -15
- package/dist/cjs/runtime/localizedPaths.js +105 -0
- package/dist/cjs/runtime/routerAdapter.js +167 -0
- package/dist/cjs/runtime/utils.js +87 -97
- package/dist/cjs/server/index.js +69 -13
- package/dist/cjs/shared/deepMerge.js +12 -8
- package/dist/cjs/shared/detection.js +9 -5
- package/dist/cjs/shared/localisedUrls.js +271 -0
- package/dist/cjs/shared/utils.js +15 -11
- package/dist/esm/cli/index.mjs +23 -0
- package/dist/esm/runtime/I18nLink.mjs +7 -22
- package/dist/esm/runtime/Link.mjs +209 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/context.mjs +34 -7
- package/dist/esm/runtime/hooks.mjs +9 -6
- package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +1 -19
- package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
- package/dist/esm/runtime/i18n/utils.mjs +0 -12
- package/dist/esm/runtime/index.mjs +23 -13
- package/dist/esm/runtime/localizedPaths.mjs +58 -0
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +25 -30
- package/dist/esm/server/index.mjs +53 -7
- package/dist/esm/shared/localisedUrls.mjs +212 -0
- package/dist/esm-node/cli/index.mjs +23 -0
- package/dist/esm-node/runtime/I18nLink.mjs +7 -22
- package/dist/esm-node/runtime/Link.mjs +210 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/context.mjs +34 -7
- package/dist/esm-node/runtime/hooks.mjs +9 -6
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
- package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
- package/dist/esm-node/runtime/index.mjs +23 -13
- package/dist/esm-node/runtime/localizedPaths.mjs +59 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +25 -30
- package/dist/esm-node/server/index.mjs +53 -7
- package/dist/esm-node/shared/localisedUrls.mjs +213 -0
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/runtime/I18nLink.d.ts +6 -0
- package/dist/types/runtime/Link.d.ts +56 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/context.d.ts +3 -0
- package/dist/types/runtime/hooks.d.ts +4 -2
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
- package/dist/types/runtime/i18n/instance.d.ts +4 -6
- package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
- package/dist/types/runtime/index.d.ts +6 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/types.d.ts +1 -1
- package/dist/types/runtime/utils.d.ts +13 -9
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/shared/localisedUrls.d.ts +21 -0
- package/dist/types/shared/type.d.ts +12 -0
- package/package.json +24 -28
- package/rstest.config.mts +39 -0
- package/src/cli/index.ts +44 -1
- package/src/runtime/I18nLink.tsx +14 -51
- package/src/runtime/Link.tsx +414 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/defaults.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +3 -30
- package/src/runtime/i18n/react-i18next.ts +25 -0
- package/src/runtime/i18n/utils.ts +4 -26
- package/src/runtime/index.tsx +47 -12
- package/src/runtime/localizedPaths.ts +118 -0
- package/src/runtime/routerAdapter.tsx +333 -0
- package/src/runtime/types.ts +1 -1
- package/src/runtime/utils.ts +44 -37
- package/src/server/index.ts +117 -10
- package/src/shared/localisedUrls.ts +453 -0
- package/src/shared/type.ts +12 -0
- package/tests/i18nUtils.test.ts +52 -0
- package/tests/link.test.tsx +475 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/localisedUrls.test.ts +312 -0
- package/tests/routerAdapter.test.tsx +452 -0
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
|
@@ -15,7 +15,9 @@ function convertPath(path: string | undefined): string | undefined {
|
|
|
15
15
|
}
|
|
16
16
|
// If it's an absolute path (starts with /), convert to relative path
|
|
17
17
|
if (path.startsWith('/')) {
|
|
18
|
-
return
|
|
18
|
+
return typeof window === 'undefined'
|
|
19
|
+
? path
|
|
20
|
+
: `${window.__assetPrefix__ || ''}${path}`;
|
|
19
21
|
}
|
|
20
22
|
return path;
|
|
21
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Backend from 'i18next-fs-backend';
|
|
1
|
+
import Backend from 'i18next-fs-backend/cjs';
|
|
2
2
|
import type { ExtendedBackendOptions } from '../../../shared/type';
|
|
3
3
|
import type { I18nInstance } from '../instance';
|
|
4
4
|
import { useI18nextBackendCommon } from './middleware.common';
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { BaseBackendOptions } from '../../shared/type';
|
|
2
2
|
|
|
3
|
-
type ReactI18nextModule = typeof import('react-i18next');
|
|
4
|
-
type InitReactI18next = ReactI18nextModule['initReactI18next'];
|
|
5
|
-
type I18nextProviderComponent = ReactI18nextModule['I18nextProvider'];
|
|
6
|
-
|
|
7
3
|
export interface I18nResourceStore {
|
|
8
4
|
data?: {
|
|
9
5
|
[language: string]: {
|
|
@@ -110,9 +106,11 @@ export interface BackendOptions extends Omit<BaseBackendOptions, 'enabled'> {
|
|
|
110
106
|
[key: string]: any;
|
|
111
107
|
}
|
|
112
108
|
|
|
109
|
+
export type ResourceValue = string | { [key: string]: ResourceValue };
|
|
110
|
+
|
|
113
111
|
export interface Resources {
|
|
114
112
|
[lng: string]: {
|
|
115
|
-
[source: string]:
|
|
113
|
+
[source: string]: ResourceValue;
|
|
116
114
|
};
|
|
117
115
|
}
|
|
118
116
|
|
|
@@ -171,15 +169,6 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
|
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
|
|
175
|
-
try {
|
|
176
|
-
const reactI18next = await import('react-i18next');
|
|
177
|
-
return reactI18next;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
172
|
export function getI18nextInstanceForProvider(
|
|
184
173
|
instance: I18nInstance | any,
|
|
185
174
|
): any {
|
|
@@ -213,19 +202,3 @@ export async function getI18nInstance(
|
|
|
213
202
|
|
|
214
203
|
throw new Error('No i18n instance found');
|
|
215
204
|
}
|
|
216
|
-
|
|
217
|
-
export async function getInitReactI18next(): Promise<InitReactI18next | null> {
|
|
218
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
219
|
-
if (reactI18nextModule) {
|
|
220
|
-
return reactI18nextModule.initReactI18next;
|
|
221
|
-
}
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export async function getI18nextProvider(): Promise<I18nextProviderComponent | null> {
|
|
226
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
227
|
-
if (reactI18nextModule) {
|
|
228
|
-
return reactI18nextModule.I18nextProvider;
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
type ReactI18nextModule = typeof import('react-i18next');
|
|
4
|
+
|
|
5
|
+
interface ReactI18nextIntegration {
|
|
6
|
+
I18nextProvider: React.ComponentType<any> | null;
|
|
7
|
+
initReactI18next: any | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
|
|
11
|
+
try {
|
|
12
|
+
return (await import('react-i18next')) as ReactI18nextModule;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getReactI18nextIntegration(): Promise<ReactI18nextIntegration> {
|
|
19
|
+
const reactI18nextModule = await tryImportReactI18next();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
I18nextProvider: reactI18nextModule?.I18nextProvider ?? null,
|
|
23
|
+
initReactI18next: reactI18nextModule?.initReactI18next ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -238,32 +238,10 @@ export const initializeI18nInstance = async (
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// 2. Asynchronously loads from the second backend (SDK) and updates the store
|
|
246
|
-
// 3. Triggers 'loaded' event when SDK resources are loaded, which causes React to re-render
|
|
247
|
-
//
|
|
248
|
-
// Note: i18next.init() returns a Promise that resolves when the first backend loads.
|
|
249
|
-
// For chained backend, it does NOT wait for the second backend (SDK) to load.
|
|
250
|
-
// The SDK backend loads asynchronously and triggers 'loaded' event automatically.
|
|
251
|
-
const defaultNS =
|
|
252
|
-
initOptions.defaultNS || initOptions.ns || 'translation';
|
|
253
|
-
const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
|
|
254
|
-
|
|
255
|
-
let retries = 20;
|
|
256
|
-
while (retries > 0) {
|
|
257
|
-
// Get the actual i18next instance to access store property
|
|
258
|
-
const actualInstance = getActualI18nextInstance(i18nInstance);
|
|
259
|
-
const store = (actualInstance as any).store;
|
|
260
|
-
if (store?.data?.[finalLanguage]?.[ns]) {
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
264
|
-
retries--;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
241
|
+
// i18next.init() is the synchronization boundary for the primary backend.
|
|
242
|
+
// Chained SDK refreshes update the store through their own loaded events and
|
|
243
|
+
// must not block SSR HTML, otherwise missing/edge-only resources add fixed
|
|
244
|
+
// latency to every route render.
|
|
267
245
|
}
|
|
268
246
|
};
|
|
269
247
|
|
package/src/runtime/index.tsx
CHANGED
|
@@ -29,11 +29,7 @@ import {
|
|
|
29
29
|
mergeDetectionOptions,
|
|
30
30
|
} from './i18n/detection';
|
|
31
31
|
import { useI18nextLanguageDetector } from './i18n/detection/middleware';
|
|
32
|
-
import {
|
|
33
|
-
getI18nextInstanceForProvider,
|
|
34
|
-
getI18nextProvider,
|
|
35
|
-
getInitReactI18next,
|
|
36
|
-
} from './i18n/instance';
|
|
32
|
+
import { getI18nextInstanceForProvider } from './i18n/instance';
|
|
37
33
|
import {
|
|
38
34
|
changeI18nLanguage,
|
|
39
35
|
ensureLanguageMatch,
|
|
@@ -54,6 +50,7 @@ export interface I18nPluginOptions {
|
|
|
54
50
|
changeLanguage?: (lang: string) => void;
|
|
55
51
|
initOptions?: I18nInitOptions;
|
|
56
52
|
htmlLangAttr?: boolean;
|
|
53
|
+
reactI18next?: boolean;
|
|
57
54
|
[key: string]: any;
|
|
58
55
|
}
|
|
59
56
|
|
|
@@ -72,6 +69,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
72
69
|
localeDetection,
|
|
73
70
|
backend,
|
|
74
71
|
htmlLangAttr = false,
|
|
72
|
+
reactI18next = true,
|
|
75
73
|
} = options;
|
|
76
74
|
const {
|
|
77
75
|
localePathRedirect = false,
|
|
@@ -80,20 +78,31 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
80
78
|
fallbackLanguage = 'en',
|
|
81
79
|
detection,
|
|
82
80
|
ignoreRedirectRoutes,
|
|
81
|
+
localisedUrls,
|
|
83
82
|
} = localeDetection || {};
|
|
84
83
|
const { enabled: backendEnabled = false } = backend || {};
|
|
85
84
|
let latestI18nInstance: I18nInstance | undefined;
|
|
86
|
-
let I18nextProvider: React.
|
|
85
|
+
let I18nextProvider: React.ComponentType<any> | null;
|
|
86
|
+
|
|
87
|
+
const loadReactI18nextIntegration = async () => {
|
|
88
|
+
if (!reactI18next) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const { getReactI18nextIntegration } = await import(
|
|
92
|
+
'./i18n/react-i18next'
|
|
93
|
+
);
|
|
94
|
+
return getReactI18nextIntegration();
|
|
95
|
+
};
|
|
87
96
|
|
|
88
97
|
api.onBeforeRender(async context => {
|
|
89
98
|
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
90
99
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
91
100
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
92
101
|
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
93
|
-
const
|
|
94
|
-
I18nextProvider =
|
|
95
|
-
if (initReactI18next) {
|
|
96
|
-
i18nInstance.use(initReactI18next);
|
|
102
|
+
const reactI18nextIntegration = await loadReactI18nextIntegration();
|
|
103
|
+
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
104
|
+
if (reactI18nextIntegration?.initReactI18next) {
|
|
105
|
+
i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
const pathname = getPathname(context);
|
|
@@ -227,6 +236,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
227
236
|
languages,
|
|
228
237
|
fallbackLanguage,
|
|
229
238
|
ignoreRedirectRoutes,
|
|
239
|
+
localisedUrls,
|
|
230
240
|
);
|
|
231
241
|
|
|
232
242
|
const contextValue = useMemo(
|
|
@@ -238,6 +248,7 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
238
248
|
languages,
|
|
239
249
|
localePathRedirect,
|
|
240
250
|
ignoreRedirectRoutes,
|
|
251
|
+
localisedUrls,
|
|
241
252
|
setLang,
|
|
242
253
|
),
|
|
243
254
|
[
|
|
@@ -247,15 +258,17 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
247
258
|
languages,
|
|
248
259
|
localePathRedirect,
|
|
249
260
|
ignoreRedirectRoutes,
|
|
261
|
+
localisedUrls,
|
|
250
262
|
forceUpdate,
|
|
251
263
|
],
|
|
252
264
|
);
|
|
253
265
|
|
|
266
|
+
const children = (props as React.PropsWithChildren).children;
|
|
254
267
|
const appContent = (
|
|
255
268
|
<>
|
|
256
269
|
{Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
|
|
257
270
|
<ModernI18nProvider value={contextValue}>
|
|
258
|
-
<App {...props}
|
|
271
|
+
{App ? <App {...props}>{children}</App> : children}
|
|
259
272
|
</ModernI18nProvider>
|
|
260
273
|
</>
|
|
261
274
|
);
|
|
@@ -280,6 +293,28 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
|
|
|
280
293
|
},
|
|
281
294
|
});
|
|
282
295
|
|
|
296
|
+
export type {
|
|
297
|
+
AllowedLinkTarget,
|
|
298
|
+
CanonicalRoutePath,
|
|
299
|
+
UltramodernCanonicalRoutes,
|
|
300
|
+
} from './canonicalRoutes';
|
|
283
301
|
export { useModernI18n } from './context';
|
|
284
|
-
export { I18nLink } from './I18nLink';
|
|
302
|
+
export { I18nLink, type I18nLinkProps } from './I18nLink';
|
|
303
|
+
export {
|
|
304
|
+
Link,
|
|
305
|
+
type LinkActiveOptions,
|
|
306
|
+
type LinkBaseProps,
|
|
307
|
+
type LinkParams,
|
|
308
|
+
type LinkProps,
|
|
309
|
+
} from './Link';
|
|
310
|
+
export {
|
|
311
|
+
canonicalPath,
|
|
312
|
+
type LocalizedPathsConfig,
|
|
313
|
+
localizePath,
|
|
314
|
+
type UseLocalizedLocationReturn,
|
|
315
|
+
type UseLocalizedPathsReturn,
|
|
316
|
+
useLocalizedLocation,
|
|
317
|
+
useLocalizedPaths,
|
|
318
|
+
} from './localizedPaths';
|
|
319
|
+
export { buildLocalizedUrl, splitUrlTarget } from './utils';
|
|
285
320
|
export default i18nPlugin;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
3
|
+
import {
|
|
4
|
+
resolveCanonicalLocalisedPath,
|
|
5
|
+
resolveLocalisedUrlsConfig,
|
|
6
|
+
} from '../shared/localisedUrls';
|
|
7
|
+
import { useModernI18n } from './context';
|
|
8
|
+
import { useI18nRouterAdapter } from './routerAdapter';
|
|
9
|
+
import { buildLocalizedUrl, splitUrlTarget } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface LocalizedPathsConfig {
|
|
12
|
+
languages: string[];
|
|
13
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Localize a canonical, language-agnostic target for the given language:
|
|
18
|
+
* adds the language prefix and applies `localisedUrls` pattern mapping.
|
|
19
|
+
* `?search`/`#hash` suffixes are preserved verbatim.
|
|
20
|
+
*/
|
|
21
|
+
export const localizePath = (
|
|
22
|
+
pathname: string,
|
|
23
|
+
language: string,
|
|
24
|
+
config: LocalizedPathsConfig,
|
|
25
|
+
): string =>
|
|
26
|
+
buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reverse of {@link localizePath}: strip the language prefix and map localized
|
|
30
|
+
* slugs back to the canonical pattern's path. `?search`/`#hash` suffixes are
|
|
31
|
+
* preserved verbatim.
|
|
32
|
+
*/
|
|
33
|
+
export const canonicalPath = (
|
|
34
|
+
target: string,
|
|
35
|
+
config: LocalizedPathsConfig,
|
|
36
|
+
): string => {
|
|
37
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
38
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
39
|
+
const pathWithoutLanguage =
|
|
40
|
+
segments.length > 0 && config.languages.includes(segments[0])
|
|
41
|
+
? `/${segments.slice(1).join('/')}`
|
|
42
|
+
: pathname || '/';
|
|
43
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(config.localisedUrls);
|
|
44
|
+
const resolvedPath = localisedUrlsConfig.enabled
|
|
45
|
+
? resolveCanonicalLocalisedPath(
|
|
46
|
+
pathWithoutLanguage,
|
|
47
|
+
config.languages,
|
|
48
|
+
localisedUrlsConfig.map,
|
|
49
|
+
)
|
|
50
|
+
: pathWithoutLanguage;
|
|
51
|
+
|
|
52
|
+
return `${resolvedPath}${search}${hash}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface UseLocalizedPathsReturn {
|
|
56
|
+
localizePath: (pathname: string, language: string) => string;
|
|
57
|
+
canonicalPath: (pathname: string) => string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Context-bound versions of {@link localizePath} and {@link canonicalPath} —
|
|
62
|
+
* the plugin configuration (languages, localisedUrls) is read from the i18n
|
|
63
|
+
* provider, so apps never copy pattern-matching helpers again.
|
|
64
|
+
*/
|
|
65
|
+
export const useLocalizedPaths = (): UseLocalizedPathsReturn => {
|
|
66
|
+
const { supportedLanguages, localisedUrls } = useModernI18n();
|
|
67
|
+
|
|
68
|
+
return useMemo(() => {
|
|
69
|
+
const config: LocalizedPathsConfig = {
|
|
70
|
+
languages: supportedLanguages,
|
|
71
|
+
localisedUrls,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
localizePath: (pathname: string, language: string) =>
|
|
76
|
+
localizePath(pathname, language, config),
|
|
77
|
+
canonicalPath: (pathname: string) => canonicalPath(pathname, config),
|
|
78
|
+
};
|
|
79
|
+
}, [supportedLanguages, localisedUrls]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface UseLocalizedLocationReturn {
|
|
83
|
+
language: string;
|
|
84
|
+
/** Canonical (language-agnostic) path of the current location. */
|
|
85
|
+
canonical: string;
|
|
86
|
+
/** Per-language hrefs for the current location, search+hash preserved. */
|
|
87
|
+
alternates: Record<string, string>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Per-language hrefs for the current location — for hreflang `<link>` tags and
|
|
92
|
+
* language switchers. SSR-safe: the location comes from the router adapter.
|
|
93
|
+
*/
|
|
94
|
+
export const useLocalizedLocation = (): UseLocalizedLocationReturn => {
|
|
95
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
96
|
+
const { location } = useI18nRouterAdapter();
|
|
97
|
+
const pathname = location?.pathname ?? '/';
|
|
98
|
+
const search = location?.search ?? '';
|
|
99
|
+
const hash = location?.hash ?? '';
|
|
100
|
+
|
|
101
|
+
return useMemo(() => {
|
|
102
|
+
const config: LocalizedPathsConfig = {
|
|
103
|
+
languages: supportedLanguages,
|
|
104
|
+
localisedUrls,
|
|
105
|
+
};
|
|
106
|
+
const alternates: Record<string, string> = {};
|
|
107
|
+
for (const supportedLanguage of supportedLanguages) {
|
|
108
|
+
alternates[supportedLanguage] =
|
|
109
|
+
`${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
language,
|
|
114
|
+
canonical: canonicalPath(pathname, config),
|
|
115
|
+
alternates,
|
|
116
|
+
};
|
|
117
|
+
}, [language, supportedLanguages, localisedUrls, pathname, search, hash]);
|
|
118
|
+
};
|