diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx index 37c2768a667..dc090511ff7 100644 --- a/src/components/Layout/TopNav/TopNav.tsx +++ b/src/components/Layout/TopNav/TopNav.tsx @@ -169,6 +169,12 @@ export default function TopNav({ const [isScrolled, setIsScrolled] = useState(false); const scrollParentRef = useRef(null); const {asPath} = useRouter(); + const translationsHref = + section === 'learn' || section === 'reference' + ? `/community/translations?from=${encodeURIComponent( + asPath.split(/[?#]/)[0] + )}` + : '/community/translations'; // HACK. Fix up the data structures instead. if ((routeTree as any).routes.length === 1) { @@ -378,7 +384,7 @@ export default function TopNav({
{languageIcon} diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 5dc177a04cd..2c5c0224560 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -12,6 +12,7 @@ import {Children, useContext, useMemo} from 'react'; import * as React from 'react'; import cn from 'classnames'; +import {useRouter} from 'next/router'; import type {HTMLAttributes} from 'react'; import CodeBlock from './CodeBlock'; @@ -41,6 +42,7 @@ import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; import {LanguagesContext} from './LanguagesContext'; import {finishedTranslations} from 'utils/finishedTranslations'; +import {ExternalLink} from 'components/ExternalLink'; import ErrorDecoder from './ErrorDecoder'; import {IconCanary} from '../Icon/IconCanary'; @@ -469,6 +471,9 @@ function InlineTocItem({items}: {items: Array}) { type TranslationProgress = 'complete' | 'in-progress'; function LanguageList({progress}: {progress: TranslationProgress}) { + const from = useRouter().query.from; + const pathname = + typeof from === 'string' && /^\/(?!\/)[^\\?#]*$/.test(from) ? from : ''; const allLanguages = React.useContext(LanguagesContext) ?? []; const languages = allLanguages .filter( @@ -482,11 +487,19 @@ function LanguageList({progress}: {progress: TranslationProgress}) { return (
    {languages.map(({code, name, enName}) => { + const href = + progress === 'complete' && pathname + ? `/api/translation?language=${encodeURIComponent( + code + )}&from=${encodeURIComponent(pathname)}` + : `https://${code}.react.dev/`; return (
  • - + {enName} ({name}) - {' '} + {' '} —{' '} Contribute diff --git a/src/pages/api/translation.ts b/src/pages/api/translation.ts new file mode 100644 index 00000000000..1e66e31b48e --- /dev/null +++ b/src/pages/api/translation.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {NextApiRequest, NextApiResponse} from 'next'; +import {finishedTranslations} from 'utils/finishedTranslations'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + res.status(405).end(); + return; + } + + const {language, from} = req.query; + if ( + typeof language !== 'string' || + language === 'en' || + !finishedTranslations.includes(language) + ) { + res.status(404).end(); + return; + } + + const homepage = `https://${language}.react.dev/`; + if ( + typeof from !== 'string' || + !/^\/(?!\/)(?:learn|reference)(?:\/|$)[^\\?#]*$/.test(from) + ) { + res.redirect(302, homepage); + return; + } + + const translatedPage = new URL(from, homepage).toString(); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2000); + + try { + const response = await fetch(translatedPage, { + method: 'HEAD', + signal: controller.signal, + }); + if (response.ok) { + res.redirect(302, translatedPage); + return; + } + } catch { + // Keep the original homepage behavior when the translation site is unavailable. + } finally { + clearTimeout(timeout); + } + + res.redirect(302, homepage); +}