mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 10:20:09 +02:00
feat(core): Add i18n.localeConfigs[locale].{url,baseUrl}
config options (#11316)
Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
parent
12bcad9837
commit
2febb76fae
29 changed files with 800 additions and 301 deletions
2
.eslintrc.js
vendored
2
.eslintrc.js
vendored
|
@ -214,7 +214,7 @@ module.exports = {
|
|||
],
|
||||
'no-useless-escape': WARNING,
|
||||
'no-void': [ERROR, {allowAsStatement: true}],
|
||||
'prefer-destructuring': WARNING,
|
||||
'prefer-destructuring': OFF,
|
||||
'prefer-named-capture-group': WARNING,
|
||||
'prefer-template': WARNING,
|
||||
yoda: WARNING,
|
||||
|
|
|
@ -9,12 +9,11 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|||
import {createStorageSlot} from '@docusaurus/theme-common';
|
||||
|
||||
// First: read the env variables (provided by Webpack)
|
||||
/* eslint-disable prefer-destructuring */
|
||||
|
||||
const PWA_SERVICE_WORKER_URL = process.env.PWA_SERVICE_WORKER_URL!;
|
||||
const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES = process.env
|
||||
.PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES as unknown as (keyof typeof OfflineModeActivationStrategiesImplementations)[];
|
||||
const PWA_DEBUG = process.env.PWA_DEBUG;
|
||||
/* eslint-enable prefer-destructuring */
|
||||
|
||||
const MAX_MOBILE_WIDTH = 996;
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, {type ReactNode} from 'react';
|
|||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import {useAlternatePageUtils} from '@docusaurus/theme-common/internal';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import {useHistorySelector} from '@docusaurus/theme-common';
|
||||
import {mergeSearchStrings, useHistorySelector} from '@docusaurus/theme-common';
|
||||
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
|
||||
import IconLanguage from '@theme/Icon/Language';
|
||||
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||
|
@ -17,31 +17,80 @@ import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
|||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function LocaleDropdownNavbarItem({
|
||||
mobile,
|
||||
dropdownItemsBefore,
|
||||
dropdownItemsAfter,
|
||||
queryString = '',
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
function useLocaleDropdownUtils() {
|
||||
const {
|
||||
i18n: {currentLocale, locales, localeConfigs},
|
||||
siteConfig,
|
||||
i18n: {localeConfigs},
|
||||
} = useDocusaurusContext();
|
||||
const alternatePageUtils = useAlternatePageUtils();
|
||||
const search = useHistorySelector((history) => history.location.search);
|
||||
const hash = useHistorySelector((history) => history.location.hash);
|
||||
|
||||
const localeItems = locales.map((locale): LinkLikeNavbarItemProps => {
|
||||
const baseTo = `pathname://${alternatePageUtils.createUrl({
|
||||
const getLocaleConfig = (locale: string) => {
|
||||
const localeConfig = localeConfigs[locale];
|
||||
if (!localeConfig) {
|
||||
throw new Error(
|
||||
`Docusaurus bug, no locale config found for locale=${locale}`,
|
||||
);
|
||||
}
|
||||
return localeConfig;
|
||||
};
|
||||
|
||||
const getBaseURLForLocale = (locale: string) => {
|
||||
const localeConfig = getLocaleConfig(locale);
|
||||
const isSameDomain = localeConfig.url === siteConfig.url;
|
||||
if (isSameDomain) {
|
||||
// Shorter paths if localized sites are hosted on the same domain
|
||||
// This reduces HTML size a bit
|
||||
return `pathname://${alternatePageUtils.createUrl({
|
||||
locale,
|
||||
fullyQualified: false,
|
||||
})}`;
|
||||
}
|
||||
return alternatePageUtils.createUrl({
|
||||
locale,
|
||||
fullyQualified: false,
|
||||
})}`;
|
||||
// preserve ?search#hash suffix on locale switches
|
||||
const to = `${baseTo}${search}${hash}${queryString}`;
|
||||
fullyQualified: true,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getURL: (locale: string, options: {queryString: string | undefined}) => {
|
||||
// We have 2 query strings because
|
||||
// - there's the current one
|
||||
// - there's one user can provide through navbar config
|
||||
// see https://github.com/facebook/docusaurus/pull/8915
|
||||
const finalSearch = mergeSearchStrings(
|
||||
[search, options.queryString],
|
||||
'append',
|
||||
);
|
||||
return `${getBaseURLForLocale(locale)}${finalSearch}${hash}`;
|
||||
},
|
||||
getLabel: (locale: string) => {
|
||||
return getLocaleConfig(locale).label;
|
||||
},
|
||||
getLang: (locale: string) => {
|
||||
return getLocaleConfig(locale).htmlLang;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function LocaleDropdownNavbarItem({
|
||||
mobile,
|
||||
dropdownItemsBefore,
|
||||
dropdownItemsAfter,
|
||||
queryString,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const utils = useLocaleDropdownUtils();
|
||||
|
||||
const {
|
||||
i18n: {currentLocale, locales},
|
||||
} = useDocusaurusContext();
|
||||
const localeItems = locales.map((locale): LinkLikeNavbarItemProps => {
|
||||
return {
|
||||
label: localeConfigs[locale]!.label,
|
||||
lang: localeConfigs[locale]!.htmlLang,
|
||||
to,
|
||||
label: utils.getLabel(locale),
|
||||
lang: utils.getLang(locale),
|
||||
to: utils.getURL(locale, {queryString}),
|
||||
target: '_self',
|
||||
autoAddBaseUrl: false,
|
||||
className:
|
||||
|
@ -66,7 +115,7 @@ export default function LocaleDropdownNavbarItem({
|
|||
id: 'theme.navbar.mobileLanguageDropdown.label',
|
||||
description: 'The label for the mobile language switcher dropdown',
|
||||
})
|
||||
: localeConfigs[currentLocale]!.label;
|
||||
: utils.getLabel(currentLocale);
|
||||
|
||||
return (
|
||||
<DropdownNavbarItem
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"devDependencies": {
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/types": "3.8.1",
|
||||
"@total-typescript/shoehorn": "^0.1.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
|
|
|
@ -124,6 +124,8 @@ export {
|
|||
useQueryString,
|
||||
useQueryStringList,
|
||||
useClearQueryString,
|
||||
mergeSearchParams,
|
||||
mergeSearchStrings,
|
||||
} from './utils/historyUtils';
|
||||
|
||||
export {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {mergeSearchParams, mergeSearchStrings} from '../historyUtils';
|
||||
|
||||
describe('mergeSearchParams', () => {
|
||||
it('can append search params', () => {
|
||||
expect(
|
||||
mergeSearchParams(
|
||||
[
|
||||
new URLSearchParams('?key1=val1&key2=val2'),
|
||||
new URLSearchParams('key2=val2-bis&key3=val3'),
|
||||
new URLSearchParams(''),
|
||||
new URLSearchParams('?key3=val3-bis&key4=val4'),
|
||||
],
|
||||
'append',
|
||||
).toString(),
|
||||
).toBe(
|
||||
'key1=val1&key2=val2&key2=val2-bis&key3=val3&key3=val3-bis&key4=val4',
|
||||
);
|
||||
});
|
||||
|
||||
it('can overwrite search params', () => {
|
||||
expect(
|
||||
mergeSearchParams(
|
||||
[
|
||||
new URLSearchParams('?key1=val1&key2=val2'),
|
||||
new URLSearchParams('key2=val2-bis&key3=val3'),
|
||||
new URLSearchParams(''),
|
||||
new URLSearchParams('?key3=val3-bis&key4=val4'),
|
||||
],
|
||||
'set',
|
||||
).toString(),
|
||||
).toBe('key1=val1&key2=val2-bis&key3=val3-bis&key4=val4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeSearchStrings', () => {
|
||||
it('can append search params', () => {
|
||||
expect(
|
||||
mergeSearchStrings(
|
||||
[
|
||||
'?key1=val1&key2=val2',
|
||||
'key2=val2-bis&key3=val3',
|
||||
'',
|
||||
'?key3=val3-bis&key4=val4',
|
||||
],
|
||||
'append',
|
||||
),
|
||||
).toBe(
|
||||
'?key1=val1&key2=val2&key2=val2-bis&key3=val3&key3=val3-bis&key4=val4',
|
||||
);
|
||||
});
|
||||
|
||||
it('can overwrite search params', () => {
|
||||
expect(
|
||||
mergeSearchStrings(
|
||||
[
|
||||
'?key1=val1&key2=val2',
|
||||
'key2=val2-bis&key3=val3',
|
||||
'',
|
||||
'?key3=val3-bis&key4=val4',
|
||||
],
|
||||
'set',
|
||||
),
|
||||
).toBe('?key1=val1&key2=val2-bis&key3=val3-bis&key4=val4');
|
||||
});
|
||||
|
||||
it('automatically adds ? if there are params', () => {
|
||||
expect(mergeSearchStrings(['key1=val1'], 'append')).toBe('?key1=val1');
|
||||
});
|
||||
|
||||
it('automatically removes ? if there are no params', () => {
|
||||
expect(mergeSearchStrings([undefined, ''], 'append')).toBe('');
|
||||
});
|
||||
});
|
|
@ -9,116 +9,185 @@ import React from 'react';
|
|||
import {renderHook} from '@testing-library/react-hooks';
|
||||
import {StaticRouter} from 'react-router-dom';
|
||||
import {Context} from '@docusaurus/core/src/client/docusaurusContext';
|
||||
import {fromPartial} from '@total-typescript/shoehorn';
|
||||
import {useAlternatePageUtils} from '../useAlternatePageUtils';
|
||||
import type {DocusaurusContext} from '@docusaurus/types';
|
||||
|
||||
describe('useAlternatePageUtils', () => {
|
||||
const createUseAlternatePageUtilsMock =
|
||||
(context: DocusaurusContext) => (location: string) =>
|
||||
renderHook(() => useAlternatePageUtils(), {
|
||||
wrapper: ({children}) => (
|
||||
<Context.Provider value={context}>
|
||||
<StaticRouter location={location}>{children}</StaticRouter>
|
||||
</Context.Provider>
|
||||
),
|
||||
}).result.current;
|
||||
it('works for baseUrl: / and currentLocale = defaultLocale', () => {
|
||||
const mockUseAlternatePageUtils = createUseAlternatePageUtilsMock({
|
||||
siteConfig: {baseUrl: '/', url: 'https://example.com'},
|
||||
i18n: {defaultLocale: 'en', currentLocale: 'en'},
|
||||
} as DocusaurusContext);
|
||||
const createTestUtils = (context: DocusaurusContext) => {
|
||||
return {
|
||||
forLocation: (location: string) => {
|
||||
return renderHook(() => useAlternatePageUtils(), {
|
||||
wrapper: ({children}) => (
|
||||
<Context.Provider value={context}>
|
||||
<StaticRouter location={location}>{children}</StaticRouter>
|
||||
</Context.Provider>
|
||||
),
|
||||
}).result.current;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('works for baseUrl: / and currentLocale === defaultLocale', () => {
|
||||
const testUtils = createTestUtils(
|
||||
fromPartial({
|
||||
siteConfig: {
|
||||
url: 'https://example.com',
|
||||
baseUrl: '/',
|
||||
},
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {
|
||||
en: {
|
||||
url: 'https://example.com',
|
||||
baseUrl: '/',
|
||||
},
|
||||
'zh-Hans': {
|
||||
url: 'https://zh.example.com',
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/').createUrl({
|
||||
testUtils.forLocation('/').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/zh-Hans/');
|
||||
).toBe('/zh-Hans-baseUrl/');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/foo').createUrl({
|
||||
testUtils.forLocation('/foo').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/zh-Hans/foo');
|
||||
).toBe('/zh-Hans-baseUrl/foo');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/foo').createUrl({
|
||||
testUtils.forLocation('/foo').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: true,
|
||||
}),
|
||||
).toBe('https://example.com/zh-Hans/foo');
|
||||
).toBe('https://zh.example.com/zh-Hans-baseUrl/foo');
|
||||
});
|
||||
|
||||
it('works for baseUrl: / and currentLocale /= defaultLocale', () => {
|
||||
const mockUseAlternatePageUtils = createUseAlternatePageUtilsMock({
|
||||
siteConfig: {baseUrl: '/zh-Hans/', url: 'https://example.com'},
|
||||
i18n: {defaultLocale: 'en', currentLocale: 'zh-Hans'},
|
||||
} as DocusaurusContext);
|
||||
it('works for baseUrl: / and currentLocale !== defaultLocale', () => {
|
||||
const testUtils = createTestUtils(
|
||||
fromPartial({
|
||||
siteConfig: {
|
||||
url: 'https://zh.example.com',
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
},
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
currentLocale: 'zh-Hans',
|
||||
localeConfigs: {
|
||||
en: {url: 'https://example.com', baseUrl: '/'},
|
||||
'zh-Hans': {
|
||||
url: 'https://zh.example.com',
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/zh-Hans/').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/zh-Hans/foo').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/foo').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/foo');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/zh-Hans/foo').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/foo').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: true,
|
||||
}),
|
||||
).toBe('https://example.com/foo');
|
||||
});
|
||||
|
||||
it('works for non-root base URL and currentLocale = defaultLocale', () => {
|
||||
const mockUseAlternatePageUtils = createUseAlternatePageUtilsMock({
|
||||
siteConfig: {baseUrl: '/base/', url: 'https://example.com'},
|
||||
i18n: {defaultLocale: 'en', currentLocale: 'en'},
|
||||
} as DocusaurusContext);
|
||||
it('works for non-root base URL and currentLocale === defaultLocale', () => {
|
||||
const testUtils = createTestUtils(
|
||||
fromPartial({
|
||||
siteConfig: {baseUrl: '/en/', url: 'https://example.com'},
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {
|
||||
en: {url: 'https://example.com', baseUrl: '/base/'},
|
||||
'zh-Hans': {
|
||||
url: 'https://zh.example.com',
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/').createUrl({
|
||||
testUtils.forLocation('/en/').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/base/zh-Hans/');
|
||||
).toBe('/zh-Hans-baseUrl/');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/foo').createUrl({
|
||||
testUtils.forLocation('/en/foo').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/base/zh-Hans/foo');
|
||||
).toBe('/zh-Hans-baseUrl/foo');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/foo').createUrl({
|
||||
testUtils.forLocation('/en/foo').createUrl({
|
||||
locale: 'zh-Hans',
|
||||
fullyQualified: true,
|
||||
}),
|
||||
).toBe('https://example.com/base/zh-Hans/foo');
|
||||
).toBe('https://zh.example.com/zh-Hans-baseUrl/foo');
|
||||
});
|
||||
|
||||
it('works for non-root base URL and currentLocale /= defaultLocale', () => {
|
||||
const mockUseAlternatePageUtils = createUseAlternatePageUtilsMock({
|
||||
siteConfig: {baseUrl: '/base/zh-Hans/', url: 'https://example.com'},
|
||||
i18n: {defaultLocale: 'en', currentLocale: 'zh-Hans'},
|
||||
} as DocusaurusContext);
|
||||
it('works for non-root base URL and currentLocale !== defaultLocale', () => {
|
||||
const testUtils = createTestUtils(
|
||||
fromPartial({
|
||||
siteConfig: {
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
url: 'https://zh.example.com',
|
||||
},
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
currentLocale: 'zh-Hans',
|
||||
localeConfigs: {
|
||||
en: {url: 'https://en.example.com', baseUrl: '/en/'},
|
||||
'zh-Hans': {
|
||||
url: 'https://zh.example.com',
|
||||
baseUrl: '/zh-Hans-baseUrl/',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/zh-Hans/').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/base/');
|
||||
).toBe('/en/');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/zh-Hans/foo').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/foo').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: false,
|
||||
}),
|
||||
).toBe('/base/foo');
|
||||
).toBe('/en/foo');
|
||||
expect(
|
||||
mockUseAlternatePageUtils('/base/zh-Hans/foo').createUrl({
|
||||
testUtils.forLocation('/zh-Hans-baseUrl/foo').createUrl({
|
||||
locale: 'en',
|
||||
fullyQualified: true,
|
||||
}),
|
||||
).toBe('https://example.com/base/foo');
|
||||
).toBe('https://en.example.com/en/foo');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -168,3 +168,32 @@ export function useClearQueryString(): () => void {
|
|||
});
|
||||
}, [history]);
|
||||
}
|
||||
|
||||
export function mergeSearchParams(
|
||||
params: URLSearchParams[],
|
||||
strategy: 'append' | 'set',
|
||||
): URLSearchParams {
|
||||
const result = new URLSearchParams();
|
||||
for (const item of params) {
|
||||
for (const [key, value] of item.entries()) {
|
||||
if (strategy === 'append') {
|
||||
result.append(key, value);
|
||||
} else {
|
||||
result.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mergeSearchStrings(
|
||||
searchStrings: (string | undefined)[],
|
||||
strategy: 'append' | 'set',
|
||||
): string {
|
||||
const params = mergeSearchParams(
|
||||
searchStrings.map((s) => new URLSearchParams(s ?? '')),
|
||||
strategy,
|
||||
);
|
||||
const str = params.toString();
|
||||
return str ? `?${str}` : str;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import {useLocation} from '@docusaurus/router';
|
||||
import {applyTrailingSlash} from '@docusaurus/utils-common';
|
||||
import type {I18nLocaleConfig} from '@docusaurus/types';
|
||||
|
||||
/**
|
||||
* Permits to obtain the url of the current page in another locale, useful to
|
||||
|
@ -36,8 +37,8 @@ export function useAlternatePageUtils(): {
|
|||
}) => string;
|
||||
} {
|
||||
const {
|
||||
siteConfig: {baseUrl, url, trailingSlash},
|
||||
i18n: {defaultLocale, currentLocale},
|
||||
siteConfig: {baseUrl, trailingSlash},
|
||||
i18n: {localeConfigs},
|
||||
} = useDocusaurusContext();
|
||||
|
||||
// TODO using useLocation().pathname is not a super idea
|
||||
|
@ -49,21 +50,19 @@ export function useAlternatePageUtils(): {
|
|||
baseUrl,
|
||||
});
|
||||
|
||||
const baseUrlUnlocalized =
|
||||
currentLocale === defaultLocale
|
||||
? baseUrl
|
||||
: baseUrl.replace(`/${currentLocale}/`, '/');
|
||||
|
||||
// Canonical pathname, without the baseUrl of the current locale
|
||||
const pathnameSuffix = canonicalPathname.replace(baseUrl, '');
|
||||
|
||||
function getLocalizedBaseUrl(locale: string) {
|
||||
return locale === defaultLocale
|
||||
? `${baseUrlUnlocalized}`
|
||||
: `${baseUrlUnlocalized}${locale}/`;
|
||||
function getLocaleConfig(locale: string): I18nLocaleConfig {
|
||||
const localeConfig = localeConfigs[locale];
|
||||
if (!localeConfig) {
|
||||
throw new Error(
|
||||
`Unexpected Docusaurus bug, no locale config found for locale=${locale}`,
|
||||
);
|
||||
}
|
||||
return localeConfig;
|
||||
}
|
||||
|
||||
// TODO support correct alternate url when localized site is deployed on
|
||||
// another domain
|
||||
function createUrl({
|
||||
locale,
|
||||
fullyQualified,
|
||||
|
@ -71,9 +70,10 @@ export function useAlternatePageUtils(): {
|
|||
locale: string;
|
||||
fullyQualified: boolean;
|
||||
}) {
|
||||
return `${fullyQualified ? url : ''}${getLocalizedBaseUrl(
|
||||
locale,
|
||||
)}${pathnameSuffix}`;
|
||||
const localeConfig = getLocaleConfig(locale);
|
||||
const newUrl = `${fullyQualified ? localeConfig.url : ''}`;
|
||||
const newBaseUrl = localeConfig.baseUrl;
|
||||
return `${newUrl}${newBaseUrl}${pathnameSuffix}`;
|
||||
}
|
||||
|
||||
return {createUrl};
|
||||
|
|
19
packages/docusaurus-types/src/i18n.d.ts
vendored
19
packages/docusaurus-types/src/i18n.d.ts
vendored
|
@ -37,6 +37,25 @@ export type I18nLocaleConfig = {
|
|||
* By default, it will only be run if the `./i18n/<locale>` exists.
|
||||
*/
|
||||
translate: boolean;
|
||||
|
||||
/**
|
||||
* For i18n sites deployed to distinct domains, it is recommended to configure
|
||||
* a site url on a per-locale basis.
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* An explicit baseUrl to use for this locale, overriding the default one:
|
||||
* Default values:
|
||||
* - Default locale: `/${siteConfig.baseUrl}/`
|
||||
* - Other locales: `/${siteConfig.baseUrl}/<locale>/`
|
||||
*
|
||||
* Exception: when using the CLI with a single `--locale` parameter, the
|
||||
* `/<locale>/` path segment is not included. This is a better default for
|
||||
* sites looking to deploy each locale to a different subdomain, such as
|
||||
* `https://<locale>.docusaurus.io`
|
||||
*/
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export type I18nConfig = {
|
||||
|
|
|
@ -5,12 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import {
|
||||
mergeTranslations,
|
||||
updateTranslationFileMessages,
|
||||
getPluginI18nPath,
|
||||
localizePath,
|
||||
getLocaleConfig,
|
||||
} from '../i18nUtils';
|
||||
import type {I18n, I18nLocaleConfig} from '@docusaurus/types';
|
||||
|
@ -97,91 +95,6 @@ describe('getPluginI18nPath', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('localizePath', () => {
|
||||
it('localizes url path with current locale', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: {},
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toBe('/baseUrl/fr/');
|
||||
});
|
||||
|
||||
it('localizes fs path with current locale', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'fs',
|
||||
path: '/baseFsPath',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toBe(`${path.sep}baseFsPath${path.sep}fr`);
|
||||
});
|
||||
|
||||
it('localizes path for default locale, if requested', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toBe('/baseUrl/en/');
|
||||
});
|
||||
|
||||
it('does not localize path for default locale by default', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
|
||||
},
|
||||
}),
|
||||
).toBe('/baseUrl/');
|
||||
});
|
||||
|
||||
it('localizes path for non-default locale by default', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
|
||||
},
|
||||
}),
|
||||
).toBe('/baseUrl/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocaleConfig', () => {
|
||||
const localeConfigEn: I18nLocaleConfig = {
|
||||
path: 'path',
|
||||
|
@ -190,6 +103,7 @@ describe('getLocaleConfig', () => {
|
|||
calendar: 'calendar',
|
||||
label: 'EN',
|
||||
translate: true,
|
||||
baseUrl: '/',
|
||||
};
|
||||
const localeConfigFr: I18nLocaleConfig = {
|
||||
path: 'path',
|
||||
|
@ -198,6 +112,7 @@ describe('getLocaleConfig', () => {
|
|||
calendar: 'calendar',
|
||||
label: 'FR',
|
||||
translate: true,
|
||||
baseUrl: '/fr/',
|
||||
};
|
||||
|
||||
function i18n(params: Partial<I18n>): I18n {
|
||||
|
|
|
@ -9,7 +9,6 @@ import path from 'path';
|
|||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {DEFAULT_PLUGIN_ID} from './constants';
|
||||
import {normalizeUrl} from './urlUtils';
|
||||
import type {
|
||||
TranslationFileContent,
|
||||
TranslationFile,
|
||||
|
@ -67,54 +66,6 @@ export function getPluginI18nPath({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a path and returns a localized a version (which is basically `path +
|
||||
* i18n.currentLocale`).
|
||||
*
|
||||
* This is used to resolve the `outDir` and `baseUrl` of each locale; it is NOT
|
||||
* used to determine plugin localization file locations.
|
||||
*/
|
||||
export function localizePath({
|
||||
pathType,
|
||||
path: originalPath,
|
||||
i18n,
|
||||
options = {},
|
||||
}: {
|
||||
/**
|
||||
* FS paths will treat Windows specially; URL paths will always have a
|
||||
* trailing slash to make it a valid base URL.
|
||||
*/
|
||||
pathType: 'fs' | 'url';
|
||||
/** The path, URL or file path, to be localized. */
|
||||
path: string;
|
||||
/** The current i18n context. */
|
||||
i18n: I18n;
|
||||
options?: {
|
||||
/**
|
||||
* By default, we don't localize the path of defaultLocale. This option
|
||||
* would override that behavior. Setting `false` is useful for `yarn build
|
||||
* -l zh-Hans` to always emit into the root build directory.
|
||||
*/
|
||||
localizePath?: boolean;
|
||||
};
|
||||
}): string {
|
||||
const shouldLocalizePath: boolean =
|
||||
options.localizePath ?? i18n.currentLocale !== i18n.defaultLocale;
|
||||
|
||||
if (!shouldLocalizePath) {
|
||||
return originalPath;
|
||||
}
|
||||
// FS paths need special care, for Windows support. Note: we don't use the
|
||||
// locale config's `path` here, because this function is used for resolving
|
||||
// outDir, which must be the same as baseUrl. When we have the baseUrl config,
|
||||
// we need to sync the two.
|
||||
if (pathType === 'fs') {
|
||||
return path.join(originalPath, i18n.currentLocale);
|
||||
}
|
||||
// Url paths; add a trailing slash so it's a valid base URL
|
||||
return normalizeUrl([originalPath, i18n.currentLocale, '/']);
|
||||
}
|
||||
|
||||
// TODO we may extract this to a separate package
|
||||
// we want to use it on the frontend too
|
||||
// but "docusaurus-utils-common" (agnostic utils) is not an ideal place since
|
||||
|
|
|
@ -33,7 +33,6 @@ export {
|
|||
mergeTranslations,
|
||||
updateTranslationFileMessages,
|
||||
getPluginI18nPath,
|
||||
localizePath,
|
||||
getLocaleConfig,
|
||||
} from './i18nUtils';
|
||||
export {mapAsyncSequential, findAsyncSequential} from './jsUtils';
|
||||
|
|
|
@ -60,7 +60,6 @@ export default function ComponentCreator(
|
|||
Object.entries(flatChunkNames).forEach(([keyPath, chunkName]) => {
|
||||
const chunkRegistry = registry[chunkName];
|
||||
if (chunkRegistry) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
loader[keyPath] = chunkRegistry[0];
|
||||
modules.push(chunkRegistry[1]);
|
||||
optsWebpack.push(chunkRegistry[2]);
|
||||
|
|
|
@ -11,6 +11,7 @@ import {mapAsyncSequential} from '@docusaurus/utils';
|
|||
import {loadContext, type LoadContextParams} from '../../server/site';
|
||||
import {loadI18n} from '../../server/i18n';
|
||||
import {buildLocale, type BuildLocaleParams} from './buildLocale';
|
||||
import {isAutomaticBaseUrlLocalizationDisabled} from './buildUtils';
|
||||
|
||||
export type BuildCLIOptions = Pick<LoadContextParams, 'config' | 'outDir'> & {
|
||||
locale?: [string, ...string[]];
|
||||
|
@ -80,21 +81,20 @@ async function getLocalesToBuild({
|
|||
siteDir: string;
|
||||
cliOptions: BuildCLIOptions;
|
||||
}): Promise<[string, ...string[]]> {
|
||||
// We disable locale path localization if CLI has single "--locale" option
|
||||
// yarn build --locale fr => baseUrl=/ instead of baseUrl=/fr/
|
||||
const localizePath = cliOptions.locale?.length === 1 ? false : undefined;
|
||||
|
||||
// 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({
|
||||
siteDir,
|
||||
outDir: cliOptions.outDir,
|
||||
config: cliOptions.config,
|
||||
localizePath,
|
||||
automaticBaseUrlLocalizationDisabled: isAutomaticBaseUrlLocalizationDisabled(cliOptions),
|
||||
});
|
||||
|
||||
const i18n = await loadI18n({
|
||||
siteDir,
|
||||
config: context.siteConfig,
|
||||
currentLocale: context.siteConfig.i18n.defaultLocale // Awkward but ok
|
||||
currentLocale: context.siteConfig.i18n.defaultLocale, // Awkward but ok
|
||||
automaticBaseUrlLocalizationDisabled: false,
|
||||
});
|
||||
|
||||
const locales = cliOptions.locale ?? i18n.locales;
|
||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
|||
import type {SiteCollectedData} from '../../common';
|
||||
import {BuildCLIOptions} from './build';
|
||||
import clearPath from '../utils/clearPath';
|
||||
import {isAutomaticBaseUrlLocalizationDisabled} from './buildUtils';
|
||||
|
||||
export type BuildLocaleParams = {
|
||||
siteDir: string;
|
||||
|
@ -56,7 +57,7 @@ export async function buildLocale({
|
|||
outDir: cliOptions.outDir,
|
||||
config: cliOptions.config,
|
||||
locale,
|
||||
localizePath: cliOptions.locale?.length === 1 ? false : undefined,
|
||||
automaticBaseUrlLocalizationDisabled: isAutomaticBaseUrlLocalizationDisabled(cliOptions),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
18
packages/docusaurus/src/commands/build/buildUtils.ts
Normal file
18
packages/docusaurus/src/commands/build/buildUtils.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BuildCLIOptions} from './build';
|
||||
|
||||
/**
|
||||
* We disable locale path localization if CLI has a single "--locale" option
|
||||
* yarn build --locale fr => baseUrl=/ instead of baseUrl=/fr/
|
||||
* By default, this makes it easier to support multi-domain deployments
|
||||
* See https://docusaurus.io/docs/i18n/tutorial#multi-domain-deployment
|
||||
*/
|
||||
export function isAutomaticBaseUrlLocalizationDisabled(cliOptions: BuildCLIOptions) {
|
||||
return cliOptions.locale?.length === 1;
|
||||
}
|
|
@ -90,7 +90,6 @@ async function createLoadSiteParams({
|
|||
siteDir,
|
||||
config: cliOptions.config,
|
||||
locale: cliOptions.locale,
|
||||
localizePath: undefined, // Should this be configurable?
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`normalizeConfig throws error for required fields 1`] = `
|
||||
""baseUrl" is required
|
||||
""url" is required
|
||||
"baseUrl" is required
|
||||
"title" is required
|
||||
"url" is required
|
||||
"themes" must be an array
|
||||
"presets" must be an array
|
||||
"scripts" must be an array
|
||||
|
|
|
@ -15,20 +15,24 @@ exports[`load loads props for site 1`] = `
|
|||
"defaultLocale": "en",
|
||||
"localeConfigs": {
|
||||
"en": {
|
||||
"baseUrl": "/",
|
||||
"calendar": "gregory",
|
||||
"direction": "ltr",
|
||||
"htmlLang": "en",
|
||||
"label": "English",
|
||||
"path": "en-custom",
|
||||
"translate": false,
|
||||
"url": "https://example.com",
|
||||
},
|
||||
"zh-Hans": {
|
||||
"baseUrl": "/zh-Hans/",
|
||||
"calendar": "gregory",
|
||||
"direction": "ltr",
|
||||
"htmlLang": "zh-Hans",
|
||||
"label": "简体中文",
|
||||
"path": "zh-Hans-custom",
|
||||
"translate": true,
|
||||
"url": "https://example.com",
|
||||
},
|
||||
},
|
||||
"locales": [
|
||||
|
@ -38,7 +42,7 @@ exports[`load loads props for site 1`] = `
|
|||
"path": "i18n",
|
||||
},
|
||||
"localizationDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/i18n/en-custom",
|
||||
"outDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/build",
|
||||
"outDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/build/",
|
||||
"plugins": [
|
||||
{
|
||||
"content": undefined,
|
||||
|
@ -109,11 +113,9 @@ exports[`load loads props for site 1`] = `
|
|||
"defaultLocale": "en",
|
||||
"localeConfigs": {
|
||||
"en": {
|
||||
"direction": "ltr",
|
||||
"path": "en-custom",
|
||||
},
|
||||
"zh-Hans": {
|
||||
"direction": "ltr",
|
||||
"path": "zh-Hans-custom",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,6 +27,8 @@ import type {
|
|||
Config,
|
||||
DocusaurusConfig,
|
||||
PluginConfig,
|
||||
I18nConfig,
|
||||
I18nLocaleConfig,
|
||||
} from '@docusaurus/types';
|
||||
import type {DeepPartial} from 'utility-types';
|
||||
|
||||
|
@ -366,6 +368,115 @@ describe('onBrokenLinks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('i18n', () => {
|
||||
function normalizeI18n(i18n: DeepPartial<I18nConfig>): I18nConfig {
|
||||
return normalizeConfig({i18n}).i18n;
|
||||
}
|
||||
|
||||
it('accepts undefined object', () => {
|
||||
expect(normalizeI18n(undefined)).toEqual(DEFAULT_CONFIG.i18n);
|
||||
});
|
||||
|
||||
it('rejects empty object', () => {
|
||||
expect(() => normalizeI18n({})).toThrowErrorMatchingInlineSnapshot(`
|
||||
""i18n.defaultLocale" is required
|
||||
"i18n.locales" is required
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('accepts minimal i18n config', () => {
|
||||
expect(normalizeI18n({defaultLocale: 'fr', locales: ['fr']})).toEqual({
|
||||
defaultLocale: 'fr',
|
||||
localeConfigs: {},
|
||||
locales: ['fr'],
|
||||
path: 'i18n',
|
||||
});
|
||||
});
|
||||
|
||||
describe('locale config', () => {
|
||||
function normalizeLocaleConfig(
|
||||
localeConfig?: Partial<I18nLocaleConfig>,
|
||||
): Partial<I18nLocaleConfig> {
|
||||
return normalizeConfig({
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['fr'],
|
||||
localeConfigs: {
|
||||
fr: localeConfig,
|
||||
},
|
||||
},
|
||||
}).i18n.localeConfigs.fr;
|
||||
}
|
||||
|
||||
it('accepts undefined locale config', () => {
|
||||
expect(normalizeLocaleConfig(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts empty locale config', () => {
|
||||
expect(normalizeLocaleConfig({})).toEqual({});
|
||||
});
|
||||
|
||||
describe('url', () => {
|
||||
it('accepts undefined', () => {
|
||||
expect(normalizeLocaleConfig({url: undefined})).toEqual({
|
||||
url: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects empty', () => {
|
||||
expect(() => normalizeLocaleConfig({url: ''}))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
""i18n.localeConfigs.fr.url" is not allowed to be empty
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('accepts valid url', () => {
|
||||
expect(
|
||||
normalizeLocaleConfig({url: 'https://fr.docusaurus.io'}),
|
||||
).toEqual({
|
||||
url: 'https://fr.docusaurus.io',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts valid url and removes trailing slash', () => {
|
||||
expect(
|
||||
normalizeLocaleConfig({url: 'https://fr.docusaurus.io/'}),
|
||||
).toEqual({
|
||||
url: 'https://fr.docusaurus.io',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('baseUrl', () => {
|
||||
it('accepts undefined baseUrl', () => {
|
||||
expect(normalizeLocaleConfig({baseUrl: undefined})).toEqual({
|
||||
baseUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts empty baseUrl', () => {
|
||||
expect(normalizeLocaleConfig({baseUrl: ''})).toEqual({
|
||||
baseUrl: '/',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts regular baseUrl', () => {
|
||||
expect(normalizeLocaleConfig({baseUrl: '/myBase/Url/'})).toEqual({
|
||||
baseUrl: '/myBase/Url/',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts baseUrl without leading/trailing slashes', () => {
|
||||
expect(normalizeLocaleConfig({baseUrl: 'myBase/Url'})).toEqual({
|
||||
baseUrl: '/myBase/Url/',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown', () => {
|
||||
function normalizeMarkdown(
|
||||
markdown: DeepPartial<MarkdownConfig>,
|
||||
|
@ -508,9 +619,9 @@ describe('markdown', () => {
|
|||
emoji: 'yes',
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""markdown.emoji" must be a boolean
|
||||
"
|
||||
`);
|
||||
""markdown.emoji" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throw for number emoji value', () => {
|
||||
|
@ -522,9 +633,9 @@ describe('markdown', () => {
|
|||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""markdown.emoji" must be a boolean
|
||||
"
|
||||
`);
|
||||
""markdown.emoji" must be a boolean
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,21 +17,31 @@ const loadI18nSiteDir = path.resolve(
|
|||
'load-i18n-site',
|
||||
);
|
||||
|
||||
const siteUrl = 'https://example.com';
|
||||
|
||||
function loadI18nTest({
|
||||
siteDir = loadI18nSiteDir,
|
||||
baseUrl = '/',
|
||||
i18nConfig,
|
||||
currentLocale,
|
||||
automaticBaseUrlLocalizationDisabled,
|
||||
}: {
|
||||
siteDir?: string;
|
||||
baseUrl?: string;
|
||||
i18nConfig: I18nConfig;
|
||||
currentLocale: string;
|
||||
automaticBaseUrlLocalizationDisabled?: boolean;
|
||||
}) {
|
||||
return loadI18n({
|
||||
siteDir,
|
||||
config: {
|
||||
i18n: i18nConfig,
|
||||
url: siteUrl,
|
||||
baseUrl,
|
||||
} as DocusaurusConfig,
|
||||
currentLocale,
|
||||
automaticBaseUrlLocalizationDisabled:
|
||||
automaticBaseUrlLocalizationDisabled ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -133,6 +143,8 @@ describe('loadI18n', () => {
|
|||
en: {
|
||||
...getDefaultLocaleConfig('en'),
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -158,14 +170,60 @@ describe('loadI18n', () => {
|
|||
en: {
|
||||
...getDefaultLocaleConfig('en'),
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/en/',
|
||||
},
|
||||
fr: {
|
||||
...getDefaultLocaleConfig('fr'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
de: {
|
||||
...getDefaultLocaleConfig('de'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/de/',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('loads I18n for multi-lang config - with automaticBaseUrlLocalizationDisabled=true', async () => {
|
||||
await expect(
|
||||
loadI18nTest({
|
||||
i18nConfig: {
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
currentLocale: 'fr',
|
||||
automaticBaseUrlLocalizationDisabled: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: {
|
||||
en: {
|
||||
...getDefaultLocaleConfig('en'),
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
fr: {
|
||||
...getDefaultLocaleConfig('fr'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
de: {
|
||||
...getDefaultLocaleConfig('de'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -191,14 +249,20 @@ describe('loadI18n', () => {
|
|||
en: {
|
||||
...getDefaultLocaleConfig('en'),
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/en/',
|
||||
},
|
||||
fr: {
|
||||
...getDefaultLocaleConfig('fr'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
de: {
|
||||
...getDefaultLocaleConfig('de'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/de/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -213,10 +277,11 @@ describe('loadI18n', () => {
|
|||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {
|
||||
fr: {label: 'Français', translate: false},
|
||||
en: {translate: true},
|
||||
de: {translate: false},
|
||||
en: {translate: true, baseUrl: 'en-EN/whatever/else'},
|
||||
de: {translate: false, baseUrl: '/de-DE/'},
|
||||
},
|
||||
},
|
||||
|
||||
currentLocale: 'de',
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
|
@ -232,19 +297,96 @@ describe('loadI18n', () => {
|
|||
calendar: 'gregory',
|
||||
path: 'fr',
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/',
|
||||
},
|
||||
en: {
|
||||
...getDefaultLocaleConfig('en'),
|
||||
translate: true,
|
||||
url: siteUrl,
|
||||
baseUrl: '/en-EN/whatever/else/',
|
||||
},
|
||||
de: {
|
||||
...getDefaultLocaleConfig('de'),
|
||||
translate: false,
|
||||
url: siteUrl,
|
||||
baseUrl: '/de-DE/',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('loads I18n for multi-locale config with baseUrl edge cases', async () => {
|
||||
await expect(
|
||||
loadI18nTest({
|
||||
baseUrl: 'siteBaseUrl',
|
||||
i18nConfig: {
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de', 'pt'],
|
||||
localeConfigs: {
|
||||
fr: {},
|
||||
en: {baseUrl: ''},
|
||||
de: {baseUrl: '/de-DE/'},
|
||||
},
|
||||
},
|
||||
currentLocale: 'de',
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
localeConfigs: {
|
||||
fr: expect.objectContaining({
|
||||
baseUrl: '/siteBaseUrl/',
|
||||
}),
|
||||
en: expect.objectContaining({
|
||||
baseUrl: '/',
|
||||
}),
|
||||
de: expect.objectContaining({
|
||||
baseUrl: '/de-DE/',
|
||||
}),
|
||||
pt: expect.objectContaining({
|
||||
baseUrl: '/siteBaseUrl/pt/',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('loads I18n for multi-locale config with custom urls', async () => {
|
||||
await expect(
|
||||
loadI18nTest({
|
||||
baseUrl: 'siteBaseUrl',
|
||||
i18nConfig: {
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de', 'pt'],
|
||||
localeConfigs: {
|
||||
fr: {url: 'https://fr.example.com'},
|
||||
en: {url: 'https://en.example.com'},
|
||||
},
|
||||
},
|
||||
currentLocale: 'de',
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
localeConfigs: {
|
||||
fr: expect.objectContaining({
|
||||
url: 'https://fr.example.com',
|
||||
}),
|
||||
en: expect.objectContaining({
|
||||
url: 'https://en.example.com',
|
||||
}),
|
||||
de: expect.objectContaining({
|
||||
url: siteUrl,
|
||||
}),
|
||||
pt: expect.objectContaining({
|
||||
url: siteUrl,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('warns when trying to load undeclared locale', async () => {
|
||||
await loadI18nTest({
|
||||
i18nConfig: {
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('load', () => {
|
|||
),
|
||||
outDir: path.join(
|
||||
__dirname,
|
||||
'__fixtures__/custom-i18n-site/build/zh-Hans',
|
||||
'__fixtures__/custom-i18n-site/build/zh-Hans/',
|
||||
),
|
||||
routesPaths: ['/zh-Hans/404.html'],
|
||||
siteConfig: expect.objectContaining({
|
||||
|
|
|
@ -26,10 +26,36 @@ import type {
|
|||
I18nConfig,
|
||||
MarkdownConfig,
|
||||
MarkdownHooks,
|
||||
I18nLocaleConfig,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
const DEFAULT_I18N_LOCALE = 'en';
|
||||
|
||||
const SiteUrlSchema = Joi.string()
|
||||
.custom((value: string, helpers) => {
|
||||
try {
|
||||
const {pathname} = new URL(value);
|
||||
if (pathname !== '/') {
|
||||
return helpers.error('docusaurus.subPathError', {pathname});
|
||||
}
|
||||
} catch {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return removeTrailingSlash(value);
|
||||
})
|
||||
.messages({
|
||||
'any.invalid':
|
||||
'"{#value}" does not look like a valid URL. Make sure it has a protocol; for example, "https://example.com".',
|
||||
'docusaurus.subPathError':
|
||||
'The url is not supposed to contain a sub-path like "{#pathname}". Please use the baseUrl field for sub-paths.',
|
||||
});
|
||||
|
||||
const BaseUrlSchema = Joi
|
||||
// Weird Joi trick needed, otherwise value '' is not normalized...
|
||||
.alternatives()
|
||||
.try(Joi.string().required().allow(''))
|
||||
.custom((value: string) => addLeadingSlash(addTrailingSlash(value)));
|
||||
|
||||
export const DEFAULT_I18N_CONFIG: I18nConfig = {
|
||||
defaultLocale: DEFAULT_I18N_LOCALE,
|
||||
path: DEFAULT_I18N_DIR_NAME,
|
||||
|
@ -220,12 +246,14 @@ const PresetSchema = Joi.alternatives()
|
|||
- A simple string, like \`"classic"\``,
|
||||
});
|
||||
|
||||
const LocaleConfigSchema = Joi.object({
|
||||
const LocaleConfigSchema = Joi.object<I18nLocaleConfig>({
|
||||
label: Joi.string(),
|
||||
htmlLang: Joi.string(),
|
||||
direction: Joi.string().equal('ltr', 'rtl').default('ltr'),
|
||||
direction: Joi.string().equal('ltr', 'rtl'),
|
||||
calendar: Joi.string(),
|
||||
path: Joi.string(),
|
||||
url: SiteUrlSchema,
|
||||
baseUrl: BaseUrlSchema,
|
||||
});
|
||||
|
||||
const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
|
||||
|
@ -313,38 +341,13 @@ const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
|
|||
.optional()
|
||||
.default(DEFAULT_FUTURE_CONFIG);
|
||||
|
||||
const SiteUrlSchema = Joi.string()
|
||||
.required()
|
||||
.custom((value: string, helpers) => {
|
||||
try {
|
||||
const {pathname} = new URL(value);
|
||||
if (pathname !== '/') {
|
||||
return helpers.error('docusaurus.subPathError', {pathname});
|
||||
}
|
||||
} catch {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return removeTrailingSlash(value);
|
||||
})
|
||||
.messages({
|
||||
'any.invalid':
|
||||
'"{#value}" does not look like a valid URL. Make sure it has a protocol; for example, "https://example.com".',
|
||||
'docusaurus.subPathError':
|
||||
'The url is not supposed to contain a sub-path like "{#pathname}". Please use the baseUrl field for sub-paths.',
|
||||
});
|
||||
|
||||
// TODO move to @docusaurus/utils-validation
|
||||
export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
||||
baseUrl: Joi
|
||||
// Weird Joi trick needed, otherwise value '' is not normalized...
|
||||
.alternatives()
|
||||
.try(Joi.string().required().allow(''))
|
||||
.required()
|
||||
.custom((value: string) => addLeadingSlash(addTrailingSlash(value))),
|
||||
url: SiteUrlSchema.required(),
|
||||
baseUrl: BaseUrlSchema.required(),
|
||||
baseUrlIssueBanner: Joi.boolean().default(DEFAULT_CONFIG.baseUrlIssueBanner),
|
||||
favicon: Joi.string().optional(),
|
||||
title: Joi.string().required(),
|
||||
url: SiteUrlSchema,
|
||||
trailingSlash: Joi.boolean(), // No default value! undefined = retrocompatible legacy behavior!
|
||||
i18n: I18N_CONFIG_SCHEMA,
|
||||
future: FUTURE_CONFIG_SCHEMA,
|
||||
|
|
|
@ -9,6 +9,7 @@ import path from 'path';
|
|||
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';
|
||||
|
||||
function inferLanguageDisplayName(locale: string) {
|
||||
|
@ -82,7 +83,7 @@ function getDefaultDirection(localeStr: string) {
|
|||
|
||||
export function getDefaultLocaleConfig(
|
||||
locale: string,
|
||||
): Omit<I18nLocaleConfig, 'translate'> {
|
||||
): Omit<I18nLocaleConfig, 'translate' | 'url' | 'baseUrl'> {
|
||||
try {
|
||||
return {
|
||||
label: getDefaultLocaleLabel(locale),
|
||||
|
@ -103,10 +104,12 @@ export async function loadI18n({
|
|||
siteDir,
|
||||
config,
|
||||
currentLocale,
|
||||
automaticBaseUrlLocalizationDisabled,
|
||||
}: {
|
||||
siteDir: string;
|
||||
config: DocusaurusConfig;
|
||||
currentLocale: string;
|
||||
automaticBaseUrlLocalizationDisabled: boolean;
|
||||
}): Promise<I18n> {
|
||||
const {i18n: i18nConfig} = config;
|
||||
|
||||
|
@ -123,7 +126,10 @@ Note: Docusaurus only support running one locale at a time.`;
|
|||
locale: string,
|
||||
): Promise<I18nLocaleConfig> {
|
||||
const localeConfigInput = i18nConfig.localeConfigs[locale] ?? {};
|
||||
const localeConfig: Omit<I18nLocaleConfig, 'translate'> = {
|
||||
const localeConfig: Omit<
|
||||
I18nLocaleConfig,
|
||||
'translate' | 'url' | 'baseUrl'
|
||||
> = {
|
||||
...getDefaultLocaleConfig(locale),
|
||||
...localeConfigInput,
|
||||
};
|
||||
|
@ -138,10 +144,36 @@ Note: Docusaurus only support running one locale at a time.`;
|
|||
return fs.pathExists(localizationDir);
|
||||
}
|
||||
|
||||
function getInferredBaseUrl(): string {
|
||||
const addLocaleSegment =
|
||||
locale !== i18nConfig.defaultLocale &&
|
||||
!automaticBaseUrlLocalizationDisabled;
|
||||
|
||||
return normalizeUrl([
|
||||
'/',
|
||||
config.baseUrl,
|
||||
addLocaleSegment ? locale : '',
|
||||
'/',
|
||||
]);
|
||||
}
|
||||
|
||||
const translate = localeConfigInput.translate ?? (await inferTranslate());
|
||||
|
||||
const url =
|
||||
typeof localeConfigInput.url !== 'undefined'
|
||||
? localeConfigInput.url
|
||||
: config.url;
|
||||
|
||||
const baseUrl =
|
||||
typeof localeConfigInput.baseUrl !== 'undefined'
|
||||
? normalizeUrl(['/', localeConfigInput.baseUrl, '/'])
|
||||
: getInferredBaseUrl();
|
||||
|
||||
return {
|
||||
...localeConfig,
|
||||
translate,
|
||||
url,
|
||||
baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import path from 'path';
|
||||
import {
|
||||
localizePath,
|
||||
DEFAULT_BUILD_DIR_NAME,
|
||||
GENERATED_FILES_DIR_NAME,
|
||||
getLocaleConfig,
|
||||
|
@ -47,13 +46,21 @@ export type LoadContextParams = {
|
|||
config?: string;
|
||||
/** Default is `i18n.defaultLocale` */
|
||||
locale?: string;
|
||||
|
||||
/**
|
||||
* `true` means the paths will have the locale prepended; `false` means they
|
||||
* won't (useful for `yarn build -l zh-Hans` where the output should be
|
||||
* emitted into `build/` instead of `build/zh-Hans/`); `undefined` is like the
|
||||
* "smart" option where only non-default locale paths are localized
|
||||
* By default, we try to automatically infer a localized baseUrl.
|
||||
* We prepend `/<siteBaseUrl>/` with a `/<locale>/` path segment,
|
||||
* except for the default locale.
|
||||
*
|
||||
* This option permits opting out of this baseUrl localization process.
|
||||
* It is mostly useful to simplify config for multi-domain i18n deployments.
|
||||
* See https://docusaurus.io/docs/i18n/tutorial#multi-domain-deployment
|
||||
*
|
||||
* In all cases, this process doesn't happen if an explicit localized baseUrl
|
||||
* has been provided using `i18n.localeConfigs[].baseUrl`. We always use the
|
||||
* provided value over the inferred one, letting you override it.
|
||||
*/
|
||||
localizePath?: boolean;
|
||||
automaticBaseUrlLocalizationDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type LoadSiteParams = LoadContextParams & {
|
||||
|
@ -79,6 +86,7 @@ export async function loadContext(
|
|||
outDir: baseOutDir = DEFAULT_BUILD_DIR_NAME,
|
||||
locale,
|
||||
config: customConfigFilePath,
|
||||
automaticBaseUrlLocalizationDisabled,
|
||||
} = params;
|
||||
const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME);
|
||||
|
||||
|
@ -101,27 +109,29 @@ export async function loadContext(
|
|||
siteDir,
|
||||
config: initialSiteConfig,
|
||||
currentLocale: locale ?? initialSiteConfig.i18n.defaultLocale,
|
||||
automaticBaseUrlLocalizationDisabled:
|
||||
automaticBaseUrlLocalizationDisabled ?? false,
|
||||
});
|
||||
|
||||
const baseUrl = localizePath({
|
||||
path: initialSiteConfig.baseUrl,
|
||||
i18n,
|
||||
options: params,
|
||||
pathType: 'url',
|
||||
});
|
||||
const outDir = localizePath({
|
||||
path: path.resolve(siteDir, baseOutDir),
|
||||
i18n,
|
||||
options: params,
|
||||
pathType: 'fs',
|
||||
});
|
||||
const localeConfig = getLocaleConfig(i18n);
|
||||
|
||||
// We use the baseUrl from the locale config.
|
||||
// By default, it is inferred as /<siteConfig.baseUrl>/
|
||||
// eventually including the /<locale>/ suffix
|
||||
const baseUrl = localeConfig.baseUrl;
|
||||
|
||||
const outDir = path.join(path.resolve(siteDir, baseOutDir), baseUrl);
|
||||
|
||||
const localizationDir = path.resolve(
|
||||
siteDir,
|
||||
i18n.path,
|
||||
getLocaleConfig(i18n).path,
|
||||
);
|
||||
|
||||
const siteConfig: DocusaurusConfig = {...initialSiteConfig, baseUrl};
|
||||
const siteConfig: DocusaurusConfig = {
|
||||
...initialSiteConfig,
|
||||
baseUrl,
|
||||
};
|
||||
|
||||
const codeTranslations = await loadSiteCodeTranslations({localizationDir});
|
||||
|
||||
|
|
|
@ -334,7 +334,6 @@ Unavatar
|
|||
unlinkable
|
||||
Unlisteds
|
||||
unlisteds
|
||||
Unlocalized
|
||||
unlocalized
|
||||
unswizzle
|
||||
upvotes
|
||||
|
|
|
@ -84,11 +84,24 @@ export default {
|
|||
};
|
||||
```
|
||||
|
||||
:::info Special case for i18n sites
|
||||
|
||||
If your site uses multiple locales, it is possible to provide a distinct `url` for each locale thanks to the [`siteConfig.i18n.localeConfigs[<locale>].url`](#i18n) attribute. This makes it possible to deploy a localized Docusaurus site [deploy a localized Docusaurus site over multiple domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment).
|
||||
|
||||
:::
|
||||
|
||||
### `baseUrl` {#baseUrl}
|
||||
|
||||
- Type: `string`
|
||||
|
||||
Base URL for your site. Can be considered as the path after the host. For example, `/metro/` is the base URL of https://facebook.github.io/metro/. For URLs that have no path, the baseUrl should be set to `/`. This field is related to the [`url`](#url) field. Always has both leading and trailing slash.
|
||||
The base URL of your site is the path segment appearing just after the [`url`](#url), letting you eventually host your site under a subpath instead of at the root of the domain.
|
||||
|
||||
For example, let's consider you want to host a site at https://facebook.github.io/metro/, then you must configure it accordingly:
|
||||
|
||||
- [`url`](#url) should be `'https://facebook.github.io'`
|
||||
- `baseUrl` should be `'/metro/'`
|
||||
|
||||
By default, a Docusaurus site is hosted at the root of the domain:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
export default {
|
||||
|
@ -96,6 +109,18 @@ export default {
|
|||
};
|
||||
```
|
||||
|
||||
:::info Special case for i18n sites
|
||||
|
||||
If your site uses multiple locales, then Docusaurus will automatically localize the `baseUrl` of your site based on smart heuristics:
|
||||
|
||||
- For the default locale, `baseUrl` will be `/<siteBaseUrl>/`
|
||||
- For other locales, `baseUrl` will be `/<siteBaseUrl>/<locale>/`
|
||||
- When building a single locale at a time (with `docusaurus build --locale <locale>`), `baseUrl` will be `/<siteBaseUrl>/`, assuming the intent is to [deploy each locale on distinct domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment).
|
||||
|
||||
When the localized `baseUrl` Docusaurus computes doesn't satisfy you, it's always possible to override it by providing an explicit localized `baseUrl` thanks to the [`siteConfig.i18n.localeConfigs[<locale>].baseUrl`](#i18n) attribute.
|
||||
|
||||
:::
|
||||
|
||||
## Optional fields {#optional-fields}
|
||||
|
||||
### `favicon` {#favicon}
|
||||
|
@ -152,6 +177,8 @@ export default {
|
|||
calendar: 'gregory',
|
||||
path: 'en',
|
||||
translate: false,
|
||||
url: 'https://en.example.com',
|
||||
baseUrl: '/',
|
||||
},
|
||||
fa: {
|
||||
label: 'فارسی',
|
||||
|
@ -160,6 +187,8 @@ export default {
|
|||
calendar: 'persian',
|
||||
path: 'fa',
|
||||
translate: true,
|
||||
url: 'https://fa.example.com',
|
||||
baseUrl: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -176,6 +205,8 @@ export default {
|
|||
- `calendar`: the [calendar](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar) used to calculate the date era. Note that it doesn't control the actual string displayed: `MM/DD/YYYY` and `DD/MM/YYYY` are both `gregory`. To choose the format (`DD/MM/YYYY` or `MM/DD/YYYY`), set your locale name to `en-GB` or `en-US` (`en` means `en-US`).
|
||||
- `path`: Root folder that all plugin localization folders of this locale are relative to. Will be resolved against `i18n.path`. Defaults to the locale's name (`i18n/<locale>`). Note: this has no effect on the locale's `baseUrl`—customization of base URL is a work-in-progress.
|
||||
- `translate`: Should we run the translation process for this locale? By default, it is enabled if the `i18n/<locale>` folder exists
|
||||
- `url`: This lets you override the [`siteConfig.url`](#url), particularly useful if your site is [deployed over multiple domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment).
|
||||
- `baseUrl`: This lets you override the default localized `baseUrl` Docusaurus infers from your [`siteConfig.baseUrl`](#baseUrl), giving you more control to host your localized site in less common ways, in particularly [deployments over multi-domains](../i18n/i18n-tutorial.mdx#multi-domain-deployment)
|
||||
|
||||
### `future` {#future}
|
||||
|
||||
|
|
|
@ -453,6 +453,14 @@ For localized sites, it is recommended to use **[explicit heading IDs](../guides
|
|||
|
||||
You can choose to deploy your site under a **single domain** or use **multiple (sub)domains**.
|
||||
|
||||
:::tip About localized baseUrls
|
||||
|
||||
Docusaurus will automatically add a `/<locale>/` path segment to your site for locales except the default one. This heuristic works well for most sites but can be configured on a per-locale basis depending on your deployment requirements.
|
||||
|
||||
Read more on the [`siteConfig.baseUrl`](../api/docusaurus.config.js.mdx#baseUrl) docs.
|
||||
|
||||
:::
|
||||
|
||||
### Single-domain deployment {#single-domain-deployment}
|
||||
|
||||
Run the following command:
|
||||
|
@ -495,7 +503,7 @@ You can also build your site for a single locale:
|
|||
npm run build -- --locale fr
|
||||
```
|
||||
|
||||
Docusaurus will not add the `/fr/` URL prefix.
|
||||
When building a single locale at a time, Docusaurus will not add the `/fr/` URL prefix automatically, assuming you want to deploy each locale to a distinct domain.
|
||||
|
||||
On your [static hosting provider](../deployment.mdx):
|
||||
|
||||
|
@ -503,6 +511,37 @@ On your [static hosting provider](../deployment.mdx):
|
|||
- configure the appropriate build command, using the `--locale` option
|
||||
- configure the (sub)domain of your choice for each deployment
|
||||
|
||||
:::tip Configuring URLs for each locale
|
||||
|
||||
Use the [`siteConfig.i18n.localeConfigs[<locale>].url`](./../api/docusaurus.config.js.mdx#i18n) attribute to configure a distinct site URL for each locale:
|
||||
|
||||
```ts title=docusaurus.config.js
|
||||
const config = {
|
||||
i18n: {
|
||||
localeConfigs: {
|
||||
// highlight-start
|
||||
en: {
|
||||
url: 'https://en.docusaurus.io',
|
||||
baseUrl: '/',
|
||||
},
|
||||
fr: {
|
||||
url: 'https://fr.docusaurus.io',
|
||||
baseUrl: '/',
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
This helps [search engines like Google know about localized versions of your page](https://developers.google.com/search/docs/specialty/international/localized-versions) thanks to `<link rel="alternate" hreflang="<locale>"/>` meta tags.
|
||||
|
||||
This also permits Docusaurus themes to redirect users to the appropriate URL when they switch locale, usually through the [Navbar locale dropdown](../api/themes/theme-configuration.mdx#navbar-locale-dropdown).
|
||||
|
||||
Read more on the [`siteConfig.url`](../api/docusaurus.config.js.mdx#baseUrl) and [`siteConfig.baseUrl`](../api/docusaurus.config.js.mdx#baseUrl) docs.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
This strategy is **not possible** with GitHub Pages, as it is only possible to **have a single deployment**.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue