Ver Fonte

Translate the English strings left in the quote and work order screens (#402)

* vscode

* translations

* keys

* Read an issued invoice's design snapshot without the designer version stamp

#401 stamps DESIGNER_LAYOUT_VERSION on any stored design without a version,
on the grounds that only the designer writes design rows. Issued-invoice
snapshots go through the same reader, but issuing writes them too, from
whatever look the invoice printed with. A workshop that never opened the
designer freezes a classic layout with no version, and reading it back
stamped turned the classic header into the designer's the moment the
invoice was shared: the share link no longer matched the downloaded PDF.

Snapshots now read back exactly as frozen; design rows keep the stamp.
Fixes e2e/specs/invoices/pdf-parity.spec.ts, red on main since #401.
Bernt Christian Egeland há 2 semanas atrás
pai
commit
6f6418db57

+ 6 - 0
.vscode/settings.json

@@ -15,4 +15,10 @@
     "source.removeUnusedImports": "always",
     "source.organizeImports.biome": "always"
   },
+  "workbench.colorCustomizations": {
+    "titleBar.activeBackground": "#1f4e8c",
+    "titleBar.activeForeground": "#ffffff",
+    "titleBar.inactiveBackground": "#1f4e8c99",
+    "titleBar.inactiveForeground": "#ffffffaa"
+  }
 }

+ 14 - 16
messages/de/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Stückpreis",
     "total": "Gesamt",
     "subtotal": "Teile Zwischensumme: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Zeile löschen",
+    "excludeFromTotal": "Von der Summe ausnehmen",
     "addFromInventory": "Aus Lager",
     "unitCost": "Kosten",
     "markupPercent": "Aufschlag %",
@@ -91,17 +91,15 @@
     "rate": "Satz ({currency}/Std.)",
     "total": "Gesamt",
     "subtotal": "Arbeitszeit Zwischensumme: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Leistung hinzufügen",
+    "qty": "Menge",
+    "qtyOrHours": "Einheit / Stunden",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Stundenbasiert: nach geleisteten Stunden berechnet (Stunden × Satz/Std.). Zum Wechseln klicken.",
     "switchToServiceHint": "Pauschale: als Festpreis pro Einheit berechnet. Zum Wechseln klicken.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Zeile löschen",
