From 8267650b2df2f2ff339f3f535e4fcf2ebb4075bf Mon Sep 17 00:00:00 2001 From: trongbinhnguyen <43725147+trongbinh15@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:50:27 +0700 Subject: [PATCH] fix(pdf): use the trip's actual currency instead of hardcoded EUR (#1519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(pdf): use the trip's actual currency instead of hardcoded EUR Co-Authored-By: Claude Sonnet 5 * fix(i18n): drop hardcoded currency text from pdf.costLabel across locales Remove redundant EUR/currency references from pdf.costLabel translations across 21 locales now that the PDF export correctly renders amounts with their actual trip currency via formatMoney(). Co-Authored-By: Claude Sonnet 5 * fix(pdf): drop hardcoded euro-shaped icon from place price chip svgEuro rendered a fixed € glyph next to the price chip regardless of the trip's actual currency, undermining the currency fix. Swap for a currency-neutral coin icon. --------- Co-authored-by: Nguyen Trong Binh Co-authored-by: Claude Sonnet 5 --- client/src/components/PDF/TripPDF.test.ts | 42 ++++++++++++++++++++--- client/src/components/PDF/TripPDF.tsx | 14 ++++---- shared/src/i18n/ar/pdf.ts | 2 +- shared/src/i18n/br/pdf.ts | 2 +- shared/src/i18n/cs/pdf.ts | 2 +- shared/src/i18n/de/pdf.ts | 2 +- shared/src/i18n/en/pdf.ts | 2 +- shared/src/i18n/es/pdf.ts | 2 +- shared/src/i18n/fr/pdf.ts | 2 +- shared/src/i18n/gr/pdf.ts | 2 +- shared/src/i18n/id/pdf.ts | 2 +- shared/src/i18n/it/pdf.ts | 2 +- shared/src/i18n/ja/pdf.ts | 2 +- shared/src/i18n/ko/pdf.ts | 2 +- shared/src/i18n/nl/pdf.ts | 2 +- shared/src/i18n/pl/pdf.ts | 2 +- shared/src/i18n/ru/pdf.ts | 2 +- shared/src/i18n/sv/pdf.ts | 2 +- shared/src/i18n/tr/pdf.ts | 2 +- shared/src/i18n/uk/pdf.ts | 2 +- shared/src/i18n/vi/pdf.ts | 2 +- shared/src/i18n/zh-TW/pdf.ts | 2 +- shared/src/i18n/zh/pdf.ts | 2 +- 23 files changed, 66 insertions(+), 32 deletions(-) diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index 8b41770b3..25ae66311 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -286,13 +286,47 @@ describe('downloadTripPDF', () => { expect(iframe!.srcdoc).toContain('CONF999') }) - it('FE-COMP-TRIPPDF-016: renders place description and price chip', async () => { + it('FE-COMP-TRIPPDF-016: renders place description and a currency-formatted price chip', async () => { await downloadTripPDF(richArgs) const iframe = getIframe() expect(iframe!.srcdoc).toContain('Ancient amphitheater') - // Price chip: 15 EUR - expect(iframe!.srcdoc).toContain('15') - expect(iframe!.srcdoc).toContain('EUR') + // richArgs trip has no explicit currency, place has no currency override — + // formatMoney falls back to EUR, formatted via Intl (symbol, not literal "EUR" text). + expect(iframe!.srcdoc).toContain('15,00') + expect(iframe!.srcdoc).toContain('€') + }) + + it('FE-COMP-TRIPPDF-016b: formats price chip and totals in the trip currency, not EUR', async () => { + const usdArgs = { + ...richArgs, + trip: { ...richArgs.trip, currency: 'USD' }, + } + await downloadTripPDF(usdArgs) + const iframe = getIframe() + // Place price chip: place has no currency override, falls back to trip.currency (USD). + expect(iframe!.srcdoc).toContain('$15.00') + // No literal "EUR" text should leak into a USD trip's export. + expect(iframe!.srcdoc).not.toContain('EUR') + // Price chip icon must stay currency-neutral — not the euro-shaped glyph. + expect(iframe!.srcdoc).not.toContain('M14 5c-3.87 0-7 3.13-7 7s3.13 7 7 7c2.17 0 4.1-.99 5.4-2.55') + }) + + it('FE-COMP-TRIPPDF-016c: a place with its own currency overrides the trip currency for its price chip', async () => { + const mixedArgs = { + ...richArgs, + trip: { ...richArgs.trip, currency: 'EUR' }, + assignments: { + '10': [{ + ...assignmentForDay, + place: { ...placeWithDetails, currency: 'JPY', price: '1500' }, + }], + } as any, + } + await downloadTripPDF(mixedArgs) + const iframe = getIframe() + // JPY is a zero-decimal currency (currencyDecimals) and uses its own symbol, not EUR. + // Note: Intl renders JPY with the fullwidth yen sign (U+FFE5 "¥"), not U+00A5 "¥". + expect(iframe!.srcdoc).toContain('¥1,500') }) it('FE-COMP-TRIPPDF-017: renders trip description on cover', async () => { diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 76139379d..32155aded 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -5,7 +5,7 @@ import { FileText, Info, Clock, MapPin, Navigation, Train, Plane, Bus, Car, Ship import { accommodationsApi, mapsApi, pluginsApi } from '../../api/client' import type { Trip, Day, Place, Category, AssignmentsMap, DayNote } from '../../types' import { isDayInAccommodationRange, getDayOrder } from '../../utils/dayOrder' -import { splitReservationDateTime } from '../../utils/formatters' +import { formatMoney, splitReservationDateTime } from '../../utils/formatters' import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs' function renderLucideIcon(icon:LucideIcon, props = {}) { @@ -40,7 +40,7 @@ const svgPin = `` const svgClock2= `` const svgCheck = `` -const svgEuro = `` +const svgMoney = `` function escHtml(str) { if (!str) return '' @@ -92,9 +92,9 @@ function longDateRange(days, locale) { return `${f.toLocaleDateString(locale, { day: 'numeric', month: 'long', timeZone: 'UTC' })} – ${l.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}` } -function dayCost(assignments, dayId, locale) { +function dayCost(assignments, dayId, locale, currency) { const total = (assignments[String(dayId)] || []).reduce((s, a) => s + (parseFloat(a.place?.price) || 0), 0) - return total > 0 ? `${total.toLocaleString(locale)} EUR` : null + return total > 0 ? formatMoney(total, currency, locale) : null } // Pre-fetch place photos for all assigned places. @@ -204,7 +204,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const daysHtml = sorted.map((day, di) => { const assigned = assignments[String(day.id)] || [] const notes = (dayNotes || []).filter(n => n.day_id === day.id) - const cost = dayCost(assignments, day.id, loc) + const cost = dayCost(assignments, day.id, loc, trip.currency) // Reservations for this day (hotel rendered via accommodations block; car middle-phase rendered in sidebar header only) const dayReservations = pdfGetTransportForDay(day.id) @@ -320,7 +320,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor const chips = [ place.place_time ? `${svgClock}${escHtml(place.place_time)}` : '', - place.price && parseFloat(place.price) > 0 ? `${svgEuro}${Number(place.price).toLocaleString(loc)} EUR` : '', + place.price && parseFloat(place.price) > 0 ? `${svgMoney}${formatMoney(Number(place.price), place.currency || trip.currency, loc)}` : '', ].filter(Boolean).join('') return ` @@ -625,7 +625,7 @@ export async function downloadTripPDF({ trip, days, places, assignments, categor
${escHtml(tr('pdf.planned'))}
${totalCost > 0 ? `
-
${totalCost.toLocaleString(loc)}
+
${formatMoney(totalCost, trip.currency, loc)}
${escHtml(tr('pdf.costLabel'))}
` : ''} diff --git a/shared/src/i18n/ar/pdf.ts b/shared/src/i18n/ar/pdf.ts index 3e2936ecc..86a7a9423 100644 --- a/shared/src/i18n/ar/pdf.ts +++ b/shared/src/i18n/ar/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'خطة السفر', 'pdf.planned': 'مخطط', - 'pdf.costLabel': 'التكلفة EUR', + 'pdf.costLabel': 'التكلفة', 'pdf.preview': 'معاينة PDF', 'pdf.saveAsPdf': 'حفظ كـ PDF', }; diff --git a/shared/src/i18n/br/pdf.ts b/shared/src/i18n/br/pdf.ts index a890a14dc..7ca63ccb4 100644 --- a/shared/src/i18n/br/pdf.ts +++ b/shared/src/i18n/br/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Plano de viagem', 'pdf.planned': 'Planejado', - 'pdf.costLabel': 'Custo (EUR)', + 'pdf.costLabel': 'Custo', 'pdf.preview': 'Pré-visualização do PDF', 'pdf.saveAsPdf': 'Salvar como PDF', }; diff --git a/shared/src/i18n/cs/pdf.ts b/shared/src/i18n/cs/pdf.ts index dae561e75..cce6e43e2 100644 --- a/shared/src/i18n/cs/pdf.ts +++ b/shared/src/i18n/cs/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Cestovní plán', 'pdf.planned': 'Naplánováno', - 'pdf.costLabel': 'Náklady EUR', + 'pdf.costLabel': 'Náklady', 'pdf.preview': 'Náhled PDF', 'pdf.saveAsPdf': 'Uložit jako PDF', }; diff --git a/shared/src/i18n/de/pdf.ts b/shared/src/i18n/de/pdf.ts index 003832ea6..a259c54ad 100644 --- a/shared/src/i18n/de/pdf.ts +++ b/shared/src/i18n/de/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Reiseplan', 'pdf.planned': 'Eingeplant', - 'pdf.costLabel': 'Kosten EUR', + 'pdf.costLabel': 'Kosten', 'pdf.preview': 'PDF Vorschau', 'pdf.saveAsPdf': 'Als PDF speichern', }; diff --git a/shared/src/i18n/en/pdf.ts b/shared/src/i18n/en/pdf.ts index 3ee899f64..1dcedfea0 100644 --- a/shared/src/i18n/en/pdf.ts +++ b/shared/src/i18n/en/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Travel Plan', 'pdf.planned': 'Planned', - 'pdf.costLabel': 'Cost EUR', + 'pdf.costLabel': 'Cost', 'pdf.preview': 'PDF Preview', 'pdf.saveAsPdf': 'Save as PDF', }; diff --git a/shared/src/i18n/es/pdf.ts b/shared/src/i18n/es/pdf.ts index 10f5dd25c..e3eaafb7f 100644 --- a/shared/src/i18n/es/pdf.ts +++ b/shared/src/i18n/es/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Plan de viaje', 'pdf.planned': 'Planificado', - 'pdf.costLabel': 'Coste EUR', + 'pdf.costLabel': 'Coste', 'pdf.preview': 'Vista previa PDF', 'pdf.saveAsPdf': 'Guardar como PDF', }; diff --git a/shared/src/i18n/fr/pdf.ts b/shared/src/i18n/fr/pdf.ts index c4a493eea..b717f9822 100644 --- a/shared/src/i18n/fr/pdf.ts +++ b/shared/src/i18n/fr/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Plan de voyage', 'pdf.planned': 'Planifié', - 'pdf.costLabel': 'Coût EUR', + 'pdf.costLabel': 'Coût', 'pdf.preview': 'Aperçu PDF', 'pdf.saveAsPdf': 'Enregistrer en PDF', }; diff --git a/shared/src/i18n/gr/pdf.ts b/shared/src/i18n/gr/pdf.ts index 8915c1996..0b616f89e 100644 --- a/shared/src/i18n/gr/pdf.ts +++ b/shared/src/i18n/gr/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Ταξιδιωτικό Πρόγραμμα', 'pdf.planned': 'Προγραμματισμένο', - 'pdf.costLabel': 'Κόστος EUR', + 'pdf.costLabel': 'Κόστος', 'pdf.preview': 'Προεπισκόπηση PDF', 'pdf.saveAsPdf': 'Αποθήκευση ως PDF', }; diff --git a/shared/src/i18n/id/pdf.ts b/shared/src/i18n/id/pdf.ts index c20334b99..f63e35b5f 100644 --- a/shared/src/i18n/id/pdf.ts +++ b/shared/src/i18n/id/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Rencana Perjalanan', 'pdf.planned': 'Direncanakan', - 'pdf.costLabel': 'Biaya EUR', + 'pdf.costLabel': 'Biaya', 'pdf.preview': 'Pratinjau PDF', 'pdf.saveAsPdf': 'Simpan sebagai PDF', }; diff --git a/shared/src/i18n/it/pdf.ts b/shared/src/i18n/it/pdf.ts index 57c07fd70..e070d6b3a 100644 --- a/shared/src/i18n/it/pdf.ts +++ b/shared/src/i18n/it/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Programma di viaggio', 'pdf.planned': 'Programmato', - 'pdf.costLabel': 'Costo EUR', + 'pdf.costLabel': 'Costo', 'pdf.preview': 'Anteprima PDF', 'pdf.saveAsPdf': 'Salva come PDF', }; diff --git a/shared/src/i18n/ja/pdf.ts b/shared/src/i18n/ja/pdf.ts index 5dd39fda5..77b45b604 100644 --- a/shared/src/i18n/ja/pdf.ts +++ b/shared/src/i18n/ja/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': '旅行計画', 'pdf.planned': '予定', - 'pdf.costLabel': '費用(EUR)', + 'pdf.costLabel': '費用', 'pdf.preview': 'PDFプレビュー', 'pdf.saveAsPdf': 'PDFとして保存', }; diff --git a/shared/src/i18n/ko/pdf.ts b/shared/src/i18n/ko/pdf.ts index f6ca14170..58950e70f 100644 --- a/shared/src/i18n/ko/pdf.ts +++ b/shared/src/i18n/ko/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': '여행 계획', 'pdf.planned': '계획됨', - 'pdf.costLabel': '비용 (원)', + 'pdf.costLabel': '비용', 'pdf.preview': 'PDF 미리보기', 'pdf.saveAsPdf': 'PDF로 저장', }; diff --git a/shared/src/i18n/nl/pdf.ts b/shared/src/i18n/nl/pdf.ts index 89cc8a246..751200f91 100644 --- a/shared/src/i18n/nl/pdf.ts +++ b/shared/src/i18n/nl/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Reisplan', 'pdf.planned': 'Gepland', - 'pdf.costLabel': 'Kosten EUR', + 'pdf.costLabel': 'Kosten', 'pdf.preview': 'PDF-voorbeeld', 'pdf.saveAsPdf': 'Opslaan als PDF', }; diff --git a/shared/src/i18n/pl/pdf.ts b/shared/src/i18n/pl/pdf.ts index 3b9f89e64..45863f772 100644 --- a/shared/src/i18n/pl/pdf.ts +++ b/shared/src/i18n/pl/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Plan podróży', 'pdf.planned': 'Zaplanowane', - 'pdf.costLabel': 'Koszt w EUR', + 'pdf.costLabel': 'Koszt', 'pdf.preview': 'Podgląd PDF', 'pdf.saveAsPdf': 'Zapisz jako PDF', }; diff --git a/shared/src/i18n/ru/pdf.ts b/shared/src/i18n/ru/pdf.ts index a8c02d050..38e42597b 100644 --- a/shared/src/i18n/ru/pdf.ts +++ b/shared/src/i18n/ru/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'План поездки', 'pdf.planned': 'Запланировано', - 'pdf.costLabel': 'Стоимость EUR', + 'pdf.costLabel': 'Стоимость', 'pdf.preview': 'Предпросмотр PDF', 'pdf.saveAsPdf': 'Сохранить как PDF', }; diff --git a/shared/src/i18n/sv/pdf.ts b/shared/src/i18n/sv/pdf.ts index 56756ce01..18e57da00 100644 --- a/shared/src/i18n/sv/pdf.ts +++ b/shared/src/i18n/sv/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Resplan', 'pdf.planned': 'Planerat', - 'pdf.costLabel': 'Kostnad EUR', + 'pdf.costLabel': 'Kostnad', 'pdf.preview': 'Förhandsgranskning av PDF', 'pdf.saveAsPdf': 'Spara som PDF', }; diff --git a/shared/src/i18n/tr/pdf.ts b/shared/src/i18n/tr/pdf.ts index f38bb7552..d0c67970b 100644 --- a/shared/src/i18n/tr/pdf.ts +++ b/shared/src/i18n/tr/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Seyahat Planı', 'pdf.planned': 'Planlandı', - 'pdf.costLabel': 'Maliyet EUR', + 'pdf.costLabel': 'Maliyet', 'pdf.preview': 'PDF Önizleme', 'pdf.saveAsPdf': 'PDF olarak Kaydet', }; diff --git a/shared/src/i18n/uk/pdf.ts b/shared/src/i18n/uk/pdf.ts index 30073b81a..415d46a00 100644 --- a/shared/src/i18n/uk/pdf.ts +++ b/shared/src/i18n/uk/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'План поїздки', 'pdf.planned': 'Заплановано', - 'pdf.costLabel': 'Вартість EUR', + 'pdf.costLabel': 'Вартість', 'pdf.preview': 'Попередній перегляд PDF', 'pdf.saveAsPdf': 'Зберегти як PDF', }; diff --git a/shared/src/i18n/vi/pdf.ts b/shared/src/i18n/vi/pdf.ts index 68decf5d4..d8bfe1618 100644 --- a/shared/src/i18n/vi/pdf.ts +++ b/shared/src/i18n/vi/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': 'Kế hoạch du lịch', 'pdf.planned': 'Đã lên kế hoạch', - 'pdf.costLabel': 'Chi phí EUR', + 'pdf.costLabel': 'Chi phí', 'pdf.preview': 'PDF Xem trước', 'pdf.saveAsPdf': 'Lưu dưới dạng PDF', }; diff --git a/shared/src/i18n/zh-TW/pdf.ts b/shared/src/i18n/zh-TW/pdf.ts index 9636e197e..8c0e4bcd7 100644 --- a/shared/src/i18n/zh-TW/pdf.ts +++ b/shared/src/i18n/zh-TW/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': '旅行計劃', 'pdf.planned': '已規劃', - 'pdf.costLabel': '費用 EUR', + 'pdf.costLabel': '費用', 'pdf.preview': 'PDF 預覽', 'pdf.saveAsPdf': '儲存為 PDF', }; diff --git a/shared/src/i18n/zh/pdf.ts b/shared/src/i18n/zh/pdf.ts index 2eb2e11cf..b7634db07 100644 --- a/shared/src/i18n/zh/pdf.ts +++ b/shared/src/i18n/zh/pdf.ts @@ -3,7 +3,7 @@ import type { TranslationStrings } from '../types'; const pdf: TranslationStrings = { 'pdf.travelPlan': '旅行计划', 'pdf.planned': '已规划', - 'pdf.costLabel': '费用 EUR', + 'pdf.costLabel': '费用', 'pdf.preview': 'PDF 预览', 'pdf.saveAsPdf': '保存为 PDF', };