mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 23:57:22 +02:00
fix(v2): fix/enhance minor i18n issues reported (#4092)
* fix comment * allow to pass custom classname in navbar items * Add IconLanguage comp to dropdown * do not trim htmlLang * Add initial hreflang SEO support * doc hreflang
This commit is contained in:
parent
8a934ac9b7
commit
869ebe7b53
12 changed files with 158 additions and 32 deletions
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import {Props} from '@theme/IconLanguage';
|
||||||
|
|
||||||
|
const IconLanguage = ({
|
||||||
|
width = 20,
|
||||||
|
height = 20,
|
||||||
|
...props
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
{...props}>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M19.753 10.909c-.624-1.707-2.366-2.726-4.661-2.726-.09 0-.176.002-.262.006l-.016-2.063 3.525-.607c.115-.019.133-.119.109-.231-.023-.111-.167-.883-.188-.976-.027-.131-.102-.127-.207-.109-.104.018-3.25.461-3.25.461l-.013-2.078c-.001-.125-.069-.158-.194-.156l-1.025.016c-.105.002-.164.049-.162.148l.033 2.307s-3.061.527-3.144.543c-.084.014-.17.053-.151.143.019.09.19 1.094.208 1.172.018.08.072.129.188.107l2.924-.504.035 2.018c-1.077.281-1.801.824-2.256 1.303-.768.807-1.207 1.887-1.207 2.963 0 1.586.971 2.529 2.328 2.695 3.162.387 5.119-3.06 5.769-4.715 1.097 1.506.256 4.354-2.094 5.98-.043.029-.098.129-.033.207l.619.756c.08.096.206.059.256.023 2.51-1.73 3.661-4.515 2.869-6.683zm-7.386 3.188c-.966-.121-.944-.914-.944-1.453 0-.773.327-1.58.876-2.156a3.21 3.21 0 011.229-.799l.082 4.277a2.773 2.773 0 01-1.243.131zm2.427-.553l.046-4.109c.084-.004.166-.01.252-.01.773 0 1.494.145 1.885.361.391.217-1.023 2.713-2.183 3.758zm-8.95-7.668a.196.196 0 00-.196-.145h-1.95a.194.194 0 00-.194.144L.008 16.916c-.017.051-.011.076.062.076h1.733c.075 0 .099-.023.114-.072l1.008-3.318h3.496l1.008 3.318c.016.049.039.072.113.072h1.734c.072 0 .078-.025.062-.076-.014-.05-3.083-9.741-3.494-11.04zm-2.618 6.318l1.447-5.25 1.447 5.25H3.226z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconLanguage;
|
|
@ -11,7 +11,36 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import type {Props} from '@theme/Layout';
|
import type {Props} from '@theme/Layout';
|
||||||
import SearchMetadatas from '@theme/SearchMetadatas';
|
import SearchMetadatas from '@theme/SearchMetadatas';
|
||||||
import {DEFAULT_SEARCH_TAG, useTitleFormatter} from '@docusaurus/theme-common';
|
import {
|
||||||
|
DEFAULT_SEARCH_TAG,
|
||||||
|
useTitleFormatter,
|
||||||
|
useAlternatePageUtils,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
|
// Useful for SEO
|
||||||
|
// See https://developers.google.com/search/docs/advanced/crawling/localized-versions
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/3317
|
||||||
|
function AlternateLangHeaders(): JSX.Element {
|
||||||
|
const {
|
||||||
|
i18n: {defaultLocale, locales},
|
||||||
|
} = useDocusaurusContext();
|
||||||
|
const alternatePageUtils = useAlternatePageUtils();
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
{locales.map((locale) => (
|
||||||
|
<link
|
||||||
|
key={locale}
|
||||||
|
rel="alternate"
|
||||||
|
href={alternatePageUtils.createUrl({
|
||||||
|
locale,
|
||||||
|
fullyQualified: true,
|
||||||
|
})}
|
||||||
|
hrefLang={locale === defaultLocale ? 'x-default' : locale}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function LayoutHead(props: Props): JSX.Element {
|
export default function LayoutHead(props: Props): JSX.Element {
|
||||||
const {
|
const {
|
||||||
|
@ -36,7 +65,10 @@ export default function LayoutHead(props: Props): JSX.Element {
|
||||||
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
||||||
const faviconUrl = useBaseUrl(favicon);
|
const faviconUrl = useBaseUrl(favicon);
|
||||||
|
|
||||||
const htmlLang = currentLocale.split('-')[0];
|
// See https://github.com/facebook/docusaurus/issues/3317#issuecomment-754661855
|
||||||
|
// const htmlLang = currentLocale.split('-')[0];
|
||||||
|
const htmlLang = currentLocale; // should we allow the user to override htmlLang with localeConfig?
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -61,6 +93,8 @@ export default function LayoutHead(props: Props): JSX.Element {
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<AlternateLangHeaders />
|
||||||
|
|
||||||
<SearchMetadatas
|
<SearchMetadatas
|
||||||
tag={DEFAULT_SEARCH_TAG}
|
tag={DEFAULT_SEARCH_TAG}
|
||||||
locale={currentLocale}
|
locale={currentLocale}
|
||||||
|
|
|
@ -119,7 +119,7 @@ function NavItemDesktop({
|
||||||
setShowDropdown(!showDropdown);
|
setShowDropdown(!showDropdown);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{props.label}
|
{props.children ?? props.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<ul ref={dropdownMenuRef} className="dropdown__menu">
|
<ul ref={dropdownMenuRef} className="dropdown__menu">
|
||||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||||
|
@ -195,7 +195,7 @@ function NavItemMobile({
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollapsed((state) => !state);
|
setCollapsed((state) => !state);
|
||||||
}}>
|
}}>
|
||||||
{props.label}
|
{props.children ?? props.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<ul
|
<ul
|
||||||
className="menu__list"
|
className="menu__list"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
import {useLatestVersion, useActiveDocContext} from '@theme/hooks/useDocs';
|
import {useLatestVersion, useActiveDocContext} from '@theme/hooks/useDocs';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
|
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
import {
|
import {
|
||||||
useVersions,
|
useVersions,
|
||||||
useLatestVersion,
|
useLatestVersion,
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
|
import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
|
||||||
import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem';
|
import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||||
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
|
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DefaultNavbarItem from './DefaultNavbarItem';
|
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
|
||||||
|
import IconLanguage from '@theme/IconLanguage';
|
||||||
import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import {useLocation} from '@docusaurus/router';
|
import {useAlternatePageUtils} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
export default function LocaleDropdownNavbarItem({
|
export default function LocaleDropdownNavbarItem({
|
||||||
mobile,
|
mobile,
|
||||||
|
@ -18,35 +19,23 @@ export default function LocaleDropdownNavbarItem({
|
||||||
...props
|
...props
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const {
|
const {
|
||||||
siteConfig: {baseUrl},
|
i18n: {currentLocale, locales, localeConfigs},
|
||||||
i18n: {defaultLocale, currentLocale, locales, localeConfigs},
|
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
const {pathname} = useLocation();
|
const alternatePageUtils = useAlternatePageUtils();
|
||||||
|
|
||||||
function getLocaleLabel(locale) {
|
function getLocaleLabel(locale) {
|
||||||
return localeConfigs[locale].label;
|
return localeConfigs[locale].label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Docusaurus expose this unlocalized baseUrl more reliably
|
|
||||||
const baseUrlUnlocalized =
|
|
||||||
currentLocale === defaultLocale
|
|
||||||
? baseUrl
|
|
||||||
: baseUrl.replace(`/${currentLocale}/`, '/');
|
|
||||||
|
|
||||||
const pathnameSuffix = pathname.replace(baseUrl, '');
|
|
||||||
|
|
||||||
function getLocalizedBaseUrl(locale) {
|
|
||||||
return locale === defaultLocale
|
|
||||||
? `${baseUrlUnlocalized}`
|
|
||||||
: `${baseUrlUnlocalized}${locale}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localeItems = locales.map((locale) => {
|
const localeItems = locales.map((locale) => {
|
||||||
const to = `${getLocalizedBaseUrl(locale)}${pathnameSuffix}`;
|
const to = `pathname://${alternatePageUtils.createUrl({
|
||||||
|
locale,
|
||||||
|
fullyQualified: false,
|
||||||
|
})}`;
|
||||||
return {
|
return {
|
||||||
isNavLink: true,
|
isNavLink: true,
|
||||||
label: getLocaleLabel(locale),
|
label: getLocaleLabel(locale),
|
||||||
to: `pathname://${to}`,
|
to,
|
||||||
target: '_self',
|
target: '_self',
|
||||||
autoAddBaseUrl: false,
|
autoAddBaseUrl: false,
|
||||||
className: locale === currentLocale ? 'dropdown__link--active' : '',
|
className: locale === currentLocale ? 'dropdown__link--active' : '',
|
||||||
|
@ -62,7 +51,14 @@ export default function LocaleDropdownNavbarItem({
|
||||||
<DefaultNavbarItem
|
<DefaultNavbarItem
|
||||||
{...props}
|
{...props}
|
||||||
mobile={mobile}
|
mobile={mobile}
|
||||||
label={dropdownLabel}
|
label={
|
||||||
|
<span>
|
||||||
|
<IconLanguage
|
||||||
|
style={{verticalAlign: 'text-bottom', marginRight: 5}}
|
||||||
|
/>
|
||||||
|
<span>{dropdownLabel}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
items={items}
|
items={items}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
13
packages/docusaurus-theme-classic/src/types.d.ts
vendored
13
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -276,7 +276,7 @@ declare module '@theme/Navbar' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps, ReactNode} from 'react';
|
||||||
|
|
||||||
export type NavLinkProps = {
|
export type NavLinkProps = {
|
||||||
activeBasePath?: string;
|
activeBasePath?: string;
|
||||||
|
@ -284,7 +284,7 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||||
to?: string;
|
to?: string;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
href?: string;
|
href?: string;
|
||||||
label?: string;
|
label?: ReactNode;
|
||||||
activeClassName?: string;
|
activeClassName?: string;
|
||||||
prependBaseUrlToHref?: string;
|
prependBaseUrlToHref?: string;
|
||||||
isActive?: () => boolean;
|
isActive?: () => boolean;
|
||||||
|
@ -529,3 +529,12 @@ declare module '@theme/IconMenu' {
|
||||||
const IconMenu: (props: Props) => JSX.Element;
|
const IconMenu: (props: Props) => JSX.Element;
|
||||||
export default IconMenu;
|
export default IconMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/IconLanguage' {
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
|
|
||||||
|
export type Props = ComponentProps<'svg'>;
|
||||||
|
|
||||||
|
const IconLanguage: (props: Props) => JSX.Element;
|
||||||
|
export default IconLanguage;
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ const DocsVersionNavbarItemSchema = Joi.object({
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
to: Joi.string(),
|
to: Joi.string(),
|
||||||
docsPluginId: Joi.string(),
|
docsPluginId: Joi.string(),
|
||||||
|
className: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||||
|
@ -85,6 +86,7 @@ const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||||
dropdownActiveClassDisabled: Joi.boolean(),
|
dropdownActiveClassDisabled: Joi.boolean(),
|
||||||
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
|
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
|
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
|
className: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const DocItemSchema = Joi.object({
|
const DocItemSchema = Joi.object({
|
||||||
|
@ -94,6 +96,7 @@ const DocItemSchema = Joi.object({
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
docsPluginId: Joi.string(),
|
docsPluginId: Joi.string(),
|
||||||
activeSidebarClassName: Joi.string().default('navbar__link--active'),
|
activeSidebarClassName: Joi.string().default('navbar__link--active'),
|
||||||
|
className: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const LocaleDropdownNavbarItemSchema = Joi.object({
|
const LocaleDropdownNavbarItemSchema = Joi.object({
|
||||||
|
@ -101,6 +104,7 @@ const LocaleDropdownNavbarItemSchema = Joi.object({
|
||||||
position: NavbarItemPosition,
|
position: NavbarItemPosition,
|
||||||
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
|
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
|
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
|
className: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Can this be made easier? :/
|
// Can this be made easier? :/
|
||||||
|
|
|
@ -16,6 +16,8 @@ export {
|
||||||
FooterLinkItem,
|
FooterLinkItem,
|
||||||
} from './utils/useThemeConfig';
|
} from './utils/useThemeConfig';
|
||||||
|
|
||||||
|
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
||||||
|
|
||||||
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||||
|
|
||||||
export {isDocsPluginEnabled} from './utils/docsUtils';
|
export {isDocsPluginEnabled} from './utils/docsUtils';
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* 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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import {useLocation} from '@docusaurus/router';
|
||||||
|
|
||||||
|
// Permits to obtain the url of the current page in another locale
|
||||||
|
// Useful to generate hreflang meta headers etc...
|
||||||
|
// See https://developers.google.com/search/docs/advanced/crawling/localized-versions
|
||||||
|
export function useAlternatePageUtils() {
|
||||||
|
const {
|
||||||
|
siteConfig: {baseUrl, url},
|
||||||
|
i18n: {defaultLocale, currentLocale},
|
||||||
|
} = useDocusaurusContext();
|
||||||
|
const {pathname} = useLocation();
|
||||||
|
|
||||||
|
const baseUrlUnlocalized =
|
||||||
|
currentLocale === defaultLocale
|
||||||
|
? baseUrl
|
||||||
|
: baseUrl.replace(`/${currentLocale}/`, '/');
|
||||||
|
|
||||||
|
const pathnameSuffix = pathname.replace(baseUrl, '');
|
||||||
|
|
||||||
|
function getLocalizedBaseUrl(locale: string) {
|
||||||
|
return locale === defaultLocale
|
||||||
|
? `${baseUrlUnlocalized}`
|
||||||
|
: `${baseUrlUnlocalized}${locale}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO support correct alternate url when localized site is deployed on another domain
|
||||||
|
function createUrl({
|
||||||
|
locale,
|
||||||
|
fullyQualified,
|
||||||
|
}: {
|
||||||
|
locale: string;
|
||||||
|
// For hreflang SEO headers, we need it to be fully qualified (full protocol/domain/path...)
|
||||||
|
// For locale dropdown, using a path is good enough
|
||||||
|
fullyQualified: boolean;
|
||||||
|
}) {
|
||||||
|
return `${fullyQualified ? url : ''}${getLocalizedBaseUrl(
|
||||||
|
locale,
|
||||||
|
)}${pathnameSuffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {createUrl};
|
||||||
|
}
|
|
@ -32,12 +32,12 @@ The goals of the Docusaurus i18n system are:
|
||||||
- **Localize assets**: an image of your site might contain text that should be translated.
|
- **Localize assets**: an image of your site might contain text that should be translated.
|
||||||
- **No coupling**: not forced to use any SaaS, yet the integration is possible.
|
- **No coupling**: not forced to use any SaaS, yet the integration is possible.
|
||||||
- **Easy to use with [Crowdin](http://crowdin.com/)**: multiple Docusaurus v1 sites use Crowdin, and should be able to migrate to v2.
|
- **Easy to use with [Crowdin](http://crowdin.com/)**: multiple Docusaurus v1 sites use Crowdin, and should be able to migrate to v2.
|
||||||
|
- **Good SEO defaults**: setting useful SEO headers like [`hreflang`](https://developers.google.com/search/docs/advanced/crawling/localized-versions) for you.
|
||||||
|
|
||||||
### i18n goals (TODO)
|
### i18n goals (TODO)
|
||||||
|
|
||||||
Features that are **not yet implemented**:
|
Features that are **not yet implemented**:
|
||||||
|
|
||||||
- **Good SEO defaults**: setting useful html meta headers like `hreflang` for you.
|
|
||||||
- **RTL support**: one locale should not be harder to use than another.
|
- **RTL support**: one locale should not be harder to use than another.
|
||||||
- **Contextual translations**: reduce friction to contribute to the translation effort.
|
- **Contextual translations**: reduce friction to contribute to the translation effort.
|
||||||
- **Anchor links**: linking should not break when you localize headings.
|
- **Anchor links**: linking should not break when you localize headings.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue