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:
Sébastien Lorber 2021-01-22 21:26:42 +01:00 committed by GitHub
parent 8a934ac9b7
commit 869ebe7b53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 158 additions and 32 deletions

View file

@ -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;

View file

@ -11,7 +11,36 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import type {Props} from '@theme/Layout';
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 {
const {
@ -36,7 +65,10 @@ export default function LayoutHead(props: Props): JSX.Element {
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
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 (
<>
<Head>
@ -61,6 +93,8 @@ export default function LayoutHead(props: Props): JSX.Element {
<meta name="twitter:card" content="summary_large_image" />
</Head>
<AlternateLangHeaders />
<SearchMetadatas
tag={DEFAULT_SEARCH_TAG}
locale={currentLocale}

View file

@ -119,7 +119,7 @@ function NavItemDesktop({
setShowDropdown(!showDropdown);
}
}}>
{props.label}
{props.children ?? props.label}
</NavLink>
<ul ref={dropdownMenuRef} className="dropdown__menu">
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
@ -195,7 +195,7 @@ function NavItemMobile({
onClick={() => {
setCollapsed((state) => !state);
}}>
{props.label}
{props.children ?? props.label}
</NavLink>
<ul
className="menu__list"

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import DefaultNavbarItem from './DefaultNavbarItem';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import {useLatestVersion, useActiveDocContext} from '@theme/hooks/useDocs';
import clsx from 'clsx';
import type {Props} from '@theme/NavbarItem/DocNavbarItem';

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import DefaultNavbarItem from './DefaultNavbarItem';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import {
useVersions,
useLatestVersion,

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import DefaultNavbarItem from './DefaultNavbarItem';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
import type {Props} from '@theme/NavbarItem/DocsVersionNavbarItem';
import {useDocsPreferredVersion} from '@docusaurus/theme-common';

View file

@ -6,10 +6,11 @@
*/
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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useLocation} from '@docusaurus/router';
import {useAlternatePageUtils} from '@docusaurus/theme-common';
export default function LocaleDropdownNavbarItem({
mobile,
@ -18,35 +19,23 @@ export default function LocaleDropdownNavbarItem({
...props
}: Props): JSX.Element {
const {
siteConfig: {baseUrl},
i18n: {defaultLocale, currentLocale, locales, localeConfigs},
i18n: {currentLocale, locales, localeConfigs},
} = useDocusaurusContext();
const {pathname} = useLocation();
const alternatePageUtils = useAlternatePageUtils();
function getLocaleLabel(locale) {
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 to = `${getLocalizedBaseUrl(locale)}${pathnameSuffix}`;
const to = `pathname://${alternatePageUtils.createUrl({
locale,
fullyQualified: false,
})}`;
return {
isNavLink: true,
label: getLocaleLabel(locale),
to: `pathname://${to}`,
to,
target: '_self',
autoAddBaseUrl: false,
className: locale === currentLocale ? 'dropdown__link--active' : '',
@ -62,7 +51,14 @@ export default function LocaleDropdownNavbarItem({
<DefaultNavbarItem
{...props}
mobile={mobile}
label={dropdownLabel}
label={
<span>
<IconLanguage
style={{verticalAlign: 'text-bottom', marginRight: 5}}
/>
<span>{dropdownLabel}</span>
</span>
}
items={items}
/>
);

View file

@ -276,7 +276,7 @@ declare module '@theme/Navbar' {
}
declare module '@theme/NavbarItem/DefaultNavbarItem' {
import type {ComponentProps} from 'react';
import type {ComponentProps, ReactNode} from 'react';
export type NavLinkProps = {
activeBasePath?: string;
@ -284,7 +284,7 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' {
to?: string;
exact?: boolean;
href?: string;
label?: string;
label?: ReactNode;
activeClassName?: string;
prependBaseUrlToHref?: string;
isActive?: () => boolean;
@ -529,3 +529,12 @@ declare module '@theme/IconMenu' {
const IconMenu: (props: Props) => JSX.Element;
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;
}

View file

@ -76,6 +76,7 @@ const DocsVersionNavbarItemSchema = Joi.object({
label: Joi.string(),
to: Joi.string(),
docsPluginId: Joi.string(),
className: Joi.string(),
});
const DocsVersionDropdownNavbarItemSchema = Joi.object({
@ -85,6 +86,7 @@ const DocsVersionDropdownNavbarItemSchema = Joi.object({
dropdownActiveClassDisabled: Joi.boolean(),
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
className: Joi.string(),
});
const DocItemSchema = Joi.object({
@ -94,6 +96,7 @@ const DocItemSchema = Joi.object({
label: Joi.string(),
docsPluginId: Joi.string(),
activeSidebarClassName: Joi.string().default('navbar__link--active'),
className: Joi.string(),
});
const LocaleDropdownNavbarItemSchema = Joi.object({
@ -101,6 +104,7 @@ const LocaleDropdownNavbarItemSchema = Joi.object({
position: NavbarItemPosition,
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
className: Joi.string(),
});
// Can this be made easier? :/

View file

@ -16,6 +16,8 @@ export {
FooterLinkItem,
} from './utils/useThemeConfig';
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
export {isDocsPluginEnabled} from './utils/docsUtils';

View file

@ -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};
}

View file

@ -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.
- **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.
- **Good SEO defaults**: setting useful SEO headers like [`hreflang`](https://developers.google.com/search/docs/advanced/crawling/localized-versions) for you.
### i18n goals (TODO)
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.
- **Contextual translations**: reduce friction to contribute to the translation effort.
- **Anchor links**: linking should not break when you localize headings.