Kaynağa Gözat

Merge branch 'main' into demo

Bernt Christian Egeland 3 hafta önce
ebeveyn
işleme
a520032478
100 değiştirilmiş dosya ile 7963 ekleme ve 507 silme
  1. 2 0
      .dockerignore
  2. 17 0
      .env.example
  3. 19 1
      .github/workflows/deploy-prod.yml
  4. 19 1
      .github/workflows/deploy-staging.yml
  5. 8 1
      Dockerfile
  6. 1 1
      messages/de/aiChat.json
  7. 27 2
      messages/de/audit.json
  8. 2 1
      messages/de/auth.json
  9. 74 7
      messages/de/calendar.json
  10. 8 40
      messages/de/customers.json
  11. 12 1
      messages/de/dashboard.json
  12. 228 0
      messages/de/dataImport.json
  13. 584 0
      messages/de/integrations.json
  14. 2 1
      messages/de/messages.json
  15. 5 1
      messages/de/messaging.json
  16. 16 2
      messages/de/navigation.json
  17. 3 1
      messages/de/onboarding.json
  18. 51 1
      messages/de/portal.json
  19. 1 0
      messages/de/scheduledMessages.json
  20. 59 2
      messages/de/service.json
  21. 101 27
      messages/de/settings.json
  22. 217 3
      messages/de/vehicles.json
  23. 1 1
      messages/en/aiChat.json
  24. 27 2
      messages/en/audit.json
  25. 2 1
      messages/en/auth.json
  26. 74 7
      messages/en/calendar.json
  27. 8 40
      messages/en/customers.json
  28. 12 1
      messages/en/dashboard.json
  29. 228 0
      messages/en/dataImport.json
  30. 584 0
      messages/en/integrations.json
  31. 2 1
      messages/en/messages.json
  32. 5 1
      messages/en/messaging.json
  33. 16 2
      messages/en/navigation.json
  34. 3 1
      messages/en/onboarding.json
  35. 51 1
      messages/en/portal.json
  36. 1 0
      messages/en/scheduledMessages.json
  37. 59 2
      messages/en/service.json
  38. 101 27
      messages/en/settings.json
  39. 217 3
      messages/en/vehicles.json
  40. 1 1
      messages/es/aiChat.json
  41. 27 2
      messages/es/audit.json
  42. 2 1
      messages/es/auth.json
  43. 74 7
      messages/es/calendar.json
  44. 8 40
      messages/es/customers.json
  45. 12 1
      messages/es/dashboard.json
  46. 228 0
      messages/es/dataImport.json
  47. 584 0
      messages/es/integrations.json
  48. 2 1
      messages/es/messages.json
  49. 5 1
      messages/es/messaging.json
  50. 16 2
      messages/es/navigation.json
  51. 3 1
      messages/es/onboarding.json
  52. 51 1
      messages/es/portal.json
  53. 1 0
      messages/es/scheduledMessages.json
  54. 59 2
      messages/es/service.json
  55. 101 27
      messages/es/settings.json
  56. 217 3
      messages/es/vehicles.json
  57. 1 1
      messages/fr/aiChat.json
  58. 27 2
      messages/fr/audit.json
  59. 2 1
      messages/fr/auth.json
  60. 74 7
      messages/fr/calendar.json
  61. 8 40
      messages/fr/customers.json
  62. 12 1
      messages/fr/dashboard.json
  63. 228 0
      messages/fr/dataImport.json
  64. 584 0
      messages/fr/integrations.json
  65. 2 1
      messages/fr/messages.json
  66. 5 1
      messages/fr/messaging.json
  67. 16 2
      messages/fr/navigation.json
  68. 3 1
      messages/fr/onboarding.json
  69. 51 1
      messages/fr/portal.json
  70. 1 0
      messages/fr/scheduledMessages.json
  71. 59 2
      messages/fr/service.json
  72. 101 27
      messages/fr/settings.json
  73. 217 3
      messages/fr/vehicles.json
  74. 1 1
      messages/it/aiChat.json
  75. 27 2
      messages/it/audit.json
  76. 2 1
      messages/it/auth.json
  77. 74 7
      messages/it/calendar.json
  78. 8 40
      messages/it/customers.json
  79. 12 1
      messages/it/dashboard.json
  80. 228 0
      messages/it/dataImport.json
  81. 584 0
      messages/it/integrations.json
  82. 2 1
      messages/it/messages.json
  83. 5 1
      messages/it/messaging.json
  84. 16 2
      messages/it/navigation.json
  85. 3 1
      messages/it/onboarding.json
  86. 51 1
      messages/it/portal.json
  87. 1 0
      messages/it/scheduledMessages.json
  88. 59 2
      messages/it/service.json
  89. 101 27
      messages/it/settings.json
  90. 217 3
      messages/it/vehicles.json
  91. 1 1
      messages/lt/aiChat.json
  92. 27 2
      messages/lt/audit.json
  93. 2 1
      messages/lt/auth.json
  94. 74 7
      messages/lt/calendar.json
  95. 8 40
      messages/lt/customers.json
  96. 12 1
      messages/lt/dashboard.json
  97. 228 0
      messages/lt/dataImport.json
  98. 584 0
      messages/lt/integrations.json
  99. 2 1
      messages/lt/messages.json
  100. 5 1
      messages/lt/messaging.json

+ 2 - 0
.dockerignore

@@ -7,3 +7,5 @@ node_modules
 .devcontainer
 plans
 public/uploads
+backups
+data/uploads

+ 17 - 0
.env.example

@@ -19,3 +19,20 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000"
 # Without the origin lock it is worse than useless, because cf-connecting-ip is
 # then just a header anyone reaching the box directly can write for themselves.
 TRUST_CF_CONNECTING_IP=false
+
+# Integrations (Settings → Integrations)
+# Key that seals third-party tokens at rest (generate with: openssl rand -hex 32).
+# Without it the key is derived from BETTER_AUTH_SECRET, with a warning at startup.
+INTEGRATIONS_ENCRYPTION_KEY=""
+# OAuth apps the platform registers with each provider. Leave empty on a
+# self-hosted install to have each workshop enter its own app credentials.
+GOOGLE_INTEGRATION_CLIENT_ID=""
+GOOGLE_INTEGRATION_CLIENT_SECRET=""
+MICROSOFT_INTEGRATION_CLIENT_ID=""
+MICROSOFT_INTEGRATION_CLIENT_SECRET=""
+ZOOM_INTEGRATION_CLIENT_ID=""
+ZOOM_INTEGRATION_CLIENT_SECRET=""
+# Intuit development keys reach sandbox companies, production keys live ones;
+# the connector works out which it was given.
+QUICKBOOKS_INTEGRATION_CLIENT_ID=""
+QUICKBOOKS_INTEGRATION_CLIENT_SECRET=""

+ 19 - 1
.github/workflows/deploy-prod.yml

@@ -48,11 +48,20 @@ jobs:
           POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
           POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
           BACKUP_HEARTBEAT_TOKEN: ${{ secrets.BACKUP_HEARTBEAT_TOKEN }}
+          INTEGRATIONS_ENCRYPTION_KEY: ${{ secrets.CLOUD_INTEGRATIONS_ENCRYPTION_KEY }}
+          GOOGLE_INTEGRATION_CLIENT_ID: ${{ secrets.CLOUD_GOOGLE_INTEGRATION_CLIENT_ID }}
+          GOOGLE_INTEGRATION_CLIENT_SECRET: ${{ secrets.CLOUD_GOOGLE_INTEGRATION_CLIENT_SECRET }}
+          MICROSOFT_INTEGRATION_CLIENT_ID: ${{ secrets.CLOUD_MICROSOFT_INTEGRATION_CLIENT_ID }}
+          MICROSOFT_INTEGRATION_CLIENT_SECRET: ${{ secrets.CLOUD_MICROSOFT_INTEGRATION_CLIENT_SECRET }}
+          ZOOM_INTEGRATION_CLIENT_ID: ${{ secrets.CLOUD_ZOOM_INTEGRATION_CLIENT_ID }}
+          ZOOM_INTEGRATION_CLIENT_SECRET: ${{ secrets.CLOUD_ZOOM_INTEGRATION_CLIENT_SECRET }}
+          QUICKBOOKS_INTEGRATION_CLIENT_ID: ${{ secrets.CLOUD_QUICKBOOKS_INTEGRATION_CLIENT_ID }}
+          QUICKBOOKS_INTEGRATION_CLIENT_SECRET: ${{ secrets.CLOUD_QUICKBOOKS_INTEGRATION_CLIENT_SECRET }}
         run: |
           mkdir -p $HOME/torqvoice-deploy/prod
           cd $HOME/torqvoice-deploy/prod
 
-          env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|STRIPE_|POSTHOG_|BACKUP_)' > .env
+          env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|STRIPE_|POSTHOG_|BACKUP_|INTEGRATIONS_|GOOGLE_INTEGRATION_|MICROSOFT_INTEGRATION_|ZOOM_INTEGRATION_|QUICKBOOKS_INTEGRATION_)' > .env
 
           cat > docker-compose.yml << 'COMPOSE'
           networks:
@@ -83,6 +92,15 @@ jobs:
                 POSTHOG_HOST: ${POSTHOG_HOST}
                 POSTHOG_KEY: ${POSTHOG_KEY}
                 BACKUP_HEARTBEAT_TOKEN: ${BACKUP_HEARTBEAT_TOKEN}
+                INTEGRATIONS_ENCRYPTION_KEY: ${INTEGRATIONS_ENCRYPTION_KEY}
+                GOOGLE_INTEGRATION_CLIENT_ID: ${GOOGLE_INTEGRATION_CLIENT_ID}
+                GOOGLE_INTEGRATION_CLIENT_SECRET: ${GOOGLE_INTEGRATION_CLIENT_SECRET}
+                MICROSOFT_INTEGRATION_CLIENT_ID: ${MICROSOFT_INTEGRATION_CLIENT_ID}
+                MICROSOFT_INTEGRATION_CLIENT_SECRET: ${MICROSOFT_INTEGRATION_CLIENT_SECRET}
+                ZOOM_INTEGRATION_CLIENT_ID: ${ZOOM_INTEGRATION_CLIENT_ID}
+                ZOOM_INTEGRATION_CLIENT_SECRET: ${ZOOM_INTEGRATION_CLIENT_SECRET}
+                QUICKBOOKS_INTEGRATION_CLIENT_ID: ${QUICKBOOKS_INTEGRATION_CLIENT_ID}
+                QUICKBOOKS_INTEGRATION_CLIENT_SECRET: ${QUICKBOOKS_INTEGRATION_CLIENT_SECRET}
               networks:
                 - proxy
           COMPOSE

+ 19 - 1
.github/workflows/deploy-staging.yml

@@ -32,11 +32,20 @@ jobs:
           POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
           POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
           BACKUP_HEARTBEAT_TOKEN: ${{ secrets.BACKUP_HEARTBEAT_TOKEN }}
+          INTEGRATIONS_ENCRYPTION_KEY: ${{ secrets.STAGING_INTEGRATIONS_ENCRYPTION_KEY }}
+          GOOGLE_INTEGRATION_CLIENT_ID: ${{ secrets.STAGING_GOOGLE_INTEGRATION_CLIENT_ID }}
+          GOOGLE_INTEGRATION_CLIENT_SECRET: ${{ secrets.STAGING_GOOGLE_INTEGRATION_CLIENT_SECRET }}
+          MICROSOFT_INTEGRATION_CLIENT_ID: ${{ secrets.STAGING_MICROSOFT_INTEGRATION_CLIENT_ID }}
+          MICROSOFT_INTEGRATION_CLIENT_SECRET: ${{ secrets.STAGING_MICROSOFT_INTEGRATION_CLIENT_SECRET }}
+          ZOOM_INTEGRATION_CLIENT_ID: ${{ secrets.STAGING_ZOOM_INTEGRATION_CLIENT_ID }}
+          ZOOM_INTEGRATION_CLIENT_SECRET: ${{ secrets.STAGING_ZOOM_INTEGRATION_CLIENT_SECRET }}
+          QUICKBOOKS_INTEGRATION_CLIENT_ID: ${{ secrets.STAGING_QUICKBOOKS_INTEGRATION_CLIENT_ID }}
+          QUICKBOOKS_INTEGRATION_CLIENT_SECRET: ${{ secrets.STAGING_QUICKBOOKS_INTEGRATION_CLIENT_SECRET }}
         run: |
           mkdir -p $HOME/torqvoice-deploy/staging
           cd $HOME/torqvoice-deploy/staging
 
-          env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|STRIPE_|POSTHOG_|BACKUP_)' > .env
+          env | grep -E '^(VIRTUAL_HOST|DATA_PATH|DATABASE_URL|BETTER_AUTH_SECRET|APP_URL|STRIPE_|POSTHOG_|BACKUP_|INTEGRATIONS_|GOOGLE_INTEGRATION_|MICROSOFT_INTEGRATION_|ZOOM_INTEGRATION_|QUICKBOOKS_INTEGRATION_)' > .env
 
           cat > docker-compose.yml << 'COMPOSE'
           networks:
@@ -66,6 +75,15 @@ jobs:
                 STRIPE_ENTERPRISE_PRICE_ID: ${STRIPE_ENTERPRISE_PRICE_ID}
                 POSTHOG_HOST: ${POSTHOG_HOST}
                 BACKUP_HEARTBEAT_TOKEN: ${BACKUP_HEARTBEAT_TOKEN}
+                INTEGRATIONS_ENCRYPTION_KEY: ${INTEGRATIONS_ENCRYPTION_KEY}
+                GOOGLE_INTEGRATION_CLIENT_ID: ${GOOGLE_INTEGRATION_CLIENT_ID}
+                GOOGLE_INTEGRATION_CLIENT_SECRET: ${GOOGLE_INTEGRATION_CLIENT_SECRET}
+                MICROSOFT_INTEGRATION_CLIENT_ID: ${MICROSOFT_INTEGRATION_CLIENT_ID}
+                MICROSOFT_INTEGRATION_CLIENT_SECRET: ${MICROSOFT_INTEGRATION_CLIENT_SECRET}
+                ZOOM_INTEGRATION_CLIENT_ID: ${ZOOM_INTEGRATION_CLIENT_ID}
+                ZOOM_INTEGRATION_CLIENT_SECRET: ${ZOOM_INTEGRATION_CLIENT_SECRET}
+                QUICKBOOKS_INTEGRATION_CLIENT_ID: ${QUICKBOOKS_INTEGRATION_CLIENT_ID}
+                QUICKBOOKS_INTEGRATION_CLIENT_SECRET: ${QUICKBOOKS_INTEGRATION_CLIENT_SECRET}
               networks:
                 - proxy
           COMPOSE

+ 8 - 1
Dockerfile

@@ -52,6 +52,13 @@ COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.
 # Copy generated Prisma client
 COPY --from=builder --chown=nextjs:nodejs /app/src/generated ./src/generated
 
+# Give the install below the lockfile the app was built with. Without it npm
+# re-resolves the whole tree against the live registry on every build, so the
+# runtime image drifts from what was tested, and a registry change can break
+# the build with nothing changed here (npm 10 crashed with "Cannot read
+# properties of null (reading 'edgesOut')" resolving vitest's peers this way).
+COPY --from=builder --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
+
 # Remove Next.js standalone's traced stubs for pg/prisma (tracer copies package.json
 # but not all files), then install the full runtime packages fresh.
 RUN rm -rf \
@@ -71,7 +78,7 @@ RUN rm -rf \
       /app/node_modules/prisma \
       /app/node_modules/@prisma \
       /app/node_modules/.prisma \
-    && npm install prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx sharp
+    && npm install --omit=dev prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx sharp
 
 # The npm install above re-resolves the dependency tree and replaces the
 # standalone build's PATCHED next package with a fresh unpatched copy from the

+ 1 - 1
messages/de/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Fragen Sie zu Ihrer Werkstatt...",
   "clear": "Gespräch löschen",
   "error": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.",
-  "notEnabled": "KI ist nicht aktiviert. Konfigurieren Sie es unter Einstellungen → KI.",
+  "notEnabled": "KI ist nicht verbunden. Verbinden Sie OpenAI oder Anthropic unter Einstellungen → Integrationen.",
   "suggestion1": "Welche Fahrzeuge sind bald fällig für den Service?",
   "suggestion2": "Zeige mir unbezahlte Rechnungen",
   "suggestion3": "Wie ist der Umsatz diesen Monat?",

+ 27 - 2
messages/de/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "WhatsApp-Nummer registriert",
     "team_giveTechnicianTheApp": "Board-Techniker ein Konto gegeben",
     "team_createDefaultRoles": "Standardrollen angelegt",
-    "settings_setCustomFieldPlacement": "Position eines benutzerdefinierten Felds geändert"
+    "settings_setCustomFieldPlacement": "Position eines benutzerdefinierten Felds geändert",
+    "import_commit": "Tabelle importiert",
+    "import_undo": "Import rückgängig gemacht",
+    "integration_connect": "Integration verbunden",
+    "integration_disconnect": "Integration getrennt",
+    "service_setInvoiceDesign": "Rechnungsdesign geändert",
+    "settings_saveDocumentDesign": "Dokumentdesign gespeichert",
+    "settings_deleteDocumentDesign": "Dokumentdesign gelöscht",
+    "settings_applyDocumentDesign": "Standard-Dokumentdesign festgelegt",
+    "settings_setDocumentDesignRule": "Verwendung eines Designs geändert",
+    "settings_freezeInvoices": "Ältere Rechnungen gesperrt",
+    "inspection_reminders_sent": "HU-Erinnerungen gesendet",
+    "inspection_reminders_send": "HU-Erinnerungen gesendet",
+    "service_videoCall_send": "Link zum Videoanruf gesendet"
   },
   "summary": {
     "customField_create": "Benutzerdefiniertes Feld „{name}“ erstellt",
@@ -275,6 +288,18 @@
     "work_bay_create": "Arbeitsplatz „{name}“ angelegt",
     "work_bay_update": "Arbeitsplatz „{name}“ aktualisiert",
     "work_bay_delete": "Arbeitsplatz {id} gelöscht",
-    "settings_setCustomFieldPlacement": "Hat geändert, wo ein benutzerdefiniertes Feld gedruckt wird"
+    "settings_setCustomFieldPlacement": "Hat geändert, wo ein benutzerdefiniertes Feld gedruckt wird",
+    "import_commit": "Tabelle importiert: {count} Datensätze angelegt",
+    "import_undo": "Tabellenimport rückgängig gemacht: {records} Datensätze entfernt",
+    "integration_connect": "{name} verbunden",
+    "integration_disconnect": "{name} getrennt",
+    "service_setInvoiceDesign": "Hat geändert, mit welchem Design eine Rechnung gedruckt wird",
+    "settings_saveDocumentDesign": "Hat das Dokumentdesign {name} gespeichert",
+    "settings_deleteDocumentDesign": "Hat das Dokumentdesign {name} gelöscht",
+    "settings_applyDocumentDesign": "Hat {name} zum Standard-Dokumentdesign gemacht",
+    "settings_setDocumentDesignRule": "Verwendung des Dokumentdesigns {name} geändert",
+    "settings_freezeInvoices": "Hat {count, plural, one {# Rechnung} other {# Rechnungen}} von vor der Sperre mit dem aktuellen Design gesperrt",
+    "inspection_reminders_sent": "{count} HU-Erinnerungen gesendet",
+    "service_videoCall_send": "Link zum Videoanruf per {channels} gesendet"
   }
 }

+ 2 - 1
messages/de/auth.json

@@ -13,7 +13,8 @@
     "errors": {
       "tooManyAttempts": "Zu viele Versuche, bitte versuchen Sie es später erneut.",
       "invalidCredentials": "Ungültige E-Mail-Adresse oder ungültiges Passwort",
-      "passkeyFailed": "Passkey-Anmeldung fehlgeschlagen. Versuchen Sie es erneut oder verwenden Sie E-Mail und Passwort."
+      "passkeyFailed": "Passkey-Anmeldung fehlgeschlagen. Versuchen Sie es erneut oder verwenden Sie E-Mail und Passwort.",
+      "invalidOrigin": "Sie haben Torqvoice unter {origin} geöffnet, es ist aber für {configured} konfiguriert. Setzen Sie NEXT_PUBLIC_APP_URL auf genau die Adresse, die Sie im Browser verwenden, und starten Sie Torqvoice neu."
     },
     "resetPasswordCta": "Hier zurücksetzen"
   },

+ 74 - 7
messages/de/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Heute",
   "workOrder": "Arbeitsauftrag",
+  "allDay": "Ganztägig",
+  "openDay": "{date} öffnen",
+  "dayTotal": "{count, plural, =1 {1 Termin} other {# Termine}}",
+  "views": {
+    "day": "Tag",
+    "week": "Woche",
+    "month": "Monat",
+    "year": "Jahr",
+    "schedule": "Terminübersicht",
+    "fourDays": "4 Tage"
+  },
+  "options": {
+    "showWeekends": "Wochenenden anzeigen",
+    "showCompleted": "Abgeschlossene anzeigen",
+    "showWeekNumbers": "Kalenderwochen anzeigen"
+  },
+  "toolbar": {
+    "previous": "Zurück",
+    "next": "Weiter",
+    "create": "Erstellen",
+    "showSidebar": "Seitenleiste anzeigen",
+    "hideSidebar": "Seitenleiste ausblenden"
+  },
+  "sidebar": {
+    "calendars": "Kalender",
+    "legend": "Statusfarben"
+  },
+  "schedule": {
+    "empty": "Nichts geplant",
+    "emptyHint": "Keine Arbeitsaufträge, Erinnerungen, Angebote oder Nachrichten in diesem Zeitraum."
+  },
+  "peek": {
+    "close": "Schließen",
+    "open": {
+      "service": "Arbeitsauftrag öffnen",
+      "reminder": "Erinnerung öffnen",
+      "quote": "Angebot öffnen",
+      "message": "Nachrichten öffnen",
+      "external": "Öffnen"
+    }
+  },
   "filters": {
-    "services": "Aufträge",
+    "services": "Arbeitsaufträge",
     "reminders": "Erinnerungen",
     "quotes": "Angebote",
-    "messages": "Nachrichten"
+    "messages": "Nachrichten",
+    "external": "Verbundene Kalender"
   },
   "selectVehicle": "Fahrzeug für Arbeitsauftrag auswählen",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Use today"
   },
   "more": "+ {count} weitere",
-  "monthTotal": "{count} Termine in diesem Monat",
+  "monthTotal": "{count} Termine",
   "contextMenu": {
     "newWorkOrder": "Neuer Arbeitsauftrag",
+    "newWorkOrderAt": "Neuer Arbeitsauftrag um {time}",
     "newReminder": "Neue Erinnerung",
     "newQuote": "Neues Angebot",
     "openEvent": "Öffnen ({count})",
     "hint": "Tipp: Rechtsklick auf einen Tag zeigt Aktionen",
     "scheduleMessage": "Nachricht planen"
   },
+  "eventMenu": {
+    "open": "Öffnen",
+    "markDone": "Als erledigt markieren",
+    "markNotDone": "Als nicht erledigt markieren",
+    "deleteReminder": "Erinnerung löschen",
+    "deleteReminderConfirm": "Erinnerung \"{title}\" löschen? Das lässt sich nicht rückgängig machen.",
+    "deleteMessage": "Geplante Nachricht löschen",
+    "deleteMessageConfirm": "Geplante Nachricht \"{title}\" löschen? Sie wird nicht gesendet.",
+    "deleted": "Gelöscht",
+    "updated": "Aktualisiert",
+    "failed": "Das hat nicht geklappt. Bitte erneut versuchen."
+  },
   "events": {
     "status": {
       "completed": "Abgeschlossen",
       "inProgress": "In Bearbeitung",
       "waitingParts": "Warte auf Teile",
       "pending": "Ausstehend",
+      "scheduled": "Geplant",
       "sent": "Gesendet",
       "approved": "Genehmigt",
       "draft": "Entwurf",
@@ -66,16 +122,27 @@
       "overdue": "Überfällig",
       "upcoming": "Bevorstehend",
       "failed": "Fehlgeschlagen",
-      "queued": "In Warteschlange"
+      "queued": "In Warteschlange",
+      "busy": "Belegt"
     },
     "type": {
-      "service": "Service",
+      "service": "Arbeitsauftrag",
       "reminder": "Erinnerung",
       "quote": "Angebot",
-      "message": "Nachricht"
+      "message": "Nachricht",
+      "external": "Verbundener Kalender"
     },
     "noEvents": "Keine Ereignisse",
-    "selectDay": "Wählen Sie einen Tag mit Ereignissen oder erstellen Sie einen neuen Arbeitsauftrag."
+    "selectDay": "Wählen Sie einen Tag mit Ereignissen oder erstellen Sie einen neuen Arbeitsauftrag.",
+    "external": {
+      "source": "Aus {source}",
+      "sourceUnknown": "Aus einem verbundenen Kalender",
+      "allDay": "Ganztägig",
+      "open": "In {source} öffnen",
+      "openGeneric": "Im Kalender öffnen",
+      "noLink": "Dieser Kalender liefert keinen Link zum Termin.",
+      "readOnly": "Belegte Zeit aus einem verbundenen Kalender. Änderungen dort erscheinen hier bei der nächsten Synchronisierung."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/de/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Telefonnummer in die Zwischenablage kopiert",
     "taxIdLabel": "USt-IdNr.",
     "taxExemptBadge": "Steuerbefreit",
+    "invoiceDesignBadge": "Rechnungsdesign: {name}",
+    "invoiceDesignBadgeHint": "Rechnungen für diesen Kunden werden mit diesem Design statt dem Werkstatt-Standard gedruckt. Im Designer öffnen.",
     "tabs": {
       "vehicles": "Fahrzeuge ({count})",
       "invoices": "Rechnungen ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Auto",
     "sectionContact": "Kontakt",
     "sectionBilling": "Rechnung",
-    "addScannedVehicle": "Fahrzeug {vehicle} anlegen"
+    "addScannedVehicle": "Fahrzeug {vehicle} anlegen",
+    "invoiceDesign": "Rechnungsdesign",
+    "invoiceDesignDefault": "Werkstatt-Standard",
+    "invoiceDesignHint": "Rechnungen für diesen Kunden werden mit diesem Design statt dem Werkstatt-Standard gedruckt. Eine Rechnung kann trotzdem ein eigenes wählen.",
+    "reminderOptOut": "Keine Erinnerungen",
+    "reminderOptOutHint": "Diesen Kunden nicht in HU-Erinnerungen oder andere Kampagnen aufnehmen."
   },
   "serviceRequests": {
     "statusNew": "Neu",
@@ -117,44 +124,5 @@
     "notesSaveError": "Notizen konnten nicht gespeichert werden",
     "smsLabel": "Serviceanfrage-Nachricht",
     "smsDefault": "Hallo {name}, bezüglich Ihrer Serviceanfrage für Ihr Fahrzeug {vehicle}: "
-  },
-  "import": {
-    "title": "Kunden importieren",
-    "description": "Kunden aus CSV-, TSV- oder vCard-Dateien importieren.",
-    "dropFile": "Datei hierher ziehen und ablegen",
-    "supportedFormats": "CSV, TSV, TXT oder vCard (.vcf)",
-    "browse": "Dateien durchsuchen",
-    "noDataFound": "Keine Kundendaten in der Datei gefunden",
-    "rowsFound": "{count} Kunde(n) gefunden",
-    "colName": "Name",
-    "colEmail": "E-Mail",
-    "colPhone": "Telefon",
-    "colCompany": "Unternehmen",
-    "colAddress": "Adresse",
-    "chooseAnother": "Andere Datei wählen",
-    "cancel": "Abbrechen",
-    "importButton": "{count} Kunde(n) importieren",
-    "importComplete": "Import abgeschlossen",
-    "importedCount": "{count} Kunde(n) erfolgreich importiert",
-    "skippedRows": "{count} Zeile(n) wegen Fehlern übersprungen",
-    "rowError": "Zeile {row} ({name}): {error}",
-    "done": "Fertig",
-    "importFailed": "Kunden konnten nicht importiert werden",
-    "checkingDuplicates": "Duplikate werden geprüft...",
-    "duplicatesFound": "{count} Duplikat(e) gefunden",
-    "duplicatesBanner": "{count} bestehende(r) Kunde(n) stimmen mit Ihren Importdaten überein.",
-    "duplicatesHint": "Klicken Sie auf Überspringen oder Zusammenführen, um festzulegen, wie jedes Duplikat behandelt wird.",
-    "colStatus": "Status",
-    "actionSkip": "Überspringen",
-    "actionMerge": "Zusammenführen",
-    "actionNew": "Neu",
-    "summaryNew": "{count} neu",
-    "summaryMerge": "{count} zusammenführen",
-    "summarySkip": "{count} überspringen",
-    "skipAll": "Alle überspringen",
-    "mergeAll": "Alle zusammenführen",
-    "resultCreated": "{count} Kunde(n) erstellt",
-    "resultMerged": "{count} Kunde(n) zusammengeführt",
-    "resultSkipped": "{count} Kunde(n) übersprungen"
   }
 }

+ 12 - 1
messages/de/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Aktuelle Beobachtungen",
     "recentActivity": "Letzte Aktivitäten",
     "gettingStarted": "Erste Schritte",
-    "tireHotel": "Reifenhotel"
+    "tireHotel": "Reifenhotel",
+    "inspectionsDue": "Fällige Hauptuntersuchungen"
   },
   "maintenance": {
     "title": "Voraussichtlich fällige Wartung",
@@ -252,5 +253,15 @@
     "roomLeft": "Platz für {count} weitere",
     "needsPrep": "Vorbereitung offen",
     "unbilled": "Nicht berechnet ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Fällige Hauptuntersuchungen",
+    "viewAll": "Alle fälligen Fahrzeuge anzeigen",
+    "overdue": "Überfällig",
+    "within30": "Nächste 30 Tage",
+    "within90": "Nächste 90 Tage",
+    "noData": "In den nächsten 90 Tagen ist keine Untersuchung fällig",
+    "overdueSince": "Überfällig seit {date}",
+    "remind": "Erinnerungen senden"
   }
 }

+ 228 - 0
messages/de/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "{entity} importieren",
+  "cancel": "Abbrechen",
+  "done": "Fertig",
+  "entities": {
+    "customers": "Kunden",
+    "vehicles": "Fahrzeuge",
+    "services": "Servicehistorie"
+  },
+  "entityHints": {
+    "customers": "Namen, Kontaktdaten, Adressen. Eine Zeile pro Kunde.",
+    "vehicles": "Marke, Modell, Baujahr, Kennzeichen, FIN. Eine Besitzer-Spalte verknüpft oder erstellt den Kunden.",
+    "services": "Frühere Aufträge und Rechnungen. Eine Zeile pro Auftrag, mit dem zugehörigen Fahrzeug."
+  },
+  "groups": {
+    "customer": "Kunde",
+    "vehicle": "Fahrzeug",
+    "service": "Service"
+  },
+  "steps": {
+    "upload": "Wählen Sie, was importiert werden soll, und laden Sie die Datei aus Ihrem bisherigen System hoch.",
+    "mapping": "Prüfen Sie, welche Spalte zu welchem Feld wird. Es wird noch nichts geschrieben.",
+    "preview": "Testlauf: Genau das wird der Import tun. Prüfen Sie es und importieren Sie dann.",
+    "importing": "Zeilen werden geschrieben. Lassen Sie dieses Fenster geöffnet.",
+    "result": "Import abgeschlossen."
+  },
+  "upload": {
+    "drop": "Datei hierher ziehen und ablegen",
+    "formats": "CSV, Excel (.xlsx) oder vCard (.vcf), bis zu 25 MB und 50.000 Zeilen",
+    "browse": "Dateien durchsuchen",
+    "reading": "Datei wird gelesen...",
+    "templateHint": "Noch kein Export? Füllen Sie eine Vorlage aus und laden Sie diese hoch.",
+    "templateXlsx": "Excel-Vorlage",
+    "templateCsv": "CSV-Vorlage",
+    "sourcesHint": "Funktioniert mit Exporten aus jedem Werkstattsystem, Buchhaltungsprogramm, Google Kontakte, Outlook oder einer Tabelle. Spaltennamen werden in vielen Sprachen erkannt; Sie bestätigen die Zuordnung, bevor etwas geschrieben wird."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# Zeile} other {# Zeilen}}",
+    "colColumn": "Spalte in Ihrer Datei",
+    "colSample": "Beispielwerte",
+    "colField": "Wird zu",
+    "ignore": "Diese Spalte ignorieren",
+    "source": {
+      "preset": "Erkanntes Layout",
+      "header": "Über Spaltenüberschrift zugeordnet",
+      "values": "Aus den Werten abgeleitet"
+    },
+    "suggestWithAi": "Mit KI vorschlagen",
+    "aiApplied": "{count, plural, one {# Spalte} other {# Spalten}} durch KI zugeordnet. Prüfen Sie die Zuordnung, bevor Sie fortfahren.",
+    "chooseAnother": "Andere Datei wählen",
+    "missingRequired": "Noch erforderlich: {fields}",
+    "dryRunHint": "Der Testlauf prüft jede Zeile gegen Ihre vorhandenen Daten, ohne etwas zu schreiben.",
+    "runDryRun": "Testlauf starten"
+  },
+  "options": {
+    "dateFormat": "Datumsformat",
+    "dateAuto": "Automatisch erkennen",
+    "dateDMY": "Tag / Monat / Jahr",
+    "dateMDY": "Monat / Tag / Jahr",
+    "dateYMD": "Jahr / Monat / Tag",
+    "decimal": "Dezimaltrennzeichen",
+    "decimalAuto": "Automatisch erkennen",
+    "decimalDot": "Punkt (1,234.56)",
+    "decimalComma": "Komma (1.234,56)",
+    "countryCode": "Ländervorwahl für Telefonnummern",
+    "duplicates": "Wenn eine Zeile einem vorhandenen Datensatz entspricht",
+    "duplicatesUpdate": "Vorhandenen Datensatz aktualisieren",
+    "duplicatesSkip": "Zeile überspringen",
+    "duplicatesCreate": "Trotzdem neuen Datensatz anlegen"
+  },
+  "summary": {
+    "create": "Wird angelegt",
+    "update": "Wird aktualisiert",
+    "skip": "Wird übersprungen",
+    "error": "Kann nicht importiert werden",
+    "warnings": "Mit Warnungen",
+    "customersToCreate": "{count, plural, one {# neuer Kunde} other {# neue Kunden}}",
+    "customersToUpdate": "{count, plural, one {# Kunde aktualisiert} other {# Kunden aktualisiert}}",
+    "vehiclesToCreate": "{count, plural, one {# neues Fahrzeug} other {# neue Fahrzeuge}}",
+    "vehiclesToUpdate": "{count, plural, one {# Fahrzeug aktualisiert} other {# Fahrzeuge aktualisiert}}",
+    "servicesToCreate": "{count, plural, one {# Serviceeintrag} other {# Serviceeinträge}}"
+  },
+  "actions": {
+    "create": "Anlegen",
+    "update": "Aktualisieren",
+    "skip": "Überspringen",
+    "error": "Fehler"
+  },
+  "matchOn": {
+    "number": "Kundennummer",
+    "email": "E-Mail",
+    "phone": "Telefon",
+    "name": "Name",
+    "vin": "FIN",
+    "plate": "Kennzeichen",
+    "makeModelYear": "Marke, Modell und Baujahr"
+  },
+  "preview": {
+    "colAction": "Aktion",
+    "colOwner": "Besitzer",
+    "colDetails": "Details",
+    "empty": "Keine Zeilen entsprechen diesem Filter.",
+    "matchedOn": "Entspricht {name} (über {on})",
+    "linksCustomer": "Verknüpft mit Kunde {name}",
+    "linksVehicle": "Verknüpft mit {label}",
+    "sameCustomerAsRow": "Gleicher Kunde wie Zeile {row}",
+    "sameVehicleAsRow": "Gleiches Fahrzeug wie Zeile {row}",
+    "createsCustomer": "Legt den Kunden an",
+    "createsVehicle": "Legt das Fahrzeug an",
+    "downloadReport": "Testlauf-Bericht herunterladen",
+    "limitExceeded": "Ihr Tarif erlaubt noch {remaining} weitere Kunden, dieser Import würde aber {needed} anlegen. Entfernen Sie Zeilen oder wechseln Sie zu einem höheren Tarif.",
+    "pageInfo": "{from}–{to} von {total}",
+    "prev": "Zurück",
+    "next": "Weiter",
+    "backToMapping": "Zurück zur Zuordnung",
+    "import": "{count, plural, one {# Zeile} other {# Zeilen}} importieren",
+    "nothingWrittenYet": "Es wurde noch nichts geschrieben. Der Import kann anschließend unter Einstellungen → Daten rückgängig gemacht werden."
+  },
+  "importing": {
+    "title": "Import läuft...",
+    "starting": "Wird gestartet",
+    "progress": "{done} von {total} Zeilen",
+    "keepOpen": "Große Dateien brauchen ein bis zwei Minuten."
+  },
+  "result": {
+    "title": "Import abgeschlossen",
+    "undoneTitle": "Import rückgängig gemacht",
+    "created": "{count, plural, one {# Datensatz angelegt} other {# Datensätze angelegt}}",
+    "updated": "{count, plural, one {# Datensatz aktualisiert} other {# Datensätze aktualisiert}}",
+    "skipped": "{count, plural, one {# Zeile übersprungen} other {# Zeilen übersprungen}}",
+    "failed": "{count, plural, one {# Zeile nicht importiert} other {# Zeilen nicht importiert}}",
+    "writeFailures": "{count, plural, one {# Zeile beim Schreiben fehlgeschlagen} other {# Zeilen beim Schreiben fehlgeschlagen}}",
+    "rowFailure": "Zeile {row}: {error}",
+    "undo": "Diesen Import rückgängig machen",
+    "undone": "{customers} Kunden, {vehicles} Fahrzeuge und {services} Serviceeinträge entfernt.",
+    "undoHint": "Rückgängig machen entfernt jeden Datensatz, den dieser Import angelegt hat. Datensätze, die nur aktualisiert wurden, behalten ihre neuen Werte."
+  },
+  "history": {
+    "title": "Importverlauf",
+    "description": "In dieser Werkstatt ausgeführte Tabellenimporte. Rückgängig machen entfernt, was ein Import angelegt hat.",
+    "loading": "Wird geladen...",
+    "statusUndone": "Rückgängig gemacht",
+    "counts": "{created} angelegt, {updated} aktualisiert, {skipped} übersprungen, {failed} fehlgeschlagen",
+    "undo": "Rückgängig",
+    "undone": "{customers} Kunden, {vehicles} Fahrzeuge und {services} Serviceeinträge entfernt.",
+    "undoFailed": "Der Import konnte nicht rückgängig gemacht werden",
+    "confirmTitle": "Diesen Import rückgängig machen?",
+    "confirmDescription": "Entfernt, was {file} angelegt hat: {customers} Kunden, {vehicles} Fahrzeuge und {services} Serviceeinträge. Datensätze, die der Import nur aktualisiert hat, bleiben erhalten.",
+    "cancel": "Abbrechen",
+    "confirm": "Import rückgängig machen"
+  },
+  "fields": {
+    "customer": {
+      "name": "Kundenname",
+      "firstName": "Vorname",
+      "lastName": "Nachname",
+      "customerNumber": "Kundennummer",
+      "email": "E-Mail",
+      "phone": "Telefon",
+      "company": "Unternehmen",
+      "address": "Adresse",
+      "street": "Straße",
+      "postalCode": "Postleitzahl",
+      "city": "Ort",
+      "state": "Bundesland / Region",
+      "country": "Land",
+      "taxId": "Steuer-/USt-IdNr.",
+      "notes": "Kundennotizen"
+    },
+    "vehicle": {
+      "description": "Fahrzeug (Baujahr Marke Modell in einer Spalte)",
+      "make": "Marke",
+      "model": "Modell",
+      "year": "Baujahr",
+      "vin": "FIN",
+      "licensePlate": "Kennzeichen",
+      "color": "Farbe",
+      "mileage": "Kilometerstand",
+      "fuelType": "Kraftstoffart",
+      "transmission": "Getriebe",
+      "engineSize": "Hubraum",
+      "engineCode": "Motorcode",
+      "purchaseDate": "Kaufdatum",
+      "purchasePrice": "Kaufpreis"
+    },
+    "service": {
+      "date": "Servicedatum",
+      "title": "Servicebezeichnung",
+      "description": "Beschreibung",
+      "total": "Gesamtbetrag",
+      "mileage": "Kilometerstand beim Service",
+      "invoiceNumber": "Rechnungsnummer",
+      "notes": "Servicenotizen",
+      "technician": "Techniker"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Kundenname fehlt",
+    "invalid_email": "E-Mail \"{value}\" ist ungültig und wurde weggelassen",
+    "invalid_vin": "FIN \"{value}\" hat nicht 17 Zeichen; wie eingegeben übernommen",
+    "vehicle_make_model_required": "Marke und Modell fehlen",
+    "vehicle_year_required": "Baujahr fehlt",
+    "vehicle_year_invalid": "Baujahr \"{value}\" ist kein gültiges Jahr",
+    "vehicle_identifier_required": "Kein Kennzeichen, keine FIN und keine Marke mit Modell, um das Fahrzeug zu identifizieren",
+    "vehicle_not_found": "Kein vorhandenes Fahrzeug entspricht \"{value}\" und die Angaben reichen nicht aus, um eines anzulegen",
+    "service_date_required": "Servicedatum fehlt",
+    "service_date_invalid": "Datum \"{value}\" konnte nicht gelesen werden",
+    "service_title_required": "Servicebezeichnung oder Beschreibung fehlt",
+    "invalid_date": "{field} \"{value}\" konnte nicht gelesen werden und wurde weggelassen",
+    "invalid_number": "{field} \"{value}\" ist keine Zahl und wurde weggelassen",
+    "duplicate_invoice_number": "Rechnungsnummer {value} existiert bereits",
+    "write_failed": "Konnte nicht geschrieben werden: {value}"
+  },
+  "errors": {
+    "uploadFailed": "Die Datei konnte nicht gelesen werden",
+    "aiFailed": "Die KI konnte keine Zuordnung vorschlagen",
+    "dryRunFailed": "Der Testlauf ist fehlgeschlagen",
+    "importFailed": "Der Import ist fehlgeschlagen",
+    "undoFailed": "Der Import konnte nicht rückgängig gemacht werden"
+  },
+  "entry": {
+    "title": "Aus einer Tabelle importieren",
+    "description": "Übernehmen Sie Kunden, Fahrzeuge und Servicehistorie aus jedem System, das CSV oder Excel exportiert. Ordnen Sie die Spalten zu, starten Sie einen Testlauf und importieren Sie dann. Jeder Import kann rückgängig gemacht werden.",
+    "customers": "Kunden importieren",
+    "vehicles": "Fahrzeuge importieren",
+    "services": "Servicehistorie importieren"
+  }
+}

+ 584 - 0
messages/de/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Integrationen",
+    "description": "Verbinden Sie die Dienste, die Ihre Werkstatt bereits nutzt. Jede Integration wird einmal eingerichtet und läuft im Hintergrund.",
+    "all": "Alle",
+    "empty": "In dieser Kategorie gibt es noch keine Integrationen.",
+    "notConnected": "Nicht verbunden",
+    "connect": "Verbinden",
+    "manage": "Verwalten",
+    "connectedTitle": "Verbunden",
+    "connectedDescription": "Dienste, die diese Werkstatt eingerichtet hat. Öffnen Sie einen, um den Status zu prüfen, Einstellungen zu ändern oder die Verbindung zu trennen.",
+    "noneConnected": "Noch nichts verbunden. Wählen Sie unten einen Dienst, um zu beginnen.",
+    "availableTitle": "Verfügbar",
+    "availableDescription": "Dienste, die Sie hinzufügen können. Jeder wird einmal eingerichtet und läuft im Hintergrund.",
+    "allConnected": "Alle verfügbaren Dienste sind verbunden.",
+    "searchPlaceholder": "Integrationen durchsuchen",
+    "noMatches": "Nichts passt zu „{query}“.",
+    "readMore": "Mehr erfahren",
+    "suggestTitle": "Fehlt ein Dienst, den Sie nutzen?",
+    "suggestDescription": "Sagen Sie uns, welche Integration Ihrer Werkstatt helfen würde, und wir prüfen sie.",
+    "suggest": "Integration vorschlagen",
+    "suggestSubject": "Integrationsvorschlag"
+  },
+  "categories": {
+    "calendar": "Kalender",
+    "conferencing": "Videoanrufe",
+    "accounting": "Buchhaltung",
+    "messaging": "Nachrichten",
+    "payments": "Zahlungen",
+    "automation": "Automatisierung",
+    "storage": "Dateien",
+    "registry": "Fahrzeugregister",
+    "other": "Sonstiges",
+    "ai": "KI"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Aufträge in den Kalender",
+      "pull": "Belegte Zeiten aus dem Kalender",
+      "conference": "Links für Videoanrufe"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Kennzeichen- und FIN-Abfrage",
+      "inspection": "HU-Fälligkeitsdaten",
+      "safety": "Rückrufe und Beschwerden von Haltern"
+    },
+    "ai": {
+      "chat": "Texte und Zusammenfassungen"
+    },
+    "payments": {
+      "checkout": "Bezahlen über den Rechnungslink"
+    },
+    "accounting": {
+      "invoices": "Rechnungen ins Buchhaltungssystem",
+      "customers": "Kunden ins Buchhaltungssystem",
+      "payments": "Zahlungen in beide Richtungen"
+    }
+  },
+  "statuses": {
+    "active": "Verbunden",
+    "error": "Aufmerksamkeit erforderlich",
+    "pending": "Einrichtung läuft",
+    "disconnected": "Getrennt",
+    "needsSetup": "Einrichtung unvollständig"
+  },
+  "connection": {
+    "back": "Alle Integrationen",
+    "account": "Verbundenes Konto",
+    "lastHealth": "Zuletzt geprüft",
+    "lastSync": "Letzte Synchronisierung",
+    "lastError": "Der letzte Durchlauf ist fehlgeschlagen",
+    "setupMissingTitle": "Einrichtung unvollständig",
+    "setupMissing": "Wähle unten in den Einstellungen {fields}. Bis dahin wird nichts synchronisiert.",
+    "connected": "{name} ist verbunden.",
+    "connectedShort": "Verbunden",
+    "connectWith": "{name} verbinden",
+    "connect": "Verbinden",
+    "reconnect": "Erneut verbinden",
+    "disconnect": "Trennen",
+    "disconnectTitle": "{name} trennen?",
+    "disconnectDescription": "Torqvoice vergisst das Konto und nutzt es nicht mehr. Was bereits an {name} gesendet oder dort gespeichert wurde, bleibt unberührt.",
+    "disconnectDescriptionCalendar": "Torqvoice vergisst das Konto und synchronisiert nicht mehr. Aufträge, die bereits in {name} stehen, bleiben dort. Die aus diesem Kalender angezeigte belegte Zeit wird aus dem Torqvoice-Kalender entfernt.",
+    "disconnectDescriptionConferencing": "Torqvoice vergisst das Konto und erstellt keine Meetings mehr. Bereits erstellte Meetings bleiben in {name}, die Links an den Aufträgen bleiben erhalten.",
+    "disconnectDescriptionAccounting": "Torqvoice vergisst das Konto und sendet und liest nichts mehr. Rechnungen, Kunden und Zahlungen, die bereits in {name} sind, bleiben dort.",
+    "removeSetup": "Entfernen",
+    "removeSetupTitle": "{name} entfernen?",
+    "removeSetupDescription": "Es ist noch nichts verbunden. Torqvoice vergisst die Eingaben, und {name} erscheint wieder in der Liste der verfügbaren Dienste.",
+    "removeSetupDone": "{name} entfernt",
+    "removeSetupHint": "Doch nicht? Entfernen Sie die Einrichtung, und der Dienst kehrt in die Liste unten zurück.",
+    "disconnected": "Getrennt",
+    "test": "Verbindung testen",
+    "testPassed": "Verbindung funktioniert",
+    "syncNow": "Jetzt synchronisieren",
+    "syncQueued": "Synchronisierung eingereiht; sie läuft innerhalb einer Minute",
+    "pullNow": "Zahlungen jetzt abrufen",
+    "pullQueued": "Zahlungsabruf eingereiht; er läuft innerhalb einer Minute",
+    "backfill": "Geplante Aufträge übertragen",
+    "backfillQueued": "Die geplanten Aufträge ab der letzten Woche wurden eingereiht",
+    "backfillInvoices": "Ausgestellte Rechnungen übertragen",
+    "backfillInvoicesQueuedCount": "{count, plural, one {# ausgestellte Rechnung} other {# ausgestellte Rechnungen}} zum Übertragen eingereiht",
+    "backfillInvoicesNone": "Keine ausgestellten Rechnungen zum Übertragen. Nur gesendete, geteilte oder bezahlte Rechnungen werden übertragen, ab dem Startdatum, falls gesetzt.",
+    "retryQueued": "Job erneut eingereiht",
+    "settingsTitle": "Einstellungen",
+    "settingsDescription": "Was synchronisiert wird und wohin. Änderungen gelten ab der nächsten Synchronisierung.",
+    "settingsSaved": "Einstellungen gespeichert",
+    "save": "Speichern",
+    "loading": "Wird geladen...",
+    "choose": "Auswählen...",
+    "search": "Suchen...",
+    "noMatch": "Nichts gefunden",
+    "leaveEmpty": "Leer lassen",
+    "noLimit": "Keine Grenze",
+    "planLocked": "Integrationen sind in Ihrem Tarif nicht enthalten.",
+    "ownAppTitle": "Diese Installation verwendet Ihre eigene App beim Anbieter",
+    "redirectUri": "Beim Anbieter zu registrierende Redirect-URI:",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Fügen Sie diese URL im Stripe-Dashboard unter Developers, Webhooks hinzu, abonniert auf checkout.session.completed. Tragen Sie dann das von Stripe angezeigte Signaturgeheimnis hier im Feld Webhook-Geheimnis ein, damit Benachrichtigungen geprüft werden können.",
+    "inboundHintVipps": "Tragen Sie diese URL als Callback-URL im Vipps-MobilePay-Portal ein. Vipps verlangt außerdem veröffentlichte Verkaufsbedingungen, die unter Einstellungen, Zahlung hinterlegt werden.",
+    "inboundHintPaypal": "Fügen Sie diese URL im PayPal-Entwicklerdashboard unter Webhooks hinzu, abonniert auf PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "HU-Abgleich",
+      "vehicles": "{count} Fahrzeuge mit Kennzeichen in dieser Werkstatt",
+      "dueNow": "{count} würden beim nächsten Lauf abgefragt",
+      "withDate": "{count} haben bereits einen HU-Termin",
+      "lastPass": "Letzter vollständiger Durchlauf {date}",
+      "never": "Noch kein vollständiger Durchlauf",
+      "inProgress": "Ein Durchlauf läuft",
+      "off": "Einstellung einschalten und speichern; bis dahin prüft ein Durchlauf nichts.",
+      "syncNowHelp": "Jetzt synchronisieren startet sofort einen Durchlauf statt auf den stündlichen zu warten. Er fragt 25 Fahrzeuge auf einmal ab und macht von selbst weiter, bis alle fälligen Fahrzeuge erledigt sind."
+    },
+    "supersedes": "Wenn Sie {next} verbinden, wird {current} getrennt. Nur einer kann zuständig sein: Sobald {next} aktiv ist, wird {current} nicht mehr genutzt.",
+    "batchRetried": "{count, plural, =0 {Nichts zu wiederholen} one {# Auftrag erneut eingereiht} other {# Aufträge erneut eingereiht}}"
+  },
+  "activity": {
+    "title": "Aktivität",
+    "description": "Aktuelle Jobs und Protokollzeilen dieser Verbindung.",
+    "retry": "Erneut versuchen",
+    "jobStatus": {
+      "queued": "Eingereiht",
+      "running": "Läuft",
+      "done": "Erledigt",
+      "failed": "Fehlgeschlagen",
+      "dead": "Aufgegeben"
+    },
+    "jobs": "{count, plural, one {# Auftrag} other {# Aufträge}}",
+    "counts": {
+      "queued": "{count} in Warteschlange",
+      "running": "{count} laufen",
+      "done": "{count} erledigt",
+      "failed": "{count} fehlgeschlagen",
+      "dead": "{count} aufgegeben"
+    },
+    "noFailures": "Nichts ist fehlgeschlagen.",
+    "retryFailed": "Fehlgeschlagene wiederholen",
+    "details": "Details anzeigen"
+  },
+  "errors": {
+    "generic": "Etwas ist schiefgelaufen",
+    "demo": "In der Demo können keine Integrationen verbunden werden.",
+    "forbidden": "Nur ein Administrator kann Integrationen verbinden.",
+    "unknown": "Unbekannte Integration.",
+    "plan": "Diese Integration ist in Ihrem Tarif nicht enthalten.",
+    "no-client": "Für diesen Anbieter sind keine App-Zugangsdaten konfiguriert.",
+    "denied": "Sie haben die Anmeldung beim Anbieter abgebrochen.",
+    "vendor": "Der Anbieter hat einen Fehler zurückgegeben. Versuchen Sie es erneut.",
+    "invalid": "Die Antwort des Anbieters war unvollständig. Versuchen Sie es erneut.",
+    "state": "Diese Anmeldung ist abgelaufen oder gehört zu einer anderen Sitzung. Beginnen Sie erneut.",
+    "exchange": "Der Anbieter hat die Anmeldung abgelehnt. Prüfen Sie die App-Zugangsdaten und versuchen Sie es erneut.",
+    "identify": "Verbunden, aber das Konto konnte nicht gelesen werden. Versuchen Sie, die Verbindung neu herzustellen."
+  },
+  "meeting": {
+    "join": "Videoanruf beitreten",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "Aus verbundenen Kalendern",
+    "busy": "Belegt"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Geplante Aufträge in einen Google Calendar eintragen, einen Google Meet-Link hinzufügen und die belegten Zeiten des Kalenders in Torqvoice sehen.",
+      "tenantHelp": "Erstellen Sie einen OAuth-Client in der Google Cloud Console (APIs & Dienste → Anmeldedaten), aktivieren Sie die Google Calendar API, fügen Sie die untenstehende Redirect-URI hinzu und tragen Sie Client-ID und Client-Secret hier ein.",
+      "fields": {
+        "clientId": "Client-ID",
+        "clientSecret": "Client-Secret"
+      },
+      "settings": {
+        "calendarId": "Kalender",
+        "pushEnabled": "Geplante Aufträge in diesen Kalender eintragen",
+        "includeCustomer": "Name und Telefonnummer des Kunden in den Termin aufnehmen",
+        "addConference": "Jedem geplanten Auftrag ein Google Meet-Meeting hinzufügen",
+        "addConferenceHelp": "Ein: jeder in den Kalender übertragene Auftrag bekommt ein Google Meet-Meeting, und der Link erscheint auf dem Auftrag. Aus: ein Google Meet-Meeting einem einzelnen Auftrag von dessen Seite aus hinzufügen. In beiden Fällen bekommt der Kunde den Link erst, wenn Sie ihn senden.",
+        "pullEnabled": "Belegte Zeiten dieses Kalenders im Torqvoice-Kalender anzeigen",
+        "pullEnabledHelp": "Nur lesend. Wird alle 15 Minuten aktualisiert, eine Woche zurück und zwei Monate voraus."
+      }
+    },
+    "microsoft-365": {
+      "description": "Geplante Aufträge in einen Outlook-Kalender eintragen, einen Microsoft Teams-Link hinzufügen und die belegten Zeiten des Kalenders in Torqvoice sehen.",
+      "tenantHelp": "Registrieren Sie eine Anwendung in Microsoft Entra (App-Registrierungen), erlauben Sie persönliche und Geschäftskonten, fügen Sie die untenstehende Redirect-URI als Web-Plattform hinzu, erstellen Sie ein Client-Secret und tragen Sie beides hier ein.",
+      "fields": {
+        "clientId": "Anwendungs-ID (Client-ID)",
+        "clientSecret": "Client-Secret"
+      },
+      "settings": {
+        "calendarId": "Kalender",
+        "pushEnabled": "Geplante Aufträge in diesen Kalender eintragen",
+        "includeCustomer": "Name und Telefonnummer des Kunden in den Termin aufnehmen",
+        "addConference": "Jedem geplanten Auftrag ein Microsoft Teams-Meeting hinzufügen",
+        "addConferenceHelp": "Ein: jeder in den Kalender übertragene Auftrag bekommt ein Microsoft Teams-Meeting, und der Link erscheint auf dem Auftrag. Aus: ein Microsoft Teams-Meeting einem einzelnen Auftrag von dessen Seite aus hinzufügen. In beiden Fällen bekommt der Kunde den Link erst, wenn Sie ihn senden.",
+        "pullEnabled": "Belegte Zeiten dieses Kalenders im Torqvoice-Kalender anzeigen",
+        "pullEnabledHelp": "Nur lesend. Wird alle 15 Minuten aktualisiert, eine Woche zurück und zwei Monate voraus."
+      }
+    },
+    "zoom": {
+      "description": "Fügen Sie einem geplanten Auftrag ein Zoom-Meeting hinzu und behalten Sie den Beitrittslink am Auftrag, für Ferndiagnosen oder um Kunden durch Befunde zu führen.",
+      "tenantHelp": "Erstellen Sie im Zoom App Marketplace eine General-App (user-managed), tragen Sie die Redirect-URI unten ein, fügen Sie die Scopes meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting und user:read:user hinzu und tragen Sie Client-ID und Client-Secret hier ein.",
+      "fields": {
+        "clientId": "Client-ID",
+        "clientSecret": "Client-Secret"
+      },
+      "settings": {
+        "autoCreate": "Für jeden geplanten Auftrag automatisch ein Zoom-Meeting erstellen",
+        "autoCreateHelp": "Aus: Sie fügen einem Auftrag den Videoanruf selbst hinzu, auf seiner Seite. An: Jeder Auftrag mit Startzeit bekommt ein Meeting, das bei Terminverschiebung mitwandert und bei Stornierung entfernt wird. In beiden Fällen erhält der Kunde den Link nur, wenn Sie ihn senden.",
+        "includeCustomer": "Kundennamen in das Meeting-Thema aufnehmen",
+        "joinBeforeHost": "Kunden vor Ihnen beitreten lassen",
+        "waitingRoom": "Warteraum verwenden"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Fahrzeug aus dem norwegischen Kennzeichen oder der FIN ausfüllen: Marke, Modell, Baujahr, Kraftstoff, Getriebe, Motor, Farbe, Reifengrößen und der nächste EU-kontroll-Termin, direkt aus dem Register von Statens vegvesen. Halterdaten werden nicht geliefert.",
+      "fields": {
+        "apiKey": "API-Schlüssel",
+        "apiKeyHelp": "Kostenlos. Bestellen Sie den Schlüssel auf vegvesen.no unter „API for tekniske kjøretøyopplysninger“: mit BankID anmelden, die Werkstatt vertreten, und der Schlüssel wird sofort ausgestellt. Er erlaubt 50.000 Abfragen pro Tag."
+      },
+      "settings": {
+        "syncInspections": "HU-Termine für alle Fahrzeuge dieser Werkstatt aktuell halten",
+        "syncInspectionsHelp": "Einmal pro Stunde werden Fahrzeuge, die länger nicht geprüft wurden, per Kennzeichen abgefragt und der nächste EU-kontroll-Termin gespeichert. Es werden nur Fahrzeuge dieser Werkstatt gesendet. Ein Fahrzeug wird monatlich geprüft, wöchentlich sobald die Frist unter zwei Monaten liegt."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Fahrzeug aus dem französischen, italienischen, spanischen, portugiesischen oder britischen Kennzeichen ausfüllen: Marke, Modell, Baujahr, FIN, Kraftstoff, Hubraum und mehr aus den Zulassungsdaten des Landes über Openapi.com. Jede Abfrage wird dem eigenen Openapi-Konto der Werkstatt berechnet. Halterdaten werden nicht geliefert.",
+      "fields": {
+        "token": "API-Token",
+        "tokenHelp": "Legen Sie ein Konto auf console.openapi.com an, laden Sie Guthaben auf und erstellen Sie unter Automotive ein Token für die Produktionsumgebung. Jede erfolgreiche Abfrage kostet einige Dutzend Cent; ein Fehltreffer ist kostenlos."
+      },
+      "settings": {
+        "country": "Land",
+        "countryHelp": "Das zu durchsuchende Register. Französische und italienische Kennzeichen sehen gleich aus, das Land lässt sich daher nicht am Kennzeichen ablesen.",
+        "bikes": "Auch Motorräder abfragen",
+        "bikesHelp": "Ist ein Kennzeichen kein Pkw, wird zusätzlich das Motorradregister gefragt. Ein Fehltreffer beim Pkw ist kostenlos, es entstehen also nur Kosten, wenn ein Motorrad gefunden wird. Für Portugal nicht verfügbar."
+      }
+    },
+    "rdw": {
+      "description": "Fahrzeug aus dem niederländischen Kennzeichen ausfüllen: Marke, Modell, Baujahr, Kraftstoff, Hubraum, Farbe, Gewichte und der nächste APK-Termin, direkt aus dem offenen Fahrzeugregister der RDW. Kostenlos, kein Schlüssel nötig. Halterdaten und FIN werden nicht veröffentlicht.",
+      "fields": {
+        "appToken": "App-Token (optional)",
+        "appTokenHelp": "Die offenen Daten der RDW funktionieren ohne Schlüssel, anonyme Anfragen teilen sich aber ein gedrosseltes Kontingent. Für die Prüfterminsynchronisation legen Sie auf opendata.rdw.nl ein kostenloses Konto an, öffnen die Entwicklereinstellungen Ihres Profils und erstellen ein App-Token."
+      },
+      "settings": {
+        "syncInspections": "APK-Termine für alle Fahrzeuge dieser Werkstatt aktuell halten",
+        "syncInspectionsHelp": "Einmal pro Stunde werden Fahrzeuge, die länger nicht geprüft wurden, per Kennzeichen abgefragt und der nächste APK-Termin gespeichert. Es werden nur Fahrzeuge dieser Werkstatt gesendet. Ein Fahrzeug wird monatlich geprüft, wöchentlich sobald die Frist unter zwei Monaten liegt."
+      }
+    },
+    "regcheck": {
+      "description": "Fahrzeug aus dem Kennzeichen in Australien, Neuseeland, den USA, Irland, Norwegen, Schweden, Dänemark, Finnland, Estland, Tschechien, der Slowakei, Ungarn oder Kroatien ausfüllen: Marke, Modell, Baujahr, FIN, Kraftstoff und Hubraum, soweit das Land sie veröffentlicht, über RegCheck. Jede erfolgreiche Abfrage verbraucht ein Guthaben aus dem eigenen RegCheck-Konto der Werkstatt. Halterdaten werden nicht geliefert.",
+      "fields": {
+        "username": "RegCheck-Benutzername",
+        "usernameHelp": "Registrieren Sie sich auf regcheck.org.uk; das Konto enthält 10 kostenlose Abfragen, weitere werden über das Dashboard in Blöcken von 100 gekauft. Hier wird nur der Benutzername benötigt, nie das Passwort. Ein nicht gefundenes Kennzeichen kostet nichts."
+      },
+      "settings": {
+        "country": "Land",
+        "countryHelp": "Das zu durchsuchende Register. Jedes Land hat einen eigenen Dienst, die Verbindung durchsucht daher eines.",
+        "auState": "Bundesstaat oder Territorium",
+        "auStateHelp": "Australien lässt Fahrzeuge je Bundesstaat zu, und dem Register muss gesagt werden, welcher durchsucht wird. Wählen Sie den Staat, in dem die meisten Fahrzeuge Ihrer Kunden zugelassen sind.",
+        "usState": "Bundesstaat",
+        "usStateHelp": "Der zweibuchstabige Code des Bundesstaats, in dem die Fahrzeuge zugelassen sind, etwa CA oder TX. Amerikanische Register werden je Bundesstaat durchsucht."
+      }
+    },
+    "openai": {
+      "description": "Servicebeschreibungen, Verlaufszusammenfassungen, Dokumentenerkennung und der Assistent, beantwortet über Ihr eigenes OpenAI-Konto. Jeder Aufruf wird diesem Konto berechnet, und Sie wählen das Modell.",
+      "fields": {
+        "apiKey": "API-Schlüssel",
+        "apiKeyHelp": "Auf platform.openai.com unter API keys anlegen. Das Konto braucht Guthaben; OpenAI rechnet pro Aufruf ab."
+      },
+      "settings": {
+        "model": "Modell",
+        "modelHelp": "Welches Modell antwortet. Die Liste kommt aus Ihrem Konto, neueste zuerst. Kleinere Modelle kosten pro Aufruf weniger und reichen für die meisten Werkstatttexte."
+      }
+    },
+    "anthropic": {
+      "description": "Servicebeschreibungen, Verlaufszusammenfassungen, Dokumentenerkennung und der Assistent, beantwortet von Claude über Ihr eigenes Anthropic-Konto. Jeder Aufruf wird diesem Konto berechnet, und Sie wählen das Modell.",
+      "fields": {
+        "apiKey": "API-Schlüssel",
+        "apiKeyHelp": "Auf console.anthropic.com unter API keys anlegen. Das Konto braucht Guthaben; Anthropic rechnet pro Aufruf ab."
+      },
+      "settings": {
+        "model": "Modell",
+        "modelHelp": "Welches Modell antwortet. Die Liste kommt aus Ihrem Konto, neueste zuerst. Kleinere Modelle kosten pro Aufruf weniger und reichen für die meisten Werkstatttexte."
+      }
+    },
+    "stripe": {
+      "description": "Kartenzahlungen über den Rechnungslink auf Ihr eigenes Stripe-Konto. Der Kunde zahlt, Stripe meldet es Torqvoice, und die Rechnung wird von selbst als bezahlt markiert.",
+      "fields": {
+        "secretKey": "Geheimer Schlüssel",
+        "secretKeyHelp": "Aus dem Stripe-Dashboard unter Developers, API keys. Testschlüssel verarbeiten nichts Echtes, probieren Sie also zuerst eine Zahlung damit und wechseln Sie danach zu Live-Schlüsseln.",
+        "publishableKey": "Veröffentlichbarer Schlüssel",
+        "webhookSecret": "Webhook-Signaturgeheimnis",
+        "webhookSecretHelp": "Wird von Stripe angezeigt, sobald Sie die Benachrichtigungs-URL dieser Seite als Webhook hinzufügen. Damit wird jede Benachrichtigung geprüft, bevor eine Zahlung erfasst wird."
+      },
+      "settings": {
+        "enabled": "Auf Rechnungslinks anbieten",
+        "enabledHelp": "Ausschalten, um den Bezahlknopf eine Weile von geteilten Rechnungen zu nehmen, ohne die Verbindung zu trennen. Bereits begonnene Zahlungen werden weiterhin erfasst."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay über den Rechnungslink. Der Kunde zahlt in der App, die er ohnehin hat, und die Rechnung wird von selbst als bezahlt markiert.",
+      "fields": {
+        "clientId": "Client-ID",
+        "clientSecret": "Client-Secret",
+        "subscriptionKey": "Subscription Key",
+        "subscriptionKeyHelp": "Der Ocp-Apim-Subscription-Key aus dem Vipps-MobilePay-Portal, unter den API-Schlüsseln der Verkaufseinheit.",
+        "merchantSerialNumber": "Merchant Serial Number"
+      },
+      "settings": {
+        "enabled": "Auf Rechnungslinks anbieten",
+        "enabledHelp": "Ausschalten, um den Bezahlknopf eine Weile von geteilten Rechnungen zu nehmen, ohne die Verbindung zu trennen. Bereits begonnene Zahlungen werden weiterhin erfasst.",
+        "testMode": "Testmodus",
+        "testModeHelp": "Die Vipps-Testumgebung mit Testschlüsseln verwenden. Lassen Sie ihn an, bis eine vollständige Zahlung durchgelaufen ist, und schalten Sie dann um auf die Live-Schlüssel."
+      }
+    },
+    "paypal": {
+      "description": "PayPal-Checkout über den Rechnungslink mit Ihrer eigenen PayPal-App. Der Kunde zahlt mit PayPal-Konto oder Karte, und die Rechnung wird von selbst als bezahlt markiert.",
+      "fields": {
+        "clientId": "Client-ID",
+        "clientSecret": "Client-Secret"
+      },
+      "settings": {
+        "enabled": "Auf Rechnungslinks anbieten",
+        "enabledHelp": "Ausschalten, um den Bezahlknopf eine Weile von geteilten Rechnungen zu nehmen, ohne die Verbindung zu trennen. Bereits begonnene Zahlungen werden weiterhin erfasst.",
+        "sandbox": "Sandbox-Modus",
+        "sandboxHelp": "Die PayPal-Sandbox mit Sandbox-App-Schlüsseln verwenden. Lassen Sie ihn an, bis eine vollständige Zahlung durchgelaufen ist, und tragen Sie dann die Schlüssel der Live-App ein."
+      }
+    },
+    "quickbooks": {
+      "description": "Sende ausgestellte Rechnungen, ihre Kunden und ihre Zahlungen an QuickBooks Online und hole dort erfasste Zahlungen zurück.",
+      "tenantHelp": "Erstelle eine App im Intuit Developer Portal. Kopiere Client-ID und Secret unter Keys and credentials und trage die Redirect-URI unten unter Settings, Redirect URIs ein. Entwicklungsschlüssel erreichen Sandbox-Firmen, Produktionsschlüssel echte.",
+      "fields": {
+        "clientId": "Client-ID",
+        "clientSecret": "Client-Secret"
+      },
+      "settings": {
+        "pushInvoices": "Ausgestellte Rechnungen an QuickBooks senden",
+        "pushOnComplete": "Rechnungen auch senden, wenn der Auftrag als abgeschlossen markiert wird",
+        "pushOnCompleteHelp": "Ohne diese Option geht eine Rechnung erst über, wenn sie gesendet, geteilt oder bezahlt wurde. Mit ihr geht ein abgeschlossener Auftrag sofort über und wird bei Änderungen aktualisiert.",
+        "startDate": "Nur Rechnungen mit Datum ab",
+        "startDateHelp": "Leer lassen, um jede ausgestellte Rechnung zu senden. Nutze das Datum, an dem deine Bücher zu QuickBooks gewechselt sind, damit ältere Rechnungen nicht doppelt erfasst werden.",
+        "includeVehicle": "Fahrzeug auf der Rechnung nennen",
+        "includeVehicleHelp": "Eine Textzeile über den Positionen mit Baujahr, Marke, Modell, Kennzeichen und Kilometerstand, damit die Buchhaltung weiß, um welches Auto es geht.",
+        "laborItemId": "Artikel für Arbeitspositionen",
+        "laborItemIdHelp": "Leer lassen, dann wird beim ersten Mal ein Artikel namens Labour unter deinem Ertragskonto angelegt.",
+        "partsItemId": "Artikel für Teilepositionen",
+        "partsItemIdHelp": "Leer lassen, dann wird beim ersten Mal ein Artikel namens Parts unter deinem Ertragskonto angelegt.",
+        "taxAmounts": "Steuerbeträge",
+        "taxAmountsHelp": "Wie berechnet sendet die Steuer genau so, wie sie auf der Rechnung des Kunden steht, sodass beide Systeme auf den Cent übereinstimmen. Von QuickBooks ermittelt lässt QuickBooks den Satz des Steuercodes anwenden, in den USA die Adresse des Kunden, und das Protokoll meldet abweichende Summen.",
+        "taxAmountsBilled": "Wie in Torqvoice berechnet",
+        "taxAmountsQuickbooks": "Von QuickBooks ermittelt",
+        "taxCodeId": "Steuercode für steuerpflichtige Rechnungen",
+        "taxCodeIdHelp": "QuickBooks bucht die Steuer unter einem Steuercode. Torqvoice wählt den Code, dessen Satz zur jeweiligen Rechnung passt; dieser wird verwendet, wenn keiner passt. Ein US-Unternehmen verwendet TAX, sofern du nichts anderes wählst.",
+        "zeroTaxCodeId": "Steuercode für steuerfreie Rechnungen",
+        "zeroTaxCodeIdHelp": "Wird verwendet, wenn die Rechnung keine Steuer hat oder der Kunde steuerbefreit ist. Ein US-Unternehmen verwendet NON, sofern du nichts anderes wählst.",
+        "pushPayments": "Zahlungen in QuickBooks erfassen",
+        "depositAccountId": "Zahlungen einzahlen auf",
+        "depositAccountIdHelp": "Leer lassen, um Undeposited Funds zu verwenden.",
+        "manualPaidAsPayment": "Zahlung erfassen, wenn eine Rechnung von Hand als bezahlt markiert wird",
+        "manualPaidAsPaymentHelp": "Eine in Torqvoice ohne Zahlungseintrag als bezahlt markierte Rechnung erhält in QuickBooks eine Zahlung über den dort noch offenen Betrag. Ausgeschaltet bleibt sie dort offen, bis eine Zahlung erfasst wird.",
+        "pullPayments": "In QuickBooks erfasste Zahlungen zurückholen",
+        "pullPaymentsHelp": "Alle 30 Minuten. Eine in QuickBooks gegen eine Rechnung aus Torqvoice erhaltene Zahlung wird hier auf der Rechnung erfasst."
+      }
+    },
+    "nhtsa": {
+      "description": "Fülle ein Fahrzeug aus seiner FIN aus und sieh Rückrufe, Halterbeschwerden und Crashtest-Bewertungen für jedes Modelljahr, von der US-Sicherheitsbehörde. Kostenlos, ohne Schlüssel.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Rückruf- und Beschwerdedaten für jedes Fahrzeug dieser Werkstatt aktuell halten",
+        "refreshFleetHelp": "Einmal täglich werden Modelle, die in der letzten Woche nicht aktualisiert wurden, erneut abgerufen, damit die Fahrzeugseite nie wartet und ein neuer Rückruf innerhalb einer Woche nach Einreichung erscheint. Nur Marke, Modell und Jahr werden gesendet."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Aufträge in den Kalender",
+      "pull": "Belegte Zeit aus dem Kalender"
+    },
+    "conference": {
+      "sync": "Videoanruf-Links"
+    },
+    "inspection": {
+      "refresh": "HU-Termine aktualisieren"
+    },
+    "accounting": {
+      "invoice": "Rechnung an die Buchhaltung",
+      "payment": "Zahlung an die Buchhaltung",
+      "customer": "Kunde an die Buchhaltung",
+      "pull": "Zahlungen aus der Buchhaltung"
+    },
+    "safety": {
+      "refresh": "Aktualisierung der Rückrufdaten",
+      "vehicle": "Rückrufprüfung für ein neues Fahrzeug"
+    }
+  }
+}

+ 2 - 1
messages/de/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Geplant",
       "sms": "SMS",
       "whatsapp": "WhatsApp",
-      "telegram": "Telegram"
+      "telegram": "Telegram",
+      "campaigns": "Kampagnen"
     },
     "smsNotConfiguredTitle": "SMS ist nicht eingerichtet",
     "smsNotConfiguredHint": "Verbinden Sie einen SMS-Anbieter, um hier Nachrichten zu senden und zu empfangen. Geplante E-Mails funktionieren auch ohne.",

+ 5 - 1
messages/de/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "Konversation konnte nicht gelöscht werden",
     "delete": "Löschen",
     "cancel": "Abbrechen",
-    "loadMore": "Mehr laden"
+    "loadMore": "Mehr laden",
+    "unread": "{count} ungelesen",
+    "markUnread": "Als ungelesen markieren",
+    "nothingToMarkUnread": "Der Kunde hat in dieser Unterhaltung noch nicht geschrieben.",
+    "markUnreadFailed": "Die Unterhaltung konnte nicht als ungelesen markiert werden."
   }
 }

+ 16 - 2
messages/de/navigation.json

@@ -59,7 +59,8 @@
     "tireStorage": "Lagerstruktur",
     "tireSetDetails": "Reifensatz",
     "tireForecast": "Nächste Saison",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "inspectionReminders": "HU-Erinnerungen"
   },
   "search": "Suchen...",
   "shortcut": "Ctrl+K",
@@ -117,6 +118,19 @@
     "auditLog": "Aktivitätsprotokoll",
     "aiAssistant": "KI-Assistent",
     "tireHotel": "Reifenhotel",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "newWorkOrder": "Neuer Arbeitsauftrag",
+    "installApp": "App installieren",
+    "roles": {
+      "owner": "Inhaber",
+      "admin": "Administrator",
+      "member": "Mitglied"
+    },
+    "badges": {
+      "messages": "{count} ungelesene Nachrichten",
+      "workOrders": "{count} offene Arbeitsaufträge",
+      "inspections": "{count} laufende Inspektionen",
+      "reminders": "{count} fällige Erinnerungen"
+    }
   }
 }

+ 3 - 1
messages/de/onboarding.json

@@ -76,7 +76,9 @@
     "allDoneTitle": "Alles erledigt",
     "allDoneDescription": "Alle Schritte sind abgeschlossen und Ihre Werkstatt ist startklar.",
     "dismiss": "Karte ausblenden",
-    "dismissAriaLabel": "Diese Checkliste ausblenden"
+    "dismissAriaLabel": "Diese Checkliste ausblenden",
+    "importTitle": "Haben Sie bereits Kunden und Fahrzeuge in einem anderen System?",
+    "importDescription": "Importieren Sie sie aus einer Tabelle oder Ihrem bisherigen System"
   },
   "sampleData": {
     "title": "Beispieldaten",

+ 51 - 1
messages/de/portal.json

@@ -158,7 +158,14 @@
     "columnVehicle": "Fahrzeug",
     "columnPlate": "Kennzeichen",
     "columnServices": "Services",
-    "columnInspections": "Inspektionen"
+    "columnInspections": "Inspektionen",
+    "reportSold": "Dieses Fahrzeug gehört mir nicht mehr",
+    "reportSoldHint": "Wir informieren die Werkstatt, damit sie keine Erinnerungen mehr dazu sendet. Ihre Historie bleibt erhalten.",
+    "reportSoldConfirm": "Ja, Werkstatt informieren",
+    "reportSoldCancel": "Behalten",
+    "reportSoldDone": "Danke, die Werkstatt wurde informiert",
+    "reportSoldFailed": "Konnte gerade nicht gesendet werden",
+    "reportSoldAlready": "Sie haben der Werkstatt mitgeteilt, dass dieses Fahrzeug nicht mehr Ihres ist."
   },
   "requestService": {
     "title": "Service anfordern",
@@ -190,5 +197,48 @@
     "modelPlaceholder": "z. B. Kuga",
     "yearLabel": "Baujahr",
     "plateLabel": "Kennzeichen"
+  },
+  "booking": {
+    "title": "Untersuchung buchen",
+    "due": "Untersuchung fällig am {date}",
+    "firstSlot": "Erster freier Termin",
+    "bookThis": "{when} buchen",
+    "pickAnother": "Oder einen anderen Tag wählen",
+    "pickTime": "Uhrzeit wählen",
+    "pickTimeFirst": "Bitte oben eine Uhrzeit wählen",
+    "confirmFor": "{when} bestätigen",
+    "notePlaceholder": "Etwas, das wir wissen sollten? (optional)",
+    "legend": "Durchgestrichene Tage sind voll; gelbe Tage haben wenige Termine.",
+    "day": {
+      "open": "Termine frei",
+      "limited": "Wenige Termine",
+      "full": "Ausgebucht",
+      "closed": "Geschlossen"
+    },
+    "booked": "Gebucht für {when}",
+    "bookedHint": "Wir haben eine Bestätigung gesendet. Bitte ein paar Minuten früher mit den Schlüsseln kommen.",
+    "pending": "Angefragt für {when}",
+    "pendingHint": "Die Werkstatt bestätigt Ihren Termin in Kürze.",
+    "changeOrCancel": "Ändern oder stornieren",
+    "expired": "Dieser Link ist abgelaufen",
+    "expiredHint": "Rufen Sie uns an, wir finden gemeinsam einen Termin.",
+    "unavailable": "Online-Buchung ist gerade nicht verfügbar",
+    "unavailableHint": "Rufen Sie uns an, wir finden einen Termin.",
+    "callUs": "Rufen Sie uns an: {phone}",
+    "modeDirectNote": "Ihr Termin wird sofort für {minutes} Minuten reserviert.",
+    "modeRequestNote": "Die Werkstatt bestätigt den gewünschten Termin.",
+    "bookedToast": "Ihr Termin ist gebucht",
+    "bookFailed": "Termin konnte nicht gebucht werden",
+    "cancelledToast": "Buchung storniert",
+    "cancelFailed": "Stornieren fehlgeschlagen",
+    "footer": "Sie haben diesen Link erhalten, weil Ihre Werkstatt Sie an die Untersuchung erinnert hat.",
+    "workOrderTitle": "Hauptuntersuchung {vehicle}",
+    "notificationTitle": "Neue Online-Buchung",
+    "notificationBody": "{customer} hat {vehicle} für {when} gebucht",
+    "cancelledNotificationTitle": "Online-Buchung storniert",
+    "cancelledNotificationBody": "{customer} hat die Buchung für {vehicle} am {when} storniert",
+    "confirmationSubject": "Ihre Buchung bei {workshop}",
+    "confirmationBody": "Ihr {vehicle} ist für {when} bei {workshop} gebucht. Änderung nötig? Nutzen Sie den Link aus unserer letzten Nachricht oder rufen Sie {phone} an.",
+    "confirmationBodyRequest": "Danke, {workshop} hat Ihre Anfrage für {when} für Ihren {vehicle} erhalten und bestätigt in Kürze. Fragen? Rufen Sie {phone} an."
   }
 }

+ 1 - 0
messages/de/scheduledMessages.json

@@ -41,6 +41,7 @@
     "scheduled": "Nachricht geplant",
     "updated": "Geplante Nachricht aktualisiert",
     "saveError": "Nachricht konnte nicht gespeichert werden",
+    "inPast": "Dieser Zeitpunkt liegt bereits in der Vergangenheit. Wähle eine Zeit in der Zukunft.",
     "whatsappNeedsTemplate": "Diese Nachricht geht lange nach Ablauf des 24-Stunden-Fensters raus, und es ist keine WhatsApp-Textvorlage eingerichtet, daher würde sie beim Senden scheitern. Richten Sie eine unter Einstellungen > Anbieter > WhatsApp ein."
   },
   "list": {

+ 59 - 2
messages/de/service.json

@@ -34,7 +34,8 @@
     "delete": "Löschen",
     "unpaid": "Unbezahlt",
     "unsavedChanges": "Nicht gespeicherte Änderungen",
-    "saved": "Gespeichert"
+    "saved": "Gespeichert",
+    "joinCall": "Videoanruf beitreten"
   },
   "basicInfo": {
     "title": "Grundlegende Informationen",
@@ -78,7 +79,17 @@
       "typePrompt": "Geben Sie einen Namen ein...",
       "boardTechnicians": "Board-Techniker",
       "teamMembers": "Teamitglieder"
-    }
+    },
+    "design": "Rechnungsdesign",
+    "designDefault": "Werkstatt-Standard",
+    "designFollowing": "Standard ({name})",
+    "designPinned": "Das Blatt wurde beim Ausstellen der Rechnung am {date} eingefroren. Eine neue Wahl gilt erst, wenn ein Inhaber die Rechnung entsperrt und sie erneut gesendet wird.",
+    "designSaved": "Rechnungsdesign aktualisiert",
+    "designFollowingRule": "Standard ({name}, {rule})",
+    "designRule": {
+      "parts_sale": "Reiner Teileverkauf"
+    },
+    "designSaveFailed": "Rechnungsdesign konnte nicht geändert werden"
   },
   "parts": {
     "title": "Teile",
@@ -349,6 +360,7 @@
     "workBay": "Arbeitsplatz",
     "selectWorkBay": "Arbeitsplatz wählen …",
     "noWorkBay": "Kein Arbeitsplatz",
+    "noTechnician": "Ohne Mechaniker",
     "technicians": "Techniker",
     "otherTeamMembers": "Weitere Teammitglieder",
     "makeTechnician": "Macht sie zum Techniker",
@@ -410,5 +422,50 @@
     "remove": "Dieses Anliegen entfernen",
     "notAnswered": "Dazu wurde noch nichts festgestellt",
     "answered": "{count} Feststellung(en) dazu"
+  },
+  "videoCall": {
+    "title": "Videoanruf",
+    "none": "Kein Videoanruf für diesen Auftrag.",
+    "needsSchedule": "Legen Sie zuerst eine Startzeit fest. Das Meeting folgt dem Termin.",
+    "add": "{provider}-Meeting hinzufügen",
+    "addedHere": "von diesem Auftrag hinzugefügt",
+    "fromCalendar": "aus dem Kalendereintrag",
+    "join": "Beitreten",
+    "copy": "Link kopieren",
+    "copied": "Link kopiert",
+    "remove": "Entfernen",
+    "removeTitle": "Videoanruf entfernen?",
+    "removeDescription": "Das Meeting wird beim Anbieter gelöscht. Wer den Link bereits hat, kann nicht mehr beitreten.",
+    "created": "Meeting erstellt. Der Link ist am Auftrag.",
+    "removed": "Meeting entfernt",
+    "failed": "Etwas ist schiefgelaufen",
+    "shareHint": "Der Kunde erhält diesen Link nur, wenn du ihn sendest.",
+    "shareHintNoChannel": "Der Kunde erhält diesen Link nur, wenn du ihn sendest. Es ist kein Nachrichtenkanal eingerichtet, kopiere ihn also selbst in eine Nachricht.",
+    "send": {
+      "button": "An Kunden senden",
+      "title": "Link zum Videoanruf senden",
+      "description": "Den Beitrittslink an {name} senden.",
+      "messageLabel": "Notiz (optional)",
+      "messagePlaceholder": "Was der Kunde vor dem Anruf wissen sollte...",
+      "linkAlwaysIncluded": "Der Beitrittslink wird immer unter deine Notiz gesetzt.",
+      "sms": "SMS",
+      "email": "E-Mail",
+      "telegram": "Telegram",
+      "noPhone": "(keine Telefonnummer)",
+      "noEmail": "(keine E-Mail)",
+      "noTelegram": "(kein Chat verknüpft)",
+      "notAvailable": "(nicht eingerichtet)",
+      "cancel": "Abbrechen",
+      "send": "Senden",
+      "sent": "Link zum Videoanruf gesendet",
+      "partlySent": "Gesendet per {sent}; nicht per {failed}",
+      "failed": "Link konnte nicht gesendet werden"
+    },
+    "notify": {
+      "invitation": "Hallo {name}, dein Videoanruf zum {vehicle} ist am {when}. Hier beitreten: {url}",
+      "invitationNoTime": "Hallo {name}, hier ist der Link zum Videoanruf zum {vehicle}: {url}",
+      "joinLine": "Hier dem Videoanruf beitreten: {url}",
+      "emailSubject": "Videoanruf zu deinem {vehicle}"
+    }
   }
 }

+ 101 - 27
messages/de/settings.json

@@ -7,7 +7,7 @@
       "billing": "Abrechnung & Dokumente",
       "communications": "Kommunikation",
       "workshop": "Werkstatt",
-      "integrations": "Integrationen",
+      "integrations": "Apps & API",
       "system": "System"
     },
     "items": {
@@ -91,6 +91,10 @@
         "title": "Berichtsplanung",
         "description": "Automatischer Berichtsversand"
       },
+      "integrations": {
+        "title": "Integrationen",
+        "description": "Kalender, Videoanrufe und verbundene Dienste"
+      },
       "webhooks": {
         "title": "Webhooks",
         "description": "Echtzeit-Ereignisbenachrichtigungen"
@@ -130,6 +134,10 @@
       "providers": {
         "title": "Anbieter",
         "description": "E-Mail, SMS, WhatsApp & Telegram"
+      },
+      "inspectionReminders": {
+        "title": "HU-Erinnerungen",
+        "description": "Buchungslink, Dauer und Vorlauf"
       }
     },
     "pro": "PRO"
@@ -572,7 +580,16 @@
     "quoteLockTriggerSent": "Sobald das Angebot versendet ist",
     "quoteLockTriggerAccepted": "Sobald das Angebot angenommen ist",
     "quoteLockTriggerSentHint": "Sperrt, sobald das Angebot zum Kunden geht. Nur wählen, wenn Sie Angebote nie nachverhandeln.",
-    "quoteLockTriggerAcceptedHint": "Sperrt, sobald der Kunde das Angebot angenommen hat oder es in einen Auftrag umgewandelt wurde. Versendete Angebote bleiben bearbeitbar."
+    "quoteLockTriggerAcceptedHint": "Sperrt, sobald der Kunde das Angebot angenommen hat oder es in einen Auftrag umgewandelt wurde. Versendete Angebote bleiben bearbeitbar.",
+    "freezeTitle": "Rechnungen von vor der Sperre",
+    "freezeBody": "{count, plural, one {# Rechnung} other {# Rechnungen}} erreichte einen Kunden, bevor diese Version sperren konnte, was eine Rechnung druckt. Sie werden weiterhin mit Ihren aktuellen Angaben, Logo und Design gedruckt, sodass eine Änderung von heute auch sie erreicht. Sperren bewahrt jede so, wie sie jetzt gedruckt wird.",
+    "freezeNone": "Jede Rechnung, die einen Kunden erreicht hat, ist so gesperrt, wie sie versandt wurde.",
+    "freezeButton": "Mit dem aktuellen Design sperren",
+    "freezeConfirmTitle": "{count, plural, one {# Rechnung} other {# Rechnungen}} sperren?",
+    "freezeConfirmBody": "Jede behält das Design, das Logo, die Werkstattangaben, den Kunden, das Fahrzeug und die Befunde, mit denen sie jetzt gedruckt wird. Das kann nicht rückgängig gemacht werden; die Rechnungen bleiben genau so bearbeitbar oder für die Bearbeitung gesperrt wie heute.",
+    "freezeProgress": "{done} von {total} gesperrt",
+    "freezeDone": "{count, plural, one {# Rechnung} other {# Rechnungen}} gesperrt",
+    "freezeFailed": "Rechnungen konnten nicht gesperrt werden"
   },
   "layoutEditor": {
     "helpText": "Ziehen zum Neuordnen. Verwenden Sie den Breiten-Umschalter, um Abschnitte nebeneinander zu platzieren. Aufeinanderfolgende Halbbreite-Abschnitte (L/R) werden in zwei Spalten dargestellt.",
@@ -634,6 +651,12 @@
       "bank_account": "Bankkonto",
       "footer_note": "Fußzeilen-Hinweis",
       "org_number": "Organisationsnummer",
+      "payment_terms": "Zahlungsbedingungen",
+      "due_date": "Fälligkeitsdatum",
+      "title": "Titel",
+      "invoice_number": "Rechnungsnummer",
+      "customer_number": "Kundennummer",
+      "date": "Datum",
       "portal_link": "Portal-Link"
     },
     "presetsTitle": "Mit einer Anordnung beginnen",
@@ -808,32 +831,17 @@
     "paymentTermsPlaceholder": "Netto 14 Tage",
     "paymentTermsHint": "Zahlungsbedingungen auf Rechnungen (z.B. \"Netto 14 Tage\", \"Zahlbar bei Erhalt\")",
     "onlinePayments": "Online-Zahlungen",
-    "onlinePaymentsDescription": "Ermoglichen Sie Kunden, Rechnungen online uber geteilte Rechnungslinks zu bezahlen.",
-    "stripe": "Stripe",
-    "stripeDescription": "Kredit-/Debitkartenzahlungen akzeptieren",
-    "secretKey": "Geheimer Schlussel",
-    "publishableKey": "Offentlicher Schlussel",
-    "webhookSecret": "Webhook-Geheimnis",
-    "webhookUrl": "Webhook-URL",
-    "stripeWebhookHint": "Fugen Sie diese URL in Ihrem Stripe-Dashboard unter Webhooks hinzu. Abonnieren Sie <code>checkout.session.completed</code>.",
-    "vipps": "Vipps",
-    "vippsDescription": "Vipps-Mobilzahlungen akzeptieren (Norwegen)",
-    "clientId": "Client-ID",
-    "clientSecret": "Client-Geheimnis",
-    "subscriptionKey": "Abonnement-Schlussel",
-    "merchantSerialNumber": "Handler-Seriennummer",
-    "testMode": "Testmodus",
-    "callbackUrl": "Callback-URL",
-    "termsOfSale": "Verkaufsbedingungen (Salgsvilkar)",
-    "termsOfSaleDescription": "Von Vipps vorgeschrieben. Kunden konnen diese auf der Rechnungsseite einsehen.",
+    "onlinePaymentsDescription": "Kunden bezahlen Rechnungen über den Link, den Sie ihnen schicken. Stripe, Vipps und PayPal werden unter Integrationen verbunden und können nebeneinander laufen.",
+    "onlinePaymentsHint": "Bezahlt ein Kunde, meldet der Anbieter es Torqvoice und die Rechnung wird von selbst als bezahlt markiert. Die Seite jedes Anbieters zeigt die Benachrichtigungs-URL, die er braucht.",
+    "offered": "Auf Rechnungslinks",
+    "paused": "Verbunden, nicht angeboten",
+    "termsOfSale": "Verkaufsbedingungen",
+    "termsOfSaleDescription": "Was ein Kunde bei der Online-Zahlung akzeptiert. Vipps verlangt veröffentlichte Bedingungen; die Rechnung verlinkt sie in jedem Fall.",
+    "termsOfSaleText": "Text der Bedingungen",
     "externalTermsUrl": "Externe Bedingungs-URL (optional)",
     "externalTermsUrlHint": "Link zu einer externen Seite mit Ihren Bedingungen. Falls gesetzt, verlinkt die Rechnung hierhin statt zur integrierten Bedingungsseite.",
     "publicTermsUrl": "Offentliche Bedingungs-URL",
     "publicTermsUrlHint": "Teilen Sie diese URL mit Vipps oder verlinken Sie sie auf Ihrer Website.",
-    "paypal": "PayPal",
-    "paypalDescription": "PayPal-Zahlungen akzeptieren",
-    "sandboxMode": "Sandbox-Modus",
-    "paypalWebhookHint": "Fugen Sie diese URL in Ihrem PayPal-Entwickler-Dashboard unter Webhooks hinzu. Abonnieren Sie <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "savePayment": "Zahlungseinstellungen speichern",
     "saved": "Zahlungseinstellungen gespeichert"
   },
@@ -1112,7 +1120,7 @@
   },
   "data": {
     "title": "Datenverwaltung",
-    "description": "Exportieren Sie Ihre Daten als Backup oder importieren Sie eine zuvor exportierte Datei.",
+    "description": "Übernehmen Sie Daten aus Ihrem alten System, laden Sie eine Sicherung von allem herunter oder stellen Sie eine wieder her.",
     "exportTitle": "Daten exportieren",
     "exportDescription": "Laden Sie Ihre Daten als ZIP-Datei herunter. Wählen Sie die Kategorien für das Backup aus.",
     "includeInBackup": "Im Backup enthalten:",
@@ -1199,7 +1207,16 @@
     "optTireHotel": "Reifenhotel",
     "optTireHotelDesc": "Eingelagerte Reifensätze, Regale, Vorbereitung und Lagervereinbarungen",
     "optWorkshopConfig": "Werkstatt-Einrichtung",
-    "optWorkshopConfigDesc": "Arbeitsvorlagen, Rollen, Webhooks und geplante Berichte"
+    "optWorkshopConfigDesc": "Arbeitsvorlagen, Rollen, Webhooks und geplante Berichte",
+    "importHubDescription": "Wählen Sie, wo Ihre Daten heute liegen. Importe aus Tabellen und Apps ergänzen Ihre Daten und lassen sich rückgängig machen; eine Wiederherstellung ersetzt alles.",
+    "sourceSpreadsheet": "Tabelle oder CSV",
+    "sourceSpreadsheetDesc": "Exporte aus jedem Werkstattsystem, Buchhaltungsprogramm, Google Kontakte, Outlook oder Excel. Sie bestätigen die Spaltenzuordnung und sehen einen Testlauf, bevor etwas geschrieben wird.",
+    "sourceLubelogDesc": "Fahrzeuge, Serviceeinträge und Notizen aus einer LubeLog-Sicherung.",
+    "sourceInvoiceNinjaDesc": "Kunden, Produkte, Rechnungen und Zahlungen aus einem Invoice-Ninja-Export.",
+    "sourceBackup": "Torqvoice-Sicherung",
+    "sourceBackupDesc": "Eine auf dieser Seite heruntergeladene .zip- oder .json-Datei. Die Wiederherstellung ersetzt alles in dieser Werkstatt.",
+    "restore": "Wiederherstellen",
+    "restoreTitle": "Sicherung wiederherstellen"
   },
   "license": {
     "title": "White-Label-Lizenz",
@@ -1740,6 +1757,17 @@
         "feature": "Telegram-Nachrichten",
         "description": "Telegram-Nachrichten mit Ihren Kunden direkt aus Torqvoice senden und empfangen."
       }
+    },
+    "moved": {
+      "title": "These settings moved to Integrations",
+      "description": "Email, SMS, WhatsApp and Telegram are set up in Integrations now, alongside calendars and everything else you connect. What you had set up came with them, so there is nothing to enter again.",
+      "connectedTo": "Connected through {vendor}",
+      "notSetUp": "Not set up yet",
+      "manage": "Manage",
+      "notConnected": "Not connected",
+      "goToIntegrations": "Go to Integrations",
+      "platformMail": "Sending through the platform mail server. Connect your own to send from your address.",
+      "onByDefault": "On by default"
     }
   },
   "designer": {
@@ -1749,6 +1777,14 @@
     "galleryTitle": "Mit einer Vorlage beginnen",
     "galleryHint": "Wählen Sie einen Ausgangspunkt. Im Designer lässt sich alles ändern.",
     "yourDesigns": "Ihre Designs",
+    "autoRuleHint": "Verkäufe ohne Fahrzeug werden mit dem dafür markierten Design gedruckt, sofern der Verkauf oder der Kunde kein anderes gewählt hat.",
+    "useForPartsSale": "Für reine Teileverkäufe verwenden",
+    "usedForPartsSale": "Wird für reine Teileverkäufe verwendet",
+    "autoRule": {
+      "parts_sale": "Reiner Teileverkauf"
+    },
+    "autoRuleSaved": "Verwendung von „{name}“ aktualisiert",
+    "couldNotSaveRule": "Regel konnte nicht aktualisiert werden",
     "templates": "Vorlagen",
     "inUse": "In Verwendung",
     "savedOn": "Gespeichert am {date}",
@@ -1908,6 +1944,7 @@
       "serviceType": "Reparatur",
       "companyName": "Ihre Werkstatt",
       "slogan": "Qualität, auf die Sie sich verlassen können",
+      "paymentTerms": "Zahlbar innerhalb von 14 Tagen",
       "footerNote": "Vielen Dank für Ihren Auftrag",
       "value": "Beispiel"
     },
@@ -1923,11 +1960,22 @@
     "sloganPlaceholderHint": "Es ist kein Slogan hinterlegt, daher zeigt das Blatt einen Platzhalter, der nicht gedruckt wird.",
     "sloganSetHint": "Ihr Slogan stammt aus Ihren Firmendaten.",
     "sloganLink": "Firmeneinstellungen",
+    "paymentTerms": "Zahlungsbedingungen",
+    "paymentTermsPlaceholderHint": "Es sind keine Zahlungsbedingungen hinterlegt, daher zeigt das Blatt einen Platzhalter, der nicht gedruckt wird.",
+    "paymentTermsSetHint": "Ihre Zahlungsbedingungen stammen aus den Zahlungseinstellungen.",
+    "paymentTermsLink": "Zahlungseinstellungen",
+    "titleText": "Titeltext",
+    "titleTextHint": "Wie sich dieses Dokument nennt. Leer gelassen, druckt es seinen eigenen Namen in der Sprache des Lesers.",
     "placeholder": "Platzhalter",
     "leaveTitle": "Ohne Speichern verlassen?",
     "leaveBody": "Ihre Änderungen an diesem Design wurden nicht gespeichert und gehen verloren.",
     "leaveConfirm": "Verlassen",
-    "boldField": "Fett"
+    "boldField": "Fett",
+    "deleteDesignDrafts": "{count, plural, one {# Rechnungsentwurf} other {# Rechnungsentwürfe}} mit diesem Design fallen auf das Standarddesign zurück.",
+    "deleteDesignCustomers": "{count, plural, one {# Kunde} other {# Kunden}} mit diesem Design fallen auf das Standarddesign zurück.",
+    "setDefault": "Als Standard festlegen",
+    "defaultSet": "„{name}“ wird jetzt verwendet",
+    "couldNotSetDefault": "Standarddesign konnte nicht festgelegt werden"
   },
   "preview": {
     "title": "Bremsenservice und Ölwechsel",
@@ -1942,5 +1990,31 @@
     "partSyntheticOil": "Synthetiköl 5W-30",
     "laborBrakeReplacement": "Bremsenwechsel",
     "laborOilChange": "Ölwechsel"
+  },
+  "inspectionReminders": {
+    "title": "HU-Erinnerungen und Buchung",
+    "description": "Wie sich der Buchungslink in einer Erinnerung verhält. Einmal festlegen; jede Kampagne nutzt diese Werte.",
+    "duration": "Dauer der Untersuchung (Minuten)",
+    "durationHint": "Wie lange eine Hauptuntersuchung eine Bühne belegt. Der Link bucht Termine dieser Länge.",
+    "leadDays": "Früheste Buchung (Tage im Voraus)",
+    "leadDaysHint": "1 bedeutet: der erste angebotene Termin ist morgen, nie heute.",
+    "horizonWeeks": "Wie weit im Voraus (Wochen)",
+    "horizonHint": "Kunden können jede freie Zeit innerhalb so vieler Wochen wählen.",
+    "reserve": "Für Laufkundschaft freihalten",
+    "reserveHint": "Bühnen oder Techniker, die der Link nie belegt. 0 erlaubt alle freien.",
+    "linkValidDays": "Link bleibt offen (Tage)",
+    "linkValidHint": "Mindestens {min}. Links bleiben außerdem bis eine Woche nach der Frist offen, je nachdem, was später ist.",
+    "bookingMode": "Was eine Buchung anlegt",
+    "bookingModeDirect": "Einen geplanten Auftrag auf der Tafel",
+    "bookingModeRequest": "Eine Anfrage, die das Büro zuerst bestätigt",
+    "bookingModeHint": "Die Direktbuchung macht den Link erst sinnvoll. Wählen Sie die Anfrage, wenn Sie jeden Termin freigeben wollen.",
+    "phone": "Telefonnummer in Nachrichten",
+    "phoneHint": "Wird als Alternative zum Link gezeigt. Standard ist die Werkstattnummer.",
+    "save": "Speichern",
+    "saved": "Einstellungen gespeichert",
+    "saveFailed": "Speichern fehlgeschlagen",
+    "timeZoneDetected": "Die Zeitzone steht unter Lokalisierung auf automatisch, daher nutzt der Server {zone}, die zuletzt in einem Browser gesehene Zone. Wählen Sie die Zone der Werkstatt ausdrücklich, damit Buchungszeiten nie davon abhängen, wer zuletzt gespeichert hat.",
+    "timeZoneExplicit": "Buchungszeiten und Öffnungszeiten werden in {zone} gelesen.",
+    "timeZoneChange": "Unter Lokalisierung ändern"
   }
 }

+ 217 - 3
messages/de/vehicles.json

@@ -4,6 +4,7 @@
     "archived": "Archiviert",
     "searchPlaceholder": "Suche nach Kennzeichen, Marke, Modell, FIN...",
     "addVehicle": "Fahrzeug hinzufügen",
+    "importVehicles": "Importieren",
     "emptySearch": "Keine Fahrzeuge entsprechen Ihrer Suche.",
     "emptyArchived": "Keine archivierten Fahrzeuge.",
     "empty": "Noch keine Fahrzeuge.",
@@ -30,7 +31,12 @@
     "viewTable": "Table view",
     "viewGrid": "Grid view",
     "viewLargeGrid": "Large grid view",
-    "openMenu": "Open menu"
+    "openMenu": "Open menu",
+    "inspectionOverdue": "HU überfällig",
+    "inspectionDue30": "Fällig in 30 Tagen",
+    "inspectionDue90": "Fällig in 90 Tagen",
+    "inspectionDue": "HU {date}",
+    "remindCustomers": "Diese Kunden erinnern"
   },
   "detail": {
     "vehicleArchived": "Dieses Fahrzeug ist archiviert",
@@ -90,7 +96,11 @@
     "severityUncommon": "Ungewöhnlich — von wenigen Besitzern gemeldet",
     "severityRare": "Selten — vereinzelte Berichte, aber kritisch wenn es auftritt",
     "openMenu": "Open menu",
-    "dismissMaintenance": "Dismiss"
+    "dismissMaintenance": "Dismiss",
+    "inspectionDue": "HU fällig {date}",
+    "inspectionCheckedAt": "Mit dem Register abgeglichen am {date}",
+    "notRegistered": "Nicht zugelassen",
+    "soldReported": "Kunde meldet Verkauf am {date}"
   },
   "quotes": {
     "newQuote": "Neues Angebot",
@@ -130,6 +140,7 @@
     "descriptionLabel": "Beschreibung",
     "descriptionPlaceholder": "Optionale Details...",
     "dueDateLabel": "Fälligkeitsdatum",
+    "dueTimeLabel": "Uhrzeit (optional)",
     "dueMileageLabel": "Fälligkeitskilometerstand",
     "dueMileagePlaceholder": "z.B. 50000",
     "reminderUpdated": "Erinnerung aktualisiert",
@@ -338,7 +349,20 @@
     "sectionAssignment": "Foto und Halter",
     "sectionDetails": "Details",
     "addScannedOwner": "Kunde {name} anlegen",
-    "ownerSaveFailed": "Kunde konnte nicht angelegt werden"
+    "ownerSaveFailed": "Kunde konnte nicht angelegt werden",
+    "lookupPlate": "Kennzeichen nachschlagen",
+    "lookingUp": "Fahrzeugregister wird abgefragt...",
+    "lookupSuccess": "Felder aus {source} ausgefüllt",
+    "lookupNotFound": "Das Register kennt kein Fahrzeug mit diesem Kennzeichen",
+    "lookupFailed": "Kennzeichen konnte nicht nachgeschlagen werden",
+    "lookupUnavailable": "Verbinden Sie ein Fahrzeugregister unter Einstellungen → Integrationen",
+    "lookupEnterPlate": "Geben Sie zuerst ein Kennzeichen ein",
+    "lookupInspectionDue": "{source}: nächste Hauptuntersuchung fällig am {date}",
+    "lookupSource": "Ausgefüllt aus {source}",
+    "inspectionDue": "Nächste Hauptuntersuchung",
+    "inspectionDueHint": "Datum eintragen, wenn kein Register es liefert. Erinnerungen und Dashboard nutzen es.",
+    "inspectionDueFromRegistry": "Aus dem Fahrzeugregister. Hier ändern, um es bis zum nächsten Abgleich zu überschreiben.",
+    "lookupEnterPlateOrVin": "Gib zuerst ein Kennzeichen oder eine FIN ein"
   },
   "archiveDialog": {
     "title": "Fahrzeug archivieren",
@@ -353,5 +377,195 @@
     "previousPage": "Previous page",
     "nextPage": "Next page",
     "lastPage": "Last page"
+  },
+  "inspectionReminders": {
+    "title": "HU-Erinnerungen",
+    "description": "Kunden informieren, deren Hauptuntersuchung ansteht, mit einem Link zur Terminbuchung. Nichts wird gesendet, bevor Sie die Liste bestätigen.",
+    "window": "Frist innerhalb",
+    "window30": "Nächste 30 Tage",
+    "window60": "Nächste 60 Tage",
+    "window90": "Nächste 90 Tage",
+    "channel": "Senden per",
+    "channels": {
+      "sms": "SMS",
+      "whatsapp": "WhatsApp",
+      "email": "E-Mail",
+      "telegram": "Telegram"
+    },
+    "willReceive": "erhalten eine Nachricht",
+    "excludedCount": "{count} ausgeschlossen",
+    "nobodyToRemind": "Niemand zu erinnern in diesem Zeitraum. Jedes Fahrzeug mit Frist wurde bereits erinnert, hat gebucht oder ist nicht erreichbar.",
+    "overdueOn": "Überfällig seit {date}",
+    "excludedTitle": "Nicht enthalten",
+    "excludedDescription": "Jeder Eintrag hat einen Grund. Beheben Sie ihn am Kunden oder Fahrzeug, dann erscheint er in der Liste.",
+    "reasons": {
+      "noCustomer": "Kein Kunde",
+      "optedOut": "Erinnerungen abgelehnt",
+      "noRecipient": "Kein Kontakt für diesen Kanal",
+      "alreadyReminded": "Für diese Frist bereits erinnert",
+      "cooldown": "Kürzlich angeschrieben",
+      "soldReported": "Kunde meldet Verkauf",
+      "notRegistered": "Nicht zugelassen",
+      "booked": "Bereits gebucht"
+    },
+    "message": "Nachricht",
+    "subject": "Betreff",
+    "body": "Text",
+    "placeholders": "Platzhalter: {customerName} {vehicle} {plate} {dueDate} {workshopName} {bookingLink} {phone}",
+    "unknownPlaceholders": "Unbekannte Platzhalter: {list}",
+    "sample": "Beispiel für den ersten Empfänger",
+    "segments": "{count, plural, one {# SMS-Teil} other {# SMS-Teile}}",
+    "bookingModeDirectNote": "Der Link bucht einen Termin von {minutes} Minuten direkt auf die Werkstatttafel.",
+    "bookingModeRequestNote": "Der Link sendet eine Anfrage mit der gewählten Zeit, die das Büro bestätigt.",
+    "linkValidNote": "Links bleiben mindestens {days} Tage und über die Frist hinaus offen.",
+    "sendTest": "Test an mich senden",
+    "testSent": "Test an {to} eingereiht",
+    "testFailed": "Test konnte nicht gesendet werden",
+    "sendButton": "An {count} senden",
+    "confirmTitle": "Erinnerungen senden?",
+    "confirmDescription": "{count} Nachrichten gehen per {channel} hinaus, eine pro Fahrzeug, einmal. Ein Fahrzeug wird für dieselbe Frist nie zweimal erinnert.",
+    "confirmInput": "Geben Sie {count} zur Bestätigung ein",
+    "cancel": "Abbrechen",
+    "sendNow": "Senden",
+    "created": "{count} Erinnerungen eingereiht",
+    "sendFailed": "Kampagne konnte nicht erstellt werden",
+    "loadFailed": "Liste konnte nicht geladen werden",
+    "campaign": {
+      "title": "Kampagne vom {date}",
+      "summary": "{channel}, Fristen innerhalb {window} Tagen",
+      "recipients": "Empfänger",
+      "sent": "Gesendet",
+      "failed": "Fehlgeschlagen",
+      "booked": "Gebucht",
+      "retryFailedButton": "{count} fehlgeschlagene erneut senden",
+      "retried": "{count} wieder eingereiht",
+      "retryFailed": "Erneuter Versuch fehlgeschlagen",
+      "rows": "Empfänger",
+      "status": {
+        "scheduled": "In Warteschlange",
+        "sent": "Gesendet",
+        "failed": "Fehlgeschlagen",
+        "cancelled": "Abgebrochen"
+      },
+      "bookedOn": "Gebucht {date}",
+      "requestedOn": "Angefragt {date}",
+      "cancelledOn": "Storniert {date}",
+      "none": "Noch keine Erinnerungskampagnen.",
+      "start": "HU-Erinnerungen senden",
+      "recipientsCount": "{count} Empfänger",
+      "failedCount": "{count} fehlgeschlagen",
+      "bookedCount": "{count} gebucht"
+    },
+    "defaults": {
+      "smsBody": "Hallo {customerName}, die Hauptuntersuchung für Ihren {vehicle} ({plate}) ist am {dueDate} fällig. Termin buchen: {bookingLink} oder rufen Sie uns an: {phone}. {workshopName}",
+      "emailSubject": "HU fällig am {dueDate} für {vehicle}",
+      "emailBody": "Hallo {customerName},\n\ndie Hauptuntersuchung für Ihren {vehicle} ({plate}) ist am {dueDate} fällig.\n\nBuchen Sie hier einen passenden Termin: {bookingLink}\n\nOder rufen Sie uns an: {phone}.\n\nMit freundlichen Grüßen\n{workshopName}"
+    },
+    "refresh": "Liste aktualisieren",
+    "timeZoneRequired": "Die Zeitzone der Werkstatt steht auf automatisch, Buchungszeiten würden also dem Browser folgen, der zuletzt gespeichert hat (derzeit {zone}). Erinnerungen können erst gesendet werden, wenn eine Zone gewählt ist.",
+    "timeZoneRequiredLink": "Unter Lokalisierung wählen"
+  },
+  "plateLookup": {
+    "open": "Kennzeichenabfrage",
+    "shortcut": "Ctrl+Shift+L",
+    "title": "Kennzeichenabfrage",
+    "description": "Geben Sie ein Kennzeichen ein, um zu sehen, was Ihre Werkstatt und das Fahrzeugregister darüber wissen",
+    "placeholder": "Kennzeichen eingeben",
+    "search": "Abfragen",
+    "hintEnter": "Enter zum Abfragen",
+    "hintClose": "Esc zum Schließen",
+    "recent": "Zuletzt",
+    "clearRecent": "Leeren",
+    "looking": "{source} wird gefragt…",
+    "lookingWorkshop": "Werkstatt wird geprüft…",
+    "invalid": "Das sieht nicht wie ein Kennzeichen aus",
+    "inWorkshop": "In Ihrer Werkstatt",
+    "inWorkshopArchived": "In Ihrer Werkstatt, archiviert",
+    "notInWorkshop": "Noch nicht in Ihrer Werkstatt",
+    "notInWorkshopHint": "Das Register kennt dieses Fahrzeug. Fügen Sie es hinzu, die Felder unten kommen mit.",
+    "notInWorkshopNoRegistry": "Von Hand hinzufügen, oder das Kennzeichen prüfen.",
+    "openVehicle": "Fahrzeug öffnen",
+    "addVehicle": "Fahrzeug hinzufügen",
+    "noOwner": "Kein Kunde verknüpft",
+    "mileage": "{km} km",
+    "services": "{count, plural, =0 {Keine Serviceeinträge} one {# Serviceeintrag} other {# Serviceeinträge}}",
+    "lastService": "Letzter Service {date}",
+    "registryNotFound": "{source} kennt kein Fahrzeug mit diesem Kennzeichen",
+    "registryFailed": "{source} konnte nicht gefragt werden",
+    "registryFailedGeneric": "Das Register konnte nicht gefragt werden",
+    "fromSource": "Von {source}",
+    "registered": "Zugelassen",
+    "notRegistered": "Nicht zugelassen",
+    "vin": "FIN",
+    "copyVin": "FIN kopieren",
+    "copied": "Kopiert",
+    "fuel": "Kraftstoff",
+    "transmission": "Getriebe",
+    "engine": "Motor",
+    "engineCode": "Motorcode",
+    "color": "Farbe",
+    "vehicleClass": "Klasse",
+    "firstRegistered": "Erstzulassung",
+    "inspectionDue": "Prüfung fällig",
+    "lastInspected": "Zuletzt geprüft",
+    "weights": "Gewichte",
+    "weightsValue": "{kerb} kg leer, {gross} kg zulässig",
+    "kerbOnly": "{kerb} kg leer",
+    "grossOnly": "{gross} kg zulässig",
+    "tyres": "Reifen",
+    "axle": "Achse {n}",
+    "tyreSize": "Reifen",
+    "rimSize": "Felge",
+    "loadSpeed": "Last / Geschwindigkeit",
+    "overdue": "Überfällig",
+    "dueSoon": "Bald fällig",
+    "emptyTitle": "Beliebiges Kennzeichen abfragen",
+    "emptyBody": "Sehen Sie das Fahrzeug, wem es hier gehört und was {source} dazu führt. Noch nicht Ihres? In einem Schritt hinzufügen.",
+    "registryWord": "das Register"
+  },
+  "safety": {
+    "title": "Sicherheit und bekannte Probleme",
+    "summaryRecalls": "{count, plural, one {# Rückruf} other {# Rückrufe}}",
+    "summaryComplaints": "{count, plural, one {# Halterbeschwerde} other {# Halterbeschwerden}}",
+    "expand": "Details anzeigen",
+    "collapse": "Details ausblenden",
+    "loading": "Sicherheitsbehörde wird gefragt...",
+    "from": "Von {source} für {model}, aktualisiert am {date}",
+    "stale": "{source} war nicht erreichbar; der Bericht vom {date} wird gezeigt",
+    "notKnown": "{source} hat keinen Eintrag zu {model}.",
+    "notKnownHint": "Prüfe Marke, Modell und Jahr oder trage die FIN ein, damit das Modell genau zugeordnet werden kann.",
+    "recalls": "Rückrufe",
+    "recallsNone": "Keine Rückrufaktionen",
+    "complaints": "Halterbeschwerden",
+    "complaintsNone": "Keine eingereicht",
+    "crashes": "{count, plural, one {# Unfall} other {# Unfälle}}",
+    "fires": "{count, plural, one {# Brand} other {# Brände}}",
+    "rating": "Crashtest-Bewertung",
+    "notRated": "Nicht bewertet",
+    "stars": "{count} von 5 Sternen",
+    "frontal": "Frontal",
+    "side": "Seite",
+    "rollover": "Überschlag",
+    "ownersReport": "Was Halter melden",
+    "ofComplaints": "von {total} eingereichten Beschwerden",
+    "crash": "Unfall",
+    "fire": "Brand",
+    "noExamples": "Die Beschwerdetexte sind in diesem Bericht noch nicht enthalten. Von der Quelle aktualisieren, um sie zu holen.",
+    "readAll": "Alle {count} auf {source} lesen",
+    "showRecalls": "{count, plural, one {# Rückruf anzeigen} other {# Rückrufe anzeigen}}",
+    "hideRecalls": "Rückrufe ausblenden",
+    "more": "Mehr",
+    "less": "Weniger",
+    "consequence": "Risiko",
+    "remedy": "Abhilfe",
+    "parkIt": "Nicht fahren",
+    "parkOutside": "Im Freien parken",
+    "addFinding": "Zu Beobachtungen hinzufügen",
+    "findingAdded": "In Beobachtungen",
+    "findingDescription": "Rückruf {campaign}: {component}",
+    "refresh": "Von der Quelle aktualisieren",
+    "refreshed": "Sicherheitsdaten aktualisiert",
+    "openSite": "Auf {source} öffnen",
+    "errorLoad": "Sicherheitsdaten konnten nicht geladen werden"
   }
 }

+ 1 - 1
messages/en/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Ask about your workshop...",
   "clear": "Clear conversation",
   "error": "Something went wrong. Please try again.",
-  "notEnabled": "AI is not enabled. Configure it in Settings → AI.",
+  "notEnabled": "AI is not connected. Connect OpenAI or Anthropic under Settings → Integrations.",
   "suggestion1": "Which vehicles are due for service?",
   "suggestion2": "Show me unpaid invoices",
   "suggestion3": "What's the revenue this month?",

+ 27 - 2
messages/en/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "Registered a WhatsApp number",
     "team_giveTechnicianTheApp": "Gave a board-only technician an account",
     "team_createDefaultRoles": "Created the standard roles",
-    "settings_setCustomFieldPlacement": "Updated Custom Field Placement"
+    "settings_setCustomFieldPlacement": "Updated Custom Field Placement",
+    "import_commit": "Imported Spreadsheet",
+    "import_undo": "Undid Import",
+    "integration_connect": "Connected Integration",
+    "integration_disconnect": "Disconnected Integration",
+    "service_setInvoiceDesign": "Changed Invoice Design",
+    "settings_saveDocumentDesign": "Saved Document Design",
+    "settings_deleteDocumentDesign": "Deleted Document Design",
+    "settings_applyDocumentDesign": "Set Default Document Design",
+    "settings_setDocumentDesignRule": "Changed When a Design Is Used",
+    "settings_freezeInvoices": "Locked Older Invoices",
+    "inspection_reminders_sent": "Sent inspection reminders",
+    "inspection_reminders_send": "Sent inspection reminders",
+    "service_videoCall_send": "Sent video call link"
   },
   "summary": {
     "customField_create": "Created custom field \"{name}\"",
@@ -275,6 +288,18 @@
     "work_bay_create": "Created work bay \"{name}\"",
     "work_bay_update": "Updated work bay \"{name}\"",
     "work_bay_delete": "Deleted work bay {id}",
-    "settings_setCustomFieldPlacement": "Changed where a custom field prints"
+    "settings_setCustomFieldPlacement": "Changed where a custom field prints",
+    "import_commit": "Imported a spreadsheet: {count} records created",
+    "import_undo": "Undid a spreadsheet import: {records} records removed",
+    "integration_connect": "Connected {name}",
+    "integration_disconnect": "Disconnected {name}",
+    "service_setInvoiceDesign": "Changed which design an invoice prints with",
+    "settings_saveDocumentDesign": "Saved the document design {name}",
+    "settings_deleteDocumentDesign": "Deleted the document design {name}",
+    "settings_applyDocumentDesign": "Made {name} the default document design",
+    "settings_setDocumentDesignRule": "Changed when the document design {name} is used",
+    "settings_freezeInvoices": "Locked {count, plural, one {# invoice} other {# invoices}} from before locking with the current design",
+    "inspection_reminders_sent": "Sent {count} inspection reminders",
+    "service_videoCall_send": "Sent the video call link via {channels}"
   }
 }

+ 2 - 1
messages/en/auth.json

@@ -14,7 +14,8 @@
     "errors": {
       "tooManyAttempts": "Too many attempts, try again later.",
       "invalidCredentials": "Invalid email or password",
-      "passkeyFailed": "Passkey sign-in failed. Try again or use email and password."
+      "passkeyFailed": "Passkey sign-in failed. Try again or use email and password.",
+      "invalidOrigin": "You opened Torqvoice at {origin}, but it is configured for {configured}. Set NEXT_PUBLIC_APP_URL to the exact address you use in the browser, then restart Torqvoice."
     }
   },
   "signUp": {

+ 74 - 7
messages/en/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Today",
   "workOrder": "Work Order",
+  "allDay": "All day",
+  "openDay": "Open {date}",
+  "dayTotal": "{count, plural, =1 {1 event} other {# events}}",
+  "views": {
+    "day": "Day",
+    "week": "Week",
+    "month": "Month",
+    "year": "Year",
+    "schedule": "Schedule",
+    "fourDays": "4 days"
+  },
+  "options": {
+    "showWeekends": "Show weekends",
+    "showCompleted": "Show completed",
+    "showWeekNumbers": "Show week numbers"
+  },
+  "toolbar": {
+    "previous": "Previous",
+    "next": "Next",
+    "create": "Create",
+    "showSidebar": "Show side panel",
+    "hideSidebar": "Hide side panel"
+  },
+  "sidebar": {
+    "calendars": "Calendars",
+    "legend": "Status colours"
+  },
+  "schedule": {
+    "empty": "Nothing scheduled",
+    "emptyHint": "No work orders, reminders, quotes or messages in this period."
+  },
+  "peek": {
+    "close": "Close",
+    "open": {
+      "service": "Open work order",
+      "reminder": "Open reminder",
+      "quote": "Open quote",
+      "message": "Open messages",
+      "external": "Open"
+    }
+  },
   "filters": {
-    "services": "Services",
+    "services": "Work orders",
     "reminders": "Reminders",
     "quotes": "Quotes",
-    "messages": "Messages"
+    "messages": "Messages",
+    "external": "Connected calendars"
   },
   "selectVehicle": "Select Vehicle for Work Order",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Use today"
   },
   "more": "+ {count} more",
-  "monthTotal": "{count} events this month",
+  "monthTotal": "{count} events",
   "contextMenu": {
     "newWorkOrder": "New work order",
+    "newWorkOrderAt": "New work order at {time}",
     "newReminder": "New reminder",
     "newQuote": "New quote",
     "openEvent": "Open ({count})",
     "hint": "Tip: right-click a day for actions",
     "scheduleMessage": "Schedule message"
   },
+  "eventMenu": {
+    "open": "Open",
+    "markDone": "Mark as done",
+    "markNotDone": "Mark as not done",
+    "deleteReminder": "Delete reminder",
+    "deleteReminderConfirm": "Delete the reminder \"{title}\"? This cannot be undone.",
+    "deleteMessage": "Delete scheduled message",
+    "deleteMessageConfirm": "Delete the scheduled message \"{title}\"? It will not be sent.",
+    "deleted": "Deleted",
+    "updated": "Updated",
+    "failed": "That did not work. Try again."
+  },
   "events": {
     "status": {
       "completed": "Completed",
       "inProgress": "In Progress",
       "waitingParts": "Waiting Parts",
       "pending": "Pending",
+      "scheduled": "Scheduled",
       "sent": "Sent",
       "approved": "Approved",
       "draft": "Draft",
@@ -66,16 +122,27 @@
       "overdue": "Overdue",
       "upcoming": "Upcoming",
       "failed": "Failed",
-      "queued": "Queued"
+      "queued": "Queued",
+      "busy": "Busy"
     },
     "type": {
-      "service": "Service",
+      "service": "Work order",
       "reminder": "Reminder",
       "quote": "Quote",
-      "message": "Message"
+      "message": "Message",
+      "external": "Connected calendar"
     },
     "noEvents": "No events",
-    "selectDay": "Select a day with events or create a new work order."
+    "selectDay": "Select a day with events or create a new work order.",
+    "external": {
+      "source": "From {source}",
+      "sourceUnknown": "From a connected calendar",
+      "allDay": "All day",
+      "open": "Open in {source}",
+      "openGeneric": "Open in calendar",
+      "noLink": "This calendar does not give a link to the event.",
+      "readOnly": "Busy time from a connected calendar. Change it there and it updates here on the next sync."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/en/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Phone number copied to clipboard",
     "taxIdLabel": "Tax ID",
     "taxExemptBadge": "Tax exempt",
+    "invoiceDesignBadge": "Invoice design: {name}",
+    "invoiceDesignBadgeHint": "Invoices for this customer print with this design instead of the workshop default. Open it in the designer.",
     "tabs": {
       "vehicles": "Vehicles ({count})",
       "invoices": "Invoices ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Auto",
     "sectionContact": "Contact",
     "sectionBilling": "Billing",
-    "addScannedVehicle": "Add vehicle {vehicle}"
+    "addScannedVehicle": "Add vehicle {vehicle}",
+    "invoiceDesign": "Invoice design",
+    "invoiceDesignDefault": "Workshop default",
+    "invoiceDesignHint": "Invoices for this customer print with this design instead of the workshop default. An invoice can still choose its own.",
+    "reminderOptOut": "No reminders",
+    "reminderOptOutHint": "Do not include this customer in inspection reminders or other campaigns."
   },
   "serviceRequests": {
     "statusNew": "New",
@@ -117,44 +124,5 @@
     "notesSaveError": "Failed to save notes",
     "smsLabel": "Service Request Message",
     "smsDefault": "Hi {name}, regarding your service request for your {vehicle}: "
-  },
-  "import": {
-    "title": "Import Customers",
-    "description": "Import customers from CSV, TSV, or vCard files.",
-    "dropFile": "Drag & drop a file here",
-    "supportedFormats": "CSV, TSV, TXT, or vCard (.vcf)",
-    "browse": "Browse Files",
-    "noDataFound": "No customer data found in file",
-    "rowsFound": "{count} customer(s) found",
-    "colName": "Name",
-    "colEmail": "Email",
-    "colPhone": "Phone",
-    "colCompany": "Company",
-    "colAddress": "Address",
-    "chooseAnother": "Choose another file",
-    "cancel": "Cancel",
-    "importButton": "Import {count} customer(s)",
-    "importComplete": "Import complete",
-    "importedCount": "{count} customer(s) imported successfully",
-    "skippedRows": "{count} row(s) skipped due to errors",
-    "rowError": "Row {row} ({name}): {error}",
-    "done": "Done",
-    "importFailed": "Failed to import customers",
-    "checkingDuplicates": "Checking for duplicates...",
-    "duplicatesFound": "{count} duplicate(s) found",
-    "duplicatesBanner": "{count} existing customer(s) match your import data.",
-    "duplicatesHint": "Click Skip or Merge to change how each duplicate is handled.",
-    "colStatus": "Status",
-    "actionSkip": "Skip",
-    "actionMerge": "Merge",
-    "actionNew": "New",
-    "summaryNew": "{count} new",
-    "summaryMerge": "{count} merge",
-    "summarySkip": "{count} skip",
-    "skipAll": "Skip All",
-    "mergeAll": "Merge All",
-    "resultCreated": "{count} customer(s) created",
-    "resultMerged": "{count} customer(s) merged",
-    "resultSkipped": "{count} customer(s) skipped"
   }
 }

+ 12 - 1
messages/en/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Recent Observations",
     "recentActivity": "Recent Activity",
     "gettingStarted": "Getting Started",
-    "tireHotel": "Tire Hotel"
+    "tireHotel": "Tire Hotel",
+    "inspectionsDue": "Inspections due"
   },
   "maintenance": {
     "title": "Predicted Maintenance Due",
@@ -252,5 +253,15 @@
     "roomLeft": "Room for {count} more",
     "needsPrep": "Needs prep",
     "unbilled": "Unbilled ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Inspections due",
+    "viewAll": "View all due vehicles",
+    "overdue": "Overdue",
+    "within30": "Next 30 days",
+    "within90": "Next 90 days",
+    "noData": "No inspections due in the next 90 days",
+    "overdueSince": "Overdue since {date}",
+    "remind": "Send reminders"
   }
 }

+ 228 - 0
messages/en/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "Import {entity}",
+  "cancel": "Cancel",
+  "done": "Done",
+  "entities": {
+    "customers": "customers",
+    "vehicles": "vehicles",
+    "services": "service history"
+  },
+  "entityHints": {
+    "customers": "Names, contact details, addresses. One row per customer.",
+    "vehicles": "Make, model, year, plate, VIN. An owner column links or creates the customer.",
+    "services": "Past jobs and invoices. One row per job, with the vehicle it belongs to."
+  },
+  "groups": {
+    "customer": "Customer",
+    "vehicle": "Vehicle",
+    "service": "Service"
+  },
+  "steps": {
+    "upload": "Choose what to import and upload the file from your current system.",
+    "mapping": "Check which column becomes which field. Nothing is written yet.",
+    "preview": "Dry run: this is exactly what the import will do. Review it, then import.",
+    "importing": "Writing rows. Keep this window open.",
+    "result": "Import finished."
+  },
+  "upload": {
+    "drop": "Drag and drop a file here",
+    "formats": "CSV, Excel (.xlsx) or vCard (.vcf), up to 25 MB and 50,000 rows",
+    "browse": "Browse files",
+    "reading": "Reading the file...",
+    "templateHint": "No export yet? Fill in a template and upload that.",
+    "templateXlsx": "Excel template",
+    "templateCsv": "CSV template",
+    "sourcesHint": "Works with exports from any shop system, accounting tool, Google Contacts, Outlook or a spreadsheet. Column names are recognised in many languages; you confirm the mapping before anything is written."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# row} other {# rows}}",
+    "colColumn": "Column in your file",
+    "colSample": "Sample values",
+    "colField": "Becomes",
+    "ignore": "Ignore this column",
+    "source": {
+      "preset": "Recognised layout",
+      "header": "Matched by header",
+      "values": "Guessed from the values"
+    },
+    "suggestWithAi": "Suggest with AI",
+    "aiApplied": "{count, plural, one {# column mapped} other {# columns mapped}} by AI. Check them before continuing.",
+    "chooseAnother": "Choose another file",
+    "missingRequired": "Still needed: {fields}",
+    "dryRunHint": "The dry run checks every row against your existing data without writing anything.",
+    "runDryRun": "Run dry run"
+  },
+  "options": {
+    "dateFormat": "Date format",
+    "dateAuto": "Detect automatically",
+    "dateDMY": "Day / month / year",
+    "dateMDY": "Month / day / year",
+    "dateYMD": "Year / month / day",
+    "decimal": "Decimal mark",
+    "decimalAuto": "Detect automatically",
+    "decimalDot": "Dot (1,234.56)",
+    "decimalComma": "Comma (1.234,56)",
+    "countryCode": "Country code for phone numbers",
+    "duplicates": "When a row matches an existing record",
+    "duplicatesUpdate": "Update the existing record",
+    "duplicatesSkip": "Skip the row",
+    "duplicatesCreate": "Create a new record anyway"
+  },
+  "summary": {
+    "create": "Will be created",
+    "update": "Will be updated",
+    "skip": "Will be skipped",
+    "error": "Cannot be imported",
+    "warnings": "With warnings",
+    "customersToCreate": "{count, plural, one {# new customer} other {# new customers}}",
+    "customersToUpdate": "{count, plural, one {# customer updated} other {# customers updated}}",
+    "vehiclesToCreate": "{count, plural, one {# new vehicle} other {# new vehicles}}",
+    "vehiclesToUpdate": "{count, plural, one {# vehicle updated} other {# vehicles updated}}",
+    "servicesToCreate": "{count, plural, one {# service record} other {# service records}}"
+  },
+  "actions": {
+    "create": "Create",
+    "update": "Update",
+    "skip": "Skip",
+    "error": "Error"
+  },
+  "matchOn": {
+    "number": "customer number",
+    "email": "email",
+    "phone": "phone",
+    "name": "name",
+    "vin": "VIN",
+    "plate": "plate",
+    "makeModelYear": "make, model and year"
+  },
+  "preview": {
+    "colAction": "Action",
+    "colOwner": "Owner",
+    "colDetails": "Details",
+    "empty": "No rows match this filter.",
+    "matchedOn": "Matches {name} (by {on})",
+    "linksCustomer": "Linked to customer {name}",
+    "linksVehicle": "Linked to {label}",
+    "sameCustomerAsRow": "Same customer as row {row}",
+    "sameVehicleAsRow": "Same vehicle as row {row}",
+    "createsCustomer": "Creates the customer",
+    "createsVehicle": "Creates the vehicle",
+    "downloadReport": "Download dry run report",
+    "limitExceeded": "Your plan allows {remaining} more customers, but this import would create {needed}. Remove rows or upgrade your plan.",
+    "pageInfo": "{from}–{to} of {total}",
+    "prev": "Previous",
+    "next": "Next",
+    "backToMapping": "Back to mapping",
+    "import": "Import {count, plural, one {# row} other {# rows}}",
+    "nothingWrittenYet": "Nothing has been written yet. The import can be undone afterwards from Settings → Data."
+  },
+  "importing": {
+    "title": "Importing...",
+    "starting": "Starting",
+    "progress": "{done} of {total} rows",
+    "keepOpen": "Large files take a minute or two."
+  },
+  "result": {
+    "title": "Import complete",
+    "undoneTitle": "Import undone",
+    "created": "{count, plural, one {# record created} other {# records created}}",
+    "updated": "{count, plural, one {# record updated} other {# records updated}}",
+    "skipped": "{count, plural, one {# row skipped} other {# rows skipped}}",
+    "failed": "{count, plural, one {# row not imported} other {# rows not imported}}",
+    "writeFailures": "{count, plural, one {# row failed while writing} other {# rows failed while writing}}",
+    "rowFailure": "Row {row}: {error}",
+    "undo": "Undo this import",
+    "undone": "Removed {customers} customers, {vehicles} vehicles and {services} service records.",
+    "undoHint": "Undo removes every record this import created. Records it only updated keep their new values."
+  },
+  "history": {
+    "title": "Import history",
+    "description": "Spreadsheet imports run on this workshop. Undo removes what an import created.",
+    "loading": "Loading...",
+    "statusUndone": "Undone",
+    "counts": "{created} created, {updated} updated, {skipped} skipped, {failed} failed",
+    "undo": "Undo",
+    "undone": "Removed {customers} customers, {vehicles} vehicles and {services} service records.",
+    "undoFailed": "Could not undo the import",
+    "confirmTitle": "Undo this import?",
+    "confirmDescription": "Removes what {file} created: {customers} customers, {vehicles} vehicles and {services} service records. Records the import only updated are kept.",
+    "cancel": "Cancel",
+    "confirm": "Undo import"
+  },
+  "fields": {
+    "customer": {
+      "name": "Customer name",
+      "firstName": "First name",
+      "lastName": "Last name",
+      "customerNumber": "Customer number",
+      "email": "Email",
+      "phone": "Phone",
+      "company": "Company",
+      "address": "Address",
+      "street": "Street",
+      "postalCode": "Postal code",
+      "city": "City",
+      "state": "State / region",
+      "country": "Country",
+      "taxId": "Tax ID",
+      "notes": "Customer notes"
+    },
+    "vehicle": {
+      "description": "Vehicle (year make model in one column)",
+      "make": "Make",
+      "model": "Model",
+      "year": "Year",
+      "vin": "VIN",
+      "licensePlate": "License plate",
+      "color": "Color",
+      "mileage": "Mileage",
+      "fuelType": "Fuel type",
+      "transmission": "Transmission",
+      "engineSize": "Engine size",
+      "engineCode": "Engine code",
+      "purchaseDate": "Purchase date",
+      "purchasePrice": "Purchase price"
+    },
+    "service": {
+      "date": "Service date",
+      "title": "Service title",
+      "description": "Description",
+      "total": "Total amount",
+      "mileage": "Mileage at service",
+      "invoiceNumber": "Invoice number",
+      "notes": "Service notes",
+      "technician": "Technician"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Customer name is missing",
+    "invalid_email": "Email \"{value}\" is not valid and was left out",
+    "invalid_vin": "VIN \"{value}\" is not 17 characters; kept as typed",
+    "vehicle_make_model_required": "Make and model are missing",
+    "vehicle_year_required": "Year is missing",
+    "vehicle_year_invalid": "Year \"{value}\" is not a valid year",
+    "vehicle_identifier_required": "No plate, VIN or make and model to identify the vehicle",
+    "vehicle_not_found": "No existing vehicle matches \"{value}\" and there is not enough to create one",
+    "service_date_required": "Service date is missing",
+    "service_date_invalid": "Date \"{value}\" could not be read",
+    "service_title_required": "Service title or description is missing",
+    "invalid_date": "{field} \"{value}\" could not be read and was left out",
+    "invalid_number": "{field} \"{value}\" is not a number and was left out",
+    "duplicate_invoice_number": "Invoice number {value} already exists",
+    "write_failed": "Could not be written: {value}"
+  },
+  "errors": {
+    "uploadFailed": "The file could not be read",
+    "aiFailed": "AI could not suggest a mapping",
+    "dryRunFailed": "The dry run failed",
+    "importFailed": "The import failed",
+    "undoFailed": "Could not undo the import"
+  },
+  "entry": {
+    "title": "Import from a spreadsheet",
+    "description": "Bring customers, vehicles and service history from any system that exports CSV or Excel. Map the columns, run a dry run, then import. Every import can be undone.",
+    "customers": "Import customers",
+    "vehicles": "Import vehicles",
+    "services": "Import service history"
+  }
+}

+ 584 - 0
messages/en/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Integrations",
+    "description": "Connect the services your workshop already uses. Each integration is set up once and runs in the background.",
+    "all": "All",
+    "empty": "No integrations in this category yet.",
+    "notConnected": "Not connected",
+    "connect": "Connect",
+    "manage": "Manage",
+    "connectedTitle": "Connected",
+    "connectedDescription": "Services this workshop has set up. Open one to check its status, change settings or disconnect.",
+    "noneConnected": "Nothing connected yet. Pick a service below to get started.",
+    "availableTitle": "Available",
+    "availableDescription": "Services you can add. Each one is set up once and runs in the background.",
+    "allConnected": "Every available service is connected.",
+    "searchPlaceholder": "Search integrations",
+    "noMatches": "Nothing matches \"{query}\".",
+    "readMore": "Read more",
+    "suggestTitle": "Missing a service you use?",
+    "suggestDescription": "Tell us which integration would help your workshop and we will look into it.",
+    "suggest": "Suggest an integration",
+    "suggestSubject": "Integration suggestion"
+  },
+  "categories": {
+    "calendar": "Calendar",
+    "conferencing": "Video calls",
+    "accounting": "Accounting",
+    "messaging": "Messaging",
+    "payments": "Payments",
+    "automation": "Automation",
+    "storage": "Files",
+    "registry": "Vehicle registry",
+    "other": "Other",
+    "ai": "AI"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Work orders to calendar",
+      "pull": "Busy time from calendar",
+      "conference": "Video call links"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Plate and VIN lookup",
+      "inspection": "Inspection due dates",
+      "safety": "Recalls and owner complaints"
+    },
+    "ai": {
+      "chat": "Writing and summaries"
+    },
+    "payments": {
+      "checkout": "Pay from the invoice link"
+    },
+    "accounting": {
+      "invoices": "Invoices to the ledger",
+      "customers": "Customers to the ledger",
+      "payments": "Payments both ways"
+    }
+  },
+  "statuses": {
+    "active": "Connected",
+    "error": "Needs attention",
+    "pending": "Setup in progress",
+    "disconnected": "Disconnected",
+    "needsSetup": "Setup not finished"
+  },
+  "connection": {
+    "back": "All integrations",
+    "account": "Connected account",
+    "lastHealth": "Last checked",
+    "lastSync": "Last sync",
+    "lastError": "The last run failed",
+    "setupMissingTitle": "Setup not finished",
+    "setupMissing": "Choose {fields} in the settings below. Until then nothing is synced.",
+    "connected": "{name} is connected.",
+    "connectedShort": "Connected",
+    "connectWith": "Connect {name}",
+    "connect": "Connect",
+    "reconnect": "Reconnect",
+    "disconnect": "Disconnect",
+    "disconnectTitle": "Disconnect {name}?",
+    "disconnectDescription": "Torqvoice forgets the account and stops using it. Nothing already sent to or stored in {name} is touched.",
+    "disconnectDescriptionCalendar": "Torqvoice forgets the account and stops syncing. Work orders already put in {name} stay there. The busy time shown from that calendar is removed from the Torqvoice calendar.",
+    "disconnectDescriptionConferencing": "Torqvoice forgets the account and stops creating meetings. Meetings already created stay in {name}, and the links on work orders are kept.",
+    "disconnectDescriptionAccounting": "Torqvoice forgets the account and stops sending and reading. Invoices, customers and payments already in {name} stay there.",
+    "removeSetup": "Remove",
+    "removeSetupTitle": "Remove {name}?",
+    "removeSetupDescription": "Nothing is connected yet. Torqvoice forgets what was entered, and {name} goes back to the list of available services.",
+    "removeSetupDone": "{name} removed",
+    "removeSetupHint": "Not going ahead with this? Remove it and the service returns to the list below.",
+    "disconnected": "Disconnected",
+    "test": "Test connection",
+    "testPassed": "Connection works",
+    "syncNow": "Sync now",
+    "syncQueued": "Sync queued; it runs within a minute",
+    "pullNow": "Pull payments now",
+    "pullQueued": "Payment pull queued; it runs within a minute",
+    "backfill": "Push scheduled work orders",
+    "backfillQueued": "Queued the scheduled work orders from the last week onwards",
+    "backfillInvoices": "Push issued invoices",
+    "backfillInvoicesQueuedCount": "Queued {count, plural, one {# issued invoice} other {# issued invoices}} to push",
+    "backfillInvoicesNone": "No issued invoices to push. Only sent, shared or paid invoices go over, dated on or after the start date if you set one.",
+    "retryQueued": "Job queued again",
+    "settingsTitle": "Settings",
+    "settingsDescription": "What to sync and where. Changes apply to the next sync.",
+    "settingsSaved": "Settings saved",
+    "save": "Save",
+    "loading": "Loading...",
+    "choose": "Choose...",
+    "search": "Search...",
+    "noMatch": "Nothing matches",
+    "leaveEmpty": "Leave empty",
+    "noLimit": "No limit",
+    "planLocked": "Integrations are not included in your plan.",
+    "ownAppTitle": "This install uses your own app with the provider",
+    "redirectUri": "Redirect URI to register with the provider:",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Add this URL in your Stripe dashboard under Developers, Webhooks, subscribed to checkout.session.completed. Then paste the signing secret Stripe shows into the webhook secret field here, so notifications can be verified.",
+    "inboundHintVipps": "Enter this URL as the callback URL in the Vipps MobilePay portal. Vipps also requires published terms of sale, which are set under Settings, Payment.",
+    "inboundHintPaypal": "Add this URL in the PayPal developer dashboard under Webhooks, subscribed to PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "Inspection sync",
+      "vehicles": "{count} vehicles with a plate in this workshop",
+      "dueNow": "{count} would be looked up on the next run",
+      "withDate": "{count} already have an inspection date",
+      "lastPass": "Last complete pass {date}",
+      "never": "No complete pass yet",
+      "inProgress": "A pass is running",
+      "off": "Turn the setting on and save; until then a pass checks nothing.",
+      "syncNowHelp": "Sync now starts a pass straight away instead of waiting for the hourly one. It looks up 25 vehicles at a time and continues on its own until every due vehicle is done."
+    },
+    "supersedes": "Connecting {next} disconnects {current}. Only one at a time can be in charge, so {current} stops being used the moment {next} goes live.",
+    "batchRetried": "{count, plural, =0 {Nothing to retry} one {# job queued again} other {# jobs queued again}}"
+  },
+  "activity": {
+    "title": "Activity",
+    "description": "Recent jobs and log lines for this connection.",
+    "retry": "Retry",
+    "jobStatus": {
+      "queued": "Queued",
+      "running": "Running",
+      "done": "Done",
+      "failed": "Failed",
+      "dead": "Gave up"
+    },
+    "jobs": "{count, plural, one {# job} other {# jobs}}",
+    "counts": {
+      "queued": "{count} queued",
+      "running": "{count} running",
+      "done": "{count} done",
+      "failed": "{count} failed",
+      "dead": "{count} gave up"
+    },
+    "noFailures": "Nothing failed.",
+    "retryFailed": "Retry failed jobs",
+    "details": "Show details"
+  },
+  "errors": {
+    "generic": "Something went wrong",
+    "demo": "Integrations cannot be connected on the demo.",
+    "forbidden": "Only an administrator can connect integrations.",
+    "unknown": "Unknown integration.",
+    "plan": "This integration is not included in your plan.",
+    "no-client": "No app credentials are configured for this provider.",
+    "denied": "You cancelled the sign-in with the provider.",
+    "vendor": "The provider returned an error. Try again.",
+    "invalid": "The provider's response was incomplete. Try again.",
+    "state": "This sign-in has expired or belongs to another session. Start again.",
+    "exchange": "The provider refused the sign-in. Check the app credentials and try again.",
+    "identify": "Connected, but the account could not be read. Try reconnecting."
+  },
+  "meeting": {
+    "join": "Join video call",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "From connected calendars",
+    "busy": "Busy"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Put scheduled work orders on a Google Calendar, add a Google Meet link, and see the calendar's busy time in Torqvoice.",
+      "tenantHelp": "Create an OAuth client in Google Cloud Console (APIs & Services → Credentials), enable the Google Calendar API, add the redirect URI below, and paste the client id and secret here.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "calendarId": "Calendar",
+        "pushEnabled": "Add scheduled work orders to this calendar",
+        "includeCustomer": "Include the customer's name and phone in the event",
+        "addConference": "Add a Google Meet meeting to every scheduled work order",
+        "addConferenceHelp": "On: every work order pushed to the calendar gets a Google Meet meeting, and the join link shows on the work order. Off: add a Google Meet meeting to a single work order from its page. Either way the customer only gets the link when you send it.",
+        "pullEnabled": "Show this calendar's busy time on the Torqvoice calendar",
+        "pullEnabledHelp": "Read-only. Refreshed every 15 minutes, one week back and two months ahead."
+      }
+    },
+    "microsoft-365": {
+      "description": "Put scheduled work orders on an Outlook calendar, add a Microsoft Teams link, and see the calendar's busy time in Torqvoice.",
+      "tenantHelp": "Register an application in Microsoft Entra (App registrations), allow personal and work accounts, add the redirect URI below as a Web platform, create a client secret, and paste both here.",
+      "fields": {
+        "clientId": "Application (client) ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "calendarId": "Calendar",
+        "pushEnabled": "Add scheduled work orders to this calendar",
+        "includeCustomer": "Include the customer's name and phone in the event",
+        "addConference": "Add a Microsoft Teams meeting to every scheduled work order",
+        "addConferenceHelp": "On: every work order pushed to the calendar gets a Microsoft Teams meeting, and the join link shows on the work order. Off: add a Microsoft Teams meeting to a single work order from its page. Either way the customer only gets the link when you send it.",
+        "pullEnabled": "Show this calendar's busy time on the Torqvoice calendar",
+        "pullEnabledHelp": "Read-only. Refreshed every 15 minutes, one week back and two months ahead."
+      }
+    },
+    "zoom": {
+      "description": "Add a Zoom meeting to a scheduled work order and keep the join link on it, for remote diagnosis or walking a customer through findings.",
+      "tenantHelp": "In the Zoom App Marketplace, build a General app (user-managed), add the redirect URI below, add the scopes meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting and user:read:user, and paste the client ID and secret here.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "autoCreate": "Create a Zoom meeting for every scheduled work order automatically",
+        "autoCreateHelp": "Off: you add a video call to a work order yourself, from its page. On: every work order with a start time gets a meeting, moved when it is rescheduled and removed when it is cancelled. Either way the customer only gets the link when you send it.",
+        "includeCustomer": "Include the customer's name in the meeting topic",
+        "joinBeforeHost": "Let the customer join before you",
+        "waitingRoom": "Use a waiting room"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Fill in a vehicle from its Norwegian plate or VIN: make, model, year, fuel, gearbox, engine, colour, tyre sizes and the next EU-kontroll date, straight from Statens vegvesen's register. No owner data is returned.",
+      "fields": {
+        "apiKey": "API key",
+        "apiKeyHelp": "Free. Order the key on vegvesen.no under \"API for tekniske kjøretøyopplysninger\": sign in with BankID, represent the workshop, and the key is issued at once. It allows 50,000 lookups a day."
+      },
+      "settings": {
+        "syncInspections": "Keep inspection dates up to date for every vehicle in this workshop",
+        "syncInspectionsHelp": "Once an hour, vehicles that have not been checked recently are looked up by plate and their next EU-kontroll date is saved. Only vehicles stored in this workshop are ever sent. A vehicle is rechecked monthly, and weekly once its deadline is within two months."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Fill in a vehicle from its French, Italian, Spanish, Portuguese or British plate: make, model, year, VIN, fuel, engine size and more, from the country's registration data through Openapi.com. Each lookup is billed to the workshop's own Openapi account. No owner data is returned.",
+      "fields": {
+        "token": "API token",
+        "tokenHelp": "Create an account on console.openapi.com, add credit to the wallet, and under Automotive create a token for the production environment. Each successful lookup costs a few tens of cents; a miss is free."
+      },
+      "settings": {
+        "country": "Country",
+        "countryHelp": "The registry to search. French and Italian plates look the same, so the country cannot be read off the plate.",
+        "bikes": "Also look up motorcycles",
+        "bikesHelp": "When a plate is not a car, ask the motorcycle registry too. A miss on the car side is free, so this costs nothing extra unless a motorcycle is found. Not available for Portugal."
+      }
+    },
+    "rdw": {
+      "description": "Fill in a vehicle from its Dutch plate: make, model, year, fuel, engine size, colour, weights and the next APK date, straight from RDW's open vehicle register. Free, no key needed. No owner data and no VIN are published.",
+      "fields": {
+        "appToken": "App token (optional)",
+        "appTokenHelp": "RDW's open data works without a key, but anonymous requests share a throttled pool. For the inspection sync, create a free account on opendata.rdw.nl, open your profile's developer settings and create an app token."
+      },
+      "settings": {
+        "syncInspections": "Keep APK dates up to date for every vehicle in this workshop",
+        "syncInspectionsHelp": "Once an hour, vehicles that have not been checked recently are looked up by plate and their next APK date is saved. Only vehicles stored in this workshop are ever sent. A vehicle is rechecked monthly, and weekly once its deadline is within two months."
+      }
+    },
+    "regcheck": {
+      "description": "Fill in a vehicle from its plate in Australia, New Zealand, the United States, Ireland, Norway, Sweden, Denmark, Finland, Estonia, Czechia, Slovakia, Hungary or Croatia: make, model, year, VIN, fuel and engine size where the country publishes them, through RegCheck. Each successful lookup spends one credit from the workshop's own RegCheck account. No owner data is returned.",
+      "fields": {
+        "username": "RegCheck username",
+        "usernameHelp": "Register on regcheck.org.uk; the account comes with 10 free lookups and more are bought in blocks of 100 through the dashboard. Only the username is needed here, never the password. A plate that is not found costs nothing."
+      },
+      "settings": {
+        "country": "Country",
+        "countryHelp": "The registry to search. Each country has its own service, so the connection searches one.",
+        "auState": "State or territory",
+        "auStateHelp": "Australia registers vehicles per state, and the registry must be told which one to search. Choose where most of your customers' vehicles are registered.",
+        "usState": "State",
+        "usStateHelp": "The two-letter code of the state the vehicles are registered in, such as CA or TX. American registries are searched per state."
+      }
+    },
+    "openai": {
+      "description": "Service descriptions, history summaries, document scanning and the assistant, answered by your own OpenAI account. Every call is billed to that account, and you choose which model answers.",
+      "fields": {
+        "apiKey": "API key",
+        "apiKeyHelp": "Create one at platform.openai.com under API keys. The account needs credit on it; OpenAI bills the usage, per call."
+      },
+      "settings": {
+        "model": "Model",
+        "modelHelp": "Which model answers. The list is read from your account, newest first. Smaller models cost less per call and are enough for most workshop text."
+      }
+    },
+    "anthropic": {
+      "description": "Service descriptions, history summaries, document scanning and the assistant, answered by Claude on your own Anthropic account. Every call is billed to that account, and you choose which model answers.",
+      "fields": {
+        "apiKey": "API key",
+        "apiKeyHelp": "Create one at console.anthropic.com under API keys. The account needs credit on it; Anthropic bills the usage, per call."
+      },
+      "settings": {
+        "model": "Model",
+        "modelHelp": "Which model answers. The list is read from your account, newest first. Smaller models cost less per call and are enough for most workshop text."
+      }
+    },
+    "stripe": {
+      "description": "Card payments from the invoice link, into your own Stripe account. The customer pays, Stripe tells Torqvoice, and the invoice marks itself paid.",
+      "fields": {
+        "secretKey": "Secret key",
+        "secretKeyHelp": "From the Stripe dashboard under Developers, API keys. Test keys process nothing real, so try a payment on them first and swap to live keys after.",
+        "publishableKey": "Publishable key",
+        "webhookSecret": "Webhook signing secret",
+        "webhookSecretHelp": "Shown by Stripe once you add the notification URL from this page as a webhook. With it, every notification is verified before a payment is recorded."
+      },
+      "settings": {
+        "enabled": "Offer on invoice links",
+        "enabledHelp": "Switch off to take the pay button off shared invoices for a while, without disconnecting. Payments already started are still recorded."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay from the invoice link. The customer pays in the app they already have, and the invoice marks itself paid.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret",
+        "subscriptionKey": "Subscription key",
+        "subscriptionKeyHelp": "The Ocp-Apim-Subscription-Key from the Vipps MobilePay portal, under the sales unit's API keys.",
+        "merchantSerialNumber": "Merchant serial number"
+      },
+      "settings": {
+        "enabled": "Offer on invoice links",
+        "enabledHelp": "Switch off to take the pay button off shared invoices for a while, without disconnecting. Payments already started are still recorded.",
+        "testMode": "Test mode",
+        "testModeHelp": "Use the Vipps test environment with test keys. Leave it on until a full payment has gone through, then turn it off and enter the live keys."
+      }
+    },
+    "paypal": {
+      "description": "PayPal checkout from the invoice link, through your own PayPal app. The customer pays with a PayPal account or a card, and the invoice marks itself paid.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "enabled": "Offer on invoice links",
+        "enabledHelp": "Switch off to take the pay button off shared invoices for a while, without disconnecting. Payments already started are still recorded.",
+        "sandbox": "Sandbox mode",
+        "sandboxHelp": "Use PayPal's sandbox with sandbox app keys. Leave it on until a full payment has gone through, then turn it off and enter the live app's keys."
+      }
+    },
+    "quickbooks": {
+      "description": "Send issued invoices, their customers and their payments to QuickBooks Online, and bring back payments recorded there.",
+      "tenantHelp": "Create an app in the Intuit Developer portal. Copy the client id and secret from Keys and credentials, and enter the redirect URI below under Settings, Redirect URIs. Development keys reach sandbox companies, production keys live ones.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "pushInvoices": "Send issued invoices to QuickBooks",
+        "pushOnComplete": "Also send invoices when the job is marked completed",
+        "pushOnCompleteHelp": "Without this an invoice goes over once it is sent, shared or paid. With it, a completed job goes over straight away and is updated if it changes.",
+        "startDate": "Only invoices dated on or after",
+        "startDateHelp": "Leave empty to send every issued invoice. Use the date your books moved to QuickBooks so older invoices are not entered twice.",
+        "includeVehicle": "Name the vehicle on the invoice",
+        "includeVehicleHelp": "A text line above the charges with the year, make, model, plate and mileage, so the books say which car.",
+        "laborItemId": "Item for labour lines",
+        "laborItemIdHelp": "Leave empty and an item named Labour is created under your income account the first time.",
+        "partsItemId": "Item for parts lines",
+        "partsItemIdHelp": "Leave empty and an item named Parts is created under your income account the first time.",
+        "taxAmounts": "Tax amounts",
+        "taxAmountsHelp": "As billed sends the tax exactly as it stands on the invoice the customer received, so both systems agree to the cent. Worked out by QuickBooks lets QuickBooks apply the rate of the tax code, or the customer's address in the US, and the log says when the totals differ.",
+        "taxAmountsBilled": "As billed in Torqvoice",
+        "taxAmountsQuickbooks": "Worked out by QuickBooks",
+        "taxCodeId": "Tax code for taxable invoices",
+        "taxCodeIdHelp": "QuickBooks books the tax under a tax code. Torqvoice picks the code whose rate matches each invoice; this one is used when none does. A US company uses TAX unless you choose otherwise.",
+        "zeroTaxCodeId": "Tax code for tax-free invoices",
+        "zeroTaxCodeIdHelp": "Used when the invoice has no tax or the customer is tax exempt. A US company uses NON unless you choose otherwise.",
+        "pushPayments": "Record payments in QuickBooks",
+        "depositAccountId": "Deposit payments to",
+        "depositAccountIdHelp": "Leave empty to use Undeposited Funds.",
+        "manualPaidAsPayment": "Record a payment when an invoice is marked paid by hand",
+        "manualPaidAsPaymentHelp": "An invoice marked as paid in Torqvoice without a payment entry gets one payment in QuickBooks for whatever it still shows as owed. Off, it stays open there until a payment is recorded.",
+        "pullPayments": "Bring back payments recorded in QuickBooks",
+        "pullPaymentsHelp": "Every 30 minutes. A payment received in QuickBooks against an invoice from Torqvoice is recorded on the invoice here."
+      }
+    },
+    "nhtsa": {
+      "description": "Fill in a vehicle from its VIN, and see recalls, owner complaints and crash ratings for every model year, from the US safety authority. Free, no key.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Keep recall and complaint data fresh for every vehicle in this workshop",
+        "refreshFleetHelp": "Once a day, models not refreshed in the last week are fetched again, so the vehicle page never waits and a new recall shows within a week of being filed. Only make, model and year are sent."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Work orders to calendar",
+      "pull": "Busy time from calendar"
+    },
+    "conference": {
+      "sync": "Video call links"
+    },
+    "inspection": {
+      "refresh": "Inspection date refresh"
+    },
+    "accounting": {
+      "invoice": "Invoice to ledger",
+      "payment": "Payment to ledger",
+      "customer": "Customer to ledger",
+      "pull": "Payments from ledger"
+    },
+    "safety": {
+      "refresh": "Recall data refresh",
+      "vehicle": "Recall check for a new vehicle"
+    }
+  }
+}

+ 2 - 1
messages/en/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Scheduled",
       "sms": "SMS",
       "whatsapp": "WhatsApp",
-      "telegram": "Telegram"
+      "telegram": "Telegram",
+      "campaigns": "Campaigns"
     },
     "smsNotConfiguredTitle": "SMS is not set up",
     "smsNotConfiguredHint": "Connect an SMS provider to send and receive texts here. Scheduled email still works without it.",

+ 5 - 1
messages/en/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "Could not delete the conversation",
     "delete": "Delete",
     "cancel": "Cancel",
-    "loadMore": "Load more"
+    "loadMore": "Load more",
+    "unread": "{count} unread",
+    "markUnread": "Mark as unread",
+    "nothingToMarkUnread": "The customer has not written in this conversation yet.",
+    "markUnreadFailed": "Could not mark the conversation as unread."
   }
 }

+ 16 - 2
messages/en/navigation.json

@@ -59,7 +59,8 @@
     "tireStorage": "Storage Layout",
     "tireSetDetails": "Tire Set",
     "tireForecast": "Next season",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "inspectionReminders": "Inspection reminders"
   },
   "search": "Search...",
   "shortcut": "Ctrl+K",
@@ -117,6 +118,19 @@
     "auditLog": "Audit Log",
     "aiAssistant": "AI Assistant",
     "tireHotel": "Tire hotel",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "newWorkOrder": "New work order",
+    "installApp": "Install app",
+    "roles": {
+      "owner": "Owner",
+      "admin": "Admin",
+      "member": "Member"
+    },
+    "badges": {
+      "messages": "{count} unread messages",
+      "workOrders": "{count} open work orders",
+      "inspections": "{count} inspections in progress",
+      "reminders": "{count} reminders due"
+    }
   }
 }

+ 3 - 1
messages/en/onboarding.json

@@ -76,7 +76,9 @@
     "allDoneTitle": "You are all set",
     "allDoneDescription": "All steps are done and your workshop is up and running.",
     "dismiss": "Hide card",
-    "dismissAriaLabel": "Hide this checklist"
+    "dismissAriaLabel": "Hide this checklist",
+    "importTitle": "Already have customers and vehicles somewhere?",
+    "importDescription": "Import them from a spreadsheet or your old system"
   },
   "sampleData": {
     "title": "Sample data",

+ 51 - 1
messages/en/portal.json

@@ -158,7 +158,14 @@
     "columnVehicle": "Vehicle",
     "columnPlate": "Plate",
     "columnServices": "Services",
-    "columnInspections": "Inspections"
+    "columnInspections": "Inspections",
+    "reportSold": "I no longer own this vehicle",
+    "reportSoldHint": "We will tell the workshop so they stop sending reminders about it. Your history stays as it is.",
+    "reportSoldConfirm": "Yes, tell the workshop",
+    "reportSoldCancel": "Keep it",
+    "reportSoldDone": "Thanks, the workshop has been told",
+    "reportSoldFailed": "Could not send that right now",
+    "reportSoldAlready": "You have told the workshop this vehicle is no longer yours."
   },
   "requestService": {
     "title": "Request Service",
@@ -190,5 +197,48 @@
     "modelPlaceholder": "e.g. Kuga",
     "yearLabel": "Year",
     "plateLabel": "License plate"
+  },
+  "booking": {
+    "title": "Book your inspection",
+    "due": "Inspection due {date}",
+    "firstSlot": "First available time",
+    "bookThis": "Book {when}",
+    "pickAnother": "Or pick another day",
+    "pickTime": "Pick a time",
+    "pickTimeFirst": "Pick a time above",
+    "confirmFor": "Confirm {when}",
+    "notePlaceholder": "Anything we should know? (optional)",
+    "legend": "Struck-through days are full; amber days have few times left.",
+    "day": {
+      "open": "Times available",
+      "limited": "Few times left",
+      "full": "Fully booked",
+      "closed": "Closed"
+    },
+    "booked": "Booked for {when}",
+    "bookedHint": "We have sent a confirmation. Come a few minutes early with your keys.",
+    "pending": "Requested for {when}",
+    "pendingHint": "The workshop will confirm your time shortly.",
+    "changeOrCancel": "Change or cancel",
+    "expired": "This link has expired",
+    "expiredHint": "Call us and we will find a time together.",
+    "unavailable": "Online booking is not available right now",
+    "unavailableHint": "Call us and we will find a time.",
+    "callUs": "Call us on {phone}",
+    "modeDirectNote": "Your time is reserved immediately for {minutes} minutes.",
+    "modeRequestNote": "The workshop confirms your requested time.",
+    "bookedToast": "Your time is booked",
+    "bookFailed": "Could not book that time",
+    "cancelledToast": "Booking cancelled",
+    "cancelFailed": "Could not cancel",
+    "footer": "You received this link because your workshop reminded you about the inspection.",
+    "workOrderTitle": "Periodic inspection {vehicle}",
+    "notificationTitle": "New online booking",
+    "notificationBody": "{customer} booked {vehicle} for {when}",
+    "cancelledNotificationTitle": "Online booking cancelled",
+    "cancelledNotificationBody": "{customer} cancelled the booking for {vehicle} on {when}",
+    "confirmationSubject": "Your booking at {workshop}",
+    "confirmationBody": "Your {vehicle} is booked for {when} at {workshop}. Need to change it? Use the link in our previous message or call {phone}.",
+    "confirmationBodyRequest": "Thanks, {workshop} has received your request for {when} for your {vehicle} and will confirm shortly. Questions? Call {phone}."
   }
 }

+ 1 - 0
messages/en/scheduledMessages.json

@@ -41,6 +41,7 @@
     "scheduled": "Message scheduled",
     "updated": "Scheduled message updated",
     "saveError": "Could not save the message",
+    "inPast": "That time has already passed. Pick a time in the future.",
     "whatsappNeedsTemplate": "This will be sent long after the customer's 24 hour window closes, and no WhatsApp text template is set up, so it would fail at send time. Set one up in Settings > Providers > WhatsApp."
   },
   "list": {

+ 59 - 2
messages/en/service.json

@@ -34,7 +34,8 @@
     "delete": "Delete",
     "unpaid": "Unpaid",
     "unsavedChanges": "Unsaved changes",
-    "saved": "Saved"
+    "saved": "Saved",
+    "joinCall": "Join video call"
   },
   "basicInfo": {
     "title": "Basic Information",
@@ -78,7 +79,17 @@
       "typePrompt": "Type a name...",
       "boardTechnicians": "Board Technicians",
       "teamMembers": "Team Members"
-    }
+    },
+    "design": "Invoice design",
+    "designDefault": "Workshop default",
+    "designFollowing": "Default ({name})",
+    "designPinned": "The sheet was frozen when the invoice was issued on {date}. A new choice applies only after an owner unlocks the invoice and it is sent again.",
+    "designSaved": "Invoice design updated",
+    "designFollowingRule": "Default ({name}, {rule})",
+    "designRule": {
+      "parts_sale": "Parts-Only Sale"
+    },
+    "designSaveFailed": "Could not change the invoice design"
   },
   "parts": {
     "title": "Parts",
@@ -349,6 +360,7 @@
     "workBay": "Work bay",
     "selectWorkBay": "Select work bay...",
     "noWorkBay": "No work bay",
+    "noTechnician": "No technician",
     "technicians": "Technicians",
     "otherTeamMembers": "Other team members",
     "makeTechnician": "Makes them a technician",
@@ -410,5 +422,50 @@
     "remove": "Remove this concern",
     "notAnswered": "Nothing found against this yet",
     "answered": "{count} finding(s) against this"
+  },
+  "videoCall": {
+    "title": "Video call",
+    "none": "No video call on this work order.",
+    "needsSchedule": "Set a start time first. The meeting follows the schedule.",
+    "add": "Add {provider} meeting",
+    "addedHere": "added from this work order",
+    "fromCalendar": "from the calendar event",
+    "join": "Join",
+    "copy": "Copy link",
+    "copied": "Link copied",
+    "remove": "Remove",
+    "removeTitle": "Remove the video call?",
+    "removeDescription": "The meeting is deleted at the provider. Anyone who already has the link can no longer join.",
+    "created": "Meeting created. The link is on the work order.",
+    "removed": "Meeting removed",
+    "failed": "Something went wrong",
+    "shareHint": "The customer only gets this link when you send it.",
+    "shareHintNoChannel": "The customer only gets this link when you send it. No messaging channel is set up, so copy it into a message yourself.",
+    "send": {
+      "button": "Send to customer",
+      "title": "Send the video call link",
+      "description": "Send the join link to {name}.",
+      "messageLabel": "Note (optional)",
+      "messagePlaceholder": "Anything the customer should know before the call...",
+      "linkAlwaysIncluded": "The join link is always added below your note.",
+      "sms": "SMS",
+      "email": "Email",
+      "telegram": "Telegram",
+      "noPhone": "(no phone)",
+      "noEmail": "(no email)",
+      "noTelegram": "(no chat linked)",
+      "notAvailable": "(not set up)",
+      "cancel": "Cancel",
+      "send": "Send",
+      "sent": "Video call link sent",
+      "partlySent": "Sent by {sent}; not by {failed}",
+      "failed": "Could not send the link"
+    },
+    "notify": {
+      "invitation": "Hi {name}, your video call about the {vehicle} is on {when}. Join here: {url}",
+      "invitationNoTime": "Hi {name}, here is the link for the video call about the {vehicle}: {url}",
+      "joinLine": "Join the video call here: {url}",
+      "emailSubject": "Video call about your {vehicle}"
+    }
   }
 }

+ 101 - 27
messages/en/settings.json

@@ -7,7 +7,7 @@
       "billing": "Billing & Documents",
       "communications": "Communications",
       "workshop": "Workshop",
-      "integrations": "Integrations",
+      "integrations": "Apps & API",
       "system": "System"
     },
     "items": {
@@ -91,6 +91,10 @@
         "title": "Report Schedule",
         "description": "Automated report delivery"
       },
+      "integrations": {
+        "title": "Integrations",
+        "description": "Calendars, video calls and connected services"
+      },
       "webhooks": {
         "title": "Webhooks",
         "description": "Real-time event notifications"
@@ -130,6 +134,10 @@
       "providers": {
         "title": "Providers",
         "description": "Email, SMS, WhatsApp & Telegram"
+      },
+      "inspectionReminders": {
+        "title": "Inspection reminders",
+        "description": "Booking link, duration and lead time"
       }
     },
     "pro": "PRO"
@@ -572,7 +580,16 @@
     "quoteLockTriggerSent": "Once the quote is sent",
     "quoteLockTriggerAccepted": "Once the quote is accepted",
     "quoteLockTriggerSentHint": "Locks as soon as the quote goes to the customer. Choose this only if you never revise a quote during negotiation.",
-    "quoteLockTriggerAcceptedHint": "Locks once the customer has accepted the quote, or it has been converted to a job. Sent quotes stay editable."
+    "quoteLockTriggerAcceptedHint": "Locks once the customer has accepted the quote, or it has been converted to a job. Sent quotes stay editable.",
+    "freezeTitle": "Invoices from before locking",
+    "freezeBody": "{count, plural, one {# invoice} other {# invoices}} reached a customer before this version could lock what an invoice prints. They still print from your current details, logo and design, so a change you make today reaches them too. Locking them keeps each one as it prints right now.",
+    "freezeNone": "Every invoice that reached a customer is locked as it was sent.",
+    "freezeButton": "Lock them with the current design",
+    "freezeConfirmTitle": "Lock {count, plural, one {# invoice} other {# invoices}}?",
+    "freezeConfirmBody": "Each one keeps the design, logo, workshop details, customer, vehicle and findings it prints with right now. This cannot be undone, and the invoices stay editable or locked for editing exactly as they are today.",
+    "freezeProgress": "Locked {done} of {total}",
+    "freezeDone": "{count, plural, one {# invoice} other {# invoices}} locked",
+    "freezeFailed": "Could not lock the invoices"
   },
   "layoutEditor": {
     "helpText": "Drag to reorder. Use the width toggle to place sections side-by-side. Consecutive half-width sections (L/R) will render in two columns.",
@@ -634,6 +651,12 @@
       "bank_account": "Bank Account",
       "footer_note": "Footer Note",
       "org_number": "Organization Number",
+      "payment_terms": "Payment Terms",
+      "due_date": "Due Date",
+      "title": "Title",
+      "invoice_number": "Invoice No.",
+      "customer_number": "Customer No.",
+      "date": "Date",
       "portal_link": "Portal Link"
     },
     "presetsTitle": "Start from an arrangement",
@@ -808,32 +831,17 @@
     "paymentTermsPlaceholder": "Net 14 days",
     "paymentTermsHint": "Payment terms text displayed on invoices (e.g. \"Net 14 days\", \"Due on receipt\")",
     "onlinePayments": "Online Payments",
-    "onlinePaymentsDescription": "Allow customers to pay invoices online via shared invoice links.",
-    "stripe": "Stripe",
-    "stripeDescription": "Accept credit/debit card payments",
-    "secretKey": "Secret Key",
-    "publishableKey": "Publishable Key",
-    "webhookSecret": "Webhook Secret",
-    "webhookUrl": "Webhook URL",
-    "stripeWebhookHint": "Add this URL in your Stripe Dashboard under Webhooks. Subscribe to <code>checkout.session.completed</code>.",
-    "vipps": "Vipps",
-    "vippsDescription": "Accept Vipps mobile payments (Norway)",
-    "clientId": "Client ID",
-    "clientSecret": "Client Secret",
-    "subscriptionKey": "Subscription Key",
-    "merchantSerialNumber": "Merchant Serial Number",
-    "testMode": "Test Mode",
-    "callbackUrl": "Callback URL",
-    "termsOfSale": "Terms of Sale (Salgsvilkar)",
-    "termsOfSaleDescription": "Required by Vipps. Customers can view these from the invoice page.",
+    "onlinePaymentsDescription": "Let customers pay invoices from the link you send them. Stripe, Vipps and PayPal are connected under Integrations, and can run side by side.",
+    "onlinePaymentsHint": "When a customer pays, the vendor notifies Torqvoice and the invoice marks itself paid. Each vendor's page shows the notification URL it needs.",
+    "offered": "On invoice links",
+    "paused": "Connected, not offered",
+    "termsOfSale": "Terms of sale",
+    "termsOfSaleDescription": "What a customer agrees to when paying online. Vipps requires published terms; the invoice links to them either way.",
+    "termsOfSaleText": "Terms text",
     "externalTermsUrl": "External Terms URL (optional)",
     "externalTermsUrlHint": "Link to an external page with your terms. If set, the invoice will link here instead of the built-in terms page.",
     "publicTermsUrl": "Public Terms URL",
     "publicTermsUrlHint": "Share this URL with Vipps or link it from your website.",
-    "paypal": "PayPal",
-    "paypalDescription": "Accept PayPal payments",
-    "sandboxMode": "Sandbox Mode",
-    "paypalWebhookHint": "Add this URL in your PayPal Developer Dashboard under Webhooks. Subscribe to <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "savePayment": "Save Payment Settings",
     "saved": "Payment settings saved"
   },
@@ -1112,7 +1120,7 @@
   },
   "data": {
     "title": "Data Management",
-    "description": "Export your data as a backup or import a previously exported file.",
+    "description": "Bring data in from your old system, download a backup of everything, or restore one.",
     "exportTitle": "Export Data",
     "exportDescription": "Download your data as a zip file. Select which categories to include in the backup.",
     "includeInBackup": "Include in backup:",
@@ -1199,7 +1207,16 @@
     "optTireHotel": "Tire hotel",
     "optTireHotelDesc": "Stored tire sets, shelves, prep work and storage agreements",
     "optWorkshopConfig": "Workshop setup",
-    "optWorkshopConfigDesc": "Labour presets, roles, webhooks and scheduled reports"
+    "optWorkshopConfigDesc": "Labour presets, roles, webhooks and scheduled reports",
+    "importHubDescription": "Pick where your data is today. Spreadsheet and app imports add to what you have and can be undone; restoring a backup replaces everything.",
+    "sourceSpreadsheet": "Spreadsheet or CSV",
+    "sourceSpreadsheetDesc": "Exports from any shop system, accounting tool, Google Contacts, Outlook or Excel. You confirm the column mapping and see a dry run before anything is written.",
+    "sourceLubelogDesc": "Vehicles, service records and notes from a LubeLog backup.",
+    "sourceInvoiceNinjaDesc": "Clients, products, invoices and payments from an Invoice Ninja export.",
+    "sourceBackup": "Torqvoice backup",
+    "sourceBackupDesc": "A .zip or .json file downloaded from this page. Restoring replaces everything in this workshop.",
+    "restore": "Restore",
+    "restoreTitle": "Restore a backup"
   },
   "license": {
     "title": "White-Label License",
@@ -1740,6 +1757,17 @@
         "feature": "Telegram Messaging",
         "description": "Send and receive Telegram messages with your customers directly from Torqvoice."
       }
+    },
+    "moved": {
+      "title": "These settings moved to Integrations",
+      "description": "Email, SMS, WhatsApp and Telegram are set up in Integrations now, alongside calendars and everything else you connect. What you had set up came with them, so there is nothing to enter again.",
+      "connectedTo": "Connected through {vendor}",
+      "notSetUp": "Not set up yet",
+      "manage": "Manage",
+      "notConnected": "Not connected",
+      "goToIntegrations": "Go to Integrations",
+      "platformMail": "Sending through the platform mail server. Connect your own to send from your address.",
+      "onByDefault": "On by default"
     }
   },
   "designer": {
@@ -1749,6 +1777,14 @@
     "galleryTitle": "Start from a template",
     "galleryHint": "Pick a starting point. You can change everything in the designer.",
     "yourDesigns": "Your designs",
+    "autoRuleHint": "Sales made without a vehicle print with the design marked for them, unless the sale or its customer chose another.",
+    "useForPartsSale": "Use for parts-only sales",
+    "usedForPartsSale": "Used for parts-only sales",
+    "autoRule": {
+      "parts_sale": "Parts-Only Sale"
+    },
+    "autoRuleSaved": "Updated when \"{name}\" is used",
+    "couldNotSaveRule": "Could not update the rule",
     "templates": "Templates",
     "inUse": "In use",
     "savedOn": "Saved {date}",
@@ -1908,6 +1944,7 @@
       "serviceType": "Repair",
       "companyName": "Your Workshop",
       "slogan": "Quality service you can trust",
+      "paymentTerms": "Due within 14 days",
       "footerNote": "Thank you for your business",
       "value": "Sample"
     },
@@ -1923,11 +1960,22 @@
     "sloganPlaceholderHint": "No slogan is set, so the sheet shows a stand-in that will not print.",
     "sloganSetHint": "Your slogan comes from your company details.",
     "sloganLink": "Company settings",
+    "paymentTerms": "Payment terms",
+    "paymentTermsPlaceholderHint": "No payment terms are set, so the sheet shows a stand-in that will not print.",
+    "paymentTermsSetHint": "Your payment terms come from your payment settings.",
+    "paymentTermsLink": "Payment settings",
+    "titleText": "Title text",
+    "titleTextHint": "What this document calls itself. Left empty, it prints its own name in the reader's language.",
     "placeholder": "Placeholder",
     "leaveTitle": "Leave without saving?",
     "leaveBody": "Your changes to this design have not been saved. They will be lost.",
     "leaveConfirm": "Leave",
-    "boldField": "Bold"
+    "boldField": "Bold",
+    "deleteDesignDrafts": "{count, plural, one {# draft invoice} other {# draft invoices}} using it will go back to the default design.",
+    "deleteDesignCustomers": "{count, plural, one {# customer} other {# customers}} set to it will go back to the default design.",
+    "setDefault": "Set as default",
+    "defaultSet": "\"{name}\" is now in use",
+    "couldNotSetDefault": "Could not set the default design"
   },
   "preview": {
     "title": "Brake Service & Oil Change",
@@ -1942,5 +1990,31 @@
     "partSyntheticOil": "Synthetic Oil 5W-30",
     "laborBrakeReplacement": "Brake Replacement",
     "laborOilChange": "Oil Change"
+  },
+  "inspectionReminders": {
+    "title": "Inspection reminders and booking",
+    "description": "How the booking link in a reminder behaves. Set once; every campaign uses these.",
+    "duration": "Inspection duration (minutes)",
+    "durationHint": "How long a periodic inspection occupies a bay. The link books slots of this length.",
+    "leadDays": "Earliest booking (days ahead)",
+    "leadDaysHint": "1 means the first time offered is tomorrow, never today.",
+    "horizonWeeks": "How far ahead (weeks)",
+    "horizonHint": "Customers can pick any free time inside this many weeks.",
+    "reserve": "Keep free for walk-ins",
+    "reserveHint": "Bays or technicians the link never fills. 0 lets it use every free one.",
+    "linkValidDays": "Link stays open (days)",
+    "linkValidHint": "At least {min}. Links also stay open until a week after the deadline, whichever is later.",
+    "bookingMode": "What a booking creates",
+    "bookingModeDirect": "A scheduled work order on the board",
+    "bookingModeRequest": "A request the office confirms first",
+    "bookingModeHint": "Direct booking is what makes the link worth clicking. Choose the request if you want to approve every time.",
+    "phone": "Phone number in messages",
+    "phoneHint": "Shown as the alternative to the link. Defaults to the workshop phone number.",
+    "save": "Save",
+    "saved": "Settings saved",
+    "saveFailed": "Could not save",
+    "timeZoneDetected": "Timezone is on automatic under Localization, so the server uses {zone}, the zone last seen in a browser that saved settings. Choose the workshop's zone explicitly so booking times never depend on who saved last.",
+    "timeZoneExplicit": "Booking times and opening hours are read in {zone}.",
+    "timeZoneChange": "Change under Localization"
   }
 }

+ 217 - 3
messages/en/vehicles.json

@@ -4,6 +4,7 @@
     "archived": "Archived",
     "searchPlaceholder": "Search by plate, make, model, VIN...",
     "addVehicle": "Add Vehicle",
+    "importVehicles": "Import",
     "emptySearch": "No vehicles match your search.",
     "emptyArchived": "No archived vehicles.",
     "empty": "No vehicles yet.",
@@ -30,7 +31,12 @@
     "viewTable": "Table view",
     "viewGrid": "Grid view",
     "viewLargeGrid": "Large grid view",
-    "openMenu": "Open menu"
+    "openMenu": "Open menu",
+    "inspectionOverdue": "Inspection overdue",
+    "inspectionDue30": "Due in 30 days",
+    "inspectionDue90": "Due in 90 days",
+    "inspectionDue": "Inspection {date}",
+    "remindCustomers": "Remind these customers"
   },
   "detail": {
     "vehicleArchived": "This vehicle is archived",
@@ -90,7 +96,11 @@
     "severityUncommon": "Uncommon — reported by few owners",
     "severityRare": "Rare — isolated reports but critical when it occurs",
     "openMenu": "Open menu",
-    "dismissMaintenance": "Dismiss"
+    "dismissMaintenance": "Dismiss",
+    "inspectionDue": "Inspection due {date}",
+    "inspectionCheckedAt": "Checked against the registry {date}",
+    "notRegistered": "Not registered",
+    "soldReported": "Customer reported sold {date}"
   },
   "quotes": {
     "newQuote": "New Quote",
@@ -130,6 +140,7 @@
     "descriptionLabel": "Description",
     "descriptionPlaceholder": "Optional details...",
     "dueDateLabel": "Due Date",
+    "dueTimeLabel": "Time (optional)",
     "dueMileageLabel": "Due Mileage",
     "dueMileagePlaceholder": "e.g. 50000",
     "reminderUpdated": "Reminder updated",
@@ -338,7 +349,20 @@
     "sectionAssignment": "Photo and owner",
     "sectionDetails": "Details",
     "addScannedOwner": "Add customer {name}",
-    "ownerSaveFailed": "Could not create the customer"
+    "ownerSaveFailed": "Could not create the customer",
+    "lookupPlate": "Look up plate",
+    "lookingUp": "Asking the vehicle registry...",
+    "lookupSuccess": "Fields filled from {source}",
+    "lookupNotFound": "The registry has no vehicle with that plate",
+    "lookupFailed": "Could not look up the plate",
+    "lookupUnavailable": "Connect a vehicle registry under Settings → Integrations",
+    "lookupEnterPlate": "Type a plate first",
+    "lookupInspectionDue": "{source}: next periodic inspection due {date}",
+    "lookupSource": "Filled from {source}",
+    "inspectionDue": "Next periodic inspection",
+    "inspectionDueHint": "Type the date if no registry supplies it. Reminders and the dashboard use it.",
+    "inspectionDueFromRegistry": "Filled from the vehicle registry. Change it here to override until the next sync.",
+    "lookupEnterPlateOrVin": "Type a plate or VIN first"
   },
   "archiveDialog": {
     "title": "Archive Vehicle",
@@ -353,5 +377,195 @@
     "previousPage": "Previous page",
     "nextPage": "Next page",
     "lastPage": "Last page"
+  },
+  "inspectionReminders": {
+    "title": "Inspection reminders",
+    "description": "Tell customers whose periodic inspection is coming up, with a link to book a time. Nothing is sent until you confirm the list.",
+    "window": "Deadline within",
+    "window30": "Next 30 days",
+    "window60": "Next 60 days",
+    "window90": "Next 90 days",
+    "channel": "Send by",
+    "channels": {
+      "sms": "SMS",
+      "whatsapp": "WhatsApp",
+      "email": "Email",
+      "telegram": "Telegram"
+    },
+    "willReceive": "will receive a message",
+    "excludedCount": "{count} excluded",
+    "nobodyToRemind": "Nobody to remind in this window. Every vehicle with a deadline has either been reminded, booked, or has no way to be reached.",
+    "overdueOn": "Overdue since {date}",
+    "excludedTitle": "Not included",
+    "excludedDescription": "Each of these has a reason. Fix the reason on the customer or vehicle and they will appear in the list.",
+    "reasons": {
+      "noCustomer": "No customer",
+      "optedOut": "Opted out of reminders",
+      "noRecipient": "No contact for this channel",
+      "alreadyReminded": "Already reminded for this deadline",
+      "cooldown": "Messaged recently",
+      "soldReported": "Customer reported sold",
+      "notRegistered": "Not registered",
+      "booked": "Already booked"
+    },
+    "message": "Message",
+    "subject": "Subject",
+    "body": "Text",
+    "placeholders": "Placeholders: {customerName} {vehicle} {plate} {dueDate} {workshopName} {bookingLink} {phone}",
+    "unknownPlaceholders": "Unknown placeholders: {list}",
+    "sample": "Example for the first recipient",
+    "segments": "{count, plural, one {# SMS segment} other {# SMS segments}}",
+    "bookingModeDirectNote": "The link books a {minutes}-minute slot straight onto the work board.",
+    "bookingModeRequestNote": "The link sends a request with the chosen time for the office to confirm.",
+    "linkValidNote": "Links stay open at least {days} days and past the deadline.",
+    "sendTest": "Send a test to me",
+    "testSent": "Test queued to {to}",
+    "testFailed": "Could not send the test",
+    "sendButton": "Send to {count}",
+    "confirmTitle": "Send the reminders?",
+    "confirmDescription": "{count} messages go out by {channel}, one per vehicle, once. A vehicle is never reminded twice for the same deadline.",
+    "confirmInput": "Type {count} to confirm",
+    "cancel": "Cancel",
+    "sendNow": "Send",
+    "created": "{count} reminders queued",
+    "sendFailed": "Could not create the campaign",
+    "loadFailed": "Could not load the list",
+    "campaign": {
+      "title": "Campaign from {date}",
+      "summary": "{channel}, deadlines within {window} days",
+      "recipients": "Recipients",
+      "sent": "Sent",
+      "failed": "Failed",
+      "booked": "Booked",
+      "retryFailedButton": "Retry {count} failed",
+      "retried": "{count} put back in the queue",
+      "retryFailed": "Could not retry",
+      "rows": "Recipients",
+      "status": {
+        "scheduled": "Queued",
+        "sent": "Sent",
+        "failed": "Failed",
+        "cancelled": "Cancelled"
+      },
+      "bookedOn": "Booked {date}",
+      "requestedOn": "Requested {date}",
+      "cancelledOn": "Cancelled {date}",
+      "none": "No reminder campaigns yet.",
+      "start": "Send inspection reminders",
+      "recipientsCount": "{count} recipients",
+      "failedCount": "{count} failed",
+      "bookedCount": "{count} booked"
+    },
+    "defaults": {
+      "smsBody": "Hi {customerName}, the periodic inspection for your {vehicle} ({plate}) is due {dueDate}. Book a time: {bookingLink} or call us on {phone}. {workshopName}",
+      "emailSubject": "Inspection due {dueDate} for {vehicle}",
+      "emailBody": "Hi {customerName},\n\nThe periodic inspection for your {vehicle} ({plate}) is due on {dueDate}.\n\nBook a time that suits you here: {bookingLink}\n\nOr call us on {phone}.\n\nKind regards,\n{workshopName}"
+    },
+    "refresh": "Refresh list",
+    "timeZoneRequired": "The workshop timezone is on automatic, so booking times would follow whichever browser saved settings last (currently {zone}). Reminders cannot be sent until a zone is chosen.",
+    "timeZoneRequiredLink": "Choose it under Localization"
+  },
+  "plateLookup": {
+    "open": "Plate lookup",
+    "shortcut": "Ctrl+Shift+L",
+    "title": "Plate lookup",
+    "description": "Type a registration number to see what your workshop and the vehicle registry know about it",
+    "placeholder": "Type a plate",
+    "search": "Look up",
+    "hintEnter": "Enter to look up",
+    "hintClose": "Esc to close",
+    "recent": "Recent",
+    "clearRecent": "Clear",
+    "looking": "Asking {source}…",
+    "lookingWorkshop": "Checking your workshop…",
+    "invalid": "That does not look like a plate",
+    "inWorkshop": "In your workshop",
+    "inWorkshopArchived": "In your workshop, archived",
+    "notInWorkshop": "Not in your workshop yet",
+    "notInWorkshopHint": "The registry knows this vehicle. Add it and the fields below come along.",
+    "notInWorkshopNoRegistry": "Add it by hand, or check the plate.",
+    "openVehicle": "Open vehicle",
+    "addVehicle": "Add vehicle",
+    "noOwner": "No customer linked",
+    "mileage": "{km} km",
+    "services": "{count, plural, =0 {No service records} one {# service record} other {# service records}}",
+    "lastService": "Last service {date}",
+    "registryNotFound": "{source} has no vehicle with that plate",
+    "registryFailed": "{source} could not be asked",
+    "registryFailedGeneric": "The registry could not be asked",
+    "fromSource": "From {source}",
+    "registered": "Registered",
+    "notRegistered": "Not registered",
+    "vin": "VIN",
+    "copyVin": "Copy VIN",
+    "copied": "Copied",
+    "fuel": "Fuel",
+    "transmission": "Gearbox",
+    "engine": "Engine",
+    "engineCode": "Engine code",
+    "color": "Colour",
+    "vehicleClass": "Class",
+    "firstRegistered": "First registered",
+    "inspectionDue": "Inspection due",
+    "lastInspected": "Last inspected",
+    "weights": "Weights",
+    "weightsValue": "{kerb} kg kerb, {gross} kg max",
+    "kerbOnly": "{kerb} kg kerb",
+    "grossOnly": "{gross} kg max",
+    "tyres": "Tyres",
+    "axle": "Axle {n}",
+    "tyreSize": "Tyre",
+    "rimSize": "Rim",
+    "loadSpeed": "Load / speed",
+    "overdue": "Overdue",
+    "dueSoon": "Due soon",
+    "emptyTitle": "Look up any plate",
+    "emptyBody": "See the vehicle, who owns it here, and what {source} has on file. Not yours yet? Add it in one step.",
+    "registryWord": "the registry"
+  },
+  "safety": {
+    "title": "Safety and known issues",
+    "summaryRecalls": "{count, plural, one {# recall} other {# recalls}}",
+    "summaryComplaints": "{count, plural, one {# owner complaint} other {# owner complaints}}",
+    "expand": "Show details",
+    "collapse": "Hide details",
+    "loading": "Asking the safety authority...",
+    "from": "From {source} for {model}, updated {date}",
+    "stale": "{source} could not be reached; showing the report from {date}",
+    "notKnown": "{source} has no record of a {model}.",
+    "notKnownHint": "Check the make, model and year, or add the VIN so the model can be matched exactly.",
+    "recalls": "Recalls",
+    "recallsNone": "No recall campaigns",
+    "complaints": "Owner complaints",
+    "complaintsNone": "None filed",
+    "crashes": "{count, plural, one {# crash} other {# crashes}}",
+    "fires": "{count, plural, one {# fire} other {# fires}}",
+    "rating": "Crash rating",
+    "notRated": "Not rated",
+    "stars": "{count} of 5 stars",
+    "frontal": "Frontal",
+    "side": "Side",
+    "rollover": "Rollover",
+    "ownersReport": "What owners report",
+    "ofComplaints": "of {total} complaints filed",
+    "crash": "Crash",
+    "fire": "Fire",
+    "noExamples": "Complaint texts are not in this report yet. Refresh from the source to fetch them.",
+    "readAll": "Read all {count} on {source}",
+    "showRecalls": "{count, plural, one {Show # recall} other {Show # recalls}}",
+    "hideRecalls": "Hide recalls",
+    "more": "More",
+    "less": "Less",
+    "consequence": "Risk",
+    "remedy": "Remedy",
+    "parkIt": "Do not drive",
+    "parkOutside": "Park outdoors",
+    "addFinding": "Add to observations",
+    "findingAdded": "In observations",
+    "findingDescription": "Recall {campaign}: {component}",
+    "refresh": "Refresh from the source",
+    "refreshed": "Safety data refreshed",
+    "openSite": "Open on {source}",
+    "errorLoad": "Could not load safety data"
   }
 }

+ 1 - 1
messages/es/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Pregunte sobre su taller...",
   "clear": "Borrar conversación",
   "error": "Algo salió mal. Por favor, inténtelo de nuevo.",
-  "notEnabled": "La IA no está habilitada. Configúrela en Ajustes → IA.",
+  "notEnabled": "La IA no está conectada. Conecta OpenAI o Anthropic en Ajustes → Integraciones.",
   "suggestion1": "¿Qué vehículos necesitan servicio pronto?",
   "suggestion2": "Muéstrame facturas sin pagar",
   "suggestion3": "¿Cuáles son los ingresos de este mes?",

+ 27 - 2
messages/es/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "Número de WhatsApp registrado",
     "team_giveTechnicianTheApp": "Cuenta dada a un técnico solo de tablero",
     "team_createDefaultRoles": "Roles estándar creados",
-    "settings_setCustomFieldPlacement": "Ubicación de campo personalizado actualizada"
+    "settings_setCustomFieldPlacement": "Ubicación de campo personalizado actualizada",
+    "import_commit": "Hoja de cálculo importada",
+    "import_undo": "Importación deshecha",
+    "integration_connect": "Integración conectada",
+    "integration_disconnect": "Integración desconectada",
+    "service_setInvoiceDesign": "Diseño de factura cambiado",
+    "settings_saveDocumentDesign": "Diseño de documento guardado",
+    "settings_deleteDocumentDesign": "Diseño de documento eliminado",
+    "settings_applyDocumentDesign": "Diseño de documento predeterminado establecido",
+    "settings_setDocumentDesignRule": "Cambió cuándo se usa un diseño",
+    "settings_freezeInvoices": "Facturas anteriores bloqueadas",
+    "inspection_reminders_sent": "Envió recordatorios de inspección",
+    "inspection_reminders_send": "Envió recordatorios de inspección",
+    "service_videoCall_send": "Enlace de videollamada enviado"
   },
   "summary": {
     "customField_create": "Campo personalizado «{name}» creado",
@@ -275,6 +288,18 @@
     "work_bay_create": "Puesto de trabajo «{name}» creado",
     "work_bay_update": "Puesto de trabajo «{name}» actualizado",
     "work_bay_delete": "Puesto de trabajo {id} eliminado",
-    "settings_setCustomFieldPlacement": "Cambió dónde se imprime un campo personalizado"
+    "settings_setCustomFieldPlacement": "Cambió dónde se imprime un campo personalizado",
+    "import_commit": "Importó una hoja de cálculo: {count} registros creados",
+    "import_undo": "Deshizo una importación de hoja de cálculo: {records} registros eliminados",
+    "integration_connect": "{name} conectado",
+    "integration_disconnect": "{name} desconectado",
+    "service_setInvoiceDesign": "Cambió con qué diseño se imprime una factura",
+    "settings_saveDocumentDesign": "Guardó el diseño de documento {name}",
+    "settings_deleteDocumentDesign": "Eliminó el diseño de documento {name}",
+    "settings_applyDocumentDesign": "Estableció {name} como diseño de documento predeterminado",
+    "settings_setDocumentDesignRule": "Cambió cuándo se usa el diseño de documento {name}",
+    "settings_freezeInvoices": "Bloqueó {count, plural, one {# factura} other {# facturas}} anteriores al bloqueo con el diseño actual",
+    "inspection_reminders_sent": "Envió {count} recordatorios de inspección",
+    "service_videoCall_send": "Envió el enlace de la videollamada por {channels}"
   }
 }

+ 2 - 1
messages/es/auth.json

@@ -13,7 +13,8 @@
     "errors": {
       "tooManyAttempts": "Demasiados intentos, vuelva a intentarlo mas tarde.",
       "invalidCredentials": "Correo electrónico o contraseña no validos",
-      "passkeyFailed": "Error al iniciar sesión con passkey. Inténtelo de nuevo o use correo y contraseña."
+      "passkeyFailed": "Error al iniciar sesión con passkey. Inténtelo de nuevo o use correo y contraseña.",
+      "invalidOrigin": "Has abierto Torqvoice en {origin}, pero está configurado para {configured}. Pon en NEXT_PUBLIC_APP_URL exactamente la dirección que usas en el navegador y reinicia Torqvoice."
     },
     "resetPasswordCta": "Restablécela aquí"
   },

+ 74 - 7
messages/es/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Hoy",
   "workOrder": "Orden de trabajo",
+  "allDay": "Todo el día",
+  "openDay": "Abrir {date}",
+  "dayTotal": "{count, plural, =1 {1 evento} other {# eventos}}",
+  "views": {
+    "day": "Día",
+    "week": "Semana",
+    "month": "Mes",
+    "year": "Año",
+    "schedule": "Agenda",
+    "fourDays": "4 días"
+  },
+  "options": {
+    "showWeekends": "Mostrar fines de semana",
+    "showCompleted": "Mostrar completados",
+    "showWeekNumbers": "Mostrar números de semana"
+  },
+  "toolbar": {
+    "previous": "Anterior",
+    "next": "Siguiente",
+    "create": "Crear",
+    "showSidebar": "Mostrar panel lateral",
+    "hideSidebar": "Ocultar panel lateral"
+  },
+  "sidebar": {
+    "calendars": "Calendarios",
+    "legend": "Colores de estado"
+  },
+  "schedule": {
+    "empty": "Nada programado",
+    "emptyHint": "No hay órdenes de trabajo, recordatorios, presupuestos ni mensajes en este periodo."
+  },
+  "peek": {
+    "close": "Cerrar",
+    "open": {
+      "service": "Abrir orden de trabajo",
+      "reminder": "Abrir recordatorio",
+      "quote": "Abrir presupuesto",
+      "message": "Abrir mensajes",
+      "external": "Abrir"
+    }
+  },
   "filters": {
-    "services": "Servicios",
+    "services": "Órdenes de trabajo",
     "reminders": "Recordatorios",
     "quotes": "Presupuestos",
-    "messages": "Mensajes"
+    "messages": "Mensajes",
+    "external": "Calendarios conectados"
   },
   "selectVehicle": "Seleccionar vehículo para orden de trabajo",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Use today"
   },
   "more": "+ {count} más",
-  "monthTotal": "{count} eventos este mes",
+  "monthTotal": "{count} eventos",
   "contextMenu": {
     "newWorkOrder": "Nueva orden de trabajo",
+    "newWorkOrderAt": "Nueva orden de trabajo a las {time}",
     "newReminder": "Nuevo recordatorio",
     "newQuote": "Nuevo presupuesto",
     "openEvent": "Abrir ({count})",
     "hint": "Consejo: haz clic derecho en un día para ver las acciones",
     "scheduleMessage": "Programar mensaje"
   },
+  "eventMenu": {
+    "open": "Abrir",
+    "markDone": "Marcar como hecho",
+    "markNotDone": "Marcar como pendiente",
+    "deleteReminder": "Eliminar recordatorio",
+    "deleteReminderConfirm": "¿Eliminar el recordatorio \"{title}\"? Esta acción no se puede deshacer.",
+    "deleteMessage": "Eliminar mensaje programado",
+    "deleteMessageConfirm": "¿Eliminar el mensaje programado \"{title}\"? No se enviará.",
+    "deleted": "Eliminado",
+    "updated": "Actualizado",
+    "failed": "No se pudo completar. Inténtalo de nuevo."
+  },
   "events": {
     "status": {
       "completed": "Completado",
       "inProgress": "En progreso",
       "waitingParts": "Esperando piezas",
       "pending": "Pendiente",
+      "scheduled": "Programado",
       "sent": "Enviado",
       "approved": "Aprobado",
       "draft": "Borrador",
@@ -66,16 +122,27 @@
       "overdue": "Vencido",
       "upcoming": "Próximo",
       "failed": "Fallido",
-      "queued": "En cola"
+      "queued": "En cola",
+      "busy": "Ocupado"
     },
     "type": {
-      "service": "Servicio",
+      "service": "Orden de trabajo",
       "reminder": "Recordatorio",
       "quote": "Presupuesto",
-      "message": "Mensaje"
+      "message": "Mensaje",
+      "external": "Calendario conectado"
     },
     "noEvents": "Sin eventos",
-    "selectDay": "Seleccione un día con eventos o cree una nueva orden de trabajo."
+    "selectDay": "Seleccione un día con eventos o cree una nueva orden de trabajo.",
+    "external": {
+      "source": "De {source}",
+      "sourceUnknown": "De un calendario conectado",
+      "allDay": "Todo el día",
+      "open": "Abrir en {source}",
+      "openGeneric": "Abrir en el calendario",
+      "noLink": "Este calendario no ofrece un enlace al evento.",
+      "readOnly": "Tiempo ocupado de un calendario conectado. Cámbialo allí y se actualizará aquí en la próxima sincronización."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/es/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Número de teléfono copiado al portapapeles",
     "taxIdLabel": "NIF",
     "taxExemptBadge": "Exento de impuestos",
+    "invoiceDesignBadge": "Diseño de factura: {name}",
+    "invoiceDesignBadgeHint": "Las facturas de este cliente se imprimen con este diseño en lugar del predeterminado del taller. Ábrelo en el diseñador.",
     "tabs": {
       "vehicles": "Vehículos ({count})",
       "invoices": "Facturas ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Auto",
     "sectionContact": "Contacto",
     "sectionBilling": "Facturación",
-    "addScannedVehicle": "Añadir el vehículo {vehicle}"
+    "addScannedVehicle": "Añadir el vehículo {vehicle}",
+    "invoiceDesign": "Diseño de factura",
+    "invoiceDesignDefault": "Predeterminado del taller",
+    "invoiceDesignHint": "Las facturas de este cliente se imprimen con este diseño en lugar del predeterminado del taller. Una factura puede elegir el suyo.",
+    "reminderOptOut": "Sin recordatorios",
+    "reminderOptOutHint": "No incluir a este cliente en recordatorios de inspección ni otras campañas."
   },
   "serviceRequests": {
     "statusNew": "Nuevo",
@@ -117,44 +124,5 @@
     "notesSaveError": "Error al guardar las notas",
     "smsLabel": "Mensaje de Solicitud de Servicio",
     "smsDefault": "Hola {name}, sobre tu solicitud de servicio para tu {vehicle}: "
-  },
-  "import": {
-    "title": "Importar Clientes",
-    "description": "Importar clientes desde archivos CSV, TSV o vCard.",
-    "dropFile": "Arrastra y suelta un archivo aquí",
-    "supportedFormats": "CSV, TSV, TXT o vCard (.vcf)",
-    "browse": "Explorar archivos",
-    "noDataFound": "No se encontraron datos de clientes en el archivo",
-    "rowsFound": "{count} cliente(s) encontrado(s)",
-    "colName": "Nombre",
-    "colEmail": "Correo electrónico",
-    "colPhone": "Teléfono",
-    "colCompany": "Empresa",
-    "colAddress": "Dirección",
-    "chooseAnother": "Elegir otro archivo",
-    "cancel": "Cancelar",
-    "importButton": "Importar {count} cliente(s)",
-    "importComplete": "Importación completada",
-    "importedCount": "{count} cliente(s) importado(s) correctamente",
-    "skippedRows": "{count} fila(s) omitida(s) por errores",
-    "rowError": "Fila {row} ({name}): {error}",
-    "done": "Listo",
-    "importFailed": "Error al importar clientes",
-    "checkingDuplicates": "Comprobando duplicados...",
-    "duplicatesFound": "{count} duplicado(s) encontrado(s)",
-    "duplicatesBanner": "{count} cliente(s) existente(s) coinciden con los datos de importación.",
-    "duplicatesHint": "Haga clic en Omitir o Fusionar para cambiar cómo se maneja cada duplicado.",
-    "colStatus": "Estado",
-    "actionSkip": "Omitir",
-    "actionMerge": "Fusionar",
-    "actionNew": "Nuevo",
-    "summaryNew": "{count} nuevos",
-    "summaryMerge": "{count} fusionar",
-    "summarySkip": "{count} omitir",
-    "skipAll": "Omitir todos",
-    "mergeAll": "Fusionar todos",
-    "resultCreated": "{count} cliente(s) creado(s)",
-    "resultMerged": "{count} cliente(s) fusionado(s)",
-    "resultSkipped": "{count} cliente(s) omitido(s)"
   }
 }

+ 12 - 1
messages/es/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Observaciones recientes",
     "recentActivity": "Actividad reciente",
     "gettingStarted": "Primeros pasos",
-    "tireHotel": "Hotel de neumáticos"
+    "tireHotel": "Hotel de neumáticos",
+    "inspectionsDue": "Inspecciones pendientes"
   },
   "maintenance": {
     "title": "Mantenimiento previsto pendiente",
@@ -252,5 +253,15 @@
     "roomLeft": "Sitio para {count} más",
     "needsPrep": "Pendiente de preparar",
     "unbilled": "Sin facturar ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Inspecciones pendientes",
+    "viewAll": "Ver todos los vehículos con vencimiento",
+    "overdue": "Vencidas",
+    "within30": "Próximos 30 días",
+    "within90": "Próximos 90 días",
+    "noData": "No hay inspecciones en los próximos 90 días",
+    "overdueSince": "Vencida desde {date}",
+    "remind": "Enviar recordatorios"
   }
 }

+ 228 - 0
messages/es/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "Importar {entity}",
+  "cancel": "Cancelar",
+  "done": "Listo",
+  "entities": {
+    "customers": "clientes",
+    "vehicles": "vehículos",
+    "services": "historial de servicio"
+  },
+  "entityHints": {
+    "customers": "Nombres, datos de contacto, direcciones. Una fila por cliente.",
+    "vehicles": "Marca, modelo, año, matrícula, VIN. Una columna de propietario vincula o crea el cliente.",
+    "services": "Trabajos y facturas anteriores. Una fila por trabajo, con el vehículo al que pertenece."
+  },
+  "groups": {
+    "customer": "Cliente",
+    "vehicle": "Vehículo",
+    "service": "Servicio"
+  },
+  "steps": {
+    "upload": "Elija qué importar y suba el archivo desde su sistema actual.",
+    "mapping": "Compruebe qué columna se convierte en qué campo. Todavía no se escribe nada.",
+    "preview": "Simulación: esto es exactamente lo que hará la importación. Revíselo y luego importe.",
+    "importing": "Escribiendo filas. Mantenga esta ventana abierta.",
+    "result": "Importación finalizada."
+  },
+  "upload": {
+    "drop": "Arrastre y suelte un archivo aquí",
+    "formats": "CSV, Excel (.xlsx) o vCard (.vcf), hasta 25 MB y 50.000 filas",
+    "browse": "Buscar archivos",
+    "reading": "Leyendo el archivo...",
+    "templateHint": "¿Aún no tiene una exportación? Rellene una plantilla y súbala.",
+    "templateXlsx": "Plantilla de Excel",
+    "templateCsv": "Plantilla CSV",
+    "sourcesHint": "Funciona con exportaciones de cualquier sistema de taller, herramienta de contabilidad, Google Contacts, Outlook o una hoja de cálculo. Los nombres de columna se reconocen en muchos idiomas; usted confirma la asignación antes de que se escriba nada."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# fila} other {# filas}}",
+    "colColumn": "Columna en su archivo",
+    "colSample": "Valores de ejemplo",
+    "colField": "Se convierte en",
+    "ignore": "Ignorar esta columna",
+    "source": {
+      "preset": "Formato reconocido",
+      "header": "Coincidencia por encabezado",
+      "values": "Deducido de los valores"
+    },
+    "suggestWithAi": "Sugerir con IA",
+    "aiApplied": "{count, plural, one {# columna asignada} other {# columnas asignadas}} por IA. Compruébelas antes de continuar.",
+    "chooseAnother": "Elegir otro archivo",
+    "missingRequired": "Todavía falta: {fields}",
+    "dryRunHint": "La simulación comprueba cada fila contra sus datos existentes sin escribir nada.",
+    "runDryRun": "Ejecutar simulación"
+  },
+  "options": {
+    "dateFormat": "Formato de fecha",
+    "dateAuto": "Detectar automáticamente",
+    "dateDMY": "Día / mes / año",
+    "dateMDY": "Mes / día / año",
+    "dateYMD": "Año / mes / día",
+    "decimal": "Separador decimal",
+    "decimalAuto": "Detectar automáticamente",
+    "decimalDot": "Punto (1,234.56)",
+    "decimalComma": "Coma (1.234,56)",
+    "countryCode": "Código de país para números de teléfono",
+    "duplicates": "Cuando una fila coincide con un registro existente",
+    "duplicatesUpdate": "Actualizar el registro existente",
+    "duplicatesSkip": "Omitir la fila",
+    "duplicatesCreate": "Crear un registro nuevo de todos modos"
+  },
+  "summary": {
+    "create": "Se crearán",
+    "update": "Se actualizarán",
+    "skip": "Se omitirán",
+    "error": "No se pueden importar",
+    "warnings": "Con advertencias",
+    "customersToCreate": "{count, plural, one {# cliente nuevo} other {# clientes nuevos}}",
+    "customersToUpdate": "{count, plural, one {# cliente actualizado} other {# clientes actualizados}}",
+    "vehiclesToCreate": "{count, plural, one {# vehículo nuevo} other {# vehículos nuevos}}",
+    "vehiclesToUpdate": "{count, plural, one {# vehículo actualizado} other {# vehículos actualizados}}",
+    "servicesToCreate": "{count, plural, one {# registro de servicio} other {# registros de servicio}}"
+  },
+  "actions": {
+    "create": "Crear",
+    "update": "Actualizar",
+    "skip": "Omitir",
+    "error": "Error"
+  },
+  "matchOn": {
+    "number": "número de cliente",
+    "email": "correo electrónico",
+    "phone": "teléfono",
+    "name": "nombre",
+    "vin": "VIN",
+    "plate": "matrícula",
+    "makeModelYear": "marca, modelo y año"
+  },
+  "preview": {
+    "colAction": "Acción",
+    "colOwner": "Propietario",
+    "colDetails": "Detalles",
+    "empty": "Ninguna fila coincide con este filtro.",
+    "matchedOn": "Coincide con {name} (por {on})",
+    "linksCustomer": "Vinculado al cliente {name}",
+    "linksVehicle": "Vinculado a {label}",
+    "sameCustomerAsRow": "Mismo cliente que la fila {row}",
+    "sameVehicleAsRow": "Mismo vehículo que la fila {row}",
+    "createsCustomer": "Crea el cliente",
+    "createsVehicle": "Crea el vehículo",
+    "downloadReport": "Descargar informe de la simulación",
+    "limitExceeded": "Su plan permite {remaining} clientes más, pero esta importación crearía {needed}. Elimine filas o mejore su plan.",
+    "pageInfo": "{from}–{to} de {total}",
+    "prev": "Anterior",
+    "next": "Siguiente",
+    "backToMapping": "Volver a la asignación",
+    "import": "Importar {count, plural, one {# fila} other {# filas}}",
+    "nothingWrittenYet": "Todavía no se ha escrito nada. La importación se puede deshacer después desde Configuración → Datos."
+  },
+  "importing": {
+    "title": "Importando...",
+    "starting": "Iniciando",
+    "progress": "{done} de {total} filas",
+    "keepOpen": "Los archivos grandes tardan uno o dos minutos."
+  },
+  "result": {
+    "title": "Importación completada",
+    "undoneTitle": "Importación deshecha",
+    "created": "{count, plural, one {# registro creado} other {# registros creados}}",
+    "updated": "{count, plural, one {# registro actualizado} other {# registros actualizados}}",
+    "skipped": "{count, plural, one {# fila omitida} other {# filas omitidas}}",
+    "failed": "{count, plural, one {# fila no importada} other {# filas no importadas}}",
+    "writeFailures": "{count, plural, one {# fila falló al escribir} other {# filas fallaron al escribir}}",
+    "rowFailure": "Fila {row}: {error}",
+    "undo": "Deshacer esta importación",
+    "undone": "Se eliminaron {customers} clientes, {vehicles} vehículos y {services} registros de servicio.",
+    "undoHint": "Deshacer elimina todos los registros que creó esta importación. Los registros que solo actualizó conservan sus nuevos valores."
+  },
+  "history": {
+    "title": "Historial de importaciones",
+    "description": "Importaciones de hojas de cálculo realizadas en este taller. Deshacer elimina lo que creó una importación.",
+    "loading": "Cargando...",
+    "statusUndone": "Deshecha",
+    "counts": "{created} creados, {updated} actualizados, {skipped} omitidos, {failed} fallidos",
+    "undo": "Deshacer",
+    "undone": "Se eliminaron {customers} clientes, {vehicles} vehículos y {services} registros de servicio.",
+    "undoFailed": "No se pudo deshacer la importación",
+    "confirmTitle": "¿Deshacer esta importación?",
+    "confirmDescription": "Elimina lo que creó {file}: {customers} clientes, {vehicles} vehículos y {services} registros de servicio. Los registros que la importación solo actualizó se conservan.",
+    "cancel": "Cancelar",
+    "confirm": "Deshacer importación"
+  },
+  "fields": {
+    "customer": {
+      "name": "Nombre del cliente",
+      "firstName": "Nombre",
+      "lastName": "Apellidos",
+      "customerNumber": "Número de cliente",
+      "email": "Correo electrónico",
+      "phone": "Teléfono",
+      "company": "Empresa",
+      "address": "Dirección",
+      "street": "Calle",
+      "postalCode": "Código postal",
+      "city": "Ciudad",
+      "state": "Estado / región",
+      "country": "País",
+      "taxId": "Identificación fiscal",
+      "notes": "Notas del cliente"
+    },
+    "vehicle": {
+      "description": "Vehículo (año marca modelo en una sola columna)",
+      "make": "Marca",
+      "model": "Modelo",
+      "year": "Año",
+      "vin": "VIN",
+      "licensePlate": "Matrícula",
+      "color": "Color",
+      "mileage": "Kilometraje",
+      "fuelType": "Tipo de combustible",
+      "transmission": "Transmisión",
+      "engineSize": "Cilindrada",
+      "engineCode": "Código de motor",
+      "purchaseDate": "Fecha de compra",
+      "purchasePrice": "Precio de compra"
+    },
+    "service": {
+      "date": "Fecha del servicio",
+      "title": "Título del servicio",
+      "description": "Descripción",
+      "total": "Importe total",
+      "mileage": "Kilometraje en el servicio",
+      "invoiceNumber": "Número de factura",
+      "notes": "Notas del servicio",
+      "technician": "Técnico"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Falta el nombre del cliente",
+    "invalid_email": "El correo electrónico \"{value}\" no es válido y se omitió",
+    "invalid_vin": "El VIN \"{value}\" no tiene 17 caracteres; se conservó tal como se escribió",
+    "vehicle_make_model_required": "Faltan la marca y el modelo",
+    "vehicle_year_required": "Falta el año",
+    "vehicle_year_invalid": "El año \"{value}\" no es un año válido",
+    "vehicle_identifier_required": "No hay matrícula, VIN ni marca y modelo para identificar el vehículo",
+    "vehicle_not_found": "Ningún vehículo existente coincide con \"{value}\" y no hay datos suficientes para crear uno",
+    "service_date_required": "Falta la fecha del servicio",
+    "service_date_invalid": "No se pudo leer la fecha \"{value}\"",
+    "service_title_required": "Falta el título o la descripción del servicio",
+    "invalid_date": "No se pudo leer {field} \"{value}\" y se omitió",
+    "invalid_number": "{field} \"{value}\" no es un número y se omitió",
+    "duplicate_invoice_number": "El número de factura {value} ya existe",
+    "write_failed": "No se pudo escribir: {value}"
+  },
+  "errors": {
+    "uploadFailed": "No se pudo leer el archivo",
+    "aiFailed": "La IA no pudo sugerir una asignación",
+    "dryRunFailed": "La simulación falló",
+    "importFailed": "La importación falló",
+    "undoFailed": "No se pudo deshacer la importación"
+  },
+  "entry": {
+    "title": "Importar desde una hoja de cálculo",
+    "description": "Traiga clientes, vehículos e historial de servicio desde cualquier sistema que exporte CSV o Excel. Asigne las columnas, ejecute una simulación y luego importe. Toda importación se puede deshacer.",
+    "customers": "Importar clientes",
+    "vehicles": "Importar vehículos",
+    "services": "Importar historial de servicio"
+  }
+}

+ 584 - 0
messages/es/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Integraciones",
+    "description": "Conecte los servicios que su taller ya utiliza. Cada integración se configura una sola vez y funciona en segundo plano.",
+    "all": "Todas",
+    "empty": "Todavía no hay integraciones en esta categoría.",
+    "notConnected": "No conectada",
+    "connect": "Conectar",
+    "manage": "Administrar",
+    "connectedTitle": "Conectados",
+    "connectedDescription": "Servicios que este taller ha configurado. Abra uno para ver su estado, cambiar ajustes o desconectarlo.",
+    "noneConnected": "Aún no hay nada conectado. Elija un servicio abajo para empezar.",
+    "availableTitle": "Disponibles",
+    "availableDescription": "Servicios que puede añadir. Cada uno se configura una vez y funciona en segundo plano.",
+    "allConnected": "Todos los servicios disponibles están conectados.",
+    "searchPlaceholder": "Buscar integraciones",
+    "noMatches": "Nada coincide con «{query}».",
+    "readMore": "Leer más",
+    "suggestTitle": "¿Falta un servicio que usas?",
+    "suggestDescription": "Cuéntanos qué integración ayudaría a tu taller y la estudiaremos.",
+    "suggest": "Sugerir una integración",
+    "suggestSubject": "Sugerencia de integración"
+  },
+  "categories": {
+    "calendar": "Calendario",
+    "conferencing": "Videollamadas",
+    "accounting": "Contabilidad",
+    "messaging": "Mensajería",
+    "payments": "Pagos",
+    "automation": "Automatización",
+    "storage": "Archivos",
+    "registry": "Registro de vehículos",
+    "other": "Otras",
+    "ai": "IA"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Órdenes de trabajo al calendario",
+      "pull": "Tiempo ocupado del calendario",
+      "conference": "Enlaces de videollamada"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Consulta por matrícula y VIN",
+      "inspection": "Fechas de inspección",
+      "safety": "Llamadas a revisión y quejas de propietarios"
+    },
+    "ai": {
+      "chat": "Textos y resúmenes"
+    },
+    "payments": {
+      "checkout": "Pagar desde el enlace de la factura"
+    },
+    "accounting": {
+      "invoices": "Facturas al libro contable",
+      "customers": "Clientes al libro contable",
+      "payments": "Pagos en ambos sentidos"
+    }
+  },
+  "statuses": {
+    "active": "Conectada",
+    "error": "Requiere atención",
+    "pending": "Configuración en curso",
+    "disconnected": "Desconectada",
+    "needsSetup": "Configuración incompleta"
+  },
+  "connection": {
+    "back": "Todas las integraciones",
+    "account": "Cuenta conectada",
+    "lastHealth": "Última comprobación",
+    "lastSync": "Última sincronización",
+    "lastError": "La última ejecución falló",
+    "setupMissingTitle": "Configuración incompleta",
+    "setupMissing": "Elige {fields} en los ajustes de abajo. Hasta entonces no se sincroniza nada.",
+    "connected": "{name} está conectado.",
+    "connectedShort": "Conectado",
+    "connectWith": "Conectar {name}",
+    "connect": "Conectar",
+    "reconnect": "Volver a conectar",
+    "disconnect": "Desconectar",
+    "disconnectTitle": "¿Desconectar {name}?",
+    "disconnectDescription": "Torqvoice olvida la cuenta y deja de usarla. Nada ya enviado o guardado en {name} se toca.",
+    "disconnectDescriptionCalendar": "Torqvoice olvida la cuenta y deja de sincronizar. Las órdenes de trabajo ya puestas en {name} se quedan allí. El tiempo ocupado mostrado desde ese calendario se quita del calendario de Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice olvida la cuenta y deja de crear reuniones. Las reuniones ya creadas se quedan en {name} y los enlaces en las órdenes de trabajo se conservan.",
+    "disconnectDescriptionAccounting": "Torqvoice olvida la cuenta y deja de enviar y leer. Las facturas, clientes y pagos ya en {name} se quedan allí.",
+    "removeSetup": "Quitar",
+    "removeSetupTitle": "¿Quitar {name}?",
+    "removeSetupDescription": "Todavía no hay nada conectado. Torqvoice olvida lo introducido y {name} vuelve a la lista de servicios disponibles.",
+    "removeSetupDone": "{name} quitado",
+    "removeSetupHint": "¿No vas a seguir con esto? Quítalo y el servicio vuelve a la lista de abajo.",
+    "disconnected": "Desconectado",
+    "test": "Probar conexión",
+    "testPassed": "La conexión funciona",
+    "syncNow": "Sincronizar ahora",
+    "syncQueued": "Sincronización en cola; se ejecuta en menos de un minuto",
+    "pullNow": "Traer pagos ahora",
+    "pullQueued": "Lectura de pagos en cola; se ejecuta en menos de un minuto",
+    "backfill": "Enviar órdenes de trabajo programadas",
+    "backfillQueued": "Se pusieron en cola las órdenes de trabajo programadas desde la semana pasada en adelante",
+    "backfillInvoices": "Enviar facturas emitidas",
+    "backfillInvoicesQueuedCount": "{count, plural, one {# factura emitida} other {# facturas emitidas}} en cola para enviar",
+    "backfillInvoicesNone": "No hay facturas emitidas para enviar. Solo pasan las facturas enviadas, compartidas o pagadas, desde la fecha de inicio si la fijaste.",
+    "retryQueued": "Tarea puesta en cola de nuevo",
+    "settingsTitle": "Configuración",
+    "settingsDescription": "Qué sincronizar y dónde. Los cambios se aplican en la próxima sincronización.",
+    "settingsSaved": "Configuración guardada",
+    "save": "Guardar",
+    "loading": "Cargando...",
+    "choose": "Elegir...",
+    "search": "Buscar...",
+    "noMatch": "Nada coincide",
+    "leaveEmpty": "Dejar vacío",
+    "noLimit": "Sin límite",
+    "planLocked": "Las integraciones no están incluidas en su plan.",
+    "ownAppTitle": "Esta instalación usa su propia aplicación con el proveedor",
+    "redirectUri": "URI de redirección que debe registrar con el proveedor:",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Añade esta URL en el panel de Stripe en Developers, Webhooks, suscrita a checkout.session.completed. Luego pega el secreto de firma que muestra Stripe en el campo de secreto del webhook aquí, para que las notificaciones puedan verificarse.",
+    "inboundHintVipps": "Introduce esta URL como URL de callback en el portal de Vipps MobilePay. Vipps también exige condiciones de venta publicadas, que se configuran en Ajustes, Pago.",
+    "inboundHintPaypal": "Añade esta URL en el panel de desarrollador de PayPal en Webhooks, suscrita a PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "Sincronización de inspecciones",
+      "vehicles": "{count} vehículos con matrícula en este taller",
+      "dueNow": "{count} se consultarían en la próxima ejecución",
+      "withDate": "{count} ya tienen fecha de inspección",
+      "lastPass": "Última pasada completa {date}",
+      "never": "Aún no hay una pasada completa",
+      "inProgress": "Hay una pasada en curso",
+      "off": "Activa el ajuste y guarda; hasta entonces una pasada no comprueba nada.",
+      "syncNowHelp": "Sincronizar ahora inicia una pasada de inmediato en vez de esperar a la horaria. Consulta 25 vehículos cada vez y continúa sola hasta terminar con todos los pendientes."
+    },
+    "supersedes": "Conectar {next} desconecta {current}. Solo uno puede estar activo a la vez, así que {current} deja de usarse en cuanto {next} entra en servicio.",
+    "batchRetried": "{count, plural, =0 {Nada que reintentar} one {# tarea vuelta a la cola} other {# tareas vueltas a la cola}}"
+  },
+  "activity": {
+    "title": "Actividad",
+    "description": "Tareas recientes y líneas de registro de esta conexión.",
+    "retry": "Reintentar",
+    "jobStatus": {
+      "queued": "En cola",
+      "running": "En ejecución",
+      "done": "Hecho",
+      "failed": "Fallido",
+      "dead": "Abandonado"
+    },
+    "jobs": "{count, plural, one {# tarea} other {# tareas}}",
+    "counts": {
+      "queued": "{count} en cola",
+      "running": "{count} en curso",
+      "done": "{count} hechas",
+      "failed": "{count} fallidas",
+      "dead": "{count} abandonadas"
+    },
+    "noFailures": "Nada ha fallado.",
+    "retryFailed": "Reintentar las fallidas",
+    "details": "Ver detalles"
+  },
+  "errors": {
+    "generic": "Algo salió mal",
+    "demo": "Las integraciones no se pueden conectar en la demo.",
+    "forbidden": "Solo un administrador puede conectar integraciones.",
+    "unknown": "Integración desconocida.",
+    "plan": "Esta integración no está incluida en su plan.",
+    "no-client": "No hay credenciales de aplicación configuradas para este proveedor.",
+    "denied": "Canceló el inicio de sesión con el proveedor.",
+    "vendor": "El proveedor devolvió un error. Inténtelo de nuevo.",
+    "invalid": "La respuesta del proveedor fue incompleta. Inténtelo de nuevo.",
+    "state": "Este inicio de sesión ha caducado o pertenece a otra sesión. Empiece de nuevo.",
+    "exchange": "El proveedor rechazó el inicio de sesión. Compruebe las credenciales de la aplicación e inténtelo de nuevo.",
+    "identify": "Conectado, pero no se pudo leer la cuenta. Intente volver a conectar."
+  },
+  "meeting": {
+    "join": "Unirse a la videollamada",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "De calendarios conectados",
+    "busy": "Ocupado"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Añada las órdenes de trabajo programadas a un calendario de Google Calendar, incluya un enlace de Google Meet y vea el tiempo ocupado del calendario en Torqvoice.",
+      "tenantHelp": "Cree un cliente OAuth en Google Cloud Console (APIs y servicios → Credenciales), habilite la API de Google Calendar, añada la URI de redirección que aparece abajo y pegue aquí el ID de cliente y el secreto.",
+      "fields": {
+        "clientId": "ID de cliente",
+        "clientSecret": "Secreto de cliente"
+      },
+      "settings": {
+        "calendarId": "Calendario",
+        "pushEnabled": "Añadir las órdenes de trabajo programadas a este calendario",
+        "includeCustomer": "Incluir el nombre y el teléfono del cliente en el evento",
+        "addConference": "Añadir una reunión de Google Meet a cada orden de trabajo programada",
+        "addConferenceHelp": "Activado: cada orden de trabajo enviada al calendario recibe una reunión de Google Meet, y el enlace se muestra en la orden. Desactivado: añade una reunión de Google Meet a una orden concreta desde su página. En ambos casos el cliente solo recibe el enlace cuando lo envías.",
+        "pullEnabled": "Mostrar el tiempo ocupado de este calendario en el calendario de Torqvoice",
+        "pullEnabledHelp": "Solo lectura. Se actualiza cada 15 minutos, una semana hacia atrás y dos meses hacia adelante."
+      }
+    },
+    "microsoft-365": {
+      "description": "Añada las órdenes de trabajo programadas a un calendario de Outlook, incluya un enlace de Microsoft Teams y vea el tiempo ocupado del calendario en Torqvoice.",
+      "tenantHelp": "Registre una aplicación en Microsoft Entra (Registros de aplicaciones), permita cuentas personales y profesionales, añada la URI de redirección que aparece abajo como plataforma Web, cree un secreto de cliente y pegue ambos aquí.",
+      "fields": {
+        "clientId": "ID de aplicación (cliente)",
+        "clientSecret": "Secreto de cliente"
+      },
+      "settings": {
+        "calendarId": "Calendario",
+        "pushEnabled": "Añadir las órdenes de trabajo programadas a este calendario",
+        "includeCustomer": "Incluir el nombre y el teléfono del cliente en el evento",
+        "addConference": "Añadir una reunión de Microsoft Teams a cada orden de trabajo programada",
+        "addConferenceHelp": "Activado: cada orden de trabajo enviada al calendario recibe una reunión de Microsoft Teams, y el enlace se muestra en la orden. Desactivado: añade una reunión de Microsoft Teams a una orden concreta desde su página. En ambos casos el cliente solo recibe el enlace cuando lo envías.",
+        "pullEnabled": "Mostrar el tiempo ocupado de este calendario en el calendario de Torqvoice",
+        "pullEnabledHelp": "Solo lectura. Se actualiza cada 15 minutos, una semana hacia atrás y dos meses hacia adelante."
+      }
+    },
+    "zoom": {
+      "description": "Añada una reunión de Zoom a una orden de trabajo programada y conserve el enlace en ella, para diagnósticos a distancia o para repasar hallazgos con el cliente.",
+      "tenantHelp": "En Zoom App Marketplace, cree una aplicación General (user-managed), añada la URI de redirección de abajo, añada los ámbitos meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting y user:read:user, y pegue aquí el ID y el secreto de cliente.",
+      "fields": {
+        "clientId": "ID de cliente",
+        "clientSecret": "Secreto de cliente"
+      },
+      "settings": {
+        "autoCreate": "Crear una reunión de Zoom automáticamente para cada orden de trabajo programada",
+        "autoCreateHelp": "Desactivado: usted añade la videollamada a la orden de trabajo desde su página. Activado: cada orden con hora de inicio recibe una reunión, que se mueve al reprogramarla y se elimina al cancelarla. En ambos casos el cliente solo recibe el enlace cuando usted se lo envía.",
+        "includeCustomer": "Incluir el nombre del cliente en el tema de la reunión",
+        "joinBeforeHost": "Permitir que el cliente entre antes que usted",
+        "waitingRoom": "Usar sala de espera"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Rellena un vehículo a partir de su matrícula o VIN noruegos: marca, modelo, año, combustible, caja de cambios, motor, color, medidas de neumáticos y la próxima fecha de EU-kontroll, directamente del registro de Statens vegvesen. No se devuelven datos del propietario.",
+      "fields": {
+        "apiKey": "Clave de API",
+        "apiKeyHelp": "Gratuita. Pide la clave en vegvesen.no, en «API for tekniske kjøretøyopplysninger»: inicia sesión con BankID, representa al taller y la clave se emite al instante. Permite 50.000 consultas al día."
+      },
+      "settings": {
+        "syncInspections": "Mantener actualizadas las fechas de inspección de todos los vehículos de este taller",
+        "syncInspectionsHelp": "Una vez por hora se consultan por matrícula los vehículos que no se han comprobado hace poco y se guarda la próxima fecha de EU-kontroll. Solo se envían vehículos almacenados en este taller. Un vehículo se revisa cada mes, y cada semana cuando faltan menos de dos meses."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Rellena un vehículo a partir de su matrícula francesa, italiana, española, portuguesa o británica: marca, modelo, año, VIN, combustible, cilindrada y más, desde los datos de matriculación del país a través de Openapi.com. Cada consulta se cobra a la cuenta de Openapi del propio taller. No se devuelven datos del propietario.",
+      "fields": {
+        "token": "Token de API",
+        "tokenHelp": "Crea una cuenta en console.openapi.com, añade saldo al monedero y, en Automotive, crea un token para el entorno de producción. Cada consulta con resultado cuesta unas decenas de céntimos; una sin resultado es gratis."
+      },
+      "settings": {
+        "country": "País",
+        "countryHelp": "El registro donde buscar. Las matrículas francesas e italianas son iguales, así que el país no puede deducirse de la matrícula.",
+        "bikes": "Buscar también motocicletas",
+        "bikesHelp": "Cuando una matrícula no es de un coche, se consulta también el registro de motocicletas. Un fallo en la búsqueda de coches es gratis, así que no cuesta nada más salvo que se encuentre una motocicleta. No disponible para Portugal."
+      }
+    },
+    "rdw": {
+      "description": "Rellena un vehículo a partir de su matrícula neerlandesa: marca, modelo, año, combustible, cilindrada, color, pesos y la próxima fecha de APK, directamente del registro abierto de vehículos de la RDW. Gratis, sin clave. No se publican datos del propietario ni el VIN.",
+      "fields": {
+        "appToken": "Token de aplicación (opcional)",
+        "appTokenHelp": "Los datos abiertos de la RDW funcionan sin clave, pero las peticiones anónimas comparten un cupo limitado. Para la sincronización de inspecciones, crea una cuenta gratuita en opendata.rdw.nl, abre los ajustes de desarrollador de tu perfil y crea un token de aplicación."
+      },
+      "settings": {
+        "syncInspections": "Mantener actualizadas las fechas de APK de todos los vehículos de este taller",
+        "syncInspectionsHelp": "Una vez por hora se consultan por matrícula los vehículos que no se han comprobado hace poco y se guarda la próxima fecha de APK. Solo se envían vehículos almacenados en este taller. Un vehículo se revisa cada mes, y cada semana cuando faltan menos de dos meses."
+      }
+    },
+    "regcheck": {
+      "description": "Rellena un vehículo a partir de su matrícula en Australia, Nueva Zelanda, Estados Unidos, Irlanda, Noruega, Suecia, Dinamarca, Finlandia, Estonia, Chequia, Eslovaquia, Hungría o Croacia: marca, modelo, año, VIN, combustible y cilindrada cuando el país los publica, a través de RegCheck. Cada consulta con resultado gasta un crédito de la cuenta de RegCheck del propio taller. No se devuelven datos del propietario.",
+      "fields": {
+        "username": "Usuario de RegCheck",
+        "usernameHelp": "Regístrate en regcheck.org.uk; la cuenta incluye 10 consultas gratuitas y las demás se compran en bloques de 100 desde el panel. Aquí solo hace falta el usuario, nunca la contraseña. Una matrícula no encontrada no cuesta nada."
+      },
+      "settings": {
+        "country": "País",
+        "countryHelp": "El registro donde buscar. Cada país tiene su propio servicio, así que la conexión busca en uno.",
+        "auState": "Estado o territorio",
+        "auStateHelp": "Australia matricula los vehículos por estado y hay que indicar al registro cuál buscar. Elige donde están matriculados la mayoría de los vehículos de tus clientes.",
+        "usState": "Estado",
+        "usStateHelp": "El código de dos letras del estado donde están matriculados los vehículos, como CA o TX. Los registros estadounidenses se consultan por estado."
+      }
+    },
+    "openai": {
+      "description": "Descripciones de servicio, resúmenes de historial, lectura de documentos y el asistente, respondidos por tu propia cuenta de OpenAI. Cada llamada se factura a esa cuenta y tú eliges el modelo.",
+      "fields": {
+        "apiKey": "Clave de API",
+        "apiKeyHelp": "Créala en platform.openai.com, en API keys. La cuenta necesita saldo; OpenAI factura el uso por llamada."
+      },
+      "settings": {
+        "model": "Modelo",
+        "modelHelp": "Qué modelo responde. La lista se lee de tu cuenta, primero los más nuevos. Los modelos pequeños cuestan menos por llamada y bastan para la mayoría de los textos del taller."
+      }
+    },
+    "anthropic": {
+      "description": "Descripciones de servicio, resúmenes de historial, lectura de documentos y el asistente, respondidos por Claude en tu propia cuenta de Anthropic. Cada llamada se factura a esa cuenta y tú eliges el modelo.",
+      "fields": {
+        "apiKey": "Clave de API",
+        "apiKeyHelp": "Créala en console.anthropic.com, en API keys. La cuenta necesita saldo; Anthropic factura el uso por llamada."
+      },
+      "settings": {
+        "model": "Modelo",
+        "modelHelp": "Qué modelo responde. La lista se lee de tu cuenta, primero los más nuevos. Los modelos pequeños cuestan menos por llamada y bastan para la mayoría de los textos del taller."
+      }
+    },
+    "stripe": {
+      "description": "Pagos con tarjeta desde el enlace de la factura, a tu propia cuenta de Stripe. El cliente paga, Stripe avisa a Torqvoice y la factura se marca como pagada sola.",
+      "fields": {
+        "secretKey": "Clave secreta",
+        "secretKeyHelp": "Del panel de Stripe en Developers, API keys. Las claves de prueba no procesan nada real, así que prueba un pago con ellas primero y cambia a las claves reales después.",
+        "publishableKey": "Clave publicable",
+        "webhookSecret": "Secreto de firma del webhook",
+        "webhookSecretHelp": "Stripe lo muestra al añadir como webhook la URL de notificación de esta página. Con él, cada notificación se verifica antes de registrar un pago."
+      },
+      "settings": {
+        "enabled": "Ofrecer en los enlaces de factura",
+        "enabledHelp": "Desactívalo para quitar el botón de pago de las facturas compartidas durante un tiempo, sin desconectar. Los pagos ya iniciados se siguen registrando."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay desde el enlace de la factura. El cliente paga en la app que ya tiene y la factura se marca como pagada sola.",
+      "fields": {
+        "clientId": "ID de cliente",
+        "clientSecret": "Secreto de cliente",
+        "subscriptionKey": "Clave de suscripción",
+        "subscriptionKeyHelp": "La Ocp-Apim-Subscription-Key del portal de Vipps MobilePay, en las claves API de la unidad de venta.",
+        "merchantSerialNumber": "Número de serie de comerciante"
+      },
+      "settings": {
+        "enabled": "Ofrecer en los enlaces de factura",
+        "enabledHelp": "Desactívalo para quitar el botón de pago de las facturas compartidas durante un tiempo, sin desconectar. Los pagos ya iniciados se siguen registrando.",
+        "testMode": "Modo de prueba",
+        "testModeHelp": "Usar el entorno de prueba de Vipps con claves de prueba. Déjalo activado hasta que un pago completo haya funcionado, luego desactívalo e introduce las claves reales."
+      }
+    },
+    "paypal": {
+      "description": "Pago con PayPal desde el enlace de la factura, a través de tu propia app de PayPal. El cliente paga con cuenta de PayPal o tarjeta y la factura se marca como pagada sola.",
+      "fields": {
+        "clientId": "ID de cliente",
+        "clientSecret": "Secreto de cliente"
+      },
+      "settings": {
+        "enabled": "Ofrecer en los enlaces de factura",
+        "enabledHelp": "Desactívalo para quitar el botón de pago de las facturas compartidas durante un tiempo, sin desconectar. Los pagos ya iniciados se siguen registrando.",
+        "sandbox": "Modo sandbox",
+        "sandboxHelp": "Usar el sandbox de PayPal con las claves de la app de sandbox. Déjalo activado hasta que un pago completo haya funcionado, luego desactívalo e introduce las claves de la app real."
+      }
+    },
+    "quickbooks": {
+      "description": "Envía las facturas emitidas, sus clientes y sus pagos a QuickBooks Online y trae de vuelta los pagos registrados allí.",
+      "tenantHelp": "Crea una app en el portal de desarrolladores de Intuit. Copia el client id y el secret de Keys and credentials e introduce la URI de redirección de abajo en Settings, Redirect URIs. Las claves de desarrollo llegan a empresas sandbox, las de producción a empresas reales.",
+      "fields": {
+        "clientId": "ID de cliente",
+        "clientSecret": "Secreto de cliente"
+      },
+      "settings": {
+        "pushInvoices": "Enviar facturas emitidas a QuickBooks",
+        "pushOnComplete": "Enviar también las facturas cuando el trabajo se marque como completado",
+        "pushOnCompleteHelp": "Sin esto, una factura pasa cuando se envía, se comparte o se paga. Con esto, un trabajo completado pasa de inmediato y se actualiza si cambia.",
+        "startDate": "Solo facturas con fecha igual o posterior a",
+        "startDateHelp": "Déjalo vacío para enviar todas las facturas emitidas. Usa la fecha en que tu contabilidad pasó a QuickBooks para no registrar dos veces las facturas antiguas.",
+        "includeVehicle": "Indicar el vehículo en la factura",
+        "includeVehicleHelp": "Una línea de texto encima de los cargos con año, marca, modelo, matrícula y kilometraje, para que la contabilidad sepa de qué coche se trata.",
+        "laborItemId": "Artículo para líneas de mano de obra",
+        "laborItemIdHelp": "Déjalo vacío y la primera vez se creará un artículo llamado Labour en tu cuenta de ingresos.",
+        "partsItemId": "Artículo para líneas de piezas",
+        "partsItemIdHelp": "Déjalo vacío y la primera vez se creará un artículo llamado Parts en tu cuenta de ingresos.",
+        "taxAmounts": "Importes de impuestos",
+        "taxAmountsHelp": "Como se facturó envía el impuesto tal como figura en la factura que recibió el cliente, así ambos sistemas coinciden al céntimo. Calculado por QuickBooks deja que QuickBooks aplique la tasa del código de impuesto, o la dirección del cliente en EE. UU., y el registro avisa cuando los totales difieren.",
+        "taxAmountsBilled": "Como se facturó en Torqvoice",
+        "taxAmountsQuickbooks": "Calculado por QuickBooks",
+        "taxCodeId": "Código de impuesto para facturas gravadas",
+        "taxCodeIdHelp": "QuickBooks registra el impuesto bajo un código de impuesto. Torqvoice elige el código cuya tasa coincide con cada factura; este se usa cuando ninguno coincide. Una empresa de EE. UU. usa TAX salvo que elijas otro código.",
+        "zeroTaxCodeId": "Código de impuesto para facturas sin impuesto",
+        "zeroTaxCodeIdHelp": "Se usa cuando la factura no lleva impuesto o el cliente está exento. Una empresa de EE. UU. usa NON salvo que elijas otro código.",
+        "pushPayments": "Registrar pagos en QuickBooks",
+        "depositAccountId": "Depositar pagos en",
+        "depositAccountIdHelp": "Déjalo vacío para usar Undeposited Funds.",
+        "manualPaidAsPayment": "Registrar un pago cuando una factura se marca como pagada a mano",
+        "manualPaidAsPaymentHelp": "Una factura marcada como pagada en Torqvoice sin registro de pago recibe en QuickBooks un pago por lo que aún figure pendiente. Desactivado, queda abierta allí hasta que se registre un pago.",
+        "pullPayments": "Traer los pagos registrados en QuickBooks",
+        "pullPaymentsHelp": "Cada 30 minutos. Un pago recibido en QuickBooks contra una factura de Torqvoice se registra aquí en la factura."
+      }
+    },
+    "nhtsa": {
+      "description": "Rellena un vehículo a partir de su VIN y consulta llamadas a revisión, quejas de propietarios y calificaciones de choque de cada año de modelo, de la autoridad de seguridad de EE. UU. Gratis, sin clave.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Mantener al día los datos de llamadas a revisión y quejas de todos los vehículos de este taller",
+        "refreshFleetHelp": "Una vez al día se vuelven a consultar los modelos no actualizados en la última semana, para que la página del vehículo nunca espere y una nueva llamada a revisión aparezca en una semana. Solo se envían marca, modelo y año."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Órdenes de trabajo al calendario",
+      "pull": "Tiempo ocupado del calendario"
+    },
+    "conference": {
+      "sync": "Enlaces de videollamada"
+    },
+    "inspection": {
+      "refresh": "Actualización de fechas de inspección"
+    },
+    "accounting": {
+      "invoice": "Factura a contabilidad",
+      "payment": "Pago a contabilidad",
+      "customer": "Cliente a contabilidad",
+      "pull": "Pagos desde contabilidad"
+    },
+    "safety": {
+      "refresh": "Actualización de datos de llamadas a revisión",
+      "vehicle": "Comprobación de llamadas a revisión para un vehículo nuevo"
+    }
+  }
+}

+ 2 - 1
messages/es/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Programados",
       "sms": "SMS",
       "whatsapp": "WhatsApp",
-      "telegram": "Telegram"
+      "telegram": "Telegram",
+      "campaigns": "Campañas"
     },
     "smsNotConfiguredTitle": "SMS no está configurado",
     "smsNotConfiguredHint": "Conecta un proveedor de SMS para enviar y recibir mensajes aquí. El correo programado funciona igual.",

+ 5 - 1
messages/es/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "No se pudo eliminar la conversación",
     "delete": "Eliminar",
     "cancel": "Cancelar",
-    "loadMore": "Cargar más"
+    "loadMore": "Cargar más",
+    "unread": "{count} sin leer",
+    "markUnread": "Marcar como no leído",
+    "nothingToMarkUnread": "El cliente aún no ha escrito en esta conversación.",
+    "markUnreadFailed": "No se pudo marcar la conversación como no leída."
   }
 }

+ 16 - 2
messages/es/navigation.json

@@ -59,7 +59,8 @@
     "tireStorage": "Distribución del almacén",
     "tireSetDetails": "Juego de neumáticos",
     "tireForecast": "La próxima temporada",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "inspectionReminders": "Recordatorios de inspección"
   },
   "search": "Buscar...",
   "shortcut": "Ctrl+K",
@@ -117,6 +118,19 @@
     "auditLog": "Registro de auditoría",
     "aiAssistant": "Asistente IA",
     "tireHotel": "Hotel de neumáticos",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "newWorkOrder": "Nueva orden de trabajo",
+    "installApp": "Instalar la app",
+    "roles": {
+      "owner": "Propietario",
+      "admin": "Administrador",
+      "member": "Miembro"
+    },
+    "badges": {
+      "messages": "{count} mensajes sin leer",
+      "workOrders": "{count} órdenes de trabajo abiertas",
+      "inspections": "{count} inspecciones en curso",
+      "reminders": "{count} recordatorios vencidos"
+    }
   }
 }

+ 3 - 1
messages/es/onboarding.json

@@ -76,7 +76,9 @@
     "allDoneTitle": "Todo listo",
     "allDoneDescription": "Todos los pasos están completados y su taller ya está en marcha.",
     "dismiss": "Ocultar tarjeta",
-    "dismissAriaLabel": "Ocultar esta lista"
+    "dismissAriaLabel": "Ocultar esta lista",
+    "importTitle": "¿Ya tiene clientes y vehículos en algún otro sitio?",
+    "importDescription": "Impórtelos desde una hoja de cálculo o desde su sistema anterior"
   },
   "sampleData": {
     "title": "Datos de ejemplo",

+ 51 - 1
messages/es/portal.json

@@ -158,7 +158,14 @@
     "columnVehicle": "Vehículo",
     "columnPlate": "Matrícula",
     "columnServices": "Servicios",
-    "columnInspections": "Inspecciones"
+    "columnInspections": "Inspecciones",
+    "reportSold": "Ya no tengo este vehículo",
+    "reportSoldHint": "Avisaremos al taller para que deje de enviarte recordatorios sobre él. Tu historial se mantiene.",
+    "reportSoldConfirm": "Sí, avisar al taller",
+    "reportSoldCancel": "Conservar",
+    "reportSoldDone": "Gracias, el taller ha sido avisado",
+    "reportSoldFailed": "No se pudo enviar ahora",
+    "reportSoldAlready": "Has indicado al taller que este vehículo ya no es tuyo."
   },
   "requestService": {
     "title": "Solicitar servicio",
@@ -190,5 +197,48 @@
     "modelPlaceholder": "p. ej. Kuga",
     "yearLabel": "Año",
     "plateLabel": "Matrícula"
+  },
+  "booking": {
+    "title": "Reserva tu inspección",
+    "due": "Inspección el {date}",
+    "firstSlot": "Primera hora disponible",
+    "bookThis": "Reservar {when}",
+    "pickAnother": "O elige otro día",
+    "pickTime": "Elige una hora",
+    "pickTimeFirst": "Elige una hora arriba",
+    "confirmFor": "Confirmar {when}",
+    "notePlaceholder": "¿Algo que debamos saber? (opcional)",
+    "legend": "Los días tachados están completos; los ámbar tienen pocas horas.",
+    "day": {
+      "open": "Horas disponibles",
+      "limited": "Pocas horas",
+      "full": "Completo",
+      "closed": "Cerrado"
+    },
+    "booked": "Reservado para {when}",
+    "bookedHint": "Te hemos enviado una confirmación. Ven unos minutos antes con las llaves.",
+    "pending": "Solicitado para {when}",
+    "pendingHint": "El taller confirmará tu hora en breve.",
+    "changeOrCancel": "Cambiar o cancelar",
+    "expired": "Este enlace ha caducado",
+    "expiredHint": "Llámanos y buscamos una hora juntos.",
+    "unavailable": "La reserva online no está disponible ahora",
+    "unavailableHint": "Llámanos y buscamos una hora.",
+    "callUs": "Llámanos al {phone}",
+    "modeDirectNote": "Tu hora queda reservada al instante durante {minutes} minutos.",
+    "modeRequestNote": "El taller confirma la hora solicitada.",
+    "bookedToast": "Hora reservada",
+    "bookFailed": "No se pudo reservar esa hora",
+    "cancelledToast": "Reserva cancelada",
+    "cancelFailed": "No se pudo cancelar",
+    "footer": "Recibiste este enlace porque tu taller te recordó la inspección.",
+    "workOrderTitle": "Inspección periódica {vehicle}",
+    "notificationTitle": "Nueva reserva online",
+    "notificationBody": "{customer} reservó {vehicle} para {when}",
+    "cancelledNotificationTitle": "Reserva online cancelada",
+    "cancelledNotificationBody": "{customer} canceló la reserva de {vehicle} del {when}",
+    "confirmationSubject": "Tu reserva en {workshop}",
+    "confirmationBody": "Tu {vehicle} está reservado para {when} en {workshop}. ¿Necesitas cambiarlo? Usa el enlace de nuestro mensaje anterior o llama al {phone}.",
+    "confirmationBodyRequest": "Gracias, {workshop} ha recibido tu solicitud para {when} para tu {vehicle} y la confirmará en breve. ¿Dudas? Llama al {phone}."
   }
 }

+ 1 - 0
messages/es/scheduledMessages.json

@@ -41,6 +41,7 @@
     "scheduled": "Mensaje programado",
     "updated": "Mensaje programado actualizado",
     "saveError": "No se pudo guardar el mensaje",
+    "inPast": "Esa hora ya ha pasado. Elige una hora futura.",
     "whatsappNeedsTemplate": "Se enviará mucho después de que se cierre la ventana de 24 horas del cliente y no hay plantilla de texto de WhatsApp configurada, así que fallaría al enviarse. Configura una en Ajustes > Proveedores > WhatsApp."
   },
   "list": {

+ 59 - 2
messages/es/service.json

@@ -34,7 +34,8 @@
     "delete": "Eliminar",
     "unpaid": "Sin Pagar",
     "unsavedChanges": "Cambios sin guardar",
-    "saved": "Guardado"
+    "saved": "Guardado",
+    "joinCall": "Unirse a la videollamada"
   },
   "basicInfo": {
     "title": "Información Básica",
@@ -78,7 +79,17 @@
       "typePrompt": "Escriba un nombre...",
       "boardTechnicians": "Técnicos del Tablero",
       "teamMembers": "Miembros del Equipo"
-    }
+    },
+    "design": "Diseño de factura",
+    "designDefault": "Predeterminado del taller",
+    "designFollowing": "Predeterminado ({name})",
+    "designPinned": "La hoja se congeló al emitir la factura el {date}. Una nueva elección se aplica solo después de que un propietario desbloquee la factura y se envíe de nuevo.",
+    "designSaved": "Diseño de factura actualizado",
+    "designFollowingRule": "Predeterminado ({name}, {rule})",
+    "designRule": {
+      "parts_sale": "Venta solo de piezas"
+    },
+    "designSaveFailed": "No se pudo cambiar el diseño de la factura"
   },
   "parts": {
     "title": "Piezas",
@@ -349,6 +360,7 @@
     "workBay": "Puesto de trabajo",
     "selectWorkBay": "Seleccionar puesto…",
     "noWorkBay": "Sin puesto",
+    "noTechnician": "Sin técnico",
     "technicians": "Técnicos",
     "otherTeamMembers": "Otros miembros del equipo",
     "makeTechnician": "Lo convierte en técnico",
@@ -410,5 +422,50 @@
     "remove": "Quitar este punto",
     "notAnswered": "Todavía no se ha encontrado nada",
     "answered": "{count} hallazgo(s) sobre esto"
+  },
+  "videoCall": {
+    "title": "Videollamada",
+    "none": "Esta orden de trabajo no tiene videollamada.",
+    "needsSchedule": "Defina primero una hora de inicio. La reunión sigue la programación.",
+    "add": "Añadir reunión de {provider}",
+    "addedHere": "añadida desde esta orden de trabajo",
+    "fromCalendar": "del evento del calendario",
+    "join": "Unirse",
+    "copy": "Copiar enlace",
+    "copied": "Enlace copiado",
+    "remove": "Eliminar",
+    "removeTitle": "¿Eliminar la videollamada?",
+    "removeDescription": "La reunión se elimina en el proveedor. Quien ya tenga el enlace no podrá unirse.",
+    "created": "Reunión creada. El enlace está en la orden de trabajo.",
+    "removed": "Reunión eliminada",
+    "failed": "Algo salió mal",
+    "shareHint": "El cliente solo recibe este enlace cuando lo envías.",
+    "shareHintNoChannel": "El cliente solo recibe este enlace cuando lo envías. No hay ningún canal de mensajes configurado, así que cópialo tú en un mensaje.",
+    "send": {
+      "button": "Enviar al cliente",
+      "title": "Enviar el enlace de la videollamada",
+      "description": "Enviar el enlace para unirse a {name}.",
+      "messageLabel": "Nota (opcional)",
+      "messagePlaceholder": "Lo que el cliente deba saber antes de la llamada...",
+      "linkAlwaysIncluded": "El enlace para unirse se añade siempre debajo de tu nota.",
+      "sms": "SMS",
+      "email": "Correo",
+      "telegram": "Telegram",
+      "noPhone": "(sin teléfono)",
+      "noEmail": "(sin correo)",
+      "noTelegram": "(sin chat vinculado)",
+      "notAvailable": "(no configurado)",
+      "cancel": "Cancelar",
+      "send": "Enviar",
+      "sent": "Enlace de la videollamada enviado",
+      "partlySent": "Enviado por {sent}; no por {failed}",
+      "failed": "No se pudo enviar el enlace"
+    },
+    "notify": {
+      "invitation": "Hola {name}, tu videollamada sobre el {vehicle} es el {when}. Únete aquí: {url}",
+      "invitationNoTime": "Hola {name}, aquí tienes el enlace de la videollamada sobre el {vehicle}: {url}",
+      "joinLine": "Únete a la videollamada aquí: {url}",
+      "emailSubject": "Videollamada sobre tu {vehicle}"
+    }
   }
 }

+ 101 - 27
messages/es/settings.json

@@ -7,7 +7,7 @@
       "billing": "Facturación y documentos",
       "communications": "Comunicaciones",
       "workshop": "Taller",
-      "integrations": "Integraciones",
+      "integrations": "Aplicaciones y API",
       "system": "Sistema"
     },
     "items": {
@@ -91,6 +91,10 @@
         "title": "Informes programados",
         "description": "Envío automático de informes"
       },
+      "integrations": {
+        "title": "Integraciones",
+        "description": "Calendarios, videollamadas y servicios conectados"
+      },
       "webhooks": {
         "title": "Webhooks",
         "description": "Notificaciones de eventos en tiempo real"
@@ -130,6 +134,10 @@
       "providers": {
         "title": "Proveedores",
         "description": "Correo, SMS, WhatsApp y Telegram"
+      },
+      "inspectionReminders": {
+        "title": "Recordatorios de inspección",
+        "description": "Enlace de reserva, duración y antelación"
       }
     },
     "pro": "PRO"
@@ -572,7 +580,16 @@
     "quoteLockTriggerSent": "Cuando se envía el presupuesto",
     "quoteLockTriggerAccepted": "Cuando se acepta el presupuesto",
     "quoteLockTriggerSentHint": "Se bloquea en cuanto el presupuesto llega al cliente. Elija esta opción solo si nunca revisa un presupuesto durante la negociación.",
-    "quoteLockTriggerAcceptedHint": "Se bloquea cuando el cliente acepta el presupuesto o este se convierte en un trabajo. Los presupuestos enviados siguen siendo editables."
+    "quoteLockTriggerAcceptedHint": "Se bloquea cuando el cliente acepta el presupuesto o este se convierte en un trabajo. Los presupuestos enviados siguen siendo editables.",
+    "freezeTitle": "Facturas anteriores al bloqueo",
+    "freezeBody": "{count, plural, one {# factura} other {# facturas}} llegaron a un cliente antes de que esta versión pudiera bloquear lo que imprime una factura. Siguen imprimiéndose con sus datos, logotipo y diseño actuales, así que un cambio de hoy también les afecta. Bloquearlas conserva cada una tal como se imprime ahora.",
+    "freezeNone": "Todas las facturas que llegaron a un cliente están bloqueadas tal como se enviaron.",
+    "freezeButton": "Bloquearlas con el diseño actual",
+    "freezeConfirmTitle": "¿Bloquear {count, plural, one {# factura} other {# facturas}}?",
+    "freezeConfirmBody": "Cada una conserva el diseño, el logotipo, los datos del taller, el cliente, el vehículo y los hallazgos con los que se imprime ahora. No se puede deshacer, y las facturas siguen editables o bloqueadas para edición exactamente como hoy.",
+    "freezeProgress": "Bloqueadas {done} de {total}",
+    "freezeDone": "{count, plural, one {# factura bloqueada} other {# facturas bloqueadas}}",
+    "freezeFailed": "No se pudieron bloquear las facturas"
   },
   "layoutEditor": {
     "helpText": "Arrastra para reordenar. Usa el selector de ancho para colocar secciones lado a lado. Las secciones consecutivas de medio ancho (I/D) se mostrarán en dos columnas.",
@@ -634,6 +651,12 @@
       "bank_account": "Cuenta bancaria",
       "footer_note": "Nota de pie",
       "org_number": "Número de organización",
+      "payment_terms": "Condiciones de pago",
+      "due_date": "Fecha de vencimiento",
+      "title": "Título",
+      "invoice_number": "N.º de factura",
+      "customer_number": "N.º de cliente",
+      "date": "Fecha",
       "portal_link": "Enlace al portal"
     },
     "presetsTitle": "Empezar con una disposición",
@@ -808,32 +831,17 @@
     "paymentTermsPlaceholder": "Neto 14 días",
     "paymentTermsHint": "Texto de condiciones de pago en las facturas (ej. \"Neto 14 días\", \"Pago al recibir\")",
     "onlinePayments": "Pagos en línea",
-    "onlinePaymentsDescription": "Permita a los clientes pagar facturas en línea a través de enlaces de factura compartidos.",
-    "stripe": "Stripe",
-    "stripeDescription": "Aceptar pagos con tarjeta de credito/debito",
-    "secretKey": "Clave secreta",
-    "publishableKey": "Clave publica",
-    "webhookSecret": "Secreto del webhook",
-    "webhookUrl": "URL del webhook",
-    "stripeWebhookHint": "Agregue esta URL en su Panel de Stripe en Webhooks. Suscribase a <code>checkout.session.completed</code>.",
-    "vipps": "Vipps",
-    "vippsDescription": "Aceptar pagos moviles Vipps (Noruega)",
-    "clientId": "ID de cliente",
-    "clientSecret": "Secreto del cliente",
-    "subscriptionKey": "Clave de suscripción",
-    "merchantSerialNumber": "Número de serie del comerciante",
-    "testMode": "Modo de prueba",
-    "callbackUrl": "URL de callback",
-    "termsOfSale": "Condiciones de venta (Salgsvilkår)",
-    "termsOfSaleDescription": "Requerido por Vipps. Los clientes pueden verlas desde la página de factura.",
+    "onlinePaymentsDescription": "Deja que los clientes paguen facturas desde el enlace que les envías. Stripe, Vipps y PayPal se conectan en Integraciones y pueden funcionar a la vez.",
+    "onlinePaymentsHint": "Cuando un cliente paga, el proveedor avisa a Torqvoice y la factura se marca como pagada sola. La página de cada proveedor muestra la URL de notificación que necesita.",
+    "offered": "En enlaces de factura",
+    "paused": "Conectado, no ofrecido",
+    "termsOfSale": "Condiciones de venta",
+    "termsOfSaleDescription": "Lo que un cliente acepta al pagar en línea. Vipps exige condiciones publicadas; la factura enlaza a ellas en cualquier caso.",
+    "termsOfSaleText": "Texto de las condiciones",
     "externalTermsUrl": "URL de condiciones externas (opcional)",
     "externalTermsUrlHint": "Enlace a una página externa con sus condiciones. Si se establece, la factura enlazara aquí.",
     "publicTermsUrl": "URL publica de condiciones",
     "publicTermsUrlHint": "Comparta esta URL con Vipps o enlacela desde su sitio web.",
-    "paypal": "PayPal",
-    "paypalDescription": "Aceptar pagos PayPal",
-    "sandboxMode": "Modo sandbox",
-    "paypalWebhookHint": "Agregue esta URL en su Panel de Desarrollador de PayPal en Webhooks. Suscribase a <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "savePayment": "Guardar configuración de pago",
     "saved": "Configuración de pago guardada"
   },
@@ -1112,7 +1120,7 @@
   },
   "data": {
     "title": "Gestión de datos",
-    "description": "Exporte sus datos como respaldo o importe un archivo previamente exportado.",
+    "description": "Traiga datos de su sistema anterior, descargue una copia de seguridad de todo o restaure una.",
     "exportTitle": "Exportar datos",
     "exportDescription": "Descargue sus datos como archivo zip. Seleccione las categorías a incluir en el respaldo.",
     "includeInBackup": "Incluir en el respaldo:",
@@ -1199,7 +1207,16 @@
     "optTireHotel": "Hotel de neumáticos",
     "optTireHotelDesc": "Juegos guardados, estantes, preparación y contratos de almacenaje",
     "optWorkshopConfig": "Configuración del taller",
-    "optWorkshopConfigDesc": "Plantillas de mano de obra, roles, webhooks e informes programados"
+    "optWorkshopConfigDesc": "Plantillas de mano de obra, roles, webhooks e informes programados",
+    "importHubDescription": "Elija dónde están sus datos hoy. Las importaciones desde hojas de cálculo y aplicaciones se añaden a lo que tiene y se pueden deshacer; restaurar una copia de seguridad lo reemplaza todo.",
+    "sourceSpreadsheet": "Hoja de cálculo o CSV",
+    "sourceSpreadsheetDesc": "Exportaciones de cualquier sistema de taller, programa contable, Contactos de Google, Outlook o Excel. Confirma la asignación de columnas y ve una prueba antes de que se escriba nada.",
+    "sourceLubelogDesc": "Vehículos, registros de servicio y notas de una copia de seguridad de LubeLog.",
+    "sourceInvoiceNinjaDesc": "Clientes, productos, facturas y pagos de una exportación de Invoice Ninja.",
+    "sourceBackup": "Copia de seguridad de Torqvoice",
+    "sourceBackupDesc": "Un archivo .zip o .json descargado desde esta página. Restaurar reemplaza todo en este taller.",
+    "restore": "Restaurar",
+    "restoreTitle": "Restaurar una copia de seguridad"
   },
   "license": {
     "title": "Licencia marca blanca",
@@ -1740,6 +1757,17 @@
         "feature": "Mensajería Telegram",
         "description": "Envía y recibe mensajes de Telegram con tus clientes desde Torqvoice."
       }
+    },
+    "moved": {
+      "title": "These settings moved to Integrations",
+      "description": "Email, SMS, WhatsApp and Telegram are set up in Integrations now, alongside calendars and everything else you connect. What you had set up came with them, so there is nothing to enter again.",
+      "connectedTo": "Connected through {vendor}",
+      "notSetUp": "Not set up yet",
+      "manage": "Manage",
+      "notConnected": "Not connected",
+      "goToIntegrations": "Go to Integrations",
+      "platformMail": "Sending through the platform mail server. Connect your own to send from your address.",
+      "onByDefault": "On by default"
     }
   },
   "designer": {
@@ -1749,6 +1777,14 @@
     "galleryTitle": "Empieza con una plantilla",
     "galleryHint": "Elige un punto de partida. Puedes cambiarlo todo en el diseñador.",
     "yourDesigns": "Tus diseños",
+    "autoRuleHint": "Las ventas sin vehículo se imprimen con el diseño marcado para ellas, salvo que la venta o su cliente hayan elegido otro.",
+    "useForPartsSale": "Usar para ventas solo de piezas",
+    "usedForPartsSale": "Se usa para ventas solo de piezas",
+    "autoRule": {
+      "parts_sale": "Venta solo de piezas"
+    },
+    "autoRuleSaved": "Uso de «{name}» actualizado",
+    "couldNotSaveRule": "No se pudo actualizar la regla",
     "templates": "Plantillas",
     "inUse": "En uso",
     "savedOn": "Guardado el {date}",
@@ -1908,6 +1944,7 @@
       "serviceType": "Reparación",
       "companyName": "Tu taller",
       "slogan": "Calidad de servicio en la que puedes confiar",
+      "paymentTerms": "Pago en 14 días",
       "footerNote": "Gracias por confiar en nosotros",
       "value": "Ejemplo"
     },
@@ -1923,11 +1960,22 @@
     "sloganPlaceholderHint": "No hay ningún eslogan definido, así que la hoja muestra un texto de ejemplo que no se imprimirá.",
     "sloganSetHint": "Tu eslogan proviene de los datos de tu empresa.",
     "sloganLink": "Ajustes de empresa",
+    "paymentTerms": "Condiciones de pago",
+    "paymentTermsPlaceholderHint": "No hay condiciones de pago definidas, así que la hoja muestra un texto de ejemplo que no se imprimirá.",
+    "paymentTermsSetHint": "Tus condiciones de pago provienen de los ajustes de pago.",
+    "paymentTermsLink": "Ajustes de pago",
+    "titleText": "Texto del título",
+    "titleTextHint": "Cómo se llama este documento. Si lo dejas vacío, imprime su propio nombre en el idioma del lector.",
     "placeholder": "Marcador",
     "leaveTitle": "¿Salir sin guardar?",
     "leaveBody": "Los cambios de este diseño no se han guardado y se perderán.",
     "leaveConfirm": "Salir",
-    "boldField": "Negrita"
+    "boldField": "Negrita",
+    "deleteDesignDrafts": "{count, plural, one {# borrador de factura} other {# borradores de factura}} que lo usan volverán al diseño predeterminado.",
+    "deleteDesignCustomers": "{count, plural, one {# cliente} other {# clientes}} asignados a él volverán al diseño predeterminado.",
+    "setDefault": "Establecer como predeterminado",
+    "defaultSet": "«{name}» está ahora en uso",
+    "couldNotSetDefault": "No se pudo establecer el diseño predeterminado"
   },
   "preview": {
     "title": "Servicio de frenos y cambio de aceite",
@@ -1942,5 +1990,31 @@
     "partSyntheticOil": "Aceite sintético 5W-30",
     "laborBrakeReplacement": "Sustitución de frenos",
     "laborOilChange": "Cambio de aceite"
+  },
+  "inspectionReminders": {
+    "title": "Recordatorios de inspección y reservas",
+    "description": "Cómo se comporta el enlace de reserva de un recordatorio. Se define una vez; todas las campañas lo usan.",
+    "duration": "Duración de la inspección (minutos)",
+    "durationHint": "Cuánto ocupa una inspección periódica un puesto. El enlace reserva citas de esta duración.",
+    "leadDays": "Reserva más temprana (días)",
+    "leadDaysHint": "1 significa que la primera hora ofrecida es mañana, nunca hoy.",
+    "horizonWeeks": "Hasta cuándo (semanas)",
+    "horizonHint": "Los clientes pueden elegir cualquier hora libre dentro de estas semanas.",
+    "reserve": "Reservar para clientes sin cita",
+    "reserveHint": "Puestos o técnicos que el enlace nunca ocupa. 0 permite usar todos los libres.",
+    "linkValidDays": "El enlace sigue abierto (días)",
+    "linkValidHint": "Al menos {min}. Los enlaces también siguen abiertos hasta una semana después del vencimiento, lo que sea más tarde.",
+    "bookingMode": "Qué crea una reserva",
+    "bookingModeDirect": "Una orden de trabajo programada en el tablero",
+    "bookingModeRequest": "Una solicitud que la oficina confirma primero",
+    "bookingModeHint": "La reserva directa es lo que hace que merezca la pena pulsar el enlace. Elige solicitud si quieres aprobar cada vez.",
+    "phone": "Teléfono en los mensajes",
+    "phoneHint": "Se muestra como alternativa al enlace. Por defecto, el teléfono del taller.",
+    "save": "Guardar",
+    "saved": "Ajustes guardados",
+    "saveFailed": "No se pudo guardar",
+    "timeZoneDetected": "La zona horaria está en automático en Localización, así que el servidor usa {zone}, la última vista en un navegador que guardó ajustes. Elige la zona del taller explícitamente para que las horas de reserva nunca dependan de quién guardó último.",
+    "timeZoneExplicit": "Las horas de reserva y el horario se leen en {zone}.",
+    "timeZoneChange": "Cambiar en Localización"
   }
 }

+ 217 - 3
messages/es/vehicles.json

@@ -4,6 +4,7 @@
     "archived": "Archivados",
     "searchPlaceholder": "Buscar por placa, marca, modelo, VIN...",
     "addVehicle": "Agregar vehículo",
+    "importVehicles": "Importar",
     "emptySearch": "Ningún vehículo coincide con su búsqueda.",
     "emptyArchived": "No hay vehículos archivados.",
     "empty": "Aún no hay vehículos.",
@@ -30,7 +31,12 @@
     "viewTable": "Table view",
     "viewGrid": "Grid view",
     "viewLargeGrid": "Large grid view",
-    "openMenu": "Open menu"
+    "openMenu": "Open menu",
+    "inspectionOverdue": "ITV vencida",
+    "inspectionDue30": "Vence en 30 días",
+    "inspectionDue90": "Vence en 90 días",
+    "inspectionDue": "ITV {date}",
+    "remindCustomers": "Recordar a estos clientes"
   },
   "detail": {
     "vehicleArchived": "Este vehículo esta archivado",
@@ -90,7 +96,11 @@
     "severityUncommon": "Poco común — reportado por pocos propietarios",
     "severityRare": "Raro — reportes aislados pero crítico cuando ocurre",
     "openMenu": "Open menu",
-    "dismissMaintenance": "Dismiss"
+    "dismissMaintenance": "Dismiss",
+    "inspectionDue": "ITV el {date}",
+    "inspectionCheckedAt": "Comprobado en el registro el {date}",
+    "notRegistered": "Sin matricular",
+    "soldReported": "El cliente indicó venta el {date}"
   },
   "quotes": {
     "newQuote": "Nueva cotización",
@@ -130,6 +140,7 @@
     "descriptionLabel": "Descripción",
     "descriptionPlaceholder": "Detalles opcionales...",
     "dueDateLabel": "Fecha de vencimiento",
+    "dueTimeLabel": "Hora (opcional)",
     "dueMileageLabel": "Kilometraje de vencimiento",
     "dueMileagePlaceholder": "ej. 50000",
     "reminderUpdated": "Recordatorio actualizado",
@@ -338,7 +349,20 @@
     "sectionAssignment": "Foto y titular",
     "sectionDetails": "Detalles",
     "addScannedOwner": "Añadir el cliente {name}",
-    "ownerSaveFailed": "No se pudo crear el cliente"
+    "ownerSaveFailed": "No se pudo crear el cliente",
+    "lookupPlate": "Buscar matrícula",
+    "lookingUp": "Consultando el registro de vehículos...",
+    "lookupSuccess": "Campos rellenados desde {source}",
+    "lookupNotFound": "El registro no tiene ningún vehículo con esa matrícula",
+    "lookupFailed": "No se pudo buscar la matrícula",
+    "lookupUnavailable": "Conecta un registro de vehículos en Ajustes → Integraciones",
+    "lookupEnterPlate": "Escribe primero una matrícula",
+    "lookupInspectionDue": "{source}: próxima inspección periódica el {date}",
+    "lookupSource": "Rellenado desde {source}",
+    "inspectionDue": "Próxima inspección periódica",
+    "inspectionDueHint": "Escribe la fecha si ningún registro la aporta. Los recordatorios y el panel la usan.",
+    "inspectionDueFromRegistry": "Tomada del registro de vehículos. Cámbiala aquí para anularla hasta la próxima sincronización.",
+    "lookupEnterPlateOrVin": "Escribe primero una matrícula o un VIN"
   },
   "archiveDialog": {
     "title": "Archivar vehículo",
@@ -353,5 +377,195 @@
     "previousPage": "Previous page",
     "nextPage": "Next page",
     "lastPage": "Last page"
+  },
+  "inspectionReminders": {
+    "title": "Recordatorios de inspección",
+    "description": "Avisa a los clientes cuya inspección periódica se acerca, con un enlace para reservar hora. No se envía nada hasta que confirmes la lista.",
+    "window": "Vencimiento en",
+    "window30": "Próximos 30 días",
+    "window60": "Próximos 60 días",
+    "window90": "Próximos 90 días",
+    "channel": "Enviar por",
+    "channels": {
+      "sms": "SMS",
+      "whatsapp": "WhatsApp",
+      "email": "Correo",
+      "telegram": "Telegram"
+    },
+    "willReceive": "recibirán un mensaje",
+    "excludedCount": "{count} excluidos",
+    "nobodyToRemind": "Nadie a quien recordar en este periodo. Cada vehículo con vencimiento ya fue avisado, tiene reserva o no se puede contactar.",
+    "overdueOn": "Vencida desde {date}",
+    "excludedTitle": "No incluidos",
+    "excludedDescription": "Cada uno tiene un motivo. Corrígelo en el cliente o el vehículo y aparecerá en la lista.",
+    "reasons": {
+      "noCustomer": "Sin cliente",
+      "optedOut": "Rechaza recordatorios",
+      "noRecipient": "Sin contacto para este canal",
+      "alreadyReminded": "Ya avisado para este vencimiento",
+      "cooldown": "Contactado hace poco",
+      "soldReported": "El cliente indicó que lo vendió",
+      "notRegistered": "Sin matricular",
+      "booked": "Ya reservado"
+    },
+    "message": "Mensaje",
+    "subject": "Asunto",
+    "body": "Texto",
+    "placeholders": "Marcadores: {customerName} {vehicle} {plate} {dueDate} {workshopName} {bookingLink} {phone}",
+    "unknownPlaceholders": "Marcadores desconocidos: {list}",
+    "sample": "Ejemplo para el primer destinatario",
+    "segments": "{count, plural, one {# segmento SMS} other {# segmentos SMS}}",
+    "bookingModeDirectNote": "El enlace reserva una cita de {minutes} minutos directamente en el tablero.",
+    "bookingModeRequestNote": "El enlace envía una solicitud con la hora elegida para que la oficina la confirme.",
+    "linkValidNote": "Los enlaces siguen abiertos al menos {days} días y hasta después del vencimiento.",
+    "sendTest": "Enviarme una prueba",
+    "testSent": "Prueba en cola para {to}",
+    "testFailed": "No se pudo enviar la prueba",
+    "sendButton": "Enviar a {count}",
+    "confirmTitle": "¿Enviar los recordatorios?",
+    "confirmDescription": "Se enviarán {count} mensajes por {channel}, uno por vehículo, una sola vez. Un vehículo nunca recibe dos avisos para el mismo vencimiento.",
+    "confirmInput": "Escribe {count} para confirmar",
+    "cancel": "Cancelar",
+    "sendNow": "Enviar",
+    "created": "{count} recordatorios en cola",
+    "sendFailed": "No se pudo crear la campaña",
+    "loadFailed": "No se pudo cargar la lista",
+    "campaign": {
+      "title": "Campaña del {date}",
+      "summary": "{channel}, vencimientos en {window} días",
+      "recipients": "Destinatarios",
+      "sent": "Enviados",
+      "failed": "Fallidos",
+      "booked": "Reservados",
+      "retryFailedButton": "Reintentar {count} fallidos",
+      "retried": "{count} devueltos a la cola",
+      "retryFailed": "No se pudo reintentar",
+      "rows": "Destinatarios",
+      "status": {
+        "scheduled": "En cola",
+        "sent": "Enviado",
+        "failed": "Fallido",
+        "cancelled": "Cancelado"
+      },
+      "bookedOn": "Reservado {date}",
+      "requestedOn": "Solicitado {date}",
+      "cancelledOn": "Cancelado {date}",
+      "none": "Aún no hay campañas.",
+      "start": "Enviar recordatorios de inspección",
+      "recipientsCount": "{count} destinatarios",
+      "failedCount": "{count} fallidos",
+      "bookedCount": "{count} reservados"
+    },
+    "defaults": {
+      "smsBody": "Hola {customerName}, la inspección periódica de tu {vehicle} ({plate}) vence el {dueDate}. Reserva hora: {bookingLink} o llámanos al {phone}. {workshopName}",
+      "emailSubject": "Inspección el {dueDate} para {vehicle}",
+      "emailBody": "Hola {customerName},\n\nLa inspección periódica de tu {vehicle} ({plate}) vence el {dueDate}.\n\nReserva aquí la hora que te venga bien: {bookingLink}\n\nO llámanos al {phone}.\n\nUn saludo,\n{workshopName}"
+    },
+    "refresh": "Actualizar lista",
+    "timeZoneRequired": "La zona horaria del taller está en automático, así que las horas de reserva seguirían al último navegador que guardó ajustes (ahora {zone}). No se pueden enviar recordatorios hasta elegir una zona.",
+    "timeZoneRequiredLink": "Elígela en Localización"
+  },
+  "plateLookup": {
+    "open": "Buscar matrícula",
+    "shortcut": "Ctrl+Shift+L",
+    "title": "Buscar matrícula",
+    "description": "Escribe una matrícula para ver lo que tu taller y el registro de vehículos saben de ella",
+    "placeholder": "Escribe una matrícula",
+    "search": "Buscar",
+    "hintEnter": "Enter para buscar",
+    "hintClose": "Esc para cerrar",
+    "recent": "Recientes",
+    "clearRecent": "Borrar",
+    "looking": "Consultando {source}…",
+    "lookingWorkshop": "Comprobando tu taller…",
+    "invalid": "Eso no parece una matrícula",
+    "inWorkshop": "En tu taller",
+    "inWorkshopArchived": "En tu taller, archivado",
+    "notInWorkshop": "Aún no está en tu taller",
+    "notInWorkshopHint": "El registro conoce este vehículo. Añádelo y los campos de abajo vienen con él.",
+    "notInWorkshopNoRegistry": "Añádelo a mano, o revisa la matrícula.",
+    "openVehicle": "Abrir vehículo",
+    "addVehicle": "Añadir vehículo",
+    "noOwner": "Sin cliente vinculado",
+    "mileage": "{km} km",
+    "services": "{count, plural, =0 {Sin registros de servicio} one {# registro de servicio} other {# registros de servicio}}",
+    "lastService": "Último servicio {date}",
+    "registryNotFound": "{source} no tiene ningún vehículo con esa matrícula",
+    "registryFailed": "No se pudo consultar {source}",
+    "registryFailedGeneric": "No se pudo consultar el registro",
+    "fromSource": "De {source}",
+    "registered": "Matriculado",
+    "notRegistered": "No matriculado",
+    "vin": "VIN",
+    "copyVin": "Copiar VIN",
+    "copied": "Copiado",
+    "fuel": "Combustible",
+    "transmission": "Caja de cambios",
+    "engine": "Motor",
+    "engineCode": "Código de motor",
+    "color": "Color",
+    "vehicleClass": "Clase",
+    "firstRegistered": "Primera matriculación",
+    "inspectionDue": "Inspección vence",
+    "lastInspected": "Última inspección",
+    "weights": "Pesos",
+    "weightsValue": "{kerb} kg en vacío, {gross} kg máx.",
+    "kerbOnly": "{kerb} kg en vacío",
+    "grossOnly": "{gross} kg máx.",
+    "tyres": "Neumáticos",
+    "axle": "Eje {n}",
+    "tyreSize": "Neumático",
+    "rimSize": "Llanta",
+    "loadSpeed": "Carga / velocidad",
+    "overdue": "Vencida",
+    "dueSoon": "Pronto",
+    "emptyTitle": "Busca cualquier matrícula",
+    "emptyBody": "Ve el vehículo, quién es su dueño aquí y lo que {source} tiene registrado. ¿Aún no es tuyo? Añádelo en un paso.",
+    "registryWord": "el registro"
+  },
+  "safety": {
+    "title": "Seguridad y problemas conocidos",
+    "summaryRecalls": "{count, plural, one {# llamada a revisión} other {# llamadas a revisión}}",
+    "summaryComplaints": "{count, plural, one {# queja de propietario} other {# quejas de propietarios}}",
+    "expand": "Ver detalles",
+    "collapse": "Ocultar detalles",
+    "loading": "Consultando a la autoridad de seguridad...",
+    "from": "De {source} para {model}, actualizado el {date}",
+    "stale": "No se pudo contactar con {source}; se muestra el informe del {date}",
+    "notKnown": "{source} no tiene registro de un {model}.",
+    "notKnownHint": "Comprueba marca, modelo y año, o añade el VIN para que el modelo se identifique con exactitud.",
+    "recalls": "Llamadas a revisión",
+    "recallsNone": "Sin campañas de revisión",
+    "complaints": "Quejas de propietarios",
+    "complaintsNone": "Ninguna presentada",
+    "crashes": "{count, plural, one {# choque} other {# choques}}",
+    "fires": "{count, plural, one {# incendio} other {# incendios}}",
+    "rating": "Calificación de choque",
+    "notRated": "Sin calificar",
+    "stars": "{count} de 5 estrellas",
+    "frontal": "Frontal",
+    "side": "Lateral",
+    "rollover": "Vuelco",
+    "ownersReport": "Lo que reportan los propietarios",
+    "ofComplaints": "de {total} quejas presentadas",
+    "crash": "Choque",
+    "fire": "Incendio",
+    "noExamples": "Los textos de las quejas aún no están en este informe. Actualiza desde la fuente para obtenerlos.",
+    "readAll": "Leer las {count} en {source}",
+    "showRecalls": "{count, plural, one {Mostrar # llamada a revisión} other {Mostrar # llamadas a revisión}}",
+    "hideRecalls": "Ocultar llamadas a revisión",
+    "more": "Más",
+    "less": "Menos",
+    "consequence": "Riesgo",
+    "remedy": "Solución",
+    "parkIt": "No conducir",
+    "parkOutside": "Aparcar al aire libre",
+    "addFinding": "Añadir a observaciones",
+    "findingAdded": "En observaciones",
+    "findingDescription": "Llamada a revisión {campaign}: {component}",
+    "refresh": "Actualizar desde la fuente",
+    "refreshed": "Datos de seguridad actualizados",
+    "openSite": "Abrir en {source}",
+    "errorLoad": "No se pudieron cargar los datos de seguridad"
   }
 }

+ 1 - 1
messages/fr/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Demandez à propos de votre atelier...",
   "clear": "Effacer la conversation",
   "error": "Une erreur s'est produite. Veuillez réessayer.",
-  "notEnabled": "L'IA n'est pas activée. Configurez-la dans Paramètres → IA.",
+  "notEnabled": "L'IA n'est pas connectée. Connectez OpenAI ou Anthropic dans Paramètres → Intégrations.",
   "suggestion1": "Quels véhicules doivent bientôt être révisés ?",
   "suggestion2": "Montrez-moi les factures impayées",
   "suggestion3": "Quel est le chiffre d'affaires ce mois-ci ?",

+ 27 - 2
messages/fr/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "Numéro WhatsApp enregistré",
     "team_giveTechnicianTheApp": "Compte donné à un technicien du tableau",
     "team_createDefaultRoles": "Rôles standard créés",
-    "settings_setCustomFieldPlacement": "Emplacement de champ personnalisé mis à jour"
+    "settings_setCustomFieldPlacement": "Emplacement de champ personnalisé mis à jour",
+    "import_commit": "Feuille de calcul importée",
+    "import_undo": "Import annulé",
+    "integration_connect": "Intégration connectée",
+    "integration_disconnect": "Intégration déconnectée",
+    "service_setInvoiceDesign": "Design de facture modifié",
+    "settings_saveDocumentDesign": "Design de document enregistré",
+    "settings_deleteDocumentDesign": "Design de document supprimé",
+    "settings_applyDocumentDesign": "Design de document par défaut défini",
+    "settings_setDocumentDesignRule": "A modifié quand un design est utilisé",
+    "settings_freezeInvoices": "Anciennes factures verrouillées",
+    "inspection_reminders_sent": "Rappels de contrôle envoyés",
+    "inspection_reminders_send": "Rappels de contrôle envoyés",
+    "service_videoCall_send": "Lien d'appel vidéo envoyé"
   },
   "summary": {
     "customField_create": "Champ personnalisé « {name} » créé",
@@ -275,6 +288,18 @@
     "work_bay_create": "Poste de travail « {name} » créé",
     "work_bay_update": "Poste de travail « {name} » mis à jour",
     "work_bay_delete": "Poste de travail {id} supprimé",
-    "settings_setCustomFieldPlacement": "A modifié l'emplacement d'impression d'un champ personnalisé"
+    "settings_setCustomFieldPlacement": "A modifié l'emplacement d'impression d'un champ personnalisé",
+    "import_commit": "Feuille de calcul importée : {count} enregistrements créés",
+    "import_undo": "Import de feuille de calcul annulé : {records} enregistrements supprimés",
+    "integration_connect": "{name} connecté",
+    "integration_disconnect": "{name} déconnecté",
+    "service_setInvoiceDesign": "A changé le design avec lequel une facture est imprimée",
+    "settings_saveDocumentDesign": "A enregistré le design de document {name}",
+    "settings_deleteDocumentDesign": "A supprimé le design de document {name}",
+    "settings_applyDocumentDesign": "A défini {name} comme design de document par défaut",
+    "settings_setDocumentDesignRule": "A modifié quand le design de document {name} est utilisé",
+    "settings_freezeInvoices": "A verrouillé {count, plural, one {# facture} other {# factures}} antérieures au verrouillage avec le design actuel",
+    "inspection_reminders_sent": "{count} rappels de contrôle envoyés",
+    "service_videoCall_send": "A envoyé le lien de l'appel vidéo par {channels}"
   }
 }

+ 2 - 1
messages/fr/auth.json

@@ -13,7 +13,8 @@
     "errors": {
       "tooManyAttempts": "Trop de tentatives, veuillez réessayer plus tard.",
       "invalidCredentials": "Adresse e-mail ou mot de passe invalide",
-      "passkeyFailed": "Échec de la connexion par passkey. Réessayez ou utilisez l'email et le mot de passe."
+      "passkeyFailed": "Échec de la connexion par passkey. Réessayez ou utilisez l'email et le mot de passe.",
+      "invalidOrigin": "Vous avez ouvert Torqvoice à l'adresse {origin}, mais il est configuré pour {configured}. Définissez NEXT_PUBLIC_APP_URL sur l'adresse exacte que vous utilisez dans le navigateur, puis redémarrez Torqvoice."
     },
     "resetPasswordCta": "Réinitialisez-le ici"
   },

+ 74 - 7
messages/fr/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Aujourd’hui",
   "workOrder": "Ordre de travail",
+  "allDay": "Toute la journée",
+  "openDay": "Ouvrir le {date}",
+  "dayTotal": "{count, plural, =1 {1 événement} other {# événements}}",
+  "views": {
+    "day": "Jour",
+    "week": "Semaine",
+    "month": "Mois",
+    "year": "Année",
+    "schedule": "Planning",
+    "fourDays": "4 jours"
+  },
+  "options": {
+    "showWeekends": "Afficher les week-ends",
+    "showCompleted": "Afficher les terminés",
+    "showWeekNumbers": "Afficher les numéros de semaine"
+  },
+  "toolbar": {
+    "previous": "Précédent",
+    "next": "Suivant",
+    "create": "Créer",
+    "showSidebar": "Afficher le panneau latéral",
+    "hideSidebar": "Masquer le panneau latéral"
+  },
+  "sidebar": {
+    "calendars": "Calendriers",
+    "legend": "Couleurs des statuts"
+  },
+  "schedule": {
+    "empty": "Rien de prévu",
+    "emptyHint": "Aucun ordre de travail, rappel, devis ni message sur cette période."
+  },
+  "peek": {
+    "close": "Fermer",
+    "open": {
+      "service": "Ouvrir l'ordre de travail",
+      "reminder": "Ouvrir le rappel",
+      "quote": "Ouvrir le devis",
+      "message": "Ouvrir les messages",
+      "external": "Ouvrir"
+    }
+  },
   "filters": {
-    "services": "Services",
+    "services": "Ordres de travail",
     "reminders": "Rappels",
     "quotes": "Devis",
-    "messages": "Messages"
+    "messages": "Messages",
+    "external": "Calendriers connectés"
   },
   "selectVehicle": "Sélectionner un véhicule pour l’ordre de travail",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Use today"
   },
   "more": "+ {count} de plus",
-  "monthTotal": "{count} événements ce mois-ci",
+  "monthTotal": "{count} événements",
   "contextMenu": {
     "newWorkOrder": "Nouvel ordre de travail",
+    "newWorkOrderAt": "Nouvel ordre de travail à {time}",
     "newReminder": "Nouveau rappel",
     "newQuote": "Nouveau devis",
     "openEvent": "Ouvrir ({count})",
     "hint": "Astuce : faites un clic droit sur un jour pour voir les actions",
     "scheduleMessage": "Programmer un message"
   },
+  "eventMenu": {
+    "open": "Ouvrir",
+    "markDone": "Marquer comme fait",
+    "markNotDone": "Marquer comme non fait",
+    "deleteReminder": "Supprimer le rappel",
+    "deleteReminderConfirm": "Supprimer le rappel \"{title}\" ? Cette action est irréversible.",
+    "deleteMessage": "Supprimer le message programmé",
+    "deleteMessageConfirm": "Supprimer le message programmé \"{title}\" ? Il ne sera pas envoyé.",
+    "deleted": "Supprimé",
+    "updated": "Mis à jour",
+    "failed": "Cela n'a pas fonctionné. Réessayez."
+  },
   "events": {
     "status": {
       "completed": "Terminé",
       "inProgress": "En cours",
       "waitingParts": "En attente de pièces",
       "pending": "En attente",
+      "scheduled": "Planifié",
       "sent": "Envoyé",
       "approved": "Approuvé",
       "draft": "Brouillon",
@@ -66,16 +122,27 @@
       "overdue": "En retard",
       "upcoming": "À venir",
       "failed": "Échec",
-      "queued": "En file"
+      "queued": "En file",
+      "busy": "Occupé"
     },
     "type": {
-      "service": "Service",
+      "service": "Ordre de travail",
       "reminder": "Rappel",
       "quote": "Devis",
-      "message": "Message"
+      "message": "Message",
+      "external": "Calendrier connecté"
     },
     "noEvents": "Aucun événement",
-    "selectDay": "Sélectionnez un jour avec des événements ou créez un nouvel ordre de travail."
+    "selectDay": "Sélectionnez un jour avec des événements ou créez un nouvel ordre de travail.",
+    "external": {
+      "source": "Depuis {source}",
+      "sourceUnknown": "Depuis un calendrier connecté",
+      "allDay": "Toute la journée",
+      "open": "Ouvrir dans {source}",
+      "openGeneric": "Ouvrir dans le calendrier",
+      "noLink": "Ce calendrier ne fournit pas de lien vers l'événement.",
+      "readOnly": "Créneau occupé venant d'un calendrier connecté. Modifiez-le là-bas, il sera mis à jour ici à la prochaine synchronisation."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/fr/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Numéro de téléphone copié dans le presse-papiers",
     "taxIdLabel": "N° TVA",
     "taxExemptBadge": "Exonéré de taxe",
+    "invoiceDesignBadge": "Design de facture : {name}",
+    "invoiceDesignBadgeHint": "Les factures de ce client s'impriment avec ce design au lieu de celui par défaut de l'atelier. Ouvrir dans le designer.",
     "tabs": {
       "vehicles": "Véhicules ({count})",
       "invoices": "Factures ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Auto",
     "sectionContact": "Contact",
     "sectionBilling": "Facturation",
-    "addScannedVehicle": "Ajouter le véhicule {vehicle}"
+    "addScannedVehicle": "Ajouter le véhicule {vehicle}",
+    "invoiceDesign": "Design de facture",
+    "invoiceDesignDefault": "Défaut de l'atelier",
+    "invoiceDesignHint": "Les factures de ce client sont imprimées avec ce design au lieu du défaut de l'atelier. Une facture peut toujours choisir le sien.",
+    "reminderOptOut": "Pas de rappels",
+    "reminderOptOutHint": "Ne pas inclure ce client dans les rappels de contrôle ni les autres campagnes."
   },
   "serviceRequests": {
     "statusNew": "Nouveau",
@@ -117,44 +124,5 @@
     "notesSaveError": "Échec de l'enregistrement des notes",
     "smsLabel": "Message de demande de service",
     "smsDefault": "Bonjour {name}, concernant votre demande de service pour votre {vehicle} : "
-  },
-  "import": {
-    "title": "Importer des clients",
-    "description": "Importer des clients depuis des fichiers CSV, TSV ou vCard.",
-    "dropFile": "Glissez-déposez un fichier ici",
-    "supportedFormats": "CSV, TSV, TXT ou vCard (.vcf)",
-    "browse": "Parcourir les fichiers",
-    "noDataFound": "Aucune donnée client trouvée dans le fichier",
-    "rowsFound": "{count} client(s) trouvé(s)",
-    "colName": "Nom",
-    "colEmail": "E-mail",
-    "colPhone": "Téléphone",
-    "colCompany": "Entreprise",
-    "colAddress": "Adresse",
-    "chooseAnother": "Choisir un autre fichier",
-    "cancel": "Annuler",
-    "importButton": "Importer {count} client(s)",
-    "importComplete": "Importation terminée",
-    "importedCount": "{count} client(s) importé(s) avec succès",
-    "skippedRows": "{count} ligne(s) ignorée(s) en raison d'erreurs",
-    "rowError": "Ligne {row} ({name}) : {error}",
-    "done": "Terminé",
-    "importFailed": "Échec de l'importation des clients",
-    "checkingDuplicates": "Vérification des doublons...",
-    "duplicatesFound": "{count} doublon(s) trouvé(s)",
-    "duplicatesBanner": "{count} client(s) existant(s) correspondent à vos données d'importation.",
-    "duplicatesHint": "Cliquez sur Ignorer ou Fusionner pour changer le traitement de chaque doublon.",
-    "colStatus": "Statut",
-    "actionSkip": "Ignorer",
-    "actionMerge": "Fusionner",
-    "actionNew": "Nouveau",
-    "summaryNew": "{count} nouveau(x)",
-    "summaryMerge": "{count} fusionner",
-    "summarySkip": "{count} ignorer",
-    "skipAll": "Tout ignorer",
-    "mergeAll": "Tout fusionner",
-    "resultCreated": "{count} client(s) créé(s)",
-    "resultMerged": "{count} client(s) fusionné(s)",
-    "resultSkipped": "{count} client(s) ignoré(s)"
   }
 }

+ 12 - 1
messages/fr/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Observations récentes",
     "recentActivity": "Activité récente",
     "gettingStarted": "Premiers pas",
-    "tireHotel": "Hôtel à pneus"
+    "tireHotel": "Hôtel à pneus",
+    "inspectionsDue": "Contrôles techniques à venir"
   },
   "maintenance": {
     "title": "Maintenance prévue à effectuer",
@@ -252,5 +253,15 @@
     "roomLeft": "Place pour {count} de plus",
     "needsPrep": "À préparer",
     "unbilled": "Non facturé ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Contrôles techniques à venir",
+    "viewAll": "Voir tous les véhicules concernés",
+    "overdue": "En retard",
+    "within30": "30 prochains jours",
+    "within90": "90 prochains jours",
+    "noData": "Aucun contrôle dans les 90 prochains jours",
+    "overdueSince": "En retard depuis le {date}",
+    "remind": "Envoyer des rappels"
   }
 }

+ 228 - 0
messages/fr/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "Importer des {entity}",
+  "cancel": "Annuler",
+  "done": "Terminé",
+  "entities": {
+    "customers": "clients",
+    "vehicles": "véhicules",
+    "services": "historiques de service"
+  },
+  "entityHints": {
+    "customers": "Noms, coordonnées, adresses. Une ligne par client.",
+    "vehicles": "Marque, modèle, année, plaque, NIV. Une colonne propriétaire associe ou crée le client.",
+    "services": "Interventions et factures passées. Une ligne par intervention, avec le véhicule concerné."
+  },
+  "groups": {
+    "customer": "Client",
+    "vehicle": "Véhicule",
+    "service": "Service"
+  },
+  "steps": {
+    "upload": "Choisissez quoi importer et téléversez le fichier exporté depuis votre système actuel.",
+    "mapping": "Vérifiez quelle colonne devient quel champ. Rien n'est encore écrit.",
+    "preview": "Simulation : voici exactement ce que l'import va faire. Vérifiez, puis importez.",
+    "importing": "Écriture des lignes. Gardez cette fenêtre ouverte.",
+    "result": "Import terminé."
+  },
+  "upload": {
+    "drop": "Glissez-déposez un fichier ici",
+    "formats": "CSV, Excel (.xlsx) ou vCard (.vcf), jusqu'à 25 Mo et 50 000 lignes",
+    "browse": "Parcourir les fichiers",
+    "reading": "Lecture du fichier...",
+    "templateHint": "Pas encore d'export ? Remplissez un modèle et téléversez-le.",
+    "templateXlsx": "Modèle Excel",
+    "templateCsv": "Modèle CSV",
+    "sourcesHint": "Fonctionne avec les exports de n'importe quel logiciel d'atelier, outil de comptabilité, Google Contacts, Outlook ou tableur. Les noms de colonnes sont reconnus dans de nombreuses langues ; vous confirmez la correspondance avant que quoi que ce soit ne soit écrit."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# ligne} other {# lignes}}",
+    "colColumn": "Colonne dans votre fichier",
+    "colSample": "Exemples de valeurs",
+    "colField": "Devient",
+    "ignore": "Ignorer cette colonne",
+    "source": {
+      "preset": "Format reconnu",
+      "header": "Correspondance par en-tête",
+      "values": "Déduit des valeurs"
+    },
+    "suggestWithAi": "Suggérer avec l'IA",
+    "aiApplied": "{count, plural, one {# colonne associée} other {# colonnes associées}} par l'IA. Vérifiez-les avant de continuer.",
+    "chooseAnother": "Choisir un autre fichier",
+    "missingRequired": "Encore requis : {fields}",
+    "dryRunHint": "La simulation vérifie chaque ligne par rapport à vos données existantes sans rien écrire.",
+    "runDryRun": "Lancer la simulation"
+  },
+  "options": {
+    "dateFormat": "Format de date",
+    "dateAuto": "Détecter automatiquement",
+    "dateDMY": "Jour / mois / année",
+    "dateMDY": "Mois / jour / année",
+    "dateYMD": "Année / mois / jour",
+    "decimal": "Séparateur décimal",
+    "decimalAuto": "Détecter automatiquement",
+    "decimalDot": "Point (1,234.56)",
+    "decimalComma": "Virgule (1.234,56)",
+    "countryCode": "Indicatif pays pour les numéros de téléphone",
+    "duplicates": "Quand une ligne correspond à un enregistrement existant",
+    "duplicatesUpdate": "Mettre à jour l'enregistrement existant",
+    "duplicatesSkip": "Ignorer la ligne",
+    "duplicatesCreate": "Créer quand même un nouvel enregistrement"
+  },
+  "summary": {
+    "create": "Seront créés",
+    "update": "Seront mis à jour",
+    "skip": "Seront ignorés",
+    "error": "Impossibles à importer",
+    "warnings": "Avec avertissements",
+    "customersToCreate": "{count, plural, one {# nouveau client} other {# nouveaux clients}}",
+    "customersToUpdate": "{count, plural, one {# client mis à jour} other {# clients mis à jour}}",
+    "vehiclesToCreate": "{count, plural, one {# nouveau véhicule} other {# nouveaux véhicules}}",
+    "vehiclesToUpdate": "{count, plural, one {# véhicule mis à jour} other {# véhicules mis à jour}}",
+    "servicesToCreate": "{count, plural, one {# fiche de service} other {# fiches de service}}"
+  },
+  "actions": {
+    "create": "Créer",
+    "update": "Mettre à jour",
+    "skip": "Ignorer",
+    "error": "Erreur"
+  },
+  "matchOn": {
+    "number": "numéro de client",
+    "email": "e-mail",
+    "phone": "téléphone",
+    "name": "nom",
+    "vin": "NIV",
+    "plate": "plaque",
+    "makeModelYear": "marque, modèle et année"
+  },
+  "preview": {
+    "colAction": "Action",
+    "colOwner": "Propriétaire",
+    "colDetails": "Détails",
+    "empty": "Aucune ligne ne correspond à ce filtre.",
+    "matchedOn": "Correspond à {name} (par {on})",
+    "linksCustomer": "Associé au client {name}",
+    "linksVehicle": "Associé à {label}",
+    "sameCustomerAsRow": "Même client que la ligne {row}",
+    "sameVehicleAsRow": "Même véhicule que la ligne {row}",
+    "createsCustomer": "Crée le client",
+    "createsVehicle": "Crée le véhicule",
+    "downloadReport": "Télécharger le rapport de simulation",
+    "limitExceeded": "Votre forfait autorise encore {remaining} clients, mais cet import en créerait {needed}. Retirez des lignes ou changez de forfait.",
+    "pageInfo": "{from}–{to} sur {total}",
+    "prev": "Précédent",
+    "next": "Suivant",
+    "backToMapping": "Retour à la correspondance",
+    "import": "Importer {count, plural, one {# ligne} other {# lignes}}",
+    "nothingWrittenYet": "Rien n'a encore été écrit. L'import pourra ensuite être annulé depuis Paramètres → Données."
+  },
+  "importing": {
+    "title": "Import en cours...",
+    "starting": "Démarrage",
+    "progress": "{done} sur {total} lignes",
+    "keepOpen": "Les fichiers volumineux prennent une à deux minutes."
+  },
+  "result": {
+    "title": "Import terminé",
+    "undoneTitle": "Import annulé",
+    "created": "{count, plural, one {# enregistrement créé} other {# enregistrements créés}}",
+    "updated": "{count, plural, one {# enregistrement mis à jour} other {# enregistrements mis à jour}}",
+    "skipped": "{count, plural, one {# ligne ignorée} other {# lignes ignorées}}",
+    "failed": "{count, plural, one {# ligne non importée} other {# lignes non importées}}",
+    "writeFailures": "{count, plural, one {# ligne a échoué à l'écriture} other {# lignes ont échoué à l'écriture}}",
+    "rowFailure": "Ligne {row} : {error}",
+    "undo": "Annuler cet import",
+    "undone": "{customers} clients, {vehicles} véhicules et {services} fiches de service supprimés.",
+    "undoHint": "L'annulation supprime tous les enregistrements créés par cet import. Ceux qu'il a seulement mis à jour conservent leurs nouvelles valeurs."
+  },
+  "history": {
+    "title": "Historique des imports",
+    "description": "Imports de tableur effectués sur cet atelier. L'annulation supprime ce qu'un import a créé.",
+    "loading": "Chargement...",
+    "statusUndone": "Annulé",
+    "counts": "{created} créés, {updated} mis à jour, {skipped} ignorés, {failed} en échec",
+    "undo": "Annuler",
+    "undone": "{customers} clients, {vehicles} véhicules et {services} fiches de service supprimés.",
+    "undoFailed": "Impossible d'annuler l'import",
+    "confirmTitle": "Annuler cet import ?",
+    "confirmDescription": "Supprime ce que {file} a créé : {customers} clients, {vehicles} véhicules et {services} fiches de service. Les enregistrements que l'import a seulement mis à jour sont conservés.",
+    "cancel": "Annuler",
+    "confirm": "Annuler l'import"
+  },
+  "fields": {
+    "customer": {
+      "name": "Nom du client",
+      "firstName": "Prénom",
+      "lastName": "Nom",
+      "customerNumber": "Numéro de client",
+      "email": "E-mail",
+      "phone": "Téléphone",
+      "company": "Entreprise",
+      "address": "Adresse",
+      "street": "Rue",
+      "postalCode": "Code postal",
+      "city": "Ville",
+      "state": "État / région",
+      "country": "Pays",
+      "taxId": "Numéro fiscal",
+      "notes": "Notes client"
+    },
+    "vehicle": {
+      "description": "Véhicule (année marque modèle dans une seule colonne)",
+      "make": "Marque",
+      "model": "Modèle",
+      "year": "Année",
+      "vin": "NIV",
+      "licensePlate": "Plaque d'immatriculation",
+      "color": "Couleur",
+      "mileage": "Kilométrage",
+      "fuelType": "Type de carburant",
+      "transmission": "Transmission",
+      "engineSize": "Cylindrée",
+      "engineCode": "Code moteur",
+      "purchaseDate": "Date d'achat",
+      "purchasePrice": "Prix d'achat"
+    },
+    "service": {
+      "date": "Date du service",
+      "title": "Titre du service",
+      "description": "Description",
+      "total": "Montant total",
+      "mileage": "Kilométrage au moment du service",
+      "invoiceNumber": "Numéro de facture",
+      "notes": "Notes de service",
+      "technician": "Technicien"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Le nom du client est manquant",
+    "invalid_email": "L'e-mail \"{value}\" n'est pas valide et a été ignoré",
+    "invalid_vin": "Le NIV \"{value}\" ne fait pas 17 caractères ; conservé tel quel",
+    "vehicle_make_model_required": "La marque et le modèle sont manquants",
+    "vehicle_year_required": "L'année est manquante",
+    "vehicle_year_invalid": "L'année \"{value}\" n'est pas une année valide",
+    "vehicle_identifier_required": "Aucune plaque, NIV ou marque et modèle pour identifier le véhicule",
+    "vehicle_not_found": "Aucun véhicule existant ne correspond à \"{value}\" et il n'y a pas assez d'informations pour en créer un",
+    "service_date_required": "La date du service est manquante",
+    "service_date_invalid": "La date \"{value}\" n'a pas pu être lue",
+    "service_title_required": "Le titre ou la description du service est manquant",
+    "invalid_date": "{field} \"{value}\" n'a pas pu être lu et a été ignoré",
+    "invalid_number": "{field} \"{value}\" n'est pas un nombre et a été ignoré",
+    "duplicate_invoice_number": "Le numéro de facture {value} existe déjà",
+    "write_failed": "Impossible d'écrire : {value}"
+  },
+  "errors": {
+    "uploadFailed": "Le fichier n'a pas pu être lu",
+    "aiFailed": "L'IA n'a pas pu suggérer de correspondance",
+    "dryRunFailed": "La simulation a échoué",
+    "importFailed": "L'import a échoué",
+    "undoFailed": "Impossible d'annuler l'import"
+  },
+  "entry": {
+    "title": "Importer depuis un tableur",
+    "description": "Récupérez clients, véhicules et historique de service depuis n'importe quel système capable d'exporter en CSV ou Excel. Associez les colonnes, lancez une simulation, puis importez. Chaque import peut être annulé.",
+    "customers": "Importer des clients",
+    "vehicles": "Importer des véhicules",
+    "services": "Importer l'historique de service"
+  }
+}

+ 584 - 0
messages/fr/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Intégrations",
+    "description": "Connectez les services que votre atelier utilise déjà. Chaque intégration se configure une seule fois et fonctionne en arrière-plan.",
+    "all": "Toutes",
+    "empty": "Aucune intégration dans cette catégorie pour le moment.",
+    "notConnected": "Non connectée",
+    "connect": "Connecter",
+    "manage": "Gérer",
+    "connectedTitle": "Connectés",
+    "connectedDescription": "Services configurés par cet atelier. Ouvrez-en un pour vérifier son état, modifier les réglages ou le déconnecter.",
+    "noneConnected": "Rien de connecté pour l'instant. Choisissez un service ci-dessous pour commencer.",
+    "availableTitle": "Disponibles",
+    "availableDescription": "Services que vous pouvez ajouter. Chacun se configure une fois et fonctionne en arrière-plan.",
+    "allConnected": "Tous les services disponibles sont connectés.",
+    "searchPlaceholder": "Rechercher une intégration",
+    "noMatches": "Rien ne correspond à « {query} ».",
+    "readMore": "En savoir plus",
+    "suggestTitle": "Il manque un service que vous utilisez ?",
+    "suggestDescription": "Dites-nous quelle intégration aiderait votre atelier et nous l'étudierons.",
+    "suggest": "Suggérer une intégration",
+    "suggestSubject": "Suggestion d'intégration"
+  },
+  "categories": {
+    "calendar": "Calendrier",
+    "conferencing": "Appels vidéo",
+    "accounting": "Comptabilité",
+    "messaging": "Messagerie",
+    "payments": "Paiements",
+    "automation": "Automatisation",
+    "storage": "Fichiers",
+    "registry": "Registre des véhicules",
+    "other": "Autre",
+    "ai": "IA"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Ordres de travail vers le calendrier",
+      "pull": "Plages occupées du calendrier",
+      "conference": "Liens d'appel vidéo"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Recherche par plaque et VIN",
+      "inspection": "Dates de contrôle technique",
+      "safety": "Rappels et plaintes de propriétaires"
+    },
+    "ai": {
+      "chat": "Textes et résumés"
+    },
+    "payments": {
+      "checkout": "Payer depuis le lien de la facture"
+    },
+    "accounting": {
+      "invoices": "Factures vers la comptabilité",
+      "customers": "Clients vers la comptabilité",
+      "payments": "Paiements dans les deux sens"
+    }
+  },
+  "statuses": {
+    "active": "Connectée",
+    "error": "Attention requise",
+    "pending": "Configuration en cours",
+    "disconnected": "Déconnectée",
+    "needsSetup": "Configuration inachevée"
+  },
+  "connection": {
+    "back": "Toutes les intégrations",
+    "account": "Compte connecté",
+    "lastHealth": "Dernière vérification",
+    "lastSync": "Dernière synchronisation",
+    "lastError": "La dernière exécution a échoué",
+    "setupMissingTitle": "Configuration inachevée",
+    "setupMissing": "Choisissez {fields} dans les réglages ci-dessous. D'ici là, rien n'est synchronisé.",
+    "connected": "{name} est connecté.",
+    "connectedShort": "Connecté",
+    "connectWith": "Connecter {name}",
+    "connect": "Connecter",
+    "reconnect": "Reconnecter",
+    "disconnect": "Déconnecter",
+    "disconnectTitle": "Déconnecter {name} ?",
+    "disconnectDescription": "Torqvoice oublie le compte et cesse de l'utiliser. Rien de ce qui a déjà été envoyé à {name} ou stocké dedans n'est touché.",
+    "disconnectDescriptionCalendar": "Torqvoice oublie le compte et cesse de synchroniser. Les ordres de travail déjà placés dans {name} y restent. Le temps occupé affiché depuis ce calendrier est retiré du calendrier Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice oublie le compte et cesse de créer des réunions. Les réunions déjà créées restent dans {name} et les liens sur les ordres de travail sont conservés.",
+    "disconnectDescriptionAccounting": "Torqvoice oublie le compte et cesse d'envoyer et de lire. Les factures, clients et paiements déjà dans {name} y restent.",
+    "removeSetup": "Retirer",
+    "removeSetupTitle": "Retirer {name} ?",
+    "removeSetupDescription": "Rien n'est encore connecté. Torqvoice oublie ce qui a été saisi et {name} revient dans la liste des services disponibles.",
+    "removeSetupDone": "{name} retiré",
+    "removeSetupHint": "Vous n'allez pas plus loin ? Retirez-le et le service revient dans la liste ci-dessous.",
+    "disconnected": "Déconnecté",
+    "test": "Tester la connexion",
+    "testPassed": "La connexion fonctionne",
+    "syncNow": "Synchroniser maintenant",
+    "syncQueued": "Synchronisation en file d'attente ; elle démarre d'ici une minute",
+    "pullNow": "Récupérer les paiements maintenant",
+    "pullQueued": "Récupération des paiements en file d'attente ; elle démarre d'ici une minute",
+    "backfill": "Envoyer les ordres de travail planifiés",
+    "backfillQueued": "Les ordres de travail planifiés depuis la semaine dernière ont été mis en file d'attente",
+    "backfillInvoices": "Envoyer les factures émises",
+    "backfillInvoicesQueuedCount": "Mise en file de {count, plural, one {# facture émise} other {# factures émises}} à transmettre",
+    "backfillInvoicesNone": "Aucune facture émise à envoyer. Seules les factures envoyées, partagées ou payées sont transmises, à partir de la date de début si vous en avez fixé une.",
+    "retryQueued": "Tâche remise en file d'attente",
+    "settingsTitle": "Paramètres",
+    "settingsDescription": "Ce qu'il faut synchroniser et vers où. Les modifications s'appliquent à la prochaine synchronisation.",
+    "settingsSaved": "Paramètres enregistrés",
+    "save": "Enregistrer",
+    "loading": "Chargement...",
+    "choose": "Choisir...",
+    "search": "Rechercher...",
+    "noMatch": "Aucun résultat",
+    "leaveEmpty": "Laisser vide",
+    "noLimit": "Sans limite",
+    "planLocked": "Les intégrations ne sont pas incluses dans votre forfait.",
+    "ownAppTitle": "Cette installation utilise votre propre application auprès du fournisseur",
+    "redirectUri": "URI de redirection à enregistrer auprès du fournisseur :",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Ajoutez cette URL dans le tableau de bord Stripe sous Developers, Webhooks, abonnée à checkout.session.completed. Collez ensuite le secret de signature affiché par Stripe dans le champ secret du webhook ici, pour que les notifications puissent être vérifiées.",
+    "inboundHintVipps": "Saisissez cette URL comme URL de rappel dans le portail Vipps MobilePay. Vipps exige aussi des conditions de vente publiées, à renseigner sous Paramètres, Paiement.",
+    "inboundHintPaypal": "Ajoutez cette URL dans le tableau de bord développeur PayPal sous Webhooks, abonnée à PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "Synchronisation des contrôles",
+      "vehicles": "{count} véhicules avec plaque dans cet atelier",
+      "dueNow": "{count} seraient recherchés à la prochaine exécution",
+      "withDate": "{count} ont déjà une date de contrôle",
+      "lastPass": "Dernier passage complet {date}",
+      "never": "Aucun passage complet pour l'instant",
+      "inProgress": "Un passage est en cours",
+      "off": "Activez le réglage et enregistrez ; d'ici là, un passage ne vérifie rien.",
+      "syncNowHelp": "Synchroniser maintenant lance un passage immédiatement au lieu d'attendre celui de l'heure. Il recherche 25 véhicules à la fois et continue seul jusqu'à ce que tous les véhicules à vérifier soient traités."
+    },
+    "supersedes": "Connecter {next} déconnecte {current}. Un seul peut être actif à la fois : {current} cesse d'être utilisé dès que {next} est en service.",
+    "batchRetried": "{count, plural, =0 {Rien à réessayer} one {# tâche remise en file} other {# tâches remises en file}}"
+  },
+  "activity": {
+    "title": "Activité",
+    "description": "Tâches récentes et lignes de journal pour cette connexion.",
+    "retry": "Réessayer",
+    "jobStatus": {
+      "queued": "En file d'attente",
+      "running": "En cours",
+      "done": "Terminée",
+      "failed": "Échec",
+      "dead": "Abandonnée"
+    },
+    "jobs": "{count, plural, one {# tâche} other {# tâches}}",
+    "counts": {
+      "queued": "{count} en attente",
+      "running": "{count} en cours",
+      "done": "{count} terminées",
+      "failed": "{count} échouées",
+      "dead": "{count} abandonnées"
+    },
+    "noFailures": "Aucun échec.",
+    "retryFailed": "Réessayer les échecs",
+    "details": "Afficher les détails"
+  },
+  "errors": {
+    "generic": "Une erreur s'est produite",
+    "demo": "Les intégrations ne peuvent pas être connectées sur la démo.",
+    "forbidden": "Seul un administrateur peut connecter des intégrations.",
+    "unknown": "Intégration inconnue.",
+    "plan": "Cette intégration n'est pas incluse dans votre forfait.",
+    "no-client": "Aucun identifiant d'application n'est configuré pour ce fournisseur.",
+    "denied": "Vous avez annulé la connexion auprès du fournisseur.",
+    "vendor": "Le fournisseur a renvoyé une erreur. Réessayez.",
+    "invalid": "La réponse du fournisseur était incomplète. Réessayez.",
+    "state": "Cette connexion a expiré ou appartient à une autre session. Recommencez.",
+    "exchange": "Le fournisseur a refusé la connexion. Vérifiez les identifiants de l'application et réessayez.",
+    "identify": "Connecté, mais le compte n'a pas pu être lu. Essayez de vous reconnecter."
+  },
+  "meeting": {
+    "join": "Rejoindre l'appel vidéo",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "Depuis les calendriers connectés",
+    "busy": "Occupé"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Ajoutez les ordres de travail planifiés à un Google Calendar, joignez un lien Google Meet et consultez les plages occupées du calendrier dans Torqvoice.",
+      "tenantHelp": "Créez un client OAuth dans Google Cloud Console (API et services → Identifiants), activez l'API Google Calendar, ajoutez l'URI de redirection ci-dessous, puis collez ici l'identifiant client et le secret.",
+      "fields": {
+        "clientId": "Identifiant client",
+        "clientSecret": "Secret client"
+      },
+      "settings": {
+        "calendarId": "Calendrier",
+        "pushEnabled": "Ajouter les ordres de travail planifiés à ce calendrier",
+        "includeCustomer": "Inclure le nom et le téléphone du client dans l'événement",
+        "addConference": "Ajouter une réunion Google Meet à chaque ordre de travail planifié",
+        "addConferenceHelp": "Activé : chaque ordre de travail envoyé au calendrier reçoit une réunion Google Meet, et le lien apparaît sur l'ordre. Désactivé : ajoutez une réunion Google Meet à un ordre précis depuis sa page. Dans les deux cas, le client ne reçoit le lien que lorsque vous l'envoyez.",
+        "pullEnabled": "Afficher les plages occupées de ce calendrier dans le calendrier Torqvoice",
+        "pullEnabledHelp": "Lecture seule. Actualisé toutes les 15 minutes, une semaine en arrière et deux mois en avant."
+      }
+    },
+    "microsoft-365": {
+      "description": "Ajoutez les ordres de travail planifiés à un calendrier Outlook, joignez un lien Microsoft Teams et consultez les plages occupées du calendrier dans Torqvoice.",
+      "tenantHelp": "Enregistrez une application dans Microsoft Entra (Inscriptions d'applications), autorisez les comptes personnels et professionnels, ajoutez l'URI de redirection ci-dessous comme plateforme Web, créez un secret client, puis collez les deux ici.",
+      "fields": {
+        "clientId": "ID d'application (client)",
+        "clientSecret": "Secret client"
+      },
+      "settings": {
+        "calendarId": "Calendrier",
+        "pushEnabled": "Ajouter les ordres de travail planifiés à ce calendrier",
+        "includeCustomer": "Inclure le nom et le téléphone du client dans l'événement",
+        "addConference": "Ajouter une réunion Microsoft Teams à chaque ordre de travail planifié",
+        "addConferenceHelp": "Activé : chaque ordre de travail envoyé au calendrier reçoit une réunion Microsoft Teams, et le lien apparaît sur l'ordre. Désactivé : ajoutez une réunion Microsoft Teams à un ordre précis depuis sa page. Dans les deux cas, le client ne reçoit le lien que lorsque vous l'envoyez.",
+        "pullEnabled": "Afficher les plages occupées de ce calendrier dans le calendrier Torqvoice",
+        "pullEnabledHelp": "Lecture seule. Actualisé toutes les 15 minutes, une semaine en arrière et deux mois en avant."
+      }
+    },
+    "zoom": {
+      "description": "Ajoutez une réunion Zoom à un ordre de travail planifié et gardez le lien dessus, pour un diagnostic à distance ou pour présenter des constats au client.",
+      "tenantHelp": "Dans Zoom App Marketplace, créez une application General (user-managed), ajoutez l'URI de redirection ci-dessous, ajoutez les scopes meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting et user:read:user, puis collez ici l'ID client et le secret.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Secret client"
+      },
+      "settings": {
+        "autoCreate": "Créer automatiquement une réunion Zoom pour chaque ordre de travail planifié",
+        "autoCreateHelp": "Désactivé : vous ajoutez vous-même l'appel vidéo à un ordre de travail, depuis sa page. Activé : chaque ordre avec une heure de début reçoit une réunion, déplacée en cas de report et supprimée en cas d'annulation. Dans les deux cas, le client ne reçoit le lien que si vous l'envoyez.",
+        "includeCustomer": "Inclure le nom du client dans le sujet de la réunion",
+        "joinBeforeHost": "Laisser le client rejoindre avant vous",
+        "waitingRoom": "Utiliser une salle d'attente"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Remplissez un véhicule à partir de sa plaque ou de son VIN norvégien : marque, modèle, année, carburant, boîte de vitesses, moteur, couleur, dimensions des pneus et date du prochain EU-kontroll, directement depuis le registre de Statens vegvesen. Aucune donnée sur le propriétaire n'est renvoyée.",
+      "fields": {
+        "apiKey": "Clé API",
+        "apiKeyHelp": "Gratuite. Commandez la clé sur vegvesen.no, rubrique « API for tekniske kjøretøyopplysninger » : connectez-vous avec BankID, représentez l'atelier, et la clé est délivrée immédiatement. Elle autorise 50 000 recherches par jour."
+      },
+      "settings": {
+        "syncInspections": "Tenir à jour les dates de contrôle de tous les véhicules de cet atelier",
+        "syncInspectionsHelp": "Une fois par heure, les véhicules non vérifiés récemment sont recherchés par plaque et la date du prochain EU-kontroll est enregistrée. Seuls les véhicules de cet atelier sont envoyés. Un véhicule est revérifié chaque mois, et chaque semaine lorsque l'échéance est à moins de deux mois."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Remplissez un véhicule à partir de sa plaque française, italienne, espagnole, portugaise ou britannique : marque, modèle, année, VIN, carburant, cylindrée et plus, depuis les données d'immatriculation du pays via Openapi.com. Chaque recherche est facturée sur le compte Openapi de l'atelier. Aucune donnée sur le propriétaire n'est renvoyée.",
+      "fields": {
+        "token": "Jeton d'API",
+        "tokenHelp": "Créez un compte sur console.openapi.com, créditez le portefeuille et, dans Automotive, créez un jeton pour l'environnement de production. Chaque recherche aboutie coûte quelques dizaines de centimes ; une recherche sans résultat est gratuite."
+      },
+      "settings": {
+        "country": "Pays",
+        "countryHelp": "Le registre à interroger. Les plaques françaises et italiennes ont le même format, le pays ne peut donc pas être déduit de la plaque.",
+        "bikes": "Rechercher aussi les motos",
+        "bikesHelp": "Quand une plaque n'est pas celle d'une voiture, le registre des motos est interrogé à son tour. Une recherche voiture sans résultat est gratuite, cela ne coûte donc rien de plus sauf si une moto est trouvée. Indisponible pour le Portugal."
+      }
+    },
+    "rdw": {
+      "description": "Remplissez un véhicule à partir de sa plaque néerlandaise : marque, modèle, année, carburant, cylindrée, couleur, masses et date du prochain APK, directement depuis le registre ouvert des véhicules de la RDW. Gratuit, sans clé. Ni les données du propriétaire ni le VIN ne sont publiés.",
+      "fields": {
+        "appToken": "Jeton d'application (facultatif)",
+        "appTokenHelp": "Les données ouvertes de la RDW fonctionnent sans clé, mais les requêtes anonymes partagent un quota limité. Pour la synchronisation des contrôles, créez un compte gratuit sur opendata.rdw.nl, ouvrez les paramètres développeur de votre profil et créez un jeton d'application."
+      },
+      "settings": {
+        "syncInspections": "Tenir à jour les dates d'APK de tous les véhicules de cet atelier",
+        "syncInspectionsHelp": "Une fois par heure, les véhicules non vérifiés récemment sont recherchés par plaque et la date du prochain APK est enregistrée. Seuls les véhicules de cet atelier sont envoyés. Un véhicule est revérifié chaque mois, et chaque semaine lorsque l'échéance est à moins de deux mois."
+      }
+    },
+    "regcheck": {
+      "description": "Remplissez un véhicule à partir de sa plaque en Australie, Nouvelle-Zélande, aux États-Unis, en Irlande, Norvège, Suède, Danemark, Finlande, Estonie, Tchéquie, Slovaquie, Hongrie ou Croatie : marque, modèle, année, VIN, carburant et cylindrée lorsque le pays les publie, via RegCheck. Chaque recherche aboutie consomme un crédit du compte RegCheck de l'atelier. Aucune donnée sur le propriétaire n'est renvoyée.",
+      "fields": {
+        "username": "Nom d'utilisateur RegCheck",
+        "usernameHelp": "Inscrivez-vous sur regcheck.org.uk ; le compte comprend 10 recherches gratuites, les suivantes s'achètent par blocs de 100 depuis le tableau de bord. Seul le nom d'utilisateur est nécessaire ici, jamais le mot de passe. Une plaque introuvable ne coûte rien."
+      },
+      "settings": {
+        "country": "Pays",
+        "countryHelp": "Le registre à interroger. Chaque pays a son propre service, la connexion n'en interroge donc qu'un.",
+        "auState": "État ou territoire",
+        "auStateHelp": "L'Australie immatricule les véhicules par État et le registre doit savoir lequel interroger. Choisissez celui où la plupart des véhicules de vos clients sont immatriculés.",
+        "usState": "État",
+        "usStateHelp": "Le code à deux lettres de l'État où les véhicules sont immatriculés, comme CA ou TX. Les registres américains sont interrogés par État."
+      }
+    },
+    "openai": {
+      "description": "Descriptions de service, résumés d'historique, lecture de documents et assistant, traités par votre propre compte OpenAI. Chaque appel est facturé à ce compte, et vous choisissez le modèle.",
+      "fields": {
+        "apiKey": "Clé API",
+        "apiKeyHelp": "À créer sur platform.openai.com, dans API keys. Le compte doit être approvisionné ; OpenAI facture à l’appel."
+      },
+      "settings": {
+        "model": "Modèle",
+        "modelHelp": "Le modèle qui répond. La liste vient de votre compte, les plus récents en premier. Les petits modèles coûtent moins par appel et suffisent à la plupart des textes d'atelier."
+      }
+    },
+    "anthropic": {
+      "description": "Descriptions de service, résumés d'historique, lecture de documents et assistant, traités par Claude sur votre propre compte Anthropic. Chaque appel est facturé à ce compte, et vous choisissez le modèle.",
+      "fields": {
+        "apiKey": "Clé API",
+        "apiKeyHelp": "À créer sur console.anthropic.com, dans API keys. Le compte doit être approvisionné ; Anthropic facture à l’appel."
+      },
+      "settings": {
+        "model": "Modèle",
+        "modelHelp": "Le modèle qui répond. La liste vient de votre compte, les plus récents en premier. Les petits modèles coûtent moins par appel et suffisent à la plupart des textes d'atelier."
+      }
+    },
+    "stripe": {
+      "description": "Paiements par carte depuis le lien de la facture, sur votre propre compte Stripe. Le client paie, Stripe prévient Torqvoice et la facture se marque payée toute seule.",
+      "fields": {
+        "secretKey": "Clé secrète",
+        "secretKeyHelp": "Depuis le tableau de bord Stripe sous Developers, API keys. Les clés de test ne traitent rien de réel : essayez d'abord un paiement avec elles, puis passez aux clés réelles.",
+        "publishableKey": "Clé publiable",
+        "webhookSecret": "Secret de signature du webhook",
+        "webhookSecretHelp": "Affiché par Stripe une fois l'URL de notification de cette page ajoutée comme webhook. Avec lui, chaque notification est vérifiée avant qu'un paiement soit enregistré."
+      },
+      "settings": {
+        "enabled": "Proposer sur les liens de facture",
+        "enabledHelp": "Désactivez pour retirer le bouton de paiement des factures partagées pendant un temps, sans déconnecter. Les paiements déjà lancés sont toujours enregistrés."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay depuis le lien de la facture. Le client paie dans l'application qu'il a déjà et la facture se marque payée toute seule.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Secret client",
+        "subscriptionKey": "Clé d'abonnement",
+        "subscriptionKeyHelp": "La Ocp-Apim-Subscription-Key du portail Vipps MobilePay, dans les clés API de l'unité de vente.",
+        "merchantSerialNumber": "Numéro de série marchand"
+      },
+      "settings": {
+        "enabled": "Proposer sur les liens de facture",
+        "enabledHelp": "Désactivez pour retirer le bouton de paiement des factures partagées pendant un temps, sans déconnecter. Les paiements déjà lancés sont toujours enregistrés.",
+        "testMode": "Mode test",
+        "testModeHelp": "Utiliser l'environnement de test Vipps avec des clés de test. Laissez-le activé jusqu'à ce qu'un paiement complet ait abouti, puis désactivez-le et saisissez les clés réelles."
+      }
+    },
+    "paypal": {
+      "description": "Paiement PayPal depuis le lien de la facture, via votre propre application PayPal. Le client paie avec un compte PayPal ou une carte et la facture se marque payée toute seule.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Secret client"
+      },
+      "settings": {
+        "enabled": "Proposer sur les liens de facture",
+        "enabledHelp": "Désactivez pour retirer le bouton de paiement des factures partagées pendant un temps, sans déconnecter. Les paiements déjà lancés sont toujours enregistrés.",
+        "sandbox": "Mode sandbox",
+        "sandboxHelp": "Utiliser le sandbox PayPal avec les clés de l'application sandbox. Laissez-le activé jusqu'à ce qu'un paiement complet ait abouti, puis désactivez-le et saisissez les clés de l'application réelle."
+      }
+    },
+    "quickbooks": {
+      "description": "Envoyez les factures émises, leurs clients et leurs paiements vers QuickBooks Online, et récupérez les paiements enregistrés là-bas.",
+      "tenantHelp": "Créez une application sur le portail développeur Intuit. Copiez le client id et le secret depuis Keys and credentials, et saisissez l'URI de redirection ci-dessous sous Settings, Redirect URIs. Les clés de développement atteignent les entreprises sandbox, les clés de production les vraies.",
+      "fields": {
+        "clientId": "Identifiant client",
+        "clientSecret": "Secret client"
+      },
+      "settings": {
+        "pushInvoices": "Envoyer les factures émises vers QuickBooks",
+        "pushOnComplete": "Envoyer aussi les factures quand le travail est marqué terminé",
+        "pushOnCompleteHelp": "Sans cette option, une facture part une fois envoyée, partagée ou payée. Avec, un travail terminé part tout de suite et est mis à jour s'il change.",
+        "startDate": "Uniquement les factures datées à partir du",
+        "startDateHelp": "Laissez vide pour envoyer toutes les factures émises. Utilisez la date où votre comptabilité est passée sur QuickBooks pour ne pas saisir deux fois les anciennes factures.",
+        "includeVehicle": "Indiquer le véhicule sur la facture",
+        "includeVehicleHelp": "Une ligne de texte au-dessus des lignes facturées avec l'année, la marque, le modèle, la plaque et le kilométrage, pour que la comptabilité sache de quelle voiture il s'agit.",
+        "laborItemId": "Article pour les lignes de main-d'œuvre",
+        "laborItemIdHelp": "Laissez vide et un article nommé Labour sera créé sous votre compte de produits la première fois.",
+        "partsItemId": "Article pour les lignes de pièces",
+        "partsItemIdHelp": "Laissez vide et un article nommé Parts sera créé sous votre compte de produits la première fois.",
+        "taxAmounts": "Montants de taxe",
+        "taxAmountsHelp": "Tel que facturé envoie la taxe exactement telle qu'elle figure sur la facture reçue par le client, pour que les deux systèmes concordent au centime. Calculé par QuickBooks laisse QuickBooks appliquer le taux du code de taxe, ou l'adresse du client aux États-Unis, et le journal signale les totaux différents.",
+        "taxAmountsBilled": "Tel que facturé dans Torqvoice",
+        "taxAmountsQuickbooks": "Calculé par QuickBooks",
+        "taxCodeId": "Code de taxe pour les factures taxables",
+        "taxCodeIdHelp": "QuickBooks enregistre la taxe sous un code de taxe. Torqvoice choisit le code dont le taux correspond à chaque facture ; celui-ci sert quand aucun ne correspond. Une société américaine utilise TAX sauf si vous choisissez un autre code.",
+        "zeroTaxCodeId": "Code de taxe pour les factures sans taxe",
+        "zeroTaxCodeIdHelp": "Utilisé quand la facture n'a pas de taxe ou que le client est exonéré. Une société américaine utilise NON sauf si vous choisissez un autre code.",
+        "pushPayments": "Enregistrer les paiements dans QuickBooks",
+        "depositAccountId": "Déposer les paiements sur",
+        "depositAccountIdHelp": "Laissez vide pour utiliser Undeposited Funds.",
+        "manualPaidAsPayment": "Enregistrer un paiement quand une facture est marquée payée à la main",
+        "manualPaidAsPaymentHelp": "Une facture marquée payée dans Torqvoice sans écriture de paiement reçoit dans QuickBooks un paiement du montant qu'elle y affiche encore comme dû. Désactivé, elle y reste ouverte jusqu'à l'enregistrement d'un paiement.",
+        "pullPayments": "Récupérer les paiements enregistrés dans QuickBooks",
+        "pullPaymentsHelp": "Toutes les 30 minutes. Un paiement reçu dans QuickBooks sur une facture Torqvoice est enregistré ici sur la facture."
+      }
+    },
+    "nhtsa": {
+      "description": "Remplissez un véhicule à partir de son VIN et consultez les rappels, les plaintes de propriétaires et les notes aux crash-tests pour chaque année-modèle, auprès de l'autorité de sécurité américaine. Gratuit, sans clé.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Tenir à jour les données de rappels et de plaintes pour chaque véhicule de cet atelier",
+        "refreshFleetHelp": "Une fois par jour, les modèles non actualisés depuis une semaine sont récupérés à nouveau, pour que la page du véhicule n'attende jamais et qu'un nouveau rappel apparaisse dans la semaine. Seuls la marque, le modèle et l'année sont envoyés."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Ordres de travail vers l'agenda",
+      "pull": "Temps occupé depuis l'agenda"
+    },
+    "conference": {
+      "sync": "Liens de visioconférence"
+    },
+    "inspection": {
+      "refresh": "Mise à jour des dates de contrôle"
+    },
+    "accounting": {
+      "invoice": "Facture vers la comptabilité",
+      "payment": "Paiement vers la comptabilité",
+      "customer": "Client vers la comptabilité",
+      "pull": "Paiements depuis la comptabilité"
+    },
+    "safety": {
+      "refresh": "Actualisation des données de rappel",
+      "vehicle": "Vérification des rappels pour un nouveau véhicule"
+    }
+  }
+}

+ 2 - 1
messages/fr/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Programmés",
       "sms": "SMS",
       "whatsapp": "WhatsApp",
-      "telegram": "Telegram"
+      "telegram": "Telegram",
+      "campaigns": "Campagnes"
     },
     "smsNotConfiguredTitle": "Le SMS n'est pas configuré",
     "smsNotConfiguredHint": "Connectez un fournisseur SMS pour envoyer et recevoir des messages ici. Les e-mails programmés fonctionnent sans cela.",

+ 5 - 1
messages/fr/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "Impossible de supprimer la conversation",
     "delete": "Supprimer",
     "cancel": "Annuler",
-    "loadMore": "Charger plus"
+    "loadMore": "Charger plus",
+    "unread": "{count} non lus",
+    "markUnread": "Marquer comme non lu",
+    "nothingToMarkUnread": "Le client n'a pas encore écrit dans cette conversation.",
+    "markUnreadFailed": "Impossible de marquer la conversation comme non lue."
   }
 }

+ 16 - 2
messages/fr/navigation.json

@@ -59,7 +59,8 @@
     "tireStorage": "Organisation du stockage",
     "tireSetDetails": "Jeu de pneus",
     "tireForecast": "La saison prochaine",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "inspectionReminders": "Rappels de contrôle"
   },
   "search": "Rechercher…",
   "shortcut": "Ctrl+K",
@@ -117,6 +118,19 @@
     "auditLog": "Journal d'audit",
     "aiAssistant": "Assistant IA",
     "tireHotel": "Hôtel à pneus",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "newWorkOrder": "Nouvel ordre de travail",
+    "installApp": "Installer l'application",
+    "roles": {
+      "owner": "Propriétaire",
+      "admin": "Administrateur",
+      "member": "Membre"
+    },
+    "badges": {
+      "messages": "{count} messages non lus",
+      "workOrders": "{count} ordres de travail ouverts",
+      "inspections": "{count} inspections en cours",
+      "reminders": "{count} rappels à échéance"
+    }
   }
 }

+ 3 - 1
messages/fr/onboarding.json

@@ -76,7 +76,9 @@
     "allDoneTitle": "Tout est prêt",
     "allDoneDescription": "Toutes les étapes sont terminées et votre atelier est opérationnel.",
     "dismiss": "Masquer la carte",
-    "dismissAriaLabel": "Masquer cette liste"
+    "dismissAriaLabel": "Masquer cette liste",
+    "importTitle": "Vous avez déjà des clients et des véhicules ailleurs ?",
+    "importDescription": "Importez-les depuis un tableur ou votre ancien système"
   },
   "sampleData": {
     "title": "Données d'exemple",

+ 51 - 1
messages/fr/portal.json

@@ -158,7 +158,14 @@
     "columnVehicle": "Véhicule",
     "columnPlate": "Plaque",
     "columnServices": "Services",
-    "columnInspections": "Inspections"
+    "columnInspections": "Inspections",
+    "reportSold": "Je ne possède plus ce véhicule",
+    "reportSoldHint": "Nous préviendrons l'atelier pour qu'il cesse d'envoyer des rappels à son sujet. Votre historique reste tel quel.",
+    "reportSoldConfirm": "Oui, prévenir l'atelier",
+    "reportSoldCancel": "Conserver",
+    "reportSoldDone": "Merci, l'atelier a été prévenu",
+    "reportSoldFailed": "Impossible d'envoyer pour le moment",
+    "reportSoldAlready": "Vous avez indiqué à l'atelier que ce véhicule n'est plus le vôtre."
   },
   "requestService": {
     "title": "Demander un service",
@@ -190,5 +197,48 @@
     "modelPlaceholder": "p. ex. Kuga",
     "yearLabel": "Année",
     "plateLabel": "Plaque d'immatriculation"
+  },
+  "booking": {
+    "title": "Réservez votre contrôle",
+    "due": "Contrôle dû le {date}",
+    "firstSlot": "Premier créneau disponible",
+    "bookThis": "Réserver {when}",
+    "pickAnother": "Ou choisir un autre jour",
+    "pickTime": "Choisir une heure",
+    "pickTimeFirst": "Choisissez une heure ci-dessus",
+    "confirmFor": "Confirmer {when}",
+    "notePlaceholder": "Quelque chose à nous signaler ? (facultatif)",
+    "legend": "Les jours barrés sont complets ; les jours ambre ont peu de créneaux.",
+    "day": {
+      "open": "Créneaux disponibles",
+      "limited": "Peu de créneaux",
+      "full": "Complet",
+      "closed": "Fermé"
+    },
+    "booked": "Réservé pour {when}",
+    "bookedHint": "Nous vous avons envoyé une confirmation. Venez quelques minutes en avance avec vos clés.",
+    "pending": "Demandé pour {when}",
+    "pendingHint": "L'atelier confirmera votre créneau sous peu.",
+    "changeOrCancel": "Modifier ou annuler",
+    "expired": "Ce lien a expiré",
+    "expiredHint": "Appelez-nous et nous trouverons un créneau ensemble.",
+    "unavailable": "La réservation en ligne n'est pas disponible pour l'instant",
+    "unavailableHint": "Appelez-nous et nous trouverons un créneau.",
+    "callUs": "Appelez-nous au {phone}",
+    "modeDirectNote": "Votre créneau est réservé immédiatement pour {minutes} minutes.",
+    "modeRequestNote": "L'atelier confirme le créneau demandé.",
+    "bookedToast": "Votre créneau est réservé",
+    "bookFailed": "Impossible de réserver ce créneau",
+    "cancelledToast": "Réservation annulée",
+    "cancelFailed": "Annulation impossible",
+    "footer": "Vous avez reçu ce lien parce que votre atelier vous a rappelé le contrôle.",
+    "workOrderTitle": "Contrôle technique {vehicle}",
+    "notificationTitle": "Nouvelle réservation en ligne",
+    "notificationBody": "{customer} a réservé {vehicle} pour {when}",
+    "cancelledNotificationTitle": "Réservation en ligne annulée",
+    "cancelledNotificationBody": "{customer} a annulé la réservation de {vehicle} du {when}",
+    "confirmationSubject": "Votre réservation chez {workshop}",
+    "confirmationBody": "Votre {vehicle} est réservé pour {when} chez {workshop}. Besoin de changer ? Utilisez le lien de notre message précédent ou appelez le {phone}.",
+    "confirmationBodyRequest": "Merci, {workshop} a reçu votre demande pour {when} pour votre {vehicle} et confirmera sous peu. Des questions ? Appelez le {phone}."
   }
 }

+ 1 - 0
messages/fr/scheduledMessages.json

@@ -41,6 +41,7 @@
     "scheduled": "Message programmé",
     "updated": "Message programmé mis à jour",
     "saveError": "Impossible d'enregistrer le message",
+    "inPast": "Cette heure est déjà passée. Choisissez un moment à venir.",
     "whatsappNeedsTemplate": "Ce message partira bien après la fermeture de la fenêtre de 24 heures et aucun modèle texte WhatsApp n'est configuré : il échouerait à l'envoi. Configurez-en un dans Paramètres > Fournisseurs > WhatsApp."
   },
   "list": {

+ 59 - 2
messages/fr/service.json

@@ -34,7 +34,8 @@
     "delete": "Supprimer",
     "unpaid": "Non Payé",
     "unsavedChanges": "Modifications non enregistrées",
-    "saved": "Enregistré"
+    "saved": "Enregistré",
+    "joinCall": "Rejoindre l'appel vidéo"
   },
   "basicInfo": {
     "title": "Informations de Base",
@@ -78,7 +79,17 @@
       "typePrompt": "Tapez un nom...",
       "boardTechnicians": "Techniciens du Tableau",
       "teamMembers": "Membres de l'Équipe"
-    }
+    },
+    "design": "Design de facture",
+    "designDefault": "Défaut de l'atelier",
+    "designFollowing": "Défaut ({name})",
+    "designPinned": "La feuille a été figée à l'émission de la facture le {date}. Un nouveau choix ne s'applique qu'après qu'un propriétaire déverrouille la facture et qu'elle soit renvoyée.",
+    "designSaved": "Design de facture mis à jour",
+    "designFollowingRule": "Par défaut ({name}, {rule})",
+    "designRule": {
+      "parts_sale": "Vente de pièces seule"
+    },
+    "designSaveFailed": "Impossible de changer le design de la facture"
   },
   "parts": {
     "title": "Pièces",
@@ -349,6 +360,7 @@
     "workBay": "Poste de travail",
     "selectWorkBay": "Choisir un poste…",
     "noWorkBay": "Aucun poste",
+    "noTechnician": "Sans technicien",
     "technicians": "Techniciens",
     "otherTeamMembers": "Autres membres de l’équipe",
     "makeTechnician": "En fait un technicien",
@@ -410,5 +422,50 @@
     "remove": "Retirer ce point",
     "notAnswered": "Rien trouvé à ce sujet pour l'instant",
     "answered": "{count} constat(s) à ce sujet"
+  },
+  "videoCall": {
+    "title": "Appel vidéo",
+    "none": "Aucun appel vidéo sur cet ordre de travail.",
+    "needsSchedule": "Définissez d'abord une heure de début. La réunion suit le planning.",
+    "add": "Ajouter une réunion {provider}",
+    "addedHere": "ajouté depuis cet ordre de travail",
+    "fromCalendar": "depuis l'événement du calendrier",
+    "join": "Rejoindre",
+    "copy": "Copier le lien",
+    "copied": "Lien copié",
+    "remove": "Supprimer",
+    "removeTitle": "Supprimer l'appel vidéo ?",
+    "removeDescription": "La réunion est supprimée chez le fournisseur. Ceux qui ont déjà le lien ne pourront plus rejoindre.",
+    "created": "Réunion créée. Le lien est sur l'ordre de travail.",
+    "removed": "Réunion supprimée",
+    "failed": "Une erreur s'est produite",
+    "shareHint": "Le client ne reçoit ce lien que si vous l'envoyez.",
+    "shareHintNoChannel": "Le client ne reçoit ce lien que si vous l'envoyez. Aucun canal de messagerie n'est configuré : copiez-le vous-même dans un message.",
+    "send": {
+      "button": "Envoyer au client",
+      "title": "Envoyer le lien de l'appel vidéo",
+      "description": "Envoyer le lien de connexion à {name}.",
+      "messageLabel": "Note (facultatif)",
+      "messagePlaceholder": "Ce que le client doit savoir avant l'appel...",
+      "linkAlwaysIncluded": "Le lien de connexion est toujours ajouté sous votre note.",
+      "sms": "SMS",
+      "email": "E-mail",
+      "telegram": "Telegram",
+      "noPhone": "(pas de téléphone)",
+      "noEmail": "(pas d'e-mail)",
+      "noTelegram": "(pas de chat lié)",
+      "notAvailable": "(non configuré)",
+      "cancel": "Annuler",
+      "send": "Envoyer",
+      "sent": "Lien de l'appel vidéo envoyé",
+      "partlySent": "Envoyé par {sent} ; pas par {failed}",
+      "failed": "Impossible d'envoyer le lien"
+    },
+    "notify": {
+      "invitation": "Bonjour {name}, votre appel vidéo concernant le véhicule {vehicle} a lieu le {when}. Rejoignez-le ici : {url}",
+      "invitationNoTime": "Bonjour {name}, voici le lien de l'appel vidéo concernant le véhicule {vehicle} : {url}",
+      "joinLine": "Rejoignez l'appel vidéo ici : {url}",
+      "emailSubject": "Appel vidéo concernant votre {vehicle}"
+    }
   }
 }

+ 101 - 27
messages/fr/settings.json

@@ -7,7 +7,7 @@
       "billing": "Facturation et documents",
       "communications": "Communications",
       "workshop": "Atelier",
-      "integrations": "Intégrations",
+      "integrations": "Applications et API",
       "system": "Système"
     },
     "items": {
@@ -91,6 +91,10 @@
         "title": "Rapports planifiés",
         "description": "Envoi automatique de rapports"
       },
+      "integrations": {
+        "title": "Intégrations",
+        "description": "Calendriers, appels vidéo et services connectés"
+      },
       "webhooks": {
         "title": "Webhooks",
         "description": "Notifications d'événements en temps réel"
@@ -130,6 +134,10 @@
       "providers": {
         "title": "Fournisseurs",
         "description": "E-mail, SMS, WhatsApp et Telegram"
+      },
+      "inspectionReminders": {
+        "title": "Rappels de contrôle",
+        "description": "Lien de réservation, durée et délai"
       }
     },
     "pro": "PRO"
@@ -572,7 +580,16 @@
     "quoteLockTriggerSent": "Une fois le devis envoyé",
     "quoteLockTriggerAccepted": "Une fois le devis accepté",
     "quoteLockTriggerSentHint": "Verrouille dès que le devis part chez le client. À choisir uniquement si vous ne révisez jamais un devis en cours de négociation.",
-    "quoteLockTriggerAcceptedHint": "Verrouille une fois le devis accepté par le client ou converti en intervention. Les devis envoyés restent modifiables."
+    "quoteLockTriggerAcceptedHint": "Verrouille une fois le devis accepté par le client ou converti en intervention. Les devis envoyés restent modifiables.",
+    "freezeTitle": "Factures antérieures au verrouillage",
+    "freezeBody": "{count, plural, one {# facture a atteint} other {# factures ont atteint}} un client avant que cette version puisse verrouiller ce qu'une facture imprime. Elles s'impriment toujours avec vos coordonnées, logo et design actuels, donc un changement fait aujourd'hui les atteint aussi. Les verrouiller conserve chacune telle qu'elle s'imprime maintenant.",
+    "freezeNone": "Chaque facture ayant atteint un client est verrouillée telle qu'elle a été envoyée.",
+    "freezeButton": "Les verrouiller avec le design actuel",
+    "freezeConfirmTitle": "Verrouiller {count, plural, one {# facture} other {# factures}} ?",
+    "freezeConfirmBody": "Chacune conserve le design, le logo, les coordonnées de l'atelier, le client, le véhicule et les constats avec lesquels elle s'imprime maintenant. Irréversible ; les factures restent modifiables ou verrouillées en modification exactement comme aujourd'hui.",
+    "freezeProgress": "{done} sur {total} verrouillées",
+    "freezeDone": "{count, plural, one {# facture verrouillée} other {# factures verrouillées}}",
+    "freezeFailed": "Impossible de verrouiller les factures"
   },
   "layoutEditor": {
     "helpText": "Glissez pour réorganiser. Utilisez le sélecteur de largeur pour placer les sections côte à côte. Les sections consécutives en demi-largeur (G/D) s'afficheront en deux colonnes.",
@@ -634,6 +651,12 @@
       "bank_account": "Compte bancaire",
       "footer_note": "Note de bas de page",
       "org_number": "Numéro d'organisation",
+      "payment_terms": "Conditions de paiement",
+      "due_date": "Date d'échéance",
+      "title": "Titre",
+      "invoice_number": "N° de facture",
+      "customer_number": "N° de client",
+      "date": "Date",
       "portal_link": "Lien du portail"
     },
     "presetsTitle": "Partir d’une disposition",
@@ -808,32 +831,17 @@
     "paymentTermsPlaceholder": "Net 14 jours",
     "paymentTermsHint": "Texte des conditions de paiement sur les factures (ex. \"Net 14 jours\", \"A réception\")",
     "onlinePayments": "Paiements en ligne",
-    "onlinePaymentsDescription": "Permettez aux clients de payer les factures en ligne via des liens de facture partages.",
-    "stripe": "Stripe",
-    "stripeDescription": "Accepter les paiements par carte de credit/debit",
-    "secretKey": "Cle secrète",
-    "publishableKey": "Cle publique",
-    "webhookSecret": "Secret du webhook",
-    "webhookUrl": "URL du webhook",
-    "stripeWebhookHint": "Ajoutez cette URL dans votre tableau de bord Stripe sous Webhooks. Abonnez-vous a <code>checkout.session.completed</code>.",
-    "vipps": "Vipps",
-    "vippsDescription": "Accepter les paiements mobiles Vipps (Norvege)",
-    "clientId": "ID client",
-    "clientSecret": "Secret client",
-    "subscriptionKey": "Cle d'abonnement",
-    "merchantSerialNumber": "Numéro de serie du commercant",
-    "testMode": "Mode test",
-    "callbackUrl": "URL de callback",
-    "termsOfSale": "Conditions de vente (Salgsvilkår)",
-    "termsOfSaleDescription": "Requis par Vipps. Les clients peuvent les consulter depuis la page de facture.",
+    "onlinePaymentsDescription": "Laissez les clients payer les factures depuis le lien que vous leur envoyez. Stripe, Vipps et PayPal se connectent sous Intégrations et peuvent fonctionner côte à côte.",
+    "onlinePaymentsHint": "Quand un client paie, le prestataire prévient Torqvoice et la facture se marque payée toute seule. La page de chaque prestataire affiche l'URL de notification dont il a besoin.",
+    "offered": "Sur les liens de facture",
+    "paused": "Connecté, non proposé",
+    "termsOfSale": "Conditions de vente",
+    "termsOfSaleDescription": "Ce qu'un client accepte en payant en ligne. Vipps exige des conditions publiées ; la facture y renvoie dans tous les cas.",
+    "termsOfSaleText": "Texte des conditions",
     "externalTermsUrl": "URL des conditions externes (optionnel)",
     "externalTermsUrlHint": "Lien vers une page externe avec vos conditions. Si defini, la facture redirigera ici.",
     "publicTermsUrl": "URL publique des conditions",
     "publicTermsUrlHint": "Partagez cette URL avec Vipps ou liez-la depuis votre site web.",
-    "paypal": "PayPal",
-    "paypalDescription": "Accepter les paiements PayPal",
-    "sandboxMode": "Mode sandbox",
-    "paypalWebhookHint": "Ajoutez cette URL dans votre tableau de bord PayPal sous Webhooks. Abonnez-vous a <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "savePayment": "Enregistrer les paramètres de paiement",
     "saved": "Paramètres de paiement enregistrés"
   },
@@ -1112,7 +1120,7 @@
   },
   "data": {
     "title": "Gestion des données",
-    "description": "Exportez vos données comme sauvegarde ou importez un fichier precedemment exporte.",
+    "description": "Récupérez les données de votre ancien système, téléchargez une sauvegarde complète ou restaurez-en une.",
     "exportTitle": "Exporter les données",
     "exportDescription": "Telechargez vos données sous forme de fichier zip. Sélectionnez les catégories a inclure.",
     "includeInBackup": "Inclure dans la sauvegarde :",
@@ -1199,7 +1207,16 @@
     "optTireHotel": "Hôtel à pneus",
     "optTireHotelDesc": "Jeux stockés, étagères, préparation et contrats de stockage",
     "optWorkshopConfig": "Configuration de l'atelier",
-    "optWorkshopConfigDesc": "Modèles de main-d'œuvre, rôles, webhooks et rapports programmés"
+    "optWorkshopConfigDesc": "Modèles de main-d'œuvre, rôles, webhooks et rapports programmés",
+    "importHubDescription": "Indiquez où se trouvent vos données aujourd'hui. Les imports depuis un tableur ou une application s'ajoutent à l'existant et peuvent être annulés ; la restauration d'une sauvegarde remplace tout.",
+    "sourceSpreadsheet": "Tableur ou CSV",
+    "sourceSpreadsheetDesc": "Exports de n'importe quel logiciel d'atelier, outil comptable, Google Contacts, Outlook ou Excel. Vous confirmez la correspondance des colonnes et voyez une simulation avant toute écriture.",
+    "sourceLubelogDesc": "Véhicules, historiques d'entretien et notes issus d'une sauvegarde LubeLog.",
+    "sourceInvoiceNinjaDesc": "Clients, produits, factures et paiements issus d'un export Invoice Ninja.",
+    "sourceBackup": "Sauvegarde Torqvoice",
+    "sourceBackupDesc": "Un fichier .zip ou .json téléchargé depuis cette page. La restauration remplace tout dans cet atelier.",
+    "restore": "Restaurer",
+    "restoreTitle": "Restaurer une sauvegarde"
   },
   "license": {
     "title": "Licence marque blanche",
@@ -1740,6 +1757,17 @@
         "feature": "Messagerie Telegram",
         "description": "Envoyez et recevez des messages Telegram avec vos clients depuis Torqvoice."
       }
+    },
+    "moved": {
+      "title": "These settings moved to Integrations",
+      "description": "Email, SMS, WhatsApp and Telegram are set up in Integrations now, alongside calendars and everything else you connect. What you had set up came with them, so there is nothing to enter again.",
+      "connectedTo": "Connected through {vendor}",
+      "notSetUp": "Not set up yet",
+      "manage": "Manage",
+      "notConnected": "Not connected",
+      "goToIntegrations": "Go to Integrations",
+      "platformMail": "Sending through the platform mail server. Connect your own to send from your address.",
+      "onByDefault": "On by default"
     }
   },
   "designer": {
@@ -1749,6 +1777,14 @@
     "galleryTitle": "Partir d'un modèle",
     "galleryHint": "Choisissez un point de départ. Vous pourrez tout modifier dans le concepteur.",
     "yourDesigns": "Vos maquettes",
+    "autoRuleHint": "Les ventes sans véhicule s'impriment avec le design qui leur est attribué, sauf si la vente ou son client en a choisi un autre.",
+    "useForPartsSale": "Utiliser pour les ventes de pièces seules",
+    "usedForPartsSale": "Utilisé pour les ventes de pièces seules",
+    "autoRule": {
+      "parts_sale": "Vente de pièces seule"
+    },
+    "autoRuleSaved": "Utilisation de « {name} » mise à jour",
+    "couldNotSaveRule": "Impossible de mettre à jour la règle",
     "templates": "Modèles",
     "inUse": "Utilisée",
     "savedOn": "Enregistrée le {date}",
@@ -1908,6 +1944,7 @@
       "serviceType": "Réparation",
       "companyName": "Votre atelier",
       "slogan": "Un service de qualité en toute confiance",
+      "paymentTerms": "Payable sous 14 jours",
       "footerNote": "Merci de votre confiance",
       "value": "Exemple"
     },
@@ -1923,11 +1960,22 @@
     "sloganPlaceholderHint": "Aucun slogan n'est défini, la feuille affiche donc un texte d'exemple qui ne sera pas imprimé.",
     "sloganSetHint": "Votre slogan provient des informations de votre entreprise.",
     "sloganLink": "Paramètres de l'entreprise",
+    "paymentTerms": "Conditions de paiement",
+    "paymentTermsPlaceholderHint": "Aucune condition de paiement n'est définie, la feuille affiche donc un texte d'exemple qui ne sera pas imprimé.",
+    "paymentTermsSetHint": "Vos conditions de paiement proviennent des paramètres de paiement.",
+    "paymentTermsLink": "Paramètres de paiement",
+    "titleText": "Texte du titre",
+    "titleTextHint": "Le nom que porte ce document. Laissé vide, il imprime son propre nom dans la langue du lecteur.",
     "placeholder": "Exemple",
     "leaveTitle": "Quitter sans enregistrer ?",
     "leaveBody": "Les modifications de cette maquette ne sont pas enregistrées et seront perdues.",
     "leaveConfirm": "Quitter",
-    "boldField": "Gras"
+    "boldField": "Gras",
+    "deleteDesignDrafts": "{count, plural, one {# brouillon de facture} other {# brouillons de facture}} qui l'utilisent reviendront au design par défaut.",
+    "deleteDesignCustomers": "{count, plural, one {# client} other {# clients}} qui y sont associés reviendront au design par défaut.",
+    "setDefault": "Définir par défaut",
+    "defaultSet": "« {name} » est maintenant utilisé",
+    "couldNotSetDefault": "Impossible de définir le design par défaut"
   },
   "preview": {
     "title": "Révision des freins et vidange",
@@ -1942,5 +1990,31 @@
     "partSyntheticOil": "Huile synthétique 5W-30",
     "laborBrakeReplacement": "Remplacement des freins",
     "laborOilChange": "Vidange"
+  },
+  "inspectionReminders": {
+    "title": "Rappels de contrôle et réservation",
+    "description": "Comment se comporte le lien de réservation d'un rappel. Réglé une fois ; chaque campagne l'utilise.",
+    "duration": "Durée du contrôle (minutes)",
+    "durationHint": "Combien de temps un contrôle occupe un pont. Le lien réserve des créneaux de cette durée.",
+    "leadDays": "Réservation au plus tôt (jours)",
+    "leadDaysHint": "1 signifie que le premier créneau proposé est demain, jamais aujourd'hui.",
+    "horizonWeeks": "Jusqu'à quand (semaines)",
+    "horizonHint": "Les clients peuvent choisir tout créneau libre dans ce nombre de semaines.",
+    "reserve": "Garder libre pour les sans rendez-vous",
+    "reserveHint": "Ponts ou techniciens que le lien ne remplit jamais. 0 autorise tous les libres.",
+    "linkValidDays": "Le lien reste ouvert (jours)",
+    "linkValidHint": "Au moins {min}. Les liens restent aussi ouverts jusqu'à une semaine après l'échéance, selon le plus tard.",
+    "bookingMode": "Ce qu'une réservation crée",
+    "bookingModeDirect": "Un ordre de travail planifié sur le planning",
+    "bookingModeRequest": "Une demande que l'accueil confirme d'abord",
+    "bookingModeHint": "La réservation directe est ce qui rend le lien utile. Choisissez la demande si vous voulez valider à chaque fois.",
+    "phone": "Numéro de téléphone dans les messages",
+    "phoneHint": "Affiché comme alternative au lien. Par défaut, le numéro de l'atelier.",
+    "save": "Enregistrer",
+    "saved": "Paramètres enregistrés",
+    "saveFailed": "Enregistrement impossible",
+    "timeZoneDetected": "Le fuseau horaire est en automatique sous Localisation, le serveur utilise donc {zone}, le dernier fuseau vu dans un navigateur ayant enregistré les paramètres. Choisissez explicitement le fuseau de l'atelier pour que les horaires de réservation ne dépendent jamais de qui a enregistré en dernier.",
+    "timeZoneExplicit": "Les horaires de réservation et d'ouverture sont lus en {zone}.",
+    "timeZoneChange": "Modifier sous Localisation"
   }
 }

+ 217 - 3
messages/fr/vehicles.json

@@ -4,6 +4,7 @@
     "archived": "Archivés",
     "searchPlaceholder": "Rechercher par plaque, marque, modèle, NIV...",
     "addVehicle": "Ajouter un véhicule",
+    "importVehicles": "Importer",
     "emptySearch": "Aucun véhicule ne correspond à votre recherche.",
     "emptyArchived": "Aucun véhicule archivé.",
     "empty": "Aucun véhicule pour le moment.",
@@ -30,7 +31,12 @@
     "viewTable": "Table view",
     "viewGrid": "Grid view",
     "viewLargeGrid": "Large grid view",
-    "openMenu": "Open menu"
+    "openMenu": "Open menu",
+    "inspectionOverdue": "Contrôle en retard",
+    "inspectionDue30": "Dans 30 jours",
+    "inspectionDue90": "Dans 90 jours",
+    "inspectionDue": "Contrôle {date}",
+    "remindCustomers": "Rappeler ces clients"
   },
   "detail": {
     "vehicleArchived": "Ce véhicule est archivé",
@@ -90,7 +96,11 @@
     "severityUncommon": "Peu courant — signalé par peu de propriétaires",
     "severityRare": "Rare — signalements isolés mais critique lorsqu'il survient",
     "openMenu": "Open menu",
-    "dismissMaintenance": "Dismiss"
+    "dismissMaintenance": "Dismiss",
+    "inspectionDue": "Contrôle le {date}",
+    "inspectionCheckedAt": "Vérifié auprès du registre le {date}",
+    "notRegistered": "Non immatriculé",
+    "soldReported": "Vente signalée par le client le {date}"
   },
   "quotes": {
     "newQuote": "Nouveau devis",
@@ -130,6 +140,7 @@
     "descriptionLabel": "Description",
     "descriptionPlaceholder": "Détails optionnels...",
     "dueDateLabel": "Date d'échéance",
+    "dueTimeLabel": "Heure (facultatif)",
     "dueMileageLabel": "Kilométrage d'échéance",
     "dueMileagePlaceholder": "ex. 50000",
     "reminderUpdated": "Rappel mis à jour",
@@ -338,7 +349,20 @@
     "sectionAssignment": "Photo et titulaire",
     "sectionDetails": "Détails",
     "addScannedOwner": "Ajouter le client {name}",
-    "ownerSaveFailed": "Impossible de créer le client"
+    "ownerSaveFailed": "Impossible de créer le client",
+    "lookupPlate": "Rechercher la plaque",
+    "lookingUp": "Interrogation du registre des véhicules...",
+    "lookupSuccess": "Champs remplis depuis {source}",
+    "lookupNotFound": "Le registre ne connaît aucun véhicule avec cette plaque",
+    "lookupFailed": "Impossible de rechercher la plaque",
+    "lookupUnavailable": "Connectez un registre des véhicules dans Paramètres → Intégrations",
+    "lookupEnterPlate": "Saisissez d'abord une plaque",
+    "lookupInspectionDue": "{source} : prochain contrôle périodique le {date}",
+    "lookupSource": "Rempli depuis {source}",
+    "inspectionDue": "Prochain contrôle périodique",
+    "inspectionDueHint": "Saisissez la date si aucun registre ne la fournit. Les rappels et le tableau de bord l'utilisent.",
+    "inspectionDueFromRegistry": "Issue du registre des véhicules. Modifiez-la ici pour la remplacer jusqu'à la prochaine synchronisation.",
+    "lookupEnterPlateOrVin": "Saisissez d'abord une plaque ou un VIN"
   },
   "archiveDialog": {
     "title": "Archiver le véhicule",
@@ -353,5 +377,195 @@
     "previousPage": "Previous page",
     "nextPage": "Next page",
     "lastPage": "Last page"
+  },
+  "inspectionReminders": {
+    "title": "Rappels de contrôle technique",
+    "description": "Prévenez les clients dont le contrôle approche, avec un lien pour réserver un créneau. Rien n'est envoyé avant que vous confirmiez la liste.",
+    "window": "Échéance sous",
+    "window30": "30 prochains jours",
+    "window60": "60 prochains jours",
+    "window90": "90 prochains jours",
+    "channel": "Envoyer par",
+    "channels": {
+      "sms": "SMS",
+      "whatsapp": "WhatsApp",
+      "email": "E-mail",
+      "telegram": "Telegram"
+    },
+    "willReceive": "recevront un message",
+    "excludedCount": "{count} exclus",
+    "nobodyToRemind": "Personne à rappeler sur cette période. Chaque véhicule à échéance a déjà été rappelé, a réservé ou ne peut pas être joint.",
+    "overdueOn": "En retard depuis le {date}",
+    "excludedTitle": "Non inclus",
+    "excludedDescription": "Chacun a une raison. Corrigez-la sur le client ou le véhicule et il apparaîtra dans la liste.",
+    "reasons": {
+      "noCustomer": "Pas de client",
+      "optedOut": "Refuse les rappels",
+      "noRecipient": "Pas de contact pour ce canal",
+      "alreadyReminded": "Déjà rappelé pour cette échéance",
+      "cooldown": "Contacté récemment",
+      "soldReported": "Le client signale une vente",
+      "notRegistered": "Non immatriculé",
+      "booked": "Déjà réservé"
+    },
+    "message": "Message",
+    "subject": "Objet",
+    "body": "Texte",
+    "placeholders": "Champs : {customerName} {vehicle} {plate} {dueDate} {workshopName} {bookingLink} {phone}",
+    "unknownPlaceholders": "Champs inconnus : {list}",
+    "sample": "Exemple pour le premier destinataire",
+    "segments": "{count, plural, one {# segment SMS} other {# segments SMS}}",
+    "bookingModeDirectNote": "Le lien réserve un créneau de {minutes} minutes directement sur le planning.",
+    "bookingModeRequestNote": "Le lien envoie une demande avec l'heure choisie, que l'accueil confirme.",
+    "linkValidNote": "Les liens restent ouverts au moins {days} jours et au-delà de l'échéance.",
+    "sendTest": "M'envoyer un test",
+    "testSent": "Test en file pour {to}",
+    "testFailed": "Impossible d'envoyer le test",
+    "sendButton": "Envoyer à {count}",
+    "confirmTitle": "Envoyer les rappels ?",
+    "confirmDescription": "{count} messages partent par {channel}, un par véhicule, une seule fois. Un véhicule n'est jamais rappelé deux fois pour la même échéance.",
+    "confirmInput": "Saisissez {count} pour confirmer",
+    "cancel": "Annuler",
+    "sendNow": "Envoyer",
+    "created": "{count} rappels en file",
+    "sendFailed": "Impossible de créer la campagne",
+    "loadFailed": "Impossible de charger la liste",
+    "campaign": {
+      "title": "Campagne du {date}",
+      "summary": "{channel}, échéances sous {window} jours",
+      "recipients": "Destinataires",
+      "sent": "Envoyés",
+      "failed": "Échoués",
+      "booked": "Réservés",
+      "retryFailedButton": "Réessayer {count} échoués",
+      "retried": "{count} remis en file",
+      "retryFailed": "Impossible de réessayer",
+      "rows": "Destinataires",
+      "status": {
+        "scheduled": "En file",
+        "sent": "Envoyé",
+        "failed": "Échoué",
+        "cancelled": "Annulé"
+      },
+      "bookedOn": "Réservé le {date}",
+      "requestedOn": "Demandé le {date}",
+      "cancelledOn": "Annulé le {date}",
+      "none": "Aucune campagne pour l'instant.",
+      "start": "Envoyer des rappels de contrôle",
+      "recipientsCount": "{count} destinataires",
+      "failedCount": "{count} échoués",
+      "bookedCount": "{count} réservés"
+    },
+    "defaults": {
+      "smsBody": "Bonjour {customerName}, le contrôle technique de votre {vehicle} ({plate}) est dû le {dueDate}. Réservez un créneau : {bookingLink} ou appelez-nous au {phone}. {workshopName}",
+      "emailSubject": "Contrôle technique le {dueDate} pour {vehicle}",
+      "emailBody": "Bonjour {customerName},\n\nLe contrôle technique de votre {vehicle} ({plate}) est dû le {dueDate}.\n\nRéservez ici le créneau qui vous convient : {bookingLink}\n\nOu appelez-nous au {phone}.\n\nCordialement,\n{workshopName}"
+    },
+    "refresh": "Actualiser la liste",
+    "timeZoneRequired": "Le fuseau horaire de l'atelier est en automatique : les horaires de réservation suivraient le dernier navigateur ayant enregistré les paramètres (actuellement {zone}). Aucun rappel ne peut partir tant qu'un fuseau n'est pas choisi.",
+    "timeZoneRequiredLink": "Choisissez-le sous Localisation"
+  },
+  "plateLookup": {
+    "open": "Recherche de plaque",
+    "shortcut": "Ctrl+Shift+L",
+    "title": "Recherche de plaque",
+    "description": "Saisissez une immatriculation pour voir ce que votre atelier et le registre des véhicules en savent",
+    "placeholder": "Saisissez une plaque",
+    "search": "Rechercher",
+    "hintEnter": "Entrée pour rechercher",
+    "hintClose": "Échap pour fermer",
+    "recent": "Récentes",
+    "clearRecent": "Effacer",
+    "looking": "Interrogation de {source}…",
+    "lookingWorkshop": "Vérification de votre atelier…",
+    "invalid": "Cela ne ressemble pas à une plaque",
+    "inWorkshop": "Dans votre atelier",
+    "inWorkshopArchived": "Dans votre atelier, archivé",
+    "notInWorkshop": "Pas encore dans votre atelier",
+    "notInWorkshopHint": "Le registre connaît ce véhicule. Ajoutez-le et les champs ci-dessous suivent.",
+    "notInWorkshopNoRegistry": "Ajoutez-le à la main, ou vérifiez la plaque.",
+    "openVehicle": "Ouvrir le véhicule",
+    "addVehicle": "Ajouter le véhicule",
+    "noOwner": "Aucun client associé",
+    "mileage": "{km} km",
+    "services": "{count, plural, =0 {Aucun historique d'entretien} one {# intervention} other {# interventions}}",
+    "lastService": "Dernier entretien {date}",
+    "registryNotFound": "{source} n'a aucun véhicule avec cette plaque",
+    "registryFailed": "{source} n'a pas pu être interrogé",
+    "registryFailedGeneric": "Le registre n'a pas pu être interrogé",
+    "fromSource": "Depuis {source}",
+    "registered": "Immatriculé",
+    "notRegistered": "Non immatriculé",
+    "vin": "VIN",
+    "copyVin": "Copier le VIN",
+    "copied": "Copié",
+    "fuel": "Carburant",
+    "transmission": "Boîte",
+    "engine": "Moteur",
+    "engineCode": "Code moteur",
+    "color": "Couleur",
+    "vehicleClass": "Catégorie",
+    "firstRegistered": "Première immatriculation",
+    "inspectionDue": "Contrôle dû",
+    "lastInspected": "Dernier contrôle",
+    "weights": "Masses",
+    "weightsValue": "{kerb} kg à vide, {gross} kg max",
+    "kerbOnly": "{kerb} kg à vide",
+    "grossOnly": "{gross} kg max",
+    "tyres": "Pneus",
+    "axle": "Essieu {n}",
+    "tyreSize": "Pneu",
+    "rimSize": "Jante",
+    "loadSpeed": "Charge / vitesse",
+    "overdue": "Dépassé",
+    "dueSoon": "Bientôt",
+    "emptyTitle": "Recherchez n'importe quelle plaque",
+    "emptyBody": "Voyez le véhicule, à qui il appartient ici et ce que {source} a enregistré. Pas encore le vôtre ? Ajoutez-le en une étape.",
+    "registryWord": "le registre"
+  },
+  "safety": {
+    "title": "Sécurité et problèmes connus",
+    "summaryRecalls": "{count, plural, one {# rappel} other {# rappels}}",
+    "summaryComplaints": "{count, plural, one {# plainte de propriétaire} other {# plaintes de propriétaires}}",
+    "expand": "Afficher les détails",
+    "collapse": "Masquer les détails",
+    "loading": "Interrogation de l'autorité de sécurité...",
+    "from": "De {source} pour {model}, mis à jour le {date}",
+    "stale": "{source} est injoignable ; rapport du {date} affiché",
+    "notKnown": "{source} n'a aucune fiche pour {model}.",
+    "notKnownHint": "Vérifiez la marque, le modèle et l'année, ou ajoutez le VIN pour identifier le modèle exactement.",
+    "recalls": "Rappels",
+    "recallsNone": "Aucune campagne de rappel",
+    "complaints": "Plaintes de propriétaires",
+    "complaintsNone": "Aucune déposée",
+    "crashes": "{count, plural, one {# accident} other {# accidents}}",
+    "fires": "{count, plural, one {# incendie} other {# incendies}}",
+    "rating": "Note aux crash-tests",
+    "notRated": "Non noté",
+    "stars": "{count} étoiles sur 5",
+    "frontal": "Frontal",
+    "side": "Latéral",
+    "rollover": "Retournement",
+    "ownersReport": "Ce que signalent les propriétaires",
+    "ofComplaints": "sur {total} plaintes déposées",
+    "crash": "Accident",
+    "fire": "Incendie",
+    "noExamples": "Les textes des plaintes ne sont pas encore dans ce rapport. Actualisez depuis la source pour les récupérer.",
+    "readAll": "Lire les {count} sur {source}",
+    "showRecalls": "{count, plural, one {Afficher # rappel} other {Afficher # rappels}}",
+    "hideRecalls": "Masquer les rappels",
+    "more": "Plus",
+    "less": "Moins",
+    "consequence": "Risque",
+    "remedy": "Remède",
+    "parkIt": "Ne pas conduire",
+    "parkOutside": "Stationner à l'extérieur",
+    "addFinding": "Ajouter aux observations",
+    "findingAdded": "Dans les observations",
+    "findingDescription": "Rappel {campaign} : {component}",
+    "refresh": "Actualiser depuis la source",
+    "refreshed": "Données de sécurité actualisées",
+    "openSite": "Ouvrir sur {source}",
+    "errorLoad": "Impossible de charger les données de sécurité"
   }
 }

+ 1 - 1
messages/it/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Chiedi della tua officina...",
   "clear": "Cancella conversazione",
   "error": "Qualcosa è andato storto. Riprova.",
-  "notEnabled": "L'IA non è abilitata. Configurala in Impostazioni → IA.",
+  "notEnabled": "L'IA non è collegata. Collega OpenAI o Anthropic in Impostazioni → Integrazioni.",
   "suggestion1": "Quali veicoli sono in scadenza per il tagliando?",
   "suggestion2": "Mostrami le fatture non pagate",
   "suggestion3": "Qual è il fatturato di questo mese?",

+ 27 - 2
messages/it/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "Numero WhatsApp registrato",
     "team_giveTechnicianTheApp": "Account dato a un tecnico solo da lavagna",
     "team_createDefaultRoles": "Ruoli standard creati",
-    "settings_setCustomFieldPlacement": "Posizione del campo personalizzato aggiornata"
+    "settings_setCustomFieldPlacement": "Posizione del campo personalizzato aggiornata",
+    "import_commit": "Foglio di calcolo importato",
+    "import_undo": "Importazione annullata",
+    "integration_connect": "Integrazione collegata",
+    "integration_disconnect": "Integrazione scollegata",
+    "service_setInvoiceDesign": "Design della fattura cambiato",
+    "settings_saveDocumentDesign": "Design del documento salvato",
+    "settings_deleteDocumentDesign": "Design del documento eliminato",
+    "settings_applyDocumentDesign": "Design del documento predefinito impostato",
+    "settings_setDocumentDesignRule": "Modificato quando viene usato un design",
+    "settings_freezeInvoices": "Fatture precedenti bloccate",
+    "inspection_reminders_sent": "Promemoria revisione inviati",
+    "inspection_reminders_send": "Promemoria revisione inviati",
+    "service_videoCall_send": "Link videochiamata inviato"
   },
   "summary": {
     "customField_create": "Campo personalizzato «{name}» creato",
@@ -275,6 +288,18 @@
     "work_bay_create": "Postazione «{name}» creata",
     "work_bay_update": "Postazione «{name}» aggiornata",
     "work_bay_delete": "Postazione {id} eliminata",
-    "settings_setCustomFieldPlacement": "Ha modificato dove viene stampato un campo personalizzato"
+    "settings_setCustomFieldPlacement": "Ha modificato dove viene stampato un campo personalizzato",
+    "import_commit": "Foglio di calcolo importato: {count} record creati",
+    "import_undo": "Importazione da foglio di calcolo annullata: {records} record rimossi",
+    "integration_connect": "{name} collegato",
+    "integration_disconnect": "{name} scollegato",
+    "service_setInvoiceDesign": "Ha cambiato il design con cui viene stampata una fattura",
+    "settings_saveDocumentDesign": "Ha salvato il design del documento {name}",
+    "settings_deleteDocumentDesign": "Ha eliminato il design del documento {name}",
+    "settings_applyDocumentDesign": "Ha impostato {name} come design del documento predefinito",
+    "settings_setDocumentDesignRule": "Modificato quando viene usato il design del documento {name}",
+    "settings_freezeInvoices": "Ha bloccato {count, plural, one {# fattura} other {# fatture}} precedenti al blocco con il design attuale",
+    "inspection_reminders_sent": "Inviati {count} promemoria revisione",
+    "service_videoCall_send": "Ha inviato il link della videochiamata via {channels}"
   }
 }

+ 2 - 1
messages/it/auth.json

@@ -13,7 +13,8 @@
     "errors": {
       "tooManyAttempts": "Troppi tentativi, riprovi più tardi.",
       "invalidCredentials": "Email o password non validi",
-      "passkeyFailed": "Accesso con passkey fallito. Riprova o usa email e password."
+      "passkeyFailed": "Accesso con passkey fallito. Riprova o usa email e password.",
+      "invalidOrigin": "Hai aperto Torqvoice su {origin}, ma è configurato per {configured}. Imposta NEXT_PUBLIC_APP_URL sull'indirizzo esatto che usi nel browser e riavvia Torqvoice."
     },
     "resetPasswordCta": "Reimpostala qui"
   },

+ 74 - 7
messages/it/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Oggi",
   "workOrder": "Ordine di lavoro",
+  "allDay": "Tutto il giorno",
+  "openDay": "Apri {date}",
+  "dayTotal": "{count, plural, =1 {1 evento} other {# eventi}}",
+  "views": {
+    "day": "Giorno",
+    "week": "Settimana",
+    "month": "Mese",
+    "year": "Anno",
+    "schedule": "Agenda",
+    "fourDays": "4 giorni"
+  },
+  "options": {
+    "showWeekends": "Mostra weekend",
+    "showCompleted": "Mostra completati",
+    "showWeekNumbers": "Mostra numeri delle settimane"
+  },
+  "toolbar": {
+    "previous": "Precedente",
+    "next": "Successivo",
+    "create": "Crea",
+    "showSidebar": "Mostra pannello laterale",
+    "hideSidebar": "Nascondi pannello laterale"
+  },
+  "sidebar": {
+    "calendars": "Calendari",
+    "legend": "Colori di stato"
+  },
+  "schedule": {
+    "empty": "Nulla in programma",
+    "emptyHint": "Nessun ordine di lavoro, promemoria, preventivo o messaggio in questo periodo."
+  },
+  "peek": {
+    "close": "Chiudi",
+    "open": {
+      "service": "Apri ordine di lavoro",
+      "reminder": "Apri promemoria",
+      "quote": "Apri preventivo",
+      "message": "Apri messaggi",
+      "external": "Apri"
+    }
+  },
   "filters": {
-    "services": "Servizi",
+    "services": "Ordini di lavoro",
     "reminders": "Promemoria",
     "quotes": "Preventivi",
-    "messages": "Messaggi"
+    "messages": "Messaggi",
+    "external": "Calendari collegati"
   },
   "selectVehicle": "Seleziona veicolo per ordine di lavoro",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Use today"
   },
   "more": "+ {count} altri",
-  "monthTotal": "{count} eventi questo mese",
+  "monthTotal": "{count} eventi",
   "contextMenu": {
     "newWorkOrder": "Nuovo ordine di lavoro",
+    "newWorkOrderAt": "Nuovo ordine di lavoro alle {time}",
     "newReminder": "Nuovo promemoria",
     "newQuote": "Nuovo preventivo",
     "openEvent": "Apri ({count})",
     "hint": "Suggerimento: fai clic con il tasto destro su un giorno per le azioni",
     "scheduleMessage": "Pianifica messaggio"
   },
+  "eventMenu": {
+    "open": "Apri",
+    "markDone": "Segna come fatto",
+    "markNotDone": "Segna come non fatto",
+    "deleteReminder": "Elimina promemoria",
+    "deleteReminderConfirm": "Eliminare il promemoria \"{title}\"? L'operazione non può essere annullata.",
+    "deleteMessage": "Elimina messaggio pianificato",
+    "deleteMessageConfirm": "Eliminare il messaggio pianificato \"{title}\"? Non verrà inviato.",
+    "deleted": "Eliminato",
+    "updated": "Aggiornato",
+    "failed": "Operazione non riuscita. Riprova."
+  },
   "events": {
     "status": {
       "completed": "Completato",
       "inProgress": "In corso",
       "waitingParts": "In attesa di ricambi",
       "pending": "In attesa",
+      "scheduled": "Pianificato",
       "sent": "Inviato",
       "approved": "Approvato",
       "draft": "Bozza",
@@ -66,16 +122,27 @@
       "overdue": "Scaduto",
       "upcoming": "In arrivo",
       "failed": "Non riuscito",
-      "queued": "In coda"
+      "queued": "In coda",
+      "busy": "Occupato"
     },
     "type": {
-      "service": "Servizio",
+      "service": "Ordine di lavoro",
       "reminder": "Promemoria",
       "quote": "Preventivo",
-      "message": "Messaggio"
+      "message": "Messaggio",
+      "external": "Calendario collegato"
     },
     "noEvents": "Nessun evento",
-    "selectDay": "Seleziona un giorno con eventi o crea un nuovo ordine di lavoro."
+    "selectDay": "Seleziona un giorno con eventi o crea un nuovo ordine di lavoro.",
+    "external": {
+      "source": "Da {source}",
+      "sourceUnknown": "Da un calendario collegato",
+      "allDay": "Tutto il giorno",
+      "open": "Apri in {source}",
+      "openGeneric": "Apri nel calendario",
+      "noLink": "Questo calendario non fornisce un link all'evento.",
+      "readOnly": "Tempo occupato da un calendario collegato. Modificalo lì e qui si aggiorna alla prossima sincronizzazione."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/it/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Numero di telefono copiato negli appunti",
     "taxIdLabel": "P. IVA",
     "taxExemptBadge": "Esente da imposta",
+    "invoiceDesignBadge": "Design fattura: {name}",
+    "invoiceDesignBadgeHint": "Le fatture di questo cliente si stampano con questo design invece di quello predefinito dell'officina. Aprilo nel designer.",
     "tabs": {
       "vehicles": "Veicoli ({count})",
       "invoices": "Fatture ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Auto",
     "sectionContact": "Contatto",
     "sectionBilling": "Fatturazione",
-    "addScannedVehicle": "Aggiungi il veicolo {vehicle}"
+    "addScannedVehicle": "Aggiungi il veicolo {vehicle}",
+    "invoiceDesign": "Design della fattura",
+    "invoiceDesignDefault": "Predefinito dell'officina",
+    "invoiceDesignHint": "Le fatture di questo cliente vengono stampate con questo design invece del predefinito dell'officina. Una fattura può comunque sceglierne uno proprio.",
+    "reminderOptOut": "Nessun promemoria",
+    "reminderOptOutHint": "Non includere questo cliente nei promemoria revisione o in altre campagne."
   },
   "serviceRequests": {
     "statusNew": "Nuova",
@@ -117,44 +124,5 @@
     "notesSaveError": "Salvataggio delle note non riuscito",
     "smsLabel": "Messaggio di richiesta servizio",
     "smsDefault": "Salve {name}, riguardo alla sua richiesta di servizio per il suo {vehicle}: "
-  },
-  "import": {
-    "title": "Importa clienti",
-    "description": "Importa clienti da file CSV, TSV o vCard.",
-    "dropFile": "Trascina e rilascia un file qui",
-    "supportedFormats": "CSV, TSV, TXT o vCard (.vcf)",
-    "browse": "Sfoglia file",
-    "noDataFound": "Nessun dato cliente trovato nel file",
-    "rowsFound": "{count} cliente/i trovato/i",
-    "colName": "Nome",
-    "colEmail": "Email",
-    "colPhone": "Telefono",
-    "colCompany": "Azienda",
-    "colAddress": "Indirizzo",
-    "chooseAnother": "Scegli un altro file",
-    "cancel": "Annulla",
-    "importButton": "Importa {count} cliente/i",
-    "importComplete": "Importazione completata",
-    "importedCount": "{count} cliente/i importato/i con successo",
-    "skippedRows": "{count} riga/e ignorata/e a causa di errori",
-    "rowError": "Riga {row} ({name}): {error}",
-    "done": "Fatto",
-    "importFailed": "Importazione dei clienti non riuscita",
-    "checkingDuplicates": "Controllo duplicati...",
-    "duplicatesFound": "{count} duplicato/i trovato/i",
-    "duplicatesBanner": "{count} cliente/i esistente/i corrispondono ai dati importati.",
-    "duplicatesHint": "Clicca Salta o Unisci per cambiare come viene gestito ogni duplicato.",
-    "colStatus": "Stato",
-    "actionSkip": "Salta",
-    "actionMerge": "Unisci",
-    "actionNew": "Nuovo",
-    "summaryNew": "{count} nuovo/i",
-    "summaryMerge": "{count} unisci",
-    "summarySkip": "{count} salta",
-    "skipAll": "Salta tutti",
-    "mergeAll": "Unisci tutti",
-    "resultCreated": "{count} cliente/i creato/i",
-    "resultMerged": "{count} cliente/i unito/i",
-    "resultSkipped": "{count} cliente/i saltato/i"
   }
 }

+ 12 - 1
messages/it/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Osservazioni recenti",
     "recentActivity": "Attività recente",
     "gettingStarted": "Per iniziare",
-    "tireHotel": "Hotel pneumatici"
+    "tireHotel": "Hotel pneumatici",
+    "inspectionsDue": "Revisioni in scadenza"
   },
   "maintenance": {
     "title": "Manutenzione prevista in scadenza",
@@ -252,5 +253,15 @@
     "roomLeft": "Spazio per altri {count}",
     "needsPrep": "Da preparare",
     "unbilled": "Non fatturato ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Revisioni in scadenza",
+    "viewAll": "Vedi tutti i veicoli in scadenza",
+    "overdue": "Scadute",
+    "within30": "Prossimi 30 giorni",
+    "within90": "Prossimi 90 giorni",
+    "noData": "Nessuna revisione nei prossimi 90 giorni",
+    "overdueSince": "Scaduta dal {date}",
+    "remind": "Invia promemoria"
   }
 }

+ 228 - 0
messages/it/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "Importa {entity}",
+  "cancel": "Annulla",
+  "done": "Fatto",
+  "entities": {
+    "customers": "clienti",
+    "vehicles": "veicoli",
+    "services": "storico interventi"
+  },
+  "entityHints": {
+    "customers": "Nomi, contatti, indirizzi. Una riga per cliente.",
+    "vehicles": "Marca, modello, anno, targa, VIN. Una colonna proprietario collega o crea il cliente.",
+    "services": "Lavori e fatture passati. Una riga per lavoro, con il veicolo a cui appartiene."
+  },
+  "groups": {
+    "customer": "Cliente",
+    "vehicle": "Veicolo",
+    "service": "Intervento"
+  },
+  "steps": {
+    "upload": "Scegli cosa importare e carica il file dal tuo sistema attuale.",
+    "mapping": "Controlla quale colonna diventa quale campo. Non viene ancora scritto nulla.",
+    "preview": "Simulazione: questo è esattamente ciò che farà l'importazione. Controlla, poi importa.",
+    "importing": "Scrittura delle righe in corso. Tieni aperta questa finestra.",
+    "result": "Importazione terminata."
+  },
+  "upload": {
+    "drop": "Trascina qui un file",
+    "formats": "CSV, Excel (.xlsx) o vCard (.vcf), fino a 25 MB e 50.000 righe",
+    "browse": "Sfoglia file",
+    "reading": "Lettura del file...",
+    "templateHint": "Non hai ancora un'esportazione? Compila un modello e carica quello.",
+    "templateXlsx": "Modello Excel",
+    "templateCsv": "Modello CSV",
+    "sourcesHint": "Funziona con le esportazioni di qualsiasi gestionale per officine, software di contabilità, Google Contatti, Outlook o foglio di calcolo. I nomi delle colonne vengono riconosciuti in molte lingue; confermi la mappatura prima che venga scritto qualsiasi dato."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# riga} other {# righe}}",
+    "colColumn": "Colonna nel tuo file",
+    "colSample": "Valori di esempio",
+    "colField": "Diventa",
+    "ignore": "Ignora questa colonna",
+    "source": {
+      "preset": "Formato riconosciuto",
+      "header": "Abbinata dall'intestazione",
+      "values": "Dedotta dai valori"
+    },
+    "suggestWithAi": "Suggerisci con IA",
+    "aiApplied": "{count, plural, one {# colonna mappata} other {# colonne mappate}} dall'IA. Controllale prima di continuare.",
+    "chooseAnother": "Scegli un altro file",
+    "missingRequired": "Ancora necessari: {fields}",
+    "dryRunHint": "La simulazione confronta ogni riga con i tuoi dati esistenti senza scrivere nulla.",
+    "runDryRun": "Esegui simulazione"
+  },
+  "options": {
+    "dateFormat": "Formato data",
+    "dateAuto": "Rileva automaticamente",
+    "dateDMY": "Giorno / mese / anno",
+    "dateMDY": "Mese / giorno / anno",
+    "dateYMD": "Anno / mese / giorno",
+    "decimal": "Separatore decimale",
+    "decimalAuto": "Rileva automaticamente",
+    "decimalDot": "Punto (1,234.56)",
+    "decimalComma": "Virgola (1.234,56)",
+    "countryCode": "Prefisso internazionale per i numeri di telefono",
+    "duplicates": "Quando una riga corrisponde a un record esistente",
+    "duplicatesUpdate": "Aggiorna il record esistente",
+    "duplicatesSkip": "Salta la riga",
+    "duplicatesCreate": "Crea comunque un nuovo record"
+  },
+  "summary": {
+    "create": "Verranno creati",
+    "update": "Verranno aggiornati",
+    "skip": "Verranno saltati",
+    "error": "Non importabili",
+    "warnings": "Con avvisi",
+    "customersToCreate": "{count, plural, one {# nuovo cliente} other {# nuovi clienti}}",
+    "customersToUpdate": "{count, plural, one {# cliente aggiornato} other {# clienti aggiornati}}",
+    "vehiclesToCreate": "{count, plural, one {# nuovo veicolo} other {# nuovi veicoli}}",
+    "vehiclesToUpdate": "{count, plural, one {# veicolo aggiornato} other {# veicoli aggiornati}}",
+    "servicesToCreate": "{count, plural, one {# intervento} other {# interventi}}"
+  },
+  "actions": {
+    "create": "Crea",
+    "update": "Aggiorna",
+    "skip": "Salta",
+    "error": "Errore"
+  },
+  "matchOn": {
+    "number": "numero cliente",
+    "email": "email",
+    "phone": "telefono",
+    "name": "nome",
+    "vin": "VIN",
+    "plate": "targa",
+    "makeModelYear": "marca, modello e anno"
+  },
+  "preview": {
+    "colAction": "Azione",
+    "colOwner": "Proprietario",
+    "colDetails": "Dettagli",
+    "empty": "Nessuna riga corrisponde a questo filtro.",
+    "matchedOn": "Corrisponde a {name} (per {on})",
+    "linksCustomer": "Collegato al cliente {name}",
+    "linksVehicle": "Collegato a {label}",
+    "sameCustomerAsRow": "Stesso cliente della riga {row}",
+    "sameVehicleAsRow": "Stesso veicolo della riga {row}",
+    "createsCustomer": "Crea il cliente",
+    "createsVehicle": "Crea il veicolo",
+    "downloadReport": "Scarica il report della simulazione",
+    "limitExceeded": "Il tuo piano consente ancora {remaining} clienti, ma questa importazione ne creerebbe {needed}. Rimuovi delle righe o passa a un piano superiore.",
+    "pageInfo": "{from}–{to} di {total}",
+    "prev": "Precedente",
+    "next": "Successivo",
+    "backToMapping": "Torna alla mappatura",
+    "import": "Importa {count, plural, one {# riga} other {# righe}}",
+    "nothingWrittenYet": "Non è ancora stato scritto nulla. L'importazione può essere annullata in seguito da Impostazioni → Dati."
+  },
+  "importing": {
+    "title": "Importazione in corso...",
+    "starting": "Avvio",
+    "progress": "{done} di {total} righe",
+    "keepOpen": "I file grandi richiedono un minuto o due."
+  },
+  "result": {
+    "title": "Importazione completata",
+    "undoneTitle": "Importazione annullata",
+    "created": "{count, plural, one {# record creato} other {# record creati}}",
+    "updated": "{count, plural, one {# record aggiornato} other {# record aggiornati}}",
+    "skipped": "{count, plural, one {# riga saltata} other {# righe saltate}}",
+    "failed": "{count, plural, one {# riga non importata} other {# righe non importate}}",
+    "writeFailures": "{count, plural, one {# riga non riuscita durante la scrittura} other {# righe non riuscite durante la scrittura}}",
+    "rowFailure": "Riga {row}: {error}",
+    "undo": "Annulla questa importazione",
+    "undone": "Rimossi {customers} clienti, {vehicles} veicoli e {services} interventi.",
+    "undoHint": "L'annullamento rimuove ogni record creato da questa importazione. I record che sono stati solo aggiornati mantengono i nuovi valori."
+  },
+  "history": {
+    "title": "Cronologia importazioni",
+    "description": "Importazioni da foglio di calcolo eseguite in questa officina. L'annullamento rimuove ciò che un'importazione ha creato.",
+    "loading": "Caricamento...",
+    "statusUndone": "Annullata",
+    "counts": "{created} creati, {updated} aggiornati, {skipped} saltati, {failed} non riusciti",
+    "undo": "Annulla",
+    "undone": "Rimossi {customers} clienti, {vehicles} veicoli e {services} interventi.",
+    "undoFailed": "Impossibile annullare l'importazione",
+    "confirmTitle": "Annullare questa importazione?",
+    "confirmDescription": "Rimuove ciò che {file} ha creato: {customers} clienti, {vehicles} veicoli e {services} interventi. I record che l'importazione ha solo aggiornato vengono conservati.",
+    "cancel": "Annulla",
+    "confirm": "Annulla importazione"
+  },
+  "fields": {
+    "customer": {
+      "name": "Nome cliente",
+      "firstName": "Nome",
+      "lastName": "Cognome",
+      "customerNumber": "Numero cliente",
+      "email": "Email",
+      "phone": "Telefono",
+      "company": "Azienda",
+      "address": "Indirizzo",
+      "street": "Via",
+      "postalCode": "CAP",
+      "city": "Città",
+      "state": "Provincia / regione",
+      "country": "Paese",
+      "taxId": "Partita IVA / codice fiscale",
+      "notes": "Note cliente"
+    },
+    "vehicle": {
+      "description": "Veicolo (anno marca modello in una sola colonna)",
+      "make": "Marca",
+      "model": "Modello",
+      "year": "Anno",
+      "vin": "VIN",
+      "licensePlate": "Targa",
+      "color": "Colore",
+      "mileage": "Chilometraggio",
+      "fuelType": "Tipo di carburante",
+      "transmission": "Cambio",
+      "engineSize": "Cilindrata",
+      "engineCode": "Codice motore",
+      "purchaseDate": "Data di acquisto",
+      "purchasePrice": "Prezzo di acquisto"
+    },
+    "service": {
+      "date": "Data intervento",
+      "title": "Titolo intervento",
+      "description": "Descrizione",
+      "total": "Importo totale",
+      "mileage": "Chilometraggio all'intervento",
+      "invoiceNumber": "Numero fattura",
+      "notes": "Note intervento",
+      "technician": "Tecnico"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Manca il nome del cliente",
+    "invalid_email": "L'email \"{value}\" non è valida ed è stata omessa",
+    "invalid_vin": "Il VIN \"{value}\" non è di 17 caratteri; conservato così com'è",
+    "vehicle_make_model_required": "Mancano marca e modello",
+    "vehicle_year_required": "Manca l'anno",
+    "vehicle_year_invalid": "L'anno \"{value}\" non è un anno valido",
+    "vehicle_identifier_required": "Nessuna targa, VIN o marca e modello per identificare il veicolo",
+    "vehicle_not_found": "Nessun veicolo esistente corrisponde a \"{value}\" e non ci sono abbastanza dati per crearne uno",
+    "service_date_required": "Manca la data dell'intervento",
+    "service_date_invalid": "Impossibile leggere la data \"{value}\"",
+    "service_title_required": "Mancano il titolo o la descrizione dell'intervento",
+    "invalid_date": "Impossibile leggere {field} \"{value}\", omesso",
+    "invalid_number": "{field} \"{value}\" non è un numero ed è stato omesso",
+    "duplicate_invoice_number": "Il numero fattura {value} esiste già",
+    "write_failed": "Impossibile scrivere: {value}"
+  },
+  "errors": {
+    "uploadFailed": "Impossibile leggere il file",
+    "aiFailed": "L'IA non è riuscita a suggerire una mappatura",
+    "dryRunFailed": "La simulazione non è riuscita",
+    "importFailed": "L'importazione non è riuscita",
+    "undoFailed": "Impossibile annullare l'importazione"
+  },
+  "entry": {
+    "title": "Importa da un foglio di calcolo",
+    "description": "Porta clienti, veicoli e storico interventi da qualsiasi sistema che esporta in CSV o Excel. Mappa le colonne, esegui una simulazione, poi importa. Ogni importazione può essere annullata.",
+    "customers": "Importa clienti",
+    "vehicles": "Importa veicoli",
+    "services": "Importa storico interventi"
+  }
+}

+ 584 - 0
messages/it/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Integrazioni",
+    "description": "Collega i servizi che la tua officina usa già. Ogni integrazione si configura una sola volta e funziona in background.",
+    "all": "Tutte",
+    "empty": "Nessuna integrazione in questa categoria per ora.",
+    "notConnected": "Non collegata",
+    "connect": "Collega",
+    "manage": "Gestisci",
+    "connectedTitle": "Collegati",
+    "connectedDescription": "Servizi configurati da questa officina. Aprine uno per controllarne lo stato, cambiare le impostazioni o scollegarlo.",
+    "noneConnected": "Ancora nulla di collegato. Scegli un servizio qui sotto per iniziare.",
+    "availableTitle": "Disponibili",
+    "availableDescription": "Servizi che puoi aggiungere. Ognuno si configura una volta e lavora in background.",
+    "allConnected": "Tutti i servizi disponibili sono collegati.",
+    "searchPlaceholder": "Cerca integrazioni",
+    "noMatches": "Nessun risultato per «{query}».",
+    "readMore": "Scopri di più",
+    "suggestTitle": "Manca un servizio che usi?",
+    "suggestDescription": "Dicci quale integrazione aiuterebbe la tua officina e la valuteremo.",
+    "suggest": "Suggerisci un'integrazione",
+    "suggestSubject": "Suggerimento di integrazione"
+  },
+  "categories": {
+    "calendar": "Calendario",
+    "conferencing": "Videochiamate",
+    "accounting": "Contabilità",
+    "messaging": "Messaggistica",
+    "payments": "Pagamenti",
+    "automation": "Automazione",
+    "storage": "File",
+    "registry": "Registro veicoli",
+    "other": "Altro",
+    "ai": "IA"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Ordini di lavoro nel calendario",
+      "pull": "Impegni dal calendario",
+      "conference": "Link per videochiamate"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Ricerca per targa e VIN",
+      "inspection": "Scadenze revisione",
+      "safety": "Richiami e reclami dei proprietari"
+    },
+    "ai": {
+      "chat": "Testi e riepiloghi"
+    },
+    "payments": {
+      "checkout": "Paga dal link della fattura"
+    },
+    "accounting": {
+      "invoices": "Fatture in contabilità",
+      "customers": "Clienti in contabilità",
+      "payments": "Pagamenti in entrambe le direzioni"
+    }
+  },
+  "statuses": {
+    "active": "Collegata",
+    "error": "Richiede attenzione",
+    "pending": "Configurazione in corso",
+    "disconnected": "Scollegata",
+    "needsSetup": "Configurazione incompleta"
+  },
+  "connection": {
+    "back": "Tutte le integrazioni",
+    "account": "Account collegato",
+    "lastHealth": "Ultimo controllo",
+    "lastSync": "Ultima sincronizzazione",
+    "lastError": "L'ultima esecuzione non è riuscita",
+    "setupMissingTitle": "Configurazione incompleta",
+    "setupMissing": "Scegli {fields} nelle impostazioni qui sotto. Fino ad allora non viene sincronizzato nulla.",
+    "connected": "{name} è collegato.",
+    "connectedShort": "Collegato",
+    "connectWith": "Collega {name}",
+    "connect": "Collega",
+    "reconnect": "Ricollega",
+    "disconnect": "Scollega",
+    "disconnectTitle": "Scollegare {name}?",
+    "disconnectDescription": "Torqvoice dimentica l'account e smette di usarlo. Nulla di già inviato a {name} o salvato lì viene toccato.",
+    "disconnectDescriptionCalendar": "Torqvoice dimentica l'account e smette di sincronizzare. Gli ordini di lavoro già inseriti in {name} restano lì. Il tempo occupato mostrato da quel calendario viene rimosso dal calendario di Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice dimentica l'account e smette di creare riunioni. Le riunioni già create restano in {name} e i link sugli ordini di lavoro vengono conservati.",
+    "disconnectDescriptionAccounting": "Torqvoice dimentica l'account e smette di inviare e leggere. Fatture, clienti e pagamenti già in {name} restano lì.",
+    "removeSetup": "Rimuovi",
+    "removeSetupTitle": "Rimuovere {name}?",
+    "removeSetupDescription": "Non c'è ancora nulla di collegato. Torqvoice dimentica quanto inserito e {name} torna nell'elenco dei servizi disponibili.",
+    "removeSetupDone": "{name} rimosso",
+    "removeSetupHint": "Non vuoi procedere? Rimuovilo e il servizio torna nell'elenco qui sotto.",
+    "disconnected": "Scollegato",
+    "test": "Prova la connessione",
+    "testPassed": "La connessione funziona",
+    "syncNow": "Sincronizza ora",
+    "syncQueued": "Sincronizzazione in coda; verrà eseguita entro un minuto",
+    "pullNow": "Recupera i pagamenti ora",
+    "pullQueued": "Recupero pagamenti in coda; verrà eseguito entro un minuto",
+    "backfill": "Invia gli ordini di lavoro pianificati",
+    "backfillQueued": "Messi in coda gli ordini di lavoro pianificati dall'ultima settimana in poi",
+    "backfillInvoices": "Invia le fatture emesse",
+    "backfillInvoicesQueuedCount": "In coda {count, plural, one {# fattura emessa} other {# fatture emesse}} da inviare",
+    "backfillInvoicesNone": "Nessuna fattura emessa da inviare. Passano solo le fatture inviate, condivise o pagate, dalla data di inizio se ne hai impostata una.",
+    "retryQueued": "Attività rimessa in coda",
+    "settingsTitle": "Impostazioni",
+    "settingsDescription": "Cosa sincronizzare e dove. Le modifiche si applicano alla prossima sincronizzazione.",
+    "settingsSaved": "Impostazioni salvate",
+    "save": "Salva",
+    "loading": "Caricamento...",
+    "choose": "Scegli...",
+    "search": "Cerca...",
+    "noMatch": "Nessuna corrispondenza",
+    "leaveEmpty": "Lascia vuoto",
+    "noLimit": "Nessun limite",
+    "planLocked": "Le integrazioni non sono incluse nel tuo piano.",
+    "ownAppTitle": "Questa installazione usa la tua app presso il provider",
+    "redirectUri": "URI di reindirizzamento da registrare presso il provider:",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Aggiungi questo URL nella dashboard di Stripe sotto Developers, Webhooks, iscritto a checkout.session.completed. Poi incolla il segreto di firma mostrato da Stripe nel campo segreto del webhook qui, così le notifiche possono essere verificate.",
+    "inboundHintVipps": "Inserisci questo URL come URL di callback nel portale Vipps MobilePay. Vipps richiede anche condizioni di vendita pubblicate, impostate in Impostazioni, Pagamento.",
+    "inboundHintPaypal": "Aggiungi questo URL nella dashboard sviluppatori di PayPal sotto Webhooks, iscritto a PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "Sincronizzazione revisioni",
+      "vehicles": "{count} veicoli con targa in questa officina",
+      "dueNow": "{count} verrebbero cercati alla prossima esecuzione",
+      "withDate": "{count} hanno già una data di revisione",
+      "lastPass": "Ultimo passaggio completo {date}",
+      "never": "Nessun passaggio completo finora",
+      "inProgress": "Un passaggio è in corso",
+      "off": "Attiva l'impostazione e salva; fino ad allora un passaggio non controlla nulla.",
+      "syncNowHelp": "Sincronizza ora avvia subito un passaggio invece di attendere quello orario. Cerca 25 veicoli alla volta e prosegue da solo finché tutti i veicoli da controllare sono stati fatti."
+    },
+    "supersedes": "Collegare {next} scollega {current}. Solo uno può essere attivo alla volta, quindi {current} smette di essere usato appena {next} entra in servizio.",
+    "batchRetried": "{count, plural, =0 {Niente da riprovare} one {# attività rimessa in coda} other {# attività rimesse in coda}}"
+  },
+  "activity": {
+    "title": "Attività",
+    "description": "Attività recenti e righe di log per questa connessione.",
+    "retry": "Riprova",
+    "jobStatus": {
+      "queued": "In coda",
+      "running": "In esecuzione",
+      "done": "Completata",
+      "failed": "Non riuscita",
+      "dead": "Abbandonata"
+    },
+    "jobs": "{count, plural, one {# attività} other {# attività}}",
+    "counts": {
+      "queued": "{count} in coda",
+      "running": "{count} in corso",
+      "done": "{count} completate",
+      "failed": "{count} non riuscite",
+      "dead": "{count} abbandonate"
+    },
+    "noFailures": "Nessun errore.",
+    "retryFailed": "Riprova quelle non riuscite",
+    "details": "Mostra dettagli"
+  },
+  "errors": {
+    "generic": "Qualcosa è andato storto",
+    "demo": "Le integrazioni non possono essere collegate nella demo.",
+    "forbidden": "Solo un amministratore può collegare le integrazioni.",
+    "unknown": "Integrazione sconosciuta.",
+    "plan": "Questa integrazione non è inclusa nel tuo piano.",
+    "no-client": "Nessuna credenziale dell'app configurata per questo provider.",
+    "denied": "Hai annullato l'accesso presso il provider.",
+    "vendor": "Il provider ha restituito un errore. Riprova.",
+    "invalid": "La risposta del provider era incompleta. Riprova.",
+    "state": "Questo accesso è scaduto o appartiene a un'altra sessione. Ricomincia da capo.",
+    "exchange": "Il provider ha rifiutato l'accesso. Controlla le credenziali dell'app e riprova.",
+    "identify": "Collegato, ma non è stato possibile leggere l'account. Prova a ricollegarlo."
+  },
+  "meeting": {
+    "join": "Partecipa alla videochiamata",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "Dai calendari collegati",
+    "busy": "Occupato"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Inserisci gli ordini di lavoro pianificati in un Google Calendar, aggiungi un link Google Meet e vedi gli impegni del calendario in Torqvoice.",
+      "tenantHelp": "Crea un client OAuth in Google Cloud Console (API e servizi → Credenziali), abilita l'API Google Calendar, aggiungi l'URI di reindirizzamento qui sotto e incolla qui l'ID client e il segreto.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Segreto client"
+      },
+      "settings": {
+        "calendarId": "Calendario",
+        "pushEnabled": "Aggiungi gli ordini di lavoro pianificati a questo calendario",
+        "includeCustomer": "Includi nome e telefono del cliente nell'evento",
+        "addConference": "Aggiungi una riunione Google Meet a ogni ordine di lavoro pianificato",
+        "addConferenceHelp": "Attivo: ogni ordine di lavoro inviato al calendario riceve una riunione Google Meet, e il link compare sull'ordine. Spento: aggiungi una riunione Google Meet a un singolo ordine dalla sua pagina. In entrambi i casi il cliente riceve il link solo quando lo invii.",
+        "pullEnabled": "Mostra gli impegni di questo calendario nel calendario di Torqvoice",
+        "pullEnabledHelp": "Sola lettura. Aggiornato ogni 15 minuti, da una settimana indietro a due mesi avanti."
+      }
+    },
+    "microsoft-365": {
+      "description": "Inserisci gli ordini di lavoro pianificati in un calendario Outlook, aggiungi un link Microsoft Teams e vedi gli impegni del calendario in Torqvoice.",
+      "tenantHelp": "Registra un'applicazione in Microsoft Entra (Registrazioni app), consenti gli account personali e aziendali, aggiungi l'URI di reindirizzamento qui sotto come piattaforma Web, crea un segreto client e incolla entrambi qui.",
+      "fields": {
+        "clientId": "ID applicazione (client)",
+        "clientSecret": "Segreto client"
+      },
+      "settings": {
+        "calendarId": "Calendario",
+        "pushEnabled": "Aggiungi gli ordini di lavoro pianificati a questo calendario",
+        "includeCustomer": "Includi nome e telefono del cliente nell'evento",
+        "addConference": "Aggiungi una riunione Microsoft Teams a ogni ordine di lavoro pianificato",
+        "addConferenceHelp": "Attivo: ogni ordine di lavoro inviato al calendario riceve una riunione Microsoft Teams, e il link compare sull'ordine. Spento: aggiungi una riunione Microsoft Teams a un singolo ordine dalla sua pagina. In entrambi i casi il cliente riceve il link solo quando lo invii.",
+        "pullEnabled": "Mostra gli impegni di questo calendario nel calendario di Torqvoice",
+        "pullEnabledHelp": "Sola lettura. Aggiornato ogni 15 minuti, da una settimana indietro a due mesi avanti."
+      }
+    },
+    "zoom": {
+      "description": "Aggiungi una riunione Zoom a un ordine di lavoro programmato e tieni il link sull'ordine, per una diagnosi a distanza o per illustrare i risultati al cliente.",
+      "tenantHelp": "Nello Zoom App Marketplace crea un'app General (user-managed), aggiungi l'URI di reindirizzamento qui sotto, aggiungi gli scope meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting e user:read:user, quindi incolla qui client ID e secret.",
+      "fields": {
+        "clientId": "Client ID",
+        "clientSecret": "Client secret"
+      },
+      "settings": {
+        "autoCreate": "Crea automaticamente una riunione Zoom per ogni ordine di lavoro programmato",
+        "autoCreateHelp": "Spento: aggiungi tu la videochiamata a un ordine di lavoro, dalla sua pagina. Acceso: ogni ordine con un'ora di inizio riceve una riunione, spostata se riprogrammato e rimossa se annullato. In entrambi i casi il cliente riceve il link solo quando glielo invii.",
+        "includeCustomer": "Includi il nome del cliente nell'argomento della riunione",
+        "joinBeforeHost": "Consenti al cliente di entrare prima di te",
+        "waitingRoom": "Usa la sala d'attesa"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Compila un veicolo dalla targa o dal VIN norvegese: marca, modello, anno, carburante, cambio, motore, colore, misure degli pneumatici e data della prossima EU-kontroll, direttamente dal registro di Statens vegvesen. Non vengono restituiti dati sul proprietario.",
+      "fields": {
+        "apiKey": "Chiave API",
+        "apiKeyHelp": "Gratuita. Richiedi la chiave su vegvesen.no alla voce «API for tekniske kjøretøyopplysninger»: accedi con BankID, rappresenta l'officina e la chiave viene rilasciata subito. Consente 50.000 ricerche al giorno."
+      },
+      "settings": {
+        "syncInspections": "Tieni aggiornate le scadenze di revisione di tutti i veicoli di questa officina",
+        "syncInspectionsHelp": "Una volta all'ora i veicoli non controllati di recente vengono cercati per targa e la data della prossima EU-kontroll viene salvata. Vengono inviati solo i veicoli di questa officina. Un veicolo è ricontrollato ogni mese, e ogni settimana quando la scadenza è a meno di due mesi."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Compila un veicolo dalla targa francese, italiana, spagnola, portoghese o britannica: marca, modello, anno, VIN, carburante, cilindrata e altro, dai dati di immatricolazione del paese tramite Openapi.com. Ogni ricerca è addebitata sull'account Openapi dell'officina. Non vengono restituiti dati sul proprietario.",
+      "fields": {
+        "token": "Token API",
+        "tokenHelp": "Crea un account su console.openapi.com, ricarica il portafoglio e, sotto Automotive, crea un token per l'ambiente di produzione. Ogni ricerca con esito costa qualche decina di centesimi; una senza esito è gratuita."
+      },
+      "settings": {
+        "country": "Paese",
+        "countryHelp": "Il registro da consultare. Le targhe francesi e italiane hanno lo stesso formato, quindi il paese non si può ricavare dalla targa.",
+        "bikes": "Cerca anche i motocicli",
+        "bikesHelp": "Quando una targa non è di un'auto, viene consultato anche il registro dei motocicli. Una ricerca auto senza esito è gratuita, quindi non costa nulla in più a meno che non venga trovato un motociclo. Non disponibile per il Portogallo."
+      }
+    },
+    "rdw": {
+      "description": "Compila un veicolo dalla targa olandese: marca, modello, anno, carburante, cilindrata, colore, masse e data del prossimo APK, direttamente dal registro veicoli aperto della RDW. Gratuito, senza chiave. Non vengono pubblicati né i dati del proprietario né il VIN.",
+      "fields": {
+        "appToken": "Token applicazione (facoltativo)",
+        "appTokenHelp": "I dati aperti della RDW funzionano senza chiave, ma le richieste anonime condividono una quota limitata. Per la sincronizzazione delle revisioni, crea un account gratuito su opendata.rdw.nl, apri le impostazioni sviluppatore del tuo profilo e crea un token applicazione."
+      },
+      "settings": {
+        "syncInspections": "Tieni aggiornate le date APK di tutti i veicoli di questa officina",
+        "syncInspectionsHelp": "Una volta all'ora i veicoli non controllati di recente vengono cercati per targa e la data del prossimo APK viene salvata. Vengono inviati solo i veicoli di questa officina. Un veicolo è ricontrollato ogni mese, e ogni settimana quando la scadenza è a meno di due mesi."
+      }
+    },
+    "regcheck": {
+      "description": "Compila un veicolo dalla targa in Australia, Nuova Zelanda, Stati Uniti, Irlanda, Norvegia, Svezia, Danimarca, Finlandia, Estonia, Cechia, Slovacchia, Ungheria o Croazia: marca, modello, anno, VIN, carburante e cilindrata quando il paese li pubblica, tramite RegCheck. Ogni ricerca con esito consuma un credito dall'account RegCheck dell'officina. Non vengono restituiti dati sul proprietario.",
+      "fields": {
+        "username": "Nome utente RegCheck",
+        "usernameHelp": "Registrati su regcheck.org.uk; l'account include 10 ricerche gratuite e le altre si acquistano in blocchi da 100 dal pannello. Qui serve solo il nome utente, mai la password. Una targa non trovata non costa nulla."
+      },
+      "settings": {
+        "country": "Paese",
+        "countryHelp": "Il registro da consultare. Ogni paese ha un proprio servizio, quindi la connessione ne consulta uno.",
+        "auState": "Stato o territorio",
+        "auStateHelp": "L'Australia immatricola i veicoli per stato e al registro va detto quale consultare. Scegli quello in cui è immatricolata la maggior parte dei veicoli dei tuoi clienti.",
+        "usState": "Stato",
+        "usStateHelp": "Il codice di due lettere dello stato in cui sono immatricolati i veicoli, come CA o TX. I registri americani si consultano per stato."
+      }
+    },
+    "openai": {
+      "description": "Descrizioni di servizio, riepiloghi dello storico, lettura dei documenti e l'assistente, risposti dal vostro account OpenAI. Ogni chiamata è addebitata su quell'account e il modello lo scegliete voi.",
+      "fields": {
+        "apiKey": "Chiave API",
+        "apiKeyHelp": "Da creare su platform.openai.com, in API keys. L'account deve avere credito; OpenAI fattura a chiamata."
+      },
+      "settings": {
+        "model": "Modello",
+        "modelHelp": "Quale modello risponde. L'elenco arriva dal vostro account, prima i più recenti. I modelli piccoli costano meno a chiamata e bastano per quasi tutti i testi dell'officina."
+      }
+    },
+    "anthropic": {
+      "description": "Descrizioni di servizio, riepiloghi dello storico, lettura dei documenti e l'assistente, risposti da Claude sul vostro account Anthropic. Ogni chiamata è addebitata su quell'account e il modello lo scegliete voi.",
+      "fields": {
+        "apiKey": "Chiave API",
+        "apiKeyHelp": "Da creare su console.anthropic.com, in API keys. L'account deve avere credito; Anthropic fattura a chiamata."
+      },
+      "settings": {
+        "model": "Modello",
+        "modelHelp": "Quale modello risponde. L'elenco arriva dal vostro account, prima i più recenti. I modelli piccoli costano meno a chiamata e bastano per quasi tutti i testi dell'officina."
+      }
+    },
+    "stripe": {
+      "description": "Pagamenti con carta dal link della fattura, sul tuo account Stripe. Il cliente paga, Stripe avvisa Torqvoice e la fattura si segna pagata da sola.",
+      "fields": {
+        "secretKey": "Chiave segreta",
+        "secretKeyHelp": "Dalla dashboard di Stripe sotto Developers, API keys. Le chiavi di test non elaborano nulla di reale, quindi prova prima un pagamento con quelle e passa poi alle chiavi live.",
+        "publishableKey": "Chiave pubblicabile",
+        "webhookSecret": "Segreto di firma del webhook",
+        "webhookSecretHelp": "Mostrato da Stripe quando aggiungi come webhook l'URL di notifica di questa pagina. Con esso ogni notifica viene verificata prima di registrare un pagamento."
+      },
+      "settings": {
+        "enabled": "Offri nei link delle fatture",
+        "enabledHelp": "Disattiva per togliere il pulsante di pagamento dalle fatture condivise per un po', senza disconnettere. I pagamenti già avviati vengono comunque registrati."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay dal link della fattura. Il cliente paga nell'app che ha già e la fattura si segna pagata da sola.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Segreto client",
+        "subscriptionKey": "Chiave di sottoscrizione",
+        "subscriptionKeyHelp": "La Ocp-Apim-Subscription-Key del portale Vipps MobilePay, tra le chiavi API dell'unità di vendita.",
+        "merchantSerialNumber": "Numero di serie del commerciante"
+      },
+      "settings": {
+        "enabled": "Offri nei link delle fatture",
+        "enabledHelp": "Disattiva per togliere il pulsante di pagamento dalle fatture condivise per un po', senza disconnettere. I pagamenti già avviati vengono comunque registrati.",
+        "testMode": "Modalità test",
+        "testModeHelp": "Usa l'ambiente di test di Vipps con chiavi di test. Lasciala attiva finché un pagamento completo non è andato a buon fine, poi disattivala e inserisci le chiavi live."
+      }
+    },
+    "paypal": {
+      "description": "Checkout PayPal dal link della fattura, tramite la tua app PayPal. Il cliente paga con un account PayPal o una carta e la fattura si segna pagata da sola.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Segreto client"
+      },
+      "settings": {
+        "enabled": "Offri nei link delle fatture",
+        "enabledHelp": "Disattiva per togliere il pulsante di pagamento dalle fatture condivise per un po', senza disconnettere. I pagamenti già avviati vengono comunque registrati.",
+        "sandbox": "Modalità sandbox",
+        "sandboxHelp": "Usa la sandbox di PayPal con le chiavi dell'app sandbox. Lasciala attiva finché un pagamento completo non è andato a buon fine, poi disattivala e inserisci le chiavi dell'app live."
+      }
+    },
+    "quickbooks": {
+      "description": "Invia le fatture emesse, i loro clienti e i loro pagamenti a QuickBooks Online e riporta i pagamenti registrati lì.",
+      "tenantHelp": "Crea un'app nel portale sviluppatori Intuit. Copia client id e secret da Keys and credentials e inserisci l'URI di reindirizzamento qui sotto in Settings, Redirect URIs. Le chiavi di sviluppo raggiungono le aziende sandbox, quelle di produzione le aziende reali.",
+      "fields": {
+        "clientId": "ID client",
+        "clientSecret": "Segreto client"
+      },
+      "settings": {
+        "pushInvoices": "Invia le fatture emesse a QuickBooks",
+        "pushOnComplete": "Invia le fatture anche quando il lavoro viene segnato come completato",
+        "pushOnCompleteHelp": "Senza questa opzione una fattura passa quando viene inviata, condivisa o pagata. Con questa, un lavoro completato passa subito e viene aggiornato se cambia.",
+        "startDate": "Solo fatture con data a partire dal",
+        "startDateHelp": "Lascia vuoto per inviare ogni fattura emessa. Usa la data in cui la contabilità è passata a QuickBooks, così le fatture più vecchie non vengono registrate due volte.",
+        "includeVehicle": "Indicare il veicolo sulla fattura",
+        "includeVehicleHelp": "Una riga di testo sopra le voci con anno, marca, modello, targa e chilometraggio, così la contabilità sa di quale auto si tratta.",
+        "laborItemId": "Articolo per le righe di manodopera",
+        "laborItemIdHelp": "Lascia vuoto e la prima volta verrà creato un articolo chiamato Labour sotto il tuo conto ricavi.",
+        "partsItemId": "Articolo per le righe di ricambi",
+        "partsItemIdHelp": "Lascia vuoto e la prima volta verrà creato un articolo chiamato Parts sotto il tuo conto ricavi.",
+        "taxAmounts": "Importi delle imposte",
+        "taxAmountsHelp": "Come fatturato invia l'imposta esattamente come compare sulla fattura ricevuta dal cliente, così i due sistemi coincidono al centesimo. Calcolato da QuickBooks lascia che QuickBooks applichi l'aliquota del codice imposta, o l'indirizzo del cliente negli USA, e il registro segnala i totali diversi.",
+        "taxAmountsBilled": "Come fatturato in Torqvoice",
+        "taxAmountsQuickbooks": "Calcolato da QuickBooks",
+        "taxCodeId": "Codice imposta per le fatture imponibili",
+        "taxCodeIdHelp": "QuickBooks registra l'imposta sotto un codice imposta. Torqvoice sceglie il codice con l'aliquota di ogni fattura; questo viene usato quando nessuno corrisponde. Un'azienda statunitense usa TAX se non scegli un altro codice.",
+        "zeroTaxCodeId": "Codice imposta per le fatture senza imposta",
+        "zeroTaxCodeIdHelp": "Usato quando la fattura non ha imposta o il cliente è esente. Un'azienda statunitense usa NON se non scegli un altro codice.",
+        "pushPayments": "Registra i pagamenti in QuickBooks",
+        "depositAccountId": "Deposita i pagamenti su",
+        "depositAccountIdHelp": "Lascia vuoto per usare Undeposited Funds.",
+        "manualPaidAsPayment": "Registrare un pagamento quando una fattura viene segnata come pagata a mano",
+        "manualPaidAsPaymentHelp": "Una fattura segnata come pagata in Torqvoice senza registrazione di pagamento riceve in QuickBooks un pagamento per quanto risulta ancora dovuto lì. Disattivato, resta aperta lì finché non viene registrato un pagamento.",
+        "pullPayments": "Riporta i pagamenti registrati in QuickBooks",
+        "pullPaymentsHelp": "Ogni 30 minuti. Un pagamento ricevuto in QuickBooks su una fattura di Torqvoice viene registrato qui sulla fattura."
+      }
+    },
+    "nhtsa": {
+      "description": "Compila un veicolo dal suo VIN e consulta richiami, reclami dei proprietari e valutazioni dei crash test per ogni anno modello, dall'autorità per la sicurezza statunitense. Gratuito, senza chiave.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Tieni aggiornati i dati su richiami e reclami per ogni veicolo di questa officina",
+        "refreshFleetHelp": "Una volta al giorno i modelli non aggiornati nell'ultima settimana vengono richiesti di nuovo, così la pagina del veicolo non attende mai e un nuovo richiamo compare entro una settimana. Vengono inviati solo marca, modello e anno."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Ordini di lavoro nel calendario",
+      "pull": "Impegni dal calendario"
+    },
+    "conference": {
+      "sync": "Link videochiamata"
+    },
+    "inspection": {
+      "refresh": "Aggiornamento scadenze revisione"
+    },
+    "accounting": {
+      "invoice": "Fattura in contabilità",
+      "payment": "Pagamento in contabilità",
+      "customer": "Cliente in contabilità",
+      "pull": "Pagamenti dalla contabilità"
+    },
+    "safety": {
+      "refresh": "Aggiornamento dei dati sui richiami",
+      "vehicle": "Controllo richiami per un nuovo veicolo"
+    }
+  }
+}

+ 2 - 1
messages/it/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Pianificati",
       "sms": "SMS",
       "whatsapp": "WhatsApp",
-      "telegram": "Telegram"
+      "telegram": "Telegram",
+      "campaigns": "Campagne"
     },
     "smsNotConfiguredTitle": "L'SMS non è configurato",
     "smsNotConfiguredHint": "Collega un provider SMS per inviare e ricevere messaggi qui. Le email pianificate funzionano comunque.",

+ 5 - 1
messages/it/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "Impossibile eliminare la conversazione",
     "delete": "Elimina",
     "cancel": "Annulla",
-    "loadMore": "Carica altro"
+    "loadMore": "Carica altro",
+    "unread": "{count} non letti",
+    "markUnread": "Segna come non letto",
+    "nothingToMarkUnread": "Il cliente non ha ancora scritto in questa conversazione.",
+    "markUnreadFailed": "Impossibile segnare la conversazione come non letta."
   }
 }

+ 16 - 2
messages/it/navigation.json

@@ -59,7 +59,8 @@
     "tireStorage": "Disposizione del deposito",
     "tireSetDetails": "Treno di pneumatici",
     "tireForecast": "La prossima stagione",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "inspectionReminders": "Promemoria revisione"
   },
   "search": "Cerca...",
   "shortcut": "Ctrl+K",
@@ -117,6 +118,19 @@
     "auditLog": "Registro attività",
     "aiAssistant": "Assistente IA",
     "tireHotel": "Hotel pneumatici",
-    "whatsapp": "WhatsApp"
+    "whatsapp": "WhatsApp",
+    "newWorkOrder": "Nuovo ordine di lavoro",
+    "installApp": "Installa l'app",
+    "roles": {
+      "owner": "Proprietario",
+      "admin": "Amministratore",
+      "member": "Membro"
+    },
+    "badges": {
+      "messages": "{count} messaggi non letti",
+      "workOrders": "{count} ordini di lavoro aperti",
+      "inspections": "{count} ispezioni in corso",
+      "reminders": "{count} promemoria in scadenza"
+    }
   }
 }

+ 3 - 1
messages/it/onboarding.json

@@ -76,7 +76,9 @@
     "allDoneTitle": "Tutto pronto",
     "allDoneDescription": "Tutti i passaggi sono completati e la Sua officina è operativa.",
     "dismiss": "Nascondi scheda",
-    "dismissAriaLabel": "Nascondi questa lista"
+    "dismissAriaLabel": "Nascondi questa lista",
+    "importTitle": "Ha già clienti e veicoli da qualche parte?",
+    "importDescription": "Li importi da un foglio di calcolo o dal Suo vecchio sistema"
   },
   "sampleData": {
     "title": "Dati di esempio",

+ 51 - 1
messages/it/portal.json

@@ -158,7 +158,14 @@
     "columnVehicle": "Veicolo",
     "columnPlate": "Targa",
     "columnServices": "Interventi",
-    "columnInspections": "Ispezioni"
+    "columnInspections": "Ispezioni",
+    "reportSold": "Non possiedo più questo veicolo",
+    "reportSoldHint": "Avviseremo l'officina perché smetta di inviare promemoria. La tua cronologia resta com'è.",
+    "reportSoldConfirm": "Sì, avvisa l'officina",
+    "reportSoldCancel": "Mantieni",
+    "reportSoldDone": "Grazie, l'officina è stata avvisata",
+    "reportSoldFailed": "Impossibile inviare al momento",
+    "reportSoldAlready": "Hai comunicato all'officina che questo veicolo non è più tuo."
   },
   "requestService": {
     "title": "Richiedi Servizio",
@@ -190,5 +197,48 @@
     "modelPlaceholder": "es. Kuga",
     "yearLabel": "Anno",
     "plateLabel": "Targa"
+  },
+  "booking": {
+    "title": "Prenota la revisione",
+    "due": "Revisione entro il {date}",
+    "firstSlot": "Primo orario disponibile",
+    "bookThis": "Prenota {when}",
+    "pickAnother": "Oppure scegli un altro giorno",
+    "pickTime": "Scegli un orario",
+    "pickTimeFirst": "Scegli un orario sopra",
+    "confirmFor": "Conferma {when}",
+    "notePlaceholder": "Qualcosa che dovremmo sapere? (facoltativo)",
+    "legend": "I giorni barrati sono pieni; quelli ambra hanno pochi orari.",
+    "day": {
+      "open": "Orari disponibili",
+      "limited": "Pochi orari",
+      "full": "Tutto prenotato",
+      "closed": "Chiuso"
+    },
+    "booked": "Prenotato per {when}",
+    "bookedHint": "Ti abbiamo inviato una conferma. Vieni qualche minuto prima con le chiavi.",
+    "pending": "Richiesto per {when}",
+    "pendingHint": "L'officina confermerà a breve l'orario.",
+    "changeOrCancel": "Modifica o annulla",
+    "expired": "Questo link è scaduto",
+    "expiredHint": "Chiamaci e troveremo un orario insieme.",
+    "unavailable": "La prenotazione online non è disponibile al momento",
+    "unavailableHint": "Chiamaci e troveremo un orario.",
+    "callUs": "Chiamaci al {phone}",
+    "modeDirectNote": "Il tuo orario è riservato subito per {minutes} minuti.",
+    "modeRequestNote": "L'officina conferma l'orario richiesto.",
+    "bookedToast": "Orario prenotato",
+    "bookFailed": "Impossibile prenotare quell'orario",
+    "cancelledToast": "Prenotazione annullata",
+    "cancelFailed": "Impossibile annullare",
+    "footer": "Hai ricevuto questo link perché la tua officina ti ha ricordato la revisione.",
+    "workOrderTitle": "Revisione periodica {vehicle}",
+    "notificationTitle": "Nuova prenotazione online",
+    "notificationBody": "{customer} ha prenotato {vehicle} per {when}",
+    "cancelledNotificationTitle": "Prenotazione online annullata",
+    "cancelledNotificationBody": "{customer} ha annullato la prenotazione di {vehicle} del {when}",
+    "confirmationSubject": "La tua prenotazione da {workshop}",
+    "confirmationBody": "La tua {vehicle} è prenotata per {when} da {workshop}. Devi cambiare? Usa il link del messaggio precedente o chiama il {phone}.",
+    "confirmationBodyRequest": "Grazie, {workshop} ha ricevuto la tua richiesta per {when} per la tua {vehicle} e confermerà a breve. Domande? Chiama il {phone}."
   }
 }

+ 1 - 0
messages/it/scheduledMessages.json

@@ -41,6 +41,7 @@
     "scheduled": "Messaggio pianificato",
     "updated": "Messaggio pianificato aggiornato",
     "saveError": "Impossibile salvare il messaggio",
+    "inPast": "Quell'ora è già passata. Scegli un orario futuro.",
     "whatsappNeedsTemplate": "Partirà molto dopo la chiusura della finestra di 24 ore e non c'è un modello di testo WhatsApp configurato, quindi fallirebbe all'invio. Configuralo in Impostazioni > Provider > WhatsApp."
   },
   "list": {

+ 59 - 2
messages/it/service.json

@@ -34,7 +34,8 @@
     "delete": "Elimina",
     "unpaid": "Non Pagato",
     "unsavedChanges": "Modifiche non salvate",
-    "saved": "Salvato"
+    "saved": "Salvato",
+    "joinCall": "Partecipa alla videochiamata"
   },
   "basicInfo": {
     "title": "Informazioni di Base",
@@ -78,7 +79,17 @@
       "typePrompt": "Digita un nome...",
       "boardTechnicians": "Tecnici della Lavagna",
       "teamMembers": "Membri del Team"
-    }
+    },
+    "design": "Design della fattura",
+    "designDefault": "Predefinito dell'officina",
+    "designFollowing": "Predefinito ({name})",
+    "designPinned": "Il foglio è stato congelato all'emissione della fattura il {date}. Una nuova scelta vale solo dopo che un titolare sblocca la fattura e questa viene inviata di nuovo.",
+    "designSaved": "Design della fattura aggiornato",
+    "designFollowingRule": "Predefinito ({name}, {rule})",
+    "designRule": {
+      "parts_sale": "Vendita solo ricambi"
+    },
+    "designSaveFailed": "Impossibile cambiare il design della fattura"
   },
   "parts": {
     "title": "Pezzi",
@@ -349,6 +360,7 @@
     "workBay": "Postazione",
     "selectWorkBay": "Seleziona postazione…",
     "noWorkBay": "Nessuna postazione",
+    "noTechnician": "Senza tecnico",
     "technicians": "Tecnici",
     "otherTeamMembers": "Altri membri del team",
     "makeTechnician": "Lo rende un tecnico",
@@ -410,5 +422,50 @@
     "remove": "Rimuovi questo punto",
     "notAnswered": "Non è ancora stato trovato nulla",
     "answered": "{count} riscontro/i su questo"
+  },
+  "videoCall": {
+    "title": "Videochiamata",
+    "none": "Nessuna videochiamata su questo ordine di lavoro.",
+    "needsSchedule": "Imposta prima un'ora di inizio. La riunione segue la programmazione.",
+    "add": "Aggiungi riunione {provider}",
+    "addedHere": "aggiunta da questo ordine di lavoro",
+    "fromCalendar": "dall'evento del calendario",
+    "join": "Partecipa",
+    "copy": "Copia link",
+    "copied": "Link copiato",
+    "remove": "Rimuovi",
+    "removeTitle": "Rimuovere la videochiamata?",
+    "removeDescription": "La riunione viene eliminata presso il fornitore. Chi ha già il link non potrà più partecipare.",
+    "created": "Riunione creata. Il link è sull'ordine di lavoro.",
+    "removed": "Riunione rimossa",
+    "failed": "Qualcosa è andato storto",
+    "shareHint": "Il cliente riceve questo link solo quando lo invii.",
+    "shareHintNoChannel": "Il cliente riceve questo link solo quando lo invii. Nessun canale di messaggistica è configurato, quindi copialo tu in un messaggio.",
+    "send": {
+      "button": "Invia al cliente",
+      "title": "Invia il link della videochiamata",
+      "description": "Invia il link per partecipare a {name}.",
+      "messageLabel": "Nota (facoltativa)",
+      "messagePlaceholder": "Ciò che il cliente deve sapere prima della chiamata...",
+      "linkAlwaysIncluded": "Il link per partecipare viene sempre aggiunto sotto la tua nota.",
+      "sms": "SMS",
+      "email": "Email",
+      "telegram": "Telegram",
+      "noPhone": "(nessun telefono)",
+      "noEmail": "(nessuna email)",
+      "noTelegram": "(nessuna chat collegata)",
+      "notAvailable": "(non configurato)",
+      "cancel": "Annulla",
+      "send": "Invia",
+      "sent": "Link della videochiamata inviato",
+      "partlySent": "Inviato via {sent}; non via {failed}",
+      "failed": "Impossibile inviare il link"
+    },
+    "notify": {
+      "invitation": "Ciao {name}, la tua videochiamata per la {vehicle} è il {when}. Partecipa qui: {url}",
+      "invitationNoTime": "Ciao {name}, ecco il link della videochiamata per la {vehicle}: {url}",
+      "joinLine": "Partecipa alla videochiamata qui: {url}",
+      "emailSubject": "Videochiamata per la tua {vehicle}"
+    }
   }
 }

+ 101 - 27
messages/it/settings.json

@@ -7,7 +7,7 @@
       "billing": "Fatturazione e Documenti",
       "communications": "Comunicazioni",
       "workshop": "Officina",
-      "integrations": "Integrazioni",
+      "integrations": "App e API",
       "system": "Sistema"
     },
     "items": {
@@ -91,6 +91,10 @@
         "title": "Report programmati",
         "description": "Invio automatico dei report"
       },
+      "integrations": {
+        "title": "Integrazioni",
+        "description": "Calendari, videochiamate e servizi collegati"
+      },
       "webhooks": {
         "title": "Webhooks",
         "description": "Notifiche di eventi in tempo reale"
@@ -130,6 +134,10 @@
       "providers": {
         "title": "Provider",
         "description": "Email, SMS, WhatsApp e Telegram"
+      },
+      "inspectionReminders": {
+        "title": "Promemoria revisione",
+        "description": "Link di prenotazione, durata e preavviso"
       }
     },
     "pro": "PRO"
@@ -572,7 +580,16 @@
     "quoteLockTriggerSent": "Quando il preventivo è inviato",
     "quoteLockTriggerAccepted": "Quando il preventivo è accettato",
     "quoteLockTriggerSentHint": "Si blocca appena il preventivo raggiunge il cliente. Da scegliere solo se non rivedi mai un preventivo durante la trattativa.",
-    "quoteLockTriggerAcceptedHint": "Si blocca quando il cliente accetta il preventivo o questo viene convertito in un lavoro. I preventivi inviati restano modificabili."
+    "quoteLockTriggerAcceptedHint": "Si blocca quando il cliente accetta il preventivo o questo viene convertito in un lavoro. I preventivi inviati restano modificabili.",
+    "freezeTitle": "Fatture precedenti al blocco",
+    "freezeBody": "{count, plural, one {# fattura ha raggiunto} other {# fatture hanno raggiunto}} un cliente prima che questa versione potesse bloccare ciò che una fattura stampa. Si stampano ancora con dati, logo e design attuali, quindi una modifica di oggi le raggiunge. Bloccarle conserva ciascuna come si stampa adesso.",
+    "freezeNone": "Ogni fattura che ha raggiunto un cliente è bloccata come è stata inviata.",
+    "freezeButton": "Bloccale con il design attuale",
+    "freezeConfirmTitle": "Bloccare {count, plural, one {# fattura} other {# fatture}}?",
+    "freezeConfirmBody": "Ciascuna conserva il design, il logo, i dati dell'officina, il cliente, il veicolo e i rilievi con cui si stampa adesso. Non si può annullare, e le fatture restano modificabili o bloccate in modifica esattamente come oggi.",
+    "freezeProgress": "Bloccate {done} di {total}",
+    "freezeDone": "{count, plural, one {# fattura bloccata} other {# fatture bloccate}}",
+    "freezeFailed": "Impossibile bloccare le fatture"
   },
   "layoutEditor": {
     "helpText": "Trascina per riordinare. Usa il selettore di larghezza per posizionare le sezioni affiancate. Le sezioni consecutive a mezza larghezza (S/D) verranno visualizzate in due colonne.",
@@ -634,6 +651,12 @@
       "bank_account": "Conto bancario",
       "footer_note": "Nota a piè di pagina",
       "org_number": "Numero organizzazione",
+      "payment_terms": "Condizioni di pagamento",
+      "due_date": "Data di scadenza",
+      "title": "Titolo",
+      "invoice_number": "N. fattura",
+      "customer_number": "N. cliente",
+      "date": "Data",
       "portal_link": "Link al portale"
     },
     "presetsTitle": "Partire da una disposizione",
@@ -808,32 +831,17 @@
     "paymentTermsPlaceholder": "A 14 giorni",
     "paymentTermsHint": "Testo dei termini di pagamento mostrato sulle fatture (es. \"A 14 giorni\", \"Alla consegna\")",
     "onlinePayments": "Pagamenti online",
-    "onlinePaymentsDescription": "Permetti ai clienti di pagare le fatture online tramite link di fattura condivisi.",
-    "stripe": "Stripe",
-    "stripeDescription": "Accetta pagamenti con carta di credito/debito",
-    "secretKey": "Chiave segreta",
-    "publishableKey": "Chiave pubblica",
-    "webhookSecret": "Segreto webhook",
-    "webhookUrl": "URL webhook",
-    "stripeWebhookHint": "Aggiungi questo URL nel tuo Stripe Dashboard sotto Webhooks. Iscriviti a <code>checkout.session.completed</code>.",
-    "vipps": "Vipps",
-    "vippsDescription": "Accetta pagamenti mobili Vipps (Norvegia)",
-    "clientId": "Client ID",
-    "clientSecret": "Client Secret",
-    "subscriptionKey": "Chiave di abbonamento",
-    "merchantSerialNumber": "Numero seriale commerciante",
-    "testMode": "Modalita test",
-    "callbackUrl": "URL callback",
-    "termsOfSale": "Condizioni di vendita (Salgsvilkar)",
-    "termsOfSaleDescription": "Richiesto da Vipps. I clienti possono visualizzarle dalla pagina della fattura.",
+    "onlinePaymentsDescription": "Lascia che i clienti paghino le fatture dal link che invii loro. Stripe, Vipps e PayPal si collegano in Integrazioni e possono funzionare insieme.",
+    "onlinePaymentsHint": "Quando un cliente paga, il fornitore avvisa Torqvoice e la fattura si segna pagata da sola. La pagina di ogni fornitore mostra l'URL di notifica di cui ha bisogno.",
+    "offered": "Nei link delle fatture",
+    "paused": "Collegato, non offerto",
+    "termsOfSale": "Condizioni di vendita",
+    "termsOfSaleDescription": "Ciò che un cliente accetta pagando online. Vipps richiede condizioni pubblicate; la fattura le collega in ogni caso.",
+    "termsOfSaleText": "Testo delle condizioni",
     "externalTermsUrl": "URL condizioni esterno (opzionale)",
     "externalTermsUrlHint": "Link a una pagina esterna con le tue condizioni. Se impostato, la fattura collegherà qui invece della pagina condizioni integrata.",
     "publicTermsUrl": "URL condizioni pubblico",
     "publicTermsUrlHint": "Condividi questo URL con Vipps o collegalo dal tuo sito web.",
-    "paypal": "PayPal",
-    "paypalDescription": "Accetta pagamenti PayPal",
-    "sandboxMode": "Modalita sandbox",
-    "paypalWebhookHint": "Aggiungi questo URL nel tuo PayPal Developer Dashboard sotto Webhooks. Iscriviti a <code>PAYMENT.CAPTURE.COMPLETED</code>.",
     "savePayment": "Salva impostazioni pagamento",
     "saved": "Impostazioni pagamento salvate"
   },
@@ -1112,7 +1120,7 @@
   },
   "data": {
     "title": "Gestione dati",
-    "description": "Esporta i tuoi dati come backup o importa un file precedentemente esportato.",
+    "description": "Porta i dati dal tuo vecchio sistema, scarica un backup di tutto o ripristinane uno.",
     "exportTitle": "Esporta dati",
     "exportDescription": "Scarica i tuoi dati come file zip. Seleziona le categorie da includere nel backup.",
     "includeInBackup": "Includi nel backup:",
@@ -1199,7 +1207,16 @@
     "optTireHotel": "Hotel pneumatici",
     "optTireHotelDesc": "Treni in deposito, scaffali, preparazione e contratti di deposito",
     "optWorkshopConfig": "Configurazione officina",
-    "optWorkshopConfigDesc": "Preset di manodopera, ruoli, webhook e report programmati"
+    "optWorkshopConfigDesc": "Preset di manodopera, ruoli, webhook e report programmati",
+    "importHubDescription": "Scegli dove sono oggi i tuoi dati. Le importazioni da fogli di calcolo e app si aggiungono a ciò che hai e si possono annullare; il ripristino di un backup sostituisce tutto.",
+    "sourceSpreadsheet": "Foglio di calcolo o CSV",
+    "sourceSpreadsheetDesc": "Esportazioni da qualsiasi gestionale per officine, programma di contabilità, Contatti Google, Outlook o Excel. Confermi l'abbinamento delle colonne e vedi una prova prima che venga scritto qualcosa.",
+    "sourceLubelogDesc": "Veicoli, registri di manutenzione e note da un backup LubeLog.",
+    "sourceInvoiceNinjaDesc": "Clienti, prodotti, fatture e pagamenti da un'esportazione di Invoice Ninja.",
+    "sourceBackup": "Backup Torqvoice",
+    "sourceBackupDesc": "Un file .zip o .json scaricato da questa pagina. Il ripristino sostituisce tutto in questa officina.",
+    "restore": "Ripristina",
+    "restoreTitle": "Ripristina un backup"
   },
   "license": {
     "title": "Licenza White-Label",
@@ -1740,6 +1757,17 @@
         "feature": "Messaggi Telegram",
         "description": "Invia e ricevi messaggi Telegram con i tuoi clienti da Torqvoice."
       }
+    },
+    "moved": {
+      "title": "These settings moved to Integrations",
+      "description": "Email, SMS, WhatsApp and Telegram are set up in Integrations now, alongside calendars and everything else you connect. What you had set up came with them, so there is nothing to enter again.",
+      "connectedTo": "Connected through {vendor}",
+      "notSetUp": "Not set up yet",
+      "manage": "Manage",
+      "notConnected": "Not connected",
+      "goToIntegrations": "Go to Integrations",
+      "platformMail": "Sending through the platform mail server. Connect your own to send from your address.",
+      "onByDefault": "On by default"
     }
   },
   "designer": {
@@ -1749,6 +1777,14 @@
     "galleryTitle": "Parti da un modello",
     "galleryHint": "Scegli un punto di partenza. Nel designer puoi cambiare tutto.",
     "yourDesigns": "I tuoi design",
+    "autoRuleHint": "Le vendite senza veicolo si stampano con il design contrassegnato per loro, salvo che la vendita o il cliente ne abbiano scelto un altro.",
+    "useForPartsSale": "Usa per le vendite di soli ricambi",
+    "usedForPartsSale": "Usato per le vendite di soli ricambi",
+    "autoRule": {
+      "parts_sale": "Vendita solo ricambi"
+    },
+    "autoRuleSaved": "Uso di \"{name}\" aggiornato",
+    "couldNotSaveRule": "Impossibile aggiornare la regola",
     "templates": "Modelli",
     "inUse": "In uso",
     "savedOn": "Salvato il {date}",
@@ -1908,6 +1944,7 @@
       "serviceType": "Riparazione",
       "companyName": "La tua officina",
       "slogan": "Un servizio di qualità di cui fidarsi",
+      "paymentTerms": "Pagamento entro 14 giorni",
       "footerNote": "Grazie per averci scelto",
       "value": "Esempio"
     },
@@ -1923,11 +1960,22 @@
     "sloganPlaceholderHint": "Nessuno slogan impostato, quindi il foglio mostra un segnaposto che non verrà stampato.",
     "sloganSetHint": "Lo slogan proviene dai dati della tua azienda.",
     "sloganLink": "Impostazioni azienda",
+    "paymentTerms": "Termini di pagamento",
+    "paymentTermsPlaceholderHint": "Nessun termine di pagamento impostato, quindi il foglio mostra un segnaposto che non verrà stampato.",
+    "paymentTermsSetHint": "I termini di pagamento provengono dalle impostazioni di pagamento.",
+    "paymentTermsLink": "Impostazioni di pagamento",
+    "titleText": "Testo del titolo",
+    "titleTextHint": "Come si chiama questo documento. Se lasciato vuoto, stampa il proprio nome nella lingua del lettore.",
     "placeholder": "Segnaposto",
     "leaveTitle": "Uscire senza salvare?",
     "leaveBody": "Le modifiche a questo design non sono state salvate e andranno perse.",
     "leaveConfirm": "Esci",
-    "boldField": "Grassetto"
+    "boldField": "Grassetto",
+    "deleteDesignDrafts": "{count, plural, one {# bozza di fattura} other {# bozze di fattura}} che lo usano torneranno al design predefinito.",
+    "deleteDesignCustomers": "{count, plural, one {# cliente} other {# clienti}} impostati su di esso torneranno al design predefinito.",
+    "setDefault": "Imposta come predefinito",
+    "defaultSet": "«{name}» è ora in uso",
+    "couldNotSetDefault": "Impossibile impostare il design predefinito"
   },
   "preview": {
     "title": "Servizio freni e cambio olio",
@@ -1942,5 +1990,31 @@
     "partSyntheticOil": "Olio sintetico 5W-30",
     "laborBrakeReplacement": "Sostituzione freni",
     "laborOilChange": "Cambio olio"
+  },
+  "inspectionReminders": {
+    "title": "Promemoria revisione e prenotazione",
+    "description": "Come si comporta il link di prenotazione in un promemoria. Si imposta una volta; ogni campagna lo usa.",
+    "duration": "Durata della revisione (minuti)",
+    "durationHint": "Quanto una revisione periodica occupa un ponte. Il link prenota slot di questa durata.",
+    "leadDays": "Prenotazione più vicina (giorni)",
+    "leadDaysHint": "1 significa che il primo orario proposto è domani, mai oggi.",
+    "horizonWeeks": "Fino a quando (settimane)",
+    "horizonHint": "I clienti possono scegliere qualsiasi orario libero entro queste settimane.",
+    "reserve": "Tieni libero per chi passa",
+    "reserveHint": "Ponti o tecnici che il link non occupa mai. 0 permette di usare tutti quelli liberi.",
+    "linkValidDays": "Il link resta aperto (giorni)",
+    "linkValidHint": "Almeno {min}. I link restano aperti anche fino a una settimana dopo la scadenza, se successiva.",
+    "bookingMode": "Cosa crea una prenotazione",
+    "bookingModeDirect": "Un ordine di lavoro pianificato sulla lavagna",
+    "bookingModeRequest": "Una richiesta che l'ufficio conferma prima",
+    "bookingModeHint": "La prenotazione diretta è ciò che rende utile il link. Scegli la richiesta se vuoi approvare ogni volta.",
+    "phone": "Telefono nei messaggi",
+    "phoneHint": "Mostrato come alternativa al link. Predefinito: il telefono dell'officina.",
+    "save": "Salva",
+    "saved": "Impostazioni salvate",
+    "saveFailed": "Impossibile salvare",
+    "timeZoneDetected": "Il fuso orario è su automatico in Localizzazione, quindi il server usa {zone}, l'ultimo visto in un browser che ha salvato le impostazioni. Scegli esplicitamente il fuso dell'officina così gli orari di prenotazione non dipendono mai da chi ha salvato per ultimo.",
+    "timeZoneExplicit": "Orari di prenotazione e di apertura sono letti in {zone}.",
+    "timeZoneChange": "Cambia in Localizzazione"
   }
 }

+ 217 - 3
messages/it/vehicles.json

@@ -4,6 +4,7 @@
     "archived": "Archiviati",
     "searchPlaceholder": "Cerca per targa, marca, modello, VIN...",
     "addVehicle": "Aggiungi Veicolo",
+    "importVehicles": "Importa",
     "emptySearch": "Nessun veicolo corrisponde alla ricerca.",
     "emptyArchived": "Nessun veicolo archiviato.",
     "empty": "Nessun veicolo ancora.",
@@ -30,7 +31,12 @@
     "viewTable": "Table view",
     "viewGrid": "Grid view",
     "viewLargeGrid": "Large grid view",
-    "openMenu": "Open menu"
+    "openMenu": "Open menu",
+    "inspectionOverdue": "Revisione scaduta",
+    "inspectionDue30": "Scade entro 30 giorni",
+    "inspectionDue90": "Scade entro 90 giorni",
+    "inspectionDue": "Revisione {date}",
+    "remindCustomers": "Avvisa questi clienti"
   },
   "detail": {
     "vehicleArchived": "Questo veicolo è archiviato",
@@ -90,7 +96,11 @@
     "severityUncommon": "Non comune — segnalato da pochi proprietari",
     "severityRare": "Raro — segnalazioni isolate ma critico quando si verifica",
     "openMenu": "Open menu",
-    "dismissMaintenance": "Dismiss"
+    "dismissMaintenance": "Dismiss",
+    "inspectionDue": "Revisione il {date}",
+    "inspectionCheckedAt": "Verificato nel registro il {date}",
+    "notRegistered": "Non immatricolato",
+    "soldReported": "Vendita segnalata dal cliente il {date}"
   },
   "quotes": {
     "newQuote": "Nuovo preventivo",
@@ -130,6 +140,7 @@
     "descriptionLabel": "Descrizione",
     "descriptionPlaceholder": "Dettagli facoltativi...",
     "dueDateLabel": "Data di Scadenza",
+    "dueTimeLabel": "Ora (facoltativa)",
     "dueMileageLabel": "Chilometraggio di Scadenza",
     "dueMileagePlaceholder": "es. 50000",
     "reminderUpdated": "Promemoria aggiornato",
@@ -338,7 +349,20 @@
     "sectionAssignment": "Foto e intestatario",
     "sectionDetails": "Dettagli",
     "addScannedOwner": "Aggiungi il cliente {name}",
-    "ownerSaveFailed": "Impossibile creare il cliente"
+    "ownerSaveFailed": "Impossibile creare il cliente",
+    "lookupPlate": "Cerca targa",
+    "lookingUp": "Interrogazione del registro veicoli...",
+    "lookupSuccess": "Campi compilati da {source}",
+    "lookupNotFound": "Il registro non ha alcun veicolo con questa targa",
+    "lookupFailed": "Impossibile cercare la targa",
+    "lookupUnavailable": "Collega un registro veicoli in Impostazioni → Integrazioni",
+    "lookupEnterPlate": "Inserisci prima una targa",
+    "lookupInspectionDue": "{source}: prossima revisione periodica il {date}",
+    "lookupSource": "Compilato da {source}",
+    "inspectionDue": "Prossima revisione periodica",
+    "inspectionDueHint": "Inserisci la data se nessun registro la fornisce. Promemoria e dashboard la usano.",
+    "inspectionDueFromRegistry": "Presa dal registro veicoli. Modificala qui per sovrascriverla fino alla prossima sincronizzazione.",
+    "lookupEnterPlateOrVin": "Scrivi prima una targa o un VIN"
   },
   "archiveDialog": {
     "title": "Archivia Veicolo",
@@ -353,5 +377,195 @@
     "previousPage": "Previous page",
     "nextPage": "Next page",
     "lastPage": "Last page"
+  },
+  "inspectionReminders": {
+    "title": "Promemoria revisione",
+    "description": "Avvisa i clienti la cui revisione periodica si avvicina, con un link per prenotare. Nulla viene inviato finché non confermi l'elenco.",
+    "window": "Scadenza entro",
+    "window30": "Prossimi 30 giorni",
+    "window60": "Prossimi 60 giorni",
+    "window90": "Prossimi 90 giorni",
+    "channel": "Invia via",
+    "channels": {
+      "sms": "SMS",
+      "whatsapp": "WhatsApp",
+      "email": "Email",
+      "telegram": "Telegram"
+    },
+    "willReceive": "riceveranno un messaggio",
+    "excludedCount": "{count} esclusi",
+    "nobodyToRemind": "Nessuno da avvisare in questo periodo. Ogni veicolo in scadenza è già stato avvisato, ha prenotato o non è raggiungibile.",
+    "overdueOn": "Scaduta dal {date}",
+    "excludedTitle": "Non inclusi",
+    "excludedDescription": "Ognuno ha un motivo. Correggilo sul cliente o sul veicolo e comparirà nell'elenco.",
+    "reasons": {
+      "noCustomer": "Nessun cliente",
+      "optedOut": "Rifiuta i promemoria",
+      "noRecipient": "Nessun contatto per questo canale",
+      "alreadyReminded": "Già avvisato per questa scadenza",
+      "cooldown": "Contattato di recente",
+      "soldReported": "Il cliente segnala la vendita",
+      "notRegistered": "Non immatricolato",
+      "booked": "Già prenotato"
+    },
+    "message": "Messaggio",
+    "subject": "Oggetto",
+    "body": "Testo",
+    "placeholders": "Segnaposto: {customerName} {vehicle} {plate} {dueDate} {workshopName} {bookingLink} {phone}",
+    "unknownPlaceholders": "Segnaposto sconosciuti: {list}",
+    "sample": "Esempio per il primo destinatario",
+    "segments": "{count, plural, one {# segmento SMS} other {# segmenti SMS}}",
+    "bookingModeDirectNote": "Il link prenota uno slot di {minutes} minuti direttamente sulla lavagna.",
+    "bookingModeRequestNote": "Il link invia una richiesta con l'orario scelto che l'ufficio conferma.",
+    "linkValidNote": "I link restano aperti almeno {days} giorni e oltre la scadenza.",
+    "sendTest": "Inviami un test",
+    "testSent": "Test in coda per {to}",
+    "testFailed": "Impossibile inviare il test",
+    "sendButton": "Invia a {count}",
+    "confirmTitle": "Inviare i promemoria?",
+    "confirmDescription": "{count} messaggi partono via {channel}, uno per veicolo, una volta sola. Un veicolo non riceve mai due avvisi per la stessa scadenza.",
+    "confirmInput": "Digita {count} per confermare",
+    "cancel": "Annulla",
+    "sendNow": "Invia",
+    "created": "{count} promemoria in coda",
+    "sendFailed": "Impossibile creare la campagna",
+    "loadFailed": "Impossibile caricare l'elenco",
+    "campaign": {
+      "title": "Campagna del {date}",
+      "summary": "{channel}, scadenze entro {window} giorni",
+      "recipients": "Destinatari",
+      "sent": "Inviati",
+      "failed": "Falliti",
+      "booked": "Prenotati",
+      "retryFailedButton": "Riprova {count} falliti",
+      "retried": "{count} rimessi in coda",
+      "retryFailed": "Impossibile riprovare",
+      "rows": "Destinatari",
+      "status": {
+        "scheduled": "In coda",
+        "sent": "Inviato",
+        "failed": "Fallito",
+        "cancelled": "Annullato"
+      },
+      "bookedOn": "Prenotato il {date}",
+      "requestedOn": "Richiesto il {date}",
+      "cancelledOn": "Annullato il {date}",
+      "none": "Nessuna campagna finora.",
+      "start": "Invia promemoria revisione",
+      "recipientsCount": "{count} destinatari",
+      "failedCount": "{count} falliti",
+      "bookedCount": "{count} prenotati"
+    },
+    "defaults": {
+      "smsBody": "Ciao {customerName}, la revisione periodica della tua {vehicle} ({plate}) scade il {dueDate}. Prenota qui: {bookingLink} o chiamaci al {phone}. {workshopName}",
+      "emailSubject": "Revisione il {dueDate} per {vehicle}",
+      "emailBody": "Ciao {customerName},\n\nla revisione periodica della tua {vehicle} ({plate}) scade il {dueDate}.\n\nPrenota qui l'orario che preferisci: {bookingLink}\n\nOppure chiamaci al {phone}.\n\nCordiali saluti,\n{workshopName}"
+    },
+    "refresh": "Aggiorna elenco",
+    "timeZoneRequired": "Il fuso orario dell'officina è su automatico, quindi gli orari di prenotazione seguirebbero l'ultimo browser che ha salvato le impostazioni (ora {zone}). Non si possono inviare promemoria finché non scegli un fuso.",
+    "timeZoneRequiredLink": "Scegli in Localizzazione"
+  },
+  "plateLookup": {
+    "open": "Ricerca targa",
+    "shortcut": "Ctrl+Shift+L",
+    "title": "Ricerca targa",
+    "description": "Digita una targa per vedere cosa sanno la tua officina e il registro veicoli",
+    "placeholder": "Digita una targa",
+    "search": "Cerca",
+    "hintEnter": "Invio per cercare",
+    "hintClose": "Esc per chiudere",
+    "recent": "Recenti",
+    "clearRecent": "Svuota",
+    "looking": "Interrogo {source}…",
+    "lookingWorkshop": "Controllo l'officina…",
+    "invalid": "Non sembra una targa",
+    "inWorkshop": "Nella tua officina",
+    "inWorkshopArchived": "Nella tua officina, archiviato",
+    "notInWorkshop": "Non ancora nella tua officina",
+    "notInWorkshopHint": "Il registro conosce questo veicolo. Aggiungilo e i campi qui sotto vengono con lui.",
+    "notInWorkshopNoRegistry": "Aggiungilo a mano, oppure controlla la targa.",
+    "openVehicle": "Apri veicolo",
+    "addVehicle": "Aggiungi veicolo",
+    "noOwner": "Nessun cliente collegato",
+    "mileage": "{km} km",
+    "services": "{count, plural, =0 {Nessun intervento} one {# intervento} other {# interventi}}",
+    "lastService": "Ultimo intervento {date}",
+    "registryNotFound": "{source} non ha alcun veicolo con quella targa",
+    "registryFailed": "Impossibile interrogare {source}",
+    "registryFailedGeneric": "Impossibile interrogare il registro",
+    "fromSource": "Da {source}",
+    "registered": "Immatricolato",
+    "notRegistered": "Non immatricolato",
+    "vin": "VIN",
+    "copyVin": "Copia VIN",
+    "copied": "Copiato",
+    "fuel": "Carburante",
+    "transmission": "Cambio",
+    "engine": "Motore",
+    "engineCode": "Codice motore",
+    "color": "Colore",
+    "vehicleClass": "Categoria",
+    "firstRegistered": "Prima immatricolazione",
+    "inspectionDue": "Revisione entro",
+    "lastInspected": "Ultima revisione",
+    "weights": "Masse",
+    "weightsValue": "{kerb} kg a vuoto, {gross} kg max",
+    "kerbOnly": "{kerb} kg a vuoto",
+    "grossOnly": "{gross} kg max",
+    "tyres": "Pneumatici",
+    "axle": "Asse {n}",
+    "tyreSize": "Pneumatico",
+    "rimSize": "Cerchio",
+    "loadSpeed": "Carico / velocità",
+    "overdue": "Scaduta",
+    "dueSoon": "In scadenza",
+    "emptyTitle": "Cerca qualsiasi targa",
+    "emptyBody": "Vedi il veicolo, chi ne è il proprietario qui e cosa ha registrato {source}. Non è ancora tuo? Aggiungilo in un passaggio.",
+    "registryWord": "il registro"
+  },
+  "safety": {
+    "title": "Sicurezza e problemi noti",
+    "summaryRecalls": "{count, plural, one {# richiamo} other {# richiami}}",
+    "summaryComplaints": "{count, plural, one {# reclamo dei proprietari} other {# reclami dei proprietari}}",
+    "expand": "Mostra dettagli",
+    "collapse": "Nascondi dettagli",
+    "loading": "Interrogazione dell'autorità per la sicurezza...",
+    "from": "Da {source} per {model}, aggiornato il {date}",
+    "stale": "{source} non raggiungibile; mostrato il rapporto del {date}",
+    "notKnown": "{source} non ha alcun record di {model}.",
+    "notKnownHint": "Controlla marca, modello e anno, oppure aggiungi il VIN per identificare il modello con esattezza.",
+    "recalls": "Richiami",
+    "recallsNone": "Nessuna campagna di richiamo",
+    "complaints": "Reclami dei proprietari",
+    "complaintsNone": "Nessuno presentato",
+    "crashes": "{count, plural, one {# incidente} other {# incidenti}}",
+    "fires": "{count, plural, one {# incendio} other {# incendi}}",
+    "rating": "Valutazione crash test",
+    "notRated": "Non valutato",
+    "stars": "{count} stelle su 5",
+    "frontal": "Frontale",
+    "side": "Laterale",
+    "rollover": "Ribaltamento",
+    "ownersReport": "Cosa segnalano i proprietari",
+    "ofComplaints": "su {total} reclami presentati",
+    "crash": "Incidente",
+    "fire": "Incendio",
+    "noExamples": "I testi dei reclami non sono ancora in questo rapporto. Aggiorna dalla fonte per recuperarli.",
+    "readAll": "Leggi tutti i {count} su {source}",
+    "showRecalls": "{count, plural, one {Mostra # richiamo} other {Mostra # richiami}}",
+    "hideRecalls": "Nascondi richiami",
+    "more": "Altro",
+    "less": "Meno",
+    "consequence": "Rischio",
+    "remedy": "Rimedio",
+    "parkIt": "Non guidare",
+    "parkOutside": "Parcheggiare all'aperto",
+    "addFinding": "Aggiungi alle osservazioni",
+    "findingAdded": "Nelle osservazioni",
+    "findingDescription": "Richiamo {campaign}: {component}",
+    "refresh": "Aggiorna dalla fonte",
+    "refreshed": "Dati di sicurezza aggiornati",
+    "openSite": "Apri su {source}",
+    "errorLoad": "Impossibile caricare i dati di sicurezza"
   }
 }

+ 1 - 1
messages/lt/aiChat.json

@@ -4,7 +4,7 @@
   "placeholder": "Klauskite apie savo dirbtuves...",
   "clear": "Išvalyti pokalbį",
   "error": "Kažkas nutiko. Bandykite dar kartą.",
-  "notEnabled": "DI neįjungtas. Sukonfigūruokite jį skiltyje Nustatymai → DI.",
+  "notEnabled": "DI neprijungtas. Prijunkite OpenAI arba Anthropic skiltyje Nustatymai → Integracijos.",
   "suggestion1": "Kurioms transporto priemonėms reikia aptarnavimo?",
   "suggestion2": "Parodykite neapmokėtas sąskaitas",
   "suggestion3": "Kokios šio mėnesio pajamos?",

+ 27 - 2
messages/lt/audit.json

@@ -151,7 +151,20 @@
     "settings_whatsappNumberRegistered": "Užregistruotas „WhatsApp“ numeris",
     "team_giveTechnicianTheApp": "Lentos technikui suteikta paskyra",
     "team_createDefaultRoles": "Sukurtos standartinės rolės",
-    "settings_setCustomFieldPlacement": "Atnaujinta pasirinktinio lauko vieta"
+    "settings_setCustomFieldPlacement": "Atnaujinta pasirinktinio lauko vieta",
+    "import_commit": "Importuota skaičiuoklė",
+    "import_undo": "Atšauktas importas",
+    "integration_connect": "Prijungta integracija",
+    "integration_disconnect": "Atjungta integracija",
+    "service_setInvoiceDesign": "Pakeistas sąskaitos dizainas",
+    "settings_saveDocumentDesign": "Išsaugotas dokumento dizainas",
+    "settings_deleteDocumentDesign": "Ištrintas dokumento dizainas",
+    "settings_applyDocumentDesign": "Nustatytas numatytasis dokumento dizainas",
+    "settings_setDocumentDesignRule": "Pakeista, kada naudojamas dizainas",
+    "settings_freezeInvoices": "Užrakintos senesnės sąskaitos",
+    "inspection_reminders_sent": "Išsiųsti apžiūros priminimai",
+    "inspection_reminders_send": "Išsiųsti apžiūros priminimai",
+    "service_videoCall_send": "Išsiųsta vaizdo skambučio nuoroda"
   },
   "summary": {
     "customField_create": "Sukurtas pasirinktinis laukas „{name}“",
@@ -275,6 +288,18 @@
     "work_bay_create": "Sukurta darbo vieta „{name}“",
     "work_bay_update": "Atnaujinta darbo vieta „{name}“",
     "work_bay_delete": "Ištrinta darbo vieta {id}",
-    "settings_setCustomFieldPlacement": "Pakeitė, kur spausdinamas pasirinktinis laukas"
+    "settings_setCustomFieldPlacement": "Pakeitė, kur spausdinamas pasirinktinis laukas",
+    "import_commit": "Importuota skaičiuoklė: sukurta įrašų: {count}",
+    "import_undo": "Atšauktas skaičiuoklės importas: pašalinta įrašų: {records}",
+    "integration_connect": "Prijungta {name}",
+    "integration_disconnect": "Atjungta {name}",
+    "service_setInvoiceDesign": "Pakeitė, kokiu dizainu spausdinama sąskaita",
+    "settings_saveDocumentDesign": "Išsaugojo dokumento dizainą {name}",
+    "settings_deleteDocumentDesign": "Ištrynė dokumento dizainą {name}",
+    "settings_applyDocumentDesign": "Nustatė {name} kaip numatytąjį dokumento dizainą",
+    "settings_setDocumentDesignRule": "Pakeista, kada naudojamas dokumento dizainas {name}",
+    "settings_freezeInvoices": "Užrakino {count, plural, one {# sąskaitą} few {# sąskaitas} other {# sąskaitų}} iki užrakinimo su dabartiniu dizainu",
+    "inspection_reminders_sent": "Išsiųsta {count} apžiūros priminimų",
+    "service_videoCall_send": "Išsiuntė vaizdo skambučio nuorodą per {channels}"
   }
 }

+ 2 - 1
messages/lt/auth.json

@@ -13,7 +13,8 @@
     "errors": {
       "tooManyAttempts": "Per daug bandymų, pabandykite vėliau.",
       "invalidCredentials": "Neteisingas el. paštas arba slaptažodis",
-      "passkeyFailed": "Prisijungimas su prieigos raktu nepavyko. Bandykite dar kartą arba naudokite el. paštą ir slaptažodį."
+      "passkeyFailed": "Prisijungimas su prieigos raktu nepavyko. Bandykite dar kartą arba naudokite el. paštą ir slaptažodį.",
+      "invalidOrigin": "Atidarėte Torqvoice adresu {origin}, bet jis sukonfigūruotas adresui {configured}. Nustatykite NEXT_PUBLIC_APP_URL tiksliai tokį adresą, kokį naudojate naršyklėje, ir paleiskite Torqvoice iš naujo."
     },
     "resetPasswordCta": "Iš naujo nustatykite čia"
   },

+ 74 - 7
messages/lt/calendar.json

@@ -30,11 +30,53 @@
   },
   "today": "Šiandien",
   "workOrder": "Darbo užsakymas",
+  "allDay": "Visą dieną",
+  "openDay": "Atidaryti {date}",
+  "dayTotal": "{count, plural, =1 {1 įvykis} one {# įvykis} few {# įvykiai} many {# įvykio} other {# įvykių}}",
+  "views": {
+    "day": "Diena",
+    "week": "Savaitė",
+    "month": "Mėnuo",
+    "year": "Metai",
+    "schedule": "Tvarkaraštis",
+    "fourDays": "4 dienos"
+  },
+  "options": {
+    "showWeekends": "Rodyti savaitgalius",
+    "showCompleted": "Rodyti užbaigtus",
+    "showWeekNumbers": "Rodyti savaičių numerius"
+  },
+  "toolbar": {
+    "previous": "Ankstesnis",
+    "next": "Kitas",
+    "create": "Sukurti",
+    "showSidebar": "Rodyti šoninį skydelį",
+    "hideSidebar": "Slėpti šoninį skydelį"
+  },
+  "sidebar": {
+    "calendars": "Kalendoriai",
+    "legend": "Būsenų spalvos"
+  },
+  "schedule": {
+    "empty": "Nieko nesuplanuota",
+    "emptyHint": "Šiuo laikotarpiu nėra darbo užsakymų, priminimų, pasiūlymų ar žinučių."
+  },
+  "peek": {
+    "close": "Uždaryti",
+    "open": {
+      "service": "Atidaryti darbo užsakymą",
+      "reminder": "Atidaryti priminimą",
+      "quote": "Atidaryti pasiūlymą",
+      "message": "Atidaryti žinutes",
+      "external": "Atidaryti"
+    }
+  },
   "filters": {
-    "services": "Paslaugos",
+    "services": "Darbo užsakymai",
     "reminders": "Priminimai",
     "quotes": "Pasiūlymai",
-    "messages": "Žinutės"
+    "messages": "Žinutės",
+    "external": "Prijungti kalendoriai"
   },
   "selectVehicle": "Pasirinkite transporto priemonę darbo užsakymui",
   "dateChoice": {
@@ -44,21 +86,35 @@
     "useToday": "Naudoti šiandienos datą"
   },
   "more": "+ {count} daugiau",
-  "monthTotal": "{count} įvykių šį mėnesį",
+  "monthTotal": "Įvykių: {count}",
   "contextMenu": {
     "newWorkOrder": "Naujas darbo užsakymas",
+    "newWorkOrderAt": "Naujas darbo užsakymas {time}",
     "newReminder": "Naujas priminimas",
     "newQuote": "Naujas pasiūlymas",
     "openEvent": "Atidaryti ({count})",
     "hint": "Patarimas: spustelėkite dieną dešiniuoju pelės mygtuku, kad pamatytumėte veiksmus",
     "scheduleMessage": "Suplanuoti žinutę"
   },
+  "eventMenu": {
+    "open": "Atidaryti",
+    "markDone": "Pažymėti kaip atliktą",
+    "markNotDone": "Pažymėti kaip neatliktą",
+    "deleteReminder": "Ištrinti priminimą",
+    "deleteReminderConfirm": "Ištrinti priminimą „{title}“? Šio veiksmo atšaukti negalima.",
+    "deleteMessage": "Ištrinti suplanuotą žinutę",
+    "deleteMessageConfirm": "Ištrinti suplanuotą žinutę „{title}“? Ji nebus išsiųsta.",
+    "deleted": "Ištrinta",
+    "updated": "Atnaujinta",
+    "failed": "Nepavyko. Bandykite dar kartą."
+  },
   "events": {
     "status": {
       "completed": "Užbaigta",
       "inProgress": "Vykdoma",
       "waitingParts": "Laukia dalių",
       "pending": "Laukia",
+      "scheduled": "Suplanuota",
       "sent": "Išsiųsta",
       "approved": "Patvirtinta",
       "draft": "Juodraštis",
@@ -66,16 +122,27 @@
       "overdue": "Vėluoja",
       "upcoming": "Artėja",
       "failed": "Nepavyko",
-      "queued": "Eilėje"
+      "queued": "Eilėje",
+      "busy": "Užimta"
     },
     "type": {
-      "service": "Paslauga",
+      "service": "Darbo užsakymas",
       "reminder": "Priminimas",
       "quote": "Pasiūlymas",
-      "message": "Žinutė"
+      "message": "Žinutė",
+      "external": "Prijungtas kalendorius"
     },
     "noEvents": "Įvykių nėra",
-    "selectDay": "Pasirinkite dieną su įvykiais arba sukurkite naują darbo užsakymą."
+    "selectDay": "Pasirinkite dieną su įvykiais arba sukurkite naują darbo užsakymą.",
+    "external": {
+      "source": "Iš {source}",
+      "sourceUnknown": "Iš prijungto kalendoriaus",
+      "allDay": "Visą dieną",
+      "open": "Atidaryti „{source}“",
+      "openGeneric": "Atidaryti kalendoriuje",
+      "noLink": "Šis kalendorius nepateikia nuorodos į įvykį.",
+      "readOnly": "Užimtas laikas iš prijungto kalendoriaus. Keiskite jį ten, čia atsinaujins per kitą sinchronizaciją."
+    }
   },
   "previousMonth": "Previous month",
   "nextMonth": "Next month"

+ 8 - 40
messages/lt/customers.json

@@ -33,6 +33,8 @@
     "phoneCopied": "Telefono numeris nukopijuotas į iškarpinę",
     "taxIdLabel": "PVM kodas",
     "taxExemptBadge": "Atleistas nuo mokesčio",
+    "invoiceDesignBadge": "Sąskaitos dizainas: {name}",
+    "invoiceDesignBadgeHint": "Šio kliento sąskaitos spausdinamos šiuo dizainu vietoj dirbtuvių numatytojo. Atidaryti dizaino įrankyje.",
     "tabs": {
       "vehicles": "Transporto priemonės ({count})",
       "invoices": "Sąskaitos ({count})",
@@ -92,7 +94,12 @@
     "customerNumberAuto": "Autom.",
     "sectionContact": "Kontaktai",
     "sectionBilling": "Atsiskaitymas",
-    "addScannedVehicle": "Pridėti transporto priemonę {vehicle}"
+    "addScannedVehicle": "Pridėti transporto priemonę {vehicle}",
+    "invoiceDesign": "Sąskaitos dizainas",
+    "invoiceDesignDefault": "Dirbtuvės numatytasis",
+    "invoiceDesignHint": "Šio kliento sąskaitos spausdinamos šiuo dizainu vietoj dirbtuvės numatytojo. Sąskaita vis tiek gali pasirinkti savo.",
+    "reminderOptOut": "Be priminimų",
+    "reminderOptOutHint": "Neįtraukti šio kliento į apžiūros priminimus ar kitas kampanijas."
   },
   "serviceRequests": {
     "statusNew": "Nauja",
@@ -117,44 +124,5 @@
     "notesSaveError": "Nepavyko išsaugoti pastabų",
     "smsLabel": "Paslaugos užklausos žinutė",
     "smsDefault": "Sveiki, {name}, dėl jūsų paslaugos užklausos jūsų {vehicle}: "
-  },
-  "import": {
-    "title": "Importuoti klientus",
-    "description": "Importuokite klientus iš CSV, TSV arba vCard failų.",
-    "dropFile": "Nuvilkite failą čia",
-    "supportedFormats": "CSV, TSV, TXT arba vCard (.vcf)",
-    "browse": "Naršyti failus",
-    "noDataFound": "Faile nerasta kliento duomenų",
-    "rowsFound": "Rasta {count} klientas(-ų)",
-    "colName": "Vardas",
-    "colEmail": "El. paštas",
-    "colPhone": "Telefonas",
-    "colCompany": "Įmonė",
-    "colAddress": "Adresas",
-    "chooseAnother": "Pasirinkti kitą failą",
-    "cancel": "Atšaukti",
-    "importButton": "Importuoti {count} klientą(-ų)",
-    "importComplete": "Importas baigtas",
-    "importedCount": "{count} klientas(-ų) sėkmingai importuotas",
-    "skippedRows": "{count} eilutė(-ių) praleista dėl klaidų",
-    "rowError": "Eilutė {row} ({name}): {error}",
-    "done": "Baigta",
-    "importFailed": "Nepavyko importuoti klientų",
-    "checkingDuplicates": "Tikrinami dublikatai...",
-    "duplicatesFound": "Rasta {count} dublikatas(-ų)",
-    "duplicatesBanner": "{count} esamas(-ų) klientas(-ų) atitinka importuojamus duomenis.",
-    "duplicatesHint": "Spustelėkite \"Praleisti\" arba \"Sujungti\", kad pakeistumėte kiekvieno dublikato apdorojimą.",
-    "colStatus": "Būsena",
-    "actionSkip": "Praleisti",
-    "actionMerge": "Sujungti",
-    "actionNew": "Naujas",
-    "summaryNew": "{count} naujas(-ų)",
-    "summaryMerge": "{count} sujungtas(-ų)",
-    "summarySkip": "{count} praleistas(-ų)",
-    "skipAll": "Praleisti visus",
-    "mergeAll": "Sujungti visus",
-    "resultCreated": "{count} klientas(-ų) sukurtas",
-    "resultMerged": "{count} klientas(-ų) sujungtas",
-    "resultSkipped": "{count} klientas(-ų) praleistas"
   }
 }

+ 12 - 1
messages/lt/dashboard.json

@@ -56,7 +56,8 @@
     "recentObservations": "Naujausi pastebėjimai",
     "recentActivity": "Naujausia veikla",
     "gettingStarted": "Pradžia",
-    "tireHotel": "Padangų viešbutis"
+    "tireHotel": "Padangų viešbutis",
+    "inspectionsDue": "Artėjančios techninės apžiūros"
   },
   "maintenance": {
     "title": "Numatoma priežiūra",
@@ -252,5 +253,15 @@
     "roomLeft": "Vietos dar {count}",
     "needsPrep": "Reikia paruošti",
     "unbilled": "Neapmokestinta ({count})"
+  },
+  "inspectionsDue": {
+    "title": "Artėjančios techninės apžiūros",
+    "viewAll": "Rodyti visas transporto priemones",
+    "overdue": "Pradelsta",
+    "within30": "Per 30 dienų",
+    "within90": "Per 90 dienų",
+    "noData": "Per artimiausias 90 dienų apžiūrų nėra",
+    "overdueSince": "Pradelsta nuo {date}",
+    "remind": "Siųsti priminimus"
   }
 }

+ 228 - 0
messages/lt/dataImport.json

@@ -0,0 +1,228 @@
+{
+  "title": "Importuoti {entity}",
+  "cancel": "Atšaukti",
+  "done": "Baigta",
+  "entities": {
+    "customers": "klientus",
+    "vehicles": "transporto priemones",
+    "services": "paslaugų istoriją"
+  },
+  "entityHints": {
+    "customers": "Vardai, kontaktiniai duomenys, adresai. Viena eilutė vienam klientui.",
+    "vehicles": "Markė, modelis, metai, valst. numeris, VIN. Savininko stulpelis susieja arba sukuria klientą.",
+    "services": "Ankstesni darbai ir sąskaitos. Viena eilutė vienam darbui su transporto priemone, kuriai jis priklauso."
+  },
+  "groups": {
+    "customer": "Klientas",
+    "vehicle": "Transporto priemonė",
+    "service": "Paslauga"
+  },
+  "steps": {
+    "upload": "Pasirinkite, ką importuoti, ir įkelkite failą iš dabartinės sistemos.",
+    "mapping": "Patikrinkite, kuris stulpelis taps kuriuo lauku. Kol kas niekas neįrašoma.",
+    "preview": "Bandomasis importas: tai tiksliai, ką atliks importas. Peržiūrėkite ir tada importuokite.",
+    "importing": "Įrašomos eilutės. Neuždarykite šio lango.",
+    "result": "Importas baigtas."
+  },
+  "upload": {
+    "drop": "Nutempkite failą čia",
+    "formats": "CSV, Excel (.xlsx) arba vCard (.vcf), iki 25 MB ir 50 000 eilučių",
+    "browse": "Naršyti failus",
+    "reading": "Skaitomas failas...",
+    "templateHint": "Dar neturite eksporto? Užpildykite šabloną ir įkelkite jį.",
+    "templateXlsx": "Excel šablonas",
+    "templateCsv": "CSV šablonas",
+    "sourcesHint": "Veikia su eksportais iš bet kurios dirbtuvių sistemos, apskaitos programos, Google Contacts, Outlook ar skaičiuoklės. Stulpelių pavadinimai atpažįstami daugeliu kalbų; susiejimą patvirtinate prieš ką nors įrašant."
+  },
+  "mapping": {
+    "rows": "{count, plural, one {# eilutė} few {# eilutės} many {# eilutės} other {# eilučių}}",
+    "colColumn": "Stulpelis jūsų faile",
+    "colSample": "Reikšmių pavyzdžiai",
+    "colField": "Taps",
+    "ignore": "Ignoruoti šį stulpelį",
+    "source": {
+      "preset": "Atpažintas išdėstymas",
+      "header": "Pagal antraštę",
+      "values": "Spėta pagal reikšmes"
+    },
+    "suggestWithAi": "Pasiūlyti su DI",
+    "aiApplied": "DI susiejo {count, plural, one {# stulpelį} few {# stulpelius} many {# stulpelio} other {# stulpelių}}. Patikrinkite prieš tęsdami.",
+    "chooseAnother": "Pasirinkti kitą failą",
+    "missingRequired": "Dar reikia: {fields}",
+    "dryRunHint": "Bandomasis importas patikrina kiekvieną eilutę pagal esamus duomenis nieko neįrašydamas.",
+    "runDryRun": "Paleisti bandomąjį importą"
+  },
+  "options": {
+    "dateFormat": "Datos formatas",
+    "dateAuto": "Nustatyti automatiškai",
+    "dateDMY": "Diena / mėnuo / metai",
+    "dateMDY": "Mėnuo / diena / metai",
+    "dateYMD": "Metai / mėnuo / diena",
+    "decimal": "Dešimtainis skirtukas",
+    "decimalAuto": "Nustatyti automatiškai",
+    "decimalDot": "Taškas (1,234.56)",
+    "decimalComma": "Kablelis (1.234,56)",
+    "countryCode": "Šalies kodas telefono numeriams",
+    "duplicates": "Kai eilutė atitinka esamą įrašą",
+    "duplicatesUpdate": "Atnaujinti esamą įrašą",
+    "duplicatesSkip": "Praleisti eilutę",
+    "duplicatesCreate": "Vis tiek sukurti naują įrašą"
+  },
+  "summary": {
+    "create": "Bus sukurta",
+    "update": "Bus atnaujinta",
+    "skip": "Bus praleista",
+    "error": "Negalima importuoti",
+    "warnings": "Su įspėjimais",
+    "customersToCreate": "{count, plural, one {# naujas klientas} few {# nauji klientai} many {# naujo kliento} other {# naujų klientų}}",
+    "customersToUpdate": "{count, plural, one {atnaujintas # klientas} few {atnaujinti # klientai} many {atnaujinta # kliento} other {atnaujinta # klientų}}",
+    "vehiclesToCreate": "{count, plural, one {# nauja transporto priemonė} few {# naujos transporto priemonės} many {# naujos transporto priemonės} other {# naujų transporto priemonių}}",
+    "vehiclesToUpdate": "{count, plural, one {atnaujinta # transporto priemonė} few {atnaujintos # transporto priemonės} many {atnaujinta # transporto priemonės} other {atnaujinta # transporto priemonių}}",
+    "servicesToCreate": "{count, plural, one {# paslaugos įrašas} few {# paslaugų įrašai} many {# paslaugos įrašo} other {# paslaugų įrašų}}"
+  },
+  "actions": {
+    "create": "Sukurti",
+    "update": "Atnaujinti",
+    "skip": "Praleisti",
+    "error": "Klaida"
+  },
+  "matchOn": {
+    "number": "kliento numerį",
+    "email": "el. paštą",
+    "phone": "telefoną",
+    "name": "vardą",
+    "vin": "VIN",
+    "plate": "valst. numerį",
+    "makeModelYear": "markę, modelį ir metus"
+  },
+  "preview": {
+    "colAction": "Veiksmas",
+    "colOwner": "Savininkas",
+    "colDetails": "Išsami informacija",
+    "empty": "Nėra šį filtrą atitinkančių eilučių.",
+    "matchedOn": "Atitinka {name} (pagal {on})",
+    "linksCustomer": "Susieta su klientu {name}",
+    "linksVehicle": "Susieta su {label}",
+    "sameCustomerAsRow": "Tas pats klientas kaip {row} eilutėje",
+    "sameVehicleAsRow": "Ta pati transporto priemonė kaip {row} eilutėje",
+    "createsCustomer": "Sukuria klientą",
+    "createsVehicle": "Sukuria transporto priemonę",
+    "downloadReport": "Atsisiųsti bandomojo importo ataskaitą",
+    "limitExceeded": "Jūsų planas leidžia dar {remaining} klientų, bet šis importas sukurtų {needed}. Pašalinkite eilutes arba atnaujinkite planą.",
+    "pageInfo": "{from}–{to} iš {total}",
+    "prev": "Ankstesnis",
+    "next": "Kitas",
+    "backToMapping": "Grįžti prie susiejimo",
+    "import": "Importuoti {count, plural, one {# eilutę} few {# eilutes} many {# eilutės} other {# eilučių}}",
+    "nothingWrittenYet": "Kol kas niekas neįrašyta. Importą vėliau galima atšaukti skiltyje Nustatymai → Duomenys."
+  },
+  "importing": {
+    "title": "Importuojama...",
+    "starting": "Pradedama",
+    "progress": "{done} iš {total} eilučių",
+    "keepOpen": "Dideli failai užtrunka minutę ar dvi."
+  },
+  "result": {
+    "title": "Importas baigtas",
+    "undoneTitle": "Importas atšauktas",
+    "created": "{count, plural, one {sukurtas # įrašas} few {sukurti # įrašai} many {sukurta # įrašo} other {sukurta # įrašų}}",
+    "updated": "{count, plural, one {atnaujintas # įrašas} few {atnaujinti # įrašai} many {atnaujinta # įrašo} other {atnaujinta # įrašų}}",
+    "skipped": "{count, plural, one {praleista # eilutė} few {praleistos # eilutės} many {praleista # eilutės} other {praleista # eilučių}}",
+    "failed": "{count, plural, one {neimportuota # eilutė} few {neimportuotos # eilutės} many {neimportuota # eilutės} other {neimportuota # eilučių}}",
+    "writeFailures": "{count, plural, one {įrašant nepavyko # eilutė} few {įrašant nepavyko # eilutės} many {įrašant nepavyko # eilutės} other {įrašant nepavyko # eilučių}}",
+    "rowFailure": "{row} eilutė: {error}",
+    "undo": "Atšaukti šį importą",
+    "undone": "Pašalinta: {customers} klientų, {vehicles} transporto priemonių ir {services} paslaugų įrašų.",
+    "undoHint": "Atšaukimas pašalina visus šio importo sukurtus įrašus. Įrašai, kuriuos importas tik atnaujino, išlaiko naujas reikšmes."
+  },
+  "history": {
+    "title": "Importų istorija",
+    "description": "Šiose dirbtuvėse atlikti skaičiuoklių importai. Atšaukimas pašalina tai, ką importas sukūrė.",
+    "loading": "Įkeliama...",
+    "statusUndone": "Atšauktas",
+    "counts": "{created} sukurta, {updated} atnaujinta, {skipped} praleista, {failed} nepavyko",
+    "undo": "Atšaukti",
+    "undone": "Pašalinta: {customers} klientų, {vehicles} transporto priemonių ir {services} paslaugų įrašų.",
+    "undoFailed": "Nepavyko atšaukti importo",
+    "confirmTitle": "Atšaukti šį importą?",
+    "confirmDescription": "Pašalina tai, ką sukūrė {file}: {customers} klientų, {vehicles} transporto priemonių ir {services} paslaugų įrašų. Įrašai, kuriuos importas tik atnaujino, išsaugomi.",
+    "cancel": "Atšaukti",
+    "confirm": "Atšaukti importą"
+  },
+  "fields": {
+    "customer": {
+      "name": "Kliento vardas",
+      "firstName": "Vardas",
+      "lastName": "Pavardė",
+      "customerNumber": "Kliento numeris",
+      "email": "El. paštas",
+      "phone": "Telefonas",
+      "company": "Įmonė",
+      "address": "Adresas",
+      "street": "Gatvė",
+      "postalCode": "Pašto kodas",
+      "city": "Miestas",
+      "state": "Valstija / regionas",
+      "country": "Šalis",
+      "taxId": "PVM kodas",
+      "notes": "Kliento pastabos"
+    },
+    "vehicle": {
+      "description": "Transporto priemonė (metai, markė ir modelis viename stulpelyje)",
+      "make": "Markė",
+      "model": "Modelis",
+      "year": "Metai",
+      "vin": "VIN",
+      "licensePlate": "Valstybinis numeris",
+      "color": "Spalva",
+      "mileage": "Rida",
+      "fuelType": "Degalų tipas",
+      "transmission": "Pavarų dėžė",
+      "engineSize": "Variklio tūris",
+      "engineCode": "Variklio kodas",
+      "purchaseDate": "Pirkimo data",
+      "purchasePrice": "Pirkimo kaina"
+    },
+    "service": {
+      "date": "Paslaugos data",
+      "title": "Paslaugos pavadinimas",
+      "description": "Aprašymas",
+      "total": "Bendra suma",
+      "mileage": "Rida paslaugos metu",
+      "invoiceNumber": "Sąskaitos numeris",
+      "notes": "Paslaugos pastabos",
+      "technician": "Technikas"
+    }
+  },
+  "issues": {
+    "customer_name_required": "Trūksta kliento vardo",
+    "invalid_email": "El. paštas \"{value}\" netinkamas ir buvo praleistas",
+    "invalid_vin": "VIN \"{value}\" nėra 17 simbolių; paliktas kaip įvestas",
+    "vehicle_make_model_required": "Trūksta markės ir modelio",
+    "vehicle_year_required": "Trūksta metų",
+    "vehicle_year_invalid": "Metai \"{value}\" nėra tinkami metai",
+    "vehicle_identifier_required": "Nėra valst. numerio, VIN arba markės ir modelio transporto priemonei atpažinti",
+    "vehicle_not_found": "Jokia esama transporto priemonė neatitinka \"{value}\" ir nepakanka duomenų naujai sukurti",
+    "service_date_required": "Trūksta paslaugos datos",
+    "service_date_invalid": "Nepavyko nuskaityti datos \"{value}\"",
+    "service_title_required": "Trūksta paslaugos pavadinimo arba aprašymo",
+    "invalid_date": "{field} \"{value}\" nepavyko nuskaityti ir buvo praleista",
+    "invalid_number": "{field} \"{value}\" nėra skaičius ir buvo praleista",
+    "duplicate_invoice_number": "Sąskaitos numeris {value} jau egzistuoja",
+    "write_failed": "Nepavyko įrašyti: {value}"
+  },
+  "errors": {
+    "uploadFailed": "Nepavyko nuskaityti failo",
+    "aiFailed": "DI nepavyko pasiūlyti susiejimo",
+    "dryRunFailed": "Bandomasis importas nepavyko",
+    "importFailed": "Importas nepavyko",
+    "undoFailed": "Nepavyko atšaukti importo"
+  },
+  "entry": {
+    "title": "Importuoti iš skaičiuoklės",
+    "description": "Perkelkite klientus, transporto priemones ir paslaugų istoriją iš bet kurios sistemos, eksportuojančios CSV arba Excel. Susiekite stulpelius, paleiskite bandomąjį importą ir tada importuokite. Kiekvieną importą galima atšaukti.",
+    "customers": "Importuoti klientus",
+    "vehicles": "Importuoti transporto priemones",
+    "services": "Importuoti paslaugų istoriją"
+  }
+}

+ 584 - 0
messages/lt/integrations.json

@@ -0,0 +1,584 @@
+{
+  "catalog": {
+    "title": "Integracijos",
+    "description": "Prijunkite paslaugas, kuriomis jūsų dirbtuvė jau naudojasi. Kiekviena integracija nustatoma vieną kartą ir veikia fone.",
+    "all": "Visos",
+    "empty": "Šioje kategorijoje integracijų kol kas nėra.",
+    "notConnected": "Neprijungta",
+    "connect": "Prijungti",
+    "manage": "Tvarkyti",
+    "connectedTitle": "Prijungta",
+    "connectedDescription": "Paslaugos, kurias ši dirbtuvė nustatė. Atidarykite, kad patikrintumėte būseną, pakeistumėte nustatymus ar atjungtumėte.",
+    "noneConnected": "Kol kas niekas neprijungta. Pasirinkite paslaugą žemiau, kad pradėtumėte.",
+    "availableTitle": "Galima pridėti",
+    "availableDescription": "Paslaugos, kurias galite pridėti. Kiekviena nustatoma vieną kartą ir veikia fone.",
+    "allConnected": "Visos galimos paslaugos prijungtos.",
+    "searchPlaceholder": "Ieškoti integracijų",
+    "noMatches": "Nieko nerasta pagal „{query}“.",
+    "readMore": "Skaityti daugiau",
+    "suggestTitle": "Trūksta paslaugos, kurią naudojate?",
+    "suggestDescription": "Pasakykite, kokia integracija padėtų jūsų dirbtuvėms, ir mes ją apsvarstysime.",
+    "suggest": "Pasiūlyti integraciją",
+    "suggestSubject": "Integracijos pasiūlymas"
+  },
+  "categories": {
+    "calendar": "Kalendorius",
+    "conferencing": "Vaizdo skambučiai",
+    "accounting": "Apskaita",
+    "messaging": "Žinutės",
+    "payments": "Mokėjimai",
+    "automation": "Automatizavimas",
+    "storage": "Failai",
+    "registry": "Transporto priemonių registras",
+    "other": "Kita",
+    "ai": "DI"
+  },
+  "capabilities": {
+    "calendar": {
+      "push": "Darbo užsakymai į kalendorių",
+      "pull": "Užimtas laikas iš kalendoriaus",
+      "conference": "Vaizdo skambučių nuorodos"
+    },
+    "sms": {
+      "send": "Text messages out",
+      "receive": "Replies back in"
+    },
+    "whatsapp": {
+      "send": "WhatsApp messages out",
+      "receive": "Replies back in"
+    },
+    "telegram": {
+      "send": "Telegram messages out",
+      "receive": "Replies back in"
+    },
+    "email": {
+      "send": "Email out"
+    },
+    "vehicle": {
+      "lookup": "Paieška pagal numerį ir VIN",
+      "inspection": "Apžiūros terminai",
+      "safety": "Atšaukimai ir savininkų skundai"
+    },
+    "ai": {
+      "chat": "Tekstai ir santraukos"
+    },
+    "payments": {
+      "checkout": "Mokėti iš sąskaitos nuorodos"
+    },
+    "accounting": {
+      "invoices": "Sąskaitos į apskaitą",
+      "customers": "Klientai į apskaitą",
+      "payments": "Mokėjimai abiem kryptimis"
+    }
+  },
+  "statuses": {
+    "active": "Prijungta",
+    "error": "Reikia dėmesio",
+    "pending": "Nustatoma",
+    "disconnected": "Atjungta",
+    "needsSetup": "Sąranka nebaigta"
+  },
+  "connection": {
+    "back": "Visos integracijos",
+    "account": "Prijungta paskyra",
+    "lastHealth": "Paskutinį kartą tikrinta",
+    "lastSync": "Paskutinis sinchronizavimas",
+    "lastError": "Paskutinis vykdymas nepavyko",
+    "setupMissingTitle": "Sąranka nebaigta",
+    "setupMissing": "Žemiau esančiuose nustatymuose pasirinkite {fields}. Iki tol niekas nesinchronizuojama.",
+    "connected": "{name} prijungta.",
+    "connectedShort": "Prijungta",
+    "connectWith": "Prijungti {name}",
+    "connect": "Prijungti",
+    "reconnect": "Prijungti iš naujo",
+    "disconnect": "Atjungti",
+    "disconnectTitle": "Atjungti {name}?",
+    "disconnectDescription": "Torqvoice pamiršta paskyrą ir nustoja ja naudotis. Tai, kas jau išsiųsta į {name} ar ten saugoma, neliečiama.",
+    "disconnectDescriptionCalendar": "Torqvoice pamiršta paskyrą ir nustoja sinchronizuoti. Darbo užsakymai, jau įrašyti į {name}, lieka ten. Iš to kalendoriaus rodytas užimtas laikas pašalinamas iš Torqvoice kalendoriaus.",
+    "disconnectDescriptionConferencing": "Torqvoice pamiršta paskyrą ir nebekuria susitikimų. Jau sukurti susitikimai lieka {name}, o nuorodos darbo užsakymuose išsaugomos.",
+    "disconnectDescriptionAccounting": "Torqvoice pamiršta paskyrą ir nustoja siųsti bei skaityti. Sąskaitos, klientai ir mokėjimai, jau esantys {name}, lieka ten.",
+    "removeSetup": "Pašalinti",
+    "removeSetupTitle": "Pašalinti {name}?",
+    "removeSetupDescription": "Dar niekas neprijungta. Torqvoice pamiršta, kas buvo įvesta, o {name} grįžta į galimų paslaugų sąrašą.",
+    "removeSetupDone": "{name} pašalinta",
+    "removeSetupHint": "Nebetęsite? Pašalinkite, ir paslauga grįš į sąrašą žemiau.",
+    "disconnected": "Atjungta",
+    "test": "Tikrinti ryšį",
+    "testPassed": "Ryšys veikia",
+    "syncNow": "Sinchronizuoti dabar",
+    "syncQueued": "Sinchronizavimas eilėje; jis įvyks per minutę",
+    "pullNow": "Gauti mokėjimus dabar",
+    "pullQueued": "Mokėjimų nuskaitymas eilėje; jis įvyks per minutę",
+    "backfill": "Perkelti suplanuotus darbo užsakymus",
+    "backfillQueued": "Į eilę įtraukti suplanuoti darbo užsakymai nuo praėjusios savaitės",
+    "backfillInvoices": "Siųsti išrašytas sąskaitas",
+    "backfillInvoicesQueuedCount": "Į eilę įtraukta {count, plural, one {# išrašyta sąskaita} few {# išrašytos sąskaitos} other {# išrašytų sąskaitų}}",
+    "backfillInvoicesNone": "Nėra išrašytų sąskaitų siųsti. Perduodamos tik išsiųstos, pasidalintos ar apmokėtos sąskaitos, nuo pradžios datos, jei ją nustatėte.",
+    "retryQueued": "Užduotis vėl įtraukta į eilę",
+    "settingsTitle": "Nustatymai",
+    "settingsDescription": "Ką sinchronizuoti ir kur. Pakeitimai įsigalios kito sinchronizavimo metu.",
+    "settingsSaved": "Nustatymai išsaugoti",
+    "save": "Išsaugoti",
+    "loading": "Kraunama...",
+    "choose": "Pasirinkite...",
+    "search": "Ieškoti...",
+    "noMatch": "Nieko nerasta",
+    "leaveEmpty": "Palikti tuščią",
+    "noLimit": "Be ribos",
+    "planLocked": "Integracijos neįtrauktos į jūsų planą.",
+    "ownAppTitle": "Šis diegimas naudoja jūsų pačių programą pas tiekėją",
+    "redirectUri": "Nukreipimo URI, kurį reikia užregistruoti pas tiekėją:",
+    "sendTestEmail": "Send test email",
+    "testEmailSent": "Test email sent to {email}",
+    "testEmailHint": "A test email goes to your own address through this vendor, which proves the from address and the sending rules a key check cannot.",
+    "inboundUrl": "Inbound webhook URL",
+    "inboundHintSecret": "Paste this URL into the vendor's inbound message settings. It carries a secret that identifies your workshop, so keep it private.",
+    "inboundHintMeta": "Paste this URL as the callback URL in your Meta app, with the verify token you entered above, and subscribe to messages.",
+    "inboundHintStripe": "Pridėkite šį URL Stripe valdymo skydelyje, skiltyje Developers, Webhooks, prenumeruodami checkout.session.completed. Tada įklijuokite Stripe rodomą parašo paslaptį į webhook paslapties lauką čia, kad pranešimus būtų galima patikrinti.",
+    "inboundHintVipps": "Įveskite šį URL kaip atgalinio iškvietimo URL Vipps MobilePay portale. Vipps taip pat reikalauja paskelbtų pardavimo sąlygų, kurios nustatomos skiltyje Nustatymai, Mokėjimas.",
+    "inboundHintPaypal": "Pridėkite šį URL PayPal kūrėjų skydelyje, skiltyje Webhooks, prenumeruodami PAYMENT.CAPTURE.COMPLETED.",
+    "copy": "Copy",
+    "copied": "Copied",
+    "updateKeys": "Update keys",
+    "updateKeysHint": "The last check failed. Enter the keys again to try once more.",
+    "inspectionSync": {
+      "title": "Apžiūrų sinchronizavimas",
+      "vehicles": "{count} transporto priemonės su numeriu šiose dirbtuvėse",
+      "dueNow": "{count} būtų patikrintos kito paleidimo metu",
+      "withDate": "{count} jau turi apžiūros datą",
+      "lastPass": "Paskutinis pilnas perėjimas {date}",
+      "never": "Pilno perėjimo dar nebuvo",
+      "inProgress": "Perėjimas vyksta",
+      "off": "Įjunkite nustatymą ir išsaugokite; iki tol perėjimas nieko netikrina.",
+      "syncNowHelp": "„Sinchronizuoti dabar“ iš karto pradeda perėjimą, nelaukdamas valandinio. Tikrinama po 25 transporto priemones ir tęsiama savaime, kol apdorojamos visos, kurioms atėjo laikas."
+    },
+    "supersedes": "Prijungus {next}, {current} bus atjungtas. Vienu metu gali veikti tik vienas, todėl {current} nustos būti naudojamas, kai tik {next} pradės veikti.",
+    "batchRetried": "{count, plural, =0 {Nėra ko kartoti} one {# užduotis vėl eilėje} few {# užduotys vėl eilėje} many {# užduoties vėl eilėje} other {# užduočių vėl eilėje}}"
+  },
+  "activity": {
+    "title": "Veikla",
+    "description": "Naujausios šio ryšio užduotys ir žurnalo įrašai.",
+    "retry": "Bandyti dar kartą",
+    "jobStatus": {
+      "queued": "Eilėje",
+      "running": "Vykdoma",
+      "done": "Atlikta",
+      "failed": "Nepavyko",
+      "dead": "Nutraukta"
+    },
+    "jobs": "{count, plural, one {# užduotis} few {# užduotys} many {# užduoties} other {# užduočių}}",
+    "counts": {
+      "queued": "{count} eilėje",
+      "running": "{count} vykdoma",
+      "done": "{count} atlikta",
+      "failed": "{count} nepavyko",
+      "dead": "{count} atsisakyta"
+    },
+    "noFailures": "Niekas nenutrūko.",
+    "retryFailed": "Kartoti nepavykusias",
+    "details": "Rodyti detales"
+  },
+  "errors": {
+    "generic": "Kažkas nepavyko",
+    "demo": "Demonstracinėje versijoje integracijų prijungti negalima.",
+    "forbidden": "Integracijas prijungti gali tik administratorius.",
+    "unknown": "Nežinoma integracija.",
+    "plan": "Ši integracija neįtraukta į jūsų planą.",
+    "no-client": "Šiam tiekėjui nesukonfigūruoti programos prisijungimo duomenys.",
+    "denied": "Atšaukėte prisijungimą pas tiekėją.",
+    "vendor": "Tiekėjas grąžino klaidą. Bandykite dar kartą.",
+    "invalid": "Tiekėjo atsakymas buvo neišsamus. Bandykite dar kartą.",
+    "state": "Šis prisijungimas baigė galioti arba priklauso kitai sesijai. Pradėkite iš naujo.",
+    "exchange": "Tiekėjas atmetė prisijungimą. Patikrinkite programos prisijungimo duomenis ir bandykite dar kartą.",
+    "identify": "Prijungta, bet paskyros nuskaityti nepavyko. Pabandykite prijungti iš naujo."
+  },
+  "meeting": {
+    "join": "Prisijungti prie vaizdo skambučio",
+    "google-meet": "Google Meet",
+    "teams": "Microsoft Teams",
+    "zoom": "Zoom"
+  },
+  "calendar": {
+    "external": "Iš prijungtų kalendorių",
+    "busy": "Užimta"
+  },
+  "connectors": {
+    "google-calendar": {
+      "description": "Įtraukite suplanuotus darbo užsakymus į Google Calendar, pridėkite Google Meet nuorodą ir matykite kalendoriaus užimtą laiką Torqvoice.",
+      "tenantHelp": "Sukurkite OAuth klientą Google Cloud Console (APIs & Services → Credentials), įjunkite Google Calendar API, pridėkite žemiau nurodytą nukreipimo URI ir įklijuokite čia kliento ID ir slaptažodį.",
+      "fields": {
+        "clientId": "Kliento ID",
+        "clientSecret": "Kliento slaptažodis"
+      },
+      "settings": {
+        "calendarId": "Kalendorius",
+        "pushEnabled": "Įtraukti suplanuotus darbo užsakymus į šį kalendorių",
+        "includeCustomer": "Įtraukti į įvykį kliento vardą ir telefoną",
+        "addConference": "Pridėti Google Meet susitikimą prie kiekvieno suplanuoto darbo užsakymo",
+        "addConferenceHelp": "Įjungta: kiekvienas į kalendorių įkeltas darbo užsakymas gauna Google Meet susitikimą, o nuoroda rodoma užsakyme. Išjungta: pridėkite Google Meet susitikimą prie vieno užsakymo iš jo puslapio. Bet kuriuo atveju klientas nuorodą gauna tik tada, kai ją išsiunčiate.",
+        "pullEnabled": "Rodyti šio kalendoriaus užimtą laiką Torqvoice kalendoriuje",
+        "pullEnabledHelp": "Tik skaitymui. Atnaujinama kas 15 minučių, viena savaitė atgal ir du mėnesiai į priekį."
+      }
+    },
+    "microsoft-365": {
+      "description": "Įtraukite suplanuotus darbo užsakymus į Outlook kalendorių, pridėkite Microsoft Teams nuorodą ir matykite kalendoriaus užimtą laiką Torqvoice.",
+      "tenantHelp": "Užregistruokite programą Microsoft Entra (App registrations), leiskite asmenines ir darbo paskyras, pridėkite žemiau nurodytą nukreipimo URI kaip Web platformą, sukurkite kliento slaptažodį ir įklijuokite abu čia.",
+      "fields": {
+        "clientId": "Programos (kliento) ID",
+        "clientSecret": "Kliento slaptažodis"
+      },
+      "settings": {
+        "calendarId": "Kalendorius",
+        "pushEnabled": "Įtraukti suplanuotus darbo užsakymus į šį kalendorių",
+        "includeCustomer": "Įtraukti į įvykį kliento vardą ir telefoną",
+        "addConference": "Pridėti Microsoft Teams susitikimą prie kiekvieno suplanuoto darbo užsakymo",
+        "addConferenceHelp": "Įjungta: kiekvienas į kalendorių įkeltas darbo užsakymas gauna Microsoft Teams susitikimą, o nuoroda rodoma užsakyme. Išjungta: pridėkite Microsoft Teams susitikimą prie vieno užsakymo iš jo puslapio. Bet kuriuo atveju klientas nuorodą gauna tik tada, kai ją išsiunčiate.",
+        "pullEnabled": "Rodyti šio kalendoriaus užimtą laiką Torqvoice kalendoriuje",
+        "pullEnabledHelp": "Tik skaitymui. Atnaujinama kas 15 minučių, viena savaitė atgal ir du mėnesiai į priekį."
+      }
+    },
+    "zoom": {
+      "description": "Pridėkite „Zoom“ susitikimą prie suplanuoto darbo užsakymo ir laikykite nuorodą jame: nuotolinei diagnostikai ar radiniams aptarti su klientu.",
+      "tenantHelp": "Zoom App Marketplace sukurkite General (user-managed) programą, pridėkite žemiau nurodytą peradresavimo URI, pridėkite apimtis meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, meeting:read:meeting ir user:read:user, tada čia įklijuokite kliento ID ir slaptažodį.",
+      "fields": {
+        "clientId": "Kliento ID",
+        "clientSecret": "Kliento slaptažodis"
+      },
+      "settings": {
+        "autoCreate": "Automatiškai kurti „Zoom“ susitikimą kiekvienam suplanuotam darbo užsakymui",
+        "autoCreateHelp": "Išjungta: vaizdo skambutį prie darbo užsakymo pridedate patys jo puslapyje. Įjungta: kiekvienas darbo užsakymas su pradžios laiku gauna susitikimą, kuris perkeliamas perplanavus ir pašalinamas atšaukus. Bet kuriuo atveju klientas nuorodą gauna tik tada, kai ją išsiunčiate.",
+        "includeCustomer": "Įtraukti kliento vardą į susitikimo temą",
+        "joinBeforeHost": "Leisti klientui prisijungti anksčiau už jus",
+        "waitingRoom": "Naudoti laukimo kambarį"
+      }
+    },
+    "twilio-sms": {
+      "description": "Send and receive text messages through a Twilio number. Reminders, ready-for-pickup messages and the replies that come back.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "vonage-sms": {
+      "description": "Send and receive text messages through a Vonage number.",
+      "fields": {
+        "apiKey": "API key",
+        "apiSecret": "API secret"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "telnyx-sms": {
+      "description": "Send and receive text messages through a Telnyx number.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "phoneNumber": "Send from this number"
+      }
+    },
+    "whatsapp-meta": {
+      "description": "Message customers on WhatsApp through Meta directly. Photos of what you found, and the conversation that follows.",
+      "fields": {
+        "phoneNumberId": "Phone number ID",
+        "accessToken": "Access token",
+        "verifyToken": "Webhook verify token",
+        "appSecret": "App secret",
+        "apiVersion": "Graph API version"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "whatsapp-twilio": {
+      "description": "Message customers on WhatsApp through a Twilio number, if Twilio is who sold you one.",
+      "fields": {
+        "accountSid": "Account SID",
+        "authToken": "Auth token",
+        "messagingServiceSid": "Messaging service SID"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "phoneNumber": "Send from this number",
+        "templateName": "Template for reopening a conversation",
+        "templateLanguage": "Template language",
+        "templateVariables": "Template values, in order",
+        "mediaTemplateName": "Template for a message with a photo",
+        "mediaTemplateLanguage": "Photo template language",
+        "mediaTemplateVariables": "Photo template values, in order"
+      }
+    },
+    "telegram": {
+      "description": "A Telegram bot that reaches customers who use it, and brings their replies into the inbox.",
+      "fields": {
+        "botToken": "Bot token"
+      },
+      "settings": {
+        "enabled": "Send messages on this channel",
+        "botUsername": "Bot username"
+      }
+    },
+    "smtp": {
+      "description": "Send invoices and reminders through your own mail server, rather than the platform mail.",
+      "fields": {
+        "host": "Server",
+        "port": "Port",
+        "user": "Username",
+        "pass": "Password"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "secure": "Connect over TLS from the start",
+        "requireTls": "Require the server to upgrade to TLS",
+        "rejectUnauthorized": "Reject a certificate that does not check out"
+      }
+    },
+    "resend": {
+      "description": "Send invoices and reminders through Resend.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "postmark": {
+      "description": "Send invoices and reminders through Postmark.",
+      "fields": {
+        "apiKey": "Server token"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "mailgun": {
+      "description": "Send invoices and reminders through Mailgun.",
+      "fields": {
+        "apiKey": "API key",
+        "domain": "Sending domain"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender",
+        "region": "Region the domain is in"
+      }
+    },
+    "sendgrid": {
+      "description": "Send invoices and reminders through SendGrid.",
+      "fields": {
+        "apiKey": "API key"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "amazon-ses": {
+      "description": "Send invoices and reminders through Amazon SES.",
+      "fields": {
+        "accessKeyId": "Access key ID",
+        "secretAccessKey": "Secret access key",
+        "region": "Region"
+      },
+      "settings": {
+        "fromEmail": "Send from this address",
+        "fromName": "Name shown as the sender"
+      }
+    },
+    "vegvesen": {
+      "description": "Užpildykite transporto priemonę pagal norvegišką numerį arba VIN: markė, modelis, metai, degalai, pavarų dėžė, variklis, spalva, padangų matmenys ir kita EU-kontroll data, tiesiai iš Statens vegvesen registro. Savininko duomenys negrąžinami.",
+      "fields": {
+        "apiKey": "API raktas",
+        "apiKeyHelp": "Nemokamas. Užsisakykite raktą vegvesen.no skiltyje „API for tekniske kjøretøyopplysninger“: prisijunkite su BankID, atstovaukite dirbtuvėms, ir raktas išduodamas iš karto. Jis leidžia 50 000 užklausų per dieną."
+      },
+      "settings": {
+        "syncInspections": "Atnaujinti visų šių dirbtuvių transporto priemonių apžiūros terminus",
+        "syncInspectionsHelp": "Kartą per valandą neseniai netikrintos transporto priemonės ieškomos pagal numerį ir išsaugoma kita EU-kontroll data. Siunčiamos tik šiose dirbtuvėse saugomos transporto priemonės. Tikrinama kas mėnesį, o kai iki termino lieka mažiau nei du mėnesiai, kas savaitę."
+      }
+    },
+    "openapi-automotive": {
+      "description": "Užpildykite transporto priemonę pagal prancūzišką, itališką, ispanišką, portugališką arba britišką numerį: markė, modelis, metai, VIN, degalai, variklio tūris ir daugiau iš šalies registracijos duomenų per Openapi.com. Kiekviena užklausa apmokestinama iš pačių dirbtuvių Openapi paskyros. Savininko duomenys negrąžinami.",
+      "fields": {
+        "token": "API žetonas",
+        "tokenHelp": "Susikurkite paskyrą console.openapi.com, papildykite piniginę ir skiltyje Automotive sukurkite žetoną gamybinei aplinkai. Kiekviena sėkminga užklausa kainuoja kelias dešimtis centų; nerastas rezultatas nemokamas."
+      },
+      "settings": {
+        "country": "Šalis",
+        "countryHelp": "Registras, kuriame ieškoti. Prancūziški ir itališki numeriai atrodo vienodai, todėl šalies negalima nustatyti iš numerio.",
+        "bikes": "Ieškoti ir motociklų",
+        "bikesHelp": "Kai numeris nėra automobilio, papildomai užklausiamas motociklų registras. Nerastas automobilis nekainuoja, todėl papildomų išlaidų nėra, nebent randamas motociklas. Portugalijai neprieinama."
+      }
+    },
+    "rdw": {
+      "description": "Užpildykite transporto priemonę pagal olandišką numerį: markė, modelis, metai, degalai, variklio tūris, spalva, masės ir kita APK data, tiesiai iš atviro RDW transporto priemonių registro. Nemokamai, be rakto. Savininko duomenys ir VIN neskelbiami.",
+      "fields": {
+        "appToken": "Programos žetonas (neprivaloma)",
+        "appTokenHelp": "RDW atviri duomenys veikia be rakto, bet anoniminės užklausos dalijasi ribotą kvotą. Apžiūrų sinchronizavimui susikurkite nemokamą paskyrą opendata.rdw.nl, atverkite profilio kūrėjo nustatymus ir sukurkite programos žetoną."
+      },
+      "settings": {
+        "syncInspections": "Atnaujinti visų šių dirbtuvių transporto priemonių APK datas",
+        "syncInspectionsHelp": "Kartą per valandą neseniai netikrintos transporto priemonės ieškomos pagal numerį ir išsaugoma kita APK data. Siunčiamos tik šiose dirbtuvėse saugomos transporto priemonės. Tikrinama kas mėnesį, o kai iki termino lieka mažiau nei du mėnesiai, kas savaitę."
+      }
+    },
+    "regcheck": {
+      "description": "Užpildykite transporto priemonę pagal numerį Australijoje, Naujojoje Zelandijoje, JAV, Airijoje, Norvegijoje, Švedijoje, Danijoje, Suomijoje, Estijoje, Čekijoje, Slovakijoje, Vengrijoje arba Kroatijoje: markė, modelis, metai, VIN, degalai ir variklio tūris, kai šalis juos skelbia, per RegCheck. Kiekviena sėkminga užklausa išleidžia vieną kreditą iš pačių dirbtuvių RegCheck paskyros. Savininko duomenys negrąžinami.",
+      "fields": {
+        "username": "RegCheck naudotojo vardas",
+        "usernameHelp": "Užsiregistruokite regcheck.org.uk; paskyroje yra 10 nemokamų užklausų, daugiau perkama po 100 valdymo skydelyje. Čia reikia tik naudotojo vardo, niekada slaptažodžio. Nerastas numeris nieko nekainuoja."
+      },
+      "settings": {
+        "country": "Šalis",
+        "countryHelp": "Registras, kuriame ieškoti. Kiekviena šalis turi savo paslaugą, todėl ryšys ieško vienoje.",
+        "auState": "Valstija arba teritorija",
+        "auStateHelp": "Australija registruoja transporto priemones pagal valstijas, ir registrui reikia nurodyti, kurioje ieškoti. Pasirinkite tą, kurioje registruota dauguma jūsų klientų transporto priemonių.",
+        "usState": "Valstija",
+        "usStateHelp": "Dviejų raidžių valstijos, kurioje registruotos transporto priemonės, kodas, pvz., CA arba TX. JAV registrai užklausiami pagal valstiją."
+      }
+    },
+    "openai": {
+      "description": "Paslaugų aprašymai, istorijos santraukos, dokumentų nuskaitymas ir asistentas — atsako jūsų pačių OpenAI paskyra. Kiekvienas užklausimas apmokestinamas toje paskyroje, o modelį pasirenkate jūs.",
+      "fields": {
+        "apiKey": "API raktas",
+        "apiKeyHelp": "Susikurkite platform.openai.com skiltyje API keys. Paskyroje reikia lėšų; OpenAI apmokestina kiekvieną užklausimą."
+      },
+      "settings": {
+        "model": "Modelis",
+        "modelHelp": "Kuris modelis atsako. Sąrašas nuskaitomas iš jūsų paskyros, naujausi viršuje. Mažesni modeliai kainuoja mažiau ir daugumai serviso tekstų visiškai pakanka."
+      }
+    },
+    "anthropic": {
+      "description": "Paslaugų aprašymai, istorijos santraukos, dokumentų nuskaitymas ir asistentas — atsako Claude jūsų pačių Anthropic paskyroje. Kiekvienas užklausimas apmokestinamas toje paskyroje, o modelį pasirenkate jūs.",
+      "fields": {
+        "apiKey": "API raktas",
+        "apiKeyHelp": "Susikurkite console.anthropic.com skiltyje API keys. Paskyroje reikia lėšų; Anthropic apmokestina kiekvieną užklausimą."
+      },
+      "settings": {
+        "model": "Modelis",
+        "modelHelp": "Kuris modelis atsako. Sąrašas nuskaitomas iš jūsų paskyros, naujausi viršuje. Mažesni modeliai kainuoja mažiau ir daugumai serviso tekstų visiškai pakanka."
+      }
+    },
+    "stripe": {
+      "description": "Mokėjimai kortele iš sąskaitos nuorodos į jūsų Stripe paskyrą. Klientas sumoka, Stripe praneša Torqvoice, ir sąskaita pati pažymima kaip apmokėta.",
+      "fields": {
+        "secretKey": "Slaptasis raktas",
+        "secretKeyHelp": "Iš Stripe valdymo skydelio, Developers, API keys. Bandomieji raktai nieko tikro neapdoroja, todėl pirmiausia išbandykite mokėjimą su jais, o paskui pereikite prie tikrų raktų.",
+        "publishableKey": "Viešasis raktas",
+        "webhookSecret": "Webhook parašo paslaptis",
+        "webhookSecretHelp": "Stripe ją parodo, kai šio puslapio pranešimų URL pridedate kaip webhook. Su ja kiekvienas pranešimas patikrinamas prieš užregistruojant mokėjimą."
+      },
+      "settings": {
+        "enabled": "Siūlyti sąskaitų nuorodose",
+        "enabledHelp": "Išjunkite, kad kuriam laikui pašalintumėte mokėjimo mygtuką iš bendrinamų sąskaitų neatjungdami. Jau pradėti mokėjimai vis tiek užregistruojami."
+      }
+    },
+    "vipps": {
+      "description": "Vipps MobilePay iš sąskaitos nuorodos. Klientas sumoka programėlėje, kurią jau turi, ir sąskaita pati pažymima kaip apmokėta.",
+      "fields": {
+        "clientId": "Kliento ID",
+        "clientSecret": "Kliento paslaptis",
+        "subscriptionKey": "Prenumeratos raktas",
+        "subscriptionKeyHelp": "Ocp-Apim-Subscription-Key iš Vipps MobilePay portalo, pardavimo vieneto API raktuose.",
+        "merchantSerialNumber": "Prekybininko serijos numeris"
+      },
+      "settings": {
+        "enabled": "Siūlyti sąskaitų nuorodose",
+        "enabledHelp": "Išjunkite, kad kuriam laikui pašalintumėte mokėjimo mygtuką iš bendrinamų sąskaitų neatjungdami. Jau pradėti mokėjimai vis tiek užregistruojami.",
+        "testMode": "Bandomasis režimas",
+        "testModeHelp": "Naudoti Vipps bandomąją aplinką su bandomaisiais raktais. Palikite įjungtą, kol pavyks visas mokėjimas, tada išjunkite ir įveskite tikrus raktus."
+      }
+    },
+    "paypal": {
+      "description": "PayPal atsiskaitymas iš sąskaitos nuorodos per jūsų PayPal programėlę. Klientas sumoka PayPal paskyra arba kortele, ir sąskaita pati pažymima kaip apmokėta.",
+      "fields": {
+        "clientId": "Kliento ID",
+        "clientSecret": "Kliento paslaptis"
+      },
+      "settings": {
+        "enabled": "Siūlyti sąskaitų nuorodose",
+        "enabledHelp": "Išjunkite, kad kuriam laikui pašalintumėte mokėjimo mygtuką iš bendrinamų sąskaitų neatjungdami. Jau pradėti mokėjimai vis tiek užregistruojami.",
+        "sandbox": "Smėlio dėžės režimas",
+        "sandboxHelp": "Naudoti PayPal smėlio dėžę su smėlio dėžės programėlės raktais. Palikite įjungtą, kol pavyks visas mokėjimas, tada išjunkite ir įveskite tikros programėlės raktus."
+      }
+    },
+    "quickbooks": {
+      "description": "Siųskite išrašytas sąskaitas, jų klientus ir mokėjimus į QuickBooks Online ir parsisiųskite ten užregistruotus mokėjimus.",
+      "tenantHelp": "Sukurkite programą Intuit kūrėjų portale. Nukopijuokite client id ir secret iš Keys and credentials, o žemiau esantį peradresavimo URI įveskite skiltyje Settings, Redirect URIs. Kūrimo raktai pasiekia sandbox įmones, gamybos raktai tikras.",
+      "fields": {
+        "clientId": "Kliento ID",
+        "clientSecret": "Kliento slaptažodis"
+      },
+      "settings": {
+        "pushInvoices": "Siųsti išrašytas sąskaitas į QuickBooks",
+        "pushOnComplete": "Siųsti sąskaitas ir tada, kai darbas pažymimas kaip užbaigtas",
+        "pushOnCompleteHelp": "Be šio nustatymo sąskaita perduodama, kai ji išsiunčiama, pasidalinama ar apmokama. Su juo užbaigtas darbas perduodamas iš karto ir atnaujinamas, jei pasikeičia.",
+        "startDate": "Tik sąskaitos, datuotos nuo",
+        "startDateHelp": "Palikite tuščią, kad būtų siunčiamos visos išrašytos sąskaitos. Naudokite datą, kada apskaita perkelta į QuickBooks, kad senesnės sąskaitos nebūtų įvestos dukart.",
+        "includeVehicle": "Nurodyti transporto priemonę sąskaitoje",
+        "includeVehicleHelp": "Teksto eilutė virš pozicijų su metais, marke, modeliu, numeriu ir rida, kad apskaita žinotų, apie kurį automobilį kalbama.",
+        "laborItemId": "Prekė darbo eilutėms",
+        "laborItemIdHelp": "Palikite tuščią ir pirmą kartą po jūsų pajamų sąskaita bus sukurta prekė pavadinimu Labour.",
+        "partsItemId": "Prekė dalių eilutėms",
+        "partsItemIdHelp": "Palikite tuščią ir pirmą kartą po jūsų pajamų sąskaita bus sukurta prekė pavadinimu Parts.",
+        "taxAmounts": "Mokesčių sumos",
+        "taxAmountsHelp": "Kaip sąskaitoje siunčia mokestį tiksliai tokį, koks nurodytas kliento gautoje sąskaitoje, todėl abi sistemos sutampa iki cento. Apskaičiuoja QuickBooks leidžia QuickBooks taikyti mokesčio kodo tarifą, JAV – kliento adresą, o žurnalas praneša, kai sumos skiriasi.",
+        "taxAmountsBilled": "Kaip sąskaitoje Torqvoice",
+        "taxAmountsQuickbooks": "Apskaičiuoja QuickBooks",
+        "taxCodeId": "Mokesčio kodas apmokestinamoms sąskaitoms",
+        "taxCodeIdHelp": "QuickBooks mokestį apskaito pagal mokesčio kodą. Torqvoice parenka kodą, kurio tarifas sutampa su kiekviena sąskaita; šis naudojamas, kai nė vienas nesutampa. JAV įmonė naudoja TAX, nebent pasirinksite kitą kodą.",
+        "zeroTaxCodeId": "Mokesčio kodas sąskaitoms be mokesčio",
+        "zeroTaxCodeIdHelp": "Naudojamas, kai sąskaitoje nėra mokesčio arba klientas atleistas nuo mokesčio. JAV įmonė naudoja NON, nebent pasirinksite kitą kodą.",
+        "pushPayments": "Registruoti mokėjimus QuickBooks",
+        "depositAccountId": "Mokėjimus įnešti į",
+        "depositAccountIdHelp": "Palikite tuščią, kad būtų naudojama Undeposited Funds.",
+        "manualPaidAsPayment": "Registruoti mokėjimą, kai sąskaita ranka pažymima kaip apmokėta",
+        "manualPaidAsPaymentHelp": "Torqvoice be mokėjimo įrašo kaip apmokėta pažymėta sąskaita QuickBooks gauna vieną mokėjimą likusiai ten rodomai sumai. Išjungus ji ten lieka atvira, kol užregistruojamas mokėjimas.",
+        "pullPayments": "Parsisiųsti QuickBooks užregistruotus mokėjimus",
+        "pullPaymentsHelp": "Kas 30 minučių. QuickBooks gautas mokėjimas už Torqvoice sąskaitą užregistruojamas čia prie sąskaitos."
+      }
+    },
+    "nhtsa": {
+      "description": "Užpildykite transporto priemonę pagal VIN ir matykite kiekvienų modelio metų atšaukimus, savininkų skundus ir smūgio bandymų įvertinimus iš JAV saugos institucijos. Nemokamai, be rakto.",
+      "fields": {},
+      "settings": {
+        "refreshFleet": "Nuolat atnaujinti atšaukimų ir skundų duomenis visoms šių dirbtuvių transporto priemonėms",
+        "refreshFleetHelp": "Kartą per dieną modeliai, neatnaujinti per pastarąją savaitę, užklausiami iš naujo, kad transporto priemonės puslapiui niekada nereikėtų laukti, o naujas atšaukimas pasirodytų per savaitę. Siunčiami tik markė, modelis ir metai."
+      }
+    }
+  },
+  "jobKinds": {
+    "calendar": {
+      "push": "Užsakymai į kalendorių",
+      "pull": "Užimtas laikas iš kalendoriaus"
+    },
+    "conference": {
+      "sync": "Vaizdo skambučių nuorodos"
+    },
+    "inspection": {
+      "refresh": "Apžiūros terminų atnaujinimas"
+    },
+    "accounting": {
+      "invoice": "Sąskaita į apskaitą",
+      "payment": "Mokėjimas į apskaitą",
+      "customer": "Klientas į apskaitą",
+      "pull": "Mokėjimai iš apskaitos"
+    },
+    "safety": {
+      "refresh": "Atšaukimų duomenų atnaujinimas",
+      "vehicle": "Naujos transporto priemonės atšaukimų patikra"
+    }
+  }
+}

+ 2 - 1
messages/lt/messages.json

@@ -7,7 +7,8 @@
       "scheduled": "Suplanuota",
       "sms": "SMS",
       "whatsapp": "„WhatsApp“",
-      "telegram": "„Telegram“"
+      "telegram": "„Telegram“",
+      "campaigns": "Kampanijos"
     },
     "smsNotConfiguredTitle": "SMS nesukonfigūruotas",
     "smsNotConfiguredHint": "Prijunkite SMS tiekėją, kad galėtumėte siųsti ir gauti žinutes čia. Suplanuoti el. laiškai veikia ir be to.",

+ 5 - 1
messages/lt/messaging.json

@@ -21,6 +21,10 @@
     "deleteFailed": "Nepavyko ištrinti pokalbio",
     "delete": "Ištrinti",
     "cancel": "Atšaukti",
-    "loadMore": "Įkelti daugiau"
+    "loadMore": "Įkelti daugiau",
+    "unread": "{count} neperskaityta",
+    "markUnread": "Pažymėti kaip neskaitytą",
+    "nothingToMarkUnread": "Klientas šiame pokalbyje dar nieko nerašė.",
+    "markUnreadFailed": "Nepavyko pažymėti pokalbio kaip neskaityto."
   }
 }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor