Skip to content

Commit 56f56e9

Browse files
edwingao28wenyao
authored andcommitted
feat(zh): localize data and tool workflows
Rewrite Reliability, Submissions, Historical, CollectiveX, AI Chart, and Feedback surfaces with complete locale-aware states, accessibility copy, safe errors, analytics, and responsive E2E coverage. 中文:完整重写可靠性、提交记录、历史趋势、CollectiveX、AI 图表与反馈相关界面的中文文案,并补齐状态提示、无障碍文案、安全错误处理、交互分析及响应式端到端测试。
1 parent 99cc0ff commit 56f56e9

31 files changed

Lines changed: 1765 additions & 635 deletions

‎packages/app/cypress/component/feedback-modal.cy.tsx‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('FeedbackForm', () => {
4343
cy.contains('Thanks for your feedback!').should('be.visible');
4444
cy.then(() => expect(submittedFired).to.be.true);
4545
// Success-hold is 2s; onDismiss fires after.
46-
cy.get('@onDismiss', { timeout: 3000 }).should('have.been.calledOnce');
46+
cy.get('@onDismiss').should('have.been.calledOnce');
4747
});
4848

4949
it('surfaces a 429 as a user-readable error', () => {
@@ -54,4 +54,51 @@ describe('FeedbackForm', () => {
5454
cy.wait('@post');
5555
cy.contains('Too many submissions').should('be.visible');
5656
});
57+
58+
it('renders validation, privacy, controls, and success in Chinese', () => {
59+
cy.viewport(1440, 900);
60+
cy.intercept('POST', '/api/v1/feedback', { statusCode: 204 }).as('postZh');
61+
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
62+
63+
cy.contains('h2', '帮助我们改进 InferenceX').should('be.visible');
64+
cy.contains('您的反馈会加密保存').should('be.visible');
65+
cy.get('[data-testid="feedback-modal-submit"]').click();
66+
cy.contains('[role="alert"]', '请至少填写一项。').should('be.visible');
67+
68+
cy.get('[data-testid="feedback-doing-well"]').type('图表很清晰');
69+
cy.get('[data-testid="feedback-modal-submit"]').click();
70+
cy.wait('@postZh');
71+
cy.contains('感谢您的反馈!').should('be.visible');
72+
});
73+
74+
it('localizes rate-limit and server errors on Chinese routes', () => {
75+
cy.intercept('POST', '/api/v1/feedback', { statusCode: 429 }).as('rateLimited');
76+
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
77+
cy.get('[data-testid="feedback-doing-well"]').type('反馈');
78+
cy.get('[data-testid="feedback-modal-submit"]').click();
79+
cy.wait('@rateLimited');
80+
cy.contains('[role="alert"]', '提交次数过多,请稍后再试。').should('be.visible');
81+
});
82+
83+
it('does not expose a raw network error on Chinese routes', () => {
84+
cy.intercept('POST', '/api/v1/feedback', { forceNetworkError: true }).as('networkFailure');
85+
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
86+
cy.get('[data-testid="feedback-doing-well"]').type('反馈');
87+
cy.get('[data-testid="feedback-modal-submit"]').click();
88+
cy.wait('@networkFailure');
89+
cy.contains('[role="alert"]', '出现意外错误,请重试。').should('be.visible');
90+
cy.contains('Failed to fetch').should('not.exist');
91+
});
92+
93+
for (const width of [375, 390]) {
94+
it(`keeps the Chinese form inside a ${width}px viewport`, () => {
95+
cy.viewport(width, 667);
96+
cy.mount(<FeedbackForm locale="zh" onDismiss={cy.stub()} />);
97+
98+
cy.get('[data-testid="feedback-modal-submit"]').should('be.visible');
99+
cy.document().then((doc) => {
100+
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
101+
});
102+
});
103+
}
57104
});

