From 7eb099c7f2cff4816ba069cd5bd91b4a297f3d1e Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 14 Nov 2025 12:37:18 +0100 Subject: [PATCH] Make it possible to infer locale config from the htmlLang attribute, to support cases where the locale name is not BCP 47 --- .../src/server/__tests__/i18n.test.ts | 67 ++++++++++++++++++- packages/docusaurus/src/server/i18n.ts | 9 ++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index a17accf37d..bcf1b50a12 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( + 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: 'fr', + 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: 'fr', + locales: ['en', '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 fa863aa449..f5135fc08e 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -100,9 +100,12 @@ export function getDefaultLocaleConfig( }; } catch (e) { throw new Error( - `Docusaurus couldn't get default locale config for ${logger.name( + `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}, ); } @@ -150,7 +153,7 @@ export async function loadI18n({ I18nLocaleConfig, 'translate' | 'url' | 'baseUrl' > = { - ...getDefaultLocaleConfig(locale), + ...getDefaultLocaleConfig(localeConfigInput.htmlLang ?? locale), ...localeConfigInput, };