feat(web): move the roadmap page onto the dictionary spine (#5337)

Copy moved verbatim. The tracks stay content in the page, picked with
pickTextLocale, and the item count becomes a one/other pair filled with
fill. sectionLabel and contributeLabel were no longer rendered and are
dropped. The buildPageMetadata usage example now shows the dictionary
form instead of an isZh ternary. CEILING drops to the new count of 10,
and check-locales now covers roadmap.ts.

Signed-off-by: Lstarsky0 <[email protected]>
This commit is contained in:
Lstarsky0 committed 2026-09-26 16:21:14 +08:00
1 parent 5ec06e1576
commit c74829514d
9 files changed
+109 -56

No files matched your search

+17 -48
View File
@@ -4,20 +4,19 @@ import { PageHeader, Section } from "@/components/page-header";
import { Status, type StatusTone } from "@/components/status-badge";
import { getCachedRoadmap, type RoadmapItem } from "@/lib/roadmap-feed";
import { getEnv } from "@/lib/kv";
import { fill, getRoadmap, pickTextLocale } from "@/lib/i18n/dictionaries";
import { buildPageMetadata } from "@/lib/page-meta";
export const revalidate = 1800;
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const t = getRoadmap(locale);
return buildPageMetadata({
path: "/roadmap",
locale,
title: isZh ? "路线图 · Codewhale" : "Roadmap · Codewhale",
description: isZh
? "Codewhale 已完成、进行中、考虑中和明确不在范围内的工作。"
: "Current Codewhale work grouped by shipped, underway, considered, and deliberately out-of-scope directions.",
title: t.metaTitle,
description: t.metaDescription,
});
}
@@ -148,8 +147,8 @@ const roadmapText = (text: string) =>
export default async function RoadmapPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const baseTracks = isZh ? tracksZh : tracksEn;
const t = getRoadmap(locale);
const baseTracks = { en: tracksEn, zh: tracksZh }[pickTextLocale(locale)];
// Live feed: shipped from GitHub Releases; underway/considered/ruled-out from issue labels.
// Per-category fallback to the static items so unlabeled categories stay populated.
@@ -180,53 +179,23 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
/* keep static fallback */
}
const copy = isZh
? {
eyebrow: "项目路线图",
title: "路线图",
introduction: "这里将已完成的仓库工作、正在推进的工作、仍在评估的方案和明确不在范围内的方向分开列出。路线图的“已完成”可包含已在源码候选版中实现的工作;安装页与首页另行标明最新已发布包。发布记录和 GitHub issues 会在可用时更新这些分类。",
sectionLabel: "当前状态",
sectionTitle: "按状态查看工作",
browseIssues: "浏览 open issues",
count: (value: number) => `${value} 项`,
contributeLabel: "参与贡献",
contributeTitle: "路线图决策公开进行。",
contributeBody: "Bug 和范围明确的功能请求请使用 issues;尚在形成中的想法可以先在 Discussions 讨论;已有具体实现时,欢迎发送带测试或文档的 pull request。来自不同语言、平台和提供商的验证结果都能帮助维护者判断优先级。",
links: [
{ title: "Issues", detail: "报告问题,或提出范围明确的工作。", href: "https://github.com/Hmbown/CodeWhale/issues" },
{ title: "Discussions", detail: "在开始实现前讨论尚未成熟的想法。", href: "https://github.com/Hmbown/CodeWhale/discussions/new?category=ideas" },
{ title: "Pull requests", detail: "审查现有改动,或发送一个范围清楚的补丁。", href: "https://github.com/Hmbown/CodeWhale/pulls" },
],
}
: {
eyebrow: "Project roadmap",
title: "Roadmap",
introduction: "This page separates completed repository work from work in progress, proposals still being evaluated, and directions intentionally kept out of scope. Roadmap Shipped can include work implemented in a source candidate; the install page and homepage separately identify the latest published package. Release records and GitHub issues refresh these categories when available.",
sectionLabel: "Current status",
sectionTitle: "Work grouped by status",
browseIssues: "Browse open issues",
count: (value: number) => `${value} ${value === 1 ? "item" : "items"}`,
contributeLabel: "Contribute",
contributeTitle: "Keep roadmap decisions in the open.",
contributeBody: "Use issues for bugs and well-scoped feature requests, Discussions for ideas that need shaping, and pull requests for concrete changes with tests or documentation. Verification across languages, platforms, and providers helps maintainers judge priority.",
links: [
{ title: "Issues", detail: "Report a problem or propose scoped work.", href: "https://github.com/Hmbown/CodeWhale/issues" },
{ title: "Discussions", detail: "Explore an early idea before implementation.", href: "https://github.com/Hmbown/CodeWhale/discussions/new?category=ideas" },
{ title: "Pull requests", detail: "Review existing work or send a focused change.", href: "https://github.com/Hmbown/CodeWhale/pulls" },
],
};
const links = [
{ title: "Issues", detail: t.issuesDetail, href: "https://github.com/Hmbown/CodeWhale/issues" },
{ title: "Discussions", detail: t.discussionsDetail, href: "https://github.com/Hmbown/CodeWhale/discussions/new?category=ideas" },
{ title: "Pull requests", detail: t.pullsDetail, href: "https://github.com/Hmbown/CodeWhale/pulls" },
];
return (
<>
<PageHeader kicker={copy.eyebrow} title={copy.title} lede={copy.introduction} pose="browse" />
<PageHeader kicker={t.eyebrow} title={t.title} lede={t.introduction} pose="browse" />
<div className="page-body">
<Section
id="roadmap-status"
title={copy.sectionTitle}
title={t.sectionTitle}
link={
<Link href="https://github.com/Hmbown/CodeWhale/issues" className="section-link">
{copy.browseIssues}
{t.browseIssues}
<Icon name="external" className="icon" />
</Link>
}
@@ -238,7 +207,7 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
<h3 id={`track-${index}`}>
<Status tone={TRACK_TONES[index] ?? "idle"}>{track.title}</Status>
</h3>
<span className="page-meta tabular">{copy.count(track.items.length)}</span>
<span className="page-meta tabular">{fill(track.items.length === 1 ? t.trackCountOne : t.trackCount, { count: track.items.length })}</span>
</div>
<ul className="group-card" role="list">
{track.items.map((item) => (
@@ -253,9 +222,9 @@ export default async function RoadmapPage({ params }: { params: Promise<{ locale
</div>
</Section>
<Section id="roadmap-contribute" title={copy.contributeTitle} scope={copy.contributeBody}>
<Section id="roadmap-contribute" title={t.contributeTitle} scope={t.contributeBody}>
<ul className="dir-list dir-list-card" role="list">
{copy.links.map((link) => (
{links.map((link) => (
<li key={link.title}>
<Link href={link.href} className="dir-row">
<span className="dir-mark" aria-hidden="true">
+3
View File
@@ -24,6 +24,7 @@ import {
EN_HOME,
EN_LEGAL_PRIVACY,
EN_LEGAL_TERMS,
EN_ROADMAP,
fill,
getChrome,
getDocsGuide,
@@ -50,6 +51,7 @@ import {
getHome,
getLegalPrivacy,
getLegalTerms,
getRoadmap,
pickText,
pickTextLocale,
splitToken,
@@ -267,6 +269,7 @@ describe("website dictionaries", () => {
["legal-privacy", getLegalPrivacy, EN_LEGAL_PRIVACY],
["digest", getDigest, EN_DIGEST],
["faq", getFaq, EN_FAQ],
["roadmap", getRoadmap, EN_ROADMAP],
] as const) {
const enKeys = Object.keys(reference).sort();
for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) {
+26
View File
@@ -0,0 +1,26 @@
import type { RoadmapDict } from "../types";
/**
* English reference dictionary for `app/[locale]/roadmap/page.tsx`. Copy
* moved verbatim from its `isZh` ternaries. The tracks themselves are content
* and stay in the page as `tracksEn` / `tracksZh`.
*/
export const roadmap: RoadmapDict = {
metaTitle: "Roadmap · Codewhale",
metaDescription:
"Current Codewhale work grouped by shipped, underway, considered, and deliberately out-of-scope directions.",
eyebrow: "Project roadmap",
title: "Roadmap",
introduction:
"This page separates completed repository work from work in progress, proposals still being evaluated, and directions intentionally kept out of scope. Roadmap Shipped can include work implemented in a source candidate; the install page and homepage separately identify the latest published package. Release records and GitHub issues refresh these categories when available.",
sectionTitle: "Work grouped by status",
browseIssues: "Browse open issues",
trackCount: "{count} items",
trackCountOne: "{count} item",
contributeTitle: "Keep roadmap decisions in the open.",
contributeBody:
"Use issues for bugs and well-scoped feature requests, Discussions for ideas that need shaping, and pull requests for concrete changes with tests or documentation. Verification across languages, platforms, and providers helps maintainers judge priority.",
issuesDetail: "Report a problem or propose scoped work.",
discussionsDetail: "Explore an early idea before implementation.",
pullsDetail: "Review existing work or send a focused change.",
};
+15 -3
View File
@@ -38,6 +38,7 @@ import type {
FaqDict,
LegalPrivacyDict,
LegalTermsDict,
RoadmapDict,
StatesDict,
} from "./types";
import { chrome as enChrome } from "./en/chrome";
@@ -106,6 +107,8 @@ import { digest as enDigest } from "./en/digest";
import { digest as zhDigest } from "./zh/digest";
import { faq as enFaq } from "./en/faq";
import { faq as zhFaq } from "./zh/faq";
import { roadmap as enRoadmap } from "./en/roadmap";
import { roadmap as zhRoadmap } from "./zh/roadmap";
import { chrome as zhChrome } from "./zh/chrome";
import { home as zhHome } from "./zh/home";
import { chrome as jaChrome } from "./ja/chrome";
@@ -297,9 +300,9 @@ const DOCS_REVIEW: Record<string, DocsReviewDict> = {
/**
* Shared surface states, the changelog page, the two legal pages, the digest
* page and the FAQ follow the same optional per-locale rule as the docs page
* dictionaries: English is the reference, every other locale falls back to it
* at lookup time.
* page, the FAQ and the roadmap follow the same optional per-locale rule as
* the docs page dictionaries: English is the reference, every other locale
* falls back to it at lookup time.
*/
const STATES: Record<string, StatesDict> = {
zh: zhStates,
@@ -325,6 +328,10 @@ const FAQ: Record<string, FaqDict> = {
zh: zhFaq,
};
const ROADMAP: Record<string, RoadmapDict> = {
zh: zhRoadmap,
};
export function getChrome(locale: string): ChromeDict {
return CHROME[locale] ?? enChrome;
}
@@ -429,6 +436,10 @@ export function getFaq(locale: string): FaqDict {
return FAQ[locale] ?? enFaq;
}
export function getRoadmap(locale: string): RoadmapDict {
return ROADMAP[locale] ?? enRoadmap;
}
/**
* Select one side of a legacy `{ en, zh }` content pair by locale. This is
* the transitional bridge for `web/lib/content/` modules that still carry
@@ -472,6 +483,7 @@ export const EN_LEGAL_TERMS = enLegalTerms;
export const EN_LEGAL_PRIVACY = enLegalPrivacy;
export const EN_DIGEST = enDigest;
export const EN_FAQ = enFaq;
export const EN_ROADMAP = enRoadmap;
/** Interpolate `{name}` tokens in a dictionary template. Unknown tokens are
* left intact so a template/variable drift is visible in review, not silent. */
+21
View File
@@ -482,6 +482,27 @@ export interface FaqDict {
noResultsBody: string;
}
/** `app/[locale]/roadmap/page.tsx`. */
export interface RoadmapDict {
metaTitle: string;
metaDescription: string;
eyebrow: string;
/** Page H1. */
title: string;
introduction: string;
sectionTitle: string;
browseIssues: string;
/** Track size; `{count}` is filled at render time. */
trackCount: string;
/** Track size when it is exactly one. */
trackCountOne: string;
contributeTitle: string;
contributeBody: string;
issuesDetail: string;
discussionsDetail: string;
pullsDetail: string;
}
export type DocsHooksDict = DocsPageDict;
export type DocsTroubleshootingDict = DocsPageDict;
+21
View File
@@ -0,0 +1,21 @@
import type { RoadmapDict } from "../types";
/** Chinese dictionary for `app/[locale]/roadmap/page.tsx`. */
export const roadmap: RoadmapDict = {
metaTitle: "路线图 · Codewhale",
metaDescription: "Codewhale 已完成、进行中、考虑中和明确不在范围内的工作。",
eyebrow: "项目路线图",
title: "路线图",
introduction:
"这里将已完成的仓库工作、正在推进的工作、仍在评估的方案和明确不在范围内的方向分开列出。路线图的“已完成”可包含已在源码候选版中实现的工作;安装页与首页另行标明最新已发布包。发布记录和 GitHub issues 会在可用时更新这些分类。",
sectionTitle: "按状态查看工作",
browseIssues: "浏览 open issues",
trackCount: "{count} 项",
trackCountOne: "{count} 项",
contributeTitle: "路线图决策公开进行。",
contributeBody:
"Bug 和范围明确的功能请求请使用 issues;尚在形成中的想法可以先在 Discussions 讨论;已有具体实现时,欢迎发送带测试或文档的 pull request。来自不同语言、平台和提供商的验证结果都能帮助维护者判断优先级。",
issuesDetail: "报告问题,或提出范围明确的工作。",
discussionsDetail: "在开始实现前讨论尚未成熟的想法。",
pullsDetail: "审查现有改动,或发送一个范围清楚的补丁。",
};
+1 -1
View File
@@ -7,7 +7,7 @@ const MIGRATION_HOME = join("lib", "i18n");
// One-way ceiling (#5519): the isZh migration converges or holds, never
// regresses. To lower the number: migrate branches into lib/i18n, then set
// CEILING to the new count reported by this test.
const CEILING = 12;
const CEILING = 10;
function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
+4 -4
View File
@@ -71,12 +71,12 @@ const OG_LOCALE: Record<string, string> = {
* ```ts
* export async function generateMetadata({ params }) {
* const { locale } = await params;
* const isZh = locale === "zh";
* const t = getFaq(locale);
* return buildPageMetadata({
* path: "/install",
* path: "/faq",
* locale,
* title: isZh ? "安装 · Codewhale" : "Install · Codewhale",
* description: isZh ? "…" : "…",
* title: t.metaTitle,
* description: t.metaDescription,
* });
* }
* ```
+1
View File
@@ -48,6 +48,7 @@ const OPTIONAL_FILES = [
"legal-privacy.ts",
"digest.ts",
"faq.ts",
"roadmap.ts",
"computer-use.ts",
];