@inlang/paraglide-js 2.1.0 → 2.2.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 (32) hide show
  1. package/dist/bundler-plugins/vite.d.ts +1 -1
  2. package/dist/bundler-plugins/vite.d.ts.map +1 -1
  3. package/dist/compiler/compiler-options.d.ts +15 -3
  4. package/dist/compiler/compiler-options.d.ts.map +1 -1
  5. package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
  6. package/dist/compiler/runtime/create-runtime.js +2 -0
  7. package/dist/compiler/runtime/extract-locale-from-request-async.d.ts +31 -0
  8. package/dist/compiler/runtime/extract-locale-from-request-async.d.ts.map +1 -0
  9. package/dist/compiler/runtime/extract-locale-from-request-async.js +55 -0
  10. package/dist/compiler/runtime/extract-locale-from-request-async.test.d.ts +2 -0
  11. package/dist/compiler/runtime/extract-locale-from-request-async.test.d.ts.map +1 -0
  12. package/dist/compiler/runtime/extract-locale-from-request-async.test.js +58 -0
  13. package/dist/compiler/runtime/extract-locale-from-request.d.ts +3 -0
  14. package/dist/compiler/runtime/extract-locale-from-request.d.ts.map +1 -1
  15. package/dist/compiler/runtime/extract-locale-from-request.js +8 -4
  16. package/dist/compiler/runtime/extract-locale-from-request.test.js +9 -8
  17. package/dist/compiler/runtime/get-locale.d.ts.map +1 -1
  18. package/dist/compiler/runtime/get-locale.js +9 -1
  19. package/dist/compiler/runtime/get-locale.test.js +1 -1
  20. package/dist/compiler/runtime/set-locale.d.ts.map +1 -1
  21. package/dist/compiler/runtime/set-locale.js +13 -3
  22. package/dist/compiler/runtime/set-locale.test.js +7 -7
  23. package/dist/compiler/runtime/strategy.d.ts +14 -14
  24. package/dist/compiler/runtime/strategy.d.ts.map +1 -1
  25. package/dist/compiler/runtime/strategy.js +12 -14
  26. package/dist/compiler/runtime/strategy.test.js +5 -5
  27. package/dist/compiler/runtime/type.d.ts +1 -0
  28. package/dist/compiler/runtime/type.d.ts.map +1 -1
  29. package/dist/compiler/server/middleware.js +1 -1
  30. package/dist/compiler/server/middleware.test.js +190 -0
  31. package/dist/services/env-variables/index.js +1 -1
  32. package/package.json +6 -6
@@ -1,2 +1,2 @@
1
- export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
1
+ export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("unplugin").VitePlugin<any> | import("unplugin").VitePlugin<any>[];
2
2
  //# sourceMappingURL=vite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+GAAoC,CAAC"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+HAAoC,CAAC"}
@@ -106,10 +106,22 @@ export type CompilerOptions = {
106
106
  cookieMaxAge?: number;
107
107
  /**
108
108
  * The host to which the cookie will be sent.
109
- * If null, this defaults to the host portion of the current document location and the cookie is not available on subdomains.
110
- * Otherwise, subdomains are always included.
109
+ * If undefined or empty, the domain attribute is omitted from the cookie, scoping it to the exact current domain only (no subdomains).
110
+ * If specified, the cookie will be available to the specified domain and all its subdomains.
111
111
  *
112
- * @default window.location.hostname
112
+ * Use this when you need cookies to be shared across subdomains (e.g., between `app.example.com` and `api.example.com`).
113
+ * The default behavior (no domain) ensures better compatibility with server-side cookies that don't specify a domain attribute.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * // Default: exact domain only (compatible with server-side cookies)
118
+ * cookieDomain: undefined // Cookie: "PARAGLIDE_LOCALE=en; path=/; max-age=34560000"
119
+ *
120
+ * // Subdomain sharing: available across all subdomains
121
+ * cookieDomain: "example.com" // Cookie: "PARAGLIDE_LOCALE=en; path=/; max-age=34560000; domain=example.com"
122
+ * ```
123
+ *
124
+ * @default "" (no domain attribute, exact domain only)
113
125
  */
114
126
  cookieDomain?: string;
115
127
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"compiler-options.d.ts","sourceRoot":"","sources":["../../src/compiler/compiler-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;CAcU,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC7B;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B;;;;;;;;;;;;;;OAcG;IACH,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACvD;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,EAAE,CAAC,EAAE,GAAG,CAAC;CACT,CAAC"}
1
+ {"version":3,"file":"compiler-options.d.ts","sourceRoot":"","sources":["../../src/compiler/compiler-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;CAcU,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC7B;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B;;;;;;;;;;;;;;OAcG;IACH,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACvD;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,EAAE,CAAC,EAAE,GAAG,CAAC;CACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,CAwIT"}
1
+ {"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,CA0IT"}
@@ -62,6 +62,8 @@ ${injectCode("./assert-is-locale.js")}
62
62
 
63
63
  ${injectCode("./extract-locale-from-request.js")}
64
64
 
65
+ ${injectCode("./extract-locale-from-request-async.js")}
66
+
65
67
  ${injectCode("./extract-locale-from-cookie.js")}
66
68
 
67
69
  ${injectCode("./extract-locale-from-header.js")}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Asynchronously extracts a locale from a request.
3
+ *
4
+ * This function supports async custom server strategies, unlike the synchronous
5
+ * `extractLocaleFromRequest`. Use this function when you have custom server strategies
6
+ * that need to perform asynchronous operations (like database calls) in their getLocale method.
7
+ *
8
+ * The function first processes any custom server strategies asynchronously, then falls back
9
+ * to the synchronous `extractLocaleFromRequest` for all other strategies.
10
+ *
11
+ * @see {@link https://github.com/opral/inlang-paraglide-js/issues/527#issuecomment-2978151022}
12
+ *
13
+ * @example
14
+ * // Basic usage
15
+ * const locale = await extractLocaleFromRequestAsync(request);
16
+ *
17
+ * @example
18
+ * // With custom async server strategy
19
+ * defineCustomServerStrategy("custom-database", {
20
+ * getLocale: async (request) => {
21
+ * const userId = extractUserIdFromRequest(request);
22
+ * return await getUserLocaleFromDatabase(userId);
23
+ * }
24
+ * });
25
+ *
26
+ * const locale = await extractLocaleFromRequestAsync(request);
27
+ *
28
+ * @type {(request: Request) => Promise<Locale>}
29
+ */
30
+ export const extractLocaleFromRequestAsync: (request: Request) => Promise<Locale>;
31
+ //# sourceMappingURL=extract-locale-from-request-async.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-locale-from-request-async.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/extract-locale-from-request-async.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,4CAFU,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAyB7C"}
@@ -0,0 +1,55 @@
1
+ import { customServerStrategies, isCustomStrategy } from "./strategy.js";
2
+ import { strategy } from "./variables.js";
3
+ import { assertIsLocale } from "./assert-is-locale.js";
4
+ import { isLocale } from "./is-locale.js";
5
+ import { extractLocaleFromRequest } from "./extract-locale-from-request.js";
6
+ /**
7
+ * Asynchronously extracts a locale from a request.
8
+ *
9
+ * This function supports async custom server strategies, unlike the synchronous
10
+ * `extractLocaleFromRequest`. Use this function when you have custom server strategies
11
+ * that need to perform asynchronous operations (like database calls) in their getLocale method.
12
+ *
13
+ * The function first processes any custom server strategies asynchronously, then falls back
14
+ * to the synchronous `extractLocaleFromRequest` for all other strategies.
15
+ *
16
+ * @see {@link https://github.com/opral/inlang-paraglide-js/issues/527#issuecomment-2978151022}
17
+ *
18
+ * @example
19
+ * // Basic usage
20
+ * const locale = await extractLocaleFromRequestAsync(request);
21
+ *
22
+ * @example
23
+ * // With custom async server strategy
24
+ * defineCustomServerStrategy("custom-database", {
25
+ * getLocale: async (request) => {
26
+ * const userId = extractUserIdFromRequest(request);
27
+ * return await getUserLocaleFromDatabase(userId);
28
+ * }
29
+ * });
30
+ *
31
+ * const locale = await extractLocaleFromRequestAsync(request);
32
+ *
33
+ * @type {(request: Request) => Promise<Locale>}
34
+ */
35
+ export const extractLocaleFromRequestAsync = async (request) => {
36
+ /** @type {string|undefined} */
37
+ let locale;
38
+ // Process custom strategies first, in order
39
+ for (const strat of strategy) {
40
+ if (isCustomStrategy(strat) && customServerStrategies.has(strat)) {
41
+ const handler = customServerStrategies.get(strat);
42
+ if (handler) {
43
+ /** @type {string|undefined} */
44
+ locale = await handler.getLocale(request);
45
+ }
46
+ // If we got a valid locale from this custom strategy, use it
47
+ if (locale !== undefined && isLocale(locale)) {
48
+ return assertIsLocale(locale);
49
+ }
50
+ }
51
+ }
52
+ // If no custom strategy provided a valid locale, fall back to sync version
53
+ locale = extractLocaleFromRequest(request);
54
+ return assertIsLocale(locale);
55
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=extract-locale-from-request-async.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-locale-from-request-async.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/extract-locale-from-request-async.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,58 @@
1
+ import { newProject } from "@inlang/sdk";
2
+ import { expect, test } from "vitest";
3
+ import { createParaglide } from "../create-paraglide.js";
4
+ test("returns locale from custom strategy which is async", async () => {
5
+ const runtime = await createParaglide({
6
+ blob: await newProject({
7
+ settings: {
8
+ baseLocale: "en",
9
+ locales: ["en", "fr", "de"],
10
+ },
11
+ }),
12
+ strategy: ["custom-header", "baseLocale"],
13
+ });
14
+ class FakeDB {
15
+ db = new Map();
16
+ constructor() {
17
+ this.db.set("1", "fr");
18
+ }
19
+ async getUserLocaleById(id) {
20
+ return this.db.get(id);
21
+ }
22
+ }
23
+ const db = new FakeDB();
24
+ async function getLocaleFromUserRequest(request) {
25
+ const userId = request?.headers.get("X-Custom-User-ID") ?? undefined;
26
+ if (!userId)
27
+ throw Error("No User ID");
28
+ const locale = await db.getUserLocaleById(userId);
29
+ return locale;
30
+ }
31
+ runtime.defineCustomServerStrategy("custom-header", {
32
+ getLocale: async (request) => (await getLocaleFromUserRequest(request)) ?? undefined,
33
+ });
34
+ const request = new Request("http://example.com", {
35
+ headers: {
36
+ "X-Custom-User-ID": "1",
37
+ },
38
+ });
39
+ const locale = await runtime.extractLocaleFromRequestAsync(request);
40
+ expect(locale).toBe("fr");
41
+ });
42
+ test("falls back to next strategy when custom strategy returns undefined", async () => {
43
+ const runtime = await createParaglide({
44
+ blob: await newProject({
45
+ settings: {
46
+ baseLocale: "en",
47
+ locales: ["en", "fr"],
48
+ },
49
+ }),
50
+ strategy: ["custom-fallback", "baseLocale"],
51
+ });
52
+ runtime.defineCustomServerStrategy("custom-fallback", {
53
+ getLocale: () => undefined,
54
+ });
55
+ const request = new Request("http://example.com");
56
+ const locale = await runtime.extractLocaleFromRequestAsync(request);
57
+ expect(locale).toBe("en"); // Should fall back to baseLocale
58
+ });
@@ -8,6 +8,9 @@
8
8
  * they are defined. If a strategy returns an invalid locale,
9
9
  * it will fall back to the next strategy.
10
10
  *
11
+ * Note: Custom server strategies are not supported in this synchronous version.
12
+ * Use `extractLocaleFromRequestAsync` if you need custom server strategies with async getLocale methods.
13
+ *
11
14
  * @example
12
15
  * const locale = extractLocaleFromRequest(request);
13
16
  *
@@ -1 +1 @@
1
- {"version":3,"file":"extract-locale-from-request.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/extract-locale-from-request.js"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;GAcG;AACH,uCAFU,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAyCpC"}
1
+ {"version":3,"file":"extract-locale-from-request.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/extract-locale-from-request.js"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;GAiBG;AACH,uCAFU,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CA0CpC"}
@@ -2,7 +2,7 @@ import { assertIsLocale } from "./assert-is-locale.js";
2
2
  import { extractLocaleFromHeader } from "./extract-locale-from-header.js";
3
3
  import { extractLocaleFromUrl } from "./extract-locale-from-url.js";
4
4
  import { isLocale } from "./is-locale.js";
5
- import { customServerStrategies, isCustomStrategy } from "./strategy.js";
5
+ import { isCustomStrategy } from "./strategy.js";
6
6
  import { baseLocale, cookieName, strategy, TREE_SHAKE_COOKIE_STRATEGY_USED, TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED, TREE_SHAKE_URL_STRATEGY_USED, } from "./variables.js";
7
7
  /**
8
8
  * Extracts a locale from a request.
@@ -14,6 +14,9 @@ import { baseLocale, cookieName, strategy, TREE_SHAKE_COOKIE_STRATEGY_USED, TREE
14
14
  * they are defined. If a strategy returns an invalid locale,
15
15
  * it will fall back to the next strategy.
16
16
  *
17
+ * Note: Custom server strategies are not supported in this synchronous version.
18
+ * Use `extractLocaleFromRequestAsync` if you need custom server strategies with async getLocale methods.
19
+ *
17
20
  * @example
18
21
  * const locale = extractLocaleFromRequest(request);
19
22
  *
@@ -46,9 +49,10 @@ export const extractLocaleFromRequest = (request) => {
46
49
  else if (strat === "localStorage") {
47
50
  continue;
48
51
  }
49
- else if (isCustomStrategy(strat) && customServerStrategies.has(strat)) {
50
- const handler = customServerStrategies.get(strat);
51
- locale = handler.getLocale(request);
52
+ else if (isCustomStrategy(strat)) {
53
+ // Custom strategies are not supported in sync version
54
+ // Use extractLocaleFromRequestAsync for custom server strategies
55
+ continue;
52
56
  }
53
57
  if (locale !== undefined) {
54
58
  if (!isLocale(locale)) {
@@ -238,7 +238,7 @@ test("preferredLanguage precedence over url", async () => {
238
238
  const locale = runtime.extractLocaleFromRequest(request);
239
239
  expect(locale).toBe("de");
240
240
  });
241
- test("returns locale from custom strategy", async () => {
241
+ test("sync version no longer supports custom strategies", async () => {
242
242
  const runtime = await createParaglide({
243
243
  blob: await newProject({
244
244
  settings: {
@@ -257,10 +257,11 @@ test("returns locale from custom strategy", async () => {
257
257
  "X-Custom-Locale": "fr",
258
258
  },
259
259
  });
260
+ // Sync version skips custom strategies and falls back to baseLocale
260
261
  const locale = runtime.extractLocaleFromRequest(request);
261
- expect(locale).toBe("fr");
262
+ expect(locale).toBe("en"); // baseLocale fallback
262
263
  });
263
- test("falls back to next strategy when custom strategy returns undefined", async () => {
264
+ test("sync version skips custom strategy and falls back to next built-in strategy", async () => {
264
265
  const runtime = await createParaglide({
265
266
  blob: await newProject({
266
267
  settings: {
@@ -275,9 +276,9 @@ test("falls back to next strategy when custom strategy returns undefined", async
275
276
  });
276
277
  const request = new Request("http://example.com");
277
278
  const locale = runtime.extractLocaleFromRequest(request);
278
- expect(locale).toBe("en"); // Should fall back to baseLocale
279
+ expect(locale).toBe("en"); // Should fall back to baseLocale (skipping custom strategy)
279
280
  });
280
- test("custom strategy takes precedence over built-in strategies", async () => {
281
+ test("sync version uses built-in strategies instead of custom strategies", async () => {
281
282
  const runtime = await createParaglide({
282
283
  blob: await newProject({
283
284
  settings: {
@@ -297,9 +298,9 @@ test("custom strategy takes precedence over built-in strategies", async () => {
297
298
  },
298
299
  });
299
300
  const locale = runtime.extractLocaleFromRequest(request);
300
- expect(locale).toBe("de"); // Should use custom strategy, not cookie
301
+ expect(locale).toBe("fr"); // Should use cookie since custom strategy is skipped in sync version
301
302
  });
302
- test("multiple custom strategies work in order", async () => {
303
+ test("sync version skips all custom strategies and uses baseLocale", async () => {
303
304
  const runtime = await createParaglide({
304
305
  blob: await newProject({
305
306
  settings: {
@@ -317,5 +318,5 @@ test("multiple custom strategies work in order", async () => {
317
318
  });
318
319
  const request = new Request("http://example.com");
319
320
  const locale = runtime.extractLocaleFromRequest(request);
320
- expect(locale).toBe("fr"); // Should use second custom strategy
321
+ expect(locale).toBe("en"); // Should skip all custom strategies and use baseLocale
321
322
  });
@@ -1 +1 @@
1
- {"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/get-locale.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;GAWG;AACH,sBAFU,MAAM,MAAM,CAiEpB;AAEF;;;;;;;;;;;;;;GAcG;AACH,iCAFU,CAAC,EAAE,EAAE,MAAM,MAAM,KAAK,IAAI,CAIlC"}
1
+ {"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/get-locale.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;GAWG;AACH,sBAFU,MAAM,MAAM,CAyEpB;AAEF;;;;;;;;;;;;;;GAcG;AACH,iCAFU,CAAC,EAAE,EAAE,MAAM,MAAM,KAAK,IAAI,CAIlC"}
@@ -69,7 +69,15 @@ export let getLocale = () => {
69
69
  }
70
70
  else if (isCustomStrategy(strat) && customClientStrategies.has(strat)) {
71
71
  const handler = customClientStrategies.get(strat);
72
- locale = handler.getLocale();
72
+ if (handler) {
73
+ const result = handler.getLocale();
74
+ // Handle both sync and async results - skip async in sync getLocale
75
+ if (result instanceof Promise) {
76
+ // Can't await in sync function, skip async strategies
77
+ continue;
78
+ }
79
+ locale = result;
80
+ }
73
81
  }
74
82
  // check if match, else continue loop
75
83
  if (locale !== undefined) {
@@ -180,7 +180,7 @@ test("initially sets the locale after resolving it for the first time", async ()
180
180
  // First call to getLocale should resolve and set the locale
181
181
  expect(runtime.getLocale()).toBe("de");
182
182
  // Cookie should be set, proving that the locale was initially set
183
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000; domain=example.com");
183
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000");
184
184
  expect(globalThis.window.location.href).toBe("https://example.com/de/page");
185
185
  });
186
186
  test("returns locale from custom strategy", async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;GAeG;AACH,sBAFU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,CAqFnE;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":"AAgBA;;;;;;;;;;;;;;;GAeG;AACH,sBAFU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,CA8FnE;AAgBK,uCAFI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,QAIrC"}
@@ -47,9 +47,11 @@ export let setLocale = (newLocale, options) => {
47
47
  typeof window === "undefined") {
48
48
  continue;
49
49
  }
50
- const domain = cookieDomain || window.location.hostname;
51
50
  // set the cookie
52
- document.cookie = `${cookieName}=${newLocale}; path=/; max-age=${cookieMaxAge}; domain=${domain}`;
51
+ const cookieString = `${cookieName}=${newLocale}; path=/; max-age=${cookieMaxAge}`;
52
+ document.cookie = cookieDomain
53
+ ? `${cookieString}; domain=${cookieDomain}`
54
+ : cookieString;
53
55
  }
54
56
  else if (strat === "baseLocale") {
55
57
  // nothing to be set here. baseLocale is only a fallback
@@ -78,7 +80,15 @@ export let setLocale = (newLocale, options) => {
78
80
  }
79
81
  else if (isCustomStrategy(strat) && customClientStrategies.has(strat)) {
80
82
  const handler = customClientStrategies.get(strat);
81
- handler.setLocale(newLocale);
83
+ if (handler) {
84
+ const result = handler.setLocale(newLocale);
85
+ // Handle async setLocale - fire and forget
86
+ if (result instanceof Promise) {
87
+ result.catch((error) => {
88
+ console.warn(`Custom strategy "${strat}" setLocale failed:`, error);
89
+ });
90
+ }
91
+ }
82
92
  }
83
93
  }
84
94
  if (!isServer &&
@@ -22,7 +22,7 @@ test("sets the cookie to a different locale", async () => {
22
22
  globalThis.document.cookie = "PARAGLIDE_LOCALE=en";
23
23
  runtime.setLocale("de");
24
24
  // set the locale
25
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000; domain=example.com");
25
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000");
26
26
  // reloads the site if window is available
27
27
  expect(globalThis.window.location.reload).toBeCalled();
28
28
  });
@@ -121,7 +121,7 @@ test("sets the cookie when it's an empty string", async () => {
121
121
  /** @ts-expect-error - client side api */
122
122
  globalThis.document = { cookie: "" };
123
123
  runtime.setLocale("en");
124
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000; domain=example.com");
124
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000");
125
125
  });
126
126
  test("when strategy precedes URL, it should set the locale and re-direct to the URL", async () => {
127
127
  const runtime = await createParaglide({
@@ -152,7 +152,7 @@ test("when strategy precedes URL, it should set the locale and re-direct to the
152
152
  // Cookie strategy should determine locale as French
153
153
  expect(runtime.getLocale()).toBe("fr");
154
154
  runtime.setLocale("en");
155
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000; domain=example.com");
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
158
  // https://github.com/opral/inlang-paraglide-js/issues/430
@@ -178,12 +178,12 @@ test("should not reload when setting locale to current locale", async () => {
178
178
  // Setting to the current locale (en)
179
179
  runtime.setLocale("en");
180
180
  // Cookie should remain unchanged
181
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000; domain=example.com");
181
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000");
182
182
  // Should not trigger a reload
183
183
  expect(globalThis.window.location.reload).not.toBeCalled();
184
184
  // Setting to a different locale should still work
185
185
  runtime.setLocale("de");
186
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000; domain=example.com");
186
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=de; path=/; max-age=34560000");
187
187
  expect(globalThis.window.location.reload).toBeCalled();
188
188
  });
189
189
  test("sets the locale to localStorage", async () => {
@@ -249,7 +249,7 @@ test("should set locale in all configured storage mechanisms regardless of which
249
249
  // Verify that all storage mechanisms are updated
250
250
  expect(globalThis.window.location.href).toBe("https://example.com/fr/page");
251
251
  expect(globalThis.localStorage.setItem).toHaveBeenCalledWith("PARAGLIDE_LOCALE", "fr");
252
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=fr; path=/; max-age=34560000; domain=example.com");
252
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=fr; path=/; max-age=34560000");
253
253
  });
254
254
  test("calls setLocale on custom strategy", async () => {
255
255
  let customLocale = "en";
@@ -350,7 +350,7 @@ test("custom strategy setLocale works with cookie and localStorage", async () =>
350
350
  runtime.setLocale("fr");
351
351
  expect(customData).toBe("fr");
352
352
  expect(globalThis.localStorage.setItem).toHaveBeenCalledWith("PARAGLIDE_LOCALE", "fr");
353
- expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=fr; path=/; max-age=34560000; domain=example.com");
353
+ expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=fr; path=/; max-age=34560000");
354
354
  });
355
355
  test("custom strategy setLocale integrates with URL strategy", async () => {
356
356
  let customStoredLocale = "en";
@@ -2,27 +2,25 @@
2
2
  * Checks if the given strategy is a custom strategy.
3
3
  *
4
4
  * @param {any} strategy The name of the custom strategy to validate.
5
- * Must be a string that starts with "custom-" followed by alphanumeric characters.
5
+ * Must be a string that starts with "custom-" followed by alphanumeric characters, hyphens, or underscores.
6
6
  * @returns {boolean} Returns true if it is a custom strategy, false otherwise.
7
7
  */
8
8
  export function isCustomStrategy(strategy: any): boolean;
9
9
  /**
10
10
  * Defines a custom strategy that is executed on the server.
11
11
  *
12
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern `custom-<name>` where
13
- * `<name>` contains only alphanumeric characters.
12
+ * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
14
13
  * @param {CustomServerStrategyHandler} handler The handler for the custom strategy, which should implement
15
- * the method `getLocale`.
14
+ * the method getLocale.
16
15
  * @returns {void}
17
16
  */
18
17
  export function defineCustomServerStrategy(strategy: any, handler: CustomServerStrategyHandler): void;
19
18
  /**
20
19
  * Defines a custom strategy that is executed on the client.
21
20
  *
22
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern `custom-<name>` where
23
- * `<name>` contains only alphanumeric characters.
21
+ * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
24
22
  * @param {CustomClientStrategyHandler} handler The handler for the custom strategy, which should implement the
25
- * methods `getLocale` and `setLocale`.
23
+ * methods getLocale and setLocale.
26
24
  * @returns {void}
27
25
  */
28
26
  export function defineCustomClientStrategy(strategy: any, handler: CustomClientStrategyHandler): void;
@@ -39,22 +37,24 @@ export function defineCustomClientStrategy(strategy: any, handler: CustomClientS
39
37
  * @typedef {Array<Strategy>} Strategies
40
38
  */
41
39
  /**
42
- * @typedef {{ getLocale: (request?: Request) => string | undefined }} CustomServerStrategyHandler
40
+ * @typedef {{ getLocale: (request?: Request) => Promise<string | undefined> | (string | undefined) }} CustomServerStrategyHandler
43
41
  */
44
42
  /**
45
- * @typedef {{ getLocale: () => string | undefined, setLocale: (locale: string) => void }} CustomClientStrategyHandler
43
+ * @typedef {{ getLocale: () => Promise<string|undefined> | (string | undefined), setLocale: (locale: string) => Promise<void> | void }} CustomClientStrategyHandler
46
44
  */
47
- export const customServerStrategies: Map<any, any>;
48
- export const customClientStrategies: Map<any, any>;
45
+ /** @type {Map<string, CustomServerStrategyHandler>} */
46
+ export const customServerStrategies: Map<string, CustomServerStrategyHandler>;
47
+ /** @type {Map<string, CustomClientStrategyHandler>} */
48
+ export const customClientStrategies: Map<string, CustomClientStrategyHandler>;
49
49
  export type BuiltInStrategy = "cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage" | "localStorage";
50
50
  export type CustomStrategy = `custom_${string}`;
51
51
  export type Strategy = BuiltInStrategy | CustomStrategy;
52
52
  export type Strategies = Array<Strategy>;
53
53
  export type CustomServerStrategyHandler = {
54
- getLocale: (request?: Request) => string | undefined;
54
+ getLocale: (request?: Request) => Promise<string | undefined> | (string | undefined);
55
55
  };
56
56
  export type CustomClientStrategyHandler = {
57
- getLocale: () => string | undefined;
58
- setLocale: (locale: string) => void;
57
+ getLocale: () => Promise<string | undefined> | (string | undefined);
58
+ setLocale: (locale: string) => Promise<void> | void;
59
59
  };
60
60
  //# sourceMappingURL=strategy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/strategy.js"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,2CAJW,GAAG,GAED,OAAO,CAInB;AAED;;;;;;;;GAQG;AACH,qDANW,GAAG,WAEH,2BAA2B,GAEzB,IAAI,CAWhB;AAED;;;;;;;;GAQG;AACH,qDANW,GAAG,WAEH,2BAA2B,GAEzB,IAAI,CAWhB;AA5ED;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH,mDAAgD;AAChD,mDAAgD;8BAxBnC,QAAQ,GAAG,YAAY,GAAG,gBAAgB,GAAG,KAAK,GAAG,mBAAmB,GAAG,cAAc;6BAIzF,UAAU,MAAM,EAAE;uBAIlB,eAAe,GAAG,cAAc;yBAIhC,KAAK,CAAC,QAAQ,CAAC;0CAIf;IAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAA;CAAE;0CAIxD;IAAE,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE"}
1
+ {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/strategy.js"],"names":[],"mappings":"AA6BA;;;;;;GAMG;AACH,2CAJW,GAAG,GAED,OAAO,CAMnB;AAED;;;;;;;GAOG;AACH,qDALW,GAAG,WACH,2BAA2B,GAEzB,IAAI,CAUhB;AAED;;;;;;;GAOG;AACH,qDALW,GAAG,WACH,2BAA2B,GAEzB,IAAI,CAUhB;AA5ED;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH;;GAEG;AAEH,uDAAuD;AACvD,qCADW,GAAG,CAAC,MAAM,EAAE,2BAA2B,CAAC,CACH;AAChD,uDAAuD;AACvD,qCADW,GAAG,CAAC,MAAM,EAAE,2BAA2B,CAAC,CACH;8BA1BnC,QAAQ,GAAG,YAAY,GAAG,gBAAgB,GAAG,KAAK,GAAG,mBAAmB,GAAG,cAAc;6BAIzF,UAAU,MAAM,EAAE;uBAIlB,eAAe,GAAG,cAAc;yBAIhC,KAAK,CAAC,QAAQ,CAAC;0CAIf;IAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE;0CAIxF;IAAE,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAC,SAAS,CAAC,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAAE"}
@@ -11,52 +11,50 @@
11
11
  * @typedef {Array<Strategy>} Strategies
12
12
  */
13
13
  /**
14
- * @typedef {{ getLocale: (request?: Request) => string | undefined }} CustomServerStrategyHandler
14
+ * @typedef {{ getLocale: (request?: Request) => Promise<string | undefined> | (string | undefined) }} CustomServerStrategyHandler
15
15
  */
16
16
  /**
17
- * @typedef {{ getLocale: () => string | undefined, setLocale: (locale: string) => void }} CustomClientStrategyHandler
17
+ * @typedef {{ getLocale: () => Promise<string|undefined> | (string | undefined), setLocale: (locale: string) => Promise<void> | void }} CustomClientStrategyHandler
18
18
  */
19
+ /** @type {Map<string, CustomServerStrategyHandler>} */
19
20
  export const customServerStrategies = new Map();
21
+ /** @type {Map<string, CustomClientStrategyHandler>} */
20
22
  export const customClientStrategies = new Map();
21
23
  /**
22
24
  * Checks if the given strategy is a custom strategy.
23
25
  *
24
26
  * @param {any} strategy The name of the custom strategy to validate.
25
- * Must be a string that starts with "custom-" followed by alphanumeric characters.
27
+ * Must be a string that starts with "custom-" followed by alphanumeric characters, hyphens, or underscores.
26
28
  * @returns {boolean} Returns true if it is a custom strategy, false otherwise.
27
29
  */
28
30
  export function isCustomStrategy(strategy) {
29
- return typeof strategy === "string" && /^custom-[A-Za-z0-9]+$/.test(strategy);
31
+ return (typeof strategy === "string" && /^custom-[A-Za-z0-9_-]+$/.test(strategy));
30
32
  }
31
33
  /**
32
34
  * Defines a custom strategy that is executed on the server.
33
35
  *
34
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern `custom-<name>` where
35
- * `<name>` contains only alphanumeric characters.
36
+ * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
36
37
  * @param {CustomServerStrategyHandler} handler The handler for the custom strategy, which should implement
37
- * the method `getLocale`.
38
+ * the method getLocale.
38
39
  * @returns {void}
39
40
  */
40
41
  export function defineCustomServerStrategy(strategy, handler) {
41
42
  if (!isCustomStrategy(strategy)) {
42
- throw new Error(`Invalid custom strategy: "${strategy}". Must be a custom strategy following the pattern custom-<name>` +
43
- " where <name> contains only alphanumeric characters.");
43
+ throw new Error(`Invalid custom strategy: "${strategy}". Must be a custom strategy following the pattern custom-name.`);
44
44
  }
45
45
  customServerStrategies.set(strategy, handler);
46
46
  }
47
47
  /**
48
48
  * Defines a custom strategy that is executed on the client.
49
49
  *
50
- * @param {any} strategy The name of the custom strategy to define. Must follow the pattern `custom-<name>` where
51
- * `<name>` contains only alphanumeric characters.
50
+ * @param {any} strategy The name of the custom strategy to define. Must follow the pattern custom-name with alphanumeric characters, hyphens, or underscores.
52
51
  * @param {CustomClientStrategyHandler} handler The handler for the custom strategy, which should implement the
53
- * methods `getLocale` and `setLocale`.
52
+ * methods getLocale and setLocale.
54
53
  * @returns {void}
55
54
  */
56
55
  export function defineCustomClientStrategy(strategy, handler) {
57
56
  if (!isCustomStrategy(strategy)) {
58
- throw new Error(`Invalid custom strategy: "${strategy}". Must be a custom strategy following the pattern custom-<name>` +
59
- " where <name> contains only alphanumeric characters.");
57
+ throw new Error(`Invalid custom strategy: "${strategy}". Must be a custom strategy following the pattern custom-name.`);
60
58
  }
61
59
  customClientStrategies.set(strategy, handler);
62
60
  }
@@ -21,8 +21,10 @@ describe("isCustomStrategy", () => {
21
21
  expect(isCustomStrategy("custom-")).toBe(false);
22
22
  expect(isCustomStrategy("header")).toBe(false);
23
23
  expect(isCustomStrategy("custom")).toBe(false);
24
- expect(isCustomStrategy("custom-invalid-name")).toBe(false);
25
- expect(isCustomStrategy("custom-invalid_name")).toBe(false);
24
+ // These are now valid with our relaxed pattern:
25
+ expect(isCustomStrategy("custom-invalid-name")).toBe(true);
26
+ expect(isCustomStrategy("custom-invalid_name")).toBe(true);
27
+ // But spaces and special chars are still invalid:
26
28
  expect(isCustomStrategy("custom-invalid name")).toBe(false);
27
29
  expect(isCustomStrategy("custom-invalid@")).toBe(false);
28
30
  expect(isCustomStrategy("Custom-header")).toBe(false);
@@ -62,14 +64,12 @@ describe.each([
62
64
  const defaultHandler = { getLocale: () => "en", setLocale: () => { } };
63
65
  const invalidInputs = [
64
66
  ["", "empty name"],
65
- ["invalid-name", "names with hyphens"],
66
- ["invalid_name", "names with underscores"],
67
67
  ["@invalid", "names with special characters (@)"],
68
68
  ["invalid!", "names with special characters (!)"],
69
69
  ["inva lid", "names with spaces"],
70
70
  ];
71
71
  test.each(invalidInputs)(`${strategyName} throws error for %s (%s)`, (input) => {
72
- expect(() => defineStrategy(input, defaultHandler)).toThrow(`Invalid custom strategy: "${input}". Must be a custom strategy following the pattern custom-<name> where <name> contains only alphanumeric characters.`);
72
+ expect(() => defineStrategy(input, defaultHandler)).toThrow(`Invalid custom strategy: "${input}". Must be a custom strategy following the pattern custom-name.`);
73
73
  });
74
74
  test(`${strategyName} should compile with custom strategies in strategy array`, async () => {
75
75
  // Test that compile accepts custom strategies
@@ -27,6 +27,7 @@ export type Runtime = {
27
27
  deLocalizeUrl: typeof import("./localize-url.js").deLocalizeUrl;
28
28
  extractLocaleFromUrl: typeof import("./extract-locale-from-url.js").extractLocaleFromUrl;
29
29
  extractLocaleFromRequest: typeof import("./extract-locale-from-request.js").extractLocaleFromRequest;
30
+ extractLocaleFromRequestAsync: typeof import("./extract-locale-from-request-async.js").extractLocaleFromRequestAsync;
30
31
  extractLocaleFromCookie: typeof import("./extract-locale-from-cookie.js").extractLocaleFromCookie;
31
32
  extractLocaleFromHeader: typeof import("./extract-locale-from-header.js").extractLocaleFromHeader;
32
33
  extractLocaleFromNavigator: typeof import("./extract-locale-from-navigator.js").extractLocaleFromNavigator;
@@ -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,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,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"}
@@ -68,7 +68,7 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
68
68
  else if (!runtime.serverAsyncLocalStorage) {
69
69
  runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
70
70
  }
71
- const locale = runtime.extractLocaleFromRequest(request);
71
+ const locale = await runtime.extractLocaleFromRequestAsync(request);
72
72
  const origin = new URL(request.url).origin;
73
73
  // if the client makes a request to a URL that doesn't match
74
74
  // the localizedUrl, redirect the client to the localized URL
@@ -579,3 +579,193 @@ test("does not catch errors thrown by downstream resolve call", async () => {
579
579
  throw new Error("Downstream error");
580
580
  })).rejects.toThrow();
581
581
  });
582
+ test("middleware supports async custom server strategies", async () => {
583
+ const runtime = await createParaglide({
584
+ blob: await newProject({
585
+ settings: {
586
+ baseLocale: "en",
587
+ locales: ["en", "fr", "de"],
588
+ },
589
+ }),
590
+ strategy: ["custom-database", "baseLocale"],
591
+ });
592
+ // Mock an async custom strategy that simulates a database call
593
+ let databaseCallCount = 0;
594
+ runtime.defineCustomServerStrategy("custom-database", {
595
+ getLocale: async (request) => {
596
+ databaseCallCount++;
597
+ // Simulate async database call delay
598
+ await new Promise((resolve) => setTimeout(resolve, 10));
599
+ // Extract user ID from a custom header (simulating authentication)
600
+ if (!request)
601
+ return undefined;
602
+ const userId = request.headers.get("X-User-ID");
603
+ if (userId === "user123") {
604
+ return "fr";
605
+ }
606
+ if (userId === "user456") {
607
+ return "de";
608
+ }
609
+ return undefined; // No user preference found
610
+ },
611
+ });
612
+ // Test 1: Request with user preference
613
+ const requestWithUserPref = new Request("https://example.com/page", {
614
+ headers: {
615
+ "X-User-ID": "user123",
616
+ "Sec-Fetch-Dest": "document",
617
+ },
618
+ });
619
+ let middlewareResolveWasCalled = false;
620
+ await runtime.paraglideMiddleware(requestWithUserPref, (args) => {
621
+ middlewareResolveWasCalled = true;
622
+ expect(args.locale).toBe("fr"); // Should get locale from async custom strategy
623
+ return new Response("User preference locale");
624
+ });
625
+ expect(middlewareResolveWasCalled).toBe(true);
626
+ expect(databaseCallCount).toBe(1);
627
+ // Test 2: Request with different user preference
628
+ const requestWithOtherUser = new Request("https://example.com/page", {
629
+ headers: {
630
+ "X-User-ID": "user456",
631
+ "Sec-Fetch-Dest": "document",
632
+ },
633
+ });
634
+ await runtime.paraglideMiddleware(requestWithOtherUser, (args) => {
635
+ expect(args.locale).toBe("de"); // Should get different locale
636
+ return new Response("Other user preference");
637
+ });
638
+ expect(databaseCallCount).toBe(2);
639
+ });
640
+ test("middleware falls back to other strategies when async custom strategy returns undefined", async () => {
641
+ const runtime = await createParaglide({
642
+ blob: await newProject({
643
+ settings: {
644
+ baseLocale: "en",
645
+ locales: ["en", "fr", "de"],
646
+ },
647
+ }),
648
+ strategy: ["custom-database", "cookie", "baseLocale"],
649
+ cookieName: "PARAGLIDE_LOCALE",
650
+ });
651
+ // Mock async custom strategy that returns undefined for unknown users
652
+ runtime.defineCustomServerStrategy("custom-database", {
653
+ getLocale: async (request) => {
654
+ await new Promise((resolve) => setTimeout(resolve, 5));
655
+ if (!request)
656
+ return undefined;
657
+ const userId = request.headers.get("X-User-ID");
658
+ // Only return locale for known users
659
+ if (userId === "known-user") {
660
+ return "fr";
661
+ }
662
+ return undefined; // Unknown user, fallback to other strategies
663
+ },
664
+ });
665
+ // Request from unknown user with cookie fallback
666
+ const request = new Request("https://example.com/page", {
667
+ headers: {
668
+ "X-User-ID": "unknown-user",
669
+ cookie: "PARAGLIDE_LOCALE=de",
670
+ "Sec-Fetch-Dest": "document",
671
+ },
672
+ });
673
+ let middlewareResolveWasCalled = false;
674
+ await runtime.paraglideMiddleware(request, (args) => {
675
+ middlewareResolveWasCalled = true;
676
+ expect(args.locale).toBe("de"); // Should fallback to cookie strategy
677
+ return new Response("Fallback locale");
678
+ });
679
+ expect(middlewareResolveWasCalled).toBe(true);
680
+ });
681
+ test("middleware handles async custom strategy errors gracefully", async () => {
682
+ const runtime = await createParaglide({
683
+ blob: await newProject({
684
+ settings: {
685
+ baseLocale: "en",
686
+ locales: ["en", "fr"],
687
+ },
688
+ }),
689
+ strategy: ["custom-database", "baseLocale"],
690
+ });
691
+ // Mock async custom strategy that throws an error
692
+ runtime.defineCustomServerStrategy("custom-database", {
693
+ getLocale: async () => {
694
+ await new Promise((resolve) => setTimeout(resolve, 5));
695
+ throw new Error("Database connection failed");
696
+ },
697
+ });
698
+ const request = new Request("https://example.com/page", {
699
+ headers: { "Sec-Fetch-Dest": "document" },
700
+ });
701
+ // The middleware should handle the error and not crash
702
+ await expect(runtime.paraglideMiddleware(request, (args) => {
703
+ // If we reach here, the error was handled and fallback worked
704
+ expect(args.locale).toBe("en"); // Should fallback to baseLocale
705
+ return new Response("Error handled");
706
+ })).rejects.toThrow("Database connection failed");
707
+ });
708
+ test("middleware works with multiple async custom strategies", async () => {
709
+ const runtime = await createParaglide({
710
+ blob: await newProject({
711
+ settings: {
712
+ baseLocale: "en",
713
+ locales: ["en", "fr", "de", "es"],
714
+ },
715
+ }),
716
+ strategy: ["custom-userPref", "custom-region", "baseLocale"],
717
+ });
718
+ let userPrefCallCount = 0;
719
+ let regionCallCount = 0;
720
+ // First strategy: user preference
721
+ runtime.defineCustomServerStrategy("custom-userPref", {
722
+ getLocale: async (request) => {
723
+ userPrefCallCount++;
724
+ await new Promise((resolve) => setTimeout(resolve, 5));
725
+ if (!request)
726
+ return undefined;
727
+ const userId = request.headers.get("X-User-ID");
728
+ return userId === "premium-user" ? "fr" : undefined;
729
+ },
730
+ });
731
+ // Second strategy: region detection
732
+ runtime.defineCustomServerStrategy("custom-region", {
733
+ getLocale: async (request) => {
734
+ regionCallCount++;
735
+ await new Promise((resolve) => setTimeout(resolve, 5));
736
+ if (!request)
737
+ return undefined;
738
+ const region = request.headers.get("X-Region");
739
+ return region === "europe" ? "de" : undefined;
740
+ },
741
+ });
742
+ // Test 1: First strategy succeeds
743
+ const premiumUserRequest = new Request("https://example.com/page", {
744
+ headers: {
745
+ "X-User-ID": "premium-user",
746
+ "X-Region": "europe",
747
+ "Sec-Fetch-Dest": "document",
748
+ },
749
+ });
750
+ await runtime.paraglideMiddleware(premiumUserRequest, (args) => {
751
+ expect(args.locale).toBe("fr"); // Should use first strategy result
752
+ return new Response("Premium user");
753
+ });
754
+ expect(userPrefCallCount).toBe(1);
755
+ // Second strategy should not be called since first one succeeded
756
+ expect(regionCallCount).toBe(0);
757
+ // Test 2: First strategy fails, second succeeds
758
+ const regionalUserRequest = new Request("https://example.com/page", {
759
+ headers: {
760
+ "X-User-ID": "regular-user",
761
+ "X-Region": "europe",
762
+ "Sec-Fetch-Dest": "document",
763
+ },
764
+ });
765
+ await runtime.paraglideMiddleware(regionalUserRequest, (args) => {
766
+ expect(args.locale).toBe("de"); // Should use second strategy result
767
+ return new Response("Regional user");
768
+ });
769
+ expect(userPrefCallCount).toBe(2);
770
+ expect(regionCallCount).toBe(1);
771
+ });
@@ -1,5 +1,5 @@
1
1
  export const ENV_VARIABLES = {
2
2
  PARJS_APP_ID: "library.inlang.paraglideJs",
3
3
  PARJS_POSTHOG_TOKEN: "phc_m5yJZCxjOGxF8CJvP5sQ3H0d76xpnLrsmiZHduT4jDz",
4
- PARJS_PACKAGE_VERSION: "2.1.0",
4
+ PARJS_PACKAGE_VERSION: "2.2.0",
5
5
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/paraglide-js",
3
3
  "type": "module",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -31,8 +31,8 @@
31
31
  "json5": "2.2.3",
32
32
  "unplugin": "^2.1.2",
33
33
  "urlpattern-polyfill": "^10.0.0",
34
- "@inlang/sdk": "2.4.9",
35
- "@inlang/recommend-sherlock": "0.2.1"
34
+ "@inlang/recommend-sherlock": "0.2.1",
35
+ "@inlang/sdk": "2.4.9"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/js": "^9.18.0",
@@ -44,14 +44,14 @@
44
44
  "memfs": "4.17.0",
45
45
  "prettier": "^3.4.2",
46
46
  "rolldown": "1.0.0-beta.1",
47
- "typedoc": "0.28.3",
48
- "typedoc-plugin-markdown": "^4.6.0",
47
+ "typedoc": "^0.28.5",
48
+ "typedoc-plugin-markdown": "^4.7.0",
49
49
  "typedoc-plugin-missing-exports": "4.0.0",
50
50
  "typescript": "^5.7.3",
51
51
  "typescript-eslint": "^8.20.0",
52
52
  "vitest": "2.1.8",
53
- "@inlang/paraglide-js": "2.1.0",
54
53
  "@inlang/plugin-message-format": "4.0.0",
54
+ "@inlang/paraglide-js": "2.2.0",
55
55
  "@opral/tsconfig": "1.1.0"
56
56
  },
57
57
  "keywords": [