Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
dist
node_modules
.yarn
build
**/build/**
coverage
.docusaurus
.idea
Expand All @@ -11,6 +11,8 @@ coverage

jest/vendor

argos/test-results

packages/lqip-loader/lib/
packages/docusaurus/lib/
packages/docusaurus-*/lib/*
Expand Down
30 changes: 12 additions & 18 deletions packages/docusaurus/src/commands/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoadContextParams, 'config' | 'outDir'> & {
locale?: [string, ...string[]];
Expand Down Expand Up @@ -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,
});
}

Expand Down
69 changes: 65 additions & 4 deletions packages/docusaurus/src/server/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
44 changes: 33 additions & 11 deletions packages/docusaurus/src/server/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand All @@ -114,14 +140,10 @@ export async function loadI18n({
}): Promise<I18n> {
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,
Expand All @@ -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,
};

Expand Down