Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -533,11 +533,15 @@
outline: none;
}

.lineage-dag-node:focus circle,
.lineage-dag-node:hover circle {
stroke-width: 2.5;
}

.lineage-dag-node:focus circle {
stroke: var(--color-focus-border);
stroke-width: 3.5;
}

.lineage-dag-node[aria-current="true"] circle {
stroke-width: 3;
stroke: var(--text-h);
Comment on lines +540 to 547

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Focus ring not visible on the current DAG node

.lineage-dag-node:focus circle and .lineage-dag-node[aria-current="true"] circle have equal specificity, and the aria-current rule comes later, so a focused current node shows the current color, not the new focus color. Not a regression; focus already lost to aria-current before this change.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
38 changes: 20 additions & 18 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ function ChatPanel({
onChange={(event) => setQuestion(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && handleAsk()}
placeholder={t("What happened between these events?")}
aria-label={t("What happened between these events?")}
/>
<button onClick={() => handleAsk()} disabled={loading || !question.trim()}>
{loading ? t("Asking...") : t("Ask")}
Expand Down Expand Up @@ -386,7 +387,7 @@ function ChatPanel({
))}
</div>
)}
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{exchanges
.filter(
(exchange) =>
Expand Down Expand Up @@ -1167,7 +1168,7 @@ function KeymanPanel({
</details>
)}
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{sourceAuthorContext ? (
<details className="keyman-source-context">
<summary>{t("Source author evidence")} · {t("Hint only")}</summary>
Expand Down Expand Up @@ -1339,7 +1340,7 @@ function EvaluationPanel({
</details>
)}
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{responses === null ? (
<p>{t("Loading evaluation...")}</p>
) : responses.length === 0 ? (
Expand Down Expand Up @@ -1413,7 +1414,7 @@ function CounterpartyPanel({
</details>
)}
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
<ul>
{counterparties.map((c) => (
<li key={c.counterparty_entity_name}>
Expand Down Expand Up @@ -1556,7 +1557,7 @@ function IssueTicketPanel({
</details>
)}
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{tickets === null ? (
<p>{t("Loading tickets...")}</p>
) : tickets.length === 0 ? (
Expand Down Expand Up @@ -1591,6 +1592,7 @@ function IssueTicketPanel({
onChange={(event) => setNewTitle(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && handleCreate()}
placeholder={t("New ticket title")}
aria-label={t("New ticket title")}
/>
<input
type="date"
Expand Down Expand Up @@ -1644,7 +1646,7 @@ function ActivityPanel({ postId, accessToken }: { postId: string; accessToken: s
<h3>{t("Activity")}</h3>
<button onClick={reload}>{t("Refresh")}</button>
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{events === null ? (
<p>{t("Loading activity...")}</p>
) : events.length === 0 ? (
Expand Down Expand Up @@ -1885,7 +1887,7 @@ function PostDetailPopup({
<div className="popup-backdrop" onClick={onClose}>
<div className="popup-panel" onClick={(event) => event.stopPropagation()}>
<PopupCloseButton onClose={onClose} label={t("Close")} />
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{!post && !error && <p>{t("Loading...")}</p>}
{post && (
<>
Expand Down Expand Up @@ -2263,7 +2265,7 @@ function PostDetailPopup({
)}
</>
) : summaryError ? (
<p className="error">{summaryError}</p>
<p className="error" role="alert">{summaryError}</p>
) : (
<p className="popup-placeholder">{t("No summary is available for this record yet.")}</p>
)}
Expand Down Expand Up @@ -2835,7 +2837,7 @@ function AnalysisRunsPanel({
}
}

if (error && runs === null) return <p className="error">{error}</p>;
if (error && runs === null) return <p className="error" role="alert">{error}</p>;
if (runs === null) return <p>Loading analysis runs...</p>;

const corpusHint = selected ? analysisRunCorpusHint(selected) : null;
Expand Down Expand Up @@ -2864,7 +2866,7 @@ function AnalysisRunsPanel({
{requestLabel}
</button>
</div>
{(error || entitiesLoadError) && <p className="error">{error ?? entitiesLoadError}</p>}
{(error || entitiesLoadError) && <p className="error" role="alert">{error ?? entitiesLoadError}</p>}
{runs.length === 0 ? (
<p className="popup-placeholder">
No analysis runs visible to this account yet. Request a lineage
Expand Down Expand Up @@ -3086,7 +3088,7 @@ function RankingsPanel({
</span>
)}
</div>
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{ranking === null && !error && <p>Loading rankings...</p>}
{ranking && ranking.status === "unavailable" && (
<p className="popup-placeholder">Rankings · RankWeave not available</p>
Expand Down Expand Up @@ -3131,7 +3133,7 @@ function CalendarPanel({
.catch((err) => setError(String(err)));
}, [accessToken]);

if (error) return <p className="error">{error}</p>;
if (error) return <p className="error" role="alert">{error}</p>;
if (calendar === null) return <p>{t("Loading calendar...")}</p>;

const events = calendar.events ?? [];
Expand Down Expand Up @@ -3560,7 +3562,7 @@ function ReportsPanel({
))}
</ul>
)}
{error && <p className="error">{error}</p>}
{error && <p className="error" role="alert">{error}</p>}
{!openedGroupingLabel && reportList}
</section>
);
Expand Down Expand Up @@ -4025,7 +4027,7 @@ function PostList({
{rebuilding ? t("Rebuilding...") : t("Rebuild lineage")}
</button>
</div>
{rebuildError && <p className="error">{rebuildError}</p>}
{rebuildError && <p className="error" role="alert">{rebuildError}</p>}
</section>
)}
<CalendarPanel accessToken={accessToken} onSelectPost={selectPost} />
Expand Down Expand Up @@ -4297,7 +4299,7 @@ function CustomerMasterPanel({
<p className="section-eyebrow">{t("Authorized customer scope")}</p>
<h2 id="customer-master-heading">{t("Customer master")}</h2>
<p className="buyer-destination-intro">{t("Customer entities available to this account.")}</p>
{error ? <p className="error">{error}</p> : null}
{error ? <p className="error" role="alert">{error}</p> : null}
{master === null && !error ? <p>{t("Loading customer master...")}</p> : null}
{master?.corporate_entities.length === 0 ? (
<p className="popup-placeholder">{t("No customer entities are connected to this account.")}</p>
Expand Down Expand Up @@ -4355,7 +4357,7 @@ function CustomerMasterPanel({
})}
</p>
)}
{resolveError ? <p className="error">{resolveError}</p> : null}
{resolveError ? <p className="error" role="alert">{resolveError}</p> : null}
<ul className="customer-master-list">
{master.source_customer_hints.slice(0, HINT_RENDER_LIMIT).map((hint) => (
<li key={`${hint.customer_code ?? "name"}:${hint.customer_name ?? "unknown"}`}>
Expand Down Expand Up @@ -4498,7 +4500,7 @@ function AskAgentPanel({
<p className="section-eyebrow">{t("Evidence-grounded questions")}</p>
<h2 id="ask-agent-heading">{t("Ask Agent")}</h2>
<p className="buyer-destination-intro">{t("Questions use authorized posts and their evidence.")}</p>
{error ? <p className="error">{error}</p> : null}
{error ? <p className="error" role="alert">{error}</p> : null}
<label className="ask-agent-source">
<span>{t("Ask a question")}</span>
<textarea
Expand Down Expand Up @@ -4638,7 +4640,7 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
}

if (!accessToken) {
return <p className="error">{t("Authenticated, but no access token was returned.")}</p>;
return <p className="error" role="alert">{t("Authenticated, but no access token was returned.")}</p>;
}

return (
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/components/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ export function AdminPanel({ currentBrandName, onBrandNameChange, accessToken }:
<button type="submit" className="btn-primary" disabled={saving || !draftName.trim() || draftName === currentBrandName}>
{saving ? t("Saving...") : t("Save settings")}
</button>
{saved && <span style={{ marginLeft: "1rem", color: "green" }}>{t("Settings saved!")}</span>}
{error && <span style={{ marginLeft: "1rem", color: "red" }}>{t(error)}</span>}
{saved && (
<span role="status" style={{ marginLeft: "1rem", color: "green" }}>
{t("Settings saved!")}
</span>
)}
{error && (
<span role="alert" style={{ marginLeft: "1rem", color: "red" }}>
{t(error)}
</span>
)}
</div>
</form>
</div>
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
LOCALE_LABELS,
SUPPORTED_LOCALES,
getLocale,
localeKeys,
setLocale,
t,
tf,
Expand Down Expand Up @@ -76,6 +77,22 @@ describe("i18n", () => {
});
});

describe("locale key parity", () => {
it("registers the same translation keys in every non-English locale block", () => {
// A key present in one locale but silently missing from another falls
// back to the raw English key text (or an untranslated aria-label) at
// runtime with no build-time signal -- this caught exactly that for
// "Affiliates of {name}" (2026-08-23).
const locales = ["ko", "zh", "ja", "vi"] as const;
const keySets = locales.map((locale) => new Set(localeKeys(locale)));
const union = new Set(keySets.flatMap((set) => [...set]));
for (const [index, locale] of locales.entries()) {
const missing = [...union].filter((key) => !keySets[index].has(key));
expect(missing, `${locale} is missing keys: ${missing.join(", ")}`).toEqual([]);
}
});
});
Comment on lines +80 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Parity test does not cover English keys

The new parity test unions ko/zh/ja/vi keys and requires each of those four to contain all of them; the four blocks are currently identical, so it passes. English has no TRANSLATIONS block (fallback is the key text), so no locale is checked against the actual set of keys t()/tf() is called with in code.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


describe("locale-aware source labels", () => {
it.each([
["en", "Voice of Customer", "Public"],
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"No tickets yet.": "아직 티켓이 없습니다.",
"New ticket title": "새 티켓 제목",
"Due date": "기한",
"Affiliates of {name}": "{name}의 계열사",
"Create ticket": "티켓 작성",
"Loading activity...": "활동을 불러오는 중...",
"No activity yet.": "아직 활동이 없습니다.",
Expand Down Expand Up @@ -580,6 +581,7 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"No tickets yet.": "暂时没有工单。",
"New ticket title": "新工单标题",
"Due date": "截止日期",
"Affiliates of {name}": "{name}的关联公司",
"Create ticket": "创建工单",
"Loading activity...": "正在加载活动...",
"No activity yet.": "暂时没有活动。",
Expand Down Expand Up @@ -933,6 +935,7 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"No tickets yet.": "チケットはまだありません。",
"New ticket title": "新しいチケットのタイトル",
"Due date": "期限",
"Affiliates of {name}": "{name}の関連会社",
"Create ticket": "チケットを作成",
"Loading activity...": "アクティビティを読み込んでいます...",
"No activity yet.": "アクティビティはまだありません。",
Expand Down Expand Up @@ -1271,6 +1274,7 @@ const TRANSLATIONS: Partial<Record<Locale, Record<string, string>>> = {
"No tickets yet.": "Chưa có phiếu công việc.",
"New ticket title": "Tiêu đề phiếu mới",
"Due date": "Hạn hoàn thành",
"Affiliates of {name}": "Công ty liên kết của {name}",
"Create ticket": "Tạo phiếu công việc",
"Loading activity...": "Đang tải hoạt động...",
"No activity yet.": "Chưa có hoạt động.",
Expand Down Expand Up @@ -1404,6 +1408,11 @@ export function getLocale(): Locale {
return currentLocale;
}

/** Registered translation keys for one locale block -- for cross-locale parity checks. */
export function localeKeys(locale: Locale): string[] {
return Object.keys(TRANSLATIONS[locale] ?? {});
}

export function setLocale(locale: Locale): void {
if (!SUPPORTED_LOCALES.includes(locale) || locale === currentLocale) return;
currentLocale = locale;
Expand Down