diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/TimingWaterfall.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/TimingWaterfall.tsx
index 03ce7cf2de..2f427dc71b 100644
--- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/TimingWaterfall.tsx
+++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/TimingWaterfall.tsx
@@ -13,7 +13,7 @@ export function TimingWaterfall({ request }: TimingWaterfallProps) {
const total = totalLatencyMs ?? (proxyLatencyMs ?? 0) + (upstreamLatencyMs ?? 0);
if (!total) {
- return
No timing data available.
;
+ return
- Timestamp
+ {t("timingTimestamp")}
{request.timestamp}
- Method
+ {t("timingMethod")}
{request.method}
- Status
+ {t("timingStatus")}
{String(request.status)}
- Request size
+ {t("timingRequestSize")}
{request.requestSize} B
- Response size
+ {t("timingResponseSize")}
{request.responseSize} B
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 521e8f1640..7123a1ccd5 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -2,6 +2,7 @@
"common": {
"save": "Save",
"cancel": "Cancel",
+ "understand": "I understand",
"delete": "Delete",
"loading": "Loading...",
"error": "An error occurred",
@@ -7480,6 +7481,13 @@
"statsSuccessful": "Successful",
"statsTotalRequests": "Total requests",
"timingProxyOverhead": "Proxy overhead",
- "timingUpstreamResponse": "Upstream response"
+ "timingUpstreamResponse": "Upstream response",
+ "timingNoData": "No timing data available.",
+ "timingTotalLatency": "Total latency",
+ "timingTimestamp": "Timestamp",
+ "timingMethod": "Method",
+ "timingStatus": "Status",
+ "timingRequestSize": "Request size",
+ "timingResponseSize": "Response size"
}
}
diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json
index 7aaa0e115c..2219e32eb9 100644
--- a/src/i18n/messages/pt-BR.json
+++ b/src/i18n/messages/pt-BR.json
@@ -2,6 +2,7 @@
"common": {
"save": "Salvar",
"cancel": "Cancelar",
+ "understand": "Eu entendo",
"delete": "Excluir",
"loading": "Carregando...",
"error": "Ocorreu um erro",
@@ -7470,6 +7471,13 @@
"statsSuccessful": "Bem-sucedidas",
"statsTotalRequests": "Total de requisições",
"timingProxyOverhead": "Overhead do proxy",
- "timingUpstreamResponse": "Resposta upstream"
+ "timingUpstreamResponse": "Resposta upstream",
+ "timingNoData": "Nenhum dado de tempo disponível.",
+ "timingTotalLatency": "Latência total",
+ "timingTimestamp": "Timestamp",
+ "timingMethod": "Método",
+ "timingStatus": "Status",
+ "timingRequestSize": "Tamanho da requisição",
+ "timingResponseSize": "Tamanho da resposta"
}
}
diff --git a/tests/unit/ui/timing-i18n.test.tsx b/tests/unit/ui/timing-i18n.test.tsx
new file mode 100644
index 0000000000..5d2ee14a77
--- /dev/null
+++ b/tests/unit/ui/timing-i18n.test.tsx
@@ -0,0 +1,73 @@
+/**
+ * i18n coverage for TimingTab + TimingWaterfall (R4 fix #3).
+ * Source-text inspection — no JSDOM render needed.
+ *
+ * Round-3 F-I18N translated ConversationTab/StatsTab/StatsCharts but missed
+ * TimingTab (5 labels) and TimingWaterfall (2 labels). Round-4 closed the gap.
+ */
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const TIMING_TAB = path.resolve(
+ __dirname,
+ "../../../src/app/(dashboard)/dashboard/tools/traffic-inspector/components/tabs/TimingTab.tsx",
+);
+const TIMING_WATERFALL = path.resolve(
+ __dirname,
+ "../../../src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/TimingWaterfall.tsx",
+);
+const EN = path.resolve(__dirname, "../../../src/i18n/messages/en.json");
+const PT = path.resolve(__dirname, "../../../src/i18n/messages/pt-BR.json");
+
+const tabSrc = readFileSync(TIMING_TAB, "utf-8");
+const waterfallSrc = readFileSync(TIMING_WATERFALL, "utf-8");
+const en = JSON.parse(readFileSync(EN, "utf-8"));
+const pt = JSON.parse(readFileSync(PT, "utf-8"));
+
+describe("Timing i18n (R4 fix #3)", () => {
+ it("TimingTab uses useTranslations and has no hardcoded English labels", () => {
+ assert.ok(tabSrc.includes('useTranslations("trafficInspector")'));
+ for (const lit of ["Timestamp", "Method", "Status", "Request size", "Response size"]) {
+ assert.ok(
+ !new RegExp(`
]*>${lit}`).test(tabSrc),
+ `TimingTab must not render hardcoded "${lit}" — use t() instead`,
+ );
+ }
+ for (const key of ["timingTimestamp", "timingMethod", "timingStatus", "timingRequestSize", "timingResponseSize"]) {
+ assert.ok(tabSrc.includes(`t("${key}")`), `TimingTab must call t("${key}")`);
+ }
+ });
+
+ it("TimingWaterfall translates the empty state and total latency label", () => {
+ assert.ok(!waterfallSrc.includes("No timing data available."));
+ assert.ok(!waterfallSrc.includes(">Total latency<"));
+ assert.ok(waterfallSrc.includes('t("timingNoData")'));
+ assert.ok(waterfallSrc.includes('t("timingTotalLatency")'));
+ });
+
+ it("All 7 new timing keys exist in both en.json and pt-BR.json", () => {
+ const keys = [
+ "timingNoData",
+ "timingTotalLatency",
+ "timingTimestamp",
+ "timingMethod",
+ "timingStatus",
+ "timingRequestSize",
+ "timingResponseSize",
+ ];
+ for (const k of keys) {
+ assert.ok(en.trafficInspector?.[k], `en.json must have trafficInspector.${k}`);
+ assert.ok(pt.trafficInspector?.[k], `pt-BR.json must have trafficInspector.${k}`);
+ }
+ });
+
+ it("common.understand is present in both locales (RiskNoticeModal namespace)", () => {
+ assert.equal(en.common?.understand, "I understand");
+ assert.equal(pt.common?.understand, "Eu entendo");
+ });
+});