Translate remaining frontend

This commit is contained in:
2026-07-22 23:25:53 +02:00
parent 7257deacdc
commit e4fd7e0df6
11 changed files with 79 additions and 53 deletions
@@ -16,6 +16,12 @@ The editor should support both:
- low-click mouse interaction
- drag-and-drop workflows
## Interface Language
All user-facing labels, descriptions, messages and actions are displayed in German.
Code, API fields and stored technical values keep their English domain names. Technical values are translated only for display so persisted data remains stable.
## Cell Editing
Cells are displayed as static text by default.
@@ -25,7 +25,7 @@ export default function CircuitTreeEditPage() {
Zum Projekt
</Link>
<Link href={`/projects/${params.projectId}/circuit-lists`} className="btn btn-outline-secondary btn-sm">
Legacy-Editor
Bisherige Listenansicht
</Link>
</div>
</div>
@@ -13,22 +13,22 @@ export default function CircuitTreePreviewPage() {
<main className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h1 className="h4 mb-1">Circuit Tree Preview</h1>
<h1 className="h4 mb-1">Vorschau der Stromkreisstruktur</h1>
<p className="text-secondary mb-0">
Read-only preview of section blocks, circuits and device rows.
Schreibgeschützte Vorschau der Bereiche, Stromkreise und Gerätezeilen.
</p>
</div>
<Link
href={`/projects/${projectId}/circuit-lists/${circuitListId}/tree-edit`}
className="btn btn-outline-primary btn-sm"
>
Open editor
Editor öffnen
</Link>
<Link
href={`/projects/${projectId}/circuit-lists`}
className="btn btn-outline-secondary btn-sm"
>
Back to legacy editor
Zurück zur bisherigen Listenansicht
</Link>
</div>
@@ -128,7 +128,7 @@ const allColumns: Array<{ key: ColumnKey; label: string }> = [
{ key: "description", label: "Beschreibung" },
{ key: "name", label: "Verbraucher" },
{ key: "projectDevice", label: "Projektgerät" },
{ key: "deviceLink", label: "Link" },
{ key: "deviceLink", label: "Verknüpfung" },
{ key: "room", label: "Raum" },
{ key: "floor", label: "Etage" },
{ key: "category", label: "Kategorie" },
@@ -646,7 +646,7 @@ export default function CircuitListsPage() {
await reloadConsumers();
setBulkEditForms((current) => ({ ...current, [slotIndex]: { ...defaultBulkEditState } }));
} catch (err) {
setError(err instanceof Error ? err.message : "Bulk-Änderung fehlgeschlagen.");
setError(err instanceof Error ? err.message : "Sammeländerung fehlgeschlagen.");
} finally {
setIsSaving(false);
}
+2 -2
View File
@@ -611,7 +611,7 @@ export default function ProjectDetailPage() {
className="btn btn-sm btn-outline-secondary"
href={`/projects/${projectId}/circuit-lists?boardId=${board.id}`}
>
Legacy
Bisherige Ansicht
</Link>
</div>
</td>
@@ -1084,7 +1084,7 @@ export default function ProjectDetailPage() {
{syncPreview.rows.length === 0 ? (
<tr>
<td colSpan={4} className="text-center text-secondary py-3">
Keine verknüpften Circuit-First-Gerätezeilen vorhanden.
Keine verknüpften Gerätezeilen im neuen Stromkreismodell vorhanden.
</td>
</tr>
) : null}
@@ -142,12 +142,22 @@ function normalizeUiError(err: unknown): string {
if (message.includes("Duplicate equipmentIdentifier")) {
return "Das Betriebsmittelkennzeichen ist in dieser Stromkreisliste bereits vorhanden.";
}
if (message.includes("Invalid number")) {
if (message.includes("Invalid number") || message.includes("Ungültiger Zahlenwert")) {
return "Bitte einen gültigen Zahlenwert eingeben.";
}
return message;
}
function formatPhaseTypeLabel(value: string): string {
if (value === "three_phase") {
return "Dreiphasig";
}
if (value === "single_phase") {
return "Einphasig";
}
return value;
}
function isPrintableKey(event: KeyboardEvent<HTMLElement>) {
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
}
@@ -2587,7 +2597,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
>
<strong>{device.displayName || device.name}</strong>
<span>Name: {device.name}</span>
<span>Phase: {device.phaseType}</span>
<span>Phasenart: {formatPhaseTypeLabel(device.phaseType)}</span>
<span>Anzahl: {device.quantity}</span>
<span>Leistung/Gerät: {device.powerPerUnit}</span>
<span>Gleichzeitigkeit: {device.simultaneityFactor}</span>
@@ -15,12 +15,22 @@ function formatNumber(value: number | undefined, digits = 2) {
}).format(value);
}
function formatPhaseType(value: string | undefined) {
if (value === "three_phase") {
return "Dreiphasig";
}
if (value === "single_phase") {
return "Einphasig";
}
return value ?? "-";
}
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
if (circuit.displayName?.trim()) {
return circuit.displayName;
}
if (circuit.deviceRows.length > 1) {
return `${circuit.deviceRows.length} devices`;
return `${circuit.deviceRows.length} Geräte`;
}
return "Reserve";
}
@@ -37,13 +47,13 @@ export function CircuitTreePreview(props: { projectId: string; circuitListId: st
getCircuitTree(projectId, circuitListId)
.then(setData)
.catch((err: unknown) =>
setError(err instanceof Error ? err.message : "Circuit tree could not be loaded.")
setError(err instanceof Error ? err.message : "Die Stromkreisstruktur konnte nicht geladen werden.")
)
.finally(() => setIsLoading(false));
}, [projectId, circuitListId]);
if (isLoading) {
return <div className="alert alert-info">Loading circuit tree...</div>;
return <div className="alert alert-info">Stromkreisstruktur wird geladen </div>;
}
if (error) {
@@ -51,12 +61,12 @@ export function CircuitTreePreview(props: { projectId: string; circuitListId: st
}
if (!data || !data.sections.length) {
return <div className="alert alert-secondary">No sections or circuits available.</div>;
return <div className="alert alert-secondary">Keine Bereiche oder Stromkreise vorhanden.</div>;
}
const hasAnyCircuits = data.sections.some((section) => section.circuits.length > 0);
if (!hasAnyCircuits) {
return <div className="alert alert-secondary">Sections exist, but no circuits were found yet.</div>;
return <div className="alert alert-secondary">Bereiche sind vorhanden, enthalten aber noch keine Stromkreise.</div>;
}
return (
@@ -66,28 +76,28 @@ export function CircuitTreePreview(props: { projectId: string; circuitListId: st
<table className="table table-sm align-middle circuit-tree-table">
<thead>
<tr>
<th>Equipment identifier</th>
<th>Display name</th>
<th>Phase type</th>
<th>Connection kind</th>
<th>Cost group</th>
<th>Category</th>
<th>Level</th>
<th>Room number</th>
<th>Room name</th>
<th className="text-end">Quantity</th>
<th className="text-end">Power / unit</th>
<th className="text-end">Simultaneity</th>
<th className="text-end">cosPhi</th>
<th className="text-end">Row total</th>
<th className="text-end">Circuit total</th>
<th>Protection type</th>
<th className="text-end">Protection current</th>
<th>Protection characteristic</th>
<th>Cable type</th>
<th>Cable cross-section</th>
<th className="text-end">Cable length</th>
<th>Remark</th>
<th>Betriebsmittelkennzeichen</th>
<th>Anzeigename</th>
<th>Phasenart</th>
<th>Anschlussart</th>
<th>Kostengruppe</th>
<th>Kategorie</th>
<th>Ebene</th>
<th>Raumnummer</th>
<th>Raumname</th>
<th className="text-end">Anzahl</th>
<th className="text-end">Leistung / Gerät</th>
<th className="text-end">Gleichzeitigkeit</th>
<th className="text-end">cos φ</th>
<th className="text-end">Zeilensumme</th>
<th className="text-end">Stromkreissumme</th>
<th>Schutzart</th>
<th className="text-end">Bemessungsstrom</th>
<th>Charakteristik</th>
<th>Kabeltyp</th>
<th>Kabelquerschnitt</th>
<th className="text-end">Kabellänge</th>
<th>Bemerkung</th>
</tr>
</thead>
<tbody>
@@ -136,7 +146,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={circuit.id} className={circuit.isReserve ? "reserve-row" : ""}>
<td>{circuit.equipmentIdentifier}</td>
<td>{row.displayName || row.name}</td>
<td>{row.phaseType ?? "-"}</td>
<td>{formatPhaseType(row.phaseType)}</td>
<td>{row.connectionKind ?? "-"}</td>
<td>{row.costGroup ?? "-"}</td>
<td>{row.category ?? "-"}</td>
@@ -179,7 +189,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={row.id} className="device-row">
<td className="text-muted"> </td>
<td className="indented-cell">{row.displayName || row.name}</td>
<td>{row.phaseType ?? "-"}</td>
<td>{formatPhaseType(row.phaseType)}</td>
<td>{row.connectionKind ?? "-"}</td>
<td>{row.costGroup ?? "-"}</td>
<td>{row.category ?? "-"}</td>
@@ -206,7 +216,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
})}
<tr className="placeholder-row">
<td>-frei-</td>
<td colSpan={21}>free placeholder</td>
<td colSpan={21}>Freie Zeile</td>
</tr>
</>
);
@@ -321,7 +321,7 @@ export function PowerBalanceWorkspace() {
setEditConsumerForm(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht geloescht werden.");
setError(err instanceof Error ? err.message : "Verbraucher konnte nicht gelöscht werden.");
} finally {
setIsSaving(false);
}
@@ -424,7 +424,7 @@ export function PowerBalanceWorkspace() {
<form className="consumerForm" onSubmit={handleCreateConsumer}>
<label>
Verbraucher
<input value={consumerForm.name} onChange={(event) => updateConsumerForm("name", event.target.value)} placeholder="z. B. Steckdosen Buero" />
<input value={consumerForm.name} onChange={(event) => updateConsumerForm("name", event.target.value)} placeholder="z. B. Steckdosen Büro" />
</label>
<label>
Kategorie
@@ -435,7 +435,7 @@ export function PowerBalanceWorkspace() {
<input min="0" type="number" value={consumerForm.quantity} onChange={(event) => updateConsumerForm("quantity", event.target.value)} />
</label>
<label>
Leistung je Stueck [kW]
Leistung je Stück [kW]
<input min="0" step="0.01" type="number" value={consumerForm.installedPowerPerUnitKw} onChange={(event) => updateConsumerForm("installedPowerPerUnitKw", event.target.value)} />
</label>
<label>
@@ -463,7 +463,7 @@ export function PowerBalanceWorkspace() {
</label>
<button className="primaryButton" type="submit" disabled={!selectedProjectId || isSaving}>
<Plus size={17} />
Hinzufuegen
Hinzufügen
</button>
</form>
</section>
@@ -472,12 +472,12 @@ export function PowerBalanceWorkspace() {
<div className="tableHeader">
<div>
<p className="eyebrow">Aktuelles Projekt</p>
<h2>{selectedProject?.name || "Noch kein Projekt ausgewaehlt"}</h2>
<h2>{selectedProject?.name || "Noch kein Projekt ausgewählt"}</h2>
<p className="subline">{selectedBoard ? `Verteilung: ${selectedBoard.name}` : "Alle Verteilungen"}</p>
</div>
<div className="statusPill">
<Activity size={16} />
{isLoading ? "Laedt" : "Bereit"}
{isLoading ? "Lädt" : "Bereit"}
</div>
</div>
@@ -505,7 +505,7 @@ export function PowerBalanceWorkspace() {
{!totalsByBoard.length ? (
<tr>
<td colSpan={4} className="emptyState">
Noch keine Verbraucher fuer eine Summenbildung vorhanden.
Noch keine Verbraucher für eine Summenbildung vorhanden.
</td>
</tr>
) : null}
@@ -528,7 +528,7 @@ export function PowerBalanceWorkspace() {
<th>Verteilung</th>
<th>Kategorie</th>
<th>Anzahl</th>
<th>Leistung je Stueck [kW]</th>
<th>Leistung je Stück [kW]</th>
<th>Installierte Leistung [kW]</th>
<th>GZF</th>
<th>Berechnete Leistung [kW]</th>
@@ -642,7 +642,7 @@ export function PowerBalanceWorkspace() {
<Pencil size={14} />
</button>
)}
<button className="iconButton small danger" type="button" title="Loeschen" onClick={() => handleDeleteConsumer(consumer.id)} disabled={isSaving}>
<button className="iconButton small danger" type="button" title="Löschen" onClick={() => handleDeleteConsumer(consumer.id)} disabled={isSaving}>
<Trash2 size={14} />
</button>
</div>
+1 -1
View File
@@ -37,7 +37,7 @@ async function request<T>(url: string, init?: RequestInit): Promise<T> {
if (!response.ok) {
const details = await response.text();
throw new Error(details || `Request failed with ${response.status}`);
throw new Error(details || `Anfrage fehlgeschlagen (Status ${response.status})`);
}
if (response.status === 204) {
+1 -1
View File
@@ -184,7 +184,7 @@ export function parseNumeric(cellKey: CellKey, draft: string): number | undefine
}
const parsed = Number(trimmed);
if (Number.isNaN(parsed)) {
throw new Error(`Invalid number in ${cellKey}`);
throw new Error(`Ungültiger Zahlenwert in ${cellKey}`);
}
return parsed;
}
+1 -1
View File
@@ -83,6 +83,6 @@ describe("circuit grid model", () => {
it("parses numeric drafts and rejects invalid values", () => {
assert.equal(parseNumeric("quantity", " 2.5 "), 2.5);
assert.equal(parseNumeric("quantity", ""), undefined);
assert.throws(() => parseNumeric("quantity", "two"), /Invalid number/);
assert.throws(() => parseNumeric("quantity", "two"), /Ungültiger Zahlenwert/);
});
});