@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6
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 +0 -22
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +26 -61
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/core.js +17 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +35 -140
- package/dist/cjs/runtime/utils.js +17 -19
- package/dist/cjs/server/index.js +10 -7
- package/dist/cjs/server/redirectPolicy.js +36 -31
- package/dist/cjs/shared/urlStrategy.js +18 -0
- package/dist/esm/cli/index.mjs +0 -22
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +18 -62
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/core.mjs +8 -5
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +30 -141
- package/dist/esm/runtime/utils.mjs +15 -17
- package/dist/esm/server/index.mjs +10 -7
- package/dist/esm/server/redirectPolicy.mjs +29 -24
- package/dist/esm/shared/urlStrategy.mjs +0 -0
- package/dist/esm-node/cli/index.mjs +0 -22
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +18 -62
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/core.mjs +8 -5
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
- package/dist/esm-node/runtime/utils.mjs +15 -17
- package/dist/esm-node/server/index.mjs +10 -7
- package/dist/esm-node/server/redirectPolicy.mjs +29 -24
- package/dist/esm-node/shared/urlStrategy.mjs +1 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/core.d.ts +11 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/type.d.ts +0 -14
- package/dist/types/shared/urlStrategy.d.ts +7 -0
- package/package.json +21 -11
- package/rstest.config.mts +2 -8
- package/src/cli/index.ts +1 -43
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +34 -133
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/core.tsx +26 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +73 -338
- package/src/runtime/utils.ts +40 -31
- package/src/server/index.ts +44 -9
- package/src/server/redirectPolicy.ts +70 -70
- package/src/shared/type.ts +0 -14
- package/src/shared/urlStrategy.ts +11 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/linkTypes.test.ts +12 -3
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
- package/tests/localisedUrls.test.ts +7 -216
- package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
- package/tests/redirectPolicy.test.ts +39 -30
- package/tests/routerAdapter.test.tsx +95 -1055
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/peerCohort.test.ts +0 -25
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
|
|
2
1
|
import { getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
3
2
|
import { collectApiPrefixes, matchesApiPrefix } from "./apiPrefix.mjs";
|
|
4
3
|
import { convertToHonoLanguageDetectorOptions } from "./detectorOptions.mjs";
|
|
@@ -24,7 +23,11 @@ const i18nServerPlugin = (options)=>({
|
|
|
24
23
|
const { entryName } = route;
|
|
25
24
|
if (!entryName) return;
|
|
26
25
|
if (!options.localeDetection) return;
|
|
27
|
-
const
|
|
26
|
+
const entryLocaleDetection = getLocaleDetectionOptions(entryName, options.localeDetection);
|
|
27
|
+
const { localePathRedirect, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = entryLocaleDetection;
|
|
28
|
+
const legacyLocalisedUrls = Reflect.get(entryLocaleDetection, 'localisedUrls');
|
|
29
|
+
const urlStrategy = options.resolveUrlStrategy?.(entryName);
|
|
30
|
+
if (legacyLocalisedUrls && 'object' == typeof legacyLocalisedUrls && Object.keys(legacyLocalisedUrls).length > 0 && !urlStrategy) throw new Error('Mapped locale URLs require a URL strategy. Use the i18n integration server plugin or supply resolveUrlStrategy.');
|
|
28
31
|
const staticRoutePrefixes = options.staticRoutePrefixes;
|
|
29
32
|
const originUrlPath = route.urlPath;
|
|
30
33
|
const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
|
|
@@ -39,6 +42,7 @@ const i18nServerPlugin = (options)=>({
|
|
|
39
42
|
const url = new URL(c.req.url);
|
|
40
43
|
const pathname = url.pathname;
|
|
41
44
|
if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
|
|
45
|
+
if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages)) return await next();
|
|
42
46
|
if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
|
|
43
47
|
if ('/' === originUrlPath) {
|
|
44
48
|
const pathSegments = pathname.split('/').filter(Boolean);
|
|
@@ -59,7 +63,7 @@ const i18nServerPlugin = (options)=>({
|
|
|
59
63
|
const pathname = url.pathname;
|
|
60
64
|
if (matchesApiPrefix(pathname, apiPrefixes)) return await next();
|
|
61
65
|
if (isStaticResourceRequest(pathname, staticRoutePrefixes, languages)) return await next();
|
|
62
|
-
if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes)) return await next();
|
|
66
|
+
if (shouldIgnoreRedirect(pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages)) return await next();
|
|
63
67
|
if ('/' === originUrlPath) {
|
|
64
68
|
const pathSegments = pathname.split('/').filter(Boolean);
|
|
65
69
|
if (pathSegments.length > 0) {
|
|
@@ -72,12 +76,11 @@ const i18nServerPlugin = (options)=>({
|
|
|
72
76
|
let detectedLanguage = null;
|
|
73
77
|
if (i18nextDetector) detectedLanguage = c.get('language') || null;
|
|
74
78
|
const targetLanguage = detectedLanguage || fallbackLanguage;
|
|
75
|
-
const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages,
|
|
79
|
+
const localizedUrl = buildLocalizedUrl(c.req, originUrlPath, targetLanguage, languages, urlStrategy);
|
|
76
80
|
return createLocaleRedirectResponse(localizedUrl);
|
|
77
81
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const expectedUrl = buildLocalizedUrl(c.req, originUrlPath, language, languages, localisedUrls);
|
|
82
|
+
if (urlStrategy) {
|
|
83
|
+
const expectedUrl = buildLocalizedUrl(c.req, originUrlPath, language, languages, urlStrategy);
|
|
81
84
|
if (expectedUrl !== `${pathname}${url.search}${url.hash}`) return createLocaleRedirectResponse(expectedUrl);
|
|
82
85
|
}
|
|
83
86
|
await next();
|
|
@@ -1,39 +1,44 @@
|
|
|
1
|
-
import { isDefaultLocaleRedirectSkipPath, localiseTargetPathname, matchesPathPrefix, shouldSkipLocaleRedirect, stripLanguagePrefix } from "@modern-js/i18n-runtime-extensions";
|
|
2
1
|
const stripUrlPathPrefix = (pathname, urlPath)=>{
|
|
3
2
|
const basePath = urlPath.replace('/*', '');
|
|
4
3
|
if (!basePath || '/' === basePath) return pathname;
|
|
5
|
-
|
|
6
|
-
return remainingPath || '/';
|
|
4
|
+
return pathname === basePath || pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) || '/' : pathname;
|
|
7
5
|
};
|
|
8
|
-
const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes)=>{
|
|
6
|
+
const shouldIgnoreRedirect = (pathname, urlPath, ignoreRedirectRoutes, urlStrategy, languages = [])=>{
|
|
9
7
|
const remainingPath = stripUrlPathPrefix(pathname, urlPath);
|
|
10
|
-
|
|
8
|
+
if (urlStrategy?.shouldSkipRedirect?.(remainingPath, languages)) return true;
|
|
9
|
+
if (!ignoreRedirectRoutes) return false;
|
|
10
|
+
const segments = remainingPath.split('/').filter(Boolean);
|
|
11
|
+
if (segments[0] && languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
|
|
12
|
+
const normalizedPath = `/${segments.join('/')}`;
|
|
13
|
+
return 'function' == typeof ignoreRedirectRoutes ? ignoreRedirectRoutes(normalizedPath) : ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
11
14
|
};
|
|
12
15
|
const isStaticResourceRequest = (pathname, staticRoutePrefixes, languages = [])=>{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const prefixes = [
|
|
17
|
+
...staticRoutePrefixes,
|
|
18
|
+
'/static',
|
|
19
|
+
'/upload'
|
|
20
|
+
];
|
|
21
|
+
const matches = (target)=>prefixes.some((prefix)=>target === prefix || target.startsWith(`${prefix}/`));
|
|
22
|
+
if (matches(pathname)) return true;
|
|
23
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
24
|
+
if (segments[0] && languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) return matches(`/${segments.slice(1).join('/')}`);
|
|
25
|
+
return false;
|
|
18
26
|
};
|
|
19
27
|
const getLanguageFromPath = (req, urlPath, languages)=>{
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const firstSegment = segments[0];
|
|
25
|
-
if (languages.includes(firstSegment)) return firstSegment;
|
|
26
|
-
return null;
|
|
28
|
+
const host = req.header?.().host;
|
|
29
|
+
const url = new URL(req.url, host ? `http://${host}` : void 0);
|
|
30
|
+
const firstSegment = stripUrlPathPrefix(url.pathname, urlPath).split('/').filter(Boolean)[0];
|
|
31
|
+
return languages.find((language)=>language.toLowerCase() === firstSegment?.toLowerCase()) ?? null;
|
|
27
32
|
};
|
|
28
|
-
const buildLocalizedUrl = (req, urlPath, language, languages,
|
|
33
|
+
const buildLocalizedUrl = (req, urlPath, language, languages, urlStrategy)=>{
|
|
29
34
|
const url = new URL(req.url);
|
|
30
|
-
const pathname = url.pathname;
|
|
31
35
|
const basePath = urlPath.replace('/*', '');
|
|
32
|
-
const remainingPath = stripUrlPathPrefix(pathname, urlPath);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const remainingPath = stripUrlPathPrefix(url.pathname, urlPath);
|
|
37
|
+
const segments = remainingPath.split('/').filter(Boolean);
|
|
38
|
+
if (segments[0] && languages.some((item)=>item.toLowerCase() === segments[0].toLowerCase())) segments[0] = language;
|
|
39
|
+
else segments.unshift(language);
|
|
40
|
+
const pathname = urlStrategy ? urlStrategy.localizePathname(remainingPath, language, languages) : `/${segments.join('/')}`;
|
|
41
|
+
return `${'/' === basePath ? '' : basePath}${pathname}${url.search}${url.hash}`;
|
|
37
42
|
};
|
|
38
43
|
const createLocaleRedirectResponse = (location)=>new Response(null, {
|
|
39
44
|
status: 302,
|
|
File without changes
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "@modern-js/i18n-runtime-extensions";
|
|
3
2
|
import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
|
|
4
3
|
import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
5
4
|
import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
|
|
@@ -44,27 +43,6 @@ const i18nPlugin = (options = {})=>({
|
|
|
44
43
|
plugins
|
|
45
44
|
};
|
|
46
45
|
});
|
|
47
|
-
api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
|
|
48
|
-
if (!localeDetection) return {
|
|
49
|
-
entrypoint,
|
|
50
|
-
routes
|
|
51
|
-
};
|
|
52
|
-
const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
|
|
53
|
-
const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
|
|
54
|
-
if (!localePathRedirect || 0 === languages.length) return {
|
|
55
|
-
entrypoint,
|
|
56
|
-
routes
|
|
57
|
-
};
|
|
58
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
59
|
-
if (!localisedUrlsConfig.enabled) return {
|
|
60
|
-
entrypoint,
|
|
61
|
-
routes
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
entrypoint,
|
|
65
|
-
routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
46
|
api._internalServerPlugins(({ plugins })=>{
|
|
69
47
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
70
48
|
const normalizedConfig = api.getNormalizedConfig();
|
|
@@ -10,10 +10,10 @@ const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
|
10
10
|
const Link = (props)=>{
|
|
11
11
|
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
|
|
12
12
|
const adapter = useI18nRouterAdapter();
|
|
13
|
-
const { language, supportedLanguages,
|
|
13
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
14
14
|
const config = {
|
|
15
15
|
languages: supportedLanguages,
|
|
16
|
-
|
|
16
|
+
urlStrategy
|
|
17
17
|
};
|
|
18
18
|
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
19
19
|
const isBareHash = to.startsWith('#');
|
|
@@ -23,7 +23,7 @@ const Link = (props)=>{
|
|
|
23
23
|
const interpolated = interpolateRouteParams(pathname || '/', params);
|
|
24
24
|
const firstSegment = interpolated.split('/').filter(Boolean)[0];
|
|
25
25
|
if (firstSegment && supportedLanguages.includes(firstSegment)) warnOnce(`lang-prefix:${to}`, `[plugin-i18n] <Link to="${to}"> starts with a language prefix. Write language-agnostic canonical paths; the Link localizes them automatically.`);
|
|
26
|
-
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages,
|
|
26
|
+
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, urlStrategy);
|
|
27
27
|
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
28
28
|
const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
|
|
29
29
|
return {
|
|
@@ -43,7 +43,7 @@ const Link = (props)=>{
|
|
|
43
43
|
isBareHash,
|
|
44
44
|
language,
|
|
45
45
|
supportedLanguages,
|
|
46
|
-
|
|
46
|
+
urlStrategy
|
|
47
47
|
]);
|
|
48
48
|
const isActive = useMemo(()=>{
|
|
49
49
|
if (!target || !adapter.location) return false;
|
|
@@ -58,7 +58,7 @@ const Link = (props)=>{
|
|
|
58
58
|
adapter.location,
|
|
59
59
|
activeOptions?.exact,
|
|
60
60
|
supportedLanguages,
|
|
61
|
-
|
|
61
|
+
urlStrategy
|
|
62
62
|
]);
|
|
63
63
|
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
64
64
|
const activeAttributes = isActive ? {
|
|
@@ -73,7 +73,7 @@ const Link = (props)=>{
|
|
|
73
73
|
children: children
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
const { Link: RouterLink, hasRouter
|
|
76
|
+
const { Link: RouterLink, hasRouter } = adapter;
|
|
77
77
|
if (!hasRouter || !RouterLink) {
|
|
78
78
|
const { replace: _replace, ...anchorProps } = rest;
|
|
79
79
|
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
@@ -96,38 +96,25 @@ const Link = (props)=>{
|
|
|
96
96
|
...rest.style,
|
|
97
97
|
...activeStyle
|
|
98
98
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} : {},
|
|
109
|
-
...void 0 === hashScrollIntoView ? {} : {
|
|
110
|
-
hashScrollIntoView
|
|
111
|
-
},
|
|
112
|
-
...void 0 === tanstackPreload ? {} : {
|
|
113
|
-
preload: tanstackPreload
|
|
114
|
-
},
|
|
115
|
-
...rest,
|
|
116
|
-
...activeRest,
|
|
117
|
-
...activeAttributes,
|
|
118
|
-
className: mergedClassName,
|
|
119
|
-
style: mergedStyle,
|
|
120
|
-
children: children
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return /*#__PURE__*/ jsx(RouterLink, {
|
|
99
|
+
const routerProps = adapter.createLinkProps?.({
|
|
100
|
+
pathname: target.localizedPathname,
|
|
101
|
+
href: target.href,
|
|
102
|
+
search: target.searchObject,
|
|
103
|
+
hash: target.hash,
|
|
104
|
+
hashScrollIntoView,
|
|
105
|
+
prefetch,
|
|
106
|
+
preload
|
|
107
|
+
}) ?? {
|
|
124
108
|
to: target.href,
|
|
125
109
|
...void 0 === prefetch ? {} : {
|
|
126
110
|
prefetch
|
|
127
111
|
},
|
|
128
112
|
...void 0 === preload ? {} : {
|
|
129
113
|
preload
|
|
130
|
-
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
117
|
+
...routerProps,
|
|
131
118
|
...rest,
|
|
132
119
|
...activeRest,
|
|
133
120
|
...activeAttributes,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { RuntimeComponentResolverContext } from "@modern-js/runtime/context";
|
|
3
4
|
import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
|
|
4
5
|
import { changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
|
|
5
|
-
import { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
6
6
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
7
|
const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
|
|
8
8
|
const reactI18nextProviderContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext');
|
|
@@ -23,68 +23,23 @@ const ModernI18nProvider = ({ children, i18nextProvider, value })=>{
|
|
|
23
23
|
children: content
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
initImmediate: false,
|
|
38
|
-
lng: parent.language,
|
|
39
|
-
ns: [
|
|
40
|
-
defaultNamespace
|
|
41
|
-
],
|
|
42
|
-
resources,
|
|
43
|
-
supportedLngs: languages
|
|
44
|
-
});
|
|
45
|
-
if (!clone) throw new Error('FederatedI18nBoundary requires an i18n instance with cloneInstance support');
|
|
46
|
-
const resourceStore = clone.store;
|
|
47
|
-
const parentResourceStore = parentInstance.store;
|
|
48
|
-
const sharesNestedResourceState = Object.entries(resourceStore?.data ?? {}).some(([language, namespaces])=>{
|
|
49
|
-
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
50
|
-
return namespaces === parentNamespaces || Object.entries(namespaces).some(([namespace, resource])=>null !== resource && 'object' == typeof resource && resource === parentNamespaces?.[namespace]);
|
|
51
|
-
});
|
|
52
|
-
if (clone === parentInstance || resourceStore === parentResourceStore || resourceStore?.data === parentResourceStore?.data || sharesNestedResourceState) throw new Error('FederatedI18nBoundary cloneInstance did not isolate the host resource store');
|
|
53
|
-
if (!resourceStore?.addResourceBundle || !clone.removeResourceBundle || !resourceStore.data) throw new Error('FederatedI18nBoundary requires an isolated mutable i18n resource store');
|
|
54
|
-
for (const [language, namespaces] of Object.entries(resourceStore.data))for (const namespace of Object.keys(namespaces))clone.removeResourceBundle(language, namespace);
|
|
55
|
-
for (const [language, namespaces] of Object.entries(resources))for (const [namespace, resource] of Object.entries(namespaces))resourceStore.addResourceBundle(language, namespace, resource, true, true);
|
|
56
|
-
return clone;
|
|
57
|
-
}, [
|
|
58
|
-
defaultNamespace,
|
|
59
|
-
fallbackLanguage,
|
|
60
|
-
languages,
|
|
61
|
-
parent.i18nInstance,
|
|
62
|
-
parent.language,
|
|
63
|
-
resources
|
|
64
|
-
]);
|
|
65
|
-
const value = useMemo(()=>({
|
|
66
|
-
...parent,
|
|
67
|
-
i18nInstance: scopedInstance,
|
|
68
|
-
language: parent.language,
|
|
69
|
-
languages
|
|
70
|
-
}), [
|
|
71
|
-
languages,
|
|
72
|
-
parent,
|
|
73
|
-
scopedInstance
|
|
74
|
-
]);
|
|
75
|
-
const scopedContent = /*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
76
|
-
value: value,
|
|
77
|
-
children: children
|
|
26
|
+
const MissingFederatedI18nBoundary = ()=>{
|
|
27
|
+
throw new Error('FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin');
|
|
28
|
+
};
|
|
29
|
+
const FederatedI18nBoundary = (props)=>{
|
|
30
|
+
const resolveComponent = useContext(RuntimeComponentResolverContext);
|
|
31
|
+
const Boundary = resolveComponent?.(MissingFederatedI18nBoundary, {
|
|
32
|
+
name: 'i18n.FederatedI18nBoundary'
|
|
33
|
+
}) ?? MissingFederatedI18nBoundary;
|
|
34
|
+
if (Boundary === FederatedI18nBoundary) throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
|
|
35
|
+
return /*#__PURE__*/ jsx(Boundary, {
|
|
36
|
+
...props
|
|
78
37
|
});
|
|
79
|
-
return I18nextProvider ? /*#__PURE__*/ jsx(I18nextProvider, {
|
|
80
|
-
i18n: scopedInstance,
|
|
81
|
-
children: scopedContent
|
|
82
|
-
}) : scopedContent;
|
|
83
38
|
};
|
|
84
39
|
const useModernI18n = ()=>{
|
|
85
40
|
const context = useContext(ModernI18nContext);
|
|
86
41
|
if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
|
|
87
|
-
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
42
|
+
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, urlStrategy, updateLanguage, synchronizeLanguage } = context;
|
|
88
43
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
89
44
|
const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
|
|
90
45
|
languages,
|
|
@@ -103,7 +58,7 @@ const useModernI18n = ()=>{
|
|
|
103
58
|
updateLanguage,
|
|
104
59
|
localePathRedirect,
|
|
105
60
|
ignoreRedirectRoutes,
|
|
106
|
-
|
|
61
|
+
urlStrategy,
|
|
107
62
|
languages,
|
|
108
63
|
hasRouter,
|
|
109
64
|
navigate,
|
|
@@ -113,7 +68,7 @@ const useModernI18n = ()=>{
|
|
|
113
68
|
updateLanguage,
|
|
114
69
|
localePathRedirect,
|
|
115
70
|
ignoreRedirectRoutes,
|
|
116
|
-
|
|
71
|
+
urlStrategy,
|
|
117
72
|
languages,
|
|
118
73
|
hasRouter,
|
|
119
74
|
navigate,
|
|
@@ -136,9 +91,10 @@ const useModernI18n = ()=>{
|
|
|
136
91
|
t,
|
|
137
92
|
i18nInstance: i18nInstance,
|
|
138
93
|
supportedLanguages: languages || [],
|
|
139
|
-
|
|
94
|
+
urlStrategy,
|
|
140
95
|
isLanguageSupported,
|
|
141
96
|
isResourcesReady
|
|
142
97
|
};
|
|
143
98
|
};
|
|
144
|
-
export {
|
|
99
|
+
export { getActualI18nextInstance } from "./i18n/instance.mjs";
|
|
100
|
+
export { FederatedI18nBoundary, ModernI18nContext, ModernI18nProvider, ReactI18nextProviderContext, useModernI18n };
|
|
@@ -18,7 +18,7 @@ async function changeI18nInstanceLanguage(i18nInstance, language) {
|
|
|
18
18
|
else await i18nInstance.changeLanguage?.(language);
|
|
19
19
|
}
|
|
20
20
|
async function changeModernI18nLanguage(newLang, options) {
|
|
21
|
-
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes,
|
|
21
|
+
const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, urlStrategy, languages, hasRouter, navigate, location } = options;
|
|
22
22
|
try {
|
|
23
23
|
if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
|
|
24
24
|
await changeI18nInstanceLanguage(i18nInstance, newLang);
|
|
@@ -29,8 +29,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
29
29
|
const relativePath = currentPath.replace(entryPath, '');
|
|
30
30
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
31
31
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
32
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
33
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
32
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
33
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
34
34
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
35
35
|
await navigate(newUrl, {
|
|
36
36
|
replace: true
|
|
@@ -42,8 +42,8 @@ async function changeModernI18nLanguage(newLang, options) {
|
|
|
42
42
|
const relativePath = currentPath.replace(entryPath, '');
|
|
43
43
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
44
44
|
if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
|
|
45
|
-
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
46
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [],
|
|
45
|
+
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes, urlStrategy)) {
|
|
46
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], urlStrategy);
|
|
47
47
|
const newUrl = entryPath + newPath + window.location.search + window.location.hash;
|
|
48
48
|
window.history.pushState(null, '', newUrl);
|
|
49
49
|
}
|
|
@@ -6,8 +6,8 @@ import "./types.mjs";
|
|
|
6
6
|
const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
7
7
|
name: '@modern-js/plugin-i18n',
|
|
8
8
|
setup: (api)=>{
|
|
9
|
-
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
10
|
-
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes
|
|
9
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true, urlStrategy, NavigationProvider, LanguageSynchronization } = options;
|
|
10
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
|
|
11
11
|
const { enabled: backendEnabled = false } = backend || {};
|
|
12
12
|
let latestI18nInstance;
|
|
13
13
|
let I18nextProvider;
|
|
@@ -36,15 +36,18 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
|
|
|
36
36
|
languages,
|
|
37
37
|
fallbackLanguage,
|
|
38
38
|
ignoreRedirectRoutes,
|
|
39
|
-
|
|
39
|
+
urlStrategy,
|
|
40
|
+
NavigationProvider,
|
|
41
|
+
LanguageSynchronization,
|
|
40
42
|
getLatestI18nInstance: ()=>latestI18nInstance,
|
|
41
43
|
getI18nextProvider: ()=>I18nextProvider
|
|
42
44
|
}));
|
|
43
45
|
}
|
|
44
46
|
});
|
|
47
|
+
export { I18nNavigationProvider, useI18nRouterAdapter, useNativeI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
45
48
|
export { FederatedI18nBoundary, useModernI18n } from "./context.mjs";
|
|
46
49
|
export { I18nLink } from "./I18nLink.mjs";
|
|
47
50
|
export { Link } from "./Link.mjs";
|
|
48
|
-
export {
|
|
49
|
-
export {
|
|
51
|
+
export { useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
52
|
+
export { splitUrlTarget } from "./utils.mjs";
|
|
50
53
|
export { createI18nPlugin };
|
|
@@ -19,7 +19,7 @@ function createMinimalI18nInstance(language) {
|
|
|
19
19
|
};
|
|
20
20
|
return minimalInstance;
|
|
21
21
|
}
|
|
22
|
-
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes,
|
|
22
|
+
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy) {
|
|
23
23
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
24
24
|
return {
|
|
25
25
|
language: lang,
|
|
@@ -28,9 +28,8 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
|
|
|
28
28
|
languages,
|
|
29
29
|
localePathRedirect,
|
|
30
30
|
ignoreRedirectRoutes,
|
|
31
|
-
|
|
32
|
-
updateLanguage: setLang
|
|
33
|
-
synchronizeLanguage
|
|
31
|
+
urlStrategy,
|
|
32
|
+
updateLanguage: setLang
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
@@ -79,7 +78,7 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
|
79
78
|
setForceUpdate
|
|
80
79
|
]);
|
|
81
80
|
}
|
|
82
|
-
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes,
|
|
81
|
+
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy) {
|
|
83
82
|
const urlRef = useRef('');
|
|
84
83
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
85
84
|
useEffect(()=>{
|
|
@@ -96,10 +95,10 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
96
95
|
const currentHash = hasRouter && location ? location.hash : window.location.hash;
|
|
97
96
|
const entryPath = getEntryPath();
|
|
98
97
|
const relativePath = currentPathname.replace(entryPath, '');
|
|
99
|
-
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) return;
|
|
98
|
+
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes, urlStrategy)) return;
|
|
100
99
|
const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
|
|
101
100
|
const targetLanguage = pathDetection.language || i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
102
|
-
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages,
|
|
101
|
+
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, urlStrategy);
|
|
103
102
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
104
103
|
urlRef.current = currentPathname;
|
|
105
104
|
if (newUrl !== currentPathname + currentSearch + currentHash) if (hasRouter && navigate && location) navigate(newUrl, {
|
|
@@ -115,7 +114,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
115
114
|
languages,
|
|
116
115
|
fallbackLanguage,
|
|
117
116
|
ignoreRedirectRoutes,
|
|
118
|
-
|
|
117
|
+
urlStrategy
|
|
119
118
|
]);
|
|
120
119
|
}
|
|
121
120
|
function useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang) {
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import { canonicalTargetPathname } from "@modern-js/i18n-runtime-extensions";
|
|
3
2
|
import { useMemo } from "react";
|
|
4
3
|
import { useModernI18n } from "./context.mjs";
|
|
5
4
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
5
|
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
7
|
-
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.
|
|
6
|
+
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.urlStrategy);
|
|
8
7
|
const canonicalPath = (target, config)=>{
|
|
9
8
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
10
|
-
|
|
9
|
+
if (config.urlStrategy) return `${config.urlStrategy.canonicalPathname(pathname, config.languages)}${search}${hash}`;
|
|
10
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
11
|
+
if (segments[0] && config.languages.some((language)=>language.toLowerCase() === segments[0].toLowerCase())) segments.shift();
|
|
12
|
+
const resolvedPath = `/${segments.join('/')}`;
|
|
11
13
|
return `${resolvedPath}${search}${hash}`;
|
|
12
14
|
};
|
|
13
15
|
const useLocalizedPaths = ()=>{
|
|
14
|
-
const { supportedLanguages,
|
|
16
|
+
const { supportedLanguages, urlStrategy } = useModernI18n();
|
|
15
17
|
return useMemo(()=>{
|
|
16
18
|
const config = {
|
|
17
19
|
languages: supportedLanguages,
|
|
18
|
-
|
|
20
|
+
urlStrategy
|
|
19
21
|
};
|
|
20
22
|
return {
|
|
21
23
|
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
@@ -23,11 +25,11 @@ const useLocalizedPaths = ()=>{
|
|
|
23
25
|
};
|
|
24
26
|
}, [
|
|
25
27
|
supportedLanguages,
|
|
26
|
-
|
|
28
|
+
urlStrategy
|
|
27
29
|
]);
|
|
28
30
|
};
|
|
29
31
|
const useLocalizedLocation = ()=>{
|
|
30
|
-
const { language, supportedLanguages,
|
|
32
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
31
33
|
const { location } = useI18nRouterAdapter();
|
|
32
34
|
const pathname = location?.pathname ?? '/';
|
|
33
35
|
const search = location?.search ?? '';
|
|
@@ -35,7 +37,7 @@ const useLocalizedLocation = ()=>{
|
|
|
35
37
|
return useMemo(()=>{
|
|
36
38
|
const config = {
|
|
37
39
|
languages: supportedLanguages,
|
|
38
|
-
|
|
40
|
+
urlStrategy
|
|
39
41
|
};
|
|
40
42
|
const alternates = {};
|
|
41
43
|
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
@@ -47,7 +49,7 @@ const useLocalizedLocation = ()=>{
|
|
|
47
49
|
}, [
|
|
48
50
|
language,
|
|
49
51
|
supportedLanguages,
|
|
50
|
-
|
|
52
|
+
urlStrategy,
|
|
51
53
|
pathname,
|
|
52
54
|
search,
|
|
53
55
|
hash
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useLanguageSync } from "@modern-js/i18n-runtime-extensions";
|
|
4
3
|
import { RuntimeContext } from "@modern-js/runtime";
|
|
5
4
|
import { Helmet } from "@modern-js/runtime/head";
|
|
6
5
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
6
|
import { ModernI18nProvider } from "./context.mjs";
|
|
8
|
-
import { createContextValue, useClientSideRedirect, useSdkResourcesLoader } from "./hooks.mjs";
|
|
7
|
+
import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
|
|
9
8
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
10
9
|
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
11
|
-
const
|
|
12
|
-
|
|
10
|
+
const NativeLanguageSynchronization = ({ i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang, children })=>{
|
|
11
|
+
const synchronizeLanguage = useLanguageSync(i18nInstance, localePathRedirect, languages, pathname, prevLangRef, setLang);
|
|
12
|
+
return children(synchronizeLanguage);
|
|
13
|
+
};
|
|
14
|
+
const createI18nRootWrapper = (options)=>(App)=>{
|
|
15
|
+
const LanguageSynchronization = options.LanguageSynchronization ?? NativeLanguageSynchronization;
|
|
16
|
+
const I18nRoot = (props)=>{
|
|
17
|
+
const { entryName, htmlLangAttr, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy, getLatestI18nInstance, getI18nextProvider } = options;
|
|
13
18
|
const runtimeContext = useContext(RuntimeContext);
|
|
14
19
|
const i18nInstance = runtimeContext.i18nInstance || getLatestI18nInstance();
|
|
15
20
|
const initialLang = useMemo(()=>i18nInstance?.language || fallbackLanguage, [
|
|
@@ -34,18 +39,16 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
34
39
|
lang
|
|
35
40
|
]);
|
|
36
41
|
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang, synchronizeLanguage), [
|
|
42
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, urlStrategy);
|
|
43
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang, urlStrategy), [
|
|
40
44
|
lang,
|
|
41
45
|
i18nInstance,
|
|
42
46
|
entryName,
|
|
43
47
|
languages,
|
|
44
48
|
localePathRedirect,
|
|
45
49
|
ignoreRedirectRoutes,
|
|
46
|
-
|
|
47
|
-
forceUpdate
|
|
48
|
-
synchronizeLanguage
|
|
50
|
+
urlStrategy,
|
|
51
|
+
forceUpdate
|
|
49
52
|
]);
|
|
50
53
|
const I18nextProvider = getI18nextProvider();
|
|
51
54
|
const i18nextInstanceForProvider = useMemo(()=>i18nInstance ? getI18nextInstanceForProvider(i18nInstance, lang) : void 0, [
|
|
@@ -68,12 +71,33 @@ const createI18nRootWrapper = (options)=>(App)=>(props)=>{
|
|
|
68
71
|
lang
|
|
69
72
|
}
|
|
70
73
|
}),
|
|
71
|
-
/*#__PURE__*/ jsx(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
/*#__PURE__*/ jsx(LanguageSynchronization, {
|
|
75
|
+
i18nInstance: i18nInstance,
|
|
76
|
+
localePathRedirect: localePathRedirect,
|
|
77
|
+
languages: languages,
|
|
78
|
+
pathname: location?.pathname,
|
|
79
|
+
prevLangRef: prevLangRef,
|
|
80
|
+
setLang: setLang,
|
|
81
|
+
children: (synchronizeLanguage)=>/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
82
|
+
i18nextProvider: I18nextProvider,
|
|
83
|
+
value: {
|
|
84
|
+
...contextValue,
|
|
85
|
+
synchronizeLanguage
|
|
86
|
+
},
|
|
87
|
+
children: appContent
|
|
88
|
+
})
|
|
75
89
|
})
|
|
76
90
|
]
|
|
77
91
|
});
|
|
78
92
|
};
|
|
93
|
+
const { NavigationProvider } = options;
|
|
94
|
+
if (!NavigationProvider) return I18nRoot;
|
|
95
|
+
return function(props) {
|
|
96
|
+
return /*#__PURE__*/ jsx(NavigationProvider, {
|
|
97
|
+
children: /*#__PURE__*/ jsx(I18nRoot, {
|
|
98
|
+
...props
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
};
|
|
79
103
|
export { createI18nRootWrapper };
|