diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx index be30cce7..2274e45e 100644 --- a/src/components/Layout/Page.tsx +++ b/src/components/Layout/Page.tsx @@ -16,6 +16,7 @@ import {IconNavArrow} from 'components/Icon/IconNavArrow'; import PageHeading from 'components/PageHeading'; import {getRouteMeta} from './getRouteMeta'; import {TocContext} from '../MDX/TocContext'; +import {Languages, LanguagesContext} from '../MDX/LanguagesContext'; import type {TocItem} from 'components/MDX/TocContext'; import type {RouteItem} from 'components/Layout/getRouteMeta'; import {HomeContent} from './HomeContent'; @@ -36,9 +37,17 @@ interface PageProps { description?: string; }; section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown'; + languages?: Languages | null; } -export function Page({children, toc, routeTree, meta, section}: PageProps) { +export function Page({ + children, + toc, + routeTree, + meta, + section, + languages = null, +}: PageProps) { const {asPath} = useRouter(); const cleanedPath = asPath.split(/[\?\#]/)[0]; const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta( @@ -75,7 +84,11 @@ export function Page({children, toc, routeTree, meta, section}: PageProps) { 'max-w-7xl mx-auto', section === 'blog' && 'lg:flex lg:flex-col lg:items-center' )}> - {children} + + + {children} + + {!isBlogIndex && ( ); +const languageIcon = ( + + + +); + const githubIcon = ( +
+ + {languageIcon} + +
; + +export const LanguagesContext = createContext(null); diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index a89edf9c..6f99121f 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -31,6 +31,8 @@ import ButtonLink from 'components/ButtonLink'; import {TocContext} from './TocContext'; import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; +import {LanguagesContext} from './LanguagesContext'; +import {finishedTranslations} from 'utils/finishedTranslations'; import ErrorDecoder from './ErrorDecoder'; import {IconCanary} from '../Icon/IconCanary'; @@ -380,6 +382,38 @@ function InlineTocItem({items}: {items: Array}) { ); } +type TranslationProgress = 'complete' | 'in-progress'; + +function LanguageList({progress}: {progress: TranslationProgress}) { + const allLanguages = React.useContext(LanguagesContext) ?? []; + const languages = allLanguages + .filter( + ({code}) => + code !== 'en' && + (progress === 'complete' + ? finishedTranslations.includes(code) + : !finishedTranslations.includes(code)) + ) + .sort((a, b) => a.enName.localeCompare(b.enName)); + return ( +
    + {languages.map(({code, name, enName}) => { + return ( +
  • + + {enName} ({name}) + {' '} + —{' '} + + Contribute + +
  • + ); + })} +
+ ); +} + function YouTubeIframe(props: any) { return (
@@ -442,6 +476,7 @@ export const MDXComponents = { IllustrationBlock, Intro, InlineToc, + LanguageList, LearnMore, Math, MathI, diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx index dfc4f610..62808574 100644 --- a/src/components/Seo.tsx +++ b/src/components/Seo.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import Head from 'next/head'; import {withRouter, Router} from 'next/router'; import {siteConfig} from '../siteConfig'; +import {finishedTranslations} from 'utils/finishedTranslations'; export interface SeoProps { title: string; @@ -18,17 +19,8 @@ export interface SeoProps { searchOrder?: number; } -const deployedTranslations = [ - 'en', - 'zh-hans', - 'es', - 'fr', - 'ja', - 'tr', - // We'll add more languages when they have enough content. - // Please DO NOT edit this list without a discussion in the reactjs/react.dev repo. - // It must be the same between all translations. -]; +// If you are a maintainer of a language fork, +// deployedTranslations has been moved to src/utils/finishedTranslations.ts. function getDomain(languageCode: string): string { const subdomain = languageCode === 'en' ? '' : languageCode + '.'; @@ -71,7 +63,7 @@ export const Seo = withRouter( href={canonicalUrl.replace(siteDomain, getDomain('en'))} hrefLang="x-default" /> - {deployedTranslations.map((languageCode) => ( + {finishedTranslations.map((languageCode) => ( + +React docs are translated by the global community into many languages all over the world. + + + +## Source site {/*main-site*/} + +All translations are provided from the canonical source docs: + +- [English](https://react.dev/) — [Contribute](https://github.com/reactjs/react.dev/) + +## Full translations {/*full-translations*/} + +{/* If you are a language maintainer and want to add your language here, finish the "Core" translations and edit `deployedTranslations` under `src/utils`. */} + + + +## In-progress translations {/*in-progress-translations*/} + +For the progress of each translation, see: [Is React Translated Yet?](https://translations.react.dev/) + + + +## How to contribute {/*how-to-contribute*/} + +You can contribute to the translation efforts! + +The community conducts the translation work for the React docs on each language-specific fork of react.dev. Typical translation work involves directly translating a Markdown file and creating a pull request. Click the "contribute" link above to the GitHub repository for your language, and follow the instructions there to help with the translation effort. + +If you want to start a new translation for your language, visit: [translations.react.dev](https://github.com/reactjs/translations.react.dev) \ No newline at end of file diff --git a/src/pages/[[...markdownPath]].js b/src/pages/[[...markdownPath]].js index 79f7970a..63fcfcc8 100644 --- a/src/pages/[[...markdownPath]].js +++ b/src/pages/[[...markdownPath]].js @@ -13,7 +13,8 @@ import sidebarBlog from '../sidebarBlog.json'; import {MDXComponents} from 'components/MDX/MDXComponents'; import compileMDX from 'utils/compileMDX'; import {generateRssFeed} from '../utils/rss'; -export default function Layout({content, toc, meta}) { + +export default function Layout({content, toc, meta, languages}) { const parsedContent = useMemo( () => JSON.parse(content, reviveNodeOnClient), [content] @@ -40,7 +41,12 @@ export default function Layout({content, toc, meta}) { break; } return ( - + {parsedContent} ); @@ -110,12 +116,13 @@ export async function getStaticProps(context) { mdx = fs.readFileSync(rootDir + path + '/index.md', 'utf8'); } - const {toc, content, meta} = await compileMDX(mdx, path, {}); + const {toc, content, meta, languages} = await compileMDX(mdx, path, {}); return { props: { toc, content, meta, + languages, }, }; } diff --git a/src/sidebarCommunity.json b/src/sidebarCommunity.json index 6b3e4eca..ac8a172d 100644 --- a/src/sidebarCommunity.json +++ b/src/sidebarCommunity.json @@ -31,6 +31,10 @@ "title": "Docs Contributors", "path": "/community/docs-contributors" }, + { + "title": "Translations", + "path": "/community/translations" + }, { "title": "Acknowledgements", "path": "/community/acknowledgements" diff --git a/src/utils/compileMDX.ts b/src/utils/compileMDX.ts index 5f54d12b..4c5ee15b 100644 --- a/src/utils/compileMDX.ts +++ b/src/utils/compileMDX.ts @@ -1,8 +1,9 @@ +import {LanguageItem} from 'components/MDX/LanguagesContext'; import {MDXComponents} from 'components/MDX/MDXComponents'; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~ -const DISK_CACHE_BREAKER = 8; +const DISK_CACHE_BREAKER = 9; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ export default async function compileMDX( @@ -124,10 +125,21 @@ export default async function compileMDX( const fm = require('gray-matter'); const meta = fm(mdx).data; + // Load the list of translated languages conditionally. + let languages: Array | null = null; + if (typeof path === 'string' && path.endsWith('/translations')) { + languages = await ( + await fetch( + 'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json' + ) + ).json(); // { code: string; name: string; enName: string}[] + } + const output = { content: JSON.stringify(children, stringifyNodeOnServer), toc: JSON.stringify(toc, stringifyNodeOnServer), meta, + languages, }; // Serialize a server React tree node to JSON. diff --git a/src/utils/finishedTranslations.ts b/src/utils/finishedTranslations.ts new file mode 100644 index 00000000..b2aceb17 --- /dev/null +++ b/src/utils/finishedTranslations.ts @@ -0,0 +1,15 @@ +// This is a list of languages with enough translated content. +// Add more languages here when they have enough content. +// Please DO NOT edit this list without a discussion in the reactjs/react.dev repo. +// It must be the same between all translations. +// This will also affect the 'Translations' article. + +// prettier-ignore +export const finishedTranslations = [ + 'en', + 'zh-hans', + 'es', + 'fr', + 'ja', + 'tr' +];