feat(frontend): English UI + 'Datamart Builder with Human In The Loop' tagline

The interface is now fully English regardless of the chat language (session
titles stay in whatever language the user typed). Changed the right-rail
subtitle to 'Datamart Builder with Human In The Loop'. Translated all chrome:
new-question/settings dialogs, workflow phase strip (F1-F8), session rail,
steer input, widgets (select/multiselect/freetext/fallback/reserved controls)
and viewers (SQL, results, schema linking). Test matchers updated in lockstep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 18:27:11 +02:00
co-authored by Claude Opus 4.8
parent acb674b4bc
commit bcaf6708de
27 changed files with 98 additions and 97 deletions
+6 -6
View File
@@ -48,8 +48,8 @@ test("(a) renders grid cells for a 2×2 preview result", async () => {
// Grid must be present, not bold scalar
expect(screen.queryByTestId("scalar-value")).not.toBeInTheDocument();
// truncated:true → show the "risultati troncati" indicator
expect(screen.getByText(/risultati troncati/i)).toBeInTheDocument();
// truncated:true → show the "results truncated" indicator
expect(screen.getByText(/results truncated/i)).toBeInTheDocument();
});
// (b) 1×1 preview → bold scalar shown, no grid
@@ -105,7 +105,7 @@ test("(c) Esporta CSV button calls the export endpoint", async () => {
// Wait for preview to load (use role to avoid ambiguity with paging panel)
await waitFor(() => expect(screen.getAllByRole("gridcell")).not.toHaveLength(0));
const btn = screen.getByRole("button", { name: /esporta csv/i });
const btn = screen.getByRole("button", { name: /export csv/i });
await user.click(btn);
await waitFor(() => expect(exportCalled).toBe(true));
@@ -136,9 +136,9 @@ test("(d) selecting tutti re-fetches preview with the large limit", async () =>
// Initial fetch uses limit 10
await waitFor(() => expect(limits).toContain(10));
// Select "tutti"
const select = screen.getByLabelText(/righe da visualizzare/i);
await user.selectOptions(select, "tutti");
// Select "all"
const select = screen.getByLabelText(/rows to display/i);
await user.selectOptions(select, "all");
// A re-fetch with the large/unlimited limit must occur
await waitFor(() => expect(limits.some((l) => (l as number) > 10)).toBe(true));
+7 -7
View File
@@ -47,20 +47,20 @@ export function ResultsPanel({ sessionId }: Props) {
<select
value={limit}
onChange={(e) =>
setLimit(e.target.value === "tutti" ? 1_000_000 : Number(e.target.value))
setLimit(e.target.value === "all" ? 1_000_000 : Number(e.target.value))
}
aria-label="Righe da visualizzare"
aria-label="Rows to display"
>
<option value={10}>10</option>
<option value="tutti">tutti</option>
<option value="all">all</option>
</select>
<button onClick={handleExport}>Esporta CSV</button>
<button onClick={handleExport}>Export CSV</button>
</div>
{/* Loading / error states */}
{isLoading && <div>Caricamento...</div>}
{error && <div>Errore: {String(error)}</div>}
{isLoading && <div>Loading…</div>}
{error && <div>Error: {String(error)}</div>}
{/* Scalar result: 1 col × 1 row → bold value */}
{data && isScalar && (
@@ -73,7 +73,7 @@ export function ResultsPanel({ sessionId }: Props) {
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
<AgGridReact columnDefs={columnDefs} rowData={rowData} />
</div>
{data.truncated && <div>(risultati troncati)</div>}
{data.truncated && <div>(results truncated)</div>}
</>
)}
</div>
@@ -42,7 +42,7 @@ test("(b) clicking toggle switches to table listing a promoted table name", asyn
// wait for mermaid to load
await waitFor(() => expect(screen.getByTestId("mm")).toBeInTheDocument());
const toggle = screen.getByRole("button", { name: /tabella/i });
const toggle = screen.getByRole("button", { name: /table/i });
await userEvent.click(toggle);
expect(screen.getByText("orders")).toBeInTheDocument();
@@ -52,7 +52,7 @@ test("(b) clicking toggle switches to table listing a promoted table name", asyn
test("(c) oversized linking (>45) shows cap notice", () => {
render(<SchemaLinkingViewer linking={bigLinking} />);
expect(
screen.getByText(/troppi elementi per il grafico/i)
screen.getByText(/too many elements for the graph/i)
).toBeInTheDocument();
});
+9 -9
View File
@@ -109,19 +109,19 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
disabled={oversized}
className={view === "chart" ? "font-bold" : ""}
>
Grafico
Graph
</button>
<button
onClick={() => setView("table")}
className={view === "table" ? "font-bold" : ""}
>
Tabella
Table
</button>
</div>
{oversized && (
<p className="text-sm text-amber-600">
Troppi elementi per il grafico, usa la tabella
Too many elements for the graph, use the table
</p>
)}
@@ -137,9 +137,9 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<table className="w-full text-sm border-collapse">
<thead>
<tr>
<th className="text-left border-b py-1">Nome</th>
<th className="text-left border-b py-1">Tipo</th>
<th className="text-left border-b py-1">Perché</th>
<th className="text-left border-b py-1">Name</th>
<th className="text-left border-b py-1">Type</th>
<th className="text-left border-b py-1">Why</th>
</tr>
</thead>
<tbody>
@@ -147,7 +147,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<React.Fragment key={table.name}>
<tr className="font-semibold bg-muted/30">
<td className="py-1 pl-1">{table.name}</td>
<td>tabella</td>
<td>table</td>
<td className="text-xs text-muted-foreground">
{reasonFor(table)}
</td>
@@ -157,7 +157,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<td className="py-0.5 pl-4 text-muted-foreground">
{col.name}
</td>
<td>colonna</td>
<td>column</td>
<td className="text-xs text-muted-foreground">
{reasonFor(col)}
</td>
@@ -170,7 +170,7 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<td className="py-0.5 pl-1 text-muted-foreground">
{col.name}
</td>
<td>colonna</td>
<td>column</td>
<td className="text-xs text-muted-foreground">
{reasonFor(col)}
</td>
+3 -3
View File
@@ -71,16 +71,16 @@ test("(c) layout toggle flips its label between Orizzontale and Verticale", asyn
await waitFor(() => expect(screen.getAllByTestId("hl")).toHaveLength(2));
// Default layout is vertical, so the toggle offers "Orizzontale".
const toggle = screen.getByRole("button", { name: /orizzontale/i });
const toggle = screen.getByRole("button", { name: /horizontal/i });
expect(toggle).toBeInTheDocument();
await userEvent.click(toggle);
// After switching to horizontal, the toggle now offers "Verticale".
expect(
screen.getByRole("button", { name: /verticale/i })
screen.getByRole("button", { name: /vertical/i })
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /orizzontale/i })
screen.queryByRole("button", { name: /horizontal/i })
).not.toBeInTheDocument();
});
+1 -1
View File
@@ -101,7 +101,7 @@ export function SqlViewer({ blocks }: { blocks: SqlBlock[] }) {
setLayout((l) => (l === "vertical" ? "horizontal" : "vertical"))
}
>
{layout === "vertical" ? "Orizzontale" : "Verticale"}
{layout === "vertical" ? "Horizontal" : "Vertical"}
</button>
</div>