diff --git a/.changeset/tame-crabs-reply.md b/.changeset/tame-crabs-reply.md new file mode 100644 index 000000000000..e20c17528e94 --- /dev/null +++ b/.changeset/tame-crabs-reply.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Refactors internals of the `astro:i18n` module to be more maintainable. diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 3b30d7700886..8542faa471a9 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -149,147 +149,7 @@ declare module 'astro:prefetch' { } declare module 'astro:i18n' { - export type GetLocaleOptions = import('./dist/virtual-modules/i18n.js').GetLocaleOptions; - - /** - * @param {string} locale A locale - * @param {string} [path=""] An optional path to add after the `locale`. - * @param {import('./dist/virtual-modules/i18n.js').GetLocaleOptions} options Customise the generated path - * @return {string} - * - * Returns a _relative_ path with passed locale. - * - * ## Errors - * - * Throws an error if the locale doesn't exist in the list of locales defined in the configuration. - * - * ## Examples - * - * ```js - * import { getRelativeLocaleUrl } from "astro:i18n"; - * getRelativeLocaleUrl("es"); // /es - * getRelativeLocaleUrl("es", "getting-started"); // /es/getting-started - * getRelativeLocaleUrl("es_US", "getting-started", { prependWith: "blog" }); // /blog/es-us/getting-started - * getRelativeLocaleUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // /blog/es_US/getting-started - * ``` - */ - export const getRelativeLocaleUrl: ( - locale: string, - path?: string, - options?: GetLocaleOptions - ) => string; - - /** - * - * @param {string} locale A locale - * @param {string} [path=""] An optional path to add after the `locale`. - * @param {import('./dist/virtual-modules/i18n.js').GetLocaleOptions} options Customise the generated path - * @return {string} - * - * Returns an absolute path with the passed locale. The behaviour is subject to change based on `site` configuration. - * If _not_ provided, the function will return a _relative_ URL. - * - * ## Errors - * - * Throws an error if the locale doesn't exist in the list of locales defined in the configuration. - * - * ## Examples - * - * If `site` is `https://example.com`: - * - * ```js - * import { getAbsoluteLocaleUrl } from "astro:i18n"; - * getAbsoluteLocaleUrl("es"); // https://example.com/es - * getAbsoluteLocaleUrl("es", "getting-started"); // https://example.com/es/getting-started - * getAbsoluteLocaleUrl("es_US", "getting-started", { prependWith: "blog" }); // https://example.com/blog/es-us/getting-started - * getAbsoluteLocaleUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // https://example.com/blog/es_US/getting-started - * ``` - */ - export const getAbsoluteLocaleUrl: ( - locale: string, - path?: string, - options?: GetLocaleOptions - ) => string; - - /** - * @param {string} [path=""] An optional path to add after the `locale`. - * @param {import('./dist/virtual-modules/i18n.js').GetLocaleOptions} options Customise the generated path - * @return {string[]} - * - * Works like `getRelativeLocaleUrl` but it emits the relative URLs for ALL locales: - */ - export const getRelativeLocaleUrlList: (path?: string, options?: GetLocaleOptions) => string[]; - /** - * @param {string} [path=""] An optional path to add after the `locale`. - * @param {import('./dist/virtual-modules/i18n.js').GetLocaleOptions} options Customise the generated path - * @return {string[]} - * - * Works like `getAbsoluteLocaleUrl` but it emits the absolute URLs for ALL locales: - */ - export const getAbsoluteLocaleUrlList: (path?: string, options?: GetLocaleOptions) => string[]; - - /** - * A function that return the `path` associated to a locale (defined as code). It's particularly useful in case you decide - * to use locales that are broken down in paths and codes. - * - * @param {string} code The code of the locale - * @returns {string} The path associated to the locale - * - * ## Example - * - * ```js - * // astro.config.mjs - * - * export default defineConfig({ - * i18n: { - * locales: [ - * { codes: ["it", "it-VT"], path: "italiano" }, - * "es" - * ] - * } - * }) - * ``` - * - * ```js - * import { getPathByLocale } from "astro:i18n"; - * getPathByLocale("it"); // returns "italiano" - * getPathByLocale("it-VT"); // returns "italiano" - * getPathByLocale("es"); // returns "es" - * ``` - */ - export const getPathByLocale: (code: string) => string; - - /** - * A function that returns the preferred locale given a certain path. This is particularly useful if you configure a locale using - * `path` and `codes`. When you define multiple `code`, this function will return the first code of the array. - * - * Astro will treat the first code as the one that the user prefers. - * - * @param {string} path The path that maps to a locale - * @returns {string} The path associated to the locale - * - * ## Example - * - * ```js - * // astro.config.mjs - * - * export default defineConfig({ - * i18n: { - * locales: [ - * { codes: ["it-VT", "it"], path: "italiano" }, - * "es" - * ] - * } - * }) - * ``` - * - * ```js - * import { getLocaleByPath } from "astro:i18n"; - * getLocaleByPath("italiano"); // returns "it-VT" because that's the first code configured - * getLocaleByPath("es"); // returns "es" - * ``` - */ - export const getLocaleByPath: (path: string) => string; + export * from 'astro/virtual-modules/i18n.js'; } declare module 'astro:middleware' { diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 99082b830419..0b0c7d47ff04 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -143,7 +143,7 @@ export async function createVite( astroTransitions({ settings }), astroDevToolbar({ settings, logger }), vitePluginFileURL({}), - !!settings.config.i18n && astroInternationalization({ settings }), + astroInternationalization({ settings }), ], publicDir: fileURLToPath(settings.config.publicDir), root: fileURLToPath(settings.config.root), diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index fb463a77fe1c..74bd29770280 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -1034,6 +1034,18 @@ export const UnhandledRejection = { hint: 'Make sure your promises all have an `await` or a `.catch()` handler.', } satisfies ErrorData; +/** + * @docs + * @description + * Astro could not find any code to handle a rejected `Promise`. Make sure all your promises have an `await` or `.catch()` handler. + */ +export const i18nNotEnabled = { + name: 'i18nNotEnabled', + title: 'i18n Not Enabled', + message: 'The `astro:i18n` module can not be used without enabling i18n in your Astro config.', + hint: 'See https://docs.astro.build/en/guides/internationalization for a guide on setting up i18n.', +} satisfies ErrorData; + /** * @docs * @kind heading diff --git a/packages/astro/src/i18n/index.ts b/packages/astro/src/i18n/index.ts index 5a44d084092b..4b882b7829df 100644 --- a/packages/astro/src/i18n/index.ts +++ b/packages/astro/src/i18n/index.ts @@ -84,14 +84,14 @@ export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) { } } -type GetLocalesBaseUrl = GetLocaleOptions & { +interface GetLocalesRelativeUrlList extends GetLocaleOptions { base: string; locales: Locales; trailingSlash: AstroConfig['trailingSlash']; format: AstroConfig['build']['format']; routing?: RoutingStrategies; defaultLocale: string; -}; +} export function getLocaleRelativeUrlList({ base, @@ -103,7 +103,7 @@ export function getLocaleRelativeUrlList({ normalizeLocale = false, routing = 'pathname-prefix-other-locales', defaultLocale, -}: GetLocalesBaseUrl) { +}: GetLocalesRelativeUrlList) { const locales = toPaths(_locales); return locales.map((locale) => { const pathsToJoin = [base, prependWith]; @@ -123,7 +123,11 @@ export function getLocaleRelativeUrlList({ }); } -export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl) { +interface GetLocalesAbsoluteUrlList extends GetLocalesRelativeUrlList { + site?: string; +} + +export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocalesAbsoluteUrlList) { const locales = getLocaleRelativeUrlList(rest); return locales.map((locale) => { if (site) { @@ -139,7 +143,7 @@ export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl * @param locale * @param locales */ -export function getPathByLocale(locale: string, locales: Locales) { +export function getPathByLocale(locale: string, locales: Locales): string { for (const loopLocale of locales) { if (typeof loopLocale === 'string') { if (loopLocale === locale) { @@ -153,6 +157,7 @@ export function getPathByLocale(locale: string, locales: Locales) { } } } + throw new Unreachable(); } /** @@ -161,19 +166,20 @@ export function getPathByLocale(locale: string, locales: Locales) { * @param path * @param locales */ -export function getLocaleByPath(path: string, locales: Locales): string | undefined { +export function getLocaleByPath(path: string, locales: Locales): string { for (const locale of locales) { if (typeof locale !== 'string') { if (locale.path === path) { // the first code is the one that user usually wants const code = locale.codes.at(0); + if (code === undefined) throw new Unreachable(); return code; } } else if (locale === path) { return locale; } } - return undefined; + throw new Unreachable(); } /** @@ -235,3 +241,14 @@ function peekCodePathToUse(locales: Locales, locale: string): undefined | string return undefined; } + +class Unreachable extends Error { + constructor() { + super( + 'Astro encountered an unexpected line of code.\n' + + 'In most cases, this is not your fault, but a bug in astro code.\n' + + "If there isn't one already, please create an issue.\n" + + 'https://astro.build/issues' + ); + } +} diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index b2d04f586fd0..1298dcac8154 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -29,27 +29,16 @@ export function createI18nMiddleware( base: SSRManifest['base'], trailingSlash: SSRManifest['trailingSlash'], buildFormat: SSRManifest['buildFormat'] -): MiddlewareHandler | undefined { - if (!i18n) { - return undefined; - } +): MiddlewareHandler { + if (!i18n) return (_, next) => next(); return async (context, next) => { - if (!i18n) { + 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 routeData = Reflect.get(context.request, routeDataSymbol); - if (routeData) { - // If the route we're processing is not a page, then we ignore it - if ( - (routeData as RouteData).type !== 'page' && - (routeData as RouteData).type !== 'fallback' - ) { - return await next(); - } - } - const url = context.url; const { locales, defaultLocale, fallback, routing } = i18n; const response = await next(); diff --git a/packages/astro/src/i18n/vite-plugin-i18n.ts b/packages/astro/src/i18n/vite-plugin-i18n.ts index f01116f6cf41..856ce46a7415 100644 --- a/packages/astro/src/i18n/vite-plugin-i18n.ts +++ b/packages/astro/src/i18n/vite-plugin-i18n.ts @@ -1,69 +1,55 @@ import type * as vite from 'vite'; -import type { AstroSettings } from '../@types/astro.js'; +import type { AstroConfig, AstroSettings } from '../@types/astro.js'; +import { AstroError } from '../core/errors/errors.js'; +import { AstroErrorData } from '../core/errors/index.js'; const virtualModuleId = 'astro:i18n'; -const resolvedVirtualModuleId = '\0' + virtualModuleId; +const configId = 'astro-internal:i18n-config'; +const resolvedConfigId = `\0${configId}`; type AstroInternationalization = { settings: AstroSettings; }; +export interface I18nInternalConfig + extends Pick, + NonNullable, + Pick {} + export default function astroInternationalization({ settings, }: AstroInternationalization): vite.Plugin { + const { + base, + build: { format }, + i18n, + site, + trailingSlash, + } = settings.config; return { name: 'astro:i18n', enforce: 'pre', async resolveId(id) { if (id === virtualModuleId) { - return resolvedVirtualModuleId; + if (i18n === undefined) throw new AstroError(AstroErrorData.i18nNotEnabled); + return this.resolve('astro/virtual-modules/i18n.js'); } + if (id === configId) return resolvedConfigId; }, load(id) { - if (id === resolvedVirtualModuleId) { - return ` - import { - getLocaleRelativeUrl as _getLocaleRelativeUrl, - getLocaleRelativeUrlList as _getLocaleRelativeUrlList, - getLocaleAbsoluteUrl as _getLocaleAbsoluteUrl, - getLocaleAbsoluteUrlList as _getLocaleAbsoluteUrlList, - getPathByLocale as _getPathByLocale, - getLocaleByPath as _getLocaleByPath, - } from "astro/virtual-modules/i18n.js"; - - const base = ${JSON.stringify(settings.config.base)}; - const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)}; - const format = ${JSON.stringify(settings.config.build.format)}; - const site = ${JSON.stringify(settings.config.site)}; - const i18n = ${JSON.stringify(settings.config.i18n)}; - - export const getRelativeLocaleUrl = (locale, path = "", opts) => _getLocaleRelativeUrl({ - locale, - path, - base, - trailingSlash, - format, - ...i18n, - ...opts - }); - export const getAbsoluteLocaleUrl = (locale, path = "", opts) => _getLocaleAbsoluteUrl({ - locale, - path, - base, - trailingSlash, - format, - site, - ...i18n, - ...opts - }); - - export const getRelativeLocaleUrlList = (path = "", opts) => _getLocaleRelativeUrlList({ - base, path, trailingSlash, format, ...i18n, ...opts }); - export const getAbsoluteLocaleUrlList = (path = "", opts) => _getLocaleAbsoluteUrlList({ base, path, trailingSlash, format, site, ...i18n, ...opts }); - - export const getPathByLocale = (locale) => _getPathByLocale(locale, i18n.locales); - export const getLocaleByPath = (path) => _getLocaleByPath(path, i18n.locales); - `; + if (id === resolvedConfigId) { + const { defaultLocale, locales, routing, fallback } = i18n!; + const config: I18nInternalConfig = { + base, + format, + site, + trailingSlash, + defaultLocale, + locales, + routing, + fallback, + }; + return `export default ${JSON.stringify(config)};`; } }, }; diff --git a/packages/astro/src/virtual-modules/i18n.ts b/packages/astro/src/virtual-modules/i18n.ts index a55c1f6cfb59..d9d470431117 100644 --- a/packages/astro/src/virtual-modules/i18n.ts +++ b/packages/astro/src/virtual-modules/i18n.ts @@ -1 +1,183 @@ -export * from '../i18n/index.js'; +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 +import config from 'astro-internal:i18n-config'; +const { trailingSlash, format, site, defaultLocale, locales, routing } = + config as I18nInternalConfig; +const base = import.meta.env.BASE_URL; + +export type GetLocaleOptions = I18nInternals.GetLocaleOptions; + +/** + * @param locale A locale + * @param path An optional path to add after the `locale`. + * @param options Customise the generated path + * + * Returns a _relative_ path with passed locale. + * + * ## Errors + * + * Throws an error if the locale doesn't exist in the list of locales defined in the configuration. + * + * ## Examples + * + * ```js + * import { getRelativeLocaleUrl } from "astro:i18n"; + * getRelativeLocaleUrl("es"); // /es + * getRelativeLocaleUrl("es", "getting-started"); // /es/getting-started + * getRelativeLocaleUrl("es_US", "getting-started", { prependWith: "blog" }); // /blog/es-us/getting-started + * getRelativeLocaleUrl("es_US", "getting-started", { prependWith: "blog", normalizeLocale: false }); // /blog/es_US/getting-started + * ``` + */ +export const getRelativeLocaleUrl = (locale: string, path?: string, options?: GetLocaleOptions) => + I18nInternals.getLocaleRelativeUrl({ + locale, + path, + base, + trailingSlash, + format, + defaultLocale, + locales, + routing, + ...options, + }); + +/** + * + * @param locale A locale + * @param path An optional path to add after the `locale`. + * @param options Customise the generated path + * + * Returns an absolute path with the passed locale. The behaviour is subject to change based on `site` configuration. + * If _not_ provided, the function will return a _relative_ URL. + * + * ## Errors + * + * Throws an error if the locale doesn't exist in the list of locales defined in the configuration. + * + * ## Examples + * + * If `site` is `https://example.com`: + * + * ```js + * import { getAbsoluteLocaleUrl } from "astro:i18n"; + * getAbsoluteLocaleUrl("es"); // https://example.com/es + * getAbsoluteLocaleUrl("es", "getting-started"); // https://example.com/es/getting-started + * getAbsoluteLocaleUrl("es_US", "getting-started", { prependWith: "blog" }); // https://example.com/blog/es-us/getting-started + * 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) => + I18nInternals.getLocaleAbsoluteUrl({ + locale, + path, + base, + trailingSlash, + format, + site, + defaultLocale, + locales, + routing, + ...options, + }); + +/** + * @param path An optional path to add after the `locale`. + * @param options Customise the generated path + * + * Works like `getRelativeLocaleUrl` but it emits the relative URLs for ALL locales: + */ +export const getRelativeLocaleUrlList = (path?: string, options?: GetLocaleOptions) => + I18nInternals.getLocaleRelativeUrlList({ + base, + path, + trailingSlash, + format, + defaultLocale, + locales, + routing, + ...options, + }); + +/** + * @param path An optional path to add after the `locale`. + * @param options Customise the generated path + * + * Works like `getAbsoluteLocaleUrl` but it emits the absolute URLs for ALL locales: + */ +export const getAbsoluteLocaleUrlList = (path?: string, options?: GetLocaleOptions) => + I18nInternals.getLocaleAbsoluteUrlList({ + site, + base, + path, + trailingSlash, + format, + defaultLocale, + locales, + routing, + ...options, + }); + +/** + * A function that return the `path` associated to a locale (defined as code). It's particularly useful in case you decide + * to use locales that are broken down in paths and codes. + * + * @param locale The code of the locale + * @returns The path associated to the locale + * + * ## Example + * + * ```js + * // astro.config.mjs + * + * export default defineConfig({ + * i18n: { + * locales: [ + * { codes: ["it", "it-VT"], path: "italiano" }, + * "es" + * ] + * } + * }) + * ``` + * + * ```js + * import { getPathByLocale } from "astro:i18n"; + * getPathByLocale("it"); // returns "italiano" + * getPathByLocale("it-VT"); // returns "italiano" + * getPathByLocale("es"); // returns "es" + * ``` + */ +export const getPathByLocale = (locale: string) => I18nInternals.getPathByLocale(locale, locales); + +/** + * A function that returns the preferred locale given a certain path. This is particularly useful if you configure a locale using + * `path` and `codes`. When you define multiple `code`, this function will return the first code of the array. + * + * Astro will treat the first code as the one that the user prefers. + * + * @param path The path that maps to a locale + * @returns The path associated to the locale + * + * ## Example + * + * ```js + * // astro.config.mjs + * + * export default defineConfig({ + * i18n: { + * locales: [ + * { codes: ["it-VT", "it"], path: "italiano" }, + * "es" + * ] + * } + * }) + * ``` + * + * ```js + * import { getLocaleByPath } from "astro:i18n"; + * getLocaleByPath("italiano"); // returns "it-VT" because that's the first code configured + * getLocaleByPath("es"); // returns "es" + * ``` + */ +export const getLocaleByPath = (path: string) => I18nInternals.getLocaleByPath(path, locales);