From 449075deaf36c727bbe84d7211105453e43a8290 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 20 Apr 2026 17:06:21 +0200 Subject: [PATCH] fix(i18n): preserve default HTML text when key missing + add all projekte.* keys Root cause: applyTranslations() in client/i18n.ts unconditionally overwrote textContent/placeholder/title with t(key), and t() falls back to the raw key name when no translation exists. Result: every projekte.* data-i18n attr in the v2 pages rendered the literal key string ('projekte.heading', 'projekte.subtitle', ...) because I shipped the pages with new i18n keys without adding the translations. Two fixes, both in client/i18n.ts: 1. **Fallback behaviour**: applyTranslations() now uses a new internal tOrEmpty(key) that returns '' when the key is missing in DE and EN, and the call site only overwrites the DOM when the lookup yielded a real value. Missing keys no longer clobber the author-provided default text. This is belt-and-braces for any future page that ships a key before its translation does. 2. **Missing translations added**: ~90 projekte.* keys for DE and EN, covering the list page (projekte.heading/subtitle/new/search/filter.*/ view.*/col.*/empty.*/unavailable), the create form (projekte.neu.*, projekte.field.*, projekte.cancel/submit/error.*), and the detail page (projekte.detail.title/back/loading/notfound/edit/save, tab.* for all eight tabs, verlauf.*, team.form.*/col.*, kinder.*, parteien.* form/role/col/empty, fristen.*, termine.*, checklisten.*, delete.*). go build/vet/test + bun run build all clean. --- frontend/src/client/i18n.ts | 231 +++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 3 deletions(-) diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 85847c5..ef13e99 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -728,6 +728,113 @@ const translations: Record> = { "dezernat.add": "Hinzuf\u00fcgen", "dezernat.remove": "Entfernen", "dezernat.confirm_remove": "Mitglied entfernen?", + "projekte.title": "Projekte \u2014 Paliad", + "projekte.heading": "Projekte", + "projekte.subtitle": "Mandanten, Streitsachen, Patente und F\u00e4lle \u2014 hierarchisch organisiert.", + "projekte.new": "Neues Projekt", + "projekte.search.placeholder": "Titel, Referenz oder ClientMatter...", + "projekte.filter.type": "Typ", + "projekte.filter.type.all": "Alle Typen", + "projekte.filter.status": "Status", + "projekte.filter.status.all": "Alle Status", + "projekte.filter.status.active": "Aktiv", + "projekte.filter.status.archived": "Archiviert", + "projekte.filter.status.closed": "Abgeschlossen", + "projekte.filter.view": "Ansicht", + "projekte.view.flat": "Flache Liste", + "projekte.view.roots": "Nur Wurzeln", + "projekte.unavailable": "Projektverwaltung zurzeit nicht verf\u00fcgbar \u2014 bitte Administrator kontaktieren.", + "projekte.col.title": "Titel", + "projekte.col.type": "Typ", + "projekte.col.reference": "Referenz", + "projekte.col.clientmatter": "ClientMatter", + "projekte.col.status": "Status", + "projekte.col.updated": "Zuletzt ge\u00e4ndert", + "projekte.empty.title": "Noch kein Projekt angelegt", + "projekte.empty.hint": "Starten Sie \u00fcber \u201eNeues Projekt\u201c \u2014 legen Sie zuerst einen Mandanten an, darunter Streitsachen, Patente und F\u00e4lle.", + "projekte.empty.filtered": "Keine Treffer f\u00fcr diese Filter.", + "projekte.cancel": "Abbrechen", + "projekte.submit": "Projekt anlegen", + "projekte.neu.title": "Neues Projekt \u2014 Paliad", + "projekte.neu.heading": "Neues Projekt anlegen", + "projekte.neu.subtitle": "Mandant, Streitsache, Patent, Verfahren oder generisches Projekt \u2014 hierarchisch einordnen. Sichtbarkeit folgt dem Team (Sie werden als \u201eLead\u201c automatisch hinzugef\u00fcgt).", + "projekte.field.type": "Typ", + "projekte.field.parent": "\u00dcbergeordnetes Projekt", + "projekte.field.parent.placeholder": "Titel eingeben, um ein \u00dcberprojekt zu suchen...", + "projekte.field.parent.hint": "Leer lassen f\u00fcr ein Wurzel-Projekt (typisch: Mandant).", + "projekte.field.title": "Titel", + "projekte.field.title.placeholder": "z.B. Siemens AG | Siemens v. Huawei | EP 1 234 567", + "projekte.field.reference": "Interne Referenz (optional)", + "projekte.field.reference.placeholder": "z.B. HL-2026-0042", + "projekte.field.client_number": "Client-Nr. (7 Ziffern)", + "projekte.field.matter_number": "Matter-Nr. (7 Ziffern)", + "projekte.field.clientmatter.hint": "HLC-Billing-Nummern. Format CCCCCCC.MMMMMMM. Client-Nr. wird an Unterprojekte vererbt (\u00fcberschreibbar).", + "projekte.field.netdocuments_url": "netDocuments-URL (optional)", + "projekte.field.industry": "Branche", + "projekte.field.country": "Land (ISO-2)", + "projekte.field.billing_reference": "Billing-Referenz", + "projekte.field.patent_number": "Patentnummer", + "projekte.field.filing_date": "Anmeldetag", + "projekte.field.grant_date": "Erteilungstag", + "projekte.field.court": "Gericht", + "projekte.field.case_number": "Aktenzeichen (Gericht)", + "projekte.field.status": "Status", + "projekte.error.title_required": "Titel erforderlich", + "projekte.detail.title": "Projekt \u2014 Paliad", + "projekte.detail.back": "\u2190 Zur\u00fcck zur \u00dcbersicht", + "projekte.detail.loading": "L\u00e4dt\u2026", + "projekte.detail.notfound": "Projekt nicht gefunden oder keine Berechtigung.", + "projekte.detail.edit": "Bearbeiten", + "projekte.detail.save": "Speichern", + "projekte.detail.tab.verlauf": "Verlauf", + "projekte.detail.tab.team": "Team", + "projekte.detail.tab.kinder": "Untergeordnet", + "projekte.detail.tab.parteien": "Parteien", + "projekte.detail.tab.fristen": "Fristen", + "projekte.detail.tab.termine": "Termine", + "projekte.detail.tab.notizen": "Notizen", + "projekte.detail.tab.checklisten": "Checklisten", + "projekte.detail.verlauf.empty": "Noch keine Ereignisse aufgezeichnet.", + "projekte.detail.verlauf.loadMore": "Mehr laden", + "projekte.detail.team.form.user": "Benutzer", + "projekte.detail.team.form.role": "Rolle", + "projekte.detail.team.form.cancel": "Abbrechen", + "projekte.detail.team.form.submit": "Hinzuf\u00fcgen", + "projekte.detail.team.col.name": "Name", + "projekte.detail.team.col.role": "Rolle", + "projekte.detail.team.col.source": "Herkunft", + "projekte.detail.kinder.add": "Untervorhaben anlegen", + "projekte.detail.kinder.empty": "Keine untergeordneten Projekte.", + "projekte.detail.parteien.add": "Partei hinzuf\u00fcgen", + "projekte.detail.parteien.form.name": "Name", + "projekte.detail.parteien.form.role": "Rolle", + "projekte.detail.parteien.form.rep": "Vertreter (optional)", + "projekte.detail.parteien.form.cancel": "Abbrechen", + "projekte.detail.parteien.form.submit": "Hinzuf\u00fcgen", + "projekte.detail.parteien.role.claimant": "Kl\u00e4ger", + "projekte.detail.parteien.role.defendant": "Beklagter", + "projekte.detail.parteien.role.thirdparty": "Streitverk\u00fcndeter / Drittpartei", + "projekte.detail.parteien.col.name": "Name", + "projekte.detail.parteien.col.role": "Rolle", + "projekte.detail.parteien.col.rep": "Vertreter", + "projekte.detail.parteien.empty": "Noch keine Parteien eingetragen.", + "projekte.detail.fristen.add": "Frist hinzuf\u00fcgen", + "projekte.detail.fristen.empty": "F\u00fcr dieses Projekt sind noch keine Fristen erfasst.", + "projekte.detail.termine.add": "Termin hinzuf\u00fcgen", + "projekte.detail.termine.form.cancel": "Abbrechen", + "projekte.detail.termine.form.submit": "Hinzuf\u00fcgen", + "projekte.detail.termine.empty": "F\u00fcr dieses Projekt sind noch keine Termine erfasst.", + "projekte.detail.checklisten.empty": "F\u00fcr dieses Projekt sind noch keine Checklisten-Instanzen erfasst.", + "projekte.detail.checklisten.col.template": "Vorlage", + "projekte.detail.checklisten.col.name": "Name", + "projekte.detail.checklisten.col.progress": "Fortschritt", + "projekte.detail.checklisten.col.created": "Angelegt", + "projekte.detail.checklisten.hint": "Instanzen werden auf der Vorlagen-Seite unter Checklisten angelegt.", + "projekte.detail.delete": "Projekt archivieren", + "projekte.detail.delete.confirm.title": "Projekt wirklich archivieren?", + "projekte.detail.delete.confirm.body": "Das Projekt wird archiviert. Es kann nicht direkt wiederhergestellt werden.", + "projekte.detail.delete.confirm.cancel": "Abbrechen", + "projekte.detail.delete.confirm.ok": "Archivieren", "projekte.type.client": "Mandant", "projekte.type.litigation": "Streitsache", "projekte.type.patent": "Patent", @@ -1622,6 +1729,113 @@ const translations: Record> = { "dezernat.add": "Add", "dezernat.remove": "Remove", "dezernat.confirm_remove": "Remove member?", + "projekte.title": "Projects \u2014 Paliad", + "projekte.heading": "Projects", + "projekte.subtitle": "Clients, litigations, patents and cases \u2014 organised hierarchically.", + "projekte.new": "New project", + "projekte.search.placeholder": "Title, reference or ClientMatter...", + "projekte.filter.type": "Type", + "projekte.filter.type.all": "All types", + "projekte.filter.status": "Status", + "projekte.filter.status.all": "All statuses", + "projekte.filter.status.active": "Active", + "projekte.filter.status.archived": "Archived", + "projekte.filter.status.closed": "Closed", + "projekte.filter.view": "View", + "projekte.view.flat": "Flat list", + "projekte.view.roots": "Roots only", + "projekte.unavailable": "Project management is currently unavailable \u2014 please contact an administrator.", + "projekte.col.title": "Title", + "projekte.col.type": "Type", + "projekte.col.reference": "Reference", + "projekte.col.clientmatter": "ClientMatter", + "projekte.col.status": "Status", + "projekte.col.updated": "Last modified", + "projekte.empty.title": "No projects yet", + "projekte.empty.hint": "Start via \u201cNew project\u201d \u2014 create a client first, then litigations, patents and cases underneath.", + "projekte.empty.filtered": "No matches for these filters.", + "projekte.cancel": "Cancel", + "projekte.submit": "Create project", + "projekte.neu.title": "New project \u2014 Paliad", + "projekte.neu.heading": "Create a new project", + "projekte.neu.subtitle": "Client, litigation, patent, case or generic project \u2014 place it in the hierarchy. Visibility follows the team (you are auto-added as \u201cLead\u201d).", + "projekte.field.type": "Type", + "projekte.field.parent": "Parent project", + "projekte.field.parent.placeholder": "Type to search for a parent project...", + "projekte.field.parent.hint": "Leave blank for a root project (typically a client).", + "projekte.field.title": "Title", + "projekte.field.title.placeholder": "e.g. Siemens AG | Siemens v. Huawei | EP 1 234 567", + "projekte.field.reference": "Internal reference (optional)", + "projekte.field.reference.placeholder": "e.g. HL-2026-0042", + "projekte.field.client_number": "Client no. (7 digits)", + "projekte.field.matter_number": "Matter no. (7 digits)", + "projekte.field.clientmatter.hint": "HLC billing numbers. Format CCCCCCC.MMMMMMM. Client no. is inherited by sub-projects (overridable).", + "projekte.field.netdocuments_url": "netDocuments URL (optional)", + "projekte.field.industry": "Industry", + "projekte.field.country": "Country (ISO-2)", + "projekte.field.billing_reference": "Billing reference", + "projekte.field.patent_number": "Patent number", + "projekte.field.filing_date": "Filing date", + "projekte.field.grant_date": "Grant date", + "projekte.field.court": "Court", + "projekte.field.case_number": "Case number (court)", + "projekte.field.status": "Status", + "projekte.error.title_required": "Title required", + "projekte.detail.title": "Project \u2014 Paliad", + "projekte.detail.back": "\u2190 Back to overview", + "projekte.detail.loading": "Loading\u2026", + "projekte.detail.notfound": "Project not found or no access.", + "projekte.detail.edit": "Edit", + "projekte.detail.save": "Save", + "projekte.detail.tab.verlauf": "Activity", + "projekte.detail.tab.team": "Team", + "projekte.detail.tab.kinder": "Sub-projects", + "projekte.detail.tab.parteien": "Parties", + "projekte.detail.tab.fristen": "Deadlines", + "projekte.detail.tab.termine": "Appointments", + "projekte.detail.tab.notizen": "Notes", + "projekte.detail.tab.checklisten": "Checklists", + "projekte.detail.verlauf.empty": "No events recorded yet.", + "projekte.detail.verlauf.loadMore": "Load more", + "projekte.detail.team.form.user": "User", + "projekte.detail.team.form.role": "Role", + "projekte.detail.team.form.cancel": "Cancel", + "projekte.detail.team.form.submit": "Add", + "projekte.detail.team.col.name": "Name", + "projekte.detail.team.col.role": "Role", + "projekte.detail.team.col.source": "Source", + "projekte.detail.kinder.add": "Create sub-project", + "projekte.detail.kinder.empty": "No sub-projects.", + "projekte.detail.parteien.add": "Add party", + "projekte.detail.parteien.form.name": "Name", + "projekte.detail.parteien.form.role": "Role", + "projekte.detail.parteien.form.rep": "Representative (optional)", + "projekte.detail.parteien.form.cancel": "Cancel", + "projekte.detail.parteien.form.submit": "Add", + "projekte.detail.parteien.role.claimant": "Claimant", + "projekte.detail.parteien.role.defendant": "Defendant", + "projekte.detail.parteien.role.thirdparty": "Third-party / intervenor", + "projekte.detail.parteien.col.name": "Name", + "projekte.detail.parteien.col.role": "Role", + "projekte.detail.parteien.col.rep": "Representative", + "projekte.detail.parteien.empty": "No parties recorded yet.", + "projekte.detail.fristen.add": "Add deadline", + "projekte.detail.fristen.empty": "No deadlines recorded for this project.", + "projekte.detail.termine.add": "Add appointment", + "projekte.detail.termine.form.cancel": "Cancel", + "projekte.detail.termine.form.submit": "Add", + "projekte.detail.termine.empty": "No appointments recorded for this project.", + "projekte.detail.checklisten.empty": "No checklist instances recorded for this project.", + "projekte.detail.checklisten.col.template": "Template", + "projekte.detail.checklisten.col.name": "Name", + "projekte.detail.checklisten.col.progress": "Progress", + "projekte.detail.checklisten.col.created": "Created", + "projekte.detail.checklisten.hint": "Instances are created on the template page under Checklists.", + "projekte.detail.delete": "Archive project", + "projekte.detail.delete.confirm.title": "Archive project?", + "projekte.detail.delete.confirm.body": "The project will be archived. It cannot be directly restored.", + "projekte.detail.delete.confirm.cancel": "Cancel", + "projekte.detail.delete.confirm.ok": "Archive", "projekte.type.client": "Client", "projekte.type.litigation": "Litigation", "projekte.type.patent": "Patent", @@ -1803,6 +2017,12 @@ export function t(key: string): string { return translations[currentLang][key] ?? translations.de[key] ?? key; } +// tOrEmpty returns the translation if present, else "" — so callers that +// want to fall back to the existing default text in the DOM can do so. +function tOrEmpty(key: string): string { + return translations[currentLang][key] ?? translations.de[key] ?? ""; +} + export function getLang(): Lang { return currentLang; } @@ -1822,17 +2042,22 @@ export function setLang(lang: Lang) { } function applyTranslations() { + // When a key is missing from every locale, preserve whatever static text + // the HTML was authored with — never overwrite with the raw key string. document.querySelectorAll("[data-i18n]").forEach((el) => { const key = el.getAttribute("data-i18n")!; - el.textContent = t(key); + const val = tOrEmpty(key); + if (val !== "") el.textContent = val; }); document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { const key = el.getAttribute("data-i18n-placeholder")!; - (el as HTMLInputElement).placeholder = t(key); + const val = tOrEmpty(key); + if (val !== "") (el as HTMLInputElement).placeholder = val; }); document.querySelectorAll("[data-i18n-title]").forEach((el) => { const key = el.getAttribute("data-i18n-title")!; - el.setAttribute("title", t(key)); + const val = tOrEmpty(key); + if (val !== "") el.setAttribute("title", val); }); }