feat(i18n): first draft of configuration (#8607)

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
Emanuele Stoppa 2023-09-22 15:09:20 +02:00
parent 78adbc4433
commit 18223d9cde
8 changed files with 211 additions and 1 deletions

View file

@ -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';
}

View file

@ -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"

View file

@ -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;

View file

@ -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.`

View 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}/`;
}
}

View 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});
`;
}
},
};
}

View file

@ -23,6 +23,9 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
staticOutput: UNSUPPORTED,
hybridOutput: UNSUPPORTED,
assets: UNSUPPORTED_ASSETS_FEATURE,
i18n: {
detectBrowserLanguage: UNSUPPORTED,
},
};
type ValidationResult = {

View 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/');
});
});