‎packages/app/cypress/e2e/ai-chart.cy.ts‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,124 @@ describe('AI chart metric semantics', () => {
169169
});
170170
});
171171
});
172+
173+
describe('AI chart Chinese workflow', () => {
174+
it('requests Chinese presentation copy and renders the returned chart and summary', () => {
175+
cy.viewport(1440, 900);
176+
cy.fixture<FixtureRow[]>('api/benchmarks.json').then((fixtureRows) => {
177+
const rows = [
178+
fixtureRow(fixtureRows, 'b200', 40, 12_000),
179+
fixtureRow(fixtureRows, 'mi355x', 40, 10_000),
180+
];
181+
const spec = benchmarkSpec({
182+
title: '每芯片吞吐量对比',
183+
description: '在目标交互性下对比两种芯片。',
184+
yAxisLabel: '每芯片吞吐量',
185+
});
186+
187+
cy.intercept('GET', '**/api/v1/benchmarks?*', rows).as('zhBenchmarks');
188+
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', (request) => {
189+
const systemPrompt = request.body.messages?.[0]?.content ?? '';
190+
if (systemPrompt.includes('chart generation assistant')) {
191+
expect(systemPrompt).to.contain('natural Simplified Chinese');
192+
request.reply({ choices: [{ message: { content: JSON.stringify(spec) } }] });
193+
return;
194+
}
195+
196+
expect(systemPrompt).to.contain('用自然、准确的简体中文回答');
197+
request.reply({ choices: [{ message: { content: 'B200 在该配置下吞吐量更高。' } }] });
198+
}).as('zhOpenAi');
199+
200+
cy.visit('/zh/ai-chart');
201+
cy.get('input[placeholder="OpenAI API Key"]').type('test-api-key', { log: false });
202+
cy.get('textarea[placeholder="描述您想查看的图表……"]').type(
203+
'对比 B200 和 MI355X 的每芯片吞吐量',
204+
);
205+
cy.contains('button', '生成图表').click();
206+
207+
cy.wait('@zhBenchmarks');
208+
cy.get('#ai-chart-bar-export')
209+
.should('contain.text', '每芯片吞吐量对比')
210+
.and('contain.text', '在目标交互性下对比两种芯片。')
211+
.and('contain.text', '每芯片吞吐量');
212+
cy.get('[role="img"][aria-label="AI 生成的条形图"]').should('be.visible');
213+
cy.contains('[data-slot="card-title"]', 'AI 总结').should('be.visible');
214+
cy.contains('B200 在该配置下吞吐量更高。').should('be.visible');
215+
});
216+
});
217+
218+
it('shows a localized empty state for one unmatched chart in a multi-chart result', () => {
219+
cy.fixture<FixtureRow[]>('api/benchmarks.json').then((fixtureRows) => {
220+
const rows = [fixtureRow(fixtureRows, 'b200', 40, 12_000)];
221+
const specs = [
222+
benchmarkSpec({
223+
title: 'B200 吞吐量',
224+
description: '实测配置。',
225+
hardwareKeys: ['b200'],
226+
yAxisLabel: '每芯片吞吐量',
227+
}),
228+
benchmarkSpec({
229+
title: 'H100 吞吐量',
230+
description: '当前数据集中没有匹配项。',
231+
hardwareKeys: ['h100'],
232+
yAxisLabel: '每芯片吞吐量',
233+
}),
234+
];
235+
236+
cy.intercept('GET', '**/api/v1/benchmarks?*', rows).as('multiChartBenchmarks');
237+
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', (request) => {
238+
const systemPrompt = request.body.messages?.[0]?.content ?? '';
239+
request.reply({
240+
choices: [
241+
{
242+
message: {
243+
content: systemPrompt.includes('chart generation assistant')
244+
? JSON.stringify(specs)
245+
: '已对比可用数据。',
246+
},
247+
},
248+
],
249+
});
250+
});
251+
252+
cy.visit('/zh/ai-chart');
253+
cy.get('input[placeholder="OpenAI API Key"]').type('test-api-key', { log: false });
254+
cy.get('textarea').type('生成两张吞吐量图表');
255+
cy.contains('button', '生成图表').click();
256+
257+
cy.wait('@multiChartBenchmarks');
258+
cy.contains('H100 吞吐量')
259+
.closest('[data-slot="card"]')
260+
.should('contain.text', '没有数据符合这项图表配置。');
261+
});
262+
});
263+
264+
it('does not expose a provider error body or API key on the Chinese route', () => {
265+
cy.intercept('POST', 'https://api.openai.com/v1/chat/completions', {
266+
statusCode: 401,
267+
body: { error: { message: 'provider-internal-error sk-sensitive-example-key' } },
268+
}).as('failedProvider');
269+
cy.visit('/zh/ai-chart');
270+
cy.get('input[placeholder="OpenAI API Key"]').type('sk-sensitive-example-key', { log: false });
271+
cy.get('textarea').type('对比吞吐量');
272+
cy.contains('button', '生成图表').click();
273+
cy.wait('@failedProvider');
274+
275+
cy.contains('图表请求失败。请检查 API 密钥和服务商设置后重试。').should('be.visible');
276+
cy.contains('provider-internal-error').should('not.exist');
277+
cy.get('[data-testid="ai-chart-error"]').should('not.contain.text', 'sk-sensitive-example-key');
278+
});
279+
280+
for (const width of [375, 390]) {
281+
it(`keeps Chinese provider controls and examples within ${width}px`, () => {
282+
cy.viewport(width, 844);
283+
cy.visit('/zh/ai-chart');
284+
cy.get('input[placeholder="OpenAI API Key"]').should('be.visible');
285+
cy.get('textarea[placeholder="描述您想查看的图表……"]').should('be.visible');
286+
cy.contains('示例提示').should('be.visible');
287+
cy.document().then((doc) => {
288+
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
289+
});
290+
});
291+
}
292+
});

‎packages/app/cypress/e2e/collectivex.cy.ts‎

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,45 @@ describe('CollectiveX neutral run view', () => {
418418
.and('contain.text', 'EP')
419419
.and('contain.text', 'KV');
420420
});
421+
422+
it('localizes the complete chart and run-table click path on the Chinese route', () => {
423+
cy.viewport(1440, 900);
424+
cy.visit('/zh/collectivex');
425+
cy.wait('@runs');
426+
cy.wait('@run');
427+
428+
cy.get('[data-testid="collectivex-run-conclusion"]')
429+
.should('contain.text', `#${runId}`)
430+
.and('contain.text', '成功');
431+
cy.get('[data-testid="collectivex-runs"]')
432+
.should('contain.text', '运行记录')
433+
.and('contain.text', '终态数据点');
434+
cy.get('[data-testid="collectivex-main-chart"]')
435+
.should('contain.text', '往返(实测)')
436+
.and('contain.text', '解码')
437+
.and('contain.text', '延迟(µs)');
438+
439+
cy.get('[data-testid="collectivex-explorer-chart"] .point').first().click({ force: true });
440+
cy.get('[data-chart-tooltip]:visible')
441+
.should('contain.text', '点击其他区域关闭')
442+
.and('contain.text', '往返')
443+
.and('contain.text', '延迟 p50 / p90 / p95 / p99');
444+
});
445+
446+
for (const width of [375, 390]) {
447+
it(`keeps the Chinese explorer and runs table reachable at ${width}px`, () => {
448+
cy.viewport(width, 844);
449+
cy.visit('/zh/collectivex');
450+
cy.wait('@runs');
451+
cy.wait('@run');
452+
453+
cy.get('[data-testid="collectivex-main-chart"] svg').should('exist');
454+
cy.get('[data-testid="collectivex-runs-table"]').scrollTo('right').should('be.visible');
455+
cy.document().then((doc) => {
456+
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
457+
});
458+
});
459+
}
421460
});
422461

