/* eslint-disable */ import * as runtime from "./runtime.js"; /** * Server middleware that handles locale-based routing and request processing. * * Configure `disableAsyncLocalStorage` when generating Paraglide with * `paraglideVitePlugin()` or `compile()`, not when calling * `paraglideMiddleware()`. Keep AsyncLocalStorage enabled by default and * only disable it for runtimes that lack `AsyncLocalStorage` support and * guarantee request isolation. * * This middleware performs several key functions: * * 1. Determines the locale for the incoming request using configured strategies * 2. Handles URL localization and redirects (only for document requests) * 3. Maintains locale state using AsyncLocalStorage to prevent request interference * * When URL strategy is used: * * - The locale is extracted from the URL for all request types * - If URL doesn't match the determined locale, redirects to localized URL (only for document requests) * - De-localizes URLs before passing to server (e.g., `/fr/about` → `/about`) * * @see https://inlang.com/m/gerre34r/library-inlang-paraglideJs/middleware * * @template T - The return type of the resolve function * * @param {Request} request - The incoming request object * @param {(args: { request: Request, locale: import("./runtime.js").Locale }) => T | Promise} resolve - Function to handle the request. The callback receives: * - `request`: A modified request with a delocalized URL when the URL strategy is used (e.g., `/fr/about` → `/about`). * If your framework handles URL localization itself (e.g., TanStack Router's `rewrite` option), use the original * request instead to avoid redirect loops. * - `locale`: The determined locale for this request. * @param {{ onRedirect:(response: Response) => void }} [callbacks] - Callbacks to handle events from middleware * @returns {Promise} * * @example * ```typescript * // Basic usage in metaframeworks like NextJS, SvelteKit, Astro, Nuxt, etc. * export const handle = async ({ event, resolve }) => { * return paraglideMiddleware(event.request, ({ request, locale }) => { * // let the framework further resolve the request * return resolve(request); * }); * }; * ``` * * @example * ```typescript * // Usage in a framework like Express JS or Hono * app.use(async (req, res, next) => { * const result = await paraglideMiddleware(req, ({ request, locale }) => { * // If a redirect happens this won't be called * return next(request); * }); * }); * ``` * * @example * ```typescript * // Usage with frameworks that handle URL localization/delocalization themselves * // * // Some frameworks like TanStack Router handle URL localization and delocalization * // themselves via their own rewrite APIs (e.g., `rewrite.input`/`rewrite.output`). * // * // When the framework handles this, the middleware's URL delocalization is not needed. * // Using the modified `request` from the callback would cause a redirect loop because * // both the middleware and the framework would attempt to delocalize the URL. * // * // Solution: Pass the original request to the handler instead of the modified one. * // The middleware still handles locale detection, cookies, and AsyncLocalStorage context. * // * // ❌ WRONG - causes redirect loop when framework handles URL rewriting: * // paraglideMiddleware(req, ({ request }) => handler.fetch(request)) * // * // ✅ CORRECT - use original request when framework handles URL localization: * // paraglideMiddleware(req, () => handler.fetch(req)) * * * * * export default { * fetch(req: Request): Promise { * // TanStack Router handles URL rewriting via deLocalizeUrl/localizeUrl * // so we pass the original `req` instead of the modified `request` * return paraglideMiddleware(req, () => handler.fetch(req)) * }, * } * ``` */ export async function paraglideMiddleware(request, resolve, callbacks) { if (!runtime.disableAsyncLocalStorage && !runtime.serverAsyncLocalStorage) { const { AsyncLocalStorage } = await import("async_hooks"); runtime.overwriteServerAsyncLocalStorage(new AsyncLocalStorage()); } else if (!runtime.serverAsyncLocalStorage) { runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage()); } if (runtime.isExcludedByRouteStrategy(request.url)) { const locale = runtime.baseLocale; const origin = new URL(request.url).origin; const newRequest = cloneRequestWithFallback(request); /** @type {Set} */ const messageCalls = new Set(); return /** @type {Response} */ (await runtime.serverAsyncLocalStorage?.run({ locale, origin, messageCalls }, () => resolve({ locale, request: newRequest }))); } const strategy = runtime.getStrategyForUrl(request.url); const decision = await runtime.shouldRedirect({ request }); const locale = decision.locale; const origin = new URL(request.url).origin; // if the client makes a request to a URL that doesn't match // the localizedUrl, redirect the client to the localized URL if (request.headers.get("Sec-Fetch-Dest") === "document" && decision.shouldRedirect && decision.redirectUrl) { // Create headers object with Vary header if preferredLanguage strategy is used /** @type {Record} */ const headers = {}; if (strategy.includes("preferredLanguage")) { headers["Vary"] = "Accept-Language"; } const response = new Response(null, { status: 307, headers: { Location: decision.redirectUrl.href, ...headers, }, }); callbacks?.onRedirect(response); return response; } // If the strategy includes "url", we need to de-localize the URL // before passing it to the server middleware. // // The middleware is responsible for mapping a localized URL to the // de-localized URL e.g. `/en/about` to `/about`. Otherwise, // the server can't render the correct page. let newRequest; if (strategy.includes("url")) { newRequest = new Request(runtime.deLocalizeUrl(request.url), request); } else { newRequest = cloneRequestWithFallback(request); } // the message functions that have been called in this request /** @type {Set} */ const messageCalls = new Set(); const response = await runtime.serverAsyncLocalStorage?.run({ locale, origin, messageCalls }, () => resolve({ locale, request: newRequest })); // Only modify HTML responses if (runtime.experimentalMiddlewareLocaleSplitting && response.headers.get("Content-Type")?.includes("html")) { const body = await response.text(); const messages = []; // using .values() to avoid polyfilling in older projects. else the following error is thrown // Type 'Set' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher. for (const messageCall of Array.from(messageCalls)) { const [id, locale] = /** @type {[string, import("./runtime.js").Locale]} */ (messageCall.split(":")); messages.push(`${id}: ${compiledBundles[id]?.[locale]}`); } // Prevent translated content from terminating the inline script tag. const escapedMessages = messages.join(",").replace(/<\/(script)/gi, "<\\/$1"); const script = ``; // Insert the script before the closing head tag const newBody = body.replace("", `${script}`); // Create a new response with the modified body // Clone all headers except Content-Length which will be set automatically const newHeaders = new Headers(response.headers); newHeaders.delete("Content-Length"); // Let the browser calculate the correct length return new Response(newBody, { status: response.status, statusText: response.statusText, headers: newHeaders, }); } return response; } /** * Some metaframeworks (NextJS) require a new Request object. * https://github.com/opral/inlang-paraglide-js/issues/411 * * However, some frameworks (TanStack Start 1.143+) use custom Request * implementations that cannot be cloned with `new Request(request)`. * https://github.com/opral/paraglide-js/issues/573 * * @param {Request} request * @returns {Request} */ function cloneRequestWithFallback(request) { try { // Clone first so building a new Request does not consume the original body stream. return new Request(request.clone()); } catch { try { return new Request(request); } catch { return request; } } } /** * Creates a mock AsyncLocalStorage implementation for environments where * native AsyncLocalStorage is not available or disabled. * * This mock implementation mimics the behavior of the native AsyncLocalStorage * but doesn't require the async_hooks module. It's used as a fallback when * the runtime does not expose AsyncLocalStorage or when it has been disabled. * * @returns {import("./runtime.js").ParaglideAsyncLocalStorage} */ function createMockAsyncLocalStorage() { /** @type {any} */ let currentStore = undefined; return { getStore() { return currentStore; }, async run(store, callback) { currentStore = store; try { return await callback(); } finally { currentStore = undefined; } }, }; } // Used in generated server.js when async local storage is disabled. void createMockAsyncLocalStorage; /** * The compiled messages for the server middleware. * * Only populated if `enableMiddlewareOptimizations` is set to `true`. * * @type {Record>} */ const compiledBundles = {};