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:
@@ -176,6 +176,7 @@ describe('API response mappers (snake_case → camelCase)', () => {
|
||||
workspace_id: 'ws1',
|
||||
child_id: 'child1',
|
||||
name: 'Sommar',
|
||||
name_message_key: 'wishEditor.presetSommar',
|
||||
window_start_month: 6,
|
||||
window_start_day: 15,
|
||||
window_end_month: 7,
|
||||
@@ -205,6 +206,7 @@ describe('API response mappers (snake_case → camelCase)', () => {
|
||||
expect(result[0]!.workspaceId).toBe('ws1')
|
||||
expect(result[0]!.childId).toBe('child1')
|
||||
expect(result[0]!.name).toBe('Sommar')
|
||||
expect(result[0]!.nameMessageKey).toBe('wishEditor.presetSommar')
|
||||
expect(result[0]!.windowStartMonth).toBe(6)
|
||||
expect(result[0]!.windowEndDay).toBe(26)
|
||||
expect(result[0]!.targetWeeks).toBe(6)
|
||||
@@ -301,6 +303,57 @@ describe('API response mappers (snake_case → camelCase)', () => {
|
||||
expect(result.windowStartMonth).toBe(6)
|
||||
expect(result.targetWeeks).toBe(6)
|
||||
})
|
||||
|
||||
it('sends name_message_key for a preset-created wish and maps it back', async () => {
|
||||
const rawResponse = {
|
||||
id: 'lw1',
|
||||
workspace_id: 'ws1',
|
||||
child_id: null,
|
||||
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,
|
||||
recurrence: 'yearly',
|
||||
first_year: 2026,
|
||||
last_year: null,
|
||||
priority: 1,
|
||||
parent_mode: 'either',
|
||||
parent_personal_data_uuid: null,
|
||||
goal_override: null,
|
||||
created_at: '2026-06-14T17:09:47',
|
||||
updated_at: '2026-06-14T17:09:47',
|
||||
}
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => rawResponse,
|
||||
})
|
||||
|
||||
const result = await api.createLeaveWish({
|
||||
workspaceId: 'ws1',
|
||||
name: 'Jul',
|
||||
nameMessageKey: 'wishEditor.seasonJul',
|
||||
windowStartMonth: 12,
|
||||
windowStartDay: 1,
|
||||
windowEndMonth: 1,
|
||||
windowEndDay: 31,
|
||||
targetWeeks: 4,
|
||||
recurrence: 'yearly',
|
||||
firstYear: 2026,
|
||||
})
|
||||
|
||||
const calls = (fetchMock.mock?.calls ?? []) as any[][]
|
||||
if (calls.length === 0) throw new Error('No fetch calls made')
|
||||
const [, options] = calls[0]!
|
||||
const body = JSON.parse(options.body)
|
||||
|
||||
expect(body.name_message_key).toBe('wishEditor.seasonJul')
|
||||
expect(result.nameMessageKey).toBe('wishEditor.seasonJul')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspace', () => {
|
||||
@@ -829,6 +882,46 @@ describe('endpoint functions (representative coverage)', () => {
|
||||
expect(result.targetWeeks).toBe(8)
|
||||
})
|
||||
|
||||
it('updateLeaveWish sends name_message_key: null to explicitly clear it on rename', async () => {
|
||||
// Mirrors what WishEditor sends the moment a user edits a preset wish's
|
||||
// name — the request must carry an explicit `null`, not omit the field
|
||||
// (omitting it would mean "leave unchanged" server-side).
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
id: 'lw1',
|
||||
workspace_id: 'ws1',
|
||||
child_id: null,
|
||||
name: 'Vinterresa',
|
||||
name_message_key: null,
|
||||
window_start_month: 6,
|
||||
window_start_day: 15,
|
||||
window_end_month: 7,
|
||||
window_end_day: 26,
|
||||
target_weeks: 6,
|
||||
recurrence: 'yearly',
|
||||
first_year: 2026,
|
||||
last_year: null,
|
||||
priority: 1,
|
||||
parent_mode: 'either',
|
||||
parent_personal_data_uuid: null,
|
||||
goal_override: null,
|
||||
created_at: '2026-01-01',
|
||||
updated_at: '2026-01-01',
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await api.updateLeaveWish('lw1', {
|
||||
name: 'Vinterresa',
|
||||
nameMessageKey: null,
|
||||
})
|
||||
|
||||
expect(lastRequestBody()).toEqual({ name: 'Vinterresa', name_message_key: null })
|
||||
expect(result.name).toBe('Vinterresa')
|
||||
expect(result.nameMessageKey).toBeNull()
|
||||
})
|
||||
|
||||
it('updateLeaveWish sends every provided field, snake_cased', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user