From 6c5da4625e55812652c9eef78b9987a318b95bec Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 23 Aug 2026 05:41:30 -0700 Subject: [PATCH 1/9] fix(zh): polish explanatory content pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct localized routes and terminology, preserve source scope, contain API schema overflow on mobile, and add focused desktop/mobile regression coverage. 中文:修正本地化路由与术语,保持英文原文的语义范围,解决 API 数据结构在移动端的横向溢出,并补齐桌面端与移动端回归覆盖。 --- .../app/cypress/e2e/api-documentation.cy.ts | 23 ++++ packages/app/cypress/e2e/zh-pages.cy.ts | 100 ++++++++++++++++++ packages/app/src/app/zh/about/page.tsx | 10 +- .../src/app/zh/land-acknowledgement/page.tsx | 44 ++++---- .../app/src/components/about/faq-data-zh.ts | 2 +- .../app/src/components/about/faq-data.test.ts | 29 +++++ .../api-documentation/api-reference-page.tsx | 2 +- .../app/src/lib/api-documentation.test.ts | 19 ++++ packages/app/src/lib/api-documentation.ts | 12 +-- packages/app/src/lib/glossary-zh.ts | 26 ++--- packages/app/src/lib/glossary.test.ts | 7 ++ 11 files changed, 225 insertions(+), 49 deletions(-) create mode 100644 packages/app/src/components/about/faq-data.test.ts create mode 100644 packages/app/src/lib/api-documentation.test.ts diff --git a/packages/app/cypress/e2e/api-documentation.cy.ts b/packages/app/cypress/e2e/api-documentation.cy.ts index f22f9e9b5..a646f015a 100644 --- a/packages/app/cypress/e2e/api-documentation.cy.ts +++ b/packages/app/cypress/e2e/api-documentation.cy.ts @@ -2,6 +2,7 @@ const SITE_URL = 'https://inferencex.semianalysis.com'; describe('API documentation', () => { it('exposes the localized reference and its OpenAPI contract', () => { + cy.viewport(1440, 900); cy.visit('/api'); cy.get('[data-testid="api-reference"]') @@ -78,4 +79,26 @@ describe('API documentation', () => { .should('have.attr', 'href', '/zh/api') .and('have.text', 'API 文档'); }); + + for (const [path, width] of [ + ['/api', 375], + ['/zh/api', 390], + ] as const) { + it(`keeps wide API data reachable without page overflow at ${path} on ${width}px`, () => { + cy.viewport(width, 844); + cy.visit(path); + cy.get('[data-testid="api-endpoint-list-benchmarks"] summary').click(); + cy.get('[data-testid="api-endpoint-list-benchmarks"] .overflow-x-auto') + .first() + .should('be.visible') + .then(($scroller) => { + expect($scroller[0].scrollWidth).to.be.greaterThan($scroller[0].clientWidth); + }); + cy.document().then((document) => { + expect(document.documentElement.scrollWidth).to.be.at.most( + document.documentElement.clientWidth, + ); + }); + }); + } }); diff --git a/packages/app/cypress/e2e/zh-pages.cy.ts b/packages/app/cypress/e2e/zh-pages.cy.ts index 7e114d37c..ecb6827c3 100644 --- a/packages/app/cypress/e2e/zh-pages.cy.ts +++ b/packages/app/cypress/e2e/zh-pages.cy.ts @@ -122,6 +122,106 @@ describe('Chinese (/zh) pages', () => { }); }); + describe('About pages', () => { + it('keeps English article links on the English click path', () => { + cy.viewport(1440, 900); + cy.visit('/about'); + + cy.get('link[rel="alternate"][hreflang="zh-CN"]').should('exist'); + cy.contains('a', 'InferenceX v1') + .should('have.attr', 'href', '/blog/inferencemax-open-source-inference-benchmarking') + .click(); + cy.location('pathname').should('eq', '/blog/inferencemax-open-source-inference-benchmarking'); + }); + + it('keeps both Chinese article links inside /zh and preserves the FAQ structure', () => { + cy.viewport(390, 844); + cy.visit('/zh/about'); + + cy.get('link[rel="alternate"][hreflang="en"]').should('exist'); + cy.contains('NeoCloud').should('exist'); + cy.contains('dt', '什么是 InferenceX?').should('exist'); + cy.contains('a', 'InferenceX v1') + .should('have.attr', 'href', '/zh/blog/inferencemax-open-source-inference-benchmarking') + .and('not.have.attr', 'hreflang', 'en'); + cy.contains('a', 'InferenceX v2') + .should('have.attr', 'href', '/zh/blog/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper') + .and('not.have.attr', 'hreflang', 'en') + .click(); + cy.location('pathname').should( + 'eq', + '/zh/blog/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper', + ); + }); + }); + + describe('Glossary pages', () => { + it('supports search, category filtering, empty recovery, and a Chinese term click path', () => { + cy.viewport(1440, 900); + cy.visit('/zh/glossary'); + + cy.get('link[rel="alternate"][hreflang="en"]').should('exist'); + cy.get('input[placeholder="搜索 MTP、延迟、FP4…"]').as('search').type('MTP'); + cy.get('a[href="/zh/glossary/multi-token-prediction"]').should('be.visible'); + cy.get('@search').clear().type('不存在的术语-xyz'); + cy.contains('h2', '未找到相关术语').should('be.visible'); + cy.contains('button', '显示全部术语').click(); + cy.get('@search').should('have.value', ''); + + cy.contains('button', '智能体推理').click().should('have.attr', 'aria-pressed', 'true'); + cy.get('a[href="/zh/glossary/agentx"]').click(); + cy.location('pathname').should('eq', '/zh/glossary/agentx'); + cy.contains('a', 'AI 推理术语表').click(); + cy.location('pathname').should('eq', '/zh/glossary'); + }); + + for (const width of [375, 390]) { + it(`keeps the Chinese glossary inside the ${width}px viewport`, () => { + cy.viewport(width, 844); + cy.visit('/zh/glossary'); + cy.document().then((document) => { + expect(document.documentElement.scrollWidth).to.be.at.most( + document.documentElement.clientWidth, + ); + }); + cy.get('input[placeholder="搜索 MTP、延迟、FP4…"]').should('be.visible'); + cy.contains('button', '智能体推理').should('be.visible'); + }); + } + }); + + describe('Land acknowledgement pages', () => { + it('renders the English source and its Chinese hreflang at 1440px', () => { + cy.viewport(1440, 900); + cy.visit('/land-acknowledgement'); + cy.contains('h1', 'Indigenous homelands').should('be.visible'); + cy.get('link[rel="alternate"][hreflang="zh-CN"]').should('exist'); + }); + + it('uses the approved Chinese page term and keeps every nation visible on mobile', () => { + cy.viewport(375, 812); + cy.visit('/zh/land-acknowledgement'); + + cy.get('[data-testid="land-acknowledgement-page"]').should( + 'contain.text', + '原住民传统领地声明', + ); + cy.title().should('contain', '原住民传统领地声明'); + cy.get('[data-testid="land-acknowledgement-san-jose"]').should( + 'contain.text', + 'Muwekma Ohlone', + ); + cy.get('[data-testid="land-acknowledgement-los-angeles"]').should('contain.text', 'Tongva'); + cy.get('[data-testid="land-acknowledgement-chicago"]').should('contain.text', 'Potawatomi'); + cy.get('link[rel="alternate"][hreflang="en"]').should('exist'); + cy.document().then((document) => { + expect(document.documentElement.scrollWidth).to.be.at.most( + document.documentElement.clientWidth, + ); + }); + }); + }); + describe('zh blog post page', () => { before(() => { cy.visit('/zh/blog/inferencemax-open-source-inference-benchmarking'); diff --git a/packages/app/src/app/zh/about/page.tsx b/packages/app/src/app/zh/about/page.tsx index e34141ffb..6213e8b72 100644 --- a/packages/app/src/app/zh/about/page.tsx +++ b/packages/app/src/app/zh/about/page.tsx @@ -59,19 +59,17 @@ export default function AboutPageZh() { 实验室的容量规划策略团队,以及多家数十亿美元级 NeoCloud。了解更多详情请阅读我们的文章:{' '} - InferenceX v1(英文文章) + InferenceX v1 、{' '} - InferenceX v2(英文文章) + InferenceX v2 。

diff --git a/packages/app/src/app/zh/land-acknowledgement/page.tsx b/packages/app/src/app/zh/land-acknowledgement/page.tsx index ddfcffc27..5d3acc9eb 100644 --- a/packages/app/src/app/zh/land-acknowledgement/page.tsx +++ b/packages/app/src/app/zh/land-acknowledgement/page.tsx @@ -7,40 +7,41 @@ import { SITE_URL } from '@semianalysisai/inferencex-constants'; const REGIONAL_ACKNOWLEDGEMENTS_ZH = [ { region: 'San Jose', - peoples: 'Muwekma Ohlone 部落', + peoples: 'Muwekma Ohlone Tribe', acknowledgement: - '我们位于 San Jose 地区的基准测试基础设施运行在旧金山湾区 Muwekma Ohlone 部落未被让渡的祖传家园之上。', + '我们位于 San Jose 地区的基准测试基础设施,建在旧金山湾区 Muwekma Ohlone Tribe 尚未割让的祖居地上。', }, { region: 'Los Angeles', - peoples: 'Tongva、Tataviam、Serrano、Kizh 和 Chumash 族群', + peoples: 'Tongva、Tataviam、Serrano、Kizh 和 Chumash 原住民族', acknowledgement: - '我们位于 Los Angeles 地区的基准测试基础设施运行在 Tongva、Tataviam、Serrano、Kizh 和 Chumash 族群最初居住并至今仍在守护的土地之上。', + '我们位于 Los Angeles 地区的基准测试基础设施,建在 Tongva、Tataviam、Serrano、Kizh 和 Chumash 原住民族最早居住、至今仍在生活并守护的土地上。', }, { region: 'Chicago', - peoples: '三火议会、Illinois 联盟、Miami、Ho-Chunk、Menominee、Fox 和 Sac 族群', + peoples: + 'Council of the Three Fires、Illinois Confederacy、Miami、Ho-Chunk、Menominee、Fox 和 Sac 原住民族', acknowledgement: - '我们位于 Chicago 地区的基准测试基础设施运行在由三火议会(Ojibwe、Odawa 和 Potawatomi 部落)、Illinois 联盟以及包括 Miami、Ho-Chunk、Menominee、Fox 和 Sac 在内的众多原住民族群守护的土地之上。', + '我们位于 Chicago 地区的基准测试基础设施,建在 Council of the Three Fires(Ojibwe、Odawa 和 Potawatomi Nations)、Illinois Confederacy 以及 Miami、Ho-Chunk、Menominee、Fox 和 Sac 等其他原住民族世代守护的土地上。', }, ]; +const LAND_ACKNOWLEDGEMENT_DESCRIPTION = + 'InferenceX 就 San Jose、Los Angeles 和 Chicago 美国基准测试集群所在的原住民族与传统领地所作的声明。'; + export const metadata: Metadata = { - title: '土地致谢', - description: - '对与 InferenceX 美国基准测试集群(San Jose、Los Angeles 和 Chicago)所在土地相关的原住民族群和家园的致谢。', + title: '原住民传统领地声明', + description: LAND_ACKNOWLEDGEMENT_DESCRIPTION, alternates: zhAlternates('/land-acknowledgement'), openGraph: { - title: '土地致谢 | InferenceX', - description: - '对与 InferenceX 美国基准测试集群(San Jose、Los Angeles 和 Chicago)所在土地相关的原住民族群和家园的致谢。', + title: '原住民传统领地声明 | InferenceX', + description: LAND_ACKNOWLEDGEMENT_DESCRIPTION, url: `${SITE_URL}/zh/land-acknowledgement`, locale: ZH_OG_LOCALE, }, twitter: { - title: '土地致谢 | InferenceX', - description: - '对与 InferenceX 美国基准测试集群(San Jose、Los Angeles 和 Chicago)所在土地相关的原住民族群和家园的致谢。', + title: '原住民传统领地声明 | InferenceX', + description: LAND_ACKNOWLEDGEMENT_DESCRIPTION, }, }; @@ -51,22 +52,21 @@ export default function LandAcknowledgementPageZh() {

- 土地致谢 + 原住民传统领地声明

- 我们致敬与我们美国基础设施所在土地相关的原住民家园。 + 我们承认并尊重美国基础设施所在地的原住民传统领地。

- InferenceX 基准测试集群为多个地区提供服务。本页聚焦于我们在美国的 San Jose、Los - Angeles 和 Chicago - 站点,并向世代守护这些土地、至今仍在延续这一使命的原住民族群致以敬意。 + InferenceX 基准测试集群服务多个地区。本页聚焦 San Jose、Los Angeles 和 Chicago + 三个美国站点,向世代守护这些土地并延续至今的原住民族致意。

{REGIONAL_ACKNOWLEDGEMENTS_ZH.map((entry) => (

- 致谢只是一个起点。我们怀着对原住民主权、历史和持续存在的社区的尊重分享这份声明,如果措辞需要改进,欢迎指正。 + 承认这些传统领地只是起点。我们怀着对原住民族主权、历史及延续至今的社群的尊重作出本声明;如有措辞需要改进,欢迎指正。

diff --git a/packages/app/src/components/about/faq-data-zh.ts b/packages/app/src/components/about/faq-data-zh.ts index 8b247b103..616d9b141 100644 --- a/packages/app/src/components/about/faq-data-zh.ts +++ b/packages/app/src/components/about/faq-data-zh.ts @@ -53,7 +53,7 @@ export const FAQ_ITEMS_ZH: readonly FaqItem[] = [ '每兆瓦 token 吞吐量(tok/s/MW)', 'P99 首 token 延迟(TTFT)', 'AgentX 场景的端到端延迟、token 间延迟(ITL)、输出吞吐量、prefix cache 行为以及会话与 subagent 执行情况', - '每百万 token 成本(总计、输入、输出)——涵盖超大规模云、NeoCoud 和裸机租赁定价', + '每百万 token 成本(总计、输入、输出)——涵盖超大规模云、NeoCloud 和裸机租赁定价', '每 token 能耗(焦耳,总计、输入、输出)', '用户自定义成本和功耗计算', ], diff --git a/packages/app/src/components/about/faq-data.test.ts b/packages/app/src/components/about/faq-data.test.ts new file mode 100644 index 000000000..68de50116 --- /dev/null +++ b/packages/app/src/components/about/faq-data.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { FAQ_ITEMS_ZH } from './faq-data-zh'; +import { FAQ_ITEMS } from './faq-data'; + +describe('localized About FAQ', () => { + it('keeps every English FAQ item and optional field represented in Chinese', () => { + expect(FAQ_ITEMS_ZH).toHaveLength(FAQ_ITEMS.length); + expect( + FAQ_ITEMS_ZH.map((item) => ({ + hasAnswer: Boolean(item.answer), + listLength: item.list?.length ?? 0, + linkHref: item.link?.href ?? null, + })), + ).toEqual( + FAQ_ITEMS.map((item) => ({ + hasAnswer: Boolean(item.answer), + listLength: item.list?.length ?? 0, + linkHref: item.link?.href ?? null, + })), + ); + }); + + it('spells the NeoCloud pricing category correctly', () => { + const metrics = FAQ_ITEMS_ZH.find((item) => item.question === 'InferenceX 测量哪些指标?'); + expect(metrics?.list?.join('\n')).toContain('NeoCloud'); + expect(metrics?.list?.join('\n')).not.toContain('NeoCoud'); + }); +}); diff --git a/packages/app/src/components/api-documentation/api-reference-page.tsx b/packages/app/src/components/api-documentation/api-reference-page.tsx index 4db1ee4ad..9c8773713 100644 --- a/packages/app/src/components/api-documentation/api-reference-page.tsx +++ b/packages/app/src/components/api-documentation/api-reference-page.tsx @@ -305,7 +305,7 @@ export function ApiReferencePage({ locale }: { locale: ApiDocumentationLocale }) {documentation.schemaNotes.map((schema) => (
{schema.title}
diff --git a/packages/app/src/lib/api-documentation.test.ts b/packages/app/src/lib/api-documentation.test.ts new file mode 100644 index 000000000..89c5c50d9 --- /dev/null +++ b/packages/app/src/lib/api-documentation.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { getApiDocumentation } from './api-documentation'; + +describe('Chinese API documentation copy', () => { + it('uses the established evaluation and speculative-decoding terminology', () => { + const documentation = getApiDocumentation('zh'); + const availability = documentation.groups + .flatMap((group) => group.operations) + .find((operation) => operation.id === 'get-availability'); + const evaluations = documentation.groups + .flatMap((group) => group.operations) + .find((operation) => operation.id === 'list-evaluations'); + + expect(availability?.description).toContain('投机解码方式'); + expect(evaluations?.summary).toContain('评估'); + expect(evaluations?.description).toContain('评估结果'); + }); +}); diff --git a/packages/app/src/lib/api-documentation.ts b/packages/app/src/lib/api-documentation.ts index 4a22d16c5..470ad3233 100644 --- a/packages/app/src/lib/api-documentation.ts +++ b/packages/app/src/lib/api-documentation.ts @@ -506,7 +506,7 @@ export const apiDocumentationGroups: readonly ApiDocumentationGroup[] = [ title: text('Core benchmark data', '核心基准数据'), description: text( 'Benchmark results, availability, workflow provenance, evaluations, and reliability.', - '基准结果、可用配置、工作流来源、评测与可靠性数据。', + '基准结果、可用配置、工作流来源、评估与可靠性数据。', ), }, { @@ -552,7 +552,7 @@ export const apiOperations: readonly ApiOperation[] = [ summary: text('List available benchmark configurations', '列出可用的基准配置'), description: text( 'Returns model, sequence, precision, hardware, framework, speculative method, benchmark type, and date combinations that have benchmark data.', - '返回已有基准数据的模型、序列、精度、硬件、框架、推测方法、基准类型和日期组合。', + '返回已有基准数据的模型、序列、精度、硬件、框架、投机解码方式、基准类型和日期组合。', ), audience: 'public', stability: 'stable', @@ -829,16 +829,16 @@ export const apiOperations: readonly ApiOperation[] = [ group: 'core', method: 'GET', path: '/api/v1/evaluations', - summary: text('List evaluation aggregates', '列出评测汇总'), + summary: text('List evaluation aggregates', '列出评估汇总'), description: text( 'Returns latest-attempt evaluation results with configuration, task, provenance, and metric values.', - '返回最新尝试的评测结果,包含配置、任务、来源和指标值。', + '返回最新尝试的评估结果,包含配置、任务、来源和指标值。', ), audience: 'public', stability: 'stable', parameters: [], responses: [ - success('Evaluation result rows.', '评测结果行。', evaluationsSchema, [ + success('Evaluation result rows.', '评估结果行。', evaluationsSchema, [ { id: 72, config_id: 11, @@ -870,7 +870,7 @@ export const apiOperations: readonly ApiOperation[] = [ errorResponse( '500', 'The evaluation query failed.', - '评测查询失败。', + '评估查询失败。', 'Internal server error', ), ], diff --git a/packages/app/src/lib/glossary-zh.ts b/packages/app/src/lib/glossary-zh.ts index cbbb159ad..a03b69e1b 100644 --- a/packages/app/src/lib/glossary-zh.ts +++ b/packages/app/src/lib/glossary-zh.ts @@ -36,7 +36,7 @@ const translations: Readonly> = { definition: 'AI 推理是使用已经训练好的模型处理新输入并生成输出的过程;对大语言模型而言,通常就是处理提示词并生成 token。', explanation: - '训练阶段会更新模型权重,推理阶段则使用这些权重。生产系统还需要推理引擎负责调度请求、管理内存、合并批次,并在一个或多个加速器上执行内核。相同模型在不同软硬件栈上的表现可能相差数倍。', + '训练阶段会更新模型权重,推理阶段则使用这些权重。生产系统还需要推理引擎负责调度请求、管理内存、合并批次,并在一个或多个加速器上执行内核。周边软硬件栈不同,性能也会随之变化。', significance: '推理既是模型问题,也是系统问题。用户体验取决于延迟和交互性,运营成本则取决于吞吐量、利用率、功耗与硬件成本;只优化其中一个维度,往往会牺牲另一个维度。', benchmarkContext: @@ -221,7 +221,7 @@ const translations: Readonly> = { '批处理就像让多名乘客坐同一辆巴士:芯片一次处理多个请求,让每趟计算完成更多有效工作。', definition: '批处理将多个请求的工作组合起来,使加速器能够一起处理它们的 token。', explanation: - '大型矩阵运算比大量微小运算更能发挥芯片效率。现代推理引擎采用连续批处理,请求到达和结束时动态加入或退出,无需等待固定批次全部完成。', + '大型矩阵运算比大量微小运算更能发挥芯片效率。现代推理引擎采用连续批处理,请求到达和结束时动态加入或退出,无需等待固定批次全部完成。由此形成的批次形状会在预填充和解码过程中不断变化。', significance: '批处理是吞吐量与延迟核心权衡的来源。更大的有效批次能摊薄权重读取和内核启动开销,但通常会增加每位用户的 token 间隔。', benchmarkContext: @@ -274,7 +274,7 @@ const translations: Readonly> = { explanation: 'InferenceX 根据每小时总体拥有成本和实测 token 吞吐量计算该指标。它可能按总 token 报告,也可能区分输入和输出 token,因此比较前必须确认分母。', significance: - '该指标把系统性能转化为服务经济性,但仍受工作负载、交互性、利用率、缓存命中和成本假设影响;离线低交互点不能直接与实时端点比较。', + '该指标把系统性能转化为服务经济性,但仍受工作负载、交互性、利用率、缓存命中和成本假设影响;低吞吐量的离线运行点与高交互性实时端点属于不同的运行区间,不能直接比较。', benchmarkContext: '成本曲线使用与吞吐曲线相同的并发扫描。在等交互性下,更低的 $/M 表示以更少建模成本提供相同流式体验。', measurement: { @@ -339,7 +339,7 @@ const translations: Readonly> = { explanation: '每个新 token 都依赖此前 token,因此时间维度无法完全并行。模型会反复读取权重与该序列的 KV 缓存,使解码对内存带宽、批处理和通信尤其敏感。', significance: - '解码决定流式交互性,也常主导长输出成本。推测解码、MTP、量化和宽专家并行都试图减少每个有效 token 的工作量或耗时。', + '解码决定流式交互性,也常主导长输出成本。投机解码、MTP、量化和宽专家并行都试图减少每个有效 token 的工作量或耗时。', benchmarkContext: 'InferenceX 用 tok/s/user 与总 tok/s/chip 展示不同并发下的解码性能。公平比较必须匹配输出长度、批形状、精度和并行策略。', }, @@ -382,12 +382,12 @@ const translations: Readonly> = { 'InferenceX 中的 disagg 不是万能开关。应查看预填充/解码 world size、TP/EP 布局、框架、网络域,以及分离前沿真正领先的交互性区间。', }, 'speculative-decoding': { - term: '推测解码', + term: '投机解码', aliases: ['speculative decoding', '草稿与验证解码'], plainEnglish: - '推测解码让一个便宜的助手先起草多个 token,再由完整模型一次性审核,省去部分逐个生成步骤。', + '投机解码让一个便宜的助手先起草多个 token,再由完整模型一次性审核,省去部分逐个生成步骤。', definition: - '推测解码先以低成本提出多个未来 token,再由目标模型批量验证,从而减少昂贵的串行解码步数。', + '投机解码先以低成本提出多个未来 token,再由目标模型批量验证,从而减少昂贵的串行解码步数。', explanation: '草稿模型或内置预测头生成候选,目标模型在一次批量验证中评估这些候选并接受有效前缀;严格实现时不会改变目标分布。', significance: @@ -400,7 +400,7 @@ const translations: Readonly> = { aliases: ['multi-token prediction', '多 token 预测头'], plainEnglish: 'MTP 让模型一次猜测多个后续 token 并一起验证,从而减少缓慢的逐 token 步骤。', definition: - '多 token 预测(MTP)使用与主模型共同训练的辅助预测头,提出多个未来 token 供推测验证。', + '多 token 预测(MTP)使用与主模型共同训练的辅助预测头,提出多个未来 token 供投机验证。', explanation: 'MTP 不需要独立草稿模型,候选来自目标模型自身表示,因此分布更一致、部署也更简单;但它要求检查点包含兼容 MTP 模块,且推理引擎支持验证路径。', significance: @@ -410,12 +410,12 @@ const translations: Readonly> = { }, eagle: { term: 'EAGLE', - aliases: ['EAGLE 推测解码', 'EAGLE-3'], + aliases: ['EAGLE 投机解码', 'EAGLE-3'], plainEnglish: 'EAGLE 是一种为主模型起草多个可能后续 token 的方法,可让答案流式输出得更快。', definition: - 'EAGLE 是一组推测解码方法:利用与目标语言模型相关的特征预测草稿序列,再由目标模型验证。', + 'EAGLE 是一组投机解码方法:利用与目标语言模型相关的特征预测草稿序列,再由目标模型验证。', explanation: - '推理框架通常通过推测步数、草稿 token 数和候选宽度等参数暴露 EAGLE。模型检查点、草稿组件与引擎实现必须匹配。', + '推理框架通常通过投机步数、草稿 token 数和候选宽度等参数暴露 EAGLE。模型检查点、草稿组件与引擎实现必须匹配。', significance: 'EAGLE 能提高每个目标模型步接受的 token 数,但结果依赖工作负载;接受行为、草稿开销、模型架构和批大小共同决定端到端收益。', benchmarkContext: @@ -690,11 +690,11 @@ const translations: Readonly> = { 'SGLang 是用于快速服务语言模型的开源软件,提供面向复杂 AI 工作负载的调度和优化功能。', definition: 'SGLang 是面向高性能 LLM 与多模态推理的开源服务引擎和语言模型编程系统。', explanation: - '服务运行时包含连续批处理、前缀感知调度、分布式并行、推测解码,以及面向 NVIDIA/AMD 芯片的多种注意力和 MoE 内核后端。', + '服务运行时包含连续批处理、前缀感知调度、分布式并行、投机解码,以及面向 NVIDIA/AMD 芯片的多种注意力和 MoE 内核后端。', significance: 'SGLang 快速迭代的版本和模型专用内核可在硬件不变时显著改变吞吐量;低并发受调度开销影响,其他区间则由注意力、MoE 与通信内核主导。', benchmarkContext: - 'InferenceX 持续重跑固定版本的 SGLang 方案。跨版本曲线会保留改动对完整性能区间的影响。', + 'InferenceX 持续重跑版本固定的 SGLang 方案。对比不同版本的曲线,可以看出改动如何影响完整运行区间,并发现单个峰值点掩盖的回归或提升。', }, 'tensorrt-llm': { term: 'TensorRT-LLM', diff --git a/packages/app/src/lib/glossary.test.ts b/packages/app/src/lib/glossary.test.ts index e524c5805..760c019ce 100644 --- a/packages/app/src/lib/glossary.test.ts +++ b/packages/app/src/lib/glossary.test.ts @@ -123,6 +123,7 @@ describe('Chinese glossary content', () => { expect(getZhGlossaryEntry('trace-replay')?.term).toBe('轨迹回放'); expect(getZhGlossaryEntry('closed-loop-benchmark')?.term).toBe('闭环基准测试'); expect(getZhGlossaryEntry('subagent')?.term).toBe('子智能体'); + expect(getZhGlossaryEntry('speculative-decoding')?.term).toBe('投机解码'); expect(getZhGlossaryEntry('multi-token-prediction')?.term).toBe('多 token 预测'); expect(getZhGlossaryEntry('not-a-real-term')).toBeUndefined(); @@ -134,6 +135,12 @@ describe('Chinese glossary content', () => { }); } }); + + it('preserves the source scope in audited Chinese definitions', () => { + expect(getZhGlossaryEntry('ai-inference')?.explanation).not.toContain('相差数倍'); + expect(getZhGlossaryEntry('cost-per-million-tokens')?.significance).toContain('低吞吐量'); + expect(getZhGlossaryEntry('sglang')?.benchmarkContext).toContain('回归或提升'); + }); }); describe('glossary navigation', () => { From b95d088c427978d50f88bec407ee230615f0de41 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 23 Aug 2026 05:41:48 -0700 Subject: [PATCH 2/9] fix(zh): preserve audited article terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the established speculative-decoding term and keep the audited DeepSeek V4 and GB300 inline code byte-identical to their English sources. 中文:统一使用既定的“投机解码”术语,并确保 DeepSeek V4 与 GB300 审核段落中的内联代码与英文原文逐字节一致。 --- ...nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx | 2 +- ...seekv4-16t-day-0-to-day-43-performance.mdx | 2 +- ...nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx | 2 +- ...x-v2-nvidia-blackwell-vs-amd-vs-hopper.mdx | 8 ++--- packages/app/src/lib/blog-content.test.ts | 33 +++++++++++++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx b/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx index ec6db67ae..2034f99e1 100644 --- a/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx +++ b/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx @@ -71,7 +71,7 @@ NVIDIA 还发布了量化权重版本 [`nvidia/GLM-5-NVFP4`](https://huggingface - [sgl-project/sglang #21405](https://github.com/sgl-project/sglang/pull/21405) 为稀疏 MLA 启用了 **IndexCache**,在连续 decode 步骤间复用索引张量,在相同内核调用序列上带来 >10% 的 decode 吞吐量提升。 - [flashinfer-ai/flashinfer #2726](https://github.com/flashinfer-ai/flashinfer/pull/2726)(FlashInfer v0.6.6.post1)修复了一个间歇性 NVFP4 非法内存访问 bug,此前一直[阻塞](https://github.com/sgl-project/sglang/issues/19081) NVFP4 的功能验证签核;[flashinfer-ai/flashinfer #2836](https://github.com/flashinfer-ai/flashinfer/pull/2836)(v0.6.7)提升了 trtllm-gen 稀疏 MLA 的性能上限。 -**MTP。** GLM-5 复用了 SGLang 为 DeepSeek V3.2 构建的 EAGLE 推测解码管线(`--speculative-algorithm EAGLE --speculative-num-steps 3 --speculative-eagle-topk 1 --speculative-num-draft-tokens 4`),并通过 `SGLANG_ENABLE_SPEC_V2=1` 启用 overlap 调度器。H200 和 B200 使用完全相同的参数集——两款 SKU 在下面方案中唯一的不同是模型检查点和注意力后端的选择。 +**MTP。** GLM-5 复用了 SGLang 为 DeepSeek V3.2 构建的 EAGLE 投机解码管线(`--speculative-algorithm EAGLE --speculative-num-steps 3 --speculative-eagle-topk 1 --speculative-num-draft-tokens 4`),并通过 `SGLANG_ENABLE_SPEC_V2=1` 启用 overlap 调度器。H200 和 B200 使用完全相同的参数集——两款 SKU 在下面方案中唯一的不同是模型检查点和注意力后端的选择。 ## 详细数据 diff --git a/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx b/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx index bceea89d9..49c04a597 100644 --- a/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx +++ b/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx @@ -179,7 +179,7 @@ ATOM 的几乎每条热路径也都走了 fallback:FP4 MoE 被迫使用 Triton ### NVIDIA TensorRT-LLM 的 Bug 及缺乏第 0 天 DeepSeekV4 Pro 支持 -TensorRT 无法开箱即用地支持 DeepSeek v4,因为 `mhcFusedHcKernel.cu` 中有一个硬编码的 `FHC_HIDDEN = 4096` 常量。问题在于 SHAPE_K、residual/x TMA 描述符以及 MMA kernel 模板实例化都绑定在该隐藏维度大小上。所有之前的 DeepSeek 模型和 DeepSeek v4 flash 的隐藏维度均为 4096,因此暂时不出问题。但尝试对 DeepSeek v4 Pro 进行推理时,会触发 `"mhcFusedHcLaunch: hidden_size=7168 not supported (only 4096)"` 的保护错误。 +TensorRT 无法开箱即用地支持 DeepSeek v4,因为 `mhcFusedHcKernel.cu` 中有一个硬编码的 `FHC_HIDDEN = 4096` 常量。问题在于 SHAPE_K、residual/x TMA 描述符以及 MMA kernel 模板实例化都绑定在该隐藏维度大小上。所有之前的 DeepSeek 模型和 DeepSeek v4 flash 的隐藏维度均为 4096,因此暂时不出问题。但尝试对 DeepSeek v4 Pro 进行推理时,会触发 `“mhcFusedHcLaunch: hidden_size=7168 not supported (only 4096)”` 的保护错误。 Nvidia 工程师也遇到了这个保护错误,但他们没有添加代码来支持 DeepSeek v4 Pro 的 7168 隐藏维度,而是直接[移除了保护检查](https://github.com/NVIDIA/TensorRT-LLM/commit/b3f45bb608aecca666a451ca5138b81470487f05)。毫不意外,错误确实消失了。 diff --git a/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx b/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx index 9970710b7..a94028942 100644 --- a/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx +++ b/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx @@ -121,7 +121,7 @@ GB200 的每 GPU 峰值吞吐量为 8,933,交互性为 15.3 tok/s/user。GB300 | 36 | 1,376 | 2,036 | 1.48x | $0.65 | $0.35 | 1.88x | | 50 | 649 | 941 | 1.45x | $4.78 | $1.58 | 3.03x | -标题中 **2.83 倍每 GPU 吞吐量峰值出现在 27 tok/s/user(性价比 2.31 倍),位于曲线中段**而非峰值吞吐处。在 20 tok/s/user 以下,两套机架都运行足够宽的预填充批次,HBM 余量优势被抹平;在 36 tok/s/user 以上,两者都运行窄批次,没有哪套机架拥有宽 EP 能充分利用的配方。22–32 tok/s/user 区间是 GB300 的 1.5 倍 HBM 容量让其停留在一个更高 Pareto 节点上的地方(`conc=3072, 28 GPU 预填充, 32 GPU 解码 EP=16, 6,812 tok/s/GPU at 25.9 tok/s/user`),而 GB200 在同等交互性下没有等效配方——其最接近的配方是在 32-GPU 解码池上 conc=256 / 512,仅能提供 1,614–2,005 tok/s/GPU。 +标题中 **2.83 倍每 GPU 吞吐量峰值出现在 27 tok/s/user(性价比 2.31 倍),位于曲线中段**而非峰值吞吐处。在 20 tok/s/user 以下,两套机架都运行足够宽的预填充批次,HBM 余量优势被抹平;在 36 tok/s/user 以上,两者都运行窄批次,没有哪套机架拥有宽 EP 能充分利用的配方。22–32 tok/s/user 区间是 GB300 的 1.5 倍 HBM 容量让其停留在一个更高 Pareto 节点上的地方(`conc=3072, 28 GPU prefill, 32 GPU decode EP=16, 6,812 tok/s/GPU at 25.9 tok/s/user`),而 GB200 在同等交互性下没有等效配方——其最接近的配方是在 32-GPU 解码池上 conc=256 / 512,仅能提供 1,614–2,005 tok/s/GPU。 50 tok/s/user 行显示成本比率(3.03x)再次扩大,因为两条曲线都进入了右侧的陡峭衰减区。这里的解读需要更谨慎——两套机架在该区域的 Pareto 覆盖都很薄(GB200 在约 33 tok/s/user 处各有一个节点,GB300 在约 37 tok/s/user 处各有一个节点,然后是到约 73 tok/s/user 的长尾),因此插值是在两个间隔较大的实测节点之间读取差距。22–32 tok/s/user 区间才是 GB300 优势的可靠甜蜜点;将 50 tok/s/user 行视为方向性参考。 diff --git a/packages/app/content/blog/zh/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper.mdx b/packages/app/content/blog/zh/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper.mdx index e9051378a..48fc4ddb8 100644 --- a/packages/app/content/blog/zh/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper.mdx +++ b/packages/app/content/blog/zh/inferencex-v2-nvidia-blackwell-vs-amd-vs-hopper.mdx @@ -359,7 +359,7 @@ OpenRouter 数据还显示 Nebius AI Studio (Fast) 以 167 tok/sec/user 的交 caption="来源:SemiAnalysis InferenceX" /> -在如此高的交互性下,有必要采用 MTP 等推测解码技术来实现足够高的吞吐量,使推理具有经济可行性。幸运的是,MTP 能够在对模型精度影响极小的情况下提升吞吐量。我们将在文章后续部分进一步讨论 MTP 及其如何用于提升吞吐量/降低成本。 +在如此高的交互性下,有必要采用 MTP 等投机解码技术来实现足够高的吞吐量,使推理具有经济可行性。幸运的是,MTP 能够在对模型精度影响极小的情况下提升吞吐量。我们将在文章后续部分进一步讨论 MTP 及其如何用于提升吞吐量/降低成本。 最后,我们再展示一张 FP8 DeepSeek 工作负载在 125 tok/s/user 下的图表。这是另一个低延迟工作负载,MTP 在其中显著改善了经济可行性。与前面的例子一样,我们注意到在这些较高交互性范围内,最便宜的配置都使用了 MTP。 @@ -519,7 +519,7 @@ AMD 有许多才华横溢的优秀工程师在 ATOM 上工作,我们鼓励 AMD ## 多 Token 预测(MTP) -推测解码通过使用一个小型、低成本的草稿模型提前提议多个 token 来降低自回归生成的成本。大模型然后在一次类似预填充计算的前向传播中验证所提议的 token。对于给定的输入序列长度,当输入多出 N 个 token 时,单次前向传播的耗时大致相同。推测解码利用这一特性,在小模型上运行推理生成多个 token 供主模型在一次前向传播中验证,在相似的时间预算内最多额外产出 N 个 token。 +投机解码通过使用一个小型、低成本的草稿模型提前提议多个 token 来降低自回归生成的成本。大模型然后在一次类似预填充计算的前向传播中验证所提议的 token。对于给定的输入序列长度,当输入多出 N 个 token 时,单次前向传播的耗时大致相同。投机解码利用这一特性,在小模型上运行推理生成多个 token 供主模型在一次前向传播中验证,在相似的时间预算内最多额外产出 N 个 token。
-在大批次下,推理机制相比小批次受内存带宽限制更少。由于推测解码(包括 MTP)的工作原理是用多余的计算换取更少的内存受限解码步骤,推测 token 带来的额外验证工作可能无法恰好利用空闲算力,导致在大批次下的改进幅度较小。 +在大批次下,推理机制相比小批次受内存带宽限制更少。由于投机解码(包括 MTP)的工作原理是用多余的计算换取更少的内存受限解码步骤,投机 token 带来的额外验证工作可能无法恰好利用空闲算力,导致在大批次下的改进幅度较小。 从成本角度来看,MTP 能带来巨大的成本节省。在下表中,我们看到使用 Dynamo TRT 运行 FP4 的 DeepSeek-R1-0528 每百万总 token 成本为 $0.251,但启用 MTP 可将成本大幅降低至每百万总 token 仅 $0.057。 @@ -615,7 +615,7 @@ Anthropic 最近伴随 Opus 4.6 发布了"[fast mode](https://code.claude.com/do caption="来源:SemiAnalysis InferenceX" /> -此外,我们观察到推测解码等推理优化技术可以直接降低推理成本,无需新芯片。 +此外,我们观察到投机解码等推理优化技术可以直接降低推理成本,无需新芯片。 以下面的例子为例,DeepSeek R1 FP4 在 8k/1k 工作负载上。在 150 tok/sec/user 的交互性水平下,基线 GB300 Dynamo TRT 的每百万 token 成本约为 $2.35,而启用 MTP 将价格降至约 $0.11。仅通过采用一种推理优化技术,就实现了该交互性水平下约 21 倍的价格降低。 diff --git a/packages/app/src/lib/blog-content.test.ts b/packages/app/src/lib/blog-content.test.ts index 8b660d5c7..2563644b1 100644 --- a/packages/app/src/lib/blog-content.test.ts +++ b/packages/app/src/lib/blog-content.test.ts @@ -55,6 +55,15 @@ function localImageRefs(raw: string): string[] { const countFigures = (raw: string) => (raw.match(/ (raw.match(/^\$\$\s*$/gmu) ?? []).length; +function inlineCodeSpans(raw: string): string[] { + const withoutStructuredContent = raw + .replaceAll(/\{`[\s\S]*?`\}<\/JsonLd>/gu, '') + .replaceAll(/```[\s\S]*?```/gu, ''); + return [...withoutStructuredContent.matchAll(/(?[^`\n]+)`(?!`)/gu)].map( + (match) => match.groups!.code, + ); +} + it('finds English posts to check', () => { expect(enFiles.length).toBeGreaterThan(0); }); @@ -102,3 +111,27 @@ describe.each(enFiles)('%s', (file) => { } }); }); + +it('preserves protected inline code in the audited Chinese article passages', () => { + const auditedSpans = [ + { + file: 'deepseekv4-16t-day-0-to-day-43-performance.mdx', + code: '“mhcFusedHcLaunch: hidden_size=7168 not supported (only 4096)”', + }, + { + file: 'gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx', + code: 'conc=3072, 28 GPU prefill, 32 GPU decode EP=16, 6,812 tok/s/GPU at 25.9 tok/s/user', + }, + ] as const; + + for (const { file, code } of auditedSpans) { + expect( + inlineCodeSpans(read(path.join(CONTENT_DIR, file))), + `${file}: English source`, + ).toContain(code); + expect( + inlineCodeSpans(read(path.join(ZH_DIR, file))), + `${file}: Chinese translation`, + ).toContain(code); + } +}); From 3ad1629fbb05773bf148b28c76241f394c698361 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 23 Aug 2026 06:11:09 -0700 Subject: [PATCH 3/9] fix(zh): close content review gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse multiline blog tags and restore complete locale metadata parity. Cover API schema containment plus every localized Blog and Glossary listing-to-detail journey at desktop and mobile widths. Correct the TCO glossary translation to preserve software and workload scope. 中文:修复博客多行标签解析并恢复中英文 metadata 完整一致;覆盖 API 数据结构布局,以及 Blog 和 Glossary 在桌面端与移动端的全部双语列表到详情页点击链路;同时修正 TCO 术语页中软件实现与工作负载范围的翻译。 --- ...seekv4-16t-day-0-to-day-43-performance.mdx | 1 + .../app/cypress/e2e/api-documentation.cy.ts | 17 ++--- packages/app/cypress/e2e/blog.cy.ts | 54 ++++++++++++++++ packages/app/cypress/e2e/zh-pages.cy.ts | 62 +++++++++++++++++++ packages/app/src/app/blog/[slug]/page.tsx | 2 +- packages/app/src/app/glossary/[slug]/page.tsx | 2 +- packages/app/src/app/zh/blog/[slug]/page.tsx | 2 +- .../app/src/app/zh/glossary/[slug]/page.tsx | 2 +- .../api-documentation/api-reference-page.tsx | 1 + .../src/components/blog/blog-post-card.tsx | 2 + .../components/glossary/glossary-browser.tsx | 2 + packages/app/src/lib/blog-content.test.ts | 38 ++++++++---- packages/app/src/lib/glossary-zh.ts | 2 +- packages/app/src/lib/glossary.test.ts | 6 ++ 14 files changed, 170 insertions(+), 23 deletions(-) diff --git a/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx b/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx index 49c04a597..d404b5aae 100644 --- a/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx +++ b/packages/app/content/blog/zh/deepseekv4-16t-day-0-to-day-43-performance.mdx @@ -19,6 +19,7 @@ tags: - sglang - vllm - trtllm + - cann --- _本文最初于 2026 年 6 月 9 日发布在 [SemiAnalysis 通讯](https://newsletter.semianalysis.com/p/deepseekv4-16t-day-0-to-day-43-performance)。_ diff --git a/packages/app/cypress/e2e/api-documentation.cy.ts b/packages/app/cypress/e2e/api-documentation.cy.ts index a646f015a..ca11fe531 100644 --- a/packages/app/cypress/e2e/api-documentation.cy.ts +++ b/packages/app/cypress/e2e/api-documentation.cy.ts @@ -81,18 +81,21 @@ describe('API documentation', () => { }); for (const [path, width] of [ + ['/api', 1440], ['/api', 375], ['/zh/api', 390], ] as const) { - it(`keeps wide API data reachable without page overflow at ${path} on ${width}px`, () => { + it(`contains wide schema notes without page overflow at ${path} on ${width}px`, () => { cy.viewport(width, 844); cy.visit(path); - cy.get('[data-testid="api-endpoint-list-benchmarks"] summary').click(); - cy.get('[data-testid="api-endpoint-list-benchmarks"] .overflow-x-auto') - .first() - .should('be.visible') - .then(($scroller) => { - expect($scroller[0].scrollWidth).to.be.greaterThan($scroller[0].clientWidth); + cy.get('[data-testid="api-schema-note"]').first().as('schemaNote').should('be.visible'); + cy.get('@schemaNote').should('have.css', 'min-width', '0px'); + cy.get('@schemaNote') + .find('.overflow-x-auto') + .then(($scrollers) => { + expect( + [...$scrollers].some((scroller) => scroller.scrollWidth > scroller.clientWidth), + ).to.equal(true); }); cy.document().then((document) => { expect(document.documentElement.scrollWidth).to.be.at.most( diff --git a/packages/app/cypress/e2e/blog.cy.ts b/packages/app/cypress/e2e/blog.cy.ts index 351f37fef..fefe484a5 100644 --- a/packages/app/cypress/e2e/blog.cy.ts +++ b/packages/app/cypress/e2e/blog.cy.ts @@ -1,4 +1,58 @@ +const BLOG_SITE_URL = 'https://inferencex.semianalysis.com'; +const BLOG_POST_COUNT = 19; + +const BLOG_JOURNEY_MATRIX = [ + { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 1440 }, + { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 375 }, + { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 1440 }, + { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 390 }, +] as const; + +function exerciseEveryBlogJourney({ + basePath, + detailPrefix, + alternateLocale, + width, +}: (typeof BLOG_JOURNEY_MATRIX)[number]) { + cy.viewport(width, 900); + cy.visit(basePath); + cy.get('[data-testid="blog-post-card"]') + .should('have.length', BLOG_POST_COUNT) + .then(($cards) => { + const hrefs = [...$cards].map((card) => card.getAttribute('href')); + expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); + expect(new Set(hrefs).size).to.equal(BLOG_POST_COUNT); + expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); + + for (const href of hrefs as string[]) { + const slug = href.slice(detailPrefix.length); + const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); + cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).click(); + cy.location('pathname').should('eq', href); + cy.get('[data-testid="blog-post-page"]') + .should('be.visible') + .and('have.attr', 'data-blog-slug', slug); + cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( + 'have.attr', + 'href', + `${BLOG_SITE_URL}${alternatePath}`, + ); + cy.go('back'); + cy.location('pathname').should('eq', basePath); + cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).should('exist'); + } + }); +} + describe('Blog', () => { + describe('complete localized listing-to-article journeys', () => { + for (const scenario of BLOG_JOURNEY_MATRIX) { + it(`opens every ${scenario.basePath} article from the listing at ${scenario.width}px`, () => { + exerciseEveryBlogJourney(scenario); + }); + } + }); + describe('Blog listing page', () => { before(() => { cy.visit('/blog'); diff --git a/packages/app/cypress/e2e/zh-pages.cy.ts b/packages/app/cypress/e2e/zh-pages.cy.ts index ecb6827c3..8b23bee3b 100644 --- a/packages/app/cypress/e2e/zh-pages.cy.ts +++ b/packages/app/cypress/e2e/zh-pages.cy.ts @@ -1,3 +1,59 @@ +const GLOSSARY_SITE_URL = 'https://inferencex.semianalysis.com'; +const GLOSSARY_ENTRY_COUNT = 54; + +const GLOSSARY_JOURNEY_MATRIX = [ + { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 1440 }, + { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 375 }, + { + basePath: '/zh/glossary', + detailPrefix: '/zh/glossary/', + alternateLocale: 'en', + width: 1440, + }, + { + basePath: '/zh/glossary', + detailPrefix: '/zh/glossary/', + alternateLocale: 'en', + width: 390, + }, +] as const; + +function exerciseEveryGlossaryJourney({ + basePath, + detailPrefix, + alternateLocale, + width, +}: (typeof GLOSSARY_JOURNEY_MATRIX)[number]) { + cy.viewport(width, 900); + cy.visit(basePath); + cy.get('[data-testid="glossary-entry-link"]') + .should('have.length', GLOSSARY_ENTRY_COUNT) + .then(($links) => { + const hrefs = [...$links].map((link) => link.getAttribute('href')); + expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); + expect(new Set(hrefs).size).to.equal(GLOSSARY_ENTRY_COUNT); + expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); + + for (const href of hrefs as string[]) { + const slug = href.slice(detailPrefix.length); + const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); + cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).click(); + cy.location('pathname').should('eq', href); + cy.get('[data-testid="glossary-detail-page"]') + .should('be.visible') + .and('have.attr', 'data-glossary-slug', slug); + cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( + 'have.attr', + 'href', + `${GLOSSARY_SITE_URL}${alternatePath}`, + ); + cy.go('back'); + cy.location('pathname').should('eq', basePath); + cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).should('exist'); + } + }); +} + describe('Chinese (/zh) pages', () => { describe('zh landing page', () => { before(() => { @@ -156,6 +212,12 @@ describe('Chinese (/zh) pages', () => { }); describe('Glossary pages', () => { + for (const scenario of GLOSSARY_JOURNEY_MATRIX) { + it(`opens every ${scenario.basePath} term from the index at ${scenario.width}px`, () => { + exerciseEveryGlossaryJourney(scenario); + }); + } + it('supports search, category filtering, empty recovery, and a Chinese term click path', () => { cy.viewport(1440, 900); cy.visit('/zh/glossary'); diff --git a/packages/app/src/app/blog/[slug]/page.tsx b/packages/app/src/app/blog/[slug]/page.tsx index 4fa7a7bc2..e0c28ecc4 100644 --- a/packages/app/src/app/blog/[slug]/page.tsx +++ b/packages/app/src/app/blog/[slug]/page.tsx @@ -92,7 +92,7 @@ export default async function BlogPostPage({ params }: Props) { const breadcrumbJsonLd = buildBlogBreadcrumbJsonLd(slug, meta.title); return ( -
+
diff --git a/packages/app/src/app/glossary/[slug]/page.tsx b/packages/app/src/app/glossary/[slug]/page.tsx index 91722f144..dbe7adf24 100644 --- a/packages/app/src/app/glossary/[slug]/page.tsx +++ b/packages/app/src/app/glossary/[slug]/page.tsx @@ -119,7 +119,7 @@ export default async function GlossaryTermPage({ params }: Props) { }; return ( -
+
diff --git a/packages/app/src/app/zh/blog/[slug]/page.tsx b/packages/app/src/app/zh/blog/[slug]/page.tsx index 5b6deabe6..f7cce1af3 100644 --- a/packages/app/src/app/zh/blog/[slug]/page.tsx +++ b/packages/app/src/app/zh/blog/[slug]/page.tsx @@ -88,7 +88,7 @@ export default async function ZhBlogPostPage({ params }: Props) { const breadcrumbJsonLd = buildBlogBreadcrumbJsonLdZh(slug, meta.title); return ( -
+
diff --git a/packages/app/src/app/zh/glossary/[slug]/page.tsx b/packages/app/src/app/zh/glossary/[slug]/page.tsx index 3cc736db0..01337b307 100644 --- a/packages/app/src/app/zh/glossary/[slug]/page.tsx +++ b/packages/app/src/app/zh/glossary/[slug]/page.tsx @@ -123,7 +123,7 @@ export default async function ZhGlossaryTermPage({ params }: Props) { }; return ( -
+
diff --git a/packages/app/src/components/api-documentation/api-reference-page.tsx b/packages/app/src/components/api-documentation/api-reference-page.tsx index 9c8773713..7c670d951 100644 --- a/packages/app/src/components/api-documentation/api-reference-page.tsx +++ b/packages/app/src/components/api-documentation/api-reference-page.tsx @@ -305,6 +305,7 @@ export function ApiReferencePage({ locale }: { locale: ApiDocumentationLocale }) {documentation.schemaNotes.map((schema) => (
{schema.title}
diff --git a/packages/app/src/components/blog/blog-post-card.tsx b/packages/app/src/components/blog/blog-post-card.tsx index 4e3363582..369f38fc5 100644 --- a/packages/app/src/components/blog/blog-post-card.tsx +++ b/packages/app/src/components/blog/blog-post-card.tsx @@ -17,6 +17,8 @@ export function BlogPostCard({ slug, title, basePath = '/blog', children }: Blog return ( track('blog_post_clicked', { slug, title })} > diff --git a/packages/app/src/components/glossary/glossary-browser.tsx b/packages/app/src/components/glossary/glossary-browser.tsx index 72f6a2624..dcaede952 100644 --- a/packages/app/src/components/glossary/glossary-browser.tsx +++ b/packages/app/src/components/glossary/glossary-browser.tsx @@ -252,6 +252,8 @@ export function GlossaryBrowser({
diff --git a/packages/app/src/lib/blog-content.test.ts b/packages/app/src/lib/blog-content.test.ts index 2563644b1..2cad30d7a 100644 --- a/packages/app/src/lib/blog-content.test.ts +++ b/packages/app/src/lib/blog-content.test.ts @@ -21,6 +21,10 @@ const enFiles = fs .readdirSync(CONTENT_DIR) .filter((f) => f.endsWith('.mdx')) .toSorted(); +const zhFiles = fs + .readdirSync(ZH_DIR) + .filter((f) => f.endsWith('.mdx')) + .toSorted(); const read = (file: string) => fs.readFileSync(file, 'utf8'); @@ -33,17 +37,28 @@ function frontmatterField(raw: string, field: string): string | null { function tagList(raw: string): string[] { const fm = raw.split('---')[1] ?? ''; - const after = fm.split(/^tags:\s*$/mu)[1]; - if (!after) return []; - const tags: string[] = []; - for (const line of after.split('\n')) { - const item = /^\s+-\s+(?.*)$/u.exec(line); - if (!item) break; - tags.push(item.groups!.tag.trim()); - } - return tags; + const match = /^tags:\s*\n(?(?:[ \t]+-[^\n]*(?:\n|$))+)/mu.exec(fm); + if (!match?.groups) return []; + return [...match.groups.items.matchAll(/^\s+-\s+(?.*)$/gmu)].map((item) => + item.groups!.tag.trim(), + ); } +describe('frontmatter tag parsing', () => { + it('reads every item from a multiline tags field before the next frontmatter key', () => { + const frontmatter = `--- +title: Example +tags: + - benchmark + - cann +publishDate: '2026-08-23' +--- +`; + + expect(tagList(frontmatter)).toEqual(['benchmark', 'cann']); + }); +}); + /** Local `
` and markdown `![alt](/images/...)` references. */ function localImageRefs(raw: string): string[] { return [ @@ -64,8 +79,9 @@ function inlineCodeSpans(raw: string): string[] { ); } -it('finds English posts to check', () => { - expect(enFiles.length).toBeGreaterThan(0); +it('checks all 19 current locale pairs in both directions', () => { + expect(enFiles).toHaveLength(19); + expect(zhFiles).toEqual(enFiles); }); describe.each(enFiles)('%s', (file) => { diff --git a/packages/app/src/lib/glossary-zh.ts b/packages/app/src/lib/glossary-zh.ts index a03b69e1b..bf6c0e4bf 100644 --- a/packages/app/src/lib/glossary-zh.ts +++ b/packages/app/src/lib/glossary-zh.ts @@ -304,7 +304,7 @@ const translations: Readonly> = { significance: 'TCO 比标价更适合跨系统经济性比较,尤其是网络与电力基础设施不同的机架级产品;但它仍是模型,必须连同假设一起阅读。', benchmarkContext: - 'InferenceX 将 SemiAnalysis AI Cloud TCO 输入与实测 tok/s/chip 结合,从而区分每小时系统成本和决定该小时 token 产出的软硬件行为。', + 'InferenceX 将 SemiAnalysis AI Cloud 的 TCO 输入与实测 tok/s/chip 结合,从而把系统每小时成本与决定这一小时 token 产出的软件实现及工作负载特征分开考察。', }, 'tokens-per-megawatt': { term: '每兆瓦 token 吞吐量', diff --git a/packages/app/src/lib/glossary.test.ts b/packages/app/src/lib/glossary.test.ts index 760c019ce..5a80eeb73 100644 --- a/packages/app/src/lib/glossary.test.ts +++ b/packages/app/src/lib/glossary.test.ts @@ -139,6 +139,12 @@ describe('Chinese glossary content', () => { it('preserves the source scope in audited Chinese definitions', () => { expect(getZhGlossaryEntry('ai-inference')?.explanation).not.toContain('相差数倍'); expect(getZhGlossaryEntry('cost-per-million-tokens')?.significance).toContain('低吞吐量'); + expect(getZhGlossaryEntry('total-cost-of-ownership')?.benchmarkContext).toContain( + '软件实现及工作负载特征', + ); + expect(getZhGlossaryEntry('total-cost-of-ownership')?.benchmarkContext).not.toContain( + '软硬件行为', + ); expect(getZhGlossaryEntry('sglang')?.benchmarkContext).toContain('回归或提升'); }); }); From 6ebc42b7f6d3c384c0d914071b21c298afc883c0 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 23 Aug 2026 06:28:14 -0700 Subject: [PATCH 4/9] fix(zh): preserve land acknowledgement scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate each infrastructure relationship as operational rather than constructional, with a focused fidelity guard. Move exhaustive localized Blog and Glossary navigation matrices into a dedicated integration spec so smoke coverage stays lean. 中文:将三处基础设施与传统领地的关系准确表达为“运行于”,避免误写成建设关系,并加入聚焦语义范围的回归测试;同时把 Blog 与 Glossary 的完整双语导航矩阵移入独立集成测试,保持 smoke 测试轻量。 --- packages/app/cypress/e2e/blog.cy.ts | 54 -------- .../app/cypress/e2e/content-journeys.cy.ts | 117 ++++++++++++++++++ packages/app/cypress/e2e/zh-pages.cy.ts | 62 ---------- .../src/app/zh/land-acknowledgement/page.tsx | 6 +- .../lib/land-acknowledgement-content.test.ts | 30 +++++ 5 files changed, 150 insertions(+), 119 deletions(-) create mode 100644 packages/app/cypress/e2e/content-journeys.cy.ts create mode 100644 packages/app/src/lib/land-acknowledgement-content.test.ts diff --git a/packages/app/cypress/e2e/blog.cy.ts b/packages/app/cypress/e2e/blog.cy.ts index fefe484a5..351f37fef 100644 --- a/packages/app/cypress/e2e/blog.cy.ts +++ b/packages/app/cypress/e2e/blog.cy.ts @@ -1,58 +1,4 @@ -const BLOG_SITE_URL = 'https://inferencex.semianalysis.com'; -const BLOG_POST_COUNT = 19; - -const BLOG_JOURNEY_MATRIX = [ - { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 1440 }, - { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 375 }, - { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 1440 }, - { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 390 }, -] as const; - -function exerciseEveryBlogJourney({ - basePath, - detailPrefix, - alternateLocale, - width, -}: (typeof BLOG_JOURNEY_MATRIX)[number]) { - cy.viewport(width, 900); - cy.visit(basePath); - cy.get('[data-testid="blog-post-card"]') - .should('have.length', BLOG_POST_COUNT) - .then(($cards) => { - const hrefs = [...$cards].map((card) => card.getAttribute('href')); - expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); - expect(new Set(hrefs).size).to.equal(BLOG_POST_COUNT); - expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); - - for (const href of hrefs as string[]) { - const slug = href.slice(detailPrefix.length); - const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); - cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).click(); - cy.location('pathname').should('eq', href); - cy.get('[data-testid="blog-post-page"]') - .should('be.visible') - .and('have.attr', 'data-blog-slug', slug); - cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( - 'have.attr', - 'href', - `${BLOG_SITE_URL}${alternatePath}`, - ); - cy.go('back'); - cy.location('pathname').should('eq', basePath); - cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).should('exist'); - } - }); -} - describe('Blog', () => { - describe('complete localized listing-to-article journeys', () => { - for (const scenario of BLOG_JOURNEY_MATRIX) { - it(`opens every ${scenario.basePath} article from the listing at ${scenario.width}px`, () => { - exerciseEveryBlogJourney(scenario); - }); - } - }); - describe('Blog listing page', () => { before(() => { cy.visit('/blog'); diff --git a/packages/app/cypress/e2e/content-journeys.cy.ts b/packages/app/cypress/e2e/content-journeys.cy.ts new file mode 100644 index 000000000..d9f603e99 --- /dev/null +++ b/packages/app/cypress/e2e/content-journeys.cy.ts @@ -0,0 +1,117 @@ +const CONTENT_SITE_URL = 'https://inferencex.semianalysis.com'; +const BLOG_POST_COUNT = 19; +const GLOSSARY_ENTRY_COUNT = 54; + +const BLOG_JOURNEY_MATRIX = [ + { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 1440 }, + { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 375 }, + { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 1440 }, + { basePath: '/zh/blog', detailPrefix: '/zh/blog/', alternateLocale: 'en', width: 390 }, +] as const; + +const GLOSSARY_JOURNEY_MATRIX = [ + { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 1440 }, + { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 375 }, + { + basePath: '/zh/glossary', + detailPrefix: '/zh/glossary/', + alternateLocale: 'en', + width: 1440, + }, + { + basePath: '/zh/glossary', + detailPrefix: '/zh/glossary/', + alternateLocale: 'en', + width: 390, + }, +] as const; + +function exerciseEveryBlogJourney({ + basePath, + detailPrefix, + alternateLocale, + width, +}: (typeof BLOG_JOURNEY_MATRIX)[number]) { + cy.viewport(width, 900); + cy.visit(basePath); + cy.get('[data-testid="blog-post-card"]') + .should('have.length', BLOG_POST_COUNT) + .then(($cards) => { + const hrefs = [...$cards].map((card) => card.getAttribute('href')); + expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); + expect(new Set(hrefs).size).to.equal(BLOG_POST_COUNT); + expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); + + for (const href of hrefs as string[]) { + const slug = href.slice(detailPrefix.length); + const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); + cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).click(); + cy.location('pathname').should('eq', href); + cy.get('[data-testid="blog-post-page"]') + .should('be.visible') + .and('have.attr', 'data-blog-slug', slug); + cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( + 'have.attr', + 'href', + `${CONTENT_SITE_URL}${alternatePath}`, + ); + cy.go('back'); + cy.location('pathname').should('eq', basePath); + cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).should('exist'); + } + }); +} + +function exerciseEveryGlossaryJourney({ + basePath, + detailPrefix, + alternateLocale, + width, +}: (typeof GLOSSARY_JOURNEY_MATRIX)[number]) { + cy.viewport(width, 900); + cy.visit(basePath); + cy.get('[data-testid="glossary-entry-link"]') + .should('have.length', GLOSSARY_ENTRY_COUNT) + .then(($links) => { + const hrefs = [...$links].map((link) => link.getAttribute('href')); + expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); + expect(new Set(hrefs).size).to.equal(GLOSSARY_ENTRY_COUNT); + expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); + + for (const href of hrefs as string[]) { + const slug = href.slice(detailPrefix.length); + const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); + cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).click(); + cy.location('pathname').should('eq', href); + cy.get('[data-testid="glossary-detail-page"]') + .should('be.visible') + .and('have.attr', 'data-glossary-slug', slug); + cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( + 'have.attr', + 'href', + `${CONTENT_SITE_URL}${alternatePath}`, + ); + cy.go('back'); + cy.location('pathname').should('eq', basePath); + cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).should('exist'); + } + }); +} + +describe('complete localized content journeys', () => { + describe('Blog listing to article', () => { + for (const scenario of BLOG_JOURNEY_MATRIX) { + it(`opens every ${scenario.basePath} article from the listing at ${scenario.width}px`, () => { + exerciseEveryBlogJourney(scenario); + }); + } + }); + + describe('Glossary index to term', () => { + for (const scenario of GLOSSARY_JOURNEY_MATRIX) { + it(`opens every ${scenario.basePath} term from the index at ${scenario.width}px`, () => { + exerciseEveryGlossaryJourney(scenario); + }); + } + }); +}); diff --git a/packages/app/cypress/e2e/zh-pages.cy.ts b/packages/app/cypress/e2e/zh-pages.cy.ts index 8b23bee3b..ecb6827c3 100644 --- a/packages/app/cypress/e2e/zh-pages.cy.ts +++ b/packages/app/cypress/e2e/zh-pages.cy.ts @@ -1,59 +1,3 @@ -const GLOSSARY_SITE_URL = 'https://inferencex.semianalysis.com'; -const GLOSSARY_ENTRY_COUNT = 54; - -const GLOSSARY_JOURNEY_MATRIX = [ - { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 1440 }, - { basePath: '/glossary', detailPrefix: '/glossary/', alternateLocale: 'zh-CN', width: 375 }, - { - basePath: '/zh/glossary', - detailPrefix: '/zh/glossary/', - alternateLocale: 'en', - width: 1440, - }, - { - basePath: '/zh/glossary', - detailPrefix: '/zh/glossary/', - alternateLocale: 'en', - width: 390, - }, -] as const; - -function exerciseEveryGlossaryJourney({ - basePath, - detailPrefix, - alternateLocale, - width, -}: (typeof GLOSSARY_JOURNEY_MATRIX)[number]) { - cy.viewport(width, 900); - cy.visit(basePath); - cy.get('[data-testid="glossary-entry-link"]') - .should('have.length', GLOSSARY_ENTRY_COUNT) - .then(($links) => { - const hrefs = [...$links].map((link) => link.getAttribute('href')); - expect(hrefs.every((href): href is string => Boolean(href))).to.equal(true); - expect(new Set(hrefs).size).to.equal(GLOSSARY_ENTRY_COUNT); - expect(hrefs.every((href) => href?.startsWith(detailPrefix))).to.equal(true); - - for (const href of hrefs as string[]) { - const slug = href.slice(detailPrefix.length); - const alternatePath = alternateLocale === 'zh-CN' ? `/zh${href}` : href.replace('/zh', ''); - cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).click(); - cy.location('pathname').should('eq', href); - cy.get('[data-testid="glossary-detail-page"]') - .should('be.visible') - .and('have.attr', 'data-glossary-slug', slug); - cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( - 'have.attr', - 'href', - `${GLOSSARY_SITE_URL}${alternatePath}`, - ); - cy.go('back'); - cy.location('pathname').should('eq', basePath); - cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).should('exist'); - } - }); -} - describe('Chinese (/zh) pages', () => { describe('zh landing page', () => { before(() => { @@ -212,12 +156,6 @@ describe('Chinese (/zh) pages', () => { }); describe('Glossary pages', () => { - for (const scenario of GLOSSARY_JOURNEY_MATRIX) { - it(`opens every ${scenario.basePath} term from the index at ${scenario.width}px`, () => { - exerciseEveryGlossaryJourney(scenario); - }); - } - it('supports search, category filtering, empty recovery, and a Chinese term click path', () => { cy.viewport(1440, 900); cy.visit('/zh/glossary'); diff --git a/packages/app/src/app/zh/land-acknowledgement/page.tsx b/packages/app/src/app/zh/land-acknowledgement/page.tsx index 5d3acc9eb..944c78fce 100644 --- a/packages/app/src/app/zh/land-acknowledgement/page.tsx +++ b/packages/app/src/app/zh/land-acknowledgement/page.tsx @@ -9,20 +9,20 @@ const REGIONAL_ACKNOWLEDGEMENTS_ZH = [ region: 'San Jose', peoples: 'Muwekma Ohlone Tribe', acknowledgement: - '我们位于 San Jose 地区的基准测试基础设施,建在旧金山湾区 Muwekma Ohlone Tribe 尚未割让的祖居地上。', + '我们位于 San Jose 地区的基准测试基础设施运行于旧金山湾区 Muwekma Ohlone Tribe 尚未割让的祖居地上。', }, { region: 'Los Angeles', peoples: 'Tongva、Tataviam、Serrano、Kizh 和 Chumash 原住民族', acknowledgement: - '我们位于 Los Angeles 地区的基准测试基础设施,建在 Tongva、Tataviam、Serrano、Kizh 和 Chumash 原住民族最早居住、至今仍在生活并守护的土地上。', + '我们位于 Los Angeles 地区的基准测试基础设施运行于 Tongva、Tataviam、Serrano、Kizh 和 Chumash 原住民族最早居住、至今仍在生活并守护的土地上。', }, { region: 'Chicago', peoples: 'Council of the Three Fires、Illinois Confederacy、Miami、Ho-Chunk、Menominee、Fox 和 Sac 原住民族', acknowledgement: - '我们位于 Chicago 地区的基准测试基础设施,建在 Council of the Three Fires(Ojibwe、Odawa 和 Potawatomi Nations)、Illinois Confederacy 以及 Miami、Ho-Chunk、Menominee、Fox 和 Sac 等其他原住民族世代守护的土地上。', + '我们位于 Chicago 地区的基准测试基础设施运行于 Council of the Three Fires(Ojibwe、Odawa 和 Potawatomi Nations)、Illinois Confederacy 以及 Miami、Ho-Chunk、Menominee、Fox 和 Sac 等其他原住民族世代守护的土地上。', }, ]; diff --git a/packages/app/src/lib/land-acknowledgement-content.test.ts b/packages/app/src/lib/land-acknowledgement-content.test.ts new file mode 100644 index 000000000..19f807e7f --- /dev/null +++ b/packages/app/src/lib/land-acknowledgement-content.test.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const englishPage = readFileSync( + resolve(import.meta.dirname, '../app/land-acknowledgement/page.tsx'), + 'utf8', +); +const chinesePage = readFileSync( + resolve(import.meta.dirname, '../app/zh/land-acknowledgement/page.tsx'), + 'utf8', +); + +describe('Land Acknowledgement translation fidelity', () => { + it('keeps all three infrastructure relationships operational rather than constructional', () => { + const englishOperationalRelationships = + englishPage.match(/benchmark infrastructure operates on/gu) ?? []; + const chineseAcknowledgements = [ + ...chinesePage.matchAll(/acknowledgement:\s*\n\s*'(?[^']+)'/gu), + ].map((match) => match.groups!.copy); + + expect(englishOperationalRelationships).toHaveLength(3); + expect(chineseAcknowledgements).toHaveLength(englishOperationalRelationships.length); + for (const acknowledgement of chineseAcknowledgements) { + expect(acknowledgement).toContain('基准测试基础设施运行于'); + expect(acknowledgement).not.toContain('建在'); + } + }); +}); From 0748d30c32c1a541e5d84f76a9730a76884a68fa Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 23 Aug 2026 07:13:33 -0700 Subject: [PATCH 5/9] feat(zh): localize Blog navigation chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Localize Blog table-of-contents and heading-link controls, preserve the English rendering, and cover mobile layout plus copy analytics in content-owned tests. 中文:完善 Blog 目录与标题链接控件的中文显示,保持英文界面不变,并在内容分支覆盖移动端布局和复制操作埋点。 --- .../component/blog-shared-chrome.cy.tsx | 38 +++++++++++++++++++ packages/app/cypress/e2e/zh-pages.cy.ts | 17 +++++++++ packages/app/src/app/zh/blog/[slug]/page.tsx | 2 +- .../blog/blog-shared-chrome.test.tsx | 32 ++++++++++++++++ packages/app/src/components/blog/blog-toc.tsx | 28 +++++++++++--- .../app/src/components/blog/heading-link.tsx | 18 ++++++--- .../src/components/blog/mdx-components.tsx | 6 +-- 7 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 packages/app/cypress/component/blog-shared-chrome.cy.tsx create mode 100644 packages/app/src/components/blog/blog-shared-chrome.test.tsx diff --git a/packages/app/cypress/component/blog-shared-chrome.cy.tsx b/packages/app/cypress/component/blog-shared-chrome.cy.tsx new file mode 100644 index 000000000..7210a4225 --- /dev/null +++ b/packages/app/cypress/component/blog-shared-chrome.cy.tsx @@ -0,0 +1,38 @@ +import { HeadingLink } from '@/components/blog/heading-link'; +import { registerAnalyticsClient } from '@/lib/analytics'; + +describe('Blog shared chrome', () => { + it('localizes heading-link feedback and tracks a Chinese copy interaction', () => { + const capture = cy.stub().as('capture'); + const writeText = cy.stub(); + writeText.resolves(); + cy.wrap(writeText).as('writeText'); + registerAnalyticsClient({ capture }); + cy.window().then((win) => { + win.history.replaceState(null, '', '/zh/blog/example'); + Object.defineProperty(win.navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }); + }); + + cy.mount(); + cy.get('a[aria-label="复制本节链接"]').click(); + + cy.window().then((win) => { + cy.get('@writeText').should( + 'have.been.calledWith', + `${win.location.origin}/zh/blog/example#test-section`, + ); + }); + cy.contains('链接已复制').should('be.visible'); + cy.get('@capture').should('have.been.calledWith', 'blog_heading_link_clicked', { + id: 'test-section', + locale: 'zh', + }); + cy.get('@capture').should('have.been.calledWith', 'blog_heading_link_copied', { + id: 'test-section', + locale: 'zh', + }); + }); +}); diff --git a/packages/app/cypress/e2e/zh-pages.cy.ts b/packages/app/cypress/e2e/zh-pages.cy.ts index ecb6827c3..4340674d0 100644 --- a/packages/app/cypress/e2e/zh-pages.cy.ts +++ b/packages/app/cypress/e2e/zh-pages.cy.ts @@ -236,6 +236,23 @@ describe('Chinese (/zh) pages', () => { it('links to the English original', () => { cy.get('a[href="/blog/inferencemax-open-source-inference-benchmarking"]').should('exist'); }); + + it('localizes the table of contents and heading-link controls at mobile widths', () => { + cy.viewport(390, 844); + cy.get('details[aria-label="本页目录"]') + .should('be.visible') + .find('summary') + .should('contain.text', '点击展开') + .and('not.contain.text', 'click to expand'); + cy.get('article.prose a[aria-label="复制本节链接"]') + .first() + .should('have.attr', 'href') + .and('match', /^#/u); + cy.get('article.prose a[aria-label="Copy link to section"]').should('not.exist'); + cy.document().then((doc) => { + expect(doc.documentElement.scrollWidth).to.be.at.most(doc.documentElement.clientWidth); + }); + }); }); describe('zh blog post with math', () => { diff --git a/packages/app/src/app/zh/blog/[slug]/page.tsx b/packages/app/src/app/zh/blog/[slug]/page.tsx index f7cce1af3..631bee7b4 100644 --- a/packages/app/src/app/zh/blog/[slug]/page.tsx +++ b/packages/app/src/app/zh/blog/[slug]/page.tsx @@ -135,7 +135,7 @@ export default async function ZhBlogPostPage({ params }: Props) { {headings.length > 0 && (
- +
)}
diff --git a/packages/app/src/components/blog/blog-shared-chrome.test.tsx b/packages/app/src/components/blog/blog-shared-chrome.test.tsx new file mode 100644 index 000000000..e7f448989 --- /dev/null +++ b/packages/app/src/components/blog/blog-shared-chrome.test.tsx @@ -0,0 +1,32 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import type { TocHeading } from '@/lib/blog'; +import { BlogToc } from './blog-toc'; +import { HeadingLink } from './heading-link'; + +const headings = [{ id: 'results', text: 'Results', level: 2 }] satisfies TocHeading[]; + +describe('shared Blog localization chrome', () => { + it('preserves the existing English table-of-contents and heading-link copy', () => { + const toc = renderToStaticMarkup(); + const link = renderToStaticMarkup(); + + expect(toc).toContain('aria-label="Table of contents"'); + expect(toc).toContain('On this page'); + expect(toc).toContain('(click to expand)'); + expect(link).toContain('aria-label="Copy link to section"'); + }); + + it('renders the equivalent Chinese controls without English fallback copy', () => { + const toc = renderToStaticMarkup(); + const link = renderToStaticMarkup(); + + expect(toc).toContain('aria-label="本页目录"'); + expect(toc).toContain('(点击展开)'); + expect(toc).not.toContain('Table of contents'); + expect(toc).not.toContain('click to expand'); + expect(link).toContain('aria-label="复制本节链接"'); + expect(link).not.toContain('Copy link to section'); + }); +}); diff --git a/packages/app/src/components/blog/blog-toc.tsx b/packages/app/src/components/blog/blog-toc.tsx index 591b9da3d..98fb76226 100644 --- a/packages/app/src/components/blog/blog-toc.tsx +++ b/packages/app/src/components/blog/blog-toc.tsx @@ -4,11 +4,26 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { track } from '@/lib/analytics'; import type { TocHeading } from '@/lib/blog'; +import type { Locale } from '@/lib/i18n'; + +const STRINGS = { + en: { + defaultLabel: 'On this page', + tableOfContents: 'Table of contents', + clickToExpand: '(click to expand)', + }, + zh: { + defaultLabel: '本页目录', + tableOfContents: '本页目录', + clickToExpand: '(点击展开)', + }, +} as const; interface BlogTocProps { headings: TocHeading[]; /** Heading label, e.g. '本页目录' on Chinese pages. */ label?: string; + locale?: Locale; } function handleClick(heading: TocHeading) { @@ -19,7 +34,9 @@ function handleClick(heading: TocHeading) { window.scrollTo({ top, behavior: 'smooth' }); } -export function BlogToc({ headings, label = 'On this page' }: BlogTocProps) { +export function BlogToc({ headings, label, locale = 'en' }: BlogTocProps) { + const t = STRINGS[locale]; + const displayLabel = label ?? t.defaultLabel; const [activeId, setActiveId] = useState(''); const [showSidebar, setShowSidebar] = useState(false); const observerRef = useRef(null); @@ -141,9 +158,10 @@ export function BlogToc({ headings, label = 'On this page' }: BlogTocProps) { <> {/* Inline: when sidebar doesn't fit */} {!showSidebar && ( -
+
- {label} (click to expand) + {displayLabel}{' '} + {t.clickToExpand}
{list}
@@ -160,9 +178,9 @@ export function BlogToc({ headings, label = 'On this page' }: BlogTocProps) { top: Math.max(32, sectionTopRef.current - window.scrollY), scrollbarWidth: 'none', }} - aria-label="Table of contents" + aria-label={t.tableOfContents} > -

{label}

+

{displayLabel}

{list} , document.body, diff --git a/packages/app/src/components/blog/heading-link.tsx b/packages/app/src/components/blog/heading-link.tsx index c72296e14..662fe8628 100644 --- a/packages/app/src/components/blog/heading-link.tsx +++ b/packages/app/src/components/blog/heading-link.tsx @@ -3,8 +3,15 @@ import { useCallback, useRef, useState } from 'react'; import { LinkIcon } from 'lucide-react'; import { track } from '@/lib/analytics'; +import type { Locale } from '@/lib/i18n'; -export function HeadingLink({ id }: { id: string }) { +const STRINGS = { + en: { copy: 'Copy link to section', copied: 'Link copied' }, + zh: { copy: '复制本节链接', copied: '链接已复制' }, +} as const; + +export function HeadingLink({ id, locale = 'en' }: { id: string; locale?: Locale }) { + const t = STRINGS[locale]; const [state, setState] = useState<'idle' | 'copied' | 'fading'>('idle'); const timerRef = useRef>(undefined); @@ -12,10 +19,11 @@ export function HeadingLink({ id }: { id: string }) { (e: React.MouseEvent) => { e.preventDefault(); clearTimeout(timerRef.current); + track('blog_heading_link_clicked', { id, locale }); const url = `${window.location.origin}${window.location.pathname}#${id}`; navigator.clipboard.writeText(url).then( () => { - track('blog_heading_link_copied', { id }); + track('blog_heading_link_copied', { id, locale }); setState('copied'); timerRef.current = setTimeout(() => { setState('fading'); @@ -27,7 +35,7 @@ export function HeadingLink({ id }: { id: string }) { }, ); }, - [id], + [id, locale], ); const visible = state !== 'idle'; @@ -36,13 +44,13 @@ export function HeadingLink({ id }: { id: string }) { {state === 'idle' ? ( ) : ( - Link copied + {t.copied} )} ); diff --git a/packages/app/src/components/blog/mdx-components.tsx b/packages/app/src/components/blog/mdx-components.tsx index b684c78e4..e422ed871 100644 --- a/packages/app/src/components/blog/mdx-components.tsx +++ b/packages/app/src/components/blog/mdx-components.tsx @@ -105,7 +105,7 @@ export function createMdxComponents( return (

{props.children} - +

); }, @@ -114,7 +114,7 @@ export function createMdxComponents( return (

{props.children} - +

); }, @@ -123,7 +123,7 @@ export function createMdxComponents( return (

{props.children} - +

); }, From 675eb6657b2fffd07457b58925666e792ff10411 Mon Sep 17 00:00:00 2001 From: edwingao28 Date: Fri, 28 Aug 2026 23:30:25 +0000 Subject: [PATCH 6/9] fix(tests): refresh content counts after rebase onto master Master grew the blog to 31 locale pairs and the glossary to 118 entries, so the hardcoded 19/54 counts in blog-content.test.ts and content-journeys.cy.ts were stale after the rebase. Also register the new content-journeys.cy.ts spec in timings.json (and restore its sort order) so the cypress-split baseline test passes. --- packages/app/cypress/e2e/content-journeys.cy.ts | 4 ++-- packages/app/src/lib/blog-content.test.ts | 4 ++-- packages/app/timings.json | 12 ++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/app/cypress/e2e/content-journeys.cy.ts b/packages/app/cypress/e2e/content-journeys.cy.ts index d9f603e99..25918d38c 100644 --- a/packages/app/cypress/e2e/content-journeys.cy.ts +++ b/packages/app/cypress/e2e/content-journeys.cy.ts @@ -1,6 +1,6 @@ const CONTENT_SITE_URL = 'https://inferencex.semianalysis.com'; -const BLOG_POST_COUNT = 19; -const GLOSSARY_ENTRY_COUNT = 54; +const BLOG_POST_COUNT = 31; +const GLOSSARY_ENTRY_COUNT = 118; const BLOG_JOURNEY_MATRIX = [ { basePath: '/blog', detailPrefix: '/blog/', alternateLocale: 'zh-CN', width: 1440 }, diff --git a/packages/app/src/lib/blog-content.test.ts b/packages/app/src/lib/blog-content.test.ts index 2cad30d7a..359c7bbd4 100644 --- a/packages/app/src/lib/blog-content.test.ts +++ b/packages/app/src/lib/blog-content.test.ts @@ -79,8 +79,8 @@ function inlineCodeSpans(raw: string): string[] { ); } -it('checks all 19 current locale pairs in both directions', () => { - expect(enFiles).toHaveLength(19); +it('checks all 31 current locale pairs in both directions', () => { + expect(enFiles).toHaveLength(31); expect(zhFiles).toEqual(enFiles); }); diff --git a/packages/app/timings.json b/packages/app/timings.json index 3207a42db..6cc117157 100644 --- a/packages/app/timings.json +++ b/packages/app/timings.json @@ -44,10 +44,6 @@ "spec": "cypress/e2e/blog.cy.ts", "duration": 3125 }, - { - "spec": "cypress/e2e/fleet-lifecycle.cy.ts", - "duration": 18000 - }, { "spec": "cypress/e2e/calculator-overlay.cy.ts", "duration": 9124 @@ -88,6 +84,10 @@ "spec": "cypress/e2e/compare-table.cy.ts", "duration": 1597 }, + { + "spec": "cypress/e2e/content-journeys.cy.ts", + "duration": 150000 + }, { "spec": "cypress/e2e/csv-export-overlay.cy.ts", "duration": 1116 @@ -124,6 +124,10 @@ "spec": "cypress/e2e/fixed-sequence-logs.cy.ts", "duration": 1528 }, + { + "spec": "cypress/e2e/fleet-lifecycle.cy.ts", + "duration": 18000 + }, { "spec": "cypress/e2e/gpu-compare-agentic-detail.cy.ts", "duration": 1789 From 57cbbd418a170f43f313433a00bbf5420a7af41f Mon Sep 17 00:00:00 2001 From: edwingao28 Date: Fri, 28 Aug 2026 23:39:07 +0000 Subject: [PATCH 7/9] fix(e2e): tolerate streamed head metadata in content journeys After a client-side navigation Next.js updates the document body before the streamed tags replace the listing page's, so asserting have.attr on the first matching alternate raced the head update and flaked on the first article/term of a journey. Query for the fully-updated link (hreflang + href) and retry until it exists. --- .../app/cypress/e2e/content-journeys.cy.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/app/cypress/e2e/content-journeys.cy.ts b/packages/app/cypress/e2e/content-journeys.cy.ts index 25918d38c..19a3603a4 100644 --- a/packages/app/cypress/e2e/content-journeys.cy.ts +++ b/packages/app/cypress/e2e/content-journeys.cy.ts @@ -50,11 +50,13 @@ function exerciseEveryBlogJourney({ cy.get('[data-testid="blog-post-page"]') .should('be.visible') .and('have.attr', 'data-blog-slug', slug); - cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( - 'have.attr', - 'href', - `${CONTENT_SITE_URL}${alternatePath}`, - ); + // Next.js streams head metadata after the body during client-side + // navigation, so match the fully-updated link instead of asserting on + // whichever (possibly stale) alternate is first in document order. + cy.get( + `link[rel="alternate"][hreflang="${alternateLocale}"][href="${CONTENT_SITE_URL}${alternatePath}"]`, + { timeout: 15000 }, + ).should('exist'); cy.go('back'); cy.location('pathname').should('eq', basePath); cy.get(`[data-testid="blog-post-card"][data-blog-slug="${slug}"]`).should('exist'); @@ -86,11 +88,12 @@ function exerciseEveryGlossaryJourney({ cy.get('[data-testid="glossary-detail-page"]') .should('be.visible') .and('have.attr', 'data-glossary-slug', slug); - cy.get(`link[rel="alternate"][hreflang="${alternateLocale}"]`).should( - 'have.attr', - 'href', - `${CONTENT_SITE_URL}${alternatePath}`, - ); + // See the blog journey above: retry until the streamed head metadata + // for this term has replaced the listing page's alternate link. + cy.get( + `link[rel="alternate"][hreflang="${alternateLocale}"][href="${CONTENT_SITE_URL}${alternatePath}"]`, + { timeout: 15000 }, + ).should('exist'); cy.go('back'); cy.location('pathname').should('eq', basePath); cy.get(`[data-testid="glossary-entry-link"][data-glossary-slug="${slug}"]`).should('exist'); From 9ac9f6cc4a5fff788dbaa33379f04fee209492af Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 28 Aug 2026 20:15:23 -0700 Subject: [PATCH 8/9] fix(zh): refine explanatory page copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:优化关于页面、常见问题与闭环并发术语,并移除不符合测试规范的内联超时。 --- .../app/cypress/e2e/content-journeys.cy.ts | 2 -- packages/app/src/app/zh/about/page.tsx | 6 ++--- .../app/src/components/about/faq-data-zh.ts | 6 ++--- .../app/src/components/about/faq-data.test.ts | 27 +++++++++++++++++++ packages/app/src/lib/glossary-zh.ts | 4 +-- packages/app/src/lib/glossary.test.ts | 11 ++++++++ 6 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/app/cypress/e2e/content-journeys.cy.ts b/packages/app/cypress/e2e/content-journeys.cy.ts index 19a3603a4..3acfdb13e 100644 --- a/packages/app/cypress/e2e/content-journeys.cy.ts +++ b/packages/app/cypress/e2e/content-journeys.cy.ts @@ -55,7 +55,6 @@ function exerciseEveryBlogJourney({ // whichever (possibly stale) alternate is first in document order. cy.get( `link[rel="alternate"][hreflang="${alternateLocale}"][href="${CONTENT_SITE_URL}${alternatePath}"]`, - { timeout: 15000 }, ).should('exist'); cy.go('back'); cy.location('pathname').should('eq', basePath); @@ -92,7 +91,6 @@ function exerciseEveryGlossaryJourney({ // for this term has replaced the listing page's alternate link. cy.get( `link[rel="alternate"][hreflang="${alternateLocale}"][href="${CONTENT_SITE_URL}${alternatePath}"]`, - { timeout: 15000 }, ).should('exist'); cy.go('back'); cy.location('pathname').should('eq', basePath); diff --git a/packages/app/src/app/zh/about/page.tsx b/packages/app/src/app/zh/about/page.tsx index 6213e8b72..1038f5838 100644 --- a/packages/app/src/app/zh/about/page.tsx +++ b/packages/app/src/app/zh/about/page.tsx @@ -43,11 +43,11 @@ export default function AboutPageZh() {

- 开源持续智能体推理基准测试——受万亿美元级吉瓦规模 Token 工厂运营者的信赖 + 持续运行的开源智能体推理基准测试,受到万亿美元级、吉瓦规模 Token 工厂运营方的信赖

- 随着世界以指数级速度迈向 - AGI,软件开发和模型发布日新月异。现有基准测试因其静态性质而迅速过时,参与者往往提交专为基准测试定制的软件镜像,无法反映真实的线上推理性能。 + 随着全球迈向 AGI + 的进程呈指数级加速,软件开发和模型发布也日新月异。静态基准测试很快就会过时;参与者提交的镜像往往专为基准测试打造,无法反映真实环境中的性能。

InferenceX™(原名 diff --git a/packages/app/src/components/about/faq-data-zh.ts b/packages/app/src/components/about/faq-data-zh.ts index 616d9b141..7acdebd84 100644 --- a/packages/app/src/components/about/faq-data-zh.ts +++ b/packages/app/src/components/about/faq-data-zh.ts @@ -19,14 +19,14 @@ export const FAQ_ITEMS_ZH: readonly FaqItem[] = [ { id: 'faq-chips', question: 'InferenceX 测试了哪些芯片?', - answer: '我们会在新加速器可用时持续添加。', + answer: '新加速器可用后,我们会持续将其纳入基准测试。', list: GENERATED_FAQ_DATA.gpuGroups, }, { id: 'faq-models', question: '测试了哪些 AI 模型?', answer: - '各模型会在其已有数据所覆盖的固定序列配置(1k/1k、1k/8k、8k/1k tokens)与多个并发级别下进行测试。具备对应数据的模型还包含 AgentX 长上下文多轮智能体编码运行。', + '各模型会在相应的固定序列配置(1k/1k、1k/8k、8k/1k tokens)和多个并发级别下接受测试;如果已有对应数据,还会运行 AgentX 长上下文多轮智能体编码场景。', list: GENERATED_FAQ_DATA.modelNames, }, { @@ -95,7 +95,7 @@ export const FAQ_ITEMS_ZH: readonly FaqItem[] = [ id: 'faq-raw-logs', question: '在哪里可以查看原始基准测试日志?', answer: - '在图表上点击任意数据点即可打开 tooltip。其中的"GitHub Actions Run"链接将直接跳转到生成该数据点的工作流运行。在那里您可以查看完整的任务日志、框架和驱动版本、命令行参数,以及下载原始产物(包括请求延迟、token 计数和芯片功耗遥测数据)。', + '点击图表中的任意数据点即可打开提示框。其中的“GitHub Actions 运行记录”链接会直接跳转到生成该数据点的工作流运行。您可以在那里查看完整的任务日志、框架和驱动版本、命令行参数,并下载原始产物,包括请求延迟、token 计数和芯片功耗遥测数据。', }, { id: 'faq-rerun-benchmark', diff --git a/packages/app/src/components/about/faq-data.test.ts b/packages/app/src/components/about/faq-data.test.ts index 68de50116..a6f7b5b73 100644 --- a/packages/app/src/components/about/faq-data.test.ts +++ b/packages/app/src/components/about/faq-data.test.ts @@ -1,8 +1,15 @@ +import { readFileSync } from 'node:fs'; + import { describe, expect, it } from 'vitest'; import { FAQ_ITEMS_ZH } from './faq-data-zh'; import { FAQ_ITEMS } from './faq-data'; +const aboutPageSource = readFileSync( + new URL('../../app/zh/about/page.tsx', import.meta.url), + 'utf8', +); + describe('localized About FAQ', () => { it('keeps every English FAQ item and optional field represented in Chinese', () => { expect(FAQ_ITEMS_ZH).toHaveLength(FAQ_ITEMS.length); @@ -26,4 +33,24 @@ describe('localized About FAQ', () => { expect(metrics?.list?.join('\n')).toContain('NeoCloud'); expect(metrics?.list?.join('\n')).not.toContain('NeoCoud'); }); + + it('uses natural Chinese for the About introduction without changing its claims', () => { + expect(aboutPageSource).toContain( + '持续运行的开源智能体推理基准测试,受到万亿美元级、吉瓦规模 Token 工厂运营方的信赖', + ); + expect(aboutPageSource).toContain('参与者提交的镜像往往专为基准测试打造'); + expect(aboutPageSource).not.toContain('开源持续智能体推理基准测试'); + }); + + it('uses natural, interface-consistent Chinese in audited FAQ answers', () => { + const chips = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-chips'); + const models = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-models'); + const logs = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-raw-logs'); + + expect(chips?.answer).toBe('新加速器可用后,我们会持续将其纳入基准测试。'); + expect(models?.answer).toContain('如果已有对应数据,还会运行 AgentX'); + expect(logs?.answer).toContain('“GitHub Actions 运行记录”'); + expect(logs?.answer).not.toContain('tooltip'); + expect(logs?.answer).not.toContain('"GitHub Actions Run"'); + }); }); diff --git a/packages/app/src/lib/glossary-zh.ts b/packages/app/src/lib/glossary-zh.ts index bf6c0e4bf..5a326a8db 100644 --- a/packages/app/src/lib/glossary-zh.ts +++ b/packages/app/src/lib/glossary-zh.ts @@ -105,11 +105,11 @@ const translations: Readonly> = { definition: '闭环基准测试中的客户端会在前一个依赖请求完成后生成新工作,同时遵循工作负载记录的等待时间和分支结构。', explanation: - 'Concurrency 表示活跃客户端或会话数量,同时存在的请求数会随时间变化。更快的系统更早完成轮次,因此会在同一个 profiling 窗口内发出更多请求。每条采样会话的推进速度取决于请求完成时间,实际请求组合可能有小幅变化。', + '并发量表示活跃客户端或会话的数量,同时在途的请求数会随时间变化。系统越快,完成轮次越早,因此在同一个 profiling 窗口内发出的请求也越多。由于每条采样会话的推进取决于请求完成时间,实际请求组合可能略有变化。', significance: '这种负载模型符合交互式 agent 的运行方式,因为下一步动作依赖上一步结果。响应更快时,会话也会更快地产生后续工作,所以吞吐量与延迟相互关联。低并发运行的采样波动通常会比大型请求池更明显。', benchmarkContext: - 'AgentX 使用闭环 concurrency。该数值表示同时运行的 agent 客户端数量;request batch 会随着会话推进而变化。解读结果时需要结合吞吐量、首 token 延迟与交互性。', + 'AgentX 采用闭环并发。并发量表示同时运行的 agent 客户端数量;随着会话推进,请求批次大小会不断变化。解读结果时,应结合吞吐量、首 token 延迟和交互性。', }, subagent: { term: '子智能体', diff --git a/packages/app/src/lib/glossary.test.ts b/packages/app/src/lib/glossary.test.ts index 5a80eeb73..3dcb72358 100644 --- a/packages/app/src/lib/glossary.test.ts +++ b/packages/app/src/lib/glossary.test.ts @@ -147,6 +147,17 @@ describe('Chinese glossary content', () => { ); expect(getZhGlossaryEntry('sglang')?.benchmarkContext).toContain('回归或提升'); }); + + it('explains closed-loop concurrency in natural Chinese', () => { + const entry = getZhGlossaryEntry('closed-loop-benchmark'); + + expect(entry?.explanation).toContain('并发量表示活跃客户端或会话的数量'); + expect(entry?.benchmarkContext).toContain('AgentX 采用闭环并发'); + expect(entry?.benchmarkContext).toContain('请求批次大小会不断变化'); + expect(entry?.explanation).not.toContain('Concurrency'); + expect(entry?.benchmarkContext).not.toContain('concurrency'); + expect(entry?.benchmarkContext).not.toContain('request batch'); + }); }); describe('glossary navigation', () => { From c39785108648e5c3c6a6be1a49e7c872cd92a4f5 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 28 Aug 2026 20:41:34 -0700 Subject: [PATCH 9/9] fix(zh): refine content provenance copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:优化 About 可复现性说明、FAQ 测试配置术语和 Glossary 提示框用词。 --- packages/app/src/app/zh/about/page.tsx | 22 +++++++------- .../app/src/components/about/faq-data-zh.ts | 6 ++-- .../app/src/components/about/faq-data.test.ts | 30 +++++++++++++++++++ packages/app/src/lib/glossary-zh.ts | 10 +++---- packages/app/src/lib/glossary.test.ts | 16 ++++++++++ 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/packages/app/src/app/zh/about/page.tsx b/packages/app/src/app/zh/about/page.tsx index 1038f5838..29050de0c 100644 --- a/packages/app/src/app/zh/about/page.tsx +++ b/packages/app/src/app/zh/about/page.tsx @@ -82,9 +82,8 @@ export default function AboutPageZh() {

可复现性

- 仪表板上的每一个数据点均来自公开的 GitHub Actions - 工作流运行。测试配方、日志、产物以及数据库记录端到端关联,任何人都可以审计、重新运行或 - fork 基准测试。 + 仪表板上的每个数据点都由公开的 GitHub Actions + 工作流生成。测试配置、日志、产物及其对应的数据库记录彼此关联,任何人都可以核查结果来源、重新运行测试,或基于现有配置创建新的基准测试。

  1. @@ -92,9 +91,9 @@ export default function AboutPageZh() { 1
    - 配方提交至仓库。{' '} - 每种硬件、框架、模型和精度的组合都是一个提交到公开仓库的 shell - 脚本。镜像、命令行和并行度均在源码中固定。 + 测试配置已提交到仓库。{' '} + 每组硬件、框架、模型和精度组合都对应一个提交到公开仓库的 shell + 脚本。脚本中固定了所用镜像、命令行参数和并行度。
  2. @@ -123,10 +122,9 @@ export default function AboutPageZh() { 4
    - 导入仪表板。{' '} - 成功的运行将被加载到数据库中并在此展示。每个图表 tooltip - 都附带一个直接链接,指向生成该数据点的 GitHub Actions - 运行。点击任意数据点即可审计其来源。 + 结果写入仪表板。{' '} + 运行成功后,结果将写入数据库并在仪表板中展示。每个图表的提示框都提供对应的 GitHub + Actions 运行记录链接。点击任意数据点即可核查其来源。
@@ -137,7 +135,7 @@ export default function AboutPageZh() { rel="noopener noreferrer" className="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 hover:bg-accent transition-colors" > - 浏览工作流运行 + 查看工作流运行记录 - 查看基准测试配方 + 查看测试配置 { it('keeps every English FAQ item and optional field represented in Chinese', () => { @@ -53,4 +54,33 @@ describe('localized About FAQ', () => { expect(logs?.answer).not.toContain('tooltip'); expect(logs?.answer).not.toContain('"GitHub Actions Run"'); }); + + it('uses formal, natural Chinese for benchmark provenance', () => { + expect(normalizedAboutPageSource).toContain( + '测试配置、日志、产物及其对应的数据库记录彼此关联,任何人都可以核查结果来源、重新运行测试,或基于现有配置创建新的基准测试。', + ); + expect(normalizedAboutPageSource).toContain('测试配置已提交到仓库。'); + expect(normalizedAboutPageSource).toContain('运行成功后,结果将写入数据库并在仪表板中展示。'); + expect(normalizedAboutPageSource).toContain('每个图表的提示框'); + expect(normalizedAboutPageSource).toContain('查看工作流运行记录'); + expect(normalizedAboutPageSource).toContain('查看测试配置'); + expect(normalizedAboutPageSource).not.toContain('成功的运行将被加载到数据库中'); + expect(normalizedAboutPageSource).not.toContain('图表 tooltip'); + }); + + it('uses test configuration and run-record terminology in reproducibility answers', () => { + const differences = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-benchmark-differences'); + const reproducibility = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-reproducibility'); + const rerun = FAQ_ITEMS_ZH.find((item) => item.id === 'faq-rerun-benchmark'); + + expect(differences?.answer).toContain('测试配置保存在代码仓库中'); + expect(differences?.answer).toContain('GitHub Actions 运行记录'); + expect(reproducibility?.answer).toContain( + '测试配置(模型、框架、精度、并行度、序列长度和并发数)', + ); + expect(reproducibility?.answer).toContain('每个图表的提示框都提供链接'); + expect(reproducibility?.answer).not.toContain('tooltip'); + expect(rerun?.answer).toContain('基准测试脚本位于代码仓库的 /benchmarks 目录中'); + expect(rerun?.answer).not.toContain('基准测试配方'); + }); }); diff --git a/packages/app/src/lib/glossary-zh.ts b/packages/app/src/lib/glossary-zh.ts index 5a326a8db..2f689d413 100644 --- a/packages/app/src/lib/glossary-zh.ts +++ b/packages/app/src/lib/glossary-zh.ts @@ -393,7 +393,7 @@ const translations: Readonly> = { significance: '加速取决于草稿 token 的接受数量,以及草稿与验证成本。稠密模型和 MoE 的表现可能不同,因为验证多个位置可能激活更多专家权重。', benchmarkContext: - '应在真实接受率下比较投机解码方案并验证模型质量。定长场景仍把投机解码作为曲线标识的一部分,因此开启和关闭 MTP 的方案会分开绘制;agentic 曲线则把它当作数据点级元数据并合并这些点,在 tooltip 中标明具体方式,因为 AgentX 按模型、芯片 SKU 和引擎给出可获得的最佳曲线。由于 AgentX 回放的内容是合成的,speculator 接受的 draft token 数会失真,因此运行时会套用一套按模型、speculator、draft 长度和思考模式在外部 agentic 编码数据集上采集的接受长度。', + '应在真实接受率下比较投机解码方案并验证模型质量。定长场景仍把投机解码作为曲线标识的一部分,因此开启和关闭 MTP 的方案会分开绘制;agentic 曲线则把它当作数据点级元数据并合并这些点,在提示框中标明具体方式,因为 AgentX 按模型、芯片 SKU 和引擎给出可获得的最佳曲线。由于 AgentX 回放的内容是合成的,speculator 接受的 draft token 数会失真,因此运行时会套用一套按模型、speculator、draft 长度和思考模式在外部 agentic 编码数据集上采集的接受长度。', }, 'multi-token-prediction': { term: '多 token 预测', @@ -779,7 +779,7 @@ const translations: Readonly> = { significance: '张量并行会在每个 rank 上复制完整 KV cache,数据并行注意力则把会话绑定在持有其分片的 rank 上,两者在上下文达到几十万 token 时都难以扩展。上下文并行直接针对这一点,而且收益随输入长度增长,而不是随 batch 大小增长。', benchmarkContext: - 'InferenceX 在数据点 tooltip 和并行标签中与 TP、EP、DP 一起展示 DCP 与 PCP 的并行度。各厂商支持程度并不均衡:在 AgentX 1.0 结果发布时,vLLM 支持矩阵中 AMD 的注意力后端仍标为不支持,因此该技术仍构成 CUDA 实际优势的一部分。', + 'InferenceX 在数据点提示框和并行标签中与 TP、EP、DP 一起展示 DCP 与 PCP 的并行度。各厂商支持程度并不均衡:在 AgentX 1.0 结果发布时,vLLM 支持矩阵中 AMD 的注意力后端仍标为不支持,因此该技术仍构成 CUDA 实际优势的一部分。', }, 'kv-cache-offload': { term: 'KV cache offload', @@ -847,7 +847,7 @@ const translations: Readonly> = { significance: '芯片峰值规格无法描述服务性能,同一颗芯片在不同配置下可以相差数倍。把整套组合写清楚,结论才可核查:脱离配置的单个数字既无法复现,也无法与其他厂商公平比较。', benchmarkContext: - 'InferenceX 的测试配置主要跟随 vLLM 与 SGLang 官方 cookbook,并使用上游镜像,因此结果反映用户实际能部署的性能,而不是为基准测试特调过的镜像。数据点 tooltip 会展示背后的配置,并给出运行溯源链接。', + 'InferenceX 的测试配置主要跟随 vLLM 与 SGLang 官方 cookbook,并使用上游镜像,因此结果反映用户实际能部署的性能,而不是为基准测试特调过的镜像。数据点的提示框会展示背后的配置,并给出运行溯源链接。', }, 'tail-latency': { term: '尾部延迟', @@ -991,7 +991,7 @@ const translations: Readonly> = { significance: '对最大的那批模型来说,这首先是容量手段,其次才是提速手段。有些前沿模型根本装不进单个节点,流水线并行才让它们可服务;当某项竞争性优化拒绝与任何方案组合时,它甚至是唯一选项。', benchmarkContext: - 'InferenceX 在数据点 tooltip 和并行标签中与 TP、EP、DP 一起展示流水线并行度,且仅在大于 1 时显示。可组合性与并行度同样重要:一种会导致投机解码无法启用的阶段切分,代价可能超过它节省的显存。', + 'InferenceX 在数据点提示框和并行标签中与 TP、EP、DP 一起展示流水线并行度,且仅在大于 1 时显示。可组合性与并行度同样重要:一种会导致投机解码无法启用的阶段切分,代价可能超过它节省的显存。', }, 'dp-attention': { term: '数据并行注意力', @@ -1005,7 +1005,7 @@ const translations: Readonly> = { significance: '由于每个 rank 只拥有缓存池的私有一份,请求落在哪里就成了影响性能的关键:长会话一旦被路由到不持有其前缀的 rank,就要全部重算。此时实测命中率会远低于理论上限,而原因与缓存大小毫无关系。', benchmarkContext: - 'InferenceX 会在数据点 tooltip 的并行策略部分展示 DP attention。它是否有利取决于模型;当缓存局部性变成路由约束时,不启用它的配置有时反而占据前沿曲线。', + 'InferenceX 会在数据点提示框的并行策略部分展示 DP attention。它是否有利取决于模型;当缓存局部性变成路由约束时,不启用它的配置有时反而占据前沿曲线。', }, int4: { term: 'INT4', diff --git a/packages/app/src/lib/glossary.test.ts b/packages/app/src/lib/glossary.test.ts index 3dcb72358..4e5903941 100644 --- a/packages/app/src/lib/glossary.test.ts +++ b/packages/app/src/lib/glossary.test.ts @@ -158,6 +158,22 @@ describe('Chinese glossary content', () => { expect(entry?.benchmarkContext).not.toContain('concurrency'); expect(entry?.benchmarkContext).not.toContain('request batch'); }); + + it('uses the established Chinese term for point tooltips in audited entries', () => { + const slugs = [ + 'speculative-decoding', + 'context-parallelism', + 'recipe', + 'pipeline-parallelism', + 'dp-attention', + ]; + + for (const slug of slugs) { + const benchmarkContext = getZhGlossaryEntry(slug)?.benchmarkContext; + expect(benchmarkContext, slug).toContain('提示框'); + expect(benchmarkContext, slug).not.toContain('tooltip'); + } + }); }); describe('glossary navigation', () => {