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>
Please contact the owner of the site that linked you to the <Translate
original URL and let them know their link is broken. 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
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}>
Skip to main content <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
</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,
)}> )}>
Live Editor <Translate
id="theme.Playground.liveEditor"
description="The live editor label of the live codeblocks">
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,
)}> )}>
Result <Translate
id="theme.Playground.result"
description="The result label of the live codeblocks">
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",