diff --git a/client/src/pages/SharedTripPage.test.tsx b/client/src/pages/SharedTripPage.test.tsx index 4618b3f2a..15b61fe2f 100644 --- a/client/src/pages/SharedTripPage.test.tsx +++ b/client/src/pages/SharedTripPage.test.tsx @@ -1391,8 +1391,9 @@ describe('SharedTripPage', () => { fireEvent.click(screen.getByRole('button', { name: /costs/i })); await waitFor(() => expect(screen.getByText('Hostel')).toBeInTheDocument()); - // Each category twice: its line in the overview and its own card. - expect(screen.getAllByText('Stay')).toHaveLength(2); + // A free-text category the fixed list does not know joins Other, as in the + // Costs tab: one group, so its line in the overview and its own card. + expect(screen.queryByText('Stay')).toBeNull(); expect(screen.getAllByText('Other')).toHaveLength(2); // The priceless row states no amount rather than a placeholder. expect(screen.getByText('Museum pass').closest('li')?.textContent).toBe('Museum pass'); @@ -1896,4 +1897,33 @@ describe('SharedTripPage', () => { expect(screen.queryByText('Not planned yet')).toBeNull(); }); }); + + describe('FE-PAGE-SHARED-041: cost categories follow the reader language (#2596)', () => { + const payload = { + trip: { id: 1, title: 'Shared Paris Trip', start_date: '2026-07-01', end_date: '2026-07-03', currency: 'EUR' }, + days: [], assignments: {}, dayNotes: {}, places: [], reservations: [], accommodations: [], packing: [], + budget: [ + { id: 1, name: 'Le Meurice', total_price: '300', category: 'accommodation' }, + { id: 2, name: 'Air France', total_price: '120', category: 'flights' }, + // A free-text category from before the fixed list joins its key, as in the Costs tab. + { id: 3, name: 'Hostel night', total_price: '40', category: 'Hotel' }, + ], + categories: [], collab: [], + permissions: { share_bookings: false, share_packing: false, share_budget: true, share_collab: false }, + }; + + it('labels each group with the translated category, not the stored key', async () => { + seedStore(useSettingsStore, { settings: buildSettings({ language: 'de' }) }); + server.use(http.get('/api/shared/:token', () => HttpResponse.json(payload))); + renderSharedTrip('cat-token'); + fireEvent.click(await screen.findByRole('button', { name: /kosten/i })); + await screen.findByText('Le Meurice'); + // Once in the overview, once on the group card. + await waitFor(() => expect(screen.getAllByText('Unterkunft')).toHaveLength(2)); + expect(screen.getAllByText('Flüge')).toHaveLength(2); + expect(screen.queryByText('accommodation')).toBeNull(); + expect(screen.queryByText('flights')).toBeNull(); + expect(screen.queryByText('Hotel')).toBeNull(); + }); + }); }); diff --git a/client/src/pages/sharedTrip/SharedListsViews.tsx b/client/src/pages/sharedTrip/SharedListsViews.tsx index 20b170a33..89323b4cc 100644 --- a/client/src/pages/sharedTrip/SharedListsViews.tsx +++ b/client/src/pages/sharedTrip/SharedListsViews.tsx @@ -1,4 +1,5 @@ import { CheckCircle2, Circle, MessageCircle, PackageCheck, Wallet } from 'lucide-react' +import { catMeta } from '../../components/Budget/costsCategories' import { fs, NEUTRAL_TINT } from '../../components/shared/DialogShell' import { CountPill } from '../../components/Planner/bookings/bookingParts' import { TripMemberAvatar } from '../../components/Trips/TripMemberAvatar' @@ -105,7 +106,10 @@ export function SharedCostsView({ items, base, tripCurrency, convert }: CostsPro } const sumOf = (list: Expense[]) => list.reduce((s, i) => s + valueOf(i), 0) const total = sumOf(items) - const groups = groupInOrder(items, i => i.category || t('shared.other')) + // Grouped by the fixed key and labelled in the reader's language, as the Costs + // tab does: the stored value is a key ("flights") or an old free-text label. + const groups = groupInOrder(items, i => catMeta(i.category).key) + const labelOf = (key: string) => t(catMeta(key).labelKey) return (