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
@@ -133,6 +133,57 @@ class TestSQLiteLeaveWishRepository:
result = await leave_wish_repo.get_by_id(uuid4())
assert result is None
async def test_create_and_get_round_trip_name_message_key(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
assert created.name_message_key == "wishEditor.seasonJul"
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key == "wishEditor.seasonJul"
async def test_create_without_name_message_key_defaults_to_none(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Existing-row simulation: a wish created before this field existed
(or a free-typed custom name) has no key and must read back `None`,
not a synthesized or empty-string value.
"""
entity = _make(workspace_id=seeded_ids.workspace_id, name="Custom trip")
created = await leave_wish_repo.create(entity)
assert created.name_message_key is None
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key is None
async def test_update_round_trips_name_message_key(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(workspace_id=seeded_ids.workspace_id, name="Jul", name_message_key=None)
created = await leave_wish_repo.create(entity)
created.name_message_key = "wishEditor.seasonJul"
updated = await leave_wish_repo.update(created.id, created)
assert updated.name_message_key == "wishEditor.seasonJul"
fetched = await leave_wish_repo.get_by_id(created.id)
assert fetched is not None
assert fetched.name_message_key == "wishEditor.seasonJul"
async def test_create_preserves_all_fields(
self,
leave_wish_repo: SQLiteLeaveWishRepository,
@@ -902,6 +953,105 @@ class TestLeaveWishService:
assert result.parent_mode == "either"
assert result.parent_personal_data_uuid is None
# -- name_message_key (fixes the "Jul"/"Höstlov" always-Swedish bug) ---
async def test_create_passes_through_name_message_key(
self,
leave_wish_service: LeaveWishService,
seeded_ids: SeededIds,
) -> None:
result = await leave_wish_service.create(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
window_start_month=12,
window_start_day=1,
window_end_month=1,
window_end_day=31,
target_weeks=4,
first_year=2027,
)
assert result.name_message_key == "wishEditor.seasonJul"
async def test_update_without_changing_name_preserves_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, target_weeks=5)
assert result.name_message_key == "wishEditor.seasonJul"
assert result.name == "Jul"
async def test_update_that_resends_the_same_name_preserves_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""The auto-save flow resends the unchanged `name` on every edit —
only an *actual* change to the text may clear the key.
"""
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, name="Jul")
assert result.name_message_key == "wishEditor.seasonJul"
async def test_update_renaming_clears_name_message_key_even_without_being_told_to(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
"""Backend enforcement, not caller-trust: renaming clears the key
even if the caller never mentions `name_message_key` at all — this
is the critical guard against a user's custom name silently
reverting to a stale preset label.
"""
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(leave_wish_id=created.id, name="Vinterresa")
assert result.name == "Vinterresa"
assert result.name_message_key is None
async def test_update_can_explicitly_clear_name_message_key(
self,
leave_wish_service: LeaveWishService,
leave_wish_repo: SQLiteLeaveWishRepository,
seeded_ids: SeededIds,
) -> None:
entity = _make(
workspace_id=seeded_ids.workspace_id,
name="Jul",
name_message_key="wishEditor.seasonJul",
)
created = await leave_wish_repo.create(entity)
result = await leave_wish_service.update(
leave_wish_id=created.id, _clear_name_message_key=True
)
assert result.name_message_key is None
assert result.name == "Jul" # unrelated to the clear
async def test_update_missing_raises(
self,
leave_wish_service: LeaveWishService,