From f583c650a22d9bf25bb888b2db373a2a4ba23355 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 29 Apr 2026 14:26:04 +0200 Subject: [PATCH] fix(t-paliad-067): PR-1 i18n leak sweep + activity narrative (F-04, F-07, F-10, F-12, F-21, F-29, F-35, F-46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per docs/audit-polish-2-2026-04-29.md PR-1. Single concern: text rendered to a German narrative that was still English or raw-keyed. - F-04 deadlines-new.ts now references the existing fristen.field.akte.* keys (the SSR template already used them) instead of the non-existent fristen.field.project.* keys, so the picker no longer renders the raw i18n key. - F-07 + F-21 dashboard activity log + project Verlauf: • i18n.ts gains the missing dashboard.action.short.project_type_changed plus a parallel event.title.* key set (full noun-phrase form for Verlauf, complementing the dashboard's verb form) and event.description.* templates with {title}/{count}/{parent} placeholders. • New translateEvent(eventType, title, description) helper localizes a stored project_events row for display; parses both new value-only descriptions and legacy English+DE-mix shapes ("Deadline „Foo" geändert", "Type case → litigation", "Note zu deadline hinzugefügt"). Wired into dashboard.ts and projects-detail.ts renderers. • Go services now write descriptions as value-only payloads (the title, the count, the parent slug, or "old → new") so future rows are locale-clean. Affected services: deadline_service.go (5 sites), appointment_service.go (3 sites), note_service.go (1 site), project_service.go (2 sites: status_changed, project_type_changed). • Translation covers historical project_events rows too — the legacy-format parsers in translateEventDescription strip the English "Type"/"Status" prefix and pull the quoted title out of "Deadline „Foo" geändert" so DE/EN renders correctly without DB migration. • Renamed dashboard.action.short.project_* DE labels from "...Akte" to "...Projekt" to match the project-rename direction. - F-10 deadlines list REGEL column now resolves rule_name/rule_name_en via a JOIN-side alias on deadline_service.ListWithProjects (added RuleName/RuleNameEN to DeadlineWithProject). New ruleDisplay() helper prefers the localized rule name and falls back to em-dash; never renders the raw rule_code slug ("inf.rejoin"). - F-12 fristen.col.akte and termine.col.akte DE values flip "Akte" → "Projekt"; matching SSR placeholder text on deadlines.tsx and appointments.tsx column headers (EN already said "Matter"). - F-29 the checklists empty-state hint on /projects/{id}/checklists is split into prefix/link/suffix spans so the stays intact after applyTranslations() runs (the previous single-string i18n value collapsed the anchor on first paint). - F-35 projekte.subtitle DE flips "Fälle" → "Verfahren" (matches the actual type taxonomy: Mandant/Streitsache/Patent/Verfahren/Projekt). Same fix on projekte.empty.hint. EN keeps "cases" since EN labels the case type as "case". - F-46 dashboard.greeting.prefix EN flips "Good day" → "Hello". Verified - go build ./... + go vet ./... + go test ./... all green. - bun run build clean. - Dashboard activity widget + project Verlauf renderer verified by reading the translated paths; live smoke pending deploy. --- frontend/src/appointments.tsx | 2 +- frontend/src/client/dashboard.ts | 12 +- frontend/src/client/deadlines-new.ts | 4 +- frontend/src/client/deadlines.ts | 14 +- frontend/src/client/i18n.ts | 191 +++++++++++++++++++++-- frontend/src/client/projects-detail.ts | 15 +- frontend/src/deadlines.tsx | 2 +- frontend/src/projects-detail.tsx | 6 +- frontend/src/projects.tsx | 4 +- internal/models/models.go | 8 +- internal/services/appointment_service.go | 9 +- internal/services/deadline_service.go | 23 ++- internal/services/note_service.go | 4 +- internal/services/project_service.go | 6 +- 14 files changed, 252 insertions(+), 48 deletions(-) diff --git a/frontend/src/appointments.tsx b/frontend/src/appointments.tsx index 0f1fbc8..c228c9d 100644 --- a/frontend/src/appointments.tsx +++ b/frontend/src/appointments.tsx @@ -98,7 +98,7 @@ export function renderAppointments(): string { Beginn Titel - Akte + Projekt Ort Typ diff --git a/frontend/src/client/dashboard.ts b/frontend/src/client/dashboard.ts index 55d278d..b7bf7bc 100644 --- a/frontend/src/client/dashboard.ts +++ b/frontend/src/client/dashboard.ts @@ -1,4 +1,4 @@ -import { initI18n, onLangChange, t, getLang } from "./i18n"; +import { initI18n, onLangChange, t, getLang, translateEvent } from "./i18n"; import { initSidebar } from "./sidebar"; interface DashboardUser { @@ -228,10 +228,12 @@ function renderActivity(items: ActivityEntry[]): void { const shortAction = hasI18n ? translated : (e.action || t("dashboard.activity.event")); - // The German sentence on the detail line ("Deadline „ok“ geändert"). - // Legacy rows without an i18n match fall back to the static English - // details so the row still says something instead of going blank. - const detail = e.description || (hasI18n ? "" : e.details); + // Localize the muted detail line so it speaks DE in DE and EN in EN — + // historical rows carry English nouns inside DE narrative ("Deadline „ok" + // geändert", "Note zu deadline hinzugefügt"); translateEvent parses both + // legacy and new (value-only) shapes. + const stored = e.description ?? (hasI18n ? "" : e.details); + const { description: detail } = translateEvent(e.action, "", stored); return `
  • ${esc(formatDateTime(e.timestamp))}
    diff --git a/frontend/src/client/deadlines-new.ts b/frontend/src/client/deadlines-new.ts index ade7bf7..02673e5 100644 --- a/frontend/src/client/deadlines-new.ts +++ b/frontend/src/client/deadlines-new.ts @@ -38,11 +38,11 @@ async function loadProjects() { const projects: Project[] = await resp.json(); if (projects.length === 0) { hint.style.display = ""; - hint.innerHTML = `${esc(t("fristen.field.project.empty"))} ${esc(t("fristen.field.project.empty.link"))}`; + hint.innerHTML = `${esc(t("fristen.field.akte.empty"))} ${esc(t("fristen.field.akte.empty.link"))}`; return; } const options: string[] = [ - ``, + ``, ]; for (const p of projects) { const isSelected = preselectedProjectID === p.id ? " selected" : ""; diff --git a/frontend/src/client/deadlines.ts b/frontend/src/client/deadlines.ts index 1731c07..bbf2382 100644 --- a/frontend/src/client/deadlines.ts +++ b/frontend/src/client/deadlines.ts @@ -12,6 +12,8 @@ interface Deadline { project_reference: string; project_title: string; rule_code?: string; + rule_name?: string; + rule_name_en?: string; } interface Project { @@ -150,6 +152,16 @@ function esc(s: string): string { return d.innerHTML; } +// REGEL cell label. Prefer the localized rule name ("Replik" / "Reply"); fall +// back to em-dash when no rule is attached. We never render the raw machine +// slug ("inf.rejoin") — the audit (F-10) flagged that as implementation leak. +function ruleDisplay(f: Deadline): string { + const lang = getLang(); + const localized = lang === "en" ? f.rule_name_en : f.rule_name; + if (localized && localized.trim()) return esc(localized); + return "—"; +} + function render() { if (!loadedOK) return; const tbody = document.getElementById("deadlines-body")!; @@ -179,7 +191,7 @@ function render() { .map((f) => { const urgency = urgencyClass(f.due_date, f.status); const statusLabel = t(`fristen.status.${f.status}`) || f.status; - const ruleLabel = f.rule_code ? esc(f.rule_code) : "—"; + const ruleLabel = ruleDisplay(f); const isDone = f.status === "completed"; const titleClass = isDone ? "frist-title-done" : ""; const reopenLabel = esc(t("fristen.action.reopen")); diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 904b618..59289c7 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -538,7 +538,7 @@ const translations: Record> = { "fristen.filter.akte.all": "Alle Projekte", "fristen.col.due": "F\u00e4llig", "fristen.col.title": "Titel", - "fristen.col.akte": "Akte", + "fristen.col.akte": "Projekt", "fristen.col.rule": "Regel", "fristen.col.status": "Status", "fristen.empty.title": "Keine Fristen vorhanden", @@ -688,9 +688,10 @@ const translations: Record> = { // bold summary line ("Matthias änderte Frist"); the full German // sentence ("Deadline „ok“ geändert") lives on a second // muted line via the project_events.description column. - "dashboard.action.short.project_created": "legte Akte an", - "dashboard.action.short.project_archived": "archivierte Akte", - "dashboard.action.short.project_reparented": "ordnete Akte neu zu", + "dashboard.action.short.project_created": "legte Projekt an", + "dashboard.action.short.project_archived": "archivierte Projekt", + "dashboard.action.short.project_reparented": "ordnete Projekt neu zu", + "dashboard.action.short.project_type_changed": "\u00e4nderte Projekt-Typ", "dashboard.action.short.status_changed": "\u00e4nderte Status", "dashboard.action.short.visibility_changed": "\u00e4nderte Sichtbarkeit", "dashboard.action.short.collaborators_updated": "aktualisierte Bearbeiter", @@ -704,6 +705,41 @@ const translations: Record> = { "dashboard.action.short.appointment_created": "legte Termin an", "dashboard.action.short.appointment_updated": "\u00e4nderte Termin", "dashboard.action.short.appointment_deleted": "l\u00f6schte Termin", + // Localized event-row title for the project Verlauf tab \u2014 full noun + // phrase ("Frist ge\u00e4ndert") complementing the dashboard's verb form. + "event.title.project_created": "Projekt angelegt", + "event.title.project_archived": "Projekt archiviert", + "event.title.project_reparented": "Projekt umstrukturiert", + "event.title.project_type_changed": "Projekt-Typ ge\u00e4ndert", + "event.title.status_changed": "Status ge\u00e4ndert", + "event.title.note_created": "Notiz hinzugef\u00fcgt", + "event.title.deadline_created": "Frist angelegt", + "event.title.deadline_updated": "Frist ge\u00e4ndert", + "event.title.deadline_completed": "Frist erledigt", + "event.title.deadline_reopened": "Frist wiederer\u00f6ffnet", + "event.title.deadline_deleted": "Frist gel\u00f6scht", + "event.title.deadlines_imported": "Fristen importiert", + "event.title.appointment_created": "Termin angelegt", + "event.title.appointment_updated": "Termin ge\u00e4ndert", + "event.title.appointment_deleted": "Termin gel\u00f6scht", + // Note-parent slugs for note_created descriptions + // ("Notiz zu Frist hinzugef\u00fcgt"). Used by translateEventDescription(). + "event.note.parent.project": "Projekt", + "event.note.parent.deadline": "Frist", + "event.note.parent.appointment": "Termin", + "event.note.added_to": "Notiz zu {parent} hinzugef\u00fcgt", + // Localized full-sentence descriptions for the project Verlauf and the + // dashboard activity feed. Stored descriptions carry just the variable + // payload (title/count/slugs); the renderer interpolates here. + "event.description.deadline_created": "Frist \u201e{title}\u201c angelegt", + "event.description.deadline_updated": "Frist \u201e{title}\u201c ge\u00e4ndert", + "event.description.deadline_completed": "Frist \u201e{title}\u201c als erledigt markiert", + "event.description.deadline_reopened": "Frist \u201e{title}\u201c wieder ge\u00f6ffnet", + "event.description.deadline_deleted": "Frist \u201e{title}\u201c gel\u00f6scht", + "event.description.deadlines_imported": "{count} Fristen aus Fristenrechner \u00fcbernommen", + "event.description.appointment_created": "Termin \u201e{title}\u201c angelegt", + "event.description.appointment_updated": "Termin \u201e{title}\u201c ge\u00e4ndert", + "event.description.appointment_deleted": "Termin \u201e{title}\u201c gel\u00f6scht", "dashboard.action.short.checklist_created": "legte Checkliste an", "dashboard.action.short.checklist_renamed": "benannte Checkliste um", "dashboard.action.short.checklist_unlinked": "trennte Checkliste", @@ -841,7 +877,7 @@ const translations: Record> = { "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.subtitle": "Mandanten, Streitsachen, Patente und Verfahren \u2014 hierarchisch organisiert.", "projekte.new": "Neues Projekt", "projekte.search.placeholder": "Titel, Referenz oder ClientMatter...", "projekte.filter.type": "Typ", @@ -862,7 +898,7 @@ const translations: Record> = { "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.hint": "Starten Sie \u00fcber \u201eNeues Projekt\u201c \u2014 legen Sie zuerst einen Mandanten an, darunter Streitsachen, Patente und Verfahren.", "projekte.empty.filtered": "Keine Treffer f\u00fcr diese Filter.", "projekte.cancel": "Abbrechen", "projekte.submit": "Projekt anlegen", @@ -946,7 +982,9 @@ const translations: Record> = { "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.checklisten.hint.prefix": "Instanzen werden auf der Vorlagen-Seite unter ", + "projekte.detail.checklisten.hint.link": "Checklisten", + "projekte.detail.checklisten.hint.suffix": " 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.", @@ -1040,7 +1078,7 @@ const translations: Record> = { "termine.filter.to": "Bis", "termine.col.start": "Beginn", "termine.col.title": "Titel", - "termine.col.akte": "Akte", + "termine.col.akte": "Projekt", "termine.col.location": "Ort", "termine.col.type": "Typ", "termine.empty.title": "Keine Termine vorhanden", @@ -1903,7 +1941,7 @@ const translations: Record> = { // Dashboard (logged-in landing) "dashboard.title": "Dashboard \u2014 Paliad", - "dashboard.greeting.prefix": "Good day", + "dashboard.greeting.prefix": "Hello", "dashboard.unavailable": "Dashboard requires the database \u2014 contact an administrator.", "dashboard.onboarding": "Please complete onboarding before deadlines and matters are shown.", "dashboard.summary.heading": "Deadlines at a glance", @@ -1929,9 +1967,10 @@ const translations: Record> = { "dashboard.urgency.soon": "Soon", "dashboard.when.today": "today", "dashboard.when.tomorrow": "tomorrow", - "dashboard.action.short.project_created": "created matter", - "dashboard.action.short.project_archived": "archived matter", - "dashboard.action.short.project_reparented": "re-parented matter", + "dashboard.action.short.project_created": "created project", + "dashboard.action.short.project_archived": "archived project", + "dashboard.action.short.project_reparented": "re-parented project", + "dashboard.action.short.project_type_changed": "changed project type", "dashboard.action.short.status_changed": "changed status", "dashboard.action.short.visibility_changed": "changed visibility", "dashboard.action.short.collaborators_updated": "updated collaborators", @@ -1945,6 +1984,41 @@ const translations: Record> = { "dashboard.action.short.appointment_created": "added appointment", "dashboard.action.short.appointment_updated": "updated appointment", "dashboard.action.short.appointment_deleted": "deleted appointment", + // Localized event-row title for the project Verlauf tab — full noun + // phrase ("Deadline updated") complementing the dashboard's verb form. + "event.title.project_created": "Project created", + "event.title.project_archived": "Project archived", + "event.title.project_reparented": "Project re-parented", + "event.title.project_type_changed": "Project type changed", + "event.title.status_changed": "Status changed", + "event.title.note_created": "Note added", + "event.title.deadline_created": "Deadline created", + "event.title.deadline_updated": "Deadline updated", + "event.title.deadline_completed": "Deadline completed", + "event.title.deadline_reopened": "Deadline reopened", + "event.title.deadline_deleted": "Deadline deleted", + "event.title.deadlines_imported": "Deadlines imported", + "event.title.appointment_created": "Appointment created", + "event.title.appointment_updated": "Appointment updated", + "event.title.appointment_deleted": "Appointment deleted", + // Note-parent slugs for note_created descriptions + // ("Note added to deadline"). Used by translateEventDescription(). + "event.note.parent.project": "project", + "event.note.parent.deadline": "deadline", + "event.note.parent.appointment": "appointment", + "event.note.added_to": "Note added to {parent}", + // Localized full-sentence descriptions for the project Verlauf and the + // dashboard activity feed. Stored descriptions carry just the variable + // payload (title/count/slugs); the renderer interpolates here. + "event.description.deadline_created": "Deadline “{title}” added", + "event.description.deadline_updated": "Deadline “{title}” updated", + "event.description.deadline_completed": "Deadline “{title}” completed", + "event.description.deadline_reopened": "Deadline “{title}” reopened", + "event.description.deadline_deleted": "Deadline “{title}” deleted", + "event.description.deadlines_imported": "{count} deadlines imported from Fristenrechner", + "event.description.appointment_created": "Appointment “{title}” added", + "event.description.appointment_updated": "Appointment “{title}” updated", + "event.description.appointment_deleted": "Appointment “{title}” deleted", "dashboard.action.short.checklist_created": "added checklist", "dashboard.action.short.checklist_renamed": "renamed checklist", "dashboard.action.short.checklist_unlinked": "unlinked checklist", @@ -2187,7 +2261,9 @@ const translations: Record> = { "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.checklisten.hint.prefix": "Instances are created on the template page under ", + "projekte.detail.checklisten.hint.link": "Checklists", + "projekte.detail.checklisten.hint.suffix": ".", "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.", @@ -2513,6 +2589,95 @@ export function getLang(): Lang { return currentLang; } +// translateEvent localizes a stored project_events row for display. +// +// project_events rows are written with stable English event_type slugs +// ("project_type_changed") and English fallback titles ("Project type +// changed"). The description column is freeform but for known event types +// follows a value-only pattern ("case → litigation", "active → archived", +// "deadline" for note_created). Legacy rows from before this PR carry the +// English prefix ("Type case → litigation", "Note zu deadline hinzugefügt" +// — DE/EN-mixed); both legacy and new shapes parse here. +// +// Caller provides whatever the backend returned for title + description; +// receives a localized pair ready for innerHTML/textContent. +export function translateEvent( + eventType: string | null | undefined, + storedTitle: string, + storedDescription: string | null | undefined, +): { title: string; description: string } { + // Title: prefer the per-event-type localized noun phrase. + let title = storedTitle; + if (eventType) { + const v = tOrEmpty(`event.title.${eventType}`); + if (v) title = v; + } + const description = storedDescription + ? translateEventDescription(eventType ?? "", storedDescription) + : ""; + return { title, description }; +} + +function translateEventDescription(eventType: string, description: string): string { + const body = description.trim(); + if (!body) return ""; + + if (eventType === "project_type_changed") { + // New format: "case → litigation". Legacy: "Type case → litigation". + return translateArrowSlugs(body.replace(/^Type\s+/, ""), "projekte.type."); + } + if (eventType === "status_changed") { + // New format: "active → archived". Legacy: "Status active → archived". + return translateArrowSlugs(body.replace(/^Status\s+/, ""), "projekte.filter.status."); + } + if (eventType === "note_created") { + // New format: just the parent slug. Legacy: "Note zu hinzugefügt". + const m = body.match(/^Note zu (project|deadline|appointment) hinzugef[üu]gt$/i); + const slug = (m ? m[1] : body).toLowerCase(); + const parent = tOrEmpty(`event.note.parent.${slug}`) || slug; + const phrase = tOrEmpty("event.note.added_to"); + return phrase ? phrase.replace("{parent}", parent) : body; + } + if (eventType.startsWith("deadline_") || eventType.startsWith("appointment_")) { + // New format: just the entity title. Legacy: 'Deadline „Foo" geändert' / + // 'Appointment „Foo" angelegt' etc. Strip the noun + verb wrapper, keep + // the quoted title, then re-wrap in the localized template. + const title = extractQuotedTitle(body) ?? body; + return interpolateTemplate(`event.description.${eventType}`, { title }) ?? body; + } + if (eventType === "deadlines_imported") { + // New format: just the count. Legacy: "N Deadlines aus Fristenrechner übernommen". + const m = body.match(/^(\d+)\b/); + const count = m ? m[1] : body; + return interpolateTemplate("event.description.deadlines_imported", { count }) ?? body; + } + return description; +} + +// extractQuotedTitle pulls the body out of a legacy DE description like +// `Deadline „Foo" geändert` — German double-low-9 (U+201E) opening quote and +// double-high-6 (U+201C) closing quote, the pair the Go services actually +// write today. Returns null if no quoted run is found, so callers can fall +// back to the raw body. +function extractQuotedTitle(body: string): string | null { + const m = body.match(/„([^“”„]+)[“”]/); + return m ? m[1] : null; +} + +function interpolateTemplate(key: string, vars: Record): string | null { + const tmpl = tOrEmpty(key); + if (!tmpl) return null; + return tmpl.replace(/\{(\w+)\}/g, (_, name) => vars[name] ?? `{${name}}`); +} + +function translateArrowSlugs(body: string, prefix: string): string { + const m = body.match(/^(\S+)\s*[→→]\s*(\S+)$/); + if (!m) return body; + const a = tOrEmpty(`${prefix}${m[1]}`) || m[1]; + const b = tOrEmpty(`${prefix}${m[2]}`) || m[2]; + return `${a} → ${b}`; +} + export function onLangChange(cb: () => void) { changeCallbacks.push(cb); } diff --git a/frontend/src/client/projects-detail.ts b/frontend/src/client/projects-detail.ts index cc128a9..93fe7b2 100644 --- a/frontend/src/client/projects-detail.ts +++ b/frontend/src/client/projects-detail.ts @@ -1,4 +1,4 @@ -import { initI18n, onLangChange, t, getLang } from "./i18n"; +import { initI18n, onLangChange, t, getLang, translateEvent } from "./i18n"; import { initSidebar } from "./sidebar"; import { initNotes } from "./notes"; import { @@ -572,15 +572,16 @@ function renderEvents() { } empty.style.display = "none"; list.innerHTML = events - .map( - (e) => `
  • + .map((e) => { + const { title, description } = translateEvent(e.event_type, e.title, e.description ?? null); + return `
  • ${fmtDateTime(e.created_at)}
    -
    ${esc(e.title)}
    - ${e.description ? `
    ${esc(e.description)}
    ` : ""} +
    ${esc(title)}
    + ${description ? `
    ${esc(description)}
    ` : ""}
    -
  • `, - ) + `; + }) .join(""); if (moreWrap) moreWrap.style.display = eventsHasMore ? "" : "none"; } diff --git a/frontend/src/deadlines.tsx b/frontend/src/deadlines.tsx index 7bf0641..afe567b 100644 --- a/frontend/src/deadlines.tsx +++ b/frontend/src/deadlines.tsx @@ -98,7 +98,7 @@ export function renderDeadlines(): string { Fällig Titel - Akte + Projekt Regel Status diff --git a/frontend/src/projects-detail.tsx b/frontend/src/projects-detail.tsx index c243e3d..67abbaf 100644 --- a/frontend/src/projects-detail.tsx +++ b/frontend/src/projects-detail.tsx @@ -325,8 +325,10 @@ export function renderProjectsDetail(): string { -

    - Instanzen werden auf der Vorlagen-Seite unter Checklisten angelegt. +

    + Instanzen werden auf der Vorlagen-Seite unter + Checklisten + angelegt.

    diff --git a/frontend/src/projects.tsx b/frontend/src/projects.tsx index 40c5139..38ac467 100644 --- a/frontend/src/projects.tsx +++ b/frontend/src/projects.tsx @@ -31,7 +31,7 @@ export function renderProjects(): string {

    Projekte

    - Mandanten, Streitsachen, Patente und Fälle — hierarchisch organisiert. + Mandanten, Streitsachen, Patente und Verfahren — hierarchisch organisiert.

    @@ -114,7 +114,7 @@ export function renderProjects(): string { diff --git a/internal/models/models.go b/internal/models/models.go index 9215917..5c3c9f8 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -185,13 +185,19 @@ type Deadline struct { } // DeadlineWithProject enriches a Deadline with parent-Project display fields -// (reference + title) for list views. +// (reference + title) for list views. RuleName/RuleNameEN are the +// human-readable label of the linked deadline-rule (e.g. "Replik" / "Reply"), +// while RuleCode is the machine-readable slug ("inf.rejoin") — list views +// should prefer the localized name and fall back to the code only when no +// rule is attached. type DeadlineWithProject struct { Deadline ProjectReference *string `db:"project_reference" json:"project_reference,omitempty"` ProjectTitle string `db:"project_title" json:"project_title"` ProjectType string `db:"project_type" json:"project_type"` RuleCode *string `db:"rule_code" json:"rule_code,omitempty"` + RuleName *string `db:"rule_name" json:"rule_name,omitempty"` + RuleNameEN *string `db:"rule_name_en" json:"rule_name_en,omitempty"` } // Appointment is one appointment. project_id is nullable: NULL = personal diff --git a/internal/services/appointment_service.go b/internal/services/appointment_service.go index 56b426f..b867782 100644 --- a/internal/services/appointment_service.go +++ b/internal/services/appointment_service.go @@ -254,7 +254,10 @@ func (s *AppointmentService) Create(ctx context.Context, userID uuid.UUID, input } if input.ProjectID != nil { - desc := fmt.Sprintf("Appointment \u201E%s\u201C angelegt", title) + // Description carries value-only payload (the appointment title); frontend + // renders via the localized event.description.appointment_* template. Same + // pattern for updated/deleted below. + desc := title descPtr := &desc if err := insertProjectEvent(ctx, tx, *input.ProjectID, userID, "appointment_created", "Appointment created", descPtr); err != nil { return nil, err @@ -342,7 +345,7 @@ func (s *AppointmentService) Update(ctx context.Context, userID, terminID uuid.U } if current.ProjectID != nil { - desc := fmt.Sprintf("Appointment \u201E%s\u201C ge\u00e4ndert", current.Title) + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, *current.ProjectID, userID, "appointment_updated", "Appointment updated", descPtr); err != nil { return nil, err @@ -386,7 +389,7 @@ func (s *AppointmentService) Delete(ctx context.Context, userID, terminID uuid.U return fmt.Errorf("delete appointment: %w", err) } if current.ProjectID != nil { - desc := fmt.Sprintf("Appointment \u201E%s\u201C gel\u00f6scht", current.Title) + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, *current.ProjectID, userID, "appointment_deleted", "Appointment deleted", descPtr); err != nil { return err diff --git a/internal/services/deadline_service.go b/internal/services/deadline_service.go index 0ba9fad..5322837 100644 --- a/internal/services/deadline_service.go +++ b/internal/services/deadline_service.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "strconv" "strings" "time" @@ -125,7 +126,9 @@ func (s *DeadlineService) ListVisibleForUser(ctx context.Context, userID uuid.UU p.reference AS project_reference, p.title AS project_title, p.type AS project_type, - r.code AS rule_code + r.code AS rule_code, + r.name AS rule_name, + r.name_en AS rule_name_en FROM paliad.deadlines f JOIN paliad.projects p ON p.id = f.project_id LEFT JOIN paliad.deadline_rules r ON r.id = f.rule_id @@ -215,7 +218,10 @@ func (s *DeadlineService) CreateBulk(ctx context.Context, userID, projektID uuid ids = append(ids, id) } - desc := fmt.Sprintf("%d Deadlines aus Fristenrechner übernommen", len(inputs)) + // Description carries the value-only payload (the import count); the + // frontend renders via the localized event.description.deadlines_imported + // template. + desc := strconv.Itoa(len(inputs)) descPtr := &desc if err := insertProjectEvent(ctx, tx, projektID, userID, "deadlines_imported", "Deadlines imported", descPtr); err != nil { return nil, err @@ -301,7 +307,10 @@ func (s *DeadlineService) Update(ctx context.Context, userID, fristID uuid.UUID, return nil, fmt.Errorf("update deadline: %w", err) } - desc := fmt.Sprintf("Deadline \u201E%s\u201C geändert", current.Title) + // Description carries value-only payload (the deadline title); frontend + // renders via the localized event.description.deadline_updated template. + // Same pattern below for completed/reopened/deleted/created. + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, current.ProjectID, userID, "deadline_updated", "Deadline updated", descPtr); err != nil { return nil, err @@ -335,7 +344,7 @@ func (s *DeadlineService) Complete(ctx context.Context, userID, fristID uuid.UUI WHERE id = $2`, now, fristID); err != nil { return nil, fmt.Errorf("complete deadline: %w", err) } - desc := fmt.Sprintf("Deadline \u201E%s\u201C als erledigt markiert", current.Title) + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, current.ProjectID, userID, "deadline_completed", "Deadline completed", descPtr); err != nil { return nil, err @@ -375,7 +384,7 @@ func (s *DeadlineService) Reopen(ctx context.Context, userID, fristID uuid.UUID) WHERE id = $2`, now, fristID); err != nil { return nil, fmt.Errorf("reopen deadline: %w", err) } - desc := fmt.Sprintf("Deadline \u201E%s\u201C wieder ge\u00f6ffnet", current.Title) + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, current.ProjectID, userID, "deadline_reopened", "Deadline reopened", descPtr); err != nil { return nil, err @@ -451,7 +460,7 @@ func (s *DeadlineService) Delete(ctx context.Context, userID, fristID uuid.UUID) `DELETE FROM paliad.deadlines WHERE id = $1`, fristID); err != nil { return fmt.Errorf("delete deadline: %w", err) } - desc := fmt.Sprintf("Deadline \u201E%s\u201C gelöscht", current.Title) + desc := current.Title descPtr := &desc if err := insertProjectEvent(ctx, tx, current.ProjectID, userID, "deadline_deleted", "Deadline deleted", descPtr); err != nil { return err @@ -534,7 +543,7 @@ func (s *DeadlineService) insert(ctx context.Context, userID, projektID uuid.UUI return uuid.Nil, err } - desc := fmt.Sprintf("Deadline \u201E%s\u201C angelegt", strings.TrimSpace(input.Title)) + desc := strings.TrimSpace(input.Title) descPtr := &desc if err := insertProjectEvent(ctx, tx, projektID, userID, "deadline_created", "Deadline created", descPtr); err != nil { return uuid.Nil, err diff --git a/internal/services/note_service.go b/internal/services/note_service.go index ab49aea..c2b76ad 100644 --- a/internal/services/note_service.go +++ b/internal/services/note_service.go @@ -245,8 +245,10 @@ func (s *NoteService) insertWithAudit(ctx context.Context, userID uuid.UUID, con } if projektAuditID != nil { + // Description carries the value-only payload (the parent slug); the + // frontend renders it via the localized event.note.added_to template. title := "Note added" - desc := fmt.Sprintf("Note zu %s hinzugef\u00fcgt", parentLabel) + desc := parentLabel descPtr := &desc if err := insertProjectEvent(ctx, tx, *projektAuditID, userID, "note_created", title, descPtr); err != nil { return uuid.Nil, err diff --git a/internal/services/project_service.go b/internal/services/project_service.go index 5b4d14e..3ba4927 100644 --- a/internal/services/project_service.go +++ b/internal/services/project_service.go @@ -612,15 +612,17 @@ func (s *ProjectService) Update(ctx context.Context, userID, id uuid.UUID, input return nil, fmt.Errorf("update project: %w", err) } + // Descriptions carry the value-only payload (`old → new`); the frontend + // renderer translates both slugs and prepends the localized prefix. if input.Status != nil && *input.Status != current.Status { - desc := fmt.Sprintf("Status %s → %s", current.Status, *input.Status) + desc := fmt.Sprintf("%s → %s", current.Status, *input.Status) descPtr := &desc if err := insertProjectEvent(ctx, tx, id, userID, "status_changed", "Status changed", descPtr); err != nil { return nil, err } } if typeChanged { - desc := fmt.Sprintf("Type %s → %s", current.Type, *input.Type) + desc := fmt.Sprintf("%s → %s", current.Type, *input.Type) descPtr := &desc if err := insertProjectEvent(ctx, tx, id, userID, "project_type_changed", "Project type changed", descPtr); err != nil { return nil, err