Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(i18n): disable redirect to default language #9638

Merged
merged 26 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f44a51
feat(i18n): disable redirect
ematipico Jan 4, 2024
6aff285
feat(i18n): add option to disable redirect to default language
ematipico Jan 8, 2024
3165346
chore: add schema validation
ematipico Jan 8, 2024
eefdf81
docs
ematipico Jan 8, 2024
6130aed
changeset
ematipico Jan 8, 2024
312ec61
Update packages/astro/src/core/config/schema.ts
ematipico Jan 11, 2024
12e139c
chore: address feedback
ematipico Jan 11, 2024
5a864a1
fix test
ematipico Jan 11, 2024
3bd5d53
Update .changeset/cyan-grapes-suffer.md
ematipico Jan 12, 2024
81f9d9f
Update packages/astro/src/@types/astro.ts
ematipico Jan 12, 2024
fad30d5
Fix discord fetch code (#9663)
bluwy Jan 11, 2024
4e5f64f
Force re-execution of Partytown's head snippet on view transitions (#…
martrapp Jan 11, 2024
c24746e
[ci] format
martrapp Jan 11, 2024
92cca7e
fix(assets): Implement all hooks in the passthrough image service (#9…
Princesseuh Jan 11, 2024
3e408a3
refactor(toolbar): Rename every internal reference of overlay/plugins…
Princesseuh Jan 11, 2024
9bb779d
Disable file watcher for internal one-off vite servers (#9665)
bluwy Jan 12, 2024
9b1f251
Use node:test and node:assert/strict (#9649)
bluwy Jan 12, 2024
5291484
[ci] format
bluwy Jan 12, 2024
22152bc
fix(i18n): emit an error when the index isn't found (#9678)
ematipico Jan 12, 2024
8574140
feat(i18n): add option to disable redirect to default language
ematipico Jan 8, 2024
e3255cb
chore: rebase
ematipico Jan 12, 2024
a5fd580
Merge remote-tracking branch 'origin/main' into feat/redirect-default…
ematipico Jan 16, 2024
668fb66
Update packages/astro/src/@types/astro.ts
ematipico Jan 16, 2024
92c83ed
lock file update
ematipico Jan 16, 2024
e159b10
Merge remote-tracking branch 'origin/main' into feat/redirect-default…
ematipico Jan 17, 2024
598a1ae
Merge branch 'main' into feat/redirect-default-language
ematipico Jan 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/cyan-grapes-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"astro": minor
---


Adds a new `i18n.routing` config option `redirectToDefaultLocale` to disable automatic redirects of the root URL (`/`) to the default locale when `prefixDefaultLocale: true` is set.

In projects where every route, including the default locale, is prefixed with `/[locale]/` path, this property allows you to control whether or not `src/pages/index.astro` should automatically redirect your site visitors from `/` to `/[defaultLocale]`.
ematipico marked this conversation as resolved.
Show resolved Hide resolved

You can now opt out of this automatic redirection by setting `redirectToDefaultLocale: false`:

```js
// astro.config.mjs
export default defineConfig({
i18n:{
defaultLocale: "en",
locales: ["en", "fr"],
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: false
}
}
})
```
29 changes: 29 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,35 @@ export interface AstroUserConfig {
*/
prefixDefaultLocale: boolean;

/**
* @docs
* @name i18n.routing.redirectToDefaultLocale
* @kind h4
* @type {boolean}
* @default `true`
* @version 4.2.0
* @description
*
* Configures whether or not the home URL (`/`) generated by `src/pages/index.astro`
* will redirect to `/[defaultLocale]` when `prefixDefaultLocale: true` is set.
*
* Set `redirectToDefaultLocale: false` to disable this automatic redirection at the root of your site:
* ```js
* // astro.config.mjs
* export default defineConfig({
* i18n:{
* defaultLocale: "en",
* locales: ["en", "fr"],
* routing: {
* prefixDefaultLocale: true,
* redirectToDefaultLocale: false
* }
* }
* })
*```
* */
redirectToDefaultLocale: boolean;

/**
* @name i18n.routing.strategy
* @type {"pathname"}
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
SSRResult,
} from '../../@types/astro.js';
import type { SinglePageBuiltModule } from '../build/types.js';
import type { RoutingStrategies } from '../config/schema.js';

export type ComponentPath = string;

Expand Down Expand Up @@ -56,7 +57,7 @@ export type SSRManifest = {

export type SSRManifestI18n = {
fallback?: Record<string, string>;
routing?: 'prefix-always' | 'prefix-other-locales';
routing?: RoutingStrategies;
locales: Locales;
defaultLocale: string;
};
Expand Down
23 changes: 20 additions & 3 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ const ASTRO_CONFIG_DEFAULTS = {
},
} satisfies AstroUserConfig & { server: { open: boolean } };

type RoutingStrategies = 'prefix-always' | 'prefix-other-locales';
export type RoutingStrategies =
| 'pathname-prefix-always'
| 'pathname-prefix-other-locales'
| 'pathname-prefix-always-no-redirect';

export const AstroConfigSchema = z.object({
root: z
Expand Down Expand Up @@ -329,17 +332,31 @@ export const AstroConfigSchema = z.object({
routing: z
.object({
prefixDefaultLocale: z.boolean().default(false),
redirectToDefaultLocale: z.boolean().default(true),
strategy: z.enum(['pathname']).default('pathname'),
})
.default({})
.refine(
({ prefixDefaultLocale, redirectToDefaultLocale }) => {
return !(prefixDefaultLocale === false && redirectToDefaultLocale === false);
},
{
message:
'The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`.',
}
)
.transform((routing) => {
let strategy: RoutingStrategies;
switch (routing.strategy) {
case 'pathname': {
if (routing.prefixDefaultLocale === true) {
strategy = 'prefix-always';
if (routing.redirectToDefaultLocale) {
strategy = 'pathname-prefix-always';
} else {
strategy = 'pathname-prefix-always-no-redirect';
}
} else {
strategy = 'prefix-other-locales';
strategy = 'pathname-prefix-other-locales';
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
computePreferredLocaleList,
} from '../render/context.js';
import { type Environment, type RenderContext } from '../render/index.js';
import type { RoutingStrategies } from '../config/schema.js';

const clientAddressSymbol = Symbol.for('astro.clientAddress');
const clientLocalsSymbol = Symbol.for('astro.locals');
Expand All @@ -27,7 +28,7 @@ type CreateAPIContext = {
props: Record<string, any>;
adapterName?: string;
locales: Locales | undefined;
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
routingStrategy: RoutingStrategies | undefined;
defaultLocale: string | undefined;
};

Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/core/render/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { normalizeTheLocale, toCodes } from '../../i18n/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Environment } from './environment.js';
import { getParamsAndProps } from './params-and-props.js';
import type { RoutingStrategies } from '../config/schema.js';

const clientLocalsSymbol = Symbol.for('astro.locals');

Expand All @@ -31,7 +32,7 @@ export interface RenderContext {
locals?: object;
locales: Locales | undefined;
defaultLocale: string | undefined;
routing: 'prefix-always' | 'prefix-other-locales' | undefined;
routing: RoutingStrategies | undefined;
}

export type CreateRenderContextArgs = Partial<
Expand Down Expand Up @@ -239,7 +240,7 @@ export function computePreferredLocaleList(request: Request, locales: Locales):
export function computeCurrentLocale(
request: Request,
locales: Locales,
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined,
routingStrategy: RoutingStrategies | undefined,
defaultLocale: string | undefined
): undefined | string {
const requestUrl = new URL(request.url);
Expand All @@ -256,7 +257,7 @@ export function computeCurrentLocale(
}
}
}
if (routingStrategy === 'prefix-other-locales') {
if (routingStrategy === 'pathname-prefix-other-locales') {
return defaultLocale;
}
return undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/render/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
computePreferredLocale,
computePreferredLocaleList,
} from './context.js';
import type { RoutingStrategies } from '../config/schema.js';

const clientAddressSymbol = Symbol.for('astro.clientAddress');
const responseSentSymbol = Symbol.for('astro.responseSent');
Expand Down Expand Up @@ -53,7 +54,7 @@ export interface CreateResultArgs {
cookies?: AstroCookies;
locales: Locales | undefined;
defaultLocale: string | undefined;
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
routingStrategy: RoutingStrategies | undefined;
}

function getFunctionExpression(slot: any) {
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export function createRouteManifest(
const i18n = settings.config.i18n;
if (i18n) {
// First we check if the user doesn't have an index page.
if (i18n.routing === 'prefix-always') {
if (i18n.routing === 'pathname-prefix-always') {
let index = routes.find((route) => route.route === '/');
if (!index) {
let relativePath = path.relative(
Expand Down Expand Up @@ -583,7 +583,7 @@ export function createRouteManifest(

// Work done, now we start creating "fallback" routes based on the configuration

if (i18n.routing === 'prefix-always') {
if (i18n.routing === 'pathname-prefix-always') {
// we attempt to retrieve the index page of the default locale
const defaultLocaleRoutes = routesByLocale.get(i18n.defaultLocale);
if (defaultLocaleRoutes) {
Expand Down Expand Up @@ -656,7 +656,7 @@ export function createRouteManifest(
let route: string;
if (
fallbackToLocale === i18n.defaultLocale &&
i18n.routing === 'prefix-other-locales'
i18n.routing === 'pathname-prefix-other-locales'
) {
if (fallbackToRoute.pathname) {
pathname = `/${fallbackFromLocale}${fallbackToRoute.pathname}`;
Expand Down
13 changes: 7 additions & 6 deletions packages/astro/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import type { AstroConfig, Locales } from '../@types/astro.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';
import { MissingLocale } from '../core/errors/errors-data.js';
import { AstroError } from '../core/errors/index.js';
import type { RoutingStrategies } from '../core/config/schema.js';

type GetLocaleRelativeUrl = GetLocaleOptions & {
locale: string;
base: string;
locales: Locales;
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
routing?: 'prefix-always' | 'prefix-other-locales';
routing?: RoutingStrategies;
defaultLocale: string;
};

Expand Down Expand Up @@ -45,7 +46,7 @@ export function getLocaleRelativeUrl({
path,
prependWith,
normalizeLocale = true,
routing = 'prefix-other-locales',
routing = 'pathname-prefix-other-locales',
defaultLocale,
}: GetLocaleRelativeUrl) {
const codeToUse = peekCodePathToUse(_locales, locale);
Expand All @@ -57,7 +58,7 @@ export function getLocaleRelativeUrl({
}
const pathsToJoin = [base, prependWith];
const normalizedLocale = normalizeLocale ? normalizeTheLocale(codeToUse) : codeToUse;
if (routing === 'prefix-always') {
if (routing === 'pathname-prefix-always') {
pathsToJoin.push(normalizedLocale);
} else if (locale !== defaultLocale) {
pathsToJoin.push(normalizedLocale);
Expand Down Expand Up @@ -88,7 +89,7 @@ type GetLocalesBaseUrl = GetLocaleOptions & {
locales: Locales;
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
routing?: 'prefix-always' | 'prefix-other-locales';
routing?: RoutingStrategies;
defaultLocale: string;
};

Expand All @@ -100,15 +101,15 @@ export function getLocaleRelativeUrlList({
path,
prependWith,
normalizeLocale = false,
routing = 'prefix-other-locales',
routing = 'pathname-prefix-other-locales',
defaultLocale,
}: GetLocalesBaseUrl) {
const locales = toPaths(_locales);
return locales.map((locale) => {
const pathsToJoin = [base, prependWith];
const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;

if (routing === 'prefix-always') {
if (routing === 'pathname-prefix-always') {
pathsToJoin.push(normalizedLocale);
} else if (locale !== defaultLocale) {
pathsToJoin.push(normalizedLocale);
Expand Down
63 changes: 43 additions & 20 deletions packages/astro/src/i18n/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
import type { Locales, MiddlewareHandler, RouteData, SSRManifest } from '../@types/astro.js';
import type { PipelineHookFunction } from '../core/pipeline.js';
import { getPathByLocale, normalizeTheLocale } from './index.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';

const routeDataSymbol = Symbol.for('astro.routeData');

Expand Down Expand Up @@ -54,30 +55,52 @@ export function createI18nMiddleware(

if (response instanceof Response) {
const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`);
if (i18n.routing === 'prefix-other-locales' && pathnameContainsDefaultLocale) {
const newLocation = url.pathname.replace(`/${defaultLocale}`, '');
response.headers.set('Location', newLocation);
return new Response(null, {
status: 404,
headers: response.headers,
});
} else if (i18n.routing === 'prefix-always') {
if (url.pathname === base + '/' || url.pathname === base) {
if (trailingSlash === 'always') {
return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
} else {
return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
switch (i18n.routing) {
case 'pathname-prefix-other-locales': {
if (pathnameContainsDefaultLocale) {
const newLocation = url.pathname.replace(`/${defaultLocale}`, '');
response.headers.set('Location', newLocation);
return new Response(null, {
status: 404,
headers: response.headers,
});
}
break;
}

case 'pathname-prefix-always-no-redirect': {
// We return a 404 if:
// - the current path isn't a root. e.g. / or /<base>
// - the URL doesn't contain a locale
const isRoot = url.pathname === base + '/' || url.pathname === base;
if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) {
return new Response(null, {
status: 404,
headers: response.headers,
});
}
break;
}

// Astro can't know where the default locale is supposed to be, so it returns a 404 with no content.
else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
return new Response(null, {
status: 404,
headers: response.headers,
});
case 'pathname-prefix-always': {
if (url.pathname === base + '/' || url.pathname === base) {
if (trailingSlash === 'always') {
Comment on lines +85 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated: It looks like the prefix is only added when trailingSlash is set to always. Shouldn't the prefix also be added for other trailingSlash configurations?

Copy link
Member Author

@ematipico ematipico Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, although to check the other variants, we need build.format, which is a piece of information that we don't have in SSR (in the SSR manifest), that's why I only check always. Should we store build.format in the SSRManifest?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok that makes sense. I think it's fine to store build.format then if it helps improve things here. We can definitely improve this in a later PR.

return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
} else {
return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
}
}

// Astro can't know where the default locale is supposed to be, so it returns a 404 with no content.
else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
return new Response(null, {
status: 404,
headers: response.headers,
});
}
}
}

if (response.status >= 300 && fallback) {
const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : [];

Expand All @@ -103,7 +126,7 @@ export function createI18nMiddleware(
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 (pathFallbackLocale === defaultLocale && routing === 'prefix-other-locales') {
if (pathFallbackLocale === defaultLocale && routing === 'pathname-prefix-other-locales') {
newPathname = url.pathname.replace(`/${urlLocale}`, ``);
} else {
newPathname = url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Astro</title>
</head>
<body>
I am index
</body>
</html>
Loading
Loading