feat(v2): Extract/translate hardcoded labels from classic theme (#4168)

* Translate theme hardcoded strings

* improve test
This commit is contained in:
Sébastien Lorber 2021-02-03 20:06:26 +01:00 committed by GitHub
parent 823d0fe3c2
commit ab7951571e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 217 additions and 60 deletions

View file

@ -7,6 +7,7 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Metadata} from '@theme/BlogListPage'; import type {Metadata} from '@theme/BlogListPage';
function BlogListPaginator(props: {readonly metadata: Metadata}): JSX.Element { function BlogListPaginator(props: {readonly metadata: Metadata}): JSX.Element {
@ -18,14 +19,28 @@ function BlogListPaginator(props: {readonly metadata: Metadata}): JSX.Element {
<div className="pagination-nav__item"> <div className="pagination-nav__item">
{previousPage && ( {previousPage && (
<Link className="pagination-nav__link" to={previousPage}> <Link className="pagination-nav__link" to={previousPage}>
<div className="pagination-nav__label">&laquo; Newer Entries</div> <div className="pagination-nav__label">
&laquo;{' '}
<Translate
id="theme.BlogListPaginator.newerEntries"
description="The label used to navigate to the newer blog posts page (previous page)">
Newer Entries
</Translate>
</div>
</Link> </Link>
)} )}
</div> </div>
<div className="pagination-nav__item pagination-nav__item--next"> <div className="pagination-nav__item pagination-nav__item--next">
{nextPage && ( {nextPage && (
<Link className="pagination-nav__link" to={nextPage}> <Link className="pagination-nav__link" to={nextPage}>
<div className="pagination-nav__label">Older Entries &raquo;</div> <div className="pagination-nav__label">
<Translate
id="theme.BlogListPaginator.olderEntries"
description="The label used to navigate to the older blog posts page (next page)">
Older Entries
</Translate>{' '}
&raquo;
</div>
</Link> </Link>
)} )}
</div> </div>

View file

@ -8,7 +8,7 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import {MDXProvider} from '@mdx-js/react'; import {MDXProvider} from '@mdx-js/react';
import Translate from '@docusaurus/Translate';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import MDXComponents from '@theme/MDXComponents'; import MDXComponents from '@theme/MDXComponents';
@ -133,7 +133,13 @@ function BlogPostItem(props: Props): JSX.Element {
<Link <Link
to={metadata.permalink} to={metadata.permalink}
aria-label={`Read more about ${title}`}> aria-label={`Read more about ${title}`}>
<strong>Read More</strong> <strong>
<Translate
id="theme.BlogPostItem.readMore"
description="The label used in blog post item excerps to link to full blog posts">
Read More
</Translate>
</strong>
</Link> </Link>
</div> </div>
)} )}

View file

