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": "Session expired — please log in again."
},
"common": {
"cancel": "Cancel",
"save": "Save",
"saveChanges": "Save changes",
"saving": "Saving…",
"saved": "Saved ✓",
"loading": "Loading…",
"edit": "Edit",
"remove": "Remove",
"delete": "Delete",
"done": "Done",
"undo": "Undo",
"download": "Download",
"empty": "—",
"yes": "Yes",
"no": "No"
},
"nav": {
"garage": "Garage",
"settings": "Settings",
"users": "Users",
"lightMode": "Light mode",
"darkMode": "Dark mode",
"signedIn": "Signed in",
"logOut": "Log out"
},
"login": {
"title": "Sign in",
"tagline": "Your car, on track.",
"email": "Email",
"password": "Password",
"showPassword": "Show password",
"hidePassword": "Hide password",
"submit": "Sign in",
"submitting": "Signing in…",
"failed": "Login failed",
"serverSettings": "Server settings",
"apiServerUrl": "API server URL",
"leaveBlank": "Leave blank to use the default ({url}).",
"resetToDefault": "Reset to default"
},
"dashboard": {
"eyebrow": "Garage",
"title": "Your cars",
"subtitle": "Maintenance overview and service history.",
"addCar": "Add car",
"empty": "No cars yet. Click {action} to get started.",
"shared": "Shared",
"sharedReadOnly": "Shared · read-only",
"serviceLife": "Service life",
"lastService": "Last service",
"odometer": "Odometer",
"nextDue": "Next due",
"nextDueKm": "Next due km",
"serviceRecords": {
"one": "{n} service record",
"other": "{n} service records"
}
},
"admin": {
"eyebrow": "Admin",
"title": "Users",
"subtitleAll": "Accounts across every organization.",
"subtitleOrg": "Accounts in your organization.",
"subtitleOrgsNote": "Organizations are assigned in the API panel.",
"addUser": "Add user",
"colEmail": "Email",
"colName": "Name",
"colOrganization": "Organization",
"colRole": "Role",
"colCreated": "Created",
"you": "(you)",
"resetPassword": "Reset password",
"confirmDelete": "Delete {name}? This cannot be undone.",
"cantDeleteSelf": "You can't delete your own account.",
"onlySuperadminDeletes": "Only a superadmin can delete a superadmin.",
"cantChangeOwnRole": "You can't change your own role.",
"onlySuperadminEdits": "Only a superadmin can edit a superadmin.",
"createTitle": "Add a user",
"emailRequired": "Email *",
"passwordRequired": "Password *",
"minChars": "(min 8)",
"creating": "Creating…",
"createUser": "Create user",
"resetTitle": "Reset password — {email}",
"newPassword": "New password",
"setPassword": "Set password",
"roles": {
"user": "user",
"admin": "admin",
"superadmin": "superadmin"
}
},
"settings": {
"eyebrow": "Account",
"title": "Settings",
"subtitle": "Manage your account, appearance, and data.",
"account": {
"title": "Account",
"name": "Name",
"email": "Email",
"verified": "Verified",
"notVerified": "Not verified",
"resendVerification": "Resend verification email",
"sending": "Sending…",
"verificationRequested": "Verification email requested.",
"changePassword": "Change password",
"currentPassword": "Current password",
"newPassword": "New password",
"confirmNewPassword": "Confirm new password",
"mismatchYet": "Passwords don't match yet.",
"tooShort": "New password must be at least 8 characters.",
"mismatch": "New password and confirmation don't match.",
"updating": "Updating…",
"passwordUpdated": "Password updated ✓",
"updatePassword": "Update password"
},
"appearance": {
"title": "Appearance",
"theme": "Theme",
"themeLight": "light",
"themeDark": "dark",
"themeSystem": "system",
"language": "Language",
"languageHint": "App text, and the names of months and days.",
"languageFallbackHint": "This language isn't translated yet — the app text stays in English.",
"region": "Region",
"regionHint": "Number and currency layout.",
"dateFormat": "Date format",
"dateExample": "Example: {example}",
"currency": "Currency",
"currencyExample": "Example: {example} — display only, no amounts are converted.",
"fontSize": "Font size",
"fontSmall": "small",
"fontMedium": "medium",
"fontLarge": "large"
},
"profile": {
"title": "Profile",
"avatarAlt": "Avatar",
"uploading": "Uploading…",
"uploadPhoto": "Upload photo",
"bio": "Bio",
"bioPlaceholder": "A short note visible to other people in your household.",
"saveBio": "Save bio"
},
"privacy": {
"title": "Privacy & security",
"signOut": "Sign out",
"body": "Two-factor authentication isn't available yet. Sessions are held as server-issued tokens that expire on their own, so signing out here ends this device's session only — there's no per-device list to revoke from. To lock out every device, change your password above."
},
"advanced": {
"title": "Advanced",
"exportTitle": "Export your data",
"exportBody": "Download your profile and all cars, service records, and parts as JSON.",
"preparing": "Preparing…",
"exportAction": "Export data",
"importTitle": "Import your data",
"importBody": "Add cars from a previously exported JSON file. This creates new records — it doesn't merge with or overwrite anything existing.",
"importing": "Importing…",
"importAction": "Import data",
"notJson": "That file isn't valid JSON.",
"notExport": "That file doesn't look like a DriverVault export (missing a \"cars\" list).",
"confirmImport": "Import {count} car(s) from this file? This adds new records — it does not merge with or overwrite existing cars.",
"imported": "Imported {cars} car(s), {services} service record(s), {parts} part(s)."
},
"danger": {
"title": "Danger zone",
"body": "Deleting your account removes your login and profile. It does not delete your household's shared cars or service history. There's a 3-day cooldown before the deletion is final, and you can cancel any time before then.",
"deleteAccount": "Delete my account",
"typeToConfirm": "Type {email} to confirm",
"requesting": "Requesting…",
"requestDeletion": "Request deletion",
"requestedOn": "Account deletion requested on {date}.",
"canStillCancel": "You can still cancel — it becomes permanent after the 3-day cooldown.",
"cooldownPassed": "The cooldown has passed. You can now finalize the deletion.",
"cancelRequest": "Cancel deletion request",
"finalize": "Permanently delete my account",
"confirmFinalize": "This permanently deletes your account. This cannot be undone. Continue?"
}
},
"car": {
"allCars": "← All cars",
"share": "Share",
"shared": "Shared",
"sharedReadOnly": "Shared · read-only",
"tabs": {
"info": "Information",
"services": "Service history",
"technical": "Technical check history",
"maintenance": "Maintenance",
"fuel": "Fuel",
"documents": "Documents",
"parts": "Parts catalog",
"reminders": "Reminders"
},
"info": {
"oilSpec": "Engine oil spec",
"transmissionOil": "Transmission oil",
"differentialOil": "Differential oil",
"brakeFluid": "Brake fluid",
"coolant": "Coolant",
"odometer": "Odometer",
"serviceInterval": "Service interval",
"nextDue": "Next due",
"registrationPlate": "Registration plate",
"registrationCountry": "Registration country",
"vin": "VIN",
"fuelType": "Fuel type",
"buildDate": "Build date",
"firstRegistration": "First registration"
},
"services": {
"title": "Service history",
"add": "Add service",
"empty": "No service records yet.",
"colDate": "Date",
"colKm": "Km",
"colNextDate": "Next date",
"colNextKm": "Next km",
"colOil": "Oil & Oil filter",
"colEngineFilter": "Engine air filter",
"colCabinFilter": "Cabin air filter",
"colNotes": "Notes",
"colFile": "File",
"confirmDelete": "Delete this service record?"
},
"technical": {
"title": "Technical check history",
"subtitle": "Mandatory roadworthiness inspections. Recurs on time alone, whatever the odometer reads.",
"add": "Add check",
"empty": "No technical checks yet.",
"colDate": "Date",
"colResult": "Result",
"colNextCheck": "Next check",
"colStatus": "Status",
"colStation": "Station",
"colCost": "Cost",
"colNotes": "Notes",
"colFile": "File",
"passed": "Passed",
"failed": "Failed",
"confirmDelete": "Delete this technical check?"
},
"maintenance": {
"title": "Maintenance",
"subtitle": "Workshop visits and repairs. Routine servicing lives under Service history.",
"add": "Log visit",
"empty": "No workshop visits logged yet.",
"colDate": "Date",
"colKm": "Km",
"colType": "Type",
"colWork": "Work done",
"colWorkshop": "Workshop",
"colStatus": "Status",
"colCost": "Cost",
"colFile": "File",
"underWarranty": "Under warranty · {days}d left",
"confirmDelete": "Delete this workshop visit?"
},
"fuel": {
"title": "Fuel",
"subtitle": "Consumption is measured between full tanks.",
"add": "Log refill",
"empty": "No refills logged yet.",
"average": "Average",
"best": "Best",
"worst": "Worst",
"costPerKm": "Cost per km",
"refills": "Refills",
"totalLiters": "Total litres",
"totalSpent": "Total spent",
"trackedDistance": "Tracked distance",
"avgPrice": "Avg. price {price}/L",
"needTwoTanks": "Log at least two full tanks to see consumption figures.",
"colDate": "Date",
"colKm": "Km",
"colLiters": "Litres",
"colCost": "Cost",
"colPerLiter": "Per litre",
"colDistance": "Distance",
"colConsumption": "Consumption",
"colStation": "Station",
"colFile": "File",
"partial": "partial",
"gap": "gap",
"confirmDelete": "Delete this refill?"
},
"documents": {
"title": "Documents",
"subtitle": "Insurance, pollution certificates and other paperwork with renewal dates.",
"add": "Add document",
"empty": "No documents yet.",
"colType": "Type",
"colTitle": "Title",
"colProvider": "Provider",
"colIssued": "Issued",
"colRenewal": "Renewal",
"colStatus": "Status",
"colFile": "File",
"confirmDelete": "Delete this document?"
},
"reminders": {
"title": "Reminders",
"subtitle": "Renewal and service reminders are added automatically from your documents and service history.",
"add": "Add reminder",
"empty": "Nothing to be reminded about yet.",
"automatic": "Automatic",
"repeats": "Repeats",
"at": "at {km}",
"doneRollForward": "Done · roll forward",
"markDone": "Mark done",
"reopen": "Reopen",
"confirmDelete": "Delete this reminder?"
},
"parts": {
"title": "Parts catalog",
"add": "Add part",
"empty": "No parts yet.",
"colPart": "Part",
"colPartNumber": "Part number",
"colNotes": "Notes",
"colFile": "File",
"confirmDelete": "Delete this part?"
},
"delete": {
"title": "Delete this car?",
"body": "This permanently deletes {name} and everything logged against it — {services}, {maintenance}, {fuel}, {documents} and {parts}. This cannot be undone.",
"services": {
"one": "{n} service record",
"other": "{n} service records"
},
"maintenance": {
"one": "{n} workshop visit",
"other": "{n} workshop visits"
},
"fuel": {
"one": "{n} refill",
"other": "{n} refills"
},
"documents": {
"one": "{n} document",
"other": "{n} documents"
},
"parts": {
"one": "{n} part",
"other": "{n} parts"
},
"typeToConfirm": "Type {name} to confirm",
"deleting": "Deleting…",
"confirm": "Delete permanently"
}
},
"attachment": {
"legend": "Attachment",
"hint": "PDF or image, up to 10MB.",
"attached": "Attached: {name}",
"willBeRemoved": "Attachment will be removed on save."
},
"forms": {
"car": {
"addTitle": "Add a car",
"editTitle": "Edit car",
"name": "Name *",
"make": "Make",
"model": "Model",
"year": "Year",
"registration": "Registration",
"registrationCountry": "Registration country",
"registrationCountryPlaceholder": "Poland",
"vin": "VIN",
"vinPlaceholder": "Vehicle Identification Number",
"fuelType": "Fuel type",
"buildDate": "Build date",
"firstRegistration": "First registration",
"oilSpec": "Engine oil spec",
"currentKm": "Current odometer (km)",
"transmissionOilSpec": "Transmission oil spec",
"differentialOilSpec": "Differential oil spec",
"brakeFluidSpec": "Brake fluid spec",
"coolantSpec": "Coolant spec",
"serviceIntervalDays": "Service interval (days)",
"serviceIntervalKm": "Service interval (km)",
"technicalCheckIntervalDays": "Technical check interval (days)",
"technicalCheckHint": "Prefills each check's next-due date. Any check can override it with the date printed on its certificate.",
"submit": "Add car"
},
"service": {
"addTitle": "Add service record",
"editTitle": "Edit service record",
"date": "Date *",
"odometer": "Odometer (km)",
"changedParts": "Changed parts",
"oil": "Oil & Oil filter",
"engineFilter": "Engine air filter",
"cabinFilter": "Cabin air filter",
"attachmentLegend": "Receipt or service-book page",
"notes": "Notes",
"autoHint": "Next service date (+{days}d) and km (+{km}) are computed automatically.",
"submit": "Add service"
},
"technical": {
"addTitle": "Add technical check",
"editTitle": "Edit technical check",
"date": "Check date *",
"result": "Result *",
"passed": "Passed",
"failed": "Failed",
"validUntil": "Valid until",
"failedHint": "A failed check certifies nothing, so no next date is derived from it.",
"derivedHint": "Leave blank to use the car's interval (+{days}d → {date}). Enter the date on the certificate when it differs.",
"cost": "Cost",
"station": "Station",
"stationPlaceholder": "Stacja Kontroli Pojazdów",
"attachmentLegend": "Inspection certificate",
"notes": "Notes",
"submit": "Add check"
},
"part": {
"addTitle": "Add part",
"editTitle": "Edit part",
"name": "Part name *",
"namePlaceholder": "Oil Filter",
"partNumber": "Part number",
"notes": "Notes",
"notesPlaceholder": "Fits 2015–2020 · buy in pairs",
"attachmentLegend": "Photo or spec sheet",
"submit": "Add part"
},
"fuel": {
"addTitle": "Log refill",
"editTitle": "Edit refill",
"date": "Date *",
"odometer": "Odometer (km) *",
"liters": "Litres *",
"cost": "Total cost",
"pricePerLiter": "Price per litre: {price}",
"tank": "Tank",
"fullTank": "Filled to full",
"missedFill": "I missed logging a refill before this one",
"tankHint": "Consumption is measured between full tanks, so partial fills count towards the next full one. Flagging a missed refill leaves that stretch out of the figures instead of reporting it as unrealistically economical.",
"station": "Station",
"notes": "Notes",
"attachmentLegend": "Receipt",
"submit": "Log refill"
},
"maintenance": {
"addTitle": "Log workshop visit",
"editTitle": "Edit workshop visit",
"date": "Date *",
"odometer": "Odometer (km)",
"type": "Type",
"status": "Status",
"description": "What was done *",
"descriptionPlaceholder": "Replaced alternator and drive belt",
"workshop": "Workshop",
"location": "Location",
"partsUsed": "Parts replaced",
"partsUsedPlaceholder": "Alternator 27060-0T010, belt 90916-02660",
"laborCost": "Labour cost",
"partsCost": "Parts cost",
"total": "Total: {total}",
"invoiceNumber": "Invoice number",
"warrantyUntil": "Warranty until",
"attachmentLegend": "Invoice",
"notes": "Notes",
"submit": "Log visit"
},
"document": {
"addTitle": "Add document",
"editTitle": "Edit document",
"type": "Type",
"title": "Title *",
"titlePlaceholder": "Third-party liability 2026",
"provider": "Provider",
"reference": "Policy / certificate no.",
"issued": "Issued",
"renewalDate": "Renewal date",
"renewalHint": "Leave the renewal date blank for a document that never expires. Setting it adds a reminder automatically.",
"cost": "Cost",
"attachmentLegend": "Scan or photo",
"notes": "Notes",
"submit": "Add document"
},
"reminder": {
"addTitle": "Add reminder",
"editTitle": "Edit reminder",
"title": "Title *",
"titlePlaceholder": "Swap to winter tyres",
"type": "Type",
"remindMe": "Remind me",
"onDate": "On date",
"atOdometer": "At odometer (km)",
"triggerHint": "Set either or both — with both, whichever comes first wins.",
"currentKm": "The car is at {km} now.",
"repeat": "Repeat (optional)",
"everyDays": "Every … days",
"everyKm": "Every … km",
"recurringHint": "Marking this done will roll it forward instead of closing it.",
"oneOffHint": "Leave blank for a one-off reminder that closes when you mark it done.",
"notes": "Notes",
"noTrigger": "Set a due date, a due odometer reading, or both.",
"submit": "Add reminder"
},
"share": {
"title": "Share {name}",
"body": "Give another user access to this car. Read-only lets them view; read & write also lets them edit the car and its service records and parts.",
"userEmail": "User email",
"read": "Read-only",
"write": "Read & write",
"submit": "Share",
"peopleWithAccess": "People with access",
"notShared": "Not shared with anyone yet."
}
},
"enums": {
"fuelType": {
"petrol": "Petrol (gasoline)",
"petrol_lpg": "Petrol (gasoline) + LPG",
"diesel": "Diesel",
"diesel_lpg": "Diesel + LPG",
"hybrid": "Hybrid",
"electric": "Electric",
"hydrogen": "Hydrogen"
},
"maintenanceType": {
"repair": "Repair",
"inspection": "Inspection",
"bodywork": "Bodywork",
"tyres": "Tyres",
"diagnostics": "Diagnostics",
"recall": "Recall",
"warranty": "Warranty work",
"other": "Other"
},
"maintenanceStatus": {
"scheduled": "Scheduled",
"in_progress": "In progress",
"completed": "Completed"
},
"documentType": {
"insurance": "Insurance",
"pollution": "Pollution certificate",
"registration": "Registration",
"inspection": "Inspection",
"roadTax": "Road tax",
"warranty": "Warranty",
"other": "Other"
},
"reminderTypeShort": {
"maintenance": "Maintenance",
"document": "Document",
"service": "Service",
"inspection": "Inspection",
"other": "Other"
},
"reminderType": {
"maintenance": "Maintenance",
"document": "Document renewal",
"service": "Service",
"inspection": "Inspection",
"other": "Other"
}
},
"status": {
"noData": "No data",
"serviceOverdueDays": "Service Overdue {days}d",
"dueInDays": "Due in {days}d",
"okDays": "OK · {days}d",
"noKm": "No km",
"serviceOverdueKm": "Service Overdue {km} km",
"inKm": "In {km} km",
"kmLeft": "{km} km left",
"expiredAgo": "Expired {days}d ago",
"expiresToday": "Expires today",
"renewInDays": "Renew in {days}d",
"validDays": "Valid · {days}d",
"noExpiry": "No expiry",
"done": "Done",
"noTrigger": "No trigger",
"overdue": "Overdue",
"overdueBy": "Overdue {parts}",
"dueIn": "Due in {parts}",
"upcoming": "Upcoming",
"today": "today",
"days": "{days}d",
"km": "{km} km"
}
}