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' { declare module 'astro:i18n' {
type I18nModule = typeof import('./dist/i18n/index.js'); type I18nModule = typeof import('./dist/i18n/index.js');
// TODO: documentation
export const getI18nBaseUrl: (locale: string) => string; export const getI18nBaseUrl: (locale: string) => string;
// TODO: documentation
export const getLocalesBaseUrl: () => string[];
} }
declare module 'astro:middleware' { declare module 'astro:middleware' {

View file

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

View file

@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url';
import type { OutputAsset, OutputChunk } from 'rollup'; import type { OutputAsset, OutputChunk } from 'rollup';
import type { BufferEncoding } from 'vfile'; import type { BufferEncoding } from 'vfile';
import type { import type {
AstroConfig,
AstroSettings, AstroSettings,
ComponentInstance, ComponentInstance,
GetStaticPathsItem, GetStaticPathsItem,
@ -58,7 +57,7 @@ import type {
StaticBuildOptions, StaticBuildOptions,
StylesheetAsset, StylesheetAsset,
} from './types.js'; } from './types.js';
import { getTimeStat } from './util.js'; import { getTimeStat, shouldAppendForwardSlash } from './util.js';
function createEntryURL(filePath: string, outFolder: URL) { function createEntryURL(filePath: string, outFolder: URL) {
return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); return new URL('./' + filePath + `?time=${Date.now()}`, outFolder);
@ -431,26 +430,6 @@ interface GeneratePathOptions {
mod: ComponentInstance; 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 { function addPageName(pathname: string, opts: StaticBuildOptions): void {
const trailingSlash = opts.settings.config.trailingSlash; const trailingSlash = opts.settings.config.trailingSlash;
const buildFormat = opts.settings.config.build.format; 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) { export function getTimeStat(timeStart: number, timeEnd: number) {
const buildTime = timeEnd - timeStart; const buildTime = timeEnd - timeStart;
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`; 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 astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js'; import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
import { joinPaths } from './path.js'; import { joinPaths } from './path.js';
import astroInternalization from '../i18n/vite-plugin-i18n.js';
interface CreateViteOptions { interface CreateViteOptions {
settings: AstroSettings; settings: AstroSettings;
@ -134,6 +135,7 @@ export async function createVite(
vitePluginSSRManifest(), vitePluginSSRManifest(),
astroAssetsPlugin({ settings, logger, mode }), astroAssetsPlugin({ settings, logger, mode }),
astroTransitions(), astroTransitions(),
!!settings.config.experimental.i18n && astroInternalization({ settings, logger }),
], ],
publicDir: fileURLToPath(settings.config.publicDir), publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root), 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', hint: 'See the devalue library for all supported types: https://github.com/rich-harris/devalue',
} satisfies ErrorData; } 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 // 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; 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 { 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 * The base URL
*/ */
export function getI18nBaseUrl(locale: string, config: AstroConfig, logger: Logger) { export function getI18nBaseUrl({ locale, base, locales, trailingSlash, format }: GetI18nBaseUrl) {
const base = config.base; if (!locales.includes(locale)) {
throw new AstroError({
if (!config.experimental.i18n) { ...MissingLocale,
logger.error('i18n', "The project isn't using i18n features, no need to use this function."); message: MissingLocale.message(locale, locales),
return base ? `/${base}/` : '/'; });
} }
if (base) { const normalizedLocale = normalizeLocale(locale);
logger.debug('i18n', 'The project has a base directory, using it.'); if (shouldAppendForwardSlash(trailingSlash, format)) {
return `${base}/${locale}/`; return `${base}${normalizedLocale}/`;
} else { } 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 { export default function astroInternalization({ settings }: AstroInternalization): vite.Plugin {
return { return {
name: 'astro:i18n', name: 'astro:i18n',
async resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
},
load(id) { load(id) {
if (id === resolvedVirtualModuleId) { if (id === resolvedVirtualModuleId) {
return ` return `
import { getI18nBaseUrl as getI18nBaseUrlInternal } from "astro/i18n"; import { getI18nBaseUrl as getI18nBaseUrlInternal, getLocalesBaseUrl as _getLocalesBaseUrl } from "astro/i18n";
export getI18nBaseUrl = (locale) => getI18nBaseUrlInternal(locale, ${settings.config}); 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 { expect } from 'chai';
import { Logger } from '../../../dist/core/logger/core.js';
const logger = new Logger();
describe('getI18nBaseUrl', () => { describe('getI18nBaseUrl', () => {
it('should correctly return the URL with the base', () => { it('should correctly return the URL with the base', () => {
/** /**
@ -14,13 +12,70 @@ describe('getI18nBaseUrl', () => {
experimental: { experimental: {
i18n: { i18n: {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'es'], locales: ['en', 'en_US', 'es'],
}, },
}, },
}; };
expect(getI18nBaseUrl('en', config, logger)).to.eq('/blog/en/'); // directory format
expect(getI18nBaseUrl('es', config, logger)).to.eq('/blog/es/'); 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', () => { it('should correctly return the URL without base', () => {
@ -37,7 +92,286 @@ describe('getI18nBaseUrl', () => {
}, },
}; };
expect(getI18nBaseUrl('en', config, logger)).to.eq('/en/'); expect(
expect(getI18nBaseUrl('es', config, logger)).to.eq('/es/'); 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. // if you make any changes to the flow or wording here.
export async function main() { export async function main() {
// Clear console because PNPM startup is super ugly // Clear console because PNPM startup is super ugly
// eslint-disable-next-line no-console
console.clear(); console.clear();
// NOTE: In the v7.x version of npm, the default behavior of `npm init` was changed // 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 // to no longer require `--` to pass args and instead pass `--` directly to us. This

13
pnpm-lock.yaml generated
View file

@ -2755,6 +2755,18 @@ importers:
specifier: ^10.17.1 specifier: ^10.17.1
version: 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: packages/astro/test/fixtures/import-ts-with-js:
dependencies: dependencies:
astro: astro:
@ -17267,7 +17279,6 @@ packages:
engines: {node: '>=14.0'} engines: {node: '>=14.0'}
dependencies: dependencies:
busboy: 1.6.0 busboy: 1.6.0
dev: true
/unherit@3.0.1: /unherit@3.0.1:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}