feat(pdf): transport notes in the trip PDF, with a switch in the preview

The notes of a flight, train or rental print under its booking code, like the notes of a place. A Transport notes switch next to Page break per day leaves them out and is remembered; it only shows when a transport has a note. (#1571)
This commit is contained in:
Maurice
2026-10-01 09:30:43 +02:00
parent f7782b9ef7
commit b4f0a6dea3
29 changed files with 135 additions and 20 deletions
+50
View File
@@ -1363,3 +1363,53 @@ describe('a printed day follows the plan (#1978)', () => {
expect(seen).toEqual([...seen].sort((a, b) => a - b))
})
})
// ── Transport notes (#1571) ─────────────────────────────────────────────────
describe('downloadTripPDF — transport notes', () => {
const withNote = { ...transportReservation, notes: 'Seat 14A\nCheck in online the day before' }
beforeEach(() => {
localStorage.removeItem('trek_pdf_transport_notes')
})
const notesToggle = () => document.querySelector<HTMLButtonElement>('#pdf-transport-notes-toggle')
it('FE-PDF-TNOTES-001: prints the note of a transport, escaped, and offers the switch', async () => {
await downloadTripPDF({ ...richArgs, reservations: [{ ...withNote, notes: 'Gate <b>B12</b>' }] })
const html = getIframe()!.srcdoc
expect(html).toContain('<div class="note-time transport-note">Gate &lt;b&gt;B12&lt;/b&gt;</div>')
expect(html).not.toContain('pdf-no-transport-notes"')
expect(notesToggle()!.getAttribute('aria-checked')).toBe('true')
})
it('FE-PDF-TNOTES-002: without any transport note there is nothing to switch', async () => {
await downloadTripPDF(richArgs)
expect(getIframe()!.srcdoc).not.toContain('class="note-time transport-note"')
expect(notesToggle()).toBeNull()
// The page break switch is still there on its own.
expect(document.querySelector('#pdf-daybreak-toggle')).not.toBeNull()
})
it('FE-PDF-TNOTES-003: turning the notes off hides them at once and is remembered', async () => {
await downloadTripPDF({ ...richArgs, reservations: [withNote] })
notesToggle()!.click()
expect(notesToggle()!.getAttribute('aria-checked')).toBe('false')
expect(localStorage.getItem('trek_pdf_transport_notes')).toBe('0')
const body = getIframe()!.contentDocument?.body
if (body) expect(body.classList.contains('pdf-no-transport-notes')).toBe(true)
})
it('FE-PDF-TNOTES-004: the remembered choice comes back, next to the flowing layout', async () => {
localStorage.setItem('trek_pdf_transport_notes', '0')
localStorage.setItem('trek_pdf_page_break_per_day', '0')
await downloadTripPDF({ ...richArgs, reservations: [withNote] })
expect(getIframe()!.srcdoc).toContain('<body class="pdf-flow pdf-no-transport-notes">')
expect(notesToggle()!.getAttribute('aria-checked')).toBe('false')
localStorage.removeItem('trek_pdf_page_break_per_day')
})
})
+58 -20
View File
@@ -29,17 +29,25 @@ import { onlyMyPlan } from './pdfScope'
*/
const PAGE_BREAK_KEY = 'trek_pdf_page_break_per_day'
function pageBreakPerDay(): boolean {
/**
* The notes of a flight, train or rental (#1571): printed by default, like the
* notes of a place, and a switch in the preview leaves them out for a reader who
* wants the plan without the fine print.
*/
const TRANSPORT_NOTES_KEY = 'trek_pdf_transport_notes'
/** A preview switch that is on unless it was turned off; both default to on. */
function pdfSwitchOn(key: string): boolean {
try {
return localStorage.getItem(PAGE_BREAK_KEY) !== '0'
return localStorage.getItem(key) !== '0'
} catch {
return true
}
}
function rememberPageBreakPerDay(on: boolean): void {
function rememberPdfSwitch(key: string, on: boolean): void {
try {
localStorage.setItem(PAGE_BREAK_KEY, on ? '1' : '0')
localStorage.setItem(key, on ? '1' : '0')
} catch {
// A blocked localStorage costs the preference, not the export.
}
@@ -231,7 +239,10 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
? onlyMyPlan(allStored, allReservations, onlyUserId)
: { assignments: allStored, reservations: allReservations }
const assignments = planAssignments(stored, showServiceStops)
const breaksPerDay = pageBreakPerDay()
const breaksPerDay = pdfSwitchOn(PAGE_BREAK_KEY)
const showTransportNotes = pdfSwitchOn(TRANSPORT_NOTES_KEY)
// Set while the days render: the switch only shows when there is a note to hide.
let hasTransportNotes = false
const loc = _locale || undefined
const tr = _t || (k => k)
// The store read is the fallback, not the source: a caller that forgets the
@@ -516,6 +527,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
const endTime = phase === 'single' ? fmtTime(splitReservationDateTime(r.reservation_end_time).time) : ''
const time = [startTime, endTime].filter(Boolean).join(' – ')
const titleHtml = `${spanLabel ? escHtml(spanLabel) + ': ' : ''}${escHtml(r.title)}`
const transportNote = typeof r.notes === 'string' ? r.notes.trim() : ''
if (transportNote) hasTransportNotes = true
return `
<div class="note-card" style="border-left: 3px solid ${color};">
<div class="note-line" style="background: ${color};"></div>
@@ -525,6 +538,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
${subtitleLines.filter(Boolean).map(s => `<div class="note-time">${escHtml(s)}</div>`).join('')}
${locationLine ? `<div class="note-time">${escHtml(locationLine)}</div>` : ''}
${r.confirmation_number ? `<div class="note-time" style="font-size:9px;">Code: ${escHtml(r.confirmation_number)}</div>` : ''}
${transportNote ? `<div class="note-time transport-note">${escHtml(transportNote)}</div>` : ''}
</div>
</div>`
}
@@ -751,6 +765,8 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
Flowing days butt against each other without the page edge between them. */
.day-break { page-break-before: always; }
.pdf-flow .day-break { page-break-before: auto; }
.transport-note { font-style: italic; white-space: pre-line; }
.pdf-no-transport-notes .transport-note { display: none; }
.pdf-flow .day-section + .day-section { margin-top: 18px; }
/* Hold a flowing day together. Without this the header bar can be placed at the
foot of a sheet while its content moves to the next one, which then repeats
@@ -868,7 +884,7 @@ export async function downloadTripPDF({ trip, days, places, assignments: allStor
}
</style>
</head>
<body${breaksPerDay ? '' : ' class="pdf-flow"'}>
<body${bodyClassAttr(breaksPerDay, showTransportNotes)}>
<!-- Footer on every page -->
<div class="pdf-footer">
@@ -937,10 +953,8 @@ ${pluginSectionsHtml}
header.innerHTML = `
<span style="font-size:13px;font-weight:600;color:var(--text-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(trip?.title || tr('pdf.travelPlan'))}</span>
<div style="display:flex;align-items:center;gap:8px">
<label for="pdf-daybreak-toggle" style="font-size:12px;color:var(--text-muted);cursor:pointer;user-select:none">${escHtml(tr('pdf.pageBreakPerDay'))}</label>
<button id="pdf-daybreak-toggle" type="button" role="switch" aria-checked="${breaksPerDay}" aria-label="${escHtml(tr('pdf.pageBreakPerDay'))}" style="${TOGGLE_TRACK} background:${trackColour(breaksPerDay)}">
<span style="${TOGGLE_KNOB} left:${knobOffset(breaksPerDay)}"></span>
</button>
${hasTransportNotes ? previewSwitch('pdf-transport-notes-toggle', tr('pdf.transportNotes'), showTransportNotes) : ''}
${previewSwitch('pdf-daybreak-toggle', tr('pdf.pageBreakPerDay'), breaksPerDay)}
<button type="button" id="pdf-print-btn" style="display:flex;align-items:center;gap:5px;font-size:12px;font-weight:500;color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px 8px;border-radius:6px;font-family:inherit">${tr('pdf.saveAsPdf')}</button>
<button type="button" id="pdf-close-btn" style="background:none;border:none;cursor:pointer;color:var(--text-faint);display:flex;padding:4px;border-radius:6px">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
@@ -965,16 +979,40 @@ ${pluginSectionsHtml}
const printBtn = header.querySelector<HTMLElement>('#pdf-print-btn')
if (printBtn) printBtn.onclick = () => { iframe.contentWindow?.print() }
// The two layouts differ by one class, so switching is instant in the preview
// and there is no need to re-fetch photos or rebuild the document.
const dayBreakSwitch = header.querySelector<HTMLButtonElement>('#pdf-daybreak-toggle')
if (dayBreakSwitch) dayBreakSwitch.onclick = () => {
const on = dayBreakSwitch.getAttribute('aria-checked') !== 'true'
dayBreakSwitch.setAttribute('aria-checked', String(on))
dayBreakSwitch.style.background = trackColour(on)
const knob = dayBreakSwitch.firstElementChild as HTMLElement | null
if (knob) knob.style.left = knobOffset(on)
rememberPageBreakPerDay(on)
// Both choices differ by one class on <body>, so switching is instant in the
// preview and there is no need to re-fetch photos or rebuild the document.
wirePreviewSwitch(header, 'pdf-daybreak-toggle', on => {
rememberPdfSwitch(PAGE_BREAK_KEY, on)
iframe.contentDocument?.body.classList.toggle('pdf-flow', !on)
})
wirePreviewSwitch(header, 'pdf-transport-notes-toggle', on => {
rememberPdfSwitch(TRANSPORT_NOTES_KEY, on)
iframe.contentDocument?.body.classList.toggle('pdf-no-transport-notes', !on)
})
}
function bodyClassAttr(breaksPerDay: boolean, showTransportNotes: boolean): string {
const classes = [!breaksPerDay && 'pdf-flow', !showTransportNotes && 'pdf-no-transport-notes'].filter(Boolean)
return classes.length ? ` class="${classes.join(' ')}"` : ''
}
/** A labelled switch for the preview header, as markup; wirePreviewSwitch gives it behaviour. */
function previewSwitch(id: string, label: string, on: boolean): string {
return `<label for="${id}" style="font-size:12px;color:var(--text-muted);cursor:pointer;user-select:none">${escHtml(label)}</label>
<button id="${id}" type="button" role="switch" aria-checked="${on}" aria-label="${escHtml(label)}" style="${TOGGLE_TRACK} background:${trackColour(on)}">
<span style="${TOGGLE_KNOB} left:${knobOffset(on)}"></span>
</button>`
}
function wirePreviewSwitch(header: HTMLElement, id: string, onChange: (on: boolean) => void): void {
const button = header.querySelector<HTMLButtonElement>(`#${id}`)
if (!button) return
button.onclick = () => {
const on = button.getAttribute('aria-checked') !== 'true'
button.setAttribute('aria-checked', String(on))
button.style.background = trackColour(on)
const knob = button.firstElementChild as HTMLElement | null
if (knob) knob.style.left = knobOffset(on)
onChange(on)
}
}
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'معاينة PDF',
'pdf.saveAsPdf': 'حفظ كـ PDF',
'pdf.pageBreakPerDay': 'فاصل صفحة لكل يوم',
'pdf.transportNotes': 'ملاحظات التنقل',
'pdf.mapTitle': 'نظرة عامة على المسار',
'pdf.distanceLabel': 'المسافة',
'pdf.mapCredit': 'حدود الدول: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF önizləməsi',
'pdf.saveAsPdf': 'PDF kimi yadda saxla',
'pdf.pageBreakPerDay': 'Hər gün üçün yeni səhifə',
'pdf.transportNotes': 'Nəqliyyat qeydləri',
'pdf.mapTitle': 'Marşrutun icmalı',
'pdf.distanceLabel': 'Məsafə',
'pdf.mapCredit': 'Ölkə sərhədləri: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Pré-visualização do PDF',
'pdf.saveAsPdf': 'Salvar como PDF',
'pdf.pageBreakPerDay': 'Quebra de página por dia',
'pdf.transportNotes': 'Notas de transporte',
'pdf.mapTitle': 'Visão geral da rota',
'pdf.distanceLabel': 'Distância',
'pdf.mapCredit': 'Contornos dos países: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Vista prèvia del PDF',
'pdf.saveAsPdf': 'Desa com a PDF',
'pdf.pageBreakPerDay': 'Salt de pàgina per dia',
'pdf.transportNotes': 'Notes del transport',
'pdf.mapTitle': 'Resum de la ruta',
'pdf.distanceLabel': 'Distància',
'pdf.mapCredit': 'Contorns dels països: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Náhled PDF',
'pdf.saveAsPdf': 'Uložit jako PDF',
'pdf.pageBreakPerDay': 'Zalomení stránky pro každý den',
'pdf.transportNotes': 'Poznámky k dopravě',
'pdf.mapTitle': 'Přehled trasy',
'pdf.distanceLabel': 'Vzdálenost',
'pdf.mapCredit': 'Obrysy zemí: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF Vorschau',
'pdf.saveAsPdf': 'Als PDF speichern',
'pdf.pageBreakPerDay': 'Seitenumbruch pro Tag',
'pdf.transportNotes': 'Notizen zu Transporten',
'pdf.mapTitle': 'Routenübersicht',
'pdf.distanceLabel': 'Strecke',
'pdf.mapCredit': 'Ländergrenzen: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF Preview',
'pdf.saveAsPdf': 'Save as PDF',
'pdf.pageBreakPerDay': 'Page break per day',
'pdf.transportNotes': 'Transport notes',
'pdf.mapTitle': 'Route overview',
'pdf.distanceLabel': 'Distance',
'pdf.mapCredit': 'Country outlines: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Vista previa PDF',
'pdf.saveAsPdf': 'Guardar como PDF',
'pdf.pageBreakPerDay': 'Salto de página por día',
'pdf.transportNotes': 'Notas de transporte',
'pdf.mapTitle': 'Resumen de la ruta',
'pdf.distanceLabel': 'Distancia',
'pdf.mapCredit': 'Contornos de países: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF-i eelvaade',
'pdf.saveAsPdf': 'Salvesta PDF-ina',
'pdf.pageBreakPerDay': 'Iga päev uuel lehel',
'pdf.transportNotes': 'Transpordi märkmed',
'pdf.mapTitle': 'Marsruudi ülevaade',
'pdf.distanceLabel': 'Vahemaa',
'pdf.mapCredit': 'Riigipiirid: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Aperçu PDF',
'pdf.saveAsPdf': 'Enregistrer en PDF',
'pdf.pageBreakPerDay': 'Saut de page par jour',
'pdf.transportNotes': 'Notes de transport',
'pdf.mapTitle': 'Aperçu de l\'itinéraire',
'pdf.distanceLabel': 'Distance',
'pdf.mapCredit': 'Contours des pays : geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Προεπισκόπηση PDF',
'pdf.saveAsPdf': 'Αποθήκευση ως PDF',
'pdf.pageBreakPerDay': 'Αλλαγή σελίδας ανά ημέρα',
'pdf.transportNotes': 'Σημειώσεις μεταφορών',
'pdf.mapTitle': 'Επισκόπηση διαδρομής',
'pdf.distanceLabel': 'Απόσταση',
'pdf.mapCredit': 'Περιγράμματα χωρών: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF előnézet',
'pdf.saveAsPdf': 'Mentés PDF-ként',
'pdf.pageBreakPerDay': 'Oldaltörés naponta',
'pdf.transportNotes': 'Közlekedési jegyzetek',
'pdf.mapTitle': 'Útvonal áttekintése',
'pdf.distanceLabel': 'Távolság',
'pdf.mapCredit': 'Országhatárok: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Pratinjau PDF',
'pdf.saveAsPdf': 'Simpan sebagai PDF',
'pdf.pageBreakPerDay': 'Pemisah halaman per hari',
'pdf.transportNotes': 'Catatan transportasi',
'pdf.mapTitle': 'Ringkasan rute',
'pdf.distanceLabel': 'Jarak',
'pdf.mapCredit': 'Garis batas negara: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Anteprima PDF',
'pdf.saveAsPdf': 'Salva come PDF',
'pdf.pageBreakPerDay': 'Interruzione di pagina per giorno',
'pdf.transportNotes': 'Note sui trasporti',
'pdf.mapTitle': 'Panoramica del percorso',
'pdf.distanceLabel': 'Distanza',
'pdf.mapCredit': 'Confini dei paesi: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDFプレビュー',
'pdf.saveAsPdf': 'PDFとして保存',
'pdf.pageBreakPerDay': '日ごとに改ページ',
'pdf.transportNotes': '交通機関のメモ',
'pdf.mapTitle': 'ルート概要',
'pdf.distanceLabel': '距離',
'pdf.mapCredit': '国境線: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF 미리보기',
'pdf.saveAsPdf': 'PDF로 저장',
'pdf.pageBreakPerDay': '날짜별 페이지 나누기',
'pdf.transportNotes': '교통편 메모',
'pdf.mapTitle': '경로 개요',
'pdf.distanceLabel': '거리',
'pdf.mapCredit': '국가 윤곽: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF-voorbeeld',
'pdf.saveAsPdf': 'Opslaan als PDF',
'pdf.pageBreakPerDay': 'Pagina-einde per dag',
'pdf.transportNotes': 'Notities bij vervoer',
'pdf.mapTitle': 'Routeoverzicht',
'pdf.distanceLabel': 'Afstand',
'pdf.mapCredit': 'Landcontouren: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Podgląd PDF',
'pdf.saveAsPdf': 'Zapisz jako PDF',
'pdf.pageBreakPerDay': 'Podział strony dla każdego dnia',
'pdf.transportNotes': 'Notatki do transportu',
'pdf.mapTitle': 'Przegląd trasy',
'pdf.distanceLabel': 'Dystans',
'pdf.mapCredit': 'Kontury krajów: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Предпросмотр PDF',
'pdf.saveAsPdf': 'Сохранить как PDF',
'pdf.pageBreakPerDay': 'Разрыв страницы для каждого дня',
'pdf.transportNotes': 'Заметки к транспорту',
'pdf.mapTitle': 'Обзор маршрута',
'pdf.distanceLabel': 'Расстояние',
'pdf.mapCredit': 'Контуры стран: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Náhľad PDF',
'pdf.saveAsPdf': 'Uložiť ako PDF',
'pdf.pageBreakPerDay': 'Zalomenie strany pre každý deň',
'pdf.transportNotes': 'Poznámky k doprave',
'pdf.mapTitle': 'Prehľad trasy',
'pdf.distanceLabel': 'Vzdialenosť',
'pdf.mapCredit': 'Obrysy krajín: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Förhandsgranskning av PDF',
'pdf.saveAsPdf': 'Spara som PDF',
'pdf.pageBreakPerDay': 'Sidbrytning per dag',
'pdf.transportNotes': 'Anteckningar om transporter',
'pdf.mapTitle': 'Ruttöversikt',
'pdf.distanceLabel': 'Sträcka',
'pdf.mapCredit': 'Landskonturer: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': "ดูตัวอย่าง PDF",
'pdf.saveAsPdf': "บันทึกเป็น PDF",
'pdf.pageBreakPerDay': "ขึ้นหน้าใหม่ทุกวัน",
'pdf.transportNotes': 'บันทึกการเดินทาง',
'pdf.mapTitle': "ภาพรวมเส้นทาง",
'pdf.distanceLabel': "ระยะทาง",
'pdf.mapCredit': "ขอบเขตประเทศ: geoBoundaries (CC BY 4.0)",
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF Önizleme',
'pdf.saveAsPdf': 'PDF olarak Kaydet',
'pdf.pageBreakPerDay': 'Her gün için sayfa sonu',
'pdf.transportNotes': 'Ulaşım notları',
'pdf.mapTitle': 'Rota genel görünümü',
'pdf.distanceLabel': 'Mesafe',
'pdf.mapCredit': 'Ülke sınırları: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'Попередній перегляд PDF',
'pdf.saveAsPdf': 'Зберегти як PDF',
'pdf.pageBreakPerDay': 'Розрив сторінки для кожного дня',
'pdf.transportNotes': 'Нотатки до транспорту',
'pdf.mapTitle': 'Огляд маршруту',
'pdf.distanceLabel': 'Відстань',
'pdf.mapCredit': 'Контури країн: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF Xem trước',
'pdf.saveAsPdf': 'Lưu dưới dạng PDF',
'pdf.pageBreakPerDay': 'Ngắt trang cho mỗi ngày',
'pdf.transportNotes': 'Ghi chú phương tiện',
'pdf.mapTitle': 'Tổng quan lộ trình',
'pdf.distanceLabel': 'Quãng đường',
'pdf.mapCredit': 'Đường viền quốc gia: geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF 預覽',
'pdf.saveAsPdf': '儲存為 PDF',
'pdf.pageBreakPerDay': '每天分頁',
'pdf.transportNotes': '交通備註',
'pdf.mapTitle': '路線總覽',
'pdf.distanceLabel': '距離',
'pdf.mapCredit': '國家輪廓:geoBoundaries (CC BY 4.0)',
+1
View File
@@ -7,6 +7,7 @@ const pdf: TranslationStrings = {
'pdf.preview': 'PDF 预览',
'pdf.saveAsPdf': '保存为 PDF',
'pdf.pageBreakPerDay': '每天分页',
'pdf.transportNotes': '交通备注',
'pdf.mapTitle': '路线总览',
'pdf.distanceLabel': '距离',
'pdf.mapCredit': '国家轮廓:geoBoundaries (CC BY 4.0)',