+    "excludeFromTotal": "Von der Summe ausnehmen"
   },
   "notes": {
     "title": "Notizen",
@@ -139,8 +137,8 @@
     "selectVehicle": "Fahrzeug auswählen...",
     "selectCustomer": "Kunden auswählen...",
     "none": "Keine",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Fahrzeug entfernen",
+    "clearCustomer": "Kunde entfernen"
   },
   "totals": {
     "title": "Summen",
@@ -172,7 +170,7 @@
     "failedEmail": "E-Mail konnte nicht gesendet werden",
     "smsSent": "SMS gesendet",
     "failedSms": "SMS konnte nicht gesendet werden",
-    "copyLink": "Copy link"
+    "copyLink": "Link kopieren"
   },
   "images": {
     "title": "Angebotsbilder",
@@ -187,8 +185,8 @@
     "uploadingCount": "{count} Bild(er) werden hochgeladen...",
     "uploadedCount": "{count} Bild(er) hochgeladen",
     "uploadFailed": "Hochladen fehlgeschlagen",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "{name} konnte nicht hochgeladen werden",
+    "failedSaveFile": "{name} konnte nicht gespeichert werden",
     "deleted": "Bild gelöscht",
     "failedDelete": "Bild konnte nicht gelöscht werden",
     "failedUpdate": "Aktualisierung fehlgeschlagen",
@@ -207,8 +205,8 @@
     "uploadingCount": "{count} Datei(en) werden hochgeladen...",
     "uploadedCount": "{count} Datei(en) hochgeladen",
     "uploadFailed": "Hochladen fehlgeschlagen",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "{name} konnte nicht hochgeladen werden",
+    "failedSaveFile": "{name} konnte nicht gespeichert werden",
     "deleted": "Datei gelöscht",
     "failedDelete": "Datei konnte nicht gelöscht werden",
     "failedUpdate": "Aktualisierung fehlgeschlagen",

+ 9 - 12
messages/de/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Barcode scannen",
     "partFound": "\"{name}\" aus dem Lager hinzugefügt",
     "partNotFound": "Kein Teil für Barcode \"{barcode}\" gefunden",
-    "deleteRow": "Delete row",
+    "deleteRow": "Zeile löschen",
     "outOfStock": "Nicht auf Lager",
     "onBackorder": "Im Rückstand ({count})",
     "nameMissingHint": "Zeilen ohne Teilenamen werden nicht gespeichert."
@@ -151,19 +151,17 @@
     "total": "Gesamt",
     "descriptionPlaceholder": "Beschreibung *",
     "subtotal": "Arbeitssumme: {amount}",
-    "addService": "Add Service",
+    "addService": "Leistung hinzufügen",
     "addFinding": "Beobachtung",
     "newObservation": "Neue Beobachtung",
     "addExisting": "Vorhandene anzeigen ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Menge",
+    "qtyOrHours": "Einheit / Stunden",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Stundenbasiert: nach geleisteten Stunden berechnet (Stunden × Satz/Std.). Zum Wechseln klicken.",
     "switchToServiceHint": "Pauschale: als Festpreis pro Einheit berechnet. Zum Wechseln klicken.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Zeile löschen",
     "descriptionMissingHint": "Zeilen ohne Beschreibung werden nicht gespeichert."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Videos ({count})",
     "diagnostics": "Diagnoseberichte ({count})",
     "documents": "Dokumente ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Herunterladen",
+    "close": "Schließen"
   },
   "share": {
     "title": "Rechnung teilen",
@@ -279,7 +277,7 @@
     "failedEmail": "E-Mail konnte nicht gesendet werden",
     "smsSent": "SMS gesendet",
     "failedSms": "SMS konnte nicht gesendet werden",
-    "copyLink": "Copy link"
+    "copyLink": "Link kopieren"
   },
   "sidebar": {
     "vehicle": "Fahrzeug",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Endzeit muss nach der Startzeit liegen",
     "failedUpdate": "Zeitplan konnte nicht aktualisiert werden",
     "splitCreated": "Auftrag erstreckt sich über mehrere Tage — Fortsetzung für den nächsten Tag erstellt",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Eigene",
     "readMore": "Mehr erfahren",
     "setDefaults": "Standardwerte",
     "workBay": "Arbeitsplatz",

+ 0 - 2
messages/en/quotes.json

@@ -96,8 +96,6 @@
     "subtotal": "Labor Subtotal: {amount}",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Hourly: priced by hours worked (hours × rate/hr). Click to switch.",
     "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch.",
     "deleteRow": "Delete row",

+ 0 - 3
messages/en/service.json

@@ -159,8 +159,6 @@
     "subtotal": "Labor Subtotal: {amount}",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Hourly: priced by hours worked (hours × rate/hr). Click to switch.",
     "switchToServiceHint": "Service: priced as a flat fee per unit. Click to switch.",
     "deleteRow": "Delete row",
@@ -374,7 +372,6 @@
     "endBeforeStart": "End time must be after start time",
     "failedUpdate": "Failed to update schedule times",
     "splitCreated": "Job spans multiple days — continuation created for next day",
-    "platformUsers": "Platform Users",
     "customTechnicians": "Custom",
     "readMore": "Read More",
     "setDefaults": "Set Defaults",

+ 14 - 16
messages/es/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Precio unitario",
     "total": "Total",
     "subtotal": "Subtotal de repuestos: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Eliminar fila",
+    "excludeFromTotal": "Excluir del total",
     "addFromInventory": "Del inventario",
     "unitCost": "Costo",
     "markupPercent": "Margen %",
@@ -91,17 +91,15 @@
     "rate": "Tarifa ({currency}/hr)",
     "total": "Total",
     "subtotal": "Subtotal de mano de obra: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Agregar servicio",
+    "qty": "Cant.",
+    "qtyOrHours": "Unidad / Horas",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Por horas: se calcula por las horas trabajadas (horas × tarifa/h). Haz clic para cambiar.",
     "switchToServiceHint": "Servicio: se cobra como tarifa fija por unidad. Haz clic para cambiar.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Eliminar fila",
+    "excludeFromTotal": "Excluir del total"
   },
   "notes": {
     "title": "Notas",
@@ -139,8 +137,8 @@
     "selectVehicle": "Seleccionar vehículo...",
     "selectCustomer": "Seleccionar cliente...",
     "none": "Ninguno",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Quitar vehículo",
+    "clearCustomer": "Quitar cliente"
   },
   "totals": {
     "title": "Totales",
@@ -172,7 +170,7 @@
     "failedEmail": "Error al enviar el correo",
     "smsSent": "SMS enviado",
     "failedSms": "Error al enviar el SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copiar enlace"
   },
   "images": {
     "title": "Imágenes del presupuesto",
@@ -187,8 +185,8 @@
     "uploadingCount": "Subiendo {count} imagen(es)...",
     "uploadedCount": "{count} imagen(es) subida(s)",
     "uploadFailed": "Error al subir",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "No se pudo subir {name}",
+    "failedSaveFile": "No se pudo guardar {name}",
     "deleted": "Imagen eliminada",
     "failedDelete": "Error al eliminar la imagen",
     "failedUpdate": "Error al actualizar",
@@ -207,8 +205,8 @@
     "uploadingCount": "Subiendo {count} archivo(s)...",
     "uploadedCount": "{count} archivo(s) subido(s)",
     "uploadFailed": "Error al subir",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "No se pudo subir {name}",
+    "failedSaveFile": "No se pudo guardar {name}",
     "deleted": "Archivo eliminado",
     "failedDelete": "Error al eliminar el archivo",
     "failedUpdate": "Error al actualizar",

+ 9 - 12
messages/es/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Escanear código de barras",
     "partFound": "\"{name}\" añadido del inventario",
     "partNotFound": "No se encontró pieza para el código de barras \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Eliminar fila",
     "outOfStock": "Agotado",
     "onBackorder": "En pedido pendiente ({count})",
     "nameMissingHint": "Las filas sin nombre de pieza no se guardan."
@@ -151,19 +151,17 @@
     "total": "Total",
     "descriptionPlaceholder": "Descripción *",
     "subtotal": "Subtotal de Mano de Obra: {amount}",
-    "addService": "Add Service",
+    "addService": "Agregar Servicio",
     "addFinding": "Observación",
     "newObservation": "Nueva observación",
     "addExisting": "Ver existentes ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Cantidad",
+    "qtyOrHours": "Unidad / Horas",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Por horas: se calcula por las horas trabajadas (horas × tarifa/h). Haz clic para cambiar.",
     "switchToServiceHint": "Servicio: se cobra como tarifa fija por unidad. Haz clic para cambiar.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Eliminar fila",
     "descriptionMissingHint": "Las filas sin descripción no se guardan."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Videos ({count})",
     "diagnostics": "Reportes de Diagnóstico ({count})",
     "documents": "Documentos ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Descargar",
+    "close": "Cerrar"
   },
   "share": {
     "title": "Compartir Factura",
@@ -279,7 +277,7 @@
     "failedEmail": "Error al enviar correo electrónico",
     "smsSent": "SMS enviado",
     "failedSms": "Error al enviar SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copiar enlace"
   },
   "sidebar": {
     "vehicle": "Vehículo",
@@ -374,8 +372,7 @@
     "endBeforeStart": "La hora de fin debe ser posterior a la hora de inicio",
     "failedUpdate": "Error al actualizar el horario",
     "splitCreated": "El trabajo abarca varios días — continuación creada para el día siguiente",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Personalizados",
     "readMore": "Leer más",
     "setDefaults": "Configurar valores",
     "workBay": "Puesto de trabajo",

+ 14 - 16
messages/fr/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Prix unitaire",
     "total": "Total",
     "subtotal": "Sous-total pièces : {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Supprimer la ligne",
+    "excludeFromTotal": "Exclure du total",
     "addFromInventory": "Du stock",
     "unitCost": "Coût",
     "markupPercent": "Marge %",
@@ -91,17 +91,15 @@
     "rate": "Tarif ({currency}/h)",
     "total": "Total",
     "subtotal": "Sous-total main-d'œuvre : {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Ajouter une prestation",
+    "qty": "Qté",
+    "qtyOrHours": "Unité / Heures",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "À l'heure : facturé selon les heures travaillées (heures × taux/h). Cliquez pour changer.",
     "switchToServiceHint": "Forfait : facturé au prix fixe par unité. Cliquez pour changer.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Supprimer la ligne",
+    "excludeFromTotal": "Exclure du total"
   },
   "notes": {
     "title": "Notes",
@@ -139,8 +137,8 @@
     "selectVehicle": "Sélectionner un véhicule...",
     "selectCustomer": "Sélectionner un client...",
     "none": "Aucun",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Retirer le véhicule",
+    "clearCustomer": "Retirer le client"
   },
   "totals": {
     "title": "Totaux",
@@ -172,7 +170,7 @@
     "failedEmail": "Échec de l'envoi de l'e-mail",
     "smsSent": "SMS envoyé",
     "failedSms": "Échec de l'envoi du SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copier le lien"
   },
   "images": {
     "title": "Images du devis",
@@ -187,8 +185,8 @@
     "uploadingCount": "Téléversement de {count} image(s)...",
     "uploadedCount": "{count} image(s) téléversée(s)",
     "uploadFailed": "Échec du téléversement",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Échec du téléversement de {name}",
+    "failedSaveFile": "Échec de l'enregistrement de {name}",
     "deleted": "Image supprimée",
     "failedDelete": "Échec de la suppression de l'image",
     "failedUpdate": "Échec de la mise à jour",
@@ -207,8 +205,8 @@
     "uploadingCount": "Téléversement de {count} fichier(s)...",
     "uploadedCount": "{count} fichier(s) téléversé(s)",
     "uploadFailed": "Échec du téléversement",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Échec du téléversement de {name}",
+    "failedSaveFile": "Échec de l'enregistrement de {name}",
     "deleted": "Fichier supprimé",
     "failedDelete": "Échec de la suppression du fichier",
     "failedUpdate": "Échec de la mise à jour",

+ 9 - 12
messages/fr/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Scanner le code-barres",
     "partFound": "\"{name}\" ajouté depuis l'inventaire",
     "partNotFound": "Aucune pièce trouvée pour le code-barres \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Supprimer la ligne",
     "outOfStock": "En rupture de stock",
     "onBackorder": "En réapprovisionnement ({count})",
     "nameMissingHint": "Les lignes sans nom de pièce ne sont pas enregistrées."
@@ -151,19 +151,17 @@
     "total": "Total",
     "descriptionPlaceholder": "Description *",
     "subtotal": "Sous-total Main-d'œuvre: {amount}",
-    "addService": "Add Service",
+    "addService": "Ajouter une Prestation",
     "addFinding": "Observation",
     "newObservation": "Nouvelle observation",
     "addExisting": "Voir existantes ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Qté",
+    "qtyOrHours": "Unité / Heures",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "À l'heure : facturé selon les heures travaillées (heures × taux/h). Cliquez pour changer.",
     "switchToServiceHint": "Forfait : facturé au prix fixe par unité. Cliquez pour changer.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Supprimer la ligne",
     "descriptionMissingHint": "Les lignes sans description ne sont pas enregistrées."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Vidéos ({count})",
     "diagnostics": "Rapports Diagnostiques ({count})",
     "documents": "Documents ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Télécharger",
+    "close": "Fermer"
   },
   "share": {
     "title": "Partager la Facture",
@@ -279,7 +277,7 @@
     "failedEmail": "Impossible d'envoyer l'email",
     "smsSent": "SMS envoyé",
     "failedSms": "Impossible d'envoyer le SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copier le lien"
   },
   "sidebar": {
     "vehicle": "Véhicule",
@@ -374,8 +372,7 @@
     "endBeforeStart": "L'heure de fin doit être après l'heure de début",
     "failedUpdate": "Impossible de mettre à jour le planning",
     "splitCreated": "Le travail s'étend sur plusieurs jours — suite créée pour le jour suivant",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Personnalisés",
     "readMore": "En savoir plus",
     "setDefaults": "Paramètres par défaut",
     "workBay": "Poste de travail",

+ 14 - 16
messages/it/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Prezzo unitario",
     "total": "Totale",
     "subtotal": "Subtotale ricambi: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Elimina riga",
+    "excludeFromTotal": "Escludi dal totale",
     "addFromInventory": "Dal magazzino",
     "unitCost": "Costo",
     "markupPercent": "Ricarico %",
@@ -91,17 +91,15 @@
     "rate": "Tariffa ({currency}/ora)",
     "total": "Totale",
     "subtotal": "Subtotale manodopera: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Aggiungi servizio",
+    "qty": "Qtà",
+    "qtyOrHours": "Unità / Ore",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "A ore: calcolato sulle ore lavorate (ore × tariffa/h). Clicca per cambiare.",
     "switchToServiceHint": "Servizio: addebitato come importo fisso per unità. Clicca per cambiare.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Elimina riga",
+    "excludeFromTotal": "Escludi dal totale"
   },
   "notes": {
     "title": "Note",
@@ -139,8 +137,8 @@
     "selectVehicle": "Seleziona veicolo...",
     "selectCustomer": "Seleziona cliente...",
     "none": "Nessuno",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Rimuovi veicolo",
+    "clearCustomer": "Rimuovi cliente"
   },
   "totals": {
     "title": "Totali",
@@ -172,7 +170,7 @@
     "failedEmail": "Invio e-mail non riuscito",
     "smsSent": "SMS inviato",
     "failedSms": "Invio SMS non riuscito",
-    "copyLink": "Copy link"
+    "copyLink": "Copia link"
   },
   "images": {
     "title": "Immagini del preventivo",
@@ -187,8 +185,8 @@
     "uploadingCount": "Caricamento di {count} immagine/i...",
     "uploadedCount": "{count} immagine/i caricata/e",
     "uploadFailed": "Caricamento non riuscito",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Caricamento di {name} non riuscito",
+    "failedSaveFile": "Salvataggio di {name} non riuscito",
     "deleted": "Immagine eliminata",
     "failedDelete": "Eliminazione immagine non riuscita",
     "failedUpdate": "Aggiornamento non riuscito",
@@ -207,8 +205,8 @@
     "uploadingCount": "Caricamento di {count} file...",
     "uploadedCount": "{count} file caricato/i",
     "uploadFailed": "Caricamento non riuscito",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Caricamento di {name} non riuscito",
+    "failedSaveFile": "Salvataggio di {name} non riuscito",
     "deleted": "File eliminato",
     "failedDelete": "Eliminazione file non riuscita",
     "failedUpdate": "Aggiornamento non riuscito",

+ 10 - 13
messages/it/service.json

@@ -117,7 +117,7 @@
     "addPart": "Aggiungi Pezzo",
     "partNumber": "Numero Pezzo",
     "name": "Nome",
-    "qty": "Qty",
+    "qty": "Qtà",
     "unitCost": "Costo",
     "unitCostHint": "Costo fornitore (interno — non mostrato al cliente)",
     "markupPercent": "Ricarico %",
@@ -136,7 +136,7 @@
     "scanTitle": "Scansiona codice a barre",
     "partFound": "\"{name}\" aggiunto dall'inventario",
     "partNotFound": "Nessun pezzo trovato per il codice a barre \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Elimina riga",
     "outOfStock": "Esaurito",
     "onBackorder": "In arretrato ({count})",
     "nameMissingHint": "Le righe senza nome del ricambio non vengono salvate."
@@ -151,19 +151,17 @@
     "total": "Totale",
     "descriptionPlaceholder": "Descrizione *",
     "subtotal": "Subtotale Manodopera: {amount}",
-    "addService": "Add Service",
+    "addService": "Aggiungi Servizio",
     "addFinding": "Osservazione",
     "newObservation": "Nuova osservazione",
     "addExisting": "Mostra esistenti ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Qtà",
+    "qtyOrHours": "Unità / Ore",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "A ore: calcolato sulle ore lavorate (ore × tariffa/h). Clicca per cambiare.",
     "switchToServiceHint": "Servizio: addebitato come importo fisso per unità. Clicca per cambiare.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Elimina riga",
     "descriptionMissingHint": "Le righe senza descrizione non vengono salvate."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Video ({count})",
     "diagnostics": "Rapporti Diagnostici ({count})",
     "documents": "Documenti ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Scarica",
+    "close": "Chiudi"
   },
   "share": {
     "title": "Condividi Fattura",
@@ -279,7 +277,7 @@
     "failedEmail": "Impossibile inviare email",
     "smsSent": "SMS inviato",
     "failedSms": "Impossibile inviare SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copia link"
   },
   "sidebar": {
     "vehicle": "Veicolo",
@@ -374,8 +372,7 @@
     "endBeforeStart": "L'ora di fine deve essere dopo l'ora di inizio",
     "failedUpdate": "Impossibile aggiornare la pianificazione",
     "splitCreated": "Il lavoro si estende su più giorni — continuazione creata per il giorno successivo",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Personalizzati",
     "readMore": "Scopri di più",
     "setDefaults": "Valori predefiniti",
     "workBay": "Postazione",

+ 11 - 13
messages/lt/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Vnt. kaina",
     "total": "Suma",
     "subtotal": "Dalių tarpinė suma: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Ištrinti eilutę",
+    "excludeFromTotal": "Neįskaičiuoti į sumą",
     "addFromInventory": "Iš sandėlio",
     "unitCost": "Savikaina",
     "markupPercent": "Antkainis %",
@@ -96,12 +96,10 @@
     "subtotal": "Darbų tarpinė suma: {amount}",
     "hourlyTag": "VAL",
     "serviceTag": "PSL",
-    "switchToHourly": "Perjungti į valandinį įkainį",
-    "switchToService": "Perjungti į paslaugos įkainį",
     "switchToHourlyHint": "Valandinis: skaičiuojama pagal darbo valandas (valandos × įkainis/val.). Spustelėkite, kad pakeistumėte.",
     "switchToServiceHint": "Paslauga: apmokestinama fiksuota suma už vienetą. Spustelėkite, kad pakeistumėte.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Ištrinti eilutę",
+    "excludeFromTotal": "Neįskaičiuoti į sumą"
   },
   "notes": {
     "title": "Pastabos",
@@ -139,8 +137,8 @@
     "selectVehicle": "Pasirinkite transporto priemonę...",
     "selectCustomer": "Pasirinkite klientą...",
     "none": "Nėra",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Pašalinti transporto priemonę",
+    "clearCustomer": "Pašalinti klientą"
   },
   "totals": {
     "title": "Sumos",
@@ -172,7 +170,7 @@
     "failedEmail": "Nepavyko išsiųsti laiško",
     "smsSent": "SMS išsiųsta",
     "failedSms": "Nepavyko išsiųsti SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Kopijuoti nuorodą"
   },
   "images": {
     "title": "Pasiūlymo vaizdai",
@@ -187,8 +185,8 @@
     "uploadingCount": "Įkeliama {count} vaizdas(-ų)...",
     "uploadedCount": "{count} vaizdas(-ų) įkelta",
     "uploadFailed": "Įkėlimas nepavyko",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Nepavyko įkelti {name}",
+    "failedSaveFile": "Nepavyko išsaugoti {name}",
     "deleted": "Vaizdas ištrintas",
     "failedDelete": "Nepavyko ištrinti vaizdo",
     "failedUpdate": "Nepavyko atnaujinti",
@@ -207,8 +205,8 @@
     "uploadingCount": "Įkeliama {count} failas(-ų)...",
     "uploadedCount": "{count} failas(-ų) įkelta",
     "uploadFailed": "Įkėlimas nepavyko",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Nepavyko įkelti {name}",
+    "failedSaveFile": "Nepavyko išsaugoti {name}",
     "deleted": "Failas ištrintas",
     "failedDelete": "Nepavyko ištrinti failo",
     "failedUpdate": "Nepavyko atnaujinti",

+ 5 - 8
messages/lt/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Nuskaityti brūkšninį kodą",
     "partFound": "Pridėta \"{name}\" iš inventoriaus",
     "partNotFound": "Dalis brūkšniniam kodui \"{barcode}\" nerasta",
-    "deleteRow": "Delete row",
+    "deleteRow": "Ištrinti eilutę",
     "outOfStock": "Nėra sandėlyje",
     "onBackorder": "Užsakoma ({count})",
     "nameMissingHint": "Eilutės be dalies pavadinimo neišsaugomos."
@@ -159,11 +159,9 @@
     "subtotal": "Darbų tarpinė suma: {amount}",
     "hourlyTag": "VAL",
     "serviceTag": "PSL",
-    "switchToHourly": "Perjungti į valandinį įkainį",
-    "switchToService": "Perjungti į paslaugos įkainį",
     "switchToHourlyHint": "Valandinis: skaičiuojama pagal darbo valandas (valandos × įkainis/val.). Spustelėkite, kad pakeistumėte.",
     "switchToServiceHint": "Paslauga: apmokestinama fiksuota suma už vienetą. Spustelėkite, kad pakeistumėte.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Ištrinti eilutę",
     "descriptionMissingHint": "Eilutės be aprašymo neišsaugomos."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Vaizdo įrašai ({count})",
     "diagnostics": "Diagnostikos ataskaitos ({count})",
     "documents": "Dokumentai ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Atsisiųsti",
+    "close": "Uždaryti"
   },
   "share": {
     "title": "Dalintis sąskaita",
@@ -279,7 +277,7 @@
     "failedEmail": "Nepavyko išsiųsti laiško",
     "smsSent": "SMS išsiųsta",
     "failedSms": "Nepavyko išsiųsti SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Kopijuoti nuorodą"
   },
   "sidebar": {
     "vehicle": "Transporto priemonė",
@@ -374,7 +372,6 @@
     "endBeforeStart": "Pabaigos laikas turi būti po pradžios laiko",
     "failedUpdate": "Nepavyko atnaujinti grafiko laikų",
     "splitCreated": "Darbas apima kelias dienas — tęsinys sukurtas kitai dienai",
-    "platformUsers": "Platformos naudotojai",
     "customTechnicians": "Pasirinktiniai",
     "readMore": "Skaityti daugiau",
     "setDefaults": "Nustatyti numatytuosius",

+ 14 - 16
messages/nb/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Enhetspris",
     "total": "Totalt",
     "subtotal": "Deler delsum: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Slett rad",
+    "excludeFromTotal": "Hold utenfor summen",
     "addFromInventory": "Fra lager",
     "unitCost": "Kostnad",
     "markupPercent": "Påslag %",
@@ -91,17 +91,15 @@
     "rate": "Sats ({currency}/t)",
     "total": "Totalt",
     "subtotal": "Arbeid delsum: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Legg til tjeneste",
+    "qty": "Ant.",
+    "qtyOrHours": "Enhet / Timer",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Timepris: beregnes etter arbeidede timer (timer × sats/t). Klikk for å bytte.",
     "switchToServiceHint": "Fastpris: beregnes som en fast sum per enhet. Klikk for å bytte.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Slett rad",
+    "excludeFromTotal": "Hold utenfor summen"
   },
   "notes": {
     "title": "Notater",
@@ -139,8 +137,8 @@
     "selectVehicle": "Velg kjøretøy...",
     "selectCustomer": "Velg kunde...",
     "none": "Ingen",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Fjern kjøretøy",
+    "clearCustomer": "Fjern kunde"
   },
   "totals": {
     "title": "Totaler",
@@ -172,7 +170,7 @@
     "failedEmail": "Kunne ikke sende e-post",
     "smsSent": "SMS sendt",
     "failedSms": "Kunne ikke sende SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Kopier lenke"
   },
   "images": {
     "title": "Tilbudsbilder",
@@ -187,8 +185,8 @@
     "uploadingCount": "Laster opp {count} bilde(r)...",
     "uploadedCount": "{count} bilde(r) lastet opp",
     "uploadFailed": "Opplasting mislyktes",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Kunne ikke laste opp {name}",
+    "failedSaveFile": "Kunne ikke lagre {name}",
     "deleted": "Bilde slettet",
     "failedDelete": "Kunne ikke slette bildet",
     "failedUpdate": "Kunne ikke oppdatere",
@@ -207,8 +205,8 @@
     "uploadingCount": "Laster opp {count} fil(er)...",
     "uploadedCount": "{count} fil(er) lastet opp",
     "uploadFailed": "Opplasting mislyktes",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Kunne ikke laste opp {name}",
+    "failedSaveFile": "Kunne ikke lagre {name}",
     "deleted": "Fil slettet",
     "failedDelete": "Kunne ikke slette filen",
     "failedUpdate": "Kunne ikke oppdatere",

+ 9 - 12
messages/nb/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Skann strekkode",
     "partFound": "\"{name}\" lagt til fra lageret",
     "partNotFound": "Ingen del funnet for strekkode \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Slett rad",
     "outOfStock": "Utsolgt",
     "onBackorder": "På restordre ({count})",
     "nameMissingHint": "Rader uten delenavn lagres ikke."
@@ -151,19 +151,17 @@
     "total": "Total",
     "descriptionPlaceholder": "Beskrivelse *",
     "subtotal": "Arbeid delsum: {amount}",
-    "addService": "Add Service",
+    "addService": "Legg til tjeneste",
     "addFinding": "Observasjon",
     "newObservation": "Ny observasjon",
     "addExisting": "Vis eksisterende ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Antall",
+    "qtyOrHours": "Enhet / Timer",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Timepris: beregnes etter arbeidede timer (timer × sats/t). Klikk for å bytte.",
     "switchToServiceHint": "Fastpris: beregnes som en fast sum per enhet. Klikk for å bytte.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Slett rad",
     "descriptionMissingHint": "Rader uten beskrivelse lagres ikke."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Videoer ({count})",
     "diagnostics": "Diagnostikkrapporter ({count})",
     "documents": "Dokumenter ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Last ned",
+    "close": "Lukk"
   },
   "share": {
     "title": "Del faktura",
@@ -279,7 +277,7 @@
     "failedEmail": "Kunne ikke sende e-post",
     "smsSent": "SMS sendt",
     "failedSms": "Kunne ikke sende SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Kopier lenke"
   },
   "sidebar": {
     "vehicle": "Kjøretøy",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Sluttid må være etter starttid",
     "failedUpdate": "Kunne ikke oppdatere tidsplan",
     "splitCreated": "Jobben går over flere dager — fortsettelse opprettet for neste dag",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Egne",
     "readMore": "Les mer",
     "setDefaults": "Standardinnstillinger",
     "workBay": "Arbeidsplass",

+ 14 - 16
messages/nl/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Stuksprijs",
     "total": "Totaal",
     "subtotal": "Subtotaal onderdelen: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Rij verwijderen",
+    "excludeFromTotal": "Buiten het totaal houden",
     "addFromInventory": "Uit voorraad",
     "unitCost": "Inkoop",
     "markupPercent": "Marge %",
@@ -91,17 +91,15 @@
     "rate": "Tarief ({currency}/uur)",
     "total": "Totaal",
     "subtotal": "Subtotaal arbeid: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Dienst toevoegen",
+    "qty": "Aantal",
+    "qtyOrHours": "Eenheid / Uren",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Per uur: berekend op gewerkte uren (uren × tarief/uur). Klik om te wisselen.",
     "switchToServiceHint": "Service: berekend als vast bedrag per eenheid. Klik om te wisselen.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Rij verwijderen",
+    "excludeFromTotal": "Buiten het totaal houden"
   },
   "notes": {
     "title": "Notities",
@@ -139,8 +137,8 @@
     "selectVehicle": "Selecteer voertuig...",
     "selectCustomer": "Selecteer klant...",
     "none": "Geen",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Voertuig wissen",
+    "clearCustomer": "Klant wissen"
   },
   "totals": {
     "title": "Totalen",
@@ -172,7 +170,7 @@
     "failedEmail": "E-mail verzenden mislukt",
     "smsSent": "SMS verzonden",
     "failedSms": "SMS verzenden mislukt",
-    "copyLink": "Copy link"
+    "copyLink": "Link kopiëren"
   },
   "images": {
     "title": "Offerteafbeeldingen",
@@ -187,8 +185,8 @@
     "uploadingCount": "{count} afbeelding(en) uploaden...",
     "uploadedCount": "{count} afbeelding(en) geüpload",
     "uploadFailed": "Upload mislukt",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Uploaden van {name} mislukt",
+    "failedSaveFile": "Opslaan van {name} mislukt",
     "deleted": "Afbeelding verwijderd",
     "failedDelete": "Afbeelding verwijderen mislukt",
     "failedUpdate": "Bijwerken mislukt",
@@ -207,8 +205,8 @@
     "uploadingCount": "{count} bestand(en) uploaden...",
     "uploadedCount": "{count} bestand(en) geüpload",
     "uploadFailed": "Upload mislukt",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Uploaden van {name} mislukt",
+    "failedSaveFile": "Opslaan van {name} mislukt",
     "deleted": "Bestand verwijderd",
     "failedDelete": "Bestand verwijderen mislukt",
     "failedUpdate": "Bijwerken mislukt",

+ 9 - 12
messages/nl/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Barcode scannen",
     "partFound": "\"{name}\" toegevoegd uit voorraad",
     "partNotFound": "Geen onderdeel gevonden voor barcode \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Rij verwijderen",
     "outOfStock": "Niet op voorraad",
     "onBackorder": "In nabestelling ({count})",
     "nameMissingHint": "Rijen zonder onderdeelnaam worden niet opgeslagen."
@@ -151,19 +151,17 @@
     "total": "Totaal",
     "descriptionPlaceholder": "Beschrijving *",
     "subtotal": "Arbeidskosten totaal: {amount}",
-    "addService": "Add Service",
+    "addService": "Dienst toevoegen",
     "addFinding": "Observatie",
     "newObservation": "Nieuwe observatie",
     "addExisting": "Bestaande tonen ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Hoeveelheid",
+    "qtyOrHours": "Eenheid / Uren",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Per uur: berekend op gewerkte uren (uren × tarief/uur). Klik om te wisselen.",
     "switchToServiceHint": "Service: berekend als vast bedrag per eenheid. Klik om te wisselen.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Rij verwijderen",
     "descriptionMissingHint": "Regels zonder omschrijving worden niet opgeslagen."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Video's ({count})",
     "diagnostics": "Diagnostische verslagen ({count})",
     "documents": "Documenten ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Downloaden",
+    "close": "Sluiten"
   },
   "share": {
     "title": "Factuur delen",
@@ -279,7 +277,7 @@
     "failedEmail": "E-mail verzenden mislukt",
     "smsSent": "SMS verzonden",
     "failedSms": "SMS verzenden mislukt",
-    "copyLink": "Copy link"
+    "copyLink": "Link kopiëren"
   },
   "sidebar": {
     "vehicle": "Voertuig",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Eindtijd moet na de starttijd zijn",
     "failedUpdate": "Planning bijwerken mislukt",
     "splitCreated": "Taak beslaat meerdere dagen — vervolg aangemaakt voor de volgende dag",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Eigen",
     "readMore": "Meer lezen",
     "setDefaults": "Standaardwaarden",
     "workBay": "Werkplek",

+ 14 - 16
messages/pl/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Cena jedn.",
     "total": "Suma",
     "subtotal": "Części suma częściowa: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Usuń wiersz",
+    "excludeFromTotal": "Wyłącz z sumy",
     "addFromInventory": "Z magazynu",
     "unitCost": "Koszt",
     "markupPercent": "Marża %",
@@ -91,17 +91,15 @@
     "rate": "Stawka ({currency}/godz.)",
     "total": "Suma",
     "subtotal": "Robocizna suma częściowa: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Dodaj usługę",
+    "qty": "Ilość",
+    "qtyOrHours": "Jednostka / Godziny",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Godzinowo: liczone według przepracowanych godzin (godziny × stawka/godz.). Kliknij, aby zmienić.",
     "switchToServiceHint": "Usługa: rozliczana jako stała opłata za jednostkę. Kliknij, aby zmienić.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Usuń wiersz",
+    "excludeFromTotal": "Wyłącz z sumy"
   },
   "notes": {
     "title": "Notatki",
@@ -139,8 +137,8 @@
     "selectVehicle": "Wybierz pojazd...",
     "selectCustomer": "Wybierz klienta...",
     "none": "Brak",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Usuń pojazd",
+    "clearCustomer": "Usuń klienta"
   },
   "totals": {
     "title": "Podsumowanie",
@@ -172,7 +170,7 @@
     "failedEmail": "Nie udało się wysłać e-maila",
     "smsSent": "SMS wysłany",
     "failedSms": "Nie udało się wysłać SMS-a",
-    "copyLink": "Copy link"
+    "copyLink": "Kopiuj link"
   },
   "images": {
     "title": "Zdjęcia wyceny",
@@ -187,8 +185,8 @@
     "uploadingCount": "Przesyłanie {count} zdjęcia/zdjęć...",
     "uploadedCount": "Przesłano {count} zdjęcie/zdjęć",
     "uploadFailed": "Przesyłanie nie powiodło się",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Nie udało się przesłać {name}",
+    "failedSaveFile": "Nie udało się zapisać {name}",
     "deleted": "Zdjęcie usunięte",
     "failedDelete": "Nie udało się usunąć zdjęcia",
     "failedUpdate": "Nie udało się zaktualizować",
@@ -207,8 +205,8 @@
     "uploadingCount": "Przesyłanie {count} pliku/plików...",
     "uploadedCount": "Przesłano {count} plik/plików",
     "uploadFailed": "Przesyłanie nie powiodło się",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Nie udało się przesłać {name}",
+    "failedSaveFile": "Nie udało się zapisać {name}",
     "deleted": "Plik usunięty",
     "failedDelete": "Nie udało się usunąć pliku",
     "failedUpdate": "Nie udało się zaktualizować",

+ 9 - 12
messages/pl/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Skanuj kod kreskowy",
     "partFound": "Dodano \"{name}\" z magazynu",
     "partNotFound": "Nie znaleziono części dla kodu kreskowego \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Usuń wiersz",
     "outOfStock": "Brak w magazynie",
     "onBackorder": "W zamówieniu ({count})",
     "nameMissingHint": "Wiersze bez nazwy części nie są zapisywane."
@@ -151,19 +151,17 @@
     "total": "Razem",
     "descriptionPlaceholder": "Opis *",
     "subtotal": "Razem Praca: {amount}",
-    "addService": "Add Service",
+    "addService": "Dodaj Usługę",
     "addFinding": "Obserwacja",
     "newObservation": "Nowa obserwacja",
     "addExisting": "Pokaż istniejące ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Ilość",
+    "qtyOrHours": "Jednostka / Godziny",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Godzinowo: liczone według przepracowanych godzin (godziny × stawka/godz.). Kliknij, aby zmienić.",
     "switchToServiceHint": "Usługa: rozliczana jako stała opłata za jednostkę. Kliknij, aby zmienić.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Usuń wiersz",
     "descriptionMissingHint": "Wiersze bez opisu nie są zapisywane."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Wideo ({count})",
     "diagnostics": "Raporty Diagnostyczne ({count})",
     "documents": "Dokumenty ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Pobierz",
+    "close": "Zamknij"
   },
   "share": {
     "title": "Udostępnij Fakturę",
@@ -279,7 +277,7 @@
     "failedEmail": "Nie udało się wysłać email",
     "smsSent": "SMS wysłany",
     "failedSms": "Nie udało się wysłać SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Kopiuj link"
   },
   "sidebar": {
     "vehicle": "Pojazd",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Czas zakończenia musi być po czasie rozpoczęcia",
     "failedUpdate": "Nie udało się zaktualizować harmonogramu",
     "splitCreated": "Zadanie obejmuje wiele dni — kontynuacja utworzona na następny dzień",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Własne",
     "readMore": "Czytaj więcej",
     "setDefaults": "Ustaw domyślne",
     "workBay": "Stanowisko",

+ 14 - 16
messages/pt-BR/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Preço unitário",
     "total": "Total",
     "subtotal": "Subtotal de peças: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Excluir linha",
+    "excludeFromTotal": "Não somar ao total",
     "addFromInventory": "Do estoque",
     "unitCost": "Custo",
     "markupPercent": "Margem %",
@@ -91,17 +91,15 @@
     "rate": "Taxa ({currency}/h)",
     "total": "Total",
     "subtotal": "Subtotal de mão de obra: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Adicionar serviço",
+    "qty": "Qtd.",
+    "qtyOrHours": "Unidade / Horas",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Por hora: calculado pelas horas trabalhadas (horas × taxa/h). Clique para alterar.",
     "switchToServiceHint": "Serviço: cobrado como valor fixo por unidade. Clique para alterar.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Excluir linha",
+    "excludeFromTotal": "Não somar ao total"
   },
   "notes": {
     "title": "Notas",
@@ -139,8 +137,8 @@
     "selectVehicle": "Selecionar veículo...",
     "selectCustomer": "Selecionar cliente...",
     "none": "Nenhum",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Remover veículo",
+    "clearCustomer": "Remover cliente"
   },
   "totals": {
     "title": "Totais",
@@ -172,7 +170,7 @@
     "failedEmail": "Falha ao enviar o e-mail",
     "smsSent": "SMS enviado",
     "failedSms": "Falha ao enviar o SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copiar link"
   },
   "images": {
     "title": "Imagens do orçamento",
@@ -187,8 +185,8 @@
     "uploadingCount": "Enviando {count} imagem(ns)...",
     "uploadedCount": "{count} imagem(ns) enviada(s)",
     "uploadFailed": "Falha no envio",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Falha ao enviar {name}",
+    "failedSaveFile": "Falha ao salvar {name}",
     "deleted": "Imagem excluída",
     "failedDelete": "Falha ao excluir a imagem",
     "failedUpdate": "Falha ao atualizar",
@@ -207,8 +205,8 @@
     "uploadingCount": "Enviando {count} arquivo(s)...",
     "uploadedCount": "{count} arquivo(s) enviado(s)",
     "uploadFailed": "Falha no envio",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Falha ao enviar {name}",
+    "failedSaveFile": "Falha ao salvar {name}",
     "deleted": "Arquivo excluído",
     "failedDelete": "Falha ao excluir o arquivo",
     "failedUpdate": "Falha ao atualizar",

+ 9 - 12
messages/pt-BR/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Escanear código de barras",
     "partFound": "\"{name}\" adicionado do estoque",
     "partNotFound": "Nenhuma peça encontrada para o código de barras \"{barcode}\"",
-    "deleteRow": "Delete row",
+    "deleteRow": "Excluir linha",
     "outOfStock": "Fora de estoque",
     "onBackorder": "Sob encomenda ({count})",
     "nameMissingHint": "Linhas sem nome da peça não são salvas."
@@ -151,19 +151,17 @@
     "total": "Total",
     "descriptionPlaceholder": "Descrição *",
     "subtotal": "Subtotal de Mão de Obra: {amount}",
-    "addService": "Add Service",
+    "addService": "Adicionar Serviço",
     "addFinding": "Observação",
     "newObservation": "Nova observação",
     "addExisting": "Ver existentes ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Qtd",
+    "qtyOrHours": "Unidade / Horas",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Por hora: calculado pelas horas trabalhadas (horas × taxa/h). Clique para alterar.",
     "switchToServiceHint": "Serviço: cobrado como valor fixo por unidade. Clique para alterar.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Excluir linha",
     "descriptionMissingHint": "Linhas sem descrição não são salvas."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Vídeos ({count})",
     "diagnostics": "Relatórios de Diagnóstico ({count})",
     "documents": "Documentos ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Baixar",
+    "close": "Fechar"
   },
   "share": {
     "title": "Compartilhar Fatura",
@@ -279,7 +277,7 @@
     "failedEmail": "Falha ao enviar email",
     "smsSent": "SMS enviado",
     "failedSms": "Falha ao enviar SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Copiar link"
   },
   "sidebar": {
     "vehicle": "Veículo",
@@ -374,8 +372,7 @@
     "endBeforeStart": "A hora de término deve ser após a hora de início",
     "failedUpdate": "Falha ao atualizar o agendamento",
     "splitCreated": "O trabalho abrange vários dias — continuação criada para o dia seguinte",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Personalizados",
     "readMore": "Saiba mais",
     "setDefaults": "Valores padrão",
     "workBay": "Baia",

+ 11 - 13
messages/ru/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Цена за ед.",
     "total": "Итого",
     "subtotal": "Подитог запчастей: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Удалить строку",
+    "excludeFromTotal": "Не включать в итог",
     "addFromInventory": "Со склада",
     "unitCost": "Себестоимость",
     "markupPercent": "Наценка %",
@@ -96,12 +96,10 @@
     "qtyOrHours": "Услуга / Часы",
     "hourlyTag": "н/ч",
     "serviceTag": "усл.",
-    "switchToHourly": "Переключить на почасовую оплату",
-    "switchToService": "Переключить на оплату услуг",
     "switchToHourlyHint": "Почасовая: оценка по нормо-часам (часы × ставка/час). Нажмите для изменения.",
     "switchToServiceHint": "Услуга: фиксированная цена за единицу. Нажмите для изменения.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Удалить строку",
+    "excludeFromTotal": "Не включать в итог"
   },
   "notes": {
     "title": "Заметки",
@@ -139,8 +137,8 @@
     "selectVehicle": "Выберите транспортное средство...",
     "selectCustomer": "Выберите клиента...",
     "none": "Нет",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Убрать автомобиль",
+    "clearCustomer": "Убрать клиента"
   },
   "totals": {
     "title": "Итоги",
@@ -172,7 +170,7 @@
     "failedEmail": "Не удалось отправить письмо",
     "smsSent": "SMS отправлено",
     "failedSms": "Не удалось отправить SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Копировать ссылку"
   },
   "images": {
     "title": "Изображения предложения",
@@ -187,8 +185,8 @@
     "uploadingCount": "Загрузка изображений: {count}...",
     "uploadedCount": "Загружено изображений: {count}",
     "uploadFailed": "Загрузка не удалась",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Не удалось загрузить {name}",
+    "failedSaveFile": "Не удалось сохранить {name}",
     "deleted": "Изображение удалено",
     "failedDelete": "Не удалось удалить изображение",
     "failedUpdate": "Не удалось обновить",
@@ -207,8 +205,8 @@
     "uploadingCount": "Загрузка файлов: {count}...",
     "uploadedCount": "Загружено файлов: {count}",
     "uploadFailed": "Загрузка не удалась",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "Не удалось загрузить {name}",
+    "failedSaveFile": "Не удалось сохранить {name}",
     "deleted": "Файл удалён",
     "failedDelete": "Не удалось удалить файл",
     "failedUpdate": "Не удалось обновить",

+ 7 - 10
messages/ru/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Сканировать штрихкод",
     "partFound": "\"{name}\" добавлено из склада",
     "partNotFound": "Деталь для штрихкода \"{barcode}\" не найдена",
-    "deleteRow": "Delete row",
+    "deleteRow": "Удалить строку",
     "outOfStock": "Нет в наличии",
     "onBackorder": "Под заказ ({count})",
     "nameMissingHint": "Строки без названия детали не сохраняются."
@@ -155,15 +155,13 @@
     "addFinding": "Наблюдение",
     "newObservation": "Новое наблюдение",
     "addExisting": "Показать существующие ({count})",
-    "qty": "кол-во",
+    "qty": "Кол-во",
     "qtyOrHours": "Услуга / Часы",
     "hourlyTag": "н/ч",
     "serviceTag": "усл.",
-    "switchToHourly": "Изменить на почасовую оплату",
-    "switchToService": "Изменить на оплату услуг",
     "switchToHourlyHint": "Почасовая: оценка по нормо-часам (часы × ставка/час). Нажмите для изменения.",
     "switchToServiceHint": "Услуга: фиксированная цена за единицу. Нажмите для изменения.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Удалить строку",
     "descriptionMissingHint": "Строки без описания не сохраняются."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Видео ({count})",
     "diagnostics": "Диагностические отчёты ({count})",
     "documents": "Документы ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "Скачать",
+    "close": "Закрыть"
   },
   "share": {
     "title": "Поделиться счётом",
@@ -279,7 +277,7 @@
     "failedEmail": "Не удалось отправить письмо",
     "smsSent": "SMS отправлено",
     "failedSms": "Не удалось отправить SMS",
-    "copyLink": "Copy link"
+    "copyLink": "Копировать ссылку"
   },
   "sidebar": {
     "vehicle": "Транспортное средство",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Время окончания должно быть позже времени начала",
     "failedUpdate": "Не удалось обновить время в расписании",
     "splitCreated": "Работа занимает несколько дней — создано продолжение на следующий день",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Свои",
     "readMore": "Подробнее",
     "setDefaults": "Настройки по умолчанию",
     "workBay": "Пост",

+ 14 - 16
messages/tr/quotes.json

@@ -74,8 +74,8 @@
     "unitPrice": "Birim Fiyat",
     "total": "Toplam",
     "subtotal": "Parçalar Ara Toplam: {amount}",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total",
+    "deleteRow": "Satırı sil",
+    "excludeFromTotal": "Toplama dahil etme",
     "addFromInventory": "Stoktan",
     "unitCost": "Maliyet",
     "markupPercent": "Kâr Marjı %",
@@ -91,17 +91,15 @@
     "rate": "Ücret ({currency}/sa)",
     "total": "Toplam",
     "subtotal": "İşçilik Ara Toplam: {amount}",
-    "addService": "Add Service",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "addService": "Hizmet Ekle",
+    "qty": "Adet",
+    "qtyOrHours": "Birim / Saat",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Saatlik: çalışılan saate göre hesaplanır (saat × ücret/saat). Değiştirmek için tıklayın.",
     "switchToServiceHint": "Hizmet: birim başına sabit ücret olarak hesaplanır. Değiştirmek için tıklayın.",
-    "deleteRow": "Delete row",
-    "excludeFromTotal": "Exclude from total"
+    "deleteRow": "Satırı sil",
+    "excludeFromTotal": "Toplama dahil etme"
   },
   "notes": {
     "title": "Notlar",
@@ -139,8 +137,8 @@
     "selectVehicle": "Araç seçin...",
     "selectCustomer": "Müşteri seçin...",
     "none": "Yok",
-    "clearVehicle": "Clear vehicle",
-    "clearCustomer": "Clear customer"
+    "clearVehicle": "Aracı kaldır",
+    "clearCustomer": "Müşteriyi kaldır"
   },
   "totals": {
     "title": "Toplamlar",
@@ -172,7 +170,7 @@
     "failedEmail": "E-posta gönderilemedi",
     "smsSent": "SMS gönderildi",
     "failedSms": "SMS gönderilemedi",
-    "copyLink": "Copy link"
+    "copyLink": "Bağlantıyı kopyala"
   },
   "images": {
     "title": "Teklif Görselleri",
@@ -187,8 +185,8 @@
     "uploadingCount": "{count} görsel yükleniyor...",
     "uploadedCount": "{count} görsel yüklendi",
     "uploadFailed": "Yükleme başarısız",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "{name} yüklenemedi",
+    "failedSaveFile": "{name} kaydedilemedi",
     "deleted": "Görsel silindi",
     "failedDelete": "Görsel silinemedi",
     "failedUpdate": "Güncelleme başarısız",
@@ -207,8 +205,8 @@
     "uploadingCount": "{count} dosya yükleniyor...",
     "uploadedCount": "{count} dosya yüklendi",
     "uploadFailed": "Yükleme başarısız",
-    "failedUploadFile": "Failed to upload {name}",
-    "failedSaveFile": "Failed to save {name}",
+    "failedUploadFile": "{name} yüklenemedi",
+    "failedSaveFile": "{name} kaydedilemedi",
     "deleted": "Dosya silindi",
     "failedDelete": "Dosya silinemedi",
     "failedUpdate": "Güncelleme başarısız",

+ 9 - 12
messages/tr/service.json

@@ -136,7 +136,7 @@
     "scanTitle": "Barkod tara",
     "partFound": "\"{name}\" envanterden eklendi",
     "partNotFound": "\"{barcode}\" barkodu için parça bulunamadı",
-    "deleteRow": "Delete row",
+    "deleteRow": "Satırı sil",
     "outOfStock": "Stokta yok",
     "onBackorder": "Ön siparişte ({count})",
     "nameMissingHint": "Parça adı olmayan satırlar kaydedilmez."
@@ -151,19 +151,17 @@
     "total": "Toplam",
     "descriptionPlaceholder": "Açıklama *",
     "subtotal": "İşçilik Ara Toplamı: {amount}",
-    "addService": "Add Service",
+    "addService": "Hizmet Ekle",
     "addFinding": "Gözlem",
     "newObservation": "Yeni gözlem",
     "addExisting": "Mevcut göster ({count})",
-    "qty": "Qty",
-    "qtyOrHours": "Unit / Hours",
+    "qty": "Mik.",
+    "qtyOrHours": "Birim / Saat",
     "hourlyTag": "HR",
     "serviceTag": "SVC",
-    "switchToHourly": "Switch to hourly pricing",
-    "switchToService": "Switch to service pricing",
     "switchToHourlyHint": "Saatlik: çalışılan saate göre hesaplanır (saat × ücret/saat). Değiştirmek için tıklayın.",
     "switchToServiceHint": "Hizmet: birim başına sabit ücret olarak hesaplanır. Değiştirmek için tıklayın.",
-    "deleteRow": "Delete row",
+    "deleteRow": "Satırı sil",
     "descriptionMissingHint": "Açıklaması olmayan satırlar kaydedilmez."
   },
   "totals": {
@@ -261,8 +259,8 @@
     "videos": "Videolar ({count})",
     "diagnostics": "Tanı Raporları ({count})",
     "documents": "Belgeler ({count})",
-    "download": "Download",
-    "close": "Close"
+    "download": "İndir",
+    "close": "Kapat"
   },
   "share": {
     "title": "Faturayı Paylaş",
@@ -279,7 +277,7 @@
     "failedEmail": "E-posta gönderilemedi",
     "smsSent": "SMS gönderildi",
     "failedSms": "SMS gönderilemedi",
-    "copyLink": "Copy link"
+    "copyLink": "Bağlantıyı kopyala"
   },
   "sidebar": {
     "vehicle": "Araç",
@@ -374,8 +372,7 @@
     "endBeforeStart": "Bitiş saati başlangıç saatinden sonra olmalıdır",
     "failedUpdate": "Zamanlama güncellenemedi",
     "splitCreated": "İş birden fazla güne yayılıyor — sonraki gün için devam oluşturuldu",
-    "platformUsers": "Platform Users",
-    "customTechnicians": "Custom",
+    "customTechnicians": "Özel",
     "readMore": "Daha fazla",
     "setDefaults": "Varsayılanlar",
     "workBay": "Çalışma alanı",

+ 34 - 0
src/__tests__/features/invoices/design-source-parity.test.ts

@@ -22,7 +22,9 @@ import { buildInvoicePrintSpec } from '@/features/invoice-designer/Pdf/buildInvo
 import { liveInvoiceSettings } from '@/features/invoices/Lib/assembleInvoicePrint'
 import {
   designSourceFromSettings,
+  designSourceFromSnapshot,
   designSourceFromStored,
+  materializeDesignSource,
   templateConfigFromSource,
 } from '@/features/invoice-designer/Lib/designSource'
 import {
@@ -122,3 +124,35 @@ describe('a design row saved before the stamp was written to it', () => {
     )
   })
 })
