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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user