@inlang/paraglide-js 2.26.0 → 2.28.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 (225) hide show
  1. package/README.md +11 -1
  2. package/dist/bundler-plugins/per-locale-build/index.js +14 -7
  3. package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
  4. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +237 -4
  5. package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
  6. package/dist/bundler-plugins/unplugin.d.ts +1 -1
  7. package/dist/bundler-plugins/unplugin.js +79 -10
  8. package/dist/bundler-plugins/unplugin.js.map +1 -1
  9. package/dist/bundler-plugins/unplugin.test.js +334 -1
  10. package/dist/bundler-plugins/unplugin.test.js.map +1 -1
  11. package/dist/bundler-plugins/vite.js +2 -1
  12. package/dist/bundler-plugins/vite.js.map +1 -1
  13. package/dist/bundler-plugins/vite.test.js +1 -1
  14. package/dist/bundler-plugins/vite.test.js.map +1 -1
  15. package/dist/cli/commands/compile/command.js +13 -0
  16. package/dist/cli/commands/compile/command.js.map +1 -1
  17. package/dist/cli/commands/compile/command.test.js +87 -3
  18. package/dist/cli/commands/compile/command.test.js.map +1 -1
  19. package/dist/cli/commands/init/command.d.ts +5 -0
  20. package/dist/cli/commands/init/command.js +216 -44
  21. package/dist/cli/commands/init/command.js.map +1 -1
  22. package/dist/cli/commands/init/command.test.d.ts +1 -0
  23. package/dist/cli/commands/init/command.test.js +123 -0
  24. package/dist/cli/commands/init/command.test.js.map +1 -0
  25. package/dist/cli/defaults.js +2 -2
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/init-options.d.ts +11 -0
  28. package/dist/cli/init-options.js +2 -0
  29. package/dist/cli/init-options.js.map +1 -0
  30. package/dist/cli/steps/add-inlang-check.d.ts +19 -0
  31. package/dist/cli/steps/add-inlang-check.js +60 -0
  32. package/dist/cli/steps/add-inlang-check.js.map +1 -0
  33. package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
  34. package/dist/cli/steps/add-inlang-check.test.js +88 -0
  35. package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
  36. package/dist/cli/steps/add-message-lint.d.ts +34 -0
  37. package/dist/cli/steps/add-message-lint.js +105 -0
  38. package/dist/cli/steps/add-message-lint.js.map +1 -0
  39. package/dist/cli/steps/add-message-lint.test.d.ts +1 -0
  40. package/dist/cli/steps/add-message-lint.test.js +74 -0
  41. package/dist/cli/steps/add-message-lint.test.js.map +1 -0
  42. package/dist/cli/steps/add-vite-plugin.d.ts +18 -0
  43. package/dist/cli/steps/add-vite-plugin.js +34 -6
  44. package/dist/cli/steps/add-vite-plugin.js.map +1 -1
  45. package/dist/cli/steps/add-vite-plugin.test.js +25 -9
  46. package/dist/cli/steps/add-vite-plugin.test.js.map +1 -1
  47. package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
  48. package/dist/cli/steps/agents-md-suggestion.js +19 -0
  49. package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
  50. package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
  51. package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
  52. package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
  53. package/dist/cli/steps/check-for-uncomitted-changes.d.ts +3 -0
  54. package/dist/cli/steps/check-for-uncomitted-changes.js +15 -3
  55. package/dist/cli/steps/check-for-uncomitted-changes.js.map +1 -1
  56. package/dist/cli/steps/detect-bundler.d.ts +1 -0
  57. package/dist/cli/steps/detect-bundler.js +7 -1
  58. package/dist/cli/steps/detect-bundler.js.map +1 -1
  59. package/dist/cli/steps/initialize-inlang-project.d.ts +4 -3
  60. package/dist/cli/steps/initialize-inlang-project.js +60 -157
  61. package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
  62. package/dist/cli/steps/maybe-add-machine-translation.d.ts +4 -0
  63. package/dist/cli/steps/maybe-add-machine-translation.js +14 -8
  64. package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
  65. package/dist/cli/steps/maybe-add-sherlock.d.ts +2 -0
  66. package/dist/cli/steps/maybe-add-sherlock.js +13 -7
  67. package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
  68. package/dist/cli/steps/maybe-add-sherlock.test.d.ts +1 -0
  69. package/dist/cli/steps/maybe-add-sherlock.test.js +44 -0
  70. package/dist/cli/steps/maybe-add-sherlock.test.js.map +1 -0
  71. package/dist/cli/steps/prompt-for-outdir.d.ts +2 -0
  72. package/dist/cli/steps/prompt-for-outdir.js +4 -0
  73. package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
  74. package/dist/cli/steps/update-package-json.js +8 -13
  75. package/dist/cli/steps/update-package-json.js.map +1 -1
  76. package/dist/cli/steps/update-ts-config.d.ts +6 -0
  77. package/dist/cli/steps/update-ts-config.js +27 -4
  78. package/dist/cli/steps/update-ts-config.js.map +1 -1
  79. package/dist/cli/utils.d.ts +11 -0
  80. package/dist/cli/utils.js +21 -0
  81. package/dist/cli/utils.js.map +1 -1
  82. package/dist/cli/utils.test.d.ts +1 -0
  83. package/dist/cli/utils.test.js +16 -0
  84. package/dist/cli/utils.test.js.map +1 -0
  85. package/dist/compiler/compile-annotation.d.ts +48 -1
  86. package/dist/compiler/compile-annotation.js +243 -10
  87. package/dist/compiler/compile-annotation.js.map +1 -1
  88. package/dist/compiler/compile-bundle.d.ts +7 -2
  89. package/dist/compiler/compile-bundle.js +97 -3
  90. package/dist/compiler/compile-bundle.js.map +1 -1
  91. package/dist/compiler/compile-bundle.test.js +232 -19
  92. package/dist/compiler/compile-bundle.test.js.map +1 -1
  93. package/dist/compiler/compile-lifecycle.test.d.ts +1 -0
  94. package/dist/compiler/compile-lifecycle.test.js +69 -0
  95. package/dist/compiler/compile-lifecycle.test.js.map +1 -0
  96. package/dist/compiler/compile-local-variable.d.ts +3 -0
  97. package/dist/compiler/compile-local-variable.js +5 -2
  98. package/dist/compiler/compile-local-variable.js.map +1 -1
  99. package/dist/compiler/compile-local-variable.test.js +55 -15
  100. package/dist/compiler/compile-local-variable.test.js.map +1 -1
  101. package/dist/compiler/compile-message.d.ts +29 -1
  102. package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
  103. package/dist/compiler/compile-message.exact-number.test.js +786 -0
  104. package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
  105. package/dist/compiler/compile-message.js +162 -66
  106. package/dist/compiler/compile-message.js.map +1 -1
  107. package/dist/compiler/compile-message.locals.test.d.ts +1 -0
  108. package/dist/compiler/compile-message.locals.test.js +498 -0
  109. package/dist/compiler/compile-message.locals.test.js.map +1 -0
  110. package/dist/compiler/compile-message.test.js +113 -22
  111. package/dist/compiler/compile-message.test.js.map +1 -1
  112. package/dist/compiler/compile-pattern.d.ts +5 -0
  113. package/dist/compiler/compile-pattern.js +6 -18
  114. package/dist/compiler/compile-pattern.js.map +1 -1
  115. package/dist/compiler/compile-pattern.test.js +169 -5
  116. package/dist/compiler/compile-pattern.test.js.map +1 -1
  117. package/dist/compiler/compile-project.js +5 -1
  118. package/dist/compiler/compile-project.js.map +1 -1
  119. package/dist/compiler/compile-project.test.js +1260 -185
  120. package/dist/compiler/compile-project.test.js.map +1 -1
  121. package/dist/compiler/compile.js +36 -17
  122. package/dist/compiler/compile.js.map +1 -1
  123. package/dist/compiler/compile.test.js +71 -1
  124. package/dist/compiler/compile.test.js.map +1 -1
  125. package/dist/compiler/compiler-options.d.ts +41 -0
  126. package/dist/compiler/compiler-options.js.map +1 -1
  127. package/dist/compiler/create-readme.d.ts +4 -1
  128. package/dist/compiler/create-readme.js +33 -125
  129. package/dist/compiler/create-readme.js.map +1 -1
  130. package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
  131. package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
  132. package/dist/compiler/emit-ts-declarations.test.js +4 -4
  133. package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
  134. package/dist/compiler/index.d.ts +1 -1
  135. package/dist/compiler/index.js +1 -1
  136. package/dist/compiler/index.js.map +1 -1
  137. package/dist/compiler/input-alias.d.ts +21 -0
  138. package/dist/compiler/input-alias.js +42 -0
  139. package/dist/compiler/input-alias.js.map +1 -0
  140. package/dist/compiler/input-alias.test.d.ts +1 -0
  141. package/dist/compiler/input-alias.test.js +58 -0
  142. package/dist/compiler/input-alias.test.js.map +1 -0
  143. package/dist/compiler/message-locals.d.ts +46 -0
  144. package/dist/compiler/message-locals.js +129 -0
  145. package/dist/compiler/message-locals.js.map +1 -0
  146. package/dist/compiler/output-structure/locale-modules.js +7 -1
  147. package/dist/compiler/output-structure/locale-modules.js.map +1 -1
  148. package/dist/compiler/output-structure/locale-modules.test.js +35 -0
  149. package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
  150. package/dist/compiler/output-structure/message-modules.js +6 -4
  151. package/dist/compiler/output-structure/message-modules.js.map +1 -1
  152. package/dist/compiler/output-structure/message-modules.test.js +35 -0
  153. package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
  154. package/dist/compiler/registry.d.ts +12 -0
  155. package/dist/compiler/registry.js +158 -31
  156. package/dist/compiler/registry.js.map +1 -1
  157. package/dist/compiler/registry.test.d.ts +1 -0
  158. package/dist/compiler/registry.test.js +291 -0
  159. package/dist/compiler/registry.test.js.map +1 -0
  160. package/dist/compiler/runtime/create-runtime.d.ts +2 -0
  161. package/dist/compiler/runtime/create-runtime.js +9 -0
  162. package/dist/compiler/runtime/create-runtime.js.map +1 -1
  163. package/dist/compiler/runtime/extract-locale-from-cookie.js +8 -1
  164. package/dist/compiler/runtime/extract-locale-from-cookie.js.map +1 -1
  165. package/dist/compiler/runtime/extract-locale-from-cookie.test.js +26 -0
  166. package/dist/compiler/runtime/extract-locale-from-cookie.test.js.map +1 -1
  167. package/dist/compiler/runtime/extract-locale-from-header.js +47 -5
  168. package/dist/compiler/runtime/extract-locale-from-header.js.map +1 -1
  169. package/dist/compiler/runtime/extract-locale-from-header.test.js +58 -0
  170. package/dist/compiler/runtime/extract-locale-from-header.test.js.map +1 -1
  171. package/dist/compiler/runtime/get-locale.js +7 -1
  172. package/dist/compiler/runtime/get-locale.js.map +1 -1
  173. package/dist/compiler/runtime/get-locale.test.js +95 -0
  174. package/dist/compiler/runtime/get-locale.test.js.map +1 -1
  175. package/dist/compiler/runtime/localize-href.d.ts +8 -0
  176. package/dist/compiler/runtime/localize-href.js +60 -0
  177. package/dist/compiler/runtime/localize-href.js.map +1 -1
  178. package/dist/compiler/runtime/localize-href.test.js +227 -1
  179. package/dist/compiler/runtime/localize-href.test.js.map +1 -1
  180. package/dist/compiler/runtime/set-locale.js +24 -6
  181. package/dist/compiler/runtime/set-locale.js.map +1 -1
  182. package/dist/compiler/runtime/set-locale.test.js +182 -0
  183. package/dist/compiler/runtime/set-locale.test.js.map +1 -1
  184. package/dist/compiler/runtime/type.d.ts +2 -0
  185. package/dist/compiler/runtime/type.js.map +1 -1
  186. package/dist/compiler/runtime/type.test.js +34 -0
  187. package/dist/compiler/runtime/type.test.js.map +1 -1
  188. package/dist/compiler/runtime/variables.d.ts +2 -0
  189. package/dist/compiler/runtime/variables.js +7 -0
  190. package/dist/compiler/runtime/variables.js.map +1 -1
  191. package/dist/compiler/server/create-server-file.js +24 -0
  192. package/dist/compiler/server/create-server-file.js.map +1 -1
  193. package/dist/compiler/server/create-server-file.test.js +8 -2
  194. package/dist/compiler/server/create-server-file.test.js.map +1 -1
  195. package/dist/compiler/server/middleware.d.ts +8 -2
  196. package/dist/compiler/server/middleware.js +204 -11
  197. package/dist/compiler/server/middleware.js.map +1 -1
  198. package/dist/compiler/server/middleware.test.js +750 -3
  199. package/dist/compiler/server/middleware.test.js.map +1 -1
  200. package/dist/eslint/index.d.ts +4 -0
  201. package/dist/eslint/index.js +8 -0
  202. package/dist/eslint/index.js.map +1 -0
  203. package/dist/eslint/no-dynamic-message-access.d.ts +3 -0
  204. package/dist/eslint/no-dynamic-message-access.js +150 -0
  205. package/dist/eslint/no-dynamic-message-access.js.map +1 -0
  206. package/dist/eslint/no-dynamic-message-access.test.d.ts +1 -0
  207. package/dist/eslint/no-dynamic-message-access.test.js +126 -0
  208. package/dist/eslint/no-dynamic-message-access.test.js.map +1 -0
  209. package/dist/eslint/oxlint.test.d.ts +1 -0
  210. package/dist/eslint/oxlint.test.js +64 -0
  211. package/dist/eslint/oxlint.test.js.map +1 -0
  212. package/dist/services/config/config-schema.d.ts +2 -0
  213. package/dist/services/config/config-schema.js +2 -0
  214. package/dist/services/config/config-schema.js.map +1 -1
  215. package/dist/services/config/load-paraglide-config.test.js +12 -0
  216. package/dist/services/config/load-paraglide-config.test.js.map +1 -1
  217. package/dist/services/env-variables/index.js +1 -1
  218. package/dist/services/env-variables/index.js.map +1 -1
  219. package/dist/services/file-handling/write-output.bench.js +51 -11
  220. package/dist/services/file-handling/write-output.bench.js.map +1 -1
  221. package/dist/services/file-handling/write-output.js +60 -20
  222. package/dist/services/file-handling/write-output.js.map +1 -1
  223. package/dist/services/file-handling/write-output.test.js +175 -0
  224. package/dist/services/file-handling/write-output.test.js.map +1 -1
  225. package/package.json +15 -5
@@ -1,4 +1,6 @@
1
- import { test, expect } from "vitest";
1
+ import { test, expect, describe, beforeAll, vi } from "vitest";
2
+ import { once } from "node:events";
3
+ import { createServer } from "node:http";
2
4
  import { createParaglide } from "../create-paraglide.js";
3
5
  import { newProject } from "@inlang/sdk";
4
6
  test("sets the locale and origin", async () => {
@@ -93,6 +95,7 @@ test("skips i18n middleware behavior for excluded routeStrategies", async () =>
93
95
  });
94
96
  const response = await runtime.paraglideMiddleware(request, ({ locale, request }) => new Response(`${locale}|${request.url}`));
95
97
  expect(response.status).toBe(200);
98
+ expect(response.headers.get("Vary")).toBe(null);
96
99
  expect(await response.text()).toBe("en|https://example.com/api/data");
97
100
  });
98
101
  test("localized URLs still match routeStrategies after delocalization", async () => {
@@ -426,7 +429,7 @@ test("sets Vary: Accept-Language header when preferredLanguage strategy is used
426
429
  // Should have Vary header when preferredLanguage strategy is used
427
430
  expect(response.headers.get("Vary")).toBe("Accept-Language");
428
431
  });
429
- test("does not set Vary header when preferredLanguage strategy is not used", async () => {
432
+ test("sets Vary: Cookie without Accept-Language for a cookie redirect", async () => {
430
433
  const runtime = await createParaglide({
431
434
  blob: await newProject({
432
435
  settings: {
@@ -460,9 +463,337 @@ test("does not set Vary header when preferredLanguage strategy is not used", asy
460
463
  expect(response instanceof Response).toBe(true);
461
464
  expect(response.status).toBe(307); // Redirect status code
462
465
  expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path");
463
- // Should NOT have Vary header when preferredLanguage strategy is not used
466
+ expect(response.headers.get("Vary")).toBe("Cookie");
467
+ });
468
+ test.each([
469
+ {
470
+ strategy: ["cookie", "url", "baseLocale"],
471
+ vary: "Cookie",
472
+ locale: "fr",
473
+ },
474
+ {
475
+ strategy: ["preferredLanguage", "url", "baseLocale"],
476
+ vary: "Accept-Language",
477
+ locale: "fr",
478
+ },
479
+ {
480
+ strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
481
+ vary: "Cookie, Accept-Language",
482
+ locale: "fr",
483
+ },
484
+ { strategy: ["url", "baseLocale"], vary: null, locale: "en" },
485
+ ].flatMap((scenario) => [
486
+ {
487
+ ...scenario,
488
+ method: "GET",
489
+ destination: "document",
490
+ status: 307,
491
+ body: "",
492
+ location: `https://example.com/${scenario.locale}/page?campaign=example`,
493
+ },
494
+ {
495
+ ...scenario,
496
+ method: "GET",
497
+ destination: "",
498
+ status: 200,
499
+ body: scenario.locale,
500
+ location: null,
501
+ },
502
+ {
503
+ ...scenario,
504
+ method: "HEAD",
505
+ destination: "",
506
+ status: 200,
507
+ body: "",
508
+ location: null,
509
+ },
510
+ ]))("sets Vary from $strategy for $method with destination '$destination'", async ({ strategy, vary, method, destination, status, body, location }) => {
511
+ const runtime = await createParaglide({
512
+ blob: await newProject({
513
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
514
+ }),
515
+ strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
516
+ routeStrategies: [{ match: "/page", strategy }],
517
+ urlPatterns: [
518
+ {
519
+ pattern: "https://example.com/:path(.*)?",
520
+ localized: [
521
+ ["en", "https://example.com/en/:path(.*)?"],
522
+ ["fr", "https://example.com/fr/:path(.*)?"],
523
+ ],
524
+ },
525
+ ],
526
+ });
527
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page?campaign=example", {
528
+ method,
529
+ headers: {
530
+ cookie: "PARAGLIDE_LOCALE=fr",
531
+ "Accept-Language": "fr",
532
+ "Sec-Fetch-Dest": destination,
533
+ },
534
+ }), ({ locale }) => new Response(method === "HEAD" ? null : locale), {
535
+ onRedirect: (response) => {
536
+ expect(response.headers.get("Vary")).toBe(vary);
537
+ },
538
+ });
539
+ expect(response.headers.get("Vary")).toBe(vary);
540
+ expect(response.status).toBe(status);
541
+ expect(response.headers.get("Location")).toBe(location);
542
+ expect(await response.text()).toBe(body);
543
+ });
544
+ test.each([
545
+ ["Accept-Encoding", "Accept-Encoding, Cookie, Accept-Language"],
546
+ ["accept-encoding, cookie", "accept-encoding, cookie, Accept-Language"],
547
+ [
548
+ "Accept-Encoding, ACCEPT-LANGUAGE",
549
+ "Accept-Encoding, ACCEPT-LANGUAGE, Cookie",
550
+ ],
551
+ ["cookie, ACCEPT-LANGUAGE", "cookie, ACCEPT-LANGUAGE"],
552
+ ["*", "*"],
553
+ ["Origin, *", "Origin, *"],
554
+ ])("merges negotiated response Vary with %s", async (vary, expected) => {
555
+ const runtime = await createParaglide({
556
+ blob: await newProject({
557
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
558
+ }),
559
+ strategy: ["cookie", "preferredLanguage", "baseLocale"],
560
+ });
561
+ const headers = new Headers({ Vary: vary, "Content-Type": "text/plain" });
562
+ headers.append("Set-Cookie", "theme=dark; Path=/");
563
+ headers.append("Set-Cookie", "session=example; Path=/");
564
+ const original = new Response("fr", {
565
+ status: 202,
566
+ statusText: "Accepted",
567
+ headers,
568
+ });
569
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page", {
570
+ headers: { cookie: "PARAGLIDE_LOCALE=fr" },
571
+ }), () => original);
572
+ expect(response.headers.get("Vary")).toBe(expected);
573
+ expect(response).toBe(original);
574
+ expect(response.headers.get("Content-Type")).toBe("text/plain");
575
+ expect(response.headers.getSetCookie()).toEqual(headers.getSetCookie());
576
+ expect(response.status).toBe(202);
577
+ expect(response.statusText).toBe("Accepted");
578
+ expect(response.body).toBe(original.body);
579
+ expect(await response.text()).toBe("fr");
580
+ });
581
+ test("varies negotiated responses even when preference headers are absent", async () => {
582
+ const runtime = await createParaglide({
583
+ blob: await newProject({
584
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
585
+ }),
586
+ strategy: ["cookie", "preferredLanguage", "baseLocale"],
587
+ });
588
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), ({ locale }) => new Response(locale));
589
+ expect(response.headers.get("Vary")).toBe("Cookie, Accept-Language");
590
+ expect(await response.text()).toBe("en");
591
+ });
592
+ test.each([
593
+ ["https://example.com/fr/page", "fr"],
594
+ ["https://example.com/page", "en"],
595
+ ])("preserves Vary when the effective URL determines the locale: %s", async (url, locale) => {
596
+ const runtime = await createParaglide({
597
+ blob: await newProject({
598
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
599
+ }),
600
+ strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
601
+ });
602
+ const original = new Response(locale, {
603
+ headers: { Vary: "Accept-Encoding" },
604
+ });
605
+ const response = await runtime.paraglideMiddleware(new Request("http://internal/page", {
606
+ headers: { cookie: "PARAGLIDE_LOCALE=fr" },
607
+ }), ({ locale: resolved }) => {
608
+ expect(resolved).toBe(locale);
609
+ return original;
610
+ }, { effectiveRequestUrl: url });
611
+ expect(response).toBe(original);
612
+ expect(response.headers.get("Vary")).toBe("Accept-Encoding");
613
+ });
614
+ test.each([
615
+ {
616
+ strategy: ["cookie", "url", "preferredLanguage", "baseLocale"],
617
+ vary: "Cookie",
618
+ },
619
+ {
620
+ strategy: ["preferredLanguage", "url", "cookie", "baseLocale"],
621
+ vary: "Accept-Language",
622
+ },
623
+ {
624
+ strategy: ["cookie", "preferredLanguage", "url", "baseLocale"],
625
+ vary: "Cookie, Accept-Language",
626
+ },
627
+ ].flatMap((scenario) => ["/page", "/en/page"].flatMap((path) => ["GET", "HEAD"].flatMap((method) => ["", "en", "fr"].map((preference) => ({
628
+ ...scenario,
629
+ path,
630
+ method,
631
+ preference,
632
+ }))))))("varies $method $path for $strategy with preference '$preference'", async ({ strategy, vary, path, method, preference }) => {
633
+ const runtime = await createParaglide({
634
+ blob: await newProject({
635
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
636
+ }),
637
+ strategy,
638
+ });
639
+ const response = await runtime.paraglideMiddleware(new Request(`https://example.com${path}`, {
640
+ method,
641
+ headers: preference
642
+ ? {
643
+ Cookie: `PARAGLIDE_LOCALE=${preference}`,
644
+ "Accept-Language": preference,
645
+ }
646
+ : {},
647
+ }), ({ locale }) => {
648
+ expect(locale).toBe(preference || "en");
649
+ return new Response(method === "HEAD" ? null : locale);
650
+ });
651
+ expect(response.status).toBe(200);
652
+ expect(response.headers.get("Vary")).toBe(vary);
653
+ expect(await response.text()).toBe(method === "HEAD" ? "" : preference || "en");
654
+ });
655
+ test("preserves mutable response subclasses with the default strategy", async () => {
656
+ const runtime = await createParaglide({
657
+ blob: await newProject({
658
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
659
+ }),
660
+ });
661
+ class CustomResponse extends Response {
662
+ marker = "custom";
663
+ }
664
+ const original = new CustomResponse("page");
665
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
666
+ expect(response).toBe(original);
667
+ expect(response).toBeInstanceOf(CustomResponse);
668
+ expect(response.marker).toBe("custom");
669
+ expect(response.headers.get("Vary")).toBe("Cookie");
670
+ });
671
+ test("preserves Response.error() without adding Vary", async () => {
672
+ const runtime = await createParaglide({
673
+ blob: await newProject({
674
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
675
+ }),
676
+ });
677
+ const original = Response.error();
678
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
679
+ expect(response).toBe(original);
680
+ expect(response.status).toBe(0);
464
681
  expect(response.headers.get("Vary")).toBe(null);
465
682
  });
683
+ test("propagates unexpected header update errors", async () => {
684
+ const runtime = await createParaglide({
685
+ blob: await newProject({
686
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
687
+ }),
688
+ });
689
+ const original = new Response("page");
690
+ const failure = new Error("Cannot update headers");
691
+ original.headers.set = () => {
692
+ throw failure;
693
+ };
694
+ await expect(runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original)).rejects.toBe(failure);
695
+ });
696
+ test("adds Vary to a resolved response with immutable headers", async () => {
697
+ const runtime = await createParaglide({
698
+ blob: await newProject({
699
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
700
+ }),
701
+ strategy: ["cookie", "baseLocale"],
702
+ });
703
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => Response.redirect("https://example.com/login", 302));
704
+ expect(response.status).toBe(302);
705
+ expect(response.headers.get("Location")).toBe("https://example.com/login");
706
+ expect(response.headers.get("Vary")).toBe("Cookie");
707
+ });
708
+ test.each(["/page", "/redirect"])("preserves fetch response metadata when adding Vary for %s", async (path) => {
709
+ const runtime = await createParaglide({
710
+ blob: await newProject({
711
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
712
+ }),
713
+ });
714
+ const server = createServer((request, response) => {
715
+ if (request.url === "/redirect") {
716
+ response.writeHead(302, { Location: "/page" });
717
+ response.end();
718
+ }
719
+ else {
720
+ response.writeHead(202, "Accepted", { Vary: "Accept-Encoding" });
721
+ response.end("page");
722
+ }
723
+ });
724
+ server.listen(0, "127.0.0.1");
725
+ await once(server, "listening");
726
+ try {
727
+ const address = server.address();
728
+ if (!address || typeof address === "string") {
729
+ throw new Error("Expected a TCP server address");
730
+ }
731
+ const origin = `http://127.0.0.1:${address.port}`;
732
+ const original = await fetch(`${origin}${path}`);
733
+ const metadata = {
734
+ url: `${origin}/page`,
735
+ redirected: path === "/redirect",
736
+ type: "basic",
737
+ };
738
+ expect(original).toMatchObject(metadata);
739
+ expect(() => original.headers.set("Vary", "Cookie")).toThrow(TypeError);
740
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
741
+ const clone = response.clone();
742
+ const secondClone = clone.clone();
743
+ for (const result of [response, clone, secondClone]) {
744
+ expect
745
+ .soft({
746
+ url: result.url,
747
+ redirected: result.redirected,
748
+ type: result.type,
749
+ })
750
+ .toEqual(metadata);
751
+ expect(result.status).toBe(202);
752
+ expect(result.statusText).toBe("Accepted");
753
+ expect(result.headers.get("Vary")).toBe("Accept-Encoding, Cookie");
754
+ expect(await result.text()).toBe("page");
755
+ }
756
+ expect(original.headers.get("Vary")).toBe("Accept-Encoding");
757
+ expect(() => response.clone()).toThrow(TypeError);
758
+ }
759
+ finally {
760
+ server.closeAllConnections();
761
+ await new Promise((resolve, reject) => {
762
+ server.close((error) => (error ? reject(error) : resolve()));
763
+ });
764
+ }
765
+ });
766
+ test("allows overriding copied response properties", async () => {
767
+ const runtime = await createParaglide({
768
+ blob: await newProject({
769
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
770
+ }),
771
+ });
772
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => Response.redirect("https://example.com/login", 302));
773
+ for (const result of [new Response(), response, response.clone()]) {
774
+ result.clone = () => new Response("instrumented");
775
+ expect(await result.clone().text()).toBe("instrumented");
776
+ expect(Reflect.deleteProperty(result, "clone")).toBe(true);
777
+ Object.defineProperties(result, {
778
+ url: { value: "https://example.com/upstream" },
779
+ redirected: { value: true },
780
+ type: { value: "basic" },
781
+ });
782
+ expect(result.url).toBe("https://example.com/upstream");
783
+ expect(result.redirected).toBe(true);
784
+ expect(result.type).toBe("basic");
785
+ }
786
+ });
787
+ test("preserves non-Response results from resolve with cookie negotiation", async () => {
788
+ const runtime = await createParaglide({
789
+ blob: await newProject({
790
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
791
+ }),
792
+ strategy: ["cookie", "baseLocale"],
793
+ });
794
+ const result = { handled: true };
795
+ expect(await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => result)).toBe(result);
796
+ });
466
797
  test("does not call onRedirect callback when there is no redirecting", async () => {
467
798
  const runtime = await createParaglide({
468
799
  blob: await newProject({
@@ -836,6 +1167,422 @@ test("only redirects if the request.headers.get('Sec-Fetch-Dest') === 'document'
836
1167
  // Middleware should be called since no redirect for API requests
837
1168
  expect(apiMiddlewareResolveWasCalled).toBe(true);
838
1169
  });
1170
+ describe("navigation redirects", () => {
1171
+ test.each([
1172
+ ["/fr/page", "fr", null],
1173
+ ["/page", "en", "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"],
1174
+ ])("varies navigation responses according to URL resolution for %s", async (path, locale, expectedVary) => {
1175
+ const instance = await createParaglide({
1176
+ blob: await newProject({
1177
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1178
+ }),
1179
+ strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
1180
+ urlPatterns: [
1181
+ {
1182
+ pattern: "https://example.com/:path(.*)?",
1183
+ localized: [
1184
+ ["en", "https://example.com/en/:path(.*)?"],
1185
+ ["fr", "https://example.com/fr/:path(.*)?"],
1186
+ ],
1187
+ },
1188
+ ],
1189
+ });
1190
+ const response = await instance.paraglideMiddleware(new Request(`https://example.com${path}`, {
1191
+ headers: {
1192
+ Cookie: "PARAGLIDE_LOCALE=en",
1193
+ "Accept-Language": "en",
1194
+ Accept: "text/html",
1195
+ "Sec-Fetch-Dest": "empty",
1196
+ },
1197
+ }), ({ locale }) => new Response(locale), { redirect: "navigation" });
1198
+ expect(response.headers.get("Vary")).toBe(expectedVary);
1199
+ expect(await response.text()).toBe(locale);
1200
+ });
1201
+ let runtime;
1202
+ const vary = "Cookie, Accept-Language, Accept, Sec-Fetch-Dest";
1203
+ beforeAll(async () => {
1204
+ runtime = await createParaglide({
1205
+ blob: await newProject({
1206
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1207
+ }),
1208
+ strategy: ["preferredLanguage", "cookie", "url"],
1209
+ routeStrategies: [
1210
+ { match: "/api/:path(.*)?", exclude: true },
1211
+ { match: "/no-url", strategy: ["preferredLanguage"] },
1212
+ ],
1213
+ });
1214
+ });
1215
+ test.each([
1216
+ ["GET", undefined, "text/html", 307],
1217
+ ["HEAD", undefined, "text/html", 307],
1218
+ ["GET", undefined, "*/*", 307],
1219
+ ["GET", undefined, "text/*", 307],
1220
+ [
1221
+ "GET",
1222
+ undefined,
1223
+ "application/json, TEXT/HTML; charset=utf-8; Q=0.5",
1224
+ 307,
1225
+ ],
1226
+ ["GET", undefined, "text/html;q=0.001", 307],
1227
+ ["GET", undefined, "text/html;q=0, */*", 200],
1228
+ ["GET", undefined, "*/*, text/html;q=0", 200],
1229
+ ["GET", undefined, "text/*;q=0, */*", 200],
1230
+ ["GET", undefined, "text/html;q=0.5, text/*;q=0", 307],
1231
+ ["GET", undefined, "text/html;q=invalid", 200],
1232
+ ["GET", undefined, "text/html;q=2", 200],
1233
+ ["GET", undefined, "text/html;q=-1", 200],
1234
+ ["GET", undefined, "text/html;q=0.1234", 200],
1235
+ ["GET", undefined, 'text/html;profile="a,b";q=0, */*;q=0', 200],
1236
+ ["GET", undefined, 'text/html;profile="x;q=0";q=1', 307],
1237
+ ["GET", undefined, 'text/html;profile="x;q=1";q=0', 200],
1238
+ ["GET", undefined, 'text/html;profile="a\\\",b";q=0, */*;q=0', 200],
1239
+ ["GET", undefined, 'text/html;profile="a\\\";q=0";q=1', 307],
1240
+ ["GET", undefined, 'text/html;profile="a\\\\";q=0, */*;q=0', 200],
1241
+ ["GET", undefined, 'text/html;profile="unterminated', 200],
1242
+ ["GET", undefined, "application/json", 200],
1243
+ ["GET", undefined, "application/xhtml+xml", 200],
1244
+ ["GET", undefined, "text/html-invalid", 200],
1245
+ ["GET", undefined, "", 200],
1246
+ ["GET", undefined, undefined, 200],
1247
+ ["GET", "empty", "text/html", 200],
1248
+ ["GET", "", "text/html", 200],
1249
+ ["GET", "script", "text/html", 200],
1250
+ ["GET", "document", "application/json", 307],
1251
+ ["HEAD", "document", undefined, 307],
1252
+ ["POST", undefined, "text/html", 200],
1253
+ ["POST", "document", "text/html", 200],
1254
+ ["PUT", "document", "text/html", 200],
1255
+ ["OPTIONS", undefined, "text/html", 200],
1256
+ ])("%s with destination %s and Accept %s returns %i", async (method, destination, accept, status) => {
1257
+ const headers = new Headers({
1258
+ "Accept-Language": "fr",
1259
+ "Content-Type": "application/x-www-form-urlencoded",
1260
+ });
1261
+ if (destination !== undefined)
1262
+ headers.set("Sec-Fetch-Dest", destination);
1263
+ if (accept !== undefined)
1264
+ headers.set("Accept", accept);
1265
+ const body = method === "POST" ? "name=Alice" : undefined;
1266
+ const request = new Request("https://example.com/about?from=test", {
1267
+ method,
1268
+ headers,
1269
+ body,
1270
+ });
1271
+ const resolve = vi.fn(async ({ request: localized }) => {
1272
+ expect(localized.method).toBe(method);
1273
+ expect(await localized.text()).toBe(body ?? "");
1274
+ return new Response(null);
1275
+ });
1276
+ const onRedirect = vi.fn();
1277
+ const response = await runtime.paraglideMiddleware(request, resolve, {
1278
+ redirect: "navigation",
1279
+ onRedirect,
1280
+ });
1281
+ expect(response.status).toBe(status);
1282
+ expect(resolve).toHaveBeenCalledTimes(status === 307 ? 0 : 1);
1283
+ expect(onRedirect).toHaveBeenCalledTimes(status === 307 ? 1 : 0);
1284
+ expect(response.headers.get("Location")).toBe(status === 307 ? "https://example.com/fr/about?from=test" : null);
1285
+ expect(response.headers.get("Vary")).toBe(method === "GET" || method === "HEAD" ? vary : "Cookie, Accept-Language");
1286
+ expect(response.body).toBeNull();
1287
+ expect(request.bodyUsed).toBe(false);
1288
+ expect(await request.text()).toBe(body ?? "");
1289
+ });
1290
+ test.each([undefined, "document", null, "nav"])("keeps default behavior for redirect=%s", async (redirect) => {
1291
+ for (const method of ["GET", "HEAD", "POST"]) {
1292
+ for (const destination of [undefined, "document", "empty"]) {
1293
+ const headers = new Headers({
1294
+ Accept: "text/html",
1295
+ "Accept-Language": "fr",
1296
+ });
1297
+ if (destination)
1298
+ headers.set("Sec-Fetch-Dest", destination);
1299
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", { method, headers }), () => new Response(null), redirect === undefined
1300
+ ? undefined
1301
+ : { redirect: redirect });
1302
+ expect(response.status).toBe(destination === "document" ? 307 : 200);
1303
+ expect(response.headers.get("Vary")).toBe("Cookie, Accept-Language");
1304
+ }
1305
+ }
1306
+ });
1307
+ test.each(["GET", "HEAD"])("separates cached 200 and 307 responses for %s", async (method) => {
1308
+ for (const field of ["Accept-Language", "Cookie"]) {
1309
+ for (const locale of ["en", "fr"]) {
1310
+ for (const previous of [
1311
+ "",
1312
+ "Accept-Encoding",
1313
+ vary.toLowerCase(),
1314
+ "*",
1315
+ ]) {
1316
+ const downstream = new Response(method === "HEAD" ? null : "ok", {
1317
+ headers: {
1318
+ Vary: previous,
1319
+ "Cache-Control": "public, max-age=600",
1320
+ },
1321
+ });
1322
+ const response = await runtime.paraglideMiddleware(new Request("http://internal.example.com/about", {
1323
+ method,
1324
+ headers: {
1325
+ Accept: "text/html",
1326
+ [field]: field === "Cookie" ? `PARAGLIDE_LOCALE=${locale}` : locale,
1327
+ },
1328
+ }), () => downstream, {
1329
+ redirect: "navigation",
1330
+ effectiveRequestUrl: "https://example.com/about?from=test",
1331
+ });
1332
+ expect(response.status).toBe(locale === "fr" ? 307 : 200);
1333
+ expect(response.headers.get("Location")).toBe(locale === "fr" ? "https://example.com/fr/about?from=test" : null);
1334
+ const expected = previous === "Accept-Encoding"
1335
+ ? `${previous}, ${vary}`
1336
+ : previous || vary;
1337
+ expect(response.headers.get("Vary")).toBe(locale === "fr" ? vary : expected);
1338
+ if (locale === "en") {
1339
+ expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
1340
+ expect(await response.text()).toBe(method === "HEAD" ? "" : "ok");
1341
+ expect(response).toBe(downstream);
1342
+ expect(downstream.headers.get("Vary")).toBe(expected);
1343
+ }
1344
+ }
1345
+ }
1346
+ }
1347
+ });
1348
+ test("preserves response identity and custom state when adding Vary", async () => {
1349
+ class FrameworkResponse extends Response {
1350
+ context = { compression: "manual", framework: "example" };
1351
+ }
1352
+ const downstream = new FrameworkResponse("ok", {
1353
+ status: 201,
1354
+ statusText: "Created",
1355
+ headers: { Vary: "Accept-Encoding", "X-Framework": "example" },
1356
+ });
1357
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
1358
+ headers: { Accept: "application/json" },
1359
+ }), () => downstream, { redirect: "navigation" });
1360
+ expect(response).toBe(downstream);
1361
+ expect(response).toBeInstanceOf(FrameworkResponse);
1362
+ expect(downstream.context).toEqual({
1363
+ compression: "manual",
1364
+ framework: "example",
1365
+ });
1366
+ expect(response.status).toBe(201);
1367
+ expect(response.statusText).toBe("Created");
1368
+ expect(response.headers.get("X-Framework")).toBe("example");
1369
+ expect(response.headers.get("Vary")).toBe(`Accept-Encoding, ${vary}`);
1370
+ expect(await response.text()).toBe("ok");
1371
+ });
1372
+ test("preserves callback returns with unusable bodies", async () => {
1373
+ const consumed = new Response("ok");
1374
+ await consumed.text();
1375
+ const locked = new Response("ok");
1376
+ const reader = locked.body.getReader();
1377
+ try {
1378
+ for (const value of [undefined, {}, Response.error(), consumed, locked]) {
1379
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
1380
+ headers: { Accept: "application/json" },
1381
+ }), async () => value, { redirect: "navigation" });
1382
+ expect(response).toBe(value);
1383
+ }
1384
+ }
1385
+ finally {
1386
+ reader.releaseLock();
1387
+ }
1388
+ });
1389
+ test("preserves a fetched body when immutable headers require a replacement", async () => {
1390
+ const downstream = await fetch("data:text/plain,ok");
1391
+ expect(() => downstream.headers.set("Vary", "Accept")).toThrow();
1392
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
1393
+ headers: { Accept: "application/json" },
1394
+ }), () => downstream, { redirect: "navigation" });
1395
+ expect(response).not.toBe(downstream);
1396
+ expect(response.status).toBe(downstream.status);
1397
+ expect(response.statusText).toBe(downstream.statusText);
1398
+ expect(response.headers.get("Content-Type")).toBe("text/plain");
1399
+ expect(response.headers.get("Vary")).toBe(vary);
1400
+ expect(downstream.headers.get("Vary")).toBeNull();
1401
+ expect(await response.text()).toBe("ok");
1402
+ });
1403
+ test("varies immutable headers only on routes using URL strategy", async () => {
1404
+ for (const path of ["/about", "/api/data", "/no-url"]) {
1405
+ const downstream = Response.redirect("https://example.com/login", 303);
1406
+ const response = await runtime.paraglideMiddleware(new Request(`https://example.com${path}`, {
1407
+ headers: {
1408
+ Accept: path === "/about" ? "application/json" : "text/html",
1409
+ Cookie: "PARAGLIDE_LOCALE=fr",
1410
+ "Accept-Language": "fr",
1411
+ },
1412
+ }), () => downstream, { redirect: "navigation" });
1413
+ expect(response.status).toBe(303);
1414
+ expect(response.statusText).toBe(downstream.statusText);
1415
+ expect(response.headers.get("Location")).toBe("https://example.com/login");
1416
+ expect(response.headers.get("Vary")).toBe(path === "/about" ? vary : path === "/no-url" ? "Accept-Language" : null);
1417
+ expect(downstream.headers.get("Vary")).toBeNull();
1418
+ }
1419
+ });
1420
+ });
1421
+ test.each([
1422
+ ["/fr/about", "application/json", false, "en", 200],
1423
+ ["/fr/about", "text/html", false, "en", 200],
1424
+ ["/fr/about/", "application/json", false, "en", 200],
1425
+ ["/fr/about/", "text/html", false, "en", 307],
1426
+ ["/fr/about", "text/html", true, "fr", 200],
1427
+ ["/fr/about", "text/html", true, "en", 307],
1428
+ ])("navigation cache for %s with Accept %s, custom strategy=%s and preference=%s", async (path, accept, custom, preference, status) => {
1429
+ const runtime = await createParaglide({
1430
+ blob: await newProject({
1431
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1432
+ }),
1433
+ strategy: [
1434
+ "url",
1435
+ "cookie",
1436
+ "preferredLanguage",
1437
+ "baseLocale",
1438
+ ...(custom ? ["custom-header"] : []),
1439
+ ],
1440
+ trailingSlash: "never",
1441
+ });
1442
+ if (custom)
1443
+ runtime.defineCustomServerStrategy("custom-header", {
1444
+ getLocale: (request) => request?.headers.get("Accept-Language") ?? undefined,
1445
+ });
1446
+ for (const method of ["GET", "HEAD"]) {
1447
+ const downstream = new Response(method === "HEAD" ? null : "ok", {
1448
+ headers: {
1449
+ Vary: "Accept-Encoding",
1450
+ "Cache-Control": "public, max-age=600",
1451
+ },
1452
+ });
1453
+ const response = await runtime.paraglideMiddleware(new Request("http://internal.example.com/about", {
1454
+ method,
1455
+ headers: {
1456
+ Accept: accept,
1457
+ Cookie: "PARAGLIDE_LOCALE=en",
1458
+ "Accept-Language": preference,
1459
+ },
1460
+ }), () => downstream, {
1461
+ redirect: "navigation",
1462
+ effectiveRequestUrl: `https://example.com${path}`,
1463
+ });
1464
+ expect(response.status).toBe(status);
1465
+ expect(response.headers.get("Location")).toBe(status === 307
1466
+ ? `https://example.com${custom ? "/about" : "/fr/about"}`
1467
+ : null);
1468
+ const fields = custom
1469
+ ? "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"
1470
+ : "Accept, Sec-Fetch-Dest";
1471
+ const needsVary = custom || path.endsWith("/");
1472
+ expect(response.headers.get("Vary")).toBe(status === 307
1473
+ ? fields
1474
+ : needsVary
1475
+ ? `Accept-Encoding, ${fields}`
1476
+ : "Accept-Encoding");
1477
+ if (!needsVary)
1478
+ expect(response).toBe(downstream);
1479
+ if (status === 200) {
1480
+ expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
1481
+ expect(await response.text()).toBe(method === "HEAD" ? "" : "ok");
1482
+ }
1483
+ }
1484
+ });
1485
+ test.each(["document", "navigation"])("custom server strategies retain preference Vary in %s mode", async (redirect) => {
1486
+ const runtime = await createParaglide({
1487
+ blob: await newProject({
1488
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1489
+ }),
1490
+ strategy: ["url", "cookie", "preferredLanguage", "custom-header"],
1491
+ });
1492
+ runtime.defineCustomServerStrategy("custom-header", {
1493
+ getLocale: (request) => request?.headers.get("Accept-Language") ?? undefined,
1494
+ });
1495
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/fr/page", {
1496
+ headers: { Accept: "application/json", "Accept-Language": "en" },
1497
+ }), ({ locale }) => new Response(locale), { redirect });
1498
+ expect(await response.text()).toBe("en");
1499
+ expect(response.headers.get("Vary")).toBe(redirect === "navigation"
1500
+ ? "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"
1501
+ : "Cookie, Accept-Language");
1502
+ });
1503
+ test.each([
1504
+ "sync undefined",
1505
+ "async undefined",
1506
+ "object",
1507
+ "object with headers",
1508
+ "error",
1509
+ "consumed body",
1510
+ "locked body",
1511
+ "html",
1512
+ ].flatMap((kind) => [false, true].flatMap((experimentalMiddlewareLocaleSplitting) => ["document", "navigation"].map((redirect) => ({
1513
+ kind,
1514
+ experimentalMiddlewareLocaleSplitting,
1515
+ redirect,
1516
+ })))))("$redirect redirects handle callback returns: $kind with locale splitting=$experimentalMiddlewareLocaleSplitting", async ({ kind, experimentalMiddlewareLocaleSplitting, redirect }) => {
1517
+ const runtime = await createParaglide({
1518
+ blob: await newProject({
1519
+ settings: { baseLocale: "en", locales: ["en", "fr"] },
1520
+ }),
1521
+ strategy: ["preferredLanguage", "url"],
1522
+ experimentalMiddlewareLocaleSplitting,
1523
+ });
1524
+ let downstream;
1525
+ let reader;
1526
+ if (kind === "object")
1527
+ downstream = { handled: true };
1528
+ if (kind === "object with headers") {
1529
+ downstream = { headers: new Headers({ "Content-Type": "text/html" }) };
1530
+ }
1531
+ if (kind === "error")
1532
+ downstream = Response.error();
1533
+ if (["consumed body", "locked body", "html"].includes(kind)) {
1534
+ const response = new Response("<html><head></head><body>ok</body></html>", {
1535
+ headers: { "Content-Type": "text/html" },
1536
+ });
1537
+ if (kind === "consumed body")
1538
+ await response.text();
1539
+ if (kind === "locked body")
1540
+ reader = response.body.getReader();
1541
+ downstream = response;
1542
+ }
1543
+ try {
1544
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
1545
+ headers: { Accept: "application/json", "Accept-Language": "fr" },
1546
+ }), kind === "async undefined" ? async () => downstream : () => downstream, { redirect });
1547
+ if (kind === "html") {
1548
+ const htmlResponse = response;
1549
+ expect(htmlResponse.headers.get("Vary")).toBe(redirect === "navigation"
1550
+ ? "Accept-Language, Accept, Sec-Fetch-Dest"
1551
+ : "Accept-Language");
1552
+ const html = await htmlResponse.text();
1553
+ expect(html).toContain("<body>ok</body>");
1554
+ expect(html.includes("globalThis.__paraglide.ssr")).toBe(experimentalMiddlewareLocaleSplitting);
1555
+ }
1556
+ else
1557
+ expect(response).toBe(downstream);
1558
+ }
1559
+ finally {
1560
+ reader?.releaseLock();
1561
+ }
1562
+ });
1563
+ test("locale splitting accepts responses created before the global Response is replaced", async () => {
1564
+ const runtime = await createParaglide({
1565
+ blob: await newProject({
1566
+ settings: { baseLocale: "en", locales: ["en", "de"] },
1567
+ }),
1568
+ strategy: ["globalVariable", "baseLocale"],
1569
+ experimentalMiddlewareLocaleSplitting: true,
1570
+ });
1571
+ const NativeResponse = Response;
1572
+ const downstream = new NativeResponse("<html><head></head><body>ok</body></html>", { headers: { "Content-Type": "text/html" } });
1573
+ vi.stubGlobal("Response", class extends NativeResponse {
1574
+ });
1575
+ try {
1576
+ expect(downstream).not.toBeInstanceOf(Response);
1577
+ const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => downstream);
1578
+ const html = await response.text();
1579
+ expect(html).toContain("globalThis.__paraglide.ssr");
1580
+ expect(html).toContain("<body>ok</body>");
1581
+ }
1582
+ finally {
1583
+ vi.unstubAllGlobals();
1584
+ }
1585
+ });
839
1586
  // https://github.com/opral/inlang-paraglide-js/issues/477
840
1587
  test("does not catch errors thrown by downstream resolve call", async () => {
841
1588
  const runtime = await createParaglide({