diff --git a/README.md b/README.md index 6e86b908..006d4e71 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte ## Coverage -114 of the 225 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown: +117 of the 225 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown: | Area | Supported | |---|---| @@ -170,7 +170,7 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte | Text & fonts | 7 / 14 | | Print quality | 10 / 18 | | Configuration & persistence | 3 / 5 | -| Hardware / Host comm / RFID / Network | 1 / 87 | +| Hardware / Host comm / RFID / Network | 4 / 87 | --- diff --git a/docs/zpl-roadmap.md b/docs/zpl-roadmap.md index f131ac00..033fcdab 100644 --- a/docs/zpl-roadmap.md +++ b/docs/zpl-roadmap.md @@ -308,12 +308,12 @@ password-coupled ^RL. Read-back stays native. ^RM/^RR folded into ^RS, | `[ ]` | `^RM` | enable motion (pre-Link-OS; folded into ^RS; passthrough) | `Out of scope` | | `[ ]` | `^RN` | detect multiple tags (pre-Link-OS; absent from current guide; passthrough) | `Out of scope` | | `[ ]` | `^RR` | RFID retries (pre-Link-OS; folded into ^RS; passthrough) | `Out of scope` | -| `[ ]` | `^RB` | define EPC data structure (partitions consumed by ^RF EPC writes) | `Coming soon` | -| `[ ]` | `^RS` | RFID setup (tag type, programming position, retries, error handling) | `Coming soon` | +| `[x]` | `^RB` | define EPC data structure (partitions consumed by ^RF EPC writes; spec-modelled, hardware-unverified) | | +| `[x]` | `^RS` | RFID setup (tag type, position, VOID handling, retries; legacy/a/c slots flagged partial; spec-modelled, hardware-unverified) | | | `[ ]` | `^RT` | read tag (legacy; superseded by ^RF read) | `Native build` | | `[ ]` | `^RU` | read unique chip serialization (TID-derived EPC serial) | `Native build` | | `[ ]` | `~RV` | report encoding result (pre-Link-OS; absent from current guide; passthrough) | `Out of scope` | -| `[ ]` | `^RW` | set read & write power | `Coming soon` | +| `[x]` | `^RW` | set read & write power (antenna slot flagged partial; spec-modelled, hardware-unverified) | | | `[ ]` | `^RL` | lock / permalock tag memory (password-coupled companion of ^RF) | `Coming soon` | | `[ ]` | `^HR` | calibrate RFID tag position | `Native build` | | `[ ]` | `^HL` / `~HL` | RFID data log (return to host) | `Native build` | diff --git a/packages/core/src/lib/designFile.rfid.test.ts b/packages/core/src/lib/designFile.rfid.test.ts new file mode 100644 index 00000000..1f30475d --- /dev/null +++ b/packages/core/src/lib/designFile.rfid.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { sanitizeRfidEpc } from "../types/LabelConfig"; +import type { LabelConfig } from "../types/LabelConfig"; + +// Parser and UI keep the partitions summing to the bit count; a hand-edited +// design file must not slip an invalid ^RB past them. +describe("sanitizeRfidEpc", () => { + const label = (over: Partial): LabelConfig => + ({ widthMm: 50, heightMm: 30, dpmm: 8, ...over }) as LabelConfig; + + it("keeps a pair whose partitions sum to the total", () => { + const l = label({ rfidEpcBits: 96, rfidEpcPartitions: [8, 24, 64] }); + sanitizeRfidEpc(l); + expect(l.rfidEpcBits).toBe(96); + expect(l.rfidEpcPartitions).toEqual([8, 24, 64]); + }); + + it("drops the pair whole when the sum disagrees", () => { + const l = label({ rfidEpcBits: 96, rfidEpcPartitions: [1] }); + sanitizeRfidEpc(l); + expect(l.rfidEpcBits).toBeUndefined(); + expect(l.rfidEpcPartitions).toBeUndefined(); + }); + + it("leaves an unpartitioned total alone", () => { + const l = label({ rfidEpcBits: 96 }); + sanitizeRfidEpc(l); + expect(l.rfidEpcBits).toBe(96); + }); +}); diff --git a/packages/core/src/lib/designFile.ts b/packages/core/src/lib/designFile.ts index 769ea429..eee3dc8e 100644 --- a/packages/core/src/lib/designFile.ts +++ b/packages/core/src/lib/designFile.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { JM_DENSITY_VALUES, labelConfigSchema, type JmDensity, type LabelConfig } from "../types/LabelConfig"; +import { JM_DENSITY_VALUES, labelConfigSchema, sanitizeRfidEpc, type JmDensity, type LabelConfig } from "../types/LabelConfig"; import { labelObjectBaseSchema } from "../types/LabelObject"; import { variableSchema, @@ -103,6 +103,7 @@ export function parseDesignFile(text: string): Result { + it("splits an existing total instead of dropping it", () => { + expect(splitEpcBits(96)).toEqual([48, 48]); + expect(splitEpcBits(97)).toEqual([49, 48]); + }); + + it("adds fields once 64-bit partitions cannot hold the total", () => { + expect(splitEpcBits(200)).toEqual([50, 50, 50, 50]); + expect(splitEpcBits(200)?.every((b) => b <= 64)).toBe(true); + }); + + it("starts from two default fields when there is no total yet", () => { + expect(splitEpcBits(undefined)).toEqual([8, 8]); + }); + + it("refuses to split a tag too narrow for two fields", () => { + expect(splitEpcBits(1)).toBeNull(); + }); + + it("reports totals past 16 x 64 bits as unpartitionable", () => { + expect(splitEpcBits(1024)).not.toBeNull(); + expect(splitEpcBits(1025)).toBeNull(); + }); +}); + +describe("fixed-total partition edits", () => { + const SGTIN = [8, 3, 3, 20, 24, 38]; + + it("derives the trailing field from the total", () => { + expect(epcTrailingField(96, SGTIN.slice(0, -1))).toBe(38); + }); + + it("keeps the total when a field is retyped", () => { + const next = epcSetField(96, SGTIN, 3, 26); + expect(next).toEqual([8, 3, 3, 26, 24, 32]); + expect(next.reduce((a, b) => a + b, 0)).toBe(96); + }); + + it("clamps a retype to what leaves the trailing field valid", () => { + // The other typed fields claim 34 bits, so this one may grow to 61 before + // the trailing field would fall under its 1-bit minimum. + expect(epcSetField(96, SGTIN, 4, 99)).toEqual([8, 3, 3, 20, 61, 1]); + expect(epcFieldRange(96, SGTIN, 4)).toEqual({ min: 1, max: 61 }); + }); + + it("keeps the total when a field is added or removed", () => { + const added = epcAddField(96, SGTIN); + expect(added).toEqual([8, 3, 3, 20, 24, 19, 19]); + expect(added?.reduce((a, b) => a + b, 0)).toBe(96); + const removed = epcRemoveField(96, SGTIN, 0); + expect(removed).toEqual([3, 3, 20, 24, 46]); + expect(removed?.reduce((a, b) => a + b, 0)).toBe(96); + }); + + it("collapses to a lone field, which the caller drops", () => { + expect(epcRemoveField(96, [48, 48], 0)).toEqual([48]); + }); + + it("spreads the freed bits so no field passes 64", () => { + expect(epcRemoveField(96, [48, 12, 36], 0)).toEqual([32, 64]); + expect(epcRemoveField(96, [48, 12, 36], 0)?.every((b) => b <= 64)).toBe(true); + }); + + it("refuses a removal the remaining fields cannot absorb", () => { + expect(epcRemoveField(192, [64, 64, 64], 0)).toBeNull(); + }); + + it("bounds the total by what the typed fields already claim", () => { + expect(epcTotalRange(SGTIN, 65535)).toEqual({ min: 59, max: 122 }); + expect(epcTotalRange(undefined, 65535)).toEqual({ min: 1, max: 65535 }); + }); + + it("grows the tag when the trailing field is retyped", () => { + expect(epcSetTrailing(SGTIN, 46)).toEqual({ + partitions: [8, 3, 3, 20, 24, 46], + total: 104, + }); + expect(epcSetTrailing(SGTIN, 99).partitions.at(-1)).toBe(64); + }); + + it("moves a total change into the trailing field", () => { + expect(epcSetTotal(SGTIN, 104)).toEqual([8, 3, 3, 20, 24, 46]); + }); +}); diff --git a/packages/core/src/lib/rfidEpc.ts b/packages/core/src/lib/rfidEpc.ts new file mode 100644 index 00000000..37b11808 --- /dev/null +++ b/packages/core/src/lib/rfidEpc.ts @@ -0,0 +1,119 @@ +import { RFID_EPC_MAX_PARTITIONS, RFID_EPC_PARTITION_RANGE } from "../types/LabelConfig"; + +const { min: FIELD_MIN, max: FIELD_MAX } = RFID_EPC_PARTITION_RANGE; + +/** The one EPC layout the ZPL guide documents (p.425): header, filter, + * partition, company prefix, item reference, serial number. */ +export const SGTIN_96_FIELDS = [8, 3, 3, 20, 24, 38] as const; + +/** Seed a partition list: a structure needs at least two fields, and wide + * totals need more because a field holds 64 bits at most. Null when the + * total does not fit 16 fields, or is too narrow for two. */ +export function splitEpcBits(total: number | undefined): number[] | null { + if (total === undefined) return [8, 8]; + // A 1-bit tag cannot hold two fields; growing it silently would be worse. + if (total < 2) return null; + const parts = Math.max(2, Math.ceil(total / FIELD_MAX)); + if (parts > RFID_EPC_MAX_PARTITIONS) return null; + const base = Math.floor(total / parts); + const remainder = total - base * parts; + return Array.from({ length: parts }, (_, i) => base + (i < remainder ? 1 : 0)); +} + +/** The tag width is the given and the fields divide it (spec p.424: the + * partitions add up to n), so the last field is derived, never typed. + * Every helper below preserves that. */ +export function epcTrailingField(total: number, leading: readonly number[]): number { + return total - leading.reduce((a, b) => a + b, 0); +} + +/** Bounds for a typed field: what keeps the derived trailing field within + * 1..64 while the total stays put. */ +export function epcFieldRange( + total: number, + parts: readonly number[], + index: number, +): { min: number; max: number } { + const others = parts + .slice(0, -1) + .reduce((sum, bits, i) => (i === index ? sum : sum + bits), 0); + return { + min: Math.max(FIELD_MIN, total - others - FIELD_MAX), + max: Math.min(FIELD_MAX, total - others - FIELD_MIN), + }; +} + +/** Bounds for the total: the typed fields claim their bits, and the trailing + * field has to fit in what remains. Unpartitioned totals are free. */ +export function epcTotalRange( + parts: readonly number[] | undefined, + maxBits: number, +): { min: number; max: number } { + if (!parts) return { min: FIELD_MIN, max: maxBits }; + const leading = parts.slice(0, -1).reduce((a, b) => a + b, 0); + return { min: leading + FIELD_MIN, max: Math.min(maxBits, leading + FIELD_MAX) }; +} + +/** Add a field by halving the trailing one, so the tag width holds. Null when + * the structure is full or the trailing field cannot be split. */ +export function epcAddField(total: number, parts: readonly number[]): number[] | null { + if (parts.length >= RFID_EPC_MAX_PARTITIONS) return null; + const trailing = epcTrailingField(total, parts.slice(0, -1)); + const taken = Math.floor(trailing / 2); + if (taken < FIELD_MIN || trailing - taken < FIELD_MIN) return null; + return [...parts.slice(0, -1), taken, trailing - taken]; +} + +/** Remove a field; its bits flow back from the end, total unchanged. Null + * when they fit nowhere without breaking the 64-bit cap; a lone survivor is + * no structure, so the caller drops it. */ +export function epcRemoveField( + total: number, + parts: readonly number[], + index: number, +): number[] | null { + const kept = parts.filter((_, i) => i !== index); + if (kept.length <= 1) return kept; + const out = [...kept]; + let freed = total - out.reduce((a, b) => a + b, 0); + for (let i = out.length - 1; i >= 0 && freed > 0; i--) { + const room = FIELD_MAX - (out[i] ?? 0); + const take = Math.min(room, freed); + out[i] = (out[i] ?? 0) + take; + freed -= take; + } + return freed === 0 ? out : null; +} + +/** Retype one field, then let the trailing field absorb the difference. */ +export function epcSetField( + total: number, + parts: readonly number[], + index: number, + bits: number, +): number[] { + const range = epcFieldRange(total, parts, index); + const clamped = Math.min(range.max, Math.max(range.min, bits)); + const leading = parts.slice(0, -1).map((b, i) => (i === index ? clamped : b)); + return [...leading, epcTrailingField(total, leading)]; +} + +/** Retype the trailing field: the leading ones are already claimed, so this + * sets the tag width rather than redistributing within it. */ +export function epcSetTrailing( + parts: readonly number[], + bits: number, +): { partitions: number[]; total: number } { + const clamped = Math.min(FIELD_MAX, Math.max(FIELD_MIN, bits)); + const leading = parts.slice(0, -1); + return { + partitions: [...leading, clamped], + total: leading.reduce((a, b) => a + b, 0) + clamped, + }; +} + +/** Retype the tag width; the trailing field takes the difference. */ +export function epcSetTotal(parts: readonly number[], total: number): number[] { + const leading = parts.slice(0, -1); + return [...leading, epcTrailingField(total, leading)]; +} diff --git a/packages/core/src/lib/rfidPosition.test.ts b/packages/core/src/lib/rfidPosition.test.ts new file mode 100644 index 00000000..90edb4be --- /dev/null +++ b/packages/core/src/lib/rfidPosition.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { + rfidAmountRange, + rfidPositionConvert, + rfidPositionFromParts, + rfidPositionOf, + rfidPositionParts, + rfidPositionValue, +} from "./rfidPosition"; + +describe("rfidPositionValue", () => { + it("reads the three wire forms as mm off the leading edge", () => { + expect(rfidPositionValue("520", 8)).toEqual({ mode: "abs", mm: 65 }); + expect(rfidPositionValue("F90", 8)).toEqual({ mode: "F", mm: 90 }); + expect(rfidPositionValue("B14", 8)).toEqual({ mode: "B", mm: -14 }); + }); + + it("rejects an unset or malformed position", () => { + expect(rfidPositionValue(undefined, 8)).toBeNull(); + expect(rfidPositionValue("X1", 8)).toBeNull(); + }); +}); + +describe("rfidPositionOf", () => { + it("keeps the notation the design already uses", () => { + expect(rfidPositionOf(65, "abs", 8, 100)).toBe("520"); + expect(rfidPositionOf(65, "F", 8, 100)).toBe("F65"); + }); + + it("switches to backfeed past the leading edge, capped at B30", () => { + expect(rfidPositionOf(-14, "F", 8, 100)).toBe("B14"); + expect(rfidPositionOf(-99, "abs", 8, 100)).toBe("B30"); + }); + + it("clamps forward travel to the label length and the F999 domain", () => { + expect(rfidPositionOf(500, "abs", 8, 100)).toBe("800"); + expect(rfidPositionOf(5000, "F", 8, 2000)).toBe("F999"); + }); +}); + +describe("rfidPositionParts", () => { + it("splits each notation into its own unit", () => { + expect(rfidPositionParts("520")).toEqual({ mode: "abs", amount: 520 }); + expect(rfidPositionParts("F90")).toEqual({ mode: "F", amount: 90 }); + expect(rfidPositionParts("B14")).toEqual({ mode: "B", amount: 14 }); + expect(rfidPositionParts("nonsense")).toBeNull(); + }); + + it("round-trips through rfidPositionFromParts", () => { + for (const wire of ["520", "F90", "B14"]) { + const p = rfidPositionParts(wire)!; + expect(rfidPositionFromParts(p.mode, p.amount)).toBe(wire); + } + }); + + it("bounds the amount per notation", () => { + expect(rfidAmountRange("abs", 240)).toEqual({ min: 0, max: 240 }); + expect(rfidAmountRange("F", 240)).toEqual({ min: 0, max: 999 }); + expect(rfidAmountRange("B", 240)).toEqual({ min: 0, max: 30 }); + }); +}); + +describe("rfidPositionConvert", () => { + it("keeps the distance when the unit changes", () => { + expect(rfidPositionConvert("abs", "F", 520, 8)).toBe(65); + expect(rfidPositionConvert("F", "abs", 65, 8)).toBe(520); + }); + + it("leaves same-notation switches alone", () => { + expect(rfidPositionConvert("abs", "abs", 520, 8)).toBe(520); + }); + + it("drops to the leading edge when the direction flips", () => { + // B points before the edge; no forward notation can say that, so keeping + // the magnitude would move the position by twice the distance. + expect(rfidPositionConvert("B", "F", 14, 8)).toBe(0); + expect(rfidPositionConvert("B", "abs", 14, 8)).toBe(0); + expect(rfidPositionConvert("abs", "B", 520, 8)).toBe(0); + }); +}); + +describe("absolute positions stay inside the wire domain", () => { + it("caps a tall label at five digits", () => { + expect(rfidPositionOf(5000, "abs", 24, 5000)).toBe("99999"); + expect(rfidAmountRange("abs", 120000)).toEqual({ min: 0, max: 99999 }); + }); +}); diff --git a/packages/core/src/lib/rfidPosition.ts b/packages/core/src/lib/rfidPosition.ts new file mode 100644 index 00000000..dff8145c --- /dev/null +++ b/packages/core/src/lib/rfidPosition.ts @@ -0,0 +1,93 @@ +import { RFID_POSITION_RE } from "../types/LabelConfig"; + +/** ^RS p wire forms (spec p.435): absolute dot rows from the label top, + * `F` mm forward off the leading edge, `B` mm of backfeed (before it). */ +export type RfidPositionMode = "abs" | "F" | "B"; + +/** Spec default for Link-OS printers: leading edge at the print line. */ +export const RFID_POSITION_DEFAULT = "F0"; + +export interface RfidPositionValue { + mode: RfidPositionMode; + /** Distance from the label's leading edge; negative for backfeed. */ + mm: number; +} + +/** Media-motion distance the position declares, in mm off the label top. + * Dots are physical head dots like ^ML (^JM does not scale the feed). */ +export function rfidPositionValue( + position: string | undefined, + dpmm: number, +): RfidPositionValue | null { + if (position === undefined || !RFID_POSITION_RE.test(position)) return null; + if (position.startsWith("F")) return { mode: "F", mm: Number(position.slice(1)) }; + if (position.startsWith("B")) return { mode: "B", mm: -Number(position.slice(1)) }; + return { mode: "abs", mm: Number(position) / dpmm }; +} + +const RFID_FORWARD_MM_MAX = 999; +const RFID_BACKFEED_MM_MAX = 30; +/** The absolute form is five digits wide on the wire (RFID_POSITION_RE). */ +const RFID_ABS_DOTS_MAX = 99999; + +/** Wire form for a dragged distance, keeping the mode the user set: forward + * and absolute are the same direction in two notations, so only crossing the + * leading edge switches (to `B`, the sole backfeed notation). */ +export function rfidPositionOf( + mm: number, + mode: RfidPositionMode, + dpmm: number, + labelHeightMm: number, +): string { + if (mm < 0) { + return `B${Math.min(RFID_BACKFEED_MM_MAX, Math.round(-mm))}`; + } + const forward = Math.min(mm, labelHeightMm); + if (mode === "abs") { + return String(Math.min(RFID_ABS_DOTS_MAX, Math.round(forward * dpmm))); + } + return `F${Math.min(RFID_FORWARD_MM_MAX, Math.round(forward))}`; +} + +/** ^RS p split into its notation and that notation's own unit (absolute in + * dots, F/B in mm), for typed UI controls over the wire string. */ +export function rfidPositionParts( + position: string | undefined, +): { mode: RfidPositionMode; amount: number } | null { + if (position === undefined || !RFID_POSITION_RE.test(position)) return null; + if (position.startsWith("F")) return { mode: "F", amount: Number(position.slice(1)) }; + if (position.startsWith("B")) return { mode: "B", amount: Number(position.slice(1)) }; + return { mode: "abs", amount: Number(position) }; +} + +/** Carry an amount into another notation: absolute counts dots, F/B count + * mm. A direction change lands on the leading edge, since no forward + * notation can express backfeed. */ +export function rfidPositionConvert( + from: RfidPositionMode, + to: RfidPositionMode, + amount: number, + dpmm: number, +): number { + if ((from === "B") !== (to === "B")) return 0; + if ((from === "abs") === (to === "abs")) return amount; + return to === "abs" ? Math.round(amount * dpmm) : Math.round(amount / dpmm); +} + +export function rfidPositionFromParts(mode: RfidPositionMode, amount: number): string { + return mode === "abs" ? String(amount) : `${mode}${amount}`; +} + +/** Domain of the amount slot per notation (spec p.435); the absolute cap is + * the label length, so callers pass it in dots. */ +export function rfidAmountRange( + mode: RfidPositionMode, + labelHeightDots: number, +): { min: number; max: number } { + if (mode === "abs") { + return { min: 0, max: Math.min(RFID_ABS_DOTS_MAX, Math.round(labelHeightDots)) }; + } + return mode === "F" + ? { min: 0, max: RFID_FORWARD_MM_MAX } + : { min: 0, max: RFID_BACKFEED_MM_MAX }; +} diff --git a/packages/core/src/lib/zplGenerator.ts b/packages/core/src/lib/zplGenerator.ts index 29666972..7faf23d0 100644 --- a/packages/core/src/lib/zplGenerator.ts +++ b/packages/core/src/lib/zplGenerator.ts @@ -758,6 +758,29 @@ function generateZplBlock( if (label.mediaMode) lines.push(`^MM${label.mediaMode}`); if (label.mediaType) lines.push(`^MT${label.mediaType}`); if (label.mediaTracking) lines.push(`^MN${label.mediaTracking}`); + + const rsSlots = trimTrailingEmptySlots([ + label.rfidTagType?.toString() ?? '', + label.rfidPosition ?? '', + label.rfidVoidLength?.toString() ?? '', + label.rfidRetries?.toString() ?? '', + label.rfidErrorHandling ?? '', + '', + '', + label.rfidVoidSpeed?.toString() ?? '', + ]); + if (rsSlots.length > 0) lines.push(`^RS${rsSlots.join(',')}`); + if (label.rfidEpcBits !== undefined) { + const parts = label.rfidEpcPartitions ? `,${label.rfidEpcPartitions.join(',')}` : ''; + lines.push(`^RB${label.rfidEpcBits}${parts}`); + } + if (label.rfidReadPower !== undefined || label.rfidWritePower !== undefined) { + const slots = trimTrailingEmptySlots([ + label.rfidReadPower?.toString() ?? '', + label.rfidWritePower?.toString() ?? '', + ]); + lines.push(`^RW${slots.join(',')}`); + } if (label.maxLabelLength !== undefined) lines.push(`^ML${label.maxLabelLength}`); // Positional pair; default the unset slot to "N" (no motion). if (label.mediaFeedPowerUp || label.mediaFeedHeadClose) { diff --git a/packages/core/src/lib/zplParser/handlers/labelConfig.ts b/packages/core/src/lib/zplParser/handlers/labelConfig.ts index 863efcc5..6bb4a405 100644 --- a/packages/core/src/lib/zplParser/handlers/labelConfig.ts +++ b/packages/core/src/lib/zplParser/handlers/labelConfig.ts @@ -1,4 +1,4 @@ -import { DARKNESS_INSTANT_RANGE, DARKNESS_PERMANENT_RANGE, MAX_LABEL_LENGTH_RANGE, SLEW_DOT_ROWS_RANGE, SPEED_RANGE, isBackfeedPercent, isBackfeedSequence, isMediaFeedMode, isMediaMode, isMediaTracking, isMediaType, isPrintOrientation } from "../../../types/LabelConfig"; +import { DARKNESS_INSTANT_RANGE, DARKNESS_PERMANENT_RANGE, MAX_LABEL_LENGTH_RANGE, RFID_EPC_BITS_RANGE, RFID_EPC_MAX_PARTITIONS, RFID_EPC_PARTITION_RANGE, RFID_POSITION_RE, RFID_RETRIES_RANGE, SLEW_DOT_ROWS_RANGE, SPEED_RANGE, isBackfeedPercent, isBackfeedSequence, isMediaFeedMode, isMediaMode, isMediaTracking, isMediaType, isPrintOrientation, isRfidErrorHandling, parseRfidPower, type RfidPower } from "../../../types/LabelConfig"; import { parseIntOrUndef } from "../../inputParse"; import { isYesNo } from "../../../types/typeHelpers"; import { dotsToMm } from "../../coordinates"; @@ -47,6 +47,94 @@ export function createLabelConfigHandlers( if (isYesNo(o)) labelConfig.overridePauseCount = o; } }, + // ^RSt,p,v,n,e,a,c,s (spec p.434-437). Every slot the design carried but + // we could not adopt is reported: dropping it silently would hide a real + // setting (legacy tag types, the unmodelled a/c slots, out-of-domain values). + RS(p) { + const adopt = (slot: number, take: (raw: string | undefined) => boolean) => { + if (take(p[slot])) return; + if (strParam(p[slot]) !== "") s.result.partialCmds.add("^RS"); + }; + adopt(0, (raw) => { + if (int(raw, 0) !== 8) return false; + labelConfig.rfidTagType = 8; + return true; + }); + adopt(1, (raw) => { + const pos = strParam(raw); + if (!RFID_POSITION_RE.test(pos)) return false; + labelConfig.rfidPosition = pos; + return true; + }); + adopt(2, (raw) => { + const v = inRange(physDots(raw), SLEW_DOT_ROWS_RANGE); + if (v === undefined) return false; + labelConfig.rfidVoidLength = v; + return true; + }); + adopt(3, (raw) => { + const n = inRange(parseIntOrUndef(raw), RFID_RETRIES_RANGE); + if (n === undefined) return false; + labelConfig.rfidRetries = n; + return true; + }); + adopt(4, (raw) => { + const e = strParam(raw); + if (!isRfidErrorHandling(e)) return false; + labelConfig.rfidErrorHandling = e; + return true; + }); + adopt(5, () => false); + adopt(6, () => false); + adopt(7, (raw) => { + const vs = inRange(parseIntOrUndef(raw), SPEED_RANGE); + if (vs === undefined) return false; + labelConfig.rfidVoidSpeed = vs; + return true; + }); + }, + // ^RBn,p0..p15: partitions must sum to n (spec p.424), else the whole + // command drops (a half-adopted structure would encode wrong fields). + RB(p) { + const bits = inRange(parseIntOrUndef(p[0]), RFID_EPC_BITS_RANGE); + if (bits === undefined) { + s.result.partialCmds.add("^RB"); + return; + } + // Only a trailing delimiter is noise; an empty slot inside the list is + // a value we cannot read, so it must not be normalised away. + const slots = p.slice(1); + while (slots.length > 0 && (slots[slots.length - 1] ?? "").trim() === "") slots.pop(); + if (slots.length === 0) { + labelConfig.rfidEpcBits = bits; + delete labelConfig.rfidEpcPartitions; + return; + } + const parts = slots.map((x) => inRange(parseIntOrUndef(x), RFID_EPC_PARTITION_RANGE)); + if ( + slots.length > RFID_EPC_MAX_PARTITIONS || + slots.some((x) => (x ?? "").trim() === "") || + parts.some((x) => x === undefined) || + parts.reduce((a, b) => (a ?? 0) + (b ?? 0), 0) !== bits + ) { + s.result.partialCmds.add("^RB"); + return; + } + labelConfig.rfidEpcBits = bits; + labelConfig.rfidEpcPartitions = parts as number[]; + }, + // ^RWr,w,a: power as 0-30 or H/M/L (firmware union). + RW(p) { + const take = (slot: number, set: (v: RfidPower) => void) => { + const value = parseRfidPower(p[slot]); + if (value !== undefined) set(value); + else if (strParam(p[slot]) !== "") s.result.partialCmds.add("^RW"); + }; + take(0, (v) => (labelConfig.rfidReadPower = v)); + take(1, (v) => (labelConfig.rfidWritePower = v)); + // The antenna slot is unmodelled, so any value in it is a loss. + if (strParam(p[2]) !== "") s.result.partialCmds.add("^RW"); + }, MM(_, rest) { const mode = firstChar(rest); if (isMediaMode(mode)) labelConfig.mediaMode = mode; diff --git a/packages/core/src/types/LabelConfig.test.ts b/packages/core/src/types/LabelConfig.test.ts index dedff6c9..b89a99f5 100644 --- a/packages/core/src/types/LabelConfig.test.ts +++ b/packages/core/src/types/LabelConfig.test.ts @@ -28,6 +28,9 @@ describe('LABEL_CONFIG_FIELDS derivations', () => { 'labelHomeX', 'labelHomeY', 'labelTop', 'labelShift', 'printQuantity', 'pauseCount', 'replicates', 'overridePauseCount', 'mapClear', 'slewDotRows', 'slewToHome', 'programmablePause', + 'rfidTagType', 'rfidPosition', 'rfidVoidLength', 'rfidRetries', + 'rfidErrorHandling', 'rfidVoidSpeed', 'rfidEpcBits', 'rfidEpcPartitions', + 'rfidReadPower', 'rfidWritePower', ]), ); }); diff --git a/packages/core/src/types/LabelConfig.ts b/packages/core/src/types/LabelConfig.ts index 173233af..ec11f47c 100644 --- a/packages/core/src/types/LabelConfig.ts +++ b/packages/core/src/types/LabelConfig.ts @@ -101,6 +101,41 @@ export const SLEW_DOT_ROWS_RANGE = { min: 0, max: 32000 } as const; /** ^LT y range (Zebra -120..+120); shared with the density rescale clamp. */ export const LABEL_TOP_RANGE = { min: -120, max: 120 } as const; +/** ^RS n: encode retries per label before error handling kicks in. */ +export const RFID_RETRIES_RANGE = { min: 1, max: 10 } as const; +/** ^RW r/w numeric power band; some firmware takes H/M/L instead. */ +export const RFID_POWER_RANGE = { min: 0, max: 30 } as const; +export const RFID_POWER_LEVELS = ['H', 'M', 'L'] as const; +export type RfidPowerLevel = (typeof RFID_POWER_LEVELS)[number]; +export const isRfidPowerLevel = makeEnumGuard(RFID_POWER_LEVELS); +const rfidPowerSchema = z.union([ + intInRange(RFID_POWER_RANGE), + z.enum(RFID_POWER_LEVELS), +]); +export type RfidPower = z.infer; +/** ^RW r/w: the firmware-dependent union of a level letter and 0-30. */ +export function parseRfidPower(raw: string | undefined): RfidPower | undefined { + const value = (raw ?? '').trim().toUpperCase(); + if (isRfidPowerLevel(value)) return value; + const n = Number.parseInt(value, 10); + return String(n) === value && n >= RFID_POWER_RANGE.min && n <= RFID_POWER_RANGE.max + ? n + : undefined; +} + +/** ^RS e: drop format (N), pause (P) or error mode (E) after retries. */ +export const RFID_ERROR_HANDLING_VALUES = ['N', 'P', 'E'] as const; +export type RfidErrorHandling = (typeof RFID_ERROR_HANDLING_VALUES)[number]; +export const isRfidErrorHandling = makeEnumGuard(RFID_ERROR_HANDLING_VALUES); +/** ^RS p: absolute dots from the label top, or F/B + mm off the leading + * edge (F0-F999, B0-B30, spec p.435); kept in its wire form. */ +export const RFID_POSITION_RE = /^(?:\d{1,5}|F\d{1,3}|B(?:\d|[12]\d|30))$/; +export const RFID_EPC_PARTITION_RANGE = { min: 1, max: 64 } as const; +export const RFID_EPC_MAX_PARTITIONS = 16; +/** ^RB n: spec-unbounded ("bit size of the tag"); sane 16-bit cap. The + * partition form is implicitly tighter (16 x 64 via the sum rule). */ +export const RFID_EPC_BITS_RANGE = { min: 1, max: 65535 } as const; + /** ^MU b,c dpi tokens; 200 = 203 dpi; ratio drives resampling. */ export const MU_DPI_VALUES = [150, 200, 300, 600] as const; export type MuDpi = (typeof MU_DPI_VALUES)[number]; @@ -246,6 +281,32 @@ export const labelConfigSchema = z.object({ slewToHome: z.boolean().optional(), /** ^PP: pause after the format prints (until PAUSE or ~PS). */ programmablePause: z.boolean().optional(), + /** ^RS t: 8 = Gen 2, the only tag type current RFID printers support. + * Typed number (not literal) so the numeric-field derivations stay uniform. */ + rfidTagType: z.number().int().refine((n): boolean => n === 8).optional(), + /** ^RS p: programming position, wire form (dots or F/B mm). */ + rfidPosition: z.string().regex(RFID_POSITION_RE).optional(), + /** ^RS v: VOID printout length in dot rows. */ + rfidVoidLength: intInRange(SLEW_DOT_ROWS_RANGE).optional(), + /** ^RS n: encode retries. */ + rfidRetries: intInRange(RFID_RETRIES_RANGE).optional(), + /** ^RS e: error handling after retries. */ + rfidErrorHandling: z.enum(RFID_ERROR_HANDLING_VALUES).optional(), + /** ^RS s: VOID print speed. */ + rfidVoidSpeed: intInRange(SPEED_RANGE).optional(), + /** ^RB n: EPC data-structure size in bits. */ + rfidEpcBits: intInRange(RFID_EPC_BITS_RANGE).optional(), + /** ^RB p0..p15: partition sizes in bits; only stored when they sum to + * rfidEpcBits (spec p.424). */ + rfidEpcPartitions: z + .array(intInRange(RFID_EPC_PARTITION_RANGE)) + .min(1) + .max(RFID_EPC_MAX_PARTITIONS) + .optional(), + /** ^RW r: read power. */ + rfidReadPower: rfidPowerSchema.optional(), + /** ^RW w: write power. */ + rfidWritePower: rfidPowerSchema.optional(), }); export type LabelConfig = z.infer; @@ -345,6 +406,19 @@ export const LABEL_CONFIG_FIELDS = { slewDotRows: { scope: 'perLabel', emits: true, scales: 'jmOnly', perFormat: true, clamp: SLEW_DOT_ROWS_RANGE }, slewToHome: { scope: 'perLabel', emits: true, scales: 'never', perFormat: true }, programmablePause: { scope: 'perLabel', emits: true, scales: 'never', perFormat: true }, + // RFID setup persists on the printer across formats (spec p.424/435), so + // none is perFormat; spec-only (no RFID hardware), scales stay 'never' + // (rfidVoidLength parses via physDots like ^ML; rfidPosition is wire-form). + rfidTagType: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidPosition: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidVoidLength: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidRetries: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidErrorHandling: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidVoidSpeed: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidEpcBits: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidEpcPartitions: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidReadPower: { scope: 'perLabel', emits: true, scales: 'never' }, + rfidWritePower: { scope: 'perLabel', emits: true, scales: 'never' }, } as const satisfies { [K in keyof LabelConfig]-?: SpecFor }; const FIELD_ENTRIES = Object.entries(LABEL_CONFIG_FIELDS) as [ @@ -381,3 +455,17 @@ export const PER_FORMAT_ZPL_FIELDS = fieldsWhere((s) => s.perFormat === true); /** Config keys that never reach emitted ZPL. */ export const NON_EMITTING_CONFIG_FIELDS = fieldsWhere((s) => !s.emits); + +/** Enforce the ^RB cross-field invariant on a loaded envelope: partitions + * must sum to the bit count (parser and UI already guarantee it; a + * hand-edited design file must not emit an invalid ^RB). Mirrors the + * parser: a mismatched pair drops whole, not half. */ +export function sanitizeRfidEpc(label: LabelConfig): void { + const { rfidEpcBits, rfidEpcPartitions } = label; + if (rfidEpcPartitions === undefined) return; + if (rfidEpcBits !== undefined && rfidEpcPartitions.reduce((a, b) => a + b, 0) === rfidEpcBits) { + return; + } + delete label.rfidEpcBits; + delete label.rfidEpcPartitions; +} diff --git a/packages/mcp-server/src/tools.test.ts b/packages/mcp-server/src/tools.test.ts index e942af30..cad51e19 100644 --- a/packages/mcp-server/src/tools.test.ts +++ b/packages/mcp-server/src/tools.test.ts @@ -89,6 +89,22 @@ describe("mcp-server tools", () => { } }); + it("drops a mismatched ^RB pair from a hand-edited envelope", () => { + // Parser and UI keep partitions summing to the bits; the envelope path + // must not emit an invalid ^RB from a hand-edited file. + const design = { + schemaVersion: 5, + label: { widthMm: 50, heightMm: 30, dpmm: 8, rfidEpcBits: 96, rfidEpcPartitions: [1] }, + pages: [{ objects: [] }], + }; + expect(ok(exportZpl(design)).zpl).not.toContain("^RB"); + const intact = { + ...design, + label: { ...design.label, rfidEpcPartitions: [8, 24, 64] }, + }; + expect(ok(exportZpl(intact)).zpl).toContain("^RB96,8,24,64"); + }); + it("rejects duplicate explicit ids with a structured error", () => { const created = createDraft({ widthMm: 50, diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index dff61c56..02b17d6c 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -43,6 +43,7 @@ import { isEditableTarget } from "../../lib/dom"; import { KonvaObject } from "./KonvaObject"; import { PreflightOverlay } from "./PreflightOverlay"; import { CAPTURE_CHROME } from "./konvaObjectProps"; +import { RfidPositionGuide } from "./RfidPositionGuide"; import { Grid } from "./Grid"; import { GuideLines } from "./GuideLines"; import { Ruler, RULER_SIZE } from "./Ruler"; @@ -224,6 +225,14 @@ export const LabelCanvas = forwardRef(function LabelCa }, []); const colors = useColorScheme(); + // The programming position is media motion, not artwork: unlike the safe + // area it constrains nothing on the label, so the guide stays contextual + // (pick in progress or its settings tab open) instead of permanent chrome. + const pickingRfidPosition = useLabelStore((st) => st.pickingRfidPosition); + const endRfidPositionPick = useLabelStore((st) => st.endRfidPositionPick); + const rfidGuideVisible = useLabelStore( + (st) => st.printerSettingsTab === "rfid" || st.pickingRfidPosition, + ); const t = useT(); const { @@ -262,6 +271,20 @@ export const LabelCanvas = forwardRef(function LabelCa const paletteRows = useLabelStore((s) => s.paletteRows); const previewMode = useLabelStore((s) => s.previewMode); const previewLocks = useLabelStore(selectPreviewLocksEditor); + + useEffect(() => { + if (!pickingRfidPosition) return; + // A preview covers the canvas, so the pick has nothing left to click. + if (previewLocks) { + endRfidPositionPick(); + return; + } + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") endRfidPositionPick(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [pickingRfidPosition, previewLocks, endRfidPositionPick]); const exitPreviewMode = useLabelStore((s) => s.exitPreviewMode); // Entering preview unmounts the dragged node, so a mid-drag dragend may never // fire; clear the flag so off-label marks aren't stranded hidden afterwards. @@ -1341,7 +1364,7 @@ export const LabelCanvas = forwardRef(function LabelCa backgroundImage: `radial-gradient(circle, ${colors.canvasDot} 1px, transparent 1px)`, backgroundSize: "24px 24px", // Locus-of-attention feedback for preview lock. - cursor: previewLocks ? 'not-allowed' : cursor, + cursor: previewLocks ? 'not-allowed' : pickingRfidPosition ? 'crosshair' : cursor, }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} @@ -1372,6 +1395,22 @@ export const LabelCanvas = forwardRef(function LabelCa )} + {pickingRfidPosition && ( +
+
+ + {t.printerSettings.rfid.pickHint} + + +
+
+ )} +
@@ -1531,6 +1570,22 @@ export const LabelCanvas = forwardRef(function LabelCa /> )} + {/* ^RS programming position: label-config guide, drawn with + the safe area rather than as an object. */} + {!previewLocks && rfidGuideVisible && ( + + )} + {showGrid && ( void; + color: string; + mutedColor: string; +}) { + const label = useLabelStore((s) => s.label); + const setLabelConfig = useLabelStore((s) => s.setLabelConfig); + const [hoverY, setHoverY] = useState(null); + const value = + rfidPositionValue(label.rfidPosition, label.dpmm) ?? + rfidPositionValue(RFID_POSITION_DEFAULT, label.dpmm); + if (!value) return null; + const committed = label.rfidPosition !== undefined; + + const yOf = (mm: number) => labelY + mm * scale; + const captureY = yOf(-BACKFEED_MM_MAX); + const clampY = (y: number) => Math.min(yOf(label.heightMm), Math.max(captureY, y)); + const wireAt = (y: number) => + rfidPositionOf((y - labelY) / scale, value.mode, label.dpmm, label.heightMm); + const commit = (y: number) => setLabelConfig({ rfidPosition: wireAt(y) }); + // Pointer in the guide's own space: the canvas group rotates with the view, + // so stage coordinates would land on the wrong axis at 90/270 degrees. + const localY = (e: KonvaEventObject): number | null => { + const rel = e.target.getRelativePointerPosition(); + return rel ? clampY(captureY + rel.y) : null; + }; + + const onDrag = (e: KonvaEventObject) => { + e.target.x(labelX); + e.target.y(clampY(e.target.y())); + }; + const endDrag = (e: KonvaEventObject) => { + commit(e.target.y()); + if (picking) onPicked(); + }; + + const line = (y: number, stroke: string, text: string, ghost: boolean) => ( + + + + + + ); + + return ( + <> + {picking && ( + setHoverY(localY(e))} + onMouseLeave={() => setHoverY(null)} + onMouseDown={(e) => { + // Konva bubbles to the stage, where a mousedown starts a lasso. + e.cancelBubble = true; + const y = localY(e); + if (y !== null) commit(y); + setHoverY(null); + onPicked(); + }} + /> + )} + {/* Backfeed band above the leading edge: the only zone a `B` value can + express, so a pick inside the label necessarily reads as forward. */} + {picking && ( + + + + + )} + {/* Live preview of the value a click would commit. */} + {picking && hoverY !== null && ( + {line(hoverY, color, wireAt(hoverY), true)} + )} + { + e.cancelBubble = true; + }} + onDragMove={onDrag} + onDragEnd={endDrag} + onMouseEnter={(e) => { + const stage = e.target.getStage(); + if (stage) stage.container().style.cursor = "ns-resize"; + }} + onMouseLeave={(e) => { + const stage = e.target.getStage(); + if (stage) stage.container().style.cursor = ""; + }} + > + {/* Wide invisible strip so the 1px line stays grabbable. */} + + {line(0, committed ? color : mutedColor, label.rfidPosition ?? RFID_POSITION_DEFAULT, false)} + + + ); +} diff --git a/src/components/PrinterSettings/PrinterSettingsModal.tsx b/src/components/PrinterSettings/PrinterSettingsModal.tsx index 6ed235aa..f7e33a6c 100644 --- a/src/components/PrinterSettings/PrinterSettingsModal.tsx +++ b/src/components/PrinterSettings/PrinterSettingsModal.tsx @@ -21,6 +21,7 @@ import { IdentityTab } from "./IdentityTab"; import { MaintenanceTab } from "./MaintenanceTab"; import { McpServerTab } from "./McpServerTab"; import { MediaFeedTab } from "./MediaFeedTab"; +import { RfidTab } from "./RfidTab"; import { OutputTab } from "./OutputTab"; import { PreviewSettingsTab } from "./PreviewSettingsTab"; import { PrintQualityTab } from "./PrintQualityTab"; @@ -38,6 +39,8 @@ const TOP_TAB_OF = { mediaFeed: 'perLabel', printQuality: 'perLabel', output: 'perLabel', + // Niche hardware last in the group. + rfid: 'perLabel', clockTime: 'setupScript', encodingLanguage: 'setupScript', fonts: 'setupScript', @@ -70,6 +73,7 @@ const TAB_COMPONENTS: Partial> = { mcpServer: McpServerTab, dataSources: DataSourcesTab, mediaFeed: MediaFeedTab, + rfid: RfidTab, printQuality: PrintQualityTab, output: OutputTab, clockTime: ClockAndTimeTab, @@ -112,7 +116,15 @@ function ResetPerLabelButton({ : "border-border text-muted hover:text-text hover:bg-surface-2") } > - {armed ? confirmLabel : label} + {/* Both labels share one grid cell so the button keeps the taller + height: a shrink on arming would shift the layout mid-click and + let the second click land unconfirmed. */} + + {label} + + {confirmLabel} + + ); } diff --git a/src/components/PrinterSettings/RfidTab.test.tsx b/src/components/PrinterSettings/RfidTab.test.tsx new file mode 100644 index 00000000..47106a14 --- /dev/null +++ b/src/components/PrinterSettings/RfidTab.test.tsx @@ -0,0 +1,103 @@ +// @vitest-environment jsdom +import { describe, it, expect, afterEach } from "vitest"; +import { render, cleanup, fireEvent, act } from "@testing-library/react"; +import { TAB_GATES } from "./tabVisibility"; +import { RfidTab } from "./RfidTab"; +import { useLabelStore } from "../../store/labelStore"; + +afterEach(() => { + cleanup(); + act(() => { + useLabelStore + .getState() + .setLabelConfig({ rfidEpcBits: undefined, rfidEpcPartitions: undefined, rfidPosition: undefined }); + }); +}); + +describe("TAB_GATES.rfid", () => { + it("has no gate: the tab is always visible like other per-label tabs", () => { + expect(TAB_GATES.rfid).toBeUndefined(); + }); +}); + +describe("RfidTab programming position", () => { + it("carries the distance across a unit change, not the raw number", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidPosition: "520" })); + const { getByRole } = render(); + // 520 dots at 8 dpmm is 65 mm forward. + fireEvent.click(getByRole("button", { name: /Absolute from the top/ })); + fireEvent.click(getByRole("option", { name: /Forward from the edge/ })); + expect(useLabelStore.getState().label.rfidPosition).toBe("F65"); + }); + + it("lands on the leading edge when the direction flips", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidPosition: "B14" })); + const { getByRole } = render(); + fireEvent.click(getByRole("button", { name: /Backfeed before the edge/ })); + fireEvent.click(getByRole("option", { name: /Forward from the edge/ })); + expect(useLabelStore.getState().label.rfidPosition).toBe("F0"); + }); +}); + +describe("RfidTab EPC editor", () => { + it("shows the unpartitioned tag as one field, so adding splits it", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidEpcBits: 96 })); + const { getByLabelText, queryAllByLabelText } = render(); + expect((getByLabelText("Partition sizes 1") as HTMLInputElement).value).toBe("96"); + expect(queryAllByLabelText("Remove partition")).toHaveLength(0); + fireEvent.click(getByLabelText("Add partition")); + expect(queryAllByLabelText(/Partition sizes/)).toHaveLength(2); + }); + + it("splits an existing total on the first partition instead of dropping it", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidEpcBits: 96 })); + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("Add partition")); + expect(useLabelStore.getState().label.rfidEpcPartitions).toEqual([48, 48]); + expect(useLabelStore.getState().label.rfidEpcBits).toBe(96); + }); + + it("keeps the tag width while fields are added and retyped", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidEpcBits: 96 })); + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("Add partition")); + expect(useLabelStore.getState().label.rfidEpcPartitions).toEqual([48, 48]); + fireEvent.click(getByLabelText("Add partition")); + expect(useLabelStore.getState().label.rfidEpcPartitions).toEqual([48, 24, 24]); + + const first = getByLabelText("Partition sizes 1"); + fireEvent.change(first, { target: { value: "8" } }); + fireEvent.blur(first); + expect(useLabelStore.getState().label.rfidEpcPartitions).toEqual([8, 24, 64]); + expect(useLabelStore.getState().label.rfidEpcBits).toBe(96); + }); + + it("lets the trailing field set the tag width", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidEpcBits: 96 })); + const { getByLabelText } = render(); + fireEvent.click(getByLabelText("Add partition")); + const trailing = getByLabelText("Partition sizes 2"); + fireEvent.change(trailing, { target: { value: "60" } }); + fireEvent.blur(trailing); + const label = useLabelStore.getState().label; + expect(label.rfidEpcPartitions).toEqual([48, 60]); + expect(label.rfidEpcBits).toBe(108); + }); + + it("collapses to the plain total when only one field would remain", () => { + act(() => useLabelStore.getState().setLabelConfig({ rfidEpcBits: 96 })); + const { getByLabelText, getAllByLabelText } = render(); + fireEvent.click(getByLabelText("Add partition")); + fireEvent.click(getAllByLabelText("Remove partition")[0]!); + const label = useLabelStore.getState().label; + expect(label.rfidEpcPartitions).toBeUndefined(); + expect(label.rfidEpcBits).toBe(96); + }); + + it("dropping the last partition clears the structure", () => { + const { getByLabelText, getAllByLabelText } = render(); + fireEvent.click(getByLabelText("Add partition")); + fireEvent.click(getAllByLabelText("Remove partition")[0]!); + expect(useLabelStore.getState().label.rfidEpcPartitions).toBeUndefined(); + }); +}); diff --git a/src/components/PrinterSettings/RfidTab.tsx b/src/components/PrinterSettings/RfidTab.tsx new file mode 100644 index 00000000..3556e7c8 --- /dev/null +++ b/src/components/PrinterSettings/RfidTab.tsx @@ -0,0 +1,440 @@ +import { useT } from "../../hooks/useT"; +import { useLabelStore } from "../../store/labelStore"; +import { + RFID_EPC_BITS_RANGE, + RFID_EPC_PARTITION_RANGE, + RFID_ERROR_HANDLING_VALUES, + parseRfidPower, + RFID_RETRIES_RANGE, + SLEW_DOT_ROWS_RANGE, + SPEED_RANGE, + type RfidErrorHandling, +} from "@zplab/core/types/LabelConfig"; +import { + BoundedIntControl, + SafeStringInput, + ZplBoundedIntInput, + ZplCheckbox, + ZplCommandLabel, + ZplEnumCustomSelect, + ZplField, + ZplFieldHint, + ZplSubField, +} from "./zplFieldPrimitives"; +import { fieldGridCols, fieldGridCell, labelCls, zplCommandTagCls } from "../ui/formStyles"; +import { PlusIcon, TrashIcon, ViewfinderCircleIcon, XMarkIcon } from "@heroicons/react/16/solid"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { Select } from "../ui/Select"; +import { Tooltip } from "../ui/Tooltip"; +import { + SGTIN_96_FIELDS, + epcAddField, + epcFieldRange, + epcRemoveField, + epcSetField, + epcSetTotal, + epcSetTrailing, + epcTotalRange, + splitEpcBits, +} from "@zplab/core/lib/rfidEpc"; +import { + rfidAmountRange, + rfidPositionConvert, + rfidPositionFromParts, + rfidPositionParts, + type RfidPositionMode, +} from "@zplab/core/lib/rfidPosition"; +import { RegionFocus } from "./printerIllustration"; + +type LocRfid = ReturnType["printerSettings"]["rfid"]; + +const sectionHeadingCls = "font-mono text-[10px] uppercase tracking-widest text-muted"; + +const iconBtnCls = + "p-1 rounded border border-border text-muted hover:text-text hover:bg-surface-2 transition-colors"; + +const POSITION_MODES = ["F", "B", "abs"] as const; + +const MODE_LABEL_KEYS = { + F: "positionModeForward", + B: "positionModeBackfeed", + abs: "positionModeAbsolute", +} as const satisfies Record; + +const MODE_HINT_KEYS = { + F: "positionForwardHint", + B: "positionBackfeedHint", + abs: "positionAbsoluteHint", +} as const satisfies Record; + +const ERROR_LABEL_KEYS = { + N: "errorHandlingN", + P: "errorHandlingP", + E: "errorHandlingE", +} as const satisfies Record; + +const upperAlnum = (raw: string): string => raw.toUpperCase().replace(/[^0-9HML]/g, ""); + +/** Spec-only RFID setup (^RS / ^RB / ^RW): encoding needs an R-series + * printer, so the design just carries the commands. */ +export function RfidTab() { + const t = useT(); + const label = useLabelStore((s) => s.label); + const setLabelConfig = useLabelStore((s) => s.setLabelConfig); + const startRfidPositionPick = useLabelStore((s) => s.startRfidPositionPick); + const loc = t.printerSettings.rfid; + + const epcBits = label.rfidEpcBits; + const partitions = label.rfidEpcPartitions; + const totalRange = epcTotalRange(partitions, RFID_EPC_BITS_RANGE.max); + const fields = partitions ?? (epcBits !== undefined ? [epcBits] : []); + const nextFields = partitions + ? epcAddField(epcBits ?? 0, partitions) + : splitEpcBits(epcBits); + const parts = rfidPositionParts(label.rfidPosition); + // A lone field is no structure, so it collapses to the plain total. + const setPartitions = (next: number[]) => + setLabelConfig( + next.length > 1 + ? { rfidEpcPartitions: next, rfidEpcBits: next.reduce((a, b) => a + b, 0) } + : { rfidEpcPartitions: undefined }, + ); + const amountRange = rfidAmountRange(parts?.mode ?? "F", label.heightMm * label.dpmm); + + return ( +
+

{loc.specOnlyHint}

+ + {/* Grouped by what the printer does: what to encode, and what happens + when encoding fails. Both groups are ^RS parameters, which is why + the tag stays on the fields rather than the headings. */} +
+

{loc.encodingHeading}

+ + + setLabelConfig({ rfidTagType: v ? 8 : undefined })} + /> + + + + + +
+
+ + value={parts?.mode ?? ""} + onChange={(next) => { + if (next === "") { + setLabelConfig({ rfidPosition: undefined }); + return; + } + const range = rfidAmountRange(next, label.heightMm * label.dpmm); + // Absolute counts dots, F/B count mm: carry the distance, + // not the raw number. + const carried = parts + ? rfidPositionConvert(parts.mode, next, parts.amount, label.dpmm) + : 0; + setLabelConfig({ + rfidPosition: rfidPositionFromParts( + next, + Math.max(range.min, Math.min(carried, range.max)), + ), + }); + }} + groups={[ + { + options: [ + { value: "" as const, label: t.printerSettings.defaultOption }, + ...POSITION_MODES.map((m) => ({ + value: m, + label: loc[MODE_LABEL_KEYS[m]], + badge: m === "abs" ? undefined : m, + })), + ], + }, + ]} + /> +
+
+ + setLabelConfig({ + rfidPosition: + amount === undefined + ? undefined + : rfidPositionFromParts(parts?.mode ?? "F", amount), + }) + } + /> +
+ + {parts?.mode === "abs" ? t.printerSettings.dotsUnit : loc.mmUnit} + + + + +
+ {/* The hint describes the selected notation, so it waits for one. */} + {parts && {loc[MODE_HINT_KEYS[parts.mode]]}} +
+
+ +
+ +
+

{loc.failureHeading}

+ + + setLabelConfig({ rfidVoidLength: v })} + unit={t.printerSettings.dotsUnit} + /> + + + + setLabelConfig({ rfidRetries: v })} + /> + + + + setLabelConfig({ rfidErrorHandling: v })} + defaultLabel={t.printerSettings.defaultOption} + optionLabel={(m) => loc[ERROR_LABEL_KEYS[m]]} + /> + + + + setLabelConfig({ rfidVoidSpeed: v })} + /> + + +
+ + + +
+ + {loc.epcHeading} + + + + + ^RB +
+ {/* The tag width is the given; the fields divide it and the last + one is derived, so no edit can break the sum rule. */} +
+ {loc.epcBits} +
+ { + if (next === undefined) { + setLabelConfig({ rfidEpcBits: undefined, rfidEpcPartitions: undefined }); + return; + } + setLabelConfig({ + rfidEpcBits: next, + rfidEpcPartitions: partitions ? epcSetTotal(partitions, next) : undefined, + }); + }} + /> +
+
+ + {/* Unpartitioned still shows one field: the whole tag is the + remainder, so adding one splits it instead of conjuring two. */} +
+ {loc.epcPartitions} + {fields.map((bits, i) => { + // The trailing field is the remainder: typing into it claims + // bits the leading ones do not, so it sets the tag width. + const trailing = i === fields.length - 1; + const range = trailing + ? { min: RFID_EPC_PARTITION_RANGE.min, max: RFID_EPC_BITS_RANGE.max } + : epcFieldRange(epcBits ?? 0, fields, i); + return ( +
+ { + if (next === undefined) { + if (!partitions) { + setLabelConfig({ rfidEpcBits: undefined }); + return; + } + const kept = epcRemoveField(epcBits ?? 0, partitions, i); + if (kept) setPartitions(kept); + return; + } + if (!partitions) { + // No structure yet: this field IS the tag width, so + // the per-partition 64-bit cap does not apply. + setLabelConfig({ rfidEpcBits: next }); + return; + } + if (!trailing) { + setPartitions(epcSetField(epcBits ?? 0, fields, i, next)); + return; + } + const grown = epcSetTrailing(fields, next); + setLabelConfig({ + rfidEpcBits: grown.total, + rfidEpcPartitions: grown.partitions, + }); + }} + /> + {partitions && epcRemoveField(epcBits ?? 0, partitions, i) && ( + + )} +
+ ); + })} + + {nextFields !== null && ( + + + + )} +
+ + {epcBits === undefined ? ( +
+ {loc.epcEmpty} +
+ ) : ( +
+ {fields.map((bits, i) => ( +
0 ? "border-l border-border" : "" + } ${i % 2 === 0 ? "bg-accent/20 text-text" : "bg-surface-2 text-muted"}`} + > + {bits} +
+ ))} +
+ )} + +
+ + + + {partitions && ( + + + + )} +
+
+
+ + + + +
+ + {(id) => ( + setLabelConfig({ rfidReadPower: parseRfidPower(raw) })} + placeholder="16" + /> + )} + + + {(id) => ( + setLabelConfig({ rfidWritePower: parseRfidPower(raw) })} + placeholder="L" + /> + )} + +
+ {loc.powerHint} +
+
+
+ ); +} diff --git a/src/components/PrinterSettings/printerIllustration.tsx b/src/components/PrinterSettings/printerIllustration.tsx index cc9d60ed..e7be5544 100644 --- a/src/components/PrinterSettings/printerIllustration.tsx +++ b/src/components/PrinterSettings/printerIllustration.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useState, type ReactNode } from "react"; +import { rfidPositionValue } from "@zplab/core/lib/rfidPosition"; import { useLabelStore } from "../../store/labelStore"; /** Physical printer regions the settings fields map onto. The illustration @@ -15,7 +16,8 @@ export type PrinterRegion = | "originY" | "top" | "shift" - | "stack"; + | "stack" + | "antenna"; type Source = "focus" | "hover"; @@ -104,6 +106,48 @@ export function PrinterIllustration() { // hides the label boundary entirely; gap/web show the die-cut notches, // mark the black mark, auto a scanning sensor. const tracking = useLabelStore((s) => s.label.mediaTracking); + // RFID gadgets: inlay + encoder waves ride the ^RS position when one is + // set (absolute dots or F-mm over the label length; B sits at the leading + // edge), waves scale with the write power, VOID previews on exit focus. + const rfidSet = useLabelStore( + (s) => + s.label.rfidTagType !== undefined || + s.label.rfidPosition !== undefined || + s.label.rfidEpcBits !== undefined || + s.label.rfidReadPower !== undefined || + s.label.rfidWritePower !== undefined, + ); + // The whole gadget set also ghosts while the RFID tab itself is open, so + // browsing the tab explains the geometry before any value is set. + const rfidTabActive = useLabelStore((s) => s.printerSettingsTab === "rfid"); + const rfidPosition = useLabelStore((s) => s.label.rfidPosition); + const rfidWritePower = useLabelStore((s) => s.label.rfidWritePower); + const rfidVoid = useLabelStore((s) => s.label.rfidVoidLength !== undefined); + const heightMm = useLabelStore((s) => s.label.heightMm); + const dpmm = useLabelStore((s) => s.label.dpmm); + const STRIP_TOP = 56; + const STRIP_HEIGHT = 56; + // Same reader as the canvas guide, so both pictures cannot drift apart. + const rfidValue = rfidPositionValue(rfidPosition, dpmm); + const rfidLine = + rfidValue === null + ? null + : STRIP_TOP + Math.min(1, Math.max(0, rfidValue.mm / heightMm)) * STRIP_HEIGHT; + const rfidY = rfidLine !== null ? Math.min(105, Math.max(63, rfidLine)) : 102.5; + const waves = + rfidWritePower === undefined + ? 2 + : rfidWritePower === "H" + ? 3 + : rfidWritePower === "M" + ? 2 + : rfidWritePower === "L" + ? 1 + : rfidWritePower <= 10 + ? 1 + : rfidWritePower <= 20 + ? 2 + : 3; const axisMarkerActive = r === "originX" || r === "originY" || r === "top" || r === "shift"; return ( @@ -220,6 +264,40 @@ export function PrinterIllustration() { {/* ^LS lateral shift arrows */} + {/* RFID inlay + encoder waves, a state gadget like ^MM/^MT. The + wave count scales with the write power (L/M/H or thirds of 0-30). */} + {(rfidSet || rfidTabActive || r === "antenna") && ( + + + {waves >= 1 && } + {waves >= 2 && } + {waves >= 3 && } + {rfidWritePower !== undefined && ( + + {String(rfidWritePower)} + + )} + + )} + {/* ^RS programming position: encode line across the strip, placed + proportionally (absolute dots / F-mm over the label length; + B-forms sit at the leading edge). */} + {rfidLine !== null && ( + + + + {rfidPosition} + + + )} + {/* ^RS VOID handling: the failed label prints VOID on its way out */} + {(rfidVoid || rfidTabActive) && r === "exit" && ( + VOID + )} {/* media motion beside the strip: ^MF feeds forward, ^XB/~JS back */} diff --git a/src/components/PrinterSettings/zplFieldPrimitives.tsx b/src/components/PrinterSettings/zplFieldPrimitives.tsx index 1032c257..659ccf56 100644 --- a/src/components/PrinterSettings/zplFieldPrimitives.tsx +++ b/src/components/PrinterSettings/zplFieldPrimitives.tsx @@ -163,6 +163,7 @@ export function BoundedIntControl({ onChange, disabled, required, + ariaLabel, }: { id?: string; min: number; @@ -171,6 +172,8 @@ export function BoundedIntControl({ onChange: (next: number | undefined) => void; disabled?: boolean; required?: boolean; + /** For repeated controls that share one group label (e.g. EPC partitions). */ + ariaLabel?: string; }) { const externalText = value === undefined ? "" : String(value); const [draft, setDraft] = useState(externalText); @@ -183,6 +186,7 @@ export function BoundedIntControl({ { + const base = { widthMm: 50, heightMm: 30, dpmm: 8 } as LabelConfig; + + it("emits nothing when no RFID field is set", () => { + const zpl = generateZPL(base, []); + expect(zpl).not.toContain("^RS"); + expect(zpl).not.toContain("^RB"); + expect(zpl).not.toContain("^RW"); + }); + + it("round-trips the full ^RS slot set (a and c stay unmodelled)", () => { + const cfg = { + ...base, + rfidTagType: 8, rfidPosition: "F1", rfidVoidLength: 200, + rfidRetries: 5, rfidErrorHandling: "P" as const, rfidVoidSpeed: 4, + }; + const zpl = generateZPL(cfg, []); + expect(zpl).toContain("^RS8,F1,200,5,P,,,4"); + const back = parseZPL(zpl, 8).labelConfig; + expect(back.rfidTagType).toBe(8); + expect(back.rfidPosition).toBe("F1"); + expect(back.rfidVoidLength).toBe(200); + expect(back.rfidRetries).toBe(5); + expect(back.rfidErrorHandling).toBe("P"); + expect(back.rfidVoidSpeed).toBe(4); + }); + + it("trims trailing empty ^RS slots", () => { + expect(generateZPL({ ...base, rfidTagType: 8 }, [])).toContain("^RS8" + String.fromCharCode(10)); + expect(generateZPL({ ...base, rfidPosition: "520" }, [])).toContain("^RS,520" + String.fromCharCode(10)); + }); + + it("round-trips ^RB with partitions and drops a mismatched sum", () => { + const cfg = { ...base, rfidEpcBits: 96, rfidEpcPartitions: [8, 3, 3, 20, 24, 38] }; + const zpl = generateZPL(cfg, []); + expect(zpl).toContain("^RB96,8,3,3,20,24,38"); + const back = parseZPL(zpl, 8).labelConfig; + expect(back.rfidEpcBits).toBe(96); + expect(back.rfidEpcPartitions).toEqual([8, 3, 3, 20, 24, 38]); + // Partitions summing to 95 != 96 would encode wrong fields: drop whole cmd. + const badParse = parseZPL("^XA^RB96,8,3,3,20,24,37^XZ", 8); + expect(badParse.labelConfig.rfidEpcBits).toBeUndefined(); + expect(badParse.labelConfig.rfidEpcPartitions).toBeUndefined(); + // The drop is reported, never silent. + expect( + badParse.pages[0]?.findings.filter((f) => f.kind === "partial").map((f) => f.command), + ).toContain("^RB"); + }); + + it("reports a gap inside the ^RB partition list instead of closing it", () => { + const r = parseZPL("^XA^RB96,8,,24,64^XZ", 8); + expect(r.labelConfig.rfidEpcPartitions).toBeUndefined(); + expect( + r.pages[0]?.findings.filter((f) => f.kind === "partial").map((f) => f.command), + ).toContain("^RB"); + // A trailing delimiter is only noise, so that list still adopts. + expect(parseZPL("^XA^RB96,8,24,64,^XZ", 8).labelConfig.rfidEpcPartitions).toEqual([8, 24, 64]); + }); + + it("round-trips ^RW across the numeric/level power union", () => { + expect(generateZPL({ ...base, rfidReadPower: 16, rfidWritePower: "L" }, [])).toContain("^RW16,L"); + const back = parseZPL("^XA^RWH,30^XZ", 8).labelConfig; + expect(back.rfidReadPower).toBe("H"); + expect(back.rfidWritePower).toBe(30); + }); + + it("flags the unmodelled ^RS a/c and ^RW antenna slots as partial", () => { + const r = parseZPL("^XA^RS8,,,,,A2^RW16,16,A3^XZ", 8); + const partial = r.pages[0]?.findings.filter((f) => f.kind === "partial").map((f) => f.command); + expect(partial).toContain("^RS"); + expect(partial).toContain("^RW"); + // The modelled slots still adopt. + expect(r.labelConfig.rfidTagType).toBe(8); + expect(r.labelConfig.rfidReadPower).toBe(16); + }); + + it("flags a legacy tag type as partial and keeps the other slots", () => { + const r = parseZPL("^XA^RS1,520^XZ", 8); + expect(r.labelConfig.rfidTagType).toBeUndefined(); + expect(r.labelConfig.rfidPosition).toBe("520"); + const partial = r.pages[0]?.findings.filter((f) => f.kind === "partial").map((f) => f.command); + expect(partial).toContain("^RS"); + }); + + it("adopts a partition-less ^RB beyond the 16x64 form", () => { + expect(parseZPL("^XA^RB2048^XZ", 8).labelConfig.rfidEpcBits).toBe(2048); + }); + + it("bounds the forward position at F999 per the Link-OS spec", () => { + expect(parseZPL("^XA^RS8,F999^XZ", 8).labelConfig.rfidPosition).toBe("F999"); + expect(parseZPL("^XA^RS8,F1000^XZ", 8).labelConfig.rfidPosition).toBeUndefined(); + }); + + it("reads the VOID length through the ^MU multiplier like ^ML", () => { + // ^MUI: 2 inches = 406 dots at 8 dpmm (203 dpi). + expect(parseZPL("^XA^MUI^RS8,,2^XZ", 8).labelConfig.rfidVoidLength).toBe(406); + }); + + it("ignores invalid RFID slot values", () => { + const back = parseZPL("^XA^RS9,X99,-1,11,Q^RW31,Z^XZ", 8).labelConfig; + expect(back.rfidTagType).toBeUndefined(); + expect(back.rfidPosition).toBeUndefined(); + expect(back.rfidVoidLength).toBeUndefined(); + expect(back.rfidRetries).toBeUndefined(); + expect(back.rfidErrorHandling).toBeUndefined(); + expect(back.rfidReadPower).toBeUndefined(); + expect(back.rfidWritePower).toBeUndefined(); + }); +}); diff --git a/src/locales/ar.ts b/src/locales/ar.ts index 78f1b4bf..819842e6 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -481,6 +481,7 @@ const ar = { dotsUnit: 'نقاط', tabs: { mediaFeed: 'الوسائط والتغذية', + rfid: 'RFID', appSettings: 'التطبيق', previewSettings: 'معاينة', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const ar = { maintenance: 'Maintenance', }, railGroupPerLabel: 'لكل ملصق', + rfid: { + specOnlyHint: 'يتطلب ترميز RFID طابعة من سلسلة R.', + encodingHeading: 'الترميز', + failureHeading: 'عند فشل الترميز', + tagTypeGen2: 'تحديد نوع الوسم Gen 2', + position: 'موضع البرمجة', + positionModeForward: 'للأمام من الحافة', + positionModeBackfeed: 'سحب للخلف قبل الحافة', + positionModeAbsolute: 'مطلق من الأعلى', + positionForwardHint: 'تطبع الطابعة حتى هذه المسافة، ثم ترمّز هناك، ثم تطبع الباقي.', + positionBackfeedHint: 'تسحب الطابعة الوسائط للخلف بهذا القدر قبل الترميز؛ يتطلب وجود بطانة فارغة في المقدمة.', + positionAbsoluteHint: 'تُحرّك الطابعة الوسائط إلى صف النقاط هذا من أعلى الملصق قبل الترميز.', + mmUnit: 'مم', + pickOnLabel: 'تحديد على الملصق…', + pickHint: 'انقر على الملصق أو اسحب الدليل لتحديد موضع البرمجة: نقطة توقف الوسائط للترميز، وليس موضع الرقاقة (inlay).', + pickDone: 'تم', + voidLength: 'طول طباعة VOID', + retries: 'محاولات الترميز لكل ملصق', + errorHandling: 'بعد فشل المحاولات', + errorHandlingN: 'إسقاط التنسيق والمتابعة', + errorHandlingP: 'إيقاف الطابعة مؤقتًا', + errorHandlingE: 'وضع الخطأ', + voidSpeed: 'سرعة طباعة VOID', + epcHeading: 'بنية بيانات EPC', + epcBits: 'إجمالي البتات', + epcPartitions: 'أحجام الأقسام', + epcHint: 'حتى 16 قسمًا مفصولًا بفواصل من 1-64 بت؛ يجب أن يكون مجموعها مساويًا للإجمالي.', + epcPresetSgtin: 'استخدام تخطيط SGTIN-96', + epcAddPartition: 'إضافة تقسيم', + epcRemovePartition: 'إزالة التقسيم', + epcClearPartitions: 'مسح جميع التقسيمات', + epcEmpty: 'لا يوجد هيكل بعد: حدد عدد البتات أو ابدأ بتخطيط SGTIN-96.', + powerHeading: 'طاقة القراءة / الكتابة', + readPower: 'قراءة', + writePower: 'كتابة', + powerHint: '0-30، أو H / M / L في البرامج الثابتة الأقدم.', + }, resetPerLabel: 'إعادة الضبط إلى إعدادات الطابعة الافتراضية', resetPerLabelConfirm: 'إعادة الضبط فعلاً؟', railGroupApp: 'التطبيق', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 2712af3f..3623d5b5 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -481,6 +481,7 @@ const bg = { dotsUnit: 'точки', tabs: { mediaFeed: 'Носител и подаване', + rfid: 'RFID', appSettings: 'Приложение', previewSettings: 'Преглед', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const bg = { maintenance: 'Maintenance', }, railGroupPerLabel: 'На етикет', + rfid: { + specOnlyHint: 'Кодирането на RFID изисква принтер от серия R.', + encodingHeading: 'Кодиране', + failureHeading: 'При неуспешно кодиране', + tagTypeGen2: 'Деклариране на тип тагове Gen 2', + position: 'Позиция на програмиране', + positionModeForward: 'Напред от края', + positionModeBackfeed: 'Обратно подаване преди края', + positionModeAbsolute: 'Абсолютно от горния край', + positionForwardHint: 'Принтерът отпечатва до това разстояние, кодира там, след което отпечатва останалото.', + positionBackfeedHint: 'Принтерът дърпа лентата назад с това разстояние преди кодиране; изисква празна подложка отпред.', + positionAbsoluteHint: 'Принтерът премества лентата до този ред точки от горния край на етикета преди кодиране.', + mmUnit: 'мм', + pickOnLabel: 'Задаване върху етикета…', + pickHint: 'Щракнете върху етикета или плъзнете направляващата линия, за да зададете позицията за програмиране: къде спира лентата за кодиране, не къде се намира инлеят (inlay).', + pickDone: 'Готово', + voidLength: 'Дължина на разпечатката VOID', + retries: 'Опити за кодиране на етикет', + errorHandling: 'След неуспешни опити', + errorHandlingN: 'Изхвърляне на формата, продължаване', + errorHandlingP: 'Пауза на принтера', + errorHandlingE: 'Режим на грешка', + voidSpeed: 'Скорост на печат VOID', + epcHeading: 'Структура на данните EPC', + epcBits: 'Общо битове', + epcPartitions: 'Размери на дяловете', + epcHint: 'До 16 дяла, разделени със запетая, от 1-64 бита; сумата им трябва да е равна на общия брой.', + epcPresetSgtin: 'Използване на оформление SGTIN-96', + epcAddPartition: 'Добавяне на дял', + epcRemovePartition: 'Премахване на дял', + epcClearPartitions: 'Изчистване на всички дялове', + epcEmpty: 'Все още няма структура: задайте брой битове или започнете с SGTIN-96.', + powerHeading: 'Мощност за четене/запис', + readPower: 'Четене', + writePower: 'Запис', + powerHint: '0-30, или H / M / L при по-стар фърмуер.', + }, resetPerLabel: 'Нулиране до подразбиращите настройки на принтера', resetPerLabelConfirm: 'Наистина ли да се нулира?', railGroupApp: 'Приложение', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 0a2468ad..185ca246 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -481,6 +481,7 @@ const cs = { dotsUnit: 'body', tabs: { mediaFeed: 'Média a posuv', + rfid: 'RFID', appSettings: 'Aplikace', previewSettings: 'Náhled', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const cs = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Na štítek', + rfid: { + specOnlyHint: 'Kódování RFID vyžaduje tiskárnu řady R.', + encodingHeading: 'Kódování', + failureHeading: 'Při selhání kódování', + tagTypeGen2: 'Deklarovat typ tagu Gen 2', + position: 'Pozice programování', + positionModeForward: 'Vpřed od hrany', + positionModeBackfeed: 'Zpětný posun před hranou', + positionModeAbsolute: 'Absolutně od horního okraje', + positionForwardHint: 'Tiskárna tiskne až do této vzdálenosti, tam zakóduje a poté vytiskne zbytek.', + positionBackfeedHint: 'Tiskárna posune médium zpět o tuto vzdálenost před kódováním; vyžaduje prázdný podklad vpředu.', + positionAbsoluteHint: 'Tiskárna posune médium na tuto řadu bodů od horního okraje etikety před kódováním.', + mmUnit: 'mm', + pickOnLabel: 'Nastavit na štítku…', + pickHint: 'Klikněte na štítek nebo přetáhněte vodicí čáru a nastavte programovací pozici: kde se médium zastaví pro kódování, nikoli kde leží inlay.', + pickDone: 'Hotovo', + voidLength: 'Délka výtisku VOID', + retries: 'Počet pokusů o kódování na etiketu', + errorHandling: 'Po neúspěšných pokusech', + errorHandlingN: 'Zahodit formát, pokračovat', + errorHandlingP: 'Pozastavit tiskárnu', + errorHandlingE: 'Chybový režim', + voidSpeed: 'Rychlost tisku VOID', + epcHeading: 'Struktura dat EPC', + epcBits: 'Celkem bitů', + epcPartitions: 'Velikosti oddílů', + epcHint: 'Až 16 oddílů oddělených čárkou o 1-64 bitech; jejich součet musí odpovídat celkovému počtu.', + epcPresetSgtin: 'Použít rozvržení SGTIN-96', + epcAddPartition: 'Přidat oddíl', + epcRemovePartition: 'Odebrat oddíl', + epcClearPartitions: 'Vymazat všechny oddíly', + epcEmpty: 'Zatím žádná struktura: zadejte počet bitů nebo začněte s SGTIN-96.', + powerHeading: 'Výkon čtení/zápisu', + readPower: 'Čtení', + writePower: 'Zápis', + powerHint: '0-30, nebo H / M / L u starších firmwarů.', + }, resetPerLabel: 'Obnovit výchozí nastavení tiskárny', resetPerLabelConfirm: 'Opravdu resetovat?', railGroupApp: 'Aplikace', diff --git a/src/locales/da.ts b/src/locales/da.ts index 9e20144a..cf68680e 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -481,6 +481,7 @@ const da = { dotsUnit: 'dots', tabs: { mediaFeed: 'Medie og fremføring', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Forhåndsvisning', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const da = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Pr. etiket', + rfid: { + specOnlyHint: 'RFID-kodning kræver en printer i R-serien.', + encodingHeading: 'Kodning', + failureHeading: 'Hvis kodning mislykkes', + tagTypeGen2: 'Erklær Gen 2-tagtype', + position: 'Programmeringsposition', + positionModeForward: 'Fremad fra kanten', + positionModeBackfeed: 'Tilbagetræk før kanten', + positionModeAbsolute: 'Absolut fra toppen', + positionForwardHint: 'Printeren printer op til denne afstand, koder der og printer derefter resten.', + positionBackfeedHint: 'Printeren trækker medieremmen tilbage så langt før kodning; kræver tom liner foran.', + positionAbsoluteHint: 'Printeren flytter medieremmen til denne punktlinje fra etikettens top før kodning.', + mmUnit: 'mm', + pickOnLabel: 'Angiv på etiketten…', + pickHint: 'Klik på etiketten, eller træk guiden for at angive programmeringspositionen: hvor medieremmen stopper for kodning, ikke hvor inlayet sidder.', + pickDone: 'Udført', + voidLength: 'VOID-udskriftslængde', + retries: 'Kodningsforsøg pr. etiket', + errorHandling: 'Efter mislykkede forsøg', + errorHandlingN: 'Kassér formatet, fortsæt', + errorHandlingP: 'Sæt printeren på pause', + errorHandlingE: 'Fejltilstand', + voidSpeed: 'VOID-udskriftshastighed', + epcHeading: 'EPC-datastruktur', + epcBits: 'Bits i alt', + epcPartitions: 'Partitionsstørrelser', + epcHint: 'Op til 16 kommaseparerede partitioner på 1-64 bit; de skal summere til totalen.', + epcPresetSgtin: 'Brug SGTIN-96-layoutet', + epcAddPartition: 'Tilføj partition', + epcRemovePartition: 'Fjern partition', + epcClearPartitions: 'Ryd alle partitioner', + epcEmpty: 'Ingen struktur endnu: angiv et bitantal, eller start med SGTIN-96.', + powerHeading: 'Læse-/skriveeffekt', + readPower: 'Læs', + writePower: 'Skriv', + powerHint: '0-30, eller H / M / L på ældre firmware.', + }, resetPerLabel: 'Nulstil til printerens standardindstillinger', resetPerLabelConfirm: 'Virkelig nulstille?', railGroupApp: 'App', diff --git a/src/locales/de.ts b/src/locales/de.ts index f2ce8148..648cc69e 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -481,6 +481,7 @@ const de = { dotsUnit: 'Punkte', tabs: { mediaFeed: 'Medien & Vorschub', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Vorschau', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const de = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Pro Etikett', + rfid: { + specOnlyHint: 'RFID-Kodierung erfordert einen Drucker der R-Serie.', + encodingHeading: 'Kodierung', + failureHeading: 'Bei fehlgeschlagener Kodierung', + tagTypeGen2: 'Gen-2-Tag-Typ deklarieren', + position: 'Programmierposition', + positionModeForward: 'Vorwärts ab der Kante', + positionModeBackfeed: 'Rückzug vor der Kante', + positionModeAbsolute: 'Absolut ab der Oberkante', + positionForwardHint: 'Der Drucker druckt bis zu dieser Distanz, kodiert dort und druckt dann den Rest.', + positionBackfeedHint: 'Der Drucker zieht das Medium um diese Strecke zurück, bevor kodiert wird; erfordert leeren Liner davor.', + positionAbsoluteHint: 'Der Drucker bewegt das Medium vor dem Kodieren zu dieser Punktzeile ab der Etikettenoberkante.', + mmUnit: 'mm', + pickOnLabel: 'Auf dem Etikett festlegen…', + pickHint: 'Klicke auf das Etikett oder ziehe die Hilfslinie, um die Programmierposition festzulegen: wo das Medium für die Kodierung anhält, nicht wo das Inlay sitzt.', + pickDone: 'Fertig', + voidLength: 'VOID-Ausdrucklänge', + retries: 'Kodierversuche pro Etikett', + errorHandling: 'Nach fehlgeschlagenen Versuchen', + errorHandlingN: 'Format verwerfen, fortfahren', + errorHandlingP: 'Drucker anhalten', + errorHandlingE: 'Fehlermodus', + voidSpeed: 'VOID-Druckgeschwindigkeit', + epcHeading: 'EPC-Datenstruktur', + epcBits: 'Bits gesamt', + epcPartitions: 'Partitionsgrößen', + epcHint: 'Bis zu 16 kommagetrennte Partitionen von 1-64 Bit; die Summe muss der Gesamtzahl entsprechen.', + epcPresetSgtin: 'SGTIN-96-Aufteilung verwenden', + epcAddPartition: 'Partition hinzufügen', + epcRemovePartition: 'Partition entfernen', + epcClearPartitions: 'Alle Partitionen löschen', + epcEmpty: 'Noch keine Struktur: Bitanzahl festlegen oder mit SGTIN-96 starten.', + powerHeading: 'Lese-/Schreibleistung', + readPower: 'Lesen', + writePower: 'Schreiben', + powerHint: '0-30, oder H / M / L bei älterer Firmware.', + }, resetPerLabel: 'Auf Druckerstandard zurücksetzen', resetPerLabelConfirm: 'Wirklich zurücksetzen?', railGroupApp: 'App', diff --git a/src/locales/el.ts b/src/locales/el.ts index 91b2f349..ac0fd17a 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -481,6 +481,7 @@ const el = { dotsUnit: 'κουκκίδες', tabs: { mediaFeed: 'Μέσα και τροφοδοσία', + rfid: 'RFID', appSettings: 'Εφαρμογή', previewSettings: 'Προεπισκόπηση', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const el = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Ανά ετικέτα', + rfid: { + specOnlyHint: 'Η κωδικοποίηση RFID απαιτεί εκτυπωτή της σειράς R.', + encodingHeading: 'Κωδικοποίηση', + failureHeading: 'Σε αποτυχία κωδικοποίησης', + tagTypeGen2: 'Δήλωση τύπου ετικέτας Gen 2', + position: 'Θέση προγραμματισμού', + positionModeForward: 'Εμπρός από το άκρο', + positionModeBackfeed: 'Οπισθοτροφοδοσία πριν το άκρο', + positionModeAbsolute: 'Απόλυτη θέση από την κορυφή', + positionForwardHint: 'Ο εκτυπωτής εκτυπώνει έως αυτή την απόσταση, κωδικοποιεί εκεί και μετά εκτυπώνει το υπόλοιπο.', + positionBackfeedHint: 'Ο εκτυπωτής τραβά το μέσο προς τα πίσω κατά αυτή την απόσταση πριν την κωδικοποίηση· απαιτεί κενό υπόστρωμα μπροστά.', + positionAbsoluteHint: 'Ο εκτυπωτής μετακινεί το μέσο σε αυτή τη γραμμή κουκκίδων από την κορυφή της ετικέτας πριν την κωδικοποίηση.', + mmUnit: 'mm', + pickOnLabel: 'Ορισμός στην ετικέτα…', + pickHint: 'Κάντε κλικ στην ετικέτα ή σύρετε τον οδηγό για να ορίσετε τη θέση προγραμματισμού: πού σταματά το μέσο για κωδικοποίηση, όχι πού βρίσκεται το inlay.', + pickDone: 'Τέλος', + voidLength: 'Μήκος εκτύπωσης VOID', + retries: 'Επαναλήψεις κωδικοποίησης ανά ετικέτα', + errorHandling: 'Μετά από αποτυχημένες προσπάθειες', + errorHandlingN: 'Απόρριψη της μορφής, συνέχεια', + errorHandlingP: 'Παύση εκτυπωτή', + errorHandlingE: 'Λειτουργία σφάλματος', + voidSpeed: 'Ταχύτητα εκτύπωσης VOID', + epcHeading: 'Δομή δεδομένων EPC', + epcBits: 'Σύνολο bit', + epcPartitions: 'Μεγέθη διαμερισμάτων', + epcHint: 'Έως 16 διαμερίσματα χωρισμένα με κόμμα των 1-64 bit· το άθροισμά τους πρέπει να ισούται με το σύνολο.', + epcPresetSgtin: 'Χρήση διάταξης SGTIN-96', + epcAddPartition: 'Προσθήκη διαμερίσματος', + epcRemovePartition: 'Κατάργηση διαμερίσματος', + epcClearPartitions: 'Εκκαθάριση όλων των διαμερισμάτων', + epcEmpty: 'Δεν υπάρχει ακόμη δομή: ορίστε αριθμό bit ή ξεκινήστε με SGTIN-96.', + powerHeading: 'Ισχύς ανάγνωσης/εγγραφής', + readPower: 'Ανάγνωση', + writePower: 'Εγγραφή', + powerHint: '0-30, ή H / M / L σε παλαιότερο firmware.', + }, resetPerLabel: 'Επαναφορά στις προεπιλογές του εκτυπωτή', resetPerLabelConfirm: 'Πραγματικά επαναφορά;', railGroupApp: 'Εφαρμογή', diff --git a/src/locales/en.ts b/src/locales/en.ts index b51b2491..89f476f1 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -481,6 +481,7 @@ const en = { dotsUnit: 'dots', tabs: { mediaFeed: 'Media & Feed', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Preview', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const en = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per Label', + rfid: { + specOnlyHint: 'RFID encoding needs an R-series printer.', + encodingHeading: 'Encoding', + failureHeading: 'If encoding fails', + tagTypeGen2: 'Declare Gen 2 tag type', + position: 'Programming position', + positionModeForward: 'Forward from the edge', + positionModeBackfeed: 'Backfeed before the edge', + positionModeAbsolute: 'Absolute from the top', + positionForwardHint: 'The printer prints up to this distance, encodes there, then prints the rest.', + positionBackfeedHint: 'The printer pulls the media back this far before encoding; needs empty liner in front.', + positionAbsoluteHint: 'The printer moves the media to this dot row from the label top before encoding.', + mmUnit: 'mm', + pickOnLabel: 'Set on the label…', + pickHint: 'Click the label or drag the guide to set the programming position: where the media stops for encoding, not where the inlay sits.', + pickDone: 'Done', + voidLength: 'VOID printout length', + retries: 'Encode retries per label', + errorHandling: 'After failed retries', + errorHandlingN: 'Drop the format, continue', + errorHandlingP: 'Pause the printer', + errorHandlingE: 'Error mode', + voidSpeed: 'VOID print speed', + epcHeading: 'EPC data structure', + epcBits: 'Total bits', + epcPartitions: 'Partition sizes', + epcHint: 'Up to 16 comma-separated partitions of 1-64 bits; they must sum to the total.', + epcPresetSgtin: 'Use the SGTIN-96 layout', + epcAddPartition: 'Add partition', + epcRemovePartition: 'Remove partition', + epcClearPartitions: 'Clear all partitions', + epcEmpty: 'No structure yet: set a bit count or start from SGTIN-96.', + powerHeading: 'Read / write power', + readPower: 'Read', + writePower: 'Write', + powerHint: '0-30, or H / M / L on older firmware.', + }, resetPerLabel: 'Reset to printer defaults', resetPerLabelConfirm: 'Really reset?', railGroupApp: 'App', diff --git a/src/locales/es.ts b/src/locales/es.ts index 1d3f0ef3..c966f309 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -481,6 +481,7 @@ const es = { dotsUnit: 'puntos', tabs: { mediaFeed: 'Medio y avance', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Vista previa', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const es = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Por etiqueta', + rfid: { + specOnlyHint: 'La codificación RFID requiere una impresora de la serie R.', + encodingHeading: 'Codificación', + failureHeading: 'Si falla la codificación', + tagTypeGen2: 'Declarar tipo de etiqueta Gen 2', + position: 'Posición de programación', + positionModeForward: 'Hacia delante desde el borde', + positionModeBackfeed: 'Retroceso antes del borde', + positionModeAbsolute: 'Absoluta desde arriba', + positionForwardHint: 'La impresora imprime hasta esta distancia, codifica ahí y luego imprime el resto.', + positionBackfeedHint: 'La impresora retrae el medio esta distancia antes de codificar; requiere liner vacío por delante.', + positionAbsoluteHint: 'La impresora mueve el medio hasta esta fila de puntos desde la parte superior de la etiqueta antes de codificar.', + mmUnit: 'mm', + pickOnLabel: 'Definir en la etiqueta…', + pickHint: 'Haz clic en la etiqueta o arrastra la guía para fijar la posición de programación: dónde se detiene el medio para la codificación, no dónde está el inlay.', + pickDone: 'Listo', + voidLength: 'Longitud de impresión VOID', + retries: 'Reintentos de codificación por etiqueta', + errorHandling: 'Tras reintentos fallidos', + errorHandlingN: 'Descartar el formato, continuar', + errorHandlingP: 'Pausar la impresora', + errorHandlingE: 'Modo de error', + voidSpeed: 'Velocidad de impresión VOID', + epcHeading: 'Estructura de datos EPC', + epcBits: 'Bits totales', + epcPartitions: 'Tamaños de partición', + epcHint: 'Hasta 16 particiones separadas por comas de 1-64 bits; deben sumar el total.', + epcPresetSgtin: 'Usar el diseño SGTIN-96', + epcAddPartition: 'Añadir partición', + epcRemovePartition: 'Quitar partición', + epcClearPartitions: 'Borrar todas las particiones', + epcEmpty: 'Aún no hay estructura: indique un número de bits o empiece con SGTIN-96.', + powerHeading: 'Potencia de lectura/escritura', + readPower: 'Lectura', + writePower: 'Escritura', + powerHint: '0-30, o H / M / L en firmware antiguo.', + }, resetPerLabel: 'Restablecer valores predeterminados de la impresora', resetPerLabelConfirm: '¿Restablecer de verdad?', railGroupApp: 'App', diff --git a/src/locales/et.ts b/src/locales/et.ts index bab7db40..c31e6b3d 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -481,6 +481,7 @@ const et = { dotsUnit: 'punkti', tabs: { mediaFeed: 'Kandja ja söötmine', + rfid: 'RFID', appSettings: 'Rakendus', previewSettings: 'Eelvaade', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const et = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Sildi kohta', + rfid: { + specOnlyHint: 'RFID-kodeerimine nõuab R-seeria printerit.', + encodingHeading: 'Kodeerimine', + failureHeading: 'Kodeerimise nurjumisel', + tagTypeGen2: 'Deklareeri Gen 2 sildi tüüp', + position: 'Programmeerimisasukoht', + positionModeForward: 'Edasi servast', + positionModeBackfeed: 'Tagasisöötmine enne serva', + positionModeAbsolute: 'Absoluutne ülaservast', + positionForwardHint: 'Printer prindib selle vahemaani, kodeerib seal ja prindib seejärel ülejäänu.', + positionBackfeedHint: 'Printer tõmbab meediat enne kodeerimist selle vahemaa võrra tagasi; vajab eesosas tühja alusribat.', + positionAbsoluteHint: 'Printer liigutab meedia enne kodeerimist sellele punktireale sildi ülaservast.', + mmUnit: 'mm', + pickOnLabel: 'Määra etiketil…', + pickHint: 'Klõpsa etiketil või lohista joont, et määrata programmeerimise asukoht: kus meedia kodeerimiseks peatub, mitte kus asub inlay.', + pickDone: 'Valmis', + voidLength: 'VOID väljatrüki pikkus', + retries: 'Kodeerimiskatsed sildi kohta', + errorHandling: 'Pärast ebaõnnestunud katseid', + errorHandlingN: 'Loobu vormingust, jätka', + errorHandlingP: 'Peata printer', + errorHandlingE: 'Vearežiim', + voidSpeed: 'VOID trükkimiskiirus', + epcHeading: 'EPC andmestruktuur', + epcBits: 'Bitte kokku', + epcPartitions: 'Partitsioonide suurused', + epcHint: 'Kuni 16 komaga eraldatud partitsiooni suurusega 1-64 bitti; nende summa peab võrduma kogusummaga.', + epcPresetSgtin: 'Kasuta SGTIN-96 paigutust', + epcAddPartition: 'Lisa partitsioon', + epcRemovePartition: 'Eemalda partitsioon', + epcClearPartitions: 'Tühjenda kõik partitsioonid', + epcEmpty: 'Struktuuri veel pole: määra bittide arv või alusta mallist SGTIN-96.', + powerHeading: 'Lugemis-/kirjutusvõimsus', + readPower: 'Lugemine', + writePower: 'Kirjutamine', + powerHint: '0-30 või H / M / L vanemal püsivaral.', + }, resetPerLabel: 'Lähtesta printeri vaikeseadetele', resetPerLabelConfirm: 'Kas tõesti lähtestada?', railGroupApp: 'Rakendus', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index bf284cc2..880ecfeb 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -481,6 +481,7 @@ const fa = { dotsUnit: 'نقطه', tabs: { mediaFeed: 'رسانه و تغذیه', + rfid: 'RFID', appSettings: 'برنامه', previewSettings: 'پیش‌نمایش', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const fa = { maintenance: 'Maintenance', }, railGroupPerLabel: 'برای هر برچسب', + rfid: { + specOnlyHint: 'رمزگذاری RFID به چاپگری از سری R نیاز دارد.', + encodingHeading: 'کدگذاری', + failureHeading: 'در صورت شکست کدگذاری', + tagTypeGen2: 'اعلام نوع تگ Gen 2', + position: 'موقعیت برنامه‌نویسی', + positionModeForward: 'جلو از لبه', + positionModeBackfeed: 'عقب‌کشی پیش از لبه', + positionModeAbsolute: 'مطلق از بالا', + positionForwardHint: 'چاپگر تا این فاصله چاپ می‌کند، در آنجا رمزگذاری می‌کند و سپس بقیه را چاپ می‌کند.', + positionBackfeedHint: 'چاپگر رسانه را پیش از رمزگذاری به این میزان به عقب می‌کشد؛ به لاینر خالی در جلو نیاز دارد.', + positionAbsoluteHint: 'چاپگر پیش از رمزگذاری رسانه را به این ردیف نقطه از بالای برچسب می‌برد.', + mmUnit: 'میلی‌متر', + pickOnLabel: 'تنظیم روی برچسب…', + pickHint: 'روی برچسب کلیک کنید یا راهنما را بکشید تا موقعیت برنامه‌ریزی را تنظیم کنید: جایی که رسانه برای رمزگذاری متوقف می‌شود، نه جایی که این‌له (inlay) قرار دارد.', + pickDone: 'انجام شد', + voidLength: 'طول چاپ VOID', + retries: 'تلاش‌های رمزگذاری برای هر برچسب', + errorHandling: 'پس از تلاش‌های ناموفق', + errorHandlingN: 'رها کردن قالب، ادامه', + errorHandlingP: 'توقف چاپگر', + errorHandlingE: 'حالت خطا', + voidSpeed: 'سرعت چاپ VOID', + epcHeading: 'ساختار داده EPC', + epcBits: 'مجموع بیت‌ها', + epcPartitions: 'اندازه پارتیشن‌ها', + epcHint: 'حداکثر ۱۶ پارتیشن جدا شده با کاما بین ۱ تا ۶۴ بیت؛ مجموع آن‌ها باید برابر کل باشد.', + epcPresetSgtin: 'استفاده از چیدمان SGTIN-96', + epcAddPartition: 'افزودن پارتیشن', + epcRemovePartition: 'حذف پارتیشن', + epcClearPartitions: 'پاک کردن همه پارتیشن‌ها', + epcEmpty: 'هنوز ساختاری تعریف نشده: تعداد بیت‌ها را تنظیم کنید یا از SGTIN-96 شروع کنید.', + powerHeading: 'توان خواندن/نوشتن', + readPower: 'خواندن', + writePower: 'نوشتن', + powerHint: '۰ تا ۳۰، یا H / M / L در فریمورهای قدیمی‌تر.', + }, resetPerLabel: 'بازنشانی به پیش‌فرض‌های چاپگر', resetPerLabelConfirm: 'واقعاً بازنشانی شود؟', railGroupApp: 'برنامه', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 982d5ed3..c0099114 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -481,6 +481,7 @@ const fi = { dotsUnit: 'pistettä', tabs: { mediaFeed: 'Materiaali ja syöttö', + rfid: 'RFID', appSettings: 'Sovellus', previewSettings: 'Esikatselu', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const fi = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etiketti', + rfid: { + specOnlyHint: 'RFID-koodaus edellyttää R-sarjan tulostinta.', + encodingHeading: 'Koodaus', + failureHeading: 'Jos koodaus epäonnistuu', + tagTypeGen2: 'Ilmoita Gen 2 -tunnistetyyppi', + position: 'Ohjelmointikohta', + positionModeForward: 'Eteenpäin reunasta', + positionModeBackfeed: 'Takaisinsyöttö ennen reunaa', + positionModeAbsolute: 'Absoluuttinen ylhäältä', + positionForwardHint: 'Tulostin tulostaa tähän etäisyyteen asti, koodaa siinä ja tulostaa sitten lopun.', + positionBackfeedHint: 'Tulostin vetää tarraa taaksepäin tämän verran ennen koodausta; edessä tarvitaan tyhjää pohjapaperia.', + positionAbsoluteHint: 'Tulostin siirtää tarran tälle pisteriville etiketin yläreunasta ennen koodausta.', + mmUnit: 'mm', + pickOnLabel: 'Aseta tarraan…', + pickHint: 'Napsauta tarraa tai vedä opasta asettaaksesi ohjelmointikohdan: mihin tarraraina pysähtyy koodausta varten, ei inlayn sijaintia.', + pickDone: 'Valmis', + voidLength: 'VOID-tulosteen pituus', + retries: 'Koodausyritykset per etiketti', + errorHandling: 'Epäonnistuneiden yritysten jälkeen', + errorHandlingN: 'Hylkää formaatti, jatka', + errorHandlingP: 'Keskeytä tulostin', + errorHandlingE: 'Virhetila', + voidSpeed: 'VOID-tulostusnopeus', + epcHeading: 'EPC-tietorakenne', + epcBits: 'Bittejä yhteensä', + epcPartitions: 'Osioiden koot', + epcHint: 'Enintään 16 pilkuin eroteltua osiota, kukin 1-64 bittiä; niiden summan on vastattava kokonaismäärää.', + epcPresetSgtin: 'Käytä SGTIN-96-asettelua', + epcAddPartition: 'Lisää osio', + epcRemovePartition: 'Poista osio', + epcClearPartitions: 'Tyhjennä kaikki osiot', + epcEmpty: 'Ei vielä rakennetta: aseta bittimäärä tai aloita mallista SGTIN-96.', + powerHeading: 'Luku-/kirjoitusteho', + readPower: 'Luku', + writePower: 'Kirjoitus', + powerHint: '0-30, tai H / M / L vanhemmassa laiteohjelmistossa.', + }, resetPerLabel: 'Palauta tulostimen oletusasetukset', resetPerLabelConfirm: 'Nollataanko todella?', railGroupApp: 'Sovellus', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 0173ba7e..f14cc4f2 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -481,6 +481,7 @@ const fr = { dotsUnit: 'points', tabs: { mediaFeed: 'Support et avance', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Aperçu', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const fr = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Par étiquette', + rfid: { + specOnlyHint: 'Le codage RFID nécessite une imprimante de la série R.', + encodingHeading: 'Encodage', + failureHeading: "En cas d'échec de l'encodage", + tagTypeGen2: 'Déclarer le type de tag Gen 2', + position: 'Position de programmation', + positionModeForward: "Vers l'avant depuis le bord", + positionModeBackfeed: 'Recul avant le bord', + positionModeAbsolute: 'Absolu depuis le haut', + positionForwardHint: "L'imprimante imprime jusqu'à cette distance, encode à cet endroit, puis imprime le reste.", + positionBackfeedHint: "L'imprimante recule le support de cette distance avant l'encodage ; nécessite un support vide à l'avant.", + positionAbsoluteHint: "L'imprimante déplace le support jusqu'à cette ligne de points depuis le haut de l'étiquette avant l'encodage.", + mmUnit: 'mm', + pickOnLabel: "Définir sur l'étiquette…", + pickHint: "Cliquez sur l'étiquette ou faites glisser le guide pour définir la position de programmation : où le support s'arrête pour l'encodage, pas où se trouve l'inlay.", + pickDone: 'Terminé', + voidLength: "Longueur d'impression VOID", + retries: 'Tentatives de codage par étiquette', + errorHandling: 'Après échec des tentatives', + errorHandlingN: 'Abandonner le format, continuer', + errorHandlingP: "Mettre l'imprimante en pause", + errorHandlingE: 'Mode erreur', + voidSpeed: "Vitesse d'impression VOID", + epcHeading: 'Structure des données EPC', + epcBits: 'Bits au total', + epcPartitions: 'Tailles des partitions', + epcHint: "Jusqu'à 16 partitions séparées par des virgules de 1 à 64 bits ; leur somme doit correspondre au total.", + epcPresetSgtin: 'Utiliser la disposition SGTIN-96', + epcAddPartition: 'Ajouter une partition', + epcRemovePartition: 'Supprimer la partition', + epcClearPartitions: 'Effacer toutes les partitions', + epcEmpty: "Aucune structure pour l'instant : indiquez un nombre de bits ou partez du modèle SGTIN-96.", + powerHeading: 'Puissance lecture / écriture', + readPower: 'Lecture', + writePower: 'Écriture', + powerHint: '0-30, ou H / M / L sur les firmwares plus anciens.', + }, resetPerLabel: "Réinitialiser aux valeurs de l'imprimante", resetPerLabelConfirm: 'Vraiment réinitialiser ?', railGroupApp: 'App', diff --git a/src/locales/he.ts b/src/locales/he.ts index 4f1d9ca3..ea1d42b8 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -481,6 +481,7 @@ const he = { dotsUnit: 'נקודות', tabs: { mediaFeed: 'מדיה והזנה', + rfid: 'RFID', appSettings: 'אפליקציה', previewSettings: 'תצוגה מקדימה', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const he = { maintenance: 'Maintenance', }, railGroupPerLabel: 'לכל מדבקה', + rfid: { + specOnlyHint: 'קידוד RFID דורש מדפסת מסדרת R.', + encodingHeading: 'קידוד', + failureHeading: 'אם הקידוד נכשל', + tagTypeGen2: 'הצהרה על סוג תג Gen 2', + position: 'מיקום תכנות', + positionModeForward: 'קדימה מהקצה', + positionModeBackfeed: 'משיכה לאחור לפני הקצה', + positionModeAbsolute: 'מוחלט מלמעלה', + positionForwardHint: 'המדפסת מדפיסה עד למרחק זה, מקודדת שם, ולאחר מכן מדפיסה את השאר.', + positionBackfeedHint: 'המדפסת מושכת את המדיה לאחור במרחק זה לפני הקידוד; נדרשת בטנה ריקה בחזית.', + positionAbsoluteHint: 'המדפסת מזיזה את המדיה לשורת נקודות זו מראש התווית לפני הקידוד.', + mmUnit: 'מ"מ', + pickOnLabel: 'קביעה על המדבקה…', + pickHint: 'לחצו על המדבקה או גררו את הקו כדי לקבוע את מיקום התכנות: היכן נעצר המדיה לצורך הקידוד, לא היכן ממוקם ה-inlay.', + pickDone: 'סיום', + voidLength: 'אורך הדפסת VOID', + retries: 'ניסיונות קידוד לתווית', + errorHandling: 'לאחר ניסיונות כושלים', + errorHandlingN: 'ביטול הפורמט, המשך', + errorHandlingP: 'השהיית המדפסת', + errorHandlingE: 'מצב שגיאה', + voidSpeed: 'מהירות הדפסת VOID', + epcHeading: 'מבנה נתוני EPC', + epcBits: 'סה"כ סיביות', + epcPartitions: 'גדלי מחיצות', + epcHint: 'עד 16 מחיצות מופרדות בפסיקים בגודל 1-64 סיביות; סכומן חייב להיות שווה לסך הכול.', + epcPresetSgtin: 'השתמש בפריסת SGTIN-96', + epcAddPartition: 'הוספת מחיצה', + epcRemovePartition: 'הסרת מחיצה', + epcClearPartitions: 'ניקוי כל המחיצות', + epcEmpty: 'אין עדיין מבנה: הגדירו מספר סיביות או התחילו מ-SGTIN-96.', + powerHeading: 'עוצמת קריאה/כתיבה', + readPower: 'קריאה', + writePower: 'כתיבה', + powerHint: '0-30, או H / M / L בקושחה ישנה יותר.', + }, resetPerLabel: 'איפוס לברירת המחדל של המדפסת', resetPerLabelConfirm: 'לאפס באמת?', railGroupApp: 'אפליקציה', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index 9772a1ac..45b69e26 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -481,6 +481,7 @@ const hr = { dotsUnit: 'točaka', tabs: { mediaFeed: 'Medij i pomak', + rfid: 'RFID', appSettings: 'Aplikacija', previewSettings: 'Pregled', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const hr = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Po naljepnici', + rfid: { + specOnlyHint: 'RFID kodiranje zahtijeva pisač iz R serije.', + encodingHeading: 'Kodiranje', + failureHeading: 'Ako kodiranje ne uspije', + tagTypeGen2: 'Deklariraj tip oznake Gen 2', + position: 'Pozicija programiranja', + positionModeForward: 'Naprijed od ruba', + positionModeBackfeed: 'Povlačenje unatrag prije ruba', + positionModeAbsolute: 'Apsolutno od vrha', + positionForwardHint: 'Pisač ispisuje do te udaljenosti, ondje kodira, a zatim ispisuje ostatak.', + positionBackfeedHint: 'Pisač povlači medij unatrag za tu udaljenost prije kodiranja; potreban je prazan podložni sloj sprijeda.', + positionAbsoluteHint: 'Pisač pomiče medij do tog retka točaka od vrha naljepnice prije kodiranja.', + mmUnit: 'mm', + pickOnLabel: 'Postavi na naljepnici…', + pickHint: 'Kliknite na naljepnicu ili povucite vodilicu da biste postavili programsku poziciju: gdje se medij zaustavlja radi kodiranja, a ne gdje se nalazi inlay.', + pickDone: 'Gotovo', + voidLength: 'Duljina ispisa VOID', + retries: 'Pokušaji kodiranja po naljepnici', + errorHandling: 'Nakon neuspjelih pokušaja', + errorHandlingN: 'Odbaci format, nastavi', + errorHandlingP: 'Pauziraj pisač', + errorHandlingE: 'Način rada s greškama', + voidSpeed: 'Brzina ispisa VOID', + epcHeading: 'Struktura podataka EPC', + epcBits: 'Ukupno bitova', + epcPartitions: 'Veličine particija', + epcHint: 'Do 16 particija odvojenih zarezom od 1-64 bita; njihov zbroj mora odgovarati ukupnom broju.', + epcPresetSgtin: 'Koristi raspored SGTIN-96', + epcAddPartition: 'Dodaj particiju', + epcRemovePartition: 'Ukloni particiju', + epcClearPartitions: 'Izbriši sve particije', + epcEmpty: 'Struktura još ne postoji: postavite broj bitova ili počnite sa SGTIN-96.', + powerHeading: 'Snaga čitanja/pisanja', + readPower: 'Čitanje', + writePower: 'Pisanje', + powerHint: '0-30, ili H / M / L na starijem firmveru.', + }, resetPerLabel: 'Vrati na zadane postavke pisača', resetPerLabelConfirm: 'Stvarno resetirati?', railGroupApp: 'Aplikacija', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index 039c6364..44e350c8 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -481,6 +481,7 @@ const hu = { dotsUnit: 'pont', tabs: { mediaFeed: 'Hordozó és továbbítás', + rfid: 'RFID', appSettings: 'Alkalmazás', previewSettings: 'Előnézet', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const hu = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Címkénként', + rfid: { + specOnlyHint: 'Az RFID kódoláshoz R sorozatú nyomtató szükséges.', + encodingHeading: 'Kódolás', + failureHeading: 'Ha a kódolás sikertelen', + tagTypeGen2: 'Gen 2 címketípus deklarálása', + position: 'Programozási pozíció', + positionModeForward: 'Előre a szegélytől', + positionModeBackfeed: 'Visszahúzás a szegély előtt', + positionModeAbsolute: 'Abszolút a tetejétől', + positionForwardHint: 'A nyomtató eddig a távolságig nyomtat, ott kódol, majd kinyomtatja a maradékot.', + positionBackfeedHint: 'A nyomtató ennyivel húzza vissza az anyagszalagot kódolás előtt; elöl üres alátétre van szükség.', + positionAbsoluteHint: 'A nyomtató kódolás előtt erre a pontsorra mozgatja az anyagot a címke tetejétől.', + mmUnit: 'mm', + pickOnLabel: 'Beállítás a címkén…', + pickHint: 'Kattints a címkére, vagy húzd a vezetővonalat a programozási pozíció beállításához: hol áll meg az anyagszalag a kódoláshoz, nem hol van az inlay.', + pickDone: 'Kész', + voidLength: 'VOID nyomat hossza', + retries: 'Kódolási próbálkozások címkénként', + errorHandling: 'Sikertelen próbálkozások után', + errorHandlingN: 'Formátum elvetése, folytatás', + errorHandlingP: 'Nyomtató szüneteltetése', + errorHandlingE: 'Hibamód', + voidSpeed: 'VOID nyomtatási sebesség', + epcHeading: 'EPC adatstruktúra', + epcBits: 'Bitek összesen', + epcPartitions: 'Partícióméretek', + epcHint: 'Legfeljebb 16, vesszővel elválasztott, 1-64 bites partíció; összegüknek meg kell egyeznie az össz értékkel.', + epcPresetSgtin: 'SGTIN-96 elrendezés használata', + epcAddPartition: 'Partíció hozzáadása', + epcRemovePartition: 'Partíció eltávolítása', + epcClearPartitions: 'Összes partíció törlése', + epcEmpty: 'Még nincs struktúra: adjon meg egy bitszámot, vagy induljon ki az SGTIN-96 elrendezésből.', + powerHeading: 'Olvasási/írási teljesítmény', + readPower: 'Olvasás', + writePower: 'Írás', + powerHint: '0-30, vagy H / M / L régebbi firmware esetén.', + }, resetPerLabel: 'Alapértelmezett nyomtatóbeállítások visszaállítása', resetPerLabelConfirm: 'Tényleg visszaállítja?', railGroupApp: 'Alkalmazás', diff --git a/src/locales/it.ts b/src/locales/it.ts index 7a1531f7..cce5dcb8 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -481,6 +481,7 @@ const it = { dotsUnit: 'punti', tabs: { mediaFeed: 'Supporto e avanzamento', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Anteprima', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const it = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etichetta', + rfid: { + specOnlyHint: 'La codifica RFID richiede una stampante della serie R.', + encodingHeading: 'Codifica', + failureHeading: 'Se la codifica non riesce', + tagTypeGen2: 'Dichiara tipo di tag Gen 2', + position: 'Posizione di programmazione', + positionModeForward: 'In avanti dal bordo', + positionModeBackfeed: 'Riavvolgimento prima del bordo', + positionModeAbsolute: "Assoluto dall'alto", + positionForwardHint: 'La stampante stampa fino a questa distanza, codifica lì, poi stampa il resto.', + positionBackfeedHint: 'La stampante arretra il supporto di questa distanza prima della codifica; richiede liner vuoto davanti.', + positionAbsoluteHint: "La stampante sposta il supporto su questa riga di punti dall'inizio dell'etichetta prima della codifica.", + mmUnit: 'mm', + pickOnLabel: "Imposta sull'etichetta…", + pickHint: "Fai clic sull'etichetta o trascina la guida per impostare la posizione di programmazione: dove si ferma il supporto per la codifica, non dove si trova l'inlay.", + pickDone: 'Fatto', + voidLength: 'Lunghezza di stampa VOID', + retries: 'Tentativi di codifica per etichetta', + errorHandling: 'Dopo tentativi falliti', + errorHandlingN: 'Scarta il formato, continua', + errorHandlingP: 'Metti in pausa la stampante', + errorHandlingE: 'Modalità di errore', + voidSpeed: 'Velocità di stampa VOID', + epcHeading: 'Struttura dati EPC', + epcBits: 'Bit totali', + epcPartitions: 'Dimensioni delle partizioni', + epcHint: 'Fino a 16 partizioni separate da virgola da 1-64 bit; la somma deve corrispondere al totale.', + epcPresetSgtin: 'Usa il layout SGTIN-96', + epcAddPartition: 'Aggiungi partizione', + epcRemovePartition: 'Rimuovi partizione', + epcClearPartitions: 'Cancella tutte le partizioni', + epcEmpty: 'Nessuna struttura ancora: imposta un numero di bit o parti da SGTIN-96.', + powerHeading: 'Potenza lettura/scrittura', + readPower: 'Lettura', + writePower: 'Scrittura', + powerHint: '0-30, oppure H / M / L sui firmware più vecchi.', + }, resetPerLabel: 'Ripristina le impostazioni predefinite della stampante', resetPerLabelConfirm: 'Ripristinare davvero?', railGroupApp: 'App', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 8c12e241..7d44cd3e 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -481,6 +481,7 @@ const ja = { dotsUnit: 'ドット', tabs: { mediaFeed: 'メディアと給紙', + rfid: 'RFID', appSettings: 'アプリ', previewSettings: 'プレビュー', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const ja = { maintenance: 'Maintenance', }, railGroupPerLabel: 'ラベルごと', + rfid: { + specOnlyHint: 'RFIDエンコードにはRシリーズのプリンタが必要です。', + encodingHeading: 'エンコード', + failureHeading: 'エンコードに失敗した場合', + tagTypeGen2: 'Gen 2タグタイプを宣言', + position: 'プログラミング位置', + positionModeForward: '先端から前方', + positionModeBackfeed: '先端の手前でバックフィード', + positionModeAbsolute: '上端からの絶対位置', + positionForwardHint: 'プリンターはこの距離まで印字し、そこでエンコードしてから残りを印字します。', + positionBackfeedHint: 'プリンターはエンコード前にこの距離だけメディアを引き戻します。手前に空のライナーが必要です。', + positionAbsoluteHint: 'プリンターはエンコード前にラベル上端からこのドット行までメディアを移動します。', + mmUnit: 'mm', + pickOnLabel: 'ラベル上で設定…', + pickHint: 'ラベルをクリックするかガイドをドラッグして、プログラミング位置を設定します。エンコード時にメディアが停止する位置であり、インレイの位置ではありません。', + pickDone: '完了', + voidLength: 'VOID印字の長さ', + retries: 'ラベルごとのエンコード再試行回数', + errorHandling: '再試行失敗後の動作', + errorHandlingN: 'フォーマットを破棄して続行', + errorHandlingP: 'プリンタを一時停止', + errorHandlingE: 'エラーモード', + voidSpeed: 'VOID印字速度', + epcHeading: 'EPCデータ構造', + epcBits: '合計ビット数', + epcPartitions: 'パーティションサイズ', + epcHint: '1〜64ビットのパーティションをカンマ区切りで最大16個まで指定でき、合計はビット総数と一致する必要があります。', + epcPresetSgtin: 'SGTIN-96レイアウトを使用', + epcAddPartition: 'パーティションを追加', + epcRemovePartition: 'パーティションを削除', + epcClearPartitions: 'すべてのパーティションをクリア', + epcEmpty: 'まだ構造がありません。ビット数を設定するか、SGTIN-96から始めてください。', + powerHeading: '読み取り/書き込み出力', + readPower: '読み取り', + writePower: '書き込み', + powerHint: '0〜30、または旧ファームウェアではH / M / L。', + }, resetPerLabel: 'プリンター既定値に戻す', resetPerLabelConfirm: '本当にリセットしますか?', railGroupApp: 'アプリ', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 378821d7..358f05ad 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -481,6 +481,7 @@ const ko = { dotsUnit: '도트', tabs: { mediaFeed: '미디어 및 급지', + rfid: 'RFID', appSettings: '앱', previewSettings: '미리보기', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const ko = { maintenance: 'Maintenance', }, railGroupPerLabel: '라벨별', + rfid: { + specOnlyHint: 'RFID 인코딩에는 R 시리즈 프린터가 필요합니다.', + encodingHeading: '인코딩', + failureHeading: '인코딩 실패 시', + tagTypeGen2: 'Gen 2 태그 유형 선언', + position: '프로그래밍 위치', + positionModeForward: '선단에서 앞으로', + positionModeBackfeed: '선단 앞에서 백피드', + positionModeAbsolute: '상단에서 절대 위치', + positionForwardHint: '프린터는 이 거리까지 인쇄한 후 그 위치에서 인코딩하고 나머지를 인쇄합니다.', + positionBackfeedHint: '프린터는 인코딩 전에 미디어를 이만큼 뒤로 당깁니다. 앞쪽에 빈 라이너가 필요합니다.', + positionAbsoluteHint: '프린터는 인코딩 전에 라벨 상단에서 이 도트 행까지 미디어를 이동합니다.', + mmUnit: 'mm', + pickOnLabel: '라벨에서 설정…', + pickHint: '라벨을 클릭하거나 가이드를 드래그하여 프로그래밍 위치를 설정하세요. 이는 인코딩을 위해 미디어가 멈추는 지점이며, 인레이(inlay)의 위치가 아닙니다.', + pickDone: '완료', + voidLength: 'VOID 인쇄 길이', + retries: '라벨당 인코딩 재시도 횟수', + errorHandling: '재시도 실패 후', + errorHandlingN: '포맷을 폐기하고 계속', + errorHandlingP: '프린터 일시 정지', + errorHandlingE: '오류 모드', + voidSpeed: 'VOID 인쇄 속도', + epcHeading: 'EPC 데이터 구조', + epcBits: '총 비트 수', + epcPartitions: '파티션 크기', + epcHint: '쉼표로 구분된 최대 16개의 1-64비트 파티션이며, 합계는 전체 비트 수와 같아야 합니다.', + epcPresetSgtin: 'SGTIN-96 레이아웃 사용', + epcAddPartition: '파티션 추가', + epcRemovePartition: '파티션 제거', + epcClearPartitions: '모든 파티션 지우기', + epcEmpty: '아직 구조가 없습니다. 비트 수를 설정하거나 SGTIN-96에서 시작하세요.', + powerHeading: '읽기/쓰기 출력', + readPower: '읽기', + writePower: '쓰기', + powerHint: '0-30 또는 이전 펌웨어의 경우 H / M / L.', + }, resetPerLabel: '프린터 기본값으로 초기화', resetPerLabelConfirm: '정말 초기화하시겠습니까?', railGroupApp: '앱', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 75b80693..987fe939 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -481,6 +481,7 @@ const lt = { dotsUnit: 'taškų', tabs: { mediaFeed: 'Laikmena ir tiekimas', + rfid: 'RFID', appSettings: 'Programa', previewSettings: 'Peržiūra', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const lt = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Etiketei', + rfid: { + specOnlyHint: 'RFID koduojant reikalingas R serijos spausdintuvas.', + encodingHeading: 'Kodavimas', + failureHeading: 'Jei kodavimas nepavyksta', + tagTypeGen2: 'Deklaruoti Gen 2 žymos tipą', + position: 'Programavimo pozicija', + positionModeForward: 'Pirmyn nuo krašto', + positionModeBackfeed: 'Grįžimas atgal prieš kraštą', + positionModeAbsolute: 'Absoliuti nuo viršaus', + positionForwardHint: 'Spausdintuvas spausdina iki šio atstumo, ten užkoduoja, o tada spausdina likusią dalį.', + positionBackfeedHint: 'Spausdintuvas patraukia medžiagą atgal šiuo atstumu prieš kodavimą; priekyje reikia tuščio pagrindo.', + positionAbsoluteHint: 'Spausdintuvas prieš kodavimą perkelia medžiagą į šią taškų eilutę nuo etiketės viršaus.', + mmUnit: 'mm', + pickOnLabel: 'Nustatyti etiketėje…', + pickHint: 'Spustelėkite etiketę arba vilkite kreipiančiąją, kad nustatytumėte programavimo padėtį: kur medžiaga sustoja kodavimui, o ne kur yra inlay.', + pickDone: 'Atlikta', + voidLength: 'VOID spaudinio ilgis', + retries: 'Kodavimo bandymai vienai etiketei', + errorHandling: 'Po nesėkmingų bandymų', + errorHandlingN: 'Atmesti formatą, tęsti', + errorHandlingP: 'Pristabdyti spausdintuvą', + errorHandlingE: 'Klaidos režimas', + voidSpeed: 'VOID spausdinimo greitis', + epcHeading: 'EPC duomenų struktūra', + epcBits: 'Bitų iš viso', + epcPartitions: 'Skaidinių dydžiai', + epcHint: 'Iki 16 kableliais atskirtų 1-64 bitų skaidinių; jų suma turi sutapti su bendru skaičiumi.', + epcPresetSgtin: 'Naudoti SGTIN-96 išdėstymą', + epcAddPartition: 'Pridėti skaidinį', + epcRemovePartition: 'Pašalinti skaidinį', + epcClearPartitions: 'Išvalyti visus skaidinius', + epcEmpty: 'Struktūros dar nėra: nustatykite bitų skaičių arba pradėkite nuo SGTIN-96.', + powerHeading: 'Skaitymo / rašymo galia', + readPower: 'Skaitymas', + writePower: 'Rašymas', + powerHint: '0-30 arba H / M / L senesnėje programinėje aparatinėje įrangoje.', + }, resetPerLabel: 'Atkurti numatytuosius spausdintuvo nustatymus', resetPerLabelConfirm: 'Tikrai atstatyti?', railGroupApp: 'Programa', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index add5dc17..db0a2182 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -481,6 +481,7 @@ const lv = { dotsUnit: 'punkti', tabs: { mediaFeed: 'Materiāls un padeve', + rfid: 'RFID', appSettings: 'Lietotne', previewSettings: 'Priekšskatījums', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const lv = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Uz uzlīmi', + rfid: { + specOnlyHint: 'RFID kodēšanai nepieciešams R sērijas printeris.', + encodingHeading: 'Kodēšana', + failureHeading: 'Ja kodēšana neizdodas', + tagTypeGen2: 'Deklarēt Gen 2 birkas tipu', + position: 'Programmēšanas pozīcija', + positionModeForward: 'Uz priekšu no malas', + positionModeBackfeed: 'Atpakaļpadeve pirms malas', + positionModeAbsolute: 'Absolūta no augšas', + positionForwardHint: 'Printeris drukā līdz šim attālumam, tur kodē un pēc tam izdrukā pārējo.', + positionBackfeedHint: 'Printeris pirms kodēšanas velk materiālu atpakaļ par šo attālumu; priekšpusē nepieciešams tukšs pamatnes materiāls.', + positionAbsoluteHint: 'Printeris pirms kodēšanas pārvieto materiālu līdz šai punktu rindai no etiķetes augšas.', + mmUnit: 'mm', + pickOnLabel: 'Iestatīt uz etiķetes…', + pickHint: 'Noklikšķiniet uz etiķetes vai velciet vadlīniju, lai iestatītu programmēšanas pozīciju: kur materiāls apstājas kodēšanai, nevis kur atrodas inlay.', + pickDone: 'Gatavs', + voidLength: 'VOID izdrukas garums', + retries: 'Kodēšanas mēģinājumi uz etiķeti', + errorHandling: 'Pēc neveiksmīgiem mēģinājumiem', + errorHandlingN: 'Atmest formātu, turpināt', + errorHandlingP: 'Pauzēt printeri', + errorHandlingE: 'Kļūdas režīms', + voidSpeed: 'VOID drukas ātrums', + epcHeading: 'EPC datu struktūra', + epcBits: 'Biti kopā', + epcPartitions: 'Nodalījumu izmēri', + epcHint: 'Līdz 16 ar komatu atdalītiem nodalījumiem no 1-64 bitiem; to summai jāsakrīt ar kopējo skaitu.', + epcPresetSgtin: 'Izmantot SGTIN-96 izkārtojumu', + epcAddPartition: 'Pievienot nodalījumu', + epcRemovePartition: 'Noņemt nodalījumu', + epcClearPartitions: 'Notīrīt visus nodalījumus', + epcEmpty: 'Struktūras vēl nav: iestatiet bitu skaitu vai sāciet ar SGTIN-96.', + powerHeading: 'Lasīšanas/rakstīšanas jauda', + readPower: 'Lasīšana', + writePower: 'Rakstīšana', + powerHint: '0-30 vai H / M / L vecākā aparātprogrammatūrā.', + }, resetPerLabel: 'Atiestatīt printera noklusējuma iestatījumus', resetPerLabelConfirm: 'Vai tiešām atiestatīt?', railGroupApp: 'Lietotne', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index e8209843..4536c66f 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -481,6 +481,7 @@ const nl = { dotsUnit: 'dots', tabs: { mediaFeed: 'Materiaal en doorvoer', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Voorbeeld', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const nl = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etiket', + rfid: { + specOnlyHint: 'RFID-codering vereist een printer uit de R-serie.', + encodingHeading: 'Codering', + failureHeading: 'Als coderen mislukt', + tagTypeGen2: 'Gen 2-tagtype declareren', + position: 'Programmeerpositie', + positionModeForward: 'Vooruit vanaf de rand', + positionModeBackfeed: 'Terugvoer voor de rand', + positionModeAbsolute: 'Absoluut vanaf de bovenkant', + positionForwardHint: 'De printer print tot deze afstand, codeert daar en print vervolgens de rest.', + positionBackfeedHint: 'De printer trekt het medium deze afstand terug voor het coderen; vereist een lege liner aan de voorkant.', + positionAbsoluteHint: 'De printer verplaatst het medium naar deze puntlijn vanaf de bovenkant van het etiket voor het coderen.', + mmUnit: 'mm', + pickOnLabel: 'Instellen op het etiket…', + pickHint: 'Klik op het etiket of sleep de hulplijn om de programmeerpositie in te stellen: waar het medium stopt voor het coderen, niet waar de inlay zit.', + pickDone: 'Klaar', + voidLength: 'VOID-afdruklengte', + retries: 'Coderingspogingen per label', + errorHandling: 'Na mislukte pogingen', + errorHandlingN: 'Formaat weggooien, doorgaan', + errorHandlingP: 'Printer pauzeren', + errorHandlingE: 'Foutmodus', + voidSpeed: 'VOID-afdruksnelheid', + epcHeading: 'EPC-gegevensstructuur', + epcBits: 'Bits totaal', + epcPartitions: 'Partitiegroottes', + epcHint: "Tot 16 door komma's gescheiden partities van 1-64 bits; de som moet gelijk zijn aan het totaal.", + epcPresetSgtin: 'SGTIN-96-indeling gebruiken', + epcAddPartition: 'Partitie toevoegen', + epcRemovePartition: 'Partitie verwijderen', + epcClearPartitions: 'Alle partities wissen', + epcEmpty: 'Nog geen structuur: stel een bitaantal in of begin met SGTIN-96.', + powerHeading: 'Lees-/schrijfvermogen', + readPower: 'Lezen', + writePower: 'Schrijven', + powerHint: '0-30, of H / M / L op oudere firmware.', + }, resetPerLabel: 'Terugzetten naar printerstandaard', resetPerLabelConfirm: 'Echt herstellen?', railGroupApp: 'App', diff --git a/src/locales/no.ts b/src/locales/no.ts index 800e2267..e5d5bcde 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -481,6 +481,7 @@ const no = { dotsUnit: 'dots', tabs: { mediaFeed: 'Medium og mating', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Forhåndsvisning', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const no = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etikett', + rfid: { + specOnlyHint: 'RFID-koding krever en skriver i R-serien.', + encodingHeading: 'Koding', + failureHeading: 'Hvis koding mislykkes', + tagTypeGen2: 'Deklarer Gen 2-taggtype', + position: 'Programmeringsposisjon', + positionModeForward: 'Fremover fra kanten', + positionModeBackfeed: 'Tilbaketrekk før kanten', + positionModeAbsolute: 'Absolutt fra toppen', + positionForwardHint: 'Skriveren skriver ut til denne avstanden, koder der, og skriver deretter ut resten.', + positionBackfeedHint: 'Skriveren trekker materialet tilbake denne avstanden før koding; krever tom liner foran.', + positionAbsoluteHint: 'Skriveren flytter materialet til denne punktlinjen fra etikettens topp før koding.', + mmUnit: 'mm', + pickOnLabel: 'Angi på etiketten…', + pickHint: 'Klikk på etiketten eller dra hjelpelinjen for å angi programmeringsposisjonen: hvor materialet stopper for koding, ikke hvor inlayet sitter.', + pickDone: 'Ferdig', + voidLength: 'VOID-utskriftslengde', + retries: 'Kodingsforsøk per etikett', + errorHandling: 'Etter mislykkede forsøk', + errorHandlingN: 'Forkast formatet, fortsett', + errorHandlingP: 'Sett skriveren på pause', + errorHandlingE: 'Feilmodus', + voidSpeed: 'VOID-utskriftshastighet', + epcHeading: 'EPC-datastruktur', + epcBits: 'Biter totalt', + epcPartitions: 'Partisjonsstørrelser', + epcHint: 'Opptil 16 kommaseparerte partisjoner på 1-64 bit; summen må tilsvare totalen.', + epcPresetSgtin: 'Bruk SGTIN-96-oppsettet', + epcAddPartition: 'Legg til partisjon', + epcRemovePartition: 'Fjern partisjon', + epcClearPartitions: 'Tøm alle partisjoner', + epcEmpty: 'Ingen struktur ennå: angi et antall bits, eller start med SGTIN-96.', + powerHeading: 'Lese-/skriveeffekt', + readPower: 'Lese', + writePower: 'Skrive', + powerHint: '0-30, eller H / M / L på eldre fastvare.', + }, resetPerLabel: 'Tilbakestill til skriverstandard', resetPerLabelConfirm: 'Virkelig tilbakestille?', railGroupApp: 'App', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index daca10c4..9300fc9d 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -481,6 +481,7 @@ const pl = { dotsUnit: 'punkty', tabs: { mediaFeed: 'Nośnik i podawanie', + rfid: 'RFID', appSettings: 'Aplikacja', previewSettings: 'Podgląd', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const pl = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Na etykietę', + rfid: { + specOnlyHint: 'Kodowanie RFID wymaga drukarki z serii R.', + encodingHeading: 'Kodowanie', + failureHeading: 'Jeśli kodowanie się nie powiedzie', + tagTypeGen2: 'Zadeklaruj typ znacznika Gen 2', + position: 'Pozycja programowania', + positionModeForward: 'Naprzód od krawędzi', + positionModeBackfeed: 'Cofnięcie przed krawędzią', + positionModeAbsolute: 'Bezwzględnie od góry', + positionForwardHint: 'Drukarka drukuje do tej odległości, koduje w tym miejscu, a następnie drukuje resztę.', + positionBackfeedHint: 'Drukarka cofa materiał o tę odległość przed kodowaniem; wymaga pustej podkładki z przodu.', + positionAbsoluteHint: 'Drukarka przesuwa materiał do tego wiersza punktów od góry etykiety przed kodowaniem.', + mmUnit: 'mm', + pickOnLabel: 'Ustaw na etykiecie…', + pickHint: 'Kliknij etykietę lub przeciągnij linię prowadzącą, aby ustawić pozycję programowania: gdzie materiał zatrzymuje się do kodowania, a nie gdzie znajduje się inlay.', + pickDone: 'Gotowe', + voidLength: 'Długość wydruku VOID', + retries: 'Próby kodowania na etykietę', + errorHandling: 'Po nieudanych próbach', + errorHandlingN: 'Odrzuć format, kontynuuj', + errorHandlingP: 'Wstrzymaj drukarkę', + errorHandlingE: 'Tryb błędu', + voidSpeed: 'Szybkość druku VOID', + epcHeading: 'Struktura danych EPC', + epcBits: 'Bity łącznie', + epcPartitions: 'Rozmiary partycji', + epcHint: 'Do 16 partycji rozdzielonych przecinkami po 1-64 bity; ich suma musi odpowiadać wartości łącznej.', + epcPresetSgtin: 'Użyj układu SGTIN-96', + epcAddPartition: 'Dodaj partycję', + epcRemovePartition: 'Usuń partycję', + epcClearPartitions: 'Wyczyść wszystkie partycje', + epcEmpty: 'Jeszcze nie ma struktury: ustaw liczbę bitów lub zacznij od SGTIN-96.', + powerHeading: 'Moc odczytu/zapisu', + readPower: 'Odczyt', + writePower: 'Zapis', + powerHint: '0-30 lub H / M / L w starszym firmwarze.', + }, resetPerLabel: 'Przywróć domyślne ustawienia drukarki', resetPerLabelConfirm: 'Naprawdę zresetować?', railGroupApp: 'Aplikacja', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 1433c957..de3f12d6 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -481,6 +481,7 @@ const pt = { dotsUnit: 'pontos', tabs: { mediaFeed: 'Mídia e avanço', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Pré-visualização', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const pt = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Por etiqueta', + rfid: { + specOnlyHint: 'A codificação RFID requer uma impressora da série R.', + encodingHeading: 'Codificação', + failureHeading: 'Se a codificação falhar', + tagTypeGen2: 'Declarar tipo de tag Gen 2', + position: 'Posição de programação', + positionModeForward: 'Para a frente a partir do bordo', + positionModeBackfeed: 'Retrocesso antes do bordo', + positionModeAbsolute: 'Absoluto a partir do topo', + positionForwardHint: 'A impressora imprime até essa distância, codifica ali e depois imprime o resto.', + positionBackfeedHint: 'A impressora recua a mídia por essa distância antes da codificação; requer liner vazio à frente.', + positionAbsoluteHint: 'A impressora move a mídia até essa linha de pontos a partir do topo da etiqueta antes da codificação.', + mmUnit: 'mm', + pickOnLabel: 'Definir na etiqueta…', + pickHint: 'Clique na etiqueta ou arraste a guia para definir a posição de programação: onde a mídia para para a codificação, não onde o inlay está localizado.', + pickDone: 'Concluído', + voidLength: 'Comprimento de impressão VOID', + retries: 'Tentativas de codificação por etiqueta', + errorHandling: 'Após tentativas falhadas', + errorHandlingN: 'Descartar o formato, continuar', + errorHandlingP: 'Pausar a impressora', + errorHandlingE: 'Modo de erro', + voidSpeed: 'Velocidade de impressão VOID', + epcHeading: 'Estrutura de dados EPC', + epcBits: 'Total de bits', + epcPartitions: 'Tamanhos das partições', + epcHint: 'Até 16 partições separadas por vírgulas de 1-64 bits; a soma deve corresponder ao total.', + epcPresetSgtin: 'Usar o layout SGTIN-96', + epcAddPartition: 'Adicionar partição', + epcRemovePartition: 'Remover partição', + epcClearPartitions: 'Limpar todas as partições', + epcEmpty: 'Ainda sem estrutura: defina um número de bits ou comece pelo SGTIN-96.', + powerHeading: 'Potência de leitura/escrita', + readPower: 'Leitura', + writePower: 'Escrita', + powerHint: '0-30, ou H / M / L em firmware mais antigo.', + }, resetPerLabel: 'Repor as predefinições da impressora', resetPerLabelConfirm: 'Repor mesmo?', railGroupApp: 'App', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index 69be82c0..291e4df2 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -481,6 +481,7 @@ const ro = { dotsUnit: 'puncte', tabs: { mediaFeed: 'Suport și alimentare', + rfid: 'RFID', appSettings: 'Aplicație', previewSettings: 'Previzualizare', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const ro = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Pe etichetă', + rfid: { + specOnlyHint: 'Codificarea RFID necesită o imprimantă din seria R.', + encodingHeading: 'Codificare', + failureHeading: 'Dacă codificarea eșuează', + tagTypeGen2: 'Declară tipul de etichetă Gen 2', + position: 'Poziție de programare', + positionModeForward: 'Înainte de la margine', + positionModeBackfeed: 'Derulare înapoi înainte de margine', + positionModeAbsolute: 'Absolut de sus', + positionForwardHint: 'Imprimanta imprimă până la această distanță, codifică acolo, apoi imprimă restul.', + positionBackfeedHint: 'Imprimanta trage mediul înapoi cu această distanță înainte de codificare; necesită liner gol în față.', + positionAbsoluteHint: 'Imprimanta mută mediul la acest rând de puncte de la partea de sus a etichetei înainte de codificare.', + mmUnit: 'mm', + pickOnLabel: 'Setare pe etichetă…', + pickHint: 'Faceți clic pe etichetă sau trageți ghidul pentru a seta poziția de programare: unde se oprește mediul pentru codare, nu unde se află inlay-ul.', + pickDone: 'Gata', + voidLength: 'Lungimea imprimării VOID', + retries: 'Reîncercări de codificare per etichetă', + errorHandling: 'După reîncercări eșuate', + errorHandlingN: 'Renunță la format, continuă', + errorHandlingP: 'Pune imprimanta în pauză', + errorHandlingE: 'Mod eroare', + voidSpeed: 'Viteza de imprimare VOID', + epcHeading: 'Structura datelor EPC', + epcBits: 'Total biți', + epcPartitions: 'Dimensiuni partiții', + epcHint: 'Până la 16 partiții separate prin virgulă de 1-64 biți; suma lor trebuie să fie egală cu totalul.', + epcPresetSgtin: 'Utilizează structura SGTIN-96', + epcAddPartition: 'Adăugare partiție', + epcRemovePartition: 'Eliminare partiție', + epcClearPartitions: 'Ștergeți toate partițiile', + epcEmpty: 'Încă nu există structură: setați un număr de biți sau porniți de la SGTIN-96.', + powerHeading: 'Putere citire/scriere', + readPower: 'Citire', + writePower: 'Scriere', + powerHint: '0-30, sau H / M / L pe firmware mai vechi.', + }, resetPerLabel: 'Resetare la setările implicite ale imprimantei', resetPerLabelConfirm: 'Chiar resetați?', railGroupApp: 'Aplicație', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 85a5812d..00173753 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -481,6 +481,7 @@ const sk = { dotsUnit: 'body', tabs: { mediaFeed: 'Médium a posuv', + rfid: 'RFID', appSettings: 'Aplikácia', previewSettings: 'Náhľad', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const sk = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Na štítok', + rfid: { + specOnlyHint: 'Kódovanie RFID vyžaduje tlačiareň radu R.', + encodingHeading: 'Kódovanie', + failureHeading: 'Ak kódovanie zlyhá', + tagTypeGen2: 'Deklarovať typ tagu Gen 2', + position: 'Pozícia programovania', + positionModeForward: 'Vpred od hrany', + positionModeBackfeed: 'Spätný posun pred hranou', + positionModeAbsolute: 'Absolútne od horného okraja', + positionForwardHint: 'Tlačiareň tlačí až do tejto vzdialenosti, tam zakóduje a potom vytlačí zvyšok.', + positionBackfeedHint: 'Tlačiareň posunie médium späť o túto vzdialenosť pred kódovaním; vpredu je potrebný prázdny podklad.', + positionAbsoluteHint: 'Tlačiareň presunie médium na tento riadok bodov od horného okraja etikety pred kódovaním.', + mmUnit: 'mm', + pickOnLabel: 'Nastaviť na štítku…', + pickHint: 'Kliknite na štítok alebo presuňte vodiacu čiaru a nastavte programovaciu pozíciu: kde sa médium zastaví pre kódovanie, nie kde leží inlay.', + pickDone: 'Hotovo', + voidLength: 'Dĺžka výtlačku VOID', + retries: 'Pokusy o kódovanie na etiketu', + errorHandling: 'Po neúspešných pokusoch', + errorHandlingN: 'Zahodiť formát, pokračovať', + errorHandlingP: 'Pozastaviť tlačiareň', + errorHandlingE: 'Chybový režim', + voidSpeed: 'Rýchlosť tlače VOID', + epcHeading: 'Štruktúra dát EPC', + epcBits: 'Bitov spolu', + epcPartitions: 'Veľkosti partícií', + epcHint: 'Až 16 čiarkou oddelených partícií po 1-64 bitov; ich súčet musí zodpovedať celkovému počtu.', + epcPresetSgtin: 'Použiť rozloženie SGTIN-96', + epcAddPartition: 'Pridať oddiel', + epcRemovePartition: 'Odstrániť oddiel', + epcClearPartitions: 'Vymazať všetky oddiely', + epcEmpty: 'Zatiaľ žiadna štruktúra: zadajte počet bitov alebo začnite so SGTIN-96.', + powerHeading: 'Výkon čítania/zápisu', + readPower: 'Čítanie', + writePower: 'Zápis', + powerHint: '0-30, alebo H / M / L na staršom firmvéri.', + }, resetPerLabel: 'Obnoviť predvolené nastavenia tlačiarne', resetPerLabelConfirm: 'Naozaj resetovať?', railGroupApp: 'Aplikácia', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 802f1458..ae6408ff 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -481,6 +481,7 @@ const sl = { dotsUnit: 'pik', tabs: { mediaFeed: 'Medij in podajanje', + rfid: 'RFID', appSettings: 'Aplikacija', previewSettings: 'Predogled', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const sl = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Na nalepko', + rfid: { + specOnlyHint: 'Kodiranje RFID zahteva tiskalnik serije R.', + encodingHeading: 'Kodiranje', + failureHeading: 'Če kodiranje ne uspe', + tagTypeGen2: 'Deklariraj tip oznake Gen 2', + position: 'Položaj programiranja', + positionModeForward: 'Naprej od roba', + positionModeBackfeed: 'Povratni pomik pred robom', + positionModeAbsolute: 'Absolutno od vrha', + positionForwardHint: 'Tiskalnik tiska do te razdalje, tam kodira, nato natisne preostanek.', + positionBackfeedHint: 'Tiskalnik pred kodiranjem povleče medij nazaj za to razdaljo; spredaj je potreben prazen podložni trak.', + positionAbsoluteHint: 'Tiskalnik pred kodiranjem premakne medij do te vrstice pik od vrha nalepke.', + mmUnit: 'mm', + pickOnLabel: 'Nastavi na nalepki…', + pickHint: 'Kliknite nalepko ali povlecite vodilo, da nastavite programirno mesto: kje se medij ustavi za kodiranje, ne kje se nahaja inlay.', + pickDone: 'Končano', + voidLength: 'Dolžina izpisa VOID', + retries: 'Poskusi kodiranja na nalepko', + errorHandling: 'Po neuspelih poskusih', + errorHandlingN: 'Zavrzi format, nadaljuj', + errorHandlingP: 'Zaustavi tiskalnik', + errorHandlingE: 'Način napake', + voidSpeed: 'Hitrost tiskanja VOID', + epcHeading: 'Podatkovna struktura EPC', + epcBits: 'Skupno bitov', + epcPartitions: 'Velikosti particij', + epcHint: 'Do 16 z vejico ločenih particij po 1-64 bitov; njihova vsota se mora ujemati s skupnim številom.', + epcPresetSgtin: 'Uporabi postavitev SGTIN-96', + epcAddPartition: 'Dodaj particijo', + epcRemovePartition: 'Odstrani particijo', + epcClearPartitions: 'Počisti vse particije', + epcEmpty: 'Strukture še ni: nastavite število bitov ali začnite s SGTIN-96.', + powerHeading: 'Moč branja/pisanja', + readPower: 'Branje', + writePower: 'Pisanje', + powerHint: '0-30, ali H / M / L pri starejši strojni programski opremi.', + }, resetPerLabel: 'Ponastavi na privzete nastavitve tiskalnika', resetPerLabelConfirm: 'Res ponastaviti?', railGroupApp: 'Aplikacija', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 305d0555..86c42642 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -481,6 +481,7 @@ const sr = { dotsUnit: 'tačaka', tabs: { mediaFeed: 'Медијум и увлачење', + rfid: 'RFID', appSettings: 'Апликација', previewSettings: 'Pregled', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const sr = { maintenance: 'Maintenance', }, railGroupPerLabel: 'По налепници', + rfid: { + specOnlyHint: 'RFID кодирање захтева штампач из R серије.', + encodingHeading: 'Кодирање', + failureHeading: 'Ако кодирање не успе', + tagTypeGen2: 'Декларисање типа ознаке Gen 2', + position: 'Позиција програмирања', + positionModeForward: 'Напред од ивице', + positionModeBackfeed: 'Повлачење уназад пре ивице', + positionModeAbsolute: 'Апсолутно од врха', + positionForwardHint: 'Штампач штампа до овог растојања, тамо кодира, а затим штампа остатак.', + positionBackfeedHint: 'Штампач повлачи медијум уназад за ово растојање пре кодирања; напред је потребна празна подлога.', + positionAbsoluteHint: 'Штампач помера медијум до овог реда тачака од врха налепнице пре кодирања.', + mmUnit: 'mm', + pickOnLabel: 'Постави на налепници…', + pickHint: 'Кликните на налепницу или превуците водич да бисте поставили позицију програмирања: где се медијум зауставља ради кодирања, а не где се налази inlay.', + pickDone: 'Готово', + voidLength: 'Дужина исписа VOID', + retries: 'Покушаји кодирања по налепници', + errorHandling: 'После неуспелих покушаја', + errorHandlingN: 'Одбаци формат, настави', + errorHandlingP: 'Паузирај штампач', + errorHandlingE: 'Режим грешке', + voidSpeed: 'Брзина штампе VOID', + epcHeading: 'Структура података EPC', + epcBits: 'Укупно битова', + epcPartitions: 'Величине партиција', + epcHint: 'До 16 партиција одвојених зарезом од 1-64 бита; њихов збир мора одговарати укупном броју.', + epcPresetSgtin: 'Користи распоред SGTIN-96', + epcAddPartition: 'Додај партицију', + epcRemovePartition: 'Уклони партицију', + epcClearPartitions: 'Обриши све партиције', + epcEmpty: 'Још нема структуре: подесите број битова или почните од SGTIN-96.', + powerHeading: 'Снага читања/уписа', + readPower: 'Читање', + writePower: 'Упис', + powerHint: '0-30, или H / M / L на старијем фирмверу.', + }, resetPerLabel: 'Врати на подразумевана подешавања штампача', resetPerLabelConfirm: 'Стварно ресетовати?', railGroupApp: 'Апликација', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index e2ae9d67..01b3bad5 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -481,6 +481,7 @@ const sv = { dotsUnit: 'punkter', tabs: { mediaFeed: 'Material och matning', + rfid: 'RFID', appSettings: 'App', previewSettings: 'Förhandsvisning', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const sv = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Per etikett', + rfid: { + specOnlyHint: 'RFID-kodning kräver en skrivare i R-serien.', + encodingHeading: 'Kodning', + failureHeading: 'Om kodningen misslyckas', + tagTypeGen2: 'Deklarera Gen 2-taggtyp', + position: 'Programmeringsposition', + positionModeForward: 'Framåt från kanten', + positionModeBackfeed: 'Backmatning före kanten', + positionModeAbsolute: 'Absolut från toppen', + positionForwardHint: 'Skrivaren skriver ut till detta avstånd, kodar där och skriver sedan ut resten.', + positionBackfeedHint: 'Skrivaren drar tillbaka materialet detta avstånd innan kodning; kräver tomt underlägg framtill.', + positionAbsoluteHint: 'Skrivaren flyttar materialet till denna punktrad från etikettens topp innan kodning.', + mmUnit: 'mm', + pickOnLabel: 'Ange på etiketten…', + pickHint: 'Klicka på etiketten eller dra guiden för att ange programmeringspositionen: var materialet stannar för kodning, inte var inlayen sitter.', + pickDone: 'Klart', + voidLength: 'VOID-utskriftslängd', + retries: 'Kodningsförsök per etikett', + errorHandling: 'Efter misslyckade försök', + errorHandlingN: 'Kasta formatet, fortsätt', + errorHandlingP: 'Pausa skrivaren', + errorHandlingE: 'Felläge', + voidSpeed: 'VOID-utskriftshastighet', + epcHeading: 'EPC-datastruktur', + epcBits: 'Bitar totalt', + epcPartitions: 'Partitionsstorlekar', + epcHint: 'Upp till 16 kommaseparerade partitioner på 1-64 bitar; summan måste motsvara totalen.', + epcPresetSgtin: 'Använd SGTIN-96-layouten', + epcAddPartition: 'Lägg till partition', + epcRemovePartition: 'Ta bort partition', + epcClearPartitions: 'Rensa alla partitioner', + epcEmpty: 'Ingen struktur än: ange ett antal bitar eller börja med SGTIN-96.', + powerHeading: 'Läs-/skrivkraft', + readPower: 'Läs', + writePower: 'Skriv', + powerHint: '0-30, eller H / M / L på äldre firmware.', + }, resetPerLabel: 'Återställ till skrivarens standardinställningar', resetPerLabelConfirm: 'Verkligen återställa?', railGroupApp: 'App', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 9fe1a135..d0a604ac 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -481,6 +481,7 @@ const tr = { dotsUnit: 'nokta', tabs: { mediaFeed: 'Ortam ve besleme', + rfid: 'RFID', appSettings: 'Uygulama', previewSettings: 'Önizleme', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const tr = { maintenance: 'Maintenance', }, railGroupPerLabel: 'Etiket başına', + rfid: { + specOnlyHint: 'RFID kodlama R serisi bir yazıcı gerektirir.', + encodingHeading: 'Kodlama', + failureHeading: 'Kodlama başarısız olursa', + tagTypeGen2: 'Gen 2 etiket türünü bildir', + position: 'Programlama konumu', + positionModeForward: 'Kenardan ileri', + positionModeBackfeed: 'Kenardan önce geri besleme', + positionModeAbsolute: 'Üstten mutlak', + positionForwardHint: 'Yazıcı bu mesafeye kadar yazdırır, orada kodlar, ardından kalanını yazdırır.', + positionBackfeedHint: 'Yazıcı kodlamadan önce ortamı bu kadar geri çeker; önde boş astar gerektirir.', + positionAbsoluteHint: 'Yazıcı kodlamadan önce ortamı etiketin üstünden bu nokta satırına taşır.', + mmUnit: 'mm', + pickOnLabel: 'Etiket üzerinde ayarla…', + pickHint: "Programlama konumunu ayarlamak için etikete tıklayın veya kılavuzu sürükleyin: ortamın kodlama için nerede durduğunu gösterir, inlay'in nerede olduğunu değil.", + pickDone: 'Tamam', + voidLength: 'VOID çıktı uzunluğu', + retries: 'Etiket başına kodlama deneme sayısı', + errorHandling: 'Başarısız denemelerden sonra', + errorHandlingN: 'Formatı bırak, devam et', + errorHandlingP: 'Yazıcıyı duraklat', + errorHandlingE: 'Hata modu', + voidSpeed: 'VOID baskı hızı', + epcHeading: 'EPC veri yapısı', + epcBits: 'Toplam bit', + epcPartitions: 'Bölüm boyutları', + epcHint: 'Virgülle ayrılmış, 1-64 bit arasında en fazla 16 bölüm; toplamları genel toplama eşit olmalıdır.', + epcPresetSgtin: 'SGTIN-96 düzenini kullan', + epcAddPartition: 'Bölüm ekle', + epcRemovePartition: 'Bölümü kaldır', + epcClearPartitions: 'Tüm bölümleri temizle', + epcEmpty: 'Henüz yapı yok: bit sayısı belirleyin veya SGTIN-96 ile başlayın.', + powerHeading: 'Okuma/yazma gücü', + readPower: 'Okuma', + writePower: 'Yazma', + powerHint: 'Eski donanım yazılımında 0-30 veya H / M / L.', + }, resetPerLabel: 'Yazıcı varsayılanlarına sıfırla', resetPerLabelConfirm: 'Gerçekten sıfırlansın mı?', railGroupApp: 'Uygulama', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index 3a1ee1b9..eb2da476 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -481,6 +481,7 @@ const zhHans = { dotsUnit: '点', tabs: { mediaFeed: '介质与走纸', + rfid: 'RFID', appSettings: '应用', previewSettings: '预览', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const zhHans = { maintenance: 'Maintenance', }, railGroupPerLabel: '每个标签', + rfid: { + specOnlyHint: 'RFID 编码需要 R 系列打印机。', + encodingHeading: '编码', + failureHeading: '编码失败时', + tagTypeGen2: '声明 Gen 2 标签类型', + position: '编程位置', + positionModeForward: '从前沿向前', + positionModeBackfeed: '在前沿之前回退', + positionModeAbsolute: '从顶部绝对定位', + positionForwardHint: '打印机打印到此距离,在此处编码,然后打印剩余部分。', + positionBackfeedHint: '打印机在编码前将介质回拉这么长距离;前方需要留空衬纸。', + positionAbsoluteHint: '打印机在编码前将介质移动到距标签顶部的这一点行。', + mmUnit: 'mm', + pickOnLabel: '在标签上设置…', + pickHint: '点击标签或拖动导轨来设置编程位置:即介质为编码而停止的位置,而不是嵌体(inlay)所在的位置。', + pickDone: '完成', + voidLength: 'VOID 打印长度', + retries: '每张标签的编码重试次数', + errorHandling: '重试失败后', + errorHandlingN: '丢弃格式并继续', + errorHandlingP: '暂停打印机', + errorHandlingE: '错误模式', + voidSpeed: 'VOID 打印速度', + epcHeading: 'EPC 数据结构', + epcBits: '总位数', + epcPartitions: '分区大小', + epcHint: '最多 16 个以逗号分隔的 1-64 位分区,其总和必须等于总位数。', + epcPresetSgtin: '使用 SGTIN-96 布局', + epcAddPartition: '添加分区', + epcRemovePartition: '移除分区', + epcClearPartitions: '清除所有分区', + epcEmpty: '尚无结构:设置比特数或从 SGTIN-96 开始。', + powerHeading: '读/写功率', + readPower: '读取', + writePower: '写入', + powerHint: '0-30,或旧固件上的 H / M / L。', + }, resetPerLabel: '恢复打印机默认设置', resetPerLabelConfirm: '真的要重置吗?', railGroupApp: '应用', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 653b3ddf..9d5a9ad3 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -481,6 +481,7 @@ const zhHant = { dotsUnit: '點', tabs: { mediaFeed: '介質與送紙', + rfid: 'RFID', appSettings: '應用程式', previewSettings: '預覽', mcpServer: 'MCP', @@ -494,6 +495,43 @@ const zhHant = { maintenance: 'Maintenance', }, railGroupPerLabel: '每個標籤', + rfid: { + specOnlyHint: 'RFID 編碼需要 R 系列印表機。', + encodingHeading: '編碼', + failureHeading: '編碼失敗時', + tagTypeGen2: '宣告 Gen 2 標籤類型', + position: '編程位置', + positionModeForward: '從前緣向前', + positionModeBackfeed: '在前緣之前回退', + positionModeAbsolute: '從頂部絕對定位', + positionForwardHint: '印表機列印到此距離,在該處編碼,然後列印其餘部分。', + positionBackfeedHint: '印表機在編碼前將介質回拉這麼長距離;前方需要留空襯紙。', + positionAbsoluteHint: '印表機在編碼前將介質移動到距標籤頂部的這一點列。', + mmUnit: 'mm', + pickOnLabel: '在標籤上設定…', + pickHint: '點擊標籤或拖曳導引線以設定編程位置:即介質為編碼而停止的位置,而非嵌體(inlay)所在的位置。', + pickDone: '完成', + voidLength: 'VOID 列印長度', + retries: '每張標籤的編碼重試次數', + errorHandling: '重試失敗後', + errorHandlingN: '捨棄格式並繼續', + errorHandlingP: '暫停印表機', + errorHandlingE: '錯誤模式', + voidSpeed: 'VOID 列印速度', + epcHeading: 'EPC 資料結構', + epcBits: '總位元數', + epcPartitions: '分割區大小', + epcHint: '最多 16 個以逗號分隔的 1-64 位元分割區,其總和必須等於總位元數。', + epcPresetSgtin: '使用 SGTIN-96 版面配置', + epcAddPartition: '新增分區', + epcRemovePartition: '移除分區', + epcClearPartitions: '清除所有分區', + epcEmpty: '尚無結構:設定位元數或從 SGTIN-96 開始。', + powerHeading: '讀取/寫入功率', + readPower: '讀取', + writePower: '寫入', + powerHint: '0-30,或舊韌體上的 H / M / L。', + }, resetPerLabel: '還原印表機預設設定', resetPerLabelConfirm: '真的要重設嗎?', railGroupApp: '應用程式', diff --git a/src/store/rfidPick.test.ts b/src/store/rfidPick.test.ts new file mode 100644 index 00000000..f37e3675 --- /dev/null +++ b/src/store/rfidPick.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { useLabelStore } from "./labelStore"; + +// The pick hides the settings modal to free the canvas, so its capture layer +// must never outlive the flow: every exit clears it. +describe("RFID position pick lifecycle", () => { + beforeEach(() => { + useLabelStore.setState({ pickingRfidPosition: false, printerSettingsTab: null }); + }); + + it("hides the dialog while picking and returns to its tab", () => { + useLabelStore.getState().startRfidPositionPick(); + expect(useLabelStore.getState().pickingRfidPosition).toBe(true); + expect(useLabelStore.getState().printerSettingsTab).toBeNull(); + + useLabelStore.getState().endRfidPositionPick(); + expect(useLabelStore.getState().pickingRfidPosition).toBe(false); + expect(useLabelStore.getState().printerSettingsTab).toBe("rfid"); + }); + + it("ends when the dialog is opened another way", () => { + useLabelStore.getState().startRfidPositionPick(); + useLabelStore.getState().setPrinterSettingsTab("mediaFeed"); + expect(useLabelStore.getState().pickingRfidPosition).toBe(false); + }); + + it("is not persisted, so a reload cannot resume it", () => { + useLabelStore.getState().startRfidPositionPick(); + const persisted = JSON.parse(localStorage.getItem("zpl-designer-session") ?? "{}") as { + state?: Record; + }; + expect(persisted.state && "pickingRfidPosition" in persisted.state).toBeFalsy(); + }); +}); diff --git a/src/store/slices/uiSlice.ts b/src/store/slices/uiSlice.ts index f91bc6be..45d50684 100644 --- a/src/store/slices/uiSlice.ts +++ b/src/store/slices/uiSlice.ts @@ -67,6 +67,7 @@ export type PrinterSettingsTab = | 'mcpServer' | 'dataSources' | 'mediaFeed' + | 'rfid' | 'printQuality' | 'output' | 'clockTime' @@ -167,6 +168,10 @@ export interface UiSlice { sidebarTab: SidebarTab; /** Block resize-handle mode; see BlockDragMode. Transient. */ blockDragMode: BlockDragMode; + /** Canvas pick for the ^RS programming position: the printer-settings + * modal covers the canvas, so it steps aside for the duration and + * reopens on its RFID tab afterwards. Transient. */ + pickingRfidPosition: boolean; /** Reference for the "Align" section toggle; see AlignSelectionRef. Transient. */ alignRef: AlignSelectionRef; printerSettingsTab: PrinterSettingsTab | null; @@ -235,6 +240,8 @@ export interface UiSlice { setMcpSidecarAvailable: (available: boolean) => void; setSidebarTab: (tab: SidebarTab) => void; setBlockDragMode: (mode: BlockDragMode) => void; + startRfidPositionPick: () => void; + endRfidPositionPick: () => void; setAlignRef: (ref: AlignSelectionRef) => void; setPrinterSettingsTab: (tab: PrinterSettingsTab | null) => void; openZebraPrint: (source: 'label' | 'setupScript') => void; @@ -322,6 +329,7 @@ export const createUiSlice: StateCreator = (set, ge mcpSidecarAvailable: null, sidebarTab: 'properties', blockDragMode: 'frame', + pickingRfidPosition: false, alignRef: 'selection', printerSettingsTab: null, zebraPrintSource: null, @@ -450,6 +458,8 @@ export const createUiSlice: StateCreator = (set, ge }), setPaletteView: (view) => set({ paletteView: view }), togglePaletteEditing: () => set((state) => ({ paletteEditing: !state.paletteEditing })), + startRfidPositionPick: () => set({ pickingRfidPosition: true, printerSettingsTab: null }), + endRfidPositionPick: () => set({ pickingRfidPosition: false, printerSettingsTab: 'rfid' }), setShowZplCommands: (show) => set({ showZplCommands: show }), setMcpServerEnabled: (enabled) => set({ mcpServerEnabled: enabled }), setMcpServerPort: (port) => set({ mcpServerPort: port }), @@ -498,7 +508,9 @@ export const createUiSlice: StateCreator = (set, ge setSidebarTab: (tab) => set({ sidebarTab: tab }), setBlockDragMode: (mode) => set({ blockDragMode: mode }), setAlignRef: (ref) => set({ alignRef: ref }), - setPrinterSettingsTab: (tab) => set({ printerSettingsTab: tab }), + // Opening or closing the dialog ends a pick: its capture layer must never + // outlive the flow that started it. + setPrinterSettingsTab: (tab) => set({ printerSettingsTab: tab, pickingRfidPosition: false }), openZebraPrint: (source) => set({ zebraPrintSource: source }), closeZebraPrint: () => set({ zebraPrintSource: null }), openGs1Builder: (objectId) => set({ gs1BuilderObjectId: objectId }), diff --git a/src/test/setup.ts b/src/test/setup.ts index d8f43903..436ebf4a 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -50,6 +50,27 @@ Object.defineProperty(globalThis, 'FontFace', { value: FakeFontFace, }); +// ── ResizeObserver ──────────────────────────────────────────────────────────── + +// jsdom ships none, and the headless-ui popovers the settings dialog uses +// construct one on open; without it they throw as an unhandled error. +if (typeof globalThis.ResizeObserver === 'undefined') { + Object.defineProperty(globalThis, 'ResizeObserver', { + configurable: true, + value: class { + observe(): undefined { + return undefined; + } + unobserve(): undefined { + return undefined; + } + disconnect(): undefined { + return undefined; + } + }, + }); +} + // A real DOM (jsdom) is present only for files that opt in via // `// @vitest-environment jsdom`; the default lane is pure Node. The stubs // below would clobber jsdom's window/document, so apply them only when no