Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ For detailed subsystem docs, see [docs/index.md](./docs/index.md).

> **Translation quality bar:** write natural technical Chinese, not word-for-word machine translation (style reference: [`vllm-project/vllm-ascend` `README.zh.md`](https://github.com/vllm-project/vllm-ascend/blob/main/README.zh.md)). Preserve product names, hardware SKUs, framework/library names (Next.js, React Query, D3.js, Tailwind ...), flags, and code identifiers in English. Use parenthetical English clarification for acronyms on first use. Preferred terms: benchmark 基准测试, dashboard 仪表板, chart 图表, config 配置, throughput 吞吐量, latency 延迟, single-node/multi-node 单节点/多节点, evaluation 评估, artifact 产物.

> **The website itself is bilingual too — every indexable page must ship a Simplified Chinese sibling under `/zh`.** See [Chinese Website Pages](#chinese-website-pages-zh--mandatory-for-all-indexable-surfaces) below; a new page, tab, or blog post without its `/zh` version is 🔴 BLOCKING on PR review.

## Project Overview

InferenceX App — Next.js 16 dashboard for ML inference benchmark data. DB-backed with Neon PostgreSQL, React Query for data fetching, D3.js for charts.
Expand Down Expand Up @@ -121,6 +123,20 @@ When adding a chart feature (toggle, label, overlay, filter, export, share-link

If the feature genuinely cannot apply to overlays (e.g., it depends on data only ingested for official runs), say so explicitly in code comments and the PR description. Default to "must support overlays."

## Chinese Website Pages (/zh) — Mandatory for All Indexable Surfaces

The site ships a hand-authored Simplified Chinese sibling for every indexable page under the `/zh` route prefix (`/` ↔ `/zh`, `/about` ↔ `/zh/about`, `/blog/<slug>` ↔ `/zh/blog/<slug>`, …) so the site is crawled and indexed in Chinese as well as English. There is no i18n framework — each `/zh` page is a real page that reuses the shared helpers in `packages/app/src/lib/i18n.ts` (`zhAlternates`, `enAlternates`, `ZH_OG_LOCALE`, `ZH_MIRRORED_ROUTES`) and `src/lib/tab-meta-zh.ts`. The translation quality bar above applies to all site content.

**Every new indexable page, dashboard tab, or blog post MUST ship its Chinese version in the same PR:**

1. **New page** → create `packages/app/src/app/zh/<route>/page.tsx` with fully translated content and metadata. Metadata: `alternates: zhAlternates('<en-path>')` plus `openGraph.locale: ZH_OG_LOCALE`. Switch the English page's `alternates` to `enAlternates('<en-path>')` so both sides carry bidirectional hreflang. Register the route in `ZH_MIRRORED_ROUTES` (`src/lib/i18n.ts`) so the header nav and EN↔中文 toggle link to it, and add it to the sitemap via `localizedPair()` in `src/app/sitemap.ts`.
2. **New dashboard tab** → add the tab to `ZH_TAB_KEYS`, `TAB_META_ZH`, `TAB_INTRO_ZH`, and `TAB_LABELS_ZH` in `src/lib/tab-meta-zh.ts`, then create `src/app/zh/(dashboard)/<tab>/page.tsx` mirroring the English page with `tabMetadataZh('<tab>')` and a `<ZhTabIntro tab="<tab>" />` block above the chart (the interactive chart UI itself stays English). `tab-meta-zh.test.ts` enforces dictionary completeness.
3. **New blog post** → the translation `packages/app/content/blog/zh/<same-filename>.mdx` is REQUIRED in the same PR. Translate frontmatter `title`/`subtitle` and the body; keep `date`, `publishDate`, `modifiedDate`, `tags`, and the filename/slug identical (English and Chinese posts pair by filename; visibility gating always follows the English post's `publishDate`). Rewrite internal `/blog/<slug>` links to `/zh/blog/<slug>`; never alter numbers, code blocks, or `<Figure>`/`<JsonLd>` structure. The `/zh/blog` listing, hreflang, and sitemap pick the file up automatically.
4. **Editing an existing English page or post** → update its Chinese sibling in the same PR. Content drift between languages is a 🔴 BLOCKING review issue.
5. **Shared UI chrome** (headers, footers, dashboard card titles/descriptions, control labels, buttons, nudges) is localized in place, not duplicated: client components call `useLocale()` (`src/lib/use-locale.ts`) and read from a component-local `STRINGS = { en, zh }` dict; server components take an optional `locale` prop passed from the /zh page. The `en` dict must keep the exact original strings so English pages stay byte-identical. New user-visible chrome strings MUST ship both variants. Chart-internal rendering (D3 axes/tooltips/legend series, CSV export) and data-registry display values (model/GPU/framework/precision names) stay English.
6. **Compare slug narrative sync**: the per-slug compare pages are mirrored at `/zh/compare/[slug]` and `/zh/compare-per-dollar/[slug]`; their Chinese prose templates live in `src/lib/compare-ssr-zh.ts`, a 1:1 port of the English templates in `compare-ssr.ts`. Any PR that changes the English narrative templates MUST update the zh port in the same commit.
7. **Intentionally not mirrored** (skip these, or add them to `ZH_MIRRORED_ROUTES` when you do mirror them): `/datasets`, feature-gated tabs (`ai-chart`, `current-inferencex-image`, `feedback`), `feed.xml`/`llms.txt`, and per-post OG images (Chinese posts reuse the English post's OG image — the OG renderer's font has no CJK glyphs).

## Chart Interpolation — TS and Python Helpers MUST Stay in Sync

The blog-writing workflow (`.claude/skills/write-inferencex-blog/`) ships a Python port of the chart's interpolation algorithm at `.claude/skills/write-inferencex-blog/iso_interactivity.py`. It exists so iso-interactivity tables in blog posts produce **exactly the same numbers** readers see when they hover the rendered chart. Linear-interpolation shell scripts will produce visibly different values — Cursor Bugbot has flagged this on prior posts.
Expand Down Expand Up @@ -197,7 +213,8 @@ Authoritative total / active parameter counts for every model in the dashboard.

1. Create `packages/app/content/blog/<slug>.mdx` with frontmatter: `title`, `subtitle`, `date` (required), `tags`, `modifiedDate` (optional)
2. Write content using Markdown + custom MDX components (`Figure`, `Blur`)
3. No code changes needed — the post automatically appears in the blog list, sitemap, RSS feed, llms.txt, and gets a generated OG image
3. Create the Simplified Chinese translation at `packages/app/content/blog/zh/<slug>.mdx` (**required** — see [Chinese Website Pages](#chinese-website-pages-zh--mandatory-for-all-indexable-surfaces))
4. No code changes needed — the post automatically appears in the blog list, sitemap, RSS feed, llms.txt, and gets a generated OG image; the zh file appears on `/zh/blog` with hreflang pairing

See [Blog](./docs/blog.md) for content format, available MDX components, and design details.

Expand All @@ -222,6 +239,7 @@ See [Blog](./docs/blog.md) for content format, available MDX components, and des
2. Create a per-section context provider (see `InferenceContext.tsx`, `EvaluationContext.tsx` for patterns)
3. Use `ChartLegend` with `variant="sidebar"`, sorted by `HW_REGISTRY` sort order, default expanded
4. Analytics: all interactive elements use `track()` with `{tabname}_` prefix
5. Create the Chinese sibling: extend `src/lib/tab-meta-zh.ts` dictionaries and add `src/app/zh/(dashboard)/<tab>/page.tsx` (see [Chinese Website Pages](#chinese-website-pages-zh--mandatory-for-all-indexable-surfaces))

### Bumping dependencies

Expand Down
30 changes: 30 additions & 0 deletions docs/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Chinese Pages (/zh)

Why the Simplified Chinese site is a hand-authored `/zh` page tree instead of an i18n framework, and how the pieces fit together. The authoring rules (what you MUST do when adding a page/tab/post) live in [AGENTS.md — Chinese Website Pages](../AGENTS.md#chinese-website-pages-zh--mandatory-for-all-indexable-surfaces); this doc covers the design rationale.

## Why hand-authored pages, not next-intl / `[locale]` routing

- **SEO is the goal, not full UI translation.** The objective is Chinese pages that crawlers can index: Chinese titles, meta descriptions, server-rendered Chinese content, and bidirectional hreflang. The interactive dashboard (charts, filters, tooltips) stays English — model/GPU/framework names are English-first terms in Chinese ML writing anyway, and translating deep chart UI would touch hundreds of client components for little search value.
- **A `[locale]` root segment would move every route** and force middleware rewrites to keep existing URLs stable — high blast radius for a two-locale site. A parallel `/zh` tree adds pages without touching English URLs.
- **No message-catalog indirection.** With exactly two locales and mostly page-level content, colocated `STRINGS = { en, zh }` dictionaries (landing page, quotes content) and dedicated zh files (`tab-meta-zh.ts`, `faq-data-zh.ts`) are easier to review than a parallel key-file hierarchy, and dead strings die with their page.

## Architecture

| Piece | Where | Notes |
| ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| URL mapping + hreflang | `src/lib/i18n.ts` | `zhPath`, `enAlternates`/`zhAlternates` (both sides emit the same `languages` map, `x-default` = English), `ZH_MIRRORED_ROUTES` (single source of truth for "which routes have a zh sibling"), `switchLocalePath` for the header toggle |
| Tab dictionaries | `src/lib/tab-meta-zh.ts` | `TAB_META_ZH` (metadata), `TAB_INTRO_ZH` (server-rendered intro), `TAB_LABELS_ZH` (TabNav), `NAV_LABELS_ZH` (header). Completeness enforced by `tab-meta-zh.test.ts` |
| zh page tree | `src/app/zh/**` | Mirrors the English tree; dashboard pages render `<ZhTabIntro>` above the same chart components |
| Blog translations | `content/blog/zh/<slug>.mdx` | Same filename pairs the languages. Visibility gating (publishDate) always derives from the **English** post so a translation can never publish early; title/subtitle/reading time come from the zh file |
| Locale-aware chrome | `header.tsx`, `tab-nav.tsx`, `footer.tsx` | Client components detect `/zh` from `usePathname()` — no prop drilling, no context |

## Non-obvious decisions

- **`<html lang>`**: the root layout hardcodes `lang="en"` and Next.js cannot override it per segment without multiple root layouts. The zh layout wraps content in `<div lang="zh-CN">` (valid, scopes language for crawlers/AT) and `SetDocumentLang` fixes `document.documentElement.lang` after hydration. Google detects language from content, not the attribute, so this is not an SEO problem.
- **Slugs stay English.** zh posts keep the English filename/slug — `slugify()` would previously have destroyed CJK slugs, and shared slugs are what pair the two languages for hreflang. `slugify()` now _preserves_ Han characters, but only so Chinese _headings_ get meaningful anchor ids (`extractHeadings` and the MDX heading renderer share it).
- **Reading time is CJK-aware**: `getReadingTime` counts Han characters at 400 chars/min alongside Latin words at 265 wpm; pure word-splitting counts an entire Chinese paragraph as ~1 "word".
- **zh OG images reuse the English post meta** — the `next/og` default Satori font has no CJK glyphs, so a Chinese title would render as tofu. Loading a subset CJK font is a known follow-up.
- **`/zh/inference` canonicalizes to `/zh`**, mirroring the English quirk where `/inference` canonicalizes to `/`.
- **Shared chrome is localized in place** via `useLocale()` + component-local `STRINGS = { en, zh }` dicts (footer, TabNav, dashboard display headings/labels, nudges, preset cards). The `en` dict keeps the exact original strings so English pages are byte-identical; chart-internal rendering and data-registry display values stay English.
- **Compare slug pages are mirrored** at `/zh/compare/[slug]` and `/zh/compare-per-dollar/[slug]`. The Chinese narrative templates live in `compare-ssr-zh.ts` as a 1:1 port of `compare-ssr.ts` (data logic is imported, only sentence templates differ) — the two files must change together.
- **Sitemap pairs**: `localizedPair()` in `sitemap.ts` emits the EN and zh URL together, both carrying the same `alternates.languages` map. Blog posts without a translation fall back to an English-only entry, so a missing translation degrades gracefully instead of 404-ing crawlers.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Design rationale and non-obvious conventions. See [CLAUDE.md](../CLAUDE.md) for
- [Data Transforms](./data-transforms.md) — Full pipeline from BenchmarkRow to RenderableGraph: type hierarchy, hardware key construction, derived metrics, memoization strategy
- [State Ownership](./state-ownership.md) — Which context owns which state, availability filtering cascade, comparison date mechanics, URL param sync
- [Blog](./blog.md) — MDX content system, SEO features (OG images, RSS, llms.txt, JSON-LD), TOC sidebar, reading progress, heading links, analytics events
- [Chinese Pages (/zh)](./i18n.md) — Why hand-authored /zh pages instead of an i18n framework, hreflang pairing, blog translation pairing, html lang workaround, CJK reading time/slugs
Loading
Loading