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): manual routing #10193

Merged
merged 39 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
53c5764
feat(i18n): manual routing
ematipico Feb 20, 2024
0a2f112
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Feb 22, 2024
659a6c0
one more function
ematipico Feb 22, 2024
247eafd
different typing
ematipico Feb 22, 2024
9e95b0f
tests
ematipico Feb 27, 2024
5f0e19c
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Mar 1, 2024
2eb86db
fix merge
ematipico Mar 4, 2024
1fd976f
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Mar 4, 2024
b65f2d0
throw error for missing middleware
ematipico Mar 4, 2024
5e7097c
rename function
ematipico Mar 4, 2024
0da463a
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Mar 4, 2024
3d14524
fix conflicts
ematipico Mar 4, 2024
c5e02dc
lock file update
ematipico Mar 4, 2024
c6fcdc4
fix options, error thrown and added tests
ematipico Mar 21, 2024
7849d78
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Mar 25, 2024
65c216b
rebase
ematipico Mar 25, 2024
7bed201
add tests
ematipico Mar 25, 2024
51bcf56
docs
ematipico Mar 25, 2024
fc24ba4
lock file black magic
ematipico Mar 25, 2024
e43e04a
increase timeout?
ematipico Mar 26, 2024
e87d968
fix regression
ematipico Mar 26, 2024
62d4293
Merge remote-tracking branch 'origin/main' into feat/i18n-manual-routing
ematipico Mar 27, 2024
a377295
merge conflict
ematipico Mar 27, 2024
4057715
add changeset
ematipico Mar 27, 2024
bda256c
chore: apply suggestions
ematipico Mar 29, 2024
d3a8d18
apply suggestion
ematipico Apr 2, 2024
4d9f6a7
Update .changeset/little-hornets-give.md
ematipico Apr 8, 2024
ba18514
chore: address feedback
ematipico Apr 8, 2024
e43236a
fix regression of last commit
ematipico Apr 8, 2024
742c627
update name
ematipico Apr 8, 2024
e75b46c
add comments
ematipico Apr 8, 2024
6c4aa8d
fix regression
ematipico Apr 8, 2024
fe0c01a
remove unused code
ematipico Apr 8, 2024
dbcee6e
Apply suggestions from code review
ematipico Apr 9, 2024
d746753
chore: update reference
ematipico Apr 9, 2024
1516cbd
Update packages/astro/src/@types/astro.ts
ematipico Apr 9, 2024
bbb3787
chore: improve types
ematipico Apr 9, 2024
52fcbe0
fix regression in tests
ematipico Apr 10, 2024
e781cb2
apply Sarah's suggestion
ematipico Apr 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/little-hornets-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
"astro": minor
---

Adds a new i18n routing option `manual` to allow you to write your own i18n middleware:

```js
import { defineConfig } from "astro/config"
// astro.config.mjs
export default defineConfig({
i18n: {
locales: ["en", "fr"],
defaultLocale: "fr",
routing: "manual"
}
})
```

ematipico marked this conversation as resolved.
Show resolved Hide resolved
Adding `routing: "manual"` to your i18n config disables Astro's own i18n middleware and provides you with helper functions to write your own: `redirectToDefaultLocale`, `notFound`, and `redirectToFallback`:

```js
// middleware.js
import { redirectToDefaultLocale } from "astro:i18n";
export const onRequest = defineMiddleware(async (context, next) => {
if (context.url.startsWith("/about")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

great example

return next()
} else {
return redirectToDefaultLocale(context, 302);
}
})
```

Also adds a `middleware` function that manually creates Astro's i18n middleware. This allows you to extend Astro's i18n routing instead of completely replacing it. Run `middleware` in combination with your own middleware, using the `sequence` utility to determine the order:

