@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
package/src/runtime/context.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isBrowser } from '@modern-js/runtime';
|
|
2
1
|
import type { FC, ReactNode } from 'react';
|
|
3
2
|
import {
|
|
4
3
|
createContext,
|
|
@@ -8,18 +7,19 @@ import {
|
|
|
8
7
|
useMemo,
|
|
9
8
|
} from 'react';
|
|
10
9
|
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
10
|
+
import {
|
|
11
|
+
changeModernI18nLanguage,
|
|
12
|
+
getPathLanguage,
|
|
13
|
+
isI18nLanguageSupported,
|
|
14
|
+
isI18nResourcesReady,
|
|
15
|
+
translateI18n,
|
|
16
|
+
} from './contextHelpers';
|
|
11
17
|
import type { I18nInstance } from './i18n';
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
18
|
+
import type { Resources } from './i18n/instance';
|
|
19
|
+
import { getActualI18nextInstance } from './i18n/instance';
|
|
14
20
|
import { useI18nRouterAdapter } from './routerAdapter';
|
|
15
|
-
import {
|
|
16
|
-
buildLocalizedUrl,
|
|
17
|
-
detectLanguageFromPath,
|
|
18
|
-
getEntryPath,
|
|
19
|
-
shouldIgnoreRedirect,
|
|
20
|
-
} from './utils';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
interface ModernI18nContextValue {
|
|
23
23
|
language: string;
|
|
24
24
|
i18nInstance: I18nInstance;
|
|
25
25
|
// Plugin configuration for useModernI18n hook
|
|
@@ -30,6 +30,7 @@ export interface ModernI18nContextValue {
|
|
|
30
30
|
localisedUrls?: LocalisedUrlsOption;
|
|
31
31
|
// Callback to update language in context
|
|
32
32
|
updateLanguage?: (newLang: string) => void;
|
|
33
|
+
synchronizeLanguage?: (newLang: string) => void;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const modernI18nContextKey = Symbol.for(
|
|
@@ -51,7 +52,7 @@ const getModernI18nContext = () => {
|
|
|
51
52
|
|
|
52
53
|
const ModernI18nContext = getModernI18nContext();
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
interface ModernI18nProviderProps {
|
|
55
56
|
children: ReactNode;
|
|
56
57
|
value: ModernI18nContextValue;
|
|
57
58
|
}
|
|
@@ -67,7 +68,126 @@ export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
|
|
|
67
68
|
);
|
|
68
69
|
};
|
|
69
70
|
|
|
70
|
-
export interface
|
|
71
|
+
export interface FederatedI18nBoundaryProps {
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
defaultNamespace: string;
|
|
74
|
+
fallbackLanguage?: string;
|
|
75
|
+
resources: Resources;
|
|
76
|
+
supportedLanguages?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Keeps a federated surface's translation resources inside its delivery unit.
|
|
81
|
+
* The host supplies only the active language; the remote owns and versions the
|
|
82
|
+
* resources used below this boundary.
|
|
83
|
+
*/
|
|
84
|
+
export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
|
|
85
|
+
children,
|
|
86
|
+
defaultNamespace,
|
|
87
|
+
fallbackLanguage,
|
|
88
|
+
resources,
|
|
89
|
+
supportedLanguages,
|
|
90
|
+
}) => {
|
|
91
|
+
const parent = useContext(ModernI18nContext);
|
|
92
|
+
if (!parent) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'FederatedI18nBoundary must be used within ModernI18nProvider',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const languages =
|
|
99
|
+
supportedLanguages ?? parent.languages ?? Object.keys(resources);
|
|
100
|
+
const scopedInstance = useMemo(() => {
|
|
101
|
+
const parentInstance = getActualI18nextInstance(parent.i18nInstance);
|
|
102
|
+
const clone = parentInstance.cloneInstance?.({
|
|
103
|
+
defaultNS: defaultNamespace,
|
|
104
|
+
fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
|
|
105
|
+
forkResourceStore: true,
|
|
106
|
+
initImmediate: false,
|
|
107
|
+
lng: parent.language,
|
|
108
|
+
ns: [defaultNamespace],
|
|
109
|
+
resources,
|
|
110
|
+
supportedLngs: languages,
|
|
111
|
+
});
|
|
112
|
+
if (!clone) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const resourceStore = clone.store;
|
|
118
|
+
const parentResourceStore = parentInstance.store;
|
|
119
|
+
const sharesNestedResourceState = Object.entries(
|
|
120
|
+
resourceStore?.data ?? {},
|
|
121
|
+
).some(([language, namespaces]) => {
|
|
122
|
+
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
123
|
+
return (
|
|
124
|
+
namespaces === parentNamespaces ||
|
|
125
|
+
Object.entries(namespaces).some(
|
|
126
|
+
([namespace, resource]) =>
|
|
127
|
+
resource !== null &&
|
|
128
|
+
typeof resource === 'object' &&
|
|
129
|
+
resource === parentNamespaces?.[namespace],
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
if (
|
|
134
|
+
clone === parentInstance ||
|
|
135
|
+
resourceStore === parentResourceStore ||
|
|
136
|
+
resourceStore?.data === parentResourceStore?.data ||
|
|
137
|
+
sharesNestedResourceState
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
!resourceStore?.addResourceBundle ||
|
|
145
|
+
!clone.removeResourceBundle ||
|
|
146
|
+
!resourceStore.data
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'FederatedI18nBoundary requires an isolated mutable i18n resource store',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
for (const [language, namespaces] of Object.entries(resourceStore.data)) {
|
|
153
|
+
for (const namespace of Object.keys(namespaces)) {
|
|
154
|
+
clone.removeResourceBundle(language, namespace);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [language, namespaces] of Object.entries(resources)) {
|
|
158
|
+
for (const [namespace, resource] of Object.entries(namespaces)) {
|
|
159
|
+
resourceStore.addResourceBundle(
|
|
160
|
+
language,
|
|
161
|
+
namespace,
|
|
162
|
+
resource as Record<string, string>,
|
|
163
|
+
true,
|
|
164
|
+
true,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return clone;
|
|
169
|
+
}, [
|
|
170
|
+
defaultNamespace,
|
|
171
|
+
fallbackLanguage,
|
|
172
|
+
languages,
|
|
173
|
+
parent.i18nInstance,
|
|
174
|
+
parent.language,
|
|
175
|
+
resources,
|
|
176
|
+
]);
|
|
177
|
+
const value = useMemo(
|
|
178
|
+
() => ({
|
|
179
|
+
...parent,
|
|
180
|
+
i18nInstance: scopedInstance,
|
|
181
|
+
language: parent.language,
|
|
182
|
+
languages,
|
|
183
|
+
}),
|
|
184
|
+
[languages, parent, scopedInstance],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return <ModernI18nProvider value={value}>{children}</ModernI18nProvider>;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
interface UseModernI18nReturn {
|
|
71
191
|
language: string;
|
|
72
192
|
changeLanguage: (newLang: string) => Promise<void>;
|
|
73
193
|
t: (key: string | string[], ...args: any[]) => string;
|
|
@@ -75,7 +195,7 @@ export interface UseModernI18nReturn {
|
|
|
75
195
|
supportedLanguages: string[];
|
|
76
196
|
localisedUrls?: LocalisedUrlsOption;
|
|
77
197
|
isLanguageSupported: (lang: string) => boolean;
|
|
78
|
-
// Indicates
|
|
198
|
+
// Indicates whether translation resources for current language are ready
|
|
79
199
|
isResourcesReady: boolean;
|
|
80
200
|
}
|
|
81
201
|
|
|
@@ -85,9 +205,9 @@ export interface UseModernI18nReturn {
|
|
|
85
205
|
* This hook provides:
|
|
86
206
|
* - Current language from URL params or i18n context
|
|
87
207
|
* - changeLanguage function that updates both i18n instance and URL
|
|
88
|
-
* - Direct access to
|
|
208
|
+
* - Direct access to i18n instance
|
|
89
209
|
* - List of supported languages
|
|
90
|
-
* - Helper function to check if
|
|
210
|
+
* - Helper function to check if language is supported
|
|
91
211
|
*
|
|
92
212
|
* @param options - Optional configuration to override context settings
|
|
93
213
|
* @returns Object containing i18n functionality and utilities
|
|
@@ -95,7 +215,7 @@ export interface UseModernI18nReturn {
|
|
|
95
215
|
export const useModernI18n = (): UseModernI18nReturn => {
|
|
96
216
|
const context = useContext(ModernI18nContext);
|
|
97
217
|
if (!context) {
|
|
98
|
-
throw new Error('useModernI18n must be used within
|
|
218
|
+
throw new Error('useModernI18n must be used within ModernI18nProvider');
|
|
99
219
|
}
|
|
100
220
|
|
|
101
221
|
const {
|
|
@@ -106,155 +226,47 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
106
226
|
ignoreRedirectRoutes,
|
|
107
227
|
localisedUrls,
|
|
108
228
|
updateLanguage,
|
|
229
|
+
synchronizeLanguage,
|
|
109
230
|
} = context;
|
|
110
231
|
|
|
111
232
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
112
233
|
|
|
113
|
-
const pathLanguage = useMemo(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const detected = detectLanguageFromPath(
|
|
118
|
-
location.pathname,
|
|
119
|
-
languages || [],
|
|
120
|
-
localePathRedirect,
|
|
121
|
-
);
|
|
122
|
-
return detected.detected ? detected.language : undefined;
|
|
123
|
-
}, [languages, localePathRedirect, location?.pathname]);
|
|
124
|
-
|
|
125
|
-
const currentLanguage = pathLanguage || contextLanguage;
|
|
234
|
+
const pathLanguage = useMemo(
|
|
235
|
+
() => getPathLanguage(location?.pathname, languages, localePathRedirect),
|
|
236
|
+
[languages, localePathRedirect, location?.pathname],
|
|
237
|
+
);
|
|
126
238
|
|
|
127
239
|
useEffect(() => {
|
|
128
|
-
if (
|
|
129
|
-
|
|
240
|
+
if (pathLanguage) {
|
|
241
|
+
synchronizeLanguage?.(pathLanguage);
|
|
130
242
|
}
|
|
243
|
+
}, [pathLanguage, synchronizeLanguage]);
|
|
131
244
|
|
|
132
|
-
|
|
133
|
-
i18nInstance?.setLang?.(pathLanguage);
|
|
134
|
-
void i18nInstance?.changeLanguage?.(pathLanguage);
|
|
135
|
-
|
|
136
|
-
if (isBrowser()) {
|
|
137
|
-
const detectionOptions = i18nInstance.options?.detection;
|
|
138
|
-
cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
|
|
139
|
-
}
|
|
140
|
-
}, [contextLanguage, i18nInstance, pathLanguage, updateLanguage]);
|
|
245
|
+
const currentLanguage = contextLanguage;
|
|
141
246
|
|
|
142
247
|
/**
|
|
143
|
-
* Changes the current language and updates
|
|
248
|
+
* Changes the current language and updates URL accordingly.
|
|
144
249
|
*
|
|
145
250
|
* This function:
|
|
146
|
-
* 1. Updates
|
|
147
|
-
* 2. Updates
|
|
148
|
-
* 3. Triggers
|
|
251
|
+
* 1. Updates i18n instance language
|
|
252
|
+
* 2. Updates URL by replacing language prefix in the current path
|
|
253
|
+
* 3. Triggers navigation to the new URL
|
|
149
254
|
*
|
|
150
255
|
* @param newLang - The new language code to switch to
|
|
151
256
|
*/
|
|
152
257
|
const changeLanguage = useCallback(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
cacheUserLanguage(i18nInstance, newLang, detectionOptions);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
localePathRedirect &&
|
|
170
|
-
isBrowser() &&
|
|
171
|
-
hasRouter &&
|
|
172
|
-
navigate &&
|
|
173
|
-
location
|
|
174
|
-
) {
|
|
175
|
-
const currentPath = location.pathname;
|
|
176
|
-
const entryPath = getEntryPath();
|
|
177
|
-
const relativePath = currentPath.replace(entryPath, '');
|
|
178
|
-
|
|
179
|
-
// Check if the path already contains the target language
|
|
180
|
-
const pathLanguage = detectLanguageFromPath(
|
|
181
|
-
currentPath,
|
|
182
|
-
languages || [],
|
|
183
|
-
localePathRedirect,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// If path already has the target language, skip redirect
|
|
187
|
-
if (pathLanguage.detected && pathLanguage.language === newLang) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
!shouldIgnoreRedirect(
|
|
193
|
-
relativePath,
|
|
194
|
-
languages || [],
|
|
195
|
-
ignoreRedirectRoutes,
|
|
196
|
-
)
|
|
197
|
-
) {
|
|
198
|
-
const newPath = buildLocalizedUrl(
|
|
199
|
-
relativePath,
|
|
200
|
-
newLang,
|
|
201
|
-
languages || [],
|
|
202
|
-
localisedUrls,
|
|
203
|
-
);
|
|
204
|
-
const newUrl =
|
|
205
|
-
entryPath + newPath + location.search + location.hash;
|
|
206
|
-
|
|
207
|
-
await navigate(newUrl, { replace: true });
|
|
208
|
-
}
|
|
209
|
-
} else if (localePathRedirect && isBrowser() && !hasRouter) {
|
|
210
|
-
const currentPath = window.location.pathname;
|
|
211
|
-
const entryPath = getEntryPath();
|
|
212
|
-
const relativePath = currentPath.replace(entryPath, '');
|
|
213
|
-
|
|
214
|
-
// Check if the path already contains the target language
|
|
215
|
-
const pathLanguage = detectLanguageFromPath(
|
|
216
|
-
currentPath,
|
|
217
|
-
languages || [],
|
|
218
|
-
localePathRedirect,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
// If path already has the target language, skip redirect
|
|
222
|
-
if (pathLanguage.detected && pathLanguage.language === newLang) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
!shouldIgnoreRedirect(
|
|
228
|
-
relativePath,
|
|
229
|
-
languages || [],
|
|
230
|
-
ignoreRedirectRoutes,
|
|
231
|
-
)
|
|
232
|
-
) {
|
|
233
|
-
const newPath = buildLocalizedUrl(
|
|
234
|
-
relativePath,
|
|
235
|
-
newLang,
|
|
236
|
-
languages || [],
|
|
237
|
-
localisedUrls,
|
|
238
|
-
);
|
|
239
|
-
const newUrl =
|
|
240
|
-
entryPath +
|
|
241
|
-
newPath +
|
|
242
|
-
window.location.search +
|
|
243
|
-
window.location.hash;
|
|
244
|
-
|
|
245
|
-
window.history.pushState(null, '', newUrl);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Update language state after URL update
|
|
250
|
-
if (updateLanguage) {
|
|
251
|
-
updateLanguage(newLang);
|
|
252
|
-
}
|
|
253
|
-
} catch (error) {
|
|
254
|
-
console.error('Failed to change language:', error);
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
},
|
|
258
|
+
(newLang: string) =>
|
|
259
|
+
changeModernI18nLanguage(newLang, {
|
|
260
|
+
i18nInstance,
|
|
261
|
+
updateLanguage,
|
|
262
|
+
localePathRedirect,
|
|
263
|
+
ignoreRedirectRoutes,
|
|
264
|
+
localisedUrls,
|
|
265
|
+
languages,
|
|
266
|
+
hasRouter,
|
|
267
|
+
navigate,
|
|
268
|
+
location,
|
|
269
|
+
}),
|
|
258
270
|
[
|
|
259
271
|
i18nInstance,
|
|
260
272
|
updateLanguage,
|
|
@@ -269,72 +281,23 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
269
281
|
);
|
|
270
282
|
|
|
271
283
|
const t = useCallback(
|
|
272
|
-
(key: string | string[], ...args: any[]) =>
|
|
273
|
-
|
|
274
|
-
throw new Error('i18nInstance.t is required');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return i18nInstance.t(key, ...args) as string;
|
|
278
|
-
},
|
|
284
|
+
(key: string | string[], ...args: any[]) =>
|
|
285
|
+
translateI18n(i18nInstance, key, ...args),
|
|
279
286
|
[currentLanguage, i18nInstance],
|
|
280
287
|
);
|
|
281
288
|
|
|
282
|
-
// Helper function to check if
|
|
289
|
+
// Helper function to check if language is supported
|
|
283
290
|
const isLanguageSupported = useCallback(
|
|
284
|
-
(lang: string) =>
|
|
285
|
-
return languages?.includes(lang) || false;
|
|
286
|
-
},
|
|
291
|
+
(lang: string) => isI18nLanguageSupported(languages, lang),
|
|
287
292
|
[languages],
|
|
288
293
|
);
|
|
289
294
|
|
|
290
295
|
// Check if current language resources are ready
|
|
291
|
-
// This checks if all required namespaces for
|
|
292
|
-
const isResourcesReady = useMemo(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Get backend instance
|
|
298
|
-
const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
|
|
299
|
-
|
|
300
|
-
// If using SDK backend, check loading state
|
|
301
|
-
if (backend && typeof backend.isLoading === 'function') {
|
|
302
|
-
// Check if any resource for current language is loading
|
|
303
|
-
const loadingResources = backend.getLoadingResources();
|
|
304
|
-
const isCurrentLanguageLoading = loadingResources.some(
|
|
305
|
-
({ language }) => language === currentLanguage,
|
|
306
|
-
);
|
|
307
|
-
if (isCurrentLanguageLoading) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check if resources exist in store
|
|
313
|
-
const store = (i18nInstance as any).store;
|
|
314
|
-
if (!store?.data) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const langData = store.data[currentLanguage];
|
|
319
|
-
if (!langData || typeof langData !== 'object') {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Get required namespaces
|
|
324
|
-
const options = i18nInstance.options;
|
|
325
|
-
const namespaces = options?.ns || options?.defaultNS || ['translation'];
|
|
326
|
-
const requiredNamespaces = Array.isArray(namespaces)
|
|
327
|
-
? namespaces
|
|
328
|
-
: [namespaces];
|
|
329
|
-
|
|
330
|
-
// Check if all required namespaces are loaded
|
|
331
|
-
return requiredNamespaces.every(ns => {
|
|
332
|
-
const nsData = langData[ns];
|
|
333
|
-
return (
|
|
334
|
-
nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
|
|
335
|
-
);
|
|
336
|
-
});
|
|
337
|
-
}, [currentLanguage, i18nInstance]);
|
|
296
|
+
// This checks if all required namespaces for current language are loaded
|
|
297
|
+
const isResourcesReady = useMemo(
|
|
298
|
+
() => isI18nResourcesReady(i18nInstance, currentLanguage),
|
|
299
|
+
[currentLanguage, i18nInstance],
|
|
300
|
+
);
|
|
338
301
|
|
|
339
302
|
return {
|
|
340
303
|
language: currentLanguage,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
3
|
+
import type { I18nInstance } from './i18n';
|
|
4
|
+
import type { SdkBackend } from './i18n/backend/sdk-backend';
|
|
5
|
+
import { cacheUserLanguage } from './i18n/detection';
|
|
6
|
+
import {
|
|
7
|
+
buildLocalizedUrl,
|
|
8
|
+
detectLanguageFromPath,
|
|
9
|
+
getEntryPath,
|
|
10
|
+
shouldIgnoreRedirect,
|
|
11
|
+
} from './utils';
|
|
12
|
+
|
|
13
|
+
type Navigate = (
|
|
14
|
+
url: string,
|
|
15
|
+
options?: {
|
|
16
|
+
replace?: boolean;
|
|
17
|
+
},
|
|
18
|
+
) => Promise<void> | void;
|
|
19
|
+
|
|
20
|
+
type LocationLike = {
|
|
21
|
+
pathname: string;
|
|
22
|
+
search: string;
|
|
23
|
+
hash: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function getPathLanguage(
|
|
27
|
+
pathname: string | undefined,
|
|
28
|
+
languages: string[] | undefined,
|
|
29
|
+
localePathRedirect: boolean | undefined,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (!localePathRedirect || !pathname) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const detected = detectLanguageFromPath(
|
|
36
|
+
pathname,
|
|
37
|
+
languages || [],
|
|
38
|
+
localePathRedirect,
|
|
39
|
+
);
|
|
40
|
+
return detected.detected ? detected.language : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function cacheI18nLanguage(
|
|
44
|
+
i18nInstance: I18nInstance,
|
|
45
|
+
language: string,
|
|
46
|
+
) {
|
|
47
|
+
if (isBrowser()) {
|
|
48
|
+
const detectionOptions = i18nInstance.options?.detection;
|
|
49
|
+
cacheUserLanguage(i18nInstance, language, detectionOptions);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function changeI18nInstanceLanguage(
|
|
54
|
+
i18nInstance: I18nInstance,
|
|
55
|
+
language: string,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
if (i18nInstance.setLang) {
|
|
58
|
+
await i18nInstance.setLang(language);
|
|
59
|
+
} else {
|
|
60
|
+
await i18nInstance.changeLanguage?.(language);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ChangeModernI18nLanguageOptions {
|
|
65
|
+
i18nInstance: I18nInstance;
|
|
66
|
+
updateLanguage?: (newLang: string) => void;
|
|
67
|
+
localePathRedirect?: boolean;
|
|
68
|
+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
69
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
70
|
+
languages?: string[];
|
|
71
|
+
hasRouter: boolean;
|
|
72
|
+
navigate?: Navigate | null;
|
|
73
|
+
location?: LocationLike | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function changeModernI18nLanguage(
|
|
77
|
+
newLang: string,
|
|
78
|
+
options: ChangeModernI18nLanguageOptions,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const {
|
|
81
|
+
i18nInstance,
|
|
82
|
+
updateLanguage,
|
|
83
|
+
localePathRedirect,
|
|
84
|
+
ignoreRedirectRoutes,
|
|
85
|
+
localisedUrls,
|
|
86
|
+
languages,
|
|
87
|
+
hasRouter,
|
|
88
|
+
navigate,
|
|
89
|
+
location,
|
|
90
|
+
} = options;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (!newLang || typeof newLang !== 'string') {
|
|
94
|
+
throw new Error('Language must be non-empty string');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await changeI18nInstanceLanguage(i18nInstance, newLang);
|
|
98
|
+
cacheI18nLanguage(i18nInstance, newLang);
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
localePathRedirect &&
|
|
102
|
+
isBrowser() &&
|
|
103
|
+
hasRouter &&
|
|
104
|
+
navigate &&
|
|
105
|
+
location
|
|
106
|
+
) {
|
|
107
|
+
const currentPath = location.pathname;
|
|
108
|
+
const entryPath = getEntryPath();
|
|
109
|
+
const relativePath = currentPath.replace(entryPath, '');
|
|
110
|
+
const pathLanguage = detectLanguageFromPath(
|
|
111
|
+
currentPath,
|
|
112
|
+
languages || [],
|
|
113
|
+
localePathRedirect,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (pathLanguage.detected && pathLanguage.language === newLang) {
|
|
117
|
+
updateLanguage?.(newLang);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
!shouldIgnoreRedirect(
|
|
123
|
+
relativePath,
|
|
124
|
+
languages || [],
|
|
125
|
+
ignoreRedirectRoutes,
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
const newPath = buildLocalizedUrl(
|
|
129
|
+
relativePath,
|
|
130
|
+
newLang,
|
|
131
|
+
languages || [],
|
|
132
|
+
localisedUrls,
|
|
133
|
+
);
|
|
134
|
+
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
135
|
+
|
|
136
|
+
await navigate(newUrl, { replace: true });
|
|
137
|
+
}
|
|
138
|
+
} else if (localePathRedirect && isBrowser() && !hasRouter) {
|
|
139
|
+
const currentPath = window.location.pathname;
|
|
140
|
+
const entryPath = getEntryPath();
|
|
141
|
+
const relativePath = currentPath.replace(entryPath, '');
|
|
142
|
+
const pathLanguage = detectLanguageFromPath(
|
|
143
|
+
currentPath,
|
|
144
|
+
languages || [],
|
|
145
|
+
localePathRedirect,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (pathLanguage.detected && pathLanguage.language === newLang) {
|
|
149
|
+
updateLanguage?.(newLang);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
!shouldIgnoreRedirect(
|
|
155
|
+
relativePath,
|
|
156
|
+
languages || [],
|
|
157
|
+
ignoreRedirectRoutes,
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
const newPath = buildLocalizedUrl(
|
|
161
|
+
relativePath,
|
|
162
|
+
newLang,
|
|
163
|
+
languages || [],
|
|
164
|
+
localisedUrls,
|
|
165
|
+
);
|
|
166
|
+
const newUrl =
|
|
167
|
+
entryPath + newPath + window.location.search + window.location.hash;
|
|
168
|
+
|
|
169
|
+
window.history.pushState(null, '', newUrl);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (updateLanguage) {
|
|
174
|
+
updateLanguage(newLang);
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('Failed change language:', error);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function translateI18n(
|
|
183
|
+
i18nInstance: I18nInstance,
|
|
184
|
+
key: string | string[],
|
|
185
|
+
...args: unknown[]
|
|
186
|
+
): string {
|
|
187
|
+
if (typeof i18nInstance.t !== 'function') {
|
|
188
|
+
throw new Error('i18nInstance.t required');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return i18nInstance.t(key, ...args) as string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function isI18nLanguageSupported(
|
|
195
|
+
languages: string[] | undefined,
|
|
196
|
+
lang: string,
|
|
197
|
+
): boolean {
|
|
198
|
+
return languages?.includes(lang) || false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isI18nResourcesReady(
|
|
202
|
+
i18nInstance: I18nInstance,
|
|
203
|
+
currentLanguage: string,
|
|
204
|
+
): boolean {
|
|
205
|
+
if (!i18nInstance?.isInitialized) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
|
|
210
|
+
if (backend && typeof backend.isLoading === 'function') {
|
|
211
|
+
const loadingResources = backend.getLoadingResources();
|
|
212
|
+
const isCurrentLanguageLoading = loadingResources.some(
|
|
213
|
+
({ language }) => language === currentLanguage,
|
|
214
|
+
);
|
|
215
|
+
if (isCurrentLanguageLoading) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const store = i18nInstance.store;
|
|
221
|
+
if (!store?.data) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const langData = store.data[currentLanguage];
|
|
226
|
+
if (!langData || typeof langData !== 'object') {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const options = i18nInstance.options;
|
|
231
|
+
const namespaces = options?.ns || options?.defaultNS || ['translation'];
|
|
232
|
+
const requiredNamespaces = Array.isArray(namespaces)
|
|
233
|
+
? namespaces
|
|
234
|
+
: [namespaces];
|
|
235
|
+
|
|
236
|
+
return requiredNamespaces.every(ns => {
|
|
237
|
+
const nsData = langData[ns];
|
|
238
|
+
return (
|
|
239
|
+
nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
}
|