Просмотр исходного кода

Add Russian locale, RUB currency & Cyrillic PDF support (#68)

* rur translations

* charset

* combined appearance and currency into localization tab

* fonts

* do not break word

* Rub rur locale fix (#69)

* Update admin.json

admin.json ru-RU translation change

* Update calendar.json

calendar.json change week days order mon to sun

* Update reports.json

reports.json change order mon to sun

* Update settings.json

settings.json change order mon to sun

---------

Co-authored-by: Sergey <west.mc.online@gmail.com>
Bernt Christian Egeland 6 месяцев назад
Родитель
Сommit
9beacd02bd
52 измененных файлов с 4173 добавлено и 87 удалено
  1. 17 2
      messages/de/settings.json
  2. 89 20
      messages/en/settings.json
  3. 17 2
      messages/es/settings.json
  4. 17 2
      messages/fr/settings.json
  5. 17 2
      messages/it/settings.json
  6. 89 20
      messages/nb/settings.json
  7. 17 2
      messages/nl/settings.json
  8. 17 2
      messages/pl/settings.json
  9. 17 2
      messages/pt-BR/settings.json
  10. 108 0
      messages/ru/admin.json
  11. 18 0
      messages/ru/aiChat.json
  12. 86 0
      messages/ru/audit.json
  13. 86 0
      messages/ru/auth.json
  14. 83 0
      messages/ru/billing.json
  15. 61 0
      messages/ru/calendar.json
  16. 43 0
      messages/ru/common.json
  17. 122 0
      messages/ru/customers.json
  18. 104 0
      messages/ru/dashboard.json
  19. 87 0
      messages/ru/inventory.json
  20. 45 0
      messages/ru/messages.json
  21. 92 0
      messages/ru/navigation.json
  22. 10 0
      messages/ru/notifications.json
  23. 103 0
      messages/ru/pdf.json
  24. 133 0
      messages/ru/portal.json
  25. 186 0
      messages/ru/quotes.json
  26. 11 0
      messages/ru/reminders.json
  27. 199 0
      messages/ru/reports.json
  28. 285 0
      messages/ru/service.json
  29. 1003 0
      messages/ru/settings.json
  30. 156 0
      messages/ru/share.json
  31. 225 0
      messages/ru/vehicles.json
  32. 75 0
      messages/ru/workBoard.json
  33. 80 0
      messages/ru/workOrders.json
  34. 17 2
      messages/tr/settings.json
  35. 1 0
      src/app/(authenticated)/settings/currency/currency-settings.tsx
  36. 380 0
      src/app/(authenticated)/settings/localization/localization-settings.tsx
  37. 9 0
      src/app/(authenticated)/settings/localization/page.tsx
  38. 1 5
      src/app/(authenticated)/settings/settings-nav.tsx
  39. 5 5
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  40. 1 0
      src/app/api/protected/inspections/[id]/pdf/route.ts
  41. 1 0
      src/app/api/protected/quotes/[id]/pdf/route.ts
  42. 1 0
      src/app/api/protected/services/[id]/pdf/route.ts
  43. 1 0
      src/app/api/public/share/inspection/[orgId]/[token]/pdf/route.ts
  44. 1 0
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  45. 1 0
      src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts
  46. 2 2
      src/app/layout.tsx
  47. BIN
      src/assets/fonts/Roboto-Bold.ttf
  48. BIN
      src/assets/fonts/Roboto-Regular.ttf
  49. 1 0
      src/features/email/Actions/emailActions.ts
  50. 23 0
      src/features/vehicles/Components/invoice-pdf/fonts.ts
  51. 28 18
      src/features/vehicles/Components/invoice-pdf/styles.ts
  52. 2 1
      src/i18n/config.ts

+ 17 - 2
messages/de/settings.json

@@ -42,6 +42,10 @@
         "title": "Wahrung",
         "description": "Wahrungs- & Steuerstandards"
       },
+      "localization": {
+        "title": "Lokalisierung",
+        "description": "Sprache, Währung, Datum & Uhrzeit"
+      },
       "customFields": {
         "title": "Benutzerdefinierte Felder",
         "description": "Eigene Datenfelder definieren"
@@ -66,7 +70,10 @@
         "title": "Wartung",
         "description": "Vorhergesagte Serviceintervalle"
       },
-      "ai": { "title": "AI", "description": "KI-Anbieter & Funktionen" },
+      "ai": {
+        "title": "AI",
+        "description": "KI-Anbieter & Funktionen"
+      },
       "appearance": {
         "title": "Darstellung",
         "description": "Design & Anzeige"
@@ -588,9 +595,17 @@
       "IDR": "Indonesische Rupiah",
       "MYR": "Malaysischer Ringgit",
       "AED": "VAE-Dirham",
-      "SAR": "Saudi-Riyal"
+      "SAR": "Saudi-Riyal",
+      "RUB": "Russischer Rubel"
     }
   },
+  "localization": {
+    "languageTitle": "Sprache",
+    "languageDescription": "Wählen Sie die Anzeigesprache der Anwendung.",
+    "language": "Sprache",
+    "saved": "Lokalisierungseinstellungen gespeichert",
+    "saveSettings": "Einstellungen speichern"
+  },
   "customFields": {
     "title": "Benutzerdefinierte Felder",
     "description": "Definieren Sie benutzerdefinierte Felder, die auf Serviceberichten und Angeboten erscheinen.",

+ 89 - 20
messages/en/settings.json

@@ -10,25 +10,86 @@
       "system": "System"
     },
     "items": {
-      "company": { "title": "Company", "description": "Business details & branding" },
-      "subscription": { "title": "Subscription", "description": "Plan & billing" },
-      "account": { "title": "Account", "description": "Name, email & password" },
-      "team": { "title": "Team", "description": "Members & roles" },
-      "invoice": { "title": "Invoice & Quotes", "description": "Invoice settings, layout & custom fields" },
-      "templates": { "title": "Templates", "description": "Visual style, colors & fonts" },
-      "payment": { "title": "Payment", "description": "Payment terms & accounts" },
-      "currency": { "title": "Currency", "description": "Currency & tax defaults" },
-      "customFields": { "title": "Custom Fields", "description": "Define custom data fields" },
-      "email": { "title": "Email", "description": "Email provider & sending" },
-      "sms": { "title": "SMS", "description": "SMS provider & messaging" },
-      "customerPortal": { "title": "Customer Portal", "description": "Customer self-service portal" },
-      "workshop": { "title": "Workshop", "description": "Technician & labor defaults" },
-      "maintenance": { "title": "Maintenance", "description": "Predicted service intervals" },
-      "ai": { "title": "AI", "description": "AI provider & features" },
-      "appearance": { "title": "Appearance", "description": "Theme & display" },
-      "data": { "title": "Data", "description": "Export & import backup" },
-      "license": { "title": "License", "description": "White-label license" },
-      "about": { "title": "About", "description": "Version & info" }
+      "company": {
+        "title": "Company",
+        "description": "Business details & branding"
+      },
+      "subscription": {
+        "title": "Subscription",
+        "description": "Plan & billing"
+      },
+      "account": {
+        "title": "Account",
+        "description": "Name, email & password"
+      },
+      "team": {
+        "title": "Team",
+        "description": "Members & roles"
+      },
+      "invoice": {
+        "title": "Invoice & Quotes",
+        "description": "Invoice settings, layout & custom fields"
+      },
+      "templates": {
+        "title": "Templates",
+        "description": "Visual style, colors & fonts"
+      },
+      "payment": {
+        "title": "Payment",
+        "description": "Payment terms & accounts"
+      },
+      "currency": {
+        "title": "Currency",
+        "description": "Currency & tax defaults"
+      },
+      "localization": {
+        "title": "Localization",
+        "description": "Language, currency, date & time"
+      },
+      "customFields": {
+        "title": "Custom Fields",
+        "description": "Define custom data fields"
+      },
+      "email": {
+        "title": "Email",
+        "description": "Email provider & sending"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "SMS provider & messaging"
+      },
+      "customerPortal": {
+        "title": "Customer Portal",
+        "description": "Customer self-service portal"
+      },
+      "workshop": {
+        "title": "Workshop",
+        "description": "Technician & labor defaults"
+      },
+      "maintenance": {
+        "title": "Maintenance",
+        "description": "Predicted service intervals"
+      },
+      "ai": {
+        "title": "AI",
+        "description": "AI provider & features"
+      },
+      "appearance": {
+        "title": "Appearance",
+        "description": "Theme & display"
+      },
+      "data": {
+        "title": "Data",
+        "description": "Export & import backup"
+      },
+      "license": {
+        "title": "License",
+        "description": "White-label license"
+      },
+      "about": {
+        "title": "About",
+        "description": "Version & info"
+      }
     },
     "pro": "PRO"
   },
@@ -534,9 +595,17 @@
       "IDR": "Indonesian Rupiah",
       "MYR": "Malaysian Ringgit",
       "AED": "UAE Dirham",
-      "SAR": "Saudi Riyal"
+      "SAR": "Saudi Riyal",
+      "RUB": "Russian Ruble"
     }
   },
+  "localization": {
+    "languageTitle": "Language",
+    "languageDescription": "Choose the display language for the application.",
+    "language": "Language",
+    "saved": "Localization settings saved",
+    "saveSettings": "Save Settings"
+  },
   "customFields": {
     "title": "Custom Fields",
     "description": "Define custom fields that appear on service records and quotes.",

+ 17 - 2
messages/es/settings.json

@@ -42,6 +42,10 @@
         "title": "Moneda",
         "description": "Valores predeterminados de moneda e impuestos"
       },
+      "localization": {
+        "title": "Localización",
+        "description": "Idioma, moneda, fecha y hora"
+      },
       "customFields": {
         "title": "Campos personalizados",
         "description": "Definir campos de datos personalizados"
@@ -66,7 +70,10 @@
         "title": "Mantenimiento",
         "description": "Intervalos de servicio predichos"
       },
-      "ai": { "title": "AI", "description": "Proveedor de IA y funciones" },
+      "ai": {
+        "title": "AI",
+        "description": "Proveedor de IA y funciones"
+      },
       "appearance": {
         "title": "Apariencia",
         "description": "Tema y visualizacion"
@@ -588,9 +595,17 @@
       "IDR": "Rupia indonesia",
       "MYR": "Ringgit malayo",
       "AED": "Dirham de los EAU",
-      "SAR": "Riyal saudi"
+      "SAR": "Riyal saudi",
+      "RUB": "Rublo ruso"
     }
   },
+  "localization": {
+    "languageTitle": "Idioma",
+    "languageDescription": "Elija el idioma de visualización de la aplicación.",
+    "language": "Idioma",
+    "saved": "Configuración de localización guardada",
+    "saveSettings": "Guardar configuración"
+  },
   "customFields": {
     "title": "Campos personalizados",
     "description": "Defina campos personalizados que aparecen en registros de servicio y presupuestos.",

+ 17 - 2
messages/fr/settings.json

@@ -42,6 +42,10 @@
         "title": "Devise",
         "description": "Devise et taxes par defaut"
       },
+      "localization": {
+        "title": "Localisation",
+        "description": "Langue, devise, date et heure"
+      },
       "customFields": {
         "title": "Champs personnalises",
         "description": "Definir des champs de donnees personnalises"
@@ -66,7 +70,10 @@
         "title": "Maintenance",
         "description": "Intervalles de service predits"
       },
-      "ai": { "title": "AI", "description": "Fournisseur IA et fonctionnalites" },
+      "ai": {
+        "title": "AI",
+        "description": "Fournisseur IA et fonctionnalites"
+      },
       "appearance": {
         "title": "Apparence",
         "description": "Theme et affichage"
@@ -588,9 +595,17 @@
       "IDR": "Roupie indonesienne",
       "MYR": "Ringgit malaisien",
       "AED": "Dirham des EAU",
-      "SAR": "Riyal saoudien"
+      "SAR": "Riyal saoudien",
+      "RUB": "Rouble russe"
     }
   },
+  "localization": {
+    "languageTitle": "Langue",
+    "languageDescription": "Choisissez la langue d'affichage de l'application.",
+    "language": "Langue",
+    "saved": "Paramètres de localisation enregistrés",
+    "saveSettings": "Enregistrer les paramètres"
+  },
   "customFields": {
     "title": "Champs personnalisés",
     "description": "Définissez des champs personnalisés qui apparaissent sur les fiches de service et les devis.",

+ 17 - 2
messages/it/settings.json

@@ -42,6 +42,10 @@
         "title": "Valuta",
         "description": "Valuta e imposte predefinite"
       },
+      "localization": {
+        "title": "Localizzazione",
+        "description": "Lingua, valuta, data e ora"
+      },
       "customFields": {
         "title": "Campi personalizzati",
         "description": "Definisci campi dati personalizzati"
@@ -66,7 +70,10 @@
         "title": "Manutenzione",
         "description": "Intervalli di servizio previsti"
       },
-      "ai": { "title": "AI", "description": "Provider IA e funzionalita" },
+      "ai": {
+        "title": "AI",
+        "description": "Provider IA e funzionalita"
+      },
       "appearance": {
         "title": "Aspetto",
         "description": "Tema e visualizzazione"
@@ -588,9 +595,17 @@
       "IDR": "Rupia indonesiana",
       "MYR": "Ringgit malese",
       "AED": "Dirham degli EAU",
-      "SAR": "Riyal saudita"
+      "SAR": "Riyal saudita",
+      "RUB": "Rublo russo"
     }
   },
+  "localization": {
+    "languageTitle": "Lingua",
+    "languageDescription": "Scegli la lingua di visualizzazione dell'applicazione.",
+    "language": "Lingua",
+    "saved": "Impostazioni di localizzazione salvate",
+    "saveSettings": "Salva impostazioni"
+  },
   "customFields": {
     "title": "Campi personalizzati",
     "description": "Definisci campi personalizzati che appaiono su schede di servizio e preventivi.",

+ 89 - 20
messages/nb/settings.json

@@ -10,25 +10,86 @@
       "system": "System"
     },
     "items": {
-      "company": { "title": "Bedrift", "description": "Bedriftsdetaljer og merkevare" },
-      "subscription": { "title": "Abonnement", "description": "Plan og fakturering" },
-      "account": { "title": "Konto", "description": "Navn, e-post og passord" },
-      "team": { "title": "Team", "description": "Medlemmer og roller" },
-      "invoice": { "title": "Faktura og tilbud", "description": "Fakturainnstillinger, layout og egendefinerte felter" },
-      "templates": { "title": "Maler", "description": "Visuell stil, farger og skrifter" },
-      "payment": { "title": "Betaling", "description": "Betalingsvilkar og kontoer" },
-      "currency": { "title": "Valuta", "description": "Valuta- og avgiftsstandarder" },
-      "customFields": { "title": "Egendefinerte felter", "description": "Definer egne datafelter" },
-      "email": { "title": "E-post", "description": "E-postleverandor og sending" },
-      "sms": { "title": "SMS", "description": "SMS-leverandor og meldinger" },
-      "customerPortal": { "title": "Kundeportal", "description": "Kundeselvbetjeningsportal" },
-      "workshop": { "title": "Verksted", "description": "Tekniker- og arbeidsstandarder" },
-      "maintenance": { "title": "Vedlikehold", "description": "Predikerte serviceintervaller" },
-      "ai": { "title": "AI", "description": "AI-leverandor og funksjoner" },
-      "appearance": { "title": "Utseende", "description": "Tema og visning" },
-      "data": { "title": "Data", "description": "Eksporter og importer sikkerhetskopi" },
-      "license": { "title": "Lisens", "description": "White-label-lisens" },
-      "about": { "title": "Om", "description": "Versjon og informasjon" }
+      "company": {
+        "title": "Bedrift",
+        "description": "Bedriftsdetaljer og merkevare"
+      },
+      "subscription": {
+        "title": "Abonnement",
+        "description": "Plan og fakturering"
+      },
+      "account": {
+        "title": "Konto",
+        "description": "Navn, e-post og passord"
+      },
+      "team": {
+        "title": "Team",
+        "description": "Medlemmer og roller"
+      },
+      "invoice": {
+        "title": "Faktura og tilbud",
+        "description": "Fakturainnstillinger, layout og egendefinerte felter"
+      },
+      "templates": {
+        "title": "Maler",
+        "description": "Visuell stil, farger og skrifter"
+      },
+      "payment": {
+        "title": "Betaling",
+        "description": "Betalingsvilkar og kontoer"
+      },
+      "currency": {
+        "title": "Valuta",
+        "description": "Valuta- og avgiftsstandarder"
+      },
+      "localization": {
+        "title": "Lokalisering",
+        "description": "Språk, valuta, dato og tid"
+      },
+      "customFields": {
+        "title": "Egendefinerte felter",
+        "description": "Definer egne datafelter"
+      },
+      "email": {
+        "title": "E-post",
+        "description": "E-postleverandor og sending"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "SMS-leverandor og meldinger"
+      },
+      "customerPortal": {
+        "title": "Kundeportal",
+        "description": "Kundeselvbetjeningsportal"
+      },
+      "workshop": {
+        "title": "Verksted",
+        "description": "Tekniker- og arbeidsstandarder"
+      },
+      "maintenance": {
+        "title": "Vedlikehold",
+        "description": "Predikerte serviceintervaller"
+      },
+      "ai": {
+        "title": "AI",
+        "description": "AI-leverandor og funksjoner"
+      },
+      "appearance": {
+        "title": "Utseende",
+        "description": "Tema og visning"
+      },
+      "data": {
+        "title": "Data",
+        "description": "Eksporter og importer sikkerhetskopi"
+      },
+      "license": {
+        "title": "Lisens",
+        "description": "White-label-lisens"
+      },
+      "about": {
+        "title": "Om",
+        "description": "Versjon og informasjon"
+      }
     },
     "pro": "PRO"
   },
@@ -534,9 +595,17 @@
       "IDR": "Indonesiske rupiah",
       "MYR": "Malaysiske ringgit",
       "AED": "Emiratiske dirham",
-      "SAR": "Saudiarabiske riyal"
+      "SAR": "Saudiarabiske riyal",
+      "RUB": "Russisk rubel"
     }
   },
+  "localization": {
+    "languageTitle": "Språk",
+    "languageDescription": "Velg visningsspråk for applikasjonen.",
+    "language": "Språk",
+    "saved": "Lokaliseringsinnstillinger lagret",
+    "saveSettings": "Lagre innstillinger"
+  },
   "customFields": {
     "title": "Egendefinerte felter",
     "description": "Definer egendefinerte felter som vises på serviceregistreringer og tilbud.",

+ 17 - 2
messages/nl/settings.json

@@ -42,6 +42,10 @@
         "title": "Valuta",
         "description": "Standaardvaluta en belasting"
       },
+      "localization": {
+        "title": "Lokalisatie",
+        "description": "Taal, valuta, datum en tijd"
+      },
       "customFields": {
         "title": "Aangepaste velden",
         "description": "Eigen gegevensvelden definiëren"
@@ -66,7 +70,10 @@
         "title": "Onderhoud",
         "description": "Voorspelde service-intervallen"
       },
-      "ai": { "title": "AI", "description": "AI-provider & functies" },
+      "ai": {
+        "title": "AI",
+        "description": "AI-provider & functies"
+      },
       "appearance": {
         "title": "Weergave",
         "description": "Thema en weergave"
@@ -588,9 +595,17 @@
       "IDR": "Indonesische roepia",
       "MYR": "Maleisische ringgit",
       "AED": "VAE-dirham",
-      "SAR": "Saoedische riyal"
+      "SAR": "Saoedische riyal",
+      "RUB": "Russische roebel"
     }
   },
+  "localization": {
+    "languageTitle": "Taal",
+    "languageDescription": "Kies de weergavetaal van de applicatie.",
+    "language": "Taal",
+    "saved": "Lokalisatie-instellingen opgeslagen",
+    "saveSettings": "Instellingen opslaan"
+  },
   "customFields": {
     "title": "Aangepaste velden",
     "description": "Definieer aangepaste velden die verschijnen op servicerapporten en offertes.",

+ 17 - 2
messages/pl/settings.json

@@ -42,6 +42,10 @@
         "title": "Waluta",
         "description": "Domyslna waluta i podatki"
       },
+      "localization": {
+        "title": "Lokalizacja",
+        "description": "Język, waluta, data i czas"
+      },
       "customFields": {
         "title": "Pola niestandardowe",
         "description": "Definiowanie wlasnych pol danych"
@@ -66,7 +70,10 @@
         "title": "Konserwacja",
         "description": "Przewidywane interwaly serwisowe"
       },
-      "ai": { "title": "AI", "description": "Dostawca AI i funkcje" },
+      "ai": {
+        "title": "AI",
+        "description": "Dostawca AI i funkcje"
+      },
       "appearance": {
         "title": "Wyglad",
         "description": "Motyw i wyswietlanie"
@@ -588,9 +595,17 @@
       "IDR": "Rupia indonezyjska",
       "MYR": "Ringgit malezyjski",
       "AED": "Dirham ZEA",
-      "SAR": "Rial saudyjski"
+      "SAR": "Rial saudyjski",
+      "RUB": "Rubel rosyjski"
     }
   },
+  "localization": {
+    "languageTitle": "Język",
+    "languageDescription": "Wybierz język wyświetlania aplikacji.",
+    "language": "Język",
+    "saved": "Ustawienia lokalizacji zapisane",
+    "saveSettings": "Zapisz ustawienia"
+  },
   "customFields": {
     "title": "Pola niestandardowe",
     "description": "Zdefiniuj pola niestandardowe, które pojawiają się na kartach serwisowych i wycenach.",

+ 17 - 2
messages/pt-BR/settings.json

@@ -42,6 +42,10 @@
         "title": "Moeda",
         "description": "Padroes de moeda e impostos"
       },
+      "localization": {
+        "title": "Localização",
+        "description": "Idioma, moeda, data e hora"
+      },
       "customFields": {
         "title": "Campos personalizados",
         "description": "Definir campos de dados personalizados"
@@ -66,7 +70,10 @@
         "title": "Manutencao",
         "description": "Intervalos de servico previstos"
       },
-      "ai": { "title": "AI", "description": "Provedor de IA e recursos" },
+      "ai": {
+        "title": "AI",
+        "description": "Provedor de IA e recursos"
+      },
       "appearance": {
         "title": "Aparencia",
         "description": "Tema e exibicao"
@@ -588,9 +595,17 @@
       "IDR": "Rupia indonesia",
       "MYR": "Ringgit malaio",
       "AED": "Dirham dos EAU",
-      "SAR": "Rial saudita"
+      "SAR": "Rial saudita",
+      "RUB": "Rublo russo"
     }
   },
+  "localization": {
+    "languageTitle": "Idioma",
+    "languageDescription": "Escolha o idioma de exibição do aplicativo.",
+    "language": "Idioma",
+    "saved": "Configurações de localização salvas",
+    "saveSettings": "Salvar configurações"
+  },
   "customFields": {
     "title": "Campos personalizados",
     "description": "Defina campos personalizados que aparecem em registros de serviço e orçamentos.",

+ 108 - 0
messages/ru/admin.json

@@ -0,0 +1,108 @@
+{
+  "nav": {
+    "overview": { "title": "Обзор", "description": "Статистика и состояние платформы" },
+    "users": { "title": "Пользователи", "description": "Управление всеми пользователями" },
+    "organizations": { "title": "Организации", "description": "Управление всеми организациями" },
+    "settings": { "title": "Настройки", "description": "Конфигурация платформы" }
+  },
+  "layout": {
+    "title": "Панель администратора",
+    "description": "Управление платформой для суперадминистраторов"
+  },
+  "overview": {
+    "totalUsers": "Всего пользователей",
+    "organizations": "Организации",
+    "activeSubscriptions": "Активные подписки",
+    "monthlyRevenue": "Ежемесячный доход"
+  },
+  "users": {
+    "searchPlaceholder": "Поиск по имени или эл. почте...",
+    "name": "Имя",
+    "email": "Эл. почта",
+    "role": "Роль",
+    "orgs": "Организации",
+    "created": "Создан",
+    "lastSeen": "Последний визит",
+    "noResults": "Нет пользователей, соответствующих поиску.",
+    "noUsers": "Пользователи не найдены.",
+    "superAdmin": "Суперадмин",
+    "user": "Пользователь",
+    "online": "В сети",
+    "promoteTitle": "Повысить права пользователя",
+    "demoteTitle": "Понизить права пользователя",
+    "promoteConfirm": "Вы уверены, что хотите повысить \"{name}\" до суперадмина?",
+    "demoteConfirm": "Вы уверены, что хотите понизить \"{name}\" из суперадминов?",
+    "promote": "Повысить",
+    "demote": "Понизить",
+    "promotedSuccess": "Права пользователя успешно повышены",
+    "demotedSuccess": "Права пользователя успешно понижены",
+    "failedUpdate": "Не удалось обновить пользователя",
+    "deleteTitle": "Удалить пользователя",
+    "deleteConfirm": "Вы уверены, что хотите навсегда удалить \"{name}\" ({email})? Это действие необратимо.",
+    "delete": "Удалить",
+    "deletedSuccess": "Пользователь успешно удалён",
+    "failedDelete": "Не удалось удалить пользователя",
+    "promoteToSuperAdmin": "Повысить до суперадмина",
+    "demoteFromSuperAdmin": "Понизить из суперадминов",
+    "deleteUser": "Удалить пользователя",
+    "emailVerified": "Эл. почта подтверждена"
+  },
+  "organizations": {
+    "searchPlaceholder": "Поиск по названию организации...",
+    "name": "Название",
+    "owner": "Владелец",
+    "members": "Участники",
+    "subscription": "Подписка",
+    "created": "Создана",
+    "noResults": "Нет организаций, соответствующих поиску.",
+    "noOrganizations": "Организации не найдены.",
+    "none": "Нет",
+    "deleteTitle": "Удалить организацию",
+    "deleteConfirm": "Вы уверены, что хотите навсегда удалить \"{name}\" и все её данные? Это действие необратимо.",
+    "delete": "Удалить",
+    "deletedSuccess": "Организация успешно удалена",
+    "failedDelete": "Не удалось удалить организацию",
+    "deleteOrganization": "Удалить организацию"
+  },
+  "adminSettings": {
+    "platformTitle": "Настройки платформы",
+    "platformDescription": "Общие параметры конфигурации платформы",
+    "disableRegistration": "Отключить регистрацию",
+    "disableRegistrationHint": "Заблокировать регистрацию новых пользователей на платформе",
+    "requireEmailVerification": "Требовать подтверждение эл. почты",
+    "requireEmailVerificationHint": "Новые пользователи должны подтвердить адрес эл. почты перед доступом к платформе",
+    "emailTitle": "Настройки электронной почты",
+    "emailDescription": "Провайдер эл. почты платформы — используется для системных писем, таких как сброс пароля и приглашения в команду. Также является провайдером по умолчанию для всех организаций. Организации могут настроить собственного провайдера в Настройки > Эл. почта.",
+    "smtpHost": "SMTP-сервер",
+    "smtpPort": "Порт SMTP",
+    "username": "Имя пользователя",
+    "password": "Пароль",
+    "fromEmail": "Адрес отправителя",
+    "fromName": "Имя отправителя",
+    "tlsConnection": "TLS-подключение (порт 465)",
+    "tlsConnectionHint": "Включите для неявного TLS. Отключите для STARTTLS (порт 587/25).",
+    "verifyTls": "Проверять сертификаты TLS",
+    "verifyTlsHint": "Отключите для разрешения самоподписанных сертификатов (не рекомендуется для продакшена)",
+    "requireTlsUpgrade": "Требовать обновление до TLS",
+    "requireTlsUpgradeHint": "Принудительное обновление TLS на STARTTLS-соединениях",
+    "apiKey": "Ключ API",
+    "serverToken": "Токен сервера",
+    "domain": "Домен",
+    "region": "Регион",
+    "awsRegion": "Регион AWS",
+    "accessKeyId": "ID ключа доступа",
+    "secretAccessKey": "Секретный ключ доступа",
+    "awsRegionHint": "Регион AWS, в котором настроен SES (напр. us-east-1, eu-west-1)",
+    "sendTestEmail": "Отправить тестовое письмо",
+    "testEmailHint": "Сначала сохраните настройки, затем отправьте тестовое письмо на ваш аккаунт",
+    "saveSettings": "Сохранить настройки",
+    "saved": "Настройки успешно сохранены",
+    "failedSave": "Не удалось сохранить настройки",
+    "testSentTo": "Тестовое письмо отправлено на {email}",
+    "testFailed": "Тест отправки почты не удался",
+    "resendApiKeyHint": "Получите ключ API на resend.com",
+    "postmarkApiKeyHint": "Получите токен сервера на postmarkapp.com",
+    "mailgunApiKeyHint": "Получите ключ API на app.mailgun.com",
+    "sendgridApiKeyHint": "Получите ключ API на app.sendgrid.com"
+  }
+}

+ 18 - 0
messages/ru/aiChat.json

@@ -0,0 +1,18 @@
+{
+  "title": "ИИ-ассистент",
+  "description": "Задавайте вопросы о данных вашей мастерской — транспортные средства, обслуживание, клиенты, доход и многое другое.",
+  "placeholder": "Спросите о вашей мастерской...",
+  "clear": "Очистить беседу",
+  "error": "Что-то пошло не так. Пожалуйста, попробуйте снова.",
+  "notEnabled": "ИИ не включён. Настройте его в Настройки → ИИ.",
+  "suggestion1": "Какие транспортные средства требуют обслуживания?",
+  "suggestion2": "Покажи неоплаченные счета",
+  "suggestion3": "Какой доход за этот месяц?",
+  "suggestion4": "Дай обзор мастерской",
+  "newChat": "Новый чат",
+  "noChats": "Пока нет бесед",
+  "chatsPrivate": "Ваши чаты приватны и видны только вам.",
+  "deleteChat": "Удалить чат",
+  "hideSidebar": "Скрыть боковую панель",
+  "showSidebar": "Показать боковую панель"
+}

+ 86 - 0
messages/ru/audit.json

@@ -0,0 +1,86 @@
+{
+  "searchPlaceholder": "Поиск в журнале аудита...",
+  "allActions": "Все действия",
+  "allEntities": "Все объекты",
+  "allUsers": "Все пользователи",
+  "timestamp": "Время",
+  "user": "Пользователь",
+  "action": "Действие",
+  "details": "Подробности",
+  "entityId": "ID объекта",
+  "noLogsFound": "Записи аудита не найдены",
+  "unknownUser": "Неизвестен",
+  "logDetails": "Детали записи",
+  "entity": "Объект",
+  "ipAddress": "IP-адрес",
+  "userAgentLabel": "User Agent",
+  "metadata": "Метаданные",
+  "actions": {
+    "vehicle_create": "Создано транспортное средство",
+    "vehicle_update": "Обновлено транспортное средство",
+    "vehicle_delete": "Удалено транспортное средство",
+    "vehicle_archive": "Архивировано транспортное средство",
+    "vehicle_unarchive": "Разархивировано транспортное средство",
+    "service_create": "Создана запись обслуживания",
+    "service_update": "Обновлена запись обслуживания",
+    "service_status": "Изменён статус обслуживания",
+    "service_delete": "Удалена запись обслуживания",
+    "quote_create": "Создано предложение",
+    "quote_update": "Обновлено предложение",
+    "quote_status": "Изменён статус предложения",
+    "quote_delete": "Удалено предложение",
+    "quote_convert": "Конвертировано предложение",
+    "inspection_create": "Создан осмотр",
+    "inspection_complete": "Завершён осмотр",
+    "inspection_delete": "Удалён осмотр",
+    "payment_create": "Записан платёж",
+    "payment_delete": "Удалён платёж",
+    "customer_create": "Создан клиент",
+    "customer_update": "Обновлён клиент",
+    "customer_delete": "Удалён клиент",
+    "inventory_create": "Создана складская запчасть",
+    "inventory_update": "Обновлена складская запчасть",
+    "inventory_delete": "Удалена складская запчасть",
+    "team_invite": "Приглашён участник команды",
+    "team_sendInvitation": "Отправлено приглашение",
+    "team_cancelInvitation": "Отменено приглашение",
+    "team_updateRole": "Изменена роль участника",
+    "team_removeMember": "Удалён участник команды",
+    "role_create": "Создана роль",
+    "role_update": "Обновлена роль",
+    "role_delete": "Удалена роль",
+    "technician_create": "Создан механик",
+    "technician_update": "Обновлён механик",
+    "technician_delete": "Удалён механик",
+    "recurringInvoice_create": "Создан периодический счёт",
+    "recurringInvoice_update": "Обновлён периодический счёт",
+    "recurringInvoice_delete": "Удалён периодический счёт",
+    "email_sendQuote": "Отправлено предложение по эл. почте",
+    "email_sendInvoice": "Отправлен счёт по эл. почте",
+    "email_sendInspection": "Отправлен осмотр по эл. почте",
+    "sms_send": "Отправлено SMS",
+    "note_create": "Создана заметка",
+    "note_delete": "Удалена заметка",
+    "reminder_create": "Создано напоминание",
+    "reminder_delete": "Удалено напоминание",
+    "organization_create": "Создана организация",
+    "subscription_cancel": "Отменена подписка",
+    "subscription_resume": "Возобновлена подписка",
+    "settings_deleteContent": "Удалены данные",
+    "settings_updateInvoiceLayout": "Обновлён макет счёта",
+    "settings_updateQuoteLayout": "Обновлён макет предложения",
+    "settings_updatePortalSlug": "Обновлён адрес портала",
+    "inspectionTemplate_create": "Создан шаблон осмотра",
+    "inspectionTemplate_update": "Обновлён шаблон осмотра",
+    "inspectionTemplate_delete": "Удалён шаблон осмотра",
+    "customField_create": "Создано пользовательское поле",
+    "customField_update": "Обновлено пользовательское поле",
+    "customField_delete": "Удалено пользовательское поле",
+    "quoteRequest_update": "Обновлён запрос на предложение",
+    "auth_login": "Пользователь вошёл в систему",
+    "auth_loginFailed": "Неудачная попытка входа",
+    "auth_register": "Пользователь зарегистрировался",
+    "auth_passwordReset": "Запрошен сброс пароля",
+    "auth_permissionDenied": "Доступ запрещён"
+  }
+}

+ 86 - 0
messages/ru/auth.json

@@ -0,0 +1,86 @@
+{
+  "signIn": {
+    "title": "С возвращением",
+    "description": "Войдите в свою мастерскую",
+    "passwordPlaceholder": "Введите пароль",
+    "forgotPassword": "Забыли пароль?",
+    "noAccount": "Нет аккаунта?",
+    "createOne": "Создать",
+    "termsAgreement": "Используя этот сервис, вы соглашаетесь с нашими",
+    "or": "или",
+    "passkey": "Войти с помощью ключа доступа",
+    "passkeyDescription": "Используйте отпечаток пальца, лицо или ключ безопасности",
+    "errors": {
+      "tooManyAttempts": "Слишком много попыток, попробуйте позже.",
+      "invalidCredentials": "Неверный адрес эл. почты или пароль",
+      "passkeyFailed": "Вход по ключу доступа не удался. Попробуйте снова или используйте эл. почту и пароль."
+    }
+  },
+  "signUp": {
+    "title": "Создать аккаунт",
+    "descriptionInvite": "Создайте аккаунт, чтобы присоединиться к команде",
+    "descriptionDefault": "Настройте свою мастерскую для управления транспортными средствами клиентов",
+    "fullName": "Полное имя",
+    "fullNamePlaceholder": "Иван Иванов",
+    "passwordPlaceholder": "Придумайте надёжный пароль",
+    "agreeToTerms": "Я согласен с",
+    "createAccountJoin": "Создать аккаунт и присоединиться к команде",
+    "createAccount": "Создать аккаунт",
+    "alreadyHaveAccount": "Уже есть аккаунт?",
+    "errors": {
+      "couldNotCreate": "Не удалось создать аккаунт",
+      "registrationDisabled": "Регистрация в настоящее время отключена. Обратитесь к администратору.",
+      "invitationFailed": "Не удалось принять приглашение"
+    }
+  },
+  "forgotPassword": {
+    "title": "Сброс пароля",
+    "description": "Введите адрес эл. почты, и мы отправим вам ссылку для сброса",
+    "sendResetLink": "Отправить ссылку для сброса",
+    "backToSignIn": "Вернуться к входу",
+    "successMessage": "Если аккаунт с таким адресом существует, ссылка для сброса пароля была отправлена. Проверьте входящие."
+  },
+  "resetPassword": {
+    "title": "Установить новый пароль",
+    "description": "Введите новый пароль ниже",
+    "newPassword": "Новый пароль",
+    "newPasswordPlaceholder": "Введите новый пароль",
+    "confirmPassword": "Подтвердите пароль",
+    "confirmPasswordPlaceholder": "Подтвердите новый пароль",
+    "resetButton": "Сбросить пароль",
+    "backToSignIn": "Вернуться к входу",
+    "requestNewLink": "Запросить новую ссылку",
+    "invalidToken": "Недействительный или отсутствующий токен сброса. Запросите новую ссылку для сброса пароля.",
+    "successMessage": "Ваш пароль был успешно сброшен. Теперь вы можете войти с новым паролем.",
+    "errors": {
+      "passwordsMismatch": "Пароли не совпадают.",
+      "passwordTooShort": "Пароль должен содержать не менее 8 символов.",
+      "resetFailed": "Не удалось сбросить пароль. Возможно, ссылка устарела."
+    }
+  },
+  "verifyEmail": {
+    "title": "Подтвердите адрес эл. почты",
+    "description": "Мы отправили ссылку для подтверждения на {email}. Проверьте входящие и перейдите по ссылке для подтверждения адреса.",
+    "resend": "Отправить письмо повторно",
+    "resent": "Письмо для подтверждения отправлено. Проверьте входящие.",
+    "signOut": "Выйти",
+    "resendCooldown": "Повторная отправка через {seconds}с",
+    "checkSpam": "Если вы не видите письмо, проверьте папку «Спам»."
+  },
+  "verify2fa": {
+    "title": "Двухфакторная аутентификация",
+    "descriptionTotp": "Введите 6-значный код из приложения-аутентификатора",
+    "descriptionBackup": "Введите один из резервных кодов",
+    "authCode": "Код аутентификации",
+    "backupCode": "Резервный код",
+    "backupCodePlaceholder": "Введите резервный код",
+    "totpPlaceholder": "000000",
+    "trustDevice": "Доверять этому устройству 30 дней",
+    "verify": "Подтвердить",
+    "useAuthenticator": "Использовать приложение-аутентификатор",
+    "useBackupCode": "Использовать резервный код",
+    "errors": {
+      "invalidCode": "Неверный код"
+    }
+  }
+}

+ 83 - 0
messages/ru/billing.json

@@ -0,0 +1,83 @@
+{
+  "history": {
+    "title": "История счетов",
+    "recurring": "Периодические",
+    "totalRevenue": "Общий доход",
+    "collected": "Собрано",
+    "outstanding": "Задолженность",
+    "statusAll": "Все",
+    "statusPaid": "Оплачено",
+    "statusPartial": "Частично",
+    "statusUnpaid": "Не оплачено",
+    "searchPlaceholder": "Поиск счетов...",
+    "search": "Поиск",
+    "columnInvoice": "Счёт №",
+    "columnTitle": "Название",
+    "columnVehicle": "Транспортное средство",
+    "columnCustomer": "Клиент",
+    "columnDate": "Дата",
+    "columnTotal": "Итого",
+    "columnPaid": "Оплачено",
+    "columnBalance": "Остаток",
+    "noRecords": "Записи счетов не найдены."
+  },
+  "recurring": {
+    "title": "Периодические",
+    "billingHistory": "История счетов",
+    "processNow": "Обработать сейчас",
+    "newRecurring": "Новый периодический счёт",
+    "noInvoices": "Периодических счетов пока нет.",
+    "columnTitle": "Название",
+    "columnVehicle": "Транспортное средство",
+    "columnCustomer": "Клиент",
+    "columnFrequency": "Частота",
+    "columnNextRun": "Следующий запуск",
+    "columnAmount": "Сумма",
+    "columnStatus": "Статус",
+    "columnRuns": "Запусков",
+    "statusActive": "Активен",
+    "statusPaused": "Приостановлен",
+    "frequencyWeekly": "Еженедельно",
+    "frequencyBiweekly": "Раз в 2 недели",
+    "frequencyMonthly": "Ежемесячно",
+    "frequencyQuarterly": "Ежеквартально",
+    "frequencyYearly": "Ежегодно",
+    "serviceTypeMaintenance": "Техобслуживание",
+    "serviceTypeRepair": "Ремонт",
+    "serviceTypeInspection": "Осмотр",
+    "serviceTypeUpgrade": "Модернизация",
+    "dialogTitle": "Новый периодический счёт",
+    "titleLabel": "Название *",
+    "titlePlaceholder": "напр. Ежемесячная замена масла",
+    "vehicleLabel": "Транспортное средство *",
+    "selectVehicle": "Выберите транспортное средство",
+    "descriptionLabel": "Описание",
+    "frequencyLabel": "Частота",
+    "startDate": "Дата начала *",
+    "endDate": "Дата окончания",
+    "serviceType": "Тип обслуживания",
+    "baseCost": "Базовая стоимость",
+    "taxRate": "Ставка налога (%)",
+    "invoiceNotes": "Примечания к счёту",
+    "templateParts": "Запчасти по шаблону",
+    "addPart": "Добавить запчасть",
+    "partName": "Название запчасти",
+    "partNumber": "Арт. №",
+    "templateLabor": "Работы по шаблону",
+    "addLabor": "Добавить работу",
+    "description": "Описание",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "cancel": "Отмена",
+    "create": "Создать",
+    "requiredFields": "Название и транспортное средство обязательны",
+    "created": "Периодический счёт создан",
+    "failedCreate": "Не удалось создать",
+    "statusUpdated": "Статус обновлён",
+    "failedUpdate": "Не удалось обновить",
+    "deleted": "Периодический счёт удалён",
+    "failedDelete": "Не удалось удалить",
+    "processed": "Обработано счетов: {count}",
+    "failedProcess": "Не удалось обработать"
+  }
+}

+ 61 - 0
messages/ru/calendar.json

@@ -0,0 +1,61 @@
+{
+  "weekdays": {
+    "mon": "Пн",
+    "tue": "Вт",
+    "wed": "Ср",
+    "thu": "Чт",
+    "fri": "Пт",
+    "sat": "Сб",
+    "sun": "Вс"
+  },
+  "months": {
+    "january": "Январь",
+    "february": "Февраль",
+    "march": "Март",
+    "april": "Апрель",
+    "may": "Май",
+    "june": "Июнь",
+    "july": "Июль",
+    "august": "Август",
+    "september": "Сентябрь",
+    "october": "Октябрь",
+    "november": "Ноябрь",
+    "december": "Декабрь"
+  },
+  "stats": {
+    "services": "Обслуживания",
+    "overdue": "Просрочено",
+    "pendingQuotes": "Ожидающие предложения",
+    "revenue": "Доход"
+  },
+  "today": "Сегодня",
+  "workOrder": "Заказ-наряд",
+  "filters": {
+    "services": "Обслуживания",
+    "reminders": "Напоминания",
+    "quotes": "Предложения"
+  },
+  "selectVehicle": "Выберите транспортное средство для заказ-наряда",
+  "more": "+ ещё {count}",
+  "events": {
+    "status": {
+      "completed": "Завершено",
+      "inProgress": "В работе",
+      "waitingParts": "Ожидание запчастей",
+      "pending": "Ожидает",
+      "sent": "Отправлено",
+      "approved": "Одобрено",
+      "draft": "Черновик",
+      "done": "Готово",
+      "overdue": "Просрочено",
+      "upcoming": "Предстоящее"
+    },
+    "type": {
+      "service": "Обслуживание",
+      "reminder": "Напоминание",
+      "quote": "Предложение"
+    },
+    "noEvents": "Нет событий",
+    "selectDay": "Выберите день с событиями или создайте новый заказ-наряд."
+  }
+}

+ 43 - 0
messages/ru/common.json

@@ -0,0 +1,43 @@
+{
+  "brandName": "Torqvoice",
+  "buttons": {
+    "signIn": "Войти",
+    "save": "Сохранить",
+    "saveChanges": "Сохранить изменения",
+    "cancel": "Отмена",
+    "delete": "Удалить",
+    "submit": "Отправить",
+    "back": "Назад",
+    "confirm": "Подтвердить",
+    "edit": "Редактировать",
+    "add": "Добавить"
+  },
+  "form": {
+    "email": "Эл. почта",
+    "password": "Пароль",
+    "emailPlaceholder": "you@example.com",
+    "name": "Имя",
+    "phone": "Телефон",
+    "company": "Компания",
+    "address": "Адрес",
+    "notes": "Заметки"
+  },
+  "errors": {
+    "unexpected": "Произошла непредвиденная ошибка. Пожалуйста, попробуйте снова.",
+    "error": "Ошибка"
+  },
+  "terms": {
+    "termsOfService": "Условия использования"
+  },
+  "errorPage": {
+    "title": "Что-то пошло не так",
+    "description": "Двигатель неожиданно заглох. Наши механики уведомлены и уже работают над этим.",
+    "tryAgain": "Попробовать снова",
+    "backToDashboard": "Вернуться на главную"
+  },
+  "notFoundPage": {
+    "title": "Страница не найдена",
+    "description": "Похоже, эта деталь ещё в ремонте. Страница, которую вы ищете, не существует или была перемещена.",
+    "backToDashboard": "Вернуться на главную"
+  }
+}

+ 122 - 0
messages/ru/customers.json

@@ -0,0 +1,122 @@
+{
+  "list": {
+    "addCustomer": "Добавить клиента",
+    "searchPlaceholder": "Поиск клиентов...",
+    "table": {
+      "name": "Имя",
+      "company": "Компания",
+      "phone": "Телефон",
+      "email": "Эл. почта",
+      "vehicles": "Транспортные средства"
+    },
+    "emptySearch": "Нет клиентов, соответствующих поиску.",
+    "empty": "Клиентов пока нет.",
+    "deleteTitle": "Удалить клиента",
+    "deleteDescription": "Удалить {name}? Транспортные средства будут отвязаны.",
+    "deleteError": "Не удалось удалить клиента",
+    "error": "Не удалось загрузить клиентов",
+    "importCustomers": "Импорт",
+    "selectedCount": "{count} выбрано",
+    "clearSelection": "Очистить",
+    "batchDelete": "Удалить ({count})",
+    "batchDeleteTitle": "Удалить клиентов",
+    "batchDeleteDescription": "Удалить клиентов ({count})? Транспортные средства будут отвязаны.",
+    "batchDeleteSuccess": "Удалено клиентов: {count}"
+  },
+  "detail": {
+    "backToCustomers": "К списку клиентов",
+    "editCustomer": "Редактировать клиента",
+    "notFound": "Клиент не найден",
+    "emailCopied": "Эл. почта скопирована в буфер обмена",
+    "phoneCopied": "Номер телефона скопирован в буфер обмена",
+    "tabs": {
+      "vehicles": "Транспортные средства ({count})",
+      "messages": "Сообщения",
+      "requests": "Заявки ({count})"
+    },
+    "vehicleTable": {
+      "plate": "Госномер",
+      "vehicle": "Транспортное средство",
+      "mileage": "Пробег",
+      "services": "Обслуживания"
+    },
+    "noVehicles": "У этого клиента нет транспортных средств"
+  },
+  "form": {
+    "addTitle": "Добавить нового клиента",
+    "editTitle": "Редактировать клиента",
+    "nameRequired": "Имя *",
+    "namePlaceholder": "Иван Иванов",
+    "emailPlaceholder": "ivan@example.com",
+    "phonePlaceholder": "+7 (999) 123-45-67",
+    "companyPlaceholder": "Название компании",
+    "addressPlaceholder": "ул. Главная, 123, Москва",
+    "notesPlaceholder": "Дополнительные заметки...",
+    "customerUpdated": "Клиент обновлён",
+    "customerCreated": "Клиент создан",
+    "saveError": "Не удалось сохранить клиента"
+  },
+  "serviceRequests": {
+    "statusNew": "Новая",
+    "statusConverted": "Заказ-наряд создан",
+    "statusDismissed": "Отклонена",
+    "createWorkOrder": "Создать заказ-наряд",
+    "contactCustomer": "Связаться с клиентом",
+    "dismiss": "Отклонить",
+    "viewWorkOrder": "Просмотреть заказ-наряд",
+    "notes": "Заметки",
+    "hideNotes": "Скрыть заметки",
+    "staffNotes": "Заметки персонала:",
+    "notesPlaceholder": "Добавить внутренние заметки...",
+    "saveNotes": "Сохранить заметки",
+    "submitted": "Отправлено {date}",
+    "preferred": "Предпочтительно: {date}",
+    "workOrderCreated": "Заказ-наряд создан",
+    "workOrderError": "Не удалось создать заказ-наряд",
+    "requestDismissed": "Заявка отклонена",
+    "dismissError": "Не удалось отклонить заявку",
+    "notesSaved": "Заметки сохранены",
+    "notesSaveError": "Не удалось сохранить заметки",
+    "smsLabel": "Сообщение по заявке",
+    "smsDefault": "Здравствуйте, {name}, по поводу вашей заявки на обслуживание вашего {vehicle}: "
+  },
+  "import": {
+    "title": "Импорт клиентов",
+    "description": "Импортируйте клиентов из файлов CSV, TSV или vCard.",
+    "dropFile": "Перетащите файл сюда",
+    "supportedFormats": "CSV, TSV, TXT или vCard (.vcf)",
+    "browse": "Обзор файлов",
+    "noDataFound": "Данные клиентов в файле не найдены",
+    "rowsFound": "Найдено клиентов: {count}",
+    "colName": "Имя",
+    "colEmail": "Эл. почта",
+    "colPhone": "Телефон",
+    "colCompany": "Компания",
+    "colAddress": "Адрес",
+    "chooseAnother": "Выбрать другой файл",
+    "cancel": "Отмена",
+    "importButton": "Импортировать клиентов: {count}",
+    "importComplete": "Импорт завершён",
+    "importedCount": "Успешно импортировано клиентов: {count}",
+    "skippedRows": "Пропущено строк из-за ошибок: {count}",
+    "rowError": "Строка {row} ({name}): {error}",
+    "done": "Готово",
+    "importFailed": "Не удалось импортировать клиентов",
+    "checkingDuplicates": "Проверка дубликатов...",
+    "duplicatesFound": "Найдено дубликатов: {count}",
+    "duplicatesBanner": "Совпадают с существующими клиентами: {count}.",
+    "duplicatesHint": "Нажмите «Пропустить» или «Объединить», чтобы изменить обработку каждого дубликата.",
+    "colStatus": "Статус",
+    "actionSkip": "Пропустить",
+    "actionMerge": "Объединить",
+    "actionNew": "Новый",
+    "summaryNew": "{count} новых",
+    "summaryMerge": "{count} объединённых",
+    "summarySkip": "{count} пропущенных",
+    "skipAll": "Пропустить все",
+    "mergeAll": "Объединить все",
+    "resultCreated": "Создано клиентов: {count}",
+    "resultMerged": "Объединено клиентов: {count}",
+    "resultSkipped": "Пропущено клиентов: {count}"
+  }
+}

+ 104 - 0
messages/ru/dashboard.json

@@ -0,0 +1,104 @@
+{
+  "recentActivity": "Последняя активность",
+  "noRecentActivity": "Нет недавней активности",
+  "unknownUser": "Пользователь",
+  "stats": {
+    "activeJobs": "Активные работы",
+    "pending": "Ожидающие",
+    "todaysRevenue": "Выручка за сегодня",
+    "totalCustomers": "Всего клиентов"
+  },
+  "customize": "Настроить",
+  "viewAll": "Показать все",
+  "showCards": "Показать карточки",
+  "cards": {
+    "maintenance": "Плановое ТО",
+    "reminders": "Предстоящие напоминания",
+    "inspections": "Осмотры",
+    "quoteRequests": "Запросы на предложения",
+    "quoteResponses": "Ответы на предложения",
+    "sms": "Последние сообщения",
+    "notifications": "Последние уведомления",
+    "recentCompleted": "Недавно завершённые",
+    "activeJobs": "Активные работы"
+  },
+  "maintenance": {
+    "title": "Прогнозируемое техобслуживание",
+    "description": "На основе истории обслуживания и расчётного ежедневного пробега",
+    "overdue": "Просрочено",
+    "approaching": "Приближается",
+    "estimated": "Прим. {mileage} {unit}",
+    "sinceLastService": "{mileage} {unit} с последнего обслуживания",
+    "certainty": "{percent}% уверенность",
+    "active": "Активные",
+    "dismissedTab": "Отклонённые",
+    "noActive": "Нет транспортных средств, требующих обслуживания.",
+    "dismissedOn": "Отклонено {date}",
+    "restore": "Восстановить"
+  },
+  "reminders": {
+    "title": "Предстоящие напоминания",
+    "overdue": "Просрочено",
+    "dueSoon": "Скоро срок",
+    "noData": "Нет предстоящих напоминаний."
+  },
+  "messages": {
+    "title": "Последние сообщения",
+    "viewAll": "Показать все",
+    "you": "Вы: ",
+    "noData": "Нет недавних сообщений."
+  },
+  "notifications": {
+    "title": "Последние уведомления",
+    "noData": "Нет недавних уведомлений."
+  },
+  "inspections": {
+    "title": "Осмотры",
+    "inProgress": "В процессе",
+    "completed": "Завершено",
+    "itemCount": "{inspected}/{total} пунктов",
+    "noData": "Нет осмотров."
+  },
+  "quoteRequests": {
+    "title": "Запросы на предложения",
+    "description": "Клиенты, запрашивающие предложения из общих осмотров",
+    "pending": "Ожидающие",
+    "createQuote": "Создать предложение",
+    "itemsRequested": "{count, plural, one {# пункт запрошен} few {# пункта запрошено} other {# пунктов запрошено}}",
+    "noData": "Нет ожидающих запросов на предложения."
+  },
+  "quoteResponses": {
+    "title": "Ответы клиентов на предложения",
+    "description": "Предложения, которые клиенты приняли или запросили изменения",
+    "accepted": "Принято",
+    "changesRequested": "Запрошены изменения",
+    "convertToWorkOrder": "Преобразовать в заказ-наряд",
+    "viewQuote": "Просмотреть предложение",
+    "noData": "Нет ответов от клиентов."
+  },
+  "recentCompleted": {
+    "title": "Недавно завершённые",
+    "date": "Дата",
+    "vehicle": "Транспортное средство",
+    "customer": "Клиент",
+    "serviceTitle": "Название",
+    "total": "Итого",
+    "empty": "Пока нет завершённых работ"
+  },
+  "activeJobsTable": {
+    "title": "Активные работы",
+    "vehicle": "Транспортное средство",
+    "customer": "Клиент",
+    "serviceTitle": "Название",
+    "status": "Статус",
+    "tech": "Механик",
+    "empty": "Нет активных работ"
+  },
+  "relativeTime": {
+    "now": "сейчас",
+    "minutes": "{count}м",
+    "hours": "{count}ч",
+    "days": "{count}д"
+  },
+  "error": "Не удалось загрузить панель управления"
+}

+ 87 - 0
messages/ru/inventory.json

@@ -0,0 +1,87 @@
+{
+  "searchPlaceholder": "Поиск запчастей...",
+  "categoryPlaceholder": "Категория",
+  "allCategories": "Все категории",
+  "applyMarkup": "Применить наценку",
+  "addPart": "Добавить запчасть",
+  "table": {
+    "partNumber": "Арт. №",
+    "name": "Название",
+    "category": "Категория",
+    "inStock": "В наличии",
+    "unitCost": "Себестоимость",
+    "sellPrice": "Цена продажи",
+    "supplier": "Поставщик",
+    "location": "Расположение",
+    "low": "Мало"
+  },
+  "empty": {
+    "noMatch": "Нет запчастей, соответствующих поиску.",
+    "noParts": "На складе пока нет запчастей."
+  },
+  "actions": {
+    "edit": "Редактировать",
+    "delete": "Удалить"
+  },
+  "deletePart": {
+    "title": "Удалить запчасть",
+    "description": "Удалить \"{name}\" со склада? Это действие необратимо.",
+    "confirm": "Удалить"
+  },
+  "markup": {
+    "title": "Применить наценку ко всем запчастям",
+    "multiplierLabel": "Множитель наценки",
+    "description": "Цена продажи будет установлена как себестоимость x {multiplier} для всех {count} запчастей.",
+    "percentage": "(наценка {percent}%)",
+    "cancel": "Отмена",
+    "apply": "Применить"
+  },
+  "errors": {
+    "multiplierPositive": "Множитель должен быть больше 0",
+    "deleteFailed": "Не удалось удалить запчасть",
+    "applyFailed": "Не удалось применить наценку",
+    "saveFailed": "Не удалось сохранить запчасть",
+    "error": "Ошибка"
+  },
+  "form": {
+    "editPart": "Редактировать запчасть",
+    "addNewPart": "Добавить новую запчасть",
+    "supplierLink": "Ссылка на поставщика",
+    "supplierLinkPlaceholder": "https://supplier.com/product/...",
+    "fetch": "Загрузить",
+    "fetchFailed": "Загрузка не удалась",
+    "fetchError": "Не удалось получить метаданные по URL",
+    "image": "Изображение",
+    "dropOrClick": "Перетащите или нажмите",
+    "replace": "Заменить",
+    "upload": "Загрузить",
+    "imageUploading": "Загрузка изображения...",
+    "imageUploaded": "Изображение загружено",
+    "imageUploadFailed": "Не удалось загрузить изображение",
+    "imageTypeError": "Допускаются только изображения JPEG, PNG, WebP и AVIF",
+    "imageSizeError": "Изображение должно быть менее 5МБ",
+    "partNumber": "Артикул",
+    "partNumberPlaceholder": "BRK-001",
+    "nameLabel": "Название *",
+    "namePlaceholder": "Комплект тормозных колодок",
+    "category": "Категория",
+    "categoryPlaceholder": "Тормоза",
+    "location": "Расположение",
+    "locationPlaceholder": "Полка А-3",
+    "quantity": "Количество",
+    "minQty": "Мин. кол-во",
+    "unitCost": "Себестоимость",
+    "sellPrice": "Цена продажи",
+    "supplier": "Поставщик",
+    "supplierPlaceholder": "ЗапчастиПро",
+    "supplierPhone": "Телефон поставщика",
+    "supplierPhonePlaceholder": "+7 (999) 123-45-67",
+    "supplierEmail": "Эл. почта поставщика",
+    "supplierEmailPlaceholder": "orders@autoparts.ru",
+    "description": "Описание",
+    "descriptionPlaceholder": "Описание запчасти...",
+    "cancel": "Отмена",
+    "saveChanges": "Сохранить изменения",
+    "addPart": "Добавить запчасть"
+  }
+}

+ 45 - 0
messages/ru/messages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "SMS-сообщения",
+    "featureDescription": "Отправляйте и получайте SMS-сообщения с клиентами прямо из Torqvoice."
+  },
+  "notConfigured": {
+    "title": "SMS не настроены",
+    "description": "Настройте SMS-провайдера для отправки и получения сообщений с клиентами. Поддерживаются Twilio, Vonage и Telnyx.",
+    "goToSettings": "Перейти к настройкам SMS"
+  },
+  "threads": {
+    "title": "Беседы",
+    "empty": "Бесед пока нет. Отправьте SMS со страницы клиента, чтобы начать.",
+    "loadMore": "Загрузить ещё",
+    "selectConversation": "Выберите беседу",
+    "you": "Вы: ",
+    "deleteConversation": "Удалить беседу",
+    "deleteTitle": "Удалить беседу?",
+    "deleteDescription": "Все сообщения с {name} будут безвозвратно удалены. Это действие необратимо.",
+    "deleted": "Беседа удалена",
+    "deleteError": "Не удалось удалить беседу"
+  },
+  "conversation": {
+    "noPhone": "У {name} не указан номер телефона. Добавьте номер для начала переписки.",
+    "loadOlder": "Загрузить ранние",
+    "empty": "Сообщений пока нет. Отправьте первое сообщение {name}.",
+    "placeholder": "Сообщение для {name}...",
+    "deleteMessage": "Удалить сообщение",
+    "deleteTitle": "Удалить сообщение?",
+    "deleteDescription": "Это сообщение будет безвозвратно удалено из беседы. Это действие необратимо.",
+    "messageDeleted": "Сообщение удалено",
+    "deleteError": "Не удалось удалить сообщение",
+    "sendError": "Не удалось отправить SMS"
+  },
+  "sendDialog": {
+    "send": "Отправить {label}",
+    "message": "Сообщение",
+    "placeholder": "Введите сообщение...",
+    "characters": "{count} / 1600 символов",
+    "sendSms": "Отправить SMS",
+    "smsSent": "SMS отправлено",
+    "sentTo": "{label} отправлено {name} ({phone})",
+    "sendError": "Не удалось отправить SMS"
+  }
+}

+ 92 - 0
messages/ru/navigation.json

@@ -0,0 +1,92 @@
+{
+  "breadcrumbs": {
+    "dashboard": "Панель управления",
+    "vehicles": "Транспортные средства",
+    "allVehicles": "Все транспортные средства",
+    "vehicleDetails": "Детали транспортного средства",
+    "customers": "Клиенты",
+    "allCustomers": "Все клиенты",
+    "customerDetails": "Данные клиента",
+    "workOrders": "Заказ-наряды",
+    "allWorkOrders": "Все заказ-наряды",
+    "quotes": "Коммерческие предложения",
+    "allQuotes": "Все предложения",
+    "newQuote": "Новое предложение",
+    "quoteDetails": "Детали предложения",
+    "edit": "Редактировать",
+    "billing": "Счета",
+    "billingHistory": "История счетов",
+    "inventory": "Склад",
+    "allParts": "Все запчасти",
+    "reports": "Отчёты",
+    "workBoard": "Доска работ",
+    "presenter": "Презентация",
+    "adminOverview": "Обзор администратора",
+    "admin": "Администратор",
+    "users": "Пользователи",
+    "organizations": "Организации",
+    "settings": "Настройки",
+    "company": "Компания",
+    "account": "Аккаунт",
+    "customFields": "Пользовательские поля",
+    "templates": "Шаблоны",
+    "team": "Команда",
+    "invoice": "Счёт",
+    "payment": "Оплата",
+    "currency": "Валюта",
+    "workshop": "Мастерская",
+    "appearance": "Внешний вид",
+    "email": "Эл. почта",
+    "sms": "SMS",
+    "about": "О программе",
+    "home": "Главная",
+    "newServiceRecord": "Новая запись обслуживания",
+    "serviceDetails": "Детали обслуживания",
+    "data": "Данные",
+    "license": "Лицензия",
+    "subscription": "Подписка",
+    "maintenance": "Техобслуживание",
+    "customerPortal": "Портал клиента",
+    "reminders": "Напоминания",
+    "allReminders": "Все напоминания",
+    "auditLog": "Журнал аудита",
+    "aiAssistant": "ИИ-ассистент"
+  },
+  "search": "Поиск...",
+  "shortcut": "Ctrl+K",
+  "newWorkOrder": "Новый заказ-наряд",
+  "purchaseWhiteLabel": "Купить White-Label",
+  "sidebar": {
+    "dashboard": "Панель управления",
+    "clients": "Клиенты",
+    "customers": "Клиенты",
+    "messages": "Сообщения",
+    "workshop": "Мастерская",
+    "vehicles": "Транспортные средства",
+    "reminders": "Напоминания",
+    "workOrders": "Заказ-наряды",
+    "inspections": "Осмотры",
+    "calendar": "Календарь",
+    "workBoard": "Доска работ",
+    "business": "Бизнес",
+    "quotes": "Предложения",
+    "billing": "Счета",
+    "inventory": "Склад",
+    "reports": "Отчёты",
+    "settings": "Настройки",
+    "superAdmin": "Суперадмин",
+    "adminPanel": "Панель администратора",
+    "organizations": "Организации",
+    "noOrganization": "Нет организации",
+    "addNewCompany": "Добавить новую компанию",
+    "companyName": "Название компании",
+    "companyPlaceholder": "напр. Автосервис Ивана",
+    "cancel": "Отмена",
+    "createCompany": "Создать компанию",
+    "lightMode": "Светлая тема",
+    "darkMode": "Тёмная тема",
+    "signOut": "Выйти",
+    "auditLog": "Журнал аудита",
+    "aiAssistant": "ИИ-ассистент"
+  }
+}

+ 10 - 0
messages/ru/notifications.json

@@ -0,0 +1,10 @@
+{
+  "panel": {
+    "title": "Уведомления",
+    "markAllRead": "Прочитать все",
+    "empty": "Уведомлений пока нет",
+    "confirmDelete": "Вы уверены?",
+    "delete": "Удалить",
+    "cancel": "Отмена"
+  }
+}

+ 103 - 0
messages/ru/pdf.json

@@ -0,0 +1,103 @@
+{
+  "invoice": {
+    "title": "СЧЁТ",
+    "due": "Срок: {date}",
+    "billTo": "Плательщик",
+    "vehicle": "Транспортное средство",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "mileage": "Пробег: {mileage}",
+    "km": "км",
+    "mi": "ми",
+    "service": "Обслуживание",
+    "type": "Тип: {type}",
+    "tech": "Механик: {tech}",
+    "parts": "Запчасти",
+    "partNumber": "Арт. №",
+    "description": "Описание",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "labor": "Работа",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "ratePerHour": "{rate}/ч",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountPercent": "Скидка ({percent}%)",
+    "tax": "Налог ({rate}%)",
+    "paymentsReceived": "Полученные платежи",
+    "paidInFull": "ПОЛНОСТЬЮ ОПЛАЧЕНО",
+    "amountDue": "К оплате",
+    "notes": "Заметки",
+    "bankAccount": "Банковский счёт",
+    "paymentInformation": "Платёжная информация",
+    "orgNumberLabel": "Номер организации",
+    "paymentTermsLabel": "Условия оплаты",
+    "dueDateLabel": "Срок оплаты",
+    "diagnosticNotes": "Диагностические заметки",
+    "attachedDocuments": "Прикреплённые документы",
+    "seeAppendedPages": "{name} (см. приложенные страницы)",
+    "serviceImages": "Фотографии обслуживания",
+    "paymentTerms": "Условия оплаты: {terms}",
+    "viewPortal": "Ваш портал: {url}",
+    "professionalWorkshop": "Профессиональное управление мастерской",
+    "tel": "Тел: {phone}",
+    "org": "Орг: {org}"
+  },
+  "quote": {
+    "title": "КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ",
+    "to": "Кому",
+    "vehicle": "Транспортное средство",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "quoteDetails": "Детали предложения",
+    "parts": "Запчасти",
+    "partNumber": "Арт. №",
+    "description": "Описание",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "labor": "Работа",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "ratePerHour": "{rate}/ч",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountPercent": "Скидка ({percent}%)",
+    "tax": "Налог ({rate}%)",
+    "validUntil": "Действительно до: {date}",
+    "attachedDocuments": "Прикреплённые документы",
+    "attached": "{name} (прикреплено)",
+    "quoteImages": "Изображения предложения",
+    "validityFooterUntil": "Это предложение действительно до {date}",
+    "validityFooter30": "Это предложение действительно 30 дней",
+    "viewPortal": "Ваш портал: {url}",
+    "tel": "Тел: {phone}"
+  },
+  "inspection": {
+    "title": "ОСМОТР ТРАНСПОРТНОГО СРЕДСТВА",
+    "vehicle": "Транспортное средство",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "mileage": "Пробег: {mileage}",
+    "customer": "Клиент",
+    "inspectionDetails": "Детали осмотра",
+    "statusCompleted": "Завершён",
+    "statusInProgress": "В процессе",
+    "status": "Статус: {status}",
+    "inspected": "Осмотрено",
+    "pass": "Норма",
+    "attention": "Внимание",
+    "fail": "Неисправность",
+    "item": "Пункт",
+    "statusColumn": "Статус",
+    "notesColumn": "Заметки",
+    "footerText": "Осмотр транспортного средства — {shopName}",
+    "viewPortal": "Ваш портал: {url}",
+    "tel": "Тел: {phone}"
+  },
+  "common": {
+    "poweredBy": "Работает на"
+  }
+}

+ 133 - 0
messages/ru/portal.json

@@ -0,0 +1,133 @@
+{
+  "login": {
+    "signInTitle": "Войдите в портал клиента",
+    "emailLabel": "Адрес эл. почты",
+    "emailPlaceholder": "you@example.com",
+    "sendLink": "Отправить ссылку для входа",
+    "checkEmail": "Проверьте почту",
+    "checkEmailDescription": "Если аккаунт с таким адресом существует, мы отправили ссылку для входа. Ссылка действительна 15 минут.",
+    "tryDifferentEmail": "Попробовать другой адрес",
+    "notFound": "Не найдено",
+    "portalNotExist": "Этот портал не существует.",
+    "portalNotAvailable": "Портал недоступен",
+    "portalNotEnabled": "Портал клиента не включён для этой организации."
+  },
+  "nav": {
+    "dashboard": "Панель управления",
+    "vehicles": "Транспортные средства",
+    "invoices": "Счета",
+    "quotes": "Предложения",
+    "inspections": "Осмотры",
+    "requestService": "Заявка на обслуживание"
+  },
+  "header": {
+    "signOut": "Выйти"
+  },
+  "shell": {
+    "notFound": "Не найдено",
+    "portalNotExist": "Этот портал не существует.",
+    "portalNotAvailable": "Портал недоступен",
+    "portalNotEnabled": "Портал клиента не включён для этой организации."
+  },
+  "dashboard": {
+    "welcome": "Добро пожаловать, {name}",
+    "overview": "Вот обзор вашего аккаунта.",
+    "vehicles": "Транспортные средства",
+    "openInvoices": "Открытые счета",
+    "pendingQuotes": "Ожидающие предложения",
+    "recentInvoices": "Последние счета",
+    "recentQuotes": "Последние предложения",
+    "pendingServiceRequests": "Ожидающие заявки на обслуживание",
+    "viewAll": "Показать все",
+    "newRequest": "Новая заявка",
+    "noInvoicesYet": "Счетов пока нет.",
+    "noQuotesYet": "Предложений пока нет.",
+    "view": "Просмотр",
+    "pending": "Ожидает",
+    "failedToLoad": "Не удалось загрузить панель управления."
+  },
+  "invoices": {
+    "title": "Счета",
+    "description": "Все ваши счета за обслуживание.",
+    "noInvoices": "Счетов пока нет.",
+    "failedToLoad": "Не удалось загрузить счета.",
+    "invoice": "Счёт",
+    "vehicle": "Транспортное средство",
+    "date": "Дата",
+    "amount": "Сумма",
+    "payment": "Оплата",
+    "view": "Просмотр",
+    "paid": "оплачено",
+    "partial": "частично",
+    "unpaid": "не оплачено"
+  },
+  "quotes": {
+    "title": "Предложения",
+    "description": "Предложения и сметы для ваших транспортных средств.",
+    "noQuotes": "Предложений пока нет.",
+    "failedToLoad": "Не удалось загрузить предложения.",
+    "quote": "Предложение",
+    "vehicle": "Транспортное средство",
+    "status": "Статус",
+    "amount": "Сумма",
+    "validUntil": "Действительно до",
+    "view": "Просмотр"
+  },
+  "inspections": {
+    "title": "Осмотры",
+    "description": "Отчёты осмотров транспортных средств.",
+    "noInspections": "Осмотров пока нет.",
+    "failedToLoad": "Не удалось загрузить осмотры.",
+    "vehicle": "Транспортное средство",
+    "template": "Шаблон",
+    "status": "Статус",
+    "date": "Дата",
+    "summary": "Итоги",
+    "view": "Просмотр",
+    "good": "{count} норма",
+    "fair": "{count} удовл.",
+    "poor": "{count} плохо"
+  },
+  "vehicles": {
+    "title": "Транспортные средства",
+    "description": "Ваши зарегистрированные транспортные средства.",
+    "noVehicles": "Транспортные средства не найдены.",
+    "failedToLoad": "Не удалось загрузить транспортные средства.",
+    "plate": "Госномер: {plate}",
+    "vin": "VIN: {vin}",
+    "serviceCount": "{count, plural, one {# обслуживание} few {# обслуживания} other {# обслуживаний}}",
+    "inspectionCount": "{count, plural, one {# осмотр} few {# осмотра} other {# осмотров}}",
+    "serviceHistory": "История обслуживания ({count})",
+    "inspectionsTab": "Осмотры ({count})",
+    "noServiceRecords": "Записей обслуживания пока нет.",
+    "noInspections": "Осмотров пока нет.",
+    "vehicleNotFound": "Транспортное средство не найдено.",
+    "completed": "Завершено {date}",
+    "good": "{count} норма",
+    "fair": "{count} удовл.",
+    "poor": "{count} плохо",
+    "view": "Просмотр"
+  },
+  "requestService": {
+    "title": "Заявка на обслуживание",
+    "description": "Отправьте заявку на обслуживание одного из ваших транспортных средств.",
+    "newServiceRequest": "Новая заявка на обслуживание",
+    "formDescription": "Опишите проблему, и мы свяжемся с вами.",
+    "vehicleLabel": "Транспортное средство",
+    "vehiclePlaceholder": "Выберите транспортное средство",
+    "descriptionLabel": "Опишите проблему",
+    "descriptionPlaceholder": "Что случилось с вашим транспортным средством?",
+    "preferredDate": "Предпочтительная дата",
+    "preferredDateOptional": "(необязательно)",
+    "submitRequest": "Отправить заявку",
+    "selectVehicle": "Пожалуйста, выберите транспортное средство",
+    "describeIssue": "Пожалуйста, опишите проблему",
+    "submitSuccess": "Заявка на обслуживание отправлена",
+    "submitError": "Не удалось отправить заявку",
+    "noVehicles": "У вас нет зарегистрированных транспортных средств. Свяжитесь с нами для добавления транспортного средства.",
+    "yourRequests": "Ваши заявки на обслуживание",
+    "failedToLoad": "Не удалось загрузить данные.",
+    "staffNotes": "Заметки персонала: {notes}",
+    "preferred": "Предпочтительно: {date}"
+  }
+}

+ 186 - 0
messages/ru/quotes.json

@@ -0,0 +1,186 @@
+{
+  "list": {
+    "statusAll": "Все",
+    "statusDraft": "Черновик",
+    "statusSent": "Отправлено",
+    "statusAccepted": "Принято",
+    "statusRejected": "Отклонено",
+    "statusConverted": "Конвертировано",
+    "searchPlaceholder": "Поиск предложений...",
+    "newQuote": "Новое предложение",
+    "columnQuoteNumber": "Предложение №",
+    "columnTitle": "Название",
+    "columnCustomer": "Клиент",
+    "columnVehicle": "Транспортное средство",
+    "columnStatus": "Статус",
+    "columnDate": "Дата",
+    "columnTotal": "Итого",
+    "noQuotes": "Предложения не найдены."
+  },
+  "page": {
+    "quote": "Предложение",
+    "save": "Сохранить",
+    "pdf": "PDF",
+    "email": "Эл. почта",
+    "share": "Поделиться",
+    "delete": "Удалить",
+    "deleteTitle": "Удалить предложение",
+    "deleteDescription": "Это предложение будет безвозвратно удалено.",
+    "saved": "Предложение сохранено",
+    "unsavedChanges": "Несохранённые изменения",
+    "saveFirst": "Сначала сохраните изменения",
+    "failedSave": "Не удалось сохранить предложение",
+    "failedDelete": "Не удалось удалить",
+    "failedPdf": "Не удалось сгенерировать PDF",
+    "selectVehicle": "Выберите транспортное средство",
+    "convertTitle": "Конвертировать предложение в заказ-наряд",
+    "convertDescription": "Будет создана новая запись обслуживания (заказ-наряд) из этого предложения. Выберите транспортное средство для заказ-наряда.",
+    "convert": "Конвертировать",
+    "convertToWorkOrder": "Конвертировать в заказ-наряд",
+    "cancel": "Отмена",
+    "failedConvert": "Не удалось конвертировать",
+    "changesRequested": "Запрошены изменения",
+    "quoteAccepted": "Предложение принято",
+    "markResolved": "Отметить как решённое",
+    "responseResolved": "Ответ клиента обработан",
+    "entityLabel": "Предложение",
+    "tabs": {
+      "details": "Детали",
+      "images": "Изображения",
+      "documents": "Документы"
+    }
+  },
+  "form": {
+    "newQuote": "Новое предложение",
+    "editQuote": "Редактировать предложение",
+    "cancel": "Отмена",
+    "createQuote": "Создать предложение",
+    "updateQuote": "Обновить предложение",
+    "created": "Предложение создано",
+    "updated": "Предложение обновлено",
+    "failedCreate": "Не удалось создать предложение",
+    "failedUpdate": "Не удалось обновить предложение"
+  },
+  "parts": {
+    "title": "Запчасти",
+    "addPart": "Добавить запчасть",
+    "partNumber": "Арт. №",
+    "name": "Название",
+    "namePlaceholder": "Название *",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "subtotal": "Подитог запчастей: {amount}"
+  },
+  "labor": {
+    "title": "Работа",
+    "addLabor": "Добавить работу",
+    "description": "Описание",
+    "descriptionPlaceholder": "Описание *",
+    "hours": "Часы",
+    "rate": "Ставка ({currency}/ч)",
+    "total": "Итого",
+    "subtotal": "Подитог работ: {amount}"
+  },
+  "notes": {
+    "title": "Заметки",
+    "public": "Публичные",
+    "internal": "Внутренние",
+    "publicPlaceholder": "Описание предложения, видимое клиенту...",
+    "internalPlaceholder": "Внутренние заметки (не видны клиенту)...",
+    "publicHelper": "Отображается в документе предложения",
+    "internalHelper": "Видно только вашей команде"
+  },
+  "sidebar": {
+    "sharedLink": {
+      "title": "Общая ссылка",
+      "sharedOn": "Опубликовано {date}",
+      "notViewed": "Ещё не просмотрено",
+      "viewedTimes": "Просмотрено {count, plural, one {# раз} few {# раза} other {# раз}}",
+      "lastViewed": "Последний раз {date}",
+      "copyLink": "Копировать ссылку",
+      "linkCopied": "Ссылка скопирована"
+    }
+  },
+  "details": {
+    "title": "Детали предложения",
+    "titleLabel": "Название *",
+    "titlePlaceholder": "Смета на ремонт автомобиля",
+    "status": "Статус",
+    "statusDraft": "Черновик",
+    "statusSent": "Отправлено",
+    "statusAccepted": "Принято",
+    "statusRejected": "Отклонено",
+    "validUntil": "Действительно до",
+    "viewInspection": "Просмотреть связанный осмотр",
+    "vehicle": "Транспортное средство",
+    "customer": "Клиент",
+    "selectVehicle": "Выберите транспортное средство...",
+    "selectCustomer": "Выберите клиента...",
+    "none": "Нет"
+  },
+  "totals": {
+    "title": "Итоги",
+    "parts": "Запчасти",
+    "labor": "Работа",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountNone": "Нет",
+    "discountPercentage": "Процент",
+    "discountFixed": "Фиксированная",
+    "tax": "Налог",
+    "total": "Итого"
+  },
+  "shareDialog": {
+    "title": "Поделиться предложением",
+    "description": "Создайте публичную ссылку, по которой любой сможет просмотреть и скачать это предложение без входа в систему.",
+    "notifyTitle": "Уведомить {name}",
+    "email": "Эл. почта",
+    "noEmail": "(эл. почта не указана)",
+    "sms": "SMS",
+    "noPhone": "(телефон не указан)",
+    "sendNotification": "Отправить уведомление",
+    "revokeLink": "Отозвать ссылку",
+    "generateLink": "Создать публичную ссылку",
+    "emailSent": "Письмо отправлено",
+    "failedEmail": "Не удалось отправить письмо",
+    "smsSent": "SMS отправлено",
+    "failedSms": "Не удалось отправить SMS"
+  },
+  "images": {
+    "title": "Изображения предложения",
+    "dropzone": "Перетащите изображения сюда или нажмите для выбора",
+    "uploading": "Загрузка...",
+    "formats": "JPG, PNG, WebP — макс. 10МБ каждое",
+    "uploadingImages": "Загрузка изображений...",
+    "limitReached": "Лимит изображений достигнут ({count}/{max})",
+    "upgradePrompt": "Обновите тариф для загрузки дополнительных изображений.",
+    "onlyAllowed": "Допускаются только изображения JPG, PNG и WebP. {names} пропущены.",
+    "onlyUploading": "Загружается только {count} изображений для соблюдения лимита.",
+    "uploadingCount": "Загрузка изображений: {count}...",
+    "uploadedCount": "Загружено изображений: {count}",
+    "uploadFailed": "Загрузка не удалась",
+    "deleted": "Изображение удалено",
+    "failedDelete": "Не удалось удалить изображение",
+    "failedUpdate": "Не удалось обновить",
+    "description": "Описание...",
+    "pdf": "PDF"
+  },
+  "documents": {
+    "title": "Документы",
+    "dropzone": "Перетащите файлы сюда или нажмите для выбора",
+    "uploading": "Загрузка...",
+    "formats": "PDF, CSV, TXT — макс. 10МБ каждый",
+    "uploadingDocuments": "Загрузка документов...",
+    "limitReached": "Лимит документов достигнут ({count}/{max})",
+    "upgradePrompt": "Обновите тариф для загрузки дополнительных файлов.",
+    "onlyUploading": "Загружается только {count} файлов для соблюдения лимита.",
+    "uploadingCount": "Загрузка файлов: {count}...",
+    "uploadedCount": "Загружено файлов: {count}",
+    "uploadFailed": "Загрузка не удалась",
+    "deleted": "Файл удалён",
+    "failedDelete": "Не удалось удалить файл",
+    "failedUpdate": "Не удалось обновить",
+    "pdf": "PDF"
+  }
+}

+ 11 - 0
messages/ru/reminders.json

@@ -0,0 +1,11 @@
+{
+  "error": "Не удалось загрузить напоминания",
+  "showing": "Показано {count} из {total}",
+  "deleted": "Напоминание удалено",
+  "deleteError": "Не удалось удалить напоминание",
+  "vehicle": "Транспортное средство",
+  "selectVehicle": "Выберите транспортное средство...",
+  "searchVehicle": "Поиск транспортных средств...",
+  "noVehicle": "Транспортные средства не найдены.",
+  "pickDate": "Выберите дату"
+}

+ 199 - 0
messages/ru/reports.json

@@ -0,0 +1,199 @@
+{
+  "title": "Отчёты и аналитика",
+  "tabs": {
+    "revenue": "Доход",
+    "services": "Обслуживания",
+    "customers": "Клиенты",
+    "inventory": "Склад",
+    "technicians": "Механики",
+    "parts": "Запчасти",
+    "jobAnalytics": "Аналитика работ",
+    "retention": "Удержание",
+    "tax": "Налоги"
+  },
+  "dateRange": {
+    "to": "до"
+  },
+  "actions": {
+    "refresh": "Обновить",
+    "csv": "CSV"
+  },
+  "revenue": {
+    "revenue": "Доход",
+    "collected": "Собрано",
+    "outstanding": "Задолженность",
+    "services": "Обслуживания",
+    "monthlyRevenue": "Ежемесячный доход",
+    "revenueByType": "Доход по типам",
+    "monthlyBreakdown": "Помесячная разбивка",
+    "tableHeaders": {
+      "month": "Месяц",
+      "revenue": "Доход",
+      "collected": "Собрано",
+      "count": "Количество"
+    }
+  },
+  "services": {
+    "totalServices": "Всего обслуживаний",
+    "byStatus": "По статусу",
+    "byType": "По типу"
+  },
+  "customers": {
+    "total": "Всего",
+    "active": "Активные",
+    "topCustomersBySpend": "Топ клиентов по затратам",
+    "topCustomers": "Топ клиентов",
+    "tableHeaders": {
+      "name": "Имя",
+      "company": "Компания",
+      "services": "Обслуживания",
+      "totalSpent": "Всего потрачено"
+    }
+  },
+  "inventory": {
+    "parts": "Запчасти",
+    "items": "Позиций",
+    "costValue": "Стоимость закупки",
+    "sellValue": "Стоимость продажи",
+    "potentialMargin": "Потенциальная маржа",
+    "lowStock": "Мало на складе",
+    "tableHeaders": {
+      "name": "Название",
+      "partNumber": "Арт. №",
+      "qty": "Кол-во",
+      "minQty": "Мин. кол-во"
+    }
+  },
+  "technicians": {
+    "totalJobs": "Всего работ",
+    "totalRevenue": "Общий доход",
+    "revenueByTechnician": "Доход по механикам",
+    "technicianBreakdown": "Разбивка по механикам",
+    "tableHeaders": {
+      "technician": "Механик",
+      "jobs": "Работы",
+      "revenue": "Доход",
+      "avgRevenue": "Средний доход",
+      "totalHours": "Всего часов",
+      "avgHours": "Среднее часов"
+    }
+  },
+  "parts": {
+    "partsUsed": "Использовано запчастей",
+    "partsRevenue": "Доход от запчастей",
+    "topPartsByUsage": "Топ запчастей по использованию",
+    "partsUsage": "Использование запчастей",
+    "tableHeaders": {
+      "partName": "Название запчасти",
+      "partNumber": "Арт. №",
+      "usageCount": "Раз использовано",
+      "totalQty": "Общее кол-во",
+      "revenue": "Доход"
+    }
+  },
+  "jobAnalytics": {
+    "totalJobs": "Всего работ",
+    "avgJobValue": "Средняя стоимость работы",
+    "jobsByDayOfWeek": "Работы по дням недели",
+    "serviceTypes": "Типы обслуживания",
+    "monthlyTrend": "Месячный тренд",
+    "serviceTypeBreakdown": "Разбивка по типам обслуживания",
+    "tableHeaders": {
+      "type": "Тип",
+      "count": "Количество",
+      "avgValue": "Средняя стоимость",
+      "avgHours": "Среднее часов"
+    }
+  },
+  "retention": {
+    "returning": "Вернувшиеся",
+    "new": "Новые",
+    "totalActive": "Всего активных",
+    "avgDaysBetween": "Средний интервал (дни)",
+    "topReturningCustomers": "Топ вернувшихся клиентов",
+    "returningCustomers": "Вернувшиеся клиенты",
+    "tableHeaders": {
+      "customer": "Клиент",
+      "company": "Компания",
+      "visits": "Визиты",
+      "totalSpent": "Всего потрачено",
+      "avgDaysBetween": "Средний интервал (дни)"
+    }
+  },
+  "tax": {
+    "totalTaxCollected": "Всего собрано налогов",
+    "taxableRevenue": "Налогооблагаемый доход",
+    "invoicesWithTax": "Счета с налогом",
+    "monthlyTaxCollected": "Ежемесячные налоги",
+    "taxByRate": "Налог по ставкам",
+    "monthlyBreakdown": "Помесячная разбивка",
+    "tableHeaders": {
+      "taxRate": "Ставка налога",
+      "taxCollected": "Собрано налога",
+      "invoices": "Счета",
+      "month": "Месяц",
+      "taxableAmount": "Налогооблагаемая сумма"
+    }
+  },
+  "charts": {
+    "revenue": "Доход",
+    "collected": "Собрано",
+    "count": "Количество",
+    "totalSpent": "Всего потрачено",
+    "jobs": "Работы",
+    "visits": "Визиты",
+    "taxCollected": "Собрано налога"
+  },
+  "days": {
+    "monday": "Понедельник",
+    "tuesday": "Вторник",
+    "wednesday": "Среда",
+    "thursday": "Четверг",
+    "friday": "Пятница",
+    "saturday": "Суббота",
+    "sunday": "Воскресенье"
+  },
+  "daysShort": {
+    "mon": "Пн",
+    "tue": "Вт",
+    "wed": "Ср",
+    "thu": "Чт",
+    "fri": "Пт",
+    "sat": "Сб",
+    "sun": "Вс"
+  },
+  "csvHeaders": {
+    "month": "Месяц",
+    "revenue": "Доход",
+    "collected": "Собрано",
+    "count": "Количество",
+    "category": "Категория",
+    "label": "Метка",
+    "name": "Имя",
+    "company": "Компания",
+    "services": "Обслуживания",
+    "totalSpent": "Всего потрачено",
+    "partNumber": "Арт. №",
+    "quantity": "Количество",
+    "minQuantity": "Мин. количество",
+    "unitCost": "Себестоимость",
+    "technician": "Механик",
+    "jobs": "Работы",
+    "totalRevenue": "Общий доход",
+    "avgRevenue": "Средний доход",
+    "totalHours": "Всего часов",
+    "avgHours": "Среднее часов",
+    "partName": "Название запчасти",
+    "usageCount": "Раз использовано",
+    "totalQty": "Общее кол-во",
+    "serviceType": "Тип обслуживания",
+    "avgValue": "Средняя стоимость",
+    "customer": "Клиент",
+    "visits": "Визиты",
+    "avgDaysBetweenVisits": "Средний интервал между визитами (дни)",
+    "taxCollected": "Собрано налога",
+    "taxableAmount": "Налогооблагаемая сумма",
+    "invoiceCount": "Количество счетов"
+  },
+  "empty": "Данные ещё не загружены. Нажмите «Обновить» для загрузки отчёта."
+}

+ 285 - 0
messages/ru/service.json

@@ -0,0 +1,285 @@
+{
+  "page": {
+    "notFound": "Запись обслуживания не найдена",
+    "updated": "Запись обслуживания обновлена",
+    "failedUpdate": "Не удалось обновить запись обслуживания",
+    "deleteTitle": "Удалить запись обслуживания",
+    "deleteDescription": "Это безвозвратно удалит запись обслуживания и все связанные данные. Это действие необратимо.",
+    "deleted": "Запись обслуживания удалена",
+    "failedDelete": "Не удалось удалить",
+    "customFieldsInvalid": "Заполните все обязательные пользовательские поля",
+    "customFieldsSaveFailed": "Не удалось сохранить пользовательские поля"
+  },
+  "header": {
+    "tabs": {
+      "details": "Детали",
+      "images": "Изображения",
+      "video": "Видео",
+      "documents": "Документы"
+    },
+    "save": "Сохранить",
+    "pdf": "PDF",
+    "email": "Эл. почта",
+    "share": "Поделиться",
+    "delete": "Удалить",
+    "unpaid": "Не оплачено",
+    "unsavedChanges": "Несохранённые изменения",
+    "saved": "Сохранено"
+  },
+  "basicInfo": {
+    "title": "Основная информация",
+    "vehicle": "Транспортное средство",
+    "open": "Открыть",
+    "customer": "Клиент",
+    "titleLabel": "Название *",
+    "titlePlaceholder": "Замена масла",
+    "date": "Дата",
+    "type": "Тип",
+    "mileage": "Пробег",
+    "status": "Статус",
+    "technician": "Механик",
+    "invoiceNumber": "Номер счёта",
+    "typeOptions": {
+      "maintenance": "Техобслуживание",
+      "repair": "Ремонт",
+      "upgrade": "Модернизация",
+      "inspection": "Осмотр"
+    },
+    "statusOptions": {
+      "pending": "Ожидает",
+      "in_progress": "В работе",
+      "waiting_parts": "Ожидание запчастей",
+      "completed": "Завершено"
+    },
+    "searchVehicles": "Поиск транспортных средств...",
+    "noVehicleFound": "Транспортное средство не найдено.",
+    "techPicker": {
+      "placeholder": "Выберите или введите имя...",
+      "searchPlaceholder": "Поиск или ввод имени...",
+      "useCustom": "Использовать \"{search}\"",
+      "typePrompt": "Введите имя...",
+      "boardTechnicians": "Механики доски",
+      "teamMembers": "Участники команды"
+    }
+  },
+  "parts": {
+    "title": "Запчасти",
+    "fromInventory": "Со склада",
+    "addPart": "Добавить запчасть",
+    "partNumber": "Арт. №",
+    "name": "Название",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "namePlaceholder": "Название *",
+    "subtotal": "Подитог запчастей: {amount}",
+    "inventoryTitle": "Выбор запчасти со склада",
+    "searchInventory": "Поиск на складе...",
+    "inStock": "{quantity} в наличии",
+    "noInventory": "Запчасти на складе не найдены."
+  },
+  "labor": {
+    "title": "Работа",
+    "addLabor": "Добавить работу",
+    "description": "Описание",
+    "hours": "Часы",
+    "rate": "Ставка ({currency}/ч)",
+    "total": "Итого",
+    "descriptionPlaceholder": "Описание *",
+    "subtotal": "Подитог работ: {amount}"
+  },
+  "totals": {
+    "title": "Итоги",
+    "parts": "Запчасти",
+    "labor": "Работа",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountNone": "Нет",
+    "discountPercentage": "Процент",
+    "discountFixed": "Фиксированная",
+    "tax": "Налог",
+    "total": "Итого"
+  },
+  "notes": {
+    "title": "Заметки",
+    "public": "Публичные",
+    "internal": "Внутренние",
+    "publicPlaceholder": "Заметки, видимые на счёте...",
+    "internalPlaceholder": "Внутренние заметки (не показываются на счёте)...",
+    "publicHelper": "Отображаются на счёте и в общих документах",
+    "internalHelper": "Видно только вашей команде",
+    "aiWrite": "ИИ-генерация",
+    "aiGenerated": "ИИ-заметки успешно сгенерированы",
+    "aiGenerateFailed": "Не удалось сгенерировать ИИ-заметки",
+    "aiDialogTitle": "ИИ-генерация",
+    "aiDialogDescription": "ИИ проанализирует запчасти и работы в этой записи обслуживания и создаст профессиональное описание. Результат будет записан в текущий выбранный тип заметки.",
+    "aiGenerate": "Сгенерировать",
+    "aiOverwriteDescription": "Текущая заметка уже содержит текст. Вы можете заменить его или добавить сгенерированный ИИ текст ниже.",
+    "aiOverwriteReplace": "Заменить",
+    "aiOverwriteAppend": "Добавить",
+    "aiOverwriteCancel": "Отмена",
+    "aiDontShowAgain": "Больше не показывать"
+  },
+  "invoice": {
+    "title": "Итоги по счёту",
+    "parts": "Запчасти",
+    "labor": "Работа",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "tax": "Налог ({rate}%)",
+    "total": "Итого",
+    "paid": "Оплачено",
+    "balanceDue": "К оплате",
+    "paidBadge": "ОПЛАЧЕНО",
+    "entityLabel": "Счёт",
+    "emailSubject": "Подтверждение оплаты"
+  },
+  "payments": {
+    "title": "Платежи",
+    "markUnpaid": "Отметить как неоплаченный",
+    "markPaid": "Отметить как оплаченный",
+    "recordPayment": "Записать платёж",
+    "totalPaid": "Всего оплачено",
+    "amount": "Сумма",
+    "date": "Дата",
+    "method": "Способ",
+    "note": "Примечание",
+    "notePlaceholder": "Необязательное примечание",
+    "savePayment": "Сохранить платёж",
+    "methodOptions": {
+      "cash": "Наличные",
+      "card": "Карта",
+      "transfer": "Перевод",
+      "other": "Другое"
+    },
+    "recorded": "Платёж записан",
+    "markedPaid": "Отмечено как оплаченный",
+    "markedUnpaid": "Отмечено как неоплаченный",
+    "deleted": "Платёж удалён",
+    "failedRecord": "Не удалось записать платёж",
+    "failedStatus": "Не удалось обновить статус оплаты",
+    "failedDelete": "Не удалось удалить платёж",
+    "deleteTitle": "Удалить платёж",
+    "deleteDescription": "Эта запись платежа будет удалена.",
+    "failedPdf": "Не удалось сгенерировать PDF-счёт"
+  },
+  "attachments": {
+    "deleteTitle": "Удалить вложение",
+    "deleteDescription": "Это вложение будет безвозвратно удалено. Это действие необратимо.",
+    "deleted": "Вложение удалено",
+    "failedDelete": "Не удалось удалить",
+    "images": "Изображения ({count})",
+    "videos": "Видео ({count})",
+    "diagnostics": "Диагностические отчёты ({count})",
+    "documents": "Документы ({count})"
+  },
+  "share": {
+    "title": "Поделиться счётом",
+    "description": "Создайте публичную ссылку, по которой любой сможет просмотреть и скачать этот счёт без входа в систему.",
+    "notifyTitle": "Уведомить {name}",
+    "email": "Эл. почта",
+    "noEmail": "(эл. почта не указана)",
+    "sms": "SMS",
+    "noPhone": "(телефон не указан)",
+    "sendNotification": "Отправить уведомление",
+    "revokeLink": "Отозвать ссылку",
+    "generateLink": "Создать публичную ссылку",
+    "emailSent": "Письмо отправлено",
+    "failedEmail": "Не удалось отправить письмо",
+    "smsSent": "SMS отправлено",
+    "failedSms": "Не удалось отправить SMS"
+  },
+  "sidebar": {
+    "vehicle": "Транспортное средство",
+    "customer": "Клиент",
+    "vin": "VIN: {vin}",
+    "mileageAtService": "Пробег при обслуживании: {mileage} {unit}",
+    "sharedLink": {
+      "title": "Общая ссылка",
+      "sharedOn": "Опубликовано {date}",
+      "notViewed": "Ещё не просмотрено",
+      "viewedTimes": "Просмотрено {count, plural, one {# раз} few {# раза} other {# раз}}",
+      "lastViewed": "Последний раз {date}",
+      "copyLink": "Копировать ссылку",
+      "linkCopied": "Ссылка скопирована"
+    }
+  },
+  "images": {
+    "title": "Фотографии обслуживания",
+    "dropzone": "Перетащите изображения сюда или нажмите для выбора",
+    "uploading": "Загрузка...",
+    "formats": "JPG, PNG, WebP — макс. 10МБ каждое",
+    "uploadingImages": "Загрузка изображений...",
+    "limitReached": "Лимит изображений достигнут ({count}/{max})",
+    "upgradePrompt": "Обновите тариф для загрузки дополнительных изображений.",
+    "onlyAllowed": "Допускаются только изображения JPG, PNG и WebP. {names} пропущены.",
+    "onlyUploading": "Загружается только {count} изображений для соблюдения лимита.",
+    "uploadingCount": "Загрузка изображений: {count}...",
+    "uploadedCount": "Загружено изображений: {count}",
+    "uploadFailed": "Загрузка не удалась",
+    "deleted": "Изображение удалено",
+    "failedDelete": "Не удалось удалить изображение",
+    "failedUpdate": "Не удалось обновить",
+    "description": "Описание...",
+    "invoice": "Счёт"
+  },
+  "videos": {
+    "title": "Видео обслуживания",
+    "dropzone": "Перетащите видео сюда или нажмите для выбора",
+    "uploading": "Загрузка...",
+    "formats": "MP4, WebM, MOV — макс. 10МБ каждое",
+    "uploadingVideos": "Загрузка видео...",
+    "uploadingCount": "Загрузка видео: {count}...",
+    "uploadedCount": "Загружено видео: {count}",
+    "uploadFailed": "Загрузка не удалась",
+    "deleted": "Видео удалено",
+    "failedDelete": "Не удалось удалить видео",
+    "failedUpdate": "Не удалось обновить",
+    "invoice": "Счёт"
+  },
+  "documents": {
+    "diagnosticTitle": "Диагностические отчёты",
+    "documentsTitle": "Документы",
+    "dropzone": "Перетащите файлы сюда или нажмите для выбора",
+    "uploading": "Загрузка...",
+    "diagnosticFormats": "PDF, CSV, TXT — макс. 10МБ каждый",
+    "documentFormats": "PDF, DOCX, XLSX, CSV, TXT — макс. 10МБ каждый",
+    "uploadingReports": "Загрузка отчётов...",
+    "uploadingDocuments": "Загрузка документов...",
+    "diagnosticLimitReached": "Лимит диагностических отчётов достигнут ({count}/{max})",
+    "documentLimitReached": "Лимит документов достигнут ({count}/{max})",
+    "upgradePrompt": "Обновите тариф для загрузки дополнительных файлов.",
+    "diagnosticLabel": "Диагностический отчёт",
+    "documentLabel": "Документ",
+    "limitReachedToast": "Лимит {label} достигнут ({count}/{max}). Обновите тариф для увеличения.",
+    "onlyUploading": "Загружается только {count} файлов для соблюдения лимита.",
+    "uploadingCount": "Загрузка файлов: {count}...",
+    "uploadedCount": "Загружено файлов: {count}",
+    "uploadFailed": "Загрузка не удалась",
+    "deleted": "Файл удалён",
+    "failedDelete": "Не удалось удалить файл",
+    "failedUpdate": "Не удалось обновить",
+    "invoice": "Счёт"
+  },
+  "schedule": {
+    "title": "Расписание",
+    "date": "Дата",
+    "startTime": "Время начала",
+    "endTime": "Время окончания",
+    "technician": "Механик",
+    "selectTechnician": "Выберите механика...",
+    "searchOrCreate": "Поиск или создание механика...",
+    "createTechnician": "Создать \"{name}\"",
+    "creating": "Создание...",
+    "failedCreate": "Не удалось создать механика",
+    "addNew": "+ Добавить нового",
+    "add": "Добавить",
+    "newTechPlaceholder": "Имя механика...",
+    "assign": "Назначить на доску",
+    "assigned": "Назначено на доску работ",
+    "failedAssign": "Не удалось назначить",
+    "endBeforeStart": "Время окончания должно быть позже времени начала",
+    "failedUpdate": "Не удалось обновить время в расписании",
+    "splitCreated": "Работа занимает несколько дней — создано продолжение на следующий день"
+  }
+}

+ 1003 - 0
messages/ru/settings.json

@@ -0,0 +1,1003 @@
+{
+  "title": "Настройки",
+  "subtitle": "Управление мастерской, счетами и предпочтениями",
+  "nav": {
+    "categories": {
+      "general": "Общие",
+      "billing": "Счета и документы",
+      "communications": "Коммуникации",
+      "workshop": "Мастерская",
+      "system": "Система"
+    },
+    "items": {
+      "company": {
+        "title": "Компания",
+        "description": "Данные компании и брендинг"
+      },
+      "subscription": {
+        "title": "Подписка",
+        "description": "Тариф и оплата"
+      },
+      "account": {
+        "title": "Аккаунт",
+        "description": "Имя, эл. почта и пароль"
+      },
+      "team": {
+        "title": "Команда",
+        "description": "Участники и роли"
+      },
+      "invoice": {
+        "title": "Счета и предложения",
+        "description": "Настройки счетов, макет и пользовательские поля"
+      },
+      "templates": {
+        "title": "Шаблоны",
+        "description": "Визуальный стиль, цвета и шрифты"
+      },
+      "payment": {
+        "title": "Оплата",
+        "description": "Условия оплаты и счета"
+      },
+      "currency": {
+        "title": "Валюта",
+        "description": "Валюта и налоги по умолчанию"
+      },
+      "localization": {
+        "title": "Локализация",
+        "description": "Язык, валюта, дата и время"
+      },
+      "customFields": {
+        "title": "Пользовательские поля",
+        "description": "Определение пользовательских полей данных"
+      },
+      "email": {
+        "title": "Эл. почта",
+        "description": "Провайдер и отправка писем"
+      },
+      "sms": {
+        "title": "SMS",
+        "description": "Провайдер SMS и сообщения"
+      },
+      "customerPortal": {
+        "title": "Портал клиента",
+        "description": "Портал самообслуживания клиентов"
+      },
+      "workshop": {
+        "title": "Мастерская",
+        "description": "Механики и ставки по умолчанию"
+      },
+      "maintenance": {
+        "title": "Техобслуживание",
+        "description": "Прогнозируемые интервалы обслуживания"
+      },
+      "ai": {
+        "title": "ИИ",
+        "description": "Провайдер ИИ и функции"
+      },
+      "appearance": {
+        "title": "Внешний вид",
+        "description": "Тема и отображение"
+      },
+      "data": {
+        "title": "Данные",
+        "description": "Экспорт и импорт резервных копий"
+      },
+      "license": {
+        "title": "Лицензия",
+        "description": "White-label лицензия"
+      },
+      "about": {
+        "title": "О программе",
+        "description": "Версия и информация"
+      }
+    },
+    "pro": "PRO"
+  },
+  "readOnly": {
+    "banner": "У вас есть доступ только для просмотра настроек. Обратитесь к администратору для внесения изменений."
+  },
+  "featureLocked": {
+    "requiresPro": "Требуется лицензия Pro",
+    "upgradePlan": "Обновить тариф",
+    "activateLicense": "Активировать лицензию"
+  },
+  "company": {
+    "logoTitle": "Логотип компании",
+    "logoAlt": "Логотип компании",
+    "upload": "Загрузить",
+    "remove": "Удалить",
+    "logoHint": "JPG, PNG, WebP, SVG — макс. 2МБ",
+    "uploadingLogo": "Загрузка логотипа...",
+    "logoUpdated": "Логотип обновлён",
+    "failedUploadLogo": "Не удалось загрузить логотип",
+    "detailsTitle": "Данные компании",
+    "shopName": "Название компании / мастерской",
+    "shopNamePlaceholder": "Автосервис Ивана",
+    "orgNumber": "Номер организации",
+    "orgNumberPlaceholder": "123 456 789",
+    "phone": "Телефон",
+    "phonePlaceholder": "+7 (999) 123-45-67",
+    "email": "Эл. почта",
+    "emailPlaceholder": "service@example.com",
+    "address": "Адрес",
+    "addressPlaceholder": "ул. Главная, 123\nОфис 100\nМосква, 123456",
+    "saveCompany": "Сохранить данные компании",
+    "addNewCompany": "Добавить новую компанию",
+    "companyName": "Название компании",
+    "companyNamePlaceholder": "напр. Автосервис Ивана",
+    "cancel": "Отмена",
+    "createCompany": "Создать компанию",
+    "settingsSaved": "Настройки компании сохранены",
+    "failedSave": "Не удалось сохранить настройки",
+    "companyCreated": "Компания успешно создана"
+  },
+  "account": {
+    "profileTitle": "Профиль",
+    "profileDescription": "Обновите своё отображаемое имя и адрес эл. почты.",
+    "emailVerified": "Подтверждено",
+    "emailNotVerified": "Не подтверждено",
+    "sendVerificationEmail": "Отправить письмо для подтверждения",
+    "verificationEmailSent": "Письмо для подтверждения отправлено. Проверьте входящие.",
+    "verificationEmailFailed": "Не удалось отправить письмо для подтверждения",
+    "resendCooldown": "Повторная отправка через {seconds}с",
+    "emailChangeSent": "Ссылка для подтверждения отправлена на новый адрес эл. почты. Адрес будет обновлён после перехода по ссылке.",
+    "name": "Имя",
+    "namePlaceholder": "Ваше имя",
+    "emailLabel": "Эл. почта",
+    "emailPlaceholder": "Ваша эл. почта",
+    "saveProfile": "Сохранить профиль",
+    "profileUpdated": "Профиль обновлён",
+    "failedUpdateProfile": "Не удалось обновить профиль",
+    "failedUpdateEmail": "Не удалось обновить эл. почту",
+    "changePasswordTitle": "Изменить пароль",
+    "changePasswordDescription": "Введите текущий пароль и выберите новый.",
+    "currentPassword": "Текущий пароль",
+    "newPassword": "Новый пароль",
+    "confirmNewPassword": "Подтвердите новый пароль",
+    "changePassword": "Изменить пароль",
+    "passwordChanged": "Пароль изменён",
+    "passwordsNoMatch": "Пароли не совпадают",
+    "passwordMinLength": "Пароль должен содержать не менее 8 символов",
+    "failedChangePassword": "Не удалось изменить пароль",
+    "twoFactorTitle": "Двухфакторная аутентификация",
+    "twoFactorEnabled": "Двухфакторная аутентификация включена",
+    "twoFactorDisablePrompt": "Введите пароль для отключения двухфакторной аутентификации.",
+    "twoFactorEnablePrompt": "Добавьте дополнительный уровень безопасности, включив двухфакторную аутентификацию через приложение-аутентификатор.",
+    "password": "Пароль",
+    "enterPassword": "Введите пароль",
+    "disable2FA": "Отключить 2FA",
+    "enable2FA": "Включить 2FA",
+    "twoFactorDisabled": "Двухфакторная аутентификация отключена",
+    "failedEnable2FA": "Не удалось включить 2FA",
+    "failedDisable2FA": "Не удалось отключить 2FA",
+    "enterPasswordError": "Пожалуйста, введите пароль",
+    "setupDialogTitle": "Включить двухфакторную аутентификацию",
+    "setupDialogDescription": "Введите пароль, чтобы начать настройку 2FA с приложением-аутентификатором.",
+    "continue": "Продолжить",
+    "scanQRTitle": "Сканируйте QR-код",
+    "scanQRDescription": "Сканируйте этот QR-код приложением-аутентификатором (Google Authenticator, Authy и др.)",
+    "verifyCodeTitle": "Подтвердите код",
+    "verifyCodeDescription": "Введите 6-значный код из приложения-аутентификатора для проверки настройки.",
+    "authCode": "Код аутентификации",
+    "authCodePlaceholder": "000000",
+    "back": "Назад",
+    "verify": "Подтвердить",
+    "enterCodeError": "Пожалуйста, введите код из приложения-аутентификатора",
+    "invalidCode": "Неверный код. Попробуйте снова.",
+    "verificationFailed": "Проверка не удалась",
+    "backupCodesTitle": "Сохраните резервные коды",
+    "backupCodesDescription": "Сохраните эти коды в надёжном месте. Вы сможете использовать их для входа, если потеряете доступ к приложению-аутентификатору. Каждый код можно использовать только один раз.",
+    "copyCodes": "Копировать коды",
+    "copied": "Скопировано",
+    "backupCodesCopied": "Резервные коды скопированы в буфер обмена",
+    "savedBackupCodes": "Я сохранил резервные коды",
+    "twoFactorEnabledSuccess": "Двухфакторная аутентификация включена",
+    "dangerZone": "Опасная зона",
+    "deleteContentTitle": "Удалить данные",
+    "deleteContentDescription": "Выборочное удаление данных организации: транспортные средства, клиенты, предложения или склад. Ваш аккаунт и настройки останутся нетронутыми.",
+    "deleteContentButton": "Удалить данные",
+    "deleteAccountTitle": "Удалить аккаунт",
+    "deleteAccountDescription": "Безвозвратно удалить ваш аккаунт и все связанные данные, включая транспортные средства, записи обслуживания, заказ-наряды, счета, клиентов, файлы и платежи. Это действие необратимо.",
+    "deleteAccountButton": "Удалить аккаунт",
+    "deleteAccountDialogTitle": "Удалить аккаунт",
+    "deleteAccountDialogDescription": "Это навсегда удалит ваш аккаунт и все связанные с ним данные. Это действие необратимо. Все транспортные средства, записи обслуживания, заказ-наряды, предложения, клиенты, файлы и история платежей будут безвозвратно удалены.",
+    "deleteAccountConfirmPhrase": "удалить меня",
+    "deleteAccountConfirmPrompt": "Введите <bold>удалить меня</bold> для подтверждения",
+    "permanentlyDelete": "Удалить навсегда",
+    "deleting": "Удаление...",
+    "failedDeleteAccount": "Не удалось удалить аккаунт",
+    "deleteContentDialogTitle": "Удалить данные",
+    "deleteContentDialogDescription": "Выберите данные, которые вы хотите безвозвратно удалить. Это действие необратимо.",
+    "contentVehicles": "Транспортные средства",
+    "contentVehiclesDescription": "Все транспортные средства и их записи обслуживания, заметки, напоминания, журналы топлива и периодические счета",
+    "contentCustomers": "Клиенты",
+    "contentCustomersDescription": "Все записи клиентов. Транспортные средства, привязанные к удалённым клиентам, будут отвязаны.",
+    "contentQuotes": "Предложения",
+    "contentQuotesDescription": "Все предложения и их позиции",
+    "contentInventory": "Склад",
+    "contentInventoryDescription": "Все складские запчасти и их изображения",
+    "contentInspections": "Осмотры",
+    "contentInspectionsDescription": "Все осмотры транспортных средств и их результаты",
+    "contentTechnicians": "Механики",
+    "contentTechniciansDescription": "Все профили механиков. Связанные записи будут отвязаны.",
+    "contentInspectionTemplates": "Шаблоны осмотров",
+    "contentInspectionTemplatesDescription": "Все шаблоны осмотров и их разделы. Сначала удалите осмотры.",
+    "contentNotifications": "Уведомления",
+    "contentNotificationsDescription": "Все системные уведомления",
+    "contentSmsMessages": "SMS-сообщения",
+    "contentSmsMessagesDescription": "Все отправленные и полученные SMS-сообщения",
+    "contentCustomFields": "Пользовательские поля",
+    "contentCustomFieldsDescription": "Все определения пользовательских полей и их значения",
+    "deleteContentConfirmPhrase": "удалить мои данные",
+    "deleteContentConfirmPrompt": "Введите <bold>удалить мои данные</bold> для подтверждения",
+    "deleteSelected": "Удалить {count} выбранных",
+    "contentDeleted": "Выбранные данные были безвозвратно удалены",
+    "failedDeleteContent": "Не удалось удалить данные",
+    "passkey": {
+      "title": "Ключи доступа",
+      "description": "Входите с помощью биометрии или ключа безопасности вместо пароля.",
+      "noPasskeys": "Ключи доступа ещё не зарегистрированы.",
+      "unnamed": "Безымянный ключ доступа",
+      "singleDevice": "Только на этом устройстве",
+      "multiDevice": "Синхронизирован между устройствами",
+      "register": "Зарегистрировать ключ доступа",
+      "registering": "Регистрация...",
+      "registerSuccess": "Ключ доступа успешно зарегистрирован",
+      "registerFailed": "Не удалось зарегистрировать ключ доступа",
+      "delete": "Удалить",
+      "deleteSuccess": "Ключ доступа удалён",
+      "deleteFailed": "Не удалось удалить ключ доступа",
+      "rename": "Переименовать ключ доступа",
+      "renameFailed": "Не удалось переименовать ключ доступа",
+      "nameLabel": "Название",
+      "namePlaceholder": "напр. MacBook Pro, iPhone",
+      "cancel": "Отмена",
+      "save": "Сохранить",
+      "confirmDeleteTitle": "Удалить ключ доступа",
+      "confirmDelete": "Вы уверены, что хотите удалить этот ключ доступа? Вы больше не сможете входить с его помощью."
+    }
+  },
+  "team": {
+    "title": "Команда",
+    "noOrgDescription": "Создайте команду для совместной работы с другими пользователями.",
+    "orgDescription": "Управляйте участниками организации и их ролями.",
+    "createOrg": "Создать организацию",
+    "createOrgDescription": "Создайте организацию, чтобы приглашать участников команды. Вы станете владельцем.",
+    "orgName": "Название организации",
+    "orgNamePlaceholder": "Моя мастерская",
+    "create": "Создать",
+    "orgCreated": "Организация создана",
+    "failedCreateOrg": "Не удалось создать организацию",
+    "memberCount": "{count} участник",
+    "memberCountPlural": "{count} участников",
+    "admin": "Администратор",
+    "member": "Участник",
+    "inviteByEmail": "Пригласить по эл. почте",
+    "inviteEmailPlaceholder": "user@example.com",
+    "invite": "Пригласить",
+    "invited": "Участник успешно приглашён.",
+    "userNotFoundTitle": "Пользователь не найден",
+    "userNotFoundDescription": "Аккаунт для \"{email}\" не найден. Отправить приглашение для регистрации и присоединения к команде?",
+    "sendInvitation": "Отправить приглашение",
+    "invitationSentTitle": "Приглашение отправлено",
+    "invitationSentDescription": "Приглашение отправлено на {email}.",
+    "failedInvite": "Не удалось пригласить участника",
+    "failedSendInvitation": "Не удалось отправить приглашение",
+    "pendingInvitations": "Ожидающие приглашения",
+    "invitedAs": "Приглашён как {role}",
+    "expires": "Истекает {date}",
+    "pending": "Ожидает",
+    "copyInviteLink": "Копировать ссылку приглашения",
+    "inviteLinkCopied": "Ссылка приглашения скопирована в буфер обмена",
+    "cancelInvitation": "Отменить приглашение",
+    "cancelInvitationDescription": "Отменить приглашение для {email}? Ссылка перестанет действовать.",
+    "invitationCancelled": "Приглашение отменено",
+    "failedCancelInvitation": "Не удалось отменить приглашение",
+    "customRoles": "Пользовательские роли",
+    "newRole": "Новая роль",
+    "roleName": "Название роли",
+    "roleNamePlaceholder": "напр. Механик",
+    "fullAdminAccess": "Полный доступ администратора (обходит все проверки разрешений)",
+    "permissions": "Разрешения",
+    "createRole": "Создать роль",
+    "updateRole": "Обновить роль",
+    "roleCreated": "Роль создана",
+    "roleUpdated": "Роль обновлена",
+    "failedSaveRole": "Не удалось сохранить роль",
+    "deleteRole": "Удалить роль",
+    "deleteRoleDescription": "Удалить роль \"{name}\"? Участники с этой ролью потеряют свои пользовательские разрешения.",
+    "roleDeleted": "Роль удалена",
+    "failedDeleteRole": "Не удалось удалить роль",
+    "fullAccess": "Полный доступ",
+    "permissionCount": "{count} разрешение",
+    "permissionCountPlural": "{count} разрешений",
+    "noCustomRoles": "Пользовательских ролей пока нет. Создайте одну для настройки детальных разрешений участников команды.",
+    "presetAll": "Все",
+    "presetViewOnly": "Только просмотр",
+    "presetWriteAll": "Чтение и запись",
+    "presetNone": "Нет",
+    "builtInRoles": "Встроенные роли",
+    "ownerLabel": "Владелец",
+    "ownerDescription": "Полный доступ. Может управлять участниками, изменять роли и удалять организацию.",
+    "adminLabel": "Администратор",
+    "adminDescription": "Может приглашать/удалять участников и управлять общими данными.",
+    "memberLabel": "Участник",
+    "memberDescription": "Разрешения определяются назначенной пользовательской ролью. Без пользовательской роли — доступ только для чтения.",
+    "roleAssigned": "Роль обновлена",
+    "failedAssignRole": "Не удалось обновить роль",
+    "removeMemberTitle": "Удалить участника",
+    "removeMemberDescription": "Удалить {name} из команды? Доступ к общим данным будет потерян.",
+    "removeButton": "Удалить",
+    "memberRemoved": "Участник удалён",
+    "failedRemoveMember": "Не удалось удалить участника",
+    "memberRoleUpdated": "Роль участника обновлена",
+    "failedUpdateRole": "Не удалось обновить роль"
+  },
+  "invoice": {
+    "title": "Счета и предложения",
+    "description": "Настройте параметры счетов, макет и пользовательские поля.",
+    "tabs": {
+      "general": "Общие",
+      "layout": "Макет",
+      "customFields": "Пользовательские поля"
+    },
+    "invoiceNumberFormat": "Формат номера счёта",
+    "invoiceNumberFormatHint": "Используйте <code>{year}</code> для текущего года. Предпросмотр: <bold>{preview}</bold>",
+    "nextInvoiceNumber": "Следующий номер счёта",
+    "nextInvoiceNumberPlaceholder": "напр. 94",
+    "nextInvoiceNumberHint": "Следующий счёт будет использовать этот номер (напр. {example})",
+    "dueDays": "Срок оплаты (дни)",
+    "dueDaysHint": "Количество дней до наступления срока оплаты счёта",
+    "visibilityTitle": "Видимость на счёте",
+    "companyLogo": "Логотип компании",
+    "companyLogoHint": "Отображать логотип компании в заголовке счёта",
+    "companyName": "Название компании",
+    "companyNameHint": "Отображать название компании в заголовке счёта",
+    "bankAccount": "Банковский счёт",
+    "bankAccountHint": "Показывать банковский счёт / IBAN на счёте",
+    "organizationNumber": "Номер организации",
+    "organizationNumberHint": "Показывать номер организации в заголовке счёта",
+    "customFooter": "Пользовательский подвал счёта",
+    "footerPlaceholder": "Спасибо за выбор нашего сервиса!",
+    "footerHint": "Этот текст отображается внизу каждого счёта",
+    "saveInvoice": "Сохранить настройки счёта",
+    "saved": "Настройки счёта сохранены",
+    "layoutDescription": "Настройте порядок и видимость разделов в счетах и предложениях.",
+    "layoutSaved": "Макет успешно сохранён",
+    "preview": "Предпросмотр",
+    "layoutDocInvoice": "Счёт",
+    "layoutDocQuote": "Предложение",
+    "saveLayout": "Сохранить макет",
+    "resetLayout": "Сбросить по умолчанию"
+  },
+  "layoutEditor": {
+    "helpText": "Перетаскивайте для изменения порядка. Используйте переключатель ширины для размещения разделов бок о бок. Последовательные разделы половинной ширины (Л/П) будут отображаться в два столбца.",
+    "addField": "Добавить поле",
+    "custom": "пользоват.",
+    "full": "Полная",
+    "L": "Л",
+    "R": "П",
+    "left": "левая",
+    "right": "правая",
+    "halfWidthHint": "Половина ширины ({column}) — нажмите для полной ширины",
+    "fullWidthHint": "Полная ширина — нажмите для половины ширины (левая)",
+    "sections": {
+      "header": "Заголовок",
+      "customer": "Клиент",
+      "vehicle": "Транспортное средство",
+      "service": "Обслуживание",
+      "parts_table": "Таблица запчастей",
+      "labor_table": "Таблица работ",
+      "totals": "Итоги",
+      "notes": "Заметки",
+      "diagnostic_notes": "Диагностические заметки",
+      "bank_account": "Банковский счёт",
+      "footer": "Подвал",
+      "general": "Общие"
+    },
+    "fields": {
+      "customer_name": "Имя клиента",
+      "customer_company": "Компания клиента",
+      "customer_address": "Адрес клиента",
+      "customer_email": "Эл. почта клиента",
+      "customer_phone": "Телефон клиента",
+      "vehicle_name": "Транспортное средство",
+      "vin": "VIN",
+      "license_plate": "Госномер",
+      "mileage": "Пробег",
+      "service_title": "Название обслуживания",
+      "service_type": "Тип обслуживания",
+      "tech_name": "Механик",
+      "logo": "Логотип",
+      "company_name": "Название компании",
+      "company_address": "Адрес",
+      "company_phone": "Телефон",
+      "company_email": "Эл. почта",
+      "company_org_number": "Номер организации",
+      "bank_account": "Банковский счёт",
+      "org_number": "Номер организации"
+    }
+  },
+  "templates": {
+    "title": "Шаблоны",
+    "invoiceDescription": "Настройте внешний вид PDF-счетов и предложений.",
+    "inspectionsDescription": "Управляйте чек-листами осмотров и шаблонами многоточечного контроля.",
+    "smsDescription": "Настройте SMS-сообщения, отправляемые клиентам.",
+    "tabs": {
+      "invoice": "Счёт",
+      "quotation": "Предложение",
+      "inspections": "Осмотры",
+      "sms": "SMS",
+      "layout": "Макет"
+    },
+    "presets": "Предустановки шаблонов",
+    "primaryColor": "Основной цвет",
+    "fontAndLayout": "Шрифт и макет",
+    "fontFamily": "Семейство шрифтов",
+    "helveticaDefault": "Helvetica (по умолчанию)",
+    "timesRoman": "Times Roman",
+    "courier": "Courier",
+    "headerStyle": "Стиль заголовка",
+    "logoSize": "Размер логотипа",
+    "standard": "Стандартный",
+    "compact": "Компактный",
+    "modern": "Современный",
+    "preview": "Предпросмотр {name}",
+    "billTo": "Плательщик",
+    "preparedFor": "Подготовлено для",
+    "vehicle": "Транспортное средство",
+    "part": "Запчасть",
+    "qty": "Кол-во",
+    "price": "Цена",
+    "total": "Итого",
+    "labor": "Работа",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "subtotal": "Подитог",
+    "tax": "Налог ({rate}%)",
+    "previewService": "Обслуживание",
+    "previewNotes": "Заметки",
+    "previewDiagnosticNotes": "Диагностические заметки",
+    "previewDue": "Срок: {date}",
+    "previewAdditionalInfo": "Дополнительная информация",
+    "previewThankYou": "Спасибо за выбор нашего сервиса",
+    "saveInvoiceTemplate": "Сохранить шаблон счёта",
+    "saveQuotationTemplate": "Сохранить шаблон предложения",
+    "invoiceTemplateSaved": "Настройки шаблона счёта сохранены",
+    "quotationTemplateSaved": "Настройки шаблона предложения сохранены",
+    "failedSave": "Не удалось сохранить настройки",
+    "smsTemplates": "Шаблоны SMS-сообщений",
+    "smsTemplatesDescription": "Настройте текстовые сообщения, отправляемые клиентам. Используйте переменные, такие как <code>'{companyName}'</code>, для вставки динамических значений.",
+    "reset": "Сбросить",
+    "invoiceReady": "Счёт готов",
+    "invoiceReadyDescription": "Отправляется при предоставлении счёта клиенту.",
+    "quoteReady": "Предложение готово",
+    "quoteReadyDescription": "Отправляется при предоставлении предложения клиенту.",
+    "inspectionReady": "Осмотр готов",
+    "inspectionReadyDescription": "Отправляется при предоставлении отчёта осмотра клиенту.",
+    "statusInProgress": "Статус: В работе",
+    "statusInProgressDescription": "Отправляется при изменении статуса заказ-наряда на «В работе».",
+    "statusWaitingParts": "Статус: Ожидание запчастей",
+    "statusWaitingPartsDescription": "Отправляется при изменении статуса заказ-наряда на «Ожидание запчастей».",
+    "statusReady": "Статус: Готово к выдаче",
+    "statusReadyDescription": "Отправляется при изменении статуса заказ-наряда на «Готово».",
+    "statusCompleted": "Статус: Завершено",
+    "statusCompletedDescription": "Отправляется при изменении статуса заказ-наряда на «Завершено».",
+    "paymentReceived": "Оплата получена",
+    "paymentReceivedDescription": "Отправляется при записи оплаты по счёту.",
+    "saveSmsTemplates": "Сохранить шаблоны SMS",
+    "smsTemplateSaved": "Настройки шаблонов SMS сохранены",
+    "layoutTitle": "Макет счёта",
+    "layoutDescription": "Настройте порядок и видимость разделов в счетах и предложениях.",
+    "layoutSaved": "Макет успешно сохранён",
+    "layoutDocInvoice": "Счёт",
+    "layoutDocQuote": "Предложение",
+    "smsDefaults": {
+      "invoiceReady": "Ваш счёт готов. Просмотрите его здесь: {share_link}",
+      "quoteReady": "Ваше предложение готово для рассмотрения. Просмотрите его здесь: {share_link}",
+      "inspectionReady": "Отчёт осмотра вашего транспортного средства готов. Просмотрите его здесь: {share_link}",
+      "statusInProgress": "Начаты работы над вашим транспортным средством. Мы будем держать вас в курсе.",
+      "statusWaitingParts": "Ваше транспортное средство ожидает запчасти. Мы сообщим, когда работа возобновится.",
+      "statusReady": "Ваше транспортное средство готово к выдаче!",
+      "statusCompleted": "Обслуживание завершено. Спасибо за выбор нашего сервиса!",
+      "paymentReceived": "Получена оплата {amount} по счёту {invoice_number}. Спасибо!"
+    },
+    "colorPresets": {
+      "amber": "Янтарный",
+      "blue": "Синий",
+      "emerald": "Изумрудный",
+      "red": "Красный",
+      "purple": "Фиолетовый",
+      "slate": "Серый",
+      "rose": "Розовый",
+      "indigo": "Индиго"
+    }
+  },
+  "payment": {
+    "detailsTitle": "Платёжные реквизиты",
+    "detailsDescription": "Банковский счёт и условия оплаты, отображаемые на счетах.",
+    "bankAccount": "Банковский счёт (IBAN)",
+    "bankAccountPlaceholder": "RU12 3456 7890 1234 5678 9012 3456",
+    "bankAccountHint": "Номер вашего банковского счёта или IBAN для получения платежей",
+    "paymentTerms": "Условия оплаты",
+    "paymentTermsPlaceholder": "14 дней",
+    "paymentTermsHint": "Текст условий оплаты, отображаемый на счетах (напр. «14 дней», «По получении»)",
+    "onlinePayments": "Онлайн-платежи",
+    "onlinePaymentsDescription": "Позвольте клиентам оплачивать счета онлайн через общие ссылки на счета.",
+    "stripe": "Stripe",
+    "stripeDescription": "Приём оплаты кредитными/дебетовыми картами",
+    "secretKey": "Секретный ключ",
+    "publishableKey": "Публичный ключ",
+    "webhookSecret": "Секрет вебхука",
+    "webhookUrl": "URL вебхука",
+    "stripeWebhookHint": "Добавьте этот URL в панели управления Stripe в разделе Webhooks. Подпишитесь на <code>checkout.session.completed</code>.",
+    "vipps": "Vipps",
+    "vippsDescription": "Приём мобильных платежей Vipps (Норвегия)",
+    "clientId": "ID клиента",
+    "clientSecret": "Секрет клиента",
+    "subscriptionKey": "Ключ подписки",
+    "merchantSerialNumber": "Серийный номер продавца",
+    "testMode": "Тестовый режим",
+    "callbackUrl": "URL обратного вызова",
+    "termsOfSale": "Условия продажи (Salgsvilkar)",
+    "termsOfSaleDescription": "Требуется Vipps. Клиенты могут просмотреть их со страницы счёта.",
+    "externalTermsUrl": "Внешний URL условий (необязательно)",
+    "externalTermsUrlHint": "Ссылка на внешнюю страницу с вашими условиями. Если указана, счёт будет ссылаться на неё вместо встроенной страницы условий.",
+    "publicTermsUrl": "Публичный URL условий",
+    "publicTermsUrlHint": "Предоставьте этот URL в Vipps или разместите на своём сайте.",
+    "paypal": "PayPal",
+    "paypalDescription": "Приём платежей через PayPal",
+    "sandboxMode": "Режим песочницы",
+    "paypalWebhookHint": "Добавьте этот URL в панели разработчика PayPal в разделе Webhooks. Подпишитесь на <code>PAYMENT.CAPTURE.COMPLETED</code>.",
+    "savePayment": "Сохранить настройки оплаты",
+    "saved": "Настройки оплаты сохранены"
+  },
+  "currency": {
+    "title": "Валюта и налоги",
+    "description": "Валюта и налоговая ставка по умолчанию, используемые в счетах и записях обслуживания.",
+    "currencyLabel": "Валюта",
+    "previewLabel": "Предпросмотр: {value}",
+    "enableTax": "Включить налог",
+    "enableTaxHint": "Показывать расчёт налога на счетах и записях обслуживания",
+    "defaultTaxRate": "Ставка налога по умолчанию (%)",
+    "defaultTaxRateHint": "Автоматически заполняется при создании новых записей обслуживания",
+    "saveSettings": "Сохранить настройки",
+    "saved": "Настройки сохранены",
+    "currencies": {
+      "USD": "Доллар США",
+      "EUR": "Евро",
+      "GBP": "Британский фунт",
+      "NOK": "Норвежская крона",
+      "SEK": "Шведская крона",
+      "DKK": "Датская крона",
+      "CHF": "Швейцарский франк",
+      "CAD": "Канадский доллар",
+      "AUD": "Австралийский доллар",
+      "NZD": "Новозеландский доллар",
+      "JPY": "Японская иена",
+      "CNY": "Китайский юань",
+      "INR": "Индийская рупия",
+      "BRL": "Бразильский реал",
+      "MXN": "Мексиканское песо",
+      "PLN": "Польский злотый",
+      "CZK": "Чешская крона",
+      "HUF": "Венгерский форинт",
+      "TRY": "Турецкая лира",
+      "ZAR": "Южноафриканский рэнд",
+      "KRW": "Южнокорейская вона",
+      "SGD": "Сингапурский доллар",
+      "HKD": "Гонконгский доллар",
+      "THB": "Тайский бат",
+      "ISK": "Исландская крона",
+      "RON": "Румынский лей",
+      "ILS": "Израильский шекель",
+      "PHP": "Филиппинское песо",
+      "IDR": "Индонезийская рупия",
+      "MYR": "Малайзийский ринггит",
+      "AED": "Дирхам ОАЭ",
+      "SAR": "Саудовский риял",
+      "RUB": "Российский рубль"
+    }
+  },
+  "localization": {
+    "languageTitle": "Язык",
+    "languageDescription": "Выберите язык приложения.",
+    "language": "Язык",
+    "saved": "Настройки локализации сохранены",
+    "saveSettings": "Сохранить настройки"
+  },
+  "customFields": {
+    "title": "Пользовательские поля",
+    "description": "Определите пользовательские поля, которые отображаются в записях обслуживания и предложениях.",
+    "addField": "Добавить поле",
+    "filterAll": "Все",
+    "serviceRecord": "Запись обслуживания",
+    "quote": "Предложение",
+    "emptyState": "Пользовательских полей пока нет. Нажмите «Добавить поле», чтобы создать.",
+    "required": "Обязательное",
+    "inactive": "Неактивное",
+    "editField": "Редактировать поле",
+    "newField": "Новое пользовательское поле",
+    "label": "Название",
+    "labelPlaceholder": "напр. Срок гарантии",
+    "fieldName": "Имя поля (ID)",
+    "fieldNamePlaceholder": "srok_garantii",
+    "fieldNameHint": "Только строчные буквы, цифры и символы подчёркивания.",
+    "fieldType": "Тип поля",
+    "entityType": "Тип объекта",
+    "fieldTypes": {
+      "text": "Текст",
+      "number": "Число",
+      "date": "Дата",
+      "select": "Выбор (выпадающий список)",
+      "checkbox": "Флажок",
+      "textarea": "Текстовая область"
+    },
+    "optionsLabel": "Варианты (через запятую)",
+    "optionsPlaceholder": "Вариант 1, Вариант 2, Вариант 3",
+    "active": "Активное",
+    "cancel": "Отмена",
+    "update": "Обновить",
+    "create": "Создать",
+    "deleteTitle": "Удалить пользовательское поле",
+    "deleteDescription": "Это навсегда удалит \"{label}\" и все сохранённые значения. Это действие необратимо.",
+    "fieldCreated": "Пользовательское поле создано",
+    "fieldUpdated": "Пользовательское поле обновлено",
+    "fieldDeleted": "Пользовательское поле удалено",
+    "failedSave": "Не удалось сохранить поле",
+    "failedDelete": "Не удалось удалить"
+  },
+  "workshop": {
+    "title": "Настройки мастерской",
+    "description": "Значения по умолчанию при создании новых записей обслуживания.",
+    "defaultTechnician": "Механик по умолчанию",
+    "technicianPlaceholder": "Поиск механика...",
+    "failedCreateTech": "Не удалось создать механика",
+    "creating": "Создание...",
+    "createTechnician": "Создать \"{name}\"",
+    "newTechPlaceholder": "Имя механика",
+    "addTech": "Добавить",
+    "addNewTech": "+ Добавить нового",
+    "defaultLaborRate": "Ставка за работу по умолчанию (в час)",
+    "laborRatePlaceholder": "75.00",
+    "unitsTitle": "Единицы измерения",
+    "unitsDescription": "Выберите между метрической (км, литры) и имперской (мили, галлоны) системой.",
+    "unitSystem": "Система единиц",
+    "metric": "Метрическая (км, литры)",
+    "imperial": "Имперская (мили, галлоны)",
+    "distanceLabel": "Расстояние",
+    "volumeLabel": "Объём",
+    "kilometers": "километры (км)",
+    "liters": "литры (л)",
+    "miles": "мили (mi)",
+    "gallons": "галлоны (gal)",
+    "saveWorkshop": "Сохранить настройки мастерской",
+    "saved": "Настройки мастерской сохранены",
+    "weekStartDay": "Первый день недели",
+    "workDayStart": "Начало рабочего дня",
+    "workDayEnd": "Конец рабочего дня",
+    "weekDays": {
+      "1": "Понедельник",
+      "2": "Вторник",
+      "3": "Среда",
+      "4": "Четверг",
+      "5": "Пятница",
+      "6": "Суббота",
+      "0": "Воскресенье"
+    }
+  },
+  "maintenance": {
+    "title": "Прогнозируемое ТО",
+    "description": "Прогнозирование текущего пробега каждого транспортного средства на основе истории обслуживания и выявление транспортных средств, которым требуется или скоро потребуется обслуживание.",
+    "enablePredicted": "Включить прогнозируемое ТО",
+    "enablePredictedHint": "Показывать прогнозы в деталях транспортного средства и на панели управления",
+    "serviceInterval": "Интервал обслуживания ({unit})",
+    "serviceIntervalHint": "Расстояние между рекомендуемыми обслуживаниями",
+    "approachingThreshold": "Порог приближения ({unit})",
+    "approachingThresholdHint": "Показывать предупреждение при приближении к следующему обслуживанию на это расстояние",
+    "saveMaintenance": "Сохранить настройки ТО",
+    "saved": "Настройки ТО сохранены"
+  },
+  "appearance": {
+    "title": "Внешний вид",
+    "description": "Настройте внешний вид Torqvoice.",
+    "darkMode": "Тёмная тема",
+    "darkModeHint": "Переключение между светлой и тёмной темами",
+    "dateTimeTitle": "Дата и время",
+    "dateTimeDescription": "Настройте отображение даты и времени в приложении.",
+    "dateFormat": "Формат даты",
+    "timeFormat": "Формат времени",
+    "time12h": "12-часовой (2:30 PM)",
+    "time24h": "24-часовой (14:30)",
+    "timezone": "Часовой пояс",
+    "timezoneAuto": "Автоопределение (браузер)",
+    "timezoneHint": "При автоопределении каждый пользователь видит даты в часовом поясе своего браузера.",
+    "previewLabel": "Предпросмотр",
+    "dateLabel": "Дата",
+    "dateTimeLabel": "Дата и время",
+    "saveSettings": "Сохранить настройки",
+    "saved": "Настройки сохранены"
+  },
+  "data": {
+    "title": "Управление данными",
+    "description": "Экспортируйте данные как резервную копию или импортируйте ранее экспортированный файл.",
+    "exportTitle": "Экспорт данных",
+    "exportDescription": "Скачайте данные в виде zip-файла. Выберите категории для включения в резервную копию.",
+    "includeInBackup": "Включить в резервную копию:",
+    "selectAll": "Выбрать всё",
+    "deselectAll": "Снять выделение",
+    "optSettings": "Настройки",
+    "optSettingsDesc": "Информация о мастерской, настройки счетов, платёжные провайдеры",
+    "optCustomers": "Клиенты",
+    "optCustomersDesc": "Записи клиентов и контактная информация",
+    "optVehicles": "Транспортные средства и записи обслуживания",
+    "optVehiclesDesc": "Транспортные средства, заметки, журналы топлива, напоминания, записи обслуживания",
+    "optQuotes": "Предложения",
+    "optQuotesDesc": "Предложения с запчастями и работами",
+    "optInventory": "Склад",
+    "optInventoryDesc": "Складские запчасти и уровни запасов",
+    "optTechnicians": "Механики",
+    "optTechniciansDesc": "Профили механиков и назначения на доску работ",
+    "optInspections": "Осмотры",
+    "optInspectionsDesc": "Шаблоны осмотров, осмотры и результаты",
+    "optAuditLogs": "Журналы аудита",
+    "optAuditLogsDesc": "История активности и изменений",
+    "optSmsMessages": "SMS-сообщения",
+    "optSmsMessagesDesc": "Отправленные и полученные SMS-сообщения",
+    "optNotifications": "Уведомления",
+    "optNotificationsDesc": "Системные уведомления",
+    "optCustomFields": "Пользовательские поля",
+    "optCustomFieldsDesc": "Определения пользовательских полей и их значения",
+    "optFiles": "Загруженные файлы",
+    "optFilesDesc": "Изображения, вложения к обслуживаниям, логотип",
+    "filesWarning": "Включает загруженные файлы — резервная копия может быть большой",
+    "downloadBackup": "Скачать резервную копию",
+    "exportComplete": "Экспорт завершён",
+    "exportCompleteMessage": "Файл резервной копии был загружен.",
+    "exportFailed": "Ошибка экспорта",
+    "exportFailedMessage": "Не удалось экспортировать данные. Попробуйте снова.",
+    "importTitle": "Импорт данных",
+    "importWarning": "Импорт <bold>заменит все существующие данные</bold> содержимым файла резервной копии. Это действие необратимо.",
+    "importFileHint": "Поддерживаются файлы .zip (v2) и .json (v1).",
+    "uploadRestore": "Загрузить и восстановить",
+    "importComplete": "Импорт завершён",
+    "importCompleteMessage": "Все данные были восстановлены из файла резервной копии.",
+    "importFailed": "Ошибка импорта",
+    "importFromOther": "Импорт из других сервисов",
+    "importFromOtherDescription": "Перенесите данные с другой платформы. Выберите сервис для начала.",
+    "lubelogTitle": "Импорт из LubeLog",
+    "lubelogExportSteps": "Как экспортировать данные из LubeLog:",
+    "lubelogStep1": "Откройте ваш экземпляр LubeLog",
+    "lubelogStep2": "Перейдите в <bold>Настройки</bold>",
+    "lubelogStep3": "Нажмите <bold>Создать резервную копию</bold> для скачивания zip-файла",
+    "lubelogStep4": "Загрузите скачанный zip-файл ниже",
+    "lubelogAddNote": "Это <bold>добавит данные к существующим</bold> записям — ничего не будет удалено.",
+    "lubelogImported": "Что будет импортировано:",
+    "lubelogVehicles": "Транспортные средства",
+    "lubelogServiceRecords": "Записи обслуживания с вложениями",
+    "lubelogNotes": "Заметки",
+    "lubelogFileHint": "Поддерживаются файлы до 200МБ.",
+    "lubelogImportComplete": "Импорт LubeLog завершён",
+    "cancel": "Отмена",
+    "import": "Импортировать",
+    "invoiceNinjaTitle": "Импорт из Invoice Ninja",
+    "invoiceNinjaExportSteps": "Как экспортировать данные из Invoice Ninja:",
+    "invoiceNinjaStep1": "Откройте ваш экземпляр Invoice Ninja",
+    "invoiceNinjaStep2": "Перейдите в <bold>Настройки > Импорт | Экспорт</bold>",
+    "invoiceNinjaStep3": "Нажмите <bold>Экспорт</bold> для скачивания zip-файла",
+    "invoiceNinjaStep4": "Загрузите скачанный zip-файл ниже",
+    "invoiceNinjaAddNote": "Это <bold>добавит данные к существующим</bold> записям — ничего не будет удалено.",
+    "invoiceNinjaImported": "Что будет импортировано:",
+    "invoiceNinjaCustomers": "Клиенты",
+    "invoiceNinjaProducts": "Товары (склад)",
+    "invoiceNinjaInvoices": "Счета как записи обслуживания (запчасти и работы)",
+    "invoiceNinjaDocuments": "Документы и вложения",
+    "invoiceNinjaPayments": "Платежи",
+    "invoiceNinjaFileHint": "Загрузите zip-экспорт Invoice Ninja. Поддерживаются файлы до 200МБ.",
+    "invoiceNinjaImportComplete": "Импорт Invoice Ninja завершён"
+  },
+  "license": {
+    "title": "White-Label лицензия",
+    "description": "Активируйте white-label лицензию для удаления брендинга Torqvoice со счетов и из приложения.",
+    "purchaseWhiteLabel": "Купить White-Label",
+    "status": "Статус:",
+    "active": "Активна",
+    "inactive": "Неактивна",
+    "lastChecked": "Последняя проверка: {date}",
+    "enterLicenseKey": "Введите лицензионный ключ",
+    "validate": "Проверить",
+    "validated": "Лицензия успешно подтверждена",
+    "invalid": "Недействительный лицензионный ключ",
+    "failedValidate": "Не удалось проверить лицензию",
+    "keyHint": "Каждый лицензионный ключ привязан к этой организации и не может быть использован в нескольких организациях."
+  },
+  "about": {
+    "title": "О Torqvoice",
+    "description": "Torqvoice — это комплексное решение для управления обслуживанием транспортных средств. Ведение истории обслуживания, ремонтов и многое другое.",
+    "version": "Версия",
+    "website": "Веб-сайт",
+    "documentation": "Документация",
+    "changelog": "Журнал изменений"
+  },
+  "portal": {
+    "title": "Портал клиента",
+    "description": "Позвольте клиентам входить в систему и просматривать свои транспортные средства, счета, предложения и осмотры. Клиенты также могут отправлять заявки на обслуживание.",
+    "enablePortal": "Включить портал клиента",
+    "enablePortalHint": "Клиенты с адресом эл. почты могут войти по волшебной ссылке",
+    "enabled": "Портал клиента включён",
+    "disabled": "Портал клиента отключён",
+    "failedUpdate": "Не удалось обновить настройку",
+    "customSlug": "Пользовательский адрес портала",
+    "save": "Сохранить",
+    "slugSaved": "Адрес портала сохранён",
+    "slugRemoved": "Адрес портала удалён",
+    "failedUpdateSlug": "Не удалось обновить адрес",
+    "slugHint": "Строчные буквы, цифры, дефисы и символы подчёркивания. 3-48 символов. Оставьте пустым для использования URL по умолчанию.",
+    "portalUrl": "URL портала",
+    "urlCopied": "URL портала скопирован",
+    "shareHint": "Поделитесь этой ссылкой с клиентами, чтобы они могли войти в свой портал."
+  },
+  "email": {
+    "title": "Провайдер эл. почты",
+    "description": "Настройте провайдера эл. почты вашей организации для отправки счетов и предложений. Если не настроен, будет использоваться провайдер платформы по умолчанию.",
+    "useCustomLabel": "Использовать собственного провайдера эл. почты",
+    "useCustomHint": "Включите для настройки собственного провайдера вместо провайдера платформы по умолчанию",
+    "platformDefaultInfo": "Ваша организация использует провайдера эл. почты платформы по умолчанию. Счета и предложения будут отправляться через настройки платформы.",
+    "smtpHost": "SMTP-сервер",
+    "smtpHostPlaceholder": "smtp.example.com",
+    "smtpPort": "Порт SMTP",
+    "smtpPortPlaceholder": "587",
+    "username": "Имя пользователя",
+    "usernamePlaceholder": "user@example.com",
+    "password": "Пароль",
+    "fromEmail": "Адрес отправителя",
+    "fromEmailPlaceholder": "noreply@example.com",
+    "fromEmailDomainPlaceholder": "noreply@yourdomain.com",
+    "fromName": "Имя отправителя",
+    "fromNamePlaceholder": "Ваша мастерская",
+    "tlsConnection": "TLS-подключение (порт 465)",
+    "tlsConnectionHint": "Включите для неявного TLS. Отключите для STARTTLS (порт 587/25).",
+    "verifyTlsCerts": "Проверять сертификаты TLS",
+    "verifyTlsCertsHint": "Отключите для разрешения самоподписанных сертификатов (не рекомендуется для продакшена)",
+    "requireTlsUpgrade": "Требовать обновление до TLS",
+    "requireTlsUpgradeHint": "Принудительное обновление TLS на STARTTLS-соединениях",
+    "apiKey": "Ключ API",
+    "apiKeyHintResend": "Получите ключ API на",
+    "serverToken": "Токен сервера",
+    "serverTokenHintPostmark": "Получите токен сервера на",
+    "apiKeyHintMailgun": "Получите ключ API на",
+    "domain": "Домен",
+    "domainPlaceholder": "mg.yourdomain.com",
+    "region": "Регион",
+    "apiKeyHintSendGrid": "Получите ключ API на",
+    "accessKeyId": "ID ключа доступа",
+    "secretAccessKey": "Секретный ключ доступа",
+    "awsRegion": "Регион AWS",
+    "awsRegionPlaceholder": "us-east-1",
+    "awsRegionHint": "Регион AWS, в котором настроен SES (напр. us-east-1, eu-west-1)",
+    "sendTestEmail": "Отправить тестовое письмо",
+    "testEmailHint": "Сначала сохраните настройки, затем отправьте тестовое письмо на ваш аккаунт",
+    "saveSettings": "Сохранить настройки",
+    "savedDefault": "Настройки почты сохранены — используется провайдер платформы по умолчанию",
+    "saved": "Настройки почты сохранены",
+    "failedSave": "Не удалось сохранить настройки",
+    "testSentTo": "Тестовое письмо отправлено на {email}",
+    "testFailed": "Тест отправки почты не удался"
+  },
+  "sms": {
+    "title": "Провайдер SMS",
+    "description": "Настройте провайдера SMS для двустороннего обмена сообщениями с клиентами. Каждая организация использует собственного SMS-провайдера и номер телефона.",
+    "enableLabel": "Включить SMS-сообщения",
+    "enableHint": "Отправляйте и получайте SMS-сообщения с клиентами",
+    "disabledInfo": "SMS-сообщения отключены. Включите для отправки и получения текстовых сообщений с клиентами.",
+    "phoneNumber": "Номер телефона",
+    "phoneNumberPlaceholder": "+79991234567",
+    "phoneNumberHint": "Ваш номер телефона с поддержкой SMS в формате E.164 (напр. +79991234567)",
+    "accountSid": "SID аккаунта",
+    "authToken": "Токен авторизации",
+    "apiKey": "Ключ API",
+    "apiSecret": "Секрет API",
+    "webhookUrl": "URL вебхука",
+    "webhookUrlHint": "Настройте этот URL в панели управления {provider} для получения входящих SMS.",
+    "webhookCopied": "URL вебхука скопирован",
+    "testPhoneNumber": "Тестовый номер телефона",
+    "testPhonePlaceholder": "+79999876543",
+    "sendTestSms": "Отправить тестовое SMS",
+    "testSmsHint": "Сначала сохраните настройки, затем введите номер получателя для отправки тестового SMS",
+    "enterTestPhone": "Введите номер телефона для отправки тестового SMS",
+    "saveSettings": "Сохранить настройки",
+    "savedDisabled": "Настройки SMS сохранены — SMS отключены",
+    "saved": "Настройки SMS сохранены",
+    "failedSave": "Не удалось сохранить настройки",
+    "testSentTo": "Тестовое SMS отправлено на {phone}",
+    "testFailed": "Тест SMS не удался"
+  },
+  "subscription": {
+    "title": "Тарифный план",
+    "description": "Текущий тариф подписки Torqvoice вашей организации и доступные функции.",
+    "currentPlan": "Текущий тариф",
+    "planFree": "Бесплатный",
+    "planPro": "Torq Pro",
+    "planEnterprise": "Корпоративный",
+    "price": "{price}/{interval}",
+    "perYear": "год",
+    "perMonth": "мес",
+    "statusActive": "Активна",
+    "statusCanceling": "Отменяется",
+    "statusPastDue": "Просрочена",
+    "statusCanceled": "Отменена",
+    "billingPeriod": "Период оплаты",
+    "billingPeriodDates": "{start} — {end}",
+    "nextRenewal": "Следующее продление",
+    "cancelingWarning": "Ваша подписка будет отменена {date}. После этого вы будете переведены на бесплатный тариф.",
+    "pastDueWarning": "Ваш платёж просрочен. Обновите способ оплаты, чтобы избежать прерывания сервиса.",
+    "featuresTitle": "Функции тарифа",
+    "featuresDescription": "Что включено в ваш текущий тариф.",
+    "featureCustomers": "Клиенты",
+    "featureTeamMembers": "Участники команды",
+    "featureSmtp": "Пользовательская почта (SMTP)",
+    "featureApi": "Доступ к API",
+    "featurePayments": "Онлайн-платежи",
+    "featureCustomFields": "Пользовательские поля",
+    "featureSms": "SMS-сообщения",
+    "featureCustomerPortal": "Портал клиента",
+    "featureCustomTemplates": "Пользовательские шаблоны",
+    "featureBrandingRemoved": "Брендинг удалён",
+    "usageOf": "{used} / {limit}",
+    "usageUnlimited": "{used} / Без ограничений",
+    "included": "Включено",
+    "notIncluded": "Не включено",
+    "manageTitle": "Управление подпиской",
+    "manageBilling": "Управление оплатой",
+    "manageBillingDescription": "Обновите способ оплаты, просмотрите счета и управляйте платёжными реквизитами.",
+    "cancelSubscription": "Отменить подписку",
+    "resumeSubscription": "Возобновить подписку",
+    "cancelDialogTitle": "Отменить подписку",
+    "cancelDialogDescription": "Ваша подписка останется активной до {date}. После этого вы будете переведены на бесплатный тариф.",
+    "cancelDialogConfirm": "Да, отменить",
+    "cancelDialogCancel": "Сохранить подписку",
+    "cancelSuccess": "Подписка будет отменена в конце периода оплаты",
+    "cancelError": "Не удалось отменить подписку",
+    "resumeSuccess": "Подписка успешно возобновлена",
+    "resumeError": "Не удалось возобновить подписку",
+    "billingPortalError": "Не удалось открыть платёжный портал",
+    "upgradeTitle": "Обновить тариф",
+    "upgradeDescription": "Разблокируйте больше функций, обновив тариф.",
+    "upgradeToProDescription": "Обновитесь для неограниченных клиентов, участников команды, отчётов, SMTP, доступа к API и многого другого.",
+    "upgradeToEnterpriseDescription": "Нужно более 5 участников команды? Обновитесь до корпоративного тарифа для 50 пользователей.",
+    "upgradeToPro": "Обновить до Pro — $99/год",
+    "upgradeToEnterprise": "Корпоративный — $140/год",
+    "upgradeDialogTitle": "Обновить до корпоративного тарифа",
+    "upgradeDialogDescription": "Ваш текущий тариф Pro будет обновлён до корпоративного. Вам будет начислена только пропорциональная разница за оставшийся период оплаты.",
+    "upgradeConfirm": "Подтвердить обновление",
+    "upgradeCalculating": "Расчёт пропорциональной суммы...",
+    "upgradeAmountDue": "К оплате сейчас",
+    "upgradeSuccess": "Успешно обновлено до корпоративного тарифа!",
+    "upgradeError": "Не удалось обновить подписку"
+  },
+  "ai": {
+    "title": "ИИ-ассистент",
+    "description": "Настройте ИИ для автоматического создания описаний обслуживания и обобщения истории транспортных средств.",
+    "enableLabel": "Включить ИИ",
+    "enableHint": "Используйте функции ИИ по всему приложению",
+    "disabledInfo": "Включите ИИ для использования таких функций, как автоматическое создание описаний обслуживания и обобщение истории транспортных средств.",
+    "provider": "Провайдер ИИ",
+    "apiKey": "Ключ API",
+    "apiKeyHint": "Ваш ключ API {provider}. Хранится в безопасности и никогда не передаётся.",
+    "model": "Модель",
+    "testConnection": "Проверить подключение",
+    "testSuccess": "Подключение к ИИ успешно!",
+    "testFailed": "Подключение не удалось. Проверьте ключ API и модель.",
+    "saved": "Настройки ИИ сохранены",
+    "failedSave": "Не удалось сохранить настройки ИИ",
+    "saveSettings": "Сохранить настройки",
+    "featuresTitle": "Включённые функции ИИ:",
+    "featureServiceNotes": "Автоматическое создание профессиональных описаний обслуживания из запчастей и работ",
+    "featureHistorySummary": "Обобщение истории обслуживания транспортного средства одним нажатием",
+    "cost": {
+      "high": "Высокая стоимость",
+      "medium": "Средняя",
+      "low": "Низкая стоимость"
+    }
+  }
+}

