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
This commit is contained in:
Maurice
2026-10-12 09:38:19 +02:00
parent 11ede4fbef
commit b3fe593a1e
2 changed files with 39 additions and 5 deletions
+32 -2
View File
@@ -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();
});
});
});
@@ -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 (
<div className="flex flex-col gap-4">
<div className={`${CARD} grid gap-5 p-5 md:grid-cols-[minmax(0,280px)_minmax(0,1fr)]`}>
@@ -119,7 +123,7 @@ export function SharedCostsView({ items, base, tripCurrency, convert }: CostsPro
return (
<li key={category}>
<div className="mb-1 flex items-baseline gap-2" style={fs(12.5, 'body')}>
<span className="min-w-0 flex-1 truncate font-semibold text-content-secondary">{category}</span>
<span className="min-w-0 flex-1 truncate font-semibold text-content-secondary">{labelOf(category)}</span>
<span className="font-geist font-semibold tabular-nums text-content">{money(sum)}</span>
</div>
<Meter value={total > 0 ? sum / total : 0} />
@@ -132,7 +136,7 @@ export function SharedCostsView({ items, base, tripCurrency, convert }: CostsPro
{groups.map(([category, list]) => (
<section key={category} className={CARD}>
<div className={CARD_HEAD} style={{ background: NEUTRAL_TINT }}>
<h3 className="min-w-0 flex-1 truncate font-bold text-content" style={fs(13.5, 'body')}>{category}</h3>
<h3 className="min-w-0 flex-1 truncate font-bold text-content" style={fs(13.5, 'body')}>{labelOf(category)}</h3>
<span className="font-geist font-bold tabular-nums text-content-secondary" style={fs(12.5, 'body')}>{money(sumOf(list))}</span>
</div>
<ul className="flex flex-col divide-y divide-[color:var(--border-faint)]">