+
+/** The whole printed sheet, so a changed label shows up and not just a block. */
+function sheet(template: ReturnType<typeof templateConfigFromSource>): string {
+  return JSON.stringify(buildInvoicePrintSpec({ data, template }).blocks)
+}
+
+describe('an invoice issued by a workshop that never opened the designer', () => {
+  // No layout in settings: the classic sheet, with no version on it.
+  const live = designSourceFromSettings({}, 'invoice')
+  // What issuing freezes into the snapshot row.
+  const frozen = materializeDesignSource(live)
+
+  it('reads its snapshot back without a version stamp', () => {
+    expect(frozen.layout.version).toBeUndefined()
+    expect(designSourceFromSnapshot(frozen.layout, frozen.template)?.layout.version).toBeUndefined()
+  })
+
+  it('prints the same sheet from the snapshot as it did live', () => {
+    const fromSnapshot = designSourceFromSnapshot(frozen.layout, frozen.template)
+    expect(fromSnapshot).not.toBeNull()
+    expect(sheet(templateConfigFromSource(fromSnapshot!))).toBe(
+      sheet(templateConfigFromSource(live))
+    )
+  })
+
+  it('would print a different sheet if the snapshot were read as a design row', () => {
+    // The regression this guards: the share link read the snapshot through the
+    // design-row reader, which stamps a version and swaps in the designer header.
+    const asRow = designSourceFromStored(frozen.layout, frozen.template)
+    expect(sheet(templateConfigFromSource(asRow!))).not.toBe(sheet(templateConfigFromSource(live)))
+  })
+})

