@inlang/paraglide-js 2.3.2 → 2.5.0
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/cli/steps/update-ts-config.d.ts +13 -0
- package/dist/cli/steps/update-ts-config.d.ts.map +1 -1
- package/dist/cli/steps/update-ts-config.js +131 -16
- package/dist/cli/steps/update-ts-config.test.d.ts +2 -0
- package/dist/cli/steps/update-ts-config.test.d.ts.map +1 -0
- package/dist/cli/steps/update-ts-config.test.js +59 -0
- package/dist/compiler/compile-project.js +1 -1
- package/dist/compiler/compile-project.test.js +13 -13
- package/dist/compiler/compile.test.js +20 -7
- package/dist/compiler/output-file.d.ts +6 -0
- package/dist/compiler/output-file.d.ts.map +1 -0
- package/dist/compiler/output-file.js +1 -0
- package/dist/compiler/output-structure/message-modules.d.ts.map +1 -1
- package/dist/compiler/output-structure/message-modules.js +26 -4
- package/dist/compiler/output-structure/message-modules.test.js +42 -0
- package/dist/compiler/runtime/assert-is-locale.d.ts.map +1 -1
- package/dist/compiler/runtime/assert-is-locale.js +7 -3
- package/dist/compiler/runtime/assert-is-locale.test.js +33 -2
- package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
- package/dist/compiler/runtime/create-runtime.js +2 -0
- package/dist/compiler/runtime/is-locale.d.ts.map +1 -1
- package/dist/compiler/runtime/is-locale.js +5 -1
- package/dist/compiler/runtime/is-locale.test.d.ts +2 -0
- package/dist/compiler/runtime/is-locale.test.d.ts.map +1 -0
- package/dist/compiler/runtime/is-locale.test.js +31 -0
- package/dist/compiler/runtime/set-locale.d.ts +8 -4
- package/dist/compiler/runtime/set-locale.d.ts.map +1 -1
- package/dist/compiler/runtime/set-locale.js +19 -18
- package/dist/compiler/runtime/set-locale.test.js +25 -0
- package/dist/compiler/runtime/should-redirect.d.ts +80 -0
- package/dist/compiler/runtime/should-redirect.d.ts.map +1 -0
- package/dist/compiler/runtime/should-redirect.js +119 -0
- package/dist/compiler/runtime/should-redirect.test.d.ts +2 -0
- package/dist/compiler/runtime/should-redirect.test.d.ts.map +1 -0
- package/dist/compiler/runtime/should-redirect.test.js +119 -0
- package/dist/compiler/runtime/type.d.ts +1 -0
- package/dist/compiler/runtime/type.d.ts.map +1 -1
- package/dist/compiler/server/middleware.d.ts.map +1 -1
- package/dist/compiler/server/middleware.js +18 -31
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/file-handling/write-output.test.js +7 -10
- package/package.json +9 -11
|
@@ -13,5 +13,9 @@ import { locales } from "./variables.js";
|
|
|
13
13
|
* @returns {locale is Locale}
|
|
14
14
|
*/
|
|
15
15
|
export function isLocale(locale) {
|
|
16
|
-
|
|
16
|
+
if (typeof locale !== "string")
|
|
17
|
+
return false;
|
|
18
|
+
return !locale
|
|
19
|
+
? false
|
|
20
|
+
: locales.some((item) => item.toLowerCase() === locale.toLowerCase());
|
|
17
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-locale.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/is-locale.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { newProject } from "@inlang/sdk";
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { createParaglide } from "../create-paraglide.js";
|
|
4
|
+
const runtime = await createParaglide({
|
|
5
|
+
blob: await newProject({
|
|
6
|
+
settings: {
|
|
7
|
+
baseLocale: "en",
|
|
8
|
+
locales: ["en", "pt-BR", "de-ch"],
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
test("returns true for exact matches", () => {
|
|
13
|
+
expect(runtime.isLocale("pt-BR")).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
test("is case-insensitive", () => {
|
|
16
|
+
expect(runtime.isLocale("EN")).toBe(true);
|
|
17
|
+
expect(runtime.isLocale("pt-br")).toBe(true);
|
|
18
|
+
expect(runtime.isLocale("de-CH")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
test("returns false for non-existent locales", () => {
|
|
21
|
+
expect(runtime.isLocale("es")).toBe(false);
|
|
22
|
+
expect(runtime.isLocale("xx")).toBe(false);
|
|
23
|
+
expect(runtime.isLocale("")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
test("returns false for non-string inputs", () => {
|
|
26
|
+
expect(runtime.isLocale(null)).toBe(false);
|
|
27
|
+
expect(runtime.isLocale(undefined)).toBe(false);
|
|
28
|
+
expect(runtime.isLocale(123)).toBe(false);
|
|
29
|
+
expect(runtime.isLocale({})).toBe(false);
|
|
30
|
+
expect(runtime.isLocale([])).toBe(false);
|
|
31
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
|
|
3
|
+
*/
|
|
1
4
|
/**
|
|
2
5
|
* Set the locale.
|
|
3
6
|
*
|
|
@@ -15,10 +18,11 @@
|
|
|
15
18
|
* @example
|
|
16
19
|
* setLocale('en', { reload: false });
|
|
17
20
|
*
|
|
18
|
-
* @type {
|
|
21
|
+
* @type {SetLocaleFn}
|
|
19
22
|
*/
|
|
20
|
-
export let setLocale:
|
|
23
|
+
export let setLocale: SetLocaleFn;
|
|
24
|
+
export function overwriteSetLocale(fn: SetLocaleFn): void;
|
|
25
|
+
export type SetLocaleFn = (newLocale: Locale, options?: {
|
|
21
26
|
reload?: boolean;
|
|
22
|
-
}) =>
|
|
23
|
-
export function overwriteSetLocale(fn: (newLocale: Locale) => void): void;
|
|
27
|
+
}) => void | Promise<void>;
|
|
24
28
|
//# sourceMappingURL=set-locale.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;GAEG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,WAAW,CAyGnB;AAgBK,uCAFI,WAAW,QAIrB;0BA/IY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC"}
|
|
@@ -18,6 +18,9 @@ const navigateOrReload = (newLocation) => {
|
|
|
18
18
|
window.location.reload();
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
|
|
23
|
+
*/
|
|
21
24
|
/**
|
|
22
25
|
* Set the locale.
|
|
23
26
|
*
|
|
@@ -35,7 +38,7 @@ const navigateOrReload = (newLocation) => {
|
|
|
35
38
|
* @example
|
|
36
39
|
* setLocale('en', { reload: false });
|
|
37
40
|
*
|
|
38
|
-
* @type {
|
|
41
|
+
* @type {SetLocaleFn}
|
|
39
42
|
*/
|
|
40
43
|
export let setLocale = (newLocale, options) => {
|
|
41
44
|
const optionsWithDefaults = {
|
|
@@ -44,6 +47,7 @@ export let setLocale = (newLocale, options) => {
|
|
|
44
47
|
};
|
|
45
48
|
// locale is already set
|
|
46
49
|
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
50
|
+
/** @type {Locale | undefined} */
|
|
47
51
|
let currentLocale;
|
|
48
52
|
try {
|
|
49
53
|
currentLocale = getLocale();
|
|
@@ -103,7 +107,7 @@ export let setLocale = (newLocale, options) => {
|
|
|
103
107
|
const handler = customClientStrategies.get(strat);
|
|
104
108
|
if (handler) {
|
|
105
109
|
let result = handler.setLocale(newLocale);
|
|
106
|
-
// Handle async setLocale
|
|
110
|
+
// Handle async setLocale
|
|
107
111
|
if (result instanceof Promise) {
|
|
108
112
|
result = result.catch((error) => {
|
|
109
113
|
throw new Error(`Custom strategy "${strat}" setLocale failed.`, {
|
|
@@ -115,23 +119,20 @@ export let setLocale = (newLocale, options) => {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Wait for any async custom setLocale functions
|
|
124
|
-
return Promise.all(customSetLocalePromises).then(() => {
|
|
125
|
-
navigateOrReload(newLocation);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
122
|
+
const runReload = () => {
|
|
123
|
+
if (!isServer &&
|
|
124
|
+
optionsWithDefaults.reload &&
|
|
125
|
+
window.location &&
|
|
126
|
+
newLocale !== currentLocale) {
|
|
129
127
|
navigateOrReload(newLocation);
|
|
130
128
|
}
|
|
129
|
+
};
|
|
130
|
+
if (customSetLocalePromises.length) {
|
|
131
|
+
return Promise.all(customSetLocalePromises).then(() => {
|
|
132
|
+
runReload();
|
|
133
|
+
});
|
|
131
134
|
}
|
|
132
|
-
|
|
133
|
-
return Promise.all(customSetLocalePromises);
|
|
134
|
-
}
|
|
135
|
+
runReload();
|
|
135
136
|
return;
|
|
136
137
|
};
|
|
137
138
|
/**
|
|
@@ -146,8 +147,8 @@ export let setLocale = (newLocale, options) => {
|
|
|
146
147
|
* return Cookies.set('locale', newLocale)
|
|
147
148
|
* });
|
|
148
149
|
*
|
|
149
|
-
* @param {
|
|
150
|
+
* @param {SetLocaleFn} fn
|
|
150
151
|
*/
|
|
151
152
|
export const overwriteSetLocale = (fn) => {
|
|
152
|
-
setLocale = fn;
|
|
153
|
+
setLocale = /** @type {SetLocaleFn} */ (fn);
|
|
153
154
|
};
|
|
@@ -155,6 +155,31 @@ test("when strategy precedes URL, it should set the locale and re-direct to the
|
|
|
155
155
|
expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000");
|
|
156
156
|
expect(globalThis.window.location.href).toBe("https://example.com/en/some-path");
|
|
157
157
|
});
|
|
158
|
+
test("overwriteSetLocale receives the options object", async () => {
|
|
159
|
+
const runtime = await createParaglide({
|
|
160
|
+
blob: await newProject({
|
|
161
|
+
settings: {
|
|
162
|
+
baseLocale: "en",
|
|
163
|
+
locales: ["en", "fr"],
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
strategy: ["cookie"],
|
|
167
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
168
|
+
});
|
|
169
|
+
// Provide minimal browser globals to avoid strategy branches failing.
|
|
170
|
+
/** @ts-expect-error - browser shim for tests */
|
|
171
|
+
globalThis.document = { cookie: "" };
|
|
172
|
+
globalThis.window = {
|
|
173
|
+
location: {
|
|
174
|
+
href: "https://example.com/en",
|
|
175
|
+
reload: vi.fn(),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const spy = vi.fn();
|
|
179
|
+
runtime.overwriteSetLocale(spy);
|
|
180
|
+
runtime.setLocale("fr", { reload: false });
|
|
181
|
+
expect(spy).toHaveBeenCalledWith("fr", { reload: false });
|
|
182
|
+
});
|
|
158
183
|
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
159
184
|
test("should not reload when setting locale to current locale", async () => {
|
|
160
185
|
// @ts-expect-error - global variable definition
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ShouldRedirectServerInput
|
|
3
|
+
* @property {Request} request
|
|
4
|
+
* @property {string | URL} [url]
|
|
5
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
6
|
+
*
|
|
7
|
+
* @typedef {object} ShouldRedirectClientInput
|
|
8
|
+
* @property {undefined} [request]
|
|
9
|
+
* @property {string | URL} [url]
|
|
10
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
11
|
+
*
|
|
12
|
+
* @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} ShouldRedirectResult
|
|
15
|
+
* @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
|
|
16
|
+
* @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
|
|
17
|
+
* @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether a redirect is required to align the current URL with the active locale.
|
|
21
|
+
*
|
|
22
|
+
* This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
|
|
23
|
+
* and client environments. It evaluates the configured strategies in order, computes the
|
|
24
|
+
* canonical localized URL, and reports when the current URL does not match.
|
|
25
|
+
*
|
|
26
|
+
* When called in the browser without arguments, the current `window.location.href` is used.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Client side usage (e.g. TanStack Router beforeLoad hook)
|
|
30
|
+
* async function beforeLoad({ location }) {
|
|
31
|
+
* const decision = await shouldRedirect({ url: location.href });
|
|
32
|
+
*
|
|
33
|
+
* if (decision.shouldRedirect) {
|
|
34
|
+
* throw redirect({ to: decision.redirectUrl.href });
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Server side usage with a Request
|
|
40
|
+
* export async function handle(request) {
|
|
41
|
+
* const decision = await shouldRedirect({ request });
|
|
42
|
+
*
|
|
43
|
+
* if (decision.shouldRedirect) {
|
|
44
|
+
* return Response.redirect(decision.redirectUrl, 307);
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* return render(request, decision.locale);
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* @param {ShouldRedirectInput} [input]
|
|
51
|
+
* @returns {Promise<ShouldRedirectResult>}
|
|
52
|
+
*/
|
|
53
|
+
export function shouldRedirect(input?: ShouldRedirectInput): Promise<ShouldRedirectResult>;
|
|
54
|
+
export type ShouldRedirectServerInput = {
|
|
55
|
+
request: Request;
|
|
56
|
+
url?: string | URL | undefined;
|
|
57
|
+
locale?: ReturnType<typeof assertIsLocale>;
|
|
58
|
+
};
|
|
59
|
+
export type ShouldRedirectClientInput = {
|
|
60
|
+
request?: undefined;
|
|
61
|
+
url?: string | URL | undefined;
|
|
62
|
+
locale?: ReturnType<typeof assertIsLocale>;
|
|
63
|
+
};
|
|
64
|
+
export type ShouldRedirectInput = ShouldRedirectServerInput | ShouldRedirectClientInput;
|
|
65
|
+
export type ShouldRedirectResult = {
|
|
66
|
+
/**
|
|
67
|
+
* - Indicates whether the consumer should perform a redirect.
|
|
68
|
+
*/
|
|
69
|
+
shouldRedirect: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* - Locale resolved using the configured strategies.
|
|
72
|
+
*/
|
|
73
|
+
locale: ReturnType<typeof assertIsLocale>;
|
|
74
|
+
/**
|
|
75
|
+
* - Destination URL when a redirect is required.
|
|
76
|
+
*/
|
|
77
|
+
redirectUrl: URL | undefined;
|
|
78
|
+
};
|
|
79
|
+
import { assertIsLocale } from "./assert-is-locale.js";
|
|
80
|
+
//# sourceMappingURL=should-redirect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"should-redirect.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,uCAHW,mBAAmB,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAsBzC;;aAvEa,OAAO;;aAEP,UAAU,CAAC,OAAO,cAAc,CAAC;;;cAGjC,SAAS;;aAET,UAAU,CAAC,OAAO,cAAc,CAAC;;kCAElC,yBAAyB,GAAG,yBAAyB;;;;;oBAGpD,OAAO;;;;YACP,UAAU,CAAC,OAAO,cAAc,CAAC;;;;iBACjC,GAAG,GAAG,SAAS;;+BAnBE,uBAAuB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { localizeUrl } from "./localize-url.js";
|
|
2
|
+
import { getLocale } from "./get-locale.js";
|
|
3
|
+
import { getUrlOrigin } from "./get-url-origin.js";
|
|
4
|
+
import { extractLocaleFromRequestAsync } from "./extract-locale-from-request-async.js";
|
|
5
|
+
import { assertIsLocale } from "./assert-is-locale.js";
|
|
6
|
+
import { strategy } from "./variables.js";
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} ShouldRedirectServerInput
|
|
9
|
+
* @property {Request} request
|
|
10
|
+
* @property {string | URL} [url]
|
|
11
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
12
|
+
*
|
|
13
|
+
* @typedef {object} ShouldRedirectClientInput
|
|
14
|
+
* @property {undefined} [request]
|
|
15
|
+
* @property {string | URL} [url]
|
|
16
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
17
|
+
*
|
|
18
|
+
* @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
|
|
19
|
+
*
|
|
20
|
+
* @typedef {object} ShouldRedirectResult
|
|
21
|
+
* @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
|
|
22
|
+
* @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
|
|
23
|
+
* @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Determines whether a redirect is required to align the current URL with the active locale.
|
|
27
|
+
*
|
|
28
|
+
* This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
|
|
29
|
+
* and client environments. It evaluates the configured strategies in order, computes the
|
|
30
|
+
* canonical localized URL, and reports when the current URL does not match.
|
|
31
|
+
*
|
|
32
|
+
* When called in the browser without arguments, the current `window.location.href` is used.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Client side usage (e.g. TanStack Router beforeLoad hook)
|
|
36
|
+
* async function beforeLoad({ location }) {
|
|
37
|
+
* const decision = await shouldRedirect({ url: location.href });
|
|
38
|
+
*
|
|
39
|
+
* if (decision.shouldRedirect) {
|
|
40
|
+
* throw redirect({ to: decision.redirectUrl.href });
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Server side usage with a Request
|
|
46
|
+
* export async function handle(request) {
|
|
47
|
+
* const decision = await shouldRedirect({ request });
|
|
48
|
+
*
|
|
49
|
+
* if (decision.shouldRedirect) {
|
|
50
|
+
* return Response.redirect(decision.redirectUrl, 307);
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* return render(request, decision.locale);
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* @param {ShouldRedirectInput} [input]
|
|
57
|
+
* @returns {Promise<ShouldRedirectResult>}
|
|
58
|
+
*/
|
|
59
|
+
export async function shouldRedirect(input = {}) {
|
|
60
|
+
const locale = /** @type {ReturnType<typeof assertIsLocale>} */ (await resolveLocale(input));
|
|
61
|
+
if (!strategy.includes("url")) {
|
|
62
|
+
return { shouldRedirect: false, locale, redirectUrl: undefined };
|
|
63
|
+
}
|
|
64
|
+
const currentUrl = resolveUrl(input);
|
|
65
|
+
const localizedUrl = localizeUrl(currentUrl.href, { locale });
|
|
66
|
+
const shouldRedirectToLocalizedUrl = normalizeUrl(localizedUrl.href) !== normalizeUrl(currentUrl.href);
|
|
67
|
+
return {
|
|
68
|
+
shouldRedirect: shouldRedirectToLocalizedUrl,
|
|
69
|
+
locale,
|
|
70
|
+
redirectUrl: shouldRedirectToLocalizedUrl ? localizedUrl : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the locale either from the provided input or by using the configured strategies.
|
|
75
|
+
*
|
|
76
|
+
* @param {ShouldRedirectInput} input
|
|
77
|
+
* @returns {Promise<ReturnType<typeof assertIsLocale>>}
|
|
78
|
+
*/
|
|
79
|
+
async function resolveLocale(input) {
|
|
80
|
+
if (input.locale) {
|
|
81
|
+
return assertIsLocale(input.locale);
|
|
82
|
+
}
|
|
83
|
+
if (input.request) {
|
|
84
|
+
return extractLocaleFromRequestAsync(input.request);
|
|
85
|
+
}
|
|
86
|
+
return getLocale();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolves the current URL from the provided input or runtime context.
|
|
90
|
+
*
|
|
91
|
+
* @param {ShouldRedirectInput} input
|
|
92
|
+
* @returns {URL}
|
|
93
|
+
*/
|
|
94
|
+
function resolveUrl(input) {
|
|
95
|
+
if (input.request) {
|
|
96
|
+
return new URL(input.request.url);
|
|
97
|
+
}
|
|
98
|
+
if (input.url instanceof URL) {
|
|
99
|
+
return new URL(input.url.href);
|
|
100
|
+
}
|
|
101
|
+
if (typeof input.url === "string") {
|
|
102
|
+
return new URL(input.url, getUrlOrigin());
|
|
103
|
+
}
|
|
104
|
+
if (typeof window !== "undefined" && window?.location?.href) {
|
|
105
|
+
return new URL(window.location.href);
|
|
106
|
+
}
|
|
107
|
+
throw new Error("shouldRedirect() requires either a request, an absolute URL, or must run in a browser environment.");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalize url for comparison by stripping the trailing slash.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} url
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function normalizeUrl(url) {
|
|
116
|
+
const urlObj = new URL(url);
|
|
117
|
+
urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
|
|
118
|
+
return urlObj.href;
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"should-redirect.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { createParaglide } from "../create-paraglide.js";
|
|
3
|
+
import { newProject } from "@inlang/sdk";
|
|
4
|
+
test("shouldRedirect redirects to the strategy-preferred locale on the server", async () => {
|
|
5
|
+
const runtime = await createParaglide({
|
|
6
|
+
blob: await newProject({
|
|
7
|
+
settings: {
|
|
8
|
+
baseLocale: "en",
|
|
9
|
+
locales: ["en", "fr"],
|
|
10
|
+
},
|
|
11
|
+
}),
|
|
12
|
+
strategy: ["cookie", "url"],
|
|
13
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
14
|
+
urlPatterns: [
|
|
15
|
+
{
|
|
16
|
+
pattern: "https://example.com/:path(.*)?",
|
|
17
|
+
localized: [
|
|
18
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
19
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
const request = new Request("https://example.com/en/dashboard", {
|
|
25
|
+
headers: {
|
|
26
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
30
|
+
expect(decision.shouldRedirect).toBe(true);
|
|
31
|
+
expect(decision.redirectUrl?.href).toBe("https://example.com/fr/dashboard");
|
|
32
|
+
expect(decision.locale).toBe("fr");
|
|
33
|
+
});
|
|
34
|
+
test("shouldRedirect does nothing when the URL already matches", async () => {
|
|
35
|
+
const runtime = await createParaglide({
|
|
36
|
+
blob: await newProject({
|
|
37
|
+
settings: {
|
|
38
|
+
baseLocale: "en",
|
|
39
|
+
locales: ["en", "fr"],
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
strategy: ["cookie", "url"],
|
|
43
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
44
|
+
urlPatterns: [
|
|
45
|
+
{
|
|
46
|
+
pattern: "https://example.com/:path(.*)?",
|
|
47
|
+
localized: [
|
|
48
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
49
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
const request = new Request("https://example.com/fr/dashboard", {
|
|
55
|
+
headers: {
|
|
56
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
60
|
+
expect(decision.shouldRedirect).toBe(false);
|
|
61
|
+
expect(decision.redirectUrl).toBeUndefined();
|
|
62
|
+
expect(decision.locale).toBe("fr");
|
|
63
|
+
});
|
|
64
|
+
test("shouldRedirect falls back to the browser URL when no input is provided", async () => {
|
|
65
|
+
const runtime = await createParaglide({
|
|
66
|
+
blob: await newProject({
|
|
67
|
+
settings: {
|
|
68
|
+
baseLocale: "en",
|
|
69
|
+
locales: ["en", "de"],
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
strategy: ["url", "globalVariable"],
|
|
73
|
+
isServer: "false",
|
|
74
|
+
urlPatterns: undefined,
|
|
75
|
+
});
|
|
76
|
+
const originalWindow = globalThis.window;
|
|
77
|
+
try {
|
|
78
|
+
globalThis.window = {
|
|
79
|
+
location: {
|
|
80
|
+
href: "https://example.com/en/profile",
|
|
81
|
+
origin: "https://example.com",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
runtime.overwriteGetLocale(() => "de");
|
|
85
|
+
const decision = await runtime.shouldRedirect();
|
|
86
|
+
expect(decision.shouldRedirect).toBe(true);
|
|
87
|
+
expect(decision.redirectUrl?.href).toBe("https://example.com/de/profile");
|
|
88
|
+
expect(decision.locale).toBe("de");
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (originalWindow === undefined) {
|
|
92
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
globalThis.window = originalWindow;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
test("shouldRedirect never suggests a redirect without the url strategy", async () => {
|
|
100
|
+
const runtime = await createParaglide({
|
|
101
|
+
blob: await newProject({
|
|
102
|
+
settings: {
|
|
103
|
+
baseLocale: "en",
|
|
104
|
+
locales: ["en", "fr"],
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
strategy: ["cookie"],
|
|
108
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
109
|
+
});
|
|
110
|
+
const request = new Request("https://example.com/en/dashboard", {
|
|
111
|
+
headers: {
|
|
112
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
116
|
+
expect(decision.shouldRedirect).toBe(false);
|
|
117
|
+
expect(decision.redirectUrl).toBeUndefined();
|
|
118
|
+
expect(decision.locale).toBe("fr");
|
|
119
|
+
});
|
|
@@ -25,6 +25,7 @@ export type Runtime = {
|
|
|
25
25
|
deLocalizeHref: typeof import("./localize-href.js").deLocalizeHref;
|
|
26
26
|
localizeUrl: typeof import("./localize-url.js").localizeUrl;
|
|
27
27
|
deLocalizeUrl: typeof import("./localize-url.js").deLocalizeUrl;
|
|
28
|
+
shouldRedirect: typeof import("./should-redirect.js").shouldRedirect;
|
|
28
29
|
extractLocaleFromUrl: typeof import("./extract-locale-from-url.js").extractLocaleFromUrl;
|
|
29
30
|
extractLocaleFromRequest: typeof import("./extract-locale-from-request.js").extractLocaleFromRequest;
|
|
30
31
|
extractLocaleFromRequestAsync: typeof import("./extract-locale-from-request-async.js").extractLocaleFromRequestAsync;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,cAAc,EAAE,cAAc,sBAAsB,EAAE,cAAc,CAAC;IACrE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,CAyI7B"}
|
|
@@ -68,30 +68,29 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
|
|
|
68
68
|
else if (!runtime.serverAsyncLocalStorage) {
|
|
69
69
|
runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
|
|
70
70
|
}
|
|
71
|
-
const
|
|
71
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
72
|
+
const locale = decision.locale;
|
|
72
73
|
const origin = new URL(request.url).origin;
|
|
73
74
|
// if the client makes a request to a URL that doesn't match
|
|
74
75
|
// the localizedUrl, redirect the client to the localized URL
|
|
75
76
|
if (request.headers.get("Sec-Fetch-Dest") === "document" &&
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
headers["Vary"] = "Accept-Language";
|
|
84
|
-
}
|
|
85
|
-
const response = new Response(null, {
|
|
86
|
-
status: 307,
|
|
87
|
-
headers: {
|
|
88
|
-
Location: localizedUrl.href,
|
|
89
|
-
...headers,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
callbacks?.onRedirect(response);
|
|
93
|
-
return response;
|
|
77
|
+
decision.shouldRedirect &&
|
|
78
|
+
decision.redirectUrl) {
|
|
79
|
+
// Create headers object with Vary header if preferredLanguage strategy is used
|
|
80
|
+
/** @type {Record<string, string>} */
|
|
81
|
+
const headers = {};
|
|
82
|
+
if (runtime.strategy.includes("preferredLanguage")) {
|
|
83
|
+
headers["Vary"] = "Accept-Language";
|
|
94
84
|
}
|
|
85
|
+
const response = new Response(null, {
|
|
86
|
+
status: 307,
|
|
87
|
+
headers: {
|
|
88
|
+
Location: decision.redirectUrl.href,
|
|
89
|
+
...headers,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
callbacks?.onRedirect(response);
|
|
93
|
+
return response;
|
|
95
94
|
}
|
|
96
95
|
// If the strategy includes "url", we need to de-localize the URL
|
|
97
96
|
// before passing it to the server middleware.
|
|
@@ -135,18 +134,6 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
|
|
|
135
134
|
}
|
|
136
135
|
return response;
|
|
137
136
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Normalize url for comparison.
|
|
140
|
-
* Strips trailing slash
|
|
141
|
-
* @param {string} url
|
|
142
|
-
* @returns {string} normalized url string
|
|
143
|
-
*/
|
|
144
|
-
function normalizeURL(url) {
|
|
145
|
-
const urlObj = new URL(url);
|
|
146
|
-
// // strip trailing slash from pathname
|
|
147
|
-
urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
|
|
148
|
-
return urlObj.href;
|
|
149
|
-
}
|
|
150
137
|
/**
|
|
151
138
|
* Creates a mock AsyncLocalStorage implementation for environments where
|
|
152
139
|
* native AsyncLocalStorage is not available or disabled.
|