From 8e2194909120d5bb5e033d231c2064e77845c840 Mon Sep 17 00:00:00 2001 From: Florian Ludwig Date: Sat, 25 Jul 2026 00:22:14 +0200 Subject: [PATCH 1/3] chore(l10n): TRANSLATORS hints for the cancelled-appointment family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'Cancelled' means the appointment was called off (German 'Abgesagt', not 'Abgebrochen') — hints on the status badge, list filter and group, the cancel/reactivate menu action, success toast, mobile strings and the iCal title prefix, distinguishing it from the generic dialog button 'Cancel' ('Abbrechen'). --- lib/Service/IcalService.php | 1 + src/App.vue | 2 ++ src/components/appointment/AppointmentCard.vue | 3 +++ src/views/AllAppointments.vue | 2 ++ 4 files changed, 8 insertions(+) diff --git a/lib/Service/IcalService.php b/lib/Service/IcalService.php index 8f768f7..c138e88 100644 --- a/lib/Service/IcalService.php +++ b/lib/Service/IcalService.php @@ -251,6 +251,7 @@ private function generateVEvent( if ($appointment->isCancelled()) { $status = 'CANCELLED'; $transp = 'TRANSPARENT'; + // TRANSLATORS Prefix on the calendar event title of an appointment that was called off (German "Abgesagt", not "Abgebrochen"). $summary = $l->t('Cancelled') . ': ' . $summary; } diff --git a/src/App.vue b/src/App.vue index 923658e..93793ad 100644 --- a/src/App.vue +++ b/src/App.vue @@ -398,6 +398,7 @@ t('attendance', 'App developer') t('attendance', 'Ask each time') t('attendance', 'Attendees scan this code with the Attendance app to check in. One code works for all appointments.') t('attendance', 'Bring it to your team') +// TRANSLATORS: Shown on an appointment that was called off; {when} is the date of the cancellation (German "Abgesagt am {when}", not "Abgebrochen"). t('attendance', 'Cancelled on {when}') t('attendance', 'Check in') t('attendance', 'Check-in failed') @@ -456,6 +457,7 @@ t('attendance', 'Tag written. Scan it to test the check-in.') t('attendance', 'Tell me more (optional)') t('attendance', 'Thanks — that really helps.') t('attendance', 'There is no appointment to check into right now.') +// TRANSLATORS: The appointment was called off and will not take place (German "abgesagt", not "abgebrochen"). t('attendance', 'This appointment has been cancelled.') t('attendance', 'This code belongs to {host}.') t('attendance', 'This is not an Attendance check-in tag.') diff --git a/src/components/appointment/AppointmentCard.vue b/src/components/appointment/AppointmentCard.vue index f8094bd..80c99e6 100644 --- a/src/components/appointment/AppointmentCard.vue +++ b/src/components/appointment/AppointmentCard.vue @@ -45,6 +45,7 @@ + + {{ isCancelled ? t("attendance", "Reactivate appointment") @@ -832,6 +834,7 @@ async function handleToggleCancelled() { const url = generateUrl(`/apps/attendance/api/appointments/${props.appointment.id}/${wantsCancel ? 'cancel' : 'uncancel'}`) try { const response = await axios.post(url) + // TRANSLATORS: Success toast — the appointment was called off (German "abgesagt", not "abgebrochen"). showSuccess(wantsCancel ? t('attendance', 'Appointment cancelled') : t('attendance', 'Appointment reactivated')) diff --git a/src/views/AllAppointments.vue b/src/views/AllAppointments.vue index 677276f..a13c2f2 100644 --- a/src/views/AllAppointments.vue +++ b/src/views/AllAppointments.vue @@ -254,6 +254,7 @@ const filterDefs = computed(() => [ options: [ { id: STATUS.OPEN, label: t('attendance', 'Opened') }, { id: STATUS.CLOSED, label: t('attendance', 'Closed') }, + // TRANSLATORS: Filter option — appointments that were called off (German "Abgesagt", not "Abgebrochen"). { id: STATUS.CANCELLED, label: t('attendance', 'Cancelled') }, ], }, @@ -358,6 +359,7 @@ const visibleSections = computed(() => { return [ upcoming.length && { key: 'upcoming', label: t('attendance', 'Upcoming'), items: upcoming }, past.length && { key: 'past', label: t('attendance', 'Past'), items: past }, + // TRANSLATORS: Group heading for appointments that were called off (German "Abgesagt", not "Abgebrochen"). cancelled.length && { key: 'cancelled', label: t('attendance', 'Cancelled'), items: cancelled }, ].filter(Boolean) }) From 02ec2df28d2277ff561c0bf8032ac422434272ad Mon Sep 17 00:00:00 2001 From: Florian Ludwig Date: Sat, 25 Jul 2026 00:23:05 +0200 Subject: [PATCH 2/3] =?UTF-8?q?chore(skill):=20transifex=20=E2=80=94=20adv?= =?UTF-8?q?ance=20watermark=20after=20cancelled=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/transifex/state.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/transifex/state.json b/.claude/skills/transifex/state.json index 61863c0..bdb37a8 100644 --- a/.claude/skills/transifex/state.json +++ b/.claude/skills/transifex/state.json @@ -1,3 +1,3 @@ { - "answered_up_to": "2026-07-24T17:53:15Z" + "answered_up_to": "2026-07-24T22:22:57Z" } From a7bc81fbfe2da50596bb3a82bd4fedad38e11162 Mon Sep 17 00:00:00 2001 From: Florian Ludwig Date: Sat, 25 Jul 2026 00:34:12 +0200 Subject: [PATCH 3/3] fix(l10n): make TRANSLATORS hints actually bind to their strings Verified against the Nextcloud translationtool: a TRANSLATORS comment only attaches to the first t() call on the immediately following line. Multi-line ternaries, multi-line attribute bindings and comments before an element tag silently lose their hint, and the second call of a one-line ternary never gets one. Restructure the affected spots: scheduling toggle label/tooltip and cancel/reactivate action become computeds/functions with one comment per t() line, check-in chips get computed texts, the cancel toast an if/else, the cancelled badge and the teams settings section move the translated attribute onto the line after the comment. Document the binding rule and a local verification workflow (translationtool via docker + grep of the generated .pot) in the transifex skill. --- .claude/skills/transifex/SKILL.md | 25 +++++-- .../appointment/AppointmentCard.vue | 71 ++++++++++--------- .../appointment/ResponseSummary.vue | 39 ++++++---- src/views/AdminSettings.vue | 3 +- 4 files changed, 85 insertions(+), 53 deletions(-) diff --git a/.claude/skills/transifex/SKILL.md b/.claude/skills/transifex/SKILL.md index 55e71b2..7071ef5 100644 --- a/.claude/skills/transifex/SKILL.md +++ b/.claude/skills/transifex/SKILL.md @@ -94,12 +94,29 @@ const label = t('attendance', 'copy') {{ t('attendance', 'Required') }} ``` -Rules: - -- The comment binds only to the **first** translation call on the - following line — one call per line, each with its own comment. +Rules (empirically verified against the extractor, not just the docs): + +- The comment binds **only to the first `t()` call on the immediately + following line**. A `t()` two lines down — multi-line ternary, + multi-line attribute binding, element tag line without the + translated attribute — silently gets nothing. Restructure instead: + ternaries become if/else or a computed/function with one comment per + `return t(…)` line; template attribute bindings either go on the tag + line directly after the comment or move into a computed. +- The second `t()` of a one-line ternary also gets nothing — one call + per line, each with its own comment. - In Vue `