Translate wish names in plan proposal output
Preset wish names follow the UI language in the wish editor, but the plan proposal still rendered outcome.wishName straight from the stored entity.name, so a proposal could show "Höstlov" with the UI in English. The proposal now carries wish_name_message_key alongside each wish name, and the frontend resolves it with the same helper the editor uses -- falling back to the stored literal when the key is absent or unknown. Nothing is translated on the backend; text crosses the wire as a key. Four surfaces carried a name and all four are covered: WindowOutcome itself, the explanation summary's interpolated data, and two remedies -- including REMEDY_LOWER_PRIORITY_WISH, which names a *different* sibling wish whose key can differ from the outcome's own. The proposal response mapping is hand-written and has dropped new fields twice before, so this is covered by an API-level test asserting the field reaches the response body, not only a service test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { resolveWishName } from './wishName'
|
||||
import { resolveWishName, resolveWishNameFromData } from './wishName'
|
||||
import { translate } from '@/i18n'
|
||||
|
||||
const t = (key: Parameters<typeof translate>[1]) => translate('sv', key)
|
||||
@@ -30,3 +30,32 @@ describe('resolveWishName', () => {
|
||||
expect(resolveWishName(wish, t)).toBe('Custom trip')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveWishNameFromData', () => {
|
||||
it('translates through wish_name_message_key when present, ignoring wish_name', () => {
|
||||
const data = { wish_name: 'STALE_ENGLISH_NAME', wish_name_message_key: 'wishEditor.seasonJul' }
|
||||
expect(resolveWishNameFromData(data, t)).toBe('Jul')
|
||||
})
|
||||
|
||||
it('returns wish_name verbatim when wish_name_message_key is null', () => {
|
||||
const data = { wish_name: 'Resa till mormor', wish_name_message_key: null }
|
||||
expect(resolveWishNameFromData(data, t)).toBe('Resa till mormor')
|
||||
})
|
||||
|
||||
it('returns wish_name verbatim when wish_name_message_key is absent', () => {
|
||||
// A `data` bag from before this field existed, or one that never
|
||||
// carried a wish name key at all.
|
||||
const data = { wish_name: 'Sommar' }
|
||||
expect(resolveWishNameFromData(data, t)).toBe('Sommar')
|
||||
})
|
||||
|
||||
it('falls back to wish_name for a key this build does not recognise', () => {
|
||||
const data = { wish_name: 'Sommar', wish_name_message_key: 'wishEditor.thisKeyDoesNotExist' }
|
||||
expect(resolveWishNameFromData(data, t)).toBe('Sommar')
|
||||
})
|
||||
|
||||
it('resolves to an empty string when wish_name is missing or not a string', () => {
|
||||
expect(resolveWishNameFromData({}, t)).toBe('')
|
||||
expect(resolveWishNameFromData({ wish_name: 42 }, t)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,3 +32,26 @@ export function resolveWishName(wish: NamedWish, t: (key: TranslationKey) => str
|
||||
}
|
||||
return wish.name
|
||||
}
|
||||
|
||||
/**
|
||||
* `resolveWishName`, adapted for a backend `data`/params bag that embeds a
|
||||
* wish name under the wire's snake_case keys (`wish_name` /
|
||||
* `wish_name_message_key`) rather than as a typed `NamedWish` — the shape
|
||||
* `PlanProposalResponse`'s outcome/remedy `data` dicts use (see
|
||||
* `WindowOutcome`/`ShortfallRemedy` in `backend/app/services/
|
||||
* allocation_service.py`) so `PlanResults.tsx` can interpolate `{wish_name}`
|
||||
* in a translated template with the same locale-aware rule everywhere else
|
||||
* a wish name is shown, instead of a second, divergent resolution.
|
||||
*
|
||||
* `data` arrives off the wire as `Record<string, unknown>` — both keys are
|
||||
* read defensively (a missing/non-string `wish_name` resolves to `''`,
|
||||
* matching there being nothing sensible to show).
|
||||
*/
|
||||
export function resolveWishNameFromData(
|
||||
data: Record<string, unknown>,
|
||||
t: (key: TranslationKey) => string,
|
||||
): string {
|
||||
const name = typeof data.wish_name === 'string' ? data.wish_name : ''
|
||||
const nameMessageKey = typeof data.wish_name_message_key === 'string' ? data.wish_name_message_key : null
|
||||
return resolveWishName({ name, nameMessageKey }, t)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user