| 1 | /** |
| 2 | * Locale-aware chrome link sets (#4934). |
| 3 | * |
| 4 | * Nav and footer used to carry hardcoded `/en/...` and `/zh/...` arrays plus |
| 5 | * a thin dictionary branch for everything else, so foreign locales silently |
| 6 | * lost the Start and FAQ links. There is now one generator per link set: |
| 7 | * labels come from `ChromeDict`, hrefs come from the locale, and every |
| 8 | * routed locale gets the identical route shape. Locale-swap parity is a |
| 9 | * unit test (`docs-ia.test.ts`) instead of a string scrape over the TSX. |
| 10 | */ |
| 11 | import type { ChromeDict } from "./dictionaries/types"; |
| 12 | |
| 13 | export const REPO_URL = "https://github.com/Hmbown/CodeWhale"; |
| 14 | export const REPO_ISSUES_URL = `${REPO_URL}/issues`; |
| 15 | export const REPO_RELEASES_URL = `${REPO_URL}/releases`; |
| 16 | export const REPO_LICENSE_URL = `${REPO_URL}/blob/main/LICENSE`; |
| 17 | /** Community chat. A brand name, so it is not a dictionary string. */ |
| 18 | export const DISCORD_URL = "https://discord.gg/37gfS3ksug"; |
| 19 | |
| 20 | /** The hosted Codewhale app (account sign-in / sign-up). */ |
| 21 | export const APP_URL = "https://app.codewhale.net"; |
| 22 | export const APP_LOGIN_URL = `${APP_URL}/login`; |
| 23 | export const APP_SIGNUP_URL = `${APP_URL}/signup`; |
| 24 | |
| 25 | /** A chrome link. `secondary` is the small bilingual companion label. */ |
| 26 | export interface ChromeLink { |
| 27 | href: string; |
| 28 | label: string; |
| 29 | secondary?: string; |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * The primary nav links — Product / Models / Plugins / Docs — identical |
| 34 | * in shape for every routed locale. This is the strip a serious product |
| 35 | * shows; discovery and community routes live in `secondaryNavLinks` (the |
| 36 | * compact sheet's second group) and the footer. |
| 37 | */ |
| 38 | export function navLinks(locale: string, chrome: ChromeDict): ChromeLink[] { |
| 39 | return [ |
| 40 | { |
| 41 | href: `/${locale}/product`, |
| 42 | label: chrome.navProduct, |
| 43 | secondary: chrome.navProductSecondary, |
| 44 | }, |
| 45 | { href: `/${locale}/models`, label: chrome.navModels, secondary: chrome.navModelsSecondary }, |
| 46 | { |
| 47 | href: `/${locale}/plugins`, |
| 48 | label: chrome.navPlugins, |
| 49 | secondary: chrome.navPluginsSecondary, |
| 50 | }, |
| 51 | { href: `/${locale}/docs`, label: chrome.navDocs, secondary: chrome.navDocsSecondary }, |
| 52 | ]; |
| 53 | } |
| 54 | |
| 55 | /** The compact sheet's second group: getting started, install, and community. */ |
| 56 | export function secondaryNavLinks(locale: string, chrome: ChromeDict): ChromeLink[] { |
| 57 | return [ |
| 58 | { |
| 59 | href: `/${locale}/docs/guide`, |
| 60 | label: chrome.navStart, |
| 61 | secondary: chrome.navStartSecondary, |
| 62 | }, |
| 63 | { |
| 64 | href: `/${locale}/install`, |
| 65 | label: chrome.navInstall, |
| 66 | secondary: chrome.navInstallSecondary, |
| 67 | }, |
| 68 | { href: `/${locale}/faq`, label: chrome.navFaq, secondary: chrome.navFaqSecondary }, |
| 69 | { |
| 70 | href: `/${locale}/community`, |
| 71 | label: chrome.navCommunity, |
| 72 | secondary: chrome.navCommunitySecondary, |
| 73 | }, |
| 74 | { |
| 75 | href: `/${locale}/contribute`, |
| 76 | label: chrome.navContribute, |
| 77 | secondary: chrome.navContributeSecondary, |
| 78 | }, |
| 79 | ]; |
| 80 | } |
| 81 | |
| 82 | /** Footer "Product" column — the in-site discovery links. */ |
| 83 | export function footerProductLinks(locale: string, chrome: ChromeDict): ChromeLink[] { |
| 84 | return [ |
| 85 | { href: `/${locale}/product`, label: chrome.navProduct }, |
| 86 | { href: `/${locale}/docs`, label: chrome.footerDocs }, |
| 87 | { href: `/${locale}/docs/guide`, label: chrome.footerGuide }, |
| 88 | { href: `/${locale}/install`, label: chrome.footerInstall }, |
| 89 | { href: `/${locale}/models`, label: chrome.footerModels }, |
| 90 | { href: `/${locale}/plugins`, label: chrome.navPlugins }, |
| 91 | { href: `/${locale}/runtime`, label: chrome.footerRuntime }, |
| 92 | { href: `/${locale}/faq`, label: chrome.footerFaq }, |
| 93 | { href: `/${locale}/changelog`, label: chrome.footerChangelog }, |
| 94 | ]; |
| 95 | } |
| 96 | |
| 97 | /** Footer "Project" column — GitHub plus the pinned legal link. */ |
| 98 | export function footerProjectLinks(locale: string, chrome: ChromeDict): ChromeLink[] { |
| 99 | return [ |
| 100 | { href: REPO_URL, label: "GitHub" }, |
| 101 | { href: REPO_ISSUES_URL, label: chrome.footerIssues }, |
| 102 | { href: DISCORD_URL, label: "Discord" }, |
| 103 | { href: `/${locale}/contribute`, label: chrome.footerContribute }, |
| 104 | { href: REPO_LICENSE_URL, label: chrome.footerLicense }, |
| 105 | ]; |
| 106 | } |
| 107 | |
| 108 | /** Footer legal destinations — reachable, never a 404. */ |
| 109 | export function footerLegalLinks(locale: string, chrome: ChromeDict): ChromeLink[] { |
| 110 | return [ |
| 111 | { href: `/${locale}/legal/terms`, label: chrome.footerTerms }, |
| 112 | { href: `/${locale}/legal/privacy`, label: chrome.footerPrivacy }, |
| 113 | ]; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * The one link in a chrome set that names the page currently being viewed. |
| 118 | * |
| 119 | * Nav and the compact sheet used to decide this per link with a plain |
| 120 | * prefix test (`pathname === href || pathname.startsWith(href + "/")`). |
| 121 | * Two nav links are ancestor and descendant of each other — `/de/docs` and |
| 122 | * `/de/docs/guide` — so on the guide route that test was true for both: |
| 123 | * the nav underline landed under Docs and Start at once, and two links |
| 124 | * carried `aria-current="page"`, telling assistive technology the reader is |
| 125 | * on two pages simultaneously. |
| 126 | * |
| 127 | * The longest matching href is the page; anything shorter is an ancestor |
| 128 | * section, not a location. `/de/docs/configuration` is not a nav link, so |
| 129 | * Docs still wins there — only the collision resolves. |
| 130 | */ |
| 131 | export function currentNavHref( |
| 132 | links: readonly ChromeLink[], |
| 133 | pathname: string, |
| 134 | ): string | null { |
| 135 | let current: string | null = null; |
| 136 | for (const link of links) { |
| 137 | const matches = pathname === link.href || pathname.startsWith(`${link.href}/`); |
| 138 | if (!matches) continue; |
| 139 | if (current === null || link.href.length > current.length) current = link.href; |
| 140 | } |
| 141 | return current; |
| 142 | } |
| 143 |