From 2febb76fae9a353778d9d995ced18f9515dab8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Mon, 28 Jul 2025 17:04:34 +0200 Subject: [PATCH 1/2] feat(core): Add `i18n.localeConfigs[locale].{url,baseUrl}` config options (#11316) Co-authored-by: slorber <749374+slorber@users.noreply.github.com> --- .eslintrc.js | 2 +- .../docusaurus-plugin-pwa/src/registerSw.ts | 3 +- .../LocaleDropdownNavbarItem/index.tsx | 83 +++++++-- packages/docusaurus-theme-common/package.json | 1 + packages/docusaurus-theme-common/src/index.ts | 2 + .../src/utils/__tests__/historyUtils.test.ts | 80 +++++++++ .../__tests__/useAlternatePageUtils.test.tsx | 169 ++++++++++++------ .../src/utils/historyUtils.ts | 29 +++ .../src/utils/useAlternatePageUtils.ts | 32 ++-- packages/docusaurus-types/src/i18n.d.ts | 19 ++ .../src/__tests__/i18nUtils.test.ts | 89 +-------- packages/docusaurus-utils/src/i18nUtils.ts | 49 ----- packages/docusaurus-utils/src/index.ts | 1 - .../src/client/exports/ComponentCreator.tsx | 1 - .../docusaurus/src/commands/build/build.ts | 12 +- .../src/commands/build/buildLocale.ts | 3 +- .../src/commands/build/buildUtils.ts | 18 ++ .../docusaurus/src/commands/start/utils.ts | 1 - .../configValidation.test.ts.snap | 4 +- .../__tests__/__snapshots__/site.test.ts.snap | 8 +- .../server/__tests__/configValidation.test.ts | 123 ++++++++++++- .../src/server/__tests__/i18n.test.ts | 146 ++++++++++++++- .../src/server/__tests__/site.test.ts | 2 +- .../docusaurus/src/server/configValidation.ts | 61 ++++--- packages/docusaurus/src/server/i18n.ts | 36 +++- packages/docusaurus/src/server/site.ts | 48 +++-- project-words.txt | 1 - website/docs/api/docusaurus.config.js.mdx | 33 +++- website/docs/i18n/i18n-tutorial.mdx | 41 ++++- 29 files changed, 798 insertions(+), 299 deletions(-) create mode 100644 packages/docusaurus-theme-common/src/utils/__tests__/historyUtils.test.ts create mode 100644 packages/docusaurus/src/commands/build/buildUtils.ts diff --git a/.eslintrc.js b/.eslintrc.js index 502c61506868..e8e026c9665a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/packages/docusaurus-plugin-pwa/src/registerSw.ts b/packages/docusaurus-plugin-pwa/src/registerSw.ts index d367df63a3c4..69a6cf3d8246 100644 --- a/packages/docusaurus-plugin-pwa/src/registerSw.ts +++ b/packages/docusaurus-plugin-pwa/src/registerSw.ts @@ -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; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx index 21b14d417e8c..9c16b5387c00 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx @@ -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'; +function useLocaleDropdownUtils() { + const { + siteConfig, + i18n: {localeConfigs}, + } = useDocusaurusContext(); + const alternatePageUtils = useAlternatePageUtils(); + const search = useHistorySelector((history) => history.location.search); + const hash = useHistorySelector((history) => history.location.hash); + + 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: 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 = '', + queryString, ...props }: Props): ReactNode { + const utils = useLocaleDropdownUtils(); + const { - i18n: {currentLocale, locales, localeConfigs}, + i18n: {currentLocale, locales}, } = 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({ - locale, - fullyQualified: false, - })}`; - // preserve ?search#hash suffix on locale switches - const to = `${baseTo}${search}${hash}${queryString}`; 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 ( { + 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(''); + }); +}); diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx b/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx index fb964b015fec..a16832a6489c 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx +++ b/packages/docusaurus-theme-common/src/utils/__tests__/useAlternatePageUtils.test.tsx @@ -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}) => ( - - {children} - - ), - }).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}) => ( + + {children} + + ), + }).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'); }); }); diff --git a/packages/docusaurus-theme-common/src/utils/historyUtils.ts b/packages/docusaurus-theme-common/src/utils/historyUtils.ts index eea45351d8b1..45f9cc22dd01 100644 --- a/packages/docusaurus-theme-common/src/utils/historyUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/historyUtils.ts @@ -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; +} diff --git a/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts b/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts index 073aa8956f3b..fd537163f1d9 100644 --- a/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts @@ -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}; diff --git a/packages/docusaurus-types/src/i18n.d.ts b/packages/docusaurus-types/src/i18n.d.ts index ea834788e5e6..5f1541a5b233 100644 --- a/packages/docusaurus-types/src/i18n.d.ts +++ b/packages/docusaurus-types/src/i18n.d.ts @@ -37,6 +37,25 @@ export type I18nLocaleConfig = { * By default, it will only be run if the `./i18n/` 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}//` + * + * Exception: when using the CLI with a single `--locale` parameter, the + * `//` path segment is not included. This is a better default for + * sites looking to deploy each locale to a different subdomain, such as + * `https://.docusaurus.io` + */ + baseUrl: string; }; export type I18nConfig = { diff --git a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts index 527aa2137994..778600164336 100644 --- a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts @@ -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 { diff --git a/packages/docusaurus-utils/src/i18nUtils.ts b/packages/docusaurus-utils/src/i18nUtils.ts index 8e7080b9620d..3c92381521e9 100644 --- a/packages/docusaurus-utils/src/i18nUtils.ts +++ b/packages/docusaurus-utils/src/i18nUtils.ts @@ -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 diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 74b66ca3a9b0..9370af8885da 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -33,7 +33,6 @@ export { mergeTranslations, updateTranslationFileMessages, getPluginI18nPath, - localizePath, getLocaleConfig, } from './i18nUtils'; export {mapAsyncSequential, findAsyncSequential} from './jsUtils'; diff --git a/packages/docusaurus/src/client/exports/ComponentCreator.tsx b/packages/docusaurus/src/client/exports/ComponentCreator.tsx index e0664bec5ebe..48d6640385b7 100644 --- a/packages/docusaurus/src/client/exports/ComponentCreator.tsx +++ b/packages/docusaurus/src/client/exports/ComponentCreator.tsx @@ -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]); diff --git a/packages/docusaurus/src/commands/build/build.ts b/packages/docusaurus/src/commands/build/build.ts index 4bfea69201b0..c3baf7940b99 100644 --- a/packages/docusaurus/src/commands/build/build.ts +++ b/packages/docusaurus/src/commands/build/build.ts @@ -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 & { 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; diff --git a/packages/docusaurus/src/commands/build/buildLocale.ts b/packages/docusaurus/src/commands/build/buildLocale.ts index 33ae8d346e42..8173991ba116 100644 --- a/packages/docusaurus/src/commands/build/buildLocale.ts +++ b/packages/docusaurus/src/commands/build/buildLocale.ts @@ -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), }), ); diff --git a/packages/docusaurus/src/commands/build/buildUtils.ts b/packages/docusaurus/src/commands/build/buildUtils.ts new file mode 100644 index 000000000000..4421c2646d89 --- /dev/null +++ b/packages/docusaurus/src/commands/build/buildUtils.ts @@ -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; +} diff --git a/packages/docusaurus/src/commands/start/utils.ts b/packages/docusaurus/src/commands/start/utils.ts index 4c2b3fdf155f..505c701a7f4b 100644 --- a/packages/docusaurus/src/commands/start/utils.ts +++ b/packages/docusaurus/src/commands/start/utils.ts @@ -90,7 +90,6 @@ async function createLoadSiteParams({ siteDir, config: cliOptions.config, locale: cliOptions.locale, - localizePath: undefined, // Should this be configurable? }; } diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap index d298a46eb83d..c1fea8b6ec11 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap @@ -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 diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap index 4b335e062b8e..b6b28fc9394d 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap @@ -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": "/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/i18n/en-custom", - "outDir": "/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/build", + "outDir": "/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", }, }, diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index aaad1d3baee1..815857d1ff02 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -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 { + 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, + ): Partial { + 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, @@ -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 + " + `); }); }); diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index fb075d8b2b97..d7af30b2241a 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -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: { diff --git a/packages/docusaurus/src/server/__tests__/site.test.ts b/packages/docusaurus/src/server/__tests__/site.test.ts index 7ce18bfa4d7d..e62d721035f6 100644 --- a/packages/docusaurus/src/server/__tests__/site.test.ts +++ b/packages/docusaurus/src/server/__tests__/site.test.ts @@ -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({ diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index d62e7b14c872..5f6059bfa663 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -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({ 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({ @@ -313,38 +341,13 @@ const FUTURE_CONFIG_SCHEMA = Joi.object({ .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({ - 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, diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 9d97140b3a76..9b0021ae8746 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -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 { +): Omit { 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 { const {i18n: i18nConfig} = config; @@ -123,7 +126,10 @@ Note: Docusaurus only support running one locale at a time.`; locale: string, ): Promise { const localeConfigInput = i18nConfig.localeConfigs[locale] ?? {}; - const localeConfig: Omit = { + 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, }; } diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts index a48e68dd8ad3..9668a13cd921 100644 --- a/packages/docusaurus/src/server/site.ts +++ b/packages/docusaurus/src/server/site.ts @@ -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 `//` with a `//` 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 // + // eventually including the // 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}); diff --git a/project-words.txt b/project-words.txt index 467a8d0a2da7..36c96b80c8eb 100644 --- a/project-words.txt +++ b/project-words.txt @@ -334,7 +334,6 @@ Unavatar unlinkable Unlisteds unlisteds -Unlocalized unlocalized unswizzle upvotes diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 351da0421a74..a1e5f1c09a20 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -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[].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 `//` +- For other locales, `baseUrl` will be `///` +- When building a single locale at a time (with `docusaurus build --locale `), `baseUrl` will be `//`, 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[].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/`). 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/` 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} diff --git a/website/docs/i18n/i18n-tutorial.mdx b/website/docs/i18n/i18n-tutorial.mdx index a88e2f0a388b..b76768fae579 100644 --- a/website/docs/i18n/i18n-tutorial.mdx +++ b/website/docs/i18n/i18n-tutorial.mdx @@ -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 `//` 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[].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 `` 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**. From 749b45e629bfc902955627c5bf0181e5aecb091b Mon Sep 17 00:00:00 2001 From: Weston Thayer Date: Mon, 28 Jul 2025 12:36:53 -0400 Subject: [PATCH 2/2] fix(theme): Add `aria-label` to `IconExternalLink` with value `'(opens in new tab)'` (#11331) Co-authored-by: sebastien --- .../src/theme/Icon/ExternalLink/index.tsx | 7 ++++++- .../locales/ar/theme-common.json | 1 + .../locales/base/theme-common.json | 2 ++ .../locales/bg/theme-common.json | 1 + .../locales/bn/theme-common.json | 1 + .../locales/cs/theme-common.json | 1 + .../locales/da/theme-common.json | 1 + .../locales/de/theme-common.json | 1 + .../locales/es/theme-common.json | 1 + .../locales/et/theme-common.json | 1 + .../locales/fa/theme-common.json | 1 + .../locales/fil/theme-common.json | 1 + .../locales/fr/theme-common.json | 1 + .../locales/he/theme-common.json | 1 + .../locales/hi/theme-common.json | 1 + .../locales/hu/theme-common.json | 1 + .../locales/id/theme-common.json | 1 + .../locales/is/theme-common.json | 1 + .../locales/it/theme-common.json | 1 + .../locales/ja/theme-common.json | 1 + .../locales/ko/theme-common.json | 1 + .../locales/nb/theme-common.json | 1 + .../locales/nl/theme-common.json | 1 + .../locales/pl/theme-common.json | 1 + .../locales/pt-BR/theme-common.json | 1 + .../locales/pt-PT/theme-common.json | 1 + .../locales/ru/theme-common.json | 1 + .../locales/sl/theme-common.json | 1 + .../locales/sr/theme-common.json | 1 + .../locales/sv/theme-common.json | 1 + .../locales/tk/theme-common.json | 1 + .../locales/tr/theme-common.json | 1 + .../locales/uk/theme-common.json | 1 + .../locales/vi/theme-common.json | 1 + .../locales/zh-Hans/theme-common.json | 1 + .../locales/zh-Hant/theme-common.json | 1 + 36 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-classic/src/theme/Icon/ExternalLink/index.tsx b/packages/docusaurus-theme-classic/src/theme/Icon/ExternalLink/index.tsx index 87523b6eece5..b899a1fee742 100644 --- a/packages/docusaurus-theme-classic/src/theme/Icon/ExternalLink/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Icon/ExternalLink/index.tsx @@ -6,6 +6,7 @@ */ import React, {type ReactNode} from 'react'; +import {translate} from '@docusaurus/Translate'; import type {Props} from '@theme/Icon/ExternalLink'; import styles from './styles.module.css'; @@ -22,7 +23,11 @@ export default function IconExternalLink({ diff --git a/packages/docusaurus-theme-translations/locales/ar/theme-common.json b/packages/docusaurus-theme-translations/locales/ar/theme-common.json index dab32c9c3199..fce1b0d37edd 100644 --- a/packages/docusaurus-theme-translations/locales/ar/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ar/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "هذه الصفحة لا تستجيب.", "theme.ErrorPageContent.tryAgain": "المحاولة مجددا", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "لم نتمكن من العثور على ما كنت تبحث عنه.", "theme.NotFound.p2": "يرجى الاتصال بمالك الموقع الذي ربطك بعنوان URL الأصلي وإخباره بأن الارتباط الخاص به معطل.", diff --git a/packages/docusaurus-theme-translations/locales/base/theme-common.json b/packages/docusaurus-theme-translations/locales/base/theme-common.json index b2762a0702ab..0efe3bd5c184 100644 --- a/packages/docusaurus-theme-translations/locales/base/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/base/theme-common.json @@ -19,6 +19,8 @@ "theme.ErrorPageContent.title___DESCRIPTION": "The title of the fallback page when the page crashed", "theme.ErrorPageContent.tryAgain": "Try again", "theme.ErrorPageContent.tryAgain___DESCRIPTION": "The label of the button to try again rendering when the React error boundary captures an error", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", + "theme.IconExternalLink.ariaLabel___DESCRIPTION": "The ARIA label for the external link icon", "theme.NavBar.navAriaLabel": "Main", "theme.NavBar.navAriaLabel___DESCRIPTION": "The ARIA label for the main navigation", "theme.NotFound.p1": "We could not find what you were looking for.", diff --git a/packages/docusaurus-theme-translations/locales/bg/theme-common.json b/packages/docusaurus-theme-translations/locales/bg/theme-common.json index 350fd3934c13..7461d6ed26bc 100644 --- a/packages/docusaurus-theme-translations/locales/bg/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/bg/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Разширяване на категорията'{label}'", "theme.ErrorPageContent.title": "Тази страница се срина.", "theme.ErrorPageContent.tryAgain": "Опитайте отново", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Основен", "theme.NotFound.p1": "Не успяхме да намерим това, което търсите.", "theme.NotFound.p2": "Моля, свържете се със собственика на сайта, който ви е свързал с оригиналния URL адрес, и ги уведомете, че връзката им е повредена.", diff --git a/packages/docusaurus-theme-translations/locales/bn/theme-common.json b/packages/docusaurus-theme-translations/locales/bn/theme-common.json index 918f8d243e0c..05b15f26d2f6 100644 --- a/packages/docusaurus-theme-translations/locales/bn/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/bn/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "আপনি যা খুঁজছিলেন তা আমরা খুঁজে পাইনি।", "theme.NotFound.p2": "দয়া করে সাইটের মালিকের সাথে যোগাযোগ করুন যা আপনাকে মূল URL এর সাথে যুক্ত করেছে এবং তাদের লিঙ্কটি ভাঙ্গা রয়েছে তা তাদের জানান।", diff --git a/packages/docusaurus-theme-translations/locales/cs/theme-common.json b/packages/docusaurus-theme-translations/locales/cs/theme-common.json index 809330a08452..5af85f9b8b81 100644 --- a/packages/docusaurus-theme-translations/locales/cs/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/cs/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Nepodařilo se nám najít co jste hledal(a).", "theme.NotFound.p2": "Kontaktujte prosím vlastníka webu, který vás odkázal na původní URL a upozorněte ho, že jejich odkaz nefunguje.", diff --git a/packages/docusaurus-theme-translations/locales/da/theme-common.json b/packages/docusaurus-theme-translations/locales/da/theme-common.json index 29901fd78b13..5ccc178a5f6c 100644 --- a/packages/docusaurus-theme-translations/locales/da/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/da/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Vi kunne ikke finde det, du søgte.", "theme.NotFound.p2": "Venligst kontakt ejeren til webstedet, som førte dig frem denne URL, og informer dem om at linket ikke virker.", diff --git a/packages/docusaurus-theme-translations/locales/de/theme-common.json b/packages/docusaurus-theme-translations/locales/de/theme-common.json index a4b6cd229022..b0fce33736f7 100644 --- a/packages/docusaurus-theme-translations/locales/de/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/de/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "Die Seite ist abgestürzt.", "theme.ErrorPageContent.tryAgain": "Nochmal versuchen", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.", "theme.NotFound.p2": "Bitte kontaktieren Sie den Besitzer der Seite, die Sie mit der ursprünglichen URL verlinkt hat, und teilen Sie ihm mit, dass der Link nicht mehr funktioniert.", diff --git a/packages/docusaurus-theme-translations/locales/es/theme-common.json b/packages/docusaurus-theme-translations/locales/es/theme-common.json index a9c0f5be499f..bfaa6b906adb 100644 --- a/packages/docusaurus-theme-translations/locales/es/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/es/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Ampliar la categoría '{label}' de la barra lateral", "theme.ErrorPageContent.title": "Esta página ha fallado.", "theme.ErrorPageContent.tryAgain": "Intente de nuevo", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Principal", "theme.NotFound.p1": "No pudimos encontrar lo que buscaba.", "theme.NotFound.p2": "Comuníquese con el dueño del sitio que le proporcionó la URL original y hágale saber que su vínculo está roto.", diff --git a/packages/docusaurus-theme-translations/locales/et/theme-common.json b/packages/docusaurus-theme-translations/locales/et/theme-common.json index adce580ff8dd..89e1055b67d8 100644 --- a/packages/docusaurus-theme-translations/locales/et/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/et/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Ava külgrea kategooria '{label}'", "theme.ErrorPageContent.title": "See leht jooksis kokku.", "theme.ErrorPageContent.tryAgain": "Proovi uuesti", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Peamine", "theme.NotFound.p1": "Vabandame, kuid lehte ei leitud.", "theme.NotFound.p2": "Kui arvad, et see on viga, palun võta meiega ühendust.", diff --git a/packages/docusaurus-theme-translations/locales/fa/theme-common.json b/packages/docusaurus-theme-translations/locales/fa/theme-common.json index 5166cc78e2a1..b4153a4832c8 100644 --- a/packages/docusaurus-theme-translations/locales/fa/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fa/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "باز کردن دسته بندی در نوار کناری '{label}'", "theme.ErrorPageContent.title": "بارگذاری صفحه با خطا روبرو شد.", "theme.ErrorPageContent.tryAgain": "تلاش مجدد", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "صفحه اصلی", "theme.NotFound.p1": "صفحه‌ای که دنبال آن بودید پیدا نشد.", "theme.NotFound.p2": "لطفا با صاحب وبسایت تماس بگیرید و ایشان را از مشکل پیش آمده مطلع کنید.", diff --git a/packages/docusaurus-theme-translations/locales/fil/theme-common.json b/packages/docusaurus-theme-translations/locales/fil/theme-common.json index 27de696ad954..04d89a095c56 100644 --- a/packages/docusaurus-theme-translations/locales/fil/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fil/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Hindi namin mahanap ang iyong hinananap.", "theme.NotFound.p2": "Mangyaring makipag-ugnayan sa may-ari ng site na nag-link sa iyo sa orihinal na URL at sabihin sa kanila na ang kanilang link ay putol.", diff --git a/packages/docusaurus-theme-translations/locales/fr/theme-common.json b/packages/docusaurus-theme-translations/locales/fr/theme-common.json index 598928765ccd..36037b5bffc8 100644 --- a/packages/docusaurus-theme-translations/locales/fr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/fr/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Développer la catégorie '{label}' de la barre latérale", "theme.ErrorPageContent.title": "Cette page a planté.", "theme.ErrorPageContent.tryAgain": "Réessayer", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Nous n'avons pas trouvé ce que vous recherchez.", "theme.NotFound.p2": "Veuillez contacter le propriétaire du site qui vous a lié à l'URL d'origine et leur faire savoir que leur lien est cassé.", diff --git a/packages/docusaurus-theme-translations/locales/he/theme-common.json b/packages/docusaurus-theme-translations/locales/he/theme-common.json index 87e0593b8bf1..5e4806e8c3a3 100644 --- a/packages/docusaurus-theme-translations/locales/he/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/he/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "אנחנו לא מוצאים את מה שאתה מנסה לחפש.", "theme.NotFound.p2": "הקישור אינו תקין, אנא פנה למנהל האתר ממנו קיבלת קישור זה.", diff --git a/packages/docusaurus-theme-translations/locales/hi/theme-common.json b/packages/docusaurus-theme-translations/locales/hi/theme-common.json index 5f5a4f5d35a1..8fe459cf0d27 100644 --- a/packages/docusaurus-theme-translations/locales/hi/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/hi/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "हमें वह नहीं मिला, जिसकी आपको तलाश थी।", "theme.NotFound.p2": "कृपया उस साइट के मालिक से संपर्क करें जिसने आपको मूल URL से जोड़ा है और उन्हें बताएं कि उनका लिंक टूट गया है।", diff --git a/packages/docusaurus-theme-translations/locales/hu/theme-common.json b/packages/docusaurus-theme-translations/locales/hu/theme-common.json index b01e4922d392..7d9514ea9961 100644 --- a/packages/docusaurus-theme-translations/locales/hu/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/hu/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "A(z) '{label}' nevű oldalsáv kategória kinyitása", "theme.ErrorPageContent.title": "Hiba történt a oldalon.", "theme.ErrorPageContent.tryAgain": "Próbáld újra", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Fő", "theme.NotFound.p1": "Sajnos nem találtuk azt az oldalt, amit kerestél.", "theme.NotFound.p2": "Kérjük, lépj kapcsolatba az oldal tulajdonosával, ahonnan erre a hivatkozásra léptél, hogy jelezd, hogy a hivatkozás nem működik.", diff --git a/packages/docusaurus-theme-translations/locales/id/theme-common.json b/packages/docusaurus-theme-translations/locales/id/theme-common.json index 454e94db8800..b228dcc5b6a0 100644 --- a/packages/docusaurus-theme-translations/locales/id/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/id/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Perluas kategori bilah sisi '{label}'", "theme.ErrorPageContent.title": "Terjadi kesalahan.", "theme.ErrorPageContent.tryAgain": "Coba kembali", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Utama", "theme.NotFound.p1": "Kami tak dapat menemukan yang anda cari.", "theme.NotFound.p2": "Silakan hubungi pemilik situs yang mengarahkan anda ke URL asli dan beri tahu mereka bahwa tautan mereka salah.", diff --git a/packages/docusaurus-theme-translations/locales/is/theme-common.json b/packages/docusaurus-theme-translations/locales/is/theme-common.json index 95e0a23f643f..9bc2e48265da 100644 --- a/packages/docusaurus-theme-translations/locales/is/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/is/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Víkka spássíu flokk '{label}'", "theme.ErrorPageContent.title": "Síðan hrundi.", "theme.ErrorPageContent.tryAgain": "Reyndu aftur", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Aðal", "theme.NotFound.p1": "Við fundum ekki það sem þú leitaðir að.", "theme.NotFound.p2": "Vinsamlegast hafðu samband við eiganda síðunnar sem sendi þig hingað og láttu hann vita að hlekkurinn er brotinn.", diff --git a/packages/docusaurus-theme-translations/locales/it/theme-common.json b/packages/docusaurus-theme-translations/locales/it/theme-common.json index 7babe752f174..dc35eb97c873 100644 --- a/packages/docusaurus-theme-translations/locales/it/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/it/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "Questa pagina è andata in crash.", "theme.ErrorPageContent.tryAgain": "Prova di nuovo", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Principale", "theme.NotFound.p1": "Non siamo riusciti a trovare quello che stavi cercando.", "theme.NotFound.p2": "Contatta il proprietario del sito che ti ha collegato all'URL originale e fagli sapere che il loro collegamento è interrotto.", diff --git a/packages/docusaurus-theme-translations/locales/ja/theme-common.json b/packages/docusaurus-theme-translations/locales/ja/theme-common.json index baa3964a640d..577f6a48315e 100644 --- a/packages/docusaurus-theme-translations/locales/ja/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ja/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "'{label}'の目次を開く", "theme.ErrorPageContent.title": "エラーが発生しました", "theme.ErrorPageContent.tryAgain": "もう一度試してください", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "ナビゲーション", "theme.NotFound.p1": "お探しのページが見つかりませんでした", "theme.NotFound.p2": "このページにリンクしているサイトの所有者にリンクが壊れていることを伝えてください", diff --git a/packages/docusaurus-theme-translations/locales/ko/theme-common.json b/packages/docusaurus-theme-translations/locales/ko/theme-common.json index 0eefc6ea49c5..0fc6e1466549 100644 --- a/packages/docusaurus-theme-translations/locales/ko/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ko/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "사이드바 분류 '{label}' 펼치기", "theme.ErrorPageContent.title": "페이지에 오류가 발생하였습니다.", "theme.ErrorPageContent.tryAgain": "다시 시도해 보세요", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "메인", "theme.NotFound.p1": "원하는 페이지를 찾을 수 없습니다.", "theme.NotFound.p2": "사이트 관리자에게 링크가 깨진 것을 알려주세요.", diff --git a/packages/docusaurus-theme-translations/locales/nb/theme-common.json b/packages/docusaurus-theme-translations/locales/nb/theme-common.json index 269df3c551d7..97d7c3eb77b7 100644 --- a/packages/docusaurus-theme-translations/locales/nb/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/nb/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "Denne siden krasjet.", "theme.ErrorPageContent.tryAgain": "Prøv på nytt", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Hoved", "theme.NotFound.p1": "Vi kunne ikke finne det du lette etter.", "theme.NotFound.p2": "Kontakt eieren av nettstedet som koblet deg til den opprinnelige nettadressen og la dem få vite at koblingen deres er ødelagt.", diff --git a/packages/docusaurus-theme-translations/locales/nl/theme-common.json b/packages/docusaurus-theme-translations/locales/nl/theme-common.json index 706e70871f5d..8059850dffa5 100644 --- a/packages/docusaurus-theme-translations/locales/nl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/nl/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Categorie zijbalk uitklappen '{label}'", "theme.ErrorPageContent.title": "Deze pagina is gecrasht.", "theme.ErrorPageContent.tryAgain": "Probeer opnieuw", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "We kunnen niet vinden waar je naar op zoek bent.", "theme.NotFound.p2": "Neem contact op met de eigenaar van de website die naar de originele URL heeft geleid en laat weten dat de link niet meer werkt.", diff --git a/packages/docusaurus-theme-translations/locales/pl/theme-common.json b/packages/docusaurus-theme-translations/locales/pl/theme-common.json index 3a6f707943eb..5a74b63573f2 100644 --- a/packages/docusaurus-theme-translations/locales/pl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pl/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "Ta strona uległa awarii.", "theme.ErrorPageContent.tryAgain": "Spróbuj ponownie", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Nie mogliśmy znaleźć strony której szukasz.", "theme.NotFound.p2": "Proszę skontaktuj się z właścielem strony, z której link doprowadził Cię tutaj i poinformuj go, że link jest nieprawidłowy.", diff --git a/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json b/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json index d848584d664c..5d39829d8df6 100644 --- a/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-BR/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expandir a categoria '{label}'", "theme.ErrorPageContent.title": "Esta página deu erro.", "theme.ErrorPageContent.tryAgain": "Tentar novamente", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Navegação principal", "theme.NotFound.p1": "Não foi possível encontrar o que você está procurando.", "theme.NotFound.p2": "Por favor, entre em contato com o dono do site que ligou você à URL original e informe que o link está quebrado.", diff --git a/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json b/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json index 9904d6c17751..af7fff8e742a 100644 --- a/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/pt-PT/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Não foi possível encontrar o que procura.", "theme.NotFound.p2": "Por favor, contacte o proprietário do site que o trouxe aqui e informe-lhe que o link está partido.", diff --git a/packages/docusaurus-theme-translations/locales/ru/theme-common.json b/packages/docusaurus-theme-translations/locales/ru/theme-common.json index e432ddb2c0a7..0dbeaf6aedd3 100644 --- a/packages/docusaurus-theme-translations/locales/ru/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/ru/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "На странице произошёл сбой.", "theme.ErrorPageContent.tryAgain": "Попробуйте ещё раз", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "К сожалению, мы не смогли найти запрашиваемую вами страницу.", "theme.NotFound.p2": "Пожалуйста, обратитесь к владельцу сайта, с которого вы перешли на эту ссылку, чтобы сообщить ему, что ссылка не работает.", diff --git a/packages/docusaurus-theme-translations/locales/sl/theme-common.json b/packages/docusaurus-theme-translations/locales/sl/theme-common.json index ecec838635d8..6cdefa9bd1f2 100644 --- a/packages/docusaurus-theme-translations/locales/sl/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sl/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Razširite kategorijo stranske vrstice '{label}'", "theme.ErrorPageContent.title": "Ta stran se je zrušila.", "theme.ErrorPageContent.tryAgain": "Poskusite ponovno", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Glavna navigacija", "theme.NotFound.p1": "Iskane strani ni bilo možno najti", "theme.NotFound.p2": "Prosim kontaktirajte lastnike strani, s katere ste sledili povezavi in jih obvestite, da povezava ne deluje.", diff --git a/packages/docusaurus-theme-translations/locales/sr/theme-common.json b/packages/docusaurus-theme-translations/locales/sr/theme-common.json index 8bbe9459bb7e..82b658edb1cd 100644 --- a/packages/docusaurus-theme-translations/locales/sr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sr/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "This page crashed.", "theme.ErrorPageContent.tryAgain": "Try again", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Тражени резултат не постоји.", "theme.NotFound.p2": "Молимо вас да контактирате власника сајта који вас је упутио овде и обавестите га да је њихова веза нетачна.", diff --git a/packages/docusaurus-theme-translations/locales/sv/theme-common.json b/packages/docusaurus-theme-translations/locales/sv/theme-common.json index c37e11ac9500..201816a0922f 100644 --- a/packages/docusaurus-theme-translations/locales/sv/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/sv/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Expand sidebar category '{label}'", "theme.ErrorPageContent.title": "Denna sida har kraschat.", "theme.ErrorPageContent.tryAgain": "Försök igen", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Main", "theme.NotFound.p1": "Vi kunde inte hitta det du letade efter.", "theme.NotFound.p2": "Vänligen kontakta ägaren av webbplatsen som länkade dig till den ursprungliga webbadressen och låt dem veta att denna länk är trasig.", diff --git a/packages/docusaurus-theme-translations/locales/tk/theme-common.json b/packages/docusaurus-theme-translations/locales/tk/theme-common.json index 406db5528ad5..7c4558bff143 100644 --- a/packages/docusaurus-theme-translations/locales/tk/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/tk/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Gapdal paneli görkez'{label}'", "theme.ErrorPageContent.title": "Sahypada näsazlyk ýüze çykdy.", "theme.ErrorPageContent.tryAgain": "Gaýtadan synanyşyň", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Esasy", "theme.NotFound.p1": "Gynansakda, ýüzlenen sahypaňyz tapylmady", "theme.NotFound.p2": "Web sahypanyň dolandyryjylaryna habar bermegiňizi sizden haýyş edýäris", diff --git a/packages/docusaurus-theme-translations/locales/tr/theme-common.json b/packages/docusaurus-theme-translations/locales/tr/theme-common.json index de75f5249859..433b7d0c6775 100644 --- a/packages/docusaurus-theme-translations/locales/tr/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/tr/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Kenar çubuğu kategorisini genişlet '{label}'", "theme.ErrorPageContent.title": "Bu sayfa çöktü.", "theme.ErrorPageContent.tryAgain": "Tekrar deneyin", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Ana menü", "theme.NotFound.p1": "Aradığınız şeyi bulamadık.", "theme.NotFound.p2": "Lütfen sizi orijinal URL'ye yönlendiren sitenin sahibiyle iletişime geçin ve bağlantısının bozuk olduğunu bildirin.", diff --git a/packages/docusaurus-theme-translations/locales/uk/theme-common.json b/packages/docusaurus-theme-translations/locales/uk/theme-common.json index 72ee02cdf7fb..759ca2716fd7 100644 --- a/packages/docusaurus-theme-translations/locales/uk/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/uk/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Розгорнути категорію в сайдбарі '{label}'", "theme.ErrorPageContent.title": "На сторінці стався збій.", "theme.ErrorPageContent.tryAgain": "Спробуйте ще раз", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Головна", "theme.NotFound.p1": "На жаль, ми не змогли знайти сторінку, яку ви запитували.", "theme.NotFound.p2": "Будь ласка, зверніться до власника сайту, з якого ви перейшли на це посилання, щоб повідомити, що посилання не працює.", diff --git a/packages/docusaurus-theme-translations/locales/vi/theme-common.json b/packages/docusaurus-theme-translations/locales/vi/theme-common.json index 33994fd5e2f8..04ba9c6b754d 100644 --- a/packages/docusaurus-theme-translations/locales/vi/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/vi/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "Xem thêm danh mục '{label}'", "theme.ErrorPageContent.title": "Trang này đã gặp lỗi.", "theme.ErrorPageContent.tryAgain": "Thử lại", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "Thanh điều hướng", "theme.NotFound.p1": "Chúng tôi không thể tìm thấy nội dung bạn đang tìm kiếm.", "theme.NotFound.p2": "Vui lòng liên hệ với trang web đã đưa bạn đến đây và thông báo rằng đường dẫn này bị lỗi.", diff --git a/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json b/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json index f9e580a264a7..0fc0dfd17d02 100644 --- a/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/zh-Hans/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "展开侧边栏分类 '{label}'", "theme.ErrorPageContent.title": "页面已崩溃。", "theme.ErrorPageContent.tryAgain": "重试", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "主导航", "theme.NotFound.p1": "我们找不到您要找的页面。", "theme.NotFound.p2": "请联系原始链接来源网站的所有者,并告知他们链接已损坏。", diff --git a/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json b/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json index 1ba88ded2775..4ec1bbf99e1b 100644 --- a/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json +++ b/packages/docusaurus-theme-translations/locales/zh-Hant/theme-common.json @@ -9,6 +9,7 @@ "theme.DocSidebarItem.expandCategoryAriaLabel": "展開側邊欄分類 '{label}'", "theme.ErrorPageContent.title": "此頁已當機。", "theme.ErrorPageContent.tryAgain": "重試", + "theme.IconExternalLink.ariaLabel": "(opens in new tab)", "theme.NavBar.navAriaLabel": "主導航", "theme.NotFound.p1": "我們沒有您要找的頁面。", "theme.NotFound.p2": "請聯絡原始連結來源網站的所有者,並通知他們連結已毀損。",