+ 156 - 0
messages/ru/share.json

@@ -0,0 +1,156 @@
+{
+  "invoice": {
+    "title": "Счёт",
+    "downloadPdf": "Скачать PDF",
+    "paymentReceived": "Оплата получена!",
+    "paymentApplied": "{amount} зачтено по этому счёту.",
+    "verifyingPayment": "Проверка оплаты...",
+    "balanceDue": "К оплате",
+    "fullAmount": "Полная сумма",
+    "partialPayment": "Частичная оплата",
+    "enterAmount": "Введите сумму",
+    "payWithCard": "Оплатить {amount} картой",
+    "payWithVipps": "Оплатить {amount} через Vipps",
+    "payWithPaypal": "Оплатить {amount} через PayPal",
+    "errorInvalidAmount": "Введите корректную сумму.",
+    "errorExceedsBalance": "Сумма не может превышать {amount}.",
+    "errorVerifyFailed": "Не удалось подтвердить оплату. Возможно, она ещё обрабатывается.",
+    "errorVerifyRequest": "Не удалось проверить статус оплаты.",
+    "errorCheckoutFailed": "Не удалось начать оплату.",
+    "errorPaymentFailed": "Не удалось инициировать оплату. Попробуйте снова.",
+    "billTo": "Плательщик",
+    "vehicle": "Транспортное средство",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "service": "Обслуживание",
+    "type": "Тип: {type}",
+    "tech": "Механик: {tech}",
+    "parts": "Запчасти",
+    "partNumber": "Арт. №",
+    "description": "Описание",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "labor": "Работа",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "ratePerHour": "{rate}/ч",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountPercent": "Скидка ({percent}%)",
+    "tax": "Налог ({rate}%)",
+    "paid": "Оплачено",
+    "paidInFull": "ПОЛНОСТЬЮ ОПЛАЧЕНО",
+    "amountDue": "К оплате",
+    "paymentInformation": "Платёжная информация",
+    "bankAccount": "Банковский счёт",
+    "orgNumber": "Номер организации",
+    "paymentTerms": "Условия оплаты",
+    "dueDate": "Срок оплаты",
+    "serviceImages": "Фотографии обслуживания ({count})",
+    "serviceVideos": "Видео обслуживания ({count})",
+    "diagnosticReports": "Диагностические отчёты ({count})",
+    "notes": "Заметки",
+    "diagnosticNotes": "Диагностические заметки",
+    "tel": "Тел: {phone}"
+  },
+  "quote": {
+    "title": "Предложение",
+    "downloadPdf": "Скачать PDF",
+    "preparedFor": "Подготовлено для",
+    "vehicle": "Транспортное средство",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "quoteDetails": "Детали предложения",
+    "description": "Описание",
+    "parts": "Запчасти",
+    "partNumber": "Арт. №",
+    "partDescription": "Описание",
+    "qty": "Кол-во",
+    "unitPrice": "Цена за ед.",
+    "total": "Итого",
+    "labor": "Работа",
+    "laborDescription": "Описание",
+    "hours": "Часы",
+    "rate": "Ставка",
+    "ratePerHour": "{rate}/ч",
+    "subtotal": "Подитог",
+    "discount": "Скидка",
+    "discountPercent": "Скидка ({percent}%)",
+    "tax": "Налог ({rate}%)",
+    "images": "Изображения ({count})",
+    "documents": "Документы",
+    "requestChanges": "Запросить изменения",
+    "acceptQuote": "Принять предложение",
+    "whatChanges": "Какие изменения вы хотите?",
+    "changePlaceholder": "Опишите желаемые изменения...",
+    "cancel": "Отмена",
+    "submitRequest": "Отправить запрос",
+    "quoteAccepted": "Предложение принято",
+    "quoteAcceptedMessage": "Спасибо! Это предложение принято. Мастерская свяжется с вами в ближайшее время.",
+    "changesRequested": "Запрошены изменения",
+    "changesRequestedMessage": "Ваш запрос на изменения отправлен. Мастерская рассмотрит его и свяжется с вами.",
+    "validUntil": "Действительно до: {date}",
+    "status": {
+      "draft": "Черновик",
+      "sent": "Отправлено",
+      "accepted": "Принято",
+      "rejected": "Отклонено",
+      "expired": "Истекло",
+      "converted": "Конвертировано",
+      "changes_requested": "Запрошены изменения"
+    },
+    "tel": "Тел: {phone}"
+  },
+  "inspection": {
+    "title": "Осмотр транспортного средства",
+    "vehicle": "Транспортное средство",
+    "customer": "Клиент",
+    "vin": "VIN: {vin}",
+    "plate": "Госномер: {plate}",
+    "mileage": "Пробег: {mileage}",
+    "inspected": "осмотрено",
+    "pass": "Норма",
+    "fail": "Неисправность",
+    "attention": "Внимание",
+    "notInspected": "Не осмотрено",
+    "quoteAvailable": "Для этого осмотра подготовлено предложение",
+    "quoteAvailableDescription": "Просмотрите предложение, чтобы увидеть стоимость и одобрить рекомендуемые работы.",
+    "viewQuote": "Просмотреть предложение",
+    "quoteRequested": "Предложение запрошено",
+    "quoteRequestedCancel": "— Отменить",
+    "requestQuote": "Запросить предложение",
+    "downloadPdf": "Скачать PDF",
+    "notes": "Заметки",
+    "quoteCancelled": "Запрос на предложение отменён",
+    "cancelFailed": "Не удалось отменить запрос"
+  },
+  "terms": {
+    "title": "Условия продажи (Salgsvilkar)",
+    "contact": "Контакт",
+    "tel": "Тел: {phone}"
+  },
+  "quoteRequest": {
+    "title": "Запросить предложение",
+    "description": "Выберите пункты, по которым вы хотите получить предложение на ремонт.",
+    "selectAll": "Выбрать все",
+    "deselectAll": "Снять выделение",
+    "selectedCount": "{selected} из {total} выбрано",
+    "fail": "Неисправность",
+    "attention": "Внимание",
+    "message": "Сообщение",
+    "messageOptional": "(необязательно)",
+    "messagePlaceholder": "Дополнительные детали или вопросы...",
+    "cancel": "Отмена",
+    "submitRequest": "Отправить запрос",
+    "selectAtLeastOne": "Выберите хотя бы один пункт",
+    "submitSuccess": "Запрос на предложение отправлен!",
+    "submitError": "Не удалось отправить запрос"
+  },
+  "common": {
+    "poweredBy": "Работает на",
+    "termsOfSale": "Условия продажи",
+    "portalMessage": "Просматривайте свои транспортные средства, счета и многое другое на вашем",
+    "customerPortal": "портале клиента"
+  }
+}

