astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
166 lines (165 loc) • 6.35 kB
JavaScript
import { appendForwardSlash } from "@astrojs/internal-helpers/path";
import { computeFallbackRoute } from "../../i18n/fallback.js";
import { I18nRouter } from "../../i18n/router.js";
import { markFeatureUsed, FetchFeatures } from "../fetch/features.js";
import { shouldAppendForwardSlash } from "../build/util.js";
import { createManifestMemo } from "../manifest/memo.js";
/**
* Pure compile from explicit values — used by `astro:i18n`'s manual-mode
* middleware wrapper (`src/i18n/middleware.ts`), which receives the values as
* arguments rather than reading a manifest.
*/
export function compileI18n(i18n, base, trailingSlash, format) {
const router = new I18nRouter({
strategy: i18n.strategy,
defaultLocale: i18n.defaultLocale,
locales: i18n.locales,
base,
domains: i18n.domainLookupTable ? Object.keys(i18n.domainLookupTable).reduce((acc, domain) => {
const locale = i18n.domainLookupTable[domain];
if (!acc[domain]) {
acc[domain] = [];
}
acc[domain].push(locale);
return acc;
}, {}) : undefined
});
return {
config: i18n,
base,
trailingSlash,
format,
router
};
}
// `null` sentinel means "i18n not configured OR strategy is manual" — for the
// manual strategy users wire `astro:i18n.middleware(...)` into their own
// `onRequest` instead. No HMR invalidation needed: i18n config is static per
// manifest object (a dev manifest re-evaluation creates a new object and the
// memo follows).
const i18nMemo = createManifestMemo((manifest) => {
const config = manifest.i18n;
return config && config.strategy !== "manual" ? compileI18n(config, manifest.base, manifest.trailingSlash, manifest.buildFormat) : null;
});
/**
* The compiled i18n post-processor for a manifest, or `null` when i18n is
* unset or the routing strategy is `manual`.
*/
export function getI18n(manifest) {
return i18nMemo.get(manifest);
}
/**
* Post-processes a rendered `Response` against the compiled i18n
* configuration, as an explicit step in `handleRequest` after the middleware
* chain returns. The manual-strategy public API (`astro:i18n.middleware(...)`)
* wraps this in a middleware-shaped closure.
*/
export async function finalizeI18n(compiled, state, response) {
markFeatureUsed(state.manifest, FetchFeatures.i18n);
const i18n = compiled.config;
// This is a case where we are internally rendering a 404/500, so we
// need to bypass checks that were done already.
if (state.skipErrorReroute && typeof i18n.fallback === "undefined") {
return response;
}
// If the route we're processing is not a page, then we ignore it
if (state.responseRouteType !== "page" && state.responseRouteType !== "fallback") {
return response;
}
// Use Astro's already-decoded URL (`state.url`) instead of reading the
// raw request URL again, so locale checks use the same path as routing.
const url = state.url;
const currentLocale = state.computeCurrentLocale();
const routeData = state.routeData;
const isPrerendered = routeData.prerender;
// Build context for router (responseRouteType is guaranteed to be 'page' | 'fallback' here)
const routerContext = {
currentLocale,
currentDomain: url.hostname,
routeType: state.responseRouteType,
isReroute: false,
// Routes injected by integrations live in a URL space the site author
// can't move under a locale prefix (#18223). Injected routes whose first
// segment is dynamic (e.g. `/[locale]/...`) are treated as locale-aware
// and still have their locale validated.
allowUnprefixedPath: routeData.origin === "external" && !routeData.segments?.[0]?.some((part) => part.dynamic)
};
// Step 1: Apply routing strategy
const routeDecision = compiled.router.match(url.pathname, routerContext);
switch (routeDecision.type) {
case "redirect": {
// Apply trailing slash if needed
let location = routeDecision.location;
if (shouldAppendForwardSlash(compiled.trailingSlash, compiled.format)) {
location = appendForwardSlash(location);
}
return new Response(null, {
status: routeDecision.status ?? 302,
headers: { Location: location }
});
}
case "notFound": {
if (isPrerendered) {
// Prerendered pages are authored content — preserve the body so the
// build pipeline can write the file. The REROUTE_DIRECTIVE prevents
// the App from rerouting to the error page.
const prerenderedRes = new Response(response.body, {
status: 404,
headers: response.headers
});
state.skipErrorReroute = true;
if (routeDecision.location) {
prerenderedRes.headers.set("Location", routeDecision.location);
}
return prerenderedRes;
}
// For SSR, return a null-body 404 so the App reroutes to the actual
// 404 page. This prevents dynamic routes like [locale] from serving
// their content for invalid locale paths.
const headers = new Headers();
if (routeDecision.location) {
headers.set("Location", routeDecision.location);
}
return new Response(null, {
status: 404,
headers
});
}
case "continue": break;
}
// Step 2: Apply fallback logic (if configured)
if (i18n.fallback && i18n.fallbackType) {
// The fallback sentinel (X-Astro-Route-Type: fallback, status 500) signals
// that the render pipeline couldn't find this page in the current locale.
// Treat it as a 404 so computeFallbackRoute will apply fallback logic.
const effectiveStatus = state.responseRouteType === "fallback" ? 404 : response.status;
const fallbackDecision = computeFallbackRoute({
pathname: url.pathname,
responseStatus: effectiveStatus,
currentLocale,
fallback: i18n.fallback,
fallbackType: i18n.fallbackType,
locales: i18n.locales,
defaultLocale: i18n.defaultLocale,
strategy: i18n.strategy,
base: compiled.base
});
switch (fallbackDecision.type) {
case "redirect": return new Response(null, {
status: 302,
headers: { Location: fallbackDecision.pathname + url.search }
});
case "rewrite": try {
return await state.rewrite(fallbackDecision.pathname + url.search);
} catch {
// The rewrite target has no renderable page either (e.g. a
// prerendered dynamic route where the slug is excluded from
// getStaticPaths in the fallback locale too). Fall through
// so the original 404 response is returned. See #17778.
break;
}
case "none": break;
}
}
return response;
}