@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.
Files changed (42) hide show
  1. package/dist/cli/steps/update-ts-config.d.ts +13 -0
  2. package/dist/cli/steps/update-ts-config.d.ts.map +1 -1
  3. package/dist/cli/steps/update-ts-config.js +131 -16
  4. package/dist/cli/steps/update-ts-config.test.d.ts +2 -0
  5. package/dist/cli/steps/update-ts-config.test.d.ts.map +1 -0
  6. package/dist/cli/steps/update-ts-config.test.js +59 -0
  7. package/dist/compiler/compile-project.js +1 -1
  8. package/dist/compiler/compile-project.test.js +13 -13
  9. package/dist/compiler/compile.test.js +20 -7
  10. package/dist/compiler/output-file.d.ts +6 -0
  11. package/dist/compiler/output-file.d.ts.map +1 -0
  12. package/dist/compiler/output-file.js +1 -0
  13. package/dist/compiler/output-structure/message-modules.d.ts.map +1 -1
  14. package/dist/compiler/output-structure/message-modules.js +26 -4
  15. package/dist/compiler/output-structure/message-modules.test.js +42 -0
  16. package/dist/compiler/runtime/assert-is-locale.d.ts.map +1 -1
  17. package/dist/compiler/runtime/assert-is-locale.js +7 -3
  18. package/dist/compiler/runtime/assert-is-locale.test.js +33 -2
  19. package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
  20. package/dist/compiler/runtime/create-runtime.js +2 -0
  21. package/dist/compiler/runtime/is-locale.d.ts.map +1 -1
  22. package/dist/compiler/runtime/is-locale.js +5 -1
  23. package/dist/compiler/runtime/is-locale.test.d.ts +2 -0
  24. package/dist/compiler/runtime/is-locale.test.d.ts.map +1 -0
  25. package/dist/compiler/runtime/is-locale.test.js +31 -0
  26. package/dist/compiler/runtime/set-locale.d.ts +8 -4
  27. package/dist/compiler/runtime/set-locale.d.ts.map +1 -1
  28. package/dist/compiler/runtime/set-locale.js +19 -18
  29. package/dist/compiler/runtime/set-locale.test.js +25 -0
  30. package/dist/compiler/runtime/should-redirect.d.ts +80 -0
  31. package/dist/compiler/runtime/should-redirect.d.ts.map +1 -0
  32. package/dist/compiler/runtime/should-redirect.js +119 -0
  33. package/dist/compiler/runtime/should-redirect.test.d.ts +2 -0
  34. package/dist/compiler/runtime/should-redirect.test.d.ts.map +1 -0
  35. package/dist/compiler/runtime/should-redirect.test.js +119 -0
  36. package/dist/compiler/runtime/type.d.ts +1 -0
  37. package/dist/compiler/runtime/type.d.ts.map +1 -1
  38. package/dist/compiler/server/middleware.d.ts.map +1 -1
  39. package/dist/compiler/server/middleware.js +18 -31
  40. package/dist/services/env-variables/index.js +1 -1
  41. package/dist/services/file-handling/write-output.test.js +7 -10
  42. 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
- return !locale ? false : locales.includes(locale);
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=is-locale.test.d.ts.map
@@ -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 {(newLocale: Locale, options?: { reload?: boolean }) => Promise<any> | void}
21
+ * @type {SetLocaleFn}
19
22
  */
20
- export let setLocale: (newLocale: Locale, options?: {
23
+ export let setLocale: SetLocaleFn;
24
+ export function overwriteSetLocale(fn: SetLocaleFn): void;
25
+ export type SetLocaleFn = (newLocale: Locale, options?: {
21
26
  reload?: boolean;
22
- }) => Promise<any> | void;
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,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAuGlF;AAgBK,uCAFI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,QAIrC"}
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 {(newLocale: Locale, options?: { reload?: boolean }) => Promise<any> | void}
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 - fire and forget
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
- if (!isServer &&
119
- optionsWithDefaults.reload &&
120
- window.location &&
121
- newLocale !== currentLocale) {
122
- if (customSetLocalePromises.length) {
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
- else if (customSetLocalePromises.length) {
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 {(newLocale: Locale) => void} fn
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=should-redirect.test.d.ts.map
@@ -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,CA0I7B"}
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 locale = await runtime.extractLocaleFromRequestAsync(request);
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
- runtime.strategy.includes("url")) {
77
- const localizedUrl = runtime.localizeUrl(request.url, { locale });
78
- if (normalizeURL(localizedUrl.href) !== normalizeURL(request.url)) {
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";
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.
@@ -1,5 +1,5 @@
1
1
  export const ENV_VARIABLES = {
2
2
  PARJS_APP_ID: "library.inlang.paraglideJs",
3
3
  PARJS_POSTHOG_TOKEN: undefined,
4
- PARJS_PACKAGE_VERSION: "2.3.2",
4
+ PARJS_PACKAGE_VERSION: "2.5.0",
5
5
  };