返回 CodeWhale
links.ts
根目录 / web / lib / i18n / links.ts
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/codewhale-hq/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: one label per destination. */
26 export interface ChromeLink {
27 href: string;
28 label: string;
29 }
30
31 /**
32 * The primary nav links — Product / Models / Plugins / Docs — identical
33 * in shape for every routed locale. This is the strip a serious product
34 * shows; discovery and community routes live in `secondaryNavLinks` (the
35 * compact sheet's second group) and the footer.
36 */
37 export function navLinks(locale: string, chrome: ChromeDict): ChromeLink[] {
38 return [
39 { href: `/${locale}/product`, label: chrome.navProduct },
40 { href: `/${locale}/models`, label: chrome.navModels },
41 { href: `/${locale}/plugins`, label: chrome.navPlugins },
42 { href: `/${locale}/docs`, label: chrome.navDocs },
43 ];
44 }
45
46 /** The compact sheet's second group: getting started, install, and community. */
47 export function secondaryNavLinks(locale: string, chrome: ChromeDict): ChromeLink[] {
48 return [
49 { href: `/${locale}/docs/guide`, label: chrome.navStart },
50 { href: `/${locale}/install`, label: chrome.navInstall },
51 { href: `/${locale}/ratatui`, label: "Ratatui" },
52 { href: `/${locale}/faq`, label: chrome.navFaq },
53 { href: `/${locale}/community`, label: chrome.navCommunity },
54 { href: `/${locale}/contribute`, label: chrome.navContribute },
55 ];
56 }
57
58 /** Footer "Product" column — the in-site discovery links. */
59 export function footerProductLinks(locale: string, chrome: ChromeDict): ChromeLink[] {
60 return [
61 { href: `/${locale}/product`, label: chrome.navProduct },
62 { href: `/${locale}/docs`, label: chrome.footerDocs },
63 { href: `/${locale}/docs/guide`, label: chrome.footerGuide },
64 { href: `/${locale}/install`, label: chrome.footerInstall },
65 { href: `/${locale}/models`, label: chrome.footerModels },
66 { href: `/${locale}/plugins`, label: chrome.navPlugins },
67 { href: `/${locale}/ratatui`, label: "Ratatui" },
68 { href: `/${locale}/runtime`, label: chrome.footerRuntime },
69 { href: `/${locale}/faq`, label: chrome.footerFaq },
70 { href: `/${locale}/changelog`, label: chrome.footerChangelog },
71 ];
72 }
73
74 /** Footer "Project" column — GitHub plus the pinned legal link. */
75 export function footerProjectLinks(locale: string, chrome: ChromeDict): ChromeLink[] {
76 return [
77 { href: REPO_URL, label: "GitHub" },
78 { href: REPO_ISSUES_URL, label: chrome.footerIssues },
79 { href: DISCORD_URL, label: "Discord" },
80 { href: `/${locale}/contribute`, label: chrome.footerContribute },
81 { href: REPO_LICENSE_URL, label: chrome.footerLicense },
82 ];
83 }
84
85 /** Footer legal destinations — reachable, never a 404. */
86 export function footerLegalLinks(locale: string, chrome: ChromeDict): ChromeLink[] {
87 return [
88 { href: `/${locale}/legal/terms`, label: chrome.footerTerms },
89 { href: `/${locale}/legal/privacy`, label: chrome.footerPrivacy },
90 ];
91 }
92
93 /**
94 * The one link in a chrome set that names the page currently being viewed.
95 *
96 * Nav and the compact sheet used to decide this per link with a plain
97 * prefix test (`pathname === href || pathname.startsWith(href + "/")`).
98 * Two nav links are ancestor and descendant of each other — `/de/docs` and
99 * `/de/docs/guide` — so on the guide route that test was true for both:
100 * the nav underline landed under Docs and Start at once, and two links
101 * carried `aria-current="page"`, telling assistive technology the reader is
102 * on two pages simultaneously.
103 *
104 * The longest matching href is the page; anything shorter is an ancestor
105 * section, not a location. `/de/docs/configuration` is not a nav link, so
106 * Docs still wins there — only the collision resolves.
107 */
108 export function currentNavHref(
109 links: readonly ChromeLink[],
110 pathname: string,
111 ): string | null {
112 let current: string | null = null;
113 for (const link of links) {
114 const matches = pathname === link.href || pathname.startsWith(`${link.href}/`);
115 if (!matches) continue;
116 if (current === null || link.href.length > current.length) current = link.href;
117 }
118 return current;
119 }
120
120 lines TYPESCRIPT