aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/four-masks-smell.md6
-rw-r--r--.changeset/little-panthers-relate.md5
-rw-r--r--.changeset/tidy-carrots-jump.md52
-rw-r--r--packages/astro/src/@types/astro.ts87
-rw-r--r--packages/astro/src/core/app/index.ts81
-rw-r--r--packages/astro/src/core/app/types.ts1
-rw-r--r--packages/astro/src/core/build/generate.ts13
-rw-r--r--packages/astro/src/core/build/plugins/plugin-manifest.ts20
-rw-r--r--packages/astro/src/core/config/schema.ts136
-rw-r--r--packages/astro/src/core/errors/errors-data.ts12
-rw-r--r--packages/astro/src/core/render/context.ts5
-rw-r--r--packages/astro/src/i18n/index.ts90
-rw-r--r--packages/astro/src/i18n/middleware.ts158
-rw-r--r--packages/astro/src/i18n/vite-plugin-i18n.ts12
-rw-r--r--packages/astro/src/integrations/astroFeaturesValidation.ts21
-rw-r--r--packages/astro/src/integrations/index.ts10
-rw-r--r--packages/astro/src/virtual-modules/i18n.ts15
-rw-r--r--packages/astro/src/vite-plugin-astro-server/plugin.ts1
-rw-r--r--packages/astro/src/vite-plugin-astro-server/route.ts2
-rw-r--r--packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs2
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs24
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/package.json8
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro18
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro8
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro8
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro19
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro18
-rw-r--r--packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro8
-rw-r--r--packages/astro/test/fixtures/i18n-routing/astro.config.mjs2
-rw-r--r--packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro2
-rw-r--r--packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro5
-rw-r--r--packages/astro/test/i18n-routing.test.js91
-rw-r--r--packages/astro/test/units/config/config-validate.test.js127
-rw-r--r--packages/astro/test/units/i18n/astro_i18n.test.js1313
-rw-r--r--packages/astro/test/units/integrations/api.test.js12
-rw-r--r--packages/create-astro/src/actions/typescript.ts1
-rw-r--r--packages/integrations/node/src/index.ts1
-rw-r--r--packages/integrations/vercel/src/serverless/adapter.ts1
-rw-r--r--pnpm-lock.yaml6
39 files changed, 1841 insertions, 560 deletions
diff --git a/.changeset/four-masks-smell.md b/.changeset/four-masks-smell.md
new file mode 100644
index 000000000..e0207fbe4
--- /dev/null
+++ b/.changeset/four-masks-smell.md
@@ -0,0 +1,6 @@
+---
+"@astrojs/vercel": minor
+"@astrojs/node": minor
+---
+
+Adds experimental support for internationalization domains
diff --git a/.changeset/little-panthers-relate.md b/.changeset/little-panthers-relate.md
new file mode 100644
index 000000000..5fb2c523f
--- /dev/null
+++ b/.changeset/little-panthers-relate.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Fixes an issue where the function `getLocaleRelativeUrlList` wasn't normalising the paths by default
diff --git a/.changeset/tidy-carrots-jump.md b/.changeset/tidy-carrots-jump.md
new file mode 100644
index 000000000..e18f235e3
--- /dev/null
+++ b/.changeset/tidy-carrots-jump.md
@@ -0,0 +1,52 @@
+---
+'astro': minor
+---
+
+Adds experimental support for a new i18n domain routing option (`"domains"`) that allows you to configure different domains for individual locales in entirely server-rendered projects.
+
+To enable this in your project, first configure your `server`-rendered project's i18n routing with your preferences if you have not already done so. Then, set the `experimental.i18nDomains` flag to `true` and add `i18n.domains` to map any of your supported `locales` to custom URLs:
+
+```js
+//astro.config.mjs"
+import { defineConfig } from "astro/config"
+export default defineConfig({
+ site: "https://example.com",
+ output: "server", // required, with no prerendered pages
+ adapter: node({
+ mode: 'standalone',
+ }),
+ i18n: {
+ defaultLocale: "en",
+ locales: ["es", "en", "fr", "ja"],
+ routing: {
+ prefixDefaultLocale: false
+ },
+ domains: {
+ fr: "https://fr.example.com",
+ es: "https://example.es"
+ }
+ },
+ experimental: {
+ i18nDomains: true
+ }
+})
+```
+With `"domains"` configured, the URLs emitted by `getAbsoluteLocaleUrl()` and `getAbsoluteLocaleUrlList()` will use the options set in `i18n.domains`.
+
+```js
+import { getAbsoluteLocaleUrl } from "astro:i18n";
+
+getAbsoluteLocaleUrl("en", "about"); // will return "https://example.com/about"
+getAbsoluteLocaleUrl("fr", "about"); // will return "https://fr.example.com/about"
+getAbsoluteLocaleUrl("es", "about"); // will return "https://example.es/about"
+getAbsoluteLocaleUrl("ja", "about"); // will return "https://example.com/ja/about"
+```
+
+Similarly, your localized files will create routes at corresponding URLs:
+
+- The file `/en/about.astro` will be reachable at the URL `https://example.com/about`.
+- The file `/fr/about.astro` will be reachable at the URL `https://fr.example.com/about`.
+- The file `/es/about.astro` will be reachable at the URL `https://example.es/about`.
+- The file `/ja/about.astro` will be reachable at the URL `https://example.com/ja/about`.
+
+See our [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details and limitations on this experimental routing feature.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 17c087f2d..0c15a9649 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1532,6 +1532,45 @@ export interface AstroUserConfig {
* - `"pathanme": The strategy is applied to the pathname of the URLs
*/
strategy: 'pathname';
+
+ /**
+ * @name i18n.domains
+ * @type {Record<string, string> }
+ * @default '{}'
+ * @version 4.3.0
+ * @description
+ *
+ * Configures the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
+ *
+ * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used.
+ * However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
+ *
+ * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
+ *
+ * ```js
+ * //astro.config.mjs
+ * export default defineConfig({
+ * site: "https://example.com",
+ * output: "server", // required, with no prerendered pages
+ * adapter: node({
+ * mode: 'standalone',
+ * }),
+ * i18n: {
+ * defaultLocale: "en",
+ * locales: ["en", "fr", "pt-br", "es"],
+ * prefixDefaultLocale: false,
+ * domains: {
+ * fr: "https://fr.example.com",
+ * es: "https://example.es"
+ * },
+ * })
+ * ```
+ *
+ * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
+ *
+ * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains) for more details, including the limitations of this feature.
+ */
+ domains?: Record<string, string>;
};
};
@@ -1664,6 +1703,48 @@ export interface AstroUserConfig {
* In the event of route collisions, where two routes of equal route priority attempt to build the same URL, Astro will log a warning identifying the conflicting routes.
*/
globalRoutePriority?: boolean;
+
+ /**
+ * @docs
+ * @name experimental.i18nDomains
+ * @type {boolean}
+ * @default `false`
+ * @version 4.3.0
+ * @description
+ *
+ * Enables domain support for the [experimental `domains` routing strategy](https://docs.astro.build/en/guides/internationalization/#domains-experimental) which allows you to configure the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
+ *
+ * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
+ *
+ * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
+ *
+ * ```js
+ * //astro.config.mjs
+ * export default defineConfig({
+ * site: "https://example.com",
+ * output: "server", // required, with no prerendered pages
+ * adapter: node({
+ * mode: 'standalone',
+ * }),
+ * i18n: {
+ * defaultLocale: "en",
+ * locales: ["en", "fr", "pt-br", "es"],
+ * prefixDefaultLocale: false,
+ * domains: {
+ * fr: "https://fr.example.com",
+ * es: "https://example.es"
+ * },
+ * experimental: {
+ * i18nDomains: true
+ * }
+ * })
+ * ```
+ *
+ * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
+ *
+ * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature.
+ */
+ i18nDomains?: boolean;
};
}
@@ -2133,7 +2214,7 @@ export type AstroFeatureMap = {
/**
* List of features that orbit around the i18n routing
*/
- i18n?: AstroInternationalizationFeature;
+ i18nDomains?: SupportsKind;
};
export interface AstroAssetsFeature {
@@ -2150,9 +2231,9 @@ export interface AstroAssetsFeature {
export interface AstroInternationalizationFeature {
/**
- * Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
+ * The adapter should be able to create the proper redirects
*/
- detectBrowserLanguage?: SupportsKind;
+ domains?: SupportsKind;
}
export type Locales = (string | { codes: string[]; path: string })[];
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index c7dca57ec..108b0f231 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -13,7 +13,9 @@ import { consoleLogDestination } from '../logger/console.js';
import { AstroIntegrationLogger, Logger } from '../logger/core.js';
import { sequence } from '../middleware/index.js';
import {
+ appendForwardSlash,
collapseDuplicateSlashes,
+ joinPaths,
prependForwardSlash,
removeTrailingForwardSlash,
} from '../path.js';
@@ -28,6 +30,7 @@ import {
import { matchRoute } from '../routing/match.js';
import { SSRRoutePipeline } from './ssrPipeline.js';
import type { RouteInfo } from './types.js';
+import { normalizeTheLocale } from '../../i18n/index.js';
export { deserializeManifest } from './common.js';
const localsSymbol = Symbol.for('astro.locals');
@@ -172,13 +175,85 @@ export class App {
const url = new URL(request.url);
// ignore requests matching public assets
if (this.#manifest.assets.has(url.pathname)) return undefined;
- const pathname = prependForwardSlash(this.removeBase(url.pathname));
- const routeData = matchRoute(pathname, this.#manifestData);
- // missing routes fall-through, prerendered are handled by static layer
+ let pathname = this.#computePathnameFromDomain(request);
+ if (!pathname) {
+ pathname = prependForwardSlash(this.removeBase(url.pathname));
+ }
+ let routeData = matchRoute(pathname, this.#manifestData);
+
+ // missing routes fall-through, pre rendered are handled by static layer
if (!routeData || routeData.prerender) return undefined;
return routeData;
}
+ #computePathnameFromDomain(request: Request): string | undefined {
+ let pathname: string | undefined = undefined;
+ const url = new URL(request.url);
+
+ if (
+ this.#manifest.i18n &&
+ (this.#manifest.i18n.routing === 'domains-prefix-always' ||
+ this.#manifest.i18n.routing === 'domains-prefix-other-locales' ||
+ this.#manifest.i18n.routing === 'domains-prefix-other-no-redirect')
+ ) {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
+ let host = request.headers.get('X-Forwarded-Host');
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
+ let protocol = request.headers.get('X-Forwarded-Proto');
+ if (protocol) {
+ // this header doesn't have the colum at the end, so we added to be in line with URL#protocol, which has it
+ protocol = protocol + ':';
+ } else {
+ // we fall back to the protocol of the request
+ protocol = url.protocol;
+ }
+ if (!host) {
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host
+ host = request.headers.get('Host');
+ }
+ // If we don't have a host and a protocol, it's impossible to proceed
+ if (host && protocol) {
+ // The header might have a port in their name, so we remove it
+ host = host.split(':')[0];
+ try {
+ let locale;
+ const hostAsUrl = new URL(`${protocol}//${host}`);
+ for (const [domainKey, localeValue] of Object.entries(
+ this.#manifest.i18n.domainLookupTable
+ )) {
+ // This operation should be safe because we force the protocol via zod inside the configuration
+ // If not, then it means that the manifest was tampered
+ const domainKeyAsUrl = new URL(domainKey);
+
+ if (
+ hostAsUrl.host === domainKeyAsUrl.host &&
+ hostAsUrl.protocol === domainKeyAsUrl.protocol
+ ) {
+ locale = localeValue;
+ break;
+ }
+ }
+
+ if (locale) {
+ pathname = prependForwardSlash(
+ joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname))
+ );
+ if (url.pathname.endsWith('/')) {
+ pathname = appendForwardSlash(pathname);
+ }
+ }
+ } catch (e: any) {
+ this.#logger.error(
+ 'router',
+ `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
+ );
+ this.#logger.error('router', `Error: ${e}`);
+ }
+ }
+ }
+ return pathname;
+ }
+
async render(request: Request, options?: RenderOptions): Promise<Response>;
/**
* @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index a5b93fb50..184f3b1d5 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -63,6 +63,7 @@ export type SSRManifestI18n = {
routing: RoutingStrategies;
locales: Locales;
defaultLocale: string;
+ domainLookupTable: Record<string, string>;
};
export type SerializedSSRManifest = Omit<
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index 9f3a90769..e73d34c71 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -67,6 +67,7 @@ import type {
StylesheetAsset,
} from './types.js';
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
+import { NoPrerenderedRoutesWithDomains } from '../errors/errors-data.js';
function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
@@ -180,9 +181,18 @@ export async function generatePages(opts: StaticBuildOptions, internals: BuildIn
logger.info('SKIP_FORMAT', `\n${bgGreen(black(` ${verb} static routes `))}`);
const builtPaths = new Set<string>();
const pagesToGenerate = pipeline.retrieveRoutesToGenerate();
+ const config = pipeline.getConfig();
if (ssr) {
for (const [pageData, filePath] of pagesToGenerate) {
if (pageData.route.prerender) {
+ // i18n domains won't work with pre rendered routes at the moment, so we need to to throw an error
+ if (config.experimental.i18nDomains) {
+ throw new AstroError({
+ ...NoPrerenderedRoutesWithDomains,
+ message: NoPrerenderedRoutesWithDomains.message(pageData.component),
+ });
+ }
+
const ssrEntryURLPage = createEntryURL(filePath, outFolder);
const ssrEntryPage = await import(ssrEntryURLPage.toString());
if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) {
@@ -429,7 +439,7 @@ function getInvalidRouteSegmentError(
route.route,
JSON.stringify(invalidParam),
JSON.stringify(received)
- )
+ )
: `Generated path for ${route.route} is invalid.`,
hint,
});
@@ -652,6 +662,7 @@ function createBuildManifest(
routing: settings.config.i18n.routing,
defaultLocale: settings.config.i18n.defaultLocale,
locales: settings.config.i18n.locales,
+ domainLookupTable: {},
};
}
return {
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index d57eb76f3..0c2717f4e 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -16,6 +16,7 @@ import { getOutFile, getOutFolder } from '../common.js';
import { cssOrder, mergeInlineCss, type BuildInternals } from '../internal.js';
import type { AstroBuildPlugin } from '../plugin.js';
import type { StaticBuildOptions } from '../types.js';
+import { normalizeTheLocale } from '../../../i18n/index.js';
const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@';
const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g');
@@ -153,6 +154,7 @@ function buildManifest(
const { settings } = opts;
const routes: SerializedRouteInfo[] = [];
+ const domainLookupTable: Record<string, string> = {};
const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
if (settings.scripts.some((script) => script.stage === 'page')) {
staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
@@ -229,6 +231,23 @@ function buildManifest(
});
}
+ /**
+ * logic meant for i18n domain support, where we fill the lookup table
+ */
+ const i18n = settings.config.i18n;
+ if (
+ settings.config.experimental.i18nDomains &&
+ i18n &&
+ i18n.domains &&
+ (i18n.routing === 'domains-prefix-always' ||
+ i18n.routing === 'domains-prefix-other-locales' ||
+ i18n.routing === 'domains-prefix-other-no-redirect')
+ ) {
+ for (const [locale, domainValue] of Object.entries(i18n.domains)) {
+ domainLookupTable[domainValue] = normalizeTheLocale(locale);
+ }
+ }
+
// HACK! Patch this special one.
if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
// Set this to an empty string so that the runtime knows not to try and load this.
@@ -241,6 +260,7 @@ function buildManifest(
routing: settings.config.i18n.routing,
locales: settings.config.i18n.locales,
defaultLocale: settings.config.i18n.defaultLocale,
+ domainLookupTable,
};
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 29c817bce..255360eea 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -62,13 +62,17 @@ const ASTRO_CONFIG_DEFAULTS = {
contentCollectionCache: false,
clientPrerender: false,
globalRoutePriority: false,
+ i18nDomains: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
export type RoutingStrategies =
| 'pathname-prefix-always'
| 'pathname-prefix-other-locales'
- | 'pathname-prefix-always-no-redirect';
+ | 'pathname-prefix-always-no-redirect'
+ | 'domains-prefix-always'
+ | 'domains-prefix-other-locales'
+ | 'domains-prefix-other-no-redirect';
export const AstroConfigSchema = z.object({
root: z
@@ -330,6 +334,16 @@ export const AstroConfigSchema = z.object({
}),
])
),
+ domains: z
+ .record(
+ z.string(),
+ z
+ .string()
+ .url(
+ "The domain value must be a valid URL, and it has to start with 'https' or 'http'."
+ )
+ )
+ .optional(),
fallback: z.record(z.string(), z.string()).optional(),
routing: z
.object({
@@ -346,29 +360,43 @@ export const AstroConfigSchema = z.object({
message:
'The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`.',
}
- )
- .transform((routing) => {
- let strategy: RoutingStrategies;
- switch (routing.strategy) {
- case 'pathname': {
- if (routing.prefixDefaultLocale === true) {
- if (routing.redirectToDefaultLocale) {
- strategy = 'pathname-prefix-always';
- } else {
- strategy = 'pathname-prefix-always-no-redirect';
- }
- } else {
- strategy = 'pathname-prefix-other-locales';
- }
+ ),
+ })
+ .optional()
+ .transform((i18n) => {
+ if (i18n) {
+ let { routing, domains } = i18n;
+ let strategy: RoutingStrategies;
+ const hasDomains = domains ? Object.keys(domains).length > 0 : false;
+ if (!hasDomains) {
+ if (routing.prefixDefaultLocale === true) {
+ if (routing.redirectToDefaultLocale) {
+ strategy = 'pathname-prefix-always';
+ } else {
+ strategy = 'pathname-prefix-always-no-redirect';
+ }
+ } else {
+ strategy = 'pathname-prefix-other-locales';
+ }
+ } else {
+ if (routing.prefixDefaultLocale === true) {
+ if (routing.redirectToDefaultLocale) {
+ strategy = 'domains-prefix-always';
+ } else {
+ strategy = 'domains-prefix-other-no-redirect';
}
+ } else {
+ strategy = 'domains-prefix-other-locales';
}
- return strategy;
- }),
+ }
+
+ return { ...i18n, routing: strategy };
+ }
+ return undefined;
})
- .optional()
.superRefine((i18n, ctx) => {
if (i18n) {
- const { defaultLocale, locales: _locales, fallback } = i18n;
+ const { defaultLocale, locales: _locales, fallback, domains, routing } = i18n;
const locales = _locales.map((locale) => {
if (typeof locale === 'string') {
return locale;
@@ -406,6 +434,51 @@ export const AstroConfigSchema = z.object({
}
}
}
+ if (domains) {
+ const entries = Object.entries(domains);
+ if (entries.length > 0) {
+ if (
+ routing !== 'domains-prefix-other-locales' &&
+ routing !== 'domains-prefix-other-no-redirect' &&
+ routing !== 'domains-prefix-always'
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `When specifying some domains, the property \`i18n.routingStrategy\` must be set to \`"domains"\`.`,
+ });
+ }
+ }
+
+ for (const [domainKey, domainValue] of Object.entries(domains)) {
+ if (!locales.includes(domainKey)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `The locale \`${domainKey}\` key in the \`i18n.domains\` record doesn't exist in the \`i18n.locales\` array.`,
+ });
+ }
+ if (!domainValue.startsWith('https') && !domainValue.startsWith('http')) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "The domain value must be a valid URL, and it has to start with 'https' or 'http'.",
+ path: ['domains'],
+ });
+ } else {
+ try {
+ const domainUrl = new URL(domainValue);
+ if (domainUrl.pathname !== '/') {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `The URL \`${domainValue}\` must contain only the origin. A subsequent pathname isn't allowed here. Remove \`${domainUrl.pathname}\`.`,
+ path: ['domains'],
+ });
+ }
+ } catch {
+ // no need to catch the error
+ }
+ }
+ }
+ }
}
})
),
@@ -427,6 +500,7 @@ export const AstroConfigSchema = z.object({
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority),
+ i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains),
})
.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.`
@@ -547,6 +621,30 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
.refine((obj) => !obj.outDir.toString().startsWith(obj.publicDir.toString()), {
message:
'The value of `outDir` must not point to a path within the folder set as `publicDir`, this will cause an infinite loop',
+ })
+ .superRefine((configuration, ctx) => {
+ const { site, experimental, i18n, output } = configuration;
+ if (experimental.i18nDomains) {
+ if (
+ i18n?.routing === 'domains-prefix-other-locales' ||
+ i18n?.routing === 'domains-prefix-other-no-redirect' ||
+ i18n?.routing === 'domains-prefix-always'
+ ) {
+ if (!site) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain.",
+ });
+ }
+ if (output !== 'server') {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Domain support is only available when `output` is `"server"`.',
+ });
+ }
+ }
+ }
});
return AstroConfigRelativeSchema;
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 4af4d2e64..3fc459ef8 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -1020,6 +1020,18 @@ export const MissingIndexForInternationalization = {
/**
* @docs
* @description
+ * Static pages aren't yet supported with i18n domains. If you wish to enable this feature, you have to disable pre-rendering.
+ */
+export const NoPrerenderedRoutesWithDomains = {
+ name: 'NoPrerenderedRoutesWithDomains',
+ title: "Pre-rendered routes aren't supported when internationalization domains are enabled.",
+ message: (component: string) =>
+ `Static pages aren't yet supported with multiple domains. If you wish to enable this feature, you have to disable pre-rendering for the page ${component}`,
+} satisfies ErrorData;
+
+/**
+ * @docs
+ * @description
* Astro could not find an associated file with content while trying to render the route. This is an Astro error and not a user error. If restarting the dev server does not fix the problem, please file an issue.
*/
export const CantRenderPage = {
diff --git a/packages/astro/src/core/render/context.ts b/packages/astro/src/core/render/context.ts
index 898e74d8f..faba9b86a 100644
--- a/packages/astro/src/core/render/context.ts
+++ b/packages/astro/src/core/render/context.ts
@@ -263,7 +263,10 @@ export function computeCurrentLocale(
}
}
}
- if (routingStrategy === 'pathname-prefix-other-locales') {
+ if (
+ routingStrategy === 'pathname-prefix-other-locales' ||
+ routingStrategy === 'domains-prefix-other-locales'
+ ) {
return defaultLocale;
}
return undefined;
diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts
index 4b882b782..6a8834b54 100644
--- a/packages/astro/src/i18n/index.ts
+++ b/packages/astro/src/i18n/index.ts
@@ -13,6 +13,8 @@ type GetLocaleRelativeUrl = GetLocaleOptions & {
format: AstroConfig['build']['format'];
routing?: RoutingStrategies;
defaultLocale: string;
+ domains: Record<string, string> | undefined;
+ path?: string;
};
export type GetLocaleOptions = {
@@ -22,10 +24,6 @@ export type GetLocaleOptions = {
*/
normalizeLocale?: boolean;
/**
- * An optional path to add after the `locale`.
- */
- path?: string;
- /**
* An optional path to prepend to `locale`.
*/
prependWith?: string;
@@ -33,6 +31,7 @@ export type GetLocaleOptions = {
type GetLocaleAbsoluteUrl = GetLocaleRelativeUrl & {
site: AstroConfig['site'];
+ isBuild: boolean;
};
/**
* The base URL
@@ -75,22 +74,37 @@ export function getLocaleRelativeUrl({
/**
* The absolute URL
*/
-export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) {
- const locale = getLocaleRelativeUrl(rest);
- if (site) {
- return joinPaths(site, locale);
+export function getLocaleAbsoluteUrl({ site, isBuild, ...rest }: GetLocaleAbsoluteUrl) {
+ const localeUrl = getLocaleRelativeUrl(rest);
+ const { domains, locale } = rest;
+ let url;
+ if (isBuild && domains) {
+ const base = domains[locale];
+ url = joinPaths(base, localeUrl.replace(`/${rest.locale}`, ''));
+ } else {
+ if (site) {
+ url = joinPaths(site, localeUrl);
+ } else {
+ url = localeUrl;
+ }
+ }
+
+ if (shouldAppendForwardSlash(rest.trailingSlash, rest.format)) {
+ return appendForwardSlash(url);
} else {
- return locale;
+ return url;
}
}
interface GetLocalesRelativeUrlList extends GetLocaleOptions {
base: string;
+ path?: string;
locales: Locales;
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
routing?: RoutingStrategies;
defaultLocale: string;
+ domains: Record<string, string> | undefined;
}
export function getLocaleRelativeUrlList({
@@ -100,7 +114,7 @@ export function getLocaleRelativeUrlList({
format,
path,
prependWith,
- normalizeLocale = false,
+ normalizeLocale = true,
routing = 'pathname-prefix-other-locales',
defaultLocale,
}: GetLocalesRelativeUrlList) {
@@ -124,16 +138,58 @@ export function getLocaleRelativeUrlList({
}
interface GetLocalesAbsoluteUrlList extends GetLocalesRelativeUrlList {
- site?: string;
+ site?: AstroConfig['site'];
+ isBuild: boolean;
}
-export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocalesAbsoluteUrlList) {
- const locales = getLocaleRelativeUrlList(rest);
- return locales.map((locale) => {
- if (site) {
- return joinPaths(site, locale);
+export function getLocaleAbsoluteUrlList({
+ base,
+ locales: _locales,
+ trailingSlash,
+ format,
+ path,
+ prependWith,
+ normalizeLocale = true,
+ routing = 'pathname-prefix-other-locales',
+ defaultLocale,
+ isBuild,
+ domains,
+ site,
+}: GetLocalesAbsoluteUrlList) {
+ const locales = toPaths(_locales);
+ return locales.map((currentLocale) => {
+ const pathsToJoin = [];
+ const normalizedLocale = normalizeLocale ? normalizeTheLocale(currentLocale) : currentLocale;
+ const domainBase = domains ? domains[currentLocale] : undefined;
+ if (isBuild && domainBase) {
+ if (domainBase) {
+ pathsToJoin.push(domainBase);
+ } else {
+ pathsToJoin.push(site);
+ }
+ pathsToJoin.push(base);
+ pathsToJoin.push(prependWith);
} else {
- return locale;
+ if (site) {
+ pathsToJoin.push(site);
+ }
+ pathsToJoin.push(base);
+ pathsToJoin.push(prependWith);
+ if (
+ routing === 'pathname-prefix-always' ||
+ routing === 'pathname-prefix-always-no-redirect'
+ ) {
+ pathsToJoin.push(normalizedLocale);
+ } else if (currentLocale !== defaultLocale) {
+ pathsToJoin.push(normalizedLocale);
+ }
+ }
+
+ pathsToJoin.push(path);
+ if (shouldAppendForwardSlash(trailingSlash, format)) {
+ return appendForwardSlash(joinPaths(...pathsToJoin));
+ } else {
+ return joinPaths(...pathsToJoin);
}
});
}
diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts
index a7c50dc82..9fabff13a 100644
--- a/packages/astro/src/i18n/middleware.ts
+++ b/packages/astro/src/i18n/middleware.ts
@@ -1,9 +1,16 @@
import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path';
-import type { Locales, MiddlewareHandler, RouteData, SSRManifest } from '../@types/astro.js';
+import type {
+ APIContext,
+ Locales,
+ MiddlewareHandler,
+ RouteData,
+ SSRManifest,
+} from '../@types/astro.js';
import type { PipelineHookFunction } from '../core/pipeline.js';
import { getPathByLocale, normalizeTheLocale } from './index.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';
import { ROUTE_DATA_SYMBOL } from '../core/constants.js';
+import type { SSRManifestI18n } from '../core/app/types.js';
const routeDataSymbol = Symbol.for(ROUTE_DATA_SYMBOL);
@@ -25,6 +32,13 @@ function pathnameHasLocale(pathname: string, locales: Locales): boolean {
return false;
}
+type MiddlewareOptions = {
+ i18n: SSRManifest['i18n'];
+ base: SSRManifest['base'];
+ trailingSlash: SSRManifest['trailingSlash'];
+ buildFormat: SSRManifest['buildFormat'];
+};
+
export function createI18nMiddleware(
i18n: SSRManifest['i18n'],
base: SSRManifest['base'],
@@ -33,62 +47,130 @@ export function createI18nMiddleware(
): MiddlewareHandler {
if (!i18n) return (_, next) => next();
+ const prefixAlways = (
+ url: URL,
+ response: Response,
+ context: APIContext
+ ): Response | undefined => {
+ if (url.pathname === base + '/' || url.pathname === base) {
+ if (shouldAppendForwardSlash(trailingSlash, buildFormat)) {
+ return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
+ } else {
+ return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
+ }
+ }
+
+ // Astro can't know where the default locale is supposed to be, so it returns a 404 with no content.
+ else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
+ return new Response(null, {
+ status: 404,
+ headers: response.headers,
+ });
+ }
+
+ return undefined;
+ };
+
+ const prefixOtherLocales = (url: URL, response: Response): Response | undefined => {
+ const pathnameContainsDefaultLocale = url.pathname.includes(`/${i18n.defaultLocale}`);
+ if (pathnameContainsDefaultLocale) {
+ const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, '');
+ response.headers.set('Location', newLocation);
+ return new Response(null, {
+ status: 404,
+ headers: response.headers,
+ });
+ }
+
+ return undefined;
+ };
+
+ /**
+ * We return a 404 if:
+ * - the current path isn't a root. e.g. / or /<base>
+ * - the URL doesn't contain a locale
+ * @param url
+ * @param response
+ */
+ const prefixAlwaysNoRedirect = (url: URL, response: Response): Response | undefined => {
+ // We return a 404 if:
+ // - the current path isn't a root. e.g. / or /<base>
+ // - the URL doesn't contain a locale
+ const isRoot = url.pathname === base + '/' || url.pathname === base;
+ if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) {
+ return new Response(null, {
+ status: 404,
+ headers: response.headers,
+ });
+ }
+
+ return undefined;
+ };
+
return async (context, next) => {
const routeData: RouteData | undefined = Reflect.get(context.request, routeDataSymbol);
// If the route we're processing is not a page, then we ignore it
if (routeData?.type !== 'page' && routeData?.type !== 'fallback') {
return await next();
}
+ const currentLocale = context.currentLocale;
const url = context.url;
const { locales, defaultLocale, fallback, routing } = i18n;
const response = await next();
if (response instanceof Response) {
- const pathnameContainsDefaultLocale = url.pathname.includes(`/${defaultLocale}`);
switch (i18n.routing) {
+ case 'domains-prefix-other-locales': {
+ if (localeHasntDomain(i18n, currentLocale)) {
+ const result = prefixOtherLocales(url, response);
+ if (result) {
+ return result;
+ }
+ }
+ break;
+ }
case 'pathname-prefix-other-locales': {
- if (pathnameContainsDefaultLocale) {
- const newLocation = url.pathname.replace(`/${defaultLocale}`, '');
- response.headers.set('Location', newLocation);
- return new Response(null, {
- status: 404,
- headers: response.headers,
- });
+ const result = prefixOtherLocales(url, response);
+ if (result) {
+ return result;
+ }
+ break;
+ }
+
+ case 'domains-prefix-other-no-redirect': {
+ if (localeHasntDomain(i18n, currentLocale)) {
+ const result = prefixAlwaysNoRedirect(url, response);
+ if (result) {
+ return result;
+ }
}
break;
}
case 'pathname-prefix-always-no-redirect': {
- // We return a 404 if:
- // - the current path isn't a root. e.g. / or /<base>
- // - the URL doesn't contain a locale
- const isRoot = url.pathname === base + '/' || url.pathname === base;
- if (!(isRoot || pathnameHasLocale(url.pathname, i18n.locales))) {
- return new Response(null, {
- status: 404,
- headers: response.headers,
- });
+ const result = prefixAlwaysNoRedirect(url, response);
+ if (result) {
+ return result;
}
break;
}
case 'pathname-prefix-always': {
- if (url.pathname === base + '/' || url.pathname === base) {
- if (shouldAppendForwardSlash(trailingSlash, buildFormat)) {
- return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`);
- } else {
- return context.redirect(`${joinPaths(base, i18n.defaultLocale)}`);
- }
+ const result = prefixAlways(url, response, context);
+ if (result) {
+ return result;
}
-
- // Astro can't know where the default locale is supposed to be, so it returns a 404 with no content.
- else if (!pathnameHasLocale(url.pathname, i18n.locales)) {
- return new Response(null, {
- status: 404,
- headers: response.headers,
- });
+ break;
+ }
+ case 'domains-prefix-always': {
+ if (localeHasntDomain(i18n, currentLocale)) {
+ const result = prefixAlways(url, response, context);
+ if (result) {
+ return result;
+ }
}
+ break;
}
}
@@ -138,3 +220,17 @@ export function createI18nMiddleware(
export const i18nPipelineHook: PipelineHookFunction = (ctx) => {
Reflect.set(ctx.request, routeDataSymbol, ctx.route);
};
+
+/**
+ * Checks if the current locale doesn't belong to a configured domain
+ * @param i18n
+ * @param currentLocale
+ */
+function localeHasntDomain(i18n: SSRManifestI18n, currentLocale: string | undefined) {
+ for (const domainLocale of Object.values(i18n.domainLookupTable)) {
+ if (domainLocale === currentLocale) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/packages/astro/src/i18n/vite-plugin-i18n.ts b/packages/astro/src/i18n/vite-plugin-i18n.ts
index 2ce015c26..7aa4d327b 100644
--- a/packages/astro/src/i18n/vite-plugin-i18n.ts
+++ b/packages/astro/src/i18n/vite-plugin-i18n.ts
@@ -13,6 +13,7 @@ export interface I18nInternalConfig
extends Pick<AstroConfig, 'base' | 'site' | 'trailingSlash'>,
Pick<AstroConfig['build'], 'format'> {
i18n: AstroConfig['i18n'];
+ isBuild: boolean;
}
export default function astroInternationalization({
@@ -28,8 +29,15 @@ export default function astroInternationalization({
return {
name: 'astro:i18n',
enforce: 'pre',
- config(config) {
- const i18nConfig: I18nInternalConfig = { base, format, site, trailingSlash, i18n };
+ config(config, { command }) {
+ const i18nConfig: I18nInternalConfig = {
+ base,
+ format,
+ site,
+ trailingSlash,
+ i18n,
+ isBuild: command === 'build',
+ };
return {
define: {
__ASTRO_INTERNAL_I18N_CONFIG__: JSON.stringify(i18nConfig),
diff --git a/packages/astro/src/integrations/astroFeaturesValidation.ts b/packages/astro/src/integrations/astroFeaturesValidation.ts
index 8bae77846..ce0f1b9c4 100644
--- a/packages/astro/src/integrations/astroFeaturesValidation.ts
+++ b/packages/astro/src/integrations/astroFeaturesValidation.ts
@@ -1,4 +1,5 @@
import type {
+ AstroAdapterFeatures,
AstroAssetsFeature,
AstroConfig,
AstroFeatureMap,
@@ -32,6 +33,7 @@ export function validateSupportedFeatures(
adapterName: string,
featureMap: AstroFeatureMap,
config: AstroConfig,
+ adapterFeatures: AstroAdapterFeatures | undefined,
logger: Logger
): ValidationResult {
const {
@@ -39,6 +41,7 @@ export function validateSupportedFeatures(
serverOutput = UNSUPPORTED,
staticOutput = UNSUPPORTED,
hybridOutput = UNSUPPORTED,
+ i18nDomains = UNSUPPORTED,
} = featureMap;
const validationResult: ValidationResult = {};
@@ -67,6 +70,24 @@ export function validateSupportedFeatures(
);
validationResult.assets = validateAssetsFeature(assets, adapterName, config, logger);
+ if (i18nDomains) {
+ validationResult.i18nDomains = validateSupportKind(
+ i18nDomains,
+ adapterName,
+ logger,
+ 'i18nDomains',
+ () => {
+ return config?.output === 'server' && !config?.site;
+ }
+ );
+ if (adapterFeatures?.functionPerRoute) {
+ logger.error(
+ 'config',
+ 'The Astro feature `i18nDomains` is incompatible with the Adapter feature `functionPerRoute`'
+ );
+ }
+ }
+
return validationResult;
}
diff --git a/packages/astro/src/integrations/index.ts b/packages/astro/src/integrations/index.ts
index d082c438f..63d526ad4 100644
--- a/packages/astro/src/integrations/index.ts
+++ b/packages/astro/src/integrations/index.ts
@@ -257,6 +257,8 @@ export async function runHookConfigDone({
adapter.name,
adapter.supportedAstroFeatures,
settings.config,
+ // SAFETY: we checked before if it's not present, and we throw an error
+ adapter.adapterFeatures,
logger
);
for (const [featureName, supported] of Object.entries(validationResult)) {
@@ -503,11 +505,3 @@ export function isFunctionPerRouteEnabled(adapter: AstroAdapter | undefined): bo
return false;
}
}
-
-export function isEdgeMiddlewareEnabled(adapter: AstroAdapter | undefined): boolean {
- if (adapter?.adapterFeatures?.edgeMiddleware === true) {
- return true;
- } else {
- return false;
- }
-}
diff --git a/packages/astro/src/virtual-modules/i18n.ts b/packages/astro/src/virtual-modules/i18n.ts
index aff49a492..f71068667 100644
--- a/packages/astro/src/virtual-modules/i18n.ts
+++ b/packages/astro/src/virtual-modules/i18n.ts
@@ -2,9 +2,10 @@ import * as I18nInternals from '../i18n/index.js';
import type { I18nInternalConfig } from '../i18n/vite-plugin-i18n.js';
export { normalizeTheLocale, toCodes, toPaths } from '../i18n/index.js';
-// @ts-expect-error
-const { trailingSlash, format, site, i18n } = __ASTRO_INTERNAL_I18N_CONFIG__ as I18nInternalConfig;
-const { defaultLocale, locales, routing } = i18n!;
+const { trailingSlash, format, site, i18n, isBuild } =
+ // @ts-expect-error
+ __ASTRO_INTERNAL_I18N_CONFIG__ as I18nInternalConfig;
+const { defaultLocale, locales, routing, domains } = i18n!;
const base = import.meta.env.BASE_URL;
export type GetLocaleOptions = I18nInternals.GetLocaleOptions;
@@ -40,6 +41,7 @@ export const getRelativeLocaleUrl = (locale: string, path?: string, options?: Ge
defaultLocale,
locales,
routing,
+ domains,
...options,
});
@@ -68,7 +70,7 @@ export const getRelativeLocaleUrl = (locale: string, path?: string, options?: Ge
* getAbsoluteLocaleUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // https://example.com/blog/es_US/getting-started
* ```
*/
-export const getAbsoluteLocaleUrl = (locale: string, path = '', options?: GetLocaleOptions) =>
+export const getAbsoluteLocaleUrl = (locale: string, path?: string, options?: GetLocaleOptions) =>
I18nInternals.getLocaleAbsoluteUrl({
locale,
path,
@@ -79,6 +81,8 @@ export const getAbsoluteLocaleUrl = (locale: string, path = '', options?: GetLoc
defaultLocale,
locales,
routing,
+ domains,
+ isBuild,
...options,
});
@@ -97,6 +101,7 @@ export const getRelativeLocaleUrlList = (path?: string, options?: GetLocaleOptio
defaultLocale,
locales,
routing,
+ domains,
...options,
});
@@ -116,6 +121,8 @@ export const getAbsoluteLocaleUrlList = (path?: string, options?: GetLocaleOptio
defaultLocale,
locales,
routing,
+ domains,
+ isBuild,
...options,
});
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index f1162b7bc..ba33c3ebd 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -121,6 +121,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest
routing: settings.config.i18n.routing,
defaultLocale: settings.config.i18n.defaultLocale,
locales: settings.config.i18n.locales,
+ domainLookupTable: {},
};
}
return {
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index b4b181124..35d4b7278 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -308,7 +308,7 @@ export async function handleRoute({
const onRequest: MiddlewareHandler = middleware.onRequest;
if (config.i18n) {
const i18Middleware = createI18nMiddleware(
- config.i18n,
+ manifest.i18n,
config.base,
config.trailingSlash,
config.build.format
diff --git a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
index 58eb50540..7a612488d 100644
--- a/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
+++ b/packages/astro/test/fixtures/i18n-routing-prefix-always/astro.config.mjs
@@ -13,5 +13,5 @@ export default defineConfig({
prefixDefaultLocale: true
}
},
- base: "/new-site"
+ base: "/new-site"
})
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs
new file mode 100644
index 000000000..d8fe7b4a5
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs
@@ -0,0 +1,24 @@
+import { defineConfig} from "astro/config";
+
+export default defineConfig({
+ output: "server",
+ trailingSlash: "never",
+ i18n: {
+ defaultLocale: 'en',
+ locales: [
+ 'en', 'pt', 'it'
+ ],
+ domains: {
+ pt: "https://example.pt",
+ it: "http://it.example.com"
+ },
+ routing: {
+ prefixDefaultLocale: true,
+ redirectToDefaultLocale: false
+ }
+ },
+ experimental: {
+ i18nDomains: true
+ },
+ site: "https://example.com",
+})
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/package.json b/packages/astro/test/fixtures/i18n-routing-subdomain/package.json
new file mode 100644
index 000000000..931425fa6
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/i18n-routing-subdomain",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro
new file mode 100644
index 000000000..97b41230d
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro
@@ -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>
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro
new file mode 100644
index 000000000..3e50ac6bf
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Astro</title>
+</head>
+<body>
+Hello
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro
new file mode 100644
index 000000000..990baecd9
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Astro</title>
+</head>
+<body>
+Start
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro
new file mode 100644
index 000000000..d138455a3
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro
@@ -0,0 +1,19 @@
+---
+import { getRelativeLocaleUrl, getAbsoluteLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n";
+
+let absoluteLocaleUrl_pt = getAbsoluteLocaleUrl("pt", "about");
+let absoluteLocaleUrl_it = getAbsoluteLocaleUrl("it");
+
+---
+
+<html>
+<head>
+ <title>Astro</title>
+</head>
+ <body>
+ Virtual module doesn't break
+
+ Absolute URL pt: {absoluteLocaleUrl_pt}
+ Absolute URL it: {absoluteLocaleUrl_it}
+ </body>
+</html>
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro
new file mode 100644
index 000000000..e37f83a30
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro
@@ -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>
diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro
new file mode 100644
index 000000000..5a4a84c2c
--- /dev/null
+++ b/packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Astro</title>
+</head>
+<body>
+Oi essa e start
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/i18n-routing/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing/astro.config.mjs
index 42559e778..8868d2b1f 100644
--- a/packages/astro/test/fixtures/i18n-routing/astro.config.mjs
+++ b/packages/astro/test/fixtures/i18n-routing/astro.config.mjs
@@ -11,6 +11,6 @@ export default defineConfig({
path: "spanish",
codes: ["es", "es-SP"]
}
- ]
+ ],
}
})
diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro b/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro
index 58141fec0..4b0a97b6e 100644
--- a/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro
+++ b/packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro
@@ -2,7 +2,7 @@
---
export function getStaticPaths() {
return [
- { id: "lorem" }
+ { params: {id: "lorem"}}
]
}
const currentLocale = Astro.currentLocale;
diff --git a/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro b/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro
index ca33030db..f65356e8f 100644
--- a/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro
+++ b/packages/astro/test/fixtures/i18n-routing/src/pages/virtual-module.astro
@@ -1,10 +1,12 @@
---
-import { getRelativeLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n";
+import { getRelativeLocaleUrl, getAbsoluteLocaleUrl, getPathByLocale, getLocaleByPath } from "astro:i18n";
let about = getRelativeLocaleUrl("pt", "about");
let spanish = getRelativeLocaleUrl("es", "about");
let spainPath = getPathByLocale("es-SP");
let localeByPath = getLocaleByPath("spanish");
+let italianAbout = getAbsoluteLocaleUrl("it", "about");
+
---
<html>
@@ -18,5 +20,6 @@ let localeByPath = getLocaleByPath("spanish");
About spanish: {spanish}
Spain path: {spainPath}
Preferred path: {localeByPath}
+ About it: {italianAbout}
</body>
</html>
diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js
index da9fdc7c8..b3d496ff4 100644
--- a/packages/astro/test/i18n-routing.test.js
+++ b/packages/astro/test/i18n-routing.test.js
@@ -29,6 +29,34 @@ describe('astro:i18n virtual module', () => {
expect(text).includes('About spanish: /spanish/about');
expect(text).includes('Spain path: spanish');
expect(text).includes('Preferred path: es');
+ expect(text).includes('About it: /it/about');
+ });
+
+ describe('absolute URLs', () => {
+ let app;
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/i18n-routing-subdomain/',
+ output: 'server',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('correctly renders the absolute URL', async () => {
+ let request = new Request('http://example.com/');
+ let response = await app.render(request);
+ expect(response.status).to.equal(200);
+
+ let html = await response.text();
+ let $ = cheerio.load(html);
+
+ console.log(html);
+ expect($('body').text()).includes("Virtual module doesn't break");
+ expect($('body').text()).includes('Absolute URL pt: https://example.pt/about');
+ expect($('body').text()).includes('Absolute URL it: http://it.example.com/');
+ });
});
});
describe('[DEV] i18n routing', () => {
@@ -1613,4 +1641,67 @@ describe('i18n routing does not break assets and endpoints', () => {
expect(await response.text()).includes('lorem');
});
});
+
+ describe('i18n routing with routing strategy [subdomain]', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ let app;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/i18n-routing-subdomain/',
+ output: 'server',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('should render the en locale when X-Forwarded-Host header is passed', async () => {
+ let request = new Request('http://example.pt/start', {
+ headers: {
+ 'X-Forwarded-Host': 'example.pt',
+ 'X-Forwarded-Proto': 'https',
+ },
+ });
+ let response = await app.render(request);
+ expect(response.status).to.equal(200);
+ expect(await response.text()).includes('Oi essa e start\n');
+ });
+
+ it('should render the en locale when Host header is passed', async () => {
+ let request = new Request('http://example.pt/start', {
+ headers: {
+ Host: 'example.pt',
+ 'X-Forwarded-Proto': 'https',
+ },
+ });
+ let response = await app.render(request);
+ expect(response.status).to.equal(200);
+ expect(await response.text()).includes('Oi essa e start\n');
+ });
+
+ it('should render the en locale when Host header is passed and it has the port', async () => {
+ let request = new Request('http://example.pt/start', {
+ headers: {
+ Host: 'example.pt:8080',
+ 'X-Forwarded-Proto': 'https',
+ },
+ });
+ let response = await app.render(request);
+ expect(response.status).to.equal(200);
+ expect(await response.text()).includes('Oi essa e start\n');
+ });
+
+ it('should render when the protocol header we fallback to the one of the host', async () => {
+ let request = new Request('https://example.pt/start', {
+ headers: {
+ Host: 'example.pt',
+ },
+ });
+ let response = await app.render(request);
+ expect(response.status).to.equal(200);
+ expect(await response.text()).includes('Oi essa e start\n');
+ });
+ });
});
diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.js
index 477bc4bed..d2efbbafb 100644
--- a/packages/astro/test/units/config/config-validate.test.js
+++ b/packages/astro/test/units/config/config-validate.test.js
@@ -213,5 +213,132 @@ describe('Config Validation', () => {
'The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`.'
);
});
+
+ it('errors if a domains key does not exist', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'server',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ lorem: 'https://example.com',
+ },
+ },
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ "The locale `lorem` key in the `i18n.domains` record doesn't exist in the `i18n.locales` array."
+ );
+ });
+
+ it('errors if a domains value is not an URL', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'server',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ en: 'www.example.com',
+ },
+ },
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ "The domain value must be a valid URL, and it has to start with 'https' or 'http'."
+ );
+ });
+
+ it('errors if a domains value is not an URL with incorrect protocol', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'server',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ en: 'tcp://www.example.com',
+ },
+ },
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ "The domain value must be a valid URL, and it has to start with 'https' or 'http'."
+ );
+ });
+
+ it('errors if a domain is a URL with a pathname that is not the home', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'server',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ en: 'https://www.example.com/blog/page/',
+ },
+ },
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ "The URL `https://www.example.com/blog/page/` must contain only the origin. A subsequent pathname isn't allowed here. Remove `/blog/page/`."
+ );
+ });
+
+ it('errors if domains is enabled but site is not provided', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'server',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ en: 'https://www.example.com/',
+ },
+ },
+ experimental: {
+ i18nDomains: true,
+ },
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ "The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain."
+ );
+ });
+
+ it('errors if domains is enabled but the `output` is not "server"', async () => {
+ const configError = await validateConfig(
+ {
+ output: 'static',
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['es', 'en'],
+ domains: {
+ en: 'https://www.example.com/',
+ },
+ },
+ experimental: {
+ i18nDomains: true,
+ },
+ site: 'https://foo.org',
+ },
+ process.cwd()
+ ).catch((err) => err);
+ expect(configError instanceof z.ZodError).to.equal(true);
+ expect(configError.errors[0].message).to.equal(
+ 'Domain support is only available when `output` is `"server"`.'
+ );
+ });
});
});
diff --git a/packages/astro/test/units/i18n/astro_i18n.test.js b/packages/astro/test/units/i18n/astro_i18n.test.js
index 011193acc..e3364924a 100644
--- a/packages/astro/test/units/i18n/astro_i18n.test.js
+++ b/packages/astro/test/units/i18n/astro_i18n.test.js
@@ -142,18 +142,16 @@ describe('getLocaleRelativeUrl', () => {
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
- experimental: {
- i18n: {
- defaultLocale: 'en',
- locales: [
- 'en',
- 'es',
- {
- path: 'italiano',
- codes: ['it', 'it-VA'],
- },
- ],
- },
+ i18n: {
+ defaultLocale: 'en',
+ locales: [
+ 'en',
+ 'es',
+ {
+ path: 'italiano',
+ codes: ['it', 'it-VA'],
+ },
+ ],
},
};
// directory format
@@ -161,7 +159,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'en',
base: '/blog',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'never',
format: 'directory',
})
@@ -170,7 +168,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'es',
base: '/blog/',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'always',
format: 'directory',
})
@@ -180,7 +178,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'it-VA',
base: '/blog/',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'always',
format: 'file',
})
@@ -190,7 +188,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'en',
base: '/blog/',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'ignore',
format: 'directory',
})
@@ -201,7 +199,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'en',
base: '/blog',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'never',
format: 'file',
})
@@ -210,7 +208,7 @@ describe('getLocaleRelativeUrl', () => {
getLocaleRelativeUrl({
locale: 'es',
base: '/blog/',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'always',
format: 'file',
})
@@ -221,7 +219,7 @@ describe('getLocaleRelativeUrl', () => {
locale: 'en',
// ignore + file => no trailing slash
base: '/blog',
- ...config.experimental.i18n,
+ ...config.i18n,
trailingSlash: 'ignore',
format: 'file',
})
@@ -461,7 +459,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'never',
format: 'directory',
})
- ).to.have.members(['/blog', '/blog/en_US', '/blog/es', '/blog/italiano']);
+ ).to.have.members(['/blog', '/blog/en-us', '/blog/es', '/blog/italiano']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', () => {
@@ -494,7 +492,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'always',
format: 'directory',
})
- ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/', '/blog/italiano/']);
+ ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/', '/blog/italiano/']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => {
@@ -519,7 +517,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'always',
format: 'file',
})
- ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/']);
+ ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => {
@@ -544,7 +542,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'never',
format: 'file',
})
- ).to.have.members(['/blog', '/blog/en_US', '/blog/es']);
+ ).to.have.members(['/blog', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => {
@@ -569,7 +567,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'ignore',
format: 'file',
})
- ).to.have.members(['/blog', '/blog/en_US', '/blog/es']);
+ ).to.have.members(['/blog', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => {
@@ -594,7 +592,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'ignore',
format: 'directory',
})
- ).to.have.members(['/blog/', '/blog/en_US/', '/blog/es/']);
+ ).to.have.members(['/blog/', '/blog/en-us/', '/blog/es/']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStategy: pathname-prefix-always]', () => {
@@ -620,7 +618,7 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'never',
format: 'directory',
})
- ).to.have.members(['/blog/en', '/blog/en_US', '/blog/es']);
+ ).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStategy: pathname-prefix-always-no-redirect]', () => {
@@ -646,456 +644,806 @@ describe('getLocaleRelativeUrlList', () => {
trailingSlash: 'never',
format: 'directory',
})
- ).to.have.members(['/blog/en', '/blog/en_US', '/blog/es']);
+ ).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
});
});
describe('getLocaleAbsoluteUrl', () => {
- 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',
- {
- path: 'italiano',
- codes: ['it', 'it-VA'],
+ describe('with [prefix-other-locales]', () => {
+ 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',
+ {
+ path: 'italiano',
+ codes: ['it', 'it-VA'],
+ },
+ ],
+ domains: {
+ es: 'https://es.example.com',
},
- ],
+ routingStrategy: 'prefix-other-locales',
+ },
},
- },
- };
-
- // directory format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- ...config.experimental.i18n,
- })
- ).to.eq('https://example.com/blog/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.throw;
-
- // file format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.throw;
- expect(
- getLocaleAbsoluteUrl({
- locale: 'it-VA',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/italiano/');
+ };
+
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ ...config.experimental.i18n,
+ })
+ ).to.eq('https://example.com/blog/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ isBuild: true,
+ })
+ ).to.eq('https://es.example.com/blog/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.throw;
+
+ // file format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.throw;
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'it-VA',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/italiano/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.throw;
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ isBuild: true,
+ })
+ ).to.eq('https://es.example.com/blog/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ prependWith: 'some-name',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ path: 'first-post',
+ isBuild: true,
+ })
+ ).to.eq('https://es.example.com/blog/some-name/first-post/');
+
+ // en isn't mapped to a domain
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ prependWith: 'some-name',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ path: 'first-post',
+ isBuild: true,
+ })
+ ).to.eq('/blog/some-name/first-post/');
+ });
});
-
- it('should correctly return the URL without base', () => {
- /**
- *
- * @type {import("../../../dist/@types").AstroUserConfig}
- */
- const config = {
- experimental: {
- i18n: {
- defaultLocale: 'en',
- locales: [
- 'en',
- 'es',
- {
- path: 'italiano',
- codes: ['it', 'it-VA'],
+ describe('with [prefix-always]', () => {
+ 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'],
+ domains: {
+ es: 'https://es.example.com',
},
- ],
+ routing: 'pathname-prefix-always',
+ },
},
- },
- };
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/es/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'it-VA',
- base: '/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/italiano/');
- });
-
- it('should correctly handle the trailing slash', () => {
- /**
- *
- * @type {import("../../../dist/@types").AstroUserConfig}
- */
- const config = {
- experimental: {
+ };
+
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ ...config.experimental.i18n,
+ })
+ ).to.eq('https://example.com/blog/en/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.throw;
+
+ // file format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.throw;
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ isBuild: true,
+ })
+ ).to.eq('https://es.example.com/blog/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ prependWith: 'some-name',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ path: 'first-post',
+ isBuild: true,
+ })
+ ).to.eq('https://es.example.com/blog/some-name/first-post/');
+ });
+ it('should correctly return the URL without base', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'es'],
+ routing: 'pathname-prefix-always',
+ },
+ },
+ };
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/es/');
+ });
+
+ it('should correctly handle the trailing slash', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
+ routing: 'pathname-prefix-always',
},
- },
- };
- // directory format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
+ };
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog',
+ ...config.i18n,
+ trailingSlash: 'never',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/en');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.i18n,
+ trailingSlash: 'ignore',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/en/');
+
+ // directory file
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog',
+ ...config.i18n,
+ trailingSlash: 'never',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/en');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ // ignore + file => no trailing slash
+ base: '/blog',
+ ...config.i18n,
+ trailingSlash: 'ignore',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/en');
+ });
+
+ it('should normalize locales', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
base: '/blog',
- ...config.experimental.i18n,
- trailingSlash: 'never',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'ignore',
- format: 'directory',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/');
-
- // directory file
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'en_US', 'en_AU'],
+ routingStrategy: 'pathname-prefix-always',
+ },
+ },
+ };
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('/blog/en-us/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_AU',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('/blog/en-au/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ normalizeLocale: true,
+ })
+ ).to.eq('/blog/en-us/');
+ });
+
+ it('should return the default locale when routing strategy is [pathname-prefix-always]', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
base: '/blog',
- ...config.experimental.i18n,
- trailingSlash: 'never',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- // ignore + file => no trailing slash
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'es', 'en_US', 'en_AU'],
+ routing: 'pathname-prefix-always',
+ },
+ },
+ };
+
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ trailingSlash: 'always',
+ site: 'https://example.com',
+ format: 'directory',
+ ...config.experimental.i18n,
+ })
+ ).to.eq('https://example.com/blog/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.throw;
+
+ // file format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'file',
+ })
+ ).to.eq('https://example.com/blog/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'file',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'file',
+ })
+ ).to.throw;
+ });
+
+ it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
base: '/blog',
- ...config.experimental.i18n,
- trailingSlash: 'ignore',
- format: 'file',
- site: 'https://example.com',
- })
- ).to.eq('https://example.com/blog');
- });
-
- it('should normalize locales', () => {
- /**
- *
- * @type {import("../../../dist/@types").AstroUserConfig}
- */
- const config = {
- base: '/blog',
- experimental: {
- i18n: {
- defaultLocale: 'en',
- locales: ['en', 'en_US', 'en_AU'],
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'es', 'en_US', 'en_AU'],
+ routing: 'pathname-prefix-always-no-redirect',
+ },
},
- },
- };
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.eq('/blog/en-us/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_AU',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.eq('/blog/en-au/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- trailingSlash: 'always',
- format: 'directory',
- normalizeLocale: true,
- })
- ).to.eq('/blog/en-us/');
- });
-
- it('should return the default locale when routing strategy is [pathname-prefix-always]', () => {
- /**
- *
- * @type {import("../../../dist/@types").AstroUserConfig}
- */
- const config = {
- base: '/blog',
- experimental: {
- i18n: {
- defaultLocale: 'en',
- locales: ['en', 'es', 'en_US', 'en_AU'],
- routing: 'pathname-prefix-always',
+ };
+
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ trailingSlash: 'always',
+ site: 'https://example.com',
+ format: 'directory',
+ ...config.experimental.i18n,
+ })
+ ).to.eq('https://example.com/blog/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.throw;
+
+ // file format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'file',
+ })
+ ).to.eq('https://example.com/blog/en/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ trailingSlash: 'always',
+ format: 'file',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ site: 'https://example.com',
+ 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',
+ {
+ path: 'italiano',
+ codes: ['it', 'it-VA'],
+ },
+ ],
+ routingStrategy: 'prefix-other-locales',
+ },
},
- },
- };
-
- // directory format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- trailingSlash: 'always',
- site: 'https://example.com',
- format: 'directory',
- ...config.experimental.i18n,
- })
- ).to.eq('https://example.com/blog/en/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.throw;
-
- // file format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.eq('https://example.com/blog/en/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.throw;
- });
-
- it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => {
- /**
- *
- * @type {import("../../../dist/@types").AstroUserConfig}
- */
- const config = {
- base: '/blog',
- experimental: {
- i18n: {
- defaultLocale: 'en',
- locales: ['en', 'es', 'en_US', 'en_AU'],
- routing: 'pathname-prefix-always-no-redirect',
+ };
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/es/');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'it-VA',
+ base: '/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/italiano/');
+ });
+
+ it('should correctly handle the trailing slash', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'es'],
+ routingStrategy: 'prefix-other-locales',
+ },
},
- },
- };
-
- // directory format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- trailingSlash: 'always',
- site: 'https://example.com',
- format: 'directory',
- ...config.experimental.i18n,
- })
- ).to.eq('https://example.com/blog/en/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'directory',
- })
- ).to.throw;
-
- // file format
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.eq('https://example.com/blog/en/');
- expect(
- getLocaleAbsoluteUrl({
- locale: 'es',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.eq('https://example.com/blog/es/');
-
- expect(
- getLocaleAbsoluteUrl({
- locale: 'en_US',
- base: '/blog/',
- ...config.experimental.i18n,
- site: 'https://example.com',
- trailingSlash: 'always',
- format: 'file',
- })
- ).to.throw;
+ };
+ // directory format
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog',
+ ...config.experimental.i18n,
+ trailingSlash: 'never',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'ignore',
+ format: 'directory',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/');
+
+ // directory file
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ base: '/blog',
+ ...config.experimental.i18n,
+ trailingSlash: 'never',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog');
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'es',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog/es/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en',
+ // ignore + file => no trailing slash
+ base: '/blog',
+ ...config.experimental.i18n,
+ trailingSlash: 'ignore',
+ format: 'file',
+ site: 'https://example.com',
+ })
+ ).to.eq('https://example.com/blog');
+ });
+
+ it('should normalize locales', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
+ base: '/blog',
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'en_US', 'en_AU'],
+ routingStrategy: 'prefix-other-locales',
+ },
+ },
+ };
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('/blog/en-us/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_AU',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ })
+ ).to.eq('/blog/en-au/');
+
+ expect(
+ getLocaleAbsoluteUrl({
+ locale: 'en_US',
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'always',
+ format: 'directory',
+ normalizeLocale: true,
+ })
+ ).to.eq('/blog/en-us/');
+ });
});
});
@@ -1133,7 +1481,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog',
- 'https://example.com/blog/en_US',
+ 'https://example.com/blog/en-us',
'https://example.com/blog/es',
'https://example.com/blog/italiano',
]);
@@ -1164,7 +1512,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog/',
- 'https://example.com/blog/en_US/',
+ 'https://example.com/blog/en-us/',
'https://example.com/blog/es/',
]);
});
@@ -1202,7 +1550,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog/',
- 'https://example.com/blog/en_US/',
+ 'https://example.com/blog/en-us/',
'https://example.com/blog/es/',
'https://example.com/blog/italiano/',
]);
@@ -1233,7 +1581,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog',
- 'https://example.com/blog/en_US',
+ 'https://example.com/blog/en-us',
'https://example.com/blog/es',
]);
});
@@ -1263,7 +1611,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog',
- 'https://example.com/blog/en_US',
+ 'https://example.com/blog/en-us',
'https://example.com/blog/es',
]);
});
@@ -1293,7 +1641,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog/',
- 'https://example.com/blog/en_US/',
+ 'https://example.com/blog/en-us/',
'https://example.com/blog/es/',
]);
});
@@ -1324,7 +1672,7 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog/en/',
- 'https://example.com/blog/en_US/',
+ 'https://example.com/blog/en-us/',
'https://example.com/blog/es/',
]);
});
@@ -1355,10 +1703,45 @@ describe('getLocaleAbsoluteUrlList', () => {
})
).to.have.members([
'https://example.com/blog/en/',
- 'https://example.com/blog/en_US/',
+ 'https://example.com/blog/en-us/',
'https://example.com/blog/es/',
]);
});
+
+ it('should retrieve the correct list of base URLs, swapped with the correct domain', () => {
+ /**
+ *
+ * @type {import("../../../dist/@types").AstroUserConfig}
+ */
+ const config = {
+ experimental: {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'en_US', 'es'],
+ routingStrategy: 'pathname-prefix-always',
+ domains: {
+ es: 'https://es.example.com',
+ en: 'https://example.uk',
+ },
+ },
+ },
+ };
+ // directory format
+ expect(
+ getLocaleAbsoluteUrlList({
+ base: '/blog/',
+ ...config.experimental.i18n,
+ trailingSlash: 'ignore',
+ format: 'directory',
+ site: 'https://example.com',
+ isBuild: true,
+ })
+ ).to.have.members([
+ 'https://example.uk/blog/',
+ 'https://example.com/blog/en-us/',
+ 'https://es.example.com/blog/',
+ ]);
+ });
});
describe('parse accept-header', () => {
diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js
index ff700833e..882570db2 100644
--- a/packages/astro/test/units/integrations/api.test.js
+++ b/packages/astro/test/units/integrations/api.test.js
@@ -125,6 +125,7 @@ describe('Astro feature map', function () {
{
output: 'hybrid',
},
+ {},
defaultLogger
);
expect(result['hybridOutput']).to.be.true;
@@ -137,6 +138,7 @@ describe('Astro feature map', function () {
{
output: 'hybrid',
},
+ {},
defaultLogger
);
expect(result['hybridOutput']).to.be.false;
@@ -149,6 +151,7 @@ describe('Astro feature map', function () {
{
output: 'hybrid',
},
+ {},
defaultLogger
);
expect(result['hybridOutput']).to.be.false;
@@ -162,6 +165,7 @@ describe('Astro feature map', function () {
{
output: 'static',
},
+ {},
defaultLogger
);
expect(result['staticOutput']).to.be.true;
@@ -174,6 +178,7 @@ describe('Astro feature map', function () {
{
output: 'static',
},
+ {},
defaultLogger
);
expect(result['staticOutput']).to.be.false;
@@ -187,6 +192,7 @@ describe('Astro feature map', function () {
{
output: 'hybrid',
},
+ {},
defaultLogger
);
expect(result['hybridOutput']).to.be.true;
@@ -201,6 +207,7 @@ describe('Astro feature map', function () {
{
output: 'hybrid',
},
+ {},
defaultLogger
);
expect(result['hybridOutput']).to.be.false;
@@ -214,6 +221,7 @@ describe('Astro feature map', function () {
{
output: 'server',
},
+ {},
defaultLogger
);
expect(result['serverOutput']).to.be.true;
@@ -228,6 +236,7 @@ describe('Astro feature map', function () {
{
output: 'server',
},
+ {},
defaultLogger
);
expect(result['serverOutput']).to.be.false;
@@ -251,6 +260,7 @@ describe('Astro feature map', function () {
},
},
},
+ {},
defaultLogger
);
expect(result['assets']).to.be.true;
@@ -271,6 +281,7 @@ describe('Astro feature map', function () {
},
},
},
+ {},
defaultLogger
);
expect(result['assets']).to.be.true;
@@ -292,6 +303,7 @@ describe('Astro feature map', function () {
},
},
},
+ {},
defaultLogger
);
expect(result['assets']).to.be.false;
diff --git a/packages/create-astro/src/actions/typescript.ts b/packages/create-astro/src/actions/typescript.ts
index efff60994..78f75daf5 100644
--- a/packages/create-astro/src/actions/typescript.ts
+++ b/packages/create-astro/src/actions/typescript.ts
@@ -1,5 +1,4 @@
import type { Context } from './context.js';
-
import { color } from '@astrojs/cli-kit';
import { readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts
index e7d655403..61ea5ad74 100644
--- a/packages/integrations/node/src/index.ts
+++ b/packages/integrations/node/src/index.ts
@@ -18,6 +18,7 @@ export function getAdapter(options: Options): AstroAdapter {
isSharpCompatible: true,
isSquooshCompatible: true,
},
+ i18nDomains: 'experimental',
},
};
}
diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts
index f1c75e4b4..11efdc16f 100644
--- a/packages/integrations/vercel/src/serverless/adapter.ts
+++ b/packages/integrations/vercel/src/serverless/adapter.ts
@@ -82,6 +82,7 @@ function getAdapter({
isSharpCompatible: true,
isSquooshCompatible: true,
},
+ i18nDomains: 'experimental',
},
};
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 943fe25c9..307fc01b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2900,6 +2900,12 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/i18n-routing-subdomain:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/test/fixtures/import-ts-with-js:
dependencies:
astro: