Skip to content

Commit 143bacf

Browse files
ematipicoflorian-lefebvremartrappElianCodessarah11918
authored
feat: experimental i18n routing (#8974)
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev> Co-authored-by: Martin Trapp <94928215+martrapp@users.noreply.github.com> Co-authored-by: Elian ☕️ <hello@elian.codes> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent c5010aa commit 143bacf

97 files changed

Lines changed: 4078 additions & 293 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/odd-mayflies-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
i18n routing

.changeset/rude-lizards-scream.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'astro': minor
3+
---
4+
5+
Experimental support for i18n routing.
6+
7+
Astro's experimental i18n routing API allows you to add your multilingual content with support for configuring a default language, computing relative page URLs, and accepting preferred languages provided by your visitor's browser. You can also specify fallback languages on a per-language basis so that your visitors can always be directed to existing content on your site.
8+
9+
Enable the experimental routing option by adding an `i18n` object to your Astro configuration with a default location and a list of all languages to support:
10+
11+
```js
12+
// astro.config.mjs
13+
import {defineConfig} from "astro/config";
14+
15+
export default defineConfig({
16+
experimental: {
17+
i18n: {
18+
defaultLocale: "en",
19+
locales: ["en", "es", "pt-br"]
20+
}
21+
}
22+
})
23+
```
24+
25+
Organize your content folders by locale depending on your `i18n.routingStrategy`, and Astro will handle generating your routes and showing your preferred URLs to your visitors.
26+
```
27+
├── src
28+
│ ├── pages
29+
│ │ ├── about.astro
30+
│ │ ├── index.astro
31+
│ │ ├── es
32+
│ │ │ ├── about.astro
33+
│ │ │ ├── index.astro
34+
│ │ ├── pt-br
35+
│ │ │ ├── about.astro
36+
│ │ │ ├── index.astro
37+
```
38+
39+
Compute relative URLs for your links with `getLocaleRelativeURL` from the new `astro:i18n` module:
40+
41+
```astro
42+
---
43+
import {getLocaleRelativeUrl} from "astro:i18n";
44+
const aboutUrl = getLocaleRelativeUrl("pt-br", "about");
45+
---
46+
<p>Learn more <a href={aboutURL}>About</a> this site!</p>
47+
```
48+
49+
Enabling i18n routing also provides two new properties for browser language detection: `Astro.preferredLocale` and `Astro.preferredLocaleList`. These combine the browser's `Accept-Langauge` header, and your site's list of supported languages and can be used to automatically respect your visitor's preferred languages.
50+
51+
Read more about Astro's [experimental i18n routing](https://docs.astro.build/en/guides/internationalization/) in our documentation.

packages/astro/client.d.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,87 @@ declare module 'astro:prefetch' {
125125
export { prefetch, PrefetchOptions } from 'astro/prefetch';
126126
}
127127

128+
declare module 'astro:i18n' {
129+
export type GetLocaleOptions = import('./dist/i18n/index.js').GetLocaleOptions;
130+
131+
/**
132+
* @param {string} locale A locale
133+
* @param {string} [path=""] An optional path to add after the `locale`.
134+
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
135+
* @return {string}
136+
*
137+
* Returns a _relative_ path with passed locale.
138+
*
139+
* ## Errors
140+
*
141+
* Throws an error if the locale doesn't exist in the list of locales defined in the configuration.
142+
*
143+
* ## Examples
144+
*
145+
* ```js
146+
* import { getLocaleRelativeUrl } from "astro:i18n";
147+
* getLocaleRelativeUrl("es"); // /es
148+
* getLocaleRelativeUrl("es", "getting-started"); // /es/getting-started
149+
* getLocaleRelativeUrl("es_US", "getting-started", { prependWith: "blog" }); // /blog/es-us/getting-started
150+
* getLocaleRelativeUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // /blog/es_US/getting-started
151+
* ```
152+
*/
153+
export const getLocaleRelativeUrl: (
154+
locale: string,
155+
path?: string,
156+
options?: GetLocaleOptions
157+
) => string;
158+
159+
/**
160+
*
161+
* @param {string} locale A locale
162+
* @param {string} [path=""] An optional path to add after the `locale`.
163+
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
164+
* @return {string}
165+
*
166+
* Returns an absolute path with the passed locale. The behaviour is subject to change based on `site` configuration.
167+
* If _not_ provided, the function will return a _relative_ URL.
168+
*
169+
* ## Errors
170+
*
171+
* Throws an error if the locale doesn't exist in the list of locales defined in the configuration.
172+
*
173+
* ## Examples
174+
*
175+
* If `site` is `https://example.com`:
176+
*
177+
* ```js
178+
* import { getLocaleAbsoluteUrl } from "astro:i18n";
179+
* getLocaleAbsoluteUrl("es"); // https://example.com/es
180+
* getLocaleAbsoluteUrl("es", "getting-started"); // https://example.com/es/getting-started
181+
* getLocaleAbsoluteUrl("es_US", "getting-started", { prependWith: "blog" }); // https://example.com/blog/es-us/getting-started
182+
* getLocaleAbsoluteUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // https://example.com/blog/es_US/getting-started
183+
* ```
184+
*/
185+
export const getLocaleAbsoluteUrl: (
186+
locale: string,
187+
path?: string,
188+
options?: GetLocaleOptions
189+
) => string;
190+
191+
/**
192+
* @param {string} [path=""] An optional path to add after the `locale`.
193+
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
194+
* @return {string[]}
195+
*
196+
* Works like `getLocaleRelativeUrl` but it emits the relative URLs for ALL locales:
197+
*/
198+
export const getLocaleRelativeUrlList: (path?: string, options?: GetLocaleOptions) => string[];
199+
/**
200+
* @param {string} [path=""] An optional path to add after the `locale`.
201+
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
202+
* @return {string[]}
203+
*
204+
* Works like `getLocaleAbsoluteUrl` but it emits the absolute URLs for ALL locales:
205+
*/
206+
export const getLocaleAbsoluteUrlList: (path?: string, options?: GetLocaleOptions) => string[];
207+
}
208+
128209
declare module 'astro:middleware' {
129210
export * from 'astro/middleware/namespace';
130211
}

packages/astro/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@
7979
},
8080
"./transitions": "./dist/transitions/index.js",
8181
"./transitions/router": "./dist/transitions/router.js",
82-
"./prefetch": "./dist/prefetch/index.js"
82+
"./prefetch": "./dist/prefetch/index.js",
83+
"./i18n": "./dist/i18n/index.js"
8384
},
8485
"imports": {
8586
"#astro/*": "./dist/*.js"

packages/astro/src/@types/astro.ts

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,93 @@ export interface AstroUserConfig {
14401440
* ```
14411441
*/
14421442
devOverlay?: boolean;
1443+
1444+
// TODO review with docs team before merging to `main`
1445+
/**
1446+
* @docs
1447+
* @name experimental.i18n
1448+
* @type {object}
1449+
* @version 3.5.0
1450+
* @type {object}
1451+
* @description
1452+
*
1453+
* Configures experimental i18n routing and allows you to specify some customization options.
1454+
*/
1455+
i18n?: {
1456+
/**
1457+
* @docs
1458+
* @name experimental.i18n.defaultLocale
1459+
* @type {string}
1460+
* @version 3.5.0
1461+
* @description
1462+
*
1463+
* The default locale of your website/application. This is a required field.
1464+
*/
1465+
defaultLocale: string;
1466+
/**
1467+
* @docs
1468+
* @name experimental.i18n.locales
1469+
* @type {string[]}
1470+
* @version 3.5.0
1471+
* @description
1472+
*
1473+
* A list of all locales supported by the website (e.g. `['en', 'es', 'pt_BR']`). This list should also include the `defaultLocale`. This is a required field.
1474+
*
1475+
* No particular language format or syntax is enforced, but your folder structure must match exactly the locales in the list.
1476+
*/
1477+
locales: string[];
1478+
1479+
/**
1480+
* @docs
1481+
* @name experimental.i18n.fallback
1482+
* @type {Record<string, string>}
1483+
* @version 3.5.0
1484+
* @description
1485+
*
1486+
* The fallback strategy when navigating to pages that do not exist (e.g. a translated page has not been created).
1487+
*
1488+
* Use this object to declare a fallback `locale` route for each language you support. If no fallback is specified, then unavailable pages will return a 404.
1489+
*
1490+
* #### Example
1491+
*
1492+
* The following example configures your content fallback strategy to redirect unavailable pages in `/pt/` to their `es` version, and unavailable pages in `/fr/` to their `en` version. Unavailable `/es/` pages will return a 404.
1493+
*
1494+
* ```js
1495+
* export defualt defineConfig({
1496+
* experimental: {
1497+
* i18n: {
1498+
* defaultLocale: "en",
1499+
* locales: ["en", "fr", "pt", "es"],
1500+
* fallback: {
1501+
* pt: "es",
1502+
* fr: "en"
1503+
* }
1504+
* }
1505+
* }
1506+
* })
1507+
* ```
1508+
*/
1509+
fallback?: Record<string, string>;
1510+
1511+
/**
1512+
* @docs
1513+
* @name experimental.i18n.routingStrategy
1514+
* @type {'prefix-always' | 'prefix-other-locales'}
1515+
* @default 'prefix-other-locales'
1516+
* @version 3.5.0
1517+
* @description
1518+
*
1519+
* Controls the routing strategy to determine your site URLs.
1520+
*
1521+
* - `prefix-other-locales`(default): Only non-default languages will display a language prefix. The `defaultLocale` will not show a language prefix.
1522+
* URLs will be of the form `example.com/[lang]/content/` for all non-default languages, but `example.com/content/` for the default locale.
1523+
* - `prefix-always`: All URLs will display a language prefix.
1524+
* URLs will be of the form `example.com/[lang]/content/` for every route, including the default language.
1525+
*
1526+
* Note: Astro requires all content to exist within a `/[lang]/` folder, even for the default language.
1527+
*/
1528+
routingStrategy: 'prefix-always' | 'prefix-other-locales';
1529+
};
14431530
};
14441531
}
14451532

@@ -1902,6 +1989,11 @@ export type AstroFeatureMap = {
19021989
* The adapter can emit static assets
19031990
*/
19041991
assets?: AstroAssetsFeature;
1992+
1993+
/**
1994+
* List of features that orbit around the i18n routing
1995+
*/
1996+
i18n?: AstroInternationalizationFeature;
19051997
};
19061998

19071999
export interface AstroAssetsFeature {
@@ -1916,6 +2008,13 @@ export interface AstroAssetsFeature {
19162008
isSquooshCompatible?: boolean;
19172009
}
19182010

2011+
export interface AstroInternationalizationFeature {
2012+
/**
2013+
* Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
2014+
*/
2015+
detectBrowserLanguage?: SupportsKind;
2016+
}
2017+
19192018
export interface AstroAdapter {
19202019
name: string;
19212020
serverEntrypoint?: string;
@@ -1973,6 +2072,17 @@ interface AstroSharedContext<
19732072
* Object accessed via Astro middleware
19742073
*/
19752074
locals: App.Locals;
2075+
2076+
/**
2077+
* The current locale that is computed from the `Accept-Language` header of the browser (**SSR Only**).
2078+
*/
2079+
preferredLocale: string | undefined;
2080+
2081+
/**
2082+
* The list of locales computed from the `Accept-Language` header of the browser, sorted by quality value (**SSR Only**).
2083+
*/
2084+
2085+
preferredLocaleList: string[] | undefined;
19762086
}
19772087

19782088
export interface APIContext<
@@ -2074,6 +2184,34 @@ export interface APIContext<
20742184
*/
20752185
locals: App.Locals;
20762186
ResponseWithEncoding: typeof ResponseWithEncoding;
2187+
2188+
/**
2189+
* Available only when `experimental.i18n` enabled and in SSR.
2190+
*
2191+
* It represents the preferred locale of the user. It's computed by checking the supported locales in `i18n.locales`
2192+
* and locales supported by the users's browser via the header `Accept-Language`
2193+
*
2194+
* For example, given `i18n.locales` equals to `['fr', 'de']`, and the `Accept-Language` value equals to `en, de;q=0.2, fr;q=0.6`, the
2195+
* `Astro.preferredLanguage` will be `fr` because `en` is not supported, its [quality value] is the highest.
2196+
*
2197+
* [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
2198+
*/
2199+
preferredLocale: string | undefined;
2200+
2201+
/**
2202+
* Available only when `experimental.i18n` enabled and in SSR.
2203+
*
2204+
* It represents the list of the preferred locales that are supported by the application. The list is sorted via [quality value].
2205+
*
2206+
* For example, given `i18n.locales` equals to `['fr', 'pt', 'de']`, and the `Accept-Language` value equals to `en, de;q=0.2, fr;q=0.6`, the
2207+
* `Astro.preferredLocaleList` will be equal to `['fs', 'de']` because `en` isn't supported, and `pt` isn't part of the locales contained in the
2208+
* header.
2209+
*
2210+
* When the `Accept-Header` is `*`, the original `i18n.locales` are returned. The value `*` means no preferences, so Astro returns all the supported locales.
2211+
*
2212+
* [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
2213+
*/
2214+
preferredLocaleList: string[] | undefined;
20772215
}
20782216

20792217
export type EndpointOutput =
@@ -2216,7 +2354,13 @@ export interface AstroPluginOptions {
22162354
logger: Logger;
22172355
}
22182356

2219-
export type RouteType = 'page' | 'endpoint' | 'redirect';
2357+
/**
2358+
* - page: a route that lives in the file system, usually an Astro component
2359+
* - endpoint: a route that lives in the file system, usually a JS file that exposes endpoints methods
2360+
* - redirect: a route points to another route that lives in the file system
2361+
* - fallback: a route that doesn't exist in the file system that needs to be handled with other means, usually the middleware
2362+
*/
2363+
export type RouteType = 'page' | 'endpoint' | 'redirect' | 'fallback';
22202364

22212365
export interface RoutePart {
22222366
content: string;

0 commit comments

Comments
 (0)