423462
describe('CollectiveX run deletion', () => {
@@ -545,7 +584,8 @@ describe('CollectiveX availability states', () => {
545584
cy.wait('@missing');
546585
cy.get('[data-testid="collectivex-error"]')
547586
.should('be.visible')
548-
.and('contain.text', 'API error: 404');
587+
.and('contain.text', 'The CollectiveX dataset failed to load.')
588+
.and('not.contain.text', 'API error: 404');
549589
cy.get('[data-testid="collectivex-error-version-select"]').should('contain.text', 'V1');
550590
});
551591

@@ -558,7 +598,21 @@ describe('CollectiveX availability states', () => {
558598
cy.wait('@down');
559599
cy.get('[data-testid="collectivex-error"]')
560600
.should('be.visible')
561-
.and('contain.text', 'API error: 503');
601+
.and('contain.text', 'The CollectiveX dataset failed to load.')
602+
.and('not.contain.text', 'API error: 503');
603+
});
604+
605+
it('shows a safe localized error on the Chinese route', () => {
606+
cy.intercept('GET', '/api/v1/collectivex/runs?*', {
607+
statusCode: 503,
608+
body: { error: 'collectivex-internal-storage-detail' },
609+
}).as('zhDown');
610+
cy.visit('/zh/collectivex');
611+
cy.wait('@zhDown');
612+
cy.get('[data-testid="collectivex-error"]')
613+
.should('contain.text', 'CollectiveX 运行暂不可用')
614+
.and('contain.text', 'CollectiveX 数据集加载失败。')
615+
.and('not.contain.text', 'collectivex-internal-storage-detail');
562616
});
563617

564618
it('renders the loading state while the run resolves', () => {

‎packages/app/cypress/e2e/historical-trends.cy.ts‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ describe('Historical Trends — Content & Interactions', () => {
7676
doc.body.style.removeProperty('pointer-events');
7777
});
7878
cy.get('[data-testid="model-selector"]').should('be.visible');
79-
// Radix Select may need a brief settle after scroll lock removal
80-
cy.wait(100);
8179
cy.get('[data-testid="model-selector"]').click();
8280
cy.get('[role="option"]').should('have.length.greaterThan', 0);
8381
cy.get('body').type('{esc}');
@@ -157,7 +155,6 @@ describe('Historical Trends — Content & Interactions', () => {
157155
doc.body.style.removeProperty('pointer-events');
158156
});
159157
cy.get('[data-testid="historical-trend-figure"]').should('exist');
160-
cy.wait(100);
161158

162159
cy.get('[data-testid="historical-trend-figure"] figcaption p')
163160
.first()
@@ -187,3 +184,56 @@ describe('Historical Trends — Content & Interactions', () => {
187184
});
188185
});
189186
});
187+
188+
describe('Historical Trends — Chinese route', () => {
189+
beforeEach(() => {
190+
cy.visit('/zh/historical', {
191+
onBeforeLoad(win) {
192+
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
193+
},
194+
});
195+
cy.get('[data-testid="historical-trends-display"]').should('be.visible');
196+
});
197+
198+
it('localizes the metric title, chart instructions, and point tooltip', () => {
199+
cy.viewport(1440, 900);
200+
cy.get('[data-testid="historical-trend-figure"] h2').should('contain.text', '随时间变化');
201+
cy.get('[data-testid="historical-trend-figure"]').should('contain.text', 'Shift+滚轮横向缩放');
202+
cy.get('[data-testid="trend-chart-svg"] circle').first().click({ force: true });
203+
cy.get('[data-chart-tooltip]:visible')
204+
.should('contain.text', '点击其他区域关闭')
205+
.invoke('text')
206+
.should('match', /\d{4}年/u);
207+
});
208+
209+
it('shows a settled Chinese empty state instead of leaving the skeleton mounted', () => {
210+
cy.intercept('GET', '**/api/v1/benchmarks?*', []).as('emptyBenchmarks');
211+
cy.reload();
212+
cy.wait('@emptyBenchmarks');
213+
cy.contains('所选模型和序列无可用的交互性图表数据。').should('be.visible');
214+
cy.get('[data-testid="historical-trends-display"] .animate-pulse').should('not.exist');
215+
});
216+
217+
it('shows a safe Chinese error when benchmark loading fails', () => {
218+
cy.intercept('GET', '**/api/v1/benchmarks?*', {
219+
statusCode: 500,
220+
body: { error: 'historical-database-internal-detail' },
221+
}).as('failedBenchmarks');
222+
cy.reload();
223+
cy.wait('@failedBenchmarks');
224+
cy.contains('历史基准测试数据加载失败。').should('be.visible');
225+
cy.contains('historical-database-internal-detail').should('not.exist');
226+
});
227+
228+
for (const width of [375, 390]) {
229+
it(`keeps the target controls and chart reachable at ${width}px`, () => {
230+
cy.viewport(width, 844);
231+
cy.get('[data-testid="historical-trends-display"] input[type="range"]').should('be.visible');
232+
cy.get('[data-testid="historical-trends-display"] input[type="number"]').should('be.visible');
233+
cy.get('[data-testid="historical-trend-figure"] svg').should('exist');
234+
cy.document().then((doc) => {
235+
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
236+
});
237+
});
238+
}
239+
});

