{strings.heading}
- {rows.length} cases · {measured} measured · {strings.description}
+ {strings.summary(rows.length, measured)}
+ {strings.description}
{measuredCases.length > 0 && (
<>
@@ -427,7 +476,7 @@ export function CollectiveXKvSection({
ariaLabel={strings.xAriaLabel}
testId="collectivex-kv-xaxis-toggle"
options={[
- { value: 'batch', label: 'Batch' },
+ { value: 'batch', label: strings.batch },
{ value: 'isl', label: 'ISL' },
{ value: 'frontier', label: strings.frontierOption },
{ value: 'overlap', label: strings.overlapOption },
@@ -497,10 +546,13 @@ export function CollectiveXKvSection({
showWireCeilings={showWireCeilings}
caption={
- {op} · page {pageTokens} ·{' '}
- {showWireCeilings
- ? strings.frontierCaption
- : strings.frontierCaptionWithoutCeilings}
+ {strings.caption(
+ op,
+ String(pageTokens),
+ showWireCeilings
+ ? strings.frontierCaption
+ : strings.frontierCaptionWithoutCeilings,
+ )}
}
legendElement={
@@ -523,11 +575,15 @@ export function CollectiveXKvSection({
selection={{ op, pageTokens, isl: effectiveOverlapIsl }}
caption={
- {op} · page {pageTokens} · ISL{' '}
- {effectiveOverlapIsl === undefined
- ? strings.islMaxOption
- : formatIsl(effectiveOverlapIsl)}{' '}
- · {strings.overlapCaption}
+ {strings.caption(
+ op,
+ String(pageTokens),
+ `ISL ${
+ effectiveOverlapIsl === undefined
+ ? strings.islMaxOption
+ : formatIsl(effectiveOverlapIsl)
+ } · ${strings.overlapCaption}`,
+ )}
}
legendElement={
@@ -536,7 +592,10 @@ export function CollectiveXKvSection({
legendItems={legendItems}
disableActiveSort
isLegendExpanded={legendExpanded}
- onExpandedChange={setLegendExpanded}
+ onExpandedChange={(expanded) => {
+ setLegendExpanded(expanded);
+ track('collectivex_kv_legend_expanded', { expanded });
+ }}
/>
}
/>
@@ -551,8 +610,11 @@ export function CollectiveXKvSection({
yLogScale={yLogScale}
caption={
- {op} · page {pageTokens} ·{' '}
- {xAxis === 'batch' ? strings.batchCaption : strings.islCaption}
+ {strings.caption(
+ op,
+ String(pageTokens),
+ xAxis === 'batch' ? strings.batchCaption : strings.islCaption,
+ )}
}
legendElement={
@@ -562,7 +624,10 @@ export function CollectiveXKvSection({
switches={legendSwitches}
disableActiveSort
isLegendExpanded={legendExpanded}
- onExpandedChange={setLegendExpanded}
+ onExpandedChange={(expanded) => {
+ setLegendExpanded(expanded);
+ track('collectivex_kv_legend_expanded', { expanded });
+ }}
/>
}
/>
diff --git a/packages/app/src/components/collectivex/CollectiveXRunsTable.tsx b/packages/app/src/components/collectivex/CollectiveXRunsTable.tsx
index e4009f8ac..7b8112442 100644
--- a/packages/app/src/components/collectivex/CollectiveXRunsTable.tsx
+++ b/packages/app/src/components/collectivex/CollectiveXRunsTable.tsx
@@ -7,7 +7,7 @@ import { track } from '@/lib/analytics';
import { useLocale } from '@/lib/use-locale';
import { cn } from '@/lib/utils';
-import { collectiveXRunDasharray, collectiveXSkuLabel } from './data';
+import { collectiveXConclusionLabel, collectiveXRunDasharray, collectiveXSkuLabel } from './data';
import type { CollectiveXRunSummary } from './types';
interface CollectiveXRunsTableProps {
@@ -32,30 +32,32 @@ const STRINGS = {
skus: 'SKUs',
published: 'Published (UTC)',
actions: 'Actions',
- pending: 'pending',
showRun: (id: string) => `Show run #${id}`,
lineStyle: (id: string) => `Line style for run #${id}`,
openRun: (id: string) => `Open GitHub Actions run #${id}`,
deleteRun: (id: string) => `Delete run #${id}`,
empty: 'No runs match this benchmark version.',
+ epSuite: (measured: number, requested: number) => `EP: ${measured}/${requested} measured`,
+ kvSuite: (measured: number, requested: number) =>
+ `KV transfer: ${measured}/${requested} measured`,
},
- // English placeholders per the repository's temporary language override.
zh: {
- shown: 'Shown',
- run: 'Run',
- result: 'Result',
+ shown: '显示',
+ run: '运行',
+ result: '结果',
suites: '测试套件',
- cases: 'Measured cases',
- points: 'Terminal points',
+ cases: '实测用例',
+ points: '终态数据点',
skus: 'SKUs',
- published: 'Published (UTC)',
- actions: 'Actions',
- pending: 'pending',
- showRun: (id: string) => `Show run #${id}`,
- lineStyle: (id: string) => `Line style for run #${id}`,
- openRun: (id: string) => `Open GitHub Actions run #${id}`,
- deleteRun: (id: string) => `Delete run #${id}`,
- empty: 'No runs match this benchmark version.',
+ published: '发布时间(UTC)',
+ actions: '操作',
+ showRun: (id: string) => `显示运行 #${id}`,
+ lineStyle: (id: string) => `运行 #${id} 的线型`,
+ openRun: (id: string) => `打开 GitHub Actions 运行 #${id}`,
+ deleteRun: (id: string) => `删除运行 #${id}`,
+ empty: '该基准测试版本下暂无运行记录。',
+ epSuite: (measured: number, requested: number) => `EP:已测量 ${measured}/${requested}`,
+ kvSuite: (measured: number, requested: number) => `KV 传输:已测量 ${measured}/${requested}`,
},
} as const;
@@ -122,7 +124,7 @@ export function CollectiveXRunsTable({
const visible = visibleRunIds.has(run.run_id);
const loading = loadingRunIds.has(run.run_id);
const deleting = deletingRunIds.has(run.run_id);
- const conclusion = run.conclusion ?? t.pending;
+ const conclusion = run.conclusion;
const selectedRunIndex = selectedRunIndexById.get(run.run_id);
// Summaries stored before the kv suite carry no kv_cases: EP-only.
const kvRequested = run.kv_cases?.requested ?? 0;
@@ -202,17 +204,17 @@ export function CollectiveXRunsTable({
- {conclusion}
+ {collectiveXConclusionLabel(conclusion, locale)}
{epRequested > 0 && (
0 && (
` (note ${number})`,
},
zh: {
title: '已知 Kernel 支持情况',
description:
- '与上方勾选的运行无关的完整 SKU × 集合通信库支持图景:绿色组合在集群上可用,红色组合已知不可用(编号注释说明原因),灰色组合对该配对不存在。',
+ '下表展示完整的 SKU × 集合通信库支持情况,不受上方已选运行影响:绿色表示该组合可在集群上运行,红色表示已知不可用(原因见编号注释),灰色表示该配对不存在。',
modes: {
normal: '吞吐量 Kernel',
'low-latency': '低延迟 Kernel',
@@ -42,6 +43,7 @@ const STRINGS = {
broken: '已知不可用',
na: '不适用',
notes: '注释',
+ note: (number: number) => `(注 ${number})`,
},
} as const;
@@ -56,14 +58,16 @@ function EpChip({
degree,
noteNumber,
noteText,
+ formatNote,
}: {
ep: CollectiveXKnownEp;
degree: 8 | 16;
noteNumber: number | null;
noteText: string | null;
+ formatNote: (number: number) => string;
}) {
const glyph = ep.status === 'works' ? '✓' : ep.status === 'broken' ? '✕' : '—';
- const label = `EP${degree} ${glyph}${noteNumber === null ? '' : ` (note ${noteNumber})`}`;
+ const label = `EP${degree} ${glyph}${noteNumber === null ? '' : formatNote(noteNumber)}`;
return (
)}
diff --git a/packages/app/src/components/collectivex/data.test.ts b/packages/app/src/components/collectivex/data.test.ts
index 00075d77f..693d62f9d 100644
--- a/packages/app/src/components/collectivex/data.test.ts
+++ b/packages/app/src/components/collectivex/data.test.ts
@@ -4,6 +4,7 @@ import {
chartPoints,
collectiveXCaseLabel,
collectiveXColorKey,
+ collectiveXConclusionLabel,
collectiveXLegendLabel,
collectiveXRunDasharray,
collectiveXSeriesForRun,
@@ -32,11 +33,41 @@ const dataset = makeCollectiveXDataset();
// series[1]: MoRI EP16 scale-out (xGMI scale-up + RDMA scale-out, two nodes).
const [scaleUp, scaleOut] = dataset.series;
+describe('collectiveXConclusionLabel', () => {
+ it.each([
+ ['success', '成功'],
+ ['failure', '失败'],
+ ['cancelled', '已取消'],
+ ['skipped', '已跳过'],
+ ['timed_out', '超时'],
+ ['startup_failure', '启动失败'],
+ ['action_required', '需要处理'],
+ ['neutral', '中立'],
+ ['stale', '已过期'],
+ ])('localizes the %s workflow conclusion in Chinese', (conclusion, expected) => {
+ expect(collectiveXConclusionLabel(conclusion, 'zh')).toBe(expected);
+ });
+
+ it('derives pending only from a null workflow conclusion', () => {
+ expect(collectiveXConclusionLabel(null, 'zh')).toBe('待处理');
+ expect(collectiveXConclusionLabel(null, 'en')).toBe('pending');
+ });
+
+ it('preserves known English values and hides unknown values on Chinese pages', () => {
+ expect(collectiveXConclusionLabel('startup_failure', 'en')).toBe('startup_failure');
+ expect(collectiveXConclusionLabel('future_status', 'en')).toBe('future_status');
+ expect(collectiveXConclusionLabel('future_status', 'zh')).toBe('未知状态');
+ });
+});
+
describe('collectiveXTopologyLabel', () => {
it('shows only the scale-up transport when there is no scale-out fabric', () => {
expect(collectiveXTopologyLabel(scaleUp.system)).toBe(
'1x8 · domain 8 · nvlink · h200-nvlink-island',
);
+ expect(collectiveXTopologyLabel(scaleUp.system, 'zh')).toBe(
+ '1x8 · 域内芯片数 8 · nvlink · h200-nvlink-island',
+ );
});
it('joins scale-up and scale-out transports when a scale-out fabric is present', () => {
@@ -81,6 +112,15 @@ describe('collectiveXSeriesLabel', () => {
expect(collectiveXLegendLabel(runSeries)).toBe(
'H200 · deepep-v2 · EP8 · normal · decode · bf16',
);
+ expect(collectiveXLegendLabel(runSeries, 'zh')).toBe(
+ 'H200 · deepep-v2 · EP8 · 常规 · 解码 · bf16',
+ );
+ expect(
+ collectiveXLegendLabel(
+ makeCollectiveXSeries({ mode: 'low-latency', phase: 'prefill' }),
+ 'zh',
+ ),
+ ).toBe('H200 · deepep-v2 · EP8 · 低延迟 · 预填充 · bf16');
});
});
@@ -299,6 +339,9 @@ describe('chartPoints', () => {
expect(point.x).toBeGreaterThan(0);
expect(point.y).toBeGreaterThan(0);
}
+ expect(chartPoints([scaleUp], 'dispatch', 'p50', 'latency', 'zh')[0].seriesLabel).toBe(
+ 'H200 · deepep-v2 · EP8 · 常规 · 解码 · bf16',
+ );
});
it('drops points whose metric is unavailable', () => {
diff --git a/packages/app/src/components/collectivex/data.ts b/packages/app/src/components/collectivex/data.ts
index d77671a61..d9d7883e9 100644
--- a/packages/app/src/components/collectivex/data.ts
+++ b/packages/app/src/components/collectivex/data.ts
@@ -1,5 +1,7 @@
import { GPU_KEYS } from '@semianalysisai/inferencex-constants';
+import type { Locale } from '@/lib/i18n';
+
import type {
CollectiveXChartPoint,
CollectiveXComponent,
@@ -25,6 +27,52 @@ export interface CollectiveXSeriesSelection {
const BASE_RUN_DASHARRAYS = ['none', '9 4', '3 3', '10 3 2 3', '2 3', '12 3 2 3'] as const;
+const COLLECTIVEX_CONCLUSION_LABELS = {
+ en: {
+ action_required: 'action_required',
+ cancelled: 'cancelled',
+ failure: 'failure',
+ neutral: 'neutral',
+ skipped: 'skipped',
+ stale: 'stale',
+ startup_failure: 'startup_failure',
+ success: 'success',
+ timed_out: 'timed_out',
+ },
+ zh: {
+ action_required: '需要处理',
+ cancelled: '已取消',
+ failure: '失败',
+ neutral: '中立',
+ skipped: '已跳过',
+ stale: '已过期',
+ startup_failure: '启动失败',
+ success: '成功',
+ timed_out: '超时',
+ },
+} as const;
+
+const COLLECTIVEX_MODE_LABELS_ZH: Record = {
+ normal: '常规',
+ 'low-latency': '低延迟',
+};
+
+const COLLECTIVEX_PHASE_LABELS_ZH: Record = {
+ decode: '解码',
+ prefill: '预填充',
+};
+
+/** Format every conclusion emitted by the GitHub Actions workflow-run API. */
+export function collectiveXConclusionLabel(conclusion: string | null, locale: Locale): string {
+ if (conclusion === null) return locale === 'zh' ? '待处理' : 'pending';
+ const labels = COLLECTIVEX_CONCLUSION_LABELS[locale];
+ return conclusion in labels
+ ? labels[conclusion as keyof typeof labels]
+ : locale === 'zh'
+ ? '未知状态'
+ : conclusion;
+}
+
/**
* CollectiveX artifacts identify runner pools in the SKU (for example,
* `b200-nscale` or `h100-dgxc`). Collapse known hardware identifiers to the
@@ -70,20 +118,28 @@ export function collectiveXTopologyLabel(
| 'scale_out_transport'
| 'topology_class'
>,
+ locale: Locale = 'en',
): string {
const transports = system.scale_out_transport
? `${system.scale_up_transport}+${system.scale_out_transport}`
: system.scale_up_transport;
- return `${system.nodes}x${system.gpus_per_node} · domain ${system.scale_up_domain} · ${transports} · ${system.topology_class}`;
+ const domain =
+ locale === 'zh' ? `域内芯片数 ${system.scale_up_domain}` : `domain ${system.scale_up_domain}`;
+ return `${system.nodes}x${system.gpus_per_node} · ${domain} · ${transports} · ${system.topology_class}`;
}
-export function collectiveXLegendLabel(series: CollectiveXSeries): string {
- return `${collectiveXSkuLabel(series.system.sku)} · ${series.backend} · EP${series.system.ep_size} · ${series.mode} · ${series.phase} · ${series.precision}`;
+export function collectiveXLegendLabel(series: CollectiveXSeries, locale: Locale = 'en'): string {
+ const mode = locale === 'zh' ? COLLECTIVEX_MODE_LABELS_ZH[series.mode] : series.mode;
+ const phase = locale === 'zh' ? COLLECTIVEX_PHASE_LABELS_ZH[series.phase] : series.phase;
+ return `${collectiveXSkuLabel(series.system.sku)} · ${series.backend} · EP${series.system.ep_size} · ${mode} · ${phase} · ${series.precision}`;
}
-export function collectiveXSeriesLabel(series: CollectiveXSeries | CollectiveXRunSeries): string {
+export function collectiveXSeriesLabel(
+ series: CollectiveXSeries | CollectiveXRunSeries,
+ locale: Locale = 'en',
+): string {
const runPrefix = 'run_id' in series ? `#${series.run_id} · ` : '';
- return `${runPrefix}${collectiveXLegendLabel(series)}`;
+ return `${runPrefix}${collectiveXLegendLabel(series, locale)}`;
}
export function collectiveXColorKey(series: CollectiveXSeries | CollectiveXRunSeries): string {
@@ -202,6 +258,7 @@ export function chartPoints(
operation: CollectiveXOperation,
percentile: CollectiveXPercentile,
yAxis: CollectiveXYAxis,
+ locale: Locale = 'en',
): CollectiveXChartPoint[] {
return series.flatMap((item) =>
item.points.flatMap((point) => {
@@ -211,7 +268,7 @@ export function chartPoints(
return [
{
seriesId: item.series_id,
- seriesLabel: collectiveXSeriesLabel(item),
+ seriesLabel: collectiveXSeriesLabel(item, locale),
colorKey: collectiveXColorKey(item),
x,
y,
diff --git a/packages/app/src/components/feedback-modal.tsx b/packages/app/src/components/feedback-modal.tsx
index 98e39c928..95211fe9f 100644
--- a/packages/app/src/components/feedback-modal.tsx
+++ b/packages/app/src/components/feedback-modal.tsx
@@ -5,6 +5,8 @@ import { usePathname } from 'next/navigation';
import { useCallback, useId, useState } from 'react';
import { track } from '@/lib/analytics';
+import type { Locale } from '@/lib/i18n';
+import { useLocale } from '@/lib/use-locale';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@@ -18,9 +20,58 @@ type Status = 'idle' | 'submitting' | 'success' | 'error';
export interface FeedbackFormProps {
/** Engine-supplied close + persist-dismissal hook. */
onDismiss: () => void;
+ /** Test/embedded-surface override. Production defaults to the current route locale. */
+ locale?: Locale;
+ /** Engine-owned IDs referenced by the containing dialog. */
+ titleId?: string;
+ descriptionId?: string;
}
-export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
+const STRINGS = {
+ en: {
+ validation: 'Please fill in at least one field.',
+ rateLimit: 'Too many submissions — please try again later.',
+ rejected: 'Submission rejected. Please check the fields and try again.',
+ saveFailed: 'Could not save your feedback. Please try again.',
+ unknownError: 'Something went wrong.',
+ successTitle: 'Thanks for your feedback!',
+ successBody: 'We read every response.',
+ title: 'Help us improve InferenceX',
+ description: "We'd love to hear what's working and what isn't.",
+ worksWell: 'What works well?',
+ improve: 'What could be better?',
+ want: 'What would you like to see?',
+ privacy: 'Your response is encrypted and only visible to the InferenceX team.',
+ dismiss: 'Maybe later',
+ sending: 'Sending…',
+ submit: 'Send feedback',
+ },
+ zh: {
+ validation: '请至少填写一项。',
+ rateLimit: '提交次数过多,请稍后再试。',
+ rejected: '提交未通过校验,请检查填写内容后重试。',
+ saveFailed: '反馈保存失败,请重试。',
+ unknownError: '出现意外错误,请重试。',
+ successTitle: '感谢您的反馈!',
+ successBody: '我们会认真阅读每一条反馈。',
+ title: '帮助我们改进 InferenceX',
+ description: '欢迎告诉我们哪些体验不错,以及哪些地方需要改进。',
+ worksWell: '哪些地方做得好?',
+ improve: '哪些地方可以改进?',
+ want: '还希望看到哪些功能?',
+ privacy: '您的反馈会加密保存,只有 InferenceX 团队可以查看。',
+ dismiss: '稍后再说',
+ sending: '正在发送……',
+ submit: '发送反馈',
+ },
+} as const;
+
+export function FeedbackForm({
+ onDismiss,
+ locale: localeOverride,
+ titleId: titleIdOverride,
+ descriptionId: descriptionIdOverride,
+}: FeedbackFormProps) {
const [doingWell, setDoingWell] = useState('');
const [doingPoorly, setDoingPoorly] = useState('');
const [wantToSee, setWantToSee] = useState('');
@@ -28,8 +79,13 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
const [status, setStatus] = useState('idle');
const [errorMsg, setErrorMsg] = useState(null);
const pathname = usePathname();
- const titleId = useId();
- const descId = useId();
+ const routeLocale = useLocale();
+ const locale = localeOverride ?? routeLocale;
+ const t = STRINGS[locale];
+ const generatedTitleId = useId();
+ const generatedDescriptionId = useId();
+ const titleId = titleIdOverride ?? generatedTitleId;
+ const descriptionId = descriptionIdOverride ?? generatedDescriptionId;
const handleSubmit = useCallback(async () => {
if (status === 'submitting') return;
@@ -38,9 +94,10 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
doingPoorly.trim() && 'doing_poorly',
wantToSee.trim() && 'want_to_see',
].filter(Boolean) as string[];
+ track('feedback_modal_submit_clicked', { filled_fields: filledFields.join(',') });
if (filledFields.length === 0) {
- setErrorMsg('Please fill in at least one field.');
+ setErrorMsg(t.validation);
setStatus('error');
return;
}
@@ -63,12 +120,12 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
if (!res.ok) {
if (res.status === 429) {
- throw new Error('Too many submissions — please try again later.');
+ throw new Error(t.rateLimit);
}
if (res.status === 400) {
- throw new Error('Submission rejected. Please check the fields and try again.');
+ throw new Error(t.rejected);
}
- throw new Error('Could not save your feedback. Please try again.');
+ throw new Error(t.saveFailed);
}
window.dispatchEvent(new Event(FEEDBACK_SUBMITTED_EVENT));
@@ -76,10 +133,22 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
setStatus('success');
window.setTimeout(onDismiss, SUCCESS_HOLD_MS);
} catch (error) {
- setErrorMsg(error instanceof Error ? error.message : 'Something went wrong.');
+ const knownMessage =
+ error instanceof Error &&
+ (error.message === t.rateLimit ||
+ error.message === t.rejected ||
+ error.message === t.saveFailed)
+ ? error.message
+ : t.unknownError;
+ setErrorMsg(knownMessage);
setStatus('error');
}
- }, [doingWell, doingPoorly, wantToSee, honeypot, pathname, status, onDismiss]);
+ }, [doingWell, doingPoorly, wantToSee, honeypot, pathname, status, onDismiss, t]);
+
+ const handleDismiss = useCallback(() => {
+ track('feedback_modal_later_clicked');
+ onDismiss();
+ }, [onDismiss]);
const submitting = status === 'submitting';
@@ -88,10 +157,10 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
- Thanks for your feedback!
+ {t.successTitle}
-
- We read every response.
+
+ {t.successBody}
);
@@ -102,29 +171,29 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
- Help us improve InferenceX
+ {t.title}
-
- We'd love to hear what's working and what isn't.
+
+ {t.description}
-
- Your response is encrypted and only visible to the InferenceX team.
-
+ {t.privacy}
{errorMsg && (
@@ -155,17 +222,17 @@ export function FeedbackForm({ onDismiss }: FeedbackFormProps) {
)}
-
+
diff --git a/packages/app/src/components/feedback-viewer/FeedbackViewer.tsx b/packages/app/src/components/feedback-viewer/FeedbackViewer.tsx
index b877a8470..04588e8f8 100644
--- a/packages/app/src/components/feedback-viewer/FeedbackViewer.tsx
+++ b/packages/app/src/components/feedback-viewer/FeedbackViewer.tsx
@@ -81,6 +81,7 @@ const STRINGS = {
showKey: 'Show key',
allDecryptsFailed: "All rows failed to decrypt — the key parses but doesn't match the data.",
fetchError: 'Failed to load feedback rows.',
+ retry: 'Retry',
loadingRows: 'Loading rows…',
noRows: 'No feedback rows yet.',
enterKey: 'Enter the key above to decrypt.',
@@ -91,6 +92,7 @@ const STRINGS = {
whatWorksWell: 'What works well',
whatCouldBeBetter: 'What could be better',
wouldLikeToSee: 'Would like to see',
+ invalidKey: 'The decryption key must be valid base64 for exactly 32 bytes.',
},
zh: {
heading: '用户反馈',
@@ -100,12 +102,13 @@ const STRINGS = {
keyLabel: '解密密钥(base64,32 字节)',
keyPlaceholder: 'base64 编码密钥',
decrypt: '解密',
- forgetKey: '忘记密钥',
+ forgetKey: '清除密钥',
hideKey: '隐藏密钥',
showKey: '显示密钥',
allDecryptsFailed: '所有行均解密失败——密钥格式正确但与数据不匹配。',
fetchError: '无法加载反馈数据。',
- loadingRows: '加载中……',
+ retry: '重试',
+ loadingRows: '正在加载反馈记录……',
noRows: '暂无反馈记录。',
enterKey: '请在上方输入密钥进行解密。',
encryptedRowsLoaded: (n: number) => `已加载 ${n} 条加密记录。`,
@@ -115,12 +118,13 @@ const STRINGS = {
whatWorksWell: '做得好的地方',
whatCouldBeBetter: '可以改进的地方',
wouldLikeToSee: '希望看到的功能',
+ invalidKey: '解密密钥必须是有效的 base64 编码,解码后长度为 32 字节。',
},
} as const;
export default function FeedbackViewer() {
const router = useRouter();
- const { data, isLoading, error: fetchError } = useFeedbackList();
+ const { data, isLoading, error: fetchError, refetch } = useFeedbackList();
const locale = useLocale();
const t = STRINGS[locale];
const [keyInput, setKeyInput] = useState('');
@@ -147,14 +151,13 @@ export default function FeedbackViewer() {
setCipherKey(k);
setKeyError(null);
track('feedback_viewer_key_accepted');
- } catch (error) {
- const msg = error instanceof Error ? error.message : 'invalid key';
- setKeyError(msg);
+ } catch {
+ setKeyError(t.invalidKey);
setCipherKey(null);
- track('feedback_viewer_key_rejected', { reason: msg });
+ track('feedback_viewer_key_rejected');
}
},
- [keyInput],
+ [keyInput, t.invalidKey],
);
const handleForget = useCallback(() => {
@@ -202,7 +205,7 @@ export default function FeedbackViewer() {
-
+
|