```js title="src/middleware.js"
import {defineMiddleware, sequence} from "astro:middleware";
import { middleware } from "astro:i18n"; // Astro's own i18n routing config

export const userMiddleware = defineMiddleware();

export const onRequest = sequence(
userMiddleware,
middleware({
redirectToDefaultLocale: false,
prefixDefaultLocale: true
})
)
```
124 changes: 69 additions & 55 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1497,66 +1497,80 @@ export interface AstroUserConfig {
*
* Controls the routing strategy to determine your site URLs. Set this based on your folder/URL path configuration for your default language.
*/
routing?: {
// prettier-ignore
routing?:
/**
* @docs
* @name i18n.routing.prefixDefaultLocale
* @kind h4
* @type {boolean}
* @default `false`
* @version 3.7.0
* @description
*
* When `false`, only non-default languages will display a language prefix.
* The `defaultLocale` will not show a language prefix and content files do not exist in a localized folder.
* URLs will be of the form `example.com/[locale]/content/` for all non-default languages, but `example.com/content/` for the default locale.
*
* When `true`, all URLs will display a language prefix.
* URLs will be of the form `example.com/[locale]/content/` for every route, including the default language.
* Localized folders are used for every language, including the default.
*/
prefixDefaultLocale?: boolean;

/**
* @docs
* @name i18n.routing.redirectToDefaultLocale
* @kind h4
* @type {boolean}
* @default `true`
* @version 4.2.0
* @name i18n.routing.manual
* @type {string}
* @version 4.6.0
* @description
* When this option is enabled, Astro will **disable** its i18n middleware so that you can implement your own custom logic. No other `routing` options (e.g. `prefixDefaultLocale`) may be configured with `routing: "manual"`.
*
* 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"}
* @default `"pathname"`
* @version 3.7.0
* @description
*
* - `"pathname": The strategy is applied to the pathname of the URLs
* You will be responsible for writing your own routing logic, or executing Astro's i18n middleware manually alongside your own.
*/
strategy?: 'pathname';
};
'manual'
| {
/**
* @docs
* @name i18n.routing.prefixDefaultLocale
* @kind h4
* @type {boolean}
* @default `false`
* @version 3.7.0
* @description
*
* When `false`, only non-default languages will display a language prefix.
* The `defaultLocale` will not show a language prefix and content files do not exist in a localized folder.
* URLs will be of the form `example.com/[locale]/content/` for all non-default languages, but `example.com/content/` for the default locale.
*
* When `true`, all URLs will display a language prefix.
* URLs will be of the form `example.com/[locale]/content/` for every route, including the default language.
* Localized folders are used for every language, including the default.
*/
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"}
* @default `"pathname"`
* @version 3.7.0
* @description
*
* - `"pathname": The strategy is applied to the pathname of the URLs
*/
strategy?: 'pathname';
};
ematipico marked this conversation as resolved.
Show resolved Hide resolved

/**
* @name i18n.domains
Expand Down Expand Up @@ -1592,7 +1606,7 @@ export interface AstroUserConfig {
* })
* ```
*
* Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
* Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/reference/api-reference/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
*
* See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains) for more details, including the limitations of this feature.
*/
Expand Down
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 @@ -67,7 +67,7 @@ export type SSRManifest = {
};

export type SSRManifestI18n = {
fallback?: Record<string, string>;
fallback: Record<string, string> | undefined;
strategy: RoutingStrategies;
locales: Locales;
defaultLocale: string;
Expand Down
10 changes: 7 additions & 3 deletions packages/astro/src/core/base-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ export abstract class Pipeline {
*/
readonly site = manifest.site ? new URL(manifest.site) : undefined
) {
this.internalMiddleware = [
createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat),
];
this.internalMiddleware = [];
// We do use our middleware only if the user isn't using the manual setup
if (i18n?.strategy !== 'manual') {
lilnasy marked this conversation as resolved.
Show resolved Hide resolved
this.internalMiddleware.push(
createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat)
);
}
}