@ -6,14 +6,13 @@
*/ */
import React from 'react'; import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import BlogPostItem from '@theme/BlogPostItem'; import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator'; import BlogPostPaginator from '@theme/BlogPostPaginator';
import type {Props} from '@theme/BlogPostPage'; import type {Props} from '@theme/BlogPostPage';
import BlogSidebar from '@theme/BlogSidebar'; import BlogSidebar from '@theme/BlogSidebar';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import IconEdit from '@theme/IconEdit'; import EditThisPage from '@theme/EditThisPage';
function BlogPostPage(props: Props): JSX.Element { function BlogPostPage(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props; const {content: BlogPostContents, sidebar} = props;
@ -39,14 +38,7 @@ function BlogPostPage(props: Props): JSX.Element {
isBlogPostPage> isBlogPostPage>
<BlogPostContents /> <BlogPostContents />
</BlogPostItem> </BlogPostItem>
<div> <div>{editUrl && <EditThisPage editUrl={editUrl} />}</div>
{editUrl && (
<a href={editUrl} target="_blank" rel="noreferrer noopener">
<IconEdit />
Edit this page
</a>
)}
</div>
{(nextItem || prevItem) && ( {(nextItem || prevItem) && (
<div className="margin-vert--xl"> <div className="margin-vert--xl">
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} /> <BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />

View file

@ -6,6 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostPaginator'; import type {Props} from '@theme/BlogPostPaginator';
@ -17,7 +18,13 @@ function BlogPostPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item"> <div className="pagination-nav__item">
{prevItem && ( {prevItem && (
<Link className="pagination-nav__link" to={prevItem.permalink}> <Link className="pagination-nav__link" to={prevItem.permalink}>
<div className="pagination-nav__sublabel">Newer Post</div> <div className="pagination-nav__sublabel">
<Translate
id="theme.BlogPostPaginator.newerPost"
description="The blog post button label to navigate to the newer/previous post">
Newer Post
</Translate>
</div>
<div className="pagination-nav__label"> <div className="pagination-nav__label">
&laquo; {prevItem.title} &laquo; {prevItem.title}
</div> </div>
@ -27,7 +34,13 @@ function BlogPostPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item pagination-nav__item--next"> <div className="pagination-nav__item pagination-nav__item--next">
{nextItem && ( {nextItem && (
<Link className="pagination-nav__link" to={nextItem.permalink}> <Link className="pagination-nav__link" to={nextItem.permalink}>
<div className="pagination-nav__sublabel">Older Post</div> <div className="pagination-nav__sublabel">
<Translate
id="theme.BlogPostPaginator.olderPost"
description="The blog post button label to navigate to the older/next post">
Older Post
</Translate>
</div>
<div className="pagination-nav__label"> <div className="pagination-nav__label">
{nextItem.title} &raquo; {nextItem.title} &raquo;
</div> </div>

View file

@ -49,6 +49,7 @@ function BlogTagsListPage(props: Props): JSX.Element {
)) ))
.filter((item) => item != null); .filter((item) => item != null);
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
return ( return (
<Layout <Layout
title="Tags" title="Tags"

View file

@ -21,6 +21,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar} = props;
const {allTagsPath, name: tagName, count} = metadata; const {allTagsPath, name: tagName, count} = metadata;
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
return ( return (
<Layout <Layout
title={`Posts tagged "${tagName}"`} title={`Posts tagged "${tagName}"`}

View file

@ -12,6 +12,7 @@ import copy from 'copy-text-to-clipboard';
import rangeParser from 'parse-numeric-range'; import rangeParser from 'parse-numeric-range';
import usePrismTheme from '@theme/hooks/usePrismTheme'; import usePrismTheme from '@theme/hooks/usePrismTheme';
import type {Props} from '@theme/CodeBlock'; import type {Props} from '@theme/CodeBlock';
import Translate from '@docusaurus/Translate';
import styles from './styles.module.css'; import styles from './styles.module.css';
import {useThemeConfig} from '@docusaurus/theme-common'; import {useThemeConfig} from '@docusaurus/theme-common';
@ -86,11 +87,11 @@ const highlightDirectiveRegex = (lang) => {
}; };
const codeBlockTitleRegex = /(?:title=")(.*)(?:")/; const codeBlockTitleRegex = /(?:title=")(.*)(?:")/;
export default ({ export default function CodeBlock({
children, children,
className: languageClassName, className: languageClassName,
metastring, metastring,
}: Props): JSX.Element => { }: Props): JSX.Element {
const {prism} = useThemeConfig(); const {prism} = useThemeConfig();
const [showCopied, setShowCopied] = useState(false); const [showCopied, setShowCopied] = useState(false);
@ -242,11 +243,23 @@ export default ({
aria-label="Copy code to clipboard" aria-label="Copy code to clipboard"
className={clsx(styles.copyButton)} className={clsx(styles.copyButton)}
onClick={handleCopyCode}> onClick={handleCopyCode}>
{showCopied ? 'Copied' : 'Copy'} {showCopied ? (
<Translate
id="theme.CodeBlock.copied"
description="The copied button label on code blocks">
Copied
</Translate>
) : (
<Translate
id="theme.CodeBlock.copy"
description="The copy button label on code blocks">
Copy
</Translate>
)}
</button> </button>
</div> </div>
</> </>
)} )}
</Highlight> </Highlight>
); );
}; }

View file

@ -6,7 +6,6 @@
*/ */
import React from 'react'; import React from 'react';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import {useTitleFormatter} from '@docusaurus/theme-common'; import {useTitleFormatter} from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@ -15,7 +14,7 @@ import DocPaginator from '@theme/DocPaginator';
import DocVersionSuggestions from '@theme/DocVersionSuggestions'; import DocVersionSuggestions from '@theme/DocVersionSuggestions';
import type {Props} from '@theme/DocItem'; import type {Props} from '@theme/DocItem';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import IconEdit from '@theme/IconEdit'; import EditThisPage from '@theme/EditThisPage';
import clsx from 'clsx'; import clsx from 'clsx';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -107,15 +106,7 @@ function DocItem(props: Props): JSX.Element {
<div className="margin-vert--xl"> <div className="margin-vert--xl">
<div className="row"> <div className="row">
<div className="col"> <div className="col">
{editUrl && ( {editUrl && <EditThisPage editUrl={editUrl} />}
<a
href={editUrl}
target="_blank"
rel="noreferrer noopener">
<IconEdit />
Edit this page
</a>
)}
</div> </div>
{(lastUpdatedAt || lastUpdatedBy) && ( {(lastUpdatedAt || lastUpdatedBy) && (
<div className="col text--right"> <div className="col text--right">

View file

@ -7,6 +7,7 @@
import React from 'react'; import React from 'react';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/DocPaginator'; import type {Props} from '@theme/DocPaginator';
function DocPaginator(props: Props): JSX.Element { function DocPaginator(props: Props): JSX.Element {
@ -19,7 +20,13 @@ function DocPaginator(props: Props): JSX.Element {
<Link <Link
className="pagination-nav__link" className="pagination-nav__link"
to={metadata.previous.permalink}> to={metadata.previous.permalink}>
<div className="pagination-nav__sublabel">Previous</div> <div className="pagination-nav__sublabel">
<Translate
id="theme.DocPaginator.previous"
description="The label used to navigate to the previous doc">
Previous
</Translate>
</div>
<div className="pagination-nav__label"> <div className="pagination-nav__label">
&laquo; {metadata.previous.title} &laquo; {metadata.previous.title}
</div> </div>
@ -29,7 +36,13 @@ function DocPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item pagination-nav__item--next"> <div className="pagination-nav__item pagination-nav__item--next">
{metadata.next && ( {metadata.next && (
<Link className="pagination-nav__link" to={metadata.next.permalink}> <Link className="pagination-nav__link" to={metadata.next.permalink}>
<div className="pagination-nav__sublabel">Next</div> <div className="pagination-nav__sublabel">
<Translate
id="theme.DocPaginator.next"
description="The label used to navigate to the next doc">
Next
</Translate>
</div>
<div className="pagination-nav__label"> <div className="pagination-nav__label">
{metadata.next.title} &raquo; {metadata.next.title} &raquo;
</div> </div>

View file

@ -0,0 +1,25 @@
/**
* 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 Translate from '@docusaurus/Translate';
import type {Props} from '@theme/EditThisPage';
import IconEdit from '@theme/IconEdit';
export default function EditThisPage({editUrl}: Props): JSX.Element {
return (
<a href={editUrl} target="_blank" rel="noreferrer noopener">
<IconEdit />
<Translate
id="theme.EditThisPage.editThisPage"
description="The link label to edit the current page">
Edit this page
</Translate>
</a>
);
}

View file

@ -7,6 +7,7 @@
import React from 'react'; import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import Translate from '@docusaurus/Translate';
function NotFound(): JSX.Element { function NotFound(): JSX.Element {
return ( return (
@ -14,11 +15,27 @@ function NotFound(): JSX.Element {
<main className="container margin-vert--xl"> <main className="container margin-vert--xl">
<div className="row"> <div className="row">
<div className="col col--6 col--offset-3"> <div className="col col--6 col--offset-3">
<h1 className="hero__title">Page Not Found</h1> <h1 className="hero__title">
<p>We could not find what you were looking for.</p> <Translate
id="theme.NotFound.title"
description="The title of the 404 page">
Page Not Found
</Translate>
</h1>
<p> <p>
<Translate
id="theme.NotFound.p1"
description="The first paragraph of the 404 page">
We could not find what you were looking for.
</Translate>
</p>
<p>
<Translate
id="theme.NotFound.p2"
description="The 2nd paragraph of the 404 page">
Please contact the owner of the site that linked you to the Please contact the owner of the site that linked you to the
original URL and let them know their link is broken. original URL and let them know their link is broken.
</Translate>
</p> </p>
</div> </div>
</div> </div>

View file

@ -6,8 +6,8 @@
*/ */
import React, {useRef, useEffect} from 'react'; import React, {useRef, useEffect} from 'react';
import Translate from '@docusaurus/Translate';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
import styles from './styles.module.css'; import styles from './styles.module.css';
function programmaticFocus(el) { function programmaticFocus(el) {
@ -39,7 +39,11 @@ function SkipToContent(): JSX.Element {
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<a href="#main" className={styles.skipToContent} onClick={handleSkip}> <a href="#main" className={styles.skipToContent} onClick={handleSkip}>
<Translate
id="theme.SkipToContent.skipToMainContent"
description="The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation">
Skip to main content Skip to main content
</Translate>
</a> </a>
</div> </div>
); );

View file

@ -92,6 +92,14 @@ declare module '@theme/DocVersionSuggestions' {
export default DocVersionSuggestions; export default DocVersionSuggestions;
} }
declare module '@theme/EditThisPage' {
export type Props = {
readonly editUrl: string;
};
const EditThisPage: (props: Props) => JSX.Element;
export default EditThisPage;
}
declare module '@theme/Footer' { declare module '@theme/Footer' {
const Footer: () => JSX.Element | null; const Footer: () => JSX.Element | null;
export default Footer; export default Footer;

View file

@ -8,10 +8,11 @@
import * as React from 'react'; import * as React from 'react';
import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live'; import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
import clsx from 'clsx'; import clsx from 'clsx';
import Translate from '@docusaurus/Translate';
import styles from './styles.module.css'; import styles from './styles.module.css';
function Playground({children, theme, transformCode, ...props}) { export default function Playground({children, theme, transformCode, ...props}) {
return ( return (
<LiveProvider <LiveProvider
code={children.replace(/\n$/, '')} code={children.replace(/\n$/, '')}
@ -23,7 +24,11 @@ function Playground({children, theme, transformCode, ...props}) {
styles.playgroundHeader, styles.playgroundHeader,
styles.playgroundEditorHeader, styles.playgroundEditorHeader,
)}> )}>
<Translate
id="theme.Playground.liveEditor"
description="The live editor label of the live codeblocks">
Live Editor Live Editor
</Translate>
</div> </div>
<LiveEditor className={styles.playgroundEditor} /> <LiveEditor className={styles.playgroundEditor} />
<div <div
@ -31,7 +36,11 @@ function Playground({children, theme, transformCode, ...props}) {
styles.playgroundHeader, styles.playgroundHeader,
styles.playgroundPreviewHeader, styles.playgroundPreviewHeader,
)}> )}>
<Translate
id="theme.Playground.result"
description="The result label of the live codeblocks">
Result Result
</Translate>
</div> </div>
<div className={styles.playgroundPreview}> <div className={styles.playgroundPreview}>
<LivePreview /> <LivePreview />
@ -40,5 +49,3 @@ function Playground({children, theme, transformCode, ...props}) {
</LiveProvider> </LiveProvider>
); );
} }
export default Playground;

View file

@ -238,21 +238,54 @@ describe('extractPluginsSourceCodeTranslations', () => {
return { return {
name: 'abc', name: 'abc',
getPathsToWatch() { getPathsToWatch() {
return [path.join(pluginDir, '**/*.{js,jsx,ts,tsx}')]; return [path.join(pluginDir, 'subpath', '**/*.{js,jsx,ts,tsx}')];
},
getThemePath() {
return path.join(pluginDir, 'src', 'theme');
}, },
}; };
} }
const plugin1Dir = await createTmpDir(); const plugin1Dir = await createTmpDir();
const plugin1File = path.join(plugin1Dir, 'file.jsx'); const plugin1File1 = path.join(plugin1Dir, 'subpath', 'file1.jsx');
await fs.ensureDir(path.dirname(plugin1File)); await fs.ensureDir(path.dirname(plugin1File1));
await fs.writeFile( await fs.writeFile(
plugin1File, plugin1File1,
` `
export default function MyComponent() { export default function MyComponent() {
return ( return (
<div> <div>
<input text={translate({id: 'plugin1Id',message: 'plugin1 message',description: 'plugin1 description'})}/> <input text={translate({id: 'plugin1Id1',message: 'plugin1 message 1',description: 'plugin1 description 1'})}/>
</div>
);
}
`,
);
const plugin1File2 = path.join(plugin1Dir, 'src', 'theme', 'file2.jsx');
await fs.ensureDir(path.dirname(plugin1File2));
await fs.writeFile(
plugin1File2,
`
export default function MyComponent() {
return (
<div>
<input text={translate({id: 'plugin1Id2',message: 'plugin1 message 2',description: 'plugin1 description 2'})}/>
</div>
);
}
`,
);
// This one should not be found! On purpose!
const plugin1File3 = path.join(plugin1Dir, 'unscannedFolder', 'file3.jsx');
await fs.ensureDir(path.dirname(plugin1File3));
await fs.writeFile(
plugin1File3,
`
export default function MyComponent() {
return (
<div>
<input text={translate({id: 'plugin1Id3',message: 'plugin1 message 3',description: 'plugin1 description 3'})}/>
</div> </div>
); );
} }
@ -261,7 +294,7 @@ export default function MyComponent() {
const plugin1 = createTestPlugin(plugin1Dir); const plugin1 = createTestPlugin(plugin1Dir);
const plugin2Dir = await createTmpDir(); const plugin2Dir = await createTmpDir();
const plugin2File = path.join(plugin1Dir, 'sub', 'path', 'file.tsx'); const plugin2File = path.join(plugin1Dir, 'subpath', 'file.tsx');
await fs.ensureDir(path.dirname(plugin2File)); await fs.ensureDir(path.dirname(plugin2File));
await fs.writeFile( await fs.writeFile(
plugin2File, plugin2File,
@ -271,7 +304,7 @@ type Props = {hey: string};
export default function MyComponent(props: Props) { export default function MyComponent(props: Props) {
return ( return (
<div> <div>
<input text={translate({id: 'plugin2Id',message: 'plugin2 message',description: 'plugin2 description'})}/> <input text={translate({id: 'plugin2Id1',message: 'plugin2 message 1',description: 'plugin2 description 1'})}/>
<Translate <Translate
id="plugin2Id2" id="plugin2Id2"
description="plugin2 description 2" description="plugin2 description 2"
@ -291,13 +324,17 @@ export default function MyComponent(props: Props) {
TestBabelOptions, TestBabelOptions,
); );
expect(translations).toEqual({ expect(translations).toEqual({
plugin1Id: { plugin1Id1: {
description: 'plugin1 description', description: 'plugin1 description 1',
message: 'plugin1 message', message: 'plugin1 message 1',
}, },
plugin2Id: { plugin1Id2: {
description: 'plugin2 description', description: 'plugin1 description 2',
message: 'plugin2 message', message: 'plugin1 message 2',
},
plugin2Id1: {
description: 'plugin2 description 1',
message: 'plugin2 message 1',
}, },
plugin2Id2: { plugin2Id2: {
description: 'plugin2 description 2', description: 'plugin2 description 2',

View file

@ -31,15 +31,28 @@ function isTranslatableSourceCodePath(filePath: string): boolean {
return TranslatableSourceCodeExtension.has(nodePath.extname(filePath)); return TranslatableSourceCodeExtension.has(nodePath.extname(filePath));
} }
function getPluginSourceCodeFilePaths(plugin: InitPlugin): string[] {
// The getPathsToWatch() generally returns the js/jsx/ts/tsx/md/mdx file paths
// We can use this method as well to know which folders we should try to extract translations from
// Hacky/implicit, but do we want to introduce a new lifecycle method just for that???
const codePaths: string[] = plugin.getPathsToWatch?.() ?? [];
// We also include theme code
const themePath = plugin.getThemePath?.();
if (themePath) {
codePaths.push(themePath);
}
return codePaths;
}
async function getSourceCodeFilePaths( async function getSourceCodeFilePaths(
plugins: InitPlugin[], plugins: InitPlugin[],
): Promise<string[]> { ): Promise<string[]> {
// The getPathsToWatch() generally returns the js/jsx/ts/tsx/md/mdx file paths // The getPathsToWatch() generally returns the js/jsx/ts/tsx/md/mdx file paths
// We can use this method as well to know which folders we should try to extract translations from // We can use this method as well to know which folders we should try to extract translations from
// Hacky/implicit, but do we want to introduce a new lifecycle method for that??? // Hacky/implicit, but do we want to introduce a new lifecycle method for that???
const allPathsToWatch = flatten( const allPathsToWatch = flatten(plugins.map(getPluginSourceCodeFilePaths));
plugins.map((plugin) => plugin.getPathsToWatch?.() ?? []),
);
// Required for Windows support, as paths using \ should not be used by globby // Required for Windows support, as paths using \ should not be used by globby
// (also using the windows hard drive prefix like c: is not a good idea) // (also using the windows hard drive prefix like c: is not a good idea)

View file

@ -10,6 +10,7 @@
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"clear": "docusaurus clear", "clear": "docusaurus clear",
"serve": "docusaurus serve", "serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"start:baseUrl": "cross-env BASE_URL='/build/' yarn start", "start:baseUrl": "cross-env BASE_URL='/build/' yarn start",
"build:baseUrl": "cross-env BASE_URL='/build/' yarn build", "build:baseUrl": "cross-env BASE_URL='/build/' yarn build",
"start:bootstrap": "cross-env DOCUSAURUS_PRESET=bootstrap yarn start", "start:bootstrap": "cross-env DOCUSAURUS_PRESET=bootstrap yarn start",