@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.4 → 3.5.0-ultramodern.41
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 +18 -82
- package/dist/cjs/runtime/contextHelpers.js +137 -0
- package/dist/cjs/runtime/core.js +29 -132
- 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 +7 -7
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +114 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- 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 +95 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
- package/dist/cjs/shared/localisedUrls/routes.js +129 -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 +18 -82
- package/dist/esm/runtime/contextHelpers.mjs +84 -0
- package/dist/esm/runtime/core.mjs +30 -133
- 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 +8 -5
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +76 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- 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 +48 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +88 -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 +18 -82
- package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- 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 +8 -5
- 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 +77 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- 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 +49 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -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 +6 -5
- package/dist/types/runtime/contextHelpers.d.ts +28 -0
- package/dist/types/runtime/core.d.ts +3 -7
- package/dist/types/runtime/hooks.d.ts +5 -0
- 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 +28 -26
- 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 +16 -18
- package/rstest.config.mts +1 -0
- 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 +45 -199
- package/src/runtime/contextHelpers.ts +232 -0
- package/src/runtime/core.tsx +43 -274
- package/src/runtime/hooks.ts +13 -2
- 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 +56 -31
- 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 +144 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/runtime/routerAdapter.tsx +6 -6
- 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 +91 -0
- package/src/shared/localisedUrls/resolve.ts +135 -0
- package/src/shared/localisedUrls/routes.ts +226 -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/fixtures/localised-routes.golden.json +156 -0
- package/tests/link.test.tsx +111 -0
- package/tests/linkTypes.test.ts +15 -4
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
- package/tests/localisedUrls.test.ts +129 -2
- package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
- package/tests/redirectPolicy.test.ts +43 -0
- package/tests/routerAdapter.test.tsx +85 -0
- package/tests/type-fixture/fixture-globals.d.ts +11 -0
- package/tests/type-fixture/tsconfig.json +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const warnedTargets = new Set<string>();
|
|
2
|
+
|
|
3
|
+
export const warnOnce = (key: string, message: string) => {
|
|
4
|
+
if (process.env.NODE_ENV !== 'development' || warnedTargets.has(key)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
warnedTargets.add(key);
|
|
8
|
+
console.warn(message);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LinkParams = Record<string, string | number | undefined>;
|
|
12
|
+
type NormalizedSearchValue = string | string[];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
|
|
16
|
+
* (`$` / `*`) segments with concrete values before localization, so
|
|
17
|
+
* pattern-mapped slugs localize correctly.
|
|
18
|
+
*/
|
|
19
|
+
export const interpolateRouteParams = (
|
|
20
|
+
pathname: string,
|
|
21
|
+
params?: LinkParams,
|
|
22
|
+
): string => {
|
|
23
|
+
if (!/[$:*{]/.test(pathname)) {
|
|
24
|
+
return pathname;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const resolveParam = (name: string): string | undefined => {
|
|
28
|
+
const value = params?.[name];
|
|
29
|
+
return value === undefined ? undefined : String(value);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const segments = pathname
|
|
33
|
+
.split('/')
|
|
34
|
+
.map(segment => {
|
|
35
|
+
if (!segment) {
|
|
36
|
+
return segment;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
40
|
+
const value = resolveParam(segment.slice(3, -1));
|
|
41
|
+
return value === undefined ? null : encodeURIComponent(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (segment === '$' || segment === '*') {
|
|
45
|
+
const value = resolveParam('_splat') ?? resolveParam('*');
|
|
46
|
+
return value === undefined
|
|
47
|
+
? null
|
|
48
|
+
: value.split('/').map(encodeURIComponent).join('/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (segment.startsWith('$')) {
|
|
52
|
+
const value = resolveParam(segment.slice(1));
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
warnOnce(
|
|
55
|
+
`missing-param:${pathname}:${segment}`,
|
|
56
|
+
`[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`,
|
|
57
|
+
);
|
|
58
|
+
return segment;
|
|
59
|
+
}
|
|
60
|
+
return encodeURIComponent(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (segment.startsWith(':')) {
|
|
64
|
+
const optional = segment.endsWith('?');
|
|
65
|
+
const name = segment.slice(1, optional ? -1 : undefined);
|
|
66
|
+
const value = resolveParam(name);
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
if (optional) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
warnOnce(
|
|
72
|
+
`missing-param:${pathname}:${segment}`,
|
|
73
|
+
`[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`,
|
|
74
|
+
);
|
|
75
|
+
return segment;
|
|
76
|
+
}
|
|
77
|
+
return encodeURIComponent(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return segment;
|
|
81
|
+
})
|
|
82
|
+
.filter(segment => segment !== null);
|
|
83
|
+
|
|
84
|
+
return segments.join('/') || '/';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const normalizeSearch = (
|
|
88
|
+
search: string | Record<string, unknown> | undefined,
|
|
89
|
+
searchFromTo: string,
|
|
90
|
+
): {
|
|
91
|
+
searchString: string;
|
|
92
|
+
searchObject: Record<string, NormalizedSearchValue> | undefined;
|
|
93
|
+
} => {
|
|
94
|
+
if (search && typeof search === 'object') {
|
|
95
|
+
const searchObject: Record<string, NormalizedSearchValue> = {};
|
|
96
|
+
const params = new URLSearchParams();
|
|
97
|
+
|
|
98
|
+
for (const [key, value] of Object.entries(search)) {
|
|
99
|
+
if (value === undefined || value === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const values = value
|
|
105
|
+
.filter(item => item !== undefined && item !== null)
|
|
106
|
+
.map(String);
|
|
107
|
+
if (values.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
searchObject[key] = values;
|
|
111
|
+
for (const item of values) {
|
|
112
|
+
params.append(key, item);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const stringValue = String(value);
|
|
118
|
+
searchObject[key] = stringValue;
|
|
119
|
+
params.append(key, stringValue);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const serialized = params.toString();
|
|
123
|
+
return {
|
|
124
|
+
searchString: serialized ? `?${serialized}` : '',
|
|
125
|
+
searchObject:
|
|
126
|
+
Object.keys(searchObject).length > 0 ? searchObject : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const raw = typeof search === 'string' ? search : searchFromTo;
|
|
131
|
+
if (!raw) {
|
|
132
|
+
return { searchString: '', searchObject: undefined };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const searchString = raw.startsWith('?') ? raw : `?${raw}`;
|
|
136
|
+
const searchObject: Record<string, NormalizedSearchValue> = {};
|
|
137
|
+
new URLSearchParams(searchString).forEach((value, key) => {
|
|
138
|
+
const existing = searchObject[key];
|
|
139
|
+
if (existing === undefined) {
|
|
140
|
+
searchObject[key] = value;
|
|
141
|
+
} else if (Array.isArray(existing)) {
|
|
142
|
+
existing.push(value);
|
|
143
|
+
} else {
|
|
144
|
+
searchObject[key] = [existing, value];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return { searchString, searchObject };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type ActivePropsResult<TActiveProps extends Record<string, unknown>> = Record<
|
|
152
|
+
string,
|
|
153
|
+
unknown
|
|
154
|
+
> &
|
|
155
|
+
Partial<TActiveProps>;
|
|
156
|
+
|
|
157
|
+
export const splitActiveProps = <TActiveProps extends Record<string, unknown>>(
|
|
158
|
+
active: boolean,
|
|
159
|
+
activeProps?: TActiveProps,
|
|
160
|
+
): ActivePropsResult<TActiveProps> => {
|
|
161
|
+
if (!active || !activeProps) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
return activeProps;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const mergeClassNames = (
|
|
168
|
+
...values: Array<unknown>
|
|
169
|
+
): string | undefined => {
|
|
170
|
+
const classNames = values.filter(
|
|
171
|
+
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
172
|
+
);
|
|
173
|
+
return classNames.length > 0 ? classNames.join(' ') : undefined;
|
|
174
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
|
|
3
|
+
import { merge } from '@modern-js/runtime-utils/merge';
|
|
4
|
+
import type {
|
|
5
|
+
BaseBackendOptions,
|
|
6
|
+
BaseLocaleDetectionOptions,
|
|
7
|
+
} from '../shared/type';
|
|
8
|
+
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
9
|
+
import { getI18nInstance } from './i18n';
|
|
10
|
+
import { mergeBackendOptions } from './i18n/backend';
|
|
11
|
+
import {
|
|
12
|
+
detectLanguageWithPriority,
|
|
13
|
+
exportServerLngToWindow,
|
|
14
|
+
mergeDetectionOptions,
|
|
15
|
+
} from './i18n/detection';
|
|
16
|
+
import { useI18nextLanguageDetector } from './i18n/detection/middleware';
|
|
17
|
+
import type { ReactI18nextIntegration } from './reactI18next';
|
|
18
|
+
import { getPathname } from './utils';
|
|
19
|
+
|
|
20
|
+
type I18nLifecycleHelpers = {
|
|
21
|
+
useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
|
|
22
|
+
changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
|
|
23
|
+
ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
|
|
24
|
+
initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
|
|
25
|
+
setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
|
|
29
|
+
|
|
30
|
+
function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
|
|
31
|
+
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
32
|
+
import('./i18n/backend/middleware'),
|
|
33
|
+
import('./i18n/utils'),
|
|
34
|
+
]).then(([backendMiddleware, utils]) => ({
|
|
35
|
+
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
36
|
+
changeI18nLanguage: utils.changeI18nLanguage,
|
|
37
|
+
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
38
|
+
initializeI18nInstance: utils.initializeI18nInstance,
|
|
39
|
+
setupClonedInstance: utils.setupClonedInstance,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
return i18nLifecycleHelpersPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RuntimeConfigWithI18n {
|
|
46
|
+
i18n?: {
|
|
47
|
+
initOptions?: I18nInitOptions;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface I18nRuntimeApi {
|
|
52
|
+
getRuntimeConfig: () => RuntimeConfigWithI18n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RuntimeContextWithI18n extends TInternalRuntimeContext {
|
|
56
|
+
i18nInstance?: I18nInstance;
|
|
57
|
+
changeLanguage?: (lang: string) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SetupI18nBeforeRenderOptions {
|
|
61
|
+
api: I18nRuntimeApi;
|
|
62
|
+
userI18nInstance?: I18nInstance;
|
|
63
|
+
initOptions?: I18nInitOptions;
|
|
64
|
+
backend?: BaseBackendOptions;
|
|
65
|
+
backendEnabled: boolean;
|
|
66
|
+
i18nextDetector: boolean;
|
|
67
|
+
detection?: BaseLocaleDetectionOptions['detection'];
|
|
68
|
+
localePathRedirect: boolean;
|
|
69
|
+
languages: string[];
|
|
70
|
+
fallbackLanguage: string;
|
|
71
|
+
resolveReactI18nextIntegration: () => Promise<ReactI18nextIntegration | null>;
|
|
72
|
+
setI18nextProvider: (
|
|
73
|
+
provider: ReactI18nextIntegration['I18nextProvider'],
|
|
74
|
+
) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function setupI18nBeforeRender(
|
|
78
|
+
context: RuntimeContextWithI18n,
|
|
79
|
+
options: SetupI18nBeforeRenderOptions,
|
|
80
|
+
): Promise<I18nInstance> {
|
|
81
|
+
const {
|
|
82
|
+
api,
|
|
83
|
+
userI18nInstance,
|
|
84
|
+
initOptions,
|
|
85
|
+
backend,
|
|
86
|
+
backendEnabled,
|
|
87
|
+
i18nextDetector,
|
|
88
|
+
detection,
|
|
89
|
+
localePathRedirect,
|
|
90
|
+
languages,
|
|
91
|
+
fallbackLanguage,
|
|
92
|
+
resolveReactI18nextIntegration,
|
|
93
|
+
setI18nextProvider,
|
|
94
|
+
} = options;
|
|
95
|
+
const {
|
|
96
|
+
useI18nextBackend,
|
|
97
|
+
changeI18nLanguage,
|
|
98
|
+
ensureLanguageMatch,
|
|
99
|
+
initializeI18nInstance,
|
|
100
|
+
setupClonedInstance,
|
|
101
|
+
} = await loadI18nLifecycleHelpers();
|
|
102
|
+
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
103
|
+
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
104
|
+
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
105
|
+
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
106
|
+
|
|
107
|
+
const reactI18nextIntegration = await resolveReactI18nextIntegration();
|
|
108
|
+
setI18nextProvider(reactI18nextIntegration?.I18nextProvider ?? null);
|
|
109
|
+
if (reactI18nextIntegration?.initReactI18next) {
|
|
110
|
+
i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pathname = getPathname(context);
|
|
114
|
+
if (i18nextDetector) {
|
|
115
|
+
useI18nextLanguageDetector(i18nInstance);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mergedDetection = mergeDetectionOptions(
|
|
119
|
+
i18nextDetector,
|
|
120
|
+
detection,
|
|
121
|
+
localePathRedirect,
|
|
122
|
+
userInitOptions,
|
|
123
|
+
);
|
|
124
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
125
|
+
|
|
126
|
+
// Register Backend BEFORE detectLanguageWithPriority because detection may init.
|
|
127
|
+
const hasSdkConfig =
|
|
128
|
+
typeof userInitOptions?.backend?.sdk === 'function' ||
|
|
129
|
+
(mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
|
|
130
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) {
|
|
131
|
+
useI18nextBackend(i18nInstance, mergedBackend);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
135
|
+
languages,
|
|
136
|
+
fallbackLanguage,
|
|
137
|
+
localePathRedirect,
|
|
138
|
+
i18nextDetector,
|
|
139
|
+
detection,
|
|
140
|
+
userInitOptions,
|
|
141
|
+
mergedBackend,
|
|
142
|
+
pathname,
|
|
143
|
+
ssrContext: context.ssrContext,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await initializeI18nInstance(
|
|
147
|
+
i18nInstance,
|
|
148
|
+
finalLanguage,
|
|
149
|
+
fallbackLanguage,
|
|
150
|
+
languages,
|
|
151
|
+
mergedDetection,
|
|
152
|
+
mergedBackend,
|
|
153
|
+
userInitOptions,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
157
|
+
i18nInstance = i18nInstance.cloneInstance();
|
|
158
|
+
await setupClonedInstance(
|
|
159
|
+
i18nInstance,
|
|
160
|
+
finalLanguage,
|
|
161
|
+
fallbackLanguage,
|
|
162
|
+
languages,
|
|
163
|
+
backendEnabled,
|
|
164
|
+
backend,
|
|
165
|
+
i18nextDetector,
|
|
166
|
+
detection,
|
|
167
|
+
localePathRedirect,
|
|
168
|
+
userInitOptions,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (localePathRedirect) {
|
|
173
|
+
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isBrowser()) {
|
|
177
|
+
exportServerLngToWindow(context, finalLanguage);
|
|
178
|
+
}
|
|
179
|
+
context.i18nInstance = i18nInstance;
|
|
180
|
+
|
|
181
|
+
// Add changeLanguage method to context for other runtime plugins to use.
|
|
182
|
+
context.changeLanguage = async (newLang: string) => {
|
|
183
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
184
|
+
detectionOptions: mergedDetection,
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return i18nInstance;
|
|
189
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { RuntimeContext } from '@modern-js/runtime';
|
|
2
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
3
|
+
import type React from 'react';
|
|
4
|
+
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import type { BaseLocaleDetectionOptions } from '../shared/type';
|
|
6
|
+
import { ModernI18nProvider } from './context';
|
|
7
|
+
import {
|
|
8
|
+
createContextValue,
|
|
9
|
+
useClientSideRedirect,
|
|
10
|
+
useLanguageSync,
|
|
11
|
+
useSdkResourcesLoader,
|
|
12
|
+
} from './hooks';
|
|
13
|
+
import type { I18nInstance } from './i18n';
|
|
14
|
+
import { getI18nextInstanceForProvider } from './i18n/instance';
|
|
15
|
+
import type { RuntimeContextWithI18n } from './pluginSetup';
|
|
16
|
+
|
|
17
|
+
interface I18nRootWrapperOptions {
|
|
18
|
+
entryName?: string;
|
|
19
|
+
htmlLangAttr: boolean;
|
|
20
|
+
localePathRedirect: boolean;
|
|
21
|
+
languages: string[];
|
|
22
|
+
fallbackLanguage: string;
|
|
23
|
+
ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
|
|
24
|
+
localisedUrls?: BaseLocaleDetectionOptions['localisedUrls'];
|
|
25
|
+
getLatestI18nInstance: () => I18nInstance | undefined;
|
|
26
|
+
getI18nextProvider: () => React.ComponentType<any> | null | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const createI18nRootWrapper =
|
|
30
|
+
(options: I18nRootWrapperOptions) => (App: React.ComponentType<any>) => {
|
|
31
|
+
return (props: Record<string, unknown>) => {
|
|
32
|
+
const {
|
|
33
|
+
entryName,
|
|
34
|
+
htmlLangAttr,
|
|
35
|
+
localePathRedirect,
|
|
36
|
+
languages,
|
|
37
|
+
fallbackLanguage,
|
|
38
|
+
ignoreRedirectRoutes,
|
|
39
|
+
localisedUrls,
|
|
40
|
+
getLatestI18nInstance,
|
|
41
|
+
getI18nextProvider,
|
|
42
|
+
} = options;
|
|
43
|
+
const runtimeContext = useContext(
|
|
44
|
+
RuntimeContext,
|
|
45
|
+
) as RuntimeContextWithI18n;
|
|
46
|
+
const i18nInstance =
|
|
47
|
+
runtimeContext.i18nInstance || getLatestI18nInstance();
|
|
48
|
+
const initialLang = useMemo(
|
|
49
|
+
() => i18nInstance?.language || fallbackLanguage,
|
|
50
|
+
[i18nInstance?.language, fallbackLanguage],
|
|
51
|
+
);
|
|
52
|
+
const [lang, setLang] = useState(initialLang);
|
|
53
|
+
const [forceUpdate, setForceUpdate] = useState(0);
|
|
54
|
+
const prevLangRef = useRef(lang);
|
|
55
|
+
const runtimeContextRef = useRef(runtimeContext);
|
|
56
|
+
runtimeContextRef.current = runtimeContext;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (i18nInstance?.language) {
|
|
60
|
+
const translator = (i18nInstance as any).translator;
|
|
61
|
+
if (translator) {
|
|
62
|
+
translator.language = i18nInstance.language;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [i18nInstance?.language]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
prevLangRef.current = lang;
|
|
69
|
+
}, [lang]);
|
|
70
|
+
|
|
71
|
+
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
72
|
+
useLanguageSync(
|
|
73
|
+
i18nInstance,
|
|
74
|
+
localePathRedirect,
|
|
75
|
+
languages,
|
|
76
|
+
runtimeContextRef,
|
|
77
|
+
prevLangRef,
|
|
78
|
+
setLang,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Handle client-side redirect for static deployments.
|
|
82
|
+
useClientSideRedirect(
|
|
83
|
+
i18nInstance,
|
|
84
|
+
localePathRedirect,
|
|
85
|
+
languages,
|
|
86
|
+
fallbackLanguage,
|
|
87
|
+
ignoreRedirectRoutes,
|
|
88
|
+
localisedUrls,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const contextValue = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
createContextValue(
|
|
94
|
+
lang,
|
|
95
|
+
i18nInstance,
|
|
96
|
+
entryName,
|
|
97
|
+
languages,
|
|
98
|
+
localePathRedirect,
|
|
99
|
+
ignoreRedirectRoutes,
|
|
100
|
+
localisedUrls,
|
|
101
|
+
setLang,
|
|
102
|
+
),
|
|
103
|
+
[
|
|
104
|
+
lang,
|
|
105
|
+
i18nInstance,
|
|
106
|
+
entryName,
|
|
107
|
+
languages,
|
|
108
|
+
localePathRedirect,
|
|
109
|
+
ignoreRedirectRoutes,
|
|
110
|
+
localisedUrls,
|
|
111
|
+
forceUpdate,
|
|
112
|
+
],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const children = (props as React.PropsWithChildren).children;
|
|
116
|
+
let appContent: React.ReactNode = App ? (
|
|
117
|
+
<App {...props}>{children}</App>
|
|
118
|
+
) : (
|
|
119
|
+
children
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (i18nInstance) {
|
|
123
|
+
const I18nextProvider = getI18nextProvider();
|
|
124
|
+
if (I18nextProvider) {
|
|
125
|
+
const i18nextInstanceForProvider =
|
|
126
|
+
getI18nextInstanceForProvider(i18nInstance);
|
|
127
|
+
appContent = (
|
|
128
|
+
<I18nextProvider i18n={i18nextInstanceForProvider}>
|
|
129
|
+
{appContent}
|
|
130
|
+
</I18nextProvider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
{Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
|
|
138
|
+
<ModernI18nProvider value={contextValue}>
|
|
139
|
+
{appContent}
|
|
140
|
+
</ModernI18nProvider>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ReactI18nextIntegration {
|
|
4
|
+
I18nextProvider: React.ComponentType<any> | null;
|
|
5
|
+
initReactI18next: any | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type LoadReactI18nextIntegration =
|
|
9
|
+
() => Promise<ReactI18nextIntegration | null>;
|
|
10
|
+
|
|
11
|
+
export const resolveReactI18nextIntegration = async (
|
|
12
|
+
reactI18next: boolean,
|
|
13
|
+
loadReactI18nextIntegration?: LoadReactI18nextIntegration,
|
|
14
|
+
): Promise<ReactI18nextIntegration | null> => {
|
|
15
|
+
if (!reactI18next) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return loadReactI18nextIntegration?.() ?? null;
|
|
20
|
+
};
|
|
@@ -15,31 +15,31 @@ import {
|
|
|
15
15
|
import type React from 'react';
|
|
16
16
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface I18nRouterLocation {
|
|
21
21
|
pathname: string;
|
|
22
22
|
search: string;
|
|
23
23
|
hash: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface I18nRouterNavigateOptions {
|
|
27
27
|
replace?: boolean;
|
|
28
28
|
state?: unknown;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
type I18nRouterNavigate = (
|
|
32
32
|
href: string,
|
|
33
33
|
options?: I18nRouterNavigateOptions,
|
|
34
34
|
) => void | Promise<void>;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
type I18nRouterLink = React.ComponentType<{
|
|
37
37
|
to: string;
|
|
38
38
|
children?: React.ReactNode;
|
|
39
39
|
[key: string]: unknown;
|
|
40
40
|
}>;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
interface I18nRouterAdapter {
|
|
43
43
|
framework?: I18nRouterFramework;
|
|
44
44
|
hasRouter: boolean;
|
|
45
45
|
location: I18nRouterLocation | null;
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
-
import {
|
|
3
|
-
getGlobalBasename,
|
|
4
|
-
type TInternalRuntimeContext,
|
|
5
|
-
} from '@modern-js/runtime/context';
|
|
2
|
+
import { getGlobalBasename } from '@modern-js/runtime/context';
|
|
6
3
|
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import {
|
|
5
|
+
localiseTargetPathname,
|
|
6
|
+
shouldSkipLocaleRedirect,
|
|
7
|
+
} from '../shared/localisedUrls';
|
|
8
|
+
|
|
9
|
+
// Structural parameter: hooks.ts passes a public-TRuntimeContext-based
|
|
10
|
+
// context while core.tsx passes the internal one; both carry the request
|
|
11
|
+
// pathname shape this helper needs.
|
|
12
|
+
export const getPathname = (context: {
|
|
13
|
+
ssrContext?: { request?: { pathname?: string } };
|
|
14
|
+
}): string => {
|
|
10
15
|
if (isBrowser()) {
|
|
11
16
|
return window.location.pathname;
|
|
12
17
|
}
|
|
@@ -127,32 +132,5 @@ export const shouldIgnoreRedirect = (
|
|
|
127
132
|
languages: string[],
|
|
128
133
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
129
134
|
): boolean => {
|
|
130
|
-
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Remove language prefix if present (e.g., /en/api -> /api)
|
|
135
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
136
|
-
let pathWithoutLang = pathname;
|
|
137
|
-
if (segments.length > 0 && languages.includes(segments[0])) {
|
|
138
|
-
// Remove language prefix
|
|
139
|
-
pathWithoutLang = `/${segments.slice(1).join('/')}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Normalize path (ensure it starts with /)
|
|
143
|
-
const normalizedPath = pathWithoutLang.startsWith('/')
|
|
144
|
-
? pathWithoutLang
|
|
145
|
-
: `/${pathWithoutLang}`;
|
|
146
|
-
|
|
147
|
-
if (typeof ignoreRedirectRoutes === 'function') {
|
|
148
|
-
return ignoreRedirectRoutes(normalizedPath);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check if pathname matches any of the ignore patterns
|
|
152
|
-
return ignoreRedirectRoutes.some(pattern => {
|
|
153
|
-
// Support both exact match and prefix match
|
|
154
|
-
return (
|
|
155
|
-
normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
|
|
156
|
-
);
|
|
157
|
-
});
|
|
135
|
+
return shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
|
|
158
136
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
type ApiPrefixInput = string | string[] | undefined;
|
|
2
|
+
|
|
3
|
+
const normalizeApiPrefix = (prefix: string): string | null => {
|
|
4
|
+
const trimmedPrefix = prefix.trim();
|
|
5
|
+
if (!trimmedPrefix) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const prefixedPath = trimmedPrefix.startsWith('/')
|
|
10
|
+
? trimmedPrefix
|
|
11
|
+
: `/${trimmedPrefix}`;
|
|
12
|
+
const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
|
|
13
|
+
const normalizedPrefix =
|
|
14
|
+
withoutWildcard.length > 1
|
|
15
|
+
? withoutWildcard.replace(/\/+$/, '')
|
|
16
|
+
: withoutWildcard;
|
|
17
|
+
|
|
18
|
+
return normalizedPrefix === '/' ? null : normalizedPrefix;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const collectApiPrefixes = (
|
|
22
|
+
routes: Array<{ isApi?: boolean; urlPath?: string }>,
|
|
23
|
+
bffPrefix?: ApiPrefixInput,
|
|
24
|
+
): string[] => {
|
|
25
|
+
const prefixes = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (const route of routes) {
|
|
28
|
+
if (!route.isApi || !route.urlPath) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizedPrefix = normalizeApiPrefix(route.urlPath);
|
|
33
|
+
if (normalizedPrefix) {
|
|
34
|
+
prefixes.add(normalizedPrefix);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bffPrefixes = Array.isArray(bffPrefix)
|
|
39
|
+
? bffPrefix
|
|
40
|
+
: bffPrefix
|
|
41
|
+
? [bffPrefix]
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
for (const prefix of bffPrefixes) {
|
|
45
|
+
const normalizedPrefix = normalizeApiPrefix(prefix);
|
|
46
|
+
if (normalizedPrefix) {
|
|
47
|
+
prefixes.add(normalizedPrefix);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...prefixes];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const matchesApiPrefix = (
|
|
55
|
+
pathname: string,
|
|
56
|
+
apiPrefixes: string[],
|
|
57
|
+
): boolean => {
|
|
58
|
+
const normalizedPathname = pathname.startsWith('/')
|
|
59
|
+
? pathname
|
|
60
|
+
: `/${pathname}`;
|
|
61
|
+
|
|
62
|
+
return apiPrefixes.some(
|
|
63
|
+
prefix =>
|
|
64
|
+
normalizedPathname === prefix ||
|
|
65
|
+
normalizedPathname.startsWith(`${prefix}/`),
|
|
66
|
+
);
|
|
67
|
+
};
|