@@ -94,35 +179,35 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
${!hidePayment ? `
- Betalningsinformation
+ ${L.paymentHeading}
${company.bank_name ? `
- | Bank: |
+ ${L.bank} |
${company.bank_name} |
` : ''}
${company.clearing_number && company.account_number ? `
- | Kontonummer: |
+ ${L.account} |
${company.clearing_number}-${company.account_number} |
` : ''}
${company.iban ? `
- | IBAN: |
+ ${L.iban} |
${company.iban} |
` : ''}
${company.bic ? `
- | BIC/SWIFT: |
+ ${L.bic} |
${company.bic} |
` : ''}
- | Meddelande: |
+ ${L.message} |
${invoice.invoice_number} |
@@ -132,17 +217,17 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
- Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.
+ ${L.questions}
- Med vänliga hälsningar,
+ ${L.sincerely}
${getCompanyPrimaryName(company)}
${company.org_number ? `
- Org.nr: ${company.org_number}
- ${company.vat_number ? ` | VAT: ${company.vat_number}` : ''}
- ${company.f_skatt ? ' | Innehar F-skattsedel' : ''}
+ ${L.orgNo} ${company.org_number}
+ ${company.vat_number ? ` | ${L.vat} ${company.vat_number}` : ''}
+ ${company.f_skatt ? ` | ${L.fSkatt}` : ''}
` : ''}
@@ -158,51 +243,50 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
export function generateInvoiceEmailText(data: InvoiceEmailData): string {
const { invoice, customer, company } = data
- const documentType = getDocumentLabel(invoice)
+ const lang = resolveLang(customer)
+ const L = LABELS[lang]
+ const documentType = getDocumentLabel(invoice, lang)
const isCreditNote = !!invoice.credited_invoice_id
const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice'
const isDeliveryNote = docType === 'delivery_note'
const isProforma = docType === 'proforma'
const hidePayment = isCreditNote || isDeliveryNote || isProforma
+ const firstName = customer.name ? customer.name.split(' ')[0] : ''
- let text = `${documentType} från ${getCompanyPrimaryName(company)}\n`
- text += `${documentType}nummer: ${invoice.invoice_number}\n\n`
+ let text = `${L.documentFrom(documentType, getCompanyPrimaryName(company))}\n`
+ text += `${L.documentNumber(documentType)} ${invoice.invoice_number}\n\n`
- text += `Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},\n\n`
+ text += `${L.greeting(firstName)}\n\n`
- if (isCreditNote) {
- text += `Bifogat hittar du en kreditfaktura som korrigerar en tidigare faktura.\n\n`
- } else {
- text += `Tack för ditt förtroende! Bifogat hittar du din faktura.\n\n`
- }
+ text += `${isCreditNote ? L.bodyCreditNote : L.bodyInvoice}\n\n`
- text += `${documentType}sammanfattning:\n`
+ text += `${L.documentSummary(documentType)}\n`
text += `---\n`
- text += `${documentType}nummer: ${invoice.invoice_number}\n`
- text += `${documentType}datum: ${formatDate(invoice.invoice_date)}\n`
- text += `Förfallodatum: ${formatDate(invoice.due_date)}\n`
- text += `Att betala: ${formatCurrency(invoice.total, invoice.currency)}\n`
+ text += `${L.documentNumber(documentType)} ${invoice.invoice_number}\n`
+ text += `${L.documentDate(documentType)} ${formatDate(invoice.invoice_date)}\n`
+ text += `${L.dueDate} ${formatDate(invoice.due_date)}\n`
+ text += `${L.toPay} ${formatCurrencyForCustomer(invoice.total, invoice.currency, lang)}\n`
text += `---\n\n`
if (!hidePayment) {
- text += `Betalningsinformation:\n`
- if (company.bank_name) text += `Bank: ${company.bank_name}\n`
+ text += `${L.paymentHeading}:\n`
+ if (company.bank_name) text += `${L.bank} ${company.bank_name}\n`
if (company.clearing_number && company.account_number) {
- text += `Kontonummer: ${company.clearing_number}-${company.account_number}\n`
+ text += `${L.account} ${company.clearing_number}-${company.account_number}\n`
}
- if (company.iban) text += `IBAN: ${company.iban}\n`
- if (company.bic) text += `BIC/SWIFT: ${company.bic}\n`
- text += `Meddelande: ${invoice.invoice_number}\n\n`
+ if (company.iban) text += `${L.iban} ${company.iban}\n`
+ if (company.bic) text += `${L.bic} ${company.bic}\n`
+ text += `${L.message} ${invoice.invoice_number}\n\n`
}
- text += `Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.\n\n`
- text += `Med vänliga hälsningar,\n`
+ text += `${L.questions}\n\n`
+ text += `${L.sincerely}\n`
text += `${getCompanyDisplayName(company)}\n`
if (company.org_number) {
- text += `\nOrg.nr: ${company.org_number}`
- if (company.vat_number) text += ` | VAT: ${company.vat_number}`
- if (company.f_skatt) text += ` | Innehar F-skattsedel`
+ text += `\n${L.orgNo} ${company.org_number}`
+ if (company.vat_number) text += ` | ${L.vat} ${company.vat_number}`
+ if (company.f_skatt) text += ` | ${L.fSkatt}`
text += `\n`
}
@@ -213,8 +297,10 @@ export function generateInvoiceEmailText(data: InvoiceEmailData): string {
* Generate email subject for an invoice
*/
export function generateInvoiceEmailSubject(data: InvoiceEmailData): string {
- const { invoice, company } = data
- const documentType = getDocumentLabel(invoice)
+ const { invoice, customer, company } = data
+ const lang = resolveLang(customer)
+ const L = LABELS[lang]
+ const documentType = getDocumentLabel(invoice, lang)
- return `${documentType} ${invoice.invoice_number} från ${getCompanyPrimaryName(company)}`
+ return L.subjectFrom(documentType, invoice.invoice_number ?? '', getCompanyPrimaryName(company))
}
diff --git a/lib/invoices/pdf-template.tsx b/lib/invoices/pdf-template.tsx
index 0b9b7359..a0305094 100644
--- a/lib/invoices/pdf-template.tsx
+++ b/lib/invoices/pdf-template.tsx
@@ -10,6 +10,131 @@ import type { Invoice, InvoiceItem, Customer, CompanySettings, InvoiceDocumentTy
import { generateOcrReference } from '@/lib/bankgiro/luhn'
import { getDisplayTotal } from '@/lib/invoices/rounding'
+type PdfLang = 'sv' | 'en'
+
+// Customer-facing labels. Statutory chapter references (ML 17 kap 24§, ML 3 kap.)
+// stay intact in both locales — they identify the law, not the language.
+const LABELS = {
+ sv: {
+ // Document titles
+ titleInvoice: 'FAKTURA',
+ titleCreditNote: 'KREDITFAKTURA',
+ titleProforma: 'PROFORMAFAKTURA',
+ titleDeliveryNote: 'FÖLJESEDEL',
+ titlePreview: 'FÖRHANDSGRANSKNING',
+ // Status banners
+ cancelledTitle: 'MAKULERAD – inte en giltig faktura',
+ cancelledWithNumber: (n: string) => `Faktura ${n} har makulerats. Numret behålls i serien för att hålla nummerföljden obruten enligt ML 17 kap 24§, men dokumentet är inte ett giltigt fakturaunderlag.`,
+ cancelledNoNumber: 'Detta utkast har makulerats och är inte ett giltigt fakturaunderlag.',
+ draftTitle: 'UTKAST – inte en giltig faktura',
+ draftWithNumber: 'Detta är ett utkast. Markera fakturan som skickad eller skicka via systemet för att göra den giltig som fakturaunderlag.',
+ draftNoNumber: 'Denna faktura saknar löpnummer och kan inte användas som fakturaunderlag enligt ML 17 kap 24§. Skicka fakturan via systemet för att tilldela ett nummer.',
+ // Credit note reference
+ creditNoteRef: (n: string) => `Denna kreditfaktura avser och krediterar faktura nr ${n}`,
+ // Sections
+ invoiceInfoHeading: 'Fakturainformation',
+ billedToHeading: 'Faktureras till',
+ itemsHeading: 'Specifikation',
+ // Invoice details
+ invoiceDate: 'Fakturadatum:',
+ dueDate: 'Förfallodatum:',
+ deliveryDate: 'Leveransdatum:',
+ yourReference: 'Er referens:',
+ ourReference: 'Vår referens:',
+ // Customer box
+ orgNo: 'Org.nr:',
+ vat: 'VAT:',
+ // Table columns
+ colDescription: 'Beskrivning',
+ colQty: 'Antal',
+ colUnit: 'Enhet',
+ colUnitPrice: 'à-pris',
+ colVat: 'Moms',
+ colTotal: 'Summa',
+ // Totals
+ subtotal: 'Delsumma:',
+ net: (rate: number) => `Netto ${rate}%:`,
+ vatRow: (rate: number) => `Moms ${rate}%:`,
+ rounding: 'Öresavrundning:',
+ toCredit: 'Att kreditera:',
+ toPay: 'Att betala:',
+ vatInSek: (rate: number | string) => `Moms i SEK (kurs ${rate}):`,
+ totalInSek: 'Totalt i SEK:',
+ // Proforma / exempt
+ proformaNotice: 'Detta är en proformafaktura och utgör ingen betalningsanmodan.',
+ exemptNotice: 'Undantag från skatteplikt, ML 3 kap.',
+ // Payment
+ paymentHeading: 'Betalningsinformation',
+ bank: 'Bank:',
+ account: 'Kontonummer:',
+ bankgiro: 'Bankgiro:',
+ plusgiro: 'Plusgiro:',
+ swish: 'Swish:',
+ iban: 'IBAN:',
+ bic: 'BIC/SWIFT:',
+ ocr: 'OCR/Referens:',
+ // Footer
+ orgNoLong: 'Org.nr:',
+ vatRegNo: 'Momsreg.nr:',
+ fSkatt: 'Godkänd för F-skatt',
+ },
+ en: {
+ titleInvoice: 'INVOICE',
+ titleCreditNote: 'CREDIT NOTE',
+ titleProforma: 'PROFORMA INVOICE',
+ titleDeliveryNote: 'DELIVERY NOTE',
+ titlePreview: 'PREVIEW',
+ cancelledTitle: 'VOID — not a valid invoice',
+ cancelledWithNumber: (n: string) => `Invoice ${n} has been voided. The number is retained in the sequence to keep the numbering unbroken (ML 17 kap 24§ — Swedish VAT Act), but this document is not a valid invoice.`,
+ cancelledNoNumber: 'This draft has been voided and is not a valid invoice.',
+ draftTitle: 'DRAFT — not a valid invoice',
+ draftWithNumber: 'This is a draft. Mark the invoice as sent, or send it via the system, to make it a valid invoice.',
+ draftNoNumber: 'This invoice has no serial number and cannot be used as a valid invoice under ML 17 kap 24§ (Swedish VAT Act). Send the invoice via the system to assign a number.',
+ creditNoteRef: (n: string) => `This credit note credits invoice no. ${n}`,
+ invoiceInfoHeading: 'Invoice information',
+ billedToHeading: 'Billed to',
+ itemsHeading: 'Items',
+ invoiceDate: 'Invoice date:',
+ dueDate: 'Due date:',
+ deliveryDate: 'Delivery date:',
+ yourReference: 'Your reference:',
+ ourReference: 'Our reference:',
+ orgNo: 'Reg. no.:',
+ vat: 'VAT:',
+ colDescription: 'Description',
+ colQty: 'Qty',
+ colUnit: 'Unit',
+ colUnitPrice: 'Unit price',
+ colVat: 'VAT',
+ colTotal: 'Amount',
+ subtotal: 'Subtotal:',
+ net: (rate: number) => `Net ${rate}%:`,
+ vatRow: (rate: number) => `VAT ${rate}%:`,
+ rounding: 'Rounding:',
+ toCredit: 'To credit:',
+ toPay: 'Total due:',
+ vatInSek: (rate: number | string) => `VAT in SEK (rate ${rate}):`,
+ totalInSek: 'Total in SEK:',
+ proformaNotice: 'This is a proforma invoice and is not a request for payment.',
+ exemptNotice: 'Exempt from VAT (ML 3 kap. — Swedish VAT Act).',
+ paymentHeading: 'Payment information',
+ bank: 'Bank:',
+ account: 'Account number:',
+ bankgiro: 'Bankgiro:',
+ plusgiro: 'Plusgiro:',
+ swish: 'Swish:',
+ iban: 'IBAN:',
+ bic: 'BIC/SWIFT:',
+ ocr: 'Reference:',
+ orgNoLong: 'Reg. no.:',
+ vatRegNo: 'VAT reg. no.:',
+ // Statutory Swedish phrase — kept verbatim in both locales. Peppol SE-R-005
+ // and Skatteverket's F-skatt notation expect "Godkänd för F-skatt"; an
+ // English translation has no legal standing.
+ fSkatt: 'Godkänd för F-skatt',
+ },
+} as const
+
// Create styles
const styles = StyleSheet.create({
page: {
@@ -278,19 +403,24 @@ const styles = StyleSheet.create({
},
})
-// Format currency
-function formatCurrency(amount: number, currency: string = 'SEK'): string {
- return new Intl.NumberFormat('sv-SE', {
- style: 'currency',
- currency,
+// Format currency with explicit ISO code so non-Swedish recipients see "1 234,56 SEK"
+// instead of the Swedish symbol "kr". Decimal style + appended code works for any
+// currency (SEK/EUR/USD) and avoids Intl's locale-specific symbol quirks.
+function formatCurrency(amount: number, currency: string = 'SEK', language: PdfLang = 'sv'): string {
+ const formatted = new Intl.NumberFormat(language === 'en' ? 'en-US' : 'sv-SE', {
+ style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount)
+ return `${formatted} ${currency}`
}
-// Format date
+// Format date as ISO yyyy-MM-dd in both locales — universally unambiguous and
+// matches the project's formatDate() convention (lib/utils.ts).
+// Input is already a YYYY-MM-DD string from the DB, so slice avoids the
+// new Date() + local-getter timezone hazard.
function formatDate(date: string): string {
- return new Date(date).toLocaleDateString('sv-SE')
+ return date.slice(0, 10)
}
// Format org number
@@ -302,12 +432,13 @@ function formatOrgNumber(orgNumber: string): string {
return orgNumber
}
-function getDocumentTitle(invoice: Invoice): string {
- if (invoice.credited_invoice_id) return 'KREDITFAKTURA'
+function getDocumentTitle(invoice: Invoice, lang: PdfLang): string {
+ const L = LABELS[lang]
+ if (invoice.credited_invoice_id) return L.titleCreditNote
const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice'
- if (docType === 'proforma') return 'PROFORMAFAKTURA'
- if (docType === 'delivery_note') return 'FÖLJESEDEL'
- return 'FAKTURA'
+ if (docType === 'proforma') return L.titleProforma
+ if (docType === 'delivery_note') return L.titleDeliveryNote
+ return L.titleInvoice
}
interface InvoicePDFProps {
@@ -317,9 +448,12 @@ interface InvoicePDFProps {
company: CompanySettings
originalInvoiceNumber?: string
isPreview?: boolean
+ language?: PdfLang
}
-export function InvoicePDF({ invoice, customer, items, company, originalInvoiceNumber, isPreview }: InvoicePDFProps) {
+export function InvoicePDF({ invoice, customer, items, company, originalInvoiceNumber, isPreview, language }: InvoicePDFProps) {
+ const lang: PdfLang = language ?? customer.language ?? 'sv'
+ const L = LABELS[lang]
const isCreditNote = !!invoice.credited_invoice_id
// Check if items have mixed VAT rates
@@ -354,20 +488,20 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
of a non-cancelled invoice that somehow lacks a number. */}
{invoice.status === 'cancelled' ? (
- MAKULERAD – inte en giltig faktura
+ {L.cancelledTitle}
{invoice.invoice_number
- ? `Faktura ${invoice.invoice_number} har makulerats. Numret behålls i serien för att hålla nummerföljden obruten enligt ML 17 kap 24§, men dokumentet är inte ett giltigt fakturaunderlag.`
- : 'Detta utkast har makulerats och är inte ett giltigt fakturaunderlag.'}
+ ? L.cancelledWithNumber(invoice.invoice_number)
+ : L.cancelledNoNumber}
) : isPreview ? null : (invoice.status === 'draft' || !invoice.invoice_number) && (
- UTKAST – inte en giltig faktura
+ {L.draftTitle}
{invoice.invoice_number
- ? 'Detta är ett utkast. Markera fakturan som skickad eller skicka via systemet för att göra den giltig som fakturaunderlag.'
- : 'Denna faktura saknar löpnummer och kan inte användas som fakturaunderlag enligt ML 17 kap 24§. Skicka fakturan via systemet för att tilldela ett nummer.'}
+ ? L.draftWithNumber
+ : L.draftNoNumber}
)}
@@ -385,9 +519,9 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
- {getDocumentTitle(invoice)}
+ {getDocumentTitle(invoice, lang)}
- {invoice.invoice_number ?? 'FÖRHANDSGRANSKNING'}
+ {invoice.invoice_number ?? L.titlePreview}
@@ -395,7 +529,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{isCreditNote && originalInvoiceNumber && (
- Denna kreditfaktura avser och krediterar faktura nr {originalInvoiceNumber}
+ {L.creditNoteRef(originalInvoiceNumber)}
)}
@@ -404,24 +538,24 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{/* Invoice details */}
- Fakturainformation
+ {L.invoiceInfoHeading}
- Fakturadatum:
+ {L.invoiceDate}
{formatDate(invoice.invoice_date)}
- Förfallodatum:
+ {L.dueDate}
{formatDate(invoice.due_date)}
{invoice.delivery_date && invoice.delivery_date !== invoice.invoice_date && (
- Leveransdatum:
+ {L.deliveryDate}
{formatDate(invoice.delivery_date)}
)}
{invoice.your_reference && (
- Er referens:
+ {L.yourReference}
{invoice.your_reference.split(',').map((ref, i) => (
@@ -433,7 +567,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
{invoice.our_reference && (
- Vår referens:
+ {L.ourReference}
{invoice.our_reference.split(',').map((ref, i) => (
@@ -447,7 +581,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{/* Customer */}
- Faktureras till
+ {L.billedToHeading}
{customer.name}
{customer.address_line1 && {customer.address_line1}}
@@ -459,30 +593,30 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{customer.country}
)}
{customer.org_number && (
- Org.nr: {customer.org_number}
+ {L.orgNo} {customer.org_number}
)}
- {customer.vat_number && VAT: {customer.vat_number}}
+ {customer.vat_number && {L.vat} {customer.vat_number}}
{/* Items table */}
- Specifikation
+ {L.itemsHeading}
{/* Table header */}
- Beskrivning
- Antal
- Enhet
+ {L.colDescription}
+ {L.colQty}
+ {L.colUnit}
{!isDeliveryNote && (
- à-pris
+ {L.colUnitPrice}
)}
{!isDeliveryNote && showVatColumn && (
- Moms
+ {L.colVat}
)}
{!isDeliveryNote && (
- Summa
+ {L.colTotal}
)}
@@ -493,13 +627,13 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{item.quantity}
{item.unit}
{!isDeliveryNote && (
- {formatCurrency(item.unit_price, invoice.currency)}
+ {formatCurrency(item.unit_price, invoice.currency, lang)}
)}
{!isDeliveryNote && showVatColumn && (
{item.vat_rate ?? 0}%
)}
{!isDeliveryNote && (
- {formatCurrency(item.line_total, invoice.currency)}
+ {formatCurrency(item.line_total, invoice.currency, lang)}
)}
))}
@@ -510,8 +644,8 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{!isDeliveryNote && (
- Delsumma:
- {formatCurrency(invoice.subtotal, invoice.currency)}
+ {L.subtotal}
+ {formatCurrency(invoice.subtotal, invoice.currency, lang)}
{vatByRate.size > 1 ? (
Array.from(vatByRate.entries())
@@ -519,21 +653,21 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
.map(([rate, group]) => (
- Netto {rate}%:
- {formatCurrency(group.base, invoice.currency)}
+ {L.net(rate)}
+ {formatCurrency(group.base, invoice.currency, lang)}
{group.vat > 0 && (
- Moms {rate}%:
- {formatCurrency(group.vat, invoice.currency)}
+ {L.vatRow(rate)}
+ {formatCurrency(group.vat, invoice.currency, lang)}
)}
))
) : (
- Moms ({invoice.vat_rate ?? (vatByRate.size === 1 ? vatByRate.keys().next().value : 0)}%):
- {formatCurrency(invoice.vat_amount, invoice.currency)}
+ {L.vatRow(invoice.vat_rate ?? (vatByRate.size === 1 ? (vatByRate.keys().next().value ?? 0) : 0))}
+ {formatCurrency(invoice.vat_amount, invoice.currency, lang)}
)}
{(() => {
@@ -542,13 +676,13 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
<>
{rounding.applies && (
- Öresavrundning:
- {formatCurrency(rounding.roundingDelta, 'SEK')}
+ {L.rounding}
+ {formatCurrency(rounding.roundingDelta, 'SEK', lang)}
)}
- {isCreditNote ? 'Att kreditera:' : 'Att betala:'}
- {formatCurrency(rounding.displayed, invoice.currency)}
+ {isCreditNote ? L.toCredit : L.toPay}
+ {formatCurrency(rounding.displayed, invoice.currency, lang)}
>
)
@@ -557,13 +691,13 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{invoice.vat_amount_sek != null && invoice.vat_amount_sek !== 0 && (
- Moms i SEK (kurs {invoice.exchange_rate}):
- {formatCurrency(invoice.vat_amount_sek, 'SEK')}
+ {L.vatInSek(invoice.exchange_rate ?? '')}
+ {formatCurrency(invoice.vat_amount_sek, 'SEK', lang)}
)}
- Totalt i SEK:
- {formatCurrency(invoice.total_sek, 'SEK')}
+ {L.totalInSek}
+ {formatCurrency(invoice.total_sek, 'SEK', lang)}
)}
@@ -574,7 +708,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{isProforma && (
- Detta är en proformafaktura och utgör ingen betalningsanmodan.
+ {L.proformaNotice}
)}
@@ -582,16 +716,16 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{/* Payment information - not shown for credit notes, proformas, or delivery notes */}
{!isCreditNote && !isProforma && !isDeliveryNote && (
- Betalningsinformation
+ {L.paymentHeading}
{company.bank_name && (
- Bank:
+ {L.bank}
{company.bank_name}
)}
{(company.clearing_number || company.account_number) && (
- Kontonummer:
+ {L.account}
{company.clearing_number}-{company.account_number}
@@ -599,41 +733,41 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
{company.bankgiro && (company.invoice_show_bankgiro ?? true) && (
- Bankgiro:
+ {L.bankgiro}
{company.bankgiro}
)}
{company.plusgiro && (company.invoice_show_plusgiro ?? true) && (
- Plusgiro:
+ {L.plusgiro}
{company.plusgiro}
)}
{company.swish && (company.invoice_show_swish ?? true) && (
- Swish:
+ {L.swish}
{company.swish}
)}
{company.iban && (
- IBAN:
+ {L.iban}
{company.iban}
)}
{company.bic && (
- BIC/SWIFT:
+ {L.bic}
{company.bic}
)}
- Förfallodatum:
+ {L.dueDate}
{formatDate(invoice.due_date)}
{(company.invoice_show_ocr ?? true) && (
- OCR/Referens:
+ {L.ocr}
{invoice.invoice_number ? generateOcrReference(invoice.invoice_number) : '—'}
)}
@@ -648,7 +782,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
{invoice.vat_treatment === 'exempt' && !invoice.reverse_charge_text && (
- Undantag från skatteplikt, ML 3 kap.
+ {L.exemptNotice}
)}
@@ -681,9 +815,9 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
: null,
company.address_line1,
(company.postal_code || company.city) ? `${company.postal_code ?? ''} ${company.city ?? ''}`.trim() : null,
- company.org_number ? `Org.nr: ${formatOrgNumber(company.org_number)}` : null,
- company.vat_number ? `Momsreg.nr: ${company.vat_number}` : null,
- company.f_skatt ? 'Godkänd för F-skatt' : null,
+ company.org_number ? `${L.orgNoLong} ${formatOrgNumber(company.org_number)}` : null,
+ company.vat_number ? `${L.vatRegNo} ${company.vat_number}` : null,
+ company.f_skatt ? L.fSkatt : null,
].filter(Boolean).join(' · ')}
diff --git a/messages/en.json b/messages/en.json
index e259fb57..3f8ed565 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -522,6 +522,10 @@
"vat_failed_default": "The VAT number could not be verified",
"vat_error_title": "Could not verify VAT number",
"payment_terms_label": "Payment terms (days)",
+ "language_label": "Invoice language",
+ "language_sv": "Swedish",
+ "language_en": "English",
+ "language_hint": "Invoices and emails to this customer are sent in the chosen language. Does not affect how the invoice is booked.",
"notes_label": "Notes",
"notes_placeholder": "Internal notes about the customer...",
"submit_save": "Save customer",
@@ -945,6 +949,7 @@
},
"settings_invoicing_preview": {
"title": "Preview",
+ "preview_button": "Preview invoice",
"loading": "Generating preview...",
"error": "Could not load preview",
"no_customers": "Add a customer to see a preview of your invoice.",
@@ -3146,6 +3151,8 @@
"import": {
"title": "Import",
"subtitle": "Import bank transactions or bookkeeping data into your company",
+ "export_title": "Export",
+ "export_subtitle": "Download your bookkeeping as a SIE file or back it up to Google Drive",
"tab_import": "Import",
"tab_export": "Export",
"sandbox_disabled": "Import is not available in the sandbox. Create an account to import data.",
diff --git a/messages/sv.json b/messages/sv.json
index 8325cacd..370cdf12 100644
--- a/messages/sv.json
+++ b/messages/sv.json
@@ -522,6 +522,10 @@
"vat_failed_default": "VAT-numret kunde inte verifieras",
"vat_error_title": "Kunde inte verifiera VAT-nummer",
"payment_terms_label": "Betalningsvillkor (dagar)",
+ "language_label": "Fakturaspråk",
+ "language_sv": "Svenska",
+ "language_en": "Engelska",
+ "language_hint": "Fakturor och e-post till denna kund skickas på det valda språket. Påverkar inte hur fakturan bokförs.",
"notes_label": "Anteckningar",
"notes_placeholder": "Interna anteckningar om kunden...",
"submit_save": "Spara kund",
@@ -945,6 +949,7 @@
},
"settings_invoicing_preview": {
"title": "Förhandsvisning",
+ "preview_button": "Förhandsvisa faktura",
"loading": "Genererar förhandsvisning...",
"error": "Kunde inte ladda förhandsvisning",
"no_customers": "Lägg till en kund för att se en förhandsvisning av din faktura.",
@@ -3146,6 +3151,8 @@
"import": {
"title": "Importera",
"subtitle": "Importera banktransaktioner eller bokföringsdata till ditt företag",
+ "export_title": "Exportera",
+ "export_subtitle": "Ladda ner bokföringen som SIE-fil eller säkerhetskopia till Google Drive",
"tab_import": "Importera",
"tab_export": "Exportera",
"sandbox_disabled": "Import är inte tillgängligt i sandlådemiljön. Skapa ett konto för att importera data.",
diff --git a/next.config.ts b/next.config.ts
index 54450de8..4b8e62f2 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -17,7 +17,7 @@ const cspDirectives = [
"img-src 'self' data: blob: https:",
"font-src 'self'",
"worker-src 'self' blob:",
- `frame-src 'self' ${supabaseUrl}${activepiecesUrl ? ` ${activepiecesUrl}` : ""}`,
+ `frame-src 'self' blob: ${supabaseUrl}${activepiecesUrl ? ` ${activepiecesUrl}` : ""}`,
"frame-ancestors 'none'",
].join("; ");
@@ -82,6 +82,22 @@ const nextConfig: NextConfig = {
},
],
},
+ // Document inline-preview proxy must be embeddable in same-origin
+ // iframes (used by the verifikat document preview Sheet).
+ // Overrides the strict catch-all above for this single endpoint.
+ {
+ source: "/api/documents/:id/inline",
+ headers: [
+ {
+ key: "X-Frame-Options",
+ value: "SAMEORIGIN",
+ },
+ {
+ key: "Content-Security-Policy",
+ value: "frame-ancestors 'self'",
+ },
+ ],
+ },
];
},
};
diff --git a/supabase/migrations/20260522140000_add_customer_language.sql b/supabase/migrations/20260522140000_add_customer_language.sql
new file mode 100644
index 00000000..d28a08b3
--- /dev/null
+++ b/supabase/migrations/20260522140000_add_customer_language.sql
@@ -0,0 +1,16 @@
+-- Add language preference per customer.
+--
+-- Drives the locale of the customer-facing invoice PDF and email when this
+-- customer is invoiced. Defaults to Swedish, matching the existing behavior.
+-- Adding more locales is a follow-up migration so we never end up with an
+-- orphan value the templates don't have translations for.
+--
+-- Per-customer (not per-invoice) because the user shouldn't have to pick a
+-- language every time they bill the same recipient — set it once on the
+-- customer record and every future invoice + email honors it.
+
+ALTER TABLE public.customers
+ ADD COLUMN language TEXT NOT NULL DEFAULT 'sv'
+ CHECK (language IN ('sv', 'en'));
+
+NOTIFY pgrst, 'reload schema';
diff --git a/tests/helpers.ts b/tests/helpers.ts
index fa51f278..7384be20 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -406,6 +406,7 @@ export function makeCustomer(overrides: Partial = {}): Customer {
vat_number_validated: true,
vat_number_validated_at: '2024-01-01T00:00:00Z',
personal_number: null,
+ language: 'sv',
default_payment_terms: 30,
notes: null,
created_at: '2024-01-01T00:00:00Z',
diff --git a/types/index.ts b/types/index.ts
index b96d4362..2665d461 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -479,6 +479,9 @@ export interface Customer {
vat_number_validated_at: string | null
personal_number: string | null
+ // Language for customer-facing invoice PDF and email
+ language: 'sv' | 'en'
+
// Payment
default_payment_terms: number // Days
@@ -830,6 +833,7 @@ export interface CreateCustomerInput {
org_number?: string
vat_number?: string
personal_number?: string
+ language?: 'sv' | 'en'
default_payment_terms?: number
notes?: string
}