fix(core): optimize i18n integration for site builds + improve inference of locale config (#11550)

This commit is contained in:
Sébastien Lorber 2025-11-14 13:13:05 +01:00 committed by GitHub
parent 6a38ccdfb0
commit 9c85f8689a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 116 additions and 37 deletions

View File

@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['20.0', '20', '22', '24', '25'] node: ['20.0', '20', '22', '24', '25.1']
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@ -27,7 +27,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
matrix: matrix:
node: ['20.0', '20', '22', '24', '25'] node: ['20.0', '20', '22', '24', '25.1']
steps: steps:
- name: Support longpaths - name: Support longpaths
run: git config --system core.longpaths true run: git config --system core.longpaths true

View File

@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['20.0', '20', '22', '24', '25'] node: ['20.0', '20', '22', '24', '25.1']
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@ -1,7 +1,7 @@
dist dist
node_modules node_modules
.yarn .yarn
build **/build/**
coverage coverage
.docusaurus .docusaurus
.idea .idea
@ -11,6 +11,8 @@ coverage
jest/vendor jest/vendor
argos/test-results
packages/lqip-loader/lib/ packages/lqip-loader/lib/
packages/docusaurus/lib/ packages/docusaurus/lib/
packages/docusaurus-*/lib/* packages/docusaurus-*/lib/*

View File

@ -8,10 +8,10 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import logger, {PerfLogger} from '@docusaurus/logger'; import logger, {PerfLogger} from '@docusaurus/logger';
import {mapAsyncSequential} from '@docusaurus/utils'; import {mapAsyncSequential} from '@docusaurus/utils';
import {loadContext, type LoadContextParams} from '../../server/site'; import {type LoadContextParams} from '../../server/site';
import {loadI18n} from '../../server/i18n'; import {loadI18nLocaleList} from '../../server/i18n';
import {buildLocale, type BuildLocaleParams} from './buildLocale'; import {buildLocale, type BuildLocaleParams} from './buildLocale';
import {isAutomaticBaseUrlLocalizationDisabled} from './buildUtils'; import {loadSiteConfig} from '../../server/config';
export type BuildCLIOptions = Pick<LoadContextParams, 'config' | 'outDir'> & { export type BuildCLIOptions = Pick<LoadContextParams, 'config' | 'outDir'> & {
locale?: [string, ...string[]]; locale?: [string, ...string[]];
@ -81,27 +81,21 @@ async function getLocalesToBuild({
siteDir: string; siteDir: string;
cliOptions: BuildCLIOptions; cliOptions: BuildCLIOptions;
}): Promise<[string, ...string[]]> { }): Promise<[string, ...string[]]> {
// TODO we shouldn't need to load all context + i18n just to get that list const {siteConfig} = await loadSiteConfig({
// only loading siteConfig should be enough
const context = await loadContext({
siteDir, siteDir,
outDir: cliOptions.outDir, customConfigFilePath: cliOptions.config,
config: cliOptions.config,
automaticBaseUrlLocalizationDisabled: isAutomaticBaseUrlLocalizationDisabled(cliOptions),
}); });
const i18n = await loadI18n({ const locales =
siteDir, cliOptions.locale ??
config: context.siteConfig, loadI18nLocaleList({
currentLocale: context.siteConfig.i18n.defaultLocale, // Awkward but ok i18nConfig: siteConfig.i18n,
automaticBaseUrlLocalizationDisabled: false, currentLocale: siteConfig.i18n.defaultLocale, // Awkward but ok
}); });
const locales = cliOptions.locale ?? i18n.locales;
return orderLocales({ return orderLocales({
locales: locales as [string, ...string[]], locales: locales as [string, ...string[]],
defaultLocale: i18n.defaultLocale, defaultLocale: siteConfig.i18n.defaultLocale,
}); });
} }

View File

@ -123,9 +123,11 @@ describe('defaultLocaleConfig', () => {
}); });
describe('loadI18n', () => { describe('loadI18n', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const consoleWarnSpy = jest
.spyOn(console, 'warn')
.mockImplementation(() => {});
beforeEach(() => { beforeEach(() => {
consoleSpy.mockClear(); consoleWarnSpy.mockClear();
}); });
it('loads I18n for default config', async () => { it('loads I18n for default config', async () => {
@ -397,8 +399,67 @@ describe('loadI18n', () => {
}, },
currentLocale: 'it', currentLocale: 'it',
}); });
expect(consoleSpy.mock.calls[0]![0]).toMatch( expect(consoleWarnSpy.mock.calls[0]![0]).toMatch(
/The locale .*it.* was not found in your site configuration/, /The locale .*it.* was not found in your Docusaurus site configuration/,
); );
}); });
it('throws when trying to load undeclared locale that is not a valid locale BCP47 name', async () => {
await expect(() =>
loadI18nTest({
i18nConfig: {
path: 'i18n',
defaultLocale: 'en',
locales: ['en', 'fr', 'de'],
localeConfigs: {},
},
currentLocale: 'x1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Docusaurus couldn't infer a default locale config for x1.
Make sure it is a valid BCP 47 locale name (e.g. en, fr, fr-FR, etc.) and/or provide a valid BCP 47 \`siteConfig.i18n.localeConfig['x1'].htmlLang\` attribute."
`);
});
it('throws when trying to load declared locale that is not a valid locale BCP47 name', async () => {
await expect(() =>
loadI18nTest({
i18nConfig: {
path: 'i18n',
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
localeConfigs: {x1: {}},
},
currentLocale: 'x1',
}),
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Docusaurus couldn't infer a default locale config for x1.
Make sure it is a valid BCP 47 locale name (e.g. en, fr, fr-FR, etc.) and/or provide a valid BCP 47 \`siteConfig.i18n.localeConfig['x1'].htmlLang\` attribute."
`);
});
it('loads i18n when trying to load declared locale with invalid BCP47 name but valid BCP47', async () => {
const result = await loadI18nTest({
i18nConfig: {
path: 'i18n',
defaultLocale: 'en',
locales: ['en', 'fr', 'x1'],
localeConfigs: {
x1: {htmlLang: 'en-US'},
},
},
currentLocale: 'x1',
});
expect(result.localeConfigs.x1).toEqual({
baseUrl: '/x1/',
calendar: 'gregory',
direction: 'ltr',
htmlLang: 'en-US',
label: 'American English',
path: 'en-US',
translate: false,
url: 'https://example.com',
});
expect(consoleWarnSpy).toHaveBeenCalledTimes(0);
});
}); });

View File

@ -10,7 +10,12 @@ import fs from 'fs-extra';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import combinePromises from 'combine-promises'; import combinePromises from 'combine-promises';
import {normalizeUrl} from '@docusaurus/utils'; import {normalizeUrl} from '@docusaurus/utils';
import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; import type {
I18n,
DocusaurusConfig,
I18nLocaleConfig,
I18nConfig,
} from '@docusaurus/types';
function inferLanguageDisplayName(locale: string) { function inferLanguageDisplayName(locale: string) {
const tryLocale = (l: string) => { const tryLocale = (l: string) => {
@ -95,12 +100,33 @@ export function getDefaultLocaleConfig(
}; };
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`Docusaurus couldn't get default locale config for ${locale}`, `Docusaurus couldn't infer a default locale config for ${logger.name(
locale,
)}.
Make sure it is a valid BCP 47 locale name (e.g. en, fr, fr-FR, etc.) and/or provide a valid BCP 47 ${logger.code(
`siteConfig.i18n.localeConfig['${locale}'].htmlLang`,
)} attribute.`,
{cause: e}, {cause: e},
); );
} }
} }
export function loadI18nLocaleList({
i18nConfig,
currentLocale,
}: {
i18nConfig: I18nConfig;
currentLocale: string;
}): [string, ...string[]] {
if (!i18nConfig.locales.includes(currentLocale)) {
logger.warn`The locale name=${currentLocale} was not found in your Docusaurus site configuration.
We recommend adding the name=${currentLocale} to your site i18n config, but we will still try to run your site.
Declared site config locales are: ${i18nConfig.locales}`;
return i18nConfig.locales.concat(currentLocale) as [string, ...string[]];
}
return i18nConfig.locales;
}
export async function loadI18n({ export async function loadI18n({
siteDir, siteDir,
config, config,
@ -114,14 +140,10 @@ export async function loadI18n({
}): Promise<I18n> { }): Promise<I18n> {
const {i18n: i18nConfig} = config; const {i18n: i18nConfig} = config;
if (!i18nConfig.locales.includes(currentLocale)) { const locales = loadI18nLocaleList({
logger.warn`The locale name=${currentLocale} was not found in your site configuration: Available locales are: ${i18nConfig.locales} i18nConfig,
Note: Docusaurus only support running one locale at a time.`; currentLocale,
} });
const locales = i18nConfig.locales.includes(currentLocale)
? i18nConfig.locales
: (i18nConfig.locales.concat(currentLocale) as [string, ...string[]]);
async function getFullLocaleConfig( async function getFullLocaleConfig(
locale: string, locale: string,
@ -131,7 +153,7 @@ Note: Docusaurus only support running one locale at a time.`;
I18nLocaleConfig, I18nLocaleConfig,
'translate' | 'url' | 'baseUrl' 'translate' | 'url' | 'baseUrl'
> = { > = {
...getDefaultLocaleConfig(locale), ...getDefaultLocaleConfig(localeConfigInput.htmlLang ?? locale),
...localeConfigInput, ...localeConfigInput,
}; };