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'];
|
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' {
|
declare module 'astro:middleware' {
|
||||||
export * from 'astro/middleware/namespace';
|
export * from 'astro/middleware/namespace';
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,8 @@
|
||||||
"default": "./dist/core/middleware/namespace.js"
|
"default": "./dist/core/middleware/namespace.js"
|
||||||
},
|
},
|
||||||
"./transitions": "./dist/transitions/index.js",
|
"./transitions": "./dist/transitions/index.js",
|
||||||
"./transitions/router": "./dist/transitions/router.js"
|
"./transitions": "./dist/transitions/index.js",
|
||||||
|
"./i18n": "./dist/i18n/index.js"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#astro/*": "./dist/*.js"
|
"#astro/*": "./dist/*.js"
|
||||||
|
|
|
@ -1330,6 +1330,68 @@ export interface AstroUserConfig {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
optimizeHoistedScript?: boolean;
|
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
|
* The adapter can emit static assets
|
||||||
*/
|
*/
|
||||||
assets?: AstroAssetsFeature;
|
assets?: AstroAssetsFeature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of features that orbit around the i18n routing
|
||||||
|
*/
|
||||||
|
i18n?: AstroInternalisationFeature;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AstroAssetsFeature {
|
export interface AstroAssetsFeature {
|
||||||
|
@ -1804,6 +1871,13 @@ export interface AstroAssetsFeature {
|
||||||
isSquooshCompatible?: boolean;
|
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 {
|
export interface AstroAdapter {
|
||||||
name: string;
|
name: string;
|
||||||
serverEntrypoint?: string;
|
serverEntrypoint?: string;
|
||||||
|
|
|
@ -271,6 +271,43 @@ export const AstroConfigSchema = z.object({
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
||||||
|
i18n: z.optional(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
defaultLocale: z.string(),
|
||||||
|
locales: z.string().array(),
|
||||||
|
fallback: z.record(z.string(), z.string().array()).optional(),
|
||||||
|
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(
|
.strict(
|
||||||
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
||||||
|
|
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,
|
staticOutput: UNSUPPORTED,
|
||||||
hybridOutput: UNSUPPORTED,
|
hybridOutput: UNSUPPORTED,
|
||||||
assets: UNSUPPORTED_ASSETS_FEATURE,
|
assets: UNSUPPORTED_ASSETS_FEATURE,
|
||||||
|
i18n: {
|
||||||
|
detectBrowserLanguage: UNSUPPORTED,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type ValidationResult = {
|
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…
Add table
Reference in a new issue