Show preset wish names in the current UI language

Preset wishes were created with `name: t(preset.labelKey)`, so the
translated string was persisted as the wish's name. A wish created while
the UI was Swedish stayed "Jul" in English forever -- the Swedish text
was the data, so no display-layer change could fix it.

Wish names are user-editable free text, so `name` cannot simply be
translated at render time. Migration 007 adds a nullable
`leave_wish.name_message_key` recording where a name came from: preset
wishes store the i18n key alongside the translated literal, and the UI
renders the key when present.

The key is cleared server-side the moment `name` actually changes,
compared against the stored value rather than trusting the caller, so no
client can forget to clear it and silently revert a user's own name to a
preset label.

An unknown key (a stale row, a renamed key) falls back to the stored
literal via an isTranslationKey guard, so a bad key can never crash the
page or leak a raw key to the user. Rows predating this column keep
displaying their stored name.

_FULL_SCHEMA in db/connection.py is updated in step with the migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
2026-09-14 22:32:54 +02:00
parent b03462066e
commit dc4c0a5edd
20 changed files with 668 additions and 17 deletions
@@ -37,6 +37,7 @@ class LeaveWishService:
target_weeks: int,
first_year: int,
child_id: UUID | None = None,
name_message_key: str | None = None,
recurrence: str = "once",
last_year: int | None = None,
priority: int = 1,
@@ -65,6 +66,7 @@ class LeaveWishService:
workspace_id=workspace_id,
child_id=child_id,
name=name,
name_message_key=name_message_key,
window_start_month=window_start_month,
window_start_day=window_start_day,
window_end_month=window_end_month,
@@ -97,6 +99,7 @@ class LeaveWishService:
leave_wish_id: UUID,
child_id: UUID | None = None,
name: str | None = None,
name_message_key: str | None = None,
window_start_month: int | None = None,
window_start_day: int | None = None,
window_end_month: int | None = None,
@@ -114,21 +117,35 @@ class LeaveWishService:
_clear_last_year: bool = False,
_clear_parent_personal_data_uuid: bool = False,
_clear_goal_override: bool = False,
_clear_name_message_key: bool = False,
) -> LeaveWish:
"""Partial update — only non-None fields are applied.
`_clear_*` flags exist because a nullable field's "no change" and
"set to None" both look like `None` on the wire; the router passes
them based on whether the field was present in the request payload.
`name_message_key` is additionally enforced here rather than left to
callers: whenever `name` actually changes, the key is force-cleared
regardless of what was passed for it, since a caller (frontend bug,
future API client, ...) forgetting to clear it would silently make a
user's edited name revert to a stale preset label.
"""
entity = await self.get(leave_wish_id)
original_name = entity.name
if _clear_child_id:
entity.child_id = None
elif child_id is not None:
entity.child_id = child_id
if _clear_name_message_key:
entity.name_message_key = None
elif name_message_key is not None:
entity.name_message_key = name_message_key
if name is not None:
entity.name = name
if name != original_name:
entity.name_message_key = None
if window_start_month is not None:
entity.window_start_month = window_start_month
if window_start_day is not None: