d5e41500a8
The backup destination is now chosen by the on-site admin in the UI (Setup -> Backup), not a server env var. An env-pinned target defeats the purpose: the admin can't point backups at a freshly-plugged USB or a NAS mount without editing .env and restarting. The encryption key stays a server secret. Target storage: - New site_config.backup_target_dir (migration 0016, nullable; null = not configured). BackupService reads it fresh each run, so a UI change takes effect with no restart. Only BACKUP_KEY stays env -- a key must never live in the DB it backs up. Routes: - PUT /api/backup/config -- set/clear the target (backup:update; upserts id=1). - POST /api/backup/test -- probe a candidate path server-side (exists / is a directory / writable) so the admin gets feedback before relying on it. - status() now exposes targetDir + keyPresent, so the UI distinguishes 'no target set' from 'BACKUP_KEY missing'. UI (apps/web/src/BackupSettings.tsx): - A Setup -> Backup tab (gated backup:read): an editable target-path field with a Test-target probe (localized ok/missing/not-a-dir/not-writable), Save, the status panel (config state, last-run size/pruned/error, a distinct amber missing-key warning), a Back up now button, and the restore-is-out-of-band note. Full i18n (sq + en); nav.backup. - API client: fetchBackupStatus / setBackupTarget / testBackupTarget / runBackup. Also includes a small in-progress copy trim to the setup-intro i18n strings. Verified live with Playwright: typed a path -> Test reported writable -> Save persisted it -> status reflected it and showed the key-missing warning. Whole monorepo build/lint/test green. Wiki backup-recovery + open-question #5 updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
935 lines
36 KiB
TypeScript
935 lines
36 KiB
TypeScript
// English (en). Mirrors the key structure of sq.ts (the default/fallback). Any key
|
||
// missing here falls back to Albanian. See wiki/concepts/i18n.md.
|
||
|
||
import type { Catalog } from "./sq.js";
|
||
|
||
export const en: Catalog = {
|
||
common: {
|
||
loading: "Loading…",
|
||
logout: "Log out",
|
||
cancel: "Cancel",
|
||
close: "Close",
|
||
save: "Save",
|
||
none: "—",
|
||
themeDark: "dark",
|
||
themeLight: "light",
|
||
theme: "Theme",
|
||
fontSmaller: "Smaller text",
|
||
fontLarger: "Larger text",
|
||
fontSize: "Text size",
|
||
today: "Today",
|
||
yesterday: "Yesterday",
|
||
months: [
|
||
"January",
|
||
"February",
|
||
"March",
|
||
"April",
|
||
"May",
|
||
"June",
|
||
"July",
|
||
"August",
|
||
"September",
|
||
"October",
|
||
"November",
|
||
"December",
|
||
],
|
||
},
|
||
auth: {
|
||
title: "Parking System",
|
||
username: "Username",
|
||
password: "Password",
|
||
signIn: "Sign in",
|
||
signingIn: "Signing in…",
|
||
},
|
||
update: {
|
||
available: "Update available",
|
||
prompt: "Version {{version}} is available. Install now and restart?",
|
||
},
|
||
nav: {
|
||
booth: "Booth",
|
||
shift: "Shift",
|
||
setup: "Setup",
|
||
devices: "Devices",
|
||
tariff: "Tariff",
|
||
tariffLab: "Tariff Lab",
|
||
subscriptions: "Subscriptions",
|
||
plans: "Plans",
|
||
site: "Site",
|
||
users: "Users",
|
||
roles: "Roles",
|
||
shifts: "Shifts",
|
||
reports: "Reports",
|
||
recycleBin: "Recycle bin",
|
||
logs: "Logs",
|
||
backup: "Backup",
|
||
profile: "Profile",
|
||
},
|
||
profile: {
|
||
title: "My profile",
|
||
accountSection: "Account",
|
||
fullName: "Full name",
|
||
fullNamePh: "First and last name",
|
||
email: "Email",
|
||
emailPh: "you@example.com",
|
||
username: "Username",
|
||
role: "Role",
|
||
saveProfile: "Save profile",
|
||
profileSaved: "Profile saved.",
|
||
passwordSection: "Change password",
|
||
currentPassword: "Current password",
|
||
newPassword: "New password",
|
||
confirmPassword: "Confirm password",
|
||
changePassword: "Change password",
|
||
passwordChanged: "Password changed.",
|
||
passwordsDontMatch: "Passwords don't match.",
|
||
passwordTooShort: "Password must be at least {{min}} characters.",
|
||
},
|
||
status: {
|
||
live: "LIVE",
|
||
connecting: "CONNECTING",
|
||
offline: "OFFLINE",
|
||
},
|
||
devices: {
|
||
footerTitle: "Devices",
|
||
none: "No devices configured.",
|
||
catAccess: "Relay",
|
||
catReader: "Reader",
|
||
catCamera: "Camera",
|
||
catPrinter: "Printer",
|
||
catVision: "ANPR",
|
||
// Role/direction suffixes for the chip label (e.g. "Reader entry").
|
||
role: {
|
||
entry: "entry",
|
||
exit: "exit",
|
||
both: "entry/exit",
|
||
mixed: "entry/exit",
|
||
lane: "at lane",
|
||
booth: "at booth",
|
||
},
|
||
state: {
|
||
ready: "ready",
|
||
degraded: "degraded",
|
||
offline: "offline",
|
||
},
|
||
allOk: "all ready",
|
||
issuesCount: "{{count}} with issues",
|
||
issuesTitle: "Device issues",
|
||
clickForIssues: "Click for details",
|
||
checkedAt: "checked {{time}}",
|
||
},
|
||
booth: {
|
||
processTicket: "Process ticket",
|
||
scanPlaceholder: "Scan or type ticket number…",
|
||
laneEntry: "Entry",
|
||
laneExit: "Exit",
|
||
openTicket: "Read",
|
||
occupancy: "Occupancy",
|
||
occUnavailable: "occupancy unavailable",
|
||
inside: "inside",
|
||
of: "of",
|
||
uncapped: "uncapped",
|
||
free: "free",
|
||
lotFull: "● lot full",
|
||
liveFeed: "Live feed",
|
||
events: "events",
|
||
noEventsYet: "No events yet — entries and exits will stream here.",
|
||
activeSessions: "Active sessions",
|
||
insideCount: "inside",
|
||
noActiveSessions: "No active sessions.",
|
||
noMatch: "No sessions match the filter.",
|
||
// Active-sessions table column headers.
|
||
colWho: "Ticket / subscriber",
|
||
colPlate: "Plate",
|
||
colEntry: "Entry",
|
||
colElapsed: "Elapsed",
|
||
badgeOverstay: "overstay",
|
||
badgeOverstayTitle:
|
||
"Paid session. The customer failed to exit during the grace period. A new period began.",
|
||
plateTitle: "Licence plate recognized ANPR.",
|
||
// filters
|
||
filterSearchSessions: "Search ticket / subscriber / plate…",
|
||
filterSearchFeed: "Search event / identity / plate…",
|
||
filterAll: "All",
|
||
fKindTransient: "Transient",
|
||
fKindSubscription: "Subscribers",
|
||
fSrcBooth: "Booth",
|
||
fSrcReader: "Reader",
|
||
fEvtEntry: "Entry",
|
||
fEvtExit: "Exit",
|
||
fEvtPay: "Pay",
|
||
fEvtVoid: "Void",
|
||
fEvtAnomaly: "Anomaly",
|
||
openPayExit: "Open pay / exit",
|
||
openBarrier: "Open barrier",
|
||
openBarrierTitle: "Human-intervention barrier open (audited)",
|
||
barrierOpened: "barrier opened",
|
||
openManually: "open manually",
|
||
badgeExiting: "exiting",
|
||
badgePaid: "paid",
|
||
badgeUnpaid: "unpaid",
|
||
badgeSubscription: "subscription",
|
||
evtEntry: "ENTRY",
|
||
evtExit: "EXIT",
|
||
evtPay: "PAY",
|
||
evtVoid: "VOID",
|
||
evtOpenCmd: "OPEN→",
|
||
evtOpenObserved: "OPEN✓",
|
||
evtShiftOpen: "SHIFT+",
|
||
evtShiftZ: "SHIFT Z",
|
||
evtCashMovement: "CASH",
|
||
evtCashIn: "PAY-IN",
|
||
evtCashOut: "PAY-OUT",
|
||
evtAnomaly: "ANOMALY",
|
||
evtRefused: "REFUSED",
|
||
// live-feed event detail line + classification badges (computed from payload)
|
||
evtNoReason: "no reason recorded",
|
||
badgeEntryRefused: "entry refused",
|
||
badgeExitRefused: "exit refused",
|
||
badgeLotFull: "lot full",
|
||
badgeBarrierFailed: "barrier did not open",
|
||
badgeManualOpen: "manual open",
|
||
badgeSubRefused: "subscription refused",
|
||
badgeSubSale: "subscription sale",
|
||
badgeWindowCharge: "out-of-window — owes fee",
|
||
badgeNoTicket: "ticket not printed",
|
||
feedSourceBooth: "booth",
|
||
feedSourceReader: "reader",
|
||
// event-detail modal
|
||
eventDetail: "Event detail",
|
||
edType: "Type",
|
||
edTime: "Time",
|
||
edIndex: "Ledger index",
|
||
edDirection: "Direction",
|
||
edSource: "Source",
|
||
edIdentity: "Identity",
|
||
edReason: "Reason",
|
||
edSnapshots: "Snapshots",
|
||
edPayload: "Signed payload",
|
||
edChain: "Chain",
|
||
edSignature: "Signature",
|
||
edKeyId: "Key",
|
||
edPrevHash: "Prev hash",
|
||
edNoPayload: "No payload on this event.",
|
||
edCopy: "Copy",
|
||
edCopied: "Copied",
|
||
edDetails: "Details",
|
||
edAuditData: "Audit data (signature & chain)",
|
||
edAmount: "Amount",
|
||
edTender: "Tender",
|
||
edSession: "Session",
|
||
edPlate: "Plate",
|
||
edCategory: "Category",
|
||
edOperator: "Operator",
|
||
edTariffVersion: "Tariff version",
|
||
edRawPayload: "Raw signed payload",
|
||
edOccurrence: "Occurrence id",
|
||
subscriber: "Subscriber",
|
||
edVia: "Entry medium",
|
||
viaQr: "QR code",
|
||
viaCard: "RFID card/chip",
|
||
viaPlate: "Plate",
|
||
},
|
||
// Localized messages for the signed REASON_CODES (see @parking/shared). Keys MUST
|
||
// match the codes 1:1; {{param}} placeholders are filled from the event's
|
||
// reasonParams. Legacy events with no code fall back to the signed English `reason`.
|
||
reason: {
|
||
"entry.refused.full": "Entry refused — lot full ({{count}}/{{capacity}})",
|
||
"entry.held.noTicket": "Entry held — ticket not printed: {{detail}}",
|
||
"exit.refused.closed": "Exit refused — session already closed",
|
||
"exit.refused.noSession": "Exit refused — unknown ticket",
|
||
"exit.refused.unpaid": "Exit refused — not paid (take payment first)",
|
||
"exit.refused.graceExpired": "Exit refused — walk-back grace expired (top-up required)",
|
||
"exit.open.noBarrier": "Exit recorded, but no exit barrier is configured — open manually",
|
||
"exit.open.unavailable": "Exit recorded, but the barrier is unavailable — open manually",
|
||
"exit.open.failed": "Exit recorded, but the barrier did not open — open manually",
|
||
"exit.freeGrace": "Free entry-grace (no charge)",
|
||
"exit.manualOpen": "Manual barrier open (human intervention)",
|
||
"sub.refused.notFound": "Subscription refused — not found",
|
||
"sub.refused.outOfWindow": "Subscription refused — {{status}}/out-of-window",
|
||
"sub.refused.noSession": "Subscription exit with no open session (already out / never entered)",
|
||
"sub.refused.atCapacity": "Subscription refused — at capacity ({{inUse}}/{{max}} cars in)",
|
||
"sub.refused.unpaidWindow": "Exit refused — out-of-window charge unpaid ({{amount}} {{currency}}); pay at the booth",
|
||
"void.ticketCancelled": "Ticket cancelled — {{reason}}",
|
||
},
|
||
tariff: {
|
||
title: "Tariff",
|
||
noRateCard: "No tariff published yet — the pay station can't charge until you publish one.",
|
||
activeSince: "Active since {{date}} · {{count}} version(s) in history. Publishing creates a new version; past sessions keep their original pricing.",
|
||
currency: "Currency",
|
||
freeEntryGrace: "Free entry grace (min)",
|
||
billingIncrement: "Billing increment (min)",
|
||
dailyCap: "Daily cap (blank = none)",
|
||
dailyCapPh: "e.g. 12.00",
|
||
lostTicketFee: "Lost-ticket fee",
|
||
exitGrace: "Exit walk-back grace (min)",
|
||
rateBlocks: "Rate blocks",
|
||
rateBlocksHint: "Each band lasts a number of hours and bills at its own price; bands are consumed in order (the first hours, then the next hours). The last band is \"thereafter\" (open-ended) — its price applies once the ladder is exhausted. Price is per billing increment.",
|
||
bandDuration: "Band duration",
|
||
hoursUnit: "hours",
|
||
egHours: "e.g. 2",
|
||
pricePerIncrement: "Price / increment",
|
||
thereafter: "thereafter (open-ended)",
|
||
remove: "Remove",
|
||
addBlock: "+ Add block",
|
||
publishNewVersion: "Publish new version",
|
||
publishing: "Publishing…",
|
||
publishedOk: "New tariff version published — it's now the active rate.",
|
||
defaultCard: "Base rate (always active)",
|
||
defaultCardHint: "The base rate applied when no time/seasonal tier matches. This alone is enough for most car parks.",
|
||
modeLadder: "Hourly ladder",
|
||
modeFlat: "Flat price",
|
||
modeStepped: "By duration (up-to)",
|
||
steppedHint:
|
||
"Set the TOTAL price for a stay up to a given time (e.g. up to 3h = 500). The first row whose limit ≥ the duration wins (the limit is inclusive). The last row's total repeats as a per-day price for longer stays.",
|
||
stepUpTo: "Up to",
|
||
stepTotal: "Total price",
|
||
addStep: "+ Add row",
|
||
steppedTiersConflict:
|
||
"⚠ Time/seasonal tiers do NOT apply when the base rate is 'By duration (up-to)' — the engine ignores them entirely. Remove the tiers, or switch the base rate to 'Hourly ladder' or 'Flat price'. Publishing is blocked until this is fixed.",
|
||
tiersAdvanced: "Advanced: time & seasonal tiers",
|
||
tiersHint: "Optional. Add tiers that apply only at certain hours/days/dates or for a category (e.g. happy hour, night rate, weekend, bus). With no tiers, just the base rate is published.",
|
||
tierName: "Name",
|
||
tierPriority: "Priority",
|
||
tierCategory: "Category",
|
||
tierCategoryPh: "e.g. bus",
|
||
tierDays: "Days",
|
||
tierHours: "Hours",
|
||
tierDates: "Dates",
|
||
tierOvernight: "(crosses midnight)",
|
||
addTier: "+ Add tier",
|
||
dow1: "Mon",
|
||
dow2: "Tue",
|
||
dow3: "Wed",
|
||
dow4: "Thu",
|
||
dow5: "Fri",
|
||
dow6: "Sat",
|
||
dow0: "Sun",
|
||
},
|
||
setup: {
|
||
title: "Setup",
|
||
intro:
|
||
"Add your barrier controllers first. Then add readers (QR/RF), cameras, printers",
|
||
catControllers: "Controllers (barriers + entry button)",
|
||
catReaders: "Readers (QR / RFID)",
|
||
catCameras: "Cameras (snapshot + plate)",
|
||
catPrinters: "Printers (tickets / vouchers)",
|
||
nounController: "controller",
|
||
nounReader: "reader",
|
||
nounCamera: "camera",
|
||
nounPrinter: "printer",
|
||
add: "+ Add {{noun}}",
|
||
addAnother: "+ Add another {{noun}}",
|
||
addTitle: "Add {{noun}}",
|
||
editTitle: "Edit {{noun}}",
|
||
needControllerFirst: "Add a controller first — a {{noun}} points at one of its relays.",
|
||
failedToLoad: "Failed to load setup: {{error}}",
|
||
loadingCatalog: "Loading device catalog…",
|
||
dirEntry: "Entry",
|
||
dirExit: "Exit",
|
||
dirBoth: "Both (entry + exit)",
|
||
inherits: "inherits {{direction}}",
|
||
warnTitle: "⚠ Saved, but action needed:",
|
||
dismiss: "Dismiss",
|
||
disabled: "(disabled)",
|
||
edit: "Edit",
|
||
remove: "Remove",
|
||
removing: "Removing…",
|
||
confirmRemove: "Remove this {{driver}} device?",
|
||
noRelaysSet: "no relays set",
|
||
unbound: "unbound",
|
||
noDrivers: "No drivers registered.",
|
||
chooseDevice: "Choose a device…",
|
||
scan: "Scan for controllers",
|
||
scanning: "Scanning…",
|
||
noControllersFound: "No controllers found on the LAN.",
|
||
use: "Use",
|
||
test: "Test connection",
|
||
testing: "Testing…",
|
||
saveConfigure: "Save & configure",
|
||
saveChanges: "Save changes",
|
||
saving: "Saving…",
|
||
cancel: "Cancel",
|
||
testFailed: "Test failed: {{error}}",
|
||
saveFailed: "Save failed: {{error}}",
|
||
deviceLabel: "Device:",
|
||
preconditionsOk: "● preconditions OK",
|
||
autoFixedOnSave: "(auto-fixed on save)",
|
||
backendPushIp: "Backend push IP",
|
||
chooseAddress: "Choose an address…",
|
||
onDeviceSubnet: "— on device subnet",
|
||
noNicOnSubnet: "⚠ no NIC on the device's subnet — the device may not reach the backend",
|
||
backendIpHint: "The address this device will POST input events to.",
|
||
relaysTitle: "Relays on this controller",
|
||
relaysHint:
|
||
"Each relay opens one barrier. Set its direction; for transient entry, set which input terminal the entry button is wired to.",
|
||
outputsTitle: "Outputs — relays (barriers + lamp)",
|
||
outputsHint:
|
||
"Relays are OUTPUTS: each opens a barrier (or drives the button lamp). Set the relay number and direction. The input terminals (button, sensor) are in the Inputs section below.",
|
||
pulseOpenMs: "Pulse open (ms)",
|
||
pulseOpenHint: "How long a barrier relay is held open (jog). Applies to all barrier relays.",
|
||
inputsTitle: "Inputs — terminals (button, sensor)",
|
||
inputsHint:
|
||
"Inputs are TERMINALS the host READS: the entry button and the presence/radar sensor. Each belongs to an entry barrier — it triggers or gates that relay.",
|
||
inputsIdleHigh: "Inputs idle HIGH",
|
||
inputsIdleHighHint: "This board idles inputs HIGH (status 1111); a press pulls LOW.",
|
||
relay: "Relay",
|
||
// Generic input rows: terminal + role + the relay it serves.
|
||
inputTerminal: "Terminal",
|
||
inputServesRelay: "Serves relay",
|
||
roleButton: "Entry button",
|
||
rolePresenceLoop: "Presence (loop)",
|
||
rolePresenceRadar: "Presence (radar)",
|
||
roleAlertTrigger: "Alert trigger",
|
||
addInput: "+ Add input",
|
||
entryCooldown: "Cooldown after ticket (s)",
|
||
entryCooldownHint:
|
||
"When there's no presence sensor: repeat button presses are suppressed for this many seconds after a ticket. A fallback (not a guarantee) — a determined abuser can wait it out.",
|
||
activeLow: "Active-low",
|
||
activeLowHint:
|
||
"Tick if the presence sensor (e.g. a radar) idles HIGH and goes LOW on detection — the opposite of the button. This inverts that terminal's reading so 'present' is read correctly.",
|
||
eventRadarAlert: "Radar alert (lamp)",
|
||
triggerInput: "Trigger input",
|
||
triggerInputHint:
|
||
"The input terminal (the radar) that starts this relay blinking. Blinks while the trigger is active but the camera doesn't confirm a car; solid on once the camera confirms; off otherwise.",
|
||
lockLane: "Lock from",
|
||
lockLaneHint:
|
||
"Which camera locks the lamp solid: the entry or the exit camera. An exit radar must lock on the EXIT camera.",
|
||
lockLaneEntry: "Entry camera",
|
||
lockLaneExit: "Exit camera",
|
||
blinkOnMs: "Blink on (ms)",
|
||
blinkOffMs: "Blink off (ms)",
|
||
addRelay: "+ Add relay",
|
||
anpr: "Plate recognition (ANPR)",
|
||
anprHint:
|
||
"Read plates on this camera: the vision service reads the plate from each snapshot and records it (both entry and exit). Requires the vision service running.",
|
||
anprAuto: "Auto open/close on subscriber plate",
|
||
anprAutoHint:
|
||
"Let THIS camera auto-open the barrier when it recognises a subscriber's plate. Turn OFF on a shared entry/exit lane's exit camera, so a car driving IN isn't auto-EXITed by its back plate (recognition still runs — only the auto-trigger is off).",
|
||
testAnpr: "Test ANPR",
|
||
anprTesting: "Testing ANPR…",
|
||
testAnprHint:
|
||
"Takes a live snapshot from this camera and tries to read a plate, reporting the result and the time it took. Point a plate at the camera first.",
|
||
anprOk: "✓ Read plate {{plate}} — {{confidence}}% confidence, {{ms}} ms",
|
||
anprLowConfidence: "(low confidence — advisory only)",
|
||
anprTookMs: "{{ms}} ms",
|
||
"anprFail.vision-disabled": "Vision service is disabled — enable it (VISION_ENABLED) to test ANPR.",
|
||
"anprFail.snapshot-failed": "Couldn't take a snapshot from the camera (offline or unreachable).",
|
||
"anprFail.no-plate": "No plate found in the snapshot.",
|
||
// Printer test slip — pushes a real slip so the admin can confirm it physically prints.
|
||
testPrint: "Print test slip",
|
||
printTesting: "Printing…",
|
||
testPrintHint:
|
||
"Sends a test slip to the printer now. ‘Connected’ only opens the link — this confirms the printer actually feeds paper.",
|
||
printOk: "✓ Test slip sent ({{ms}} ms). Check the printer.",
|
||
"printFail.print-failed": "The printer rejected the job (out of paper, cover open, or the link dropped).",
|
||
// Reveal/hide toggle for a secret field (e.g. the device web password).
|
||
revealSecret: "Show password",
|
||
hideSecret: "Hide password",
|
||
alarmUrlTitle: "Alarm Server settings (enter these in the camera)",
|
||
alarmUrlHint:
|
||
"Enter these in the camera at Configuration → Event → … → Alarm Settings (or Notify Surveillance Center). The camera POSTs every event here — no polling.",
|
||
alarmUrlCopy: "Copy all",
|
||
alarmUrlCopied: "Copied ✓",
|
||
alarmUrlSaveFirst:
|
||
"Save the camera first — the address is generated once the device has an ID. Re-open it for editing to see it.",
|
||
alarmUrlTestFirst:
|
||
"Click “Test connection” first — that resolves this host's IP on the camera's network (so the camera can reach it).",
|
||
alarmFieldHost: "Destination IP / Host",
|
||
alarmFieldUrl: "URL",
|
||
alarmFieldProtocol: "Protocol",
|
||
alarmFieldPort: "Port",
|
||
whichBarrier: "Which barrier does this device serve?",
|
||
controller: "Controller",
|
||
choose: "Choose…",
|
||
relayLabel: "Relay {{relay}} ({{direction}})",
|
||
noRelaysConfigured: "This controller has no relays configured.",
|
||
},
|
||
lab: {
|
||
title: "Tariff Lab",
|
||
intro:
|
||
"Test rates in time (day/night windows, daily caps, overstay) in seconds, with no waiting. Pricing uses the same logic as the booth; nothing is written to the ledger.",
|
||
loadTicket: "Load from a real ticket",
|
||
loadTicketPh: "Ticket number / identity",
|
||
load: "Load",
|
||
loaded: "Loaded session {{id}}",
|
||
tariffVersion: "Tariff version",
|
||
activeVersion: "Active version (current)",
|
||
entered: "Entered",
|
||
asOf: "As of (now/exit)",
|
||
now: "Now",
|
||
category: "Category",
|
||
categoryPh: "e.g. bus (blank = car)",
|
||
payment: "Payment",
|
||
paid: "paid",
|
||
graceMin: "grace (min)",
|
||
price: "Compute price",
|
||
pricing: "Pricing…",
|
||
outcome: "Outcome",
|
||
amountDue: "Amount due",
|
||
billedPeriod: "Billed period",
|
||
overstay: "overstay",
|
||
settled: "settled",
|
||
periodStart: "Period start",
|
||
graceExpires: "Grace expires",
|
||
curve: "Duration curve",
|
||
curveHint: "Fee from entry at several durations — see where the daily cap flattens or windows shift.",
|
||
},
|
||
subs: {
|
||
title: "Subscriptions",
|
||
unnamed: "(unnamed)",
|
||
unbound: "unbound",
|
||
car_one: "{{count}} car",
|
||
car_other: "{{count}} cars",
|
||
cred: "cred",
|
||
plates: "{{count}} plate(s)",
|
||
noPrice: "no price",
|
||
perDay: "day",
|
||
perWeek: "week",
|
||
perMonth: "month",
|
||
plan: "Plan",
|
||
quantity: "Cars",
|
||
quantityHint: "cars covered by this subscription (price ×N)",
|
||
count: "How many",
|
||
planNone: "— comp / no charge —",
|
||
planNoneAvail: "No plans defined — an admin must create one first.",
|
||
version: "Version",
|
||
versionCurrent: "current",
|
||
versionHint: "Move this subscriber to another version of the same plan. The price stays as billed; only the access hours change going forward.",
|
||
versionOnlyOne: "only one version of this plan",
|
||
everyDay: "every day",
|
||
allDay: "24/7 (no window)",
|
||
quoting: "pricing…",
|
||
quotePrompt: "pick an end date",
|
||
quoteLine: "{{periods}} × {{unit}} · {{amount}} {{currency}}",
|
||
tender: "Paid by",
|
||
tenderCash: "Cash",
|
||
tenderCard: "Card",
|
||
tenderHint: "Recorded as a signed payment (feed, drawer, Z-report).",
|
||
saleRecorded: "Sale recorded: {{amount}} {{currency}} ({{tender}}).",
|
||
saleNoShift: "⚠ No shift was open — open one so the takings land in a Z-report.",
|
||
edit: "Edit",
|
||
revoke: "Revoke",
|
||
delete: "Delete",
|
||
noneYet: "No subscriptions yet.",
|
||
add: "+ Add subscription",
|
||
new: "New subscription",
|
||
editTitle: "Edit subscription",
|
||
holderName: "Holder name",
|
||
contact: "Contact",
|
||
carLimit: "Car limit",
|
||
limitCarsInAtOnce: "limit cars in at once",
|
||
validFrom: "Valid from",
|
||
validTo: "Valid to",
|
||
validToEnd: "Valid to (end)",
|
||
isoDateOptional: "ISO date (optional)",
|
||
boundPlates: "Bound plates",
|
||
commaSeparatedOptional: "comma-separated (optional)",
|
||
credentials: "Credentials",
|
||
credentialsCardQr: "Credentials (card / QR)",
|
||
rfCardTag: "RF card/tag",
|
||
rfCardTagSoon: "RF card/tag (soon)",
|
||
rfPlaceholder: "card number (or read the card)",
|
||
readCard: "Read card",
|
||
captureChooseReader: "Choose a reader, then present the card:",
|
||
captureNoReaders: "No readers configured.",
|
||
captureWaiting: "Present the card to the reader…",
|
||
captureTimeout: "Timed out with no card read. Try again.",
|
||
captured: "Card read: {{value}}",
|
||
qr: "QR",
|
||
qrAutoGen: "QR code is auto-generated on save",
|
||
credentialValue: "credential value",
|
||
addCredential: "+ credential",
|
||
needCredentialOrPlate: "A subscription needs at least one credential OR one bound plate.",
|
||
save: "Save",
|
||
cancel: "Cancel",
|
||
saved: "Subscription saved.",
|
||
savedPrinted: "Subscription saved — QR code printed.",
|
||
savedPrintFailed: "Subscription saved, but printing failed ({{error}}). Use \"Print code\".",
|
||
printCode: "Print code",
|
||
printedOn: "Code printed on {{printer}}.",
|
||
confirmRevoke: "Revoke subscription for {{name}}? It will be refused at the barrier.",
|
||
confirmDelete: "Delete subscription for {{name}}? (Past events are kept.)",
|
||
statusActive: "active",
|
||
statusSuspended: "suspended",
|
||
statusRevoked: "revoked",
|
||
},
|
||
plans: {
|
||
title: "Subscription plans",
|
||
intro: "Admin-defined plans the operator sells from — the price is looked up, never typed. Editing a plan publishes a new version; past sales keep their recorded price.",
|
||
add: "+ Add plan",
|
||
noneYet: "No plans yet. Add one so the booth can sell subscriptions.",
|
||
colName: "Name",
|
||
colPrice: "Price",
|
||
colHours: "Hours",
|
||
colUsedBy: "Used by",
|
||
colEffective: "Effective",
|
||
allHours: "24/7",
|
||
usedByCount: "{{active}} active / {{total}}",
|
||
usedByNone: "none",
|
||
usedByTitle: "Show the subscriptions on this plan",
|
||
subscribers: "Subscribers",
|
||
inForce: "in force",
|
||
retired: "retired",
|
||
versionCount: "{{count}} versions",
|
||
newVersion: "New version",
|
||
retire: "Retire",
|
||
reactivate: "Reactivate",
|
||
delete: "Delete",
|
||
deleteTitle: "Delete this plan permanently (only when no subscription uses it)",
|
||
reactivated: "“{{name}}” is sellable again.",
|
||
confirmDelete: "Delete the plan “{{name}}” permanently? This can't be undone.",
|
||
deleted: "Plan “{{name}}” deleted.",
|
||
deleteInUse: "Can't delete — subscriptions still use this plan. Retire it instead.",
|
||
newTitle: "New plan",
|
||
newVersionTitle: "Publish new version",
|
||
newVersionHint: "This publishes a NEW version of the plan — existing sales keep their original price.",
|
||
period: "Period",
|
||
pricePer: "Price per period",
|
||
namePlaceholder: "e.g. Hotel daily",
|
||
needName: "A plan name is required.",
|
||
needPrice: "Enter a price greater than zero.",
|
||
saved: "Plan saved.",
|
||
confirmRetire: "Retire the plan “{{name}}”? It will no longer be sellable (history is kept).",
|
||
needWindow: "Enter valid window times (HH:MM).",
|
||
needDays: "Select at least one day for the window.",
|
||
restrictTimes: "Restrict parking times (charge transient tariff outside the window)",
|
||
days: "Days",
|
||
window: "Window",
|
||
enterAfter: "enter after",
|
||
exitBefore: "· exit before",
|
||
grace: "Grace",
|
||
graceHint: "minutes tolerance around the window edges",
|
||
timeframesHint: "A scan outside the allowed window is charged the normal transient tariff for the out-of-window minutes (early entry is deferred to exit; late exit is gated until paid).",
|
||
},
|
||
site: {
|
||
occupancy: "Occupancy:",
|
||
noCapacitySet: "(no capacity set)",
|
||
free: "free",
|
||
full: "FULL",
|
||
capacityLabel: "Capacity (blank = no limit):",
|
||
capacityPlaceholder: "e.g. 120",
|
||
printExitDefault: "Print exit ticket by default",
|
||
printExitHint: "(booth far from exit → customer self-exits with a voucher)",
|
||
reserveSubs: "Reserve subscriber spots",
|
||
reserveSubsHint: "Hold a spot for each active subscriber's car(s) even when they're not parked — transients see 'full' sooner. Off: only cars inside count (handle overflow by valet).",
|
||
anprEntry: "Auto-open for subscriber plates (ANPR)",
|
||
anprEntryHint: "When on, a subscriber's plate read by a lane camera opens the barrier through the normal subscription gate. Off: subscribers must use their card/QR. Plate snapshots are still recorded either way.",
|
||
parkDetails: "Park details (optional — shown on tickets/receipts)",
|
||
save: "Save",
|
||
saved: "Saved.",
|
||
fieldParkName: "Park name",
|
||
fieldParkNamePh: "e.g. Airport Parking",
|
||
fieldOperator: "Operator (legal name)",
|
||
fieldOperatorPh: "operating company",
|
||
fieldNius: "NIUS",
|
||
fieldNiusPh: "e.g. L01234567A",
|
||
fieldAddress: "Address",
|
||
fieldPhone: "Phone",
|
||
fieldEmail: "Email",
|
||
},
|
||
users: {
|
||
title: "Users",
|
||
add: "+ Add user",
|
||
new: "New user",
|
||
none: "No users.",
|
||
username: "Username",
|
||
password: "Password",
|
||
passwordHint: "At least 8 characters.",
|
||
newPassword: "new password",
|
||
role: "Role",
|
||
resetPassword: "Reset password",
|
||
edit: "Edit",
|
||
editTitle: "Edit user",
|
||
save: "Save",
|
||
delete: "Delete",
|
||
confirmDelete: "Delete user \"{{name}}\"?",
|
||
detailsSection: "Details (optional)",
|
||
fullName: "Full name",
|
||
phone: "Phone",
|
||
email: "Email",
|
||
address: "Address",
|
||
},
|
||
roles: {
|
||
title: "Roles",
|
||
add: "+ Add role",
|
||
new: "New role",
|
||
editTitle: "Edit role",
|
||
name: "Name",
|
||
permissions: "Permissions",
|
||
builtin: "built-in",
|
||
edit: "Edit",
|
||
delete: "Delete",
|
||
confirmDelete: "Delete role \"{{name}}\"?",
|
||
permCount_one: "{{count}} permission",
|
||
permCount_other: "{{count}} permissions",
|
||
userCount_one: "{{count}} user",
|
||
userCount_other: "{{count}} users",
|
||
},
|
||
shift: {
|
||
label: "Shift:",
|
||
open: "open",
|
||
notStarted: "not started",
|
||
since: "since",
|
||
startShift: "Start shift",
|
||
starting: "Starting…",
|
||
endShift: "End shift",
|
||
ending: "Ending…",
|
||
endConfirm: "End this shift? A signed Z-report is recorded and printed.",
|
||
drawer: "Drawer:",
|
||
openingFloatInherited: "(opening float inherited from the prior shift)",
|
||
drawerCashAdmin: "Drawer cash (admin) — load or remove the float",
|
||
drawerVoucher: "Drawer voucher — operator raises, an admin authorizes",
|
||
amount: "amount",
|
||
reasonPlaceholder: "reason (e.g. opening float)",
|
||
load: "Load +",
|
||
remove: "Remove −",
|
||
authName: "admin username",
|
||
authPassword: "admin password",
|
||
authRequired: "An admin must authorize: enter their username and password.",
|
||
mandatArketimi: "Receipt (in) +",
|
||
mandatPagese: "Disbursement (out) −",
|
||
voucherHint: "A receipt (Mandat Arkëtimi) adds cash; a disbursement (Mandat Pagese) removes it. The float only moves with an admin's sign-off.",
|
||
voucherRecorded: "Voucher {{no}} recorded. Drawer now {{amount}}.",
|
||
enterPositive: "Enter a positive amount.",
|
||
drawerNow: "Drawer now {{amount}}.",
|
||
zReport: "SHIFT CLOSE",
|
||
viewTakings: "Takings so far",
|
||
xReport: "TAKINGS SO FAR",
|
||
asOf: "as of",
|
||
xReportHint: "View only — nothing is recorded. These figures are signed when the shift is closed.",
|
||
payments: "Payments:",
|
||
cash: "Cash:",
|
||
card: "Card:",
|
||
srcTickets: "Tickets:",
|
||
srcSubscriptions: "Subscriptions:",
|
||
srcSubWindow: "out-of-window",
|
||
drawerSection: "— Drawer —",
|
||
openingFloat: "Opening cash:",
|
||
cashTaken: "Cash taken:",
|
||
cashAdded: "Cash added:",
|
||
cashRemoved: "Cash removed:",
|
||
expectedDrawer: "Expected drawer:",
|
||
printedToReceipt: "Printed to booth receipt.",
|
||
recordedNoPrinter: "Recorded (no printer to print to).",
|
||
// Header shift control + the booth shift gate.
|
||
headerNoShift: "No shift",
|
||
headerOpen: "Open shift",
|
||
headerClose: "Close shift",
|
||
headerHeldBy: "Shift open — {{operator}}",
|
||
headerHeldByShort: "Shift: {{operator}}",
|
||
gateTitle: "Open a shift to process tickets",
|
||
gateBody:
|
||
"No shift is open. Open your shift so payments and exits are recorded against it.",
|
||
gateOtherTitle: "The open shift belongs to another operator",
|
||
gateOtherBody:
|
||
"{{operator}} has an open shift. Only one shift may be open at a time — they must close theirs before you can open yours.",
|
||
openNow: "Open shift now",
|
||
opening: "Opening…",
|
||
},
|
||
shifts: {
|
||
title: "Shift history",
|
||
myTitle: "My shifts",
|
||
none: "No closed shifts.",
|
||
operator: "Operator",
|
||
started: "Started",
|
||
ended: "Ended",
|
||
payments: "Payments",
|
||
cash: "Cash",
|
||
card: "Card",
|
||
srcTickets: "Tickets",
|
||
srcSubscriptions: "Subscriptions",
|
||
srcSubWindow: "out-of-window",
|
||
expectedDrawer: "Expected drawer",
|
||
filterFrom: "From",
|
||
filterTo: "To",
|
||
allOperators: "All operators",
|
||
apply: "Apply",
|
||
clear: "Clear",
|
||
timeframe: "Timeframe",
|
||
preset_yesterday: "Yesterday",
|
||
preset_week: "Last week",
|
||
preset_month: "Last month",
|
||
preset_all: "All",
|
||
preset_custom: "Custom",
|
||
selectAShift: "Select a shift to see its activity log.",
|
||
noActivity: "No activity in this shift.",
|
||
current: "current",
|
||
drawerSection: "Drawer",
|
||
openingFloat: "Opening cash",
|
||
cashTaken: "Cash taken",
|
||
cashAdded: "Cash added",
|
||
cashRemoved: "Cash removed",
|
||
loadFailed: "Failed to load shifts.",
|
||
},
|
||
reports: {
|
||
title: "Reports",
|
||
groupBy: "Group by",
|
||
exportCsv: "Export CSV",
|
||
loadFailed: "Couldn't load the report: {{error}}",
|
||
noData: "No data in this range.",
|
||
footnote: "Counts and money are summed from the signed event log. Times shown in {{tz}}.",
|
||
preset: { today: "Today", "7d": "7 days", "30d": "30 days", "90d": "90 days" },
|
||
bucket: { hour: "Hour", day: "Day", month: "Month" },
|
||
kpi: {
|
||
entries: "Entries",
|
||
exits: "Exits",
|
||
revenue: "Revenue",
|
||
payments: "Payments",
|
||
avgStay: "Avg stay",
|
||
subscribers: "Subscribers",
|
||
},
|
||
chart: {
|
||
flow: "Entries & exits over time",
|
||
revenue: "Revenue ({{currency}})",
|
||
mix: "Revenue mix",
|
||
peakHours: "Entries by hour of day",
|
||
breakdown: "Breakdown",
|
||
},
|
||
mix: { ticket: "Transient", subSales: "Subscriptions", subWindow: "Out-of-window" },
|
||
row: {
|
||
cash: "Cash",
|
||
card: "Card",
|
||
closed: "Closed sessions",
|
||
medianStay: "Median stay",
|
||
subActive: "Active subscriptions",
|
||
subCars: "Cars covered",
|
||
},
|
||
},
|
||
recycleBin: {
|
||
title: "Recycle bin",
|
||
retentionNote: "Deleted items are kept for {{days}} days, then permanently removed.",
|
||
empty: "Nothing deleted. Items you delete appear here, recoverable until they expire.",
|
||
col: { type: "Type", item: "Item", deleted: "Deleted", actions: "" },
|
||
kind: {
|
||
user: "User",
|
||
role: "Role",
|
||
subscription: "Subscription",
|
||
plan: "Plan",
|
||
tariff: "Tariff",
|
||
},
|
||
restore: "Restore",
|
||
purge: "Purge",
|
||
purgeConfirmTitle: "Purge permanently?",
|
||
purgeConfirmBody: "Permanently delete “{{label}}”? It cannot be restored after this.",
|
||
purgeIrreversible: "This is irreversible.",
|
||
},
|
||
logs: {
|
||
title: "System logs",
|
||
refresh: "Refresh",
|
||
level: "Level",
|
||
source: "Source",
|
||
since: "Since",
|
||
apply: "Apply",
|
||
clear: "Clear",
|
||
allLevels: "All levels",
|
||
allSources: "All sources",
|
||
frontend: "Frontend",
|
||
backend: "Backend",
|
||
time: "Time",
|
||
message: "Message",
|
||
status: "Status",
|
||
path: "Path",
|
||
empty: "No logs.",
|
||
},
|
||
backup: {
|
||
title: "Backup",
|
||
intro:
|
||
"An encrypted copy of the database (the signed ledger) to an external disk. Runs automatically every day and from the button below.",
|
||
statusTitle: "Status",
|
||
configured: "Enabled",
|
||
notConfigured: "Not configured",
|
||
notConfiguredHint: "Set BACKUP_TARGET_DIR and BACKUP_KEY on the server to enable backups.",
|
||
running: "Running…",
|
||
idle: "Idle",
|
||
lastSuccess: "Last successful backup",
|
||
lastError: "Last error",
|
||
never: "Never",
|
||
lastFile: "File",
|
||
size: "Size",
|
||
pruned: "Pruned",
|
||
runNow: "Back up now",
|
||
runSuccess: "Backup complete.",
|
||
runFailed: "Backup failed.",
|
||
notConfiguredError: "Backup is not configured.",
|
||
restoreNote:
|
||
"Restore is not done here — it's an out-of-band step when provisioning a fresh appliance (needs the backup file + the keys you escrowed offsite).",
|
||
targetLabel: "Backup location",
|
||
targetPlaceholder: "e.g. /mnt/backup or /media/usb",
|
||
targetHint: "An absolute path to a mounted disk (USB/SATA) or a network share (SMB/NFS).",
|
||
save: "Save",
|
||
saved: "Saved.",
|
||
test: "Test target",
|
||
testOk: "The location is writable.",
|
||
testEmpty: "Enter a path.",
|
||
testMissing: "The location does not exist.",
|
||
testNotDir: "The path is not a directory.",
|
||
testNotWritable: "The directory is not writable.",
|
||
keyMissing: "The encryption key (BACKUP_KEY) is missing on the server — set it to enable backups.",
|
||
},
|
||
pay: {
|
||
ticket: "Ticket",
|
||
entry: "Entry",
|
||
now: "Now",
|
||
duration: "Duration",
|
||
statusLabel: "Status",
|
||
paid: "PAID",
|
||
unpaid: "UNPAID",
|
||
overstay: "OVERSTAY",
|
||
overstayHint: "Earlier session paid. The customer failed to exit during the grace period. Payment for the new period is required. The total below is the new period's fee.",
|
||
topUp: "New period due",
|
||
total: "Total",
|
||
noTariff: "no tariff",
|
||
tender: "Tender",
|
||
cash: "Cash",
|
||
card: "Card",
|
||
printExitVoucher: "Print exit ticket",
|
||
selfExitHint: "(customer self-exits at the exit)",
|
||
payAndOpen: "Pay + open barrier",
|
||
payAndVoucher: "Pay + print voucher",
|
||
openBarrier: "Open barrier",
|
||
printVoucher: "Print voucher",
|
||
takingPayment: "taking payment…",
|
||
printingVoucher: "printing voucher…",
|
||
opening: "opening…",
|
||
noSessionFound: "No session found for this ticket.",
|
||
alreadyClosed: "This session is already closed (exited {{time}}).",
|
||
lookingUp: "looking up…",
|
||
paidBarrierOpened: "Paid — barrier opened. Car may exit.",
|
||
paidExitRecorded: "Paid and exit recorded, but the barrier did not open: {{reason}}.",
|
||
subscription: "SUBSCRIPTION",
|
||
plan: "Plan",
|
||
prepaid: "PREPAID",
|
||
subAssistHint: "Prepaid subscription. Open the barrier to assist the exit (faulty reader / missing card). No payment.",
|
||
assistOpenReveal: "Assist open (faulty reader / lost card)…",
|
||
payWindowCharge: "Take payment",
|
||
windowPaidHint: "Window charge paid. Open the barrier to let the subscriber out.",
|
||
windowCharge: "OUT-OF-WINDOW",
|
||
windowChargeHint: "This subscriber parked outside their plan's allowed hours. They owe the transient tariff for the out-of-window time — take payment, then open the barrier.",
|
||
subBarrierOpened: "Barrier opened for the subscriber (intervention recorded).",
|
||
voucherPrinted: "Exit voucher printed on {{printer}}. Customer self-exits at the exit.",
|
||
// payment receipt (transparency slip)
|
||
receiptPrintFailed: "(receipt didn't print — use \"Reprint receipt\".)",
|
||
receiptReprinted: "Receipt reprinted on {{printer}}.",
|
||
reprintReceipt: "Reprint receipt",
|
||
reprinting: "printing…",
|
||
cancelTicket: "Cancel ticket",
|
||
cancelTicketTitle: "Cancel this ticket",
|
||
cancelTicketHint: "Cancels a wrongly-printed ticket. A signed record is kept (operator + reason); the original entry is never deleted.",
|
||
cancelReason: {
|
||
misprint: "Misprint",
|
||
test: "Test",
|
||
wrongVehicle: "Wrong vehicle",
|
||
},
|
||
cancelReasonPlaceholder: "Reason for cancelling (required)…",
|
||
confirmCancelTicket: "Confirm cancellation",
|
||
cancelling: "cancelling…",
|
||
ticketCancelled: "Ticket cancelled.",
|
||
noSnapshots: "no snapshots",
|
||
loadingSnapshots: "loading snapshots…",
|
||
snapEntry: "entry",
|
||
snapExit: "exit",
|
||
snapFailed: "camera unreachable",
|
||
plate: "Plate",
|
||
},
|
||
};
|