diff --git a/README.md b/README.md index 57ca0bf6..6341b5ff 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte ## Coverage -98 of the 202 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown: +102 of the 202 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown: | Area | Supported | |---|---| @@ -128,9 +128,9 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte | Clock & time | 2 / 3 | | Identity & access | 3 / 3 | | Graphics | 6 / 12 | -| Media & feed | 5 / 10 | +| Media & feed | 8 / 10 | | Text & fonts | 6 / 14 | -| Print quality | 4 / 14 | +| Print quality | 5 / 14 | | Configuration & persistence | 3 / 5 | | Hardware / Host comm / RFID / Network | 0 / 67 | diff --git a/docs/zpl-roadmap.md b/docs/zpl-roadmap.md index 303174ca..25176209 100644 --- a/docs/zpl-roadmap.md +++ b/docs/zpl-roadmap.md @@ -158,12 +158,12 @@ The Printer Settings Modal (Media & Feed / Print Quality / Clock & Time / Encodi | `[x]` | `^ML` | maximum label length | | | `[x]` | `^MF` | media feed | | | `[x]` | `^XB` | suppress backfeed | | -| `[ ]` | `^MA` | maintenance alert | `Coming soon` | +| `[x]` | `^MA` | maintenance alert | | | `[ ]` | `^MC` | map clear | `Coming soon` | -| `[ ]` | `^MI` | maintenance info message | `Coming soon` | +| `[x]` | `^MI` | maintenance info message | | | `[ ]` | `^MP` | mode protection | `Coming soon` | | `[x]` | `^MU` | units of measure | | -| `[ ]` | `^MW` | head cold warning | `Coming soon` | +| `[x]` | `^MW` | head cold warning | | ## Print quality @@ -173,7 +173,7 @@ The Printer Settings Modal (Media & Feed / Print Quality / Clock & Time / Encodi | `[x]` | `^JT` | head test interval | | | `[x]` | `~TA` | tear off adjust | | | `[ ]` | `^PA` | advanced text properties | `Coming soon` | -| `[ ]` | `^JH` | early warning settings | `Coming soon` | +| `[x]` | `^JH` | early warning settings | | | `[ ]` | `^PF` | slew dot rows | `Coming soon` | | `[ ]` | `^PH` / `~PH` | slew to home | `Coming soon` | | `[ ]` | `^PN` | present now | `Coming soon` | diff --git a/src/components/PrinterSettings/ClockAndTimeTab.tsx b/src/components/PrinterSettings/ClockAndTimeTab.tsx index 99e35b7c..762a76a4 100644 --- a/src/components/PrinterSettings/ClockAndTimeTab.tsx +++ b/src/components/PrinterSettings/ClockAndTimeTab.tsx @@ -148,6 +148,10 @@ export function ClockAndTimeTab() { min={CLOCK_TOLERANCE_RANGE.min} max={CLOCK_TOLERANCE_RANGE.max} value={profile.clockTolerance} + // Required while clockMode='TOL': empty blur snaps the + // draft back to the last committed value with no patch + // sent, so the input stays in sync with the store. + required onChange={(v) => patchPrinterProfile({ clockTolerance: v })} /> )} diff --git a/src/components/PrinterSettings/EncodingAndLanguageTab.tsx b/src/components/PrinterSettings/EncodingAndLanguageTab.tsx index 490ba6a3..948d65a7 100644 --- a/src/components/PrinterSettings/EncodingAndLanguageTab.tsx +++ b/src/components/PrinterSettings/EncodingAndLanguageTab.tsx @@ -1,9 +1,9 @@ import { useId } from "react"; import { useT } from "../../lib/useT"; import { useLabelStore } from "../../store/labelStore"; -import { inputCls } from "../ui/formStyles"; import { PRINTER_LOCALE_VALUES, ZPL_MODE_VALUES, isPrinterLocale, isZplMode, type PrinterLocale, type ZplMode } from "../../types/PrinterProfile"; import { + SafeStringInput, ZplCommandLabel, ZplEnumSelect, ZplField, @@ -67,15 +67,12 @@ export function EncodingAndLanguageTab() { conventions vary. */} - - patchPrinterProfile({ encodingTable: e.target.value || undefined }) - } + onChange={(v) => patchPrinterProfile({ encodingTable: v || undefined })} /> {loc.encodingTableHint} diff --git a/src/components/PrinterSettings/IdentityTab.tsx b/src/components/PrinterSettings/IdentityTab.tsx index 8845737d..ac9f093d 100644 --- a/src/components/PrinterSettings/IdentityTab.tsx +++ b/src/components/PrinterSettings/IdentityTab.tsx @@ -10,6 +10,7 @@ import { isConfigUpdateAction, } from "../../types/PrinterProfile"; import { + SafeStringInput, ZplCommandLabel, ZplEnumSelect, ZplField, @@ -40,13 +41,11 @@ export function IdentityTab() { const loc = t.printerSettings.identity; const nameId = useId(); const passwordId = useId(); - // Local mirror for the 4-digit input: progressive typing ("4", "47") - // is shown immediately but only commits to the store once the regex - // matches. Without this, partial values would always be filtered out - // and the user could never finish entering a password. Re-sync on - // store changes (import, reset) uses the adjust-state-during-render - // pattern so the React compiler doesn't flag a useEffect → setState - // cascade. + // Progressive-commit draft: store only accepts a complete 4-digit + // value, but the user has to type intermediate states. SafeStringInput + // is fully controlled and would clobber the partial draft, so the + // ^KP input stays hand-rolled. Adjust-state-during-render reseeds on + // external changes (import, reset). const storedPassword = profile.setPassword ?? ""; const [passwordDraft, setPasswordDraft] = useState(storedPassword); const [lastStoredPassword, setLastStoredPassword] = useState(storedPassword); @@ -64,15 +63,11 @@ export function IdentityTab() { MediaFeedTab). */} - - patchPrinterProfile({ printerName: e.target.value || undefined }) - } + onChange={(v) => patchPrinterProfile({ printerName: v || undefined })} /> {loc.printerNameHint} @@ -80,14 +75,10 @@ export function IdentityTab() { {(id) => ( - - patchPrinterProfile({ printerDescription: e.target.value || undefined }) - } + onChange={(v) => patchPrinterProfile({ printerDescription: v || undefined })} /> )} diff --git a/src/components/PrinterSettings/MaintenanceTab.tsx b/src/components/PrinterSettings/MaintenanceTab.tsx new file mode 100644 index 00000000..3d04d6f1 --- /dev/null +++ b/src/components/PrinterSettings/MaintenanceTab.tsx @@ -0,0 +1,284 @@ +import { useId } from "react"; +import { useT } from "../../lib/useT"; +import { useLabelStore } from "../../store/labelStore"; +import { inputCls, labelCls } from "../ui/formStyles"; +import type { PrinterProfile } from "../../types/PrinterProfile"; +import { + HEAD_CLEANING_INTERVAL_METERS, + MAINTENANCE_ALERT_DEFAULTS, + MAINTENANCE_ALERT_PRINT_VALUES, + MAINTENANCE_ALERT_TYPES, + MAINTENANCE_ALERT_UNITS, + MAINTENANCE_DISTANCE_MAX_BY_TYPE, + MAINTENANCE_MESSAGE_MAX_LEN, + isHeadCleaningIntervalMeters, + isMaintenanceAlertPrint, + isMaintenanceAlertType, + isMaintenanceAlertUnit, + type HeadCleaningIntervalMeters, + type MaintenanceAlertType, + type MaintenanceAlertUnit, +} from "../../types/PrinterProfile"; +import { + BoundedIntControl, + SafeStringInput, + ZplCommandLabel, + ZplEnumSelect, + ZplField, + ZplSubField, +} from "./zplFieldPrimitives"; + +type LocMaintenance = ReturnType["printerSettings"]["maintenance"]; +type MaintenanceAlert = NonNullable; + +const ALERT_TYPE_LABEL_KEYS = { + R: 'alertTypeR', + C: 'alertTypeC', +} as const satisfies Record; + +const ALERT_UNIT_LABEL_KEYS = { + C: 'alertUnitsC', + I: 'alertUnitsI', + M: 'alertUnitsM', +} as const satisfies Record; + +/** Maintenance tab (^MA/^MI/^MW/^JH). */ +export function MaintenanceTab() { + const t = useT(); + const profile = useLabelStore((s) => s.printerProfile); + const patchPrinterProfile = useLabelStore((s) => s.patchPrinterProfile); + const loc = t.printerSettings.maintenance; + const alert = profile.maintenanceAlert; + const message = profile.maintenanceMessage; + + // Type-cascade lives in patchPrinterProfile; this helper only owns the merge. + const updateAlert = (key: K, value: MaintenanceAlert[K]) => { + if (!alert) return; + patchPrinterProfile({ maintenanceAlert: { ...alert, [key]: value } }); + }; + + return ( +
+ + + + {(id) => ( + + )} + +
+ + {(id) => ( + + )} + + + {(id) => ( + { + if (threshold !== undefined) updateAlert("threshold", threshold); + }} + /> + )} + + + {(id) => ( + { + if (frequency !== undefined) updateAlert("frequency", frequency); + }} + /> + )} + + + {(id) => ( + + )} + +
+
+ + + +
+ + {(id) => ( + { + if (!text) { + patchPrinterProfile({ maintenanceMessage: undefined }); + return; + } + // Type sticks to alert.type when an alert exists; otherwise + // falls back to the default so superRefine stays satisfied. + const type = message?.type ?? alert?.type ?? MAINTENANCE_ALERT_DEFAULTS.type; + patchPrinterProfile({ maintenanceMessage: { type, text } }); + }} + /> + )} + + + {loc.messageTextHint} ({MAINTENANCE_MESSAGE_MAX_LEN}) + +
+
+ + v === 'E' || v === 'D'} + value={profile.earlyWarningMaintenance} + onChange={(earlyWarningMaintenance) => patchPrinterProfile({ earlyWarningMaintenance })} + defaultLabel={t.printerSettings.defaultOption} + optionLabel={(v) => v === 'E' ? loc.earlyWarningE : loc.earlyWarningD} + /> + + {loc.earlyWarningGateHint} + + + + patchPrinterProfile({ headCleaningIntervalMeters }) + } + /> + + {loc.headCleaningIntervalHint} + + + v === 'Y' || v === 'N'} + value={profile.headColdWarning} + onChange={(headColdWarning) => patchPrinterProfile({ headColdWarning })} + defaultLabel={t.printerSettings.defaultOption} + optionLabel={(v) => v === 'Y' ? loc.headColdWarningOn : loc.headColdWarningOff} + /> +
+ ); +} + +function HeadCleaningIntervalSelect({ + label, + defaultLabel, + unit, + value, + onChange, +}: { + label: string; + defaultLabel: string; + unit: string; + value: HeadCleaningIntervalMeters | undefined; + onChange: (next: HeadCleaningIntervalMeters | undefined) => void; +}) { + const id = useId(); + return ( + + +
+ + {unit} +
+
+ ); +} diff --git a/src/components/PrinterSettings/PrinterSettingsModal.tsx b/src/components/PrinterSettings/PrinterSettingsModal.tsx index 2fffb19e..b0731d2e 100644 --- a/src/components/PrinterSettings/PrinterSettingsModal.tsx +++ b/src/components/PrinterSettings/PrinterSettingsModal.tsx @@ -11,6 +11,7 @@ import { ZplLine } from "../Output/ZplLine"; import { ClockAndTimeTab } from "./ClockAndTimeTab"; import { EncodingAndLanguageTab } from "./EncodingAndLanguageTab"; import { IdentityTab } from "./IdentityTab"; +import { MaintenanceTab } from "./MaintenanceTab"; import { MediaFeedTab } from "./MediaFeedTab"; import { PrintQualityTab } from "./PrintQualityTab"; @@ -24,6 +25,7 @@ const TOP_TAB_OF = { clockTime: 'setupScript', encodingLanguage: 'setupScript', identity: 'setupScript', + maintenance: 'setupScript', } as const satisfies Record; const TOP_TAB_ORDER: readonly TopTabId[] = ['perLabel', 'setupScript']; @@ -50,6 +52,7 @@ const TAB_COMPONENTS: Partial> = { clockTime: ClockAndTimeTab, encodingLanguage: EncodingAndLanguageTab, identity: IdentityTab, + maintenance: MaintenanceTab, }; const MODAL_BOX_CLS = diff --git a/src/components/PrinterSettings/zplFieldPrimitives.tsx b/src/components/PrinterSettings/zplFieldPrimitives.tsx index 0c376d26..73bc3978 100644 --- a/src/components/PrinterSettings/zplFieldPrimitives.tsx +++ b/src/components/PrinterSettings/zplFieldPrimitives.tsx @@ -1,6 +1,7 @@ -import { useId, type ReactNode } from "react"; +import { useId, useState, type InputHTMLAttributes, type ReactNode } from "react"; import { labelCls, inputCls } from "../ui/formStyles"; import { clampBoundedInt, readBoundedInt } from "../../lib/inputParse"; +import { stripUnsafeChars } from "../../types/PrinterProfile"; /** Shared muted-monospace class for ZPL command tags so the visual * weight stays identical across the label-row, checkbox-row and @@ -92,28 +93,104 @@ export function ZplSubField({ ); } -/** Bare bounded-int `` plus the asymmetric edit/commit clamp - * pair: `readBoundedInt` caps only the upper bound during typing so - * the user can transit through non-negative values below `min` - * (e.g. type "1" on the way to "12" when min=2). `onBlur` pulls the - * committed value back into the full `[min, max]` range. Shared by - * `ZplBoundedIntInput` (full ZPL-tag row) and grid-cell wrappers - * that share one parent tag (^PR triple, ^MD pair). Keeping the - * pair colocated avoids the two callers drifting on future fixes - * (e.g. the gemini-review onBlur addition). */ +/** Text input with a local draft so a sanitiser-rejected commit can't + * cause a controlled-input revert (frozen-field UX). Default sanitiser + * strips Setup-Script-unsafe chars. IME-safe: during composition the + * raw value lives in the draft only, sanitise and commit run on + * compositionend. External value changes reseed the draft via + * adjust-state-during-render. */ +export function SafeStringInput({ + id, + value, + onChange, + sanitize = stripUnsafeChars, + className, + ...rest +}: { + id?: string; + value: string; + onChange: (next: string) => void; + sanitize?: (raw: string) => string; +} & Omit< + InputHTMLAttributes, + "id" | "value" | "onChange" | "onInput" | "defaultValue" | "type" + >) { + const [draft, setDraft] = useState(value); + const [lastExternal, setLastExternal] = useState(value); + const [composing, setComposing] = useState(false); + if (lastExternal !== value) { + setLastExternal(value); + setDraft(value); + } + return ( + { + setComposing(true); + rest.onCompositionStart?.(e); + }} + onCompositionEnd={(e) => { + setComposing(false); + const next = sanitize(e.currentTarget.value); + setDraft(next); + onChange(next); + rest.onCompositionEnd?.(e); + }} + onBlur={(e) => { + setComposing(false); + rest.onBlur?.(e); + }} + onChange={(e) => { + if (composing) { + setDraft(e.target.value); + return; + } + const next = sanitize(e.target.value); + setDraft(next); + onChange(next); + }} + /> + ); +} + +/** Bounded-int `` with a local-draft buffer for intermediate + * states (empty field, sub-min digits) the schema would reject. + * Commits on keystroke when `[min, max]`; on blur, clamps into range. + * External value changes reseed the draft via adjust-state-during-render. + * + * `required`: empty blur on an optional field emits `undefined` (clear); + * on a required field, snaps the draft back to the last committed value + * with no patch, keeping the input in sync with the store. */ export function BoundedIntControl({ id, min, max, value, onChange, + disabled, + required, }: { id?: string; min: number; max: number; value: number | undefined; onChange: (next: number | undefined) => void; + disabled?: boolean; + required?: boolean; }) { + const externalText = value === undefined ? "" : String(value); + const [draft, setDraft] = useState(externalText); + const [lastExternal, setLastExternal] = useState(externalText); + // adjust-state-during-render: external store value (undo, reset, + // sibling edit) reseeds the draft. + if (lastExternal !== externalText) { + setLastExternal(externalText); + setDraft(externalText); + } return ( onChange(readBoundedInt(e.target.value, min, max))} - onBlur={(e) => onChange(clampBoundedInt(e.target.value, min, max))} + value={draft} + disabled={disabled} + // Tells screen-reader users the field can't be cleared. Pairs + // with the snap-back behaviour on empty blur. + aria-required={required || undefined} + onChange={(e) => { + const raw = e.target.value; + setDraft(raw); + const parsed = readBoundedInt(raw, min, max); + // Only commit when the parsed value passes the schema range + // (raw==="" → undefined; sub-min digits stay in the draft). + if (parsed !== undefined && parsed >= min) onChange(parsed); + }} + onBlur={(e) => { + const raw = e.target.value; + if (raw === "") { + if (required) { + // Snap back to the last committed value so the input + // doesn't desync from the store after a rejected clear. + setDraft(lastExternal); + return; + } + onChange(undefined); + return; + } + const clamped = clampBoundedInt(raw, min, max); + if (clamped !== undefined) { + setDraft(String(clamped)); + onChange(clamped); + } + }} /> ); } diff --git a/src/lib/zplParser/handlers/setupScript.ts b/src/lib/zplParser/handlers/setupScript.ts index 2d4bfe92..c41637b4 100644 --- a/src/lib/zplParser/handlers/setupScript.ts +++ b/src/lib/zplParser/handlers/setupScript.ts @@ -1,4 +1,4 @@ -import { CLOCK_TOLERANCE_RANGE, HEAD_TEST_INTERVAL_RANGE, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, TEAR_OFF_ADJUST_RANGE, isClockFormat, isClockLanguage, isConfigUpdateAction, isPrinterLocale, isZplMode, setupScriptUnsafeCharRegex } from "../../../types/PrinterProfile"; +import { CLOCK_TOLERANCE_RANGE, HEAD_CLEANING_INTERVAL_METERS, HEAD_TEST_INTERVAL_RANGE, JH_SLOT_F, JH_SLOT_G, MAINTENANCE_ALERT_DEFAULTS, MAINTENANCE_DISTANCE_MAX_BY_TYPE, MAINTENANCE_MESSAGE_MAX_LEN, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, TEAR_OFF_ADJUST_RANGE, isClockFormat, isClockLanguage, isConfigUpdateAction, isMaintenanceAlertPrint, isMaintenanceAlertType, isMaintenanceAlertUnit, isPrinterLocale, isZplMode, setupScriptUnsafeCharRegex } from "../../../types/PrinterProfile"; import { parseIntOrUndef } from "../../inputParse"; import { parseRealtimeClock } from "../../realtimeClock"; import type { ParserState } from "../context"; @@ -6,7 +6,8 @@ import { inRange, strParam } from "../helpers"; import type { Handler } from "../types"; /** Setup-Script commands (^JZ, ^JT, ~TA, ^ST, ^KD, ^KL, ^SE, ^SZ, - * ^KN, ^SL, ^KP, ^JU): all write the shared `printerProfile` slice. */ + * ^KN, ^SL, ^KP, ^MA, ^MI, ^MW, ^JH, ^JU): all write the shared + * `printerProfile` slice. */ export function createSetupScriptHandlers(s: ParserState): Record { const printerProfile = s.result.printerProfile; return { @@ -80,5 +81,66 @@ export function createSetupScriptHandlers(s: ParserState): Record MAINTENANCE_MESSAGE_MAX_LEN) return; + if (setupScriptUnsafeCharRegex.test(text)) return; + printerProfile.maintenanceMessage = { type, text }; + }, + MW(_, rest) { + const v = rest.trim().toUpperCase(); + if (v === "Y" || v === "N") printerProfile.headColdWarning = v; + }, + // ^JH: only f/g modelled; other slots are runtime reset flags. + // g is a 0..16 index into HEAD_CLEANING_INTERVAL_METERS per spec. + JH(p) { + const f = strParam(p[JH_SLOT_F]); + if (f === "E" || f === "D") printerProfile.earlyWarningMaintenance = f; + const idx = parseIntOrUndef((p[JH_SLOT_G] ?? "").trim()); + if (idx !== undefined && idx >= 0 && idx < HEAD_CLEANING_INTERVAL_METERS.length) { + printerProfile.headCleaningIntervalMeters = HEAD_CLEANING_INTERVAL_METERS[idx]; + } + }, }; } diff --git a/src/lib/zplSetupScript.test.ts b/src/lib/zplSetupScript.test.ts index f56844fb..80c2ec91 100644 --- a/src/lib/zplSetupScript.test.ts +++ b/src/lib/zplSetupScript.test.ts @@ -5,7 +5,12 @@ import { __SETUP_SCRIPT_EMITTERS_FOR_TESTS, } from "./zplSetupScript"; import { parseZPL } from "./zplParser"; -import { printerProfileSchema, type PrinterProfile } from "../types/PrinterProfile"; +import { + MAINTENANCE_ALERT_DEFAULTS, + MAINTENANCE_DISTANCE_MAX_BY_TYPE, + printerProfileSchema, + type PrinterProfile, +} from "../types/PrinterProfile"; const base: PrinterProfile = {}; @@ -410,6 +415,11 @@ describe("generateSetupScript — output shape", () => { "clockMode", "clockTolerance", "clockLanguage", + "earlyWarningMaintenance", + "headCleaningIntervalMeters", + "maintenanceAlert", + "maintenanceMessage", + "headColdWarning", "configurationUpdate", ]); }); @@ -447,6 +457,184 @@ describe("generateSetupScript — output shape", () => { }); }); +describe("generateSetupScript — maintenance commands", () => { + it("emits ^MA with the 5 positional params", () => { + const script = generateSetupScript({ + ...base, + maintenanceAlert: { type: 'C', print: 'Y', threshold: 5, frequency: 1, units: 'M' }, + }); + expect(script).toBe("^XA\n^MAC,Y,5,1,M\n^XZ"); + }); + + it("emits ^MI as type + text", () => { + const script = generateSetupScript({ + ...base, + maintenanceMessage: { type: 'C', text: 'Clean head soon' }, + }); + expect(script).toBe("^XA\n^MIC,Clean head soon\n^XZ"); + }); + + it("emits ^MW for the head-cold-warning flag", () => { + expect(generateSetupScript({ ...base, headColdWarning: 'N' })).toBe("^XA\n^MWN\n^XZ"); + }); + + it("emits ^JH with f-slot filled and other slots empty", () => { + const script = generateSetupScript({ ...base, earlyWarningMaintenance: 'E' }); + expect(script).toBe("^XA\n^JH,,,,,E,,,,\n^XZ"); + }); + + it("emits ^JH with both f and g slots when both set", () => { + const script = generateSetupScript({ + ...base, + earlyWarningMaintenance: 'E', + headCleaningIntervalMeters: 150, + }); + expect(script).toBe("^XA\n^JH,,,,,E,1,,,\n^XZ"); + }); + + it("emits ^JH with only g-slot when only the interval is set", () => { + const script = generateSetupScript({ ...base, headCleaningIntervalMeters: 200 }); + expect(script).toBe("^XA\n^JH,,,,,,2,,,\n^XZ"); + }); + + it("parses ^MA / ^MI / ^MW / ^JH back into the profile", () => { + const zpl = [ + "^XA", + "^MAC,Y,5,1,M", + "^MIC,Clean me", + "^MWN", + "^JH,,,,,E,1,,,", + "^XZ", + ].join("\n"); + const { printerProfile } = parseZPL(zpl); + expect(printerProfile.maintenanceAlert).toEqual({ + type: 'C', print: 'Y', threshold: 5, frequency: 1, units: 'M', + }); + expect(printerProfile.maintenanceMessage).toEqual({ type: 'C', text: 'Clean me' }); + expect(printerProfile.headColdWarning).toBe('N'); + expect(printerProfile.earlyWarningMaintenance).toBe('E'); + expect(printerProfile.headCleaningIntervalMeters).toBe(150); + }); + + it("round-trips the maintenance batch", () => { + const profile: PrinterProfile = { + maintenanceAlert: { type: 'R', print: 'N', threshold: 50, frequency: 5, units: 'I' }, + maintenanceMessage: { type: 'R', text: 'Replace head' }, + headColdWarning: 'Y', + earlyWarningMaintenance: 'E', + headCleaningIntervalMeters: 300, + }; + const script = generateSetupScript(profile); + const { printerProfile } = parseZPL(script); + expect(printerProfile).toEqual(profile); + }); + + it("emits ^JH before ^MA so the alert gate is set first", () => { + const script = generateSetupScript({ + ...base, + earlyWarningMaintenance: 'E', + maintenanceAlert: { type: 'C', print: 'Y', threshold: 5, frequency: 1, units: 'I' }, + }); + expect(script.indexOf("^JH")).toBeLessThan(script.indexOf("^MA")); + }); + + it("defaults ^MA print to Y when the slot is blank", () => { + const { printerProfile } = parseZPL("^XA^MAC,,5,1,M^XZ"); + expect(printerProfile.maintenanceAlert?.print).toBe("Y"); + }); + + it("defaults ^MA units to I when the slot is blank", () => { + const { printerProfile } = parseZPL("^XA^MAC,Y,5,1^XZ"); + expect(printerProfile.maintenanceAlert?.units).toBe("I"); + }); + + it("defaults ^MA threshold and frequency to spec values when slots are blank", () => { + const { printerProfile } = parseZPL("^XA^MAR,Y,,,M^XZ"); + expect(printerProfile.maintenanceAlert).toEqual({ + type: "R", print: "Y", threshold: 5, frequency: 1, units: "M", + }); + }); + + it("drops ^MA with invalid type", () => { + const { printerProfile } = parseZPL("^XA^MAX,Y,5,1,M^XZ"); + expect(printerProfile.maintenanceAlert).toBeUndefined(); + }); + + it("drops ^MA type C with threshold over the 2000 m cap", () => { + const { printerProfile } = parseZPL("^XA^MAC,Y,2001,1,M^XZ"); + expect(printerProfile.maintenanceAlert).toBeUndefined(); + }); + + it("accepts ^MA type R with threshold up to 150000 m", () => { + const { printerProfile } = parseZPL("^XA^MAR,Y,150000,1,M^XZ"); + expect(printerProfile.maintenanceAlert?.threshold).toBe(150000); + }); + + it("accepts ^MA with frequency=0 (disabled-repeat idiom)", () => { + const { printerProfile } = parseZPL("^XA^MAC,Y,5,0,M^XZ"); + expect(printerProfile.maintenanceAlert?.frequency).toBe(0); + }); + + it("drops ^MI with embedded comma", () => { + const { printerProfile } = parseZPL("^XA^MIC,Hi, there^XZ"); + expect(printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it("drops ^MI exceeding the message length cap", () => { + const longText = "A".repeat(64); + const { printerProfile } = parseZPL(`^XA^MIC,${longText}^XZ`); + expect(printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it("parses ^JH g-slot index 1 as 150 m", () => { + const { printerProfile } = parseZPL("^XA^JH,,,,,E,1,,,^XZ"); + expect(printerProfile.headCleaningIntervalMeters).toBe(150); + }); + + it("drops ^JH g-slot index out of range", () => { + const { printerProfile } = parseZPL("^XA^JH,,,,,E,17,,,^XZ"); + expect(printerProfile.headCleaningIntervalMeters).toBeUndefined(); + }); + + it("rejects mismatched ^MA/^MI type via schema", () => { + const result = printerProfileSchema.safeParse({ + maintenanceAlert: { type: 'C', print: 'Y', threshold: 5, frequency: 1, units: 'M' }, + maintenanceMessage: { type: 'R', text: 'Mismatched' }, + }); + expect(result.success).toBe(false); + }); + + it("parser drops a mismatched ^MI so the import patch never crashes the schema", () => { + const { printerProfile } = parseZPL("^XA^MAC,Y,5,1,M^MIR,Replace head^XZ"); + expect(printerProfile.maintenanceAlert?.type).toBe("C"); + expect(printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it("parser drops a stale ^MI when a later ^MA flips the type", () => { + const { printerProfile } = parseZPL("^XA^MIC,Clean me^MAR,Y,5,1,M^XZ"); + expect(printerProfile.maintenanceAlert?.type).toBe("R"); + expect(printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it("reverse-order import: ^MI before ^MA with mismatched types, alert wins", () => { + const { printerProfile } = parseZPL("^XA^MIC,Clean me^MAR,Y,5,1,M^XZ"); + expect(printerProfile.maintenanceAlert?.type).toBe("R"); + expect(printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it("MAINTENANCE_ALERT_DEFAULTS fit the tighter (C) cap", () => { + // Parser blank-slot fallbacks are type-independent; if a future + // bump exceeds the C cap the round-trip of a partial ^MAC dump + // would silently corrupt. Pin the invariant. + const minCap = Math.min( + MAINTENANCE_DISTANCE_MAX_BY_TYPE.R, + MAINTENANCE_DISTANCE_MAX_BY_TYPE.C, + ); + expect(MAINTENANCE_ALERT_DEFAULTS.threshold).toBeLessThanOrEqual(minCap); + expect(MAINTENANCE_ALERT_DEFAULTS.frequency).toBeLessThanOrEqual(minCap); + }); +}); + describe("SETUP_SCRIPT_EMITTERS structural invariants", () => { it("every foldedInto.target points at a kind:'emit' entry, never another foldedInto", () => { // A target chain (foldedInto → foldedInto) would mean no entry diff --git a/src/lib/zplSetupScript.ts b/src/lib/zplSetupScript.ts index cc6cda2f..2f01dd73 100644 --- a/src/lib/zplSetupScript.ts +++ b/src/lib/zplSetupScript.ts @@ -1,4 +1,10 @@ -import type { PrinterProfile } from '../types/PrinterProfile'; +import { + HEAD_CLEANING_INTERVAL_METERS, + JH_SLOT_COUNT, + JH_SLOT_F, + JH_SLOT_G, + type PrinterProfile, +} from '../types/PrinterProfile'; import { formatRealtimeClockForZpl, toLocalIsoString } from './realtimeClock'; /** Generates the one-shot Setup-Script ZPL for a printer profile — @@ -120,6 +126,50 @@ const SETUP_SCRIPT_EMITTERS = { // Drop bare `^SL,` (mode unset): spec leaves the shape // implementation-defined. clockLanguage: { kind: 'foldedInto', target: 'clockMode' }, + // ^JH precedes ^MA so the master gate is on when the alert lands; + // otherwise the printer discards the alert until ^JUS + reboot. + // earlyWarningMaintenance owns the composite emit; + // headCleaningIntervalMeters folds in. + earlyWarningMaintenance: { + kind: 'emit', + channel: 'block', + emit: (p) => { + if (p.earlyWarningMaintenance === undefined && p.headCleaningIntervalMeters === undefined) { + return null; + } + const slots: string[] = Array(JH_SLOT_COUNT).fill(''); + slots[JH_SLOT_F] = p.earlyWarningMaintenance ?? ''; + if (p.headCleaningIntervalMeters !== undefined) { + const idx = HEAD_CLEANING_INTERVAL_METERS.indexOf( + p.headCleaningIntervalMeters as (typeof HEAD_CLEANING_INTERVAL_METERS)[number], + ); + if (idx >= 0) slots[JH_SLOT_G] = String(idx); + } + return `^JH${slots.join(',')}`; + }, + }, + headCleaningIntervalMeters: { kind: 'foldedInto', target: 'earlyWarningMaintenance' }, + maintenanceAlert: { + kind: 'emit', + channel: 'block', + emit: (p) => { + const m = p.maintenanceAlert; + if (!m) return null; + return `^MA${m.type},${m.print},${m.threshold},${m.frequency},${m.units}`; + }, + }, + maintenanceMessage: { + kind: 'emit', + channel: 'block', + emit: (p) => p.maintenanceMessage + ? `^MI${p.maintenanceMessage.type},${p.maintenanceMessage.text}` + : null, + }, + headColdWarning: { + kind: 'emit', + channel: 'block', + emit: (p) => p.headColdWarning !== undefined ? `^MW${p.headColdWarning}` : null, + }, // configurationUpdate is the registry's last entry on purpose: a // commit (`^JUS`) needs to follow every other persistent write so // the EEPROM lands the rest of the block before being asked to diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 03d02d41..8f94c9a3 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -128,6 +128,7 @@ const ar = { clockTime: 'التاريخ والوقت', encodingLanguage: 'الترميز واللغة', identity: 'الهوية', + maintenance: 'Maintenance', }, railGroupPerLabel: 'لكل ملصق', railGroupSetupScript: 'نص الإعداد', @@ -244,6 +245,33 @@ const ar = { configurationUpdateOptionN: 'N — إعادة تحميل إعدادات الشبكة المصنعية (مدمر)', configurationUpdateOptionF: 'F — إعادة الضبط للمصنع (مدمر)', }, + maintenance: { + maintenanceAlertHeading: 'تنبيه الصيانة', + alertType: 'نوع التنبيه', + alertTypeC: 'تنظيف الرأس', + alertTypeR: 'استبدال الرأس', + alertPrint: 'طباعة ملصق التحذير', + alertPrintY: 'طباعة', + alertPrintN: 'تخطي', + alertThreshold: 'العتبة', + alertFrequency: 'تكرار التنبيه', + alertUnits: 'الوحدات', + alertUnitsM: 'أمتار', + alertUnitsI: 'بوصات', + alertUnitsC: 'سنتيمترات', + maintenanceMessageHeading: 'رسالة مخصصة', + messageText: 'نص الرسالة', + messageTextHint: 'الأحرف المطبوعة على ملصق التنبيه.', + headColdWarning: 'تحذير برودة الرأس', + headColdWarningOn: 'تشغيل', + headColdWarningOff: 'إيقاف', + earlyWarningMaintenance: 'صيانة الإنذار المبكر', + earlyWarningE: 'ممكّن', + earlyWarningD: 'معطّل', + headCleaningIntervalMeters: 'فاصل تنظيف الرأس (م)', + headCleaningIntervalHint: 'المسافة التي يطلب الطابع بعدها تنظيف الرأس.', + earlyWarningGateHint: 'البوابة الرئيسية لنظام تنبيهات الصيانة؛ مطلوبة لتفعيل ^MA.', + }, }, app: { diff --git a/src/locales/bg.ts b/src/locales/bg.ts index e1ccd5ab..37eb524c 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -128,6 +128,7 @@ const bg = { clockTime: 'Дата и час', encodingLanguage: 'Кодиране и език', identity: 'Идентичност', + maintenance: 'Maintenance', }, railGroupPerLabel: 'На етикет', railGroupSetupScript: 'Инициализиращ скрипт', @@ -244,6 +245,33 @@ const bg = { configurationUpdateOptionN: 'N — Зареди фабрични мрежови настройки (разрушително)', configurationUpdateOptionF: 'F — Фабрично нулиране (разрушително)', }, + maintenance: { + maintenanceAlertHeading: 'Сигнал за поддръжка', + alertType: 'Тип сигнал', + alertTypeC: 'Почистване на главата', + alertTypeR: 'Подмяна на главата', + alertPrint: 'Печат на предупредителен етикет', + alertPrintY: 'Печат', + alertPrintN: 'Пропусни', + alertThreshold: 'Праг', + alertFrequency: 'Честота на повторение', + alertUnits: 'Мерни единици', + alertUnitsM: 'Метри', + alertUnitsI: 'Инчове', + alertUnitsC: 'Сантиметри', + maintenanceMessageHeading: 'Персонализирано съобщение', + messageText: 'Текст на съобщението', + messageTextHint: 'Знаци, отпечатани върху сигналния етикет.', + headColdWarning: 'Предупреждение за студена глава', + headColdWarningOn: 'Вкл.', + headColdWarningOff: 'Изкл.', + earlyWarningMaintenance: 'Поддръжка с ранно предупреждение', + earlyWarningE: 'Активирано', + earlyWarningD: 'Деактивирано', + headCleaningIntervalMeters: 'Интервал на почистване на главата (м)', + headCleaningIntervalHint: 'Разстоянието, след което принтерът сигнализира, че главата трябва да бъде почистена.', + earlyWarningGateHint: 'Главен превключвател за системата за сигнали за поддръжка; необходим, за да се задейства ^MA.', + }, }, app: { diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 6bbb09bc..377d6bec 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -128,6 +128,7 @@ const cs = { clockTime: 'Datum a čas', encodingLanguage: 'Kódování a jazyk', identity: 'Identita', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Na štítek', railGroupSetupScript: 'Inicializační skript', @@ -244,6 +245,33 @@ const cs = { configurationUpdateOptionN: 'N — Načíst tovární síťová nastavení (destruktivní)', configurationUpdateOptionF: 'F — Tovární reset (destruktivní)', }, + maintenance: { + maintenanceAlertHeading: 'Upozornění na údržbu', + alertType: 'Typ upozornění', + alertTypeC: 'Čištění hlavy', + alertTypeR: 'Výměna hlavy', + alertPrint: 'Tisk varovného štítku', + alertPrintY: 'Tisknout', + alertPrintN: 'Přeskočit', + alertThreshold: 'Práh', + alertFrequency: 'Frekvence opakování', + alertUnits: 'Jednotky', + alertUnitsM: 'Metry', + alertUnitsI: 'Palce', + alertUnitsC: 'Centimetry', + maintenanceMessageHeading: 'Vlastní zpráva', + messageText: 'Text zprávy', + messageTextHint: 'Znaky vytištěné na štítku upozornění.', + headColdWarning: 'Upozornění na studenou hlavu', + headColdWarningOn: 'Zapnuto', + headColdWarningOff: 'Vypnuto', + earlyWarningMaintenance: 'Údržba s včasným varováním', + earlyWarningE: 'Povoleno', + earlyWarningD: 'Zakázáno', + headCleaningIntervalMeters: 'Interval čištění hlavy (m)', + headCleaningIntervalHint: 'Vzdálenost, po jejímž uplynutí tiskárna označí hlavu jako vyžadující čištění.', + earlyWarningGateHint: 'Hlavní spínač systému upozornění na údržbu; vyžadováno pro spuštění ^MA.', + }, }, app: { diff --git a/src/locales/da.ts b/src/locales/da.ts index 1839036c..3997363b 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -128,6 +128,7 @@ const da = { clockTime: 'Dato og tid', encodingLanguage: 'Kodning og sprog', identity: 'Identitet', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Pr. etiket', railGroupSetupScript: 'Opsætningsscript', @@ -244,6 +245,33 @@ const da = { configurationUpdateOptionN: 'N — Indlæs fabrik-netværksindstillinger (destruktiv)', configurationUpdateOptionF: 'F — Fabriksnulstilling (destruktiv)', }, + maintenance: { + maintenanceAlertHeading: 'Vedligeholdelsesalarm', + alertType: 'Alarmtype', + alertTypeC: 'Hovedrensning', + alertTypeR: 'Hovedudskiftning', + alertPrint: 'Udskriv advarselsetiket', + alertPrintY: 'Udskriv', + alertPrintN: 'Spring over', + alertThreshold: 'Tærskel', + alertFrequency: 'Gentagelsesfrekvens', + alertUnits: 'Enheder', + alertUnitsM: 'Meter', + alertUnitsI: 'Tommer', + alertUnitsC: 'Centimeter', + maintenanceMessageHeading: 'Brugerdefineret besked', + messageText: 'Beskedtekst', + messageTextHint: 'Tegn, der udskrives på advarselsetiketten.', + headColdWarning: 'Advarsel om kold hoved', + headColdWarningOn: 'Til', + headColdWarningOff: 'Fra', + earlyWarningMaintenance: 'Tidlig advarselsvedligeholdelse', + earlyWarningE: 'Aktiveret', + earlyWarningD: 'Deaktiveret', + headCleaningIntervalMeters: 'Hovedrensningsinterval (m)', + headCleaningIntervalHint: 'Den afstand, efter hvilken printeren markerer hovedet til rengøring.', + earlyWarningGateHint: 'Hovedport for vedligeholdelsesalarmsystemet; kræves for at ^MA udløses.', + }, }, app: { diff --git a/src/locales/de.ts b/src/locales/de.ts index 75718a45..faa52143 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -128,6 +128,7 @@ const de = { clockTime: 'Datum & Uhrzeit', encodingLanguage: 'Kodierung & Sprache', identity: 'Identität', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Pro Etikett', railGroupSetupScript: 'Setup-Skript', @@ -244,6 +245,33 @@ const de = { configurationUpdateOptionN: 'N — Werks-Netzwerkeinstellungen laden (destruktiv)', configurationUpdateOptionF: 'F — Werkseinstellungen (destruktiv)', }, + maintenance: { + maintenanceAlertHeading: 'Wartungswarnung', + alertType: 'Alarmtyp', + alertTypeC: 'Druckkopfreinigung', + alertTypeR: 'Druckkopfwechsel', + alertPrint: 'Warnetikett drucken', + alertPrintY: 'Drucken', + alertPrintN: 'Überspringen', + alertThreshold: 'Schwellenwert', + alertFrequency: 'Wiederholungsfrequenz', + alertUnits: 'Einheiten', + alertUnitsM: 'Meter', + alertUnitsI: 'Zoll', + alertUnitsC: 'Zentimeter', + maintenanceMessageHeading: 'Benutzerdefinierte Meldung', + messageText: 'Meldungstext', + messageTextHint: 'Zeichen, die auf das Warnetikett gedruckt werden.', + headColdWarning: 'Warnung kalter Druckkopf', + headColdWarningOn: 'Ein', + headColdWarningOff: 'Aus', + earlyWarningMaintenance: 'Frühwarnung Wartung', + earlyWarningE: 'Aktiviert', + earlyWarningD: 'Deaktiviert', + headCleaningIntervalMeters: 'Druckkopfreinigungsintervall (m)', + headCleaningIntervalHint: 'Strecke, nach der der Drucker den Druckkopf zur Reinigung markiert.', + earlyWarningGateHint: 'Hauptschalter für das Wartungswarnsystem; erforderlich, damit ^MA ausgelöst wird.', + }, }, app: { diff --git a/src/locales/el.ts b/src/locales/el.ts index 2b099162..e19ca7e4 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -128,6 +128,7 @@ const el = { clockTime: 'Ημερομηνία και ώρα', encodingLanguage: 'Κωδικοποίηση και γλώσσα', identity: 'Ταυτότητα', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Ανά ετικέτα', railGroupSetupScript: 'Σενάριο εγκατάστασης', @@ -244,6 +245,33 @@ const el = { configurationUpdateOptionN: 'N — Φόρτωση εργοστασιακών ρυθμίσεων δικτύου (καταστροφικό)', configurationUpdateOptionF: 'F — Επαναφορά εργοστασίου (καταστροφικό)', }, + maintenance: { + maintenanceAlertHeading: 'Ειδοποίηση συντήρησης', + alertType: 'Τύπος ειδοποίησης', + alertTypeC: 'Καθαρισμός κεφαλής', + alertTypeR: 'Αντικατάσταση κεφαλής', + alertPrint: 'Εκτύπωση ετικέτας προειδοποίησης', + alertPrintY: 'Εκτύπωση', + alertPrintN: 'Παράλειψη', + alertThreshold: 'Όριο', + alertFrequency: 'Συχνότητα επανάληψης', + alertUnits: 'Μονάδες', + alertUnitsM: 'Μέτρα', + alertUnitsI: 'Ίντσες', + alertUnitsC: 'Εκατοστά', + maintenanceMessageHeading: 'Προσαρμοσμένο μήνυμα', + messageText: 'Κείμενο μηνύματος', + messageTextHint: 'Χαρακτήρες που εκτυπώνονται στην ετικέτα ειδοποίησης.', + headColdWarning: 'Προειδοποίηση ψυχρής κεφαλής', + headColdWarningOn: 'Ενεργό', + headColdWarningOff: 'Ανενεργό', + earlyWarningMaintenance: 'Συντήρηση έγκαιρης προειδοποίησης', + earlyWarningE: 'Ενεργοποιημένο', + earlyWarningD: 'Απενεργοποιημένο', + headCleaningIntervalMeters: 'Διάστημα καθαρισμού κεφαλής (m)', + headCleaningIntervalHint: 'Η απόσταση μετά την οποία ο εκτυπωτής σηματοδοτεί την κεφαλή για καθαρισμό.', + earlyWarningGateHint: 'Κύρια πύλη του συστήματος ειδοποιήσεων συντήρησης· απαιτείται για να ενεργοποιηθεί το ^MA.', + }, }, app: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 9970efc6..f07d7c0c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -128,6 +128,7 @@ const en = { clockTime: 'Clock & Time', encodingLanguage: 'Encoding & Language', identity: 'Identity', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per Label', railGroupSetupScript: 'Setup Script', @@ -244,6 +245,33 @@ const en = { configurationUpdateOptionN: 'N — Reload factory network settings (destructive)', configurationUpdateOptionF: 'F — Factory reset (destructive)', }, + maintenance: { + maintenanceAlertHeading: 'Maintenance alert', + alertType: 'Alert type', + alertTypeC: 'Head cleaning', + alertTypeR: 'Head replacement', + alertPrint: 'Print warning label', + alertPrintY: 'Print', + alertPrintN: 'Skip', + alertThreshold: 'Threshold', + alertFrequency: 'Repeat frequency', + alertUnits: 'Units', + alertUnitsM: 'Meters', + alertUnitsI: 'Inches', + alertUnitsC: 'Centimeters', + maintenanceMessageHeading: 'Custom message', + messageText: 'Message text', + messageTextHint: 'Characters printed on the alert label.', + headColdWarning: 'Head cold warning', + headColdWarningOn: 'On', + headColdWarningOff: 'Off', + earlyWarningMaintenance: 'Early warning maintenance', + earlyWarningE: 'Enabled', + earlyWarningD: 'Disabled', + headCleaningIntervalMeters: 'Head cleaning interval (m)', + headCleaningIntervalHint: 'Distance after which the printer flags the head for cleaning.', + earlyWarningGateHint: 'Master gate for the maintenance alert system; required for ^MA to fire.', + }, }, app: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 905b9765..f70fd1c0 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -128,6 +128,7 @@ const es = { clockTime: 'Fecha y hora', encodingLanguage: 'Codificación e idioma', identity: 'Identidad', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Por etiqueta', railGroupSetupScript: 'Script de configuración', @@ -244,6 +245,33 @@ const es = { configurationUpdateOptionN: 'N — Restablecer red de fábrica (destructivo)', configurationUpdateOptionF: 'F — Restablecer de fábrica (destructivo)', }, + maintenance: { + maintenanceAlertHeading: 'Alerta de mantenimiento', + alertType: 'Tipo de alerta', + alertTypeC: 'Limpieza del cabezal', + alertTypeR: 'Sustitución del cabezal', + alertPrint: 'Imprimir etiqueta de advertencia', + alertPrintY: 'Imprimir', + alertPrintN: 'Omitir', + alertThreshold: 'Umbral', + alertFrequency: 'Frecuencia de repetición', + alertUnits: 'Unidades', + alertUnitsM: 'Metros', + alertUnitsI: 'Pulgadas', + alertUnitsC: 'Centímetros', + maintenanceMessageHeading: 'Mensaje personalizado', + messageText: 'Texto del mensaje', + messageTextHint: 'Caracteres impresos en la etiqueta de alerta.', + headColdWarning: 'Aviso de cabezal frío', + headColdWarningOn: 'Activado', + headColdWarningOff: 'Desactivado', + earlyWarningMaintenance: 'Mantenimiento con aviso anticipado', + earlyWarningE: 'Activado', + earlyWarningD: 'Desactivado', + headCleaningIntervalMeters: 'Intervalo de limpieza del cabezal (m)', + headCleaningIntervalHint: 'Distancia tras la cual la impresora marca el cabezal para limpieza.', + earlyWarningGateHint: 'Interruptor principal del sistema de alertas de mantenimiento; necesario para que se active ^MA.', + }, }, app: { diff --git a/src/locales/et.ts b/src/locales/et.ts index 538a79d2..24a6b059 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -128,6 +128,7 @@ const et = { clockTime: 'Kuupäev ja kellaaeg', encodingLanguage: 'Kodeering ja keel', identity: 'Identiteet', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Sildi kohta', railGroupSetupScript: 'Seadistusskript', @@ -244,6 +245,33 @@ const et = { configurationUpdateOptionN: 'N — Tehase võrgusätete taastamine (hävitav)', configurationUpdateOptionF: 'F — Tehase lähtestamine (hävitav)', }, + maintenance: { + maintenanceAlertHeading: 'Hooldushoiatus', + alertType: 'Hoiatuse tüüp', + alertTypeC: 'Pea puhastamine', + alertTypeR: 'Pea vahetus', + alertPrint: 'Prindi hoiatussilt', + alertPrintY: 'Prindi', + alertPrintN: 'Jäta vahele', + alertThreshold: 'Lävi', + alertFrequency: 'Korduse sagedus', + alertUnits: 'Ühikud', + alertUnitsM: 'Meetrid', + alertUnitsI: 'Tollid', + alertUnitsC: 'Sentimeetrid', + maintenanceMessageHeading: 'Kohandatud sõnum', + messageText: 'Sõnumi tekst', + messageTextHint: 'Hoiatussildile prinditavad märgid.', + headColdWarning: 'Külma pea hoiatus', + headColdWarningOn: 'Sees', + headColdWarningOff: 'Väljas', + earlyWarningMaintenance: 'Varajase hoiatuse hooldus', + earlyWarningE: 'Lubatud', + earlyWarningD: 'Keelatud', + headCleaningIntervalMeters: 'Pea puhastamise intervall (m)', + headCleaningIntervalHint: 'Vahemaa, mille järel printer märgib pea puhastamiseks.', + earlyWarningGateHint: 'Hooldushoiatussüsteemi peavärav; vajalik ^MA käivitamiseks.', + }, }, app: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 5418dbd9..7bfd068a 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -128,6 +128,7 @@ const fa = { clockTime: 'تاریخ و زمان', encodingLanguage: 'کدگذاری و زبان', identity: 'هویت', + maintenance: 'Maintenance', }, railGroupPerLabel: 'برای هر برچسب', railGroupSetupScript: 'اسکریپت راه‌اندازی', @@ -244,6 +245,33 @@ const fa = { configurationUpdateOptionN: 'N — بازنشانی تنظیمات شبکه کارخانه (مخرب)', configurationUpdateOptionF: 'F — بازنشانی کارخانه (مخرب)', }, + maintenance: { + maintenanceAlertHeading: 'هشدار نگهداری', + alertType: 'نوع هشدار', + alertTypeC: 'تمیز کردن هد', + alertTypeR: 'تعویض هد', + alertPrint: 'چاپ برچسب هشدار', + alertPrintY: 'چاپ', + alertPrintN: 'رد کردن', + alertThreshold: 'آستانه', + alertFrequency: 'فرکانس تکرار', + alertUnits: 'واحدها', + alertUnitsM: 'متر', + alertUnitsI: 'اینچ', + alertUnitsC: 'سانتی‌متر', + maintenanceMessageHeading: 'پیام سفارشی', + messageText: 'متن پیام', + messageTextHint: 'نویسه‌های چاپ‌شده روی برچسب هشدار.', + headColdWarning: 'هشدار سرد بودن هد', + headColdWarningOn: 'روشن', + headColdWarningOff: 'خاموش', + earlyWarningMaintenance: 'نگهداری هشدار زودهنگام', + earlyWarningE: 'فعال', + earlyWarningD: 'غیرفعال', + headCleaningIntervalMeters: 'فاصله تمیز کردن هد (متر)', + headCleaningIntervalHint: 'فاصله‌ای که پس از آن چاپگر هد را برای تمیز کردن علامت‌گذاری می‌کند.', + earlyWarningGateHint: 'دروازه اصلی سیستم هشدار نگهداری؛ برای اجرای ^MA لازم است.', + }, }, app: { diff --git a/src/locales/fi.ts b/src/locales/fi.ts index f7b31a99..f56c3f10 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -128,6 +128,7 @@ const fi = { clockTime: 'Päivämäärä ja aika', encodingLanguage: 'Koodaus ja kieli', identity: 'Tunniste', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etiketti', railGroupSetupScript: 'Asennusskripti', @@ -244,6 +245,33 @@ const fi = { configurationUpdateOptionN: 'N — Palauta tehtaan verkkoasetukset (tuhoava)', configurationUpdateOptionF: 'F — Tehdasasetusten palautus (tuhoava)', }, + maintenance: { + maintenanceAlertHeading: 'Huoltohälytys', + alertType: 'Hälytystyyppi', + alertTypeC: 'Tulostuspään puhdistus', + alertTypeR: 'Tulostuspään vaihto', + alertPrint: 'Tulosta varoitustarra', + alertPrintY: 'Tulosta', + alertPrintN: 'Ohita', + alertThreshold: 'Kynnysarvo', + alertFrequency: 'Toistotaajuus', + alertUnits: 'Yksiköt', + alertUnitsM: 'Metrit', + alertUnitsI: 'Tuumat', + alertUnitsC: 'Senttimetrit', + maintenanceMessageHeading: 'Mukautettu viesti', + messageText: 'Viestin teksti', + messageTextHint: 'Hälytystarraan tulostettavat merkit.', + headColdWarning: 'Kylmän tulostuspään varoitus', + headColdWarningOn: 'Päällä', + headColdWarningOff: 'Pois', + earlyWarningMaintenance: 'Ennakkovaroitushuolto', + earlyWarningE: 'Käytössä', + earlyWarningD: 'Pois käytöstä', + headCleaningIntervalMeters: 'Tulostuspään puhdistusväli (m)', + headCleaningIntervalHint: 'Matka, jonka jälkeen tulostin merkitsee tulostuspään puhdistettavaksi.', + earlyWarningGateHint: 'Huoltohälytysjärjestelmän pääportti; vaaditaan ^MA-laukaisuun.', + }, }, app: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index cd798b64..5ca4ccfd 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -128,6 +128,7 @@ const fr = { clockTime: 'Date et heure', encodingLanguage: 'Encodage et langue', identity: 'Identité', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Par étiquette', railGroupSetupScript: 'Script d\'installation', @@ -244,6 +245,33 @@ const fr = { configurationUpdateOptionN: 'N — Recharger les réglages réseau d\'usine (destructif)', configurationUpdateOptionF: 'F — Réinitialisation usine (destructif)', }, + maintenance: { + maintenanceAlertHeading: 'Alerte de maintenance', + alertType: "Type d'alerte", + alertTypeC: 'Nettoyage de la tête', + alertTypeR: 'Remplacement de la tête', + alertPrint: "Imprimer l'étiquette d'avertissement", + alertPrintY: 'Imprimer', + alertPrintN: 'Ignorer', + alertThreshold: 'Seuil', + alertFrequency: 'Fréquence de répétition', + alertUnits: 'Unités', + alertUnitsM: 'Mètres', + alertUnitsI: 'Pouces', + alertUnitsC: 'Centimètres', + maintenanceMessageHeading: 'Message personnalisé', + messageText: 'Texte du message', + messageTextHint: "Caractères imprimés sur l'étiquette d'alerte.", + headColdWarning: 'Avertissement tête froide', + headColdWarningOn: 'Activé', + headColdWarningOff: 'Désactivé', + earlyWarningMaintenance: 'Maintenance avec alerte anticipée', + earlyWarningE: 'Activé', + earlyWarningD: 'Désactivé', + headCleaningIntervalMeters: 'Intervalle de nettoyage de la tête (m)', + headCleaningIntervalHint: "Distance après laquelle l'imprimante signale la tête pour nettoyage.", + earlyWarningGateHint: "Interrupteur principal du système d'alerte de maintenance ; requis pour déclencher ^MA.", + }, }, app: { diff --git a/src/locales/he.ts b/src/locales/he.ts index b957fec6..d905ef12 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -128,6 +128,7 @@ const he = { clockTime: 'תאריך ושעה', encodingLanguage: 'קידוד ושפה', identity: 'זהות', + maintenance: 'Maintenance', }, railGroupPerLabel: 'לכל מדבקה', railGroupSetupScript: 'סקריפט הגדרה', @@ -244,6 +245,33 @@ const he = { configurationUpdateOptionN: 'N — טען מחדש הגדרות רשת יצרן (הרסני)', configurationUpdateOptionF: 'F — איפוס יצרן (הרסני)', }, + maintenance: { + maintenanceAlertHeading: 'התראת תחזוקה', + alertType: 'סוג התראה', + alertTypeC: 'ניקוי ראש', + alertTypeR: 'החלפת ראש', + alertPrint: 'הדפסת תווית אזהרה', + alertPrintY: 'הדפס', + alertPrintN: 'דלג', + alertThreshold: 'סף', + alertFrequency: 'תדירות חזרה', + alertUnits: 'יחידות', + alertUnitsM: 'מטרים', + alertUnitsI: "אינצ'ים", + alertUnitsC: 'סנטימטרים', + maintenanceMessageHeading: 'הודעה מותאמת אישית', + messageText: 'טקסט ההודעה', + messageTextHint: 'התווים שיודפסו על תווית ההתראה.', + headColdWarning: 'אזהרת ראש קר', + headColdWarningOn: 'פעיל', + headColdWarningOff: 'כבוי', + earlyWarningMaintenance: 'תחזוקת אזהרה מוקדמת', + earlyWarningE: 'מופעל', + earlyWarningD: 'מושבת', + headCleaningIntervalMeters: 'מרווח ניקוי ראש (מ׳)', + headCleaningIntervalHint: 'המרחק שלאחריו המדפסת מסמנת את הראש לניקוי.', + earlyWarningGateHint: 'מתג ראשי של מערכת התראות התחזוקה; נדרש כדי ש-^MA יופעל.', + }, }, app: { diff --git a/src/locales/hr.ts b/src/locales/hr.ts index ac1efccb..302e6c7d 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -128,6 +128,7 @@ const hr = { clockTime: 'Datum i vrijeme', encodingLanguage: 'Kodiranje i jezik', identity: 'Identitet', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Po naljepnici', railGroupSetupScript: 'Skripta postavljanja', @@ -244,6 +245,33 @@ const hr = { configurationUpdateOptionN: 'N — Vrati tvorničke mrežne postavke (destruktivno)', configurationUpdateOptionF: 'F — Tvornički reset (destruktivno)', }, + maintenance: { + maintenanceAlertHeading: 'Upozorenje o održavanju', + alertType: 'Vrsta upozorenja', + alertTypeC: 'Čišćenje glave', + alertTypeR: 'Zamjena glave', + alertPrint: 'Ispiši naljepnicu upozorenja', + alertPrintY: 'Ispiši', + alertPrintN: 'Preskoči', + alertThreshold: 'Prag', + alertFrequency: 'Učestalost ponavljanja', + alertUnits: 'Jedinice', + alertUnitsM: 'Metri', + alertUnitsI: 'Inči', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Prilagođena poruka', + messageText: 'Tekst poruke', + messageTextHint: 'Znakovi ispisani na naljepnici upozorenja.', + headColdWarning: 'Upozorenje o hladnoj glavi', + headColdWarningOn: 'Uključeno', + headColdWarningOff: 'Isključeno', + earlyWarningMaintenance: 'Održavanje s ranim upozorenjem', + earlyWarningE: 'Omogućeno', + earlyWarningD: 'Onemogućeno', + headCleaningIntervalMeters: 'Interval čišćenja glave (m)', + headCleaningIntervalHint: 'Udaljenost nakon koje pisač označava glavu za čišćenje.', + earlyWarningGateHint: 'Glavni prekidač sustava upozorenja o održavanju; potreban za aktivaciju ^MA.', + }, }, app: { diff --git a/src/locales/hu.ts b/src/locales/hu.ts index c59120f0..95d2dd3e 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -128,6 +128,7 @@ const hu = { clockTime: 'Dátum és idő', encodingLanguage: 'Kódolás és nyelv', identity: 'Azonosító', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Címkénként', railGroupSetupScript: 'Beállító szkript', @@ -244,6 +245,33 @@ const hu = { configurationUpdateOptionN: 'N — Gyári hálózati beállítások visszaállítása (destruktív)', configurationUpdateOptionF: 'F — Gyári visszaállítás (destruktív)', }, + maintenance: { + maintenanceAlertHeading: 'Karbantartási riasztás', + alertType: 'Riasztás típusa', + alertTypeC: 'Fejtisztítás', + alertTypeR: 'Fejcsere', + alertPrint: 'Figyelmeztető címke nyomtatása', + alertPrintY: 'Nyomtatás', + alertPrintN: 'Kihagyás', + alertThreshold: 'Küszöbérték', + alertFrequency: 'Ismétlési gyakoriság', + alertUnits: 'Mértékegységek', + alertUnitsM: 'Méter', + alertUnitsI: 'Hüvelyk', + alertUnitsC: 'Centiméter', + maintenanceMessageHeading: 'Egyéni üzenet', + messageText: 'Üzenet szövege', + messageTextHint: 'A riasztási címkére nyomtatott karakterek.', + headColdWarning: 'Hideg fej figyelmeztetés', + headColdWarningOn: 'Be', + headColdWarningOff: 'Ki', + earlyWarningMaintenance: 'Korai figyelmeztetésű karbantartás', + earlyWarningE: 'Engedélyezve', + earlyWarningD: 'Letiltva', + headCleaningIntervalMeters: 'Fejtisztítási intervallum (m)', + headCleaningIntervalHint: 'A távolság, amely után a nyomtató jelzi a fej tisztítását.', + earlyWarningGateHint: 'A karbantartási riasztási rendszer fő kapcsolója; szükséges a ^MA aktiválásához.', + }, }, app: { diff --git a/src/locales/it.ts b/src/locales/it.ts index e9600175..bfa75526 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -128,6 +128,7 @@ const it = { clockTime: 'Data e ora', encodingLanguage: 'Codifica e lingua', identity: 'Identità', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etichetta', railGroupSetupScript: 'Script di configurazione', @@ -244,6 +245,33 @@ const it = { configurationUpdateOptionN: 'N — Ricarica impostazioni di rete di fabbrica (distruttivo)', configurationUpdateOptionF: 'F — Ripristino di fabbrica (distruttivo)', }, + maintenance: { + maintenanceAlertHeading: 'Avviso di manutenzione', + alertType: 'Tipo di avviso', + alertTypeC: 'Pulizia della testina', + alertTypeR: 'Sostituzione della testina', + alertPrint: 'Stampa etichetta di avviso', + alertPrintY: 'Stampa', + alertPrintN: 'Salta', + alertThreshold: 'Soglia', + alertFrequency: 'Frequenza di ripetizione', + alertUnits: 'Unità', + alertUnitsM: 'Metri', + alertUnitsI: 'Pollici', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Messaggio personalizzato', + messageText: 'Testo del messaggio', + messageTextHint: "Caratteri stampati sull'etichetta di avviso.", + headColdWarning: 'Avviso testina fredda', + headColdWarningOn: 'Attivo', + headColdWarningOff: 'Disattivo', + earlyWarningMaintenance: 'Manutenzione con preavviso', + earlyWarningE: 'Abilitato', + earlyWarningD: 'Disabilitato', + headCleaningIntervalMeters: 'Intervallo di pulizia della testina (m)', + headCleaningIntervalHint: 'Distanza dopo la quale la stampante segnala la testina per la pulizia.', + earlyWarningGateHint: 'Interruttore principale del sistema di avvisi di manutenzione; necessario per attivare ^MA.', + }, }, app: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 1c1fee0b..b8efa75a 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -128,6 +128,7 @@ const ja = { clockTime: '日付と時刻', encodingLanguage: 'エンコードと言語', identity: '識別情報', + maintenance: 'Maintenance', }, railGroupPerLabel: 'ラベルごと', railGroupSetupScript: 'セットアップスクリプト', @@ -244,6 +245,33 @@ const ja = { configurationUpdateOptionN: 'N — 工場ネットワーク設定の再読み込み (破壊的)', configurationUpdateOptionF: 'F — 工場リセット (破壊的)', }, + maintenance: { + maintenanceAlertHeading: 'メンテナンス警告', + alertType: '警告タイプ', + alertTypeC: 'ヘッドクリーニング', + alertTypeR: 'ヘッド交換', + alertPrint: '警告ラベルを印刷', + alertPrintY: '印刷', + alertPrintN: 'スキップ', + alertThreshold: 'しきい値', + alertFrequency: '繰り返し頻度', + alertUnits: '単位', + alertUnitsM: 'メートル', + alertUnitsI: 'インチ', + alertUnitsC: 'センチメートル', + maintenanceMessageHeading: 'カスタムメッセージ', + messageText: 'メッセージテキスト', + messageTextHint: '警告ラベルに印刷される文字。', + headColdWarning: 'ヘッド低温警告', + headColdWarningOn: 'オン', + headColdWarningOff: 'オフ', + earlyWarningMaintenance: '早期警告メンテナンス', + earlyWarningE: '有効', + earlyWarningD: '無効', + headCleaningIntervalMeters: 'ヘッドクリーニング間隔 (m)', + headCleaningIntervalHint: 'プリンターがヘッドのクリーニングを通知するまでの距離。', + earlyWarningGateHint: 'メンテナンス警告システムのマスターゲート。^MA の発火に必要です。', + }, }, app: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index b11a53a7..5ec5f86c 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -128,6 +128,7 @@ const ko = { clockTime: '날짜 및 시간', encodingLanguage: '인코딩 및 언어', identity: '식별', + maintenance: 'Maintenance', }, railGroupPerLabel: '라벨별', railGroupSetupScript: '설정 스크립트', @@ -244,6 +245,33 @@ const ko = { configurationUpdateOptionN: 'N — 공장 네트워크 설정 재로드 (파괴적)', configurationUpdateOptionF: 'F — 공장 초기화 (파괴적)', }, + maintenance: { + maintenanceAlertHeading: '유지보수 경고', + alertType: '경고 유형', + alertTypeC: '헤드 청소', + alertTypeR: '헤드 교체', + alertPrint: '경고 라벨 인쇄', + alertPrintY: '인쇄', + alertPrintN: '건너뛰기', + alertThreshold: '임계값', + alertFrequency: '반복 빈도', + alertUnits: '단위', + alertUnitsM: '미터', + alertUnitsI: '인치', + alertUnitsC: '센티미터', + maintenanceMessageHeading: '사용자 지정 메시지', + messageText: '메시지 텍스트', + messageTextHint: '경고 라벨에 인쇄되는 문자입니다.', + headColdWarning: '헤드 저온 경고', + headColdWarningOn: '켜기', + headColdWarningOff: '끄기', + earlyWarningMaintenance: '조기 경고 유지보수', + earlyWarningE: '활성화됨', + earlyWarningD: '비활성화됨', + headCleaningIntervalMeters: '헤드 청소 간격 (m)', + headCleaningIntervalHint: '프린터가 헤드 청소를 알리는 거리입니다.', + earlyWarningGateHint: '유지보수 경고 시스템의 마스터 게이트입니다. ^MA 작동에 필요합니다.', + }, }, app: { diff --git a/src/locales/lt.ts b/src/locales/lt.ts index b8736a81..0b0fa0ab 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -128,6 +128,7 @@ const lt = { clockTime: 'Data ir laikas', encodingLanguage: 'Kodavimas ir kalba', identity: 'Tapatybė', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Etiketei', railGroupSetupScript: 'Sąrankos scenarijus', @@ -244,6 +245,33 @@ const lt = { configurationUpdateOptionN: 'N — Atkurti gamyklinius tinklo nustatymus (destruktyvus)', configurationUpdateOptionF: 'F — Gamyklinis atstatymas (destruktyvus)', }, + maintenance: { + maintenanceAlertHeading: 'Techninės priežiūros įspėjimas', + alertType: 'Įspėjimo tipas', + alertTypeC: 'Galvutės valymas', + alertTypeR: 'Galvutės keitimas', + alertPrint: 'Spausdinti įspėjimo etiketę', + alertPrintY: 'Spausdinti', + alertPrintN: 'Praleisti', + alertThreshold: 'Slenkstis', + alertFrequency: 'Pakartojimo dažnis', + alertUnits: 'Vienetai', + alertUnitsM: 'Metrai', + alertUnitsI: 'Coliai', + alertUnitsC: 'Centimetrai', + maintenanceMessageHeading: 'Pasirinktinis pranešimas', + messageText: 'Pranešimo tekstas', + messageTextHint: 'Simboliai, spausdinami ant įspėjimo etiketės.', + headColdWarning: 'Šaltos galvutės įspėjimas', + headColdWarningOn: 'Įjungta', + headColdWarningOff: 'Išjungta', + earlyWarningMaintenance: 'Ankstyvojo įspėjimo techninė priežiūra', + earlyWarningE: 'Įjungta', + earlyWarningD: 'Išjungta', + headCleaningIntervalMeters: 'Galvutės valymo intervalas (m)', + headCleaningIntervalHint: 'Atstumas, po kurio spausdintuvas pažymi galvutę valymui.', + earlyWarningGateHint: 'Pagrindinis techninės priežiūros įspėjimo sistemos jungiklis; būtinas ^MA suaktyvinti.', + }, }, app: { diff --git a/src/locales/lv.ts b/src/locales/lv.ts index 38a1d192..7a408795 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -128,6 +128,7 @@ const lv = { clockTime: 'Datums un laiks', encodingLanguage: 'Kodējums un valoda', identity: 'Identitāte', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Uz uzlīmi', railGroupSetupScript: 'Iestatīšanas skripts', @@ -244,6 +245,33 @@ const lv = { configurationUpdateOptionN: 'N — Atjaunot rūpnīcas tīkla iestatījumus (destruktīvi)', configurationUpdateOptionF: 'F — Rūpnīcas atiestatīšana (destruktīvi)', }, + maintenance: { + maintenanceAlertHeading: 'Apkopes brīdinājums', + alertType: 'Brīdinājuma veids', + alertTypeC: 'Galviņas tīrīšana', + alertTypeR: 'Galviņas nomaiņa', + alertPrint: 'Drukāt brīdinājuma etiķeti', + alertPrintY: 'Drukāt', + alertPrintN: 'Izlaist', + alertThreshold: 'Slieksnis', + alertFrequency: 'Atkārtošanas biežums', + alertUnits: 'Vienības', + alertUnitsM: 'Metri', + alertUnitsI: 'Collas', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Pielāgots ziņojums', + messageText: 'Ziņojuma teksts', + messageTextHint: 'Brīdinājuma etiķetē drukājamie rakstzīmes.', + headColdWarning: 'Aukstas galviņas brīdinājums', + headColdWarningOn: 'Ieslēgts', + headColdWarningOff: 'Izslēgts', + earlyWarningMaintenance: 'Agrīnās brīdināšanas apkope', + earlyWarningE: 'Iespējots', + earlyWarningD: 'Atspējots', + headCleaningIntervalMeters: 'Galviņas tīrīšanas intervāls (m)', + headCleaningIntervalHint: 'Attālums, pēc kura printeris atzīmē galviņu tīrīšanai.', + earlyWarningGateHint: 'Apkopes brīdināšanas sistēmas galvenais slēdzis; nepieciešams, lai aktivizētu ^MA.', + }, }, app: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 519239d8..893baa55 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -128,6 +128,7 @@ const nl = { clockTime: 'Datum en tijd', encodingLanguage: 'Codering en taal', identity: 'Identiteit', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etiket', railGroupSetupScript: 'Setup-script', @@ -244,6 +245,33 @@ const nl = { configurationUpdateOptionN: 'N — Fabrieksnetwerkinstellingen opnieuw laden (destructief)', configurationUpdateOptionF: 'F — Fabrieksreset (destructief)', }, + maintenance: { + maintenanceAlertHeading: 'Onderhoudswaarschuwing', + alertType: 'Waarschuwingstype', + alertTypeC: 'Kopreiniging', + alertTypeR: 'Kopvervanging', + alertPrint: 'Waarschuwingsetiket afdrukken', + alertPrintY: 'Afdrukken', + alertPrintN: 'Overslaan', + alertThreshold: 'Drempel', + alertFrequency: 'Herhaalfrequentie', + alertUnits: 'Eenheden', + alertUnitsM: 'Meters', + alertUnitsI: 'Inches', + alertUnitsC: 'Centimeters', + maintenanceMessageHeading: 'Aangepast bericht', + messageText: 'Berichttekst', + messageTextHint: 'Tekens die op het waarschuwingsetiket worden afgedrukt.', + headColdWarning: 'Waarschuwing koude kop', + headColdWarningOn: 'Aan', + headColdWarningOff: 'Uit', + earlyWarningMaintenance: 'Vroegtijdige waarschuwingsonderhoud', + earlyWarningE: 'Ingeschakeld', + earlyWarningD: 'Uitgeschakeld', + headCleaningIntervalMeters: 'Kopreinigingsinterval (m)', + headCleaningIntervalHint: 'Afstand waarna de printer de kop markeert voor reiniging.', + earlyWarningGateHint: 'Hoofdschakelaar van het onderhoudswaarschuwingssysteem; vereist om ^MA te activeren.', + }, }, app: { diff --git a/src/locales/no.ts b/src/locales/no.ts index d0c5ce68..20150218 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -128,6 +128,7 @@ const no = { clockTime: 'Dato og tid', encodingLanguage: 'Koding og språk', identity: 'Identitet', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etikett', railGroupSetupScript: 'Oppsettsskript', @@ -244,6 +245,33 @@ const no = { configurationUpdateOptionN: 'N — Last fabrikknettverksinnstillinger (destruktiv)', configurationUpdateOptionF: 'F — Fabrikknullstilling (destruktiv)', }, + maintenance: { + maintenanceAlertHeading: 'Vedlikeholdsvarsel', + alertType: 'Varseltype', + alertTypeC: 'Hoderengjøring', + alertTypeR: 'Hodeutskifting', + alertPrint: 'Skriv ut advarselsetikett', + alertPrintY: 'Skriv ut', + alertPrintN: 'Hopp over', + alertThreshold: 'Terskel', + alertFrequency: 'Gjentakelsesfrekvens', + alertUnits: 'Enheter', + alertUnitsM: 'Meter', + alertUnitsI: 'Tommer', + alertUnitsC: 'Centimeter', + maintenanceMessageHeading: 'Egendefinert melding', + messageText: 'Meldingstekst', + messageTextHint: 'Tegn som skrives ut på varselsetiketten.', + headColdWarning: 'Advarsel om kaldt hode', + headColdWarningOn: 'På', + headColdWarningOff: 'Av', + earlyWarningMaintenance: 'Tidlig varslingsvedlikehold', + earlyWarningE: 'Aktivert', + earlyWarningD: 'Deaktivert', + headCleaningIntervalMeters: 'Hoderengjøringsintervall (m)', + headCleaningIntervalHint: 'Avstanden som skriveren bruker før den merker hodet for rengjøring.', + earlyWarningGateHint: 'Hovedbryter for vedlikeholdsvarslingssystemet; kreves for at ^MA skal utløses.', + }, }, app: { diff --git a/src/locales/pl.ts b/src/locales/pl.ts index 6dca9fba..19837396 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -128,6 +128,7 @@ const pl = { clockTime: 'Data i godzina', encodingLanguage: 'Kodowanie i język', identity: 'Identyfikacja', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Na etykietę', railGroupSetupScript: 'Skrypt konfiguracji', @@ -244,6 +245,33 @@ const pl = { configurationUpdateOptionN: 'N — Wczytaj fabryczne ustawienia sieci (destrukcyjny)', configurationUpdateOptionF: 'F — Reset fabryczny (destrukcyjny)', }, + maintenance: { + maintenanceAlertHeading: 'Alert konserwacji', + alertType: 'Typ alertu', + alertTypeC: 'Czyszczenie głowicy', + alertTypeR: 'Wymiana głowicy', + alertPrint: 'Drukuj etykietę ostrzegawczą', + alertPrintY: 'Drukuj', + alertPrintN: 'Pomiń', + alertThreshold: 'Próg', + alertFrequency: 'Częstotliwość powtarzania', + alertUnits: 'Jednostki', + alertUnitsM: 'Metry', + alertUnitsI: 'Cale', + alertUnitsC: 'Centymetry', + maintenanceMessageHeading: 'Niestandardowa wiadomość', + messageText: 'Treść wiadomości', + messageTextHint: 'Znaki drukowane na etykiecie ostrzegawczej.', + headColdWarning: 'Ostrzeżenie o zimnej głowicy', + headColdWarningOn: 'Włączone', + headColdWarningOff: 'Wyłączone', + earlyWarningMaintenance: 'Konserwacja z wczesnym ostrzeganiem', + earlyWarningE: 'Włączone', + earlyWarningD: 'Wyłączone', + headCleaningIntervalMeters: 'Interwał czyszczenia głowicy (m)', + headCleaningIntervalHint: 'Odległość, po której drukarka zgłasza głowicę do czyszczenia.', + earlyWarningGateHint: 'Główny przełącznik systemu alertów konserwacji; wymagany do uruchomienia ^MA.', + }, }, app: { diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 54dca83f..0524f83d 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -128,6 +128,7 @@ const pt = { clockTime: 'Data e hora', encodingLanguage: 'Codificação e idioma', identity: 'Identidade', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Por etiqueta', railGroupSetupScript: 'Script de configuração', @@ -244,6 +245,33 @@ const pt = { configurationUpdateOptionN: 'N — Recarregar configurações de rede de fábrica (destrutivo)', configurationUpdateOptionF: 'F — Restaurar fábrica (destrutivo)', }, + maintenance: { + maintenanceAlertHeading: 'Alerta de manutenção', + alertType: 'Tipo de alerta', + alertTypeC: 'Limpeza da cabeça', + alertTypeR: 'Substituição da cabeça', + alertPrint: 'Imprimir etiqueta de aviso', + alertPrintY: 'Imprimir', + alertPrintN: 'Ignorar', + alertThreshold: 'Limite', + alertFrequency: 'Frequência de repetição', + alertUnits: 'Unidades', + alertUnitsM: 'Metros', + alertUnitsI: 'Polegadas', + alertUnitsC: 'Centímetros', + maintenanceMessageHeading: 'Mensagem personalizada', + messageText: 'Texto da mensagem', + messageTextHint: 'Caracteres impressos na etiqueta de alerta.', + headColdWarning: 'Aviso de cabeça fria', + headColdWarningOn: 'Ativado', + headColdWarningOff: 'Desativado', + earlyWarningMaintenance: 'Manutenção com aviso antecipado', + earlyWarningE: 'Ativado', + earlyWarningD: 'Desativado', + headCleaningIntervalMeters: 'Intervalo de limpeza da cabeça (m)', + headCleaningIntervalHint: 'Distância após a qual a impressora marca a cabeça para limpeza.', + earlyWarningGateHint: 'Interruptor principal do sistema de alertas de manutenção; necessário para acionar ^MA.', + }, }, app: { diff --git a/src/locales/ro.ts b/src/locales/ro.ts index ccda4d22..d4d9762a 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -128,6 +128,7 @@ const ro = { clockTime: 'Data și ora', encodingLanguage: 'Codare și limbă', identity: 'Identitate', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Pe etichetă', railGroupSetupScript: 'Script de configurare', @@ -244,6 +245,33 @@ const ro = { configurationUpdateOptionN: 'N — Reîncărcare setări de rețea din fabrică (distructiv)', configurationUpdateOptionF: 'F — Resetare din fabrică (distructiv)', }, + maintenance: { + maintenanceAlertHeading: 'Alertă de întreținere', + alertType: 'Tip de alertă', + alertTypeC: 'Curățarea capului', + alertTypeR: 'Înlocuirea capului', + alertPrint: 'Imprimă eticheta de avertizare', + alertPrintY: 'Imprimă', + alertPrintN: 'Omite', + alertThreshold: 'Prag', + alertFrequency: 'Frecvență de repetare', + alertUnits: 'Unități', + alertUnitsM: 'Metri', + alertUnitsI: 'Țoli', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Mesaj personalizat', + messageText: 'Textul mesajului', + messageTextHint: 'Caractere imprimate pe eticheta de alertă.', + headColdWarning: 'Avertizare cap rece', + headColdWarningOn: 'Activat', + headColdWarningOff: 'Dezactivat', + earlyWarningMaintenance: 'Întreținere cu avertizare timpurie', + earlyWarningE: 'Activat', + earlyWarningD: 'Dezactivat', + headCleaningIntervalMeters: 'Interval de curățare a capului (m)', + headCleaningIntervalHint: 'Distanța după care imprimanta marchează capul pentru curățare.', + earlyWarningGateHint: 'Comutator principal al sistemului de alerte de întreținere; necesar pentru declanșarea ^MA.', + }, }, app: { diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 308a843b..7a283b28 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -128,6 +128,7 @@ const sk = { clockTime: 'Dátum a čas', encodingLanguage: 'Kódovanie a jazyk', identity: 'Identita', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Na štítok', railGroupSetupScript: 'Inicializačný skript', @@ -244,6 +245,33 @@ const sk = { configurationUpdateOptionN: 'N — Načítať továrenské sieťové nastavenia (deštruktívne)', configurationUpdateOptionF: 'F — Továrenský reset (deštruktívny)', }, + maintenance: { + maintenanceAlertHeading: 'Upozornenie na údržbu', + alertType: 'Typ upozornenia', + alertTypeC: 'Čistenie hlavy', + alertTypeR: 'Výmena hlavy', + alertPrint: 'Tlačiť varovný štítok', + alertPrintY: 'Tlačiť', + alertPrintN: 'Preskočiť', + alertThreshold: 'Prah', + alertFrequency: 'Frekvencia opakovania', + alertUnits: 'Jednotky', + alertUnitsM: 'Metre', + alertUnitsI: 'Palce', + alertUnitsC: 'Centimetre', + maintenanceMessageHeading: 'Vlastná správa', + messageText: 'Text správy', + messageTextHint: 'Znaky vytlačené na štítku upozornenia.', + headColdWarning: 'Upozornenie na studenú hlavu', + headColdWarningOn: 'Zapnuté', + headColdWarningOff: 'Vypnuté', + earlyWarningMaintenance: 'Údržba s včasným varovaním', + earlyWarningE: 'Povolené', + earlyWarningD: 'Zakázané', + headCleaningIntervalMeters: 'Interval čistenia hlavy (m)', + headCleaningIntervalHint: 'Vzdialenosť, po ktorej tlačiareň označí hlavu na čistenie.', + earlyWarningGateHint: 'Hlavný spínač systému upozornení na údržbu; vyžaduje sa na spustenie ^MA.', + }, }, app: { diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 647784fb..8ba0c4ca 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -128,6 +128,7 @@ const sl = { clockTime: 'Datum in čas', encodingLanguage: 'Kodiranje in jezik', identity: 'Identiteta', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Na nalepko', railGroupSetupScript: 'Skripta za nastavitev', @@ -244,6 +245,33 @@ const sl = { configurationUpdateOptionN: 'N — Ponovno naloži tovarniške omrežne nastavitve (destruktivno)', configurationUpdateOptionF: 'F — Tovarniška ponastavitev (destruktivno)', }, + maintenance: { + maintenanceAlertHeading: 'Opozorilo o vzdrževanju', + alertType: 'Vrsta opozorila', + alertTypeC: 'Čiščenje glave', + alertTypeR: 'Zamenjava glave', + alertPrint: 'Natisni opozorilno nalepko', + alertPrintY: 'Natisni', + alertPrintN: 'Preskoči', + alertThreshold: 'Prag', + alertFrequency: 'Pogostost ponavljanja', + alertUnits: 'Enote', + alertUnitsM: 'Metri', + alertUnitsI: 'Palci', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Sporočilo po meri', + messageText: 'Besedilo sporočila', + messageTextHint: 'Znaki, natisnjeni na opozorilni nalepki.', + headColdWarning: 'Opozorilo o hladni glavi', + headColdWarningOn: 'Vklopljeno', + headColdWarningOff: 'Izklopljeno', + earlyWarningMaintenance: 'Vzdrževanje z zgodnjim opozorilom', + earlyWarningE: 'Omogočeno', + earlyWarningD: 'Onemogočeno', + headCleaningIntervalMeters: 'Interval čiščenja glave (m)', + headCleaningIntervalHint: 'Razdalja, po kateri tiskalnik označi glavo za čiščenje.', + earlyWarningGateHint: 'Glavno stikalo sistema opozoril za vzdrževanje; potrebno za sprožitev ^MA.', + }, }, app: { diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 0c58493d..44be5722 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -128,6 +128,7 @@ const sr = { clockTime: 'Датум и време', encodingLanguage: 'Кодирање и језик', identity: 'Идентитет', + maintenance: 'Maintenance', }, railGroupPerLabel: 'По налепници', railGroupSetupScript: 'Скрипта подешавања', @@ -244,6 +245,33 @@ const sr = { configurationUpdateOptionN: 'N — Učitaj fabričke mrežne postavke (destruktivno)', configurationUpdateOptionF: 'F — Fabričko resetovanje (destruktivno)', }, + maintenance: { + maintenanceAlertHeading: 'Upozorenje o održavanju', + alertType: 'Vrsta upozorenja', + alertTypeC: 'Čišćenje glave', + alertTypeR: 'Zamena glave', + alertPrint: 'Štampaj nalepnicu upozorenja', + alertPrintY: 'Štampaj', + alertPrintN: 'Preskoči', + alertThreshold: 'Prag', + alertFrequency: 'Učestalost ponavljanja', + alertUnits: 'Jedinice', + alertUnitsM: 'Metri', + alertUnitsI: 'Inči', + alertUnitsC: 'Centimetri', + maintenanceMessageHeading: 'Prilagođena poruka', + messageText: 'Tekst poruke', + messageTextHint: 'Znakovi odštampani na nalepnici upozorenja.', + headColdWarning: 'Upozorenje o hladnoj glavi', + headColdWarningOn: 'Uključeno', + headColdWarningOff: 'Isključeno', + earlyWarningMaintenance: 'Održavanje sa ranim upozorenjem', + earlyWarningE: 'Omogućeno', + earlyWarningD: 'Onemogućeno', + headCleaningIntervalMeters: 'Interval čišćenja glave (m)', + headCleaningIntervalHint: 'Rastojanje nakon kojeg štampač označava glavu za čišćenje.', + earlyWarningGateHint: 'Glavni prekidač sistema upozorenja o održavanju; neophodan za aktivaciju ^MA.', + }, }, app: { diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 0b38908e..9bac9114 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -128,6 +128,7 @@ const sv = { clockTime: 'Datum och tid', encodingLanguage: 'Kodning och språk', identity: 'Identitet', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etikett', railGroupSetupScript: 'Konfigurationsskript', @@ -244,6 +245,33 @@ const sv = { configurationUpdateOptionN: 'N — Läs in fabriksnätverksinställningar (destruktiv)', configurationUpdateOptionF: 'F — Återställ till fabrik (destruktiv)', }, + maintenance: { + maintenanceAlertHeading: 'Underhållsvarning', + alertType: 'Varningstyp', + alertTypeC: 'Huvudrengöring', + alertTypeR: 'Huvudbyte', + alertPrint: 'Skriv ut varningsetikett', + alertPrintY: 'Skriv ut', + alertPrintN: 'Hoppa över', + alertThreshold: 'Tröskel', + alertFrequency: 'Upprepningsfrekvens', + alertUnits: 'Enheter', + alertUnitsM: 'Meter', + alertUnitsI: 'Tum', + alertUnitsC: 'Centimeter', + maintenanceMessageHeading: 'Anpassat meddelande', + messageText: 'Meddelandetext', + messageTextHint: 'Tecken som skrivs ut på varningsetiketten.', + headColdWarning: 'Varning för kallt huvud', + headColdWarningOn: 'På', + headColdWarningOff: 'Av', + earlyWarningMaintenance: 'Underhåll med tidig varning', + earlyWarningE: 'Aktiverad', + earlyWarningD: 'Inaktiverad', + headCleaningIntervalMeters: 'Huvudrengöringsintervall (m)', + headCleaningIntervalHint: 'Avstånd efter vilket skrivaren markerar huvudet för rengöring.', + earlyWarningGateHint: 'Huvudbrytare för underhållsvarningssystemet; krävs för att ^MA ska utlösas.', + }, }, app: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index f3a282d4..9ebf1bcb 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -128,6 +128,7 @@ const tr = { clockTime: 'Tarih ve saat', encodingLanguage: 'Kodlama ve dil', identity: 'Kimlik', + maintenance: 'Maintenance', }, railGroupPerLabel: 'Etiket başına', railGroupSetupScript: 'Kurulum betiği', @@ -244,6 +245,33 @@ const tr = { configurationUpdateOptionN: 'N — Fabrika ağ ayarlarını yeniden yükle (yıkıcı)', configurationUpdateOptionF: 'F — Fabrika ayarlarına sıfırla (yıkıcı)', }, + maintenance: { + maintenanceAlertHeading: 'Bakım uyarısı', + alertType: 'Uyarı türü', + alertTypeC: 'Kafa temizleme', + alertTypeR: 'Kafa değişimi', + alertPrint: 'Uyarı etiketini yazdır', + alertPrintY: 'Yazdır', + alertPrintN: 'Atla', + alertThreshold: 'Eşik', + alertFrequency: 'Tekrar sıklığı', + alertUnits: 'Birimler', + alertUnitsM: 'Metre', + alertUnitsI: 'İnç', + alertUnitsC: 'Santimetre', + maintenanceMessageHeading: 'Özel mesaj', + messageText: 'Mesaj metni', + messageTextHint: 'Uyarı etiketinde yazdırılan karakterler.', + headColdWarning: 'Soğuk kafa uyarısı', + headColdWarningOn: 'Açık', + headColdWarningOff: 'Kapalı', + earlyWarningMaintenance: 'Erken uyarı bakımı', + earlyWarningE: 'Etkin', + earlyWarningD: 'Devre dışı', + headCleaningIntervalMeters: 'Kafa temizleme aralığı (m)', + headCleaningIntervalHint: 'Yazıcının kafayı temizleme için işaretlediği mesafe.', + earlyWarningGateHint: "Bakım uyarı sisteminin ana anahtarı; ^MA'nın tetiklenmesi için gereklidir.", + }, }, app: { diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index d723d0a7..fb0a8290 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -128,6 +128,7 @@ const zhHans = { clockTime: '日期与时间', encodingLanguage: '编码与语言', identity: '标识', + maintenance: 'Maintenance', }, railGroupPerLabel: '每个标签', railGroupSetupScript: '设置脚本', @@ -244,6 +245,33 @@ const zhHans = { configurationUpdateOptionN: 'N — 重新载入出厂网络设置 (破坏性)', configurationUpdateOptionF: 'F — 恢复出厂设置 (破坏性)', }, + maintenance: { + maintenanceAlertHeading: '维护警报', + alertType: '警报类型', + alertTypeC: '打印头清洁', + alertTypeR: '打印头更换', + alertPrint: '打印警告标签', + alertPrintY: '打印', + alertPrintN: '跳过', + alertThreshold: '阈值', + alertFrequency: '重复频率', + alertUnits: '单位', + alertUnitsM: '米', + alertUnitsI: '英寸', + alertUnitsC: '厘米', + maintenanceMessageHeading: '自定义消息', + messageText: '消息文本', + messageTextHint: '打印在警报标签上的字符。', + headColdWarning: '打印头低温警告', + headColdWarningOn: '开启', + headColdWarningOff: '关闭', + earlyWarningMaintenance: '早期预警维护', + earlyWarningE: '已启用', + earlyWarningD: '已禁用', + headCleaningIntervalMeters: '打印头清洁间隔 (米)', + headCleaningIntervalHint: '打印机标记打印头需要清洁前的距离。', + earlyWarningGateHint: '维护警报系统的主开关;^MA 触发的必要条件。', + }, }, app: { diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 62280471..8547a322 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -128,6 +128,7 @@ const zhHant = { clockTime: '日期與時間', encodingLanguage: '編碼與語言', identity: '識別', + maintenance: 'Maintenance', }, railGroupPerLabel: '每個標籤', railGroupSetupScript: '設定指令稿', @@ -244,6 +245,33 @@ const zhHant = { configurationUpdateOptionN: 'N — 重新載入出廠網路設定 (破壞性)', configurationUpdateOptionF: 'F — 還原出廠設定 (破壞性)', }, + maintenance: { + maintenanceAlertHeading: '維護警報', + alertType: '警報類型', + alertTypeC: '列印頭清潔', + alertTypeR: '列印頭更換', + alertPrint: '列印警告標籤', + alertPrintY: '列印', + alertPrintN: '略過', + alertThreshold: '閾值', + alertFrequency: '重複頻率', + alertUnits: '單位', + alertUnitsM: '公尺', + alertUnitsI: '英吋', + alertUnitsC: '公分', + maintenanceMessageHeading: '自訂訊息', + messageText: '訊息文字', + messageTextHint: '列印在警報標籤上的字元。', + headColdWarning: '列印頭低溫警告', + headColdWarningOn: '開啟', + headColdWarningOff: '關閉', + earlyWarningMaintenance: '早期預警維護', + earlyWarningE: '已啟用', + earlyWarningD: '已停用', + headCleaningIntervalMeters: '列印頭清潔間隔 (公尺)', + headCleaningIntervalHint: '印表機標記列印頭需要清潔前的距離。', + earlyWarningGateHint: '維護警報系統的主開關;^MA 觸發的必要條件。', + }, }, app: { diff --git a/src/store/labelStore.test.ts b/src/store/labelStore.test.ts index 0bcad256..c8b9f639 100644 --- a/src/store/labelStore.test.ts +++ b/src/store/labelStore.test.ts @@ -1165,7 +1165,7 @@ describe('migrateLegacy — v4→v5 printerProfile extraction', () => { reprintAfterError: 'Y', headTestInterval: 250, tearOffAdjust: 15, - clockFormat: 'd1', + clockFormat: '1', printerName: 'lab-zpl-01', }, pages: [{ objects: [] }], @@ -1178,7 +1178,7 @@ describe('migrateLegacy — v4→v5 printerProfile extraction', () => { reprintAfterError: 'Y', headTestInterval: 250, tearOffAdjust: 15, - clockFormat: 'd1', + clockFormat: '1', printerName: 'lab-zpl-01', }); }); @@ -1205,6 +1205,44 @@ describe('migrateLegacy — v4→v5 printerProfile extraction', () => { }; expect(migrated.printerProfile).toEqual({}); }); + + it('drops the schema-rejected sub-field on rehydrate', () => { + // Legacy persist that survived from before the cross-field + // superRefine landed. The repair walks Zod's issue paths: the + // cross-field rule flags the message side, so the alert stays + // and the (stale) message is dropped instead of silently + // rewriting on the next edit. + const persisted = { + label: { widthMm: 100, heightMm: 50, dpmm: 8 }, + pages: [{ objects: [] }], + printerProfile: { + maintenanceAlert: { type: 'H', print: 'Y', threshold: 5, frequency: 1, units: 'M' }, + maintenanceMessage: { type: 'R', text: 'Replace head' }, + }, + }; + const migrated = migrateLegacy(persisted, 6) as typeof persisted & { + printerProfile: Record; + }; + expect(migrated.printerProfile.maintenanceAlert).toEqual({ + type: 'C', print: 'Y', threshold: 5, frequency: 1, units: 'M', + }); + expect(migrated.printerProfile.maintenanceMessage).toBeUndefined(); + }); + + it('drops clockMode "TOL" when clockTolerance is missing on rehydrate', () => { + // superRefine reports path ['clockTolerance'], which is already + // absent — a single-pass delete would be a no-op and leave the + // invariant violation in place. The fixpoint loop catches this. + const persisted = { + label: { widthMm: 100, heightMm: 50, dpmm: 8 }, + pages: [{ objects: [] }], + printerProfile: { clockMode: 'TOL' }, + }; + const migrated = migrateLegacy(persisted, 6) as typeof persisted & { + printerProfile: Record; + }; + expect(migrated.printerProfile.clockMode).toBeUndefined(); + }); }); describe('variables', () => { diff --git a/src/store/labelStore.ts b/src/store/labelStore.ts index 442bb56f..f73d3559 100644 --- a/src/store/labelStore.ts +++ b/src/store/labelStore.ts @@ -2,7 +2,7 @@ import { create, useStore } from 'zustand'; import { temporal } from 'zundo'; import { persist, createJSONStorage } from 'zustand/middleware'; import type { ObjectChanges } from '../types/LabelObject'; -import { PRINTER_PROFILE_FIELDS } from '../types/PrinterProfile'; +import { PRINTER_PROFILE_FIELDS, printerProfileSchema } from '../types/PrinterProfile'; import { visitLeavesInPages } from '../lib/objectTree'; import type { LabelObject } from '../types/Group'; import { @@ -111,9 +111,60 @@ export function migrateLegacy(persistedState: unknown, version: number): unknown s = { ...s, pages: migrateGs1DatabarInPages(s.pages) }; } + // v6→v7: ^MA/^MI type codes corrected to match spec (R/C instead of + // H/R). The legacy 'H' meant "head cleaning"; the spec letter is 'C'. + // 'R' (replacement) was already correct. + if (version < 7) { + const pp = (s as Record).printerProfile; + if (pp && typeof pp === 'object') { + s = { ...s, printerProfile: migrateMaintenanceTypeCodes(pp as Record) }; + } + } + + // Re-validate the rehydrated profile so a legacy snapshot that + // violates the schema or a cross-field rule can't crash the slice's + // safeParse on the next patch. Cross-field issues report a path + // that may already be absent (clockMode='TOL' without + // clockTolerance: path is ['clockTolerance']), so a single delete + // pass is not enough. Fixpoint-loop until stable. + const profile = (s as Record).printerProfile; + if (profile && typeof profile === 'object') { + let next = { ...(profile as Record) }; + for (let i = 0; i < 8; i++) { + const validation = printerProfileSchema.safeParse(next); + if (validation.success) break; + const drop = new Set(); + for (const issue of validation.error.issues) { + const topKey = issue.path[0]; + if (typeof topKey === 'string' && topKey in next) drop.add(topKey); + } + // No present top-key to drop means the residual violation isn't + // something the loop can resolve; bail to {} rather than spin. + if (drop.size === 0) { + next = {}; + break; + } + next = Object.fromEntries(Object.entries(next).filter(([k]) => !drop.has(k))); + } + s = { ...s, printerProfile: next }; + } + return s; } +function migrateMaintenanceTypeCodes(pp: Record): Record { + const out = { ...pp }; + const remap = (obj: unknown): unknown => { + if (!obj || typeof obj !== 'object') return obj; + const o = obj as Record; + if (o.type === 'H') return { ...o, type: 'C' }; + return obj; + }; + if (out.maintenanceAlert) out.maintenanceAlert = remap(out.maintenanceAlert); + if (out.maintenanceMessage) out.maintenanceMessage = remap(out.maintenanceMessage); + return out; +} + function migrateGs1DatabarInPages(pages: unknown): unknown { // Mutates in place via the shared leaf walker; pages is the persisted // payload not the live store, so mutation is safe. @@ -205,7 +256,7 @@ export const useLabelStore = create()( }), { name: 'zpl-designer-session', - version: 6, + version: 7, migrate: (persistedState, version) => migrateLegacy(persistedState, version) as LabelState, storage: createJSONStorage(() => localStorage), partialize: persistPartialize, diff --git a/src/store/slices/printerProfileSlice.ts b/src/store/slices/printerProfileSlice.ts index 48cc22b9..0e29a4e7 100644 --- a/src/store/slices/printerProfileSlice.ts +++ b/src/store/slices/printerProfileSlice.ts @@ -1,6 +1,7 @@ import type { StateCreator } from 'zustand'; import { EMPTY_PRINTER_PROFILE, + normalizeMaintenanceTypes, printerProfileSchema, type PrinterProfile, } from '../../types/PrinterProfile'; @@ -40,10 +41,14 @@ export const createPrinterProfileSlice: StateCreator< // undefined". Validate the merged result through the schema so the // cross-field rule (clockMode === 'TOL' ↔ clockTolerance defined) // can't be violated from any caller. - const next = pruneUndefined({ + const merged = pruneUndefined({ ...state.printerProfile, ...patch, }); + // Repair cross-field invariants at the store boundary so any + // caller (UI partial patch, import, undo replay) gets them for + // free. Direction follows patch intent; see normalizeMaintenanceTypes. + const next = normalizeMaintenanceTypes(merged, patch); const parsed = printerProfileSchema.safeParse(next); if (!parsed.success) { const msg = '[printerProfile] rejected invalid patch'; diff --git a/src/store/slices/uiSlice.ts b/src/store/slices/uiSlice.ts index c40f827d..4e1ef97c 100644 --- a/src/store/slices/uiSlice.ts +++ b/src/store/slices/uiSlice.ts @@ -35,7 +35,8 @@ export type PrinterSettingsTab = | 'printQuality' | 'clockTime' | 'encodingLanguage' - | 'identity'; + | 'identity' + | 'maintenance'; export type SidebarTab = 'properties' | 'layers' | 'variables' | 'fonts'; diff --git a/src/types/PrinterProfile.ts b/src/types/PrinterProfile.ts index c9caf2f1..8931ba04 100644 --- a/src/types/PrinterProfile.ts +++ b/src/types/PrinterProfile.ts @@ -15,8 +15,18 @@ export const PRINTER_NAME_MAX_LEN = 16; const SETUP_SCRIPT_UNSAFE_CHARS = '\\^~,\\r\\n\\x00-\\x1f'; /** Anchored, positive form for `z.string().regex(...)` schema checks. */ export const setupScriptSafeStringRegex = new RegExp(`^[^${SETUP_SCRIPT_UNSAFE_CHARS}]+$`); -/** Unanchored, negative form for parser-side dropping. */ +/** Unanchored, negative form for parser-side dropping. No `g` flag so + * `.test()` callers don't carry `lastIndex` state across calls. */ export const setupScriptUnsafeCharRegex = new RegExp(`[${SETUP_SCRIPT_UNSAFE_CHARS}]`); +/** Global form for `.replace()`-based stripping. Hoisted so UI input + * handlers don't allocate a fresh regex per keystroke. */ +const setupScriptUnsafeCharGlobalRegex = new RegExp(`[${SETUP_SCRIPT_UNSAFE_CHARS}]`, 'g'); +/** Strip every occurrence of an unsafe char from `raw`. UI inputs feed + * user keystrokes through this so a single comma in a paste doesn't + * cause the schema regex to reject the whole patch and the store to + * roll back silently (a frozen-field UX). */ +export const stripUnsafeChars = (raw: string): string => + raw.replace(setupScriptUnsafeCharGlobalRegex, ''); /** ^KL printer locale — two-letter ISO 639-1 shorthands per Zebra spec. */ export const PRINTER_LOCALE_VALUES = [ @@ -72,6 +82,57 @@ export const CONFIG_UPDATE_VALUES = ['S', 'R', 'N', 'F'] as const; export type ConfigUpdateAction = (typeof CONFIG_UPDATE_VALUES)[number]; export const isConfigUpdateAction = makeEnumGuard(CONFIG_UPDATE_VALUES); +/** ^MA/^MI alert type. R=head replacement, C=head cleaning per spec. */ +export const MAINTENANCE_ALERT_TYPES = ['R', 'C'] as const; +export type MaintenanceAlertType = (typeof MAINTENANCE_ALERT_TYPES)[number]; +export const isMaintenanceAlertType = makeEnumGuard(MAINTENANCE_ALERT_TYPES); + +/** ^MA print slot: Y=emit warning label, N=skip. */ +export const MAINTENANCE_ALERT_PRINT_VALUES = ['Y', 'N'] as const; +export type MaintenanceAlertPrint = (typeof MAINTENANCE_ALERT_PRINT_VALUES)[number]; +export const isMaintenanceAlertPrint = makeEnumGuard(MAINTENANCE_ALERT_PRINT_VALUES); + +/** ^MA units slot: C=centimeters, I=inches, M=meters. */ +export const MAINTENANCE_ALERT_UNITS = ['C', 'I', 'M'] as const; +export type MaintenanceAlertUnit = (typeof MAINTENANCE_ALERT_UNITS)[number]; +export const isMaintenanceAlertUnit = makeEnumGuard(MAINTENANCE_ALERT_UNITS); + +/** ^MA threshold/frequency caps. Type-dependent per spec: + * R (replacement) = 0-150000 m (150 km printhead life), + * C (cleaning) = 0-2000 m. Schema's outer .max uses the larger; + * the per-type cap is enforced in superRefine. */ +export const MAINTENANCE_DISTANCE_MAX_BY_TYPE = { R: 150000, C: 2000 } as const; +/** ^MI message length cap from Zebra Quick Reference. */ +export const MAINTENANCE_MESSAGE_MAX_LEN = 63; + +/** ^JH g-slot head-cleaning interval: indexed table 0..16 mapping to + * meters in steps of 50 starting at 100M. Wire value is the index. */ +export const HEAD_CLEANING_INTERVAL_METERS = [ + 100, 150, 200, 250, 300, 350, 400, 450, + 500, 550, 600, 650, 700, 750, 800, 850, 900, +] as const; +export type HeadCleaningIntervalMeters = (typeof HEAD_CLEANING_INTERVAL_METERS)[number]; +export const isHeadCleaningIntervalMeters = (n: number): n is HeadCleaningIntervalMeters => + (HEAD_CLEANING_INTERVAL_METERS as readonly number[]).includes(n); + +/** ^JH positional slot count and indices: f=early-warning master, + * g=head-cleaning interval. The other 8 slots are runtime reset + * flags we don't model. */ +export const JH_SLOT_COUNT = 10; +export const JH_SLOT_F = 5; +export const JH_SLOT_G = 6; + +/** Default seed for a freshly enabled ^MA. Matches the spec example + * `^MAR,Y,5,1,I`. Spread inline so UI placeholder display and parser + * blank-slot fallback stay in sync. */ +export const MAINTENANCE_ALERT_DEFAULTS = { + type: 'R', + print: 'Y', + threshold: 5, + frequency: 1, + units: 'I', +} as const; + /** Printer-installation profile: EEPROM-persistent printer-state * fields separated from `labelConfig` so design files don't leak * per-install values when shared. */ @@ -94,6 +155,33 @@ export const printerProfileSchema = z.object({ printerName: z.string().min(1).max(PRINTER_NAME_MAX_LEN).regex(setupScriptSafeStringRegex).optional(), printerDescription: z.string().min(1).regex(setupScriptSafeStringRegex).optional(), setPassword: z.string().regex(PRINTER_PASSWORD_REGEX).optional(), + /** ^MA alert. Requires earlyWarningMaintenance==='E' to actually + * fire; the two are stored independently. The type-specific + * threshold/frequency caps are enforced in superRefine. */ + maintenanceAlert: z.object({ + type: z.enum(MAINTENANCE_ALERT_TYPES), + print: z.enum(['Y', 'N']), + threshold: z.number().int().min(0).max(MAINTENANCE_DISTANCE_MAX_BY_TYPE.R), + frequency: z.number().int().min(0).max(MAINTENANCE_DISTANCE_MAX_BY_TYPE.R), + units: z.enum(MAINTENANCE_ALERT_UNITS), + }).optional(), + /** ^MI custom message printed when the matching `^MA{type}` fires. */ + maintenanceMessage: z.object({ + type: z.enum(MAINTENANCE_ALERT_TYPES), + text: z.string().min(1).max(MAINTENANCE_MESSAGE_MAX_LEN).regex(setupScriptSafeStringRegex), + }).optional(), + /** ^MW head-cold warning LCD/alert toggle. */ + headColdWarning: z.enum(['Y', 'N']).optional(), + /** ^JH f-slot: early-warning-maintenance master gate that turns + * the ^MA alert system on. Without `E` the maintenanceAlert + * config sits dormant on the printer. */ + earlyWarningMaintenance: z.enum(['E', 'D']).optional(), + /** ^JH g-slot head-cleaning interval. Stored as meters for UX; the + * wire format is an index into HEAD_CLEANING_INTERVAL_METERS. */ + headCleaningIntervalMeters: z.number().int().refine( + isHeadCleaningIntervalMeters, + `must be one of ${HEAD_CLEANING_INTERVAL_METERS.join(', ')}`, + ).optional(), /** ^JU action. Generator emits the value as `^JU{action}` last in * the setup-script block so an `S` commit happens after every * other persistent write in the same script. */ @@ -113,10 +201,59 @@ export const printerProfileSchema = z.object({ message: 'clockMode "TOL" requires clockTolerance to be set', }); } + if ( + p.maintenanceAlert && + p.maintenanceMessage && + p.maintenanceAlert.type !== p.maintenanceMessage.type + ) { + ctx.addIssue({ + code: 'custom', + path: ['maintenanceMessage', 'type'], + message: 'maintenanceMessage.type must match maintenanceAlert.type', + }); + } + if (p.maintenanceAlert) { + const cap = MAINTENANCE_DISTANCE_MAX_BY_TYPE[p.maintenanceAlert.type]; + if (p.maintenanceAlert.threshold > cap) { + ctx.addIssue({ + code: 'custom', + path: ['maintenanceAlert', 'threshold'], + message: `threshold for type ${p.maintenanceAlert.type} must be ≤ ${cap}`, + }); + } + if (p.maintenanceAlert.frequency > cap) { + ctx.addIssue({ + code: 'custom', + path: ['maintenanceAlert', 'frequency'], + message: `frequency for type ${p.maintenanceAlert.type} must be ≤ ${cap}`, + }); + } + } }); export type PrinterProfile = z.infer; +/** Cascade direction follows whichever side the patch touches: + * alert-touched rewrites message.type, message-touched rewrites + * alert.type. Both-touched leaves the mismatch to superRefine. */ +export function normalizeMaintenanceTypes( + merged: PrinterProfile, + patch: Partial, +): PrinterProfile { + const alert = merged.maintenanceAlert; + const message = merged.maintenanceMessage; + if (!alert || !message || alert.type === message.type) return merged; + const alertTouched = patch.maintenanceAlert?.type !== undefined; + const messageTouched = patch.maintenanceMessage?.type !== undefined; + if (alertTouched && !messageTouched) { + return { ...merged, maintenanceMessage: { ...message, type: alert.type } }; + } + if (messageTouched && !alertTouched) { + return { ...merged, maintenanceAlert: { ...alert, type: message.type } }; + } + return merged; +} + /** Derived from the schema so a new optional field is automatically * picked up by the v4→v5 migration and import loader. */ export const PRINTER_PROFILE_FIELDS = Object.keys(