abstract headElements(routeData: RouteData): Promise<HeadElements> | HeadElements;
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ function createBuildManifest(
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
strategy: toRoutingStrategy(settings.config.i18n),
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
defaultLocale: settings.config.i18n.defaultLocale,
locales: settings.config.i18n.locales,
domainLookupTable: {},
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function buildManifest(
if (settings.config.i18n) {
i18nManifest = {
fallback: settings.config.i18n.fallback,
strategy: toRoutingStrategy(settings.config.i18n),
strategy: toRoutingStrategy(settings.config.i18n.routing, settings.config.i18n.domains),
locales: settings.config.i18n.locales,
defaultLocale: settings.config.i18n.defaultLocale,
domainLookupTable,
Expand Down
34 changes: 19 additions & 15 deletions packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,21 +386,25 @@ export const AstroConfigSchema = z.object({
.optional(),
fallback: z.record(z.string(), z.string()).optional(),
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`.',
}
),
.literal('manual')
.or(
z
.object({
prefixDefaultLocale: z.boolean().optional().default(false),
redirectToDefaultLocale: z.boolean().optional().default(true),
})
.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`.',
}
)
)
.optional()
.default({}),
})
.optional()
.superRefine((i18n, ctx) => {
Expand Down
29 changes: 28 additions & 1 deletion packages/astro/src/core/errors/errors-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,21 @@ export const MissingIndexForInternationalization = {
hint: (src: string) => `Create an index page (\`index.astro, index.md, etc.\`) in \`${src}\`.`,
} satisfies ErrorData;

/**
* @docs
* @description
* Some internationalization functions are only available when Astro's own i18n routing is disabled by the configuration setting `i18n.routing: "manual"`.
*
* @see
* - [`i18n` routing](https://docs.astro.build/en/guides/internationalization/#routing)
*/
export const IncorrectStrategyForI18n = {
name: 'IncorrectStrategyForI18n',
title: "You can't use the current function with the current strategy",
message: (functionName: string) =>
`The function \`${functionName}\' can only be used when the \`i18n.routing.strategy\` is set to \`"manual"\`.`,
} satisfies ErrorData;

/**
* @docs
* @description
Expand All @@ -1076,7 +1091,19 @@ export const NoPrerenderedRoutesWithDomains = {
name: 'NoPrerenderedRoutesWithDomains',
title: "Prerendered routes aren't supported when internationalization domains are enabled.",
message: (component: string) =>
`Static pages aren't yet supported with multiple domains. If you wish to enable this feature, you have to disable prerendering for the page ${component}`,
`Static pages aren't yet supported with multiple domains. To enable this feature, you must disable prerendering for the page ${component}`,
} satisfies ErrorData;

/**
* @docs
* @description
* Astro throws an error if the user enables manual routing, but it doesn't have a middleware file.
*/
export const MissingMiddlewareForInternationalization = {
name: 'MissingMiddlewareForInternationalization',
title: 'Enabled manual internationalization routing without having a middleware.',
message:
"Your configuration setting `i18n.routing: 'manual'` requires you to provide your own i18n `middleware` file.",
} satisfies ErrorData;

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/astro/src/core/middleware/vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { addRollupInput } from '../build/add-rollup-input.js';
import type { BuildInternals } from '../build/internal.js';
import type { StaticBuildOptions } from '../build/types.js';
import { MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js';
import { MissingMiddlewareForInternationalization } from '../errors/errors-data.js';
import { AstroError } from '../errors/index.js';

export const MIDDLEWARE_MODULE_ID = '\0astro-internal:middleware';
const NOOP_MIDDLEWARE = '\0noop-middleware';
Expand Down Expand Up @@ -44,8 +46,14 @@ export function vitePluginMiddleware({ settings }: { settings: AstroSettings }):
},
async load(id) {
if (id === NOOP_MIDDLEWARE) {
if (!userMiddlewareIsPresent && settings.config.i18n?.routing === 'manual') {
throw new AstroError(MissingMiddlewareForInternationalization);
}
return 'export const onRequest = (_, next) => next()';
} else if (id === MIDDLEWARE_MODULE_ID) {
if (!userMiddlewareIsPresent && settings.config.i18n?.routing === 'manual') {
throw new AstroError(MissingMiddlewareForInternationalization);
}
// In the build, tell Vite to emit this file
if (isCommandBuild) {
this.emitFile({
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/routing/manifest/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ export function createRouteManifest(

const i18n = settings.config.i18n;
if (i18n) {
const strategy = toRoutingStrategy(i18n);
const strategy = toRoutingStrategy(i18n.routing, i18n.domains);
// First we check if the user doesn't have an index page.
if (strategy === 'pathname-prefix-always') {
let index = routes.find((route) => route.route === '/');
Expand Down