feat: enhance translation and invoice layout

This commit is contained in:
2025-03-24 08:55:27 +00:00
parent 7d5508e4d8
commit 256cf74a45
20 changed files with 2138 additions and 780 deletions

View File

@ -1,47 +1,46 @@
import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js';
import * as plugins from "./plugins.js";
import * as interfaces from "./interfaces/index.js";
const fromContact: plugins.tsclass.business.IContact = {
name: 'Awesome From Company',
type: 'company',
description: 'a company that does stuff',
name: "Awesome From Company",
type: "company",
description: "a company that does stuff",
address: {
streetName: 'Awesome Street',
houseNumber: '5',
city: 'Bremen',
country: 'Germany',
postalCode: '28359',
streetName: "Awesome Street",
houseNumber: "5",
city: "Bremen",
country: "Germany",
postalCode: "28359",
},
vatId: 'DE12345678',
vatId: "DE12345678",
sepaConnection: {
bic: 'BPOTBEB1',
iban: 'BE01234567891616'
bic: "BPOTBEB1",
iban: "BE01234567891616",
},
email: 'hello@awesome.company',
phone: '+49 421 1234567',
fax: '+49 421 1234568',
email: "hello@awesome.company",
phone: "+49 421 1234567",
fax: "+49 421 1234568",
};
const toContact: plugins.tsclass.business.IContact = {
name: 'Awesome To GmbH',
type: 'company',
customerNumber: 'LL-CLIENT-123',
description: 'a company that does stuff',
name: "Awesome To GmbH",
type: "company",
customerNumber: "LL-CLIENT-123",
description: "a company that does stuff",
address: {
streetName: 'Awesome Street',
houseNumber: '5',
city: 'Bremen',
country: 'Germany',
postalCode: '28359'
streetName: "Awesome Street",
houseNumber: "5",
city: "Bremen",
country: "Germany",
postalCode: "28359",
},
vatId: 'BE12345678',
}
vatId: "BE12345678",
};
export const demoLetter: plugins.tsclass.business.ILetter = {
versionInfo: {
type: 'draft',
version: '1.0.0',
type: "draft",
version: "1.0.0",
},
accentColor: null,
content: {
@ -49,155 +48,192 @@ export const demoLetter: plugins.tsclass.business.ILetter = {
timesheetData: null,
contractData: {
contractDate: Date.now(),
id: 'someid'
id: "someid",
},
letterData: {} as plugins.tsclass.business.ILetter,
invoiceData: {
id: 'LL-INV-48765',
id: "LL-INV-48765",
reverseCharge: true,
dueInDays: 30,
billedBy: fromContact,
billedTo: toContact,
status: null,
deliveryDate: new Date().getTime(),
periodOfPerformance: null,
periodOfPerformance: {
from: +new Date().setDate(new Date().getDate() - 7),
to: +new Date(),
},
printResult: null,
currency: 'EUR',
currency: "EUR",
notes: [],
type: 'debitnote',
type: "debitnote",
items: [
{
name: 'Item with 19% VAT',
name: "Item with 19% VAT",
unitQuantity: 2,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 19,
position: 0,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 4,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 1,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 2,
},
{
name: 'Item with 21% VAT',
name: "Item with 21% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 21,
position: 3,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 6,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 4,
},{
name: 'Item with 19% VAT',
},
{
name: "Item with 19% VAT",
unitQuantity: 8,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 19,
position: 5,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 9,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 6,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 4,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 8,
},
{
name: 'Item with 21% VAT',
name: "Item with 21% VAT",
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 21,
position: 9,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 11,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 12,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 13,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 14,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 15,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 16,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 17,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 18,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 19,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 20,
},
],
}
},
},
date: Date.now(),
type: 'invoice',
type: "invoice",
needsCoverSheet: false,
objectActions: [],
pdf: null,
@ -208,12 +244,12 @@ export const demoLetter: plugins.tsclass.business.ILetter = {
legalContact: null,
logoUrl: null,
pdfAttachments: null,
subject: 'Invoice: LL-INV-48765',
}
subject: "Invoice: LL-INV-48765",
};
export const demoDocumentSettings: interfaces.IDocumentSettings = {
enableTopDraftText: true,
enableDefaultHeader: true,
enableDefaultFooter: true,
languageCode: 'DE',
};
languageCode: "DE",
};

View File

@ -1,12 +1,15 @@
export const a4Height = 1122;
export const a4Width = 794;
export const rightMargin = 70;
export const leftMargin = 90;
const DPI = 96 / 2.54; // <PX> / <INCH>
export const A4_HEIGHT = cmToPx(29.7); // DPI * 29.7cm
export const A4_WIDTH = cmToPx(21); // DPI * 21cm
import * as interfaces from './interfaces/index.js';
export function cmToPx(value: number): number {
return DPI * value;
}
import * as interfaces from "./interfaces/index.js";
export { interfaces };
import * as translation from './translation.js';
import * as translation from "./translation.js";
export { translation };
export * from './demoletter.js';
export * from "./demoletter.js";

View File

@ -1,9 +1,23 @@
import * as translation from '../translation.js';
import * as translation from "../translation.js";
export interface IDocumentTheme {
colorPrimaryForeground?: string;
colorPrimaryBackground?: string;
colorAccentForeground?: string;
colorAccentBackground?: string;
fontFamily?: string;
pageBackground?: string;
coverPageBackground?: string;
}
export interface IDocumentSettings {
enableTopDraftText?: boolean;
enableDefaultHeader?: boolean;
enableDefaultFooter?: boolean;
languageCode?: translation.TLanguageCode;
enableFoldMarks?: boolean;
enableInvoiceContractRefSection?: boolean;
languageCode?: translation.LanguageCode;
dateStyle?: Intl.DateTimeFormatOptions["dateStyle"];
vatGroupPositions?: boolean;
}
theme?: IDocumentTheme;
}

View File