+ 225 - 0
messages/ru/vehicles.json

@@ -0,0 +1,225 @@
+{
+  "list": {
+    "active": "Активные",
+    "archived": "Архивные",
+    "searchPlaceholder": "Поиск по госномеру, марке, модели, VIN...",
+    "addVehicle": "Добавить транспортное средство",
+    "emptySearch": "Нет транспортных средств, соответствующих поиску.",
+    "emptyArchived": "Нет архивных транспортных средств.",
+    "empty": "Транспортных средств пока нет.",
+    "table": {
+      "plate": "Госномер",
+      "vehicle": "Транспортное средство",
+      "customer": "Клиент",
+      "mileage": "Пробег",
+      "services": "Обслуживания"
+    },
+    "edit": "Редактировать",
+    "archive": "Архивировать",
+    "unarchive": "Разархивировать",
+    "delete": "Удалить",
+    "deleteTitle": "Удалить транспортное средство",
+    "deleteDescription": "Удалить {name}? Все связанные записи обслуживания и заметки будут удалены.",
+    "unarchiveTitle": "Разархивировать транспортное средство",
+    "unarchiveDescription": "Восстановить {name} в список активных транспортных средств?",
+    "vehicleUnarchived": "Транспортное средство разархивировано",
+    "unarchiveError": "Не удалось разархивировать транспортное средство",
+    "deleteError": "Не удалось удалить транспортное средство",
+    "error": "Не удалось загрузить транспортные средства"
+  },
+  "detail": {
+    "vehicleArchived": "Это транспортное средство архивировано",
+    "backToVehicles": "К списку транспортных средств",
+    "newWorkOrder": "Новый заказ-наряд",
+    "editVehicle": "Редактировать транспортное средство",
+    "unarchive": "Разархивировать",
+    "archive": "Архивировать",
+    "delete": "Удалить",
+    "notFound": "Транспортное средство не найдено",
+    "predictedKm": "Прогноз км",
+    "predictedMileage": "Прогноз пробега",
+    "predictedTooltip": "Расчётный текущий пробег на основе тенденций истории обслуживания. Процент показывает уверенность прогноза.",
+    "dismissed": "Отклонено",
+    "undo": "Отменить",
+    "serviceOverdue": "Обслуживание просрочено",
+    "serviceSoon": "Скоро обслуживание",
+    "services": "обслуживаний",
+    "purchased": "Куплено {date}",
+    "tabs": {
+      "services": "Обслуживания",
+      "quotes": "Предложения",
+      "inspections": "Осмотры",
+      "notes": "Заметки",
+      "reminders": "Напоминания"
+    },
+    "noteDeleted": "Заметка удалена",
+    "noteDeleteError": "Не удалось удалить",
+    "reminderDeleted": "Напоминание удалено",
+    "reminderDeleteError": "Не удалось удалить",
+    "vehicleUnarchived": "Транспортное средство разархивировано",
+    "unarchiveError": "Не удалось разархивировать",
+    "vehicleDeleted": "Транспортное средство удалено",
+    "vehicleDeleteError": "Не удалось удалить транспортное средство",
+    "deleteTitle": "Удалить транспортное средство?",
+    "deleteDescription": "Это навсегда удалит {name} и все связанные записи обслуживания, заметки и напоминания. Это действие необратимо.",
+    "edit": "Редактировать",
+    "aiSummary": "ИИ-сводка",
+    "aiSummaryTitle": "Сводка истории обслуживания",
+    "aiSummaryGenerate": "Сгенерировать",
+    "aiSummaryRegenerate": "Перегенерировать",
+    "aiSummaryGenerated": "ИИ-сводка сгенерирована",
+    "aiSummaryUpdated": "Обновлено {date}",
+    "aiSummaryEmpty": "Нажмите «Сгенерировать» для создания ИИ-сводки истории обслуживания этого транспортного средства.",
+    "aiSummaryError": "Не удалось сгенерировать сводку",
+    "summaryMajorWork": "Основные работы",
+    "summaryRecurringIssues": "Повторяющиеся проблемы",
+    "summaryUpcoming": "Предстоящее ТО",
+    "commonIssues": "Типичные проблемы",
+    "commonIssuesEmpty": "Нажмите «Сгенерировать» для поиска типичных критических проблем этого транспортного средства.",
+    "commonIssuesTitle": "Типичные критические проблемы — {make} {model}",
+    "severityVeryCommon": "Очень распространено — отмечено большим количеством владельцев",
+    "severityCommon": "Распространено — часто встречающаяся проблема",
+    "severityModerate": "Умеренно — отмечено некоторыми владельцами",
+    "severityUncommon": "Нечасто — отмечено немногими владельцами",
+    "severityRare": "Редко — единичные случаи, но критично при возникновении"
+  },
+  "quotes": {
+    "newQuote": "Новое предложение",
+    "empty": "Предложений пока нет",
+    "columnNumber": "Предложение №",
+    "columnTitle": "Название",
+    "columnStatus": "Статус",
+    "columnDate": "Дата",
+    "columnTotal": "Итого"
+  },
+  "inspections": {
+    "newInspection": "Новый осмотр",
+    "empty": "Осмотров пока нет",
+    "template": "Шаблон",
+    "progress": "Прогресс",
+    "status": "Статус",
+    "date": "Дата",
+    "completed": "Завершён",
+    "inProgress": "В процессе"
+  },
+  "reminders": {
+    "active": "Активные",
+    "completed": "Завершённые",
+    "all": "Все",
+    "addReminder": "Добавить напоминание",
+    "emptyActive": "Нет активных напоминаний",
+    "emptyCompleted": "Нет завершённых напоминаний",
+    "empty": "Напоминаний пока нет",
+    "overdue": "Просрочено",
+    "dueSoon": "Скоро срок",
+    "due": "Срок {date}",
+    "dueAt": "Срок при {mileage} {unit}",
+    "editTitle": "Редактировать напоминание",
+    "addTitle": "Добавить напоминание",
+    "titleLabel": "Название *",
+    "titlePlaceholder": "напр. Замена масла",
+    "descriptionLabel": "Описание",
+    "descriptionPlaceholder": "Дополнительные сведения...",
+    "dueDateLabel": "Дата срока",
+    "dueMileageLabel": "Пробег до срока",
+    "dueMileagePlaceholder": "напр. 50000",
+    "reminderUpdated": "Напоминание обновлено",
+    "reminderCreated": "Напоминание создано",
+    "saveError": "Не удалось {action} напоминание"
+  },
+  "services": {
+    "searchPlaceholder": "Поиск записей...",
+    "allTypes": "Все типы",
+    "maintenance": "Техобслуживание",
+    "repair": "Ремонт",
+    "upgrade": "Модернизация",
+    "inspection": "Осмотр",
+    "newWorkOrder": "Новый заказ-наряд",
+    "table": {
+      "date": "Дата",
+      "title": "Название",
+      "type": "Тип",
+      "status": "Статус",
+      "mileage": "Пробег",
+      "technician": "Механик",
+      "files": "Файлы",
+      "total": "Итого"
+    },
+    "emptyFiltered": "Нет записей обслуживания, соответствующих фильтрам.",
+    "empty": "Записей обслуживания пока нет.",
+    "showing": "Показано {start}-{end} из {total}",
+    "perPage": "на странице",
+    "page": "Страница {page} из {totalPages}"
+  },
+  "notes": {
+    "addNote": "Добавить заметку",
+    "table": {
+      "title": "Название",
+      "content": "Содержание",
+      "date": "Дата"
+    },
+    "empty": "Заметок пока нет.",
+    "pin": "Закрепить",
+    "unpin": "Открепить",
+    "delete": "Удалить",
+    "showing": "Показано {start}-{end} из {total}",
+    "perPage": "на странице",
+    "page": "Страница {page} из {totalPages}",
+    "editTitle": "Редактировать заметку",
+    "addTitle": "Добавить заметку",
+    "titleLabel": "Название *",
+    "titlePlaceholder": "Название заметки",
+    "contentLabel": "Содержание *",
+    "contentPlaceholder": "Напишите заметку...",
+    "noteUpdated": "Заметка обновлена",
+    "noteCreated": "Заметка создана",
+    "titleRequired": "Название и содержание обязательны",
+    "saveError": "Не удалось {action} заметку"
+  },
+  "form": {
+    "editTitle": "Редактировать транспортное средство",
+    "addTitle": "Добавить новое транспортное средство",
+    "photo": "Фото",
+    "clickToUpload": "Нажмите для загрузки фото",
+    "imageFormats": "JPEG, PNG, WebP до 5МБ",
+    "imageTooLarge": "Изображение слишком большое",
+    "maxFileSize": "Максимальный размер файла 5МБ",
+    "uploadingImage": "Загрузка изображения...",
+    "uploadFailed": "Загрузка не удалась",
+    "imageUploaded": "Изображение загружено",
+    "customer": "Клиент",
+    "searchCustomers": "Поиск клиентов...",
+    "noCustomerFound": "Клиент не найден.",
+    "noCustomer": "Без клиента",
+    "addCustomer": "Добавить клиента",
+    "make": "Марка *",
+    "model": "Модель *",
+    "year": "Год *",
+    "mileage": "Пробег",
+    "vin": "VIN",
+    "licensePlate": "Госномер",
+    "color": "Цвет",
+    "fuelType": "Тип топлива",
+    "gasoline": "Бензин",
+    "diesel": "Дизель",
+    "electric": "Электро",
+    "hybrid": "Гибрид",
+    "other": "Другое",
+    "transmission": "Трансмиссия",
+    "automatic": "Автоматическая",
+    "manual": "Механическая",
+    "cvt": "Вариатор",
+    "engineSize": "Объём двигателя",
+    "vehicleUpdated": "Транспортное средство обновлено",
+    "vehicleAdded": "Транспортное средство добавлено",
+    "saveError": "Не удалось сохранить транспортное средство"
+  },
+  "archiveDialog": {
+    "title": "Архивировать транспортное средство",
+    "description": "Архивировать <span>{name}</span>? Оно будет скрыто из основного списка, но вся история обслуживания сохранится.",
+    "reasonLabel": "Причина (необязательно)",
+    "reasonPlaceholder": "напр. Продано, списано, выведено из эксплуатации...",
+    "archived": "{name} архивировано",
+    "archiveError": "Не удалось архивировать транспортное средство"
+  }
+}

+ 75 - 0
messages/ru/workBoard.json

@@ -0,0 +1,75 @@
+{
+  "page": {
+    "error": "Не удалось загрузить доску работ"
+  },
+  "toolbar": {
+    "today": "Сегодня",
+    "day": "День",
+    "week": "Неделя",
+    "presenter": "Презентация",
+    "addTechnician": "Добавить механика"
+  },
+  "board": {
+    "days": {
+      "mon": "Пн",
+      "tue": "Вт",
+      "wed": "Ср",
+      "thu": "Чт",
+      "fri": "Пт",
+      "sat": "Сб",
+      "sun": "Вс"
+    },
+    "noTechnicians": "Механиков пока нет",
+    "noTechniciansDescription": "Добавьте механика, чтобы начать планирование работ на доске.",
+    "failedMove": "Не удалось переместить назначение",
+    "failedAssign": "Не удалось назначить работу",
+    "failedRemove": "Не удалось удалить назначение",
+    "createNewWorkorder": "Создать новый заказ-наряд"
+  },
+  "unassigned": {
+    "title": "Неназначенные работы ({count})",
+    "searchPlaceholder": "Поиск работ...",
+    "empty": "Нет неназначенных работ"
+  },
+  "technician": {
+    "editTitle": "Редактировать механика",
+    "addTitle": "Добавить механика",
+    "name": "Имя",
+    "namePlaceholder": "напр. Иван Петров",
+    "color": "Цвет",
+    "linkMember": "Привязать к участнику организации (необязательно)",
+    "standalone": "Независимый механик",
+    "deleteTitle": "Удалить механика",
+    "deleteDescription": "Удалить \"{name}\" с доски работ? Все существующие назначения также будут удалены.",
+    "addTechnician": "Добавить механика"
+  },
+  "jobDetail": {
+    "viewDetails": "Просмотреть детали",
+    "remove": "Удалить",
+    "startTime": "Время начала",
+    "endTime": "Время окончания",
+    "failedDuration": "Не удалось обновить длительность",
+    "failedTimes": "Не удалось обновить время",
+    "endBeforeStart": "Время окончания должно быть позже времени начала"
+  },
+  "presenter": {
+    "title": "Доска работ",
+    "day": "День",
+    "status": "Статус",
+    "week": "Неделя",
+    "today": "Сегодня",
+    "timeline": "Хронология",
+    "live": "Онлайн",
+    "disconnected": "Обновления в реальном времени отключены",
+    "noTechnicians": "Механики не настроены",
+    "noJobsToday": "На сегодня работ нет",
+    "job": "{count} работа",
+    "jobs": "{count} работ",
+    "statusLabels": {
+      "pending": "Ожидает",
+      "inProgress": "В работе",
+      "waitingParts": "Ожидание запчастей",
+      "completed": "Завершено"
+    }
+  }
+}

+ 80 - 0
messages/ru/workOrders.json

@@ -0,0 +1,80 @@
+{
+  "page": {
+    "error": "Не удалось загрузить заказ-наряды"
+  },
+  "list": {
+    "searchPlaceholder": "Поиск заказ-нарядов...",
+    "newWorkOrder": "Новый заказ-наряд",
+    "selectVehicle": "Выберите транспортное средство",
+    "empty": "Заказ-наряды не найдены.",
+    "statusUpdated": "Статус обновлён",
+    "statusTabs": {
+      "all": "Все",
+      "active": "Активные",
+      "pending": "Ожидающие",
+      "inProgress": "В работе",
+      "waitingParts": "Ожидание запчастей",
+      "completed": "Завершённые"
+    },
+    "statusActions": {
+      "startWork": "Начать работу",
+      "waitingParts": "Ожидание запчастей",
+      "complete": "Завершить",
+      "resumeWork": "Возобновить работу",
+      "reopen": "Открыть заново"
+    },
+    "table": {
+      "invoice": "Счёт №",
+      "vehicle": "Транспортное средство",
+      "customer": "Клиент",
+      "title": "Название",
+      "status": "Статус",
+      "tech": "Механик",
+      "date": "Дата",
+      "total": "Итого"
+    },
+    "emailSubject": "Обновление статуса транспортного средства: {status}"
+  },
+  "vehiclePicker": {
+    "title": "Выберите транспортное средство для заказ-наряда",
+    "searchPlaceholder": "Поиск транспортных средств...",
+    "empty": "Транспортные средства не найдены.",
+    "emptySearch": "Нет транспортных средств, соответствующих поиску.",
+    "addNewVehicle": "Добавить новое транспортное средство",
+    "noPlate": "Без госномера",
+    "make": "Марка *",
+    "model": "Модель *",
+    "year": "Год *",
+    "licensePlate": "Госномер",
+    "customer": "Клиент",
+    "selectCustomer": "Выберите клиента (необязательно)",
+    "createNewCustomer": "Создать нового клиента",
+    "newCustomer": "Новый клиент",
+    "customerName": "Имя *",
+    "customerNamePlaceholder": "Имя клиента",
+    "phone": "Телефон",
+    "phonePlaceholder": "Номер телефона",
+    "creating": "Создание...",
+    "createAndContinue": "Создать и продолжить",
+    "invalidYear": "Введите корректный год",
+    "customerNameRequired": "Имя клиента обязательно",
+    "failedCreateCustomer": "Не удалось создать клиента",
+    "failedCreateVehicle": "Не удалось создать транспортное средство",
+    "somethingWentWrong": "Что-то пошло не так"
+  },
+  "notify": {
+    "title": "Уведомить {name}",
+    "description": "Отправить уведомление клиенту об этом обновлении.",
+    "sms": "SMS",
+    "email": "Эл. почта",
+    "noPhone": "(телефон не указан)",
+    "noEmail": "(эл. почта не указана)",
+    "notAvailable": "(недоступно)",
+    "skip": "Пропустить",
+    "send": "Отправить",
+    "smsSent": "SMS отправлено",
+    "emailSent": "Письмо отправлено",
+    "failedSms": "Не удалось отправить SMS",
+    "failedEmail": "Не удалось отправить письмо"
+  }
+}

+ 17 - 2
messages/tr/settings.json

@@ -42,6 +42,10 @@
         "title": "Para Birimi",
         "description": "Para birimi & vergi varsayılanları"
       },
+      "localization": {
+        "title": "Yerelleştirme",
+        "description": "Dil, para birimi, tarih ve saat"
+      },
       "customFields": {
         "title": "Özel Alanlar",
         "description": "Özel veri alanları tanımlayın"
@@ -66,7 +70,10 @@
         "title": "Bakım",
         "description": "Öngörülen servis aralıkları"
       },
-      "ai": { "title": "AI", "description": "Yapay zeka saglayici ve ozellikler" },
+      "ai": {
+        "title": "AI",
+        "description": "Yapay zeka saglayici ve ozellikler"
+      },
       "appearance": {
         "title": "Görünüm",
         "description": "Tema & görüntüleme"
@@ -588,9 +595,17 @@
       "IDR": "Endonezya Rupisi",
       "MYR": "Malezya Ringgiti",
       "AED": "BAE Dirhemi",
-      "SAR": "Suudi Riyali"
+      "SAR": "Suudi Riyali",
+      "RUB": "Rus Rublesi"
     }
   },
