Skip to content

Commit

Permalink
feat(i18n): fallback logic of defaultLocale for i18n routing (#8899)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Oct 30, 2023
1 parent 01c73f0 commit 0529ac1
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 88 deletions.
2 changes: 1 addition & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type SSRManifest = {
};

export type SSRManifestI18n = {
fallback?: Record<string, string[]>;
fallback?: Record<string, string>;
fallbackControl?: 'none' | 'redirect' | 'render';
locales: string[];
defaultLocale: string;
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AllPagesData } from './types.js';

import * as colors from 'kleur/colors';
import { debug } from '../logger/core.js';
import { eachPageFromAllPages } from './internal.js';

export interface CollectPagesDataOptions {
settings: AstroSettings;
Expand Down
20 changes: 9 additions & 11 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export const AstroConfigSchema = z.object({
.object({
defaultLocale: z.string(),
locales: z.string().array(),
fallback: z.record(z.string(), z.string().array()).optional(),
fallback: z.record(z.string(), z.string()).optional(),
detectBrowserLanguage: z.boolean().optional().default(false),
// TODO: properly add default when the feature goes of experimental
fallbackControl: z.enum(['none', 'redirect', 'render']).optional(),
Expand All @@ -320,21 +320,19 @@ export const AstroConfigSchema = z.object({
});
}
if (fallback) {
for (const [fallbackKey, fallbackArray] of Object.entries(fallback)) {
if (!locales.includes(fallbackKey)) {
for (const [fallbackFrom, fallbackTo] of Object.entries(fallback)) {
if (!locales.includes(fallbackFrom)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The locale \`${fallbackKey}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
message: `The locale \`${fallbackFrom}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
});
}

for (const fallbackArrayKey of fallbackArray) {
if (!locales.includes(fallbackArrayKey)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The locale \`${fallbackArrayKey}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
});
}
if (!locales.includes(fallbackTo)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The locale \`${fallbackTo}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
});
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/astro/src/core/render/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
location: redirectRouteGenerate(renderContext.route, renderContext.params),
},
});
// TODO: check this one
} else if (routeIsFallback(renderContext.route)) {
return new Response(null);
// We return a 404 because fallback routes don't exist.
// It's responsibility of the middleware to catch them and re-route the requests
return new Response(null, {
status: 404,
});
} else if (!mod) {
throw new AstroError(CantRenderPage);
}
Expand Down
136 changes: 98 additions & 38 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,50 +484,110 @@ export function createRouteManifest(
// Didn't find a good place, insert last
routes.push(routeData);
});
const i18n = settings.config.experimental.i18n;

if (i18n && i18n.fallback && i18n.fallbackControl === 'redirect') {
let fallback = Object.entries(i18n.fallback);

// A map like: locale => RouteData[]
const routesByLocale = new Map<string, RouteData[]>();
// We create a set, so we can remove the routes that have been added to the previous map
const setRoutes = new Set(routes);

// First loop
// We loop over the locales minus the default locale and add only the routes that contain `/<locale>`.
for (const locale of i18n.locales.filter((loc) => loc !== i18n.defaultLocale)) {
for (const route of setRoutes) {
if (!route.route.includes(`/${locale}`)) {
continue;
}
const currentRoutes = routesByLocale.get(locale);
if (currentRoutes) {
currentRoutes.push(route);
routesByLocale.set(locale, currentRoutes);
} else {
routesByLocale.set(locale, [route]);
}
setRoutes.delete(route);
}
}

// we loop over the remaining routes and add them to the default locale
for (const route of setRoutes) {
const currentRoutes = routesByLocale.get(i18n.defaultLocale);
if (currentRoutes) {
currentRoutes.push(route);
routesByLocale.set(i18n.defaultLocale, currentRoutes);
} else {
routesByLocale.set(i18n.defaultLocale, [route]);
}
setRoutes.delete(route);
}

if (settings.config.experimental.i18n && settings.config.experimental.i18n.fallback) {
let fallback = Object.entries(settings.config.experimental.i18n.fallback);
if (fallback.length > 0) {
for (const [fallbackLocale, fallbackLocaleList] of fallback) {
for (const fallbackLocaleEntry of fallbackLocaleList) {
const fallbackToRoutes = routes.filter((r) =>
r.component.includes(`/${fallbackLocaleEntry}`)
);
const fallbackFromRoutes = routes.filter((r) =>
r.component.includes(`/${fallbackLocale}`)
);

for (const fallbackToRoute of fallbackToRoutes) {
const hasRoute = fallbackFromRoutes.some((r) =>
r.component.replace(`/${fallbackLocaleEntry}`, `/${fallbackLocale}`)
);

if (!hasRoute) {
const pathname = fallbackToRoute.pathname?.replace(
`/${fallbackLocaleEntry}`,
`/${fallbackLocale}`
for (const [fallbackFromLocale, fallbackToLocale] of fallback) {
let fallbackToRoutes;
if (fallbackToLocale === i18n.defaultLocale) {
fallbackToRoutes = routesByLocale.get(i18n.defaultLocale);
} else {
fallbackToRoutes = routesByLocale.get(fallbackToLocale);
}
const fallbackFromRoutes = routesByLocale.get(fallbackFromLocale);

// Technically, we should always have a fallback to. Added this to make TS happy.
if (!fallbackToRoutes) {
continue;
}

for (const fallbackToRoute of fallbackToRoutes) {
const hasRoute =
fallbackFromRoutes &&
// we check if the fallback from locale (the origin) has already this route
fallbackFromRoutes.some((route) => {
if (fallbackToLocale === i18n.defaultLocale) {
return route.route.replace(`/${fallbackFromLocale}`, '') === fallbackToRoute.route;
} else {
return (
route.route.replace(`/${fallbackToLocale}`, `/${fallbackFromLocale}`) ===
fallbackToRoute.route
);
}
});

if (!hasRoute) {
let pathname: string | undefined;
let route: string;
if (fallbackToLocale === i18n.defaultLocale) {
if (fallbackToRoute.pathname) {
pathname = `/${fallbackFromLocale}${fallbackToRoute.pathname}`;
}
route = `/${fallbackFromLocale}${fallbackToRoute.route}`;
} else {
pathname = fallbackToRoute.pathname?.replace(
`/${fallbackToLocale}`,
`/${fallbackFromLocale}`
);
const route = fallbackToRoute.route?.replace(
`/${fallbackLocaleEntry}`,
`/${fallbackLocale}`
route = fallbackToRoute.route.replace(
`/${fallbackToLocale}`,
`/${fallbackFromLocale}`
);
}

const segments = removeLeadingForwardSlash(route)
.split(path.posix.sep)
.filter(Boolean)
.map((s: string) => {
validateSegment(s);
return getParts(s, route);
});
routes.push({
...fallbackToRoute,
pathname,
route,
segments,
pattern: getPattern(segments, config),
type: 'fallback',
const segments = removeLeadingForwardSlash(route)
.split(path.posix.sep)
.filter(Boolean)
.map((s: string) => {
validateSegment(s);
return getParts(s, route);
});
}
routes.push({
...fallbackToRoute,
pathname,
route,
segments,
pattern: getPattern(segments, config),
type: 'fallback',
});
}
}
}
Expand Down
44 changes: 32 additions & 12 deletions packages/astro/src/i18n/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MiddlewareEndpointHandler } from '../@types/astro.js';
import type { SSRManifest } from '../@types/astro.js';
import type { Environment } from '../core/render/index.js';

export function createI18nMiddleware(
i18n: SSRManifest['i18n']
Expand All @@ -10,23 +11,42 @@ export function createI18nMiddleware(
const locales = i18n.locales;

return async (context, next) => {
if (!i18n.fallback) {
return next();
if (!i18n.fallback || !i18n.fallback) {
return await next();
}

const response = await next();
if (i18n.fallbackControl === 'redirect' && response instanceof Response) {
const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : [];
const url = context.url;

const url = context.url;
if (response instanceof Response) {
const separators = url.pathname.split('/');
const fallbackWithRedirect = i18n.fallbackControl === 'redirect';
if (fallbackWithRedirect && url.pathname.includes(`/${i18n.defaultLocale}`)) {
const content = await response.text();
const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, '');
response.headers.set('Location', newLocation);
return new Response(content, {
status: 302,
headers: response.headers,
});
}
if (fallbackWithRedirect && response.status >= 300) {
const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : [];

const urlLocale = separators.find((s) => locales.includes(s));

const urlLocale = separators.find((s) => locales.includes(s));
if (urlLocale && fallbackKeys.includes(urlLocale)) {
const fallbackLocale = i18n.fallback[urlLocale];
let newPathname: string;
// If a locale falls back to the default locale, we want to **remove** the locale because
// the default locale doesn't have a prefix
if (fallbackLocale === i18n.defaultLocale) {
newPathname = url.pathname.replace(`/${urlLocale}`, ``);
} else {
newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
}

if (urlLocale && fallbackKeys.includes(urlLocale)) {
// TODO: correctly handle chain of fallback
const fallbackLocale = i18n.fallback[urlLocale][0];
const newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
return context.redirect(newPathname);
return context.redirect(newPathname);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export default defineConfig({
'en', 'pt', 'it'
],
fallback: {
"it": ["en"]
"it": "en",
pt: "en"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Astro</title>
</head>
<body>
End
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<title>Astro</title>
</head>
<body>
Hola
Oi essa e start
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<title>Astro</title>
</head>
<body>
Hello
Start
</body>
</html>
Loading

0 comments on commit 0529ac1

Please sign in to comment.