Files
parking_solution/apps/web/src/lib/i18n/sq.ts
T
julian 4418594af0
Build desktop / desktop (push) Successful in 4m16s
Build & push images / images (push) Successful in 2m43s
CI / check (push) Successful in 38s
refactor(setup): unify controller I/O — event-driven relays[] + generic inputs[]
The controller new/edit modal hardcoded both its outputs and its inputs, so an
operator could neither add a generic event-driven relay nor a free-standing input
(e.g. a second radar at the exit). This unifies both into symmetric, first-class
lists. Behaviour for existing booths is unchanged (back-compat, no DB migration).

Outputs — one event→action relays[] list:
- A relay is "when EVENT X happens, do its action": entry/exit/both pulse a
  barrier; a new `radarAlert` event drives a non-barrier alert lamp (blink while
  its trigger input is active, SOLID once the camera confirms a car).
- Dropped the separate config.buttonLight block — the lamp is just a relays[] row
  with direction:"radarAlert" (triggerInput + blink cadence). `alertRelaysOf()`
  replaces `buttonLightOf()`; ButtonLightController keeps its proven 3-state
  machine (serialized UDP, fail-OFF, hot-reload), now keyed per controllerId:relay
  so several alert lamps on one controller run independently. Every barrier
  resolver skips radarAlert rows (no auto-open; barrier-not-a-door intact).

Inputs — one first-class config.inputs[] list (the twin of relays[]):
- Each row is { input, role, relay?, kind?, activeLow?, cooldownSec? } with a
  "+ Add input" button. role ∈ button | presence | alertTrigger; button/presence
  name the relay they serve. An exit radar is just another presence row.
- Keystone `inputsOf(row)`: returns config.inputs[] or SYNTHESIZES it from the
  legacy relays[].button/presenceInput/... fields, so relayForButton /
  relayForPresence resolve identically from either shape — zero-downtime, no
  migration. entry-flow.ts is unchanged (resolves through the same functions).
- Fixed a latent bug this exposed: the alert lamp's camera lock was hardcoded to
  the ENTRY camera. Added relays[].lockLane ("entry"|"exit", default entry); the
  lamp now locks on its own lane's camera, so an exit radar's lamp tracks the exit
  camera. button-light tracks both #entryBusy/#exitBusy.
- Driver: extracted activeLowFrom(config) — merges inputs[] activeLow, legacy
  relays[].presenceActiveLow, and the inputActiveLow escape hatch.

UI: the relay dropdown gained a "Radar alert" option (reveals trigger/lock/blink
inputs); InputEditor is rewritten to a generic list (role select folds loop/radar);
i18n sq+en kept at type-parity.

Tests: new device-resolve.test.ts (inputs[] resolution + legacy fallback identical
+ exit-radar resolves to the exit relay); button-light gains a two-independent-
alert-relays case and an exit-lamp lockLane case; access-dingtian gains
activeLowFrom cases. Full workspace build/lint/test green (i18n parity included).

Wiki + memory updated (button-light-indicator, entry-double-press, dingtian-relay).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-28 11:23:15 +02:00

921 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Albanian (sq) — the DEFAULT and fallback language. Customer/operator-facing copy.
// Keys are dot-namespaced by area (common, nav, booth, …). When adding a string,
// add it here first (the fallback), then mirror the key in en.ts.
// See wiki/concepts/i18n.md.
export const sq = {
common: {
loading: "Duke u ngarkuar…",
logout: "Dil",
cancel: "Anulo",
close: "Mbyll",
save: "Ruaj",
none: "—",
themeDark: "errët",
themeLight: "çelët",
theme: "Tema",
today: "Sot",
yesterday: "Dje",
// Month names (index 0 = January) — kept in the catalog because the appliance's
// browser ICU may lack Albanian locale data (Intl falls back to English).
months: [
"Janar",
"Shkurt",
"Mars",
"Prill",
"Maj",
"Qershor",
"Korrik",
"Gusht",
"Shtator",
"Tetor",
"Nëntor",
"Dhjetor",
],
},
auth: {
title: "Sistemi i Parkimit",
username: "Përdoruesi",
password: "Fjalëkalimi",
signIn: "Hyr",
signingIn: "Duke hyrë…",
},
update: {
available: "Përditësim i disponueshëm",
prompt: "Versioni {{version}} është i disponueshëm. Ta instaloj tani dhe ta rinis?",
},
nav: {
booth: "Kabina",
shift: "Turni",
setup: "Konfigurimi",
devices: "Pajisjet",
tariff: "Tarifa",
tariffLab: "Lab Tarife",
subscriptions: "Abonimet",
plans: "Planet",
site: "Park",
users: "Përdoruesit",
roles: "Rolet",
shifts: "Turnet",
reports: "Raportet",
recycleBin: "Koshi",
logs: "Loget",
profile: "Profili",
},
profile: {
title: "Profili im",
accountSection: "Llogaria",
fullName: "Emri i plotë",
fullNamePh: "Emri dhe mbiemri",
email: "Email",
emailPh: "ti@shembull.com",
username: "Përdoruesi",
role: "Roli",
saveProfile: "Ruaj profilin",
profileSaved: "Profili u ruajt.",
passwordSection: "Ndrysho fjalëkalimin",
currentPassword: "Fjalëkalimi aktual",
newPassword: "Fjalëkalimi i ri",
confirmPassword: "Konfirmo fjalëkalimin",
changePassword: "Ndrysho fjalëkalimin",
passwordChanged: "Fjalëkalimi u ndryshua.",
passwordsDontMatch: "Fjalëkalimet nuk përputhen.",
passwordTooShort: "Fjalëkalimi duhet të jetë të paktën {{min}} karaktere.",
},
status: {
live: "LIVE",
connecting: "DUKE U LIDHUR",
offline: "JASHTË LINJE",
},
devices: {
footerTitle: "Pajisjet",
none: "Asnjë pajisje e konfiguruar.",
catAccess: "Rele",
catReader: "Lexuesi",
catCamera: "Kamera",
catPrinter: "Printer",
catVision: "ANPR",
// Role/direction suffixes for the chip label (e.g. "Lexuesi hyrje").
role: {
entry: "hyrje",
exit: "dalje",
both: "hyrje/dalje",
mixed: "hyrje/dalje",
lane: "në korsi",
booth: "në kabinë",
},
state: {
ready: "gati",
degraded: "i dëmtuar",
offline: "jashtë linje",
},
allOk: "të gjitha gati",
issuesCount: "{{count}} me probleme",
issuesTitle: "Problemet e pajisjeve",
clickForIssues: "Kliko për detajet",
checkedAt: "kontrolluar {{time}}",
},
booth: {
processTicket: "Proceso biletën",
scanPlaceholder: "Skano ose shkruaj numrin e biletës…",
laneEntry: "Hyrje",
laneExit: "Dalje",
open: "Hap",
occupancy: "Prania",
occUnavailable: "zënia e padisponueshme",
inside: "brenda",
of: "nga",
uncapped: "pa kufi",
free: "Vende të lira",
lotFull: "● parkimi plot",
liveFeed: "Aktiviteti live",
events: "Evente",
noEventsYet: "Asnjë event ende — hyrjet dhe daljet do të shfaqen këtu.",
activeSessions: "Sesionet aktive",
insideCount: "brenda",
noActiveSessions: "Asnjë sesion aktiv.",
noMatch: "Asnjë rezultat për filtrin.",
badgeOverstay: "tej afatit",
badgeOverstayTitle:
"Sesion i paguar. Klienti nuk doli brënda afatit kohor. Ka filluar një periudhë e re tarifimi.",
plateTitle: "Targa e njohur nga ANPR",
// filtra
filterSearchSessions: "Kërko biletë / abonent / targë…",
filterSearchFeed: "Kërko event / identitet / targë…",
filterAll: "Të gjitha",
fStatusUnpaid: "Papaguar",
fStatusPaid: "Paguar",
fStatusExiting: "Duke dalë",
fStatusOverstay: "Tej afatit",
fKindTransient: "Kalimtarë",
fKindSubscription: "Abonentë",
fDirEntry: "Hyrje",
fDirExit: "Dalje",
fSrcBooth: "Kabinë",
fSrcReader: "Lexues",
fEvtEntry: "Hyrje",
fEvtExit: "Dalje",
fEvtPay: "Pagesë",
fEvtVoid: "Anulim",
fEvtAnomaly: "Anomali",
openPayExit: "Hap pagesën / daljen",
openBarrier: "Hap barrierën",
openBarrierTitle: "Hap barrierën manualisht",
barrierOpened: "barriera u hap",
openManually: "hape me dorë",
// session row badges
badgeExiting: "duke dalë",
badgePaid: "paguar",
badgeUnpaid: "papaguar",
badgeSubscription: "abonim",
// event types (live feed labels)
evtEntry: "HYRJE",
evtExit: "DALJE",
evtPay: "PAGESË",
evtVoid: "ANULIM",
evtOpenCmd: "HAP→",
evtOpenObserved: "HAP✓",
evtShiftOpen: "TURN+",
evtShiftZ: "TURN Z",
evtCashMovement: "ARKË",
evtCashIn: "ARKËTIM",
evtCashOut: "PAGESË",
evtAnomaly: "ANOMALI",
evtRefused: "REFUZUAR",
// rreshti i detajeve të eventit live + etiketat e klasifikimit (nga payload)
evtNoReason: "pa arsye të regjistruar",
badgeEntryRefused: "hyrje e refuzuar",
badgeExitRefused: "dalje e refuzuar",
badgeLotFull: "parkimi plot",
badgeBarrierFailed: "barriera nuk u hap",
badgeManualOpen: "hapje manuale",
badgeSubRefused: "abonimi u refuzua",
badgeSubSale: "shitje abonimi",
badgeWindowCharge: "jashtë orarit — detyrim",
badgeNoTicket: "bileta nuk u printua",
feedSourceBooth: "kabinë",
feedSourceReader: "lexues",
// dritarja e detajeve të eventit
eventDetail: "Detajet e eventit",
edType: "Lloji",
edTime: "Ora",
edIndex: "Indeksi në regjistër",
edDirection: "Drejtimi",
edSource: "Burimi",
edIdentity: "Identiteti",
edReason: "Arsyeja",
edSnapshots: "Fotot",
edPayload: "Të dhënat e nënshkruara",
edChain: "Zinxhiri",
edSignature: "Nënshkrimi",
edKeyId: "Çelësi",
edPrevHash: "Hash-i i mëparshëm",
edNoPayload: "Ky event nuk ka të dhëna shtesë.",
edCopy: "Kopjo",
edCopied: "U kopjua",
edDetails: "Detajet",
edAuditData: "Të dhënat e auditimit (nënshkrimi & zinxhiri)",
edAmount: "Shuma",
edTender: "Mënyra e pagesës",
edSession: "Sesioni",
edPlate: "Targa",
edCategory: "Kategoria",
edOperator: "Operatori",
edTariffVersion: "Versioni i tarifës",
edRawPayload: "Të dhënat e papërpunuara të nënshkruara",
edOccurrence: "ID e hyrjes",
subscriber: "Abonent",
edVia: "Mënyra e hyrjes",
viaQr: "Kod QR",
viaCard: "Kartë/çip RFID",
viaPlate: "Targë",
},
// Mesazhet e përkthyera për REASON_CODES e nënshkruara (shih @parking/shared).
// Çelësat përputhen 1:1 me kodet; {{param}} mbushet nga reasonParams i eventit.
reason: {
"entry.refused.full": "Hyrja u refuzua — parkimi plot ({{count}}/{{capacity}})",
"entry.held.noTicket": "Hyrja u mbajt — bileta nuk u printua: {{detail}}",
"exit.refused.closed": "Dalja u refuzua — sesioni është mbyllur tashmë",
"exit.refused.noSession": "Dalja u refuzua — biletë e panjohur",
"exit.refused.unpaid": "Dalja u refuzua — e papaguar (bëj pagesën në fillim)",
"exit.refused.graceExpired": "Dalja u refuzua — afati i daljes skadoi (kërkohet pagesë shtesë)",
"exit.open.noBarrier": "Dalja u regjistrua, por nuk ka barrierë daljeje të konfiguruar — hape me dorë",
"exit.open.unavailable": "Dalja u regjistrua, por barriera është e padisponueshme — hape me dorë",
"exit.open.failed": "Dalja u regjistrua, por barriera nuk u hap — hape me dorë",
"exit.freeGrace": "Periudhë pa pagesë në hyrje (pa tarifë)",
"exit.manualOpen": "Hapje manuale e barrierës (ndërhyrje njerëzore)",
"sub.refused.notFound": "Abonimi u refuzua — nuk u gjet",
"sub.refused.outOfWindow": "Abonimi u refuzua — {{status}}/jashtë afatit",
"sub.refused.noSession": "Dalje me abonim pa sesion të hapur (tashmë jashtë / nuk ka hyrë kurrë)",
"sub.refused.atCapacity": "Abonimi u refuzua — në kapacitet ({{inUse}}/{{max}} makina brenda)",
"sub.refused.unpaidWindow": "Dalja u refuzua — detyrim jashtë orarit i papaguar ({{amount}} {{currency}}); paguaje në kabinë",
"void.ticketCancelled": "Bileta u anulua — {{reason}}",
},
tariff: {
title: "Tarifa",
noRateCard: "Asnjë tarifë e publikuar — arka nuk mund të faturojë derisa të publikoni një.",
activeSince: "Aktive që nga {{date}} · {{count}} version(e) në histori. Publikimi krijon një version të ri; sesionet e kaluara ruajnë çmimin origjinal.",
currency: "Monedha",
freeEntryGrace: "Periudha pa pagesë në hyrje (min)",
billingIncrement: "Intervali i faturimit (min)",
dailyCap: "Kufiri ditor (bosh = pa kufi)",
dailyCapPh: "p.sh. 12.00",
lostTicketFee: "Tarifa për biletë të humbur",
exitGrace: "Periudha e kthimit në dalje (min)",
rateBlocks: "Blloqet tarifore",
rateBlocksHint: "Çdo brez zgjat një numër orësh dhe faturohet me çmimin e tij; brezat konsumohen me radhë (orët e para, pastaj orët në vijim). Brezi i fundit është \"më pas\" (i hapur) — çmimi i tij zbatohet pas mbarimit të shkallës. Çmimi është për interval faturimi.",
bandDuration: "Kohëzgjatja e brezit",
hoursUnit: "orë",
egHours: "p.sh. 2",
pricePerIncrement: "Çmimi / interval",
thereafter: "më pas (i hapur)",
remove: "Hiq",
addBlock: "+ Shto bllok",
publishNewVersion: "Publiko version të ri",
publishing: "Duke publikuar…",
publishedOk: "U publikua versioni i ri i tarifës — tani është tarifa aktive.",
defaultCard: "Tarifa bazë (gjithmonë aktive)",
defaultCardHint: "Çmimi bazë i zbatuar kur asnjë nivel kohor/sezonal nuk vlen. Kjo e vetme është mjaftueshëm për shumicën e parkimeve.",
modeLadder: "Shkallë orësh",
modeFlat: "Çmim fiks",
modeStepped: "Sipas kohëzgjatjes (deri-në)",
steppedHint:
"Vendos çmimin TOTAL për një qëndrim deri në një kohë të caktuar (p.sh. deri 3 orë = 500). Fiton rreshti i parë me kufi ≥ kohëzgjatjes (kufiri përfshihet). Totali i rreshtit të fundit përsëritet si çmim ditor për qëndrime më të gjata.",
stepUpTo: "Deri në",
stepTotal: "Çmimi total",
addStep: "+ Shto rresht",
steppedTiersConflict:
"⚠ Nivelet kohore/sezonale NUK zbatohen kur tarifa bazë është 'Sipas kohëzgjatjes (deri-në)' — motori i shpërfill plotësisht. Hiqi nivelet, ose ndrysho tarifën bazë në 'Shkallë orësh' a 'Çmim fiks'. Publikimi bllokohet derisa kjo të rregullohet.",
tiersAdvanced: "Të avancuara: nivele kohore & sezonale",
tiersHint: "Opsionale. Shto nivele tarifore që vlejnë vetëm në orë/ditë/data ose kategori të caktuara (p.sh. orë e lirë, tarifë nate, fundjavë, autobus). Pa nivele, publikohet vetëm tarifa bazë.",
tierName: "Emri",
tierPriority: "Përparësia",
tierCategory: "Kategoria",
tierCategoryPh: "p.sh. autobus",
tierDays: "Ditët",
tierHours: "Orët",
tierDates: "Datat",
tierOvernight: "(kalon mesnatën)",
addTier: "+ Shto nivel",
dow1: "Hën",
dow2: "Mar",
dow3: "Mër",
dow4: "Enj",
dow5: "Pre",
dow6: "Sht",
dow0: "Die",
},
setup: {
title: "Konfigurimi",
intro:
"Shto fillimisht kontrollerat e barrierave — cakto cili rele është hyrje/dalje dhe në cilin terminal është lidhur butoni i hyrjes. Pastaj shto lexues, kamera dhe printera dhe drejto secilin te barriera që shërben.",
// Category titles + the singular noun used in buttons/modal titles.
catControllers: "Kontrollerat (barrierat + butoni i hyrjes)",
catReaders: "Lexuesit (QR / RFID)",
catCameras: "Kamerat (foto + targë)",
catPrinters: "Printerat (bileta / vouchera)",
nounController: "kontroller",
nounReader: "lexues",
nounCamera: "kamerë",
nounPrinter: "printer",
add: "+ Shto {{noun}}",
addAnother: "+ Shto {{noun}}",
addTitle: "Shto {{noun}}",
editTitle: "Ndrysho {{noun}}",
needControllerFirst: "Shto fillimisht një kontroller — {{noun}} drejtohet te një prej releve të tij.",
failedToLoad: "Ngarkimi i konfigurimit dështoi: {{error}}",
loadingCatalog: "Duke ngarkuar katalogun e pajisjeve…",
// Direction labels (relay direction + inherited binding).
dirEntry: "Hyrje",
dirExit: "Dalje",
dirBoth: "Hyrje + dalje",
inherits: "trashëgon {{direction}}",
// Warnings panel.
warnTitle: "⚠ U ruajt, por nevojitet veprim:",
dismiss: "Mbyll",
// Assignment row.
disabled: "(çaktivizuar)",
edit: "Ndrysho",
remove: "Hiq",
removing: "Duke hequr…",
confirmRemove: "Të hiqet kjo pajisje {{driver}}?",
noRelaysSet: "asnjë rele e caktuar",
unbound: "e palidhur",
// Device form.
noDrivers: "Asnjë drejtues i regjistruar.",
chooseDevice: "Zgjidh një pajisje…",
scan: "Skano për kontroller",
scanning: "Duke skanuar…",
noControllersFound: "Asnjë kontroller në LAN.",
use: "Përdor",
test: "Testo lidhjen",
testing: "Duke testuar…",
saveConfigure: "Ruaj & konfiguro",
saveChanges: "Ruaj ndryshimet",
saving: "Duke ruajtur…",
cancel: "Anulo",
testFailed: "Testi dështoi: {{error}}",
saveFailed: "Ruajtja dështoi: {{error}}",
deviceLabel: "Pajisja:",
preconditionsOk: "● parakushtet OK",
autoFixedOnSave: "(rregullohet vetë në ruajtje)",
backendPushIp: "IP-ja e backend-it",
chooseAddress: "Zgjidh një adresë…",
onDeviceSubnet: "— në subnetin e pajisjes",
noNicOnSubnet: "⚠ asnjë NIC në subnetin e pajisjes — pajisja mund të mos arrijë backend-in",
backendIpHint: "Adresa te e cila kjo pajisje do të dërgojë eventet e hyrjes.",
// Relay editor.
relaysTitle: "Relet në këtë kontroller",
relaysHint:
"Çdo rele hap një barrierë. Cakto drejtimin e saj; për hyrje kalimtare, cakto në cilin terminal hyrës është lidhur butoni i hyrjes.",
outputsTitle: "Daljet — relet (barrierat + drita)",
outputsHint:
"Relet janë DALJE: secila hap një barrierë (ose ndez dritën e butonit). Cakto numrin e relesë dhe drejtimin. Terminalet hyrëse (butoni, sensori) janë te seksioni Hyrjet më poshtë.",
pulseOpenMs: "Kohëzgjatja e hapjes (ms)",
pulseOpenHint: "Sa kohë mbahet rele e barrierës e hapur (jog). Vlen për të gjitha relet e barrierave.",
inputsTitle: "Hyrjet — terminalet (buton, sensor)",
inputsHint:
"Hyrjet janë TERMINALE që hosti i LEXON: butoni i hyrjes dhe sensori i pranisë/radari. Secila i përket një barriere hyrëse — e gateron ose e nis atë rele.",
inputsIdleHigh: "Hyrjet në pushim HIGH",
inputsIdleHighHint: "Kjo pllakë i mban hyrjet HIGH në pushim (statusi 1111); një shtypje e ul në LOW.",
relay: "Rele",
// Generic input rows: terminal + role + the relay it serves.
inputTerminal: "Terminali",
inputServesRelay: "I shërben reles",
roleButton: "Butoni i hyrjes",
rolePresenceLoop: "Prania (lak induktiv)",
rolePresenceRadar: "Prania (radar)",
roleAlertTrigger: "Trigger alarmi",
addInput: "+ Shto hyrje",
entryCooldown: "Pritje pas biletës (sek)",
entryCooldownHint:
"Kur nuk ka sensor pranie: shtypjet e përsëritura të butonit shtypen për kaq sekonda pas një bilete. Zgjidhje rezervë (jo garanci) — një abuzues mund ta presë afatin.",
activeLow: "Aktiv-ulët",
activeLowHint:
"Shëno nëse sensori i pranisë (p.sh. radari) qëndron HIGH në pushim dhe shkon LOW kur detekton — e kundërta e butonit. Kjo përmbys leximin e atij terminali që 'prania' të lexohet saktë.",
eventRadarAlert: "Alarm radar (dritë)",
triggerInput: "Trigger input",
triggerInputHint:
"Terminali i hyrjes (radari) që nis pulsimin e kësaj rele. Pulson kur Trigger input është aktiv por kamera s'konfirmon makinë; ndizet fiks kur kamera konfirmon; përndryshe fiket.",
lockLane: "Bllokimi nga",
lockLaneHint:
"Cila kamerë e ndez dritën fiks: hyrja apo dalja. Një radar i daljes duhet të bllokohet nga kamera e DALJES.",
lockLaneEntry: "Kamera e hyrjes",
lockLaneExit: "Kamera e daljes",
blinkOnMs: "Pulsim ndezur (ms)",
blinkOffMs: "Pulsim fikur (ms)",
addRelay: "+ Shto rele",
// Camera ANPR opt-in.
anpr: "Njohja e targave (ANPR)",
anprHint:
"Lexo targat në këtë aparat: shërbimi i vizionit lexon targën nga çdo pamje dhe e regjistron (hyrje dhe dalje). Kërkon shërbimin e vizionit aktiv.",
anprAuto: "Hapje/mbyllje automatike me targën e abonentit",
anprAutoHint:
"Lejo që KY aparat të hapë vetë barrierën kur njeh targën e një abonenti. ÇAKTIVIZOJE te aparati i daljes në një korsi të përbashkët hyrje/dalje, që një makinë që HYN të mos DALË automatikisht nga targa e pasme (njohja vazhdon — fiket vetëm hapja automatike).",
testAnpr: "Testo ANPR",
anprTesting: "Duke testuar ANPR…",
testAnprHint:
"Merr një pamje të drejtpërdrejtë nga kjo kamerë dhe përpiqet të lexojë një targë, duke raportuar rezultatin dhe kohën e nevojshme. Vendos një targë para kamerës më parë.",
anprOk: "✓ Targa u lexua {{plate}} — {{confidence}}% besueshmëri, {{ms}} ms",
anprLowConfidence: "(besueshmëri e ulët — vetëm këshillues)",
anprTookMs: "{{ms}} ms",
"anprFail.vision-disabled": "Shërbimi i vizionit është çaktivizuar — aktivizoje (VISION_ENABLED) për ta testuar ANPR.",
"anprFail.snapshot-failed": "Nuk u mor dot pamje nga kamera (jashtë linje ose e paarritshme).",
"anprFail.no-plate": "Nuk u gjet asnjë targë në pamje.",
// Printer test slip — pushes a real slip so the admin can confirm it physically prints.
testPrint: "Printo provë",
printTesting: "Duke printuar…",
testPrintHint:
"Dërgon një fletë prove te printeri tani. ‘I lidhur’ vetëm hap lidhjen — kjo konfirmon se printeri vërtet nxjerr letër.",
printOk: "✓ Fleta e provës u dërgua ({{ms}} ms). Kontrollo printerin.",
"printFail.print-failed": "Printeri nuk pranoi punën (pa letër, kapaku hapur, ose lidhja ra).",
// Reveal/hide toggle for a secret field (e.g. the device web password).
revealSecret: "Shfaq fjalëkalimin",
hideSecret: "Fshih fjalëkalimin",
// Alarm Server push settings — generated for the camera's Event → Alarm Server form.
alarmUrlTitle: "Cilësimet e Alarm Server (vendosi te kamera)",
alarmUrlHint:
"Vendosi këto te kamera: Configuration → Event → … → Alarm Settings (ose Notify Surveillance Center). Kamera do të dërgojë çdo ngjarje këtu — pa polling.",
alarmUrlCopy: "Kopjo të gjitha",
alarmUrlCopied: "U kopjua ✓",
alarmUrlSaveFirst:
"Ruaje kamerën më parë — adresa gjenerohet pasi pajisja të marrë një ID. Hape sërish për editim që ta shohësh.",
alarmUrlTestFirst:
"Kliko “Testo lidhjen” më parë — kështu përcaktohet IP-ja e këtij hosti në rrjetin e kamerës (që kamera ta thërrasë).",
alarmFieldHost: "Destination IP / Host",
alarmFieldUrl: "URL",
alarmFieldProtocol: "Protokolli",
alarmFieldPort: "Porta",
// Binding picker.
whichBarrier: "Cilën barrierë shërben kjo pajisje?",
controller: "Kontrolleri",
choose: "Zgjidh…",
relayLabel: "Rele {{relay}} ({{direction}})",
noRelaysConfigured: "Ky kontroller nuk ka rele të konfiguruar.",
},
lab: {
title: "Lab Tarife",
intro:
"Testo tarifat në kohë (dritare ditë/natë, kufi ditor, qëndrim tej afatit) në sekonda, pa pritur orë. Çmimi llogaritet me të njëjtën logjikë si kabina; nuk shkruhet asgjë në ledger.",
loadTicket: "Ngarko nga një biletë reale",
loadTicketPh: "Numri i biletës / identiteti",
load: "Ngarko",
loaded: "U ngarkua sesioni {{id}}",
tariffVersion: "Versioni i tarifës",
activeVersion: "Versioni aktiv (i tanishëm)",
entered: "Hyrja",
asOf: "Deri më (tani/dalja)",
now: "Tani",
category: "Kategoria",
categoryPh: "p.sh. bus (bosh = makinë)",
payment: "Pagesa",
paid: "u pagua",
graceMin: "afati (min)",
price: "Llogarit çmimin",
pricing: "Duke llogaritur…",
outcome: "Rezultati",
amountDue: "Shuma për pagesë",
billedPeriod: "Periudha e faturuar",
overstay: "tej afatit",
settled: "i shlyer",
periodStart: "Fillimi i periudhës",
graceExpires: "Afati skadon",
curve: "Kurba sipas kohëzgjatjes",
curveHint: "Tarifa nga hyrja për disa kohëzgjatje — shih ku rrafshohet kufiri ditor ose ndryshojnë dritaret.",
},
subs: {
title: "Abonimet",
unnamed: "(pa emër)",
unbound: "pa kufizim",
car_one: "{{count}} makinë",
car_other: "{{count}} makina",
cred: "kredencial",
plates: "{{count}} targë(a)",
noPrice: "pa çmim",
perDay: "ditë",
perWeek: "javë",
perMonth: "muaj",
plan: "Plani",
quantity: "Makina",
quantityHint: "makina të mbuluara nga ky abonim (çmimi ×N)",
count: "Sa",
planNone: "— pa pagesë / falas —",
planNoneAvail: "Asnjë plan i përcaktuar — admini duhet të krijojë një të parin.",
version: "Versioni",
versionCurrent: "aktual",
versionHint: "Zhvendos këtë abonent në një version tjetër të të njëjtit plan. Çmimi mbetet siç u faturua; ndryshon vetëm orari i lejuar nga këtu e tutje.",
versionOnlyOne: "vetëm një version i këtij plani",
everyDay: "çdo ditë",
allDay: "24/7 (pa orar)",
quoting: "duke llogaritur…",
quotePrompt: "zgjidh datën e mbarimit",
quoteLine: "{{periods}} × {{unit}} · {{amount}} {{currency}}",
tender: "Paguar me",
tenderCash: "Para në dorë",
tenderCard: "Kartë",
tenderHint: "Regjistrohet si pagesë e nënshkruar (aktiviteti, arka, raporti i turnit).",
saleRecorded: "Shitja u regjistrua: {{amount}} {{currency}} ({{tender}}).",
saleNoShift: "⚠ Asnjë turn i hapur — hapni një që arkëtimi të hyjë në një raport turni.",
edit: "Ndrysho",
revoke: "Anulo",
delete: "Fshij",
noneYet: "Asnjë abonim ende.",
add: "+ Shto abonim",
new: "Abonim i ri",
editTitle: "Ndrysho abonimin",
holderName: "Emri i mbajtësit",
contact: "Kontakti",
carLimit: "Kufiri i makinave",
limitCarsInAtOnce: "kufizo makinat brenda njëkohësisht",
validFrom: "Vlen nga",
validTo: "Vlen deri",
validToEnd: "Vlen deri (mbarimi)",
isoDateOptional: "Datë ISO (opsionale)",
boundPlates: "Targat e lidhura",
commaSeparatedOptional: "të ndara me presje (opsionale)",
credentials: "Kredencialet",
credentialsCardQr: "Kredencialet (kartë / QR)",
rfCardTag: "Kartë/Tag RF",
rfCardTagSoon: "Kartë/Tag RF (së shpejti)",
rfPlaceholder: "numri i kartës (ose lexo kartën)",
readCard: "Lexo kartën",
captureChooseReader: "Zgjidh lexuesin, pastaj afro kartën:",
captureNoReaders: "Asnjë lexues i konfiguruar.",
captureWaiting: "Afro kartën te lexuesi…",
captureTimeout: "Skadoi pa lexuar kartë. Provo sërish.",
captured: "Karta u lexua: {{value}}",
qr: "QR",
qrAutoGen: "kodi QR gjenerohet automatikisht në ruajtje",
credentialValue: "vlera e kredencialit",
addCredential: "+ kredencial",
needCredentialOrPlate: "Një abonim kërkon të paktën një kredencial OSE një targë të lidhur.",
save: "Ruaj",
cancel: "Anulo",
saved: "Abonimi u ruajt.",
savedPrinted: "Abonimi u ruajt — kodi QR u printua.",
savedPrintFailed: "Abonimi u ruajt, por printimi dështoi ({{error}}). Përdor \"Printo kodin\".",
printCode: "Printo kodin",
printedOn: "Kodi u printua te {{printer}}.",
confirmRevoke: "Të anulohet abonimi për {{name}}? Do të refuzohet te barriera.",
confirmDelete: "Të fshihet abonimi për {{name}}? (t e kaluara ruhen.)",
statusActive: "aktiv",
statusSuspended: "pezulluar",
statusRevoked: "anuluar",
},
plans: {
title: "Planet e abonimit",
intro: "Planet i përcakton admini; operatori vetëm shet prej tyre — çmimi merret automatikisht, nuk shkruhet. Ndryshimi i një plani publikon një version të ri; shitjet e mëparshme ruajnë çmimin e tyre.",
add: "+ Shto plan",
noneYet: "Asnjë plan ende. Shto një që kabina të shesë abonime.",
colName: "Emri",
colPrice: "Çmimi",
colHours: "Orari",
colUsedBy: "Përdorur nga",
colEffective: "Vlen nga",
allHours: "24/7",
usedByCount: "{{active}} aktive / {{total}}",
usedByNone: "asnjë",
usedByTitle: "Shfaq abonimet në këtë plan",
subscribers: "Abonentët",
inForce: "në fuqi",
retired: "i tërhequr",
versionCount: "{{count}} versione",
newVersion: "Version i ri",
retire: "Tërhiq",
reactivate: "Riaktivizo",
delete: "Fshij",
deleteTitle: "Fshij këtë plan përgjithmonë (vetëm kur asnjë abonim nuk e përdor)",
reactivated: "“{{name}}” është përsëri i shitshëm.",
confirmDelete: "Të fshihet plani “{{name}}” përgjithmonë? Kjo s'mund të kthehet.",
deleted: "Plani “{{name}}” u fshi.",
deleteInUse: "S'mund të fshihet — abonime ende e përdorin këtë plan. Tërhiqe në vend të kësaj.",
newTitle: "Plan i ri",
newVersionTitle: "Publiko version të ri",
newVersionHint: "Kjo publikon një version TË RI të planit — shitjet ekzistuese ruajnë çmimin origjinal.",
period: "Periudha",
pricePer: "Çmimi për periudhë",
namePlaceholder: "p.sh. Mujor standard, ose Mujor natën, ose Hotel ditor",
needName: "Emri i planit është i detyrueshëm.",
needPrice: "Shkruaj një çmim më të madh se zero.",
saved: "Plani u ruajt.",
confirmRetire: "Të tërhiqet plani “{{name}}”? Nuk do të jetë më i shitshëm (historiku ruhet).",
needWindow: "Shkruaj orare të vlefshme (HH:MM).",
needDays: "Zgjidh të paktën një ditë për intervalin.",
restrictTimes: "Kufizo oraret e parkimit (tarifë kalimtare jashtë intervalit)",
days: "Ditët",
window: "Intervali",
enterAfter: "hyrje pas",
exitBefore: "· dalje para",
grace: "Tolerancë",
graceHint: "minuta tolerancë rreth kufijve të intervalit",
timeframesHint: "Një skanim jashtë intervalit të lejuar tarifohet me tarifën normale kalimtare për minutat jashtë intervalit (hyrja e hershme shtyhet në dalje; dalja e vonuar bllokohet derisa paguhet).",
},
site: {
occupancy: "Prania:",
noCapacitySet: "(pa kapacitet të caktuar)",
free: "lirë",
full: "PLOT",
capacityLabel: "Kapaciteti (bosh = pa kufi):",
capacityPlaceholder: "p.sh. 120",
printExitDefault: "Printo biletën e daljes si parazgjedhje",
printExitHint: "(klienti skanon biletën në dalje)",
reserveSubs: "Rezervo vendet e abonentëve",
reserveSubsHint: "Mban një vend për makinat e çdo abonenti aktiv edhe kur nuk janë të parkuar — kalimtarët e shohin 'plot' më shpejt. Joaktiv: numërohen vetëm makinat brenda (mbingarkesa menaxhohet me parkim manual).",
anprEntry: "Hapje automatike për targat e abonentëve (ANPR)",
anprEntryHint: "Kur është aktiv, targa e një abonenti e lexuar nga kamera e korsisë hap barrierën përmes portës normale të abonimit. Joaktiv: abonentët duhet të përdorin kartën/QR-në. Fotot e targave regjistrohen gjithsesi.",
parkDetails: "Të dhënat e parkimit (opsionale — shfaqen në bileta/fatura)",
save: "Ruaj",
saved: "U ruajt.",
fieldParkName: "Emri i parkimit",
fieldParkNamePh: "p.sh. Parking Aeroport",
fieldOperator: "Operatori (emri ligjor)",
fieldOperatorPh: "kompania operuese",
fieldNius: "NIUS",
fieldNiusPh: "p.sh. L01234567A",
fieldAddress: "Adresa",
fieldPhone: "Telefoni",
fieldEmail: "Email",
},
users: {
title: "Përdoruesit",
add: "+ Shto përdorues",
new: "Përdorues i ri",
none: "Asnjë përdorues.",
username: "Përdoruesi",
password: "Fjalëkalimi",
passwordHint: "Të paktën 8 karaktere.",
newPassword: "fjalëkalim i ri",
role: "Roli",
resetPassword: "Rivendos fjalëkalimin",
edit: "Ndrysho",
editTitle: "Ndrysho përdoruesin",
save: "Ruaj",
delete: "Fshi",
confirmDelete: "Të fshihet përdoruesi \"{{name}}\"?",
// Optional profile metadata.
detailsSection: "Të dhënat (opsionale)",
fullName: "Emri i plotë",
phone: "Telefoni",
email: "Email",
address: "Adresa",
},
roles: {
title: "Rolet",
add: "+ Shto rol",
new: "Rol i ri",
editTitle: "Ndrysho rolin",
name: "Emri",
permissions: "Lejet",
builtin: "i integruar",
edit: "Ndrysho",
delete: "Fshi",
confirmDelete: "Të fshihet roli \"{{name}}\"?",
permCount_one: "{{count}} leje",
permCount_other: "{{count}} leje",
userCount_one: "{{count}} përdorues",
userCount_other: "{{count}} përdorues",
},
shift: {
label: "Turni:",
open: "hapur",
notStarted: "i panisur",
since: "që nga",
startShift: "Fillo turnin",
starting: "Duke filluar…",
endShift: "Mbyll turnin",
ending: "Duke mbyllur…",
endConfirm: "Të mbyllet ky turn? Regjistrohet dhe printohet një Raport Z i nënshkruar.",
drawer: "Arka:",
openingFloatInherited: "(bilanci fillestar i trashëguar nga turni i mëparshëm)",
drawerCashAdmin: "Para në arkë (admin) — shto ose hiq bilancin",
drawerVoucher: "Mandat arke — operatori e hap, admini e autorizon",
amount: "shuma",
reasonPlaceholder: "arsyeja (p.sh. bilanci fillestar)",
load: "Shto +",
remove: "Hiq −",
authName: "përdoruesi i adminit",
authPassword: "fjalëkalimi i adminit",
authRequired: "Një admin duhet ta autorizojë: shkruaj përdoruesin dhe fjalëkalimin e tij.",
mandatArketimi: "Arkëtim (hyrje) +",
mandatPagese: "Pagesë (dalje) −",
voucherHint: "Mandat Arkëtimi shton para; Mandat Pagese heq para. Arka lëviz vetëm me autorizimin e një admini.",
voucherRecorded: "Mandati {{no}} u regjistrua. Arka tani {{amount}}.",
enterPositive: "Shkruaj një shumë pozitive.",
drawerNow: "Arka tani {{amount}}.",
zReport: "MBYLLJA E TURNIT",
viewTakings: "Arkëtimet deri tani",
xReport: "ARKËTIMET DERI TANI",
asOf: "deri më",
xReportHint: "Vetëm për shikim — asgjë nuk regjistrohet. Këto shifra nënshkruhen kur mbyllet turni.",
payments: "Pagesa:",
cash: "Para:",
card: "Kartë:",
srcTickets: "Bileta:",
srcSubscriptions: "Abonime:",
srcSubSales: "shitje",
srcSubWindow: "jashtë orarit",
drawerSection: "— Arka —",
openingFloat: "Bilanci fillestar:",
cashTaken: "Para të marra:",
cashAdded: "Para të shtuara:",
cashRemoved: "Para të hequra:",
expectedDrawer: "Gjëndje Arke:",
printedToReceipt: "Printuar te printeri i kabinës.",
recordedNoPrinter: "Regjistruar (pa printer për të printuar).",
// Header shift control + the booth shift gate.
headerNoShift: "Asnjë turn",
headerOpen: "Hap turnin",
headerClose: "Mbyll turnin",
headerHeldBy: "Turn i hapur nga {{operator}}",
headerHeldByShort: "Turni: {{operator}}",
gateTitle: "Hap një turn për të proceduar biletat",
gateBody:
"Asnjë turn nuk është i hapur. Hap turnin tënd që pagesat dhe daljet të regjistrohen te ky turn.",
gateOtherTitle: "Turni i hapur i përket një operatori tjetër",
gateOtherBody:
"{{operator}} ka një turn të hapur. Vetëm një turn mund të jetë i hapur njëkohësisht — ai duhet të mbyllë turnin para se ti të hapësh tëndin.",
openNow: "Hap turnin tani",
opening: "Duke hapur…",
},
shifts: {
title: "Historiku i turneve",
myTitle: "Turnet e mia",
none: "Asnjë turn i mbyllur.",
operator: "Operatori",
started: "Filloi",
ended: "Mbaroi",
payments: "Pagesa",
cash: "Para",
card: "Kartë",
srcTickets: "Bileta",
srcSubscriptions: "Abonime",
srcSubSales: "shitje abonimesh",
srcSubWindow: "jashtë orarit",
expectedDrawer: "Gjëndje arke",
// Filter (admin only).
filterFrom: "Nga",
filterTo: "Deri",
allOperators: "Të gjithë operatorët",
apply: "Apliko",
clear: "Pastro",
timeframe: "Periudha",
preset_yesterday: "Dje",
preset_week: "Javën e fundit",
preset_month: "Muajin e fundit",
preset_all: "Të gjitha",
preset_custom: "Zgjidh periudhë",
selectAShift: "Zgjidh një turn për të parë aktivitetin e tij.",
noActivity: "Asnjë aktivitet në këtë turn.",
current: "aktual",
// Expanded drawer detail.
drawerSection: "Arka",
openingFloat: "Bilanci fillestar",
cashTaken: "Para të marra",
cashAdded: "Para të shtuara",
cashRemoved: "Para të hequra",
loadFailed: "Ngarkimi i turneve dështoi.",
},
reports: {
title: "Raportet",
groupBy: "Grupo sipas",
exportCsv: "Eksporto CSV",
loadFailed: "Raporti nuk u ngarkua dot: {{error}}",
noData: "Nuk ka të dhëna në këtë interval.",
footnote: "Numërimet dhe paratë mblidhen nga regjistri i nënshkruar. Oraret në {{tz}}.",
preset: { today: "Sot", "7d": "7 ditë", "30d": "30 ditë", "90d": "90 ditë" },
bucket: { hour: "Orë", day: "Ditë", month: "Muaj" },
kpi: {
entries: "Hyrje",
exits: "Dalje",
revenue: "Të ardhura",
payments: "Pagesa",
avgStay: "Qëndrim mes.",
subscribers: "Abonentë",
},
chart: {
flow: "Hyrjet & daljet me kalimin e kohës",
revenue: "Të ardhurat ({{currency}})",
mix: "Përbërja e të ardhurave",
peakHours: "Hyrjet sipas orës së ditës",
breakdown: "Ndarja",
},
mix: { ticket: "Tranzit", subSales: "Abonime", subWindow: "Jashtë orarit" },
row: {
cash: "Para në dorë",
card: "Kartë",
closed: "Sesione të mbyllura",
medianStay: "Qëndrim mesatar (median)",
subActive: "Abonime aktive",
subCars: "Makina të mbuluara",
},
},
recycleBin: {
title: "Koshi",
retentionNote: "Artikujt e fshirë mbahen për {{days}} ditë, pastaj hiqen përgjithmonë.",
empty: "Asgjë e fshirë. Artikujt që fshini shfaqen këtu, të rikuperueshëm derisa të skadojnë.",
col: { type: "Lloji", item: "Artikulli", deleted: "Fshirë", actions: "" },
kind: {
user: "Përdorues",
role: "Rol",
subscription: "Abonim",
plan: "Plan",
tariff: "Tarifë",
},
restore: "Rikthe",
purge: "Fshi përfundimisht",
purgeConfirmTitle: "Të fshihet përfundimisht?",
purgeConfirmBody: "Të fshihet përgjithmonë “{{label}}”? Nuk mund të rikthehet pas kësaj.",
purgeIrreversible: "Ky veprim është i pakthyeshëm.",
},
logs: {
title: "Loget e sistemit",
refresh: "Rifresko",
level: "Niveli",
source: "Burimi",
since: "Që nga",
apply: "Apliko",
clear: "Pastro",
allLevels: "Të gjitha nivelet",
allSources: "Të gjitha burimet",
frontend: "Ndërfaqja",
backend: "Serveri",
time: "Koha",
message: "Mesazhi",
status: "Statusi",
path: "Rruga",
empty: "Asnjë regjistër.",
},
pay: {
ticket: "Bileta",
entry: "Hyrja",
now: "Tani",
duration: "Kohëzgjatja",
statusLabel: "Statusi",
paid: "PAGUAR",
unpaid: "PAPAGUAR",
overstay: "TEJ AFATIT",
overstayHint: "Sesion i mëparshëm i paguar. Klienti nuk doli brënda afatit kohor. Kërkohet pagesë për periudhën e re. Totali më poshtë është tarifa e periudhës së re.",
topUp: "Periudha e re për pagesë",
total: "Totali",
noTariff: "pa tarifë",
tender: "Mënyra",
cash: "Para",
card: "Kartë",
printExitVoucher: "Printo biletë dalje",
selfExitHint: "(klienti del vetë te dalja)",
payAndOpen: "Paguaj + hap barrierën",
payAndVoucher: "Paguaj + printo biletën",
openBarrier: "Hap barrierën",
printVoucher: "Printo biletën",
takingPayment: "Duke marrë pagesën…",
printingVoucher: "Duke printuar biletën…",
opening: "Duke hapur…",
noSessionFound: "Nuk u gjet asnjë sesion për këtë biletë.",
alreadyClosed: "Ky sesion është mbyllur tashmë (doli {{time}}).",
lookingUp: "Duke kërkuar…",
paidBarrierOpened: "Paguar — barriera u hap. Automjeti mund të dalë.",
paidExitRecorded: "Paguar dhe dalja u regjistrua, por barriera nuk u hap: {{reason}}.",
subscription: "ABONIM",
plan: "Plani",
prepaid: "I PARAPAGUAR",
subAssistHint: "Abonim i parapaguar. Hap barrierën për të ndihmuar daljen (lexues me defekt / kartë e munguar). S'ka pagesë.",
assistOpenReveal: "Ndihmë për hapje (lexues me defekt / kartë e humbur)…",
payWindowCharge: "Merr pagesën",
windowPaidHint: "Pagesa jashtë orarit u krye. Hap barrierën që abonenti të dalë.",
windowCharge: "JASHTË ORARIT",
windowChargeHint: "Ky abonent parkoi jashtë orarit të lejuar të planit. Detyrohet të paguajë tarifën kalimtare për kohën jashtë orarit — merr pagesën, pastaj hap barrierën.",
subBarrierOpened: "Barriera u hap për abonentin (ndërhyrje e regjistruar).",
voucherPrinted: "Bileta e daljes u printua në {{printer}}. Klienti del vetë te dalja.",
// payment receipt (transparency slip)
receiptPrintFailed: "(fatura nuk u printua — provoni \"Riprinto faturën\".)",
receiptReprinted: "Fatura u riprintua në {{printer}}.",
reprintReceipt: "Riprinto faturën",
reprinting: "duke printuar…",
cancelTicket: "Anulo biletën",
cancelTicketTitle: "Anulo këtë biletë",
cancelTicketHint: "Anulon një biletë të printuar gabimisht. Ruhet një gjurmë e nënshkruar (operatori + arsyeja); hyrja origjinale nuk fshihet kurrë.",
cancelReason: {
misprint: "Printim i gabuar",
test: "Test",
wrongVehicle: "Automjet i gabuar",
},
cancelReasonPlaceholder: "Arsyeja e anulimit (e detyrueshme)…",
confirmCancelTicket: "Konfirmo anulimin",
cancelling: "duke anuluar…",
ticketCancelled: "Bileta u anulua.",
// snapshots
noSnapshots: "asnjë foto",
loadingSnapshots: "duke ngarkuar fotot…",
snapEntry: "hyrje",
snapExit: "dalje",
snapFailed: "kamera e paarritshme",
plate: "Targa",
},
};
// The catalog SHAPE (keys + nesting), with string-typed values — so en.ts must
// supply every key but may differ in value. (Not `typeof sq` with `as const`, which
// would pin en.ts to the Albanian literals.)
type Stringify<T> = { [K in keyof T]: T[K] extends object ? Stringify<T[K]> : string };
export type Catalog = Stringify<typeof sq>;