Skip to content

Commit 0fa5c82

Browse files
ematipicoascorbic
andauthored
fix(i18n): server island request (#13112)
Co-authored-by: ascorbic <[email protected]>
1 parent 3a26e45 commit 0fa5c82

File tree

13 files changed

+126
-11
lines changed

13 files changed

+126
-11
lines changed

.changeset/silent-worms-whisper.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes a bug where the i18n middleware was blocking a server island request when the `prefixDefaultLocale` option is set to `true`

packages/astro/src/core/render-context.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ import { callMiddleware } from './middleware/callMiddleware.js';
2929
import { sequence } from './middleware/index.js';
3030
import { renderRedirect } from './redirects/render.js';
3131
import { type Pipeline, Slots, getParams, getProps } from './render/index.js';
32-
import { isRoute404or500 } from './routing/match.js';
32+
import { isRoute404or500, isRouteServerIsland } from './routing/match.js';
3333
import { copyRequest, getOriginPathname, setOriginPathname } from './routing/rewrite.js';
34-
import { SERVER_ISLAND_COMPONENT } from './server-islands/endpoint.js';
3534
import { AstroSession } from './session.js';
3635

3736
export const apiContextRoutesSymbol = Symbol.for('context.routes');
@@ -596,7 +595,7 @@ export class RenderContext {
596595
}
597596

598597
let computedLocale;
599-
if (routeData.component === SERVER_ISLAND_COMPONENT) {
598+
if (isRouteServerIsland(routeData)) {
600599
let referer = this.request.headers.get('referer');
601600
if (referer) {
602601
if (URL.canParse(referer)) {

packages/astro/src/core/routing/match.ts

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RoutesList } from '../../types/astro.js';
22
import type { RouteData } from '../../types/public/internal.js';
3+
import { SERVER_ISLAND_BASE_PREFIX, SERVER_ISLAND_COMPONENT } from '../server-islands/endpoint.js';
34

45
/** Find matching route from pathname */
56
export function matchRoute(pathname: string, manifest: RoutesList): RouteData | undefined {
@@ -37,3 +38,41 @@ export function isRoute500(route: string) {
3738
export function isRoute404or500(route: RouteData): boolean {
3839
return isRoute404(route.route) || isRoute500(route.route);
3940
}
41+
42+
/**
43+
* Determines if a given route is associated with the server island component.
44+
*
45+
* @param {RouteData} route - The route data object to evaluate.
46+
* @return {boolean} Returns true if the route's component is the server island component, otherwise false.
47+
*/
48+
export function isRouteServerIsland(route: RouteData): boolean {
49+
return route.component === SERVER_ISLAND_COMPONENT;
50+
}
51+
52+
/**
53+
* Determines whether the given `Request` is targeted to a "server island" based on its URL.
54+
*
55+
* @param {Request} request - The request object to be evaluated.
56+
* @param {string} [base=''] - The base path provided via configuration.
57+
* @return {boolean} - Returns `true` if the request is for a server island, otherwise `false`.
58+
*/
59+
export function isRequestServerIsland(request: Request, base = ''): boolean {
60+
const url = new URL(request.url);
61+
const pathname = url.pathname.slice(base.length);
62+
63+
return pathname.startsWith(SERVER_ISLAND_BASE_PREFIX);
64+
}
65+
66+
/**
67+
* Checks if the given request corresponds to a 404 or 500 route based on the specified base path.
68+
*
69+
* @param {Request} request - The HTTP request object to be checked.
70+
* @param {string} [base=''] - The base path to trim from the request's URL before checking the route. Default is an empty string.
71+
* @return {boolean} Returns true if the request matches a 404 or 500 route; otherwise, returns false.
72+
*/
73+
export function requestIs404Or500(request: Request, base = '') {
74+
const url = new URL(request.url);
75+
const pathname = url.pathname.slice(base.length);
76+
77+
return isRoute404(pathname) || isRoute500(pathname);
78+
}

packages/astro/src/core/server-islands/endpoint.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getPattern } from '../routing/manifest/pattern.js';
1313

1414
export const SERVER_ISLAND_ROUTE = '/_server-islands/[name]';
1515
export const SERVER_ISLAND_COMPONENT = '_server-islands.astro';
16+
export const SERVER_ISLAND_BASE_PREFIX = '_server-islands';
1617

1718
type ConfigFields = Pick<SSRManifest, 'base' | 'trailingSlash'>;
1819

packages/astro/src/i18n/index.ts

-7
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@ export function requestHasLocale(locales: Locales) {
1616
};
1717
}
1818

19-
export function requestIs404Or500(request: Request, base = '') {
20-
const url = new URL(request.url);
21-
const pathname = url.pathname.slice(base.length);
22-
23-
return isRoute404(pathname) || isRoute500(pathname);
24-
}
25-
2619
// Checks if the pathname has any locale
2720
export function pathHasLocale(path: string, locales: Locales): boolean {
2821
const segments = path.split('/');

packages/astro/src/i18n/middleware.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
redirectToDefaultLocale,
1010
redirectToFallback,
1111
requestHasLocale,
12-
requestIs404Or500,
1312
} from './index.js';
13+
import { isRequestServerIsland, requestIs404Or500 } from '../core/routing/match.js';
1414

1515
export function createI18nMiddleware(
1616
i18n: SSRManifest['i18n'],
@@ -82,6 +82,12 @@ export function createI18nMiddleware(
8282
return response;
8383
}
8484

85+
// This is a case where the rendering phase belongs to a server island. Server island are
86+
// special routes, and should be exhempt from i18n routing
87+
if (isRequestServerIsland(context.request, base)) {
88+
return response;
89+
}
90+
8591
const { currentLocale } = context;
8692
switch (i18n.strategy) {
8793
// NOTE: theoretically, we should never hit this code path
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from "astro/config";
2+
3+
export default defineConfig({
4+
i18n: {
5+
defaultLocale: 'en',
6+
locales: [
7+
'en', 'pt', 'it', {
8+
path: "spanish",
9+
codes: ["es", "es-ar"]
10+
}
11+
],
12+
routing: {
13+
prefixDefaultLocale: true
14+
}
15+
},
16+
base: "/new-site"
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@test/i18n-server-island",
3+
"version": "0.0.0",
4+
"private": true,
5+
"dependencies": {
6+
"astro": "workspace:*"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>I am a server island</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
import Island from "../../components/Island.astro"
3+
---
4+
5+
<Island server:defer />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head>
3+
<title>Astro</title>
4+
</head>
5+
<body>
6+
I am index
7+
</body>
8+
</html>

packages/astro/test/i18n-routing.test.js

+27
Original file line numberDiff line numberDiff line change
@@ -2168,3 +2168,30 @@ describe('Fallback rewrite SSR', () => {
21682168
assert.match(text, /Hello world/);
21692169
});
21702170
});
2171+
2172+
2173+
describe("i18n routing with server islands", () => {
2174+
/** @type {import('./test-utils').Fixture} */
2175+
let fixture;
2176+
/** @type {import('./test-utils').DevServer} */
2177+
let devServer;
2178+
2179+
before(async () => {
2180+
fixture = await loadFixture({
2181+
root: './fixtures/i18n-server-island/',
2182+
});
2183+
devServer = await fixture.startDevServer();
2184+
});
2185+
2186+
after(async () => {
2187+
await devServer.stop();
2188+
})
2189+
2190+
it('should render the en locale with server island', async () => {
2191+
const res = await fixture.fetch('/en/island');
2192+
const html = await res.text();
2193+
const $ = cheerio.load(html);
2194+
const serverIslandScript = $('script[data-island-id]');
2195+
assert.equal(serverIslandScript.length, 1, 'has the island script');
2196+
});
2197+
})

pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)