Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
jubnl
2026-07-29 21:40:35 +02:00
28 changed files with 234 additions and 40 deletions
+119
View File
@@ -912,3 +912,122 @@ describe('downloadTripPDF defaults', () => {
expect(srcdoc()).toContain('Day 1')
})
})
// #1292 — every day started a new page, so a plan of short days printed one
// sheet per handful of lines. The flowing layout is opt-in and remembered.
describe('page breaks between days (#1292)', () => {
const twoDays = {
...minimalArgs,
days: [
{ id: 1, day_number: 1, title: 'First', date: '2025-06-01' },
{ id: 2, day_number: 2, title: 'Second', date: '2025-06-02' },
] as any[],
}
beforeEach(() => {
localStorage.removeItem('trek_pdf_page_break_per_day')
})
const toggle = () =>
document.querySelector<HTMLButtonElement>('#pdf-daybreak-toggle')
const isOn = () => toggle()!.getAttribute('aria-checked') === 'true'
it('FE-PDF-BREAK-001: days break onto their own page by default', async () => {
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
expect(html).toContain('day-section day-break')
expect(html).not.toContain('class="pdf-flow"')
expect(isOn()).toBe(true)
})
it('FE-PDF-BREAK-001b: the control is the switch from the rest of the app, not a checkbox', async () => {
await downloadTripPDF(twoDays)
const el = toggle()!
expect(el.tagName).toBe('BUTTON')
expect(el.getAttribute('role')).toBe('switch')
expect(el.style.background).toContain('--accent')
expect(document.querySelector('#pdf-daybreak-toggle input')).toBeNull()
})
it('FE-PDF-BREAK-002: the remembered flowing layout comes back on the next export', async () => {
localStorage.setItem('trek_pdf_page_break_per_day', '0')
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
// The class stays on the day; the body is what decides whether it breaks.
expect(html).toContain('day-section day-break')
expect(html).toContain('<body class="pdf-flow">')
expect(isOn()).toBe(false)
// Off uses the neutral track, so the two states are told apart by more than the knob.
expect(toggle()!.style.background).toContain('--border-primary')
})
it('FE-PDF-BREAK-003: the first day never carries a break of its own', async () => {
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
expect(html.indexOf('<table class="day-section">')).toBeGreaterThan(-1)
expect(html.match(/day-section day-break/g)).toHaveLength(1)
})
it('FE-PDF-BREAK-004: turning the breaks off is remembered and shown at once', async () => {
await downloadTripPDF(twoDays)
toggle()!.click()
expect(isOn()).toBe(false)
expect(localStorage.getItem('trek_pdf_page_break_per_day')).toBe('0')
const body = getIframe()!.contentDocument?.body
if (body) expect(body.classList.contains('pdf-flow')).toBe(true)
})
it('FE-PDF-BREAK-005: turning them back on is remembered too', async () => {
localStorage.setItem('trek_pdf_page_break_per_day', '0')
await downloadTripPDF(twoDays)
toggle()!.click()
expect(isOn()).toBe(true)
expect(localStorage.getItem('trek_pdf_page_break_per_day')).toBe('1')
const body = getIframe()!.contentDocument?.body
if (body) expect(body.classList.contains('pdf-flow')).toBe(false)
})
it('FE-PDF-BREAK-006: a storage the browser blocks costs the preference, not the export', async () => {
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('blocked') })
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('blocked') })
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
expect(html).toContain('day-section day-break')
expect(html).not.toContain('class="pdf-flow"')
expect(() => toggle()!.click()).not.toThrow()
})
// Verified against a real print engine: without this rule Chromium tore a stay
// across the page edge, the check-in time on one sheet and the hotel name on
// the next. Place and note cards already carried it.
it('FE-PDF-BREAK-007: a stay may not be split by a page edge', async () => {
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
const rule = html.slice(html.indexOf('.day-accommodation {'))
expect(rule.slice(0, rule.indexOf('}'))).toContain('break-inside: avoid')
})
// Measured against Chromium: with the days flowing, a header bar could be left
// at the foot of a sheet while its content moved on, and the repeat from #1471
// then read as the same day printed twice. break-after on the thead and
// break-before on the tbody are both ignored; holding the day together is what
// works, and a day too long for one page still breaks and still repeats.
it('FE-PDF-BREAK-008: a flowing day is held together so its header is never stranded', async () => {
localStorage.setItem('trek_pdf_page_break_per_day', '0')
await downloadTripPDF(twoDays)
const html = getIframe()!.srcdoc
expect(html).toContain('.pdf-flow .day-section { break-inside: avoid; page-break-inside: avoid; }')
// Scoped: a day that starts its own page cannot strand a header.
const at = html.indexOf('.day-section { break-inside: avoid')
expect(html.slice(at - 10, at)).toContain('.pdf-flow ')
})
})
+71 -4
View File
@@ -9,6 +9,37 @@ import { formatMoney, formatMoneySum, splitReservationDateTime, type MoneyEntry
import { fetchExchangeRates } from '../../hooks/useExchangeRates'
import { getFlightLegs, getTrainLegs } from '../../utils/flightLegs'
/**
* Every day starts a new page by default. On a trip of short days that prints
* one sheet per handful of lines, so the preview offers the flowing layout the
* in-app view already uses, and remembers which one was picked (#1292).
*/
const PAGE_BREAK_KEY = 'trek_pdf_page_break_per_day'
function pageBreakPerDay(): boolean {
try {
return localStorage.getItem(PAGE_BREAK_KEY) !== '0'
} catch {
return true
}
}
function rememberPageBreakPerDay(on: boolean): void {
try {
localStorage.setItem(PAGE_BREAK_KEY, on ? '1' : '0')
} catch {
// A blocked localStorage costs the preference, not the export.
}
}
/* The overlay is imperative DOM, so the switch from Settings/ToggleSwitch is
rebuilt here rather than rendered — as static markup it would carry no
behaviour. Same geometry and the same tokens, so it reads as the same control. */
const TOGGLE_TRACK = 'position:relative;width:44px;height:24px;min-width:44px;flex-shrink:0;border-radius:12px;border:none;padding:0;cursor:pointer;transition:background 0.2s;'
const TOGGLE_KNOB = 'position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;transition:left 0.2s;box-shadow:0 1px 3px rgba(0,0,0,0.2);'
const trackColour = (on: boolean) => (on ? 'var(--accent, #111827)' : 'var(--border-primary, #d1d5db)')
const knobOffset = (on: boolean) => (on ? '22px' : '2px')
function renderLucideIcon(icon:LucideIcon, props = {}) {
if (!_renderToStaticMarkup) return ''
return _renderToStaticMarkup(
@@ -150,6 +181,7 @@ interface downloadTripPDFProps {
// relies on it being an object.
export async function downloadTripPDF({ trip, days, places, assignments = {}, categories, dayNotes, reservations = [], t: _t, locale: _locale }: downloadTripPDFProps) {
await ensureRenderer()
const breaksPerDay = pageBreakPerDay()
const loc = _locale || undefined
const tr = _t || (k => k)
const sorted = [...(days || [])].sort((a, b) => a.day_number - b.day_number)
@@ -403,7 +435,7 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca
// every page an overflowing day spills onto (#1471). CSS `table-header-group`
// on a <div> is NOT repeated by Chromium's print engine — only real thead is.
return `
<table class="day-section${di > 0 ? ' page-break' : ''}">
<table class="day-section${di > 0 ? ' day-break' : ''}">
<thead class="day-header"><tr><td>
<div class="day-header-bar">
<span class="day-tag">${escHtml(tr('dayplan.dayN', { n: day.day_number })).toUpperCase()}</span>
@@ -505,6 +537,20 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca
/* ── Day ───────────────────────────────────────── */
/* .day-section is a real <table>; its <thead> day header repeats on overflow pages. */
.page-break { page-break-before: always; }
/* Days break by default; .pdf-flow on <body> is the toggle in the preview (#1292).
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; }
.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
the header (#1471) and reads as the same day printed twice. A day too long
for one page still breaks and still repeats its header — the engine only
honours this where it can. Days that start a page of their own cannot strand
a header, so it is scoped to the flowing layout.
Measured, not assumed: break-after on the thead and break-before on the tbody
are both ignored by Chromium here. */
.pdf-flow .day-section { break-inside: avoid; page-break-inside: avoid; }
.day-section { width: 100%; border-collapse: collapse; table-layout: fixed; }
.day-header-bar {
background: #0f172a; padding: 11px 28px;
@@ -528,6 +574,10 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca
flex: 1 1 45%; min-width: 200px; margin: 4px 0; padding: 10px;
border: 2px solid #e2e8f0; border-radius: 12px;
display: flex; flex-direction: column;
/* Place and note cards have said this all along; without it a stay is torn
in half by a page edge, its check-in time on one sheet and the hotel name
on the next. Rare while every day started a page, common once they flow. */
break-inside: avoid; page-break-inside: avoid;
}
.day-accommodation-title {
font-size: 16px; font-weight: 600; text-align: center;
@@ -608,7 +658,7 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca
}
</style>
</head>
<body>
<body${breaksPerDay ? '' : ' class="pdf-flow"'}>
<!-- Footer on every page -->
<div class="pdf-footer">
@@ -666,10 +716,14 @@ ${pluginSectionsHtml}
card.style.cssText = 'width:100%;max-width:1000px;height:95vh;background:var(--bg-card);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);'
const header = document.createElement('div')
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border-primary);flex-shrink:0;'
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;padding:10px 16px;border-bottom:1px solid var(--border-primary);flex-shrink:0;'
header.innerHTML = `
<span style="font-size:13px;font-weight:600;color:var(--text-primary)">${escHtml(trip?.title || tr('pdf.travelPlan'))}</span>
<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>
<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 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>
@@ -693,4 +747,17 @@ ${pluginSectionsHtml}
if (closeBtn) closeBtn.onclick = () => overlay.remove()
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)
iframe.contentDocument?.body.classList.toggle('pdf-flow', !on)
}
}
@@ -195,7 +195,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
const [lockedIds, setLockedIds] = useState(new Set())
const [lockHoverId, setLockHoverId] = useState(null)
const [undoHover, setUndoHover] = useState(false)
const [pdfHover, setPdfHover] = useState(false)
const [icsHover, setIcsHover] = useState(false)
const [hoveredAssignmentId, setHoveredAssignmentId] = useState<number | null>(null)
// Transit rows fold their itinerary out inline (#1065).
@@ -984,8 +983,6 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
setLockHoverId,
undoHover,
setUndoHover,
pdfHover,
setPdfHover,
icsHover,
setIcsHover,
hoveredAssignmentId,
@@ -1155,8 +1152,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
setLockHoverId,
undoHover,
setUndoHover,
pdfHover,
setPdfHover,
icsHover,
setIcsHover,
hoveredAssignmentId,
@@ -1267,8 +1262,6 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
t={t}
locale={locale}
toast={toast}
pdfHover={pdfHover}
setPdfHover={setPdfHover}
icsHover={icsHover}
setIcsHover={setIcsHover}
expandedDays={expandedDays}
@@ -46,8 +46,6 @@ function makeProps(overrides: Partial<React.ComponentProps<typeof DayPlanSidebar
t,
locale: 'en-US',
toast: makeToast(),
pdfHover: false,
setPdfHover: vi.fn((_v: boolean) => {}),
icsHover: false,
setIcsHover: vi.fn((_v: boolean) => {}),
expandedDays: new Set<number>(),
@@ -97,21 +95,29 @@ describe('DayPlanSidebarToolbar', () => {
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('dayplan.pdfError: font missing'))
})
it('FE-PLANNER-DPTOOLBAR-003: hovering the PDF button reports the hover state upward', async () => {
it('FE-PLANNER-DPTOOLBAR-003: hovering the PDF button shows the export tooltip', async () => {
const user = userEvent.setup()
const setPdfHover = vi.fn((_v: boolean) => {})
render(<DayPlanSidebarToolbar {...makeProps({ setPdfHover })} />)
render(<DayPlanSidebarToolbar {...makeProps()} />)
expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument()
await user.hover(screen.getByText('dayplan.pdf'))
expect(setPdfHover).toHaveBeenCalledWith(true)
await waitFor(() => expect(screen.getByText('dayplan.pdfTooltip')).toBeInTheDocument())
await user.unhover(screen.getByText('dayplan.pdf'))
expect(setPdfHover).toHaveBeenCalledWith(false)
await waitFor(() => expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument())
})
it('FE-PLANNER-DPTOOLBAR-004: pdfHover renders the export tooltip', () => {
const { rerender } = render(<DayPlanSidebarToolbar {...makeProps()} />)
expect(screen.queryByText('dayplan.pdfTooltip')).not.toBeInTheDocument()
rerender(<DayPlanSidebarToolbar {...makeProps({ pdfHover: true })} />)
expect(screen.getByText('dayplan.pdfTooltip')).toBeInTheDocument()
// The button sits at the right edge of the leftmost pane. Its own tooltip was
// anchored with `right: 0` and never wrapped, so a long label ran off the left
// of the window. The shared tooltip is portalled and clamped to the viewport.
it('FE-PLANNER-DPTOOLBAR-004: the export tooltip stays inside the window', async () => {
const user = userEvent.setup()
render(<DayPlanSidebarToolbar {...makeProps()} />)
await user.hover(screen.getByText('dayplan.pdf'))
const tip = await screen.findByText('dayplan.pdfTooltip')
expect(tip.closest('[role="tooltip"]')).not.toBeNull()
expect(tip.parentElement).toBe(document.body)
expect(tip.style.position).toBe('fixed')
// Placed once measured, and never to the left of the window edge.
await waitFor(() => expect(parseFloat(tip.style.left)).toBeGreaterThanOrEqual(0))
})
// ── ICS menu ──────────────────────────────────────────────────────────────
@@ -22,8 +22,6 @@ interface DayPlanSidebarToolbarProps {
t: (key: string, params?: Record<string, any>) => string
locale: string
toast: ReturnType<typeof useToast>
pdfHover: boolean
setPdfHover: (v: boolean) => void
icsHover: boolean
setIcsHover: (v: boolean) => void
expandedDays: Set<number>
@@ -41,7 +39,7 @@ interface DayPlanSidebarToolbarProps {
export function DayPlanSidebarToolbar({
tripId, trip, days, places, categories, assignments, reservations, dayNotes,
allConnectionsShown = false, onToggleAllConnections,
t, locale, toast, pdfHover, setPdfHover, setIcsHover,
t, locale, toast, setIcsHover,
expandedDays, setExpandedDays, onUndo, canUndo, undoHover, setUndoHover, lastActionLabel,
canEditDays, onReorderDays, onAddDay,
}: DayPlanSidebarToolbarProps) {
@@ -74,6 +72,7 @@ export function DayPlanSidebarToolbar({
<div className="border-b border-edge-faint" style={{ padding: '12px 16px', flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 8 }}>
<div style={{ position: 'relative', flexShrink: 0 }}>
<Tooltip label={t('dayplan.pdfTooltip')} placement="bottom">
<button
onClick={async () => {
const flatNotes = Object.entries(dayNotes).flatMap(([dayId, notes]) =>
@@ -86,8 +85,6 @@ export function DayPlanSidebarToolbar({
toast.error(t('dayplan.pdfError') + ': ' + (e?.message || String(e)))
}
}}
onMouseEnter={() => setPdfHover(true)}
onMouseLeave={() => setPdfHover(false)}
className="bg-accent text-accent-text"
style={{
display: 'flex', alignItems: 'center', gap: 5,
@@ -99,18 +96,7 @@ export function DayPlanSidebarToolbar({
<FileDown size={13} strokeWidth={2} />
{t('dayplan.pdf')}
</button>
{pdfHover && (
<div style={{
position: 'absolute', top: 'calc(100% + 6px)', right: 0,
whiteSpace: 'nowrap', pointerEvents: 'none', zIndex: 200,
background: 'var(--bg-card, white)', color: 'var(--text-primary, #111827)',
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, padding: '5px 10px',
borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
border: '1px solid var(--border-faint, #e5e7eb)',
}}>
{t('dayplan.pdfTooltip')}
</div>
)}
</Tooltip>
</div>
<div
style={{ position: 'relative', flexShrink: 0 }}
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'التكلفة',
'pdf.preview': 'معاينة PDF',
'pdf.saveAsPdf': 'حفظ كـ PDF',
'pdf.pageBreakPerDay': 'فاصل صفحة لكل يوم',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Custo',
'pdf.preview': 'Pré-visualização do PDF',
'pdf.saveAsPdf': 'Salvar como PDF',
'pdf.pageBreakPerDay': 'Quebra de página por dia',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Cost EUR',
'pdf.preview': 'Vista prèvia del PDF',
'pdf.saveAsPdf': 'Desa com a PDF',
'pdf.pageBreakPerDay': 'Salt de pàgina per dia',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Náklady',
'pdf.preview': 'Náhled PDF',
'pdf.saveAsPdf': 'Uložit jako PDF',
'pdf.pageBreakPerDay': 'Zalomení stránky pro každý den',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Kosten',
'pdf.preview': 'PDF Vorschau',
'pdf.saveAsPdf': 'Als PDF speichern',
'pdf.pageBreakPerDay': 'Seitenumbruch pro Tag',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Cost',
'pdf.preview': 'PDF Preview',
'pdf.saveAsPdf': 'Save as PDF',
'pdf.pageBreakPerDay': 'Page break per day',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Coste',
'pdf.preview': 'Vista previa PDF',
'pdf.saveAsPdf': 'Guardar como PDF',
'pdf.pageBreakPerDay': 'Salto de página por día',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Coût',
'pdf.preview': 'Aperçu PDF',
'pdf.saveAsPdf': 'Enregistrer en PDF',
'pdf.pageBreakPerDay': 'Saut de page par jour',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Κόστος',
'pdf.preview': 'Προεπισκόπηση PDF',
'pdf.saveAsPdf': 'Αποθήκευση ως PDF',
'pdf.pageBreakPerDay': 'Αλλαγή σελίδας ανά ημέρα',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Költség',
'pdf.preview': 'PDF előnézet',
'pdf.saveAsPdf': 'Mentés PDF-ként',
'pdf.pageBreakPerDay': 'Oldaltörés naponta',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Biaya',
'pdf.preview': 'Pratinjau PDF',
'pdf.saveAsPdf': 'Simpan sebagai PDF',
'pdf.pageBreakPerDay': 'Pemisah halaman per hari',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Costo',
'pdf.preview': 'Anteprima PDF',
'pdf.saveAsPdf': 'Salva come PDF',
'pdf.pageBreakPerDay': 'Interruzione di pagina per giorno',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': '費用',
'pdf.preview': 'PDFプレビュー',
'pdf.saveAsPdf': 'PDFとして保存',
'pdf.pageBreakPerDay': '日ごとに改ページ',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': '비용',
'pdf.preview': 'PDF 미리보기',
'pdf.saveAsPdf': 'PDF로 저장',
'pdf.pageBreakPerDay': '날짜별 페이지 나누기',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Kosten',
'pdf.preview': 'PDF-voorbeeld',
'pdf.saveAsPdf': 'Opslaan als PDF',
'pdf.pageBreakPerDay': 'Pagina-einde per dag',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Koszt',
'pdf.preview': 'Podgląd PDF',
'pdf.saveAsPdf': 'Zapisz jako PDF',
'pdf.pageBreakPerDay': 'Podział strony dla każdego dnia',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Стоимость',
'pdf.preview': 'Предпросмотр PDF',
'pdf.saveAsPdf': 'Сохранить как PDF',
'pdf.pageBreakPerDay': 'Разрыв страницы для каждого дня',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Kostnad',
'pdf.preview': 'Förhandsgranskning av PDF',
'pdf.saveAsPdf': 'Spara som PDF',
'pdf.pageBreakPerDay': 'Sidbrytning per dag',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Maliyet',
'pdf.preview': 'PDF Önizleme',
'pdf.saveAsPdf': 'PDF olarak Kaydet',
'pdf.pageBreakPerDay': 'Her gün için sayfa sonu',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Вартість',
'pdf.preview': 'Попередній перегляд PDF',
'pdf.saveAsPdf': 'Зберегти як PDF',
'pdf.pageBreakPerDay': 'Розрив сторінки для кожного дня',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': 'Chi phí',
'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',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': '費用',
'pdf.preview': 'PDF 預覽',
'pdf.saveAsPdf': '儲存為 PDF',
'pdf.pageBreakPerDay': '每天分頁',
};
export default pdf;
+1
View File
@@ -6,5 +6,6 @@ const pdf: TranslationStrings = {
'pdf.costLabel': '费用',
'pdf.preview': 'PDF 预览',
'pdf.saveAsPdf': '保存为 PDF',
'pdf.pageBreakPerDay': '每天分页',
};
export default pdf;