+ 30 - 9
src/features/invoice-designer/Lib/designSource.ts

@@ -52,6 +52,15 @@ export const designTemplateSchema = z
 
 export const DESIGN_DOCUMENT_TYPES: DocumentType[] = ['invoice', 'quote']
 
+/** A stored layout and template read back as they were written, or null. */
+function parseStoredSource(layout: unknown, template: unknown): DesignSource | null {
+  const parsedTemplate = designTemplateSchema.safeParse(template ?? {})
+  if (!parsedTemplate.success) return null
+  const parsedLayout = invoiceLayoutConfigSchema.partial().safeParse(layout ?? {})
+  if (!parsedLayout.success) return null
+  return { layout: parsedLayout.data, template: parsedTemplate.data }
+}
+
 /**
  * Reads a stored design row's JSON columns back into a source, or null.
  *
@@ -62,17 +71,29 @@ export const DESIGN_DOCUMENT_TYPES: DocumentType[] = ['invoice', 'quote']
  * printed one way as the workshop default and another way when an invoice
  * picked it by name, which is what the classic fallback does to the header
  * and the title.
+ *
+ * Not for issued snapshots: see designSourceFromSnapshot.
  */
 export function designSourceFromStored(layout: unknown, template: unknown): DesignSource | null {
-  const parsedTemplate = designTemplateSchema.safeParse(template ?? {})
-  if (!parsedTemplate.success) return null
-  const parsedLayout = invoiceLayoutConfigSchema.partial().safeParse(layout ?? {})
-  if (!parsedLayout.success) return null
-  const stored = parsedLayout.data
-  return {
-    layout: stored.version === undefined ? { ...stored, version: DESIGNER_LAYOUT_VERSION } : stored,
-    template: parsedTemplate.data,
-  }
+  const source = parseStoredSource(layout, template)
+  if (!source) return null
+  return source.layout.version === undefined
+    ? { ...source, layout: { ...source.layout, version: DESIGNER_LAYOUT_VERSION } }
+    : source
+}
+
+/**
+ * Reads an issued invoice's frozen design back exactly as it was frozen.
+ *
+ * No version stamp, unlike a design row. The designer is not the only writer
+ * of snapshots: issuing freezes whatever look the invoice printed with, and a
+ * workshop that never opened the designer prints a settings layout with no
+ * version, the classic sheet. Stamping that on the way out turned the classic
+ * header into the designer's the moment the invoice was shared, so the link a
+ * customer opened no longer matched the PDF they had been sent.
+ */
+export function designSourceFromSnapshot(layout: unknown, template: unknown): DesignSource | null {
+  return parseStoredSource(layout, template)
 }
 
 function parseLayoutSetting(value: string | undefined): Partial<InvoiceLayoutConfig> {

+ 2 - 1
src/features/invoices/Lib/assembleInvoicePrint.ts

@@ -35,6 +35,7 @@ import type {
 } from '@/features/vehicles/Components/invoice-pdf/types'
 import {
   designSourceFromSettings,
+  designSourceFromSnapshot,
   designSourceFromStored,
   templateConfigFromSource,
   type DesignSource,
@@ -386,7 +387,7 @@ function assembleFrozen(
   // A snapshot that cannot be read falls back to the live look rather than
   // to nothing: the words on the sheet are still the frozen ones.
   const designSource =
-    (snapshot && designSourceFromStored(snapshot.layout, snapshot.template)) ||
+    (snapshot && designSourceFromSnapshot(snapshot.layout, snapshot.template)) ||
     designSourceFromSettings(settingsMap, 'invoice')
   const template = templateConfigFromSource(designSource)
   const logoDataUri = record.issuedLogoSnapshot