1- import type { SSRManifest , SSRManifestI18n } from '../core/app/types.js' ;
1+ import { appendForwardSlash } from '@astrojs/internal-helpers/path' ;
2+ import type { SSRManifest } from '../core/app/types.js' ;
3+ import { shouldAppendForwardSlash } from '../core/build/util.js' ;
24import { REROUTE_DIRECTIVE_HEADER , ROUTE_TYPE_HEADER } from '../core/constants.js' ;
3- import { isRequestServerIsland , requestIs404Or500 } from '../core/routing/match.js' ;
45import type { MiddlewareHandler } from '../types/public/common.js' ;
5- import type { APIContext } from '../types/public/context.js' ;
6- import {
7- type MiddlewarePayload ,
8- normalizeTheLocale ,
9- notFound ,
10- redirectToDefaultLocale ,
11- redirectToFallback ,
12- requestHasLocale ,
13- } from './index.js' ;
6+ import { computeFallbackRoute } from './fallback.js' ;
7+ import { I18nRouter , type I18nRouterContext } from './router.js' ;
148
159export function createI18nMiddleware (
1610 i18n : SSRManifest [ 'i18n' ] ,
@@ -19,148 +13,102 @@ export function createI18nMiddleware(
1913 format : SSRManifest [ 'buildFormat' ] ,
2014) : MiddlewareHandler {
2115 if ( ! i18n ) return ( _ , next ) => next ( ) ;
22- const payload : MiddlewarePayload = {
23- ...i18n ,
24- trailingSlash,
25- base,
26- format,
27- domains : { } ,
28- } ;
29- const _redirectToDefaultLocale = redirectToDefaultLocale ( payload ) ;
30- const _noFoundForNonLocaleRoute = notFound ( payload ) ;
31- const _requestHasLocale = requestHasLocale ( payload . locales ) ;
32- const _redirectToFallback = redirectToFallback ( payload ) ;
33-
34- const prefixAlways = ( context : APIContext , response : Response ) : Response | undefined => {
35- const url = context . url ;
36- if ( url . pathname === base + '/' || url . pathname === base ) {
37- return _redirectToDefaultLocale ( context ) ;
38- }
39-
40- // Astro can't know where the default locale is supposed to be, so it returns a 404.
41- else if ( ! _requestHasLocale ( context ) ) {
42- return _noFoundForNonLocaleRoute ( context , response ) ;
43- }
44-
45- return undefined ;
46- } ;
47-
48- const prefixOtherLocales = ( context : APIContext , response : Response ) : Response | undefined => {
49- let pathnameContainsDefaultLocale = false ;
50- const url = context . url ;
51- for ( const segment of url . pathname . split ( '/' ) ) {
52- if ( normalizeTheLocale ( segment ) === normalizeTheLocale ( i18n . defaultLocale ) ) {
53- pathnameContainsDefaultLocale = true ;
54- break ;
55- }
56- }
57- if ( pathnameContainsDefaultLocale ) {
58- const newLocation = url . pathname . replace ( `/${ i18n . defaultLocale } ` , '' ) ;
59- response . headers . set ( 'Location' , newLocation ) ;
60- return _noFoundForNonLocaleRoute ( context ) ;
61- }
6216
63- return undefined ;
64- } ;
17+ // Create router once during middleware initialization
18+ const i18nRouter = new I18nRouter ( {
19+ strategy : i18n . strategy ,
20+ defaultLocale : i18n . defaultLocale ,
21+ locales : i18n . locales ,
22+ base,
23+ domains : i18n . domainLookupTable
24+ ? Object . keys ( i18n . domainLookupTable ) . reduce (
25+ ( acc , domain ) => {
26+ const locale = i18n . domainLookupTable [ domain ] ;
27+ if ( ! acc [ domain ] ) {
28+ acc [ domain ] = [ ] ;
29+ }
30+ acc [ domain ] . push ( locale ) ;
31+ return acc ;
32+ } ,
33+ { } as Record < string , string [ ] > ,
34+ )
35+ : undefined ,
36+ } ) ;
6537
6638 return async ( context , next ) => {
6739 const response = await next ( ) ;
68- const type = response . headers . get ( ROUTE_TYPE_HEADER ) ;
40+ const typeHeader = response . headers . get ( ROUTE_TYPE_HEADER ) ;
6941
7042 // This is case where we are internally rendering a 404/500, so we need to bypass checks that were done already
7143 const isReroute = response . headers . get ( REROUTE_DIRECTIVE_HEADER ) ;
7244 if ( isReroute === 'no' && typeof i18n . fallback === 'undefined' ) {
7345 return response ;
7446 }
75- // If the route we're processing is not a page, then we ignore it
76- if ( type !== 'page' && type !== 'fallback' ) {
77- return response ;
78- }
7947
80- // 404 and 500 are **known** routes (users can have their custom pages), so we need to let them be
81- if ( requestIs404Or500 ( context . request , base ) ) {
82- return response ;
83- }
84-
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 ) ) {
48+ // If the route we're processing is not a page, then we ignore it
49+ if ( typeHeader !== 'page' && typeHeader !== 'fallback' ) {
8850 return response ;
8951 }
9052
91- const { currentLocale } = context ;
92- switch ( i18n . strategy ) {
93- // NOTE: theoretically, we should never hit this code path
94- case 'manual' : {
95- return response ;
96- }
97- case 'domains-prefix-other-locales' : {
98- if ( localeHasntDomain ( i18n , currentLocale ) ) {
99- const result = prefixOtherLocales ( context , response ) ;
100- if ( result ) {
101- return result ;
102- }
53+ // Build context for router (typeHeader is guaranteed to be 'page' | 'fallback' here)
54+ const routerContext : I18nRouterContext = {
55+ currentLocale : context . currentLocale ,
56+ currentDomain : context . url . hostname ,
57+ routeType : typeHeader as 'page' | 'fallback' ,
58+ isReroute : isReroute === 'yes' ,
59+ } ;
60+
61+ // Step 1: Apply routing strategy
62+ const routeDecision = i18nRouter . match ( context . url . pathname , routerContext ) ;
63+
64+ switch ( routeDecision . type ) {
65+ case 'redirect' : {
66+ // Apply trailing slash if needed
67+ let location = routeDecision . location ;
68+ if ( shouldAppendForwardSlash ( trailingSlash , format ) ) {
69+ location = appendForwardSlash ( location ) ;
10370 }
104- break ;
71+ return context . redirect ( location , routeDecision . status ) ;
10572 }
106- case 'pathname-prefix-other-locales' : {
107- const result = prefixOtherLocales ( context , response ) ;
108- if ( result ) {
109- return result ;
73+ case 'notFound' : {
74+ const notFoundRes = new Response ( response . body , {
75+ status : 404 ,
76+ headers : response . headers ,
77+ } ) ;
78+ notFoundRes . headers . set ( REROUTE_DIRECTIVE_HEADER , 'no' ) ;
79+ if ( routeDecision . location ) {
80+ notFoundRes . headers . set ( 'Location' , routeDecision . location ) ;
11081 }
111- break ;
112- }
113-
114- case 'domains-prefix-always-no-redirect' : {
115- if ( localeHasntDomain ( i18n , currentLocale ) ) {
116- const result = _noFoundForNonLocaleRoute ( context , response ) ;
117- if ( result ) {
118- return result ;
119- }
120- }
121- break ;
122- }
123-
124- case 'pathname-prefix-always-no-redirect' : {
125- const result = _noFoundForNonLocaleRoute ( context , response ) ;
126- if ( result ) {
127- return result ;
128- }
129- break ;
82+ return notFoundRes ;
13083 }
84+ case 'continue' :
85+ break ; // Continue to fallback check
86+ }
13187
132- case 'pathname-prefix-always' : {
133- const result = prefixAlways ( context , response ) ;
134- if ( result ) {
135- return result ;
136- }
137- break ;
138- }
139- case 'domains-prefix-always' : {
140- if ( localeHasntDomain ( i18n , currentLocale ) ) {
141- const result = prefixAlways ( context , response ) ;
142- if ( result ) {
143- return result ;
144- }
145- }
146- break ;
88+ // Step 2: Apply fallback logic (if configured)
89+ if ( i18n . fallback && i18n . fallbackType ) {
90+ const fallbackDecision = computeFallbackRoute ( {
91+ pathname : context . url . pathname ,
92+ responseStatus : response . status ,
93+ currentLocale : context . currentLocale ,
94+ fallback : i18n . fallback ,
95+ fallbackType : i18n . fallbackType ,
96+ locales : i18n . locales ,
97+ defaultLocale : i18n . defaultLocale ,
98+ strategy : i18n . strategy ,
99+ base,
100+ } ) ;
101+
102+ switch ( fallbackDecision . type ) {
103+ case 'redirect' :
104+ return context . redirect ( fallbackDecision . pathname + context . url . search ) ;
105+ case 'rewrite' :
106+ return await context . rewrite ( fallbackDecision . pathname + context . url . search ) ;
107+ case 'none' :
108+ break ;
147109 }
148110 }
149111
150- return _redirectToFallback ( context , response ) ;
112+ return response ;
151113 } ;
152114}
153-
154- /**
155- * Checks if the current locale doesn't belong to a configured domain
156- * @param i18n
157- * @param currentLocale
158- */
159- function localeHasntDomain ( i18n : SSRManifestI18n , currentLocale : string | undefined ) {
160- for ( const domainLocale of Object . values ( i18n . domainLookupTable ) ) {
161- if ( domainLocale === currentLocale ) {
162- return false ;
163- }
164- }
165- return true ;
166- }
0 commit comments