UNPKG

@inlang/paraglide-js

Version:

[![NPM Downloads](https://img.shields.io/npm/dw/%40inlang%2Fparaglide-js?logo=npm&logoColor=red&label=npm%20downloads)](https://www.npmjs.com/package/@inlang/paraglide-js) [![GitHub Issues](https://img.shields.io/github/issues-closed/opral/paraglide-js?lo

1,103 lines 43.2 kB
import { test, expect } from "vitest"; import { createParaglide } from "../create-paraglide.js"; import { newProject } from "@inlang/sdk"; test("sets the locale and origin", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de", "fr"], }, }), strategy: ["url", "globalVariable"], }); // setting the global variable to fr to assure that runtime.setLocale("fr"); // simulating multiple requests that could interfere with each other await Promise.all([ runtime.paraglideMiddleware(new Request(new URL("https://example.com/page"), { headers: { "Sec-Fetch-Dest": "document" }, }), () => { expect(runtime.getLocale()).toBe("en"); expect(runtime.getUrlOrigin()).toBe("https://example.com"); return new Response(); }), runtime.paraglideMiddleware(new Request(new URL("https://peter.com/de/page"), { headers: { "Sec-Fetch-Dest": "document" }, }), () => { expect(runtime.getLocale()).toBe("de"); expect(runtime.getUrlOrigin()).toBe("https://peter.com"); return new Response(); }), ]); // global variable is not impacted by middleware expect(runtime.getLocale()).toBe("fr"); }); test("delocalizes the url if the url strategy is used and returns the locale", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["url"], }); const request = new Request(new URL("https://example.com/de/page"), { headers: { "Sec-Fetch-Dest": "document" }, }); const result = await runtime.paraglideMiddleware(request, (args) => { expect(args.locale).toBe("de"); expect(args.request.url).toBe("https://example.com/page"); return new Response("Hello World"); }); expect(await result.text()).toBe("Hello World"); }); test("does not delocalize the url if the url strategy is not used", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["globalVariable", "baseLocale"], }); const request = new Request(new URL("https://example.com/de/page"), { headers: { "Sec-Fetch-Dest": "document" }, }); const result = await runtime.paraglideMiddleware(request, (args) => { expect(args.locale).toBe("en"); expect(args.request.url).toBe("https://example.com/de/page"); return new Response("Hello World"); }); expect(await result.text()).toBe("Hello World"); }); test("skips i18n middleware behavior for excluded routeStrategies", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url", "baseLocale"], cookieName: "PARAGLIDE_LOCALE", routeStrategies: [{ match: "/api/:path(.*)?", exclude: true }], }); const request = new Request("https://example.com/api/data", { headers: { cookie: "PARAGLIDE_LOCALE=fr", "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, ({ locale, request }) => new Response(`${locale}|${request.url}`)); expect(response.status).toBe(200); expect(await response.text()).toBe("en|https://example.com/api/data"); }); test("excluded routeStrategies keep request-scoped locale context and clone request when possible", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["globalVariable", "baseLocale"], routeStrategies: [{ match: "/api/:path(.*)?", exclude: true }], }); runtime.setLocale("fr"); const originalRequest = new Request("https://example.com/api/data", { headers: { "Sec-Fetch-Dest": "document" }, }); const response = await runtime.paraglideMiddleware(originalRequest, ({ locale, request }) => { expect(locale).toBe("en"); expect(runtime.getLocale()).toBe("en"); expect(runtime.getUrlOrigin()).toBe("https://example.com"); expect(request).not.toBe(originalRequest); expect(request.url).toBe("https://example.com/api/data"); return new Response("ok"); }); expect(await response.text()).toBe("ok"); expect(runtime.getLocale()).toBe("fr"); }); test("excluded routeStrategies fall back to original request when cloning fails", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["globalVariable", "baseLocale"], routeStrategies: [{ match: "/api/:path(.*)?", exclude: true }], }); const customRequest = { url: "https://example.com/api/data", headers: new Headers({ "Sec-Fetch-Dest": "document" }), method: "GET", }; const response = await runtime.paraglideMiddleware(customRequest, ({ locale, request }) => { expect(locale).toBe("en"); expect(request).toBe(customRequest); return new Response("ok"); }); expect(await response.text()).toBe("ok"); }); test("excluded routeStrategies preserve original request body readability", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["globalVariable", "baseLocale"], routeStrategies: [{ match: "/api/:path(.*)?", exclude: true }], }); const originalRequest = new Request("https://example.com/api/data", { method: "POST", body: "hello=world", headers: { "Content-Type": "application/x-www-form-urlencoded", "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(originalRequest, async ({ request }) => { expect(await request.text()).toBe("hello=world"); return new Response("ok"); }); expect(await response.text()).toBe("ok"); expect(await originalRequest.text()).toBe("hello=world"); }); test("routeStrategies layering is first-match-wins when exclude comes first", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url", "baseLocale"], cookieName: "PARAGLIDE_LOCALE", routeStrategies: [ { match: "/api/:path(.*)?", exclude: true }, { match: "/api/:path(.*)?", strategy: ["cookie", "baseLocale"] }, ], }); const request = new Request("https://example.com/api/data", { headers: { cookie: "PARAGLIDE_LOCALE=fr", "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, ({ locale, request }) => new Response(`${locale}|${request.url}`)); expect(response.status).toBe(200); expect(await response.text()).toBe("en|https://example.com/api/data"); }); test("routeStrategies layering is first-match-wins when strategy comes first", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url", "baseLocale"], cookieName: "PARAGLIDE_LOCALE", routeStrategies: [ { match: "/api/:path(.*)?", strategy: ["cookie", "baseLocale"] }, { match: "/api/:path(.*)?", exclude: true }, ], }); const request = new Request("https://example.com/api/data", { headers: { cookie: "PARAGLIDE_LOCALE=fr", "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, ({ locale, request }) => new Response(`${locale}|${request.url}`)); expect(response.status).toBe(200); expect(await response.text()).toBe("fr|https://example.com/api/data"); }); test("redirects to localized URL when non-URL strategy determines locale", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request to URL in en with cookie specifying French const request = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }); expect(response instanceof Response).toBe(true); // needs to be 307 status code https://github.com/opral/inlang-paraglide-js/issues/416 expect(response.status).toBe(307); // Redirect status code expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path"); }); test("call onRedirect callback when redirecting to new url", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request to URL in en with cookie specifying French const request = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); let response; await runtime.paraglideMiddleware(request, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }, { onRedirect: (res) => { response = res; }, }); expect(response instanceof Response).toBe(true); // needs to be 307 status code https://github.com/opral/inlang-paraglide-js/issues/416 expect(response.status).toBe(307); // Redirect status code expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path"); }); test("uses the provided public url for redirects behind a proxy", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); const request = new Request("http://internal.example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, () => { throw new Error("Should not reach here"); }, { effectiveRequestUrl: "https://example.com/en/some-path", }); expect(response.status).toBe(307); expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path"); }); test("uses the provided public url for callback request and origin", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["url", "baseLocale"], urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); const request = new Request("http://internal.example.com/fr/some-path", { headers: { "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, ({ request }) => { expect(runtime.getUrlOrigin()).toBe("https://example.com"); return new Response(request.url); }, { effectiveRequestUrl: "https://example.com/fr/some-path", }); expect(await response.text()).toBe("https://example.com/some-path"); }); test("sets Vary: Accept-Language header when preferredLanguage strategy is used and redirect occurs", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["preferredLanguage", "url"], urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request with Accept-Language header preferring French const request = new Request("https://example.com/en/some-path", { headers: { "Accept-Language": "fr,en;q=0.8", "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }); expect(response instanceof Response).toBe(true); expect(response.status).toBe(307); // Redirect status code expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path"); // Should have Vary header when preferredLanguage strategy is used expect(response.headers.get("Vary")).toBe("Accept-Language"); }); test("does not set Vary header when preferredLanguage strategy is not used", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request with cookie specifying French const request = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); const response = await runtime.paraglideMiddleware(request, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }); expect(response instanceof Response).toBe(true); expect(response.status).toBe(307); // Redirect status code expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path"); // Should NOT have Vary header when preferredLanguage strategy is not used expect(response.headers.get("Vary")).toBe(null); }); test("does not call onRedirect callback when there is no redirecting", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request to URL in en with cookie specifying French const request = new Request("https://example.com/fr/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); let response = null; await runtime.paraglideMiddleware(request, () => { return new Response("Hello World"); }, { onRedirect: (res) => { response = res; }, }); expect(response).toBe(null); }); test("does not redirect if URL already matches determined locale", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request to already localized URL matching cookie locale const request = new Request("https://example.com/fr/some-path", { headers: { "Sec-Fetch-Dest": "document", cookie: `PARAGLIDE_LOCALE=fr`, }, }); let middlewareResolveWasCalled = false; await runtime.paraglideMiddleware(request, () => { middlewareResolveWasCalled = true; return new Response(); }); expect(middlewareResolveWasCalled).toBe(true); // Middleware should be called since no redirect needed }); test("works with disableAsyncLocalStorage option", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de", "fr"], }, }), strategy: ["url", "globalVariable"], disableAsyncLocalStorage: true, }); // Set a global locale to verify it doesn't interfere with request processing runtime.setLocale("fr"); // Create a request with a "de" locale in the URL const request = new Request(new URL("https://example.com/de/page"), { headers: { "Sec-Fetch-Dest": "document" }, }); // Process the request with AsyncLocalStorage disabled const response = await runtime.paraglideMiddleware(request, (args) => { // Verify we still get the correct locale expect(args.locale).toBe("de"); expect(runtime.getLocale()).toBe("de"); // Verify URL is still properly delocalized expect(args.request.url).toBe("https://example.com/page"); expect(runtime.getUrlOrigin()).toBe("https://example.com"); return new Response("hello"); }); // Verify the result contains the correct data expect(await response.text()).toBe("hello"); // Verify that global variable wasn't affected by this request expect(runtime.getLocale()).toBe("fr"); }); test("works with sequential parallel requests using disableAsyncLocalStorage", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de", "fr"], }, }), strategy: ["url", "globalVariable"], }); // setting the global variable to fr to assure that runtime.setLocale("fr"); // simulating multiple requests that could interference with each other await Promise.all([ runtime.paraglideMiddleware(new Request(new URL("https://example.com/page"), { headers: { "Sec-Fetch-Dest": "document" }, }), () => { expect(runtime.getLocale()).toBe("en"); expect(runtime.getUrlOrigin()).toBe("https://example.com"); return new Response(); }), runtime.paraglideMiddleware(new Request(new URL("https://peter.com/de/page"), { headers: { "Sec-Fetch-Dest": "document" }, }), () => { expect(runtime.getLocale()).toBe("de"); expect(runtime.getUrlOrigin()).toBe("https://peter.com"); return new Response(); }), ]); // global variable is not impacted by middleware expect(runtime.getLocale()).toBe("fr"); }); // https://github.com/opral/inlang-paraglide-js/issues/442 test("falls back to next strategy when cookie contains invalid locale", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url", "baseLocale"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Request with an invalid locale in cookie const request = new Request("https://example.com/fr/some-path", { headers: { "Sec-Fetch-Dest": "document", cookie: `PARAGLIDE_LOCALE=invalid_locale`, }, }); let middlewareResolveWasCalled = false; await runtime.paraglideMiddleware(request, (args) => { middlewareResolveWasCalled = true; // Should fall back to URL strategy, which will detect 'fr' from the URL expect(args.locale).toBe("fr"); return new Response(); }); expect(middlewareResolveWasCalled).toBe(true); // Try with a request that would fall back to baseLocale const baseRequest = new Request("https://example.com/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=invalid_locale`, }, }); await runtime.paraglideMiddleware(baseRequest, (args) => { // Should fall back all the way to baseLocale since URL has no locale expect(args.locale).toBe("en"); return new Response(); }); }); // can likely be removed once the default url pattern polyfill workaround is removed // because URLPattern seems to be handling this correctly test("prevents redirect loops by normalizing URLs with trailing slashes in different scenarios", async () => { // Create two different runtimes to test different strategy configurations const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // SCENARIO 1: Basic trailing slash normalization with cookie strategy // Case 1A: Request URL has trailing slash, but localized URL might not const requestWithTrailingSlash = new Request("https://example.com/fr/page/", { headers: { "Sec-Fetch-Dest": "document", cookie: `PARAGLIDE_LOCALE=fr`, }, }); let response = await runtime.paraglideMiddleware(requestWithTrailingSlash, () => { return new Response("No redirect needed"); }); // Middleware should be called (no redirect) because normalized URLs match expect(response.status).toBe(200); expect(await response.text()).toBe("No redirect needed"); // Case 1B: Request URL has no trailing slash, but localized URL might const requestWithoutTrailingSlash = new Request("https://example.com/fr/page", { headers: { "Sec-Fetch-Dest": "document", cookie: `PARAGLIDE_LOCALE=fr`, }, }); response = await runtime.paraglideMiddleware(requestWithoutTrailingSlash, () => { return new Response("No redirect needed"); }); expect(response.status).toBe(200); // Case 1C: redirect wanted because cookie is set to fr but url is en const requestDifferentPath = new Request("https://example.com/en/different-page", { headers: { "Sec-Fetch-Dest": "document", cookie: `PARAGLIDE_LOCALE=fr`, }, }); response = await runtime.paraglideMiddleware(requestDifferentPath, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }); // Should redirect to fr version expect(response.status).toBe(307); expect(response.headers.get("Location")).toBe("https://example.com/fr/different-page"); }); // not implemented because users should disable redirects by // making another strategy preceed the url strategy // // strategy: ["cookie", "url"] test.skip("doesn't redirect if disableUrlRedirect is true", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), cookieName: "PARAGLIDE_LOCALE", strategy: ["cookie", "url"], urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Create a request that would normally be redirected const request = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); // Test with disableUrlRedirect = true let middlewareResolveWasCalled = false; await runtime.paraglideMiddleware(request, () => { middlewareResolveWasCalled = true; return new Response("No redirect"); } // { disableUrlRedirect: true } ); // Middleware should be called since redirect is disabled expect(middlewareResolveWasCalled).toBe(true); }); test("only redirects if the request.headers.get('Sec-Fetch-Dest') === 'document'", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["cookie", "url"], cookieName: "PARAGLIDE_LOCALE", urlPatterns: [ { pattern: "https://example.com/:path(.*)?", localized: [ ["en", "https://example.com/en/:path(.*)?"], ["fr", "https://example.com/fr/:path(.*)?"], ], }, ], }); // Test with Sec-Fetch-Dest = document (should redirect) const documentRequest = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, "Sec-Fetch-Dest": "document", }, }); const documentResponse = await runtime.paraglideMiddleware(documentRequest, () => { // This shouldn't be called since we should redirect throw new Error("Should not reach here"); }); // Should redirect to fr version expect(documentResponse instanceof Response).toBe(true); expect(documentResponse.status).toBe(307); expect(documentResponse.headers.get("Location")).toBe("https://example.com/fr/some-path"); // Test with Sec-Fetch-Dest = empty (should not redirect) const apiRequest = new Request("https://example.com/en/some-path", { headers: { cookie: `PARAGLIDE_LOCALE=fr`, // No Sec-Fetch-Dest header or set to something other than "document" "Sec-Fetch-Dest": "empty", }, }); let apiMiddlewareResolveWasCalled = false; await runtime.paraglideMiddleware(apiRequest, () => { apiMiddlewareResolveWasCalled = true; return new Response("API response"); }); // Middleware should be called since no redirect for API requests expect(apiMiddlewareResolveWasCalled).toBe(true); }); // https://github.com/opral/inlang-paraglide-js/issues/477 test("does not catch errors thrown by downstream resolve call", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en"], }, }), strategy: ["url"], }); await expect(() => runtime.paraglideMiddleware(new Request(new URL("https://example.com/page"), { headers: { "Sec-Fetch-Dest": "document" }, }), () => { throw new Error("Downstream error"); })).rejects.toThrow(); }); test("middleware supports async custom server strategies", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr", "de"], }, }), strategy: ["custom-database", "baseLocale"], }); // Mock an async custom strategy that simulates a database call let databaseCallCount = 0; runtime.defineCustomServerStrategy("custom-database", { getLocale: async (request) => { databaseCallCount++; // Simulate async database call delay await new Promise((resolve) => setTimeout(resolve, 10)); // Extract user ID from a custom header (simulating authentication) if (!request) return undefined; const userId = request.headers.get("X-User-ID"); if (userId === "user123") { return "fr"; } if (userId === "user456") { return "de"; } return undefined; // No user preference found }, }); // Test 1: Request with user preference const requestWithUserPref = new Request("https://example.com/page", { headers: { "X-User-ID": "user123", "Sec-Fetch-Dest": "document", }, }); let middlewareResolveWasCalled = false; await runtime.paraglideMiddleware(requestWithUserPref, (args) => { middlewareResolveWasCalled = true; expect(args.locale).toBe("fr"); // Should get locale from async custom strategy return new Response("User preference locale"); }); expect(middlewareResolveWasCalled).toBe(true); expect(databaseCallCount).toBe(1); // Test 2: Request with different user preference const requestWithOtherUser = new Request("https://example.com/page", { headers: { "X-User-ID": "user456", "Sec-Fetch-Dest": "document", }, }); await runtime.paraglideMiddleware(requestWithOtherUser, (args) => { expect(args.locale).toBe("de"); // Should get different locale return new Response("Other user preference"); }); expect(databaseCallCount).toBe(2); }); test("middleware falls back to other strategies when async custom strategy returns undefined", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr", "de"], }, }), strategy: ["custom-database", "cookie", "baseLocale"], cookieName: "PARAGLIDE_LOCALE", }); // Mock async custom strategy that returns undefined for unknown users runtime.defineCustomServerStrategy("custom-database", { getLocale: async (request) => { await new Promise((resolve) => setTimeout(resolve, 5)); if (!request) return undefined; const userId = request.headers.get("X-User-ID"); // Only return locale for known users if (userId === "known-user") { return "fr"; } return undefined; // Unknown user, fallback to other strategies }, }); // Request from unknown user with cookie fallback const request = new Request("https://example.com/page", { headers: { "X-User-ID": "unknown-user", cookie: "PARAGLIDE_LOCALE=de", "Sec-Fetch-Dest": "document", }, }); let middlewareResolveWasCalled = false; await runtime.paraglideMiddleware(request, (args) => { middlewareResolveWasCalled = true; expect(args.locale).toBe("de"); // Should fallback to cookie strategy return new Response("Fallback locale"); }); expect(middlewareResolveWasCalled).toBe(true); }); test("middleware handles async custom strategy errors gracefully", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr"], }, }), strategy: ["custom-database", "baseLocale"], }); // Mock async custom strategy that throws an error runtime.defineCustomServerStrategy("custom-database", { getLocale: async () => { await new Promise((resolve) => setTimeout(resolve, 5)); throw new Error("Database connection failed"); }, }); const request = new Request("https://example.com/page", { headers: { "Sec-Fetch-Dest": "document" }, }); // The middleware should handle the error and not crash await expect(runtime.paraglideMiddleware(request, (args) => { // If we reach here, the error was handled and fallback worked expect(args.locale).toBe("en"); // Should fallback to baseLocale return new Response("Error handled"); })).rejects.toThrow("Database connection failed"); }); // https://github.com/opral/paraglide-js/issues/573 test("falls back to original request when cloning fails (e.g., TanStack Start custom Request)", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), // Using a strategy that doesn't include "url" to trigger the clone path strategy: ["globalVariable", "baseLocale"], }); // Create a custom request-like object that cannot be cloned // This simulates TanStack Start's custom Request implementation const customRequest = { url: "https://example.com/page", headers: new Headers({ "Sec-Fetch-Dest": "document" }), method: "GET", // Missing other required properties that would cause `new Request(request)` to fail }; let middlewareResolveWasCalled = false; const response = await runtime.paraglideMiddleware(customRequest, (args) => { middlewareResolveWasCalled = true; expect(args.locale).toBe("en"); // The request should be the original custom request since cloning failed expect(args.request).toBe(customRequest); return new Response("Success"); }); expect(middlewareResolveWasCalled).toBe(true); expect(await response.text()).toBe("Success"); }); // https://github.com/opral/paraglide-js/issues/652 test("falls back to original request when cloning a rewritten request fails", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["globalVariable", "baseLocale"], }); const customRequest = { url: "http://internal.example.com/page", headers: new Headers({ "Sec-Fetch-Dest": "document" }), method: "GET", }; const NativeRequest = globalThis.Request; Object.defineProperty(globalThis, "Request", { value: class extends NativeRequest { constructor(input, init) { if ((typeof input === "string" && input === "https://example.com/page") || (input instanceof URL && input.href === "https://example.com/page")) { throw new TypeError("Simulated rewritten request clone failure"); } super(input, init); } }, configurable: true, writable: true, }); try { let middlewareResolveWasCalled = false; const response = await runtime.paraglideMiddleware(customRequest, (args) => { middlewareResolveWasCalled = true; expect(args.locale).toBe("en"); expect(runtime.getUrlOrigin()).toBe("https://example.com"); expect(args.request).toBe(customRequest); expect(args.request.url).toBe("http://internal.example.com/page"); return new Response("Success"); }, { effectiveRequestUrl: "https://example.com/page", }); expect(middlewareResolveWasCalled).toBe(true); expect(await response.text()).toBe("Success"); } finally { Object.defineProperty(globalThis, "Request", { value: NativeRequest, configurable: true, writable: true, }); } }); test("middleware works with multiple async custom strategies", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "fr", "de", "es"], }, }), strategy: ["custom-userPref", "custom-region", "baseLocale"], }); let userPrefCallCount = 0; let regionCallCount = 0; // First strategy: user preference runtime.defineCustomServerStrategy("custom-userPref", { getLocale: async (request) => { userPrefCallCount++; await new Promise((resolve) => setTimeout(resolve, 5)); if (!request) return undefined; const userId = request.headers.get("X-User-ID"); return userId === "premium-user" ? "fr" : undefined; }, }); // Second strategy: region detection runtime.defineCustomServerStrategy("custom-region", { getLocale: async (request) => { regionCallCount++; await new Promise((resolve) => setTimeout(resolve, 5)); if (!request) return undefined; const region = request.headers.get("X-Region"); return region === "europe" ? "de" : undefined; }, }); // Test 1: First strategy succeeds const premiumUserRequest = new Request("https://example.com/page", { headers: { "X-User-ID": "premium-user", "X-Region": "europe", "Sec-Fetch-Dest": "document", }, }); await runtime.paraglideMiddleware(premiumUserRequest, (args) => { expect(args.locale).toBe("fr"); // Should use first strategy result return new Response("Premium user"); }); expect(userPrefCallCount).toBe(1); // Second strategy should not be called since first one succeeded expect(regionCallCount).toBe(0); // Test 2: First strategy fails, second succeeds const regionalUserRequest = new Request("https://example.com/page", { headers: { "X-User-ID": "regular-user", "X-Region": "europe", "Sec-Fetch-Dest": "document", }, }); await runtime.paraglideMiddleware(regionalUserRequest, (args) => { expect(args.locale).toBe("de"); // Should use second strategy result return new Response("Regional user"); }); expect(userPrefCallCount).toBe(2); expect(regionCallCount).toBe(1); }); test("injected script reuses the Content-Security-Policy nonce when present and omits it otherwise", async () => { const runtime = await createParaglide({ blob: await newProject({ settings: { baseLocale: "en", locales: ["en", "de"], }, }), strategy: ["globalVariable", "baseLocale"], experimentalMiddlewareLocaleSplitting: true, }); const request = new Request(new URL("https://example.com/page"), { headers: { "Sec-Fetch-Dest": "document" }, }); // With a CSP nonce -> the injected script reuses it. const responseWithCsp = await runtime.paraglideMiddleware(request, () => { return new Response("<html><head></head><body></body></html>", { headers: { "Content-Type": "text/html", "Content-Security-Policy": "default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'", }, }); }); expect(await responseWithCsp.text()).toContain('<script nonce="abc123">'); // Without a CSP header -> the script is still injected, without a nonce. const responseWithoutCsp = await runtime.paraglideMiddleware(request, () => { return new Response("<html><head></head><body></body></html>", { headers: { "Content-Type": "text/html" }, }); }); const html = await responseWithoutCsp.text(); expect(html).toContain("globalThis.__paraglide.ssr"); expect(html).not.toContain("nonce="); }); //# sourceMappingURL=middleware.test.js.map