From b3fe593a1e9775d02eb673ff3b41b1f3af05e8b1 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 12 Oct 2026 09:38:19 +0200 Subject: [PATCH] fix(share): translate cost categories on the public trip page The shared Costs tab grouped expenses by the stored category and printed it as is, so the fixed keys ("accommodation", "flights") showed in English whatever language the reader picked. It now groups through catMeta and labels each group with costs.cat.*, the way the Costs tab does, so old free-text labels also join their key instead of forming groups of their own. Refs #2596 --- client/src/pages/SharedTripPage.test.tsx | 34 +++++++++++++++++-- .../src/pages/sharedTrip/SharedListsViews.tsx | 10 ++++-- 2 files changed, 39 insertions(+), 5 deletions(-) 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 (
@@ -119,7 +123,7 @@ export function SharedCostsView({ items, base, tripCurrency, convert }: CostsPro return (
  • - {category} + {labelOf(category)} {money(sum)}
    0 ? sum / total : 0} /> @@ -132,7 +136,7 @@ export function SharedCostsView({ items, base, tripCurrency, convert }: CostsPro {groups.map(([category, list]) => (
    -

    {category}

    +

    {labelOf(category)}

    {money(sumOf(list))}