From 7b73b58904bd0093e2e9a8b225a279de2d0a2ff5 Mon Sep 17 00:00:00 2001 From: functionstackx <47992694+functionstackx@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:19:38 -0400 Subject: [PATCH] feat(compare): Chinese (zh) locale for /compare and /compare-per-dollar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Chinese-language variant of the compare master + detail pages, served under a /zh URL prefix. No i18n framework existed, so this uses parallel routes that reuse the same React components with a `lang` prop and a Chinese dictionary — English routes are untouched and byte-identical. New URLs: /zh/compare /zh/compare/ /zh/compare-per-dollar /zh/compare-per-dollar/ What's translated (per scope decision — "compare copy only"): - Index headings, lede, vendor-group descriptions, per-dollar CTA - Detail eyebrow, description, H1, empty states, cross-links, pricing line, figcaption, caveat - SSR narrative prose (full Chinese template pools, 1:1 with the English pools) - Interpolated-table header + metric display labels (keys stay English so filtering/React keys are language-independent) - SEO metadata (title/description) + hreflang alternates, JSON-LD ItemList / Dataset / BreadcrumbList human-readable fields The embedded interactive chart (shared app-wide InferenceChartDisplay) stays English, as agreed. Implementation: - lib/compare/i18n.ts: Lang type, locale path helpers, en/zh dictionary - lib/compare-ssr.ts: threaded `lang` through formatModelList, compareTableNarrative (+ Chinese template pools), buildJsonLd, buildBreadcrumbJsonLd — English output unchanged when lang='en' - Extracted shared server views (index-view, full-detail-view, per-dollar-detail-view) so en + zh routes share slug parsing, the 308 canonicalization redirect (now locale-aware), the benchmark fetch, and the SSR interpolation. English route files are now thin wrappers. - Moved the two page-client components into lib/compare/ and made them + compare-interpolated-table lang-aware (dict for simple strings, inline JSX branch for the few markup-rich sentences). - Reused the canonical English performance-per-dollar.png hero/OG graphic for zh (language-neutral data graphic) rather than duplicating the ~500-line Satori route. - sitemap.ts: emit the zh index + detail URLs. Tests: added cypress/e2e/compare-zh.cy.ts (zh index + detail + per-dollar: localized copy, table, chart mount, locale-correct cross-links). Verified: prod build registers all four /zh routes (dynamic); zh + en index pages render at runtime; zh alias slug 308-redirects to the canonical zh URL; all 2006 app unit tests pass; typecheck + lint clean. (Detail-page data fetch needs the Vercel Blob token, absent locally — that path 500s identically for en and zh, so it's an env limitation, exercised by Cypress in CI.) Note: pages render under the root layout's ; per-page html lang isn't overridable without a root-layout change. hreflang alternates carry the locale signal for crawlers instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/app/cypress/e2e/compare-zh.cy.ts | 87 ++++++ .../app/compare-per-dollar/[slug]/page.tsx | 174 +---------- .../app/src/app/compare-per-dollar/page.tsx | 147 +--------- packages/app/src/app/compare/[slug]/page.tsx | 179 +----------- packages/app/src/app/compare/page.tsx | 157 +--------- packages/app/src/app/sitemap.ts | 27 ++ .../app/zh/compare-per-dollar/[slug]/page.tsx | 20 ++ .../src/app/zh/compare-per-dollar/layout.tsx | 17 ++ .../src/app/zh/compare-per-dollar/page.tsx | 11 + .../app/src/app/zh/compare/[slug]/page.tsx | 20 ++ packages/app/src/app/zh/compare/layout.tsx | 18 ++ packages/app/src/app/zh/compare/page.tsx | 11 + .../compare/compare-interpolated-table.tsx | 36 ++- packages/app/src/lib/compare-ssr.ts | 203 +++++++++++-- .../app/src/lib/compare/full-detail-view.tsx | 191 ++++++++++++ .../compare/full-page-client.tsx} | 61 ++-- packages/app/src/lib/compare/i18n.ts | 271 ++++++++++++++++++ packages/app/src/lib/compare/index-view.tsx | 187 ++++++++++++ .../lib/compare/per-dollar-detail-view.tsx | 199 +++++++++++++ .../compare/per-dollar-page-client.tsx} | 96 ++++--- 20 files changed, 1381 insertions(+), 731 deletions(-) create mode 100644 packages/app/cypress/e2e/compare-zh.cy.ts create mode 100644 packages/app/src/app/zh/compare-per-dollar/[slug]/page.tsx create mode 100644 packages/app/src/app/zh/compare-per-dollar/layout.tsx create mode 100644 packages/app/src/app/zh/compare-per-dollar/page.tsx create mode 100644 packages/app/src/app/zh/compare/[slug]/page.tsx create mode 100644 packages/app/src/app/zh/compare/layout.tsx create mode 100644 packages/app/src/app/zh/compare/page.tsx create mode 100644 packages/app/src/lib/compare/full-detail-view.tsx rename packages/app/src/{app/compare/[slug]/page-client.tsx => lib/compare/full-page-client.tsx} (76%) create mode 100644 packages/app/src/lib/compare/i18n.ts create mode 100644 packages/app/src/lib/compare/index-view.tsx create mode 100644 packages/app/src/lib/compare/per-dollar-detail-view.tsx rename packages/app/src/{app/compare-per-dollar/[slug]/page-client.tsx => lib/compare/per-dollar-page-client.tsx} (72%) diff --git a/packages/app/cypress/e2e/compare-zh.cy.ts b/packages/app/cypress/e2e/compare-zh.cy.ts new file mode 100644 index 00000000..8657f475 --- /dev/null +++ b/packages/app/cypress/e2e/compare-zh.cy.ts @@ -0,0 +1,87 @@ +/** + * Chinese (`/zh/compare*`) locale smoke tests. Verifies the Chinese variants of + * the compare master + detail pages render localized copy, keep the interactive + * chart + interpolated table working, and cross-link within the zh locale. + */ +describe('Compare — Chinese (zh) locale', () => { + const SLUG = 'deepseek-r1-gb200-vs-h100'; + + beforeEach(() => { + cy.window().then((win) => { + win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); + }); + }); + + describe('master index pages', () => { + it('renders the Chinese GPU comparisons index', () => { + cy.visit('/zh/compare'); + cy.get('h1').should('contain.text', 'GPU 对比'); + // Localized per-dollar CTA links within the zh locale. + cy.get('[data-testid="compare-index-per-dollar-link"]') + .should('contain.text', '对比 GPU 每美元性能') + .and('have.attr', 'href') + .and('match', /^\/zh\/compare-per-dollar/u); + }); + + it('renders the Chinese performance-per-dollar index', () => { + cy.visit('/zh/compare-per-dollar'); + cy.get('h1').should('contain.text', 'GPU 每美元性能'); + }); + }); + + describe('full detail page', () => { + before(() => { + cy.window().then((win) => { + win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); + }); + cy.visit(`/zh/compare/${SLUG}`); + cy.get('[data-testid="compare-interpolated-table"]').should('exist'); + }); + + it('shows the localized eyebrow and description', () => { + cy.contains('GPU 对比').should('exist'); + cy.contains('AI 推理基准对比').should('exist'); + }); + + it('renders the interpolated table with localized metric labels', () => { + cy.get('[data-testid="compare-interpolated-table"]').should('be.visible'); + cy.get('[data-testid="compare-interpolated-table"]').should('contain.text', '并发数'); + // GPU labels stay in their original (language-neutral) form. + cy.get('[data-testid="compare-interpolated-table"] tbody td').should('contain.text', 'GB200'); + }); + + it('mounts the interactive chart', () => { + cy.get('[data-testid="scatter-graph"]').should('exist'); + }); + + it('cross-links to the zh per-dollar view', () => { + cy.contains('a', '查看每美元性能视图') + .should('have.attr', 'href') + .and('match', new RegExp(`^/zh/compare-per-dollar/${SLUG}$`, 'u')); + }); + }); + + describe('per-dollar detail page', () => { + before(() => { + cy.window().then((win) => { + win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); + }); + cy.visit(`/zh/compare-per-dollar/${SLUG}`); + cy.get('[data-testid="compare-interpolated-table"]').should('exist'); + }); + + it('shows the localized heading and cost-row label', () => { + cy.get('h1').should('contain.text', '每美元性能'); + cy.get('[data-testid="compare-interpolated-table"]').should( + 'contain.text', + '每百万 Token 美元成本', + ); + }); + + it('cross-links to the zh full comparison view', () => { + cy.contains('a', '查看完整的延迟 + 吞吐量对比') + .should('have.attr', 'href') + .and('match', new RegExp(`^/zh/compare/${SLUG}$`, 'u')); + }); + }); +}); diff --git a/packages/app/src/app/compare-per-dollar/[slug]/page.tsx b/packages/app/src/app/compare-per-dollar/[slug]/page.tsx index b5feb98c..a6b4a2a5 100644 --- a/packages/app/src/app/compare-per-dollar/[slug]/page.tsx +++ b/packages/app/src/app/compare-per-dollar/[slug]/page.tsx @@ -1,32 +1,6 @@ import type { Metadata } from 'next'; -import { notFound, permanentRedirect } from 'next/navigation'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; - -import { JsonLd } from '@/components/json-ld'; -import { pickPairDefaults } from '@/lib/compare-pair-defaults'; -import { - canonicalCompareSlug, - compareDisplayLabel, - compareModelDisplayLabel, - parseCompareSlug, -} from '@/lib/compare-slug'; -import { getGpuSpecs } from '@/lib/constants'; -import { - buildBreadcrumbJsonLd, - buildJsonLd, - compareTableNarrative, - computeCompareTableData, - dateRangeForPair, - getCachedBenchmarks, - KNOWN_MODELS, - KNOWN_PRECISIONS, - KNOWN_SEQUENCES, - pickString, - summarize, -} from '@/lib/compare-ssr'; - -import ComparePerDollarPageClient from './page-client'; +import PerDollarDetailView, { perDollarDetailMetadata } from '@/lib/compare/per-dollar-detail-view'; export const dynamic = 'force-dynamic'; @@ -37,150 +11,10 @@ interface Props { export async function generateMetadata({ params }: Props): Promise { const { slug } = await params; - const parsed = parseCompareSlug(slug); - if (!parsed) return {}; - const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); - const gpuLabel = compareDisplayLabel(parsed.a, parsed.b); - const url = `${SITE_URL}/compare-per-dollar/${canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b)}`; - // Description weaves the user-named SEO terms — "performance per dollar", - // "performance normalized by cost", "dollars per million tokens" — without - // keyword-stuffing. - const description = `${parsed.model.label} cost per million tokens on ${gpuLabel}. Performance normalized by owning-hyperscaler TCO — see which GPU delivers more inference dollars-per-token at every interactivity level.`; - return { - title: `${fullLabel} — Performance per Dollar`, - description, - alternates: { canonical: url }, - openGraph: { - title: `${fullLabel} — Performance per Dollar | ${SITE_NAME}`, - description, - url, - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: `${fullLabel} — Performance per Dollar`, - description, - }, - }; + return perDollarDetailMetadata(slug, 'en'); } export default async function ComparePerDollarPage({ params, searchParams }: Props) { - const { slug } = await params; - const parsed = parseCompareSlug(slug); - if (!parsed) notFound(); - - const sp = await searchParams; - - // Same one-hop 308 normalization as /compare/[slug] — bare-slug fallback, - // alias model resolution, GPU alphabetical order — but redirect target lives - // under /compare-per-dollar/. Query string is preserved across the hop. - const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); - // canonical is always lowercase; compare against lowercased input so mixed-case - // URLs don't emit a fresh 308 + CDN cache entry every hit. - if (canonical !== slug.toLowerCase()) { - const qs = Object.entries(sp) - .flatMap(([k, v]) => { - if (Array.isArray(v)) return v.map((vv) => [k, vv] as const); - if (v === undefined) return []; - return [[k, v] as const]; - }) - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join('&'); - permanentRedirect(`/compare-per-dollar/${canonical}${qs ? `?${qs}` : ''}`); - } - - const rows = await getCachedBenchmarks(parsed.model.dbKeys); - const summaryA = summarize(rows, parsed.a); - const summaryB = summarize(rows, parsed.b); - const { sequence: pickedSequence, precision: pickedPrecision } = pickPairDefaults( - rows, - parsed.a, - parsed.b, - ); - - const urlSeq = pickString(sp.i_seq); - const urlPrec = pickString(sp.i_prec); - const urlModel = pickString(sp.g_model); - const effectiveSequence = urlSeq && KNOWN_SEQUENCES.has(urlSeq) ? urlSeq : pickedSequence; - const effectivePrecision = urlPrec && KNOWN_PRECISIONS.has(urlPrec) ? urlPrec : pickedPrecision; - const effectiveModel = - urlModel && KNOWN_MODELS.has(urlModel) ? urlModel : parsed.model.displayName; - - const { defaultTargets, ssrRows, interactivityRange } = computeCompareTableData( - rows, - parsed.a, - parsed.b, - effectiveSequence, - effectivePrecision, - ); - - const url = `${SITE_URL}/compare-per-dollar/${canonical}`; - const imageUrl = `${url}/performance-per-dollar.png`; - const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b); - const jsonLd = buildJsonLd( - 'per-dollar', - parsed.model, - parsed.a, - parsed.b, - url, - summaryA, - summaryB, - ssrRows, - imageUrl, - oldest, - newest, - parsed.model.displayName, - ); - const breadcrumbJsonLd = buildBreadcrumbJsonLd( - 'per-dollar', - compareModelDisplayLabel(parsed.model, parsed.a, parsed.b), - url, - ); - const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); - const aMeta = HW_REGISTRY[parsed.a]; - const bMeta = HW_REGISTRY[parsed.b]; - const aLabel = aMeta?.label ?? parsed.a.toUpperCase(); - const bLabel = bMeta?.label ?? parsed.b.toUpperCase(); - const narrative = compareTableNarrative( - 'per-dollar', - parsed.model.label, - aLabel, - bLabel, - ssrRows, - interactivityRange, - ); - // Owning-hyperscaler $/GPU/hr — the same `costh` value the per-dollar math - // upstream uses to derive cost per million tokens. Rendered in the header - // so the reader can audit the underlying pricing inputs without leaving - // the page. - const aCostPerGpuHr = getGpuSpecs(parsed.a).costh; - const bCostPerGpuHr = getGpuSpecs(parsed.b).costh; - - return ( - <> - - - - - ); + const [{ slug }, sp] = await Promise.all([params, searchParams]); + return ; } diff --git a/packages/app/src/app/compare-per-dollar/page.tsx b/packages/app/src/app/compare-per-dollar/page.tsx index 56ccc4fd..5ced177c 100644 --- a/packages/app/src/app/compare-per-dollar/page.tsx +++ b/packages/app/src/app/compare-per-dollar/page.tsx @@ -1,150 +1,11 @@ import type { Metadata } from 'next'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; - -import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link'; -import { JsonLd } from '@/components/json-ld'; -import { Card } from '@/components/ui/card'; -import { getComparablePairsByModelSlug } from '@/lib/compare-availability'; -import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug'; -import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr'; +import CompareIndexView, { compareIndexMetadata } from '@/lib/compare/index-view'; export const dynamic = 'force-dynamic'; -const DESCRIPTION = - 'GPU performance per dollar — head-to-head cost per million tokens across every model and hardware pair we benchmark. Performance normalized by owning-hyperscaler TCO for DeepSeek V4 Pro 1.6T, DeepSeek R1, Kimi K2.5/K2.6 1T, GLM 5/5.1, Qwen 3.5 397B-A17B, and more. Pick the cheapest SKU for your workload.'; - -export const metadata: Metadata = { - title: 'GPU Performance per Dollar', - description: DESCRIPTION, - alternates: { canonical: `${SITE_URL}/compare-per-dollar` }, - openGraph: { - title: `GPU Performance per Dollar | ${SITE_NAME}`, - description: DESCRIPTION, - url: `${SITE_URL}/compare-per-dollar`, - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: `GPU Performance per Dollar | ${SITE_NAME}`, - description: DESCRIPTION, - }, -}; - -interface VendorGroup { - heading: string; - description: string; - pairs: { a: string; b: string; slug: string; label: string }[]; -} - -function groupPairsByVendorForModel( - model: CompareModelSlug, - comparablePairs: ComparePair[], -): VendorGroup[] { - const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs); - const groups: VendorGroup[] = []; - if (cross.length > 0) { - groups.push({ - heading: 'NVIDIA vs AMD', - description: 'Cross-vendor cost-per-token comparisons across architecture generations.', - pairs: cross, - }); - } - if (nvidia.length > 0) { - groups.push({ - heading: 'NVIDIA vs NVIDIA', - description: 'Hopper and Blackwell generation cost-per-token comparisons.', - pairs: nvidia, - }); - } - if (amd.length > 0) { - groups.push({ - heading: 'AMD vs AMD', - description: 'CDNA 3 and CDNA 4 generation cost-per-token comparisons.', - pairs: amd, - }); - } - return groups; -} - -const jsonLd = { - '@context': 'https://schema.org', - '@type': 'CollectionPage', - name: `GPU Performance per Dollar | ${SITE_NAME}`, - description: DESCRIPTION, - url: `${SITE_URL}/compare-per-dollar`, -}; - -export default async function ComparePerDollarIndexPage() { - // Server-side filter (Neon availability): only show (model, pair) combos - // where both GPUs have benchmark data for that model. Matches the /compare - // index's behavior — no empty-state cards in navigation. The page-level - // handler at /compare-per-dollar/[slug] still renders the empty-state for - // direct URL hits. - const comparablePairsByModel = await getComparablePairsByModelSlug(); - const totalUrls = [...comparablePairsByModel.values()].reduce((s, p) => s + p.length, 0); - const modelsWithPairs = COMPARE_MODEL_SLUGS.filter( - (m) => (comparablePairsByModel.get(m.slug)?.length ?? 0) > 0, - ); - - return ( - <> - -
- -

- GPU Performance per Dollar -

-

- {totalUrls.toLocaleString()} head-to-head cost-per-million-tokens comparisons across{' '} - {formatModelList(modelsWithPairs)}. Performance normalized by owning-hyperscaler TCO — - each page renders the cost-per-token chart and an interpolated dollars-per-million - comparison table so you can pick the cheaper SKU at any target interactivity level. -

-
-
+export const metadata: Metadata = compareIndexMetadata('per-dollar', 'en'); - {modelsWithPairs.map((model) => { - const pairs = comparablePairsByModel.get(model.slug) ?? []; - const groups = groupPairsByVendorForModel(model, pairs); - return ( -
- -
-

{model.label}

-

- {pairs.length} GPU pair{pairs.length === 1 ? '' : 's'} with cost-per-token - benchmark data on {model.label}. -

-
- {groups.map((group) => ( -
-
-

{group.heading}

-

{group.description}

-
-
- {group.pairs.map(({ slug, label, a, b }) => { - const aMeta = HW_REGISTRY[a]; - const bMeta = HW_REGISTRY[b]; - const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`; - return ( - - ); - })} -
-
- ))} -
-
- ); - })} - - ); +export default function ComparePerDollarIndexPage() { + return ; } diff --git a/packages/app/src/app/compare/[slug]/page.tsx b/packages/app/src/app/compare/[slug]/page.tsx index b957028e..65e45730 100644 --- a/packages/app/src/app/compare/[slug]/page.tsx +++ b/packages/app/src/app/compare/[slug]/page.tsx @@ -1,31 +1,6 @@ import type { Metadata } from 'next'; -import { notFound, permanentRedirect } from 'next/navigation'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; - -import { JsonLd } from '@/components/json-ld'; -import { pickPairDefaults } from '@/lib/compare-pair-defaults'; -import { - canonicalCompareSlug, - compareDisplayLabel, - compareModelDisplayLabel, - parseCompareSlug, -} from '@/lib/compare-slug'; -import { - buildBreadcrumbJsonLd, - buildJsonLd, - compareTableNarrative, - computeCompareTableData, - dateRangeForPair, - getCachedBenchmarks, - KNOWN_MODELS, - KNOWN_PRECISIONS, - KNOWN_SEQUENCES, - pickString, - summarize, -} from '@/lib/compare-ssr'; - -import ComparePageClient from './page-client'; +import FullDetailView, { fullDetailMetadata } from '@/lib/compare/full-detail-view'; export const dynamic = 'force-dynamic'; @@ -36,156 +11,10 @@ interface Props { export async function generateMetadata({ params }: Props): Promise { const { slug } = await params; - const parsed = parseCompareSlug(slug); - if (!parsed) return {}; - const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); - const gpuLabel = compareDisplayLabel(parsed.a, parsed.b); - const url = `${SITE_URL}/compare/${canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b)}`; - const description = `Head-to-head GPU inference benchmark comparison for ${parsed.model.label}: ${gpuLabel}. Latency, throughput, and cost across LLM workloads.`; - return { - title: `${fullLabel} Inference Benchmark`, - description, - alternates: { canonical: url }, - openGraph: { - title: `${fullLabel} | ${SITE_NAME}`, - description, - url, - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: `${fullLabel} Inference Benchmark`, - description, - }, - }; + return fullDetailMetadata(slug, 'en'); } export default async function ComparePage({ params, searchParams }: Props) { - const { slug } = await params; - const parsed = parseCompareSlug(slug); - if (!parsed) notFound(); - - // Await searchParams once so we can both preserve them on redirect and read - // them for URL-param overrides further down. - const sp = await searchParams; - - // One-hop redirect to the fully canonical URL. Handles all three normalization - // cases in a single 308: - // - legacy bare slug: `h100-vs-h200` → `deepseek-r1-h100-vs-h200` - // - alias model: `kimi-h100-vs-h200` → `kimi-k26-h100-vs-h200` - // - non-canonical GPUs: `kimi-k26-h200-vs-h100` → `kimi-k26-h100-vs-h200` - // - any combination of the above - // Preserves the query string so `?i_seq=1k/1k&i_prec=fp8` etc. survive the - // redirect — the original PR #351 redirect dropped these, but with bare slugs - // now redirecting unconditionally we need to keep them. - const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); - // canonical is always lowercase; compare against lowercased input so mixed-case - // URLs (e.g. /compare/H100-vs-H200) don't emit a fresh 308 + CDN cache entry - // every hit when they actually match the canonical content. - if (canonical !== slug.toLowerCase()) { - const qs = Object.entries(sp) - .flatMap(([k, v]) => { - if (Array.isArray(v)) return v.map((vv) => [k, vv] as const); - if (v === undefined) return []; - return [[k, v] as const]; - }) - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join('&'); - // 308 (not 307): bare-slug, alias model, and non-canonical GPU order are - // all permanent decisions — using a permanent redirect lets search engines - // consolidate link equity onto the canonical URL instead of keeping the - // alias URL in the index alongside the canonical one. - permanentRedirect(`/compare/${canonical}${qs ? `?${qs}` : ''}`); - } - - const rows = await getCachedBenchmarks(parsed.model.dbKeys); - const summaryA = summarize(rows, parsed.a); - const summaryB = summarize(rows, parsed.b); - const { sequence: pickedSequence, precision: pickedPrecision } = pickPairDefaults( - rows, - parsed.a, - parsed.b, - ); - - // URL params win over slug-derived defaults; this baking-into-SSR avoids the - // hydration flash where the client upgrades seeded defaults to URL values. - // `sp` was already awaited above for the redirect-query-preservation path. - const urlSeq = pickString(sp.i_seq); - const urlPrec = pickString(sp.i_prec); - const urlModel = pickString(sp.g_model); - const effectiveSequence = urlSeq && KNOWN_SEQUENCES.has(urlSeq) ? urlSeq : pickedSequence; - const effectivePrecision = urlPrec && KNOWN_PRECISIONS.has(urlPrec) ? urlPrec : pickedPrecision; - // `?g_model=` is honored only if it matches a known model — but the slug's - // model is the canonical default. Disregard URL param if user wants to - // explicitly override (rare). - const effectiveModel = - urlModel && KNOWN_MODELS.has(urlModel) ? urlModel : parsed.model.displayName; - - const { defaultTargets, ssrRows, interactivityRange } = computeCompareTableData( - rows, - parsed.a, - parsed.b, - effectiveSequence, - effectivePrecision, - ); - - const url = `${SITE_URL}/compare/${canonical}`; - const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b); - const jsonLd = buildJsonLd( - 'full', - parsed.model, - parsed.a, - parsed.b, - url, - summaryA, - summaryB, - ssrRows, - undefined, - oldest, - newest, - parsed.model.displayName, - ); - const breadcrumbJsonLd = buildBreadcrumbJsonLd( - 'full', - compareModelDisplayLabel(parsed.model, parsed.a, parsed.b), - url, - ); - const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); - const aMeta = HW_REGISTRY[parsed.a]; - const bMeta = HW_REGISTRY[parsed.b]; - const aLabel = aMeta?.label ?? parsed.a.toUpperCase(); - const bLabel = bMeta?.label ?? parsed.b.toUpperCase(); - const narrative = compareTableNarrative( - 'full', - parsed.model.label, - aLabel, - bLabel, - ssrRows, - interactivityRange, - ); - - return ( - <> - - - - - ); + const [{ slug }, sp] = await Promise.all([params, searchParams]); + return ; } diff --git a/packages/app/src/app/compare/page.tsx b/packages/app/src/app/compare/page.tsx index 38baf3b9..ef420b59 100644 --- a/packages/app/src/app/compare/page.tsx +++ b/packages/app/src/app/compare/page.tsx @@ -1,160 +1,11 @@ import type { Metadata } from 'next'; -import Link from 'next/link'; -import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; - -import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link'; -import { JsonLd } from '@/components/json-ld'; -import { Card } from '@/components/ui/card'; -import { getComparablePairsByModelSlug } from '@/lib/compare-availability'; -import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug'; -import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr'; +import CompareIndexView, { compareIndexMetadata } from '@/lib/compare/index-view'; export const dynamic = 'force-dynamic'; -const DESCRIPTION = - 'Browse head-to-head GPU inference benchmark comparisons across every model and hardware pair we test. Latency, throughput, and cost for DeepSeek V4 Pro 1.6T, DeepSeek R1, Kimi K2.5/K2.6 1T, GLM 5/5.1, Qwen 3.5 397B-A17B, and more.'; - -export const metadata: Metadata = { - title: 'GPU Comparisons', - description: DESCRIPTION, - alternates: { canonical: `${SITE_URL}/compare` }, - openGraph: { - title: `GPU Comparisons | ${SITE_NAME}`, - description: DESCRIPTION, - url: `${SITE_URL}/compare`, - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: `GPU Comparisons | ${SITE_NAME}`, - description: DESCRIPTION, - }, -}; - -interface VendorGroup { - heading: string; - description: string; - pairs: { a: string; b: string; slug: string; label: string }[]; -} - -function groupPairsByVendorForModel( - model: CompareModelSlug, - comparablePairs: ComparePair[], -): VendorGroup[] { - const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs); - const groups: VendorGroup[] = []; - if (cross.length > 0) { - groups.push({ - heading: 'NVIDIA vs AMD', - description: 'Cross-vendor comparisons across architecture generations.', - pairs: cross, - }); - } - if (nvidia.length > 0) { - groups.push({ - heading: 'NVIDIA vs NVIDIA', - description: 'Hopper and Blackwell generation comparisons.', - pairs: nvidia, - }); - } - if (amd.length > 0) { - groups.push({ - heading: 'AMD vs AMD', - description: 'CDNA 3 and CDNA 4 generation comparisons.', - pairs: amd, - }); - } - return groups; -} - -const jsonLd = { - '@context': 'https://schema.org', - '@type': 'CollectionPage', - name: `GPU Comparisons | ${SITE_NAME}`, - description: DESCRIPTION, - url: `${SITE_URL}/compare`, -}; - -export default async function CompareIndexPage() { - // Server-side filter: only show (model, pair) combinations where both GPUs - // have benchmark data for that model. Avoids cards that would link to an - // empty-state page. The page-level handler at /compare/[slug] still renders - // the empty-state for direct URL hits, so this is purely a navigation - // hygiene concern. - const comparablePairsByModel = await getComparablePairsByModelSlug(); - const totalUrls = [...comparablePairsByModel.values()].reduce((s, p) => s + p.length, 0); - const modelsWithPairs = COMPARE_MODEL_SLUGS.filter( - (m) => (comparablePairsByModel.get(m.slug)?.length ?? 0) > 0, - ); - - return ( - <> - -
- -

GPU Comparisons

-

- {totalUrls.toLocaleString()} head-to-head inference benchmark comparisons across{' '} - {formatModelList(modelsWithPairs)}. Each page includes interactive charts for latency, - throughput, and cost metrics, plus an interpolated comparison table. -

-
- - Compare GPU performance per dollar - - -
-
-
+export const metadata: Metadata = compareIndexMetadata('full', 'en'); - {modelsWithPairs.map((model) => { - const pairs = comparablePairsByModel.get(model.slug) ?? []; - const groups = groupPairsByVendorForModel(model, pairs); - return ( -
- -
-

{model.label}

-

- {pairs.length} GPU pair{pairs.length === 1 ? '' : 's'} with benchmark data on{' '} - {model.label}. -

-
- {groups.map((group) => ( -
-
-

{group.heading}

-

{group.description}

-
-
- {group.pairs.map(({ slug, label, a, b }) => { - const aMeta = HW_REGISTRY[a]; - const bMeta = HW_REGISTRY[b]; - const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`; - return ( - - ); - })} -
-
- ))} -
-
- ); - })} - - ); +export default function CompareIndexPage() { + return ; } diff --git a/packages/app/src/app/sitemap.ts b/packages/app/src/app/sitemap.ts index d1717aa3..f2df09d0 100644 --- a/packages/app/src/app/sitemap.ts +++ b/packages/app/src/app/sitemap.ts @@ -3,6 +3,7 @@ import type { MetadataRoute } from 'next'; import { getAllPosts } from '@/lib/blog'; import { getAllComparableCompareSlugs } from '@/lib/compare-availability'; import { canonicalCompareSlug } from '@/lib/compare-slug'; +import { compareBasePath, compareSlugPath } from '@/lib/compare/i18n'; import { SITE_URL as BASE_URL } from '@semianalysisai/inferencex-constants'; const TABS = [ @@ -87,5 +88,31 @@ export default async function sitemap(): Promise { priority: 0.7, }; }), + // Chinese (zh-CN) compare locale — index + detail pages mirror the English + // routes under a /zh prefix. Slightly lower priority as the secondary locale. + { + url: `${BASE_URL}${compareBasePath('zh', 'full')}`, + lastModified: now, + changeFrequency: 'daily', + priority: 0.6, + }, + { + url: `${BASE_URL}${compareBasePath('zh', 'per-dollar')}`, + lastModified: now, + changeFrequency: 'daily', + priority: 0.6, + }, + ...compareSlugs.map(({ modelSlug, a, b }) => ({ + url: `${BASE_URL}${compareSlugPath('zh', 'full', canonicalCompareSlug(modelSlug, a, b))}`, + lastModified: now, + changeFrequency: 'daily' as const, + priority: 0.5, + })), + ...compareSlugs.map(({ modelSlug, a, b }) => ({ + url: `${BASE_URL}${compareSlugPath('zh', 'per-dollar', canonicalCompareSlug(modelSlug, a, b))}`, + lastModified: now, + changeFrequency: 'daily' as const, + priority: 0.5, + })), ]; } diff --git a/packages/app/src/app/zh/compare-per-dollar/[slug]/page.tsx b/packages/app/src/app/zh/compare-per-dollar/[slug]/page.tsx new file mode 100644 index 00000000..de5945fd --- /dev/null +++ b/packages/app/src/app/zh/compare-per-dollar/[slug]/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next'; + +import PerDollarDetailView, { perDollarDetailMetadata } from '@/lib/compare/per-dollar-detail-view'; + +export const dynamic = 'force-dynamic'; + +interface Props { + params: Promise<{ slug: string }>; + searchParams: Promise>; +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + return perDollarDetailMetadata(slug, 'zh'); +} + +export default async function ComparePerDollarPageZh({ params, searchParams }: Props) { + const [{ slug }, sp] = await Promise.all([params, searchParams]); + return ; +} diff --git a/packages/app/src/app/zh/compare-per-dollar/layout.tsx b/packages/app/src/app/zh/compare-per-dollar/layout.tsx new file mode 100644 index 00000000..2f3f6549 --- /dev/null +++ b/packages/app/src/app/zh/compare-per-dollar/layout.tsx @@ -0,0 +1,17 @@ +import { UnofficialRunProvider } from '@/components/unofficial-run-provider'; + +/** + * Chinese (`/zh/compare-per-dollar/*`) layout — mirrors + * `/compare-per-dollar/layout.tsx`. + */ +export default function ComparePerDollarZhLayout({ children }: { children: React.ReactNode }) { + return ( + +
+
+ {children} +
+
+
+ ); +} diff --git a/packages/app/src/app/zh/compare-per-dollar/page.tsx b/packages/app/src/app/zh/compare-per-dollar/page.tsx new file mode 100644 index 00000000..5da02823 --- /dev/null +++ b/packages/app/src/app/zh/compare-per-dollar/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; + +import CompareIndexView, { compareIndexMetadata } from '@/lib/compare/index-view'; + +export const dynamic = 'force-dynamic'; + +export const metadata: Metadata = compareIndexMetadata('per-dollar', 'zh'); + +export default function ComparePerDollarIndexPageZh() { + return ; +} diff --git a/packages/app/src/app/zh/compare/[slug]/page.tsx b/packages/app/src/app/zh/compare/[slug]/page.tsx new file mode 100644 index 00000000..dc335582 --- /dev/null +++ b/packages/app/src/app/zh/compare/[slug]/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next'; + +import FullDetailView, { fullDetailMetadata } from '@/lib/compare/full-detail-view'; + +export const dynamic = 'force-dynamic'; + +interface Props { + params: Promise<{ slug: string }>; + searchParams: Promise>; +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params; + return fullDetailMetadata(slug, 'zh'); +} + +export default async function ComparePageZh({ params, searchParams }: Props) { + const [{ slug }, sp] = await Promise.all([params, searchParams]); + return ; +} diff --git a/packages/app/src/app/zh/compare/layout.tsx b/packages/app/src/app/zh/compare/layout.tsx new file mode 100644 index 00000000..60220c0f --- /dev/null +++ b/packages/app/src/app/zh/compare/layout.tsx @@ -0,0 +1,18 @@ +import { UnofficialRunProvider } from '@/components/unofficial-run-provider'; + +/** + * Chinese (`/zh/compare/*`) layout — mirrors `/compare/layout.tsx`. Wraps pages + * in UnofficialRunProvider and a focused container (no DashboardShell/TabNav). + * The GlobalFilterProvider is mounted inside each page's client component. + */ +export default function CompareZhLayout({ children }: { children: React.ReactNode }) { + return ( + +
+
+ {children} +
+
+
+ ); +} diff --git a/packages/app/src/app/zh/compare/page.tsx b/packages/app/src/app/zh/compare/page.tsx new file mode 100644 index 00000000..611bb92f --- /dev/null +++ b/packages/app/src/app/zh/compare/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; + +import CompareIndexView, { compareIndexMetadata } from '@/lib/compare/index-view'; + +export const dynamic = 'force-dynamic'; + +export const metadata: Metadata = compareIndexMetadata('full', 'zh'); + +export default function CompareIndexPageZh() { + return ; +} diff --git a/packages/app/src/components/compare/compare-interpolated-table.tsx b/packages/app/src/components/compare/compare-interpolated-table.tsx index 91008250..1635b924 100644 --- a/packages/app/src/components/compare/compare-interpolated-table.tsx +++ b/packages/app/src/components/compare/compare-interpolated-table.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { interpolateForGPU } from '@/components/calculator/interpolation'; import type { GPUDataPoint, InterpolatedResult } from '@/components/calculator/types'; import { track } from '@/lib/analytics'; +import { type Lang, compareDict } from '@/lib/compare/i18n'; import { cn } from '@/lib/utils'; /** True when the field shows a positive finite number strictly outside [min, max]. */ @@ -43,6 +44,10 @@ export interface CompareInterpolatedTableProps { * 'Cost ($/M tok)' → 'Dollar per Million Tokens' so the page reads in full * English to match its "Performance per Dollar" framing. */ metricLabelOverrides?: Record; + /** UI language. Drives the header / help / metric-row display labels. The + * metric *keys* (used for filtering and React keys) stay English regardless; + * only the rendered text is localized. Defaults to English. */ + lang?: Lang; } interface ColumnData { @@ -97,14 +102,17 @@ export function CompareInterpolatedTable({ gpuDataPointsB, visibleMetricLabels, metricLabelOverrides, + lang = 'en', }: CompareInterpolatedTableProps) { - const metricsToRender = ( - visibleMetricLabels ? METRICS.filter((m) => visibleMetricLabels.includes(m.label)) : METRICS - ).map((m) => - metricLabelOverrides && metricLabelOverrides[m.label] - ? { ...m, label: metricLabelOverrides[m.label] } - : m, - ); + const t = compareDict(lang).table; + // Filter by the metric's stable English key, but render a localized (or + // per-route-overridden) display label. Keys stay English so filtering and + // React keys are language-independent. + const metricsToRender = visibleMetricLabels + ? METRICS.filter((m) => visibleMetricLabels.includes(m.label)) + : METRICS; + const displayLabelFor = (key: string): string => + metricLabelOverrides?.[key] ?? t.metricLabel[key] ?? key; const [columns, setColumns] = useState(() => defaultTargets.map((target, i) => ({ target, @@ -251,22 +259,19 @@ export function CompareInterpolatedTable({ return (
- Interpolated from real benchmark data. Edit target interactivity values below to compare at - different operating points. + {t.help}
{columns.map((col, ci) => ( {cells.map((cell, ci) => (
- Metric + {t.metricColumn}
- - Interactivity (tok/s/user) - + {t.interactivity}
- {metric.label} + {displayLabel} diff --git a/packages/app/src/lib/compare-ssr.ts b/packages/app/src/lib/compare-ssr.ts index e6e87dd7..495116b5 100644 --- a/packages/app/src/lib/compare-ssr.ts +++ b/packages/app/src/lib/compare-ssr.ts @@ -36,6 +36,7 @@ import { type CompareModelSlug, compareModelDisplayLabel, } from '@/lib/compare-slug'; +import { type Lang, compareBasePath } from '@/lib/compare/i18n'; import { getHardwareConfig, getGpuSpecs } from '@/lib/constants'; import { loadFixture } from '@/lib/test-fixtures'; @@ -556,6 +557,139 @@ const FULL_SINGLE_TEMPLATES: ((args: { `${i.presentLabel}: ${i.presentValue.toFixed(0)} tok/s/GPU, ${fmtCost(i.presentCost)} per million tokens at ${i.target} tok/s/user on ${i.modelLabel}. ${i.missingLabel} is unmeasured here.`, ]; +// --------------------------------------------------------------------------- +// Chinese (zh-CN) narrative templates — 1:1 with the English pools above so the +// `/zh/compare*` pages read the same operating points in natural Chinese. +// Technical units (tok/s/user, tok/s/GPU), currency, and product names stay in +// their original form; only the connective prose is translated. +// --------------------------------------------------------------------------- + +const ZH_BAND_PHRASE: Record<'low' | 'middle' | 'high', string> = { + low: '低端', + middle: '中段', + high: '高端', +}; + +function fullSummaryZh(i: FullBoth): string { + const costPart = i.costTied + ? '每 token 成本基本持平' + : i.costRatio === null + ? null + : `${i.cheaper} 每 token 成本低 ${fmtPctDelta(i.costRatio)}`; + const tputPart = i.tputTied + ? '每 GPU 吞吐量基本持平' + : i.tputRatio === null + ? null + : `${i.faster} 的每 GPU 吞吐量高出 ${fmtPctDelta(i.tputRatio)}`; + const both = [costPart, tputPart].filter(Boolean).join(';'); + return both.length > 0 ? both : '差距过小,难分高下'; +} + +const PER_DOLLAR_BOTH_TEMPLATES_ZH: ((i: PerDollarBoth) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel} 的每百万 token 成本为 ${fmtCost(i.aCost)},${i.bLabel} 为 ${fmtCost(i.bCost)}。在该工作点,${i.cheaper} 的成本效率高出 ${fmtPctDelta(i.ratio)}。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.cheaper} 略胜 ${i.pricier}——每百万 token ${fmtCost(i.cheaperCost)} 对 ${fmtCost(i.pricierCost)},每 token 成本相差 ${fmtPctDelta(i.ratio)}。`, + (i) => + `把 ${i.modelLabel} 推到 ${i.target} tok/s/user,${i.aLabel} 的每百万 token 成本落在 ${fmtCost(i.aCost)},而 ${i.bLabel} 为 ${fmtCost(i.bCost)}——${i.cheaper} 领先 ${fmtPctDelta(i.ratio)}。`, + (i) => + `${i.aLabel}:每百万 token ${fmtCost(i.aCost)}。${i.bLabel}:${fmtCost(i.bCost)}。两者都在 ${i.modelLabel} 上、${i.target} tok/s/user 时测得,其中 ${i.cheaper} 便宜 ${fmtPctDelta(i.ratio)}。`, + (i) => + `在 ${i.range} 交互速率区间的${ZH_BAND_PHRASE[i.band]}——即 ${i.target} tok/s/user——${i.aLabel} 在 ${i.modelLabel} 上的每百万 token 成本为 ${fmtCost(i.aCost)},${i.bLabel} 为 ${fmtCost(i.bCost)}。${i.cheaper} 是更便宜的选择,低 ${fmtPctDelta(i.ratio)}。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,按每百万 token 计算,${i.aLabel} 为 ${fmtCost(i.aCost)},${i.bLabel} 为 ${fmtCost(i.bCost)};${i.cheaper} 每美元能多产出 ${fmtPctDelta(i.ratio)} 的内容。`, +]; + +const PER_DOLLAR_TIED_TEMPLATES_ZH: ((i: PerDollarBoth) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel} 与 ${i.bLabel} 的每百万 token 成本相差不到 ~1%(${fmtCost(i.aCost)} 对 ${fmtCost(i.bCost)})——在该工作点可视为打平。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel} 每百万 token ${fmtCost(i.aCost)}、${i.bLabel} ${fmtCost(i.bCost)}:成本基本相同。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel}(${fmtCost(i.aCost)})与 ${i.bLabel}(${fmtCost(i.bCost)})的每百万 token 成本基本持平。`, +]; + +const PER_DOLLAR_ZERO_TEMPLATES_ZH: ((args: { + modelLabel: string; + aLabel: string; + bLabel: string; + target: number; + aCost: number; + bCost: number; +}) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel} 与 ${i.bLabel} 分别录得每百万 token ${fmtCost(i.aCost)} 与 ${fmtCost(i.bCost)}——其中一方缺少定价或吞吐数据,因此这里的同口径比值没有意义。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel}(${fmtCost(i.aCost)})与 ${i.bLabel}(${fmtCost(i.bCost)})的每百万 token 成本:至少有一项输入为零,因此无法用比值表达差距。`, +]; + +const PER_DOLLAR_SINGLE_TEMPLATES_ZH: ((args: { + modelLabel: string; + presentLabel: string; + missingLabel: string; + target: number; + presentCost: number; +}) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.presentLabel} 的每百万 token 成本为 ${fmtCost(i.presentCost)};我们在这一精确目标上没有 ${i.missingLabel} 的基准数据。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.presentLabel} 的每百万 token 成本为 ${fmtCost(i.presentCost)}。${i.missingLabel} 尚未在该工作点进行基准测试。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,只有 ${i.presentLabel} 有成本数据——每百万 token ${fmtCost(i.presentCost)}。${i.missingLabel} 在该目标上未做测量。`, +]; + +const FULL_BOTH_TEMPLATES_ZH: ((i: FullBoth) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、交互速率 ${i.target} tok/s/user 时,${i.aLabel} 提供 ${i.aValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.aCost)};${i.bLabel} 提供 ${i.bValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.bCost)}。在该工作点,${fullSummaryZh(i)}。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.aLabel} 录得 ${i.aValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.aCost)};${i.bLabel} 录得 ${i.bValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.bCost)}。${fullSummaryZh(i)}。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时的吞吐量:${i.aLabel} 达到 ${i.aValue.toFixed(0)} tok/s/GPU,${i.bLabel} 达到 ${i.bValue.toFixed(0)}。每百万 token 成本分别为 ${fmtCost(i.aCost)} 与 ${fmtCost(i.bCost)}。${fullSummaryZh(i)}。`, + (i) => + `${i.aLabel} / ${i.bLabel} 在 ${i.modelLabel} 上、${i.target} tok/s/user 时:${i.aValue.toFixed(0)} / ${i.bValue.toFixed(0)} tok/s/GPU,每百万 token ${fmtCost(i.aCost)} / ${fmtCost(i.bCost)}。${fullSummaryZh(i)}。`, + (i) => + `在 ${i.range} 交互速率区间的${ZH_BAND_PHRASE[i.band]}、即 ${i.modelLabel} 上 ${i.target} tok/s/user 时:${i.aLabel} 跑出 ${i.aValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.aCost)},${i.bLabel} 跑出 ${i.bValue.toFixed(0)}、每百万 token ${fmtCost(i.bCost)}。${fullSummaryZh(i)}。`, + (i) => + `以 ${i.modelLabel} 上 ${i.target} tok/s/user 为目标,${i.aLabel} 产出 ${i.aValue.toFixed(0)} tok/s/GPU(每百万 token ${fmtCost(i.aCost)}),${i.bLabel} 产出 ${i.bValue.toFixed(0)}(每百万 token ${fmtCost(i.bCost)})。${fullSummaryZh(i)}。`, +]; + +const FULL_SINGLE_TEMPLATES_ZH: ((args: { + modelLabel: string; + presentLabel: string; + missingLabel: string; + target: number; + presentValue: number; + presentCost: number; +}) => string)[] = [ + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.presentLabel} 提供 ${i.presentValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.presentCost)};${i.missingLabel} 尚未在该目标上进行基准测试。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.presentLabel} 达到 ${i.presentValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.presentCost)}。该工作点没有 ${i.missingLabel} 的数据。`, + (i) => + `在 ${i.modelLabel} 上、${i.target} tok/s/user 时,${i.presentLabel}:${i.presentValue.toFixed(0)} tok/s/GPU、每百万 token ${fmtCost(i.presentCost)}。${i.missingLabel} 在此未做测量。`, +]; + +/** Bundle the per-language template pools so `compareTableNarrative` can pick a + * set by `lang` without a forest of conditionals at each call site. */ +const NARRATIVE_POOLS = { + en: { + perDollarBoth: PER_DOLLAR_BOTH_TEMPLATES, + perDollarTied: PER_DOLLAR_TIED_TEMPLATES, + perDollarZero: PER_DOLLAR_ZERO_TEMPLATES, + perDollarSingle: PER_DOLLAR_SINGLE_TEMPLATES, + fullBoth: FULL_BOTH_TEMPLATES, + fullSingle: FULL_SINGLE_TEMPLATES, + }, + zh: { + perDollarBoth: PER_DOLLAR_BOTH_TEMPLATES_ZH, + perDollarTied: PER_DOLLAR_TIED_TEMPLATES_ZH, + perDollarZero: PER_DOLLAR_ZERO_TEMPLATES_ZH, + perDollarSingle: PER_DOLLAR_SINGLE_TEMPLATES_ZH, + fullBoth: FULL_BOTH_TEMPLATES_ZH, + fullSingle: FULL_SINGLE_TEMPLATES_ZH, + }, +} as const; + /** Pick template `rowIndex` in the rotation starting from a per-page hash * offset. Within a single page, paragraphs 0/1/2 always pick three * *consecutive* templates from the pool (never repeating each other), while @@ -593,9 +727,11 @@ export function compareTableNarrative( bLabel: string, ssrRows: SsrInterpolatedRow[], interactivityRange: { min: number; max: number }, + lang: Lang = 'en', ): string[] { if (ssrRows.length === 0) return []; + const P = NARRATIVE_POOLS[lang] ?? NARRATIVE_POOLS.en; const range = `${interactivityRange.min}–${interactivityRange.max} tok/s/user`; // Page-level seed: stable across renders, varies by (route variant, model, // GPU pair). Template selection rotates by rowIndex from this seed so the @@ -614,7 +750,7 @@ export function compareTableNarrative( if (!(a.cost > 0 && b.cost > 0)) { paragraphs.push( pickRotated( - PER_DOLLAR_ZERO_TEMPLATES, + P.perDollarZero, pageSeed, rowIndex, )({ @@ -647,14 +783,14 @@ export function compareTableNarrative( range, band, }; - const pool = ratio < 1.01 ? PER_DOLLAR_TIED_TEMPLATES : PER_DOLLAR_BOTH_TEMPLATES; + const pool = ratio < 1.01 ? P.perDollarTied : P.perDollarBoth; paragraphs.push(pickRotated(pool, pageSeed, rowIndex)(inputs)); continue; } const present = (a ?? b)!; paragraphs.push( pickRotated( - PER_DOLLAR_SINGLE_TEMPLATES, + P.perDollarSingle, pageSeed, rowIndex, )({ @@ -694,13 +830,13 @@ export function compareTableNarrative( range, band, }; - paragraphs.push(pickRotated(FULL_BOTH_TEMPLATES, pageSeed, rowIndex)(inputs)); + paragraphs.push(pickRotated(P.fullBoth, pageSeed, rowIndex)(inputs)); continue; } const present = (a ?? b)!; paragraphs.push( pickRotated( - FULL_SINGLE_TEMPLATES, + P.fullSingle, pageSeed, rowIndex, )({ @@ -724,8 +860,13 @@ export function compareTableNarrative( /** "A", "A and B", or "A, B, and C" — Oxford-comma serial join. Used by the * master index ledes on both /compare and /compare-per-dollar so the * enumeration stays consistent if a model is added or removed. */ -export function formatModelList(models: CompareModelSlug[]): string { +export function formatModelList(models: CompareModelSlug[], lang: Lang = 'en'): string { const labels = models.map((m) => m.label); + if (lang === 'zh') { + if (labels.length === 0) return '暂无模型'; + if (labels.length === 1) return labels[0]; + return `${labels.slice(0, -1).join('、')} 和 ${labels.at(-1)}`; + } if (labels.length === 0) return 'no models'; if (labels.length === 1) return labels[0]; if (labels.length === 2) return `${labels[0]} and ${labels[1]}`; @@ -781,15 +922,23 @@ export function buildBreadcrumbJsonLd( variant: CompareJsonLdVariant, pairLabel: string, url: string, + lang: Lang = 'en', ) { - const indexUrl = - variant === 'per-dollar' ? `${SITE_URL}/compare-per-dollar` : `${SITE_URL}/compare`; - const indexName = variant === 'per-dollar' ? 'GPU Performance per Dollar' : 'GPU Comparisons'; + const indexUrl = `${SITE_URL}${compareBasePath(lang, variant)}`; + const homeName = lang === 'zh' ? '首页' : 'Home'; + const indexName = + lang === 'zh' + ? variant === 'per-dollar' + ? 'GPU 每美元性能' + : 'GPU 对比' + : variant === 'per-dollar' + ? 'GPU Performance per Dollar' + : 'GPU Comparisons'; return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ - { '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL }, + { '@type': 'ListItem', position: 1, name: homeName, item: SITE_URL }, { '@type': 'ListItem', position: 2, name: indexName, item: indexUrl }, { '@type': 'ListItem', position: 3, name: pairLabel, item: url }, ], @@ -831,25 +980,39 @@ export function buildJsonLd( /** Display model name accepted by /api/v1/benchmarks?model=…, used to wire the * Dataset's `distribution: DataDownload` to a real machine-readable export. */ modelApiKey?: string, + lang: Lang = 'en', ) { const aLabel = HW_REGISTRY[a]?.label ?? a.toUpperCase(); const bLabel = HW_REGISTRY[b]?.label ?? b.toUpperCase(); const fullLabel = compareModelDisplayLabel(model, a, b); - const itemListName = - variant === 'per-dollar' + const isZh = lang === 'zh'; + const itemListName = isZh + ? variant === 'per-dollar' + ? `${fullLabel} —— 每美元性能` + : `${fullLabel} 推理基准测试` + : variant === 'per-dollar' ? `${fullLabel} — Performance per Dollar` : `${fullLabel} Inference Benchmark`; - const itemListDescription = - variant === 'per-dollar' + const itemListDescription = isZh + ? variant === 'per-dollar' + ? `${aLabel} 与 ${bLabel} 在 ${model.label} 上的每百万 token 成本。GPU 性能按自建超大规模数据中心 TCO 在各类 LLM 工作负载下归一化。` + : `${aLabel} 与 ${bLabel} 在 ${model.label} 上、各类 LLM 工作负载下的 AI 推理基准对比。` + : variant === 'per-dollar' ? `Cost per million tokens of ${aLabel} versus ${bLabel} on ${model.label}. GPU performance normalized by owning-hyperscaler TCO across LLM workloads.` : `Head-to-head AI inference benchmark comparison of ${aLabel} and ${bLabel} on ${model.label} across LLM workloads.`; - const datasetName = - variant === 'per-dollar' + const datasetName = isZh + ? variant === 'per-dollar' + ? `${aLabel} vs ${bLabel}(${model.label})每美元性能对比` + : `${aLabel} vs ${bLabel}(${model.label})插值基准对比` + : variant === 'per-dollar' ? `${aLabel} vs ${bLabel} (${model.label}) Performance-per-Dollar Comparison` : `${aLabel} vs ${bLabel} (${model.label}) Interpolated Benchmark Comparison`; - const datasetDescription = - variant === 'per-dollar' + const datasetDescription = isZh + ? variant === 'per-dollar' + ? `${aLabel} 与 ${bLabel} 在 ${model.label} 上、相同交互速率下的自建超大规模数据中心每百万 token 成本——按美元归一化的推理基准。` + : `${aLabel} 与 ${bLabel} 在 ${model.label} 上、相同交互速率下的插值吞吐量、成本、能效与并发数。` + : variant === 'per-dollar' ? `Owning-hyperscaler cost per million tokens for ${aLabel} and ${bLabel} on ${model.label} at matched interactivity levels — dollar-normalized inference benchmark.` : `Interpolated throughput, cost, power efficiency, and concurrency for ${aLabel} and ${bLabel} on ${model.label} at matched interactivity levels.`; @@ -878,7 +1041,9 @@ export function buildJsonLd( } return { '@type': 'Dataset', - name: `${model.label} comparison at ${row.target} tok/s/user interactivity`, + name: isZh + ? `${model.label} 在 ${row.target} tok/s/user 交互速率下的对比` + : `${model.label} comparison at ${row.target} tok/s/user interactivity`, variableMeasured: metrics.map((m) => ({ '@type': 'PropertyValue', name: m.name, diff --git a/packages/app/src/lib/compare/full-detail-view.tsx b/packages/app/src/lib/compare/full-detail-view.tsx new file mode 100644 index 00000000..30567515 --- /dev/null +++ b/packages/app/src/lib/compare/full-detail-view.tsx @@ -0,0 +1,191 @@ +import type { Metadata } from 'next'; +import { notFound, permanentRedirect } from 'next/navigation'; + +import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; + +import { JsonLd } from '@/components/json-ld'; +import { pickPairDefaults } from '@/lib/compare-pair-defaults'; +import { + canonicalCompareSlug, + compareDisplayLabel, + compareModelDisplayLabel, + parseCompareSlug, +} from '@/lib/compare-slug'; +import { + buildBreadcrumbJsonLd, + buildJsonLd, + compareTableNarrative, + computeCompareTableData, + dateRangeForPair, + getCachedBenchmarks, + KNOWN_MODELS, + KNOWN_PRECISIONS, + KNOWN_SEQUENCES, + pickString, + summarize, +} from '@/lib/compare-ssr'; +import { type Lang, compareDict, compareSlugPath } from '@/lib/compare/i18n'; + +import ComparePageClient from './full-page-client'; + +/** + * Shared server implementation of the `/compare/[slug]` (and `/zh/compare/[slug]`) + * detail page. The route files are thin wrappers passing `lang`. All slug + * parsing, the 308 canonicalization redirect, the benchmark fetch, the SSR + * interpolation, and the JSON-LD live here so the English and Chinese pages + * never drift on those mechanics — only copy and the URL prefix change. + */ + +type SearchParams = Record; + +export function fullDetailMetadata(slug: string, lang: Lang): Metadata { + const parsed = parseCompareSlug(slug); + if (!parsed) return {}; + const t = compareDict(lang).detail.full; + const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); + const gpuLabel = compareDisplayLabel(parsed.a, parsed.b); + const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); + const url = `${SITE_URL}${compareSlugPath(lang, 'full', canonical)}`; + const title = t.metaTitle(fullLabel); + const description = t.metaDescription(parsed.model.label, gpuLabel); + return { + title, + description, + alternates: { + canonical: url, + languages: { + en: `${SITE_URL}${compareSlugPath('en', 'full', canonical)}`, + 'zh-CN': `${SITE_URL}${compareSlugPath('zh', 'full', canonical)}`, + }, + }, + openGraph: { + title: `${fullLabel} | ${SITE_NAME}`, + description, + url, + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title, + description, + }, + }; +} + +export default async function FullDetailView({ + slug, + sp, + lang, +}: { + slug: string; + sp: SearchParams; + lang: Lang; +}) { + const parsed = parseCompareSlug(slug); + if (!parsed) notFound(); + + // One-hop 308 to the fully canonical URL (legacy bare slug, alias model, + // non-canonical GPU order, mixed case), preserving the query string. The + // redirect target stays within the current locale. + const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); + if (canonical !== slug.toLowerCase()) { + const qs = Object.entries(sp) + .flatMap(([k, v]) => { + if (Array.isArray(v)) return v.map((vv) => [k, vv] as const); + if (v === undefined) return []; + return [[k, v] as const]; + }) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); + permanentRedirect(`${compareSlugPath(lang, 'full', canonical)}${qs ? `?${qs}` : ''}`); + } + + const rows = await getCachedBenchmarks(parsed.model.dbKeys); + const summaryA = summarize(rows, parsed.a); + const summaryB = summarize(rows, parsed.b); + const { sequence: pickedSequence, precision: pickedPrecision } = pickPairDefaults( + rows, + parsed.a, + parsed.b, + ); + + const urlSeq = pickString(sp.i_seq); + const urlPrec = pickString(sp.i_prec); + const urlModel = pickString(sp.g_model); + const effectiveSequence = urlSeq && KNOWN_SEQUENCES.has(urlSeq) ? urlSeq : pickedSequence; + const effectivePrecision = urlPrec && KNOWN_PRECISIONS.has(urlPrec) ? urlPrec : pickedPrecision; + const effectiveModel = + urlModel && KNOWN_MODELS.has(urlModel) ? urlModel : parsed.model.displayName; + + const { defaultTargets, ssrRows, interactivityRange } = computeCompareTableData( + rows, + parsed.a, + parsed.b, + effectiveSequence, + effectivePrecision, + ); + + const url = `${SITE_URL}${compareSlugPath(lang, 'full', canonical)}`; + const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b); + const jsonLd = buildJsonLd( + 'full', + parsed.model, + parsed.a, + parsed.b, + url, + summaryA, + summaryB, + ssrRows, + undefined, + oldest, + newest, + parsed.model.displayName, + lang, + ); + const breadcrumbJsonLd = buildBreadcrumbJsonLd( + 'full', + compareModelDisplayLabel(parsed.model, parsed.a, parsed.b), + url, + lang, + ); + const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); + const aMeta = HW_REGISTRY[parsed.a]; + const bMeta = HW_REGISTRY[parsed.b]; + const aLabel = aMeta?.label ?? parsed.a.toUpperCase(); + const bLabel = bMeta?.label ?? parsed.b.toUpperCase(); + const narrative = compareTableNarrative( + 'full', + parsed.model.label, + aLabel, + bLabel, + ssrRows, + interactivityRange, + lang, + ); + + return ( + <> + + + + + ); +} diff --git a/packages/app/src/app/compare/[slug]/page-client.tsx b/packages/app/src/lib/compare/full-page-client.tsx similarity index 76% rename from packages/app/src/app/compare/[slug]/page-client.tsx rename to packages/app/src/lib/compare/full-page-client.tsx index 6dc26b05..22a674a0 100644 --- a/packages/app/src/app/compare/[slug]/page-client.tsx +++ b/packages/app/src/lib/compare/full-page-client.tsx @@ -11,6 +11,7 @@ import { InferenceProvider } from '@/components/inference/InferenceContext'; import InferenceChartDisplay from '@/components/inference/ui/ChartDisplay'; import { Card } from '@/components/ui/card'; import { track } from '@/lib/analytics'; +import { type Lang, compareDict, compareSlugPath } from '@/lib/compare/i18n'; import { Model, Precision, Sequence } from '@/lib/data-mappings'; interface SsrTableData { @@ -46,6 +47,8 @@ interface ComparePageClientProps { bVendor: string; aArch: string; bArch: string; + /** UI language. Defaults to English; `/zh/compare/*` passes 'zh'. */ + lang?: Lang; } function toModel(value: string): Model | undefined { @@ -79,6 +82,7 @@ export default function ComparePageClient({ bVendor, aArch, bArch, + lang = 'en', }: ComparePageClientProps) { useEffect(() => { track('compare_page_view', { gpu_a: a, gpu_b: b, default_model: defaultModel }); @@ -89,6 +93,10 @@ export default function ComparePageClient({ const initialSequence = toSequence(defaultSequence); const initialPrecisions = toPrecisions(defaultPrecision); + const t = compareDict(lang).detail.full; + const seqLabel = defaultSequence ?? (lang === 'zh' ? '序列' : 'sequence'); + const precLabel = defaultPrecision ?? (lang === 'zh' ? '精度' : 'precision'); + return (
- {modelLabel} · GPU comparison + {t.eyebrow(modelLabel)}

{label}

-

- Head-to-head AI inference benchmark comparison of {aLabel} ( - {aVendor} {aArch}) and {bLabel} ({bVendor} {bArch}) on{' '} - {modelLabel}. Latency, throughput, and cost across LLM workloads. - Use the chart controls below to switch sequences, precisions, and metrics — same - interactions as{' '} - - the main inference chart - - . -

+ {lang === 'zh' ? ( +

+ {aLabel}({aVendor} {aArch})与 {bLabel}( + {bVendor} {bArch})在 {modelLabel} 上的 AI 推理基准对比。涵盖各类 + LLM + 工作负载的延迟、吞吐量与成本。使用下方的图表控件即可切换序列、精度与指标——交互方式与{' '} + + {t.mainChartLink} + {' '} + 一致。 +

+ ) : ( +

+ Head-to-head AI inference benchmark comparison of {aLabel} ( + {aVendor} {aArch}) and {bLabel} ({bVendor} {bArch}) on{' '} + {modelLabel}. Latency, throughput, and cost across LLM workloads. + Use the chart controls below to switch sequences, precisions, and metrics — same + interactions as{' '} + + {t.mainChartLink} + + . +

+ )} {narrative.length > 0 && (
{narrative.map((para, i) => ( @@ -127,10 +148,7 @@ export default function ComparePageClient({ <> {' '} - (Numbers reflect the default {defaultSequence ?? 'sequence'} ·{' '} - {defaultPrecision ?? 'precision'} selection for this URL — table and - chart below update if you change sequence, precision, or model in the - controls.) + {t.caveat(seqLabel, precLabel)} )} @@ -140,11 +158,11 @@ export default function ComparePageClient({ )}

track('compare_cross_link_to_per_dollar', { slug })} > - View performance-per-dollar view → + {t.crossLink}

@@ -154,6 +172,7 @@ export default function ComparePageClient({ aLabel={aLabel} bLabel={bLabel} ssrTableData={ssrTableData} + lang={lang} /> @@ -169,12 +188,14 @@ function CompareTableSection({ aLabel, bLabel, ssrTableData, + lang, }: { a: string; b: string; aLabel: string; bLabel: string; ssrTableData: SsrTableData; + lang: Lang; }) { const { effectiveSequence, effectivePrecisions, selectedRunDate, selectedModel } = useGlobalFilters(); @@ -206,8 +227,7 @@ function CompareTableSection({ if (ssrTableData.defaultTargets.length === 0) { return (
- No interpolated comparison data available for the default model. Use the chart controls - below to select a model with benchmark data for both GPUs. + {compareDict(lang).detail.full.emptyState}
); } @@ -221,6 +241,7 @@ function CompareTableSection({ interactivityRange={clientRange} gpuDataPointsA={pointsA} gpuDataPointsB={pointsB} + lang={lang} /> ); } diff --git a/packages/app/src/lib/compare/i18n.ts b/packages/app/src/lib/compare/i18n.ts new file mode 100644 index 00000000..07ec0757 --- /dev/null +++ b/packages/app/src/lib/compare/i18n.ts @@ -0,0 +1,271 @@ +/** + * Internationalization for the `/compare` and `/compare-per-dollar` routes. + * + * The dashboard has no i18n framework — these two route families (plus their + * `[slug]` detail pages) ship a Chinese-language variant served under a `/zh` + * URL prefix. The English pages live at `/compare*`, the Chinese ones at + * `/zh/compare*`, and both render the same React components with a `lang` prop. + * + * This module holds: + * - the `Lang` union + URL path helpers, and + * - a dictionary of the *simple* (markup-free) user-facing strings for both + * languages. + * + * Markup-rich sentences (paragraphs that interleave / with + * dynamic GPU/model names, where Chinese word order differs from English) are + * NOT in this dictionary — those are rendered with an inline `lang === 'zh'` + * JSX branch in the page components so each language reads naturally. The + * narrative prose pools and JSON-LD strings live in `@/lib/compare-ssr`. + * + * The English entries are copied verbatim from the original hard-coded pages so + * the English output is byte-identical before and after the i18n refactor. + */ + +export type Lang = 'en' | 'zh'; +export type CompareVariant = 'full' | 'per-dollar'; + +/** Root segment for a variant (no locale prefix). */ +function variantSegment(variant: CompareVariant): string { + return variant === 'per-dollar' ? '/compare-per-dollar' : '/compare'; +} + +/** Locale-aware base path for a compare route, e.g. `/zh/compare-per-dollar`. + * Used for canonical URLs, redirect targets, and cross-links so a Chinese + * page always links to other Chinese pages. */ +export function compareBasePath(lang: Lang, variant: CompareVariant): string { + const seg = variantSegment(variant); + return lang === 'zh' ? `/zh${seg}` : seg; +} + +/** Locale-aware path to a detail slug, e.g. `/zh/compare/deepseek-r1-h100-vs-h200`. */ +export function compareSlugPath(lang: Lang, variant: CompareVariant, slug: string): string { + return `${compareBasePath(lang, variant)}/${slug}`; +} + +interface VariantStrings { + metaTitle: string; + metaDescription: string; + h1: string; + lede: (total: string, models: string) => string; + modelSubtext: (count: number, label: string) => string; + vendor: { cross: string; nvidia: string; amd: string }; +} + +interface DetailStrings { + /** SEO title; receives the full "Model — A vs B" label. */ + metaTitle: (fullLabel: string) => string; + metaDescription: (modelLabel: string, gpuLabel: string) => string; + eyebrow: (modelLabel: string) => string; + emptyState: string; + /** Caveat appended to the last narrative paragraph. */ + caveat: (sequence: string, precision: string) => string; +} + +export interface CompareDict { + /** Index (master) pages. */ + index: { + full: VariantStrings & { perDollarCta: string }; + perDollar: VariantStrings; + }; + /** Vendor group headings — brand names + "vs", identical across languages. */ + vendorHeadings: { cross: string; nvidia: string; amd: string }; + detail: { + full: DetailStrings & { + mainChartLink: string; + crossLink: string; + }; + perDollar: DetailStrings & { + h1Suffix: string; + mainChartLink: string; + crossLink: string; + pricingPrefix: string; + pricingSource: string; + tcoSourceName: string; + figcaption: (aLabel: string, bLabel: string) => string; + }; + }; + table: { + help: string; + metricColumn: string; + interactivity: string; + /** Maps the metric's stable English key → localized display label. */ + metricLabel: Record; + /** Per-dollar display override for "Cost ($/M tok)". */ + perDollarCostLabel: string; + }; +} + +const EN: CompareDict = { + index: { + full: { + metaTitle: 'GPU Comparisons', + metaDescription: + 'Browse head-to-head GPU inference benchmark comparisons across every model and hardware pair we test. Latency, throughput, and cost for DeepSeek V4 Pro 1.6T, DeepSeek R1, Kimi K2.5/K2.6 1T, GLM 5/5.1, Qwen 3.5 397B-A17B, and more.', + h1: 'GPU Comparisons', + lede: (total, models) => + `${total} head-to-head inference benchmark comparisons across ${models}. Each page includes interactive charts for latency, throughput, and cost metrics, plus an interpolated comparison table.`, + modelSubtext: (count, label) => + `${count} GPU pair${count === 1 ? '' : 's'} with benchmark data on ${label}.`, + vendor: { + cross: 'Cross-vendor comparisons across architecture generations.', + nvidia: 'Hopper and Blackwell generation comparisons.', + amd: 'CDNA 3 and CDNA 4 generation comparisons.', + }, + perDollarCta: 'Compare GPU performance per dollar', + }, + perDollar: { + metaTitle: 'GPU Performance per Dollar', + metaDescription: + 'GPU performance per dollar — head-to-head cost per million tokens across every model and hardware pair we benchmark. Performance normalized by owning-hyperscaler TCO for DeepSeek V4 Pro 1.6T, DeepSeek R1, Kimi K2.5/K2.6 1T, GLM 5/5.1, Qwen 3.5 397B-A17B, and more. Pick the cheapest SKU for your workload.', + h1: 'GPU Performance per Dollar', + lede: (total, models) => + `${total} head-to-head cost-per-million-tokens comparisons across ${models}. Performance normalized by owning-hyperscaler TCO — each page renders the cost-per-token chart and an interpolated dollars-per-million comparison table so you can pick the cheaper SKU at any target interactivity level.`, + modelSubtext: (count, label) => + `${count} GPU pair${count === 1 ? '' : 's'} with cost-per-token benchmark data on ${label}.`, + vendor: { + cross: 'Cross-vendor cost-per-token comparisons across architecture generations.', + nvidia: 'Hopper and Blackwell generation cost-per-token comparisons.', + amd: 'CDNA 3 and CDNA 4 generation cost-per-token comparisons.', + }, + }, + }, + vendorHeadings: { + cross: 'NVIDIA vs AMD', + nvidia: 'NVIDIA vs NVIDIA', + amd: 'AMD vs AMD', + }, + detail: { + full: { + metaTitle: (fullLabel) => `${fullLabel} Inference Benchmark`, + metaDescription: (modelLabel, gpuLabel) => + `Head-to-head GPU inference benchmark comparison for ${modelLabel}: ${gpuLabel}. Latency, throughput, and cost across LLM workloads.`, + eyebrow: (modelLabel) => `${modelLabel} · GPU comparison`, + emptyState: + 'No interpolated comparison data available for the default model. Use the chart controls below to select a model with benchmark data for both GPUs.', + caveat: (sequence, precision) => + `(Numbers reflect the default ${sequence} · ${precision} selection for this URL — table and chart below update if you change sequence, precision, or model in the controls.)`, + mainChartLink: 'the main inference chart', + crossLink: 'View performance-per-dollar view →', + }, + perDollar: { + metaTitle: (fullLabel) => `${fullLabel} — Performance per Dollar`, + metaDescription: (modelLabel, gpuLabel) => + `${modelLabel} cost per million tokens on ${gpuLabel}. Performance normalized by owning-hyperscaler TCO — see which GPU delivers more inference dollars-per-token at every interactivity level.`, + eyebrow: (modelLabel) => `${modelLabel} · Performance per Dollar`, + emptyState: + 'No interpolated cost-per-token data available for the default model on this GPU pair. Use the chart controls below to select a model and precision with benchmark data for both GPUs.', + caveat: (sequence, precision) => + `(Numbers reflect the default ${sequence} · ${precision} selection for this URL — table and chart below update if you change sequence, precision, or model in the controls.)`, + h1Suffix: 'Performance per Dollar', + mainChartLink: 'the main inference chart', + crossLink: 'View full latency + throughput comparison →', + pricingPrefix: 'GPU pricing (owning hyperscaler):', + pricingSource: 'Source:', + tcoSourceName: 'SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model', + figcaption: (aLabel, bLabel) => + `${aLabel} versus ${bLabel} cost per million tokens for this comparison's canonical default workload. Lower cost indicates better performance per dollar.`, + }, + }, + table: { + help: 'Interpolated from real benchmark data. Edit target interactivity values below to compare at different operating points.', + metricColumn: 'Metric', + interactivity: 'Interactivity (tok/s/user)', + metricLabel: { + 'Throughput (tok/s/gpu)': 'Throughput (tok/s/gpu)', + 'Cost ($/M tok)': 'Cost ($/M tok)', + 'tok/s/MW': 'tok/s/MW', + Concurrency: 'Concurrency', + }, + perDollarCostLabel: 'Dollar per Million Tokens', + }, +}; + +const ZH: CompareDict = { + index: { + full: { + metaTitle: 'GPU 对比', + metaDescription: + '浏览我们测试的每个模型与硬件组合的 GPU 推理基准对比。涵盖 DeepSeek V4 Pro 1.6T、DeepSeek R1、Kimi K2.5/K2.6 1T、GLM 5/5.1、Qwen 3.5 397B-A17B 等模型的延迟、吞吐量与成本。', + h1: 'GPU 对比', + lede: (total, models) => + `${models} 等模型上的 ${total} 组 GPU 推理基准对比。每个页面都包含延迟、吞吐量与成本指标的交互式图表,以及一张插值对比表。`, + modelSubtext: (_count, label) => `在 ${label} 上有基准数据的 ${_count} 组 GPU 对比。`, + vendor: { + cross: '跨厂商、跨架构世代的对比。', + nvidia: 'Hopper 与 Blackwell 世代的对比。', + amd: 'CDNA 3 与 CDNA 4 世代的对比。', + }, + perDollarCta: '对比 GPU 每美元性能', + }, + perDollar: { + metaTitle: 'GPU 每美元性能', + metaDescription: + 'GPU 每美元性能——我们基准测试的每个模型与硬件组合的每百万 token 成本对比。性能按自建超大规模数据中心 TCO 归一化,涵盖 DeepSeek V4 Pro 1.6T、DeepSeek R1、Kimi K2.5/K2.6 1T、GLM 5/5.1、Qwen 3.5 397B-A17B 等模型。为你的工作负载挑选最便宜的 SKU。', + h1: 'GPU 每美元性能', + lede: (total, models) => + `${models} 等模型上的 ${total} 组每百万 token 成本对比。性能按自建超大规模数据中心 TCO 归一化——每个页面都会渲染每 token 成本图表与一张插值的每百万 token 美元成本对比表,让你在任意目标交互速率下挑选更便宜的 SKU。`, + modelSubtext: (_count, label) => + `在 ${label} 上有每 token 成本基准数据的 ${_count} 组 GPU 对比。`, + vendor: { + cross: '跨厂商、跨架构世代的每 token 成本对比。', + nvidia: 'Hopper 与 Blackwell 世代的每 token 成本对比。', + amd: 'CDNA 3 与 CDNA 4 世代的每 token 成本对比。', + }, + }, + }, + vendorHeadings: { + cross: 'NVIDIA vs AMD', + nvidia: 'NVIDIA vs NVIDIA', + amd: 'AMD vs AMD', + }, + detail: { + full: { + metaTitle: (fullLabel) => `${fullLabel} 推理基准测试`, + metaDescription: (modelLabel, gpuLabel) => + `${modelLabel} 的 GPU 推理基准对比:${gpuLabel}。涵盖各类 LLM 工作负载的延迟、吞吐量与成本。`, + eyebrow: (modelLabel) => `${modelLabel} · GPU 对比`, + emptyState: + '当前默认模型暂无插值对比数据。请使用下方的图表控件,选择一个两块 GPU 都有基准数据的模型。', + caveat: (sequence, precision) => + `(数值基于此 URL 的默认 ${sequence} · ${precision} 选择——如果你在下方控件中更改序列、精度或模型,下方的表格和图表会随之更新。)`, + mainChartLink: '主推理图表', + crossLink: '查看每美元性能视图 →', + }, + perDollar: { + metaTitle: (fullLabel) => `${fullLabel} —— 每美元性能`, + metaDescription: (modelLabel, gpuLabel) => + `${modelLabel} 在 ${gpuLabel} 上的每百万 token 成本。性能按自建超大规模数据中心 TCO 归一化——看哪块 GPU 在每个交互速率下都能以更低的成本产出更多 token。`, + eyebrow: (modelLabel) => `${modelLabel} · 每美元性能`, + emptyState: + '此 GPU 组合在当前默认模型上暂无每 token 成本的插值数据。请使用下方的图表控件,选择一个两块 GPU 都有基准数据的模型与精度。', + caveat: (sequence, precision) => + `(数值基于此 URL 的默认 ${sequence} · ${precision} 选择——如果你在下方控件中更改序列、精度或模型,下方的表格和图表会随之更新。)`, + h1Suffix: '每美元性能', + mainChartLink: '主推理图表', + crossLink: '查看完整的延迟 + 吞吐量对比 →', + pricingPrefix: 'GPU 定价(自建超大规模数据中心):', + pricingSource: '来源:', + tcoSourceName: 'SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model', + figcaption: (aLabel, bLabel) => + `${aLabel} 与 ${bLabel} 在本次对比的默认工作负载下的每百万 token 成本。成本越低,每美元性能越好。`, + }, + }, + table: { + help: '根据真实基准数据插值得出。编辑下方的目标交互速率,可在不同工作点进行对比。', + metricColumn: '指标', + interactivity: '交互速率 (tok/s/user)', + metricLabel: { + 'Throughput (tok/s/gpu)': '吞吐量 (tok/s/gpu)', + 'Cost ($/M tok)': '成本 ($/M tok)', + 'tok/s/MW': 'tok/s/MW', + Concurrency: '并发数', + }, + perDollarCostLabel: '每百万 Token 美元成本', + }, +}; + +const DICTS: Record = { en: EN, zh: ZH }; + +export function compareDict(lang: Lang): CompareDict { + return DICTS[lang] ?? EN; +} diff --git a/packages/app/src/lib/compare/index-view.tsx b/packages/app/src/lib/compare/index-view.tsx new file mode 100644 index 00000000..f0ea95f1 --- /dev/null +++ b/packages/app/src/lib/compare/index-view.tsx @@ -0,0 +1,187 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; + +import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; + +import { ComparePairCardLink } from '@/components/compare/compare-pair-card-link'; +import { JsonLd } from '@/components/json-ld'; +import { Card } from '@/components/ui/card'; +import { getComparablePairsByModelSlug } from '@/lib/compare-availability'; +import { type ComparePair, COMPARE_MODEL_SLUGS, type CompareModelSlug } from '@/lib/compare-slug'; +import { bucketComparePairsByVendor, formatModelList } from '@/lib/compare-ssr'; +import { + type CompareVariant, + type Lang, + compareBasePath, + compareDict, + compareSlugPath, +} from '@/lib/compare/i18n'; + +/** + * Shared implementation of the `/compare` and `/compare-per-dollar` master index + * pages, in both English and Chinese. The route files are thin wrappers that + * call `CompareIndexView` / `compareIndexMetadata` with the right + * `(variant, lang)` pair, so the four index URLs stay in lockstep on the + * data-fetch, vendor-bucketing, and card-grid mechanics — only the copy and the + * URL prefix differ. + */ + +interface VendorGroup { + heading: string; + description: string; + pairs: { a: string; b: string; slug: string; label: string }[]; +} + +function groupPairsByVendorForModel( + model: CompareModelSlug, + comparablePairs: ComparePair[], + variant: CompareVariant, + lang: Lang, +): VendorGroup[] { + const { cross, nvidia, amd } = bucketComparePairsByVendor(model.slug, comparablePairs); + const dict = compareDict(lang); + const headings = dict.vendorHeadings; + const vendorDesc = (variant === 'per-dollar' ? dict.index.perDollar : dict.index.full).vendor; + const groups: VendorGroup[] = []; + if (cross.length > 0) { + groups.push({ heading: headings.cross, description: vendorDesc.cross, pairs: cross }); + } + if (nvidia.length > 0) { + groups.push({ heading: headings.nvidia, description: vendorDesc.nvidia, pairs: nvidia }); + } + if (amd.length > 0) { + groups.push({ heading: headings.amd, description: vendorDesc.amd, pairs: amd }); + } + return groups; +} + +/** Locale-aware metadata for an index page. */ +export function compareIndexMetadata(variant: CompareVariant, lang: Lang): Metadata { + const dict = compareDict(lang); + const v = variant === 'per-dollar' ? dict.index.perDollar : dict.index.full; + const url = `${SITE_URL}${compareBasePath(lang, variant)}`; + const title = v.metaTitle; + const description = v.metaDescription; + return { + title, + description, + alternates: { + canonical: url, + languages: { + en: `${SITE_URL}${compareBasePath('en', variant)}`, + 'zh-CN': `${SITE_URL}${compareBasePath('zh', variant)}`, + }, + }, + openGraph: { + title: `${title} | ${SITE_NAME}`, + description, + url, + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: `${title} | ${SITE_NAME}`, + description, + }, + }; +} + +export default async function CompareIndexView({ + variant, + lang, +}: { + variant: CompareVariant; + lang: Lang; +}) { + // Server-side filter: only show (model, pair) combinations where both GPUs + // have benchmark data for that model. Avoids cards that would link to an + // empty-state page. The detail page still renders the empty-state for direct + // URL hits, so this is purely navigation hygiene. + const comparablePairsByModel = await getComparablePairsByModelSlug(); + const totalUrls = [...comparablePairsByModel.values()].reduce((s, p) => s + p.length, 0); + const modelsWithPairs = COMPARE_MODEL_SLUGS.filter( + (m) => (comparablePairsByModel.get(m.slug)?.length ?? 0) > 0, + ); + + const dict = compareDict(lang); + const v = variant === 'per-dollar' ? dict.index.perDollar : dict.index.full; + const url = `${SITE_URL}${compareBasePath(lang, variant)}`; + const isFull = variant !== 'per-dollar'; + + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: `${v.metaTitle} | ${SITE_NAME}`, + description: v.metaDescription, + url, + }; + + return ( + <> + +
+ +

{v.h1}

+

+ {v.lede(totalUrls.toLocaleString(), formatModelList(modelsWithPairs, lang))} +

+ {isFull && ( +
+ + {dict.index.full.perDollarCta} + + +
+ )} +
+
+ + {modelsWithPairs.map((model) => { + const pairs = comparablePairsByModel.get(model.slug) ?? []; + const groups = groupPairsByVendorForModel(model, pairs, variant, lang); + return ( +
+ +
+

{model.label}

+

+ {v.modelSubtext(pairs.length, model.label)} +

+
+ {groups.map((group) => ( +
+
+

{group.heading}

+

{group.description}

+
+
+ {group.pairs.map(({ slug, label, a, b }) => { + const aMeta = HW_REGISTRY[a]; + const bMeta = HW_REGISTRY[b]; + const archLine = `${aMeta?.arch ?? '—'} · ${bMeta?.arch ?? '—'}`; + return ( + + ); + })} +
+
+ ))} +
+
+ ); + })} + + ); +} diff --git a/packages/app/src/lib/compare/per-dollar-detail-view.tsx b/packages/app/src/lib/compare/per-dollar-detail-view.tsx new file mode 100644 index 00000000..b34a2086 --- /dev/null +++ b/packages/app/src/lib/compare/per-dollar-detail-view.tsx @@ -0,0 +1,199 @@ +import type { Metadata } from 'next'; +import { notFound, permanentRedirect } from 'next/navigation'; + +import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants'; + +import { JsonLd } from '@/components/json-ld'; +import { pickPairDefaults } from '@/lib/compare-pair-defaults'; +import { + canonicalCompareSlug, + compareDisplayLabel, + compareModelDisplayLabel, + parseCompareSlug, +} from '@/lib/compare-slug'; +import { + buildBreadcrumbJsonLd, + buildJsonLd, + compareTableNarrative, + computeCompareTableData, + dateRangeForPair, + getCachedBenchmarks, + KNOWN_MODELS, + KNOWN_PRECISIONS, + KNOWN_SEQUENCES, + pickString, + summarize, +} from '@/lib/compare-ssr'; +import { type Lang, compareDict, compareSlugPath } from '@/lib/compare/i18n'; +import { getGpuSpecs } from '@/lib/constants'; + +import ComparePerDollarPageClient from './per-dollar-page-client'; + +/** + * Shared server implementation of the `/compare-per-dollar/[slug]` (and the + * `/zh/…` variant) detail page. Mirrors `full-detail-view.tsx` but adds the + * owning-hyperscaler $/GPU/hr pricing inputs and wires the crawlable hero PNG. + * The PNG route itself is language-neutral (a data graphic), so both locales + * reuse the canonical English `performance-per-dollar.png` endpoint. + */ + +type SearchParams = Record; + +export function perDollarDetailMetadata(slug: string, lang: Lang): Metadata { + const parsed = parseCompareSlug(slug); + if (!parsed) return {}; + const t = compareDict(lang).detail.perDollar; + const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); + const gpuLabel = compareDisplayLabel(parsed.a, parsed.b); + const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); + const url = `${SITE_URL}${compareSlugPath(lang, 'per-dollar', canonical)}`; + const title = t.metaTitle(fullLabel); + const description = t.metaDescription(parsed.model.label, gpuLabel); + return { + title, + description, + alternates: { + canonical: url, + languages: { + en: `${SITE_URL}${compareSlugPath('en', 'per-dollar', canonical)}`, + 'zh-CN': `${SITE_URL}${compareSlugPath('zh', 'per-dollar', canonical)}`, + }, + }, + openGraph: { + title: `${fullLabel} ${lang === 'zh' ? '—— 每美元性能' : '— Performance per Dollar'} | ${SITE_NAME}`, + description, + url, + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title, + description, + }, + }; +} + +export default async function PerDollarDetailView({ + slug, + sp, + lang, +}: { + slug: string; + sp: SearchParams; + lang: Lang; +}) { + const parsed = parseCompareSlug(slug); + if (!parsed) notFound(); + + const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b); + if (canonical !== slug.toLowerCase()) { + const qs = Object.entries(sp) + .flatMap(([k, v]) => { + if (Array.isArray(v)) return v.map((vv) => [k, vv] as const); + if (v === undefined) return []; + return [[k, v] as const]; + }) + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); + permanentRedirect(`${compareSlugPath(lang, 'per-dollar', canonical)}${qs ? `?${qs}` : ''}`); + } + + const rows = await getCachedBenchmarks(parsed.model.dbKeys); + const summaryA = summarize(rows, parsed.a); + const summaryB = summarize(rows, parsed.b); + const { sequence: pickedSequence, precision: pickedPrecision } = pickPairDefaults( + rows, + parsed.a, + parsed.b, + ); + + const urlSeq = pickString(sp.i_seq); + const urlPrec = pickString(sp.i_prec); + const urlModel = pickString(sp.g_model); + const effectiveSequence = urlSeq && KNOWN_SEQUENCES.has(urlSeq) ? urlSeq : pickedSequence; + const effectivePrecision = urlPrec && KNOWN_PRECISIONS.has(urlPrec) ? urlPrec : pickedPrecision; + const effectiveModel = + urlModel && KNOWN_MODELS.has(urlModel) ? urlModel : parsed.model.displayName; + + const { defaultTargets, ssrRows, interactivityRange } = computeCompareTableData( + rows, + parsed.a, + parsed.b, + effectiveSequence, + effectivePrecision, + ); + + // Hero / OG PNG is a language-neutral data graphic — reuse the canonical + // English endpoint regardless of locale so we don't duplicate the 500-line + // Satori chart route per language. + const enCanonicalUrl = `${SITE_URL}${compareSlugPath('en', 'per-dollar', canonical)}`; + const imageUrl = `${enCanonicalUrl}/performance-per-dollar.png`; + const url = `${SITE_URL}${compareSlugPath(lang, 'per-dollar', canonical)}`; + const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b); + const jsonLd = buildJsonLd( + 'per-dollar', + parsed.model, + parsed.a, + parsed.b, + url, + summaryA, + summaryB, + ssrRows, + imageUrl, + oldest, + newest, + parsed.model.displayName, + lang, + ); + const breadcrumbJsonLd = buildBreadcrumbJsonLd( + 'per-dollar', + compareModelDisplayLabel(parsed.model, parsed.a, parsed.b), + url, + lang, + ); + const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b); + const aMeta = HW_REGISTRY[parsed.a]; + const bMeta = HW_REGISTRY[parsed.b]; + const aLabel = aMeta?.label ?? parsed.a.toUpperCase(); + const bLabel = bMeta?.label ?? parsed.b.toUpperCase(); + const narrative = compareTableNarrative( + 'per-dollar', + parsed.model.label, + aLabel, + bLabel, + ssrRows, + interactivityRange, + lang, + ); + const aCostPerGpuHr = getGpuSpecs(parsed.a).costh; + const bCostPerGpuHr = getGpuSpecs(parsed.b).costh; + + return ( + <> + + + + + ); +} diff --git a/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx b/packages/app/src/lib/compare/per-dollar-page-client.tsx similarity index 72% rename from packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx rename to packages/app/src/lib/compare/per-dollar-page-client.tsx index b6ee7550..191eea50 100644 --- a/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx +++ b/packages/app/src/lib/compare/per-dollar-page-client.tsx @@ -11,6 +11,7 @@ import { InferenceProvider } from '@/components/inference/InferenceContext'; import InferenceChartDisplay from '@/components/inference/ui/ChartDisplay'; import { Card } from '@/components/ui/card'; import { track } from '@/lib/analytics'; +import { type Lang, compareDict, compareSlugPath } from '@/lib/compare/i18n'; import { Model, Precision, Sequence } from '@/lib/data-mappings'; interface SsrTableData { @@ -50,19 +51,14 @@ interface ComparePerDollarPageClientProps { bCostPerGpuHr: number; /** Crawlable data graphic generated for the canonical default comparison. */ heroImageSrc: string; + /** UI language. Defaults to English; `/zh/compare-per-dollar/*` passes 'zh'. */ + lang?: Lang; } /** Only show Cost + Concurrency in the interpolated table — the rest of the * metric rows (Throughput, tok/s/MW) live on the sibling /compare page. */ const PER_DOLLAR_TABLE_METRICS = ['Cost ($/M tok)', 'Concurrency']; -/** Rename "Cost ($/M tok)" to the full-English "Dollar per Million Tokens" - * in the per-dollar table so the cell reads in line with the page's - * "Performance per Dollar" framing and surfaces the SEO term verbatim. */ -const PER_DOLLAR_LABEL_OVERRIDES = { - 'Cost ($/M tok)': 'Dollar per Million Tokens', -}; - /** y_costh = Cost per Million Total Tokens (Owning - Hyperscaler). Defined in * packages/app/src/components/inference/inference-chart-config.json. */ const PER_DOLLAR_DEFAULT_Y_AXIS = 'y_costh'; @@ -101,6 +97,7 @@ export default function ComparePerDollarPageClient({ aCostPerGpuHr, bCostPerGpuHr, heroImageSrc, + lang = 'en', }: ComparePerDollarPageClientProps) { useEffect(() => { track('compare_per_dollar_page_view', { gpu_a: a, gpu_b: b, default_model: defaultModel }); @@ -111,6 +108,10 @@ export default function ComparePerDollarPageClient({ const initialSequence = toSequence(defaultSequence); const initialPrecisions = toPrecisions(defaultPrecision); + const t = compareDict(lang).detail.perDollar; + const seqLabel = defaultSequence ?? (lang === 'zh' ? '序列' : 'sequence'); + const precLabel = defaultPrecision ?? (lang === 'zh' ? '精度' : 'precision'); + return (
- {modelLabel} · Performance per Dollar + {t.eyebrow(modelLabel)}

- {label} Performance per Dollar + {label} {t.h1Suffix}

-

- Cost per million tokens of {aLabel} ({aVendor} {aArch}) versus{' '} - {bLabel} ({bVendor} {bArch}) on {modelLabel}. - Owning-hyperscaler TCO normalized by output tokens — performance per dollar across - LLM workloads. Pick the more cost-efficient SKU at every target interactivity level. - Use the chart controls below to switch sequences, precisions, and metrics — same - interactions as{' '} - - the main inference chart - - . -

+ {lang === 'zh' ? ( +

+ {aLabel}({aVendor} {aArch})与 {bLabel}( + {bVendor} {bArch})在 {modelLabel} 上的每百万 token 成本。按输出 + token 对自建超大规模数据中心 TCO 归一化——各类 LLM + 工作负载下的每美元性能。在每个目标交互速率下挑选更具成本效益的 + SKU。使用下方的图表控件即可切换序列、精度与指标——交互方式与{' '} + + {t.mainChartLink} + {' '} + 一致。 +

+ ) : ( +

+ Cost per million tokens of {aLabel} ({aVendor} {aArch}) versus{' '} + {bLabel} ({bVendor} {bArch}) on {modelLabel}. + Owning-hyperscaler TCO normalized by output tokens — performance per dollar across + LLM workloads. Pick the more cost-efficient SKU at every target interactivity + level. Use the chart controls below to switch sequences, precisions, and metrics — + same interactions as{' '} + + {t.mainChartLink} + + . +

+ )} {narrative.length > 0 && (
{' '} - (Numbers reflect the default {defaultSequence ?? 'sequence'} ·{' '} - {defaultPrecision ?? 'precision'} selection for this URL — table and - chart below update if you change sequence, precision, or model in the - controls.) + {t.caveat(seqLabel, precLabel)} )} @@ -172,10 +184,12 @@ export default function ComparePerDollarPageClient({ className="mt-2 text-xs text-muted-foreground" data-testid="compare-per-dollar-pricing" > - GPU pricing (owning hyperscaler): {aLabel}{' '} + {t.pricingPrefix} {aLabel}{' '} {aCostPerGpuHr > 0 ? `$${aCostPerGpuHr.toFixed(2)}/GPU/hr` : '—'} ·{' '} {bLabel}{' '} - {bCostPerGpuHr > 0 ? `$${bCostPerGpuHr.toFixed(2)}/GPU/hr` : '—'}. Source:{' '} + {bCostPerGpuHr > 0 ? `$${bCostPerGpuHr.toFixed(2)}/GPU/hr` : '—'} + {lang === 'zh' ? '。' : '. '} + {t.pricingSource}{' '} track('compare_per_dollar_tco_source_clicked', { slug })} > - SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model + {t.tcoSourceName} - . + {lang === 'zh' ? '。' : '.'}

)}

track('compare_per_dollar_cross_link_to_full', { slug })} > - View full latency + throughput comparison → + {t.crossLink}

@@ -204,7 +218,11 @@ export default function ComparePerDollarPageClient({ > {`${modelLabel}:
- {aLabel} versus {bLabel} cost per million tokens for this comparison's canonical - default workload. Lower cost indicates better performance per dollar. + {t.figcaption(aLabel, bLabel)}
@@ -237,12 +255,14 @@ function CompareTableSection({ aLabel, bLabel, ssrTableData, + lang, }: { a: string; b: string; aLabel: string; bLabel: string; ssrTableData: SsrTableData; + lang: Lang; }) { const { effectiveSequence, effectivePrecisions, selectedRunDate, selectedModel } = useGlobalFilters(); @@ -270,8 +290,7 @@ function CompareTableSection({ if (ssrTableData.defaultTargets.length === 0) { return (
- No interpolated cost-per-token data available for the default model on this GPU pair. Use - the chart controls below to select a model and precision with benchmark data for both GPUs. + {compareDict(lang).detail.perDollar.emptyState}
); } @@ -286,7 +305,10 @@ function CompareTableSection({ gpuDataPointsA={pointsA} gpuDataPointsB={pointsB} visibleMetricLabels={PER_DOLLAR_TABLE_METRICS} - metricLabelOverrides={PER_DOLLAR_LABEL_OVERRIDES} + // Localized display override for the cost row — "Dollar per Million Tokens" + // (en) / "每百万 Token 美元成本" (zh). Keyed by the stable English label. + metricLabelOverrides={{ 'Cost ($/M tok)': compareDict(lang).table.perDollarCostLabel }} + lang={lang} /> ); }