返回 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/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
143 lines TYPESCRIPT