From 371a337b7342b96d3aab8891a576720ff0502e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=83=81=EC=9C=A4?= Date: Fri, 6 Nov 2026 11:27:39 +0900 Subject: [PATCH 1/3] Preserve the current document in translation links --- src/components/Layout/TopNav/TopNav.tsx | 8 +++++++- src/components/MDX/MDXComponents.tsx | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) 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..ac34f24d252 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'; @@ -469,6 +470,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( @@ -484,7 +488,10 @@ function LanguageList({progress}: {progress: TranslationProgress}) { {languages.map(({code, name, enName}) => { return (
  • - + {enName} ({name}) {' '} —{' '} From 9acee3f668a926a6dc6e2c5cf97956879f457b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=83=81=EC=9C=A4?= Date: Fri, 6 Nov 2026 13:50:16 +0900 Subject: [PATCH 2/3] Fall back when a translated page is missing --- src/components/MDX/MDXComponents.tsx | 16 +++++--- src/pages/api/translation.ts | 55 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/pages/api/translation.ts diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index ac34f24d252..2c5c0224560 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -42,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'; @@ -486,14 +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..6aad75ad222 --- /dev/null +++ b/src/pages/api/translation.ts @@ -0,0 +1,55 @@ +/** + * 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') { + return res.status(405).end(); + } + + const {language, from} = req.query; + if ( + typeof language !== 'string' || + language === 'en' || + !finishedTranslations.includes(language) + ) { + return res.status(404).end(); + } + + const homepage = `https://${language}.react.dev/`; + if ( + typeof from !== 'string' || + !/^\/(?!\/)(?:learn|reference)(?:\/|$)[^\\?#]*$/.test(from) + ) { + return res.redirect(302, homepage); + } + + 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) { + return res.redirect(302, translatedPage); + } + } catch { + // Keep the original homepage behavior when the translation site is unavailable. + } finally { + clearTimeout(timeout); + } + + return res.redirect(302, homepage); +} From 080f404a978e44aae4f1fa1b72fbc5319796c241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=83=81=EC=9C=A4?= Date: Fri, 6 Nov 2026 13:54:37 +0900 Subject: [PATCH 3/3] fix: avoid returning API response objects --- src/pages/api/translation.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/api/translation.ts b/src/pages/api/translation.ts index 6aad75ad222..1e66e31b48e 100644 --- a/src/pages/api/translation.ts +++ b/src/pages/api/translation.ts @@ -13,7 +13,8 @@ export default async function handler( res: NextApiResponse ) { if (req.method !== 'GET') { - return res.status(405).end(); + res.status(405).end(); + return; } const {language, from} = req.query; @@ -22,7 +23,8 @@ export default async function handler( language === 'en' || !finishedTranslations.includes(language) ) { - return res.status(404).end(); + res.status(404).end(); + return; } const homepage = `https://${language}.react.dev/`; @@ -30,7 +32,8 @@ export default async function handler( typeof from !== 'string' || !/^\/(?!\/)(?:learn|reference)(?:\/|$)[^\\?#]*$/.test(from) ) { - return res.redirect(302, homepage); + res.redirect(302, homepage); + return; } const translatedPage = new URL(from, homepage).toString(); @@ -43,7 +46,8 @@ export default async function handler( signal: controller.signal, }); if (response.ok) { - return res.redirect(302, translatedPage); + res.redirect(302, translatedPage); + return; } } catch { // Keep the original homepage behavior when the translation site is unavailable. @@ -51,5 +55,5 @@ export default async function handler( clearTimeout(timeout); } - return res.redirect(302, homepage); + res.redirect(302, homepage); }