Add a language-switch system with per-language files

Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.

- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
  reactive to the signed-in profile locale. Every view, component, form and
  the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
  language (the panel has no user profile) and a header language picker.
  Chrome, cards, login and API section titles translated; endpoint reference
  descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
  driven by AppSettings.locale. Nav, login, lock, dashboard, the full
  Settings panel (incl. language picker) and format.dart status labels
  translated; remaining detail screens fall back to English.

Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.

Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 4.8 committed 2026-07-17 20:07:48 +02:00
1 parent ee28b522c7
commit b6bb6b1df0
54 files changed
+4191 -979

No files matched your search

+628
View File
@@ -0,0 +1,628 @@
{
"errors": {
"sessionExpired": "Sessionen er udløbet — log ind igen."
},
"common": {
"cancel": "Annuller",
"save": "Gem",
"saveChanges": "Gem ændringer",
"saving": "Gemmer…",
"saved": "Gemt ✓",
"loading": "Indlæser…",
"edit": "Rediger",
"remove": "Fjern",
"delete": "Slet",
"done": "Færdig",
"undo": "Fortryd",
"download": "Download",
"empty": "—",
"yes": "Ja",
"no": "Nej"
},
"nav": {
"garage": "Garage",
"settings": "Indstillinger",
"users": "Brugere",
"lightMode": "Lys tilstand",
"darkMode": "Mørk tilstand",
"signedIn": "Logget ind",
"logOut": "Log ud"
},
"login": {
"title": "Log ind",
"tagline": "Styr på din bil.",
"email": "E-mail",
"password": "Adgangskode",
"showPassword": "Vis adgangskode",
"hidePassword": "Skjul adgangskode",
"submit": "Log ind",
"submitting": "Logger ind…",
"failed": "Login mislykkedes",
"serverSettings": "Serverindstillinger",
"apiServerUrl": "API-serverens adresse",
"leaveBlank": "Lad feltet stå tomt for at bruge standarden ({url}).",
"resetToDefault": "Nulstil til standard"
},
"dashboard": {
"eyebrow": "Garage",
"title": "Dine biler",
"subtitle": "Serviceoverblik og servicehistorik.",
"addCar": "Tilføj bil",
"empty": "Ingen biler endnu. Klik på {action} for at komme i gang.",
"shared": "Delt",
"sharedReadOnly": "Delt · skrivebeskyttet",
"serviceLife": "Serviceinterval brugt",
"lastService": "Seneste service",
"odometer": "Kilometerstand",
"nextDue": "Næste service",
"nextDueKm": "Næste service (km)",
"serviceRecords": {
"one": "{n} servicepost",
"other": "{n} serviceposter"
}
},
"admin": {
"eyebrow": "Administration",
"title": "Brugere",
"subtitleAll": "Konti på tværs af alle organisationer.",
"subtitleOrg": "Konti i din organisation.",
"subtitleOrgsNote": "Organisationer tildeles i API-panelet.",
"addUser": "Tilføj bruger",
"colEmail": "E-mail",
"colName": "Navn",
"colOrganization": "Organisation",
"colRole": "Rolle",
"colCreated": "Oprettet",
"you": "(dig)",
"resetPassword": "Nulstil adgangskode",
"confirmDelete": "Slet {name}? Dette kan ikke fortrydes.",
"cantDeleteSelf": "Du kan ikke slette din egen konto.",
"onlySuperadminDeletes": "Kun en superadministrator kan slette en superadministrator.",
"cantChangeOwnRole": "Du kan ikke ændre din egen rolle.",
"onlySuperadminEdits": "Kun en superadministrator kan redigere en superadministrator.",
"createTitle": "Tilføj en bruger",
"emailRequired": "E-mail *",
"passwordRequired": "Adgangskode *",
"minChars": "(mindst 8)",
"creating": "Opretter…",
"createUser": "Opret bruger",
"resetTitle": "Nulstil adgangskode — {email}",
"newPassword": "Ny adgangskode",
"setPassword": "Angiv adgangskode",
"roles": {
"user": "bruger",
"admin": "administrator",
"superadmin": "superadministrator"
}
},
"settings": {
"eyebrow": "Konto",
"title": "Indstillinger",
"subtitle": "Administrer din konto, udseende og dine data.",
"account": {
"title": "Konto",
"name": "Navn",
"email": "E-mail",
"verified": "Bekræftet",
"notVerified": "Ikke bekræftet",
"resendVerification": "Send bekræftelsesmail igen",
"sending": "Sender…",
"verificationRequested": "Bekræftelsesmail anmodet.",
"changePassword": "Skift adgangskode",
"currentPassword": "Nuværende adgangskode",
"newPassword": "Ny adgangskode",
"confirmNewPassword": "Bekræft ny adgangskode",
"mismatchYet": "Adgangskoderne stemmer ikke overens endnu.",
"tooShort": "Den nye adgangskode skal være på mindst 8 tegn.",
"mismatch": "Den nye adgangskode og bekræftelsen stemmer ikke overens.",
"updating": "Opdaterer…",
"passwordUpdated": "Adgangskode opdateret ✓",
"updatePassword": "Opdater adgangskode"
},
"appearance": {
"title": "Udseende",
"theme": "Tema",
"themeLight": "lyst",
"themeDark": "mørkt",
"themeSystem": "system",
"language": "Sprog",
"languageHint": "Appens tekst samt navne på måneder og dage.",
"languageFallbackHint": "Dette sprog er endnu ikke oversat — appens tekst forbliver på engelsk.",
"region": "Region",
"regionHint": "Tal- og valutaformat.",
"dateFormat": "Datoformat",
"dateExample": "Eksempel: {example}",
"currency": "Valuta",
"currencyExample": "Eksempel: {example} — kun visning, ingen beløb omregnes.",
"fontSize": "Skriftstørrelse",
"fontSmall": "lille",
"fontMedium": "mellem",
"fontLarge": "stor"
},
"profile": {
"title": "Profil",
"avatarAlt": "Profilbillede",
"uploading": "Uploader…",
"uploadPhoto": "Upload billede",
"bio": "Om mig",
"bioPlaceholder": "En kort note, som andre i din husstand kan se.",
"saveBio": "Gem beskrivelse"
},
"privacy": {
"title": "Privatliv og sikkerhed",
"signOut": "Log ud",
"body": "Tofaktorgodkendelse er ikke tilgængelig endnu. Sessioner bygger på tokens udstedt af serveren, som udløber af sig selv, så at logge ud her afslutter kun sessionen på denne enhed — der er ingen liste over enheder at tilbagekalde fra. For at logge alle enheder ud skal du skifte din adgangskode ovenfor."
},
"advanced": {
"title": "Avanceret",
"exportTitle": "Eksportér dine data",
"exportBody": "Download din profil samt alle biler, serviceposter og reservedele som JSON.",
"preparing": "Forbereder…",
"exportAction": "Eksportér data",
"importTitle": "Importér dine data",
"importBody": "Tilføj biler fra en tidligere eksporteret JSON-fil. Dette opretter nye poster — intet flettes eller overskrives.",
"importing": "Importerer…",
"importAction": "Importér data",
"notJson": "Filen er ikke gyldig JSON.",
"notExport": "Filen ligner ikke en DriverVault-eksport (der mangler en \"cars\"-liste).",
"confirmImport": "Importér {count} bil(er) fra denne fil? Dette tilføjer nye poster — eksisterende biler flettes eller overskrives ikke.",
"imported": "Importerede {cars} bil(er), {services} servicepost(er), {parts} reservedel(e)."
},
"danger": {
"title": "Farezone",
"body": "Sletning af din konto fjerner dit login og din profil. Det sletter ikke husstandens delte biler eller servicehistorik. Der er 3 dages betænkningstid, før sletningen er endelig, og du kan annullere når som helst inden da.",
"deleteAccount": "Slet min konto",
"typeToConfirm": "Skriv {email} for at bekræfte",
"requesting": "Anmoder…",
"requestDeletion": "Anmod om sletning",
"requestedOn": "Sletning af konto anmodet den {date}.",
"canStillCancel": "Du kan stadig annullere — det bliver permanent efter 3 dages betænkningstid.",
"cooldownPassed": "Betænkningstiden er udløbet. Du kan nu gennemføre sletningen.",
"cancelRequest": "Annuller anmodning om sletning",
"finalize": "Slet min konto permanent",
"confirmFinalize": "Dette sletter din konto permanent. Det kan ikke fortrydes. Fortsæt?"
}
},
"car": {
"allCars": "← Alle biler",
"share": "Del",
"shared": "Delt",
"sharedReadOnly": "Delt · skrivebeskyttet",
"tabs": {
"info": "Oplysninger",
"services": "Servicehistorik",
"technical": "Synshistorik",
"maintenance": "Værksted",
"fuel": "Brændstof",
"documents": "Dokumenter",
"parts": "Reservedelskatalog",
"reminders": "Påmindelser"
},
"info": {
"oilSpec": "Motorolie-specifikation",
"transmissionOil": "Gearolie",
"differentialOil": "Differentialeolie",
"brakeFluid": "Bremsevæske",
"coolant": "Kølervæske",
"odometer": "Kilometerstand",
"serviceInterval": "Serviceinterval",
"nextDue": "Næste service",
"registrationPlate": "Nummerplade",
"registrationCountry": "Registreringsland",
"vin": "Stelnummer",
"fuelType": "Brændstoftype",
"buildDate": "Produktionsdato",
"firstRegistration": "Første registrering"
},
"services": {
"title": "Servicehistorik",
"add": "Tilføj service",
"empty": "Ingen serviceposter endnu.",
"colDate": "Dato",
"colKm": "Km",
"colNextDate": "Næste dato",
"colNextKm": "Næste km",
"colOil": "Olie og oliefilter",
"colEngineFilter": "Luftfilter",
"colCabinFilter": "Kabinefilter",
"colNotes": "Noter",
"colFile": "Fil",
"confirmDelete": "Slet denne servicepost?"
},
"technical": {
"title": "Synshistorik",
"subtitle": "Lovpligtige syn. Gentages alene efter tid, uanset hvad kilometerstanden viser.",
"add": "Tilføj syn",
"empty": "Ingen syn endnu.",
"colDate": "Dato",
"colResult": "Resultat",
"colNextCheck": "Næste syn",
"colStatus": "Status",
"colStation": "Synssted",
"colCost": "Pris",
"colNotes": "Noter",
"colFile": "Fil",
"passed": "Godkendt",
"failed": "Ikke godkendt",
"confirmDelete": "Slet dette syn?"
},
"maintenance": {
"title": "Værksted",
"subtitle": "Værkstedsbesøg og reparationer. Almindelig service hører under Servicehistorik.",
"add": "Registrér besøg",
"empty": "Ingen værkstedsbesøg registreret endnu.",
"colDate": "Dato",
"colKm": "Km",
"colType": "Type",
"colWork": "Udført arbejde",
"colWorkshop": "Værksted",
"colStatus": "Status",
"colCost": "Pris",
"colFile": "Fil",
"underWarranty": "Under garanti · {days} dage tilbage",
"confirmDelete": "Slet dette værkstedsbesøg?"
},
"fuel": {
"title": "Brændstof",
"subtitle": "Forbruget måles mellem fulde tanke.",
"add": "Registrér tankning",
"empty": "Ingen tankninger registreret endnu.",
"average": "Gennemsnit",
"best": "Bedste",
"worst": "Værste",
"costPerKm": "Pris pr. km",
"refills": "Tankninger",
"totalLiters": "Liter i alt",
"totalSpent": "Brugt i alt",
"trackedDistance": "Målt distance",
"avgPrice": "Gns. pris {price}/L",
"needTwoTanks": "Registrér mindst to fulde tanke for at se forbrugstal.",
"colDate": "Dato",
"colKm": "Km",
"colLiters": "Liter",
"colCost": "Pris",
"colPerLiter": "Pr. liter",
"colDistance": "Distance",
"colConsumption": "Forbrug",
"colStation": "Tankstation",
"colFile": "Fil",
"partial": "delvis",
"gap": "hul",
"confirmDelete": "Slet denne tankning?"
},
"documents": {
"title": "Dokumenter",
"subtitle": "Forsikring, miljøattester og andre papirer med fornyelsesdatoer.",
"add": "Tilføj dokument",
"empty": "Ingen dokumenter endnu.",
"colType": "Type",
"colTitle": "Titel",
"colProvider": "Udbyder",
"colIssued": "Udstedt",
"colRenewal": "Fornyelse",
"colStatus": "Status",
"colFile": "Fil",
"confirmDelete": "Slet dette dokument?"
},
"reminders": {
"title": "Påmindelser",
"subtitle": "Påmindelser om fornyelse og service tilføjes automatisk ud fra dine dokumenter og din servicehistorik.",
"add": "Tilføj påmindelse",
"empty": "Intet at blive mindet om endnu.",
"automatic": "Automatisk",
"repeats": "Gentages",
"at": "ved {km}",
"doneRollForward": "Færdig · flyt frem",
"markDone": "Markér som færdig",
"reopen": "Genåbn",
"confirmDelete": "Slet denne påmindelse?"
},
"parts": {
"title": "Reservedelskatalog",
"add": "Tilføj reservedel",
"empty": "Ingen reservedele endnu.",
"colPart": "Reservedel",
"colPartNumber": "Varenummer",
"colNotes": "Noter",
"colFile": "Fil",
"confirmDelete": "Slet denne reservedel?"
},
"delete": {
"title": "Slet denne bil?",
"body": "Dette sletter {name} permanent sammen med alt, der er registreret på den — {services}, {maintenance}, {fuel}, {documents} og {parts}. Det kan ikke fortrydes.",
"services": {
"one": "{n} servicepost",
"other": "{n} serviceposter"
},
"maintenance": {
"one": "{n} værkstedsbesøg",
"other": "{n} værkstedsbesøg"
},
"fuel": {
"one": "{n} tankning",
"other": "{n} tankninger"
},
"documents": {
"one": "{n} dokument",
"other": "{n} dokumenter"
},
"parts": {
"one": "{n} reservedel",
"other": "{n} reservedele"
},
"typeToConfirm": "Skriv {name} for at bekræfte",
"deleting": "Sletter…",
"confirm": "Slet permanent"
}
},
"attachment": {
"legend": "Vedhæftet fil",
"hint": "PDF eller billede, op til 10 MB.",
"attached": "Vedhæftet: {name}",
"willBeRemoved": "Den vedhæftede fil fjernes, når der gemmes."
},
"forms": {
"car": {
"addTitle": "Tilføj en bil",
"editTitle": "Rediger bil",
"name": "Navn *",
"make": "Mærke",
"model": "Model",
"year": "Årgang",
"registration": "Nummerplade",
"registrationCountry": "Registreringsland",
"registrationCountryPlaceholder": "Danmark",
"vin": "Stelnummer",
"vinPlaceholder": "Køretøjets stelnummer",
"fuelType": "Brændstoftype",
"buildDate": "Produktionsdato",
"firstRegistration": "Første registrering",
"oilSpec": "Motorolie-specifikation",
"currentKm": "Nuværende kilometerstand (km)",
"transmissionOilSpec": "Gearolie-specifikation",
"differentialOilSpec": "Differentialeolie-specifikation",
"brakeFluidSpec": "Bremsevæske-specifikation",
"coolantSpec": "Kølervæske-specifikation",
"serviceIntervalDays": "Serviceinterval (dage)",
"serviceIntervalKm": "Serviceinterval (km)",
"technicalCheckIntervalDays": "Synsinterval (dage)",
"technicalCheckHint": "Udfylder på forhånd hvert syns næste forfaldsdato. Ethvert syn kan tilsidesætte den med datoen på attesten.",
"submit": "Tilføj bil"
},
"service": {
"addTitle": "Tilføj servicepost",
"editTitle": "Rediger servicepost",
"date": "Dato *",
"odometer": "Kilometerstand (km)",
"changedParts": "Udskiftede dele",
"oil": "Olie og oliefilter",
"engineFilter": "Luftfilter",
"cabinFilter": "Kabinefilter",
"attachmentLegend": "Kvittering eller side fra servicebogen",
"notes": "Noter",
"autoHint": "Næste servicedato (+{days} dage) og km (+{km}) beregnes automatisk.",
"submit": "Tilføj service"
},
"technical": {
"addTitle": "Tilføj syn",
"editTitle": "Rediger syn",
"date": "Synsdato *",
"result": "Resultat *",
"passed": "Godkendt",
"failed": "Ikke godkendt",
"validUntil": "Gyldig til",
"failedHint": "Et ikke-godkendt syn attesterer ingenting, så der udledes ingen næste dato af det.",
"derivedHint": "Lad feltet stå tomt for at bruge bilens interval (+{days} dage → {date}). Indtast datoen på attesten, hvis den afviger.",
"cost": "Pris",
"station": "Synssted",
"stationPlaceholder": "Synshal",
"attachmentLegend": "Synsattest",
"notes": "Noter",
"submit": "Tilføj syn"
},
"part": {
"addTitle": "Tilføj reservedel",
"editTitle": "Rediger reservedel",
"name": "Reservedelens navn *",
"namePlaceholder": "Oliefilter",
"partNumber": "Varenummer",
"notes": "Noter",
"notesPlaceholder": "Passer til 2015–2020 · køb parvis",
"attachmentLegend": "Billede eller datablad",
"submit": "Tilføj reservedel"
},
"fuel": {
"addTitle": "Registrér tankning",
"editTitle": "Rediger tankning",
"date": "Dato *",
"odometer": "Kilometerstand (km) *",
"liters": "Liter *",
"cost": "Samlet pris",
"pricePerLiter": "Pris pr. liter: {price}",
"tank": "Tank",
"fullTank": "Fyldt helt op",
"missedFill": "Jeg glemte at registrere en tankning før denne",
"tankHint": "Forbruget måles mellem fulde tanke, så delvise tankninger tæller med i den næste fulde. At markere en glemt tankning holder den strækning ude af tallene i stedet for at vise et urealistisk lavt forbrug.",
"station": "Tankstation",
"notes": "Noter",
"attachmentLegend": "Kvittering",
"submit": "Registrér tankning"
},
"maintenance": {
"addTitle": "Registrér værkstedsbesøg",
"editTitle": "Rediger værkstedsbesøg",
"date": "Dato *",
"odometer": "Kilometerstand (km)",
"type": "Type",
"status": "Status",
"description": "Hvad blev der lavet *",
"descriptionPlaceholder": "Udskiftede generator og drivrem",
"workshop": "Værksted",
"location": "Sted",
"partsUsed": "Udskiftede dele",
"partsUsedPlaceholder": "Generator 27060-0T010, rem 90916-02660",
"laborCost": "Arbejdsløn",
"partsCost": "Pris for dele",
"total": "I alt: {total}",
"invoiceNumber": "Fakturanummer",
"warrantyUntil": "Garanti til",
"attachmentLegend": "Faktura",
"notes": "Noter",
"submit": "Registrér besøg"
},
"document": {
"addTitle": "Tilføj dokument",
"editTitle": "Rediger dokument",
"type": "Type",
"title": "Titel *",
"titlePlaceholder": "Ansvarsforsikring 2026",
"provider": "Udbyder",
"reference": "Police- / attestnummer",
"issued": "Udstedt",
"renewalDate": "Fornyelsesdato",
"renewalHint": "Lad fornyelsesdatoen stå tom for et dokument, der aldrig udløber. Angives den, tilføjes der automatisk en påmindelse.",
"cost": "Pris",
"attachmentLegend": "Scan eller billede",
"notes": "Noter",
"submit": "Tilføj dokument"
},
"reminder": {
"addTitle": "Tilføj påmindelse",
"editTitle": "Rediger påmindelse",
"title": "Titel *",
"titlePlaceholder": "Skift til vinterdæk",
"type": "Type",
"remindMe": "Mind mig om",
"onDate": "På dato",
"atOdometer": "Ved kilometerstand (km)",
"triggerHint": "Angiv det ene eller begge — med begge gælder det, der indtræffer først.",
"currentKm": "Bilen står på {km} nu.",
"repeat": "Gentagelse (valgfrit)",
"everyDays": "Hver … dage",
"everyKm": "Hver … km",
"recurringHint": "Markeres den som færdig, flyttes den frem i stedet for at blive lukket.",
"oneOffHint": "Lad feltet stå tomt for en engangspåmindelse, der lukkes, når du markerer den som færdig.",
"notes": "Noter",
"noTrigger": "Angiv en forfaldsdato, en kilometerstand eller begge.",
"submit": "Tilføj påmindelse"
},
"share": {
"title": "Del {name}",
"body": "Giv en anden bruger adgang til denne bil. Skrivebeskyttet giver adgang til at se; læs og skriv giver også adgang til at redigere bilen samt dens serviceposter og reservedele.",
"userEmail": "Brugerens e-mail",
"read": "Skrivebeskyttet",
"write": "Læs og skriv",
"submit": "Del",
"peopleWithAccess": "Personer med adgang",
"notShared": "Endnu ikke delt med nogen."
}
},
"enums": {
"fuelType": {
"petrol": "Benzin",
"petrol_lpg": "Benzin + LPG",
"diesel": "Diesel",
"diesel_lpg": "Diesel + LPG",
"hybrid": "Hybrid",
"electric": "El",
"hydrogen": "Brint"
},
"maintenanceType": {
"repair": "Reparation",
"inspection": "Eftersyn",
"bodywork": "Karrosseri",
"tyres": "Dæk",
"diagnostics": "Fejlsøgning",
"recall": "Tilbagekaldelse",
"warranty": "Garantiarbejde",
"other": "Andet"
},
"maintenanceStatus": {
"scheduled": "Planlagt",
"in_progress": "I gang",
"completed": "Fuldført"
},
"documentType": {
"insurance": "Forsikring",
"pollution": "Miljøattest",
"registration": "Registreringsattest",
"inspection": "Eftersyn",
"roadTax": "Vægtafgift",
"warranty": "Garanti",
"other": "Andet"
},
"reminderTypeShort": {
"maintenance": "Vedligehold",
"document": "Dokument",
"service": "Service",
"inspection": "Eftersyn",
"other": "Andet"
},
"reminderType": {
"maintenance": "Vedligehold",
"document": "Fornyelse af dokument",
"service": "Service",
"inspection": "Eftersyn",
"other": "Andet"
}
},
"status": {
"noData": "Ingen data",
"serviceOverdueDays": "Service overskredet med {days} d",
"dueInDays": "Forfalder om {days} d",
"okDays": "OK · {days} d",
"noKm": "Ingen km",
"serviceOverdueKm": "Service overskredet med {km} km",
"inKm": "Om {km} km",
"kmLeft": "{km} km tilbage",
"expiredAgo": "Udløb for {days} d siden",
"expiresToday": "Udløber i dag",
"renewInDays": "Forny om {days} d",
"validDays": "Gyldig · {days} d",
"noExpiry": "Udløber ikke",
"done": "Færdig",
"noTrigger": "Ingen udløser",
"overdue": "Overskredet",
"overdueBy": "Overskredet {parts}",
"dueIn": "Forfalder om {parts}",
"upcoming": "Kommende",
"today": "i dag",
"days": "{days} d",
"km": "{km} km"
}
}