feat(i18n): better virtual module functions (#8778)

Co-authored-by: Martin Trapp <94928215+martrapp@users.noreply.github.com>
This commit is contained in:
Emanuele Stoppa 2023-10-09 13:24:53 +01:00 committed by GitHub
parent 9f3f110268
commit 7678ef33a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 463 additions and 46 deletions

View file

@ -129,7 +129,12 @@ declare module 'astro:transitions/client' {
declare module 'astro:i18n' {
type I18nModule = typeof import('./dist/i18n/index.js');
// TODO: documentation
export const getI18nBaseUrl: (locale: string) => string;
// TODO: documentation
export const getLocalesBaseUrl: () => string[];
}
declare module 'astro:middleware' {

View file

@ -78,7 +78,7 @@
"default": "./dist/core/middleware/namespace.js"
},
"./transitions": "./dist/transitions/index.js",
"./transitions": "./dist/transitions/index.js",
"./transitions/router": "./dist/transitions/router.js",
"./i18n": "./dist/i18n/index.js"
},
"imports": {

View file

@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url';
import type { OutputAsset, OutputChunk } from 'rollup';
import type { BufferEncoding } from 'vfile';
import type {
AstroConfig,
AstroSettings,
ComponentInstance,
GetStaticPathsItem,
@ -58,7 +57,7 @@ import type {
StaticBuildOptions,
StylesheetAsset,
} from './types.js';
import { getTimeStat } from './util.js';
import { getTimeStat, shouldAppendForwardSlash } from './util.js';
function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
@ -431,26 +430,6 @@ interface GeneratePathOptions {
mod: ComponentInstance;
}
function shouldAppendForwardSlash(
trailingSlash: AstroConfig['trailingSlash'],
buildFormat: AstroConfig['build']['format']
): boolean {
switch (trailingSlash) {
case 'always':
return true;
case 'never':
return false;
case 'ignore': {
switch (buildFormat) {
case 'directory':
return true;
case 'file':
return false;
}
}
}
}
function addPageName(pathname: string, opts: StaticBuildOptions): void {
const trailingSlash = opts.settings.config.trailingSlash;
const buildFormat = opts.settings.config.build.format;

View file

@ -1,4 +1,29 @@
import type { AstroConfig } from '../../@types/astro.js';
export function getTimeStat(timeStart: number, timeEnd: number) {
const buildTime = timeEnd - timeStart;
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
}
/**
* Given the Astro configuration, it tells if a slash should be appended or not
*/
export function shouldAppendForwardSlash(
trailingSlash: AstroConfig['trailingSlash'],
buildFormat: AstroConfig['build']['format']
): boolean {
switch (trailingSlash) {
case 'always':
return true;
case 'never':
return false;
case 'ignore': {
switch (buildFormat) {
case 'directory':
return true;
case 'file':
return false;
}
}
}
}

View file

@ -29,6 +29,7 @@ import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
import { joinPaths } from './path.js';
import astroInternalization from '../i18n/vite-plugin-i18n.js';
interface CreateViteOptions {
settings: AstroSettings;
@ -134,6 +135,7 @@ export async function createVite(
vitePluginSSRManifest(),
astroAssetsPlugin({ settings, logger, mode }),
astroTransitions(),
!!settings.config.experimental.i18n && astroInternalization({ settings, logger }),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),

View file

@ -1243,5 +1243,15 @@ export const UnsupportedConfigTransformError = {
hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
} satisfies ErrorData;
export const MissingLocale = {
name: 'MissingLocaleError',
title: 'The provided locale does not exist.',
message: (locale: string, locales: string[]) => {
return `The locale \`${locale}\` does not exist in the configured locales. Available locales: ${locales.join(
', '
)}.`;
},
} satisfies ErrorData;
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;

View file

@ -1,21 +1,58 @@
import { AstroError } from '../core/errors/index.js';
import { MissingLocale } from '../core/errors/errors-data.js';
import type { AstroConfig } from '../@types/astro.js';
import type { Logger } from '../core/logger/core.js';
import { shouldAppendForwardSlash } from '../core/build/util.js';
type GetI18nBaseUrl = {
locale: string;
base: string;
locales: string[];
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
};
/**
* 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}/` : '/';
export function getI18nBaseUrl({ locale, base, locales, trailingSlash, format }: GetI18nBaseUrl) {
if (!locales.includes(locale)) {
throw new AstroError({
...MissingLocale,
message: MissingLocale.message(locale, locales),
});
}
if (base) {
logger.debug('i18n', 'The project has a base directory, using it.');
return `${base}/${locale}/`;
const normalizedLocale = normalizeLocale(locale);
if (shouldAppendForwardSlash(trailingSlash, format)) {
return `${base}${normalizedLocale}/`;
} else {
return `/${locale}/`;
return `${base}/${normalizedLocale}`;
}
}
type GetLocalesBaseUrl = {
base: string;
locales: string[];
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
};
export function getLocalesBaseUrl({ base, locales, trailingSlash, format }: GetLocalesBaseUrl) {
return locales.map((locale) => {
const normalizedLocale = normalizeLocale(locale);
if (shouldAppendForwardSlash(trailingSlash, format)) {
return `${base}${normalizedLocale}/`;
} else {
return `${base}/${normalizedLocale}`;
}
});
}
/**
*
* Given a locale, this function:
* - replaces the `_` with a `-`;
* - transforms all letters to be lower case;
*/
function normalizeLocale(locale: string): string {
return locale.replaceAll('_', '-').toLowerCase();
}

View file

@ -13,12 +13,25 @@ type AstroInternalization = {
export default function astroInternalization({ settings }: AstroInternalization): vite.Plugin {
return {
name: 'astro:i18n',
async resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `
import { getI18nBaseUrl as getI18nBaseUrlInternal } from "astro/i18n";
export getI18nBaseUrl = (locale) => getI18nBaseUrlInternal(locale, ${settings.config});
import { getI18nBaseUrl as getI18nBaseUrlInternal, getLocalesBaseUrl as _getLocalesBaseUrl } from "astro/i18n";
const defaultLocale = ${JSON.stringify(settings.config.experimental.i18n!.defaultLocale)};
const locales = ${JSON.stringify(settings.config.experimental.i18n!.locales)};
const fallback = ${JSON.stringify(settings.config.experimental.i18n!.fallback)};
const base = ${JSON.stringify(settings.config.base)};
const trailingSlash = ${JSON.stringify(settings.config.trailingSlash)};
const format = ${JSON.stringify(settings.config.build.format)};
export const getI18nBaseUrl = (locale) => getI18nBaseUrlInternal({ locale, base, locales, trailingSlash, format });
export const getLocalesBaseUrl = () => _getLocalesBaseUrl({ base, locales, trailingSlash, format });
`;
}
},

View file

@ -1,8 +1,6 @@
import { getI18nBaseUrl } from '../../../dist/i18n/index.js';
import { getI18nBaseUrl, getLocalesBaseUrl } 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', () => {
/**
@ -14,13 +12,70 @@ describe('getI18nBaseUrl', () => {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
locales: ['en', 'en_US', 'es'],
},
},
};
expect(getI18nBaseUrl('en', config, logger)).to.eq('/blog/en/');
expect(getI18nBaseUrl('es', config, logger)).to.eq('/blog/es/');
// directory format
expect(
getI18nBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/blog/en/');
expect(
getI18nBaseUrl({
locale: 'es',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/blog/es/');
expect(
getI18nBaseUrl({
locale: 'en_US',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.throw;
// file format
expect(
getI18nBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'file',
})
).to.eq('/blog/en/');
expect(
getI18nBaseUrl({
locale: 'es',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'file',
})
).to.eq('/blog/es/');
expect(
getI18nBaseUrl({
locale: 'en_US',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'file',
})
).to.throw;
});
it('should correctly return the URL without base', () => {
@ -37,7 +92,286 @@ describe('getI18nBaseUrl', () => {
},
};
expect(getI18nBaseUrl('en', config, logger)).to.eq('/en/');
expect(getI18nBaseUrl('es', config, logger)).to.eq('/es/');
expect(
getI18nBaseUrl({
locale: 'en',
base: '/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/en/');
expect(
getI18nBaseUrl({
locale: 'es',
base: '/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/es/');
});
it('should correctly handle the trailing slash', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
},
};
// directory format
expect(
getI18nBaseUrl({
locale: 'en',
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'never',
format: 'directory',
})
).to.eq('/blog/en');
expect(
getI18nBaseUrl({
locale: 'es',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/blog/es/');
expect(
getI18nBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'ignore',
format: 'directory',
})
).to.eq('/blog/en/');
// directory file
expect(
getI18nBaseUrl({
locale: 'en',
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'never',
format: 'file',
})
).to.eq('/blog/en');
expect(
getI18nBaseUrl({
locale: 'es',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'file',
})
).to.eq('/blog/es/');
expect(
getI18nBaseUrl({
locale: 'en',
// ignore + file => no trailing slash
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'ignore',
format: 'file',
})
).to.eq('/blog/en');
});
it('should normalize locales', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
base: '/blog',
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'en_AU'],
},
},
};
expect(
getI18nBaseUrl({
locale: 'en_US',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/blog/en-us/');
expect(
getI18nBaseUrl({
locale: 'en_AU',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.eq('/blog/en-au/');
});
});
describe('getLocalesBaseUrl', () => {
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'never',
format: 'directory',
})
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'directory',
})
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'always',
format: 'file',
})
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'never',
format: 'file',
})
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog',
locales: config.experimental.i18n.locales,
trailingSlash: 'ignore',
format: 'file',
})
).to.have.members(['/blog/en', '/blog/en-us', '/blog/es']);
});
it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => {
/**
*
* @type {import("../../../dist/@types").AstroUserConfig}
*/
const config = {
experimental: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'en_US', 'es'],
},
},
};
// directory format
expect(
getLocalesBaseUrl({
locale: 'en',
base: '/blog/',
locales: config.experimental.i18n.locales,
trailingSlash: 'ignore',
format: 'directory',
})
).to.have.members(['/blog/en/', '/blog/en-us/', '/blog/es/']);
});
});

View file

@ -20,6 +20,7 @@ process.on('SIGTERM', exit);
// if you make any changes to the flow or wording here.
export async function main() {
// Clear console because PNPM startup is super ugly
// eslint-disable-next-line no-console
console.clear();
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed
// to no longer require `--` to pass args and instead pass `--` directly to us. This

View file

@ -2755,6 +2755,18 @@ importers:
specifier: ^10.17.1
version: 10.17.1
packages/astro/test/fixtures/i18n-routing:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/i18n-routing-base:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/import-ts-with-js:
dependencies:
astro:
@ -17267,7 +17279,6 @@ packages:
engines: {node: '>=14.0'}
dependencies:
busboy: 1.6.0
dev: true
/unherit@3.0.1:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}