mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
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>
This commit is contained in:
parent
c5010aad34
commit
143bacf396
97 changed files with 4079 additions and 294 deletions
5
.changeset/odd-mayflies-dress.md
Normal file
5
.changeset/odd-mayflies-dress.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
i18n routing
|
51
.changeset/rude-lizards-scream.md
Normal file
51
.changeset/rude-lizards-scream.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Experimental support for i18n routing.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
// astro.config.mjs
|
||||
import {defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "es", "pt-br"]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
||||
├── src
|
||||
│ ├── pages
|
||||
│ │ ├── about.astro
|
||||
│ │ ├── index.astro
|
||||
│ │ ├── es
|
||||
│ │ │ ├── about.astro
|
||||
│ │ │ ├── index.astro
|
||||
│ │ ├── pt-br
|
||||
│ │ │ ├── about.astro
|
||||
│ │ │ ├── index.astro
|
||||
```
|
||||
|
||||
Compute relative URLs for your links with `getLocaleRelativeURL` from the new `astro:i18n` module:
|
||||
|
||||
```astro
|
||||
---
|
||||
import {getLocaleRelativeUrl} from "astro:i18n";
|
||||
const aboutUrl = getLocaleRelativeUrl("pt-br", "about");
|
||||
---
|
||||
<p>Learn more <a href={aboutURL}>About</a> this site!</p>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Read more about Astro's [experimental i18n routing](https://docs.astro.build/en/guides/internationalization/) in our documentation.
|
81
packages/astro/client.d.ts
vendored
81
packages/astro/client.d.ts
vendored
|
@ -125,6 +125,87 @@ declare module 'astro:prefetch' {
|
|||
export { prefetch, PrefetchOptions } from 'astro/prefetch';
|
||||
}
|
||||
|
||||
declare module 'astro:i18n' {
|
||||
export type GetLocaleOptions = import('./dist/i18n/index.js').GetLocaleOptions;
|
||||
|
||||
/**
|
||||
* @param {string} locale A locale
|
||||
* @param {string} [path=""] An optional path to add after the `locale`.
|
||||
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
|
||||
* @return {string}
|
||||
*
|
||||
* Returns a _relative_ path with passed locale.
|
||||
*
|
||||
* ## Errors
|
||||
*
|
||||
* Throws an error if the locale doesn't exist in the list of locales defined in the configuration.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* ```js
|
||||
* import { getLocaleRelativeUrl } from "astro:i18n";
|
||||
* getLocaleRelativeUrl("es"); // /es
|
||||
* getLocaleRelativeUrl("es", "getting-started"); // /es/getting-started
|
||||
* getLocaleRelativeUrl("es_US", "getting-started", { prependWith: "blog" }); // /blog/es-us/getting-started
|
||||
* getLocaleRelativeUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // /blog/es_US/getting-started
|
||||
* ```
|
||||
*/
|
||||
export const getLocaleRelativeUrl: (
|
||||
locale: string,
|
||||
path?: string,
|
||||
options?: GetLocaleOptions
|
||||
) => string;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} locale A locale
|
||||
* @param {string} [path=""] An optional path to add after the `locale`.
|
||||
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
|
||||
* @return {string}
|
||||
*
|
||||
* Returns an absolute path with the passed locale. The behaviour is subject to change based on `site` configuration.
|
||||
* If _not_ provided, the function will return a _relative_ URL.
|
||||
*
|
||||
* ## Errors
|
||||
*
|
||||
* Throws an error if the locale doesn't exist in the list of locales defined in the configuration.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* If `site` is `https://example.com`:
|
||||
*
|
||||
* ```js
|
||||
* import { getLocaleAbsoluteUrl } from "astro:i18n";
|
||||
* getLocaleAbsoluteUrl("es"); // https://example.com/es
|
||||
* getLocaleAbsoluteUrl("es", "getting-started"); // https://example.com/es/getting-started
|
||||
* getLocaleAbsoluteUrl("es_US", "getting-started", { prependWith: "blog" }); // https://example.com/blog/es-us/getting-started
|
||||
* getLocaleAbsoluteUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // https://example.com/blog/es_US/getting-started
|
||||
* ```
|
||||
*/
|
||||
export const getLocaleAbsoluteUrl: (
|
||||
locale: string,
|
||||
path?: string,
|
||||
options?: GetLocaleOptions
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* @param {string} [path=""] An optional path to add after the `locale`.
|
||||
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
|
||||
* @return {string[]}
|
||||
*
|
||||
* Works like `getLocaleRelativeUrl` but it emits the relative URLs for ALL locales:
|
||||
*/
|
||||
export const getLocaleRelativeUrlList: (path?: string, options?: GetLocaleOptions) => string[];
|
||||
/**
|
||||
* @param {string} [path=""] An optional path to add after the `locale`.
|
||||
* @param {import('./dist/i18n/index.js').GetLocaleOptions} options Customise the generated path
|
||||
* @return {string[]}
|
||||
*
|
||||
* Works like `getLocaleAbsoluteUrl` but it emits the absolute URLs for ALL locales:
|
||||
*/
|
||||
export const getLocaleAbsoluteUrlList: (path?: string, options?: GetLocaleOptions) => string[];
|
||||
}
|
||||
|
||||
declare module 'astro:middleware' {
|
||||
export * from 'astro/middleware/namespace';
|
||||
}
|
||||
|
|
|
@ -79,7 +79,8 @@
|
|||
},
|
||||
"./transitions": "./dist/transitions/index.js",
|
||||
"./transitions/router": "./dist/transitions/router.js",
|
||||
"./prefetch": "./dist/prefetch/index.js"
|
||||
"./prefetch": "./dist/prefetch/index.js",
|
||||
"./i18n": "./dist/i18n/index.js"
|
||||
},
|
||||
"imports": {
|
||||
"#astro/*": "./dist/*.js"
|
||||
|
|
|
@ -1440,6 +1440,93 @@ export interface AstroUserConfig {
|
|||
* ```
|
||||
*/
|
||||
devOverlay?: boolean;
|
||||
|
||||
// TODO review with docs team before merging to `main`
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n
|
||||
* @type {object}
|
||||
* @version 3.5.0
|
||||
* @type {object}
|
||||
* @description
|
||||
*
|
||||
* Configures experimental i18n routing and allows you to specify some customization options.
|
||||
*/
|
||||
i18n?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.defaultLocale
|
||||
* @type {string}
|
||||
* @version 3.5.0
|
||||
* @description
|
||||
*
|
||||
* The default locale of your website/application. This is a required field.
|
||||
*/
|
||||
defaultLocale: string;
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.locales
|
||||
* @type {string[]}
|
||||
* @version 3.5.0
|
||||
* @description
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* No particular language format or syntax is enforced, but your folder structure must match exactly the locales in the list.
|
||||
*/
|
||||
locales: string[];
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.fallback
|
||||
* @type {Record<string, string>}
|
||||
* @version 3.5.0
|
||||
* @description
|
||||
*
|
||||
* The fallback strategy when navigating to pages that do not exist (e.g. a translated page has not been created).
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* #### Example
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ```js
|
||||
* export defualt defineConfig({
|
||||
* experimental: {
|
||||
* i18n: {
|
||||
* defaultLocale: "en",
|
||||
* locales: ["en", "fr", "pt", "es"],
|
||||
* fallback: {
|
||||
* pt: "es",
|
||||
* fr: "en"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
fallback?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.routingStrategy
|
||||
* @type {'prefix-always' | 'prefix-other-locales'}
|
||||
* @default 'prefix-other-locales'
|
||||
* @version 3.5.0
|
||||
* @description
|
||||
*
|
||||
* Controls the routing strategy to determine your site URLs.
|
||||
*
|
||||
* - `prefix-other-locales`(default): Only non-default languages will display a language prefix. The `defaultLocale` will not show a language prefix.
|
||||
* URLs will be of the form `example.com/[lang]/content/` for all non-default languages, but `example.com/content/` for the default locale.
|
||||
* - `prefix-always`: All URLs will display a language prefix.
|
||||
* URLs will be of the form `example.com/[lang]/content/` for every route, including the default language.
|
||||
*
|
||||
* Note: Astro requires all content to exist within a `/[lang]/` folder, even for the default language.
|
||||
*/
|
||||
routingStrategy: 'prefix-always' | 'prefix-other-locales';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1902,6 +1989,11 @@ export type AstroFeatureMap = {
|
|||
* The adapter can emit static assets
|
||||
*/
|
||||
assets?: AstroAssetsFeature;
|
||||
|
||||
/**
|
||||
* List of features that orbit around the i18n routing
|
||||
*/
|
||||
i18n?: AstroInternationalizationFeature;
|
||||
};
|
||||
|
||||
export interface AstroAssetsFeature {
|
||||
|
@ -1916,6 +2008,13 @@ export interface AstroAssetsFeature {
|
|||
isSquooshCompatible?: boolean;
|
||||
}
|
||||
|
||||
export interface AstroInternationalizationFeature {
|
||||
/**
|
||||
* Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
|
||||
*/
|
||||
detectBrowserLanguage?: SupportsKind;
|
||||
}
|
||||
|
||||
export interface AstroAdapter {
|
||||
name: string;
|
||||
serverEntrypoint?: string;
|
||||
|
@ -1973,6 +2072,17 @@ interface AstroSharedContext<
|
|||
* Object accessed via Astro middleware
|
||||
*/
|
||||
locals: App.Locals;
|
||||
|
||||
/**
|
||||
* The current locale that is computed from the `Accept-Language` header of the browser (**SSR Only**).
|
||||
*/
|
||||
preferredLocale: string | undefined;
|
||||
|
||||
/**
|
||||
* The list of locales computed from the `Accept-Language` header of the browser, sorted by quality value (**SSR Only**).
|
||||
*/
|
||||
|
||||
preferredLocaleList: string[] | undefined;
|
||||
}
|
||||
|
||||
export interface APIContext<
|
||||
|
@ -2074,6 +2184,34 @@ export interface APIContext<
|
|||
*/
|
||||
locals: App.Locals;
|
||||
ResponseWithEncoding: typeof ResponseWithEncoding;
|
||||
|
||||
/**
|
||||
* Available only when `experimental.i18n` enabled and in SSR.
|
||||
*
|
||||
* It represents the preferred locale of the user. It's computed by checking the supported locales in `i18n.locales`
|
||||
* and locales supported by the users's browser via the header `Accept-Language`
|
||||
*
|
||||
* 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
|
||||
* `Astro.preferredLanguage` will be `fr` because `en` is not supported, its [quality value] is the highest.
|
||||
*
|
||||
* [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
|
||||
*/
|
||||
preferredLocale: string | undefined;
|
||||
|
||||
/**
|
||||
* Available only when `experimental.i18n` enabled and in SSR.
|
||||
*
|
||||
* It represents the list of the preferred locales that are supported by the application. The list is sorted via [quality value].
|
||||
*
|
||||
* 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
|
||||
* `Astro.preferredLocaleList` will be equal to `['fs', 'de']` because `en` isn't supported, and `pt` isn't part of the locales contained in the
|
||||
* header.
|
||||
*
|
||||
* When the `Accept-Header` is `*`, the original `i18n.locales` are returned. The value `*` means no preferences, so Astro returns all the supported locales.
|
||||
*
|
||||
* [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
|
||||
*/
|
||||
preferredLocaleList: string[] | undefined;
|
||||
}
|
||||
|
||||
export type EndpointOutput =
|
||||
|
@ -2216,7 +2354,13 @@ export interface AstroPluginOptions {
|
|||
logger: Logger;
|
||||
}
|
||||
|
||||
export type RouteType = 'page' | 'endpoint' | 'redirect';
|
||||
/**
|
||||
* - page: a route that lives in the file system, usually an Astro component
|
||||
* - endpoint: a route that lives in the file system, usually a JS file that exposes endpoints methods
|
||||
* - redirect: a route points to another route that lives in the file system
|
||||
* - fallback: a route that doesn't exist in the file system that needs to be handled with other means, usually the middleware
|
||||
*/
|
||||
export type RouteType = 'page' | 'endpoint' | 'redirect' | 'fallback';
|
||||
|
||||
export interface RoutePart {
|
||||
content: string;
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
import { matchRoute } from '../routing/match.js';
|
||||
import { EndpointNotFoundError, SSRRoutePipeline } from './ssrPipeline.js';
|
||||
import type { RouteInfo } from './types.js';
|
||||
import { createI18nMiddleware } from '../../i18n/middleware.js';
|
||||
import { sequence } from '../middleware/index.js';
|
||||
export { deserializeManifest } from './common.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
@ -164,8 +166,19 @@ export class App {
|
|||
);
|
||||
let response;
|
||||
try {
|
||||
if (mod.onRequest) {
|
||||
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
||||
let i18nMiddleware = createI18nMiddleware(this.#manifest.i18n, this.#manifest.base);
|
||||
if (i18nMiddleware) {
|
||||
if (mod.onRequest) {
|
||||
this.#pipeline.setMiddlewareFunction(
|
||||
sequence(i18nMiddleware, mod.onRequest as MiddlewareEndpointHandler)
|
||||
);
|
||||
} else {
|
||||
this.#pipeline.setMiddlewareFunction(i18nMiddleware);
|
||||
}
|
||||
} else {
|
||||
if (mod.onRequest) {
|
||||
this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler);
|
||||
}
|
||||
}
|
||||
response = await this.#pipeline.renderRoute(renderContext, pageModule);
|
||||
} catch (err: any) {
|
||||
|
@ -208,6 +221,7 @@ export class App {
|
|||
const pathname = '/' + this.removeBase(url.pathname);
|
||||
const mod = await page.page();
|
||||
const handler = mod as unknown as EndpointHandler;
|
||||
|
||||
return await createRenderContext({
|
||||
request,
|
||||
pathname,
|
||||
|
@ -215,6 +229,7 @@ export class App {
|
|||
status,
|
||||
env: this.#pipeline.env,
|
||||
mod: handler as any,
|
||||
locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined,
|
||||
});
|
||||
} else {
|
||||
const pathname = prependForwardSlash(this.removeBase(url.pathname));
|
||||
|
@ -237,6 +252,7 @@ export class App {
|
|||
}
|
||||
}
|
||||
const mod = await page.page();
|
||||
|
||||
return await createRenderContext({
|
||||
request,
|
||||
pathname,
|
||||
|
@ -248,6 +264,7 @@ export class App {
|
|||
status,
|
||||
mod,
|
||||
env: this.#pipeline.env,
|
||||
locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,14 @@ export type SSRManifest = {
|
|||
componentMetadata: SSRResult['componentMetadata'];
|
||||
pageModule?: SinglePageBuiltModule;
|
||||
pageMap?: Map<ComponentPath, ImportComponentInstance>;
|
||||
i18n: SSRManifestI18n | undefined;
|
||||
};
|
||||
|
||||
export type SSRManifestI18n = {
|
||||
fallback?: Record<string, string>;
|
||||
routingStrategy?: 'prefix-always' | 'prefix-other-locales';
|
||||
locales: string[];
|
||||
defaultLocale: string;
|
||||
};
|
||||
|
||||
export type SerializedSSRManifest = Omit<
|
||||
|
@ -60,8 +68,3 @@ export type SerializedSSRManifest = Omit<
|
|||
componentMetadata: [string, SSRComponentMetadata][];
|
||||
clientDirectives: [string, string][];
|
||||
};
|
||||
|
||||
export type AdapterCreateExports<T = any> = (
|
||||
manifest: SSRManifest,
|
||||
args?: T
|
||||
) => Record<string, any>;
|
||||
|
|
|
@ -11,6 +11,8 @@ import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
|||
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
|
||||
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||
import type { PageBuildData, StaticBuildOptions } from './types.js';
|
||||
import { routeIsFallback, routeIsRedirect } from '../redirects/helpers.js';
|
||||
import { i18nHasFallback } from './util.js';
|
||||
|
||||
/**
|
||||
* This pipeline is responsible to gather the files emitted by the SSR build and generate the pages by executing these files.
|
||||
|
@ -154,11 +156,17 @@ export class BuildPipeline extends Pipeline {
|
|||
pages.set(pageData, filePath);
|
||||
}
|
||||
}
|
||||
for (const [path, pageData] of this.#internals.pagesByComponent.entries()) {
|
||||
if (pageData.route.type === 'redirect') {
|
||||
pages.set(pageData, path);
|
||||
|
||||
for (const [path, pageDataList] of this.#internals.pagesByComponents.entries()) {
|
||||
for (const pageData of pageDataList) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
pages.set(pageData, path);
|
||||
} else if (routeIsFallback(pageData.route) && i18nHasFallback(this.getConfig())) {
|
||||
pages.set(pageData, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export function getOutFolder(
|
|||
switch (routeType) {
|
||||
case 'endpoint':
|
||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||
case 'fallback':
|
||||
case 'page':
|
||||
case 'redirect':
|
||||
switch (astroConfig.build.format) {
|
||||
|
@ -52,6 +53,7 @@ export function getOutFile(
|
|||
case 'endpoint':
|
||||
return new URL(npath.basename(pathname), outFolder);
|
||||
case 'page':
|
||||
case 'fallback':
|
||||
case 'redirect':
|
||||
switch (astroConfig.build.format) {
|
||||
case 'directory': {
|
||||
|
|
|
@ -7,7 +7,6 @@ import PQueue from 'p-queue';
|
|||
import type { OutputAsset, OutputChunk } from 'rollup';
|
||||
import type { BufferEncoding } from 'vfile';
|
||||
import type {
|
||||
AstroConfig,
|
||||
AstroSettings,
|
||||
ComponentInstance,
|
||||
GetStaticPathsItem,
|
||||
|
@ -35,7 +34,11 @@ import { runHookBuildGenerated } from '../../integrations/index.js';
|
|||
import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js';
|
||||
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { RedirectSinglePageBuiltModule, getRedirectLocationOrThrow } from '../redirects/index.js';
|
||||
import {
|
||||
RedirectSinglePageBuiltModule,
|
||||
getRedirectLocationOrThrow,
|
||||
routeIsRedirect,
|
||||
} from '../redirects/index.js';
|
||||
import { createRenderContext } from '../render/index.js';
|
||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||
import {
|
||||
|
@ -60,7 +63,11 @@ import type {
|
|||
StaticBuildOptions,
|
||||
StylesheetAsset,
|
||||
} from './types.js';
|
||||
import { getTimeStat } from './util.js';
|
||||
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
|
||||
import { createI18nMiddleware } from '../../i18n/middleware.js';
|
||||
import { sequence } from '../middleware/index.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
import type { SSRManifestI18n } from '../app/types.js';
|
||||
|
||||
function createEntryURL(filePath: string, outFolder: URL) {
|
||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||
|
@ -86,6 +93,26 @@ async function getEntryForRedirectRoute(
|
|||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
async function getEntryForFallbackRoute(
|
||||
route: RouteData,
|
||||
internals: BuildInternals,
|
||||
outFolder: URL
|
||||
): Promise<SinglePageBuiltModule> {
|
||||
if (route.type !== 'fallback') {
|
||||
throw new Error(`Expected a redirect route.`);
|
||||
}
|
||||
if (route.redirectRoute) {
|
||||
const filePath = getEntryFilePathFromComponentPath(internals, route.redirectRoute.component);
|
||||
if (filePath) {
|
||||
const url = createEntryURL(filePath, outFolder);
|
||||
const ssrEntryPage: SinglePageBuiltModule = await import(url.toString());
|
||||
return ssrEntryPage;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectSinglePageBuiltModule;
|
||||
}
|
||||
|
||||
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
|
||||
return (
|
||||
// Drafts are disabled
|
||||
|
@ -179,16 +206,15 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
|
|||
await generatePage(pageData, ssrEntry, builtPaths, pipeline);
|
||||
}
|
||||
}
|
||||
if (pageData.route.type === 'redirect') {
|
||||
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [pageData, filePath] of pagesToGenerate) {
|
||||
if (pageData.route.type === 'redirect') {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
const entry = await getEntryForRedirectRoute(pageData.route, internals, outFolder);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
} else if (routeIsFallback(pageData.route)) {
|
||||
const entry = await getEntryForFallbackRoute(pageData.route, internals, outFolder);
|
||||
await generatePage(pageData, entry, builtPaths, pipeline);
|
||||
} else {
|
||||
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
|
||||
const entry: SinglePageBuiltModule = await import(ssrEntryURLPage.toString());
|
||||
|
@ -237,6 +263,7 @@ async function generatePage(
|
|||
) {
|
||||
let timeStart = performance.now();
|
||||
const logger = pipeline.getLogger();
|
||||
const config = pipeline.getConfig();
|
||||
const pageInfo = getPageDataByComponent(pipeline.getInternals(), pageData.route.component);
|
||||
|
||||
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
|
||||
|
@ -249,7 +276,19 @@ async function generatePage(
|
|||
|
||||
const pageModulePromise = ssrEntry.page;
|
||||
const onRequest = ssrEntry.onRequest;
|
||||
if (onRequest) {
|
||||
const i18nMiddleware = createI18nMiddleware(
|
||||
pipeline.getManifest().i18n,
|
||||
pipeline.getManifest().base
|
||||
);
|
||||
if (config.experimental.i18n && i18nMiddleware) {
|
||||
if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(
|
||||
sequence(i18nMiddleware, onRequest as MiddlewareEndpointHandler)
|
||||
);
|
||||
} else {
|
||||
pipeline.setMiddlewareFunction(i18nMiddleware);
|
||||
}
|
||||
} else if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler);
|
||||
}
|
||||
if (!pageModulePromise) {
|
||||
|
@ -277,7 +316,9 @@ async function generatePage(
|
|||
};
|
||||
|
||||
const icon =
|
||||
pageData.route.type === 'page' || pageData.route.type === 'redirect'
|
||||
pageData.route.type === 'page' ||
|
||||
pageData.route.type === 'redirect' ||
|
||||
pageData.route.type === 'fallback'
|
||||
? green('▶')
|
||||
: magenta('λ');
|
||||
if (isRelativePath(pageData.route.component)) {
|
||||
|
@ -410,26 +451,6 @@ interface GeneratePathOptions {
|
|||
mod: ComponentInstance;
|
||||
}
|
||||
|
||||
function shouldAppendForwardSlash(
|
||||
trailingSlash: AstroConfig['trailingSlash'],
|
||||
buildFormat: AstroConfig['build']['format']
|
||||
): boolean {
|
||||
switch (trailingSlash) {
|
||||
case 'always':
|
||||
return true;
|
||||
case 'never':
|
||||
return false;
|
||||
case 'ignore': {
|
||||
switch (buildFormat) {
|
||||
case 'directory':
|
||||
return true;
|
||||
case 'file':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||
const trailingSlash = opts.settings.config.trailingSlash;
|
||||
const buildFormat = opts.settings.config.build.format;
|
||||
|
@ -518,14 +539,16 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
|
|||
pageData.route.type
|
||||
);
|
||||
|
||||
const request = createRequest({
|
||||
url,
|
||||
headers: new Headers(),
|
||||
logger: pipeline.getLogger(),
|
||||
ssr,
|
||||
});
|
||||
const i18n = pipeline.getConfig().experimental.i18n;
|
||||
const renderContext = await createRenderContext({
|
||||
pathname,
|
||||
request: createRequest({
|
||||
url,
|
||||
headers: new Headers(),
|
||||
logger: pipeline.getLogger(),
|
||||
ssr,
|
||||
}),
|
||||
request,
|
||||
componentMetadata: manifest.componentMetadata,
|
||||
scripts,
|
||||
styles,
|
||||
|
@ -533,6 +556,7 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
|
|||
route: pageData.route,
|
||||
env: pipeline.getEnvironment(),
|
||||
mod,
|
||||
locales: i18n ? i18n.locales : undefined,
|
||||
});
|
||||
|
||||
let body: string | Uint8Array;
|
||||
|
@ -602,6 +626,15 @@ export function createBuildManifest(
|
|||
internals: BuildInternals,
|
||||
renderers: SSRLoadedRenderer[]
|
||||
): SSRManifest {
|
||||
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
||||
if (settings.config.experimental.i18n) {
|
||||
i18nManifest = {
|
||||
fallback: settings.config.experimental.i18n.fallback,
|
||||
routingStrategy: settings.config.experimental.i18n.routingStrategy,
|
||||
defaultLocale: settings.config.experimental.i18n.defaultLocale,
|
||||
locales: settings.config.experimental.i18n.locales,
|
||||
};
|
||||
}
|
||||
return {
|
||||
assets: new Set(),
|
||||
entryModules: Object.fromEntries(internals.entrySpecifierToBundleMap.entries()),
|
||||
|
@ -616,5 +649,6 @@ export function createBuildManifest(
|
|||
? new URL(settings.config.base, settings.config.site).toString()
|
||||
: settings.config.site,
|
||||
componentMetadata: internals.componentMetadata,
|
||||
i18n: i18nManifest,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -198,7 +198,9 @@ class AstroBuilder {
|
|||
await runHookBuildDone({
|
||||
config: this.settings.config,
|
||||
pages: pageNames,
|
||||
routes: Object.values(allPages).map((pd) => pd.route),
|
||||
routes: Object.values(allPages)
|
||||
.flat()
|
||||
.map((pageData) => pageData.route),
|
||||
logging: this.logger,
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ import {
|
|||
} from './plugins/plugin-pages.js';
|
||||
import { RESOLVED_SPLIT_MODULE_ID } from './plugins/plugin-ssr.js';
|
||||
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||
import type { PageBuildData, StylesheetAsset, ViteID } from './types.js';
|
||||
import type { AllPagesData, PageBuildData, StylesheetAsset, ViteID } from './types.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
|
||||
export interface BuildInternals {
|
||||
/**
|
||||
|
@ -37,9 +38,16 @@ export interface BuildInternals {
|
|||
|
||||
/**
|
||||
* A map for page-specific information.
|
||||
* // TODO: Remove in Astro 4.0
|
||||
* @deprecated
|
||||
*/
|
||||
pagesByComponent: Map<string, PageBuildData>;
|
||||
|
||||
/**
|
||||
* TODO: Use this in Astro 4.0
|
||||
*/
|
||||
pagesByComponents: Map<string, PageBuildData[]>;
|
||||
|
||||
/**
|
||||
* A map for page-specific output.
|
||||
*/
|
||||
|
@ -112,6 +120,7 @@ export function createBuildInternals(): BuildInternals {
|
|||
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||
pageToBundleMap: new Map<string, string>(),
|
||||
pagesByComponent: new Map(),
|
||||
pagesByComponents: new Map(),
|
||||
pageOptionsByPage: new Map(),
|
||||
pagesByViteID: new Map(),
|
||||
pagesByClientOnly: new Map(),
|
||||
|
@ -134,7 +143,16 @@ export function trackPageData(
|
|||
componentURL: URL
|
||||
): void {
|
||||
pageData.moduleSpecifier = componentModuleId;
|
||||
internals.pagesByComponent.set(component, pageData);
|
||||
if (!routeIsFallback(pageData.route)) {
|
||||
internals.pagesByComponent.set(component, pageData);
|
||||
}
|
||||
const list = internals.pagesByComponents.get(component);
|
||||
if (list) {
|
||||
list.push(pageData);
|
||||
internals.pagesByComponents.set(component, list);
|
||||
} else {
|
||||
internals.pagesByComponents.set(component, [pageData]);
|
||||
}
|
||||
internals.pagesByViteID.set(viteID(componentURL), pageData);
|
||||
}
|
||||
|
||||
|
@ -230,6 +248,14 @@ export function* eachPageData(internals: BuildInternals) {
|
|||
yield* internals.pagesByComponent.values();
|
||||
}
|
||||
|
||||
export function* eachPageFromAllPages(allPages: AllPagesData): Generator<[string, PageBuildData]> {
|
||||
for (const [path, list] of Object.entries(allPages)) {
|
||||
for (const pageData of list) {
|
||||
yield [path, pageData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* eachPageDataFromEntryPoint(
|
||||
internals: BuildInternals
|
||||
): Generator<[PageBuildData, string]> {
|
||||
|
|
|
@ -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;
|
||||
|
@ -47,15 +48,29 @@ export async function collectPagesData(
|
|||
clearInterval(routeCollectionLogTimeout);
|
||||
}, 10000);
|
||||
builtPaths.add(route.pathname);
|
||||
allPages[route.component] = {
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
};
|
||||
if (allPages[route.component]) {
|
||||
allPages[route.component].push({
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
});
|
||||
} else {
|
||||
allPages[route.component] = [
|
||||
{
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
clearInterval(routeCollectionLogTimeout);
|
||||
if (settings.config.output === 'static') {
|
||||
|
@ -70,18 +85,31 @@ export async function collectPagesData(
|
|||
continue;
|
||||
}
|
||||
// dynamic route:
|
||||
allPages[route.component] = {
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
};
|
||||
if (allPages[route.component]) {
|
||||
allPages[route.component].push({
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
});
|
||||
} else {
|
||||
allPages[route.component] = [
|
||||
{
|
||||
component: route.component,
|
||||
route,
|
||||
moduleSpecifier: '',
|
||||
styles: [],
|
||||
propagatedStyles: new Map(),
|
||||
propagatedScripts: new Map(),
|
||||
hoistedScript: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(dataCollectionLogTimeout);
|
||||
|
||||
return { assets, allPages };
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import { type Plugin as VitePlugin } from 'vite';
|
|||
import { runHookBuildSsr } from '../../../integrations/index.js';
|
||||
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js';
|
||||
import type { SerializedRouteInfo, SerializedSSRManifest } from '../../app/types.js';
|
||||
import type { SSRManifestI18n } from '../../app/types.js';
|
||||
import { joinPaths, prependForwardSlash } from '../../path.js';
|
||||
import { serializeRouteData } from '../../routing/index.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { getOutFile, getOutFolder } from '../common.js';
|
||||
import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js';
|
||||
import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
import type { StaticBuildOptions } from '../types.js';
|
||||
|
||||
|
@ -237,8 +238,17 @@ function buildManifest(
|
|||
// Set this to an empty string so that the runtime knows not to try and load this.
|
||||
entryModules[BEFORE_HYDRATION_SCRIPT_ID] = '';
|
||||
}
|
||||
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
||||
if (settings.config.experimental.i18n) {
|
||||
i18nManifest = {
|
||||
fallback: settings.config.experimental.i18n.fallback,
|
||||
routingStrategy: settings.config.experimental.i18n.routingStrategy,
|
||||
locales: settings.config.experimental.i18n.locales,
|
||||
defaultLocale: settings.config.experimental.i18n.defaultLocale,
|
||||
};
|
||||
}
|
||||
|
||||
const ssrManifest: SerializedSSRManifest = {
|
||||
return {
|
||||
adapterName: opts.settings.adapter?.name ?? '',
|
||||
routes,
|
||||
site: settings.config.site,
|
||||
|
@ -250,7 +260,6 @@ function buildManifest(
|
|||
clientDirectives: Array.from(settings.clientDirectives),
|
||||
entryModules,
|
||||
assets: staticFiles.map(prefixAssetPath),
|
||||
i18n: i18nManifest,
|
||||
};
|
||||
|
||||
return ssrManifest;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export function vitePluginMiddleware(
|
|||
let resolvedMiddlewareId: string;
|
||||
return {
|
||||
name: '@astro/plugin-middleware',
|
||||
|
||||
enforce: 'post',
|
||||
options(options) {
|
||||
return addRollupInput(options, [MIDDLEWARE_MODULE_ID]);
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Plugin as VitePlugin } from 'vite';
|
|||
import type { AstroSettings } from '../../../@types/astro.js';
|
||||
import { routeIsRedirect } from '../../redirects/index.js';
|
||||
import { addRollupInput } from '../add-rollup-input.js';
|
||||
import { type BuildInternals } from '../internal.js';
|
||||
import { type BuildInternals, eachPageFromAllPages } from '../internal.js';
|
||||
import type { AstroBuildPlugin } from '../plugin.js';
|
||||
import type { StaticBuildOptions } from '../types.js';
|
||||
import { MIDDLEWARE_MODULE_ID } from './plugin-middleware.js';
|
||||
|
@ -42,7 +42,7 @@ function vitePluginPages(opts: StaticBuildOptions, internals: BuildInternals): V
|
|||
if (opts.settings.config.output === 'static') {
|
||||
const inputs = new Set<string>();
|
||||
|
||||
for (const [path, pageData] of Object.entries(opts.allPages)) {
|
||||
for (const [path, pageData] of eachPageFromAllPages(opts.allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SSR_MANIFEST_VIRTUAL_MODULE_ID } from './plugin-manifest.js';
|
|||
import { ASTRO_PAGE_MODULE_ID } from './plugin-pages.js';
|
||||
import { RENDERERS_MODULE_ID } from './plugin-renderers.js';
|
||||
import { getPathFromVirtualModulePageName, getVirtualModulePageNameFromPath } from './util.js';
|
||||
import { eachPageFromAllPages } from '../internal.js';
|
||||
|
||||
export const SSR_VIRTUAL_MODULE_ID = '@astrojs-ssr-virtual-entry';
|
||||
export const RESOLVED_SSR_VIRTUAL_MODULE_ID = '\0' + SSR_VIRTUAL_MODULE_ID;
|
||||
|
@ -42,7 +43,7 @@ function vitePluginSSR(
|
|||
let i = 0;
|
||||
const pageMap: string[] = [];
|
||||
|
||||
for (const [path, pageData] of Object.entries(allPages)) {
|
||||
for (const [path, pageData] of eachPageFromAllPages(allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -148,7 +149,7 @@ function vitePluginSSRSplit(
|
|||
if (options.settings.config.build.split || functionPerRouteEnabled) {
|
||||
const inputs = new Set<string>();
|
||||
|
||||
for (const [path, pageData] of Object.entries(options.allPages)) {
|
||||
for (const [path, pageData] of eachPageFromAllPages(options.allPages)) {
|
||||
if (routeIsRedirect(pageData.route)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -294,7 +295,7 @@ function storeEntryPoint(
|
|||
fileName: string
|
||||
) {
|
||||
const componentPath = getPathFromVirtualModulePageName(RESOLVED_SPLIT_MODULE_ID, moduleKey);
|
||||
for (const [page, pageData] of Object.entries(options.allPages)) {
|
||||
for (const [page, pageData] of eachPageFromAllPages(options.allPages)) {
|
||||
if (componentPath == page) {
|
||||
const publicPath = fileURLToPath(options.settings.config.build.server);
|
||||
internals.entryPoints.set(pageData.route, pathToFileURL(join(publicPath, fileName)));
|
||||
|
|
|
@ -30,7 +30,7 @@ import { ASTRO_PAGE_RESOLVED_MODULE_ID } from './plugins/plugin-pages.js';
|
|||
import { RESOLVED_RENDERERS_MODULE_ID } from './plugins/plugin-renderers.js';
|
||||
import { RESOLVED_SPLIT_MODULE_ID, RESOLVED_SSR_VIRTUAL_MODULE_ID } from './plugins/plugin-ssr.js';
|
||||
import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js';
|
||||
import type { PageBuildData, StaticBuildOptions } from './types.js';
|
||||
import type { StaticBuildOptions } from './types.js';
|
||||
import { getTimeStat } from './util.js';
|
||||
|
||||
export async function viteBuild(opts: StaticBuildOptions) {
|
||||
|
@ -46,23 +46,20 @@ export async function viteBuild(opts: StaticBuildOptions) {
|
|||
// The pages to be built for rendering purposes.
|
||||
const pageInput = new Set<string>();
|
||||
|
||||
// A map of each page .astro file, to the PageBuildData which contains information
|
||||
// about that page, such as its paths.
|
||||
const facadeIdToPageDataMap = new Map<string, PageBuildData>();
|
||||
|
||||
// Build internals needed by the CSS plugin
|
||||
const internals = createBuildInternals();
|
||||
|
||||
for (const [component, pageData] of Object.entries(allPages)) {
|
||||
const astroModuleURL = new URL('./' + component, settings.config.root);
|
||||
const astroModuleId = prependForwardSlash(component);
|
||||
for (const [component, pageDataList] of Object.entries(allPages)) {
|
||||
for (const pageData of pageDataList) {
|
||||
const astroModuleURL = new URL('./' + component, settings.config.root);
|
||||
const astroModuleId = prependForwardSlash(component);
|
||||
|
||||
// Track the page data in internals
|
||||
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
|
||||
// Track the page data in internals
|
||||
trackPageData(internals, component, pageData, astroModuleId, astroModuleURL);
|
||||
|
||||
if (!routeIsRedirect(pageData.route)) {
|
||||
pageInput.add(astroModuleId);
|
||||
facadeIdToPageDataMap.set(fileURLToPath(astroModuleURL), pageData);
|
||||
if (!routeIsRedirect(pageData.route)) {
|
||||
pageInput.add(astroModuleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +145,9 @@ async function ssrBuild(
|
|||
const { allPages, settings, viteConfig } = opts;
|
||||
const ssr = isServerLikeOutput(settings.config);
|
||||
const out = getOutputDirectory(settings.config);
|
||||
const routes = Object.values(allPages).map((pd) => pd.route);
|
||||
const routes = Object.values(allPages)
|
||||
.flat()
|
||||
.map((pageData) => pageData.route);
|
||||
const { lastVitePlugins, vitePlugins } = container.runBeforeHook('ssr', input);
|
||||
|
||||
const viteBuildConfig: vite.InlineConfig = {
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface PageBuildData {
|
|||
hoistedScript: { type: 'inline' | 'external'; value: string } | undefined;
|
||||
styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
|
||||
}
|
||||
export type AllPagesData = Record<ComponentPath, PageBuildData>;
|
||||
export type AllPagesData = Record<ComponentPath, PageBuildData[]>;
|
||||
|
||||
/** Options for the static build */
|
||||
export interface StaticBuildOptions {
|
||||
|
|
|
@ -1,4 +1,38 @@
|
|||
import type { AstroConfig } from '../../@types/astro.js';
|
||||
|
||||
export function getTimeStat(timeStart: number, timeEnd: number) {
|
||||
const buildTime = timeEnd - timeStart;
|
||||
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the Astro configuration, it tells if a slash should be appended or not
|
||||
*/
|
||||
export function shouldAppendForwardSlash(
|
||||
trailingSlash: AstroConfig['trailingSlash'],
|
||||
buildFormat: AstroConfig['build']['format']
|
||||
): boolean {
|
||||
switch (trailingSlash) {
|
||||
case 'always':
|
||||
return true;
|
||||
case 'never':
|
||||
return false;
|
||||
case 'ignore': {
|
||||
switch (buildFormat) {
|
||||
case 'directory':
|
||||
return true;
|
||||
case 'file':
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function i18nHasFallback(config: AstroConfig): boolean {
|
||||
if (config.experimental.i18n && config.experimental.i18n.fallback) {
|
||||
// we have some fallback and the control is not none
|
||||
return Object.keys(config.experimental.i18n.fallback).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -339,6 +339,55 @@ export const AstroConfigSchema = z.object({
|
|||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
||||
devOverlay: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.devOverlay),
|
||||
i18n: z.optional(
|
||||
z
|
||||
.object({
|
||||
defaultLocale: z.string(),
|
||||
locales: z.string().array(),
|
||||
fallback: z.record(z.string(), z.string()).optional(),
|
||||
// TODO: properly add default when the feature goes of experimental
|
||||
routingStrategy: z
|
||||
.enum(['prefix-always', 'prefix-other-locales'])
|
||||
.optional()
|
||||
.default('prefix-other-locales'),
|
||||
})
|
||||
.optional()
|
||||
.superRefine((i18n, ctx) => {
|
||||
if (i18n) {
|
||||
const { defaultLocale, locales, fallback } = i18n;
|
||||
if (!locales.includes(defaultLocale)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`,
|
||||
});
|
||||
}
|
||||
if (fallback) {
|
||||
for (const [fallbackFrom, fallbackTo] of Object.entries(fallback)) {
|
||||
if (!locales.includes(fallbackFrom)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `The locale \`${fallbackFrom}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (fallbackFrom === defaultLocale) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `You can't use the default locale as a key. The default locale can only be used as value.`,
|
||||
});
|
||||
}
|
||||
|
||||
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.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
})
|
||||
.strict(
|
||||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
||||
|
|
|
@ -31,6 +31,7 @@ import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
|||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
|
||||
import { joinPaths } from './path.js';
|
||||
import astroInternationalization from '../i18n/vite-plugin-i18n.js';
|
||||
|
||||
interface CreateViteOptions {
|
||||
settings: AstroSettings;
|
||||
|
@ -138,6 +139,7 @@ export async function createVite(
|
|||
astroPrefetch({ settings }),
|
||||
astroTransitions({ settings }),
|
||||
astroDevOverlay({ settings, logger }),
|
||||
!!settings.config.experimental.i18n && astroInternationalization({ settings }),
|
||||
],
|
||||
publicDir: fileURLToPath(settings.config.publicDir),
|
||||
root: fileURLToPath(settings.config.root),
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import type { EndpointHandler } from '../../../@types/astro.js';
|
||||
import { createRenderContext, type SSROptions } from '../../render/index.js';
|
||||
import { callEndpoint } from '../index.js';
|
||||
|
||||
export async function call(options: SSROptions) {
|
||||
const { env, preload, middleware } = options;
|
||||
const endpointHandler = preload as unknown as EndpointHandler;
|
||||
|
||||
const ctx = await createRenderContext({
|
||||
request: options.request,
|
||||
pathname: options.pathname,
|
||||
route: options.route,
|
||||
env,
|
||||
mod: preload,
|
||||
});
|
||||
|
||||
return await callEndpoint(endpointHandler, env, ctx, middleware?.onRequest);
|
||||
}
|
|
@ -12,7 +12,8 @@ import { ASTRO_VERSION } from '../constants.js';
|
|||
import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import type { Environment, RenderContext } from '../render/index.js';
|
||||
import { type Environment, type RenderContext } from '../render/index.js';
|
||||
import { computePreferredLocale, computePreferredLocaleList } from '../render/context.js';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
|
@ -25,6 +26,7 @@ type CreateAPIContext = {
|
|||
site?: string;
|
||||
props: Record<string, any>;
|
||||
adapterName?: string;
|
||||
locales: string[] | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -38,7 +40,11 @@ export function createAPIContext({
|
|||
site,
|
||||
props,
|
||||
adapterName,
|
||||
locales,
|
||||
}: CreateAPIContext): APIContext {
|
||||
let preferredLocale: string | undefined = undefined;
|
||||
let preferredLocaleList: string[] | undefined = undefined;
|
||||
|
||||
const context = {
|
||||
cookies: new AstroCookies(request),
|
||||
request,
|
||||
|
@ -55,6 +61,28 @@ export function createAPIContext({
|
|||
});
|
||||
},
|
||||
ResponseWithEncoding,
|
||||
get preferredLocale(): string | undefined {
|
||||
if (preferredLocale) {
|
||||
return preferredLocale;
|
||||
}
|
||||
if (locales) {
|
||||
preferredLocale = computePreferredLocale(request, locales);
|
||||
return preferredLocale;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
get preferredLocaleList(): string[] | undefined {
|
||||
if (preferredLocaleList) {
|
||||
return preferredLocaleList;
|
||||
}
|
||||
if (locales) {
|
||||
preferredLocaleList = computePreferredLocaleList(request, locales);
|
||||
return preferredLocaleList;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
url: new URL(request.url),
|
||||
get clientAddress() {
|
||||
if (clientAddressSymbol in request) {
|
||||
|
@ -125,7 +153,8 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
|||
mod: EndpointHandler,
|
||||
env: Environment,
|
||||
ctx: RenderContext,
|
||||
onRequest?: MiddlewareHandler<MiddlewareResult> | undefined
|
||||
onRequest: MiddlewareHandler<MiddlewareResult> | undefined,
|
||||
locales: undefined | string[]
|
||||
): Promise<Response> {
|
||||
const context = createAPIContext({
|
||||
request: ctx.request,
|
||||
|
@ -133,6 +162,7 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
|||
props: ctx.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
locales,
|
||||
});
|
||||
|
||||
let response;
|
||||
|
|
|
@ -1272,5 +1272,23 @@ export const UnsupportedConfigTransformError = {
|
|||
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
|
||||
} satisfies ErrorData;
|
||||
|
||||
export const MissingLocale = {
|
||||
name: 'MissingLocaleError',
|
||||
title: 'The provided locale does not exist.',
|
||||
message: (locale: string, locales: string[]) => {
|
||||
return `The locale \`${locale}\` does not exist in the configured locales. Available locales: ${locales.join(
|
||||
', '
|
||||
)}.`;
|
||||
},
|
||||
} satisfies ErrorData;
|
||||
|
||||
export const CantRenderPage = {
|
||||
name: 'CantRenderPage',
|
||||
title: "Astro can't render the route.",
|
||||
message:
|
||||
'Astro cannot find any content to render for this route. There is no file or redirect associated with this route.',
|
||||
hint: 'If you expect to find a route here, this may be an Astro bug. Please file an issue/restart the dev server',
|
||||
} satisfies ErrorData;
|
||||
|
||||
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
|
||||
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { MiddlewareResponseHandler, Params } from '../../@types/astro.js';
|
||||
import type { MiddlewareEndpointHandler, Params } from '../../@types/astro.js';
|
||||
import { createAPIContext } from '../endpoint/index.js';
|
||||
import { sequence } from './sequence.js';
|
||||
|
||||
function defineMiddleware(fn: MiddlewareResponseHandler) {
|
||||
function defineMiddleware(fn: MiddlewareEndpointHandler) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
|
@ -18,17 +18,23 @@ export type CreateContext = {
|
|||
* Optional parameters
|
||||
*/
|
||||
params?: Params;
|
||||
|
||||
/**
|
||||
* A list of locales that are supported by the user
|
||||
*/
|
||||
userDefinedLocales?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a context to be passed to Astro middleware `onRequest` function.
|
||||
*/
|
||||
function createContext({ request, params }: CreateContext) {
|
||||
function createContext({ request, params, userDefinedLocales = [] }: CreateContext) {
|
||||
return createAPIContext({
|
||||
request,
|
||||
params: params ?? {},
|
||||
props: {},
|
||||
site: undefined,
|
||||
locales: userDefinedLocales,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { APIContext, MiddlewareResponseHandler } from '../../@types/astro.js';
|
||||
import type { APIContext, MiddlewareEndpointHandler } from '../../@types/astro.js';
|
||||
import { defineMiddleware } from './index.js';
|
||||
|
||||
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.js
|
||||
|
@ -6,10 +6,10 @@ import { defineMiddleware } from './index.js';
|
|||
*
|
||||
* It accepts one or more middleware handlers and makes sure that they are run in sequence.
|
||||
*/
|
||||
export function sequence(...handlers: MiddlewareResponseHandler[]): MiddlewareResponseHandler {
|
||||
export function sequence(...handlers: MiddlewareEndpointHandler[]): MiddlewareEndpointHandler {
|
||||
const length = handlers.length;
|
||||
if (!length) {
|
||||
const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
|
||||
const handler: MiddlewareEndpointHandler = defineMiddleware((context, next) => {
|
||||
return next();
|
||||
});
|
||||
return handler;
|
||||
|
|
|
@ -73,7 +73,7 @@ export class Pipeline {
|
|||
*/
|
||||
async renderRoute(
|
||||
renderContext: RenderContext,
|
||||
componentInstance: ComponentInstance
|
||||
componentInstance: ComponentInstance | undefined
|
||||
): Promise<Response> {
|
||||
const result = await this.#tryRenderRoute(
|
||||
renderContext,
|
||||
|
@ -106,7 +106,7 @@ export class Pipeline {
|
|||
async #tryRenderRoute<MiddlewareReturnType = Response>(
|
||||
renderContext: Readonly<RenderContext>,
|
||||
env: Readonly<Environment>,
|
||||
mod: Readonly<ComponentInstance>,
|
||||
mod: Readonly<ComponentInstance> | undefined,
|
||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||
): Promise<Response> {
|
||||
const apiContext = createAPIContext({
|
||||
|
@ -115,10 +115,12 @@ export class Pipeline {
|
|||
props: renderContext.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
locales: renderContext.locales,
|
||||
});
|
||||
|
||||
switch (renderContext.route.type) {
|
||||
case 'page':
|
||||
case 'fallback':
|
||||
case 'redirect': {
|
||||
if (onRequest) {
|
||||
return await callMiddleware<Response>(
|
||||
|
@ -144,13 +146,13 @@ export class Pipeline {
|
|||
}
|
||||
}
|
||||
case 'endpoint': {
|
||||
const result = await callEndpoint(
|
||||
return await callEndpoint(
|
||||
mod as any as EndpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
onRequest
|
||||
onRequest,
|
||||
renderContext.locales
|
||||
);
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||
|
|
|
@ -9,6 +9,10 @@ export function routeIsRedirect(route: RouteData | undefined): route is Redirect
|
|||
return route?.type === 'redirect';
|
||||
}
|
||||
|
||||
export function routeIsFallback(route: RouteData | undefined): route is RedirectRouteData {
|
||||
return route?.type === 'fallback';
|
||||
}
|
||||
|
||||
export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
|
||||
const routeData = redirectRoute.redirectRoute;
|
||||
const route = redirectRoute.redirect;
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { getParamsAndProps } from './params-and-props.js';
|
||||
import { normalizeTheLocale } from '../../i18n/index.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
|
@ -27,6 +28,7 @@ export interface RenderContext {
|
|||
params: Params;
|
||||
props: Props;
|
||||
locals?: object;
|
||||
locales: string[] | undefined;
|
||||
}
|
||||
|
||||
export type CreateRenderContextArgs = Partial<
|
||||
|
@ -34,7 +36,7 @@ export type CreateRenderContextArgs = Partial<
|
|||
> & {
|
||||
route: RouteData;
|
||||
request: RenderContext['request'];
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
env: Environment;
|
||||
};
|
||||
|
||||
|
@ -57,6 +59,7 @@ export async function createRenderContext(
|
|||
pathname,
|
||||
params,
|
||||
props,
|
||||
locales: options.locales,
|
||||
};
|
||||
|
||||
// We define a custom property, so we can check the value passed to locals
|
||||
|
@ -76,3 +79,132 @@ export async function createRenderContext(
|
|||
|
||||
return context;
|
||||
}
|
||||
|
||||
type BrowserLocale = {
|
||||
locale: string;
|
||||
qualityValue: number | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the value of the `Accept-Header` language:
|
||||
*
|
||||
* More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
|
||||
*
|
||||
* Complex example: `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5`
|
||||
*
|
||||
*/
|
||||
export function parseLocale(header: string): BrowserLocale[] {
|
||||
// Any language, early return
|
||||
if (header === '*') {
|
||||
return [{ locale: header, qualityValue: undefined }];
|
||||
}
|
||||
const result: BrowserLocale[] = [];
|
||||
// we split by `,` and trim the white spaces
|
||||
const localeValues = header.split(',').map((str) => str.trim());
|
||||
|
||||
for (const localeValue of localeValues) {
|
||||
// split the locale name from the quality value
|
||||
const split = localeValue.split(';').map((str) => str.trim());
|
||||
const localeName: string = split[0];
|
||||
const qualityValue: string | undefined = split[1];
|
||||
|
||||
if (!split) {
|
||||
// invalid value
|
||||
continue;
|
||||
}
|
||||
|
||||
// we check if the quality value is present, and it is actually `q=`
|
||||
if (qualityValue && qualityValue.startsWith('q=')) {
|
||||
const qualityValueAsFloat = Number.parseFloat(qualityValue.slice('q='.length));
|
||||
// The previous operation can return a `NaN`, so we check if it is a safe operation
|
||||
if (Number.isNaN(qualityValueAsFloat) || qualityValueAsFloat > 1) {
|
||||
result.push({
|
||||
locale: localeName,
|
||||
qualityValue: undefined,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
locale: localeName,
|
||||
qualityValue: qualityValueAsFloat,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
locale: localeName,
|
||||
qualityValue: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function sortAndFilterLocales(browserLocaleList: BrowserLocale[], locales: string[]) {
|
||||
const normalizedLocales = locales.map(normalizeTheLocale);
|
||||
return browserLocaleList
|
||||
.filter((browserLocale) => {
|
||||
if (browserLocale.locale !== '*') {
|
||||
return normalizedLocales.includes(normalizeTheLocale(browserLocale.locale));
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.qualityValue && b.qualityValue) {
|
||||
if (a.qualityValue > b.qualityValue) {
|
||||
return -1;
|
||||
} else if (a.qualityValue < b.qualityValue) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current locale by parsing the value passed from the `Accept-Header`.
|
||||
*
|
||||
* If multiple locales are present in the header, they are sorted by their quality value and the highest is selected as current locale.
|
||||
*
|
||||
*/
|
||||
export function computePreferredLocale(request: Request, locales: string[]): string | undefined {
|
||||
const acceptHeader = request.headers.get('Accept-Language');
|
||||
let result: string | undefined = undefined;
|
||||
if (acceptHeader) {
|
||||
const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
|
||||
|
||||
const firstResult = browserLocaleList.at(0);
|
||||
if (firstResult) {
|
||||
if (firstResult.locale !== '*') {
|
||||
result = locales.find(
|
||||
(locale) => normalizeTheLocale(locale) === normalizeTheLocale(firstResult.locale)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function computePreferredLocaleList(request: Request, locales: string[]) {
|
||||
const acceptHeader = request.headers.get('Accept-Language');
|
||||
let result: string[] = [];
|
||||
if (acceptHeader) {
|
||||
const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
|
||||
|
||||
// SAFETY: bang operator is safe because checked by the previous condition
|
||||
if (browserLocaleList.length === 1 && browserLocaleList.at(0)!.locale === '*') {
|
||||
return locales;
|
||||
} else if (browserLocaleList.length > 0) {
|
||||
for (const browserLocale of browserLocaleList) {
|
||||
const found = locales.find(
|
||||
(l) => normalizeTheLocale(l) === normalizeTheLocale(browserLocale.locale)
|
||||
);
|
||||
if (found) {
|
||||
result.push(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import type {
|
||||
AstroCookies,
|
||||
ComponentInstance,
|
||||
EndpointHandler,
|
||||
MiddlewareHandler,
|
||||
MiddlewareResponseHandler,
|
||||
} from '../../@types/astro.js';
|
||||
import type { AstroCookies, ComponentInstance } from '../../@types/astro.js';
|
||||
import { renderPage as runtimeRenderPage } from '../../runtime/server/index.js';
|
||||
import { attachCookiesToResponse } from '../cookies/index.js';
|
||||
import { callEndpoint, createAPIContext } from '../endpoint/index.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js';
|
||||
import type { RenderContext } from './context.js';
|
||||
import type { Environment } from './environment.js';
|
||||
import { createResult } from './result.js';
|
||||
import { AstroError } from '../errors/index.js';
|
||||
import { CantRenderPage } from '../errors/errors-data.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
|
||||
export type RenderPage = {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
renderContext: RenderContext;
|
||||
env: Environment;
|
||||
cookies: AstroCookies;
|
||||
|
@ -29,6 +24,14 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
|||
location: redirectRouteGenerate(renderContext.route, renderContext.params),
|
||||
},
|
||||
});
|
||||
} else if (routeIsFallback(renderContext.route)) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Validate the page component before rendering the page
|
||||
|
@ -56,6 +59,7 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
|||
status: renderContext.status ?? 200,
|
||||
cookies,
|
||||
locals: renderContext.locals ?? {},
|
||||
locales: renderContext.locales,
|
||||
});
|
||||
|
||||
// TODO: Remove in Astro 4.0
|
||||
|
@ -83,68 +87,3 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* It attempts to render a route. A route can be a:
|
||||
* - page
|
||||
* - redirect
|
||||
* - endpoint
|
||||
*
|
||||
* ## Errors
|
||||
*
|
||||
* It throws an error if the page can't be rendered.
|
||||
* @deprecated Use the pipeline instead
|
||||
*/
|
||||
export async function tryRenderRoute<MiddlewareReturnType = Response>(
|
||||
renderContext: Readonly<RenderContext>,
|
||||
env: Readonly<Environment>,
|
||||
mod: Readonly<ComponentInstance>,
|
||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||
): Promise<Response> {
|
||||
const apiContext = createAPIContext({
|
||||
request: renderContext.request,
|
||||
params: renderContext.params,
|
||||
props: renderContext.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
});
|
||||
|
||||
switch (renderContext.route.type) {
|
||||
case 'page':
|
||||
case 'redirect': {
|
||||
if (onRequest) {
|
||||
return await callMiddleware<Response>(
|
||||
env.logger,
|
||||
onRequest as MiddlewareResponseHandler,
|
||||
apiContext,
|
||||
() => {
|
||||
return renderPage({
|
||||
mod,
|
||||
renderContext,
|
||||
env,
|
||||
cookies: apiContext.cookies,
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return await renderPage({
|
||||
mod,
|
||||
renderContext,
|
||||
env,
|
||||
cookies: apiContext.cookies,
|
||||
});
|
||||
}
|
||||
}
|
||||
case 'endpoint': {
|
||||
const result = await callEndpoint(
|
||||
mod as any as EndpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
onRequest
|
||||
);
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { AstroMiddlewareInstance, ComponentInstance, RouteData } from '../../@types/astro.js';
|
||||
import type { Environment } from './environment.js';
|
||||
export { createRenderContext } from './context.js';
|
||||
export { createRenderContext, computePreferredLocale } from './context.js';
|
||||
export type { RenderContext } from './context.js';
|
||||
export { tryRenderRoute } from './core.js';
|
||||
export { createEnvironment } from './environment.js';
|
||||
export { getParamsAndProps } from './params-and-props.js';
|
||||
export { loadRenderer } from './renderer.js';
|
||||
|
@ -20,7 +19,7 @@ export interface SSROptions {
|
|||
preload: ComponentInstance;
|
||||
/** Request */
|
||||
request: Request;
|
||||
/** optional, in case we need to render something outside of a dev server */
|
||||
/** optional, in case we need to render something outside a dev server */
|
||||
route: RouteData;
|
||||
/**
|
||||
* Optional middlewares
|
||||
|
|
|
@ -4,9 +4,10 @@ import type { Logger } from '../logger/core.js';
|
|||
import { routeIsRedirect } from '../redirects/index.js';
|
||||
import { getParams } from '../routing/params.js';
|
||||
import { RouteCache, callGetStaticPaths, findPathItemByKey } from './route-cache.js';
|
||||
import { routeIsFallback } from '../redirects/helpers.js';
|
||||
|
||||
interface GetParamsAndPropsOptions {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
route?: RouteData | undefined;
|
||||
routeCache: RouteCache;
|
||||
pathname: string;
|
||||
|
@ -26,11 +27,13 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
|||
// This is a dynamic route, start getting the params
|
||||
const params = getRouteParams(route, pathname) ?? {};
|
||||
|
||||
if (routeIsRedirect(route)) {
|
||||
if (routeIsRedirect(route) || routeIsFallback(route)) {
|
||||
return [params, {}];
|
||||
}
|
||||
|
||||
validatePrerenderEndpointCollision(route, mod, params);
|
||||
if (mod) {
|
||||
validatePrerenderEndpointCollision(route, mod, params);
|
||||
}
|
||||
|
||||
// During build, the route cache should already be populated.
|
||||
// During development, the route cache is filled on-demand and may be empty.
|
||||
|
|
|
@ -12,6 +12,7 @@ import { chunkToString } from '../../runtime/server/render/index.js';
|
|||
import { AstroCookies } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { Logger } from '../logger/core.js';
|
||||
import { computePreferredLocale, computePreferredLocaleList } from './context.js';
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
||||
|
@ -45,6 +46,7 @@ export interface CreateResultArgs {
|
|||
status: number;
|
||||
locals: App.Locals;
|
||||
cookies?: AstroCookies;
|
||||
locales: string[] | undefined;
|
||||
}
|
||||
|
||||
function getFunctionExpression(slot: any) {
|
||||
|
@ -144,6 +146,8 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
|
||||
// Astro.cookies is defined lazily to avoid the cost on pages that do not use it.
|
||||
let cookies: AstroCookies | undefined = args.cookies;
|
||||
let preferredLocale: string | undefined = undefined;
|
||||
let preferredLocaleList: string[] | undefined = undefined;
|
||||
|
||||
// Create the result object that will be passed into the render function.
|
||||
// This object starts here as an empty shell (not yet the result) but then
|
||||
|
@ -192,6 +196,28 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
result.cookies = cookies;
|
||||
return cookies;
|
||||
},
|
||||
get preferredLocale(): string | undefined {
|
||||
if (preferredLocale) {
|
||||
return preferredLocale;
|
||||
}
|
||||
if (args.locales) {
|
||||
preferredLocale = computePreferredLocale(request, args.locales);
|
||||
return preferredLocale;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
get preferredLocaleList(): string[] | undefined {
|
||||
if (preferredLocaleList) {
|
||||
return preferredLocaleList;
|
||||
}
|
||||
if (args.locales) {
|
||||
preferredLocaleList = computePreferredLocaleList(request, args.locales);
|
||||
return preferredLocaleList;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
params,
|
||||
props,
|
||||
locals,
|
||||
|
|
|
@ -16,7 +16,7 @@ import { validateDynamicRouteModule, validateGetStaticPathsResult } from '../rou
|
|||
import { generatePaginateFunction } from './paginate.js';
|
||||
|
||||
interface CallGetStaticPathsOptions {
|
||||
mod: ComponentInstance;
|
||||
mod: ComponentInstance | undefined;
|
||||
route: RouteData;
|
||||
routeCache: RouteCache;
|
||||
logger: Logger;
|
||||
|
@ -33,7 +33,9 @@ export async function callGetStaticPaths({
|
|||
const cached = routeCache.get(route);
|
||||
if (cached?.staticPaths) return cached.staticPaths;
|
||||
|
||||
validateDynamicRouteModule(mod, { ssr, route });
|
||||
if (mod) {
|
||||
validateDynamicRouteModule(mod, { ssr, route });
|
||||
}
|
||||
|
||||
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
||||
if (ssr && !route.prerender) {
|
||||
|
@ -42,22 +44,26 @@ export async function callGetStaticPaths({
|
|||
return entry;
|
||||
}
|
||||
|
||||
let staticPaths: GetStaticPathsResult = [];
|
||||
// Add a check here to make TypeScript happy.
|
||||
// This is already checked in validateDynamicRouteModule().
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error('Unexpected Error.');
|
||||
}
|
||||
if (mod) {
|
||||
if (!mod.getStaticPaths) {
|
||||
throw new Error('Unexpected Error.');
|
||||
}
|
||||
|
||||
// Calculate your static paths.
|
||||
let staticPaths: GetStaticPathsResult = [];
|
||||
staticPaths = await mod.getStaticPaths({
|
||||
// Q: Why the cast?
|
||||
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
||||
paginate: generatePaginateFunction(route) as PaginateFunction,
|
||||
rss() {
|
||||
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
||||
},
|
||||
});
|
||||
if (mod) {
|
||||
// Calculate your static paths.
|
||||
staticPaths = await mod.getStaticPaths({
|
||||
// Q: Why the cast?
|
||||
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
||||
paginate: generatePaginateFunction(route) as PaginateFunction,
|
||||
rss() {
|
||||
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateGetStaticPathsResult(staticPaths, logger, route);
|
||||
|
||||
|
|
|
@ -60,11 +60,9 @@ function getParts(part: string, file: string) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getPattern(
|
||||
segments: RoutePart[][],
|
||||
base: string,
|
||||
addTrailingSlash: AstroConfig['trailingSlash']
|
||||
) {
|
||||
function getPattern(segments: RoutePart[][], config: AstroConfig) {
|
||||
const base = config.base;
|
||||
const addTrailingSlash = config.trailingSlash;
|
||||
const pathname = segments
|
||||
.map((segment) => {
|
||||
if (segment.length === 1 && segment[0].spread) {
|
||||
|
@ -327,7 +325,7 @@ export function createRouteManifest(
|
|||
components.push(item.file);
|
||||
const component = item.file;
|
||||
const trailingSlash = item.isPage ? settings.config.trailingSlash : 'never';
|
||||
const pattern = getPattern(segments, settings.config.base, trailingSlash);
|
||||
const pattern = getPattern(segments, settings.config);
|
||||
const generate = getRouteGenerator(segments, trailingSlash);
|
||||
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
|
||||
? `/${segments.map((segment) => segment[0].content).join('/')}`
|
||||
|
@ -388,7 +386,7 @@ export function createRouteManifest(
|
|||
const isPage = type === 'page';
|
||||
const trailingSlash = isPage ? config.trailingSlash : 'never';
|
||||
|
||||
const pattern = getPattern(segments, settings.config.base, trailingSlash);
|
||||
const pattern = getPattern(segments, settings.config);
|
||||
const generate = getRouteGenerator(segments, trailingSlash);
|
||||
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
|
||||
? `/${segments.map((segment) => segment[0].content).join('/')}`
|
||||
|
@ -435,7 +433,7 @@ export function createRouteManifest(
|
|||
return getParts(s, from);
|
||||
});
|
||||
|
||||
const pattern = getPattern(segments, settings.config.base, trailingSlash);
|
||||
const pattern = getPattern(segments, settings.config);
|
||||
const generate = getRouteGenerator(segments, trailingSlash);
|
||||
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
|
||||
? `/${segments.map((segment) => segment[0].content).join('/')}`
|
||||
|
@ -486,6 +484,115 @@ export function createRouteManifest(
|
|||
// Didn't find a good place, insert last
|
||||
routes.push(routeData);
|
||||
});
|
||||
const i18n = settings.config.experimental.i18n;
|
||||
|
||||
if (i18n && i18n.fallback) {
|
||||
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 (fallback.length > 0) {
|
||||
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}`
|
||||
);
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
routes,
|
||||
|
|
142
packages/astro/src/i18n/index.ts
Normal file
142
packages/astro/src/i18n/index.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { AstroError } from '../core/errors/index.js';
|
||||
import { MissingLocale } from '../core/errors/errors-data.js';
|
||||
import { shouldAppendForwardSlash } from '../core/build/util.js';
|
||||
import type { AstroConfig } from '../@types/astro.js';
|
||||
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
|
||||
|
||||
type GetLocaleRelativeUrl = GetLocaleOptions & {
|
||||
locale: string;
|
||||
base: string;
|
||||
locales: string[];
|
||||
trailingSlash: AstroConfig['trailingSlash'];
|
||||
format: AstroConfig['build']['format'];
|
||||
routingStrategy?: 'prefix-always' | 'prefix-other-locales';
|
||||
defaultLocale: string;
|
||||
};
|
||||
|
||||
export type GetLocaleOptions = {
|
||||
/**
|
||||
* Makes the locale URL-friendly by replacing underscores with dashes, and converting the locale to lower case.
|
||||
* @default true
|
||||
*/
|
||||
normalizeLocale?: boolean;
|
||||
/**
|
||||
* An optional path to add after the `locale`.
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* An optional path to prepend to `locale`.
|
||||
*/
|
||||
prependWith?: string;
|
||||
};
|
||||
|
||||
type GetLocaleAbsoluteUrl = GetLocaleRelativeUrl & {
|
||||
site: AstroConfig['site'];
|
||||
};
|
||||
/**
|
||||
* The base URL
|
||||
*/
|
||||
export function getLocaleRelativeUrl({
|
||||
locale,
|
||||
base,
|
||||
locales,
|
||||
trailingSlash,
|
||||
format,
|
||||
path,
|
||||
prependWith,
|
||||
normalizeLocale = true,
|
||||
routingStrategy = 'prefix-other-locales',
|
||||
defaultLocale,
|
||||
}: GetLocaleRelativeUrl) {
|
||||
if (!locales.includes(locale)) {
|
||||
throw new AstroError({
|
||||
...MissingLocale,
|
||||
message: MissingLocale.message(locale, locales),
|
||||
});
|
||||
}
|
||||
const pathsToJoin = [base, prependWith];
|
||||
const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;
|
||||
if (routingStrategy === 'prefix-always') {
|
||||
pathsToJoin.push(normalizedLocale);
|
||||
} else if (locale !== defaultLocale) {
|
||||
pathsToJoin.push(normalizedLocale);
|
||||
}
|
||||
pathsToJoin.push(path);
|
||||
|
||||
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||
return appendForwardSlash(joinPaths(...pathsToJoin));
|
||||
} else {
|
||||
return joinPaths(...pathsToJoin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The absolute URL
|
||||
*/
|
||||
export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) {
|
||||
const locale = getLocaleRelativeUrl(rest);
|
||||
if (site) {
|
||||
return joinPaths(site, locale);
|
||||
} else {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
type GetLocalesBaseUrl = GetLocaleOptions & {
|
||||
base: string;
|
||||
locales: string[];
|
||||
trailingSlash: AstroConfig['trailingSlash'];
|
||||
format: AstroConfig['build']['format'];
|
||||
routingStrategy?: 'prefix-always' | 'prefix-other-locales';
|
||||
defaultLocale: string;
|
||||
};
|
||||
|
||||
export function getLocaleRelativeUrlList({
|
||||
base,
|
||||
locales,
|
||||
trailingSlash,
|
||||
format,
|
||||
path,
|
||||
prependWith,
|
||||
normalizeLocale = false,
|
||||
routingStrategy = 'prefix-other-locales',
|
||||
defaultLocale,
|
||||
}: GetLocalesBaseUrl) {
|
||||
return locales.map((locale) => {
|
||||
const pathsToJoin = [base, prependWith];
|
||||
const normalizedLocale = normalizeLocale ? normalizeTheLocale(locale) : locale;
|
||||
|
||||
if (routingStrategy === 'prefix-always') {
|
||||
pathsToJoin.push(normalizedLocale);
|
||||
} else if (locale !== defaultLocale) {
|
||||
pathsToJoin.push(normalizedLocale);
|
||||
}
|
||||
pathsToJoin.push(path);
|
||||
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||
return appendForwardSlash(joinPaths(...pathsToJoin));
|
||||
} else {
|
||||
return joinPaths(...pathsToJoin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl) {
|
||||
const locales = getLocaleRelativeUrlList(rest);
|
||||
return locales.map((locale) => {
|
||||
if (site) {
|
||||
return joinPaths(site, locale);
|
||||
} else {
|
||||
return locale;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Given a locale, this function:
|
||||
* - replaces the `_` with a `-`;
|
||||
* - transforms all letters to be lower case;
|
||||
*/
|
||||
export function normalizeTheLocale(locale: string): string {
|
||||
return locale.replaceAll('_', '-').toLowerCase();
|
||||
}
|
81
packages/astro/src/i18n/middleware.ts
Normal file
81
packages/astro/src/i18n/middleware.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type { MiddlewareEndpointHandler } from '../@types/astro.js';
|
||||
import type { SSRManifest } from '../@types/astro.js';
|
||||
import { joinPaths } from '@astrojs/internal-helpers/path';
|
||||
|
||||
// Checks if the pathname doesn't have any locale, exception for the defaultLocale, which is ignored on purpose
|
||||
function checkIsLocaleFree(pathname: string, locales: string[]): boolean {
|
||||
for (const locale of locales) {
|
||||
if (pathname.includes(`/${locale}`)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function createI18nMiddleware(
|
||||
i18n: SSRManifest['i18n'],
|
||||
base: SSRManifest['base']
|
||||
): MiddlewareEndpointHandler | undefined {
|
||||
if (!i18n) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return async (context, next) => {
|
||||
if (!i18n) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
const { locales, defaultLocale, fallback } = i18n;
|
||||
const url = context.url;
|
||||
|
||||
const response = await next();
|
||||
|
||||
if (response instanceof Response) {
|
||||
const separators = url.pathname.split('/');
|
||||
const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`);
|
||||
const isLocaleFree = checkIsLocaleFree(url.pathname, i18n.locales);
|
||||
if (i18n.routingStrategy === '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.routingStrategy === 'prefix-always') {
|
||||
if (url.pathname === base || url.pathname === base + '/') {
|
||||
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 (isLocaleFree) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response.status >= 300 && fallback) {
|
||||
const fallbackKeys = i18n.fallback ? Object.keys(i18n.fallback) : [];
|
||||
|
||||
const urlLocale = separators.find((s) => locales.includes(s));
|
||||
|
||||
if (urlLocale && fallbackKeys.includes(urlLocale)) {
|
||||
const fallbackLocale = 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 === defaultLocale) {
|
||||
newPathname = url.pathname.replace(`/${urlLocale}`, ``);
|
||||
} else {
|
||||
newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
|
||||
}
|
||||
|
||||
return context.redirect(newPathname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
66
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
66
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import * as vite from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
|
||||
const virtualModuleId = 'astro:i18n';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
type AstroInternationalization = {
|
||||
settings: AstroSettings;
|
||||
};
|
||||
|
||||
export default function astroInternationalization({
|
||||
settings,
|
||||
}: AstroInternationalization): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:i18n',
|
||||
enforce: 'pre',
|
||||
async resolveId(id) {
|
||||
if (id === virtualModuleId) {
|
||||
return resolvedVirtualModuleId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `
|
||||
import {
|
||||
getLocaleRelativeUrl as _getLocaleRelativeUrl,
|
||||
getLocaleRelativeUrlList as _getLocaleRelativeUrlList,
|
||||
getLocaleAbsoluteUrl as _getLocaleAbsoluteUrl,
|
||||
getLocaleAbsoluteUrlList as _getLocaleAbsoluteUrlList,
|
||||
|
||||
} from "astro/i18n";
|
||||
|
||||
const base = ${JSON.stringify(settings.config.base)};
|
||||
const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)};
|
||||
const format = ${JSON.stringify(settings.config.build.format)};
|
||||
const site = ${JSON.stringify(settings.config.site)};
|
||||
const i18n = ${JSON.stringify(settings.config.experimental.i18n)};
|
||||
|
||||
export const getLocaleRelativeUrl = (locale, path = "", opts) => _getLocaleRelativeUrl({
|
||||
locale,
|
||||
path,
|
||||
base,
|
||||
trailingSlash,
|
||||
format,
|
||||
...i18n,
|
||||
...opts
|
||||
});
|
||||
export const getLocaleAbsoluteUrl = (locale, path = "", opts) => _getLocaleAbsoluteUrl({
|
||||
locale,
|
||||
path,
|
||||
base,
|
||||
trailingSlash,
|
||||
format,
|
||||
site,
|
||||
...i18n,
|
||||
...opts
|
||||
});
|
||||
|
||||
export const getLocaleRelativeUrlList = (path = "", opts) => _getLocaleRelativeUrlList({
|
||||
base, path, trailingSlash, format, ...i18n, ...opts });
|
||||
export const getLocaleAbsoluteUrlList = (path = "", opts) => _getLocaleAbsoluteUrlList({ base, path, trailingSlash, format, site, ...i18n, ...opts });
|
||||
`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -23,6 +23,9 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
|
|||
staticOutput: UNSUPPORTED,
|
||||
hybridOutput: UNSUPPORTED,
|
||||
assets: UNSUPPORTED_ASSETS_FEATURE,
|
||||
i18n: {
|
||||
detectBrowserLanguage: UNSUPPORTED,
|
||||
},
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
|
|
|
@ -91,4 +91,6 @@ export default class DevPipeline extends Pipeline {
|
|||
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
||||
return response;
|
||||
}
|
||||
|
||||
async handleFallback() {}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { baseMiddleware } from './base.js';
|
|||
import { createController } from './controller.js';
|
||||
import DevPipeline from './devPipeline.js';
|
||||
import { handleRequest } from './request.js';
|
||||
import type { SSRManifestI18n } from '../core/app/types.js';
|
||||
|
||||
export interface AstroPluginOptions {
|
||||
settings: AstroSettings;
|
||||
|
@ -85,6 +86,15 @@ export default function createVitePluginAstroServer({
|
|||
* @param renderers
|
||||
*/
|
||||
export function createDevelopmentManifest(settings: AstroSettings): SSRManifest {
|
||||
let i18nManifest: SSRManifestI18n | undefined = undefined;
|
||||
if (settings.config.experimental.i18n) {
|
||||
i18nManifest = {
|
||||
fallback: settings.config.experimental.i18n.fallback,
|
||||
routingStrategy: settings.config.experimental.i18n.routingStrategy,
|
||||
defaultLocale: settings.config.experimental.i18n.defaultLocale,
|
||||
locales: settings.config.experimental.i18n.locales,
|
||||
};
|
||||
}
|
||||
return {
|
||||
compressHTML: settings.config.compressHTML,
|
||||
assets: new Set(),
|
||||
|
@ -99,5 +109,6 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
|
|||
? new URL(settings.config.base, settings.config.site).toString()
|
||||
: settings.config.site,
|
||||
componentMetadata: new Map(),
|
||||
i18n: i18nManifest,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,12 @@ import type {
|
|||
} from '../@types/astro.js';
|
||||
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
|
||||
import { createRenderContext, getParamsAndProps, type SSROptions } from '../core/render/index.js';
|
||||
import {
|
||||
createRenderContext,
|
||||
getParamsAndProps,
|
||||
type RenderContext,
|
||||
type SSROptions,
|
||||
} from '../core/render/index.js';
|
||||
import { createRequest } from '../core/request.js';
|
||||
import { matchAllRoutes } from '../core/routing/index.js';
|
||||
import { isPage, resolveIdToUrl } from '../core/util.js';
|
||||
|
@ -24,6 +29,8 @@ import { preload } from './index.js';
|
|||
import { getComponentMetadata } from './metadata.js';
|
||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||
import { getScriptsForURL } from './scripts.js';
|
||||
import { createI18nMiddleware } from '../i18n/middleware.js';
|
||||
import { sequence } from '../core/middleware/index.js';
|
||||
|
||||
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||
|
||||
|
@ -53,7 +60,8 @@ export async function matchRoute(
|
|||
): Promise<MatchedRoute | undefined> {
|
||||
const env = pipeline.getEnvironment();
|
||||
const { routeCache, logger } = env;
|
||||
const matches = matchAllRoutes(pathname, manifestData);
|
||||
let matches = matchAllRoutes(pathname, manifestData);
|
||||
|
||||
const preloadedMatches = await getSortedPreloadedMatches({
|
||||
pipeline,
|
||||
matches,
|
||||
|
@ -158,70 +166,136 @@ export async function handleRoute({
|
|||
const config = pipeline.getConfig();
|
||||
const moduleLoader = pipeline.getModuleLoader();
|
||||
const { logger } = env;
|
||||
if (!matchedRoute) {
|
||||
if (!matchedRoute && !config.experimental.i18n) {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
|
||||
const filePath: URL | undefined = matchedRoute.filePath;
|
||||
const { route, preloadedComponent } = matchedRoute;
|
||||
const buildingToSSR = isServerLikeOutput(config);
|
||||
|
||||
// Headers are only available when using SSR.
|
||||
const request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
method: incomingRequest.method,
|
||||
body,
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : undefined,
|
||||
locals: Reflect.get(incomingRequest, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
|
||||
});
|
||||
|
||||
// Set user specified headers to response object.
|
||||
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
|
||||
if (value) incomingResponse.setHeader(name, value);
|
||||
}
|
||||
|
||||
const options: SSROptions = {
|
||||
env,
|
||||
filePath,
|
||||
preload: preloadedComponent,
|
||||
pathname,
|
||||
request,
|
||||
route,
|
||||
};
|
||||
let request: Request;
|
||||
let renderContext: RenderContext;
|
||||
let mod: ComponentInstance | undefined = undefined;
|
||||
let options: SSROptions | undefined = undefined;
|
||||
let route: RouteData;
|
||||
const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir);
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
|
||||
if (!matchedRoute) {
|
||||
if (config.experimental.i18n) {
|
||||
const locales = config.experimental.i18n.locales;
|
||||
const pathNameHasLocale = pathname
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.some((segment) => {
|
||||
return locales.includes(segment);
|
||||
});
|
||||
if (!pathNameHasLocale) {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
});
|
||||
route = {
|
||||
component: '',
|
||||
generate(_data: any): string {
|
||||
return '';
|
||||
},
|
||||
params: [],
|
||||
pattern: new RegExp(''),
|
||||
prerender: false,
|
||||
segments: [],
|
||||
type: 'fallback',
|
||||
route: '',
|
||||
};
|
||||
renderContext = await createRenderContext({
|
||||
request,
|
||||
pathname,
|
||||
env,
|
||||
mod,
|
||||
route,
|
||||
});
|
||||
} else {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
}
|
||||
} else {
|
||||
const filePath: URL | undefined = matchedRoute.filePath;
|
||||
const { preloadedComponent } = matchedRoute;
|
||||
route = matchedRoute.route;
|
||||
// Headers are only available when using SSR.
|
||||
request = createRequest({
|
||||
url,
|
||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||
method: incomingRequest.method,
|
||||
body,
|
||||
logger,
|
||||
ssr: buildingToSSR,
|
||||
clientAddress: buildingToSSR ? incomingRequest.socket.remoteAddress : undefined,
|
||||
locals: Reflect.get(incomingRequest, clientLocalsSymbol), // Allows adapters to pass in locals in dev mode.
|
||||
});
|
||||
|
||||
// Set user specified headers to response object.
|
||||
for (const [name, value] of Object.entries(config.server.headers ?? {})) {
|
||||
if (value) incomingResponse.setHeader(name, value);
|
||||
}
|
||||
|
||||
options = {
|
||||
env,
|
||||
filePath,
|
||||
preload: preloadedComponent,
|
||||
pathname,
|
||||
request,
|
||||
route,
|
||||
};
|
||||
if (middleware) {
|
||||
options.middleware = middleware;
|
||||
}
|
||||
|
||||
mod = options.preload;
|
||||
|
||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||
pipeline,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
|
||||
const i18n = pipeline.getConfig().experimental.i18n;
|
||||
|
||||
renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
pathname: options.pathname,
|
||||
scripts,
|
||||
links,
|
||||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
locales: i18n ? i18n.locales : undefined,
|
||||
});
|
||||
}
|
||||
const mod = options.preload;
|
||||
|
||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||
pipeline,
|
||||
filePath: options.filePath,
|
||||
});
|
||||
const onRequest = middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
||||
if (config.experimental.i18n) {
|
||||
const i18Middleware = createI18nMiddleware(config.experimental.i18n, config.base);
|
||||
|
||||
const renderContext = await createRenderContext({
|
||||
request: options.request,
|
||||
pathname: options.pathname,
|
||||
scripts,
|
||||
links,
|
||||
styles,
|
||||
componentMetadata: metadata,
|
||||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
});
|
||||
const onRequest = options.middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
||||
if (onRequest) {
|
||||
if (i18Middleware) {
|
||||
if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(sequence(i18Middleware, onRequest));
|
||||
} else {
|
||||
pipeline.setMiddlewareFunction(i18Middleware);
|
||||
}
|
||||
} else if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest);
|
||||
}
|
||||
} else if (onRequest) {
|
||||
pipeline.setMiddlewareFunction(onRequest);
|
||||
}
|
||||
|
||||
let response = await pipeline.renderRoute(renderContext, mod);
|
||||
if (response.status === 404 && has404Route(manifestData)) {
|
||||
const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
|
||||
if (fourOhFourRoute?.route !== options.route)
|
||||
if (options && fourOhFourRoute?.route !== options.route)
|
||||
return handleRoute({
|
||||
...options,
|
||||
matchedRoute: fourOhFourRoute,
|
||||
|
|
14
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
14
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
base: "new-site",
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
],
|
||||
routingStrategy: "prefix-always"
|
||||
}
|
||||
}
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-base/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-base",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/en/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hola
|
||||
</body>
|
||||
</html>
|
17
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
17
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
base: "new-site",
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
],
|
||||
fallback: {
|
||||
"it": "en",
|
||||
pt: "en"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-fallback/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-fallabck",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/end.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/end.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
End
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Oi essa e start
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/src/pages/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Start
|
||||
</body>
|
||||
</html>
|
14
packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
vendored
Normal file
14
packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
],
|
||||
routingStrategy: "prefix-always"
|
||||
}
|
||||
},
|
||||
base: "/new-site"
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-prefix-always/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-always/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-prefix-always",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/en/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Start
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-always/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Oi essa e start
|
||||
</body>
|
||||
</html>
|
15
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/astro.config.mjs
vendored
Normal file
15
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
],
|
||||
routingStrategy: "prefix-other-locales"
|
||||
},
|
||||
|
||||
},
|
||||
base: "/new-site"
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-prefix-other-locales",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Oi essa e start
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-prefix-other-locales/src/pages/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Start
|
||||
</body>
|
||||
</html>
|
12
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/astro.config.mjs
vendored
Normal file
12
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing-preferred-language",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/en/end.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/en/end.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
End
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
const locale = Astro.preferredLocale;
|
||||
const localeList = Astro.preferredLocaleList;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Locale: {locale ? locale : "none"}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hola
|
||||
</body>
|
||||
</html>
|
12
packages/astro/test/fixtures/i18n-routing/astro.config.mjs
vendored
Normal file
12
packages/astro/test/fixtures/i18n-routing/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig} from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: [
|
||||
'en', 'pt', 'it'
|
||||
]
|
||||
}
|
||||
},
|
||||
})
|
8
packages/astro/test/fixtures/i18n-routing/package.json
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/i18n-routing",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
18
packages/astro/test/fixtures/i18n-routing/src/pages/en/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing/src/pages/en/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hello world" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing/src/pages/en/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing/src/pages/en/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
15
packages/astro/test/fixtures/i18n-routing/src/pages/preferred-locale.astro
vendored
Normal file
15
packages/astro/test/fixtures/i18n-routing/src/pages/preferred-locale.astro
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
const locale = Astro.preferredLocale;
|
||||
const localeList = Astro.preferredLocaleList;
|
||||
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Locale: {locale ? locale : "none"}
|
||||
Locale list: {localeList.length > 0 ? localeList.join(", ") : "empty"}
|
||||
</body>
|
||||
</html>
|
18
packages/astro/test/fixtures/i18n-routing/src/pages/pt/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing/src/pages/pt/blog/[id].astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{params: {id: '1'}, props: { content: "Hola mundo" }},
|
||||
{params: {id: '2'}, props: { content: "Eat Something" }},
|
||||
{params: {id: '3'}, props: { content: "How are you?" }},
|
||||
];
|
||||
}
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
8
packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing/src/pages/pt/start.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hola
|
||||
</body>
|
||||
</html>
|
916
packages/astro/test/i18-routing.test.js
Normal file
916
packages/astro/test/i18-routing.test.js
Normal file
|
@ -0,0 +1,916 @@
|
|||
import { loadFixture } from './test-utils.js';
|
||||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import testAdapter from './test-adapter.js';
|
||||
|
||||
describe('[DEV] i18n routing', () => {
|
||||
describe('i18n routing', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
const response = await fixture.fetch('/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
|
||||
const response2 = await fixture.fetch('/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
|
||||
const response2 = await fixture.fetch('/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
const response = await fixture.fetch('/it/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing, with base', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-base/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
const response = await fixture.fetch('/new-site/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hola');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-other-locales]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-other-locales/',
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'pt', 'it'],
|
||||
fallback: {
|
||||
it: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the default locale without prefix', async () => {
|
||||
const response = await fixture.fetch('/new-site/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should return 404 when route contains the default locale', async () => {
|
||||
const response = await fixture.fetch('/new-site/en/start');
|
||||
expect(response.status).to.equal(404);
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/en/blog/1');
|
||||
expect(response2.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it('should redirect to the english locale, which is the first fallback', async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-always]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should redirect to the index of the default locale', async () => {
|
||||
const response = await fixture.fetch('/new-site');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Hello');
|
||||
});
|
||||
|
||||
it('should not render the default locale without prefix', async () => {
|
||||
const response = await fixture.fetch('/new-site/start');
|
||||
expect(response.status).to.equal(404);
|
||||
expect(await response.text()).not.includes('Start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/blog/1');
|
||||
expect(response2.status).to.equal(404);
|
||||
expect(await response2.text()).not.includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render the default locale with prefix', async () => {
|
||||
const response = await fixture.fetch('/new-site/en/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/en/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it('should not redirect to the english locale', async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing fallback', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
/** @type {import('./test-utils').DevServer} */
|
||||
let devServer;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-fallback/',
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'pt', 'it'],
|
||||
fallback: {
|
||||
it: 'en',
|
||||
},
|
||||
routingStrategy: 'prefix-other-locales',
|
||||
},
|
||||
},
|
||||
});
|
||||
devServer = await fixture.startDevServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await devServer.stop();
|
||||
});
|
||||
|
||||
it('should render the default locale without prefix', async () => {
|
||||
const response = await fixture.fetch('/new-site/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
const response = await fixture.fetch('/new-site/pt/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
|
||||
const response2 = await fixture.fetch('/new-site/pt/blog/1');
|
||||
expect(response2.status).to.equal(200);
|
||||
expect(await response2.text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it('should redirect to the english locale, which is the first fallback', async () => {
|
||||
const response = await fixture.fetch('/new-site/it/start');
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
const response = await fixture.fetch('/new-site/fr/start');
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[SSG] i18n routing', () => {
|
||||
describe('i18n routing', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let html = await fixture.readFile('/en/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Start');
|
||||
|
||||
html = await fixture.readFile('/en/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let html = await fixture.readFile('/pt/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Oi essa e start');
|
||||
|
||||
html = await fixture.readFile('/pt/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
try {
|
||||
await fixture.readFile('/it/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
try {
|
||||
await fixture.readFile('/fr/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing, with base', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-base/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let html = await fixture.readFile('/en/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello');
|
||||
|
||||
html = await fixture.readFile('/en/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let html = await fixture.readFile('/pt/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola');
|
||||
|
||||
html = await fixture.readFile('/pt/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
try {
|
||||
await fixture.readFile('/it/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
try {
|
||||
await fixture.readFile('/fr/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-other-locales]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-other-locales/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let html = await fixture.readFile('/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Start');
|
||||
|
||||
html = await fixture.readFile('/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should return 404 when route contains the default locale', async () => {
|
||||
try {
|
||||
await fixture.readFile('/start/en/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let html = await fixture.readFile('/pt/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Oi essa e start');
|
||||
|
||||
html = await fixture.readFile('/pt/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
try {
|
||||
await fixture.readFile('/it/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
try {
|
||||
await fixture.readFile('/fr/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-always]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('should redirect to the index of the default locale', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
expect(html).to.include('http-equiv="refresh');
|
||||
expect(html).to.include('url=/new-site/en');
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let html = await fixture.readFile('/en/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Start');
|
||||
|
||||
html = await fixture.readFile('/en/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let html = await fixture.readFile('/pt/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Oi essa e start');
|
||||
|
||||
html = await fixture.readFile('/pt/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
try {
|
||||
await fixture.readFile('/it/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
try {
|
||||
await fixture.readFile('/fr/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with fallback', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-fallback/',
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'pt', 'it'],
|
||||
fallback: {
|
||||
it: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let html = await fixture.readFile('/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Start');
|
||||
|
||||
html = await fixture.readFile('/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hello world');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let html = await fixture.readFile('/pt/start/index.html');
|
||||
let $ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Oi essa e start');
|
||||
|
||||
html = await fixture.readFile('/pt/blog/1/index.html');
|
||||
$ = cheerio.load(html);
|
||||
expect($('body').text()).includes('Hola mundo');
|
||||
});
|
||||
|
||||
it('should redirect to the english locale, which is the first fallback', async () => {
|
||||
const html = await fixture.readFile('/it/start/index.html');
|
||||
expect(html).to.include('http-equiv="refresh');
|
||||
expect(html).to.include('url=/new-site/start');
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
try {
|
||||
await fixture.readFile('/fr/start/index.html');
|
||||
// failed
|
||||
return false;
|
||||
} catch {
|
||||
// success
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[SSR] i18n routing', () => {
|
||||
let app;
|
||||
describe('default', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should redirect to the index of the default locale', async () => {
|
||||
let request = new Request('http://example.com/new-site');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(302);
|
||||
expect(response.headers.get('location')).to.equal('/new-site/en');
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let request = new Request('http://example.com/en/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let request = new Request('http://example.com/pt/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
let request = new Request('http://example.com/it/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
let request = new Request('http://example.com/fr/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with base', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let request = new Request('http://example.com/en/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let request = new Request('http://example.com/new-site/pt/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
let request = new Request('http://example.com/new-site/it/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
let request = new Request('http://example.com/new-site/fr/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-other-locales]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-other-locales/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let request = new Request('http://example.com/new-site/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it('should return 404 if route contains the default locale', async () => {
|
||||
let request = new Request('http://example.com/new-site/en/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let request = new Request('http://example.com/new-site/pt/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
let request = new Request('http://example.com/new-site/it/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
let request = new Request('http://example.com/new-site/fr/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing with routing strategy [prefix-always]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should redirect the index to the default locale', async () => {
|
||||
let request = new Request('http://example.com/new-site');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(302);
|
||||
expect(response.headers.get('location')).to.equal('/new-site/en');
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let request = new Request('http://example.com/new-site/en/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let request = new Request('http://example.com/pt/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
});
|
||||
|
||||
it("should NOT render the default locale if there isn't a fallback and the route is missing", async () => {
|
||||
let request = new Request('http://example.com/it/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
let request = new Request('http://example.com/fr/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with fallback', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-fallback/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'pt', 'it'],
|
||||
fallback: {
|
||||
it: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should render the en locale', async () => {
|
||||
let request = new Request('http://example.com/new-site/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Start');
|
||||
});
|
||||
|
||||
it('should render localised page correctly', async () => {
|
||||
let request = new Request('http://example.com/new-site/pt/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Oi essa e start');
|
||||
});
|
||||
|
||||
it('should redirect to the english locale, which is the first fallback', async () => {
|
||||
let request = new Request('http://example.com/new-site/it/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(302);
|
||||
expect(response.headers.get('location')).to.equal('/new-site/start');
|
||||
});
|
||||
|
||||
it("should render a 404 because the route `fr` isn't included in the list of locales of the configuration", async () => {
|
||||
let request = new Request('http://example.com/new-site/fr/start');
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preferred locale', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should not render the locale when the value is *', async () => {
|
||||
let request = new Request('http://example.com/preferred-locale', {
|
||||
headers: {
|
||||
'Accept-Language': '*',
|
||||
},
|
||||
});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Locale: none');
|
||||
});
|
||||
|
||||
it('should render the locale pt', async () => {
|
||||
let request = new Request('http://example.com/preferred-locale', {
|
||||
headers: {
|
||||
'Accept-Language': 'pt',
|
||||
},
|
||||
});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Locale: pt');
|
||||
});
|
||||
|
||||
it('should render empty locales', async () => {
|
||||
let request = new Request('http://example.com/preferred-locale', {
|
||||
headers: {
|
||||
'Accept-Language': 'fr;q=0.1,fr-AU;q=0.9',
|
||||
},
|
||||
});
|
||||
let response = await app.render(request);
|
||||
const text = await response.text();
|
||||
expect(response.status).to.equal(200);
|
||||
expect(text).includes('Locale: none');
|
||||
expect(text).includes('Locale list: empty');
|
||||
});
|
||||
|
||||
it('should render none as preferred locale, but have a list of locales that correspond to the initial locales', async () => {
|
||||
let request = new Request('http://example.com/preferred-locale', {
|
||||
headers: {
|
||||
'Accept-Language': '*',
|
||||
},
|
||||
});
|
||||
let response = await app.render(request);
|
||||
const text = await response.text();
|
||||
expect(response.status).to.equal(200);
|
||||
expect(text).includes('Locale: none');
|
||||
expect(text).includes('Locale list: en, pt, it');
|
||||
});
|
||||
|
||||
describe('in case the configured locales use underscores', () => {
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en_AU', 'pt_BR', 'es_US'],
|
||||
},
|
||||
},
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('they should be still considered when parsing the Accept-Language header', async () => {
|
||||
let request = new Request('http://example.com/preferred-locale', {
|
||||
headers: {
|
||||
'Accept-Language': 'en-AU;q=0.1,pt-BR;q=0.9',
|
||||
},
|
||||
});
|
||||
let response = await app.render(request);
|
||||
const text = await response.text();
|
||||
expect(response.status).to.equal(200);
|
||||
expect(text).includes('Locale: pt_BR');
|
||||
expect(text).includes('Locale list: pt_BR, en_AU');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -77,4 +77,87 @@ describe('Config Validation', () => {
|
|||
'The value of `outDir` must not point to a path within the folder set as `publicDir`, this will cause an infinite loop'
|
||||
);
|
||||
});
|
||||
|
||||
describe('i18n', async () => {
|
||||
it('defaultLocale is not in locales', async () => {
|
||||
const configError = await validateConfig(
|
||||
{
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['es'],
|
||||
},
|
||||
},
|
||||
},
|
||||
process.cwd()
|
||||
).catch((err) => err);
|
||||
expect(configError instanceof z.ZodError).to.equal(true);
|
||||
expect(configError.errors[0].message).to.equal(
|
||||
'The default locale `en` is not present in the `i18n.locales` array.'
|
||||
);
|
||||
});
|
||||
|
||||
it('errors if a fallback value does not exist', async () => {
|
||||
const configError = await validateConfig(
|
||||
{
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['es', 'en'],
|
||||
fallback: {
|
||||
es: 'it',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
process.cwd()
|
||||
).catch((err) => err);
|
||||
expect(configError instanceof z.ZodError).to.equal(true);
|
||||
expect(configError.errors[0].message).to.equal(
|
||||
"The locale `it` value in the `i18n.fallback` record doesn't exist in the `i18n.locales` array."
|
||||
);
|
||||
});
|
||||
|
||||
it('errors if a fallback key does not exist', async () => {
|
||||
const configError = await validateConfig(
|
||||
{
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['es', 'en'],
|
||||
fallback: {
|
||||
it: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
process.cwd()
|
||||
).catch((err) => err);
|
||||
expect(configError instanceof z.ZodError).to.equal(true);
|
||||
expect(configError.errors[0].message).to.equal(
|
||||
"The locale `it` key in the `i18n.fallback` record doesn't exist in the `i18n.locales` array."
|
||||
);
|
||||
});
|
||||
|
||||
it('errors if a fallback key contains the default locale', async () => {
|
||||
const configError = await validateConfig(
|
||||
{
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['es', 'en'],
|
||||
fallback: {
|
||||
en: 'es',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
process.cwd()
|
||||
).catch((err) => err);
|
||||
expect(configError instanceof z.ZodError).to.equal(true);
|
||||
expect(configError.errors[0].message).to.equal(
|
||||
"You can't use the default locale as a key. The default locale can only be used as value."
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
1071
packages/astro/test/units/i18n/astro_i18n.js
Normal file
1071
packages/astro/test/units/i18n/astro_i18n.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,9 +9,10 @@ import {
|
|||
renderHead,
|
||||
Fragment,
|
||||
} from '../../../dist/runtime/server/index.js';
|
||||
import { createRenderContext, tryRenderRoute } from '../../../dist/core/render/index.js';
|
||||
import { createRenderContext } from '../../../dist/core/render/index.js';
|
||||
import { createBasicEnvironment } from '../test-utils.js';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Pipeline } from '../../../dist/core/pipeline.js';
|
||||
|
||||
const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
|
||||
|
||||
|
@ -97,7 +98,8 @@ describe('core/render', () => {
|
|||
env,
|
||||
});
|
||||
|
||||
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||
const pipeline = new Pipeline(env);
|
||||
const response = await pipeline.renderRoute(ctx, PageModule);
|
||||
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
@ -178,7 +180,9 @@ describe('core/render', () => {
|
|||
mod: PageModule,
|
||||
});
|
||||
|
||||
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||
const pipeline = new Pipeline(env);
|
||||
|
||||
const response = await pipeline.renderRoute(ctx, PageModule);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
|
@ -225,7 +229,8 @@ describe('core/render', () => {
|
|||
mod: PageModule,
|
||||
});
|
||||
|
||||
const response = await tryRenderRoute(ctx, env, PageModule);
|
||||
const pipeline = new Pipeline(env);
|
||||
const response = await pipeline.renderRoute(ctx, PageModule);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
|
|
|
@ -6,13 +6,10 @@ import {
|
|||
renderSlot,
|
||||
} from '../../../dist/runtime/server/index.js';
|
||||
import { jsx } from '../../../dist/jsx-runtime/index.js';
|
||||
import {
|
||||
createRenderContext,
|
||||
tryRenderRoute,
|
||||
loadRenderer,
|
||||
} from '../../../dist/core/render/index.js';
|
||||
import { createRenderContext, loadRenderer } from '../../../dist/core/render/index.js';
|
||||
import { createAstroJSXComponent, renderer as jsxRenderer } from '../../../dist/jsx/index.js';
|
||||
import { createBasicEnvironment } from '../test-utils.js';
|
||||
import { Pipeline } from '../../../dist/core/pipeline.js';
|
||||
|
||||
const createAstroModule = (AstroComponent) => ({ default: AstroComponent });
|
||||
const loadJSXRenderer = () => loadRenderer(jsxRenderer, { import: (s) => import(s) });
|
||||
|
@ -51,7 +48,8 @@ describe('core/render', () => {
|
|||
mod,
|
||||
});
|
||||
|
||||
const response = await tryRenderRoute(ctx, env, mod);
|
||||
const pipeline = new Pipeline(env);
|
||||
const response = await pipeline.renderRoute(ctx, mod);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
|
||||
|
@ -96,7 +94,8 @@ describe('core/render', () => {
|
|||
env,
|
||||
mod,
|
||||
});
|
||||
const response = await tryRenderRoute(ctx, env, mod);
|
||||
const pipeline = new Pipeline(env);
|
||||
const response = await pipeline.renderRoute(ctx, mod);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
|
||||
|
@ -123,7 +122,8 @@ describe('core/render', () => {
|
|||
mod,
|
||||
});
|
||||
|
||||
const response = await tryRenderRoute(ctx, env, mod);
|
||||
const pipeline = new Pipeline(env);
|
||||
const response = await pipeline.renderRoute(ctx, mod);
|
||||
|
||||
try {
|
||||
await response.text();
|
||||
|
|
|
@ -20,6 +20,7 @@ process.on('SIGTERM', exit);
|
|||
// if you make any changes to the flow or wording here.
|
||||
export async function main() {
|
||||
// Clear console because PNPM startup is super ugly
|
||||
// eslint-disable-next-line no-console
|
||||
console.clear();
|
||||
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
|
||||
// to no longer require `--` to pass args and instead pass `--` directly to us. This
|
||||
|
|
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
|
@ -2797,6 +2797,42 @@ importers:
|
|||
specifier: ^10.17.1
|
||||
version: 10.18.1
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing-base:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing-fallback:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing-prefix-always:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing-prefix-other-locales:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/i18n-routing-redirect-preferred-language:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/import-ts-with-js:
|
||||
dependencies:
|
||||
astro:
|
||||
|
|
Loading…
Add table
Reference in a new issue