feat(i18n): translate TimingTab + TimingWaterfall + add common.understand (R4 #1, #3)

Round-3 F-I18N covered ConversationTab, StatsTab, StatsCharts, and labels in
TimingWaterfall, but missed:

 - TimingTab.tsx — 5 user-visible labels (Timestamp, Method, Status, Request
   size, Response size) were hardcoded English.
 - TimingWaterfall.tsx — empty state ("No timing data available.") and Total
   latency label were also hardcoded.
 - common.understand — RiskNoticeModal calls
   `useTranslations("common")("understand")` but the key did not exist;
   only the `|| "I understand"` fallback rescued it, leaving pt-BR users
   seeing English.

Adds the missing 7 trafficInspector.timing* keys and common.understand to both
en.json and pt-BR.json, wires `useTranslations` in both components, and adds a
source-grep test asserting no English literals remain in JSX and that all 8
keys exist in both locales.
This commit is contained in:
diegosouzapw
2026-05-28 21:06:47 -03:00
parent 1a0aca9b94
commit 51740a7279
5 changed files with 100 additions and 9 deletions

View File

@@ -13,7 +13,7 @@ export function TimingWaterfall({ request }: TimingWaterfallProps) {
const total = totalLatencyMs ?? (proxyLatencyMs ?? 0) + (upstreamLatencyMs ?? 0);
if (!total) {
return <p className="text-sm text-text-muted">No timing data available.</p>;
return <p className="text-sm text-text-muted">{t("timingNoData")}</p>;
}
const segments: Array<{ label: string; ms: number; color: string }> = [
@@ -51,7 +51,7 @@ export function TimingWaterfall({ request }: TimingWaterfallProps) {
})}
</div>
<div className="flex justify-between text-xs font-medium text-text-main border-t border-border pt-2">
<span>Total latency</span>
<span>{t("timingTotalLatency")}</span>
<span>{total}ms</span>
</div>
</div>

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import type { InterceptedRequest } from "@/mitm/inspector/types";
import { TimingWaterfall } from "../shared/TimingWaterfall";
@@ -8,28 +9,29 @@ interface TimingTabProps {
}
export function TimingTab({ request }: TimingTabProps) {
const t = useTranslations("trafficInspector");
return (
<div className="p-4 h-full overflow-auto space-y-4">
<TimingWaterfall request={request} />
<div className="border-t border-border pt-3 space-y-1 text-xs text-text-muted">
<div className="flex justify-between">
<span>Timestamp</span>
<span>{t("timingTimestamp")}</span>
<span className="font-mono">{request.timestamp}</span>
</div>
<div className="flex justify-between">
<span>Method</span>
<span>{t("timingMethod")}</span>
<span className="font-mono">{request.method}</span>
</div>
<div className="flex justify-between">
<span>Status</span>
<span>{t("timingStatus")}</span>
<span className="font-mono">{String(request.status)}</span>
</div>
<div className="flex justify-between">
<span>Request size</span>
<span>{t("timingRequestSize")}</span>
<span className="font-mono">{request.requestSize} B</span>
</div>
<div className="flex justify-between">
<span>Response size</span>
<span>{t("timingResponseSize")}</span>
<span className="font-mono">{request.responseSize} B</span>
</div>
</div>

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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(`<span[^>]*>${lit}</span>`).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");
});
});