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 Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Metadata} from '@theme/BlogListPage';
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">
{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>
)}
</div>
<div className="pagination-nav__item pagination-nav__item--next">
{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>
)}
</div>

View file

@ -8,7 +8,7 @@
import React from 'react';
import clsx from 'clsx';
import {MDXProvider} from '@mdx-js/react';
import Translate from '@docusaurus/Translate';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import MDXComponents from '@theme/MDXComponents';
@ -133,7 +133,13 @@ function BlogPostItem(props: Props): JSX.Element {
<Link
to={metadata.permalink}
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>
</div>
)}

View file

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

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import Translate from '@docusaurus/Translate';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogPostPaginator';
@ -17,7 +18,13 @@ function BlogPostPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item">
{prevItem && (
<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">
&laquo; {prevItem.title}
</div>
@ -27,7 +34,13 @@ function BlogPostPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item pagination-nav__item--next">
{nextItem && (
<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">
{nextItem.title} &raquo;
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
import React from 'react';
import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/DocPaginator';
function DocPaginator(props: Props): JSX.Element {
@ -19,7 +20,13 @@ function DocPaginator(props: Props): JSX.Element {
<Link
className="pagination-nav__link"
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">
&laquo; {metadata.previous.title}
</div>
@ -29,7 +36,13 @@ function DocPaginator(props: Props): JSX.Element {
<div className="pagination-nav__item pagination-nav__item--next">
{metadata.next && (
<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">
{metadata.next.title} &raquo;
</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 Layout from '@theme/Layout';
import Translate from '@docusaurus/Translate';
function NotFound(): JSX.Element {
return (
@ -14,11 +15,27 @@ function NotFound(): JSX.Element {
<main className="container margin-vert--xl">
<div className="row">
<div className="col col--6 col--offset-3">
<h1 className="hero__title">Page Not Found</h1>
<p>We could not find what you were looking for.</p>
<h1 className="hero__title">
<Translate
id="theme.NotFound.title"
description="The title of the 404 page">
Page Not Found
</Translate>
</h1>
<p>
Please contact the owner of the site that linked you to the
original URL and let them know their link is broken.
<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
original URL and let them know their link is broken.
</Translate>
</p>
</div>
</div>

View file

@ -6,8 +6,8 @@
*/
import React, {useRef, useEffect} from 'react';
import Translate from '@docusaurus/Translate';
import {useLocation} from '@docusaurus/router';
import styles from './styles.module.css';
function programmaticFocus(el) {
@ -39,7 +39,11 @@ function SkipToContent(): JSX.Element {
return (
<div ref={containerRef}>
<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>
</div>
);

View file

@ -92,6 +92,14 @@ declare module '@theme/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' {
const Footer: () => JSX.Element | null;
export default Footer;

View file

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

View file

@ -238,21 +238,54 @@ describe('extractPluginsSourceCodeTranslations', () => {
return {
name: 'abc',
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 plugin1File = path.join(plugin1Dir, 'file.jsx');
await fs.ensureDir(path.dirname(plugin1File));
const plugin1File1 = path.join(plugin1Dir, 'subpath', 'file1.jsx');
await fs.ensureDir(path.dirname(plugin1File1));
await fs.writeFile(
plugin1File,
plugin1File1,
`
export default function MyComponent() {
return (
<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>
);
}
@ -261,7 +294,7 @@ export default function MyComponent() {
const plugin1 = createTestPlugin(plugin1Dir);
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.writeFile(
plugin2File,
@ -271,7 +304,7 @@ type Props = {hey: string};
export default function MyComponent(props: Props) {
return (
<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
id="plugin2Id2"
description="plugin2 description 2"
@ -291,13 +324,17 @@ export default function MyComponent(props: Props) {
TestBabelOptions,
);
expect(translations).toEqual({
plugin1Id: {
description: 'plugin1 description',
message: 'plugin1 message',
plugin1Id1: {
description: 'plugin1 description 1',
message: 'plugin1 message 1',
},
plugin2Id: {
description: 'plugin2 description',
message: 'plugin2 message',
plugin1Id2: {
description: 'plugin1 description 2',
message: 'plugin1 message 2',
},
plugin2Id1: {
description: 'plugin2 description 1',
message: 'plugin2 message 1',
},
plugin2Id2: {
description: 'plugin2 description 2',

View file

@ -31,15 +31,28 @@ function isTranslatableSourceCodePath(filePath: string): boolean {
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(
plugins: InitPlugin[],
): Promise<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 for that???
const allPathsToWatch = flatten(
plugins.map((plugin) => plugin.getPathsToWatch?.() ?? []),
);
const allPathsToWatch = flatten(plugins.map(getPluginSourceCodeFilePaths));
// 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)

View file

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