From 9c85f8689ae4277583d4d6cd740b3ed17427c1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 14 Nov 2025 13:13:05 +0100 Subject: [PATCH] fix(core): optimize i18n integration for site builds + improve inference of locale config (#11550) --- .github/workflows/tests-e2e.yml | 2 +- .github/workflows/tests-windows.yml | 2 +- .github/workflows/tests.yml | 2 +- .prettierignore | 4 +- .../docusaurus/src/commands/build/build.ts | 30 ++++---- .../src/server/__tests__/i18n.test.ts | 69 +++++++++++++++++-- packages/docusaurus/src/server/i18n.ts | 44 +++++++++--- 7 files changed, 116 insertions(+), 37 deletions(-) diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml index f14b4ddf14..4dd9eed596 100644 --- a/.github/workflows/tests-e2e.yml +++ b/.github/workflows/tests-e2e.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['20.0', '20', '22', '24', '25'] + node: ['20.0', '20', '22', '24', '25.1'] steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.github/workflows/tests-windows.yml b/.github/workflows/tests-windows.yml index 1c98be0bd1..6f0a32c97a 100644 --- a/.github/workflows/tests-windows.yml +++ b/.github/workflows/tests-windows.yml @@ -27,7 +27,7 @@ jobs: runs-on: windows-latest strategy: matrix: - node: ['20.0', '20', '22', '24', '25'] + node: ['20.0', '20', '22', '24', '25.1'] steps: - name: Support longpaths run: git config --system core.longpaths true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87bd04ea29..f88833ff15 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['20.0', '20', '22', '24', '25'] + node: ['20.0', '20', '22', '24', '25.1'] steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 diff --git a/.prettierignore b/.prettierignore index 19af204b9c..c611d3a49e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ dist node_modules .yarn -build +**/build/** coverage .docusaurus .idea @@ -11,6 +11,8 @@ coverage jest/vendor +argos/test-results + packages/lqip-loader/lib/ packages/docusaurus/lib/ packages/docusaurus-*/lib/* diff --git a/packages/docusaurus/src/commands/build/build.ts b/packages/docusaurus/src/commands/build/build.ts index c3baf7940b..2c2b6ea15f 100644 --- a/packages/docusaurus/src/commands/build/build.ts +++ b/packages/docusaurus/src/commands/build/build.ts @@ -8,10 +8,10 @@ import fs from 'fs-extra'; import logger, {PerfLogger} from '@docusaurus/logger'; import {mapAsyncSequential} from '@docusaurus/utils'; -import {loadContext, type LoadContextParams} from '../../server/site'; -import {loadI18n} from '../../server/i18n'; +import {type LoadContextParams} from '../../server/site'; +import {loadI18nLocaleList} from '../../server/i18n'; import {buildLocale, type BuildLocaleParams} from './buildLocale'; -import {isAutomaticBaseUrlLocalizationDisabled} from './buildUtils'; +import {loadSiteConfig} from '../../server/config'; export type BuildCLIOptions = Pick & { locale?: [string, ...string[]]; @@ -81,27 +81,21 @@ async function getLocalesToBuild({ siteDir: string; cliOptions: BuildCLIOptions; }): Promise<[string, ...string[]]> { - // TODO we shouldn't need to load all context + i18n just to get that list - // only loading siteConfig should be enough - const context = await loadContext({ + const {siteConfig} = await loadSiteConfig({ siteDir, - outDir: cliOptions.outDir, - config: cliOptions.config, - automaticBaseUrlLocalizationDisabled: isAutomaticBaseUrlLocalizationDisabled(cliOptions), + customConfigFilePath: cliOptions.config, }); - const i18n = await loadI18n({ - siteDir, - config: context.siteConfig, - currentLocale: context.siteConfig.i18n.defaultLocale, // Awkward but ok - automaticBaseUrlLocalizationDisabled: false, - }); - - const locales = cliOptions.locale ?? i18n.locales; + const locales = + cliOptions.locale ?? + loadI18nLocaleList({ + i18nConfig: siteConfig.i18n, + currentLocale: siteConfig.i18n.defaultLocale, // Awkward but ok + }); return orderLocales({ locales: locales as [string, ...string[]], - defaultLocale: i18n.defaultLocale, + defaultLocale: siteConfig.i18n.defaultLocale, }); } diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index d7af30b224..9cc9674569 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -123,9 +123,11 @@ describe('defaultLocaleConfig', () => { }); describe('loadI18n', () => { - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); beforeEach(() => { - consoleSpy.mockClear(); + consoleWarnSpy.mockClear(); }); it('loads I18n for default config', async () => { @@ -397,8 +399,67 @@ describe('loadI18n', () => { }, currentLocale: 'it', }); - expect(consoleSpy.mock.calls[0]![0]).toMatch( - /The locale .*it.* was not found in your site configuration/, + expect(consoleWarnSpy.mock.calls[0]![0]).toMatch( + /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); + }); }); diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 7c1aae5bcd..f5135fc08e 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -10,7 +10,12 @@ import fs from 'fs-extra'; import logger from '@docusaurus/logger'; import combinePromises from 'combine-promises'; 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) { const tryLocale = (l: string) => { @@ -95,12 +100,33 @@ export function getDefaultLocaleConfig( }; } catch (e) { 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}, ); } } +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({ siteDir, config, @@ -114,14 +140,10 @@ export async function loadI18n({ }): Promise { const {i18n: i18nConfig} = config; - if (!i18nConfig.locales.includes(currentLocale)) { - logger.warn`The locale name=${currentLocale} was not found in your site configuration: Available locales are: ${i18nConfig.locales} -Note: Docusaurus only support running one locale at a time.`; - } - - const locales = i18nConfig.locales.includes(currentLocale) - ? i18nConfig.locales - : (i18nConfig.locales.concat(currentLocale) as [string, ...string[]]); + const locales = loadI18nLocaleList({ + i18nConfig, + currentLocale, + }); async function getFullLocaleConfig( locale: string, @@ -131,7 +153,7 @@ Note: Docusaurus only support running one locale at a time.`; I18nLocaleConfig, 'translate' | 'url' | 'baseUrl' > = { - ...getDefaultLocaleConfig(locale), + ...getDefaultLocaleConfig(localeConfigInput.htmlLang ?? locale), ...localeConfigInput, };