feat(i18n): first draft of configuration (#8607)
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
parent
78adbc4433
commit
18223d9cde
8 changed files with 211 additions and 1 deletions
5
packages/astro/client.d.ts
vendored
5
packages/astro/client.d.ts
vendored
|
@ -127,6 +127,11 @@ declare module 'astro:transitions/client' {
|
|||
export const navigate: TransitionRouterModule['navigate'];
|
||||
}
|
||||
|
||||
declare module 'astro:i18n' {
|
||||
type I18nModule = typeof import('./dist/i18n/index.js');
|
||||
export const getI18nBaseUrl: (locale: string) => string;
|
||||
}
|
||||
|
||||
declare module 'astro:middleware' {
|
||||
export * from 'astro/middleware/namespace';
|
||||
}
|
||||
|
|
|
@ -78,7 +78,8 @@
|
|||
"default": "./dist/core/middleware/namespace.js"
|
||||
},
|
||||
"./transitions": "./dist/transitions/index.js",
|
||||
"./transitions/router": "./dist/transitions/router.js"
|
||||
"./transitions": "./dist/transitions/index.js",
|
||||
"./i18n": "./dist/i18n/index.js"
|
||||
},
|
||||
"imports": {
|
||||
"#astro/*": "./dist/*.js"
|
||||
|
|
|
@ -1330,6 +1330,68 @@ export interface AstroUserConfig {
|
|||
* ```
|
||||
*/
|
||||
optimizeHoistedScript?: boolean;
|
||||
|
||||
// TODO review with docs team before merging to `main`
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n
|
||||
* @type {object}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* Allows to configure the beaviour of the i18n routing
|
||||
*/
|
||||
i18n?: {
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.defaultLocale
|
||||
* @type {string}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* The default locale of your website/application
|
||||
*/
|
||||
defaultLocale: string;
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.locales
|
||||
* @type {string[]}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* A list of locales supported by the website.
|
||||
*/
|
||||
locales: string[];
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.fallback
|
||||
* @type {Record<string, string[]>}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* The fallback system of the locales. By default, the fallback system affect the **content only**, and it doesn't
|
||||
* do any redirects.
|
||||
*
|
||||
* This means that when attempting to navigate to a page that hasn't been translated, Astro will pull the content
|
||||
* from the page of the default locale and render it. No redirects will happen.
|
||||
*/
|
||||
fallback?: Record<string, string[]>;
|
||||
|
||||
/**
|
||||
* @docs
|
||||
* @name experimental.i18n.detectBrowserLanguage
|
||||
* @type {boolean}
|
||||
* @version 3.*.*
|
||||
* @description
|
||||
*
|
||||
* Whether Astro should detect the language of the browser - usually using the `Accept-Language` header. This is a feature
|
||||
* that should be supported by the adapter. If detected, the adapter can decide to redirect the user to the localised version of the website.
|
||||
*
|
||||
* When set to `true`, you should make sure that the adapter you're using is able to provide this feature to you.
|
||||
*/
|
||||
detectBrowserLanguage: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1790,6 +1852,11 @@ export type AstroFeatureMap = {
|
|||
* The adapter can emit static assets
|
||||
*/
|
||||
assets?: AstroAssetsFeature;
|
||||
|
||||
/**
|
||||
* List of features that orbit around the i18n routing
|
||||
*/
|
||||
i18n?: AstroInternalisationFeature;
|
||||
};
|
||||
|
||||
export interface AstroAssetsFeature {
|
||||
|
@ -1804,6 +1871,13 @@ export interface AstroAssetsFeature {
|
|||
isSquooshCompatible?: boolean;
|
||||
}
|
||||
|
||||
export interface AstroInternalisationFeature {
|
||||
/**
|
||||
* Wether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
|
||||
*/
|
||||
detectBrowserLanguage?: SupportsKind;
|
||||
}
|
||||
|
||||
export interface AstroAdapter {
|
||||
name: string;
|
||||
serverEntrypoint?: string;
|
||||
|
|
|
@ -271,6 +271,43 @@ export const AstroConfigSchema = z.object({
|
|||
.boolean()
|
||||
.optional()
|
||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
||||
i18n: z.optional(
|
||||
z
|
||||
.object({
|
||||
defaultLocale: z.string(),
|
||||
locales: z.string().array(),
|
||||
fallback: z.record(z.string(), z.string().array()).optional(),
|
||||
detectBrowserLanguage: z.boolean().optional().default(false),
|
||||
})
|
||||
.optional()
|
||||
.refine((i18n) => {
|
||||
if (i18n) {
|
||||
const { defaultLocale, locales, fallback } = i18n;
|
||||
if (locales.includes(defaultLocale)) {
|
||||
return {
|
||||
message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`,
|
||||
};
|
||||
}
|
||||
if (fallback) {
|
||||
for (const [fallbackKey, fallbackArray] of Object.entries(fallback)) {
|
||||
if (!locales.includes(fallbackKey)) {
|
||||
return {
|
||||
message: `The locale \`${fallbackKey}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||
};
|
||||
}
|
||||
|
||||
for (const fallbackArrayKey of fallbackArray) {
|
||||
if (!locales.includes(fallbackArrayKey)) {
|
||||
return {
|
||||
message: `The locale \`${fallbackArrayKey}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
})
|
||||
.strict(
|
||||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
||||
|
|
21
packages/astro/src/i18n/index.ts
Normal file
21
packages/astro/src/i18n/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { AstroConfig } from '../@types/astro.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
/**
|
||||
* The base URL
|
||||
*/
|
||||
export function getI18nBaseUrl(locale: string, config: AstroConfig, logger: Logger) {
|
||||
const base = config.base;
|
||||
|
||||
if (!config.experimental.i18n) {
|
||||
logger.error('i18n', "The project isn't using i18n features, no need to use this function.");
|
||||
return base ? `/${base}/` : '/';
|
||||
}
|
||||
|
||||
if (base) {
|
||||
logger.debug('i18n', 'The project has a base directory, using it.');
|
||||
return `${base}/${locale}/`;
|
||||
} else {
|
||||
return `/${locale}/`;
|
||||
}
|
||||
}
|
26
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
26
packages/astro/src/i18n/vite-plugin-i18n.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import * as vite from 'vite';
|
||||
import type { AstroSettings } from '../@types/astro.js';
|
||||
import type { Logger } from '../core/logger/core.js';
|
||||
|
||||
const virtualModuleId = 'astro:i18n';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
type AstroInternalization = {
|
||||
settings: AstroSettings;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export default function astroInternalization({ settings }: AstroInternalization): vite.Plugin {
|
||||
return {
|
||||
name: 'astro:i18n',
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `
|
||||
import { getI18nBaseUrl as getI18nBaseUrlInternal } from "astro/i18n";
|
||||
|
||||
export getI18nBaseUrl = (locale) => getI18nBaseUrlInternal(locale, ${settings.config});
|
||||
`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -23,6 +23,9 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
|
|||
staticOutput: UNSUPPORTED,
|
||||
hybridOutput: UNSUPPORTED,
|
||||
assets: UNSUPPORTED_ASSETS_FEATURE,
|
||||
i18n: {
|
||||
detectBrowserLanguage: UNSUPPORTED,
|
||||
},
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
|
|
43
packages/astro/test/units/i18n/getI18nBaseUrl.test.js
Normal file
43
packages/astro/test/units/i18n/getI18nBaseUrl.test.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { getI18nBaseUrl } from '../../../dist/i18n/index.js';
|
||||
import { expect } from 'chai';
|
||||
import { Logger } from '../../../dist/core/logger/core.js';
|
||||
|
||||
const logger = new Logger();
|
||||
describe('getI18nBaseUrl', () => {
|
||||
it('should correctly return the URL with the base', () => {
|
||||
/**
|
||||
*
|
||||
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||
*/
|
||||
const config = {
|
||||
base: '/blog',
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getI18nBaseUrl('en', config, logger)).to.eq('/blog/en/');
|
||||
expect(getI18nBaseUrl('es', config, logger)).to.eq('/blog/es/');
|
||||
});
|
||||
|
||||
it('should correctly return the URL without base', () => {
|
||||
/**
|
||||
*
|
||||
* @type {import("../../../dist/@types").AstroUserConfig}
|
||||
*/
|
||||
const config = {
|
||||
experimental: {
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getI18nBaseUrl('en', config, logger)).to.eq('/en/');
|
||||
expect(getI18nBaseUrl('es', config, logger)).to.eq('/es/');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue