Compare commits
4 commits
main
...
feat/i18n-
Author | SHA1 | Date | |
---|---|---|---|
|
a4015a22c4 | ||
|
7678ef33a0 | ||
|
9f3f110268 | ||
|
18223d9cde |
49 changed files with 1369 additions and 110 deletions
10
packages/astro/client.d.ts
vendored
10
packages/astro/client.d.ts
vendored
|
@ -127,6 +127,16 @@ declare module 'astro:transitions/client' {
|
||||||
export const navigate: TransitionRouterModule['navigate'];
|
export const navigate: TransitionRouterModule['navigate'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'astro:i18n' {
|
||||||
|
type I18nModule = typeof import('./dist/i18n/index.js');
|
||||||
|
|
||||||
|
// TODO: documentation
|
||||||
|
export const getI18nBaseUrl: (locale: string) => string;
|
||||||
|
|
||||||
|
// TODO: documentation
|
||||||
|
export const getLocalesBaseUrl: () => string[];
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'astro:middleware' {
|
declare module 'astro:middleware' {
|
||||||
export * from 'astro/middleware/namespace';
|
export * from 'astro/middleware/namespace';
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,8 @@
|
||||||
"default": "./dist/core/middleware/namespace.js"
|
"default": "./dist/core/middleware/namespace.js"
|
||||||
},
|
},
|
||||||
"./transitions": "./dist/transitions/index.js",
|
"./transitions": "./dist/transitions/index.js",
|
||||||
"./transitions/router": "./dist/transitions/router.js"
|
"./transitions/router": "./dist/transitions/router.js",
|
||||||
|
"./i18n": "./dist/i18n/index.js"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#astro/*": "./dist/*.js"
|
"#astro/*": "./dist/*.js"
|
||||||
|
|
|
@ -1330,6 +1330,84 @@ export interface AstroUserConfig {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
optimizeHoistedScript?: boolean;
|
optimizeHoistedScript?: boolean;
|
||||||
|
|
||||||
|
// TODO review with docs team before merging to `main`
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n
|
||||||
|
* @type {object}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @type {object}
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* Allows to configure the beaviour of the i18n routing
|
||||||
|
*/
|
||||||
|
i18n?: {
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n.defaultLocale
|
||||||
|
* @type {string}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* The default locale of your website/application
|
||||||
|
*/
|
||||||
|
defaultLocale: string;
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n.locales
|
||||||
|
* @type {string[]}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* A list of locales supported by the website.
|
||||||
|
*/
|
||||||
|
locales: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n.fallback
|
||||||
|
* @type {Record<string, string[]>}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* The fallback system of the locales. By default, the fallback system affect the **content only**, and it doesn't
|
||||||
|
* do any redirects.
|
||||||
|
*
|
||||||
|
* This means that when attempting to navigate to a page that hasn't been translated, Astro will pull the content
|
||||||
|
* from the page of the default locale and render it. No redirects will happen.
|
||||||
|
*/
|
||||||
|
fallback?: Record<string, string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n.fallbackControl
|
||||||
|
* @type {"none" | "render" | "redirect"}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* Controls the fallback system of the internationalisation:
|
||||||
|
* - `none`: Astro will do nothing and will return `404` if a translated page isn't translated;
|
||||||
|
* - `redirect`: Astro will do a redirect to the fallback language if the translated page returns a `404`;
|
||||||
|
* - `render`: currently unsupported by Astro
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fallbackControl: 'none' | 'render' | 'redirect';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @docs
|
||||||
|
* @name experimental.i18n.detectBrowserLanguage
|
||||||
|
* @type {boolean}
|
||||||
|
* @version 3.*.*
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* Whether Astro should detect the language of the browser - usually using the `Accept-Language` header. This is a feature
|
||||||
|
* that should be supported by the adapter. If detected, the adapter can decide to redirect the user to the localised version of the website.
|
||||||
|
*
|
||||||
|
* When set to `true`, you should make sure that the adapter you're using is able to provide this feature to you.
|
||||||
|
*/
|
||||||
|
detectBrowserLanguage: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1790,6 +1868,11 @@ export type AstroFeatureMap = {
|
||||||
* The adapter can emit static assets
|
* The adapter can emit static assets
|
||||||
*/
|
*/
|
||||||
assets?: AstroAssetsFeature;
|
assets?: AstroAssetsFeature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of features that orbit around the i18n routing
|
||||||
|
*/
|
||||||
|
i18n?: AstroInternationalisationFeature;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AstroAssetsFeature {
|
export interface AstroAssetsFeature {
|
||||||
|
@ -1804,6 +1887,13 @@ export interface AstroAssetsFeature {
|
||||||
isSquooshCompatible?: boolean;
|
isSquooshCompatible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AstroInternationalisationFeature {
|
||||||
|
/**
|
||||||
|
* Wether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
|
||||||
|
*/
|
||||||
|
detectBrowserLanguage?: SupportsKind;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AstroAdapter {
|
export interface AstroAdapter {
|
||||||
name: string;
|
name: string;
|
||||||
serverEntrypoint?: string;
|
serverEntrypoint?: string;
|
||||||
|
@ -2103,7 +2193,13 @@ export interface AstroPluginOptions {
|
||||||
logger: Logger;
|
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 {
|
export interface RoutePart {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export function getOutFolder(
|
||||||
switch (routeType) {
|
switch (routeType) {
|
||||||
case 'endpoint':
|
case 'endpoint':
|
||||||
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
return new URL('.' + appendForwardSlash(npath.dirname(pathname)), outRoot);
|
||||||
|
case 'fallback':
|
||||||
case 'page':
|
case 'page':
|
||||||
case 'redirect':
|
case 'redirect':
|
||||||
switch (astroConfig.build.format) {
|
switch (astroConfig.build.format) {
|
||||||
|
@ -52,6 +53,7 @@ export function getOutFile(
|
||||||
case 'endpoint':
|
case 'endpoint':
|
||||||
return new URL(npath.basename(pathname), outFolder);
|
return new URL(npath.basename(pathname), outFolder);
|
||||||
case 'page':
|
case 'page':
|
||||||
|
case 'fallback':
|
||||||
case 'redirect':
|
case 'redirect':
|
||||||
switch (astroConfig.build.format) {
|
switch (astroConfig.build.format) {
|
||||||
case 'directory': {
|
case 'directory': {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url';
|
||||||
import type { OutputAsset, OutputChunk } from 'rollup';
|
import type { OutputAsset, OutputChunk } from 'rollup';
|
||||||
import type { BufferEncoding } from 'vfile';
|
import type { BufferEncoding } from 'vfile';
|
||||||
import type {
|
import type {
|
||||||
AstroConfig,
|
|
||||||
AstroSettings,
|
AstroSettings,
|
||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
GetStaticPathsItem,
|
GetStaticPathsItem,
|
||||||
|
@ -58,7 +57,7 @@ import type {
|
||||||
StaticBuildOptions,
|
StaticBuildOptions,
|
||||||
StylesheetAsset,
|
StylesheetAsset,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { getTimeStat } from './util.js';
|
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
|
||||||
|
|
||||||
function createEntryURL(filePath: string, outFolder: URL) {
|
function createEntryURL(filePath: string, outFolder: URL) {
|
||||||
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
|
||||||
|
@ -431,26 +430,6 @@ interface GeneratePathOptions {
|
||||||
mod: ComponentInstance;
|
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 {
|
function addPageName(pathname: string, opts: StaticBuildOptions): void {
|
||||||
const trailingSlash = opts.settings.config.trailingSlash;
|
const trailingSlash = opts.settings.config.trailingSlash;
|
||||||
const buildFormat = opts.settings.config.build.format;
|
const buildFormat = opts.settings.config.build.format;
|
||||||
|
|
|
@ -1,4 +1,29 @@
|
||||||
|
import type { AstroConfig } from '../../@types/astro.js';
|
||||||
|
|
||||||
export function getTimeStat(timeStart: number, timeEnd: number) {
|
export function getTimeStat(timeStart: number, timeEnd: number) {
|
||||||
const buildTime = timeEnd - timeStart;
|
const buildTime = timeEnd - timeStart;
|
||||||
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -271,6 +271,48 @@ export const AstroConfigSchema = z.object({
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
||||||
|
i18n: z.optional(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
defaultLocale: z.string(),
|
||||||
|
locales: z.string().array(),
|
||||||
|
fallback: z.record(z.string(), z.string().array()).optional().default({}),
|
||||||
|
detectBrowserLanguage: z.boolean().optional().default(false),
|
||||||
|
// TODO: properly add default when the feature goes of experimental
|
||||||
|
fallbackControl: z.enum(['none', 'redirect', 'render']).optional().default('none'),
|
||||||
|
})
|
||||||
|
.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 [fallbackKey, fallbackArray] of Object.entries(fallback)) {
|
||||||
|
if (!locales.includes(fallbackKey)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `The locale \`${fallbackKey}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fallbackArrayKey of fallbackArray) {
|
||||||
|
if (!locales.includes(fallbackArrayKey)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `The locale \`${fallbackArrayKey}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.strict(
|
.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.`
|
`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.`
|
||||||
|
|
|
@ -29,6 +29,7 @@ import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
|
||||||
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
|
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
|
||||||
import { joinPaths } from './path.js';
|
import { joinPaths } from './path.js';
|
||||||
|
import astroInternalization from '../i18n/vite-plugin-i18n.js';
|
||||||
|
|
||||||
interface CreateViteOptions {
|
interface CreateViteOptions {
|
||||||
settings: AstroSettings;
|
settings: AstroSettings;
|
||||||
|
@ -134,6 +135,7 @@ export async function createVite(
|
||||||
vitePluginSSRManifest(),
|
vitePluginSSRManifest(),
|
||||||
astroAssetsPlugin({ settings, logger, mode }),
|
astroAssetsPlugin({ settings, logger, mode }),
|
||||||
astroTransitions(),
|
astroTransitions(),
|
||||||
|
!!settings.config.experimental.i18n && astroInternalization({ settings, logger }),
|
||||||
],
|
],
|
||||||
publicDir: fileURLToPath(settings.config.publicDir),
|
publicDir: fileURLToPath(settings.config.publicDir),
|
||||||
root: fileURLToPath(settings.config.root),
|
root: fileURLToPath(settings.config.root),
|
||||||
|
|
|
@ -1243,5 +1243,21 @@ export const UnsupportedConfigTransformError = {
|
||||||
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
|
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
|
||||||
} satisfies ErrorData;
|
} 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: "You tried to render a route that isn't a redirect and doesn't have any component.",
|
||||||
|
} satisfies ErrorData;
|
||||||
|
|
||||||
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
|
// 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;
|
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 { createAPIContext } from '../endpoint/index.js';
|
||||||
import { sequence } from './sequence.js';
|
import { sequence } from './sequence.js';
|
||||||
|
|
||||||
function defineMiddleware(fn: MiddlewareResponseHandler) {
|
function defineMiddleware(fn: MiddlewareEndpointHandler) {
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
import { defineMiddleware } from './index.js';
|
||||||
|
|
||||||
// From SvelteKit: https://github.com/sveltejs/kit/blob/master/packages/kit/src/exports/hooks/sequence.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.
|
* 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;
|
const length = handlers.length;
|
||||||
if (!length) {
|
if (!length) {
|
||||||
const handler: MiddlewareResponseHandler = defineMiddleware((context, next) => {
|
const handler: MiddlewareEndpointHandler = defineMiddleware((context, next) => {
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
return handler;
|
return handler;
|
||||||
|
|
|
@ -67,7 +67,7 @@ export class Pipeline {
|
||||||
*/
|
*/
|
||||||
async renderRoute(
|
async renderRoute(
|
||||||
renderContext: RenderContext,
|
renderContext: RenderContext,
|
||||||
componentInstance: ComponentInstance
|
componentInstance: ComponentInstance | undefined
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const result = await this.#tryRenderRoute(
|
const result = await this.#tryRenderRoute(
|
||||||
renderContext,
|
renderContext,
|
||||||
|
@ -100,7 +100,8 @@ export class Pipeline {
|
||||||
async #tryRenderRoute<MiddlewareReturnType = Response>(
|
async #tryRenderRoute<MiddlewareReturnType = Response>(
|
||||||
renderContext: Readonly<RenderContext>,
|
renderContext: Readonly<RenderContext>,
|
||||||
env: Readonly<Environment>,
|
env: Readonly<Environment>,
|
||||||
mod: Readonly<ComponentInstance>,
|
mod: Readonly<ComponentInstance> | undefined,
|
||||||
|
|
||||||
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
onRequest?: MiddlewareHandler<MiddlewareReturnType>
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const apiContext = createAPIContext({
|
const apiContext = createAPIContext({
|
||||||
|
@ -113,6 +114,7 @@ export class Pipeline {
|
||||||
|
|
||||||
switch (renderContext.route.type) {
|
switch (renderContext.route.type) {
|
||||||
case 'page':
|
case 'page':
|
||||||
|
case 'fallback':
|
||||||
case 'redirect': {
|
case 'redirect': {
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
return await callMiddleware<Response>(
|
return await callMiddleware<Response>(
|
||||||
|
@ -138,13 +140,7 @@ export class Pipeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'endpoint': {
|
case 'endpoint': {
|
||||||
const result = await callEndpoint(
|
return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest);
|
||||||
mod as any as EndpointHandler,
|
|
||||||
env,
|
|
||||||
renderContext,
|
|
||||||
onRequest
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
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';
|
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 {
|
export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
|
||||||
const routeData = redirectRoute.redirectRoute;
|
const routeData = redirectRoute.redirectRoute;
|
||||||
const route = redirectRoute.redirect;
|
const route = redirectRoute.redirect;
|
||||||
|
|
|
@ -34,7 +34,7 @@ export type CreateRenderContextArgs = Partial<
|
||||||
> & {
|
> & {
|
||||||
route: RouteData;
|
route: RouteData;
|
||||||
request: RenderContext['request'];
|
request: RenderContext['request'];
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance | undefined;
|
||||||
env: Environment;
|
env: Environment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,12 @@ import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../
|
||||||
import type { RenderContext } from './context.js';
|
import type { RenderContext } from './context.js';
|
||||||
import type { Environment } from './environment.js';
|
import type { Environment } from './environment.js';
|
||||||
import { createResult } from './result.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 = {
|
export type RenderPage = {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance | undefined;
|
||||||
renderContext: RenderContext;
|
renderContext: RenderContext;
|
||||||
env: Environment;
|
env: Environment;
|
||||||
cookies: AstroCookies;
|
cookies: AstroCookies;
|
||||||
|
@ -29,6 +32,11 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
||||||
location: redirectRouteGenerate(renderContext.route, renderContext.params),
|
location: redirectRouteGenerate(renderContext.route, renderContext.params),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// TODO: check this one
|
||||||
|
} else if (routeIsFallback(renderContext.route)) {
|
||||||
|
return new Response(null);
|
||||||
|
} else if (!mod) {
|
||||||
|
throw new AstroError(CantRenderPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the page component before rendering the page
|
// Validate the page component before rendering the page
|
||||||
|
|
|
@ -4,9 +4,10 @@ import type { Logger } from '../logger/core.js';
|
||||||
import { routeIsRedirect } from '../redirects/index.js';
|
import { routeIsRedirect } from '../redirects/index.js';
|
||||||
import { getParams } from '../routing/params.js';
|
import { getParams } from '../routing/params.js';
|
||||||
import { RouteCache, callGetStaticPaths, findPathItemByKey } from './route-cache.js';
|
import { RouteCache, callGetStaticPaths, findPathItemByKey } from './route-cache.js';
|
||||||
|
import { routeIsFallback } from '../redirects/helpers.js';
|
||||||
|
|
||||||
interface GetParamsAndPropsOptions {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance | undefined;
|
||||||
route?: RouteData | undefined;
|
route?: RouteData | undefined;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
|
@ -26,11 +27,13 @@ export async function getParamsAndProps(opts: GetParamsAndPropsOptions): Promise
|
||||||
// This is a dynamic route, start getting the params
|
// This is a dynamic route, start getting the params
|
||||||
const params = getRouteParams(route, pathname) ?? {};
|
const params = getRouteParams(route, pathname) ?? {};
|
||||||
|
|
||||||
if (routeIsRedirect(route)) {
|
if (routeIsRedirect(route) || routeIsFallback(route)) {
|
||||||
return [params, {}];
|
return [params, {}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mod) {
|
||||||
validatePrerenderEndpointCollision(route, mod, params);
|
validatePrerenderEndpointCollision(route, mod, params);
|
||||||
|
}
|
||||||
|
|
||||||
// During build, the route cache should already be populated.
|
// During build, the route cache should already be populated.
|
||||||
// During development, the route cache is filled on-demand and may be empty.
|
// During development, the route cache is filled on-demand and may be empty.
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { validateDynamicRouteModule, validateGetStaticPathsResult } from '../rou
|
||||||
import { generatePaginateFunction } from './paginate.js';
|
import { generatePaginateFunction } from './paginate.js';
|
||||||
|
|
||||||
interface CallGetStaticPathsOptions {
|
interface CallGetStaticPathsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance | undefined;
|
||||||
route: RouteData;
|
route: RouteData;
|
||||||
routeCache: RouteCache;
|
routeCache: RouteCache;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
@ -33,7 +33,9 @@ export async function callGetStaticPaths({
|
||||||
const cached = routeCache.get(route);
|
const cached = routeCache.get(route);
|
||||||
if (cached?.staticPaths) return cached.staticPaths;
|
if (cached?.staticPaths) return cached.staticPaths;
|
||||||
|
|
||||||
|
if (mod) {
|
||||||
validateDynamicRouteModule(mod, { ssr, route });
|
validateDynamicRouteModule(mod, { ssr, route });
|
||||||
|
}
|
||||||
|
|
||||||
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
// No static paths in SSR mode. Return an empty RouteCacheEntry.
|
||||||
if (ssr && !route.prerender) {
|
if (ssr && !route.prerender) {
|
||||||
|
@ -42,14 +44,16 @@ export async function callGetStaticPaths({
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let staticPaths: GetStaticPathsResult = [];
|
||||||
// Add a check here to make TypeScript happy.
|
// Add a check here to make TypeScript happy.
|
||||||
// This is already checked in validateDynamicRouteModule().
|
// This is already checked in validateDynamicRouteModule().
|
||||||
|
if (mod) {
|
||||||
if (!mod.getStaticPaths) {
|
if (!mod.getStaticPaths) {
|
||||||
throw new Error('Unexpected Error.');
|
throw new Error('Unexpected Error.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mod) {
|
||||||
// Calculate your static paths.
|
// Calculate your static paths.
|
||||||
let staticPaths: GetStaticPathsResult = [];
|
|
||||||
staticPaths = await mod.getStaticPaths({
|
staticPaths = await mod.getStaticPaths({
|
||||||
// Q: Why the cast?
|
// 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
|
// A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
|
||||||
|
@ -58,6 +62,8 @@ export async function callGetStaticPaths({
|
||||||
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
throw new AstroError(AstroErrorData.GetStaticPathsRemovedRSSHelper);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validateGetStaticPathsResult(staticPaths, logger, route);
|
validateGetStaticPathsResult(staticPaths, logger, route);
|
||||||
|
|
||||||
|
|
58
packages/astro/src/i18n/index.ts
Normal file
58
packages/astro/src/i18n/index.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { AstroError } from '../core/errors/index.js';
|
||||||
|
import { MissingLocale } from '../core/errors/errors-data.js';
|
||||||
|
import type { AstroConfig } from '../@types/astro.js';
|
||||||
|
import { shouldAppendForwardSlash } from '../core/build/util.js';
|
||||||
|
|
||||||
|
type GetI18nBaseUrl = {
|
||||||
|
locale: string;
|
||||||
|
base: string;
|
||||||
|
locales: string[];
|
||||||
|
trailingSlash: AstroConfig['trailingSlash'];
|
||||||
|
format: AstroConfig['build']['format'];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The base URL
|
||||||
|
*/
|
||||||
|
export function getI18nBaseUrl({ locale, base, locales, trailingSlash, format }: GetI18nBaseUrl) {
|
||||||
|
if (!locales.includes(locale)) {
|
||||||
|
throw new AstroError({
|
||||||
|
...MissingLocale,
|
||||||
|
message: MissingLocale.message(locale, locales),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedLocale = normalizeLocale(locale);
|
||||||
|
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||||
|
return `${base}${normalizedLocale}/`;
|
||||||
|
} else {
|
||||||
|
return `${base}/${normalizedLocale}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetLocalesBaseUrl = {
|
||||||
|
base: string;
|
||||||
|
locales: string[];
|
||||||
|
trailingSlash: AstroConfig['trailingSlash'];
|
||||||
|
format: AstroConfig['build']['format'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocalesBaseUrl({ base, locales, trailingSlash, format }: GetLocalesBaseUrl) {
|
||||||
|
return locales.map((locale) => {
|
||||||
|
const normalizedLocale = normalizeLocale(locale);
|
||||||
|
if (shouldAppendForwardSlash(trailingSlash, format)) {
|
||||||
|
return `${base}${normalizedLocale}/`;
|
||||||
|
} else {
|
||||||
|
return `${base}/${normalizedLocale}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Given a locale, this function:
|
||||||
|
* - replaces the `_` with a `-`;
|
||||||
|
* - transforms all letters to be lower case;
|
||||||
|
*/
|
||||||
|
function normalizeLocale(locale: string): string {
|
||||||
|
return locale.replaceAll('_', '-').toLowerCase();
|
||||||
|
}
|
38
packages/astro/src/i18n/middleware.ts
Normal file
38
packages/astro/src/i18n/middleware.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import type { AstroConfig, MiddlewareEndpointHandler } from '../@types/astro.js';
|
||||||
|
import type { Logger } from '../core/logger/core.js';
|
||||||
|
|
||||||
|
export function createI18nMiddleware(
|
||||||
|
config: Readonly<AstroConfig>,
|
||||||
|
logger: Logger
|
||||||
|
): MiddlewareEndpointHandler | undefined {
|
||||||
|
const i18n = config.experimental?.i18n;
|
||||||
|
if (!i18n) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const fallbackKeys = Object.keys(i18n.fallback);
|
||||||
|
const locales = i18n.locales;
|
||||||
|
|
||||||
|
logger.debug('i18n', 'Successfully created middleware');
|
||||||
|
return async (context, next) => {
|
||||||
|
if (fallbackKeys.length <= 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await next();
|
||||||
|
if (i18n.fallbackControl === 'redirect' && response instanceof Response) {
|
||||||
|
const url = context.url;
|
||||||
|
const separators = url.pathname.split('/');
|
||||||
|
|
||||||
|
const urlLocale = separators.find((s) => locales.includes(s));
|
||||||
|
|
||||||
|
if (urlLocale && fallbackKeys.includes(urlLocale)) {
|
||||||
|
// TODO: correctly handle chain of fallback
|
||||||
|
const fallbackLocale = i18n.fallback[urlLocale][0];
|
||||||
|
const newPathname = url.pathname.replace(`/${urlLocale}`, `/${fallbackLocale}`);
|
||||||
|
return context.redirect(newPathname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
}
|
39
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
39
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as vite from 'vite';
|
||||||
|
import type { AstroSettings } from '../@types/astro.js';
|
||||||
|
import type { Logger } from '../core/logger/core.js';
|
||||||
|
|
||||||
|
const virtualModuleId = 'astro:i18n';
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
|
||||||
|
type AstroInternalization = {
|
||||||
|
settings: AstroSettings;
|
||||||
|
logger: Logger;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function astroInternalization({ settings }: AstroInternalization): vite.Plugin {
|
||||||
|
return {
|
||||||
|
name: 'astro:i18n',
|
||||||
|
async resolveId(id) {
|
||||||
|
if (id === virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
return `
|
||||||
|
import { getI18nBaseUrl as getI18nBaseUrlInternal, getLocalesBaseUrl as _getLocalesBaseUrl } from "astro/i18n";
|
||||||
|
|
||||||
|
const defaultLocale = ${JSON.stringify(settings.config.experimental.i18n!.defaultLocale)};
|
||||||
|
const locales = ${JSON.stringify(settings.config.experimental.i18n!.locales)};
|
||||||
|
const fallback = ${JSON.stringify(settings.config.experimental.i18n!.fallback)};
|
||||||
|
const base = ${JSON.stringify(settings.config.base)};
|
||||||
|
const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)};
|
||||||
|
const format = ${JSON.stringify(settings.config.build.format)};
|
||||||
|
|
||||||
|
export const getI18nBaseUrl = (locale) => getI18nBaseUrlInternal({ locale, base, locales, trailingSlash, format });
|
||||||
|
export const getLocalesBaseUrl = () => _getLocalesBaseUrl({ base, locales, trailingSlash, format });
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -23,6 +23,9 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
|
||||||
staticOutput: UNSUPPORTED,
|
staticOutput: UNSUPPORTED,
|
||||||
hybridOutput: UNSUPPORTED,
|
hybridOutput: UNSUPPORTED,
|
||||||
assets: UNSUPPORTED_ASSETS_FEATURE,
|
assets: UNSUPPORTED_ASSETS_FEATURE,
|
||||||
|
i18n: {
|
||||||
|
detectBrowserLanguage: UNSUPPORTED,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type ValidationResult = {
|
type ValidationResult = {
|
||||||
|
|
|
@ -91,4 +91,6 @@ export default class DevPipeline extends Pipeline {
|
||||||
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
async #handleEndpointResult(_: Request, response: Response): Promise<Response> {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleFallback() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,12 @@ import type {
|
||||||
} from '../@types/astro.js';
|
} from '../@types/astro.js';
|
||||||
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
import { AstroErrorData, isAstroError } from '../core/errors/index.js';
|
||||||
import { loadMiddleware } from '../core/middleware/loadMiddleware.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 { createRequest } from '../core/request.js';
|
||||||
import { matchAllRoutes } from '../core/routing/index.js';
|
import { matchAllRoutes } from '../core/routing/index.js';
|
||||||
import { isPage } from '../core/util.js';
|
import { isPage } from '../core/util.js';
|
||||||
|
@ -23,6 +28,8 @@ import { preload } from './index.js';
|
||||||
import { getComponentMetadata } from './metadata.js';
|
import { getComponentMetadata } from './metadata.js';
|
||||||
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
import { handle404Response, writeSSRResult, writeWebResponse } from './response.js';
|
||||||
import { getScriptsForURL } from './scripts.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');
|
const clientLocalsSymbol = Symbol.for('astro.locals');
|
||||||
|
|
||||||
|
@ -52,7 +59,8 @@ export async function matchRoute(
|
||||||
): Promise<MatchedRoute | undefined> {
|
): Promise<MatchedRoute | undefined> {
|
||||||
const env = pipeline.getEnvironment();
|
const env = pipeline.getEnvironment();
|
||||||
const { routeCache, logger } = env;
|
const { routeCache, logger } = env;
|
||||||
const matches = matchAllRoutes(pathname, manifestData);
|
let matches = matchAllRoutes(pathname, manifestData);
|
||||||
|
|
||||||
const preloadedMatches = await getSortedPreloadedMatches({
|
const preloadedMatches = await getSortedPreloadedMatches({
|
||||||
pipeline,
|
pipeline,
|
||||||
matches,
|
matches,
|
||||||
|
@ -157,16 +165,65 @@ export async function handleRoute({
|
||||||
const config = pipeline.getConfig();
|
const config = pipeline.getConfig();
|
||||||
const moduleLoader = pipeline.getModuleLoader();
|
const moduleLoader = pipeline.getModuleLoader();
|
||||||
const { logger } = env;
|
const { logger } = env;
|
||||||
if (!matchedRoute) {
|
if (!matchedRoute && !config.experimental.i18n) {
|
||||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath: URL | undefined = matchedRoute.filePath;
|
|
||||||
const { route, preloadedComponent } = matchedRoute;
|
|
||||||
const buildingToSSR = isServerLikeOutput(config);
|
const buildingToSSR = isServerLikeOutput(config);
|
||||||
|
|
||||||
|
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 (!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.
|
// Headers are only available when using SSR.
|
||||||
const request = createRequest({
|
request = createRequest({
|
||||||
url,
|
url,
|
||||||
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
headers: buildingToSSR ? incomingRequest.headers : new Headers(),
|
||||||
method: incomingRequest.method,
|
method: incomingRequest.method,
|
||||||
|
@ -182,7 +239,7 @@ export async function handleRoute({
|
||||||
if (value) incomingResponse.setHeader(name, value);
|
if (value) incomingResponse.setHeader(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: SSROptions = {
|
options = {
|
||||||
env,
|
env,
|
||||||
filePath,
|
filePath,
|
||||||
preload: preloadedComponent,
|
preload: preloadedComponent,
|
||||||
|
@ -190,18 +247,18 @@ export async function handleRoute({
|
||||||
request,
|
request,
|
||||||
route,
|
route,
|
||||||
};
|
};
|
||||||
const middleware = await loadMiddleware(moduleLoader, settings.config.srcDir);
|
|
||||||
if (middleware) {
|
if (middleware) {
|
||||||
options.middleware = middleware;
|
options.middleware = middleware;
|
||||||
}
|
}
|
||||||
const mod = options.preload;
|
|
||||||
|
mod = options.preload;
|
||||||
|
|
||||||
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
const { scripts, links, styles, metadata } = await getScriptsAndStyles({
|
||||||
pipeline,
|
pipeline,
|
||||||
filePath: options.filePath,
|
filePath: options.filePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderContext = await createRenderContext({
|
renderContext = await createRenderContext({
|
||||||
request: options.request,
|
request: options.request,
|
||||||
pathname: options.pathname,
|
pathname: options.pathname,
|
||||||
scripts,
|
scripts,
|
||||||
|
@ -212,15 +269,29 @@ export async function handleRoute({
|
||||||
mod,
|
mod,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
const onRequest = options.middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
}
|
||||||
|
|
||||||
|
const onRequest = middleware?.onRequest as MiddlewareEndpointHandler | undefined;
|
||||||
|
if (config.experimental.i18n) {
|
||||||
|
const i18Middleware = createI18nMiddleware(config, logger);
|
||||||
|
|
||||||
|
if (i18Middleware) {
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
|
pipeline.setMiddlewareFunction(sequence(i18Middleware, onRequest));
|
||||||
|
} else {
|
||||||
|
pipeline.setMiddlewareFunction(i18Middleware);
|
||||||
|
}
|
||||||
|
} else if (onRequest) {
|
||||||
|
pipeline.setMiddlewareFunction(onRequest);
|
||||||
|
}
|
||||||
|
} else if (onRequest) {
|
||||||
pipeline.setMiddlewareFunction(onRequest);
|
pipeline.setMiddlewareFunction(onRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = await pipeline.renderRoute(renderContext, mod);
|
let response = await pipeline.renderRoute(renderContext, mod);
|
||||||
if (response.status === 404 && has404Route(manifestData)) {
|
if (response.status === 404 && has404Route(manifestData)) {
|
||||||
const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
|
const fourOhFourRoute = await matchRoute('/404', manifestData, pipeline);
|
||||||
if (fourOhFourRoute?.route !== options.route)
|
if (options && fourOhFourRoute?.route !== options.route)
|
||||||
return handleRoute({
|
return handleRoute({
|
||||||
...options,
|
...options,
|
||||||
matchedRoute: fourOhFourRoute,
|
matchedRoute: fourOhFourRoute,
|
||||||
|
|
|
@ -339,4 +339,159 @@ describe('Development Routing', () => {
|
||||||
expect(await response.text()).includes('html: 1');
|
expect(await response.text()).includes('html: 1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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/',
|
||||||
|
});
|
||||||
|
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('Hello');
|
||||||
|
|
||||||
|
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('Hola');
|
||||||
|
|
||||||
|
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 fallback [redirect]', () => {
|
||||||
|
/** @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'],
|
||||||
|
},
|
||||||
|
fallbackControl: 'redirect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 render 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('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
13
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
13
packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig} from "astro/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "new-site",
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: [
|
||||||
|
'en', 'pt', 'it'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
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>
|
16
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
16
packages/astro/test/fixtures/i18n-routing-fallback/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig} from "astro/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "new-site",
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: [
|
||||||
|
'en', 'pt', 'it'
|
||||||
|
],
|
||||||
|
fallback: {
|
||||||
|
"it": ["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/en/blog/[id].astro
vendored
Normal file
18
packages/astro/test/fixtures/i18n-routing-fallback/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-fallback/src/pages/en/start.astro
vendored
Normal file
8
packages/astro/test/fixtures/i18n-routing-fallback/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-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>
|
||||||
|
Hola
|
||||||
|
</body>
|
||||||
|
</html>
|
13
packages/astro/test/fixtures/i18n-routing/astro.config.mjs
vendored
Normal file
13
packages/astro/test/fixtures/i18n-routing/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { defineConfig} from "astro/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: [
|
||||||
|
'en', 'pt', 'it'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
base: "/new-site"
|
||||||
|
})
|
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>
|
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>
|
|
@ -77,4 +77,66 @@ 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'
|
'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."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
377
packages/astro/test/units/i18n/getI18nBaseUrl.test.js
Normal file
377
packages/astro/test/units/i18n/getI18nBaseUrl.test.js
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
import { getI18nBaseUrl, getLocalesBaseUrl } from '../../../dist/i18n/index.js';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('getI18nBaseUrl', () => {
|
||||||
|
it('should correctly return the URL with the base', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
base: '/blog',
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en/');
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'es',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/es/');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en_US',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.throw;
|
||||||
|
|
||||||
|
// file format
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en/');
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'es',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/es/');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en_US',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly return the URL without base', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/en/');
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'es',
|
||||||
|
base: '/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/es/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle the trailing slash', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'never',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en');
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'es',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/es/');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'ignore',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en/');
|
||||||
|
|
||||||
|
// directory file
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'never',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en');
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'es',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/es/');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
// ignore + file => no trailing slash
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'ignore',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize locales', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
base: '/blog',
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'en_AU'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en_US',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en-us/');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getI18nBaseUrl({
|
||||||
|
locale: 'en_AU',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.eq('/blog/en-au/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLocalesBaseUrl', () => {
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'never',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'always',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'never',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'ignore',
|
||||||
|
format: 'file',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: 'en',
|
||||||
|
locales: ['en', 'en_US', 'es'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// directory format
|
||||||
|
expect(
|
||||||
|
getLocalesBaseUrl({
|
||||||
|
locale: 'en',
|
||||||
|
base: '/blog/',
|
||||||
|
locales: config.experimental.i18n.locales,
|
||||||
|
trailingSlash: 'ignore',
|
||||||
|
format: 'directory',
|
||||||
|
})
|
||||||
|
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -20,6 +20,7 @@ process.on('SIGTERM', exit);
|
||||||
// if you make any changes to the flow or wording here.
|
// if you make any changes to the flow or wording here.
|
||||||
export async function main() {
|
export async function main() {
|
||||||
// Clear console because PNPM startup is super ugly
|
// Clear console because PNPM startup is super ugly
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.clear();
|
console.clear();
|
||||||
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
|
// 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
|
// to no longer require `--` to pass args and instead pass `--` directly to us. This
|
||||||
|
|
|
@ -2755,6 +2755,24 @@ importers:
|
||||||
specifier: ^10.17.1
|
specifier: ^10.17.1
|
||||||
version: 10.17.1
|
version: 10.17.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/import-ts-with-js:
|
packages/astro/test/fixtures/import-ts-with-js:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro:
|
astro:
|
||||||
|
@ -17267,7 +17285,6 @@ packages:
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/unherit@3.0.1:
|
/unherit@3.0.1:
|
||||||
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}
|
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}
|
||||||
|
|
Loading…
Reference in a new issue