Skip to content

Commit

Permalink
feat(i18n): expand fallback system
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Aug 12, 2024
1 parent 7adb350 commit 71e2a41
Show file tree
Hide file tree
Showing 21 changed files with 242 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-pens-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a bug in the rewrite logic, where Astro didn't correctly rewrite to website with `base`.
30 changes: 30 additions & 0 deletions .changeset/lazy-feet-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'astro': minor
---

Adds a new option to `i18n` called `fallbackType` that allows to control the fallback logic. The new option accepts
`"redirect"` or `"rewrite"`. The `"redirect"` option is the default value and matches the current behaviour of the fallback system.

The option `"rewrite"` uses the new [rewriting system](https://docs.astro.build/en/guides/routing/#rewrites), and it generates
the fallback pages based on the configuration.

For example, the following configuration will generate a page `/fr/index.html` that will contain the same HTML rendered by the page `/en/index.html`

```js
// astro.config.mjs
export default defineConfig({
i18n: {
locals: ["en", "fr"],
defaultLocale: "en",
routing: {
prefixDefaultLocale: true
},
fallback: {
fr: "en"
},
fallbackType: "rewrite"
}
})
```


9 changes: 9 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,15 @@ export interface AstroUserConfig {
*/
fallback?: Record<string, string>;

/**
* @docs
* @name i18n.fallback
* @type {Record<string, string>}
* @version 4.*.0
* @description
*/
fallbackType: "redirect" | "rewrite"

/**
* @docs
* @name i18n.routing
Expand Down
10 changes: 5 additions & 5 deletions packages/astro/src/container/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
SSRElement,
SSRResult,
} from '../@types/astro.js';
import { type HeadElements, Pipeline } from '../core/base-pipeline.js';
import {type HeadElements, Pipeline, type TryRewriteResult} from '../core/base-pipeline.js';
import type { SinglePageBuiltModule } from '../core/build/types.js';
import {
createModuleScriptElement,
Expand Down Expand Up @@ -71,8 +71,8 @@ export class ContainerPipeline extends Pipeline {
async tryRewrite(
payload: RewritePayload,
request: Request,
): Promise<[RouteData, ComponentInstance, URL]> {
const [foundRoute, finalUrl] = findRouteToRewrite({
): Promise<TryRewriteResult> {
const {newUrl,pathname,routeData} = findRouteToRewrite({
payload,
request,
routes: this.manifest?.routes.map((r) => r.routeData),
Expand All @@ -81,8 +81,8 @@ export class ContainerPipeline extends Pipeline {
base: this.manifest.base,
});

const componentInstance = await this.getComponentByRoute(foundRoute);
return [foundRoute, componentInstance, finalUrl];
const componentInstance = await this.getComponentByRoute(routeData);
return {componentInstance, routeData, newUrl, pathname};
}

insertRoute(route: RouteData, componentInstance: ComponentInstance): void {
Expand Down
10 changes: 5 additions & 5 deletions packages/astro/src/core/app/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
SSRElement,
SSRResult,
} from '../../@types/astro.js';
import { Pipeline } from '../base-pipeline.js';
import {Pipeline, type TryRewriteResult} from '../base-pipeline.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import { RedirectSinglePageBuiltModule } from '../redirects/component.js';
import { createModuleScriptElement, createStylesheetElementSet } from '../render/ssr-element.js';
Expand Down Expand Up @@ -94,8 +94,8 @@ export class AppPipeline extends Pipeline {
payload: RewritePayload,
request: Request,
_sourceRoute: RouteData,
): Promise<[RouteData, ComponentInstance, URL]> {
const [foundRoute, finalUrl] = findRouteToRewrite({
): Promise<TryRewriteResult> {
const { newUrl,pathname,routeData} = findRouteToRewrite({
payload,
request,
routes: this.manifest?.routes.map((r) => r.routeData),
Expand All @@ -104,8 +104,8 @@ export class AppPipeline extends Pipeline {
base: this.manifest.base,
});

const componentInstance = await this.getComponentByRoute(foundRoute);
return [foundRoute, componentInstance, finalUrl];
const componentInstance = await this.getComponentByRoute(routeData);
return {newUrl, pathname, componentInstance, routeData};
}

async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type SSRManifest = {

export type SSRManifestI18n = {
fallback: Record<string, string> | undefined;
fallbackType: "redirect" | "rewrite";
strategy: RoutingStrategies;
locales: Locales;
defaultLocale: string;
Expand Down
9 changes: 8 additions & 1 deletion packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export abstract class Pipeline {
rewritePayload: RewritePayload,
request: Request,
sourceRoute: RouteData,
): Promise<[RouteData, ComponentInstance, URL]>;
): Promise<TryRewriteResult>;

/**
* Tells the pipeline how to retrieve a component give a `RouteData`
Expand All @@ -106,3 +106,10 @@ export abstract class Pipeline {

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface HeadElements extends Pick<SSRResult, 'scripts' | 'styles' | 'links'> {}

export interface TryRewriteResult {
routeData: RouteData,
componentInstance: ComponentInstance,
newUrl: URL,
pathname: string
}
1 change: 1 addition & 0 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ function createBuildManifest(
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
fallbackType: settings.config.i18n.fallbackType,
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
defaultLocale: settings.config.i18n.defaultLocale,
locales: settings.config.i18n.locales,
Expand Down
9 changes: 5 additions & 4 deletions packages/astro/src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
import { getPagesFromVirtualModulePageName, getVirtualModulePageName } from './plugins/util.js';
import type { PageBuildData, SinglePageBuiltModule, StaticBuildOptions } from './types.js';
import { i18nHasFallback } from './util.js';
import type {TryRewriteResult} from "../base-pipeline.js";

/**
* The build pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
Expand Down Expand Up @@ -289,8 +290,8 @@ export class BuildPipeline extends Pipeline {
payload: RewritePayload,
request: Request,
_sourceRoute: RouteData,
): Promise<[RouteData, ComponentInstance, URL]> {
const [foundRoute, finalUrl] = findRouteToRewrite({
): Promise<TryRewriteResult> {
const { routeData, pathname, newUrl} = findRouteToRewrite({
payload,
request,
routes: this.options.manifest.routes,
Expand All @@ -299,8 +300,8 @@ export class BuildPipeline extends Pipeline {
base: this.config.base,
});

const componentInstance = await this.getComponentByRoute(foundRoute);
return [foundRoute, componentInstance, finalUrl];
const componentInstance = await this.getComponentByRoute(routeData);
return { routeData, componentInstance, newUrl, pathname };
}

async retrieveSsrEntry(route: RouteData, filePath: string): Promise<SinglePageBuiltModule> {
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ function buildManifest(
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
fallbackType: settings.config.i18n.fallbackType,
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
locales: settings.config.i18n.locales,
defaultLocale: settings.config.i18n.defaultLocale,
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export const AstroConfigSchema = z.object({
)
.optional(),
fallback: z.record(z.string(), z.string()).optional(),
fallbackType: z.enum(["redirect", "rewrite"]).optional().default("redirect"),
routing: z
.literal('manual')
.or(
Expand Down
14 changes: 7 additions & 7 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ export class RenderContext {
if (payload) {
pipeline.logger.debug('router', 'Called rewriting to:', payload);
// we intentionally let the error bubble up
const [routeData, component] = await pipeline.tryRewrite(
const { routeData, componentInstance: newComponent} = await pipeline.tryRewrite(
payload,
this.request,
this.originalRoute,
);
this.routeData = routeData;
componentInstance = component;
componentInstance = newComponent;
this.isRewriting = true;
this.status = 200;
}
Expand Down Expand Up @@ -223,7 +223,7 @@ export class RenderContext {

async #executeRewrite(reroutePayload: RewritePayload) {
this.pipeline.logger.debug('router', 'Calling rewrite: ', reroutePayload);
const [routeData, component, newURL] = await this.pipeline.tryRewrite(
const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite(
reroutePayload,
this.request,
this.originalRoute,
Expand All @@ -232,16 +232,16 @@ export class RenderContext {
if (reroutePayload instanceof Request) {
this.request = reroutePayload;
} else {
this.request = this.#copyRequest(newURL, this.request);
this.request = this.#copyRequest(newUrl, this.request);
}
this.url = new URL(this.request.url);
this.cookies = new AstroCookies(this.request);
this.params = getParams(routeData, this.url.pathname);
this.pathname = this.url.pathname;
this.params = getParams(routeData, pathname);
this.pathname = pathname;
this.isRewriting = true;
// we found a route and a component, we can change the status code to 200
this.status = 200;
return await this.render(component);
return await this.render(componentInstance);
}

createActionAPIContext(): ActionAPIContext {
Expand Down
47 changes: 34 additions & 13 deletions packages/astro/src/core/routing/rewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,65 @@ export type FindRouteToRewrite = {
base: AstroConfig['base'];
};

export interface FindRouteToRewriteResult {
routeData: RouteData;
newUrl: URL;
pathname: string;
}

/**
* Shared logic to retrieve the rewritten route. It returns a tuple that represents:
* 1. The new `Request` object. It contains `base`
* 2.
*/
export function findRouteToRewrite({
payload,
routes,
request,
trailingSlash,
buildFormat,
base,
}: FindRouteToRewrite): [RouteData, URL] {
let finalUrl: URL | undefined = undefined;
}: FindRouteToRewrite): FindRouteToRewriteResult {
let newUrl: URL | undefined = undefined;
if (payload instanceof URL) {
finalUrl = payload;
newUrl = payload;
} else if (payload instanceof Request) {
finalUrl = new URL(payload.url);
newUrl = new URL(payload.url);
} else {
finalUrl = new URL(payload, new URL(request.url).origin);
newUrl = new URL(payload, new URL(request.url).origin);
}
let pathname = newUrl.pathname;
if (base !== '/' && newUrl.pathname.includes(base)) {
pathname = shouldAppendForwardSlash(trailingSlash, buildFormat)
? appendForwardSlash(newUrl.pathname)
: base !== '/'
? removeTrailingForwardSlash(newUrl.pathname)
: newUrl.pathname;
if (base !== '/') {
pathname = pathname.slice(base.length);
}
}

let foundRoute;
for (const route of routes) {
const pathname = shouldAppendForwardSlash(trailingSlash, buildFormat)
? appendForwardSlash(finalUrl.pathname)
: base !== '/'
? removeTrailingForwardSlash(finalUrl.pathname)
: finalUrl.pathname;
if (route.pattern.test(decodeURI(pathname))) {
foundRoute = route;
break;
}
}

if (foundRoute) {
return [foundRoute, finalUrl];
return {
routeData: foundRoute,
newUrl,
pathname
};
} else {
const custom404 = routes.find((route) => route.route === '/404');
if (custom404) {
return [custom404, finalUrl];
return { routeData: custom404, newUrl, pathname };
} else {
return [DEFAULT_404_ROUTE, finalUrl];
return { routeData: DEFAULT_404_ROUTE, newUrl, pathname };
}
}
}
13 changes: 10 additions & 3 deletions packages/astro/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export type MiddlewarePayload = {
defaultLocale: string;
domains: Record<string, string> | undefined;
fallback: Record<string, string> | undefined;
fallbackType: NonNullable<AstroConfig['i18n']>['fallbackType']
};

// NOTE: public function exported to the users via `astro:i18n` module
Expand Down Expand Up @@ -332,16 +333,17 @@ export function notFound({ base, locales }: MiddlewarePayload) {
}

// NOTE: public function exported to the users via `astro:i18n` module
export type RedirectToFallback = (context: APIContext, response: Response) => Response;
export type RedirectToFallback = (context: APIContext, response: Response) => Promise<Response>;

export function redirectToFallback({
fallback,
locales,
defaultLocale,
strategy,
base,
fallbackType
}: MiddlewarePayload) {
return function (context: APIContext, response: Response): Response {
return async function (context: APIContext, response: Response): Promise<Response> {
if (response.status >= 300 && fallback) {
const fallbackKeys = fallback ? Object.keys(fallback) : [];
// we split the URL using the `/`, and then check in the returned array we have the locale
Expand Down Expand Up @@ -375,7 +377,12 @@ export function redirectToFallback({
} else {
newPathname = context.url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
}
return context.redirect(newPathname);

if (fallbackType === "rewrite") {
return await context.rewrite(newPathname);
} else {
return context.redirect(newPathname);
}
}
}
return response;
Expand Down
Loading

0 comments on commit 71e2a41

Please sign in to comment.