‎packages/app/cypress/e2e/reliability-chart.cy.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,46 @@ describe('Reliability Chart — Content & Interactions', () => {
156156
});
157157
});
158158
});
159+
160+
describe('Reliability Chart — Chinese route and settled states', () => {
161+
it('localizes chart chrome, SVG labels, and accessibility text', () => {
162+
cy.viewport(1440, 900);
163+
cy.visit('/zh/reliability');
164+
cy.get('[data-testid="reliability-chart-display"]').should('be.visible');
165+
cy.contains('h2', '芯片可靠性').should('be.visible');
166+
cy.get('#reliability-chart svg').should('contain.text', '成功率(%)');
167+
cy.get('#reliability-chart svg .overlay-label').first().should('contain.text', '次运行');
168+
cy.get('#reliability-chart').closest('figure').should('contain.text', 'Shift+滚轮横向缩放');
169+
});
170+
171+
it('shows a Chinese empty state only after an empty response settles', () => {
172+
cy.intercept('GET', '**/api/v1/reliability', []).as('emptyReliability');
173+
cy.visit('/zh/reliability');
174+
cy.wait('@emptyReliability');
175+
cy.contains('所选时间范围内暂无可靠性数据。').should('be.visible');
176+
cy.contains('正在加载可靠性数据……').should('not.exist');
177+
});
178+
179+
it('shows a safe Chinese error instead of a raw API response', () => {
180+
cy.intercept('GET', '**/api/v1/reliability', {
181+
statusCode: 500,
182+
body: { error: 'database-internal-detail' },
183+
}).as('failedReliability');
184+
cy.visit('/zh/reliability');
185+
cy.wait('@failedReliability');
186+
cy.contains('可靠性数据加载失败。').should('be.visible');
187+
cy.contains('database-internal-detail').should('not.exist');
188+
});
189+
190+
for (const width of [375, 390]) {
191+
it(`keeps controls reachable without body overflow at ${width}px`, () => {
192+
cy.viewport(width, 844);
193+
cy.visit('/zh/reliability');
194+
cy.get('[data-testid="reliability-date-range"]').should('be.visible').click();
195+
cy.contains('[role="option"]', '全部时间').should('be.visible');
196+
cy.document().then((doc) => {
197+
expect(doc.documentElement.scrollWidth).to.be.lte(doc.documentElement.clientWidth);
198+
});
199+
});
200+
}
201+
});

0 commit comments

Comments
 (0)