@ -1,143 +1,272 @@
import * as interfaces from './interfaces/index.js';
// Define English translations without enforcing TTranslationImplementation yet
export const EN_translations = {
address: 'Address',
bankConnection: 'Bank Connection',
contactInfo: 'Contact Info',
description: 'Description',
invoice: 'Invoice',
itemPos: 'Item Pos.',
quantity: 'Quantity',
registrationInfo: 'Registration Info',
reverseVatNote: 'VAT arises on a reverse charge basis and is payable by the customer.',
totalNetPrice: 'Total Net Price',
unitNetPrice: 'Unit Net Price',
unitType: 'Unit Type',
yourCustomerId: 'Your Customer ID:',
yourVatId: 'Your vat id on file:',
continuesOnPage: 'Continues on page',
finalPageStatement: 'This is the final page of this document.',
page: 'Page',
vatShort: 'VAT',
} as const;
address: "Address",
"bank.accountHolder": "beneficiary",
"bank.bic": "bic",
"bank.iban": "iban",
"bank.institution": "institution",
"bankConnection@@title": "Bank Connection",
"contact@@title": "Contact Info",
"customer.number": "Your Customer ID",
description: "Description",
"empty.logo": "no logo provided",
"empty.number.customer": "not registered",
empty: "not provided",
fax: "Fax",
introStatement: "We hereby invoice the following products and services",
"invoice.number": "Invoice number",
invoice: "Invoice",
"item.position": "Pos.",
mail: "Mail",
"overlay@@draft": "Draft",
"page.continueNext": "Continues on page",
"page.final": "This is the final page of this document.",
page: "Page",
pageOf: "of",
"payment.qr": "Pay via QR code",
"payment.qr.description": "Scan the QR code with you banking app",
"payment.terms": "Payment Terms",
"payment.terms.direct": "Without deduction until",
"periodOfPerformance.day": "Delivery Date",
"periodOfPerformance.range": "Delivery Period",
phone: "Phone",
"price.total.net": "Total Net Price",
"price.unit.net": "Unit Net Price",
price: "Price",
quantity: "Quantity",
referencedContract: "Referenced contract",
"referencedContract.text":
"This invoice is adhering to agreements made by contract between the parties on",
"registration.label": "Registration Info",
subject: "Subject",
sum: "Sum",
totalGross: "Total gross",
"unit.type": "Unit Type",
"vat.position": "on item positions",
"vat.reverseCharge.note":
"VAT arises on a reverse charge basis and is payable by the customer.",
"vat.short": "VAT",
"vat.yourId": "Your vat id on file",
vat: "Valued Added Tax",
};
// Infer keys of EN_translations
export type TTranslationKey = keyof typeof EN_translations;
/**
* For example:
* - price
*/
type RawTranslationKeys = keyof typeof EN_translations;
/**
* For example:
* - price.item
* - price.sum
* - price.unit
* - vat.yourId
*/
type NestedTranslationKeys =
| RawTranslationKeys
| `${RawTranslationKeys}.${string}`
| `${RawTranslationKeys}.${string}.${string}`
| `${RawTranslationKeys}.${string}.${string}.${string}`
| `${RawTranslationKeys}.${string}.${string}.${string}.${string}`;
/**
* For example:
* - contact@@mail
* - vat = 'VAT'
* - vat.yourId = 'your vat id'
* - footer@@vat.yourId = 'your vat id'
* - header@@vat.yourId = 'THIS IS YOUR VAT'
*/
type LocationBasedTranslationKeys = `${string}@@${NestedTranslationKeys}`;
/**
* Mix of everything
*/
export type TranslationKey =
| NestedTranslationKeys
| LocationBasedTranslationKeys;
// Define the type for all translations based on EN_translations keys
export type TTranslationImplementation = {
[key in TTranslationKey]: string;
export type Dictionary = {
[key in TranslationKey]: string;
};
// Define German translations
export const DE_translations: TTranslationImplementation = {
address: 'Adresse',
bankConnection: 'Bankverbindung',
contactInfo: 'Kontaktinformationen',
description: 'Beschreibung',
invoice: 'Rechnung',
itemPos: 'Pos.',
quantity: 'Anzahl',
registrationInfo: 'HRA/HRB Info',
reverseVatNote:
'Umkehr der Umsatzsteuerpflicht: Der Rechnungsempfänger ist für die korrekte Abrechnung der Umsatzsteuer zuständig.',
totalNetPrice: 'Summe netto',
unitNetPrice: 'Einheit netto',
unitType: 'Einheit',
yourCustomerId: 'Ihre Kundennummer:',
yourVatId: 'Ihre Umsatzsteuer-ID:',
continuesOnPage: 'Fortsetzung auf Seite',
finalPageStatement: 'Dies ist die letzte Seite dieses Dokuments.',
page: 'Seite',
vatShort: 'USt',
export const DE_translations: Dictionary = {
address: "Adresse",
"bank.accountHolder": "Kontoinhaber",
"bank.bic": "BIC",
"bank.iban": "IBAN",
"bank.institution": "Bankinstitut",
"bankConnection@@title": "Bankverbindung",
"contact@@title": "Kontaktinformationen",
"customer.number": "Ihre Kundennummer",
description: "Beschreibung",
"empty.logo": "Kein Logo gesetzt",
"empty.number.customer": "nicht registriert",
empty: "nicht angegeben",
fax: "Fax",
introStatement:
"Wir stellen Ihnen hiermit folgende Produkte und Dienstleistungen in Rechnung",
"invoice.number": "Rechnungsnr.",
invoice: "Rechnung",
"item.position": "Pos.",
mail: "E-Mail",
"overlay@@draft": "Entwurf",
"page.continueNext": "Fortsetzung auf Seite",
"page.final": "Dies ist die letzte Seite dieses Dokuments.",
page: "Seite",
pageOf: "von",
"payment.qr": "Überweisen per QR-Code",
"payment.qr.description":
"Den QR-Code einfach mit der Banking-App einscannen",
"payment.terms": "Zahlungsbedingungen",
"payment.terms.direct": "Ohne Abzug bis zum",
"periodOfPerformance.day": "Lieferdatum",
"periodOfPerformance.range": "Lieferzeitraum",
phone: "Telefon",
"price.total.net": "Gesamtpreis",
"price.unit.net": "Stückpreis",
price: "Preis",
quantity: "Menge",
referencedContract: "Referenzierter Vertrag",
"referencedContract.text":
"Diese Rechnung bezieht sich auf die getroffenen Vertragsvereinbarungen vom",
"registration.label": "Registrierungsinfo",
subject: "Betreff",
sum: "Summe",
totalGross: "Gesamtbetrag brutto",
"unit.type": "Einheit",
"vat.position": "auf Positionen",
"vat.reverseCharge.note":
"Die Umsatzsteuer entsteht im Reverse-Charge-Verfahren und ist vom Kunden zu zahlen.",
"vat.short": "MwSt.",
"vat.yourId": "Ihre hinterlegte USt-Id",
vat: "Umsatzsteuer",
};
// Define Spanish translations
export const ES_translations: TTranslationImplementation = {
address: 'Dirección',
bankConnection: 'Conexión bancaria',
contactInfo: 'Información de contacto',
description: 'Descripción',
invoice: 'Factura',
itemPos: 'Pos.',
quantity: 'Cantidad',
registrationInfo: 'Información de registro',
reverseVatNote: 'El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.',
totalNetPrice: 'Precio total neto',
unitNetPrice: 'Precio unitario neto',
unitType: 'Tipo de unidad',
yourCustomerId: 'Su número de cliente:',
yourVatId: 'Su ID de IVA:',
continuesOnPage: 'Continúa en la página',
finalPageStatement: 'Esta es la última página de este documento.',
page: 'Página',
vatShort: 'IVA',
};
// export const ES_translations: TTranslationImplementation = {
// address: "Dirección",
// bankConnection: "Conexión bancaria",
// contactInfo: "Información de contacto",
// description: "Descripción",
// invoice: "Factura",
// itemPos: "Pos.",
// quantity: "Cantidad",
// registrationInfo: "Información de registro",
// reverseVatNote:
// "El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.",
// totalNetPrice: "Precio total neto",
// unitNetPrice: "Precio unitario neto",
// unitType: "Tipo de unidad",
// yourCustomerId: "Su número de cliente:",
// yourVatId: "Su ID de IVA:",
// continuesOnPage: "Continúa en la página",
// finalPageStatement: "Esta es la última página de este documento.",
// page: "Página",
// vatShort: "IVA",
// };
// Define French translations
export const FR_translations: TTranslationImplementation = {
address: 'Adresse',
bankConnection: 'Coordonnées bancaires',
contactInfo: 'Informations de contact',
description: 'Description',
invoice: 'Facture',
itemPos: 'Position',
quantity: 'Quantité',
registrationInfo: "Informations d'enregistrement",
reverseVatNote:
"La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.",
totalNetPrice: 'Prix net total',
unitNetPrice: 'Prix unitaire net',
unitType: "Type d'unité",
yourCustomerId: 'Votre numéro de client :',
yourVatId: 'Votre numéro de TVA :',
continuesOnPage: 'Continue sur la page',
finalPageStatement: 'Ceci est la dernière page de ce document.',
page: 'Page',
vatShort: 'TVA',
};
// export const FR_translations: TTranslationImplementation = {
// address: "Adresse",
// bankConnection: "Coordonnées bancaires",
// contactInfo: "Informations de contact",
// description: "Description",
// invoice: "Facture",
// itemPos: "Position",
// quantity: "Quantité",
// registrationInfo: "Informations d'enregistrement",
// reverseVatNote:
// "La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.",
// totalNetPrice: "Prix net total",
// unitNetPrice: "Prix unitaire net",
// unitType: "Type d'unité",
// yourCustomerId: "Votre numéro de client :",
// yourVatId: "Votre numéro de TVA :",
// continuesOnPage: "Continue sur la page",
// finalPageStatement: "Ceci est la dernière page de ce document.",
// page: "Page",
// vatShort: "TVA",
// };
// Define Italian translations
export const IT_translations: TTranslationImplementation = {
address: 'Indirizzo',
bankConnection: 'Coordinate bancarie',
contactInfo: 'Informazioni di contatto',
description: 'Descrizione',
invoice: 'Fattura',
itemPos: 'Pos.',
quantity: 'Quantità',
registrationInfo: 'Informazioni di registrazione',
reverseVatNote: "L'IVA è applicata con inversione contabile ed è a carico del cliente.",
totalNetPrice: 'Prezzo netto totale',
unitNetPrice: 'Prezzo netto unitario',
unitType: 'Tipo di unità',
yourCustomerId: 'Il tuo numero cliente:',
yourVatId: 'Il tuo numero di partita IVA:',
continuesOnPage: 'Continua alla pagina',
finalPageStatement: 'Questa è l\'ultima pagina di questo documento.',
page: 'Pagina',
vatShort: 'IVA',
};
// export const IT_translations: TTranslationImplementation = {
// address: "Indirizzo",
// bankConnection: "Coordinate bancarie",
// contactInfo: "Informazioni di contatto",
// description: "Descrizione",
// invoice: "Fattura",
// itemPos: "Pos.",
// quantity: "Quantità",
// registrationInfo: "Informazioni di registrazione",
// reverseVatNote:
// "L'IVA è applicata con inversione contabile ed è a carico del cliente.",
// totalNetPrice: "Prezzo netto totale",
// unitNetPrice: "Prezzo netto unitario",
// unitType: "Tipo di unità",
// yourCustomerId: "Il tuo numero cliente:",
// yourVatId: "Il tuo numero di partita IVA:",
// continuesOnPage: "Continua alla pagina",
// finalPageStatement: "Questa è l'ultima pagina di questo documento.",
// page: "Pagina",
// vatShort: "IVA",
// };
// Language Code Map
export const languageCodeMap: Record<string, TTranslationImplementation> = {
export const languageCodeMap: Record<string, Dictionary> = {
EN: EN_translations,
DE: DE_translations,
ES: ES_translations,
FR: FR_translations,
IT: IT_translations,
// ES: ES_translations,
// FR: FR_translations,
// IT: IT_translations,
};
// Language Code Type
export type TLanguageCode = keyof typeof languageCodeMap;
export type LanguageCode = keyof typeof languageCodeMap;
function* getTranslationKeyHierarchy(
key: TranslationKey
): Generator<TranslationKey, TranslationKey> {
yield key;
const areaSplit = key.split("@@") as [TranslationKey, TranslationKey];
let rest = areaSplit[1];
if (rest) {
yield rest;
} else {
rest = areaSplit[0];
}
if (!rest.includes(".")) return;
const parts = rest.split(".");
for (let i = parts.length - 1; i > 0; i--) {
yield parts.slice(0, i).join(".") as TranslationKey;
}
}
// Translate Function
export const translate = (
languageCode: TLanguageCode,
key: TTranslationKey,
defaultValue: string
languageCode: LanguageCode,
key: TranslationKey
): string => {
const translations = languageCodeMap[languageCode] || EN_translations;
return translations[key] || defaultValue;
};
const dictionary = languageCodeMap[languageCode] || EN_translations;
const lookupHierarchy = getTranslationKeyHierarchy(key);
let found: string;
for (let keyOption of lookupHierarchy) {
found = dictionary[keyOption] || EN_translations[keyOption];
if (found) {
break;
}
}
return found;
};