+  "localization": {
+    "languageTitle": "Dil",
+    "languageDescription": "Uygulamanın görüntüleme dilini seçin.",
+    "language": "Dil",
+    "saved": "Yerelleştirme ayarları kaydedildi",
+    "saveSettings": "Ayarları kaydet"
+  },
   "customFields": {
     "title": "Özel alanlar",
     "description": "Servis kayıtlarında ve tekliflerde görünen özel alanları tanımlayın.",

+ 1 - 0
src/app/(authenticated)/settings/currency/currency-settings.tsx

@@ -56,6 +56,7 @@ const CURRENCIES = [
   { code: "MYR", key: "MYR" },
   { code: "AED", key: "AED" },
   { code: "SAR", key: "SAR" },
+  { code: "RUB", key: "RUB" },
 ];
 
 export function CurrencySettings({ settings }: { settings: Record<string, string> }) {

+ 380 - 0
src/app/(authenticated)/settings/localization/localization-settings.tsx

@@ -0,0 +1,380 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useLocale, useTranslations } from "next-intl";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { toast } from "sonner";
+import { setSettings } from "@/features/settings/Actions/settingsActions";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import { Calendar, Clock, Coins, Globe, Loader2, Moon, Palette, Save, Sun } from "lucide-react";
+import { formatCurrency, formatDate, formatDateTime, DEFAULT_DATE_FORMAT } from "@/lib/format";
+import { useTheme } from "@/components/theme-provider";
+import { setLocale } from "@/i18n/actions";
+import { locales, localeNames } from "@/i18n/config";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
+
+const CURRENCIES = [
+  { code: "USD", key: "USD" },
+  { code: "EUR", key: "EUR" },
+  { code: "GBP", key: "GBP" },
+  { code: "NOK", key: "NOK" },
+  { code: "SEK", key: "SEK" },
+  { code: "DKK", key: "DKK" },
+  { code: "CHF", key: "CHF" },
+  { code: "CAD", key: "CAD" },
+  { code: "AUD", key: "AUD" },
+  { code: "NZD", key: "NZD" },
+  { code: "JPY", key: "JPY" },
+  { code: "CNY", key: "CNY" },
+  { code: "INR", key: "INR" },
+  { code: "BRL", key: "BRL" },
+  { code: "MXN", key: "MXN" },
+  { code: "PLN", key: "PLN" },
+  { code: "CZK", key: "CZK" },
+  { code: "HUF", key: "HUF" },
+  { code: "TRY", key: "TRY" },
+  { code: "ZAR", key: "ZAR" },
+  { code: "KRW", key: "KRW" },
+  { code: "SGD", key: "SGD" },
+  { code: "HKD", key: "HKD" },
+  { code: "THB", key: "THB" },
+  { code: "ISK", key: "ISK" },
+  { code: "RON", key: "RON" },
+  { code: "ILS", key: "ILS" },
+  { code: "PHP", key: "PHP" },
+  { code: "IDR", key: "IDR" },
+  { code: "MYR", key: "MYR" },
+  { code: "AED", key: "AED" },
+  { code: "SAR", key: "SAR" },
+  { code: "RUB", key: "RUB" },
+];
+
+const DATE_FORMAT_OPTIONS = [
+  { value: "MMM d, yyyy", label: "MMM D, YYYY", example: "Feb 15, 2026" },
+  { value: "dd/MM/yyyy", label: "DD/MM/YYYY", example: "15/02/2026" },
+  { value: "MM/dd/yyyy", label: "MM/DD/YYYY", example: "02/15/2026" },
+  { value: "yyyy-MM-dd", label: "YYYY-MM-DD", example: "2026-02-15" },
+  { value: "d. MMM yyyy", label: "D. MMM YYYY", example: "15. Feb 2026" },
+  { value: "d MMMM yyyy", label: "D MMMM YYYY", example: "15 February 2026" },
+];
+
+const TIMEZONE_OPTIONS = [
+  "UTC",
+  "America/New_York",
+  "America/Chicago",
+  "America/Denver",
+  "America/Los_Angeles",
+  "America/Anchorage",
+  "Pacific/Honolulu",
+  "America/Toronto",
+  "America/Vancouver",
+  "America/Mexico_City",
+  "America/Sao_Paulo",
+  "America/Argentina/Buenos_Aires",
+  "Europe/London",
+  "Europe/Paris",
+  "Europe/Berlin",
+  "Europe/Madrid",
+  "Europe/Rome",
+  "Europe/Amsterdam",
+  "Europe/Brussels",
+  "Europe/Zurich",
+  "Europe/Vienna",
+  "Europe/Stockholm",
+  "Europe/Oslo",
+  "Europe/Copenhagen",
+  "Europe/Helsinki",
+  "Europe/Warsaw",
+  "Europe/Prague",
+  "Europe/Budapest",
+  "Europe/Bucharest",
+  "Europe/Athens",
+  "Europe/Istanbul",
+  "Europe/Moscow",
+  "Asia/Dubai",
+  "Asia/Kolkata",
+  "Asia/Bangkok",
+  "Asia/Singapore",
+  "Asia/Hong_Kong",
+  "Asia/Shanghai",
+  "Asia/Tokyo",
+  "Asia/Seoul",
+  "Australia/Sydney",
+  "Australia/Melbourne",
+  "Australia/Perth",
+  "Pacific/Auckland",
+  "Africa/Johannesburg",
+  "Africa/Cairo",
+  "Africa/Lagos",
+];
+
+export function LocalizationSettings({ settings }: { settings: Record<string, string> }) {
+  const router = useRouter();
+  const t = useTranslations("settings");
+  const { theme, setTheme } = useTheme();
+  const currentLocale = useLocale();
+  const [saving, setSaving] = useState(false);
+
+  // Currency & Tax
+  const [currencyCode, setCurrencyCode] = useState(settings[SETTING_KEYS.CURRENCY_CODE] || "USD");
+  const [taxEnabled, setTaxEnabled] = useState(settings[SETTING_KEYS.TAX_ENABLED] !== "false");
+  const [defaultTaxRate, setDefaultTaxRate] = useState(settings[SETTING_KEYS.DEFAULT_TAX_RATE] || "0");
+
+  // Date & Time
+  const [dateFormat, setDateFormat] = useState(settings[SETTING_KEYS.DATE_FORMAT] || DEFAULT_DATE_FORMAT);
+  const [timeFormat, setTimeFormat] = useState(settings[SETTING_KEYS.TIME_FORMAT] || "12h");
+  const [timezone, setTimezone] = useState(settings[SETTING_KEYS.TIMEZONE] || "");
+
+  const handleSave = async () => {
+    setSaving(true);
+    await setSettings({
+      [SETTING_KEYS.CURRENCY_CODE]: currencyCode,
+      [SETTING_KEYS.TAX_ENABLED]: String(taxEnabled),
+      [SETTING_KEYS.DEFAULT_TAX_RATE]: taxEnabled ? defaultTaxRate : "0",
+      [SETTING_KEYS.DATE_FORMAT]: dateFormat,
+      [SETTING_KEYS.TIME_FORMAT]: timeFormat,
+      [SETTING_KEYS.TIMEZONE]: timezone,
+    });
+    setSaving(false);
+    router.refresh();
+    toast.success(t("localization.saved"));
+  };
+
+  return (
+    <div className="space-y-6">
+      <ReadOnlyBanner />
+
+      {/* Language */}
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="flex flex-row items-center gap-3 pb-4">
+          <Globe className="h-5 w-5 text-muted-foreground" />
+          <CardTitle className="text-lg">{t("localization.languageTitle")}</CardTitle>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          <p className="text-sm text-muted-foreground">
+            {t("localization.languageDescription")}
+          </p>
+          <div className="space-y-2">
+            <Label>{t("localization.language")}</Label>
+            <Select
+              value={currentLocale}
+              onValueChange={async (value) => {
+                await setLocale(value);
+                router.refresh();
+              }}
+            >
+              <SelectTrigger className="w-64">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {locales.map((loc) => (
+                  <SelectItem key={loc} value={loc}>
+                    {localeNames[loc]}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Currency & Tax */}
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="flex flex-row items-center gap-3 pb-4">
+          <Coins className="h-5 w-5 text-muted-foreground" />
+          <CardTitle className="text-lg">{t("currency.title")}</CardTitle>
+        </CardHeader>
+        <CardContent className="space-y-6">
+          <p className="text-sm text-muted-foreground">
+            {t("currency.description")}
+          </p>
+
+          <ReadOnlyWrapper>
+            <div className="space-y-6">
+              <div className="space-y-2">
+                <Label>{t("currency.currencyLabel")}</Label>
+                <Select value={currencyCode} onValueChange={setCurrencyCode}>
+                  <SelectTrigger className="w-64">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {CURRENCIES.map((c) => (
+                      <SelectItem key={c.code} value={c.code}>
+                        {c.code} &mdash; {t("currency.currencies." + c.code)}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+                <p className="text-xs text-muted-foreground">
+                  {t("currency.previewLabel", { value: formatCurrency(1234.56, currencyCode) })}
+                </p>
+              </div>
+
+              <Separator />
+
+              <div className="flex items-center justify-between">
+                <div>
+                  <Label>{t("currency.enableTax")}</Label>
+                  <p className="text-xs text-muted-foreground">
+                    {t("currency.enableTaxHint")}
+                  </p>
+                </div>
+                <Switch checked={taxEnabled} onCheckedChange={setTaxEnabled} />
+              </div>
+
+              {taxEnabled && (
+                <div className="space-y-2">
+                  <Label htmlFor="defaultTaxRate">{t("currency.defaultTaxRate")}</Label>
+                  <Input
+                    id="defaultTaxRate"
+                    type="number"
+                    min="0"
+                    step="0.1"
+                    placeholder="0"
+                    value={defaultTaxRate}
+                    onChange={(e) => setDefaultTaxRate(e.target.value)}
+                    className="w-32"
+                  />
+                  <p className="text-xs text-muted-foreground">
+                    {t("currency.defaultTaxRateHint")}
+                  </p>
+                </div>
+              )}
+            </div>
+          </ReadOnlyWrapper>
+        </CardContent>
+      </Card>
+
+      {/* Date & Time */}
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="flex flex-row items-center gap-3 pb-4">
+          <Calendar className="h-5 w-5 text-muted-foreground" />
+          <CardTitle className="text-lg">{t("appearance.dateTimeTitle")}</CardTitle>
+        </CardHeader>
+        <CardContent className="space-y-6">
+          <p className="text-sm text-muted-foreground">
+            {t("appearance.dateTimeDescription")}
+          </p>
+
+          <ReadOnlyWrapper>
+            <div className="space-y-6">
+              <div className="space-y-2">
+                <Label>{t("appearance.dateFormat")}</Label>
+                <Select value={dateFormat} onValueChange={setDateFormat}>
+                  <SelectTrigger className="w-64">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    {DATE_FORMAT_OPTIONS.map((opt) => (
+                      <SelectItem key={opt.value} value={opt.value}>
+                        {opt.label} ({opt.example})
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="space-y-2">
+                <Label>{t("appearance.timeFormat")}</Label>
+                <Select value={timeFormat} onValueChange={setTimeFormat}>
+                  <SelectTrigger className="w-48">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="12h">{t("appearance.time12h")}</SelectItem>
+                    <SelectItem value="24h">{t("appearance.time24h")}</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="space-y-2">
+                <Label>{t("appearance.timezone")}</Label>
+                <Select value={timezone || "__auto__"} onValueChange={(v) => setTimezone(v === "__auto__" ? "" : v)}>
+                  <SelectTrigger className="w-72">
+                    <SelectValue />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value="__auto__">{t("appearance.timezoneAuto")}</SelectItem>
+                    {TIMEZONE_OPTIONS.map((tz) => (
+                      <SelectItem key={tz} value={tz}>
+                        {tz.replace(/_/g, " ")}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+                <p className="text-xs text-muted-foreground">
+                  {t("appearance.timezoneHint")}
+                </p>
+              </div>
+
+              <div className="rounded-lg border p-3 text-sm text-muted-foreground">
+                <div className="flex items-center gap-2 mb-1">
+                  <Clock className="h-3.5 w-3.5" />
+                  <span className="font-medium text-foreground">{t("appearance.previewLabel")}</span>
+                </div>
+                <p>{t("appearance.dateLabel")}: <span className="font-medium text-foreground">{formatDate(new Date(), dateFormat, timezone || undefined)}</span></p>
+                <p>{t("appearance.dateTimeLabel")}: <span className="font-medium text-foreground">{formatDateTime(new Date(), dateFormat, timeFormat as "12h" | "24h", timezone || undefined)}</span></p>
+              </div>
+            </div>
+          </ReadOnlyWrapper>
+        </CardContent>
+      </Card>
+
+      {/* Theme */}
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="flex flex-row items-center gap-3 pb-4">
+          <Palette className="h-5 w-5 text-muted-foreground" />
+          <CardTitle className="text-lg">{t("appearance.title")}</CardTitle>
+        </CardHeader>
+        <CardContent className="space-y-6">
+          <div className="flex items-center justify-between rounded-lg border p-4">
+            <div className="flex items-center gap-3">
+              {theme === "dark" ? (
+                <Moon className="h-5 w-5 text-muted-foreground" />
+              ) : (
+                <Sun className="h-5 w-5 text-muted-foreground" />
+              )}
+              <div>
+                <Label className="text-sm font-medium">{t("appearance.darkMode")}</Label>
+                <p className="text-xs text-muted-foreground">
+                  {t("appearance.darkModeHint")}
+                </p>
+              </div>
+            </div>
+            <Switch
+              checked={theme === "dark"}
+              onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
+            />
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Save */}
+      <SaveButton>
+        <div className="flex items-center gap-3">
+          <Button onClick={handleSave} disabled={saving}>
+            {saving ? (
+              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+            ) : (
+              <Save className="mr-2 h-4 w-4" />
+            )}
+            {t("localization.saveSettings")}
+          </Button>
+        </div>
+      </SaveButton>
+    </div>
+  );
+}

+ 9 - 0
src/app/(authenticated)/settings/localization/page.tsx

@@ -0,0 +1,9 @@
+import { getSettings } from "@/features/settings/Actions/settingsActions";
+import { LocalizationSettings } from "./localization-settings";
+
+export default async function LocalizationSettingsPage() {
+  const result = await getSettings();
+  const settings = result.success && result.data ? result.data : {};
+
+  return <LocalizationSettings settings={settings} />;
+}

+ 1 - 5
src/app/(authenticated)/settings/settings-nav.tsx

@@ -15,7 +15,6 @@ import {
 import {
   Banknote,
   Building2,
-  Coins,
   CreditCard,
   Database,
   FileText,
@@ -24,10 +23,8 @@ import {
   Info,
   Key,
   Layout,
-
   Mail,
   MessageSquare,
-  Palette,
   Sparkles,
   UserCog,
   UsersRound,
@@ -56,6 +53,7 @@ const settingsCategories: SettingsCategory[] = [
       { key: "subscription", href: "/settings/subscription", icon: CreditCard, cloudOnly: true },
       { key: "account", href: "/settings/account", icon: UserCog },
       { key: "team", href: "/settings/team", icon: UsersRound },
+      { key: "localization", href: "/settings/localization", icon: Globe },
     ],
   },
   {
@@ -64,7 +62,6 @@ const settingsCategories: SettingsCategory[] = [
       { key: "invoice", href: "/settings/invoice", icon: FileText },
       { key: "templates", href: "/settings/templates", icon: Layout, gate: "customTemplates" },
       { key: "payment", href: "/settings/payment", icon: Banknote, gate: "payments" },
-      { key: "currency", href: "/settings/currency", icon: Coins },
     ],
   },
   {
@@ -86,7 +83,6 @@ const settingsCategories: SettingsCategory[] = [
   {
     key: "system",
     items: [
-      { key: "appearance", href: "/settings/appearance", icon: Palette },
       { key: "data", href: "/settings/data", icon: Database },
       { key: "license", href: "/settings/license", icon: Key, selfHostedOnly: true },
       { key: "about", href: "/settings/about", icon: Info },

+ 5 - 5
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -889,15 +889,15 @@ export function InvoiceView({
                         </div>
                         {balanceDue <= 0 ? (
                           <div className="rounded-lg bg-emerald-50 px-4 py-3 dark:bg-emerald-900/20">
-                            <div className="flex justify-between text-lg font-bold text-emerald-600">
-                              <span>{t('balanceDue')}</span>
-                              <span>{t('paidInFull')}</span>
+                            <div className="flex items-center justify-between gap-2 text-lg font-bold text-emerald-600">
+                              <span className="whitespace-nowrap">{t('balanceDue')}</span>
+                              <span className="whitespace-nowrap">{t('paidInFull')}</span>
                             </div>
                           </div>
                         ) : (
                           <div className="rounded-lg bg-amber-50 px-4 py-3 ring-2 ring-amber-400 dark:bg-amber-900/20 dark:ring-amber-600">
-                            <div className="flex justify-between text-lg font-bold">
-                              <span>{t('amountDue')}</span>
+                            <div className="flex items-center justify-between gap-2 text-lg font-bold">
+                              <span className="whitespace-nowrap">{t('amountDue')}</span>
                               <span className="text-amber-700 dark:text-amber-400">
                                 {formatCurrency(balanceDue, currencyCode)}
                               </span>

+ 1 - 0
src/app/api/protected/inspections/[id]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";

+ 1 - 0
src/app/api/protected/quotes/[id]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";

+ 1 - 0
src/app/api/protected/services/[id]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { db } from "@/lib/db";

+ 1 - 0
src/app/api/public/share/inspection/[orgId]/[token]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { db } from "@/lib/db";
 import { InspectionPDF } from "@/features/inspections/Components/InspectionPDF";

+ 1 - 0
src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { db } from "@/lib/db";
 import { InvoicePDF } from "@/features/vehicles/Components/InvoicePDF";

+ 1 - 0
src/app/api/public/share/quote/[orgId]/[token]/pdf/route.ts

@@ -1,5 +1,6 @@
 import { NextResponse } from "next/server";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import { cookies } from "next/headers";
 import { db } from "@/lib/db";
 import { QuotePDF } from "@/features/quotes/Components/QuotePDF";

+ 2 - 2
src/app/layout.tsx

@@ -12,12 +12,12 @@ import './globals.css'
 
 const geistSans = Geist({
   variable: '--font-geist-sans',
-  subsets: ['latin'],
+  subsets: ['latin', 'latin-ext'],
 })
 
 const geistMono = Geist_Mono({
   variable: '--font-geist-mono',
-  subsets: ['latin'],
+  subsets: ['latin', 'latin-ext'],
 })
 
 export const metadata: Metadata = {

BIN
src/assets/fonts/Roboto-Bold.ttf


BIN
src/assets/fonts/Roboto-Regular.ttf


+ 1 - 0
src/features/email/Actions/emailActions.ts

@@ -4,6 +4,7 @@ import { db } from "@/lib/db";
 import { sendOrgMail, getOrgFromAddress } from "@/lib/email";
 import { withAuth } from "@/lib/with-auth";
 import { renderToBuffer } from "@react-pdf/renderer";
+import "@/features/vehicles/Components/invoice-pdf/fonts";
 import React from "react";
 import { readFile } from "fs/promises";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";

+ 23 - 0
src/features/vehicles/Components/invoice-pdf/fonts.ts

@@ -0,0 +1,23 @@
+import { readFileSync } from 'node:fs'
+import path from 'node:path'
+import { Font } from '@react-pdf/renderer'
+
+const fontsDir = path.join(process.cwd(), 'src/assets/fonts')
+const regularDataUri = `data:font/truetype;base64,${readFileSync(path.join(fontsDir, 'Roboto-Regular.ttf')).toString('base64')}`
+const boldDataUri = `data:font/truetype;base64,${readFileSync(path.join(fontsDir, 'Roboto-Bold.ttf')).toString('base64')}`
+
+Font.register({
+  family: 'Roboto',
+  fonts: [
+    { src: regularDataUri, fontWeight: 400 },
+    { src: boldDataUri, fontWeight: 700 },
+  ],
+})
+
+Font.register({
+  family: 'Roboto-Bold',
+  src: boldDataUri,
+})
+
+// Disable word hyphenation so Cyrillic text is not broken mid-word
+Font.registerHyphenationCallback((word) => [word])

+ 28 - 18
src/features/vehicles/Components/invoice-pdf/styles.ts

@@ -21,20 +21,30 @@ export function darkenColor(hex: string, factor: number = 0.3) {
   return `rgb(${Math.round(r * (1 - factor))}, ${Math.round(g * (1 - factor))}, ${Math.round(b * (1 - factor))})`
 }
 
-export function getFontBold(font: string) {
-  const boldMap: Record<string, string> = {
-    'Times-Roman': 'Times-Bold',
-  }
-  return boldMap[font] || `${font}-Bold`
+// Map any built-in PDF font to the registered Roboto font so that
+// Cyrillic (and other non-Latin scripts) render correctly.
+// Users still see "Helvetica" / "Times-Roman" etc. in settings,
+// but the PDF always uses the Unicode-capable Roboto under the hood.
+function resolveFont(font: string): string {
+  return 'Roboto'
+}
+
+function resolveFontBold(font: string): string {
+  return 'Roboto-Bold'
+}
+
+export function getFontBold(_font: string) {
+  return resolveFontBold(_font)
 }
 
 export function createStyles(primary: string, font: string) {
   const primaryLight = lightenColor(primary)
   const primaryDark = darkenColor(primary)
-  const fontBold = getFontBold(font)
+  const resolved = resolveFont(font)
+  const resolvedBold = resolveFontBold(font)
 
   return StyleSheet.create({
-    page: { padding: 40, fontSize: 10, fontFamily: font, color: dark },
+    page: { padding: 40, fontSize: 10, fontFamily: resolved, color: dark },
     header: {
       flexDirection: 'row',
       justifyContent: 'space-between',
@@ -43,26 +53,26 @@ export function createStyles(primary: string, font: string) {
       borderBottomWidth: 3,
       borderBottomColor: primary,
     },
-    brandName: { fontSize: 22, fontFamily: fontBold, color: primary },
+    brandName: { fontSize: 22, fontFamily: resolvedBold, color: primary },
     brandSub: { fontSize: 9, color: gray, marginTop: 2 },
     brandContact: { fontSize: 8, color: gray, marginTop: 1 },
-    invoiceTitle: { fontSize: 18, fontFamily: fontBold, textAlign: 'right' as const },
+    invoiceTitle: { fontSize: 18, fontFamily: resolvedBold, textAlign: 'right' as const },
     invoiceNumber: { fontSize: 9, color: gray, textAlign: 'right' as const, marginTop: 4 },
     infoRow: { flexDirection: 'row', gap: 20, marginBottom: 20 },
     infoBox: { padding: 12, backgroundColor: grayLight, borderRadius: 4, marginBottom: 4 },
     infoLabel: {
       fontSize: 8,
-      fontFamily: fontBold,
+      fontFamily: resolvedBold,
       color: primary,
       textTransform: 'uppercase' as const,
       marginBottom: 6,
     },
     infoText: { fontSize: 10, marginBottom: 2 },
-    infoTextBold: { fontSize: 10, fontFamily: fontBold, marginBottom: 2 },
+    infoTextBold: { fontSize: 10, fontFamily: resolvedBold, marginBottom: 2 },
     infoTextSmall: { fontSize: 9, color: gray, marginBottom: 2 },
     sectionTitle: {
       fontSize: 12,
-      fontFamily: fontBold,
+      fontFamily: resolvedBold,
       marginBottom: 8,
       marginTop: 16,
       color: dark,
@@ -83,20 +93,20 @@ export function createStyles(primary: string, font: string) {
       borderBottomColor: '#e5e7eb',
     },
     tableCell: { fontSize: 9 },
-    tableCellBold: { fontSize: 9, fontFamily: fontBold },
-    tableHeaderCell: { fontSize: 8, fontFamily: fontBold, color: primaryDark },
-    totalsBox: { marginTop: 16, marginLeft: 'auto', width: 220 },
+    tableCellBold: { fontSize: 9, fontFamily: resolvedBold },
+    tableHeaderCell: { fontSize: 8, fontFamily: resolvedBold, color: primaryDark },
+    totalsBox: { marginTop: 16, marginLeft: 'auto', width: 250 },
     totalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3 },
     totalLabel: { fontSize: 10, color: gray },
     totalValue: { fontSize: 10 },
     totalDivider: { borderTopWidth: 1, borderTopColor: primary, marginVertical: 4 },
     grandTotalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
-    grandTotalLabel: { fontSize: 14, fontFamily: fontBold },
-    grandTotalValue: { fontSize: 14, fontFamily: fontBold, color: primary },
+    grandTotalLabel: { fontSize: 14, fontFamily: resolvedBold },
+    grandTotalValue: { fontSize: 14, fontFamily: resolvedBold, color: primary },
     notesSection: { marginTop: 20, padding: 12, backgroundColor: grayLight, borderRadius: 4 },
     notesLabel: {
       fontSize: 8,
-      fontFamily: fontBold,
+      fontFamily: resolvedBold,
       color: primary,
       textTransform: 'uppercase' as const,
       marginBottom: 4,

+ 2 - 1
src/i18n/config.ts

@@ -1,4 +1,4 @@
-export const locales = ['en', 'de', 'es', 'fr', 'nb', 'pt-BR', 'pl', 'nl', 'it', 'tr'] as const
+export const locales = ['en', 'de', 'es', 'fr', 'nb', 'pt-BR', 'pl', 'nl', 'it', 'tr', 'ru'] as const
 
 export type Locale = (typeof locales)[number]
 
@@ -15,4 +15,5 @@ export const localeNames: Record<Locale, string> = {
   nl: 'Nederlands',
   it: 'Italiano',
   tr: 'Türkçe',
+  ru: 'Русский',
 }