From a8e95cbb7e9da55daee71566f8af2afa343a37d4 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:37:59 +0200 Subject: [PATCH] Add locale routes for generated API pages --- src/components/api/ApiPage.astro | 56 ++++++++++++++++--- src/pages/[locale]/[...slug].astro | 26 ++++----- .../api-reference/[tag]/[operation].astro | 28 ++++++++++ .../api-reference/[tag]/[operation].astro | 28 ++++++++++ src/util/locales.ts | 25 +++++++++ 5 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 src/pages/[locale]/clickstack/api-reference/[tag]/[operation].astro create mode 100644 src/pages/[locale]/products/cloud/api-reference/[tag]/[operation].astro diff --git a/src/components/api/ApiPage.astro b/src/components/api/ApiPage.astro index 83d56dcb4..d0befebd6 100644 --- a/src/components/api/ApiPage.astro +++ b/src/components/api/ApiPage.astro @@ -1,30 +1,46 @@ --- import DocsLayout from "@/layouts/DocsLayout.astro"; import ApiOperation from "./ApiOperation.astro"; +import { Aside } from "@/components/ui/aside"; import { Badge } from "@/components/ui/badge"; import { Breadcrumbs } from "@/components/ui/breadcrumbs"; import { getPrevNext } from "@cloudflare/nimbus-docs"; import { withBase } from "@/lib/base"; import { buildRailFromConfig, type ConfigItem } from "@/lib/sidebar-lazy"; +import { loadGeneratedNavigation } from "@/lib/sidebar-navigation.server"; import { getApiOperations, type RoutedApiOperationPage } from "@/lib/openapi"; +import { AVAILABLE_LOCALES } from "@/content.config"; +import { localeInfo, localePath, notTranslatedNotice } from "@/util/locales"; import navigation from "@/generated/sidebar.items.json"; interface Props { page: RoutedApiOperationPage; + currentLocale?: string; + activeLocales?: string[]; + fallback?: boolean; } -const { page } = Astro.props; +const { + page, + currentLocale = "en", + activeLocales = AVAILABLE_LOCALES, + fallback = false, +} = Astro.props; const { source, view } = page; -const browserPath = withBase(`/${source.route}`); -const sidebar = buildRailFromConfig(navigation as ConfigItem[], browserPath); +const localized = currentLocale.toLowerCase() !== "en"; +const browserPath = withBase(localePath(`/${source.route}`, currentLocale)); +const navItems = localized + ? loadGeneratedNavigation(currentLocale).items + : navigation as ConfigItem[]; +const sidebar = buildRailFromConfig(navItems, browserPath, localized ? [currentLocale] : []); const prevNext = await getPrevNext(browserPath, { sidebarTree: sidebar }); const isCloud = source.collection === "cloud"; const overview = isCloud - ? withBase("/products/cloud/features/admin-features/api/api-overview") - : withBase("/clickstack/api-reference"); + ? withBase(localePath("/products/cloud/features/admin-features/api/api-overview", currentLocale)) + : withBase(localePath("/clickstack/api-reference", currentLocale)); const firstInTag = getApiOperations(source.collection) .find((candidate) => candidate.tagSlug === source.tagSlug) ?? source; -const tagRoute = withBase(`/${firstInTag.route}`); +const tagRoute = withBase(localePath(`/${firstInTag.route}`, currentLocale)); const breadcrumbs = [ { label: isCloud ? "Cloud API" : "ClickStack", href: overview }, { label: "API reference", href: overview }, @@ -39,6 +55,21 @@ const headings = [ ]; const markdownPath = withBase(`/${source.route}.md`); const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdownPath; +const language = localeInfo(currentLocale); +const canonicalUrl = localized && Astro.site + ? new URL(withBase(`/${source.route}/`), Astro.site).href + : undefined; +const alternates = Astro.site + ? [ + { hreflang: "en", href: new URL(withBase(`/${source.route}/`), Astro.site).href }, + { hreflang: "x-default", href: new URL(withBase(`/${source.route}/`), Astro.site).href }, + ...activeLocales.map((locale) => ({ + hreflang: localeInfo(locale).hreflang, + href: new URL(withBase(localePath(`/${source.route}/`, locale)), Astro.site!).href, + })), + ] + : []; +const fallbackNotice = notTranslatedNotice(currentLocale); ---
@@ -63,6 +96,11 @@ const markdownUrl = Astro.site ? new URL(markdownPath, Astro.site).href : markdo {view.deprecated && }
+ {fallback && ( + + )} ; for (const p of localePaths) { + const slug = p.params.slug; + if ( + slug?.startsWith("products/cloud/api-reference/") + || slug?.startsWith("clickstack/api-reference/") + ) continue; paths.push({ params: { locale, slug: p.params.slug }, props: p.props, @@ -66,7 +71,12 @@ export async function getStaticPaths() { const translated = translatedPageIds(sourceLocale, SECTIONS); const navigationDigest = loadGeneratedNavigation(sourceLocale).digest; for (const entry of english) { - if (translated.has(entry.id) || entry.id === "index") continue; + if ( + translated.has(entry.id) + || entry.id === "index" + || entry.id.startsWith("products/cloud/api-reference/") + || entry.id.startsWith("clickstack/api-reference/") + ) continue; paths.push({ params: { locale, slug: entry.id }, props: { entry, fallback: true }, @@ -128,17 +138,7 @@ const alternates = Astro.site ] : []; const htmlLang = HREFLANG[localeCode] ?? localeParam; -const NOT_TRANSLATED: Record = { - ar: { label: "ملاحظة", title: "لم تُترجم بعد", message: "هذه الصفحة لم تُترجم بعد؛ يتم عرض النسخة الإنجليزية." }, - es: { label: "Nota", title: "Aún no traducida", message: "Esta página aún no está traducida; se muestra la versión en inglés." }, - fr: { label: "Remarque", title: "Pas encore traduit", message: "Cette page n'est pas encore traduite ; la version anglaise est affichée." }, - ja: { label: "注記", title: "未翻訳", message: "このページはまだ翻訳されていません。英語版を表示しています。" }, - ko: { label: "참고", title: "아직 번역되지 않음", message: "이 페이지는 아직 번역되지 않았습니다. 영어 버전이 표시됩니다." }, - "pt-br": { label: "Observação", title: "Ainda não traduzida", message: "Esta página ainda não foi traduzida; a versão em inglês é exibida." }, - ru: { label: "Примечание", title: "Перевод пока недоступен", message: "Эта страница ещё не переведена; показана английская версия." }, - zh: { label: "注意", title: "尚未翻译", message: "此页面尚未翻译,显示的是英文版本。" }, -}; -const fallbackNotice = NOT_TRANSLATED[localeCode] ?? { label: "Note", title: "Not translated yet", message: "This page is not translated yet; the English version is shown." }; +const fallbackNotice = notTranslatedNotice(localeCode); --- { + const routeLocale = localeRouteName(locale); + const navigationDigest = loadGeneratedNavigation(routeLocale).digest; + return getApiOperations("clickstack").map((page) => ({ + params: { locale: routeLocale, tag: page.tagSlug, operation: page.slug }, + cacheKey: `${apiPageCacheKey(page)}:locale:${routeLocale}:nav:${navigationDigest}`, + })); + }); +} + +const scope = readScope(); +const locale = Astro.params.locale!; +const page = await getRoutedApiOperation("clickstack", Astro.params.tag!, Astro.params.operation!); +--- + + diff --git a/src/pages/[locale]/products/cloud/api-reference/[tag]/[operation].astro b/src/pages/[locale]/products/cloud/api-reference/[tag]/[operation].astro new file mode 100644 index 000000000..83abc81c7 --- /dev/null +++ b/src/pages/[locale]/products/cloud/api-reference/[tag]/[operation].astro @@ -0,0 +1,28 @@ +--- +import ApiPage from "@/components/api/ApiPage.astro"; +import { apiPageCacheKey, getApiOperations, getRoutedApiOperation } from "@/lib/openapi"; +import { readScope } from "@/lib/scope"; +import { loadGeneratedNavigation } from "@/lib/sidebar-navigation.server"; +import { localeRouteName } from "@/util/locales"; + +export const prerender = true; + +export function getStaticPaths() { + const scope = readScope(); + if (scope.remotePreview) return []; + return scope.locales.flatMap((locale) => { + const routeLocale = localeRouteName(locale); + const navigationDigest = loadGeneratedNavigation(routeLocale).digest; + return getApiOperations("cloud").map((page) => ({ + params: { locale: routeLocale, tag: page.tagSlug, operation: page.slug }, + cacheKey: `${apiPageCacheKey(page)}:locale:${routeLocale}:nav:${navigationDigest}`, + })); + }); +} + +const scope = readScope(); +const locale = Astro.params.locale!; +const page = await getRoutedApiOperation("cloud", Astro.params.tag!, Astro.params.operation!); +--- + + diff --git a/src/util/locales.ts b/src/util/locales.ts index da1b0ca99..54e393f23 100644 --- a/src/util/locales.ts +++ b/src/util/locales.ts @@ -13,10 +13,35 @@ export const LOCALE_METADATA = { export type LocaleCode = keyof typeof LOCALE_METADATA; +export interface NotTranslatedNotice { + label: string; + title: string; + message: string; +} + +const NOT_TRANSLATED: Partial> = { + ar: { label: "ملاحظة", title: "لم تُترجم بعد", message: "هذه الصفحة لم تُترجم بعد؛ يتم عرض النسخة الإنجليزية." }, + es: { label: "Nota", title: "Aún no traducida", message: "Esta página aún no está traducida; se muestra la versión en inglés." }, + fr: { label: "Remarque", title: "Pas encore traduit", message: "Cette page n'est pas encore traduite ; la version anglaise est affichée." }, + ja: { label: "注記", title: "未翻訳", message: "このページはまだ翻訳されていません。英語版を表示しています。" }, + ko: { label: "참고", title: "아직 번역되지 않음", message: "이 페이지는 아직 번역되지 않았습니다. 영어 버전이 표시됩니다." }, + "pt-br": { label: "Observação", title: "Ainda não traduzida", message: "Esta página ainda não foi traduzida; a versão em inglês é exibida." }, + ru: { label: "Примечание", title: "Перевод пока недоступен", message: "Эта страница ещё не переведена; показана английская версия." }, + zh: { label: "注意", title: "尚未翻译", message: "此页面尚未翻译,显示的是英文版本。" }, +}; + export function localeInfo(locale: string) { return LOCALE_METADATA[locale.toLowerCase() as LocaleCode] ?? LOCALE_METADATA.en; } +export function notTranslatedNotice(locale: string): NotTranslatedNotice { + return NOT_TRANSLATED[locale.toLowerCase() as LocaleCode] ?? { + label: "Note", + title: "Not translated yet", + message: "This page is not translated yet; the English version is shown.", + }; +} + /** Canonical locale segment used by the deployed Mintlify documentation. */ export function localeRouteName(locale: string): string { const code = locale.toLowerCase();