@inlang/paraglide-js
Version:
[](https://www.npmjs.com/package/@inlang/paraglide-js) [ => {
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