diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index e0053d8f..87a7bf19 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { ObjectPalette } from "./Palette/ObjectPalette"; import { LabelCanvas } from "./Canvas/LabelCanvas"; +import type { LabelCanvasHandle } from "./Canvas/LabelCanvas"; import { PropertiesPanel } from "./Properties/PropertiesPanel"; import { LayersPanel } from "./Properties/LayersPanel"; import { FontManager } from "./Fonts/FontManager"; @@ -91,6 +92,9 @@ export function AppShell() { handlePrint, } = useZplImportExport(); const outputPanel = useOutputPanel(OUTPUT_DEFAULT_H); + // Imperative handle to the canvas for actions PropertiesPanel needs live + // render bboxes for (e.g. align-to-label centring). + const canvasRef = useRef(null); return (
@@ -271,6 +275,7 @@ export function AppShell() {
setCanvasSettings({ showGrid: !showGrid })} @@ -319,7 +324,7 @@ export function AppShell() {
- {rightTab === "properties" && } + {rightTab === "properties" && } {rightTab === "layers" && } {rightTab === "fonts" && }
diff --git a/src/components/Canvas/LabelCanvas.tsx b/src/components/Canvas/LabelCanvas.tsx index 59981fec..7f0fb1e0 100644 --- a/src/components/Canvas/LabelCanvas.tsx +++ b/src/components/Canvas/LabelCanvas.tsx @@ -1,4 +1,7 @@ import { + forwardRef, + useImperativeHandle, + useMemo, useRef, useEffect, useState, @@ -14,6 +17,8 @@ import { SNAP_OPTIONS } from "../../lib/units"; import type { Unit } from "../../lib/units"; import { computeSnap } from "../../lib/snapGuides"; import type { SnapGuide } from "../../lib/snapGuides"; +import { computeGroupCenterDelta } from "../../lib/alignment"; +import type { AlignAxis } from "../../lib/alignment"; import { KonvaObject } from "./KonvaObject"; import { Grid } from "./Grid"; import { GuideLines } from "./GuideLines"; @@ -50,7 +55,14 @@ interface Props { onViewRotationChange: (rotation: ViewRotation) => void; } -export function LabelCanvas({ +/** Imperative actions sibling components (PropertiesPanel) need from the + * canvas. The canvas owns the live render bboxes via Konva — co-locating + * the action with the data avoids round-tripping through the store. */ +export interface LabelCanvasHandle { + alignSelectionToLabel: (axis: AlignAxis) => void; +} + +export const LabelCanvas = forwardRef(function LabelCanvas({ unit, showGrid, onGridToggle, @@ -62,7 +74,7 @@ export function LabelCanvas({ onZoomChange, viewRotation, onViewRotationChange, -}: Props) { +}, ref) { const containerRef = useRef(null); const stageRef = useRef(null); const transformerRef = useRef(null); @@ -246,13 +258,73 @@ export function LabelCanvas({ // here matches the visual (rotation-aware) bounds — snap math operates in // stage-screen space, so it must reflect what the user sees, not the // un-rotated layout coordinates. - const transformerSnapLabelRect = { - id: "_lbl", - x: visualLabelX, - y: visualLabelY, - width: visualLabelWidthPx, - height: visualLabelHeightPx, - }; + const transformerSnapLabelRect = useMemo( + () => ({ + id: "_lbl", + x: visualLabelX, + y: visualLabelY, + width: visualLabelWidthPx, + height: visualLabelHeightPx, + }), + [visualLabelX, visualLabelY, visualLabelWidthPx, visualLabelHeightPx], + ); + + // Imperative align-to-label: PropertiesPanel calls this directly via the + // forwarded ref. Co-located with the render data — measure each selected + // node's rendered bbox via Konva clientRect (single source of truth for + // type-specific footprints like text baselines / barcode text-zones), + // compute the centre-delta in screen px, map back through view rotation + // and px-per-dot to model coordinates. + useImperativeHandle( + ref, + () => ({ + alignSelectionToLabel: (axis) => { + const stage = stageRef.current; + if (!stage) return; + const state = useLabelStore.getState(); + const ids = state.selectedIds; + if (ids.length === 0) return; + const objs = currentObjects(state); + + const boxes = ids.flatMap((id) => { + const node = stage.findOne(`#${id}`); + if (!node) return []; + const r = node.getClientRect({ relativeTo: stage }); + return [{ id, x: r.x, y: r.y, width: r.width, height: r.height }]; + }); + if (boxes.length === 0) return; + + const { dx: screenDx, dy: screenDy } = computeGroupCenterDelta( + boxes, + transformerSnapLabelRect, + axis, + ); + if (screenDx === 0 && screenDy === 0) return; + + const [layoutDx, layoutDy] = inverseRotateDelta( + screenDx, + screenDy, + viewRotation, + ); + // Round to integer dots — matches the `mmToDots` convention used by + // PropertiesPanel inputs and keeps the store's x/y invariant so ZPL + // emit doesn't see fractional coordinates. + const pxPerDot = scale / label.dpmm; + const dxDots = Math.round(layoutDx / pxPerDot); + const dyDots = Math.round(layoutDy / pxPerDot); + + const updates = ids.flatMap((id) => { + const obj = objs.find((o) => o.id === id); + if (!obj) return []; + return [ + { id, changes: { x: obj.x + dxDots, y: obj.y + dyDots } }, + ]; + }); + if (updates.length > 0) updateObjects(updates); + }, + }), + [transformerSnapLabelRect, scale, label.dpmm, viewRotation, updateObjects], + ); const { rotateEnabled, @@ -673,4 +745,4 @@ export function LabelCanvas({ )} ); -} +}); diff --git a/src/components/Properties/AlignButtons.tsx b/src/components/Properties/AlignButtons.tsx new file mode 100644 index 00000000..db83945f --- /dev/null +++ b/src/components/Properties/AlignButtons.tsx @@ -0,0 +1,39 @@ +import { useT } from "../../lib/useT"; +import type { AlignAxis } from "../../lib/alignment"; +import { CenterHIcon, CenterVIcon, CenterBothIcon } from "./AlignIcons"; + +const BUTTON_CLS = + "p-1.5 rounded border border-border text-muted hover:text-text hover:bg-surface-2 transition-colors"; + +/** + * Three-button row that centres the current selection on the label. Pure + * presentation: the caller supplies the imperative `onAlign` handler so the + * component stays decoupled from the canvas (which owns the live render + * bboxes). + */ +export function AlignButtons({ onAlign }: { onAlign: (axis: AlignAxis) => void }) { + const t = useT(); + + const buttons: { axis: AlignAxis; title: string; Icon: typeof CenterHIcon }[] = [ + { axis: "h", title: t.properties.alignCenterH, Icon: CenterHIcon }, + { axis: "v", title: t.properties.alignCenterV, Icon: CenterVIcon }, + { axis: "both", title: t.properties.alignCenterBoth, Icon: CenterBothIcon }, + ]; + + return ( +
+ {buttons.map(({ axis, title, Icon }) => ( + + ))} +
+ ); +} diff --git a/src/components/Properties/AlignIcons.tsx b/src/components/Properties/AlignIcons.tsx new file mode 100644 index 00000000..39638c62 --- /dev/null +++ b/src/components/Properties/AlignIcons.tsx @@ -0,0 +1,34 @@ +interface IconProps { + className?: string; +} + +/** Object centred on a vertical dashed line — represents horizontal centring. */ +export function CenterHIcon({ className }: IconProps) { + return ( + + ); +} + +/** Object centred on a horizontal dashed line — represents vertical centring. */ +export function CenterVIcon({ className }: IconProps) { + return ( + + ); +} + +/** Object on the cross-hair of two dashed lines — represents bi-axial centring. */ +export function CenterBothIcon({ className }: IconProps) { + return ( + + ); +} diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index 2698b3fb..b6fbde2e 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -1,5 +1,8 @@ +import type { RefObject } from "react"; import { InformationCircleIcon } from "@heroicons/react/16/solid"; import { useLabelStore, useCurrentObjects } from "../../store/labelStore"; +import type { LabelCanvasHandle } from "../Canvas/LabelCanvas"; +import type { AlignAxis } from "../../lib/alignment"; import { ObjectRegistry } from "../../registry"; import { BWIP_VISUAL_APPROX_TYPES } from "../Canvas/bwipConstants"; import { stripZplCommandChars } from "../../registry/zplHelpers"; @@ -15,10 +18,19 @@ import type { Unit } from "../../lib/units"; import { useT } from "../../lib/useT"; import { parseIntOrUndef } from "../../lib/inputParse"; import { CollapsibleSection } from "../ui/CollapsibleSection"; +import { AlignButtons } from "./AlignButtons"; import { inputCls, labelCls } from "./styles"; import type { LabelConfig } from "../../types/ObjectType"; -export function PropertiesPanel() { +interface PropertiesPanelProps { + /** Imperative handle on the canvas — used for actions that need live render + * bboxes (alignment, future zoom-to-selection, etc.). Required so the + * type system forces the caller to wire it up; the inner null-check on + * `.current` only covers the brief window before LabelCanvas mounts. */ + canvasRef: RefObject; +} + +export function PropertiesPanel({ canvasRef }: PropertiesPanelProps) { const t = useT(); const { selectedIds, @@ -31,6 +43,8 @@ export function PropertiesPanel() { const objects = useCurrentObjects(); const unit = canvasSettings.unit; const obj = objects.find((o) => o.id === selectedIds[0]); + const handleAlign = (axis: AlignAxis) => + canvasRef.current?.alignSelectionToLabel(axis); if (selectedIds.length > 1) { return ( @@ -41,9 +55,12 @@ export function PropertiesPanel() { {t.properties.multipleSelectedFmt.replace('{n}', String(selectedIds.length))} -

- {t.properties.x} / {t.properties.y}: {t.properties.multipleSelectedHint} -

+
+

+ {t.properties.x} / {t.properties.y}: {t.properties.multipleSelectedHint} +

+ +
); } @@ -127,6 +144,7 @@ export function PropertiesPanel() { /> +
diff --git a/src/lib/alignment.test.ts b/src/lib/alignment.test.ts new file mode 100644 index 00000000..3981d437 --- /dev/null +++ b/src/lib/alignment.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { computeGroupCenterDelta } from "./alignment"; + +const target = { x: 0, y: 0, width: 100, height: 60 }; + +describe("computeGroupCenterDelta", () => { + it("returns zero delta for empty input", () => { + expect(computeGroupCenterDelta([], target, "both")).toEqual({ dx: 0, dy: 0 }); + }); + + it("centres a single box on both axes", () => { + const box = { id: "a", x: 0, y: 0, width: 20, height: 10 }; + expect(computeGroupCenterDelta([box], target, "both")).toEqual({ + dx: 40, + dy: 25, + }); + }); + + it("only shifts the requested axis", () => { + const box = { id: "a", x: 0, y: 0, width: 20, height: 10 }; + expect(computeGroupCenterDelta([box], target, "h")).toEqual({ dx: 40, dy: 0 }); + expect(computeGroupCenterDelta([box], target, "v")).toEqual({ dx: 0, dy: 25 }); + }); + + it("centres the group bbox of a multi-select, preserving relative positions", () => { + // Two boxes at x=10..30 and x=50..60 → group bbox 10..60 (width 50) + // Centre target (width 100) → group should land at 25..75 → dx = +15 + const boxes = [ + { id: "a", x: 10, y: 5, width: 20, height: 10 }, + { id: "b", x: 50, y: 20, width: 10, height: 10 }, + ]; + const { dx } = computeGroupCenterDelta(boxes, target, "h"); + expect(dx).toBe(15); + }); + + it("handles bboxes that already exceed the target (no clamp)", () => { + const box = { id: "a", x: -10, y: 0, width: 200, height: 10 }; + // Centre 200-wide box on 100-wide target → leftEdge target = -50, dx = -40 + expect(computeGroupCenterDelta([box], target, "h").dx).toBe(-40); + }); + + it("respects target offset (target not anchored at origin)", () => { + const offsetTarget = { x: 100, y: 100, width: 100, height: 60 }; + const box = { id: "a", x: 100, y: 100, width: 20, height: 10 }; + expect(computeGroupCenterDelta([box], offsetTarget, "both")).toEqual({ + dx: 40, + dy: 25, + }); + }); +}); diff --git a/src/lib/alignment.ts b/src/lib/alignment.ts new file mode 100644 index 00000000..30ff1a28 --- /dev/null +++ b/src/lib/alignment.ts @@ -0,0 +1,59 @@ +/** + * Group-bbox alignment math, decoupled from Konva and the store. Given a + * collection of object footprints (rendered bboxes, expressed in any unit- + * consistent space) plus a target label rect, returns the position deltas + * required to centre the group bbox within the label. + * + * Multi-select uses the union of all bboxes and shifts every object by the + * same delta — matches Figma's "align to canvas" behaviour for grouped + * selections, where children keep their relative positions. + */ + +export type AlignAxis = "h" | "v" | "both"; + +/** Object footprint in the caller's coordinate space (screen pixels or dots). */ +export interface AlignBox { + id: string; + x: number; + y: number; + width: number; + height: number; +} + +export interface AlignTarget { + x: number; + y: number; + width: number; + height: number; +} + +/** + * Compute (dx, dy) — the same shift applied to every member of the group so + * that its combined bbox sits centred inside `target` along the requested + * axis. Returns zero on the inactive axis. Empty input → {0,0}. + */ +export function computeGroupCenterDelta( + boxes: readonly AlignBox[], + target: AlignTarget, + axis: AlignAxis, +): { dx: number; dy: number } { + if (boxes.length === 0) return { dx: 0, dy: 0 }; + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const b of boxes) { + if (b.x < minX) minX = b.x; + if (b.y < minY) minY = b.y; + if (b.x + b.width > maxX) maxX = b.x + b.width; + if (b.y + b.height > maxY) maxY = b.y + b.height; + } + const groupW = maxX - minX; + const groupH = maxY - minY; + + const targetX = target.x + (target.width - groupW) / 2; + const targetY = target.y + (target.height - groupH) / 2; + + return { + dx: axis === "h" || axis === "both" ? targetX - minX : 0, + dy: axis === "v" || axis === "both" ? targetY - minY : 0, + }; +} diff --git a/src/locales/ar.ts b/src/locales/ar.ts index c36a433c..5db7d777 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -52,6 +52,9 @@ const ar = { multipleSelectedFmt: '{n} عناصر مختارة', multipleSelectedHint: 'استخدم أسهم الاتجاه للتحريك', visualApproxHint: 'العرض المرئي تقريبي؛ الأبعاد تطابق طباعة ZPL', + alignCenterH: 'توسيط أفقياً على الملصق', + alignCenterV: 'توسيط رأسياً على الملصق', + alignCenterBoth: 'توسيط على الملصق', }, label: { diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 4fde2ab4..10024910 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -52,6 +52,9 @@ const bg = { multipleSelectedFmt: 'Избрани обекти: {n}', multipleSelectedHint: 'със стрелките местиш', visualApproxHint: 'Визуалното изобразяване е приблизително; размерите съответстват на ZPL отпечатъка', + alignCenterH: 'Хоризонтално центриране върху етикета', + alignCenterV: 'Вертикално центриране върху етикета', + alignCenterBoth: 'Центриране върху етикета', }, label: { diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 76e56115..52645dc6 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -52,6 +52,9 @@ const cs = { multipleSelectedFmt: 'Vybráno objektů: {n}', multipleSelectedHint: 'šipkami posunete', visualApproxHint: 'Vizuální zobrazení je přibližné; rozměry odpovídají tisku ZPL', + alignCenterH: 'Vystředit vodorovně na štítku', + alignCenterV: 'Vystředit svisle na štítku', + alignCenterBoth: 'Vystředit na štítku', }, label: { diff --git a/src/locales/da.ts b/src/locales/da.ts index 0de45051..4d2909af 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -52,6 +52,9 @@ const da = { multipleSelectedFmt: '{n} objekter valgt', multipleSelectedHint: 'piletaster flytter', visualApproxHint: 'Visuel gengivelse er omtrentlig; dimensionerne svarer til ZPL-udskriften', + alignCenterH: 'Centrer vandret på label', + alignCenterV: 'Centrer lodret på label', + alignCenterBoth: 'Centrer på label', }, label: { diff --git a/src/locales/de.ts b/src/locales/de.ts index b004eed1..420df759 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -52,6 +52,9 @@ const de = { multipleSelectedFmt: '{n} Objekte ausgewählt', multipleSelectedHint: 'Pfeiltasten zum Verschieben', visualApproxHint: 'Visuelle Darstellung näherungsweise; Maße entsprechen dem ZPL-Druck', + alignCenterH: 'Horizontal auf Label zentrieren', + alignCenterV: 'Vertikal auf Label zentrieren', + alignCenterBoth: 'Auf Label zentrieren', }, label: { diff --git a/src/locales/el.ts b/src/locales/el.ts index 52d57817..827878c2 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -52,6 +52,9 @@ const el = { multipleSelectedFmt: '{n} αντικείμενα επιλέχθηκαν', multipleSelectedHint: 'τα βέλη μετακινούν', visualApproxHint: 'Η οπτική απόδοση είναι κατά προσέγγιση· οι διαστάσεις αντιστοιχούν στην εκτύπωση ZPL', + alignCenterH: 'Οριζόντιο κεντράρισμα στην ετικέτα', + alignCenterV: 'Κατακόρυφο κεντράρισμα στην ετικέτα', + alignCenterBoth: 'Κεντράρισμα στην ετικέτα', }, label: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 73aeb8c6..daa38f8d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -52,6 +52,9 @@ const en = { multipleSelectedFmt: '{n} objects selected', multipleSelectedHint: 'use arrow keys to move', visualApproxHint: 'Visual rendering approximate; dimensions match the ZPL print', + alignCenterH: 'Center horizontally on label', + alignCenterV: 'Center vertically on label', + alignCenterBoth: 'Center on label', }, label: { diff --git a/src/locales/es.ts b/src/locales/es.ts index 8123d922..317a8654 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -52,6 +52,9 @@ const es = { multipleSelectedFmt: '{n} objetos seleccionados', multipleSelectedHint: 'flechas para mover', visualApproxHint: 'Renderizado visual aproximado; las dimensiones coinciden con la impresión ZPL', + alignCenterH: 'Centrar horizontalmente en la etiqueta', + alignCenterV: 'Centrar verticalmente en la etiqueta', + alignCenterBoth: 'Centrar en la etiqueta', }, label: { diff --git a/src/locales/et.ts b/src/locales/et.ts index 7ba44eb5..3fe3f0fe 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -52,6 +52,9 @@ const et = { multipleSelectedFmt: '{n} objekti valitud', multipleSelectedHint: 'nooltega liigutad', visualApproxHint: 'Visuaalne kuva on ligikaudne; mõõtmed vastavad ZPL-väljatrükile', + alignCenterH: 'Keskjoonda sildil rõhtsalt', + alignCenterV: 'Keskjoonda sildil püstiselt', + alignCenterBoth: 'Keskjoonda sildil', }, label: { diff --git a/src/locales/fa.ts b/src/locales/fa.ts index e6eeb293..dc99e558 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -52,6 +52,9 @@ const fa = { multipleSelectedFmt: '{n} مورد انتخاب شده', multipleSelectedHint: 'با کلیدهای جهت‌دار جابه‌جا کنید', visualApproxHint: 'نمایش بصری تقریبی است؛ ابعاد با چاپ ZPL مطابقت دارد', + alignCenterH: 'وسط‌چین افقی روی برچسب', + alignCenterV: 'وسط‌چین عمودی روی برچسب', + alignCenterBoth: 'وسط‌چین برچسب', }, label: { diff --git a/src/locales/fi.ts b/src/locales/fi.ts index e44c69db..9c3c7534 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -52,6 +52,9 @@ const fi = { multipleSelectedFmt: '{n} objektia valittu', multipleSelectedHint: 'nuolinäppäimillä siirrät', visualApproxHint: 'Visuaalinen esitys on likimääräinen; mitat vastaavat ZPL-tulostetta', + alignCenterH: 'Keskitä vaakasuunnassa etikettiin', + alignCenterV: 'Keskitä pystysuunnassa etikettiin', + alignCenterBoth: 'Keskitä etikettiin', }, label: { diff --git a/src/locales/fr.ts b/src/locales/fr.ts index af83b6ec..edb460ba 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -52,6 +52,9 @@ const fr = { multipleSelectedFmt: '{n} objets sélectionnés', multipleSelectedHint: 'flèches pour déplacer', visualApproxHint: 'Rendu visuel approximatif ; les dimensions correspondent à l\'impression ZPL', + alignCenterH: 'Centrer horizontalement sur l\'étiquette', + alignCenterV: 'Centrer verticalement sur l\'étiquette', + alignCenterBoth: 'Centrer sur l\'étiquette', }, label: { diff --git a/src/locales/he.ts b/src/locales/he.ts index 7986c14b..1e093cc7 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -52,6 +52,9 @@ const he = { multipleSelectedFmt: '{n} פריטים נבחרו', multipleSelectedHint: 'מקשי החצים מזיזים', visualApproxHint: 'התצוגה החזותית מקורבת; הממדים תואמים את הדפסת ה-ZPL', + alignCenterH: 'מרכז אופקית במדבקה', + alignCenterV: 'מרכז אנכית במדבקה', + alignCenterBoth: 'מרכז במדבקה', }, label: { diff --git a/src/locales/hr.ts b/src/locales/hr.ts index 46d5e533..45200e92 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -52,6 +52,9 @@ const hr = { multipleSelectedFmt: 'Odabrano objekata: {n}', multipleSelectedHint: 'strelicama pomičeš', visualApproxHint: 'Vizualni prikaz je približan; dimenzije odgovaraju ZPL ispisu', + alignCenterH: 'Centriraj vodoravno na naljepnici', + alignCenterV: 'Centriraj okomito na naljepnici', + alignCenterBoth: 'Centriraj na naljepnici', }, label: { diff --git a/src/locales/hu.ts b/src/locales/hu.ts index cdd2b2b5..ccec1246 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -52,6 +52,9 @@ const hu = { multipleSelectedFmt: '{n} objektum kijelölve', multipleSelectedHint: 'nyilakkal mozgasd', visualApproxHint: 'A vizuális megjelenítés közelítő; a méretek megegyeznek a ZPL nyomtatással', + alignCenterH: 'Vízszintesen középre a címkén', + alignCenterV: 'Függőlegesen középre a címkén', + alignCenterBoth: 'Igazítás a címkén középre', }, label: { diff --git a/src/locales/it.ts b/src/locales/it.ts index 2e511354..7e09afb6 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -52,6 +52,9 @@ const it = { multipleSelectedFmt: '{n} oggetti selezionati', multipleSelectedHint: 'frecce per spostare', visualApproxHint: 'Rendering visivo approssimato; le dimensioni corrispondono alla stampa ZPL', + alignCenterH: 'Centra orizzontalmente sull\'etichetta', + alignCenterV: 'Centra verticalmente sull\'etichetta', + alignCenterBoth: 'Centra sull\'etichetta', }, label: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 9edff78c..929419f7 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -52,6 +52,9 @@ const ja = { multipleSelectedFmt: '{n} 個のオブジェクトが選択されました', multipleSelectedHint: '矢印キーで移動', visualApproxHint: '視覚的表示は概略です。寸法は ZPL 印刷と一致します', + alignCenterH: 'ラベル上で水平方向に中央揃え', + alignCenterV: 'ラベル上で垂直方向に中央揃え', + alignCenterBoth: 'ラベル中央に配置', }, label: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 4db12362..6a46bdf3 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -52,6 +52,9 @@ const ko = { multipleSelectedFmt: '{n}개 항목 선택됨', multipleSelectedHint: '화살표 키로 이동', visualApproxHint: '시각적 표시는 근사치이며, 치수는 ZPL 인쇄와 일치합니다', + alignCenterH: '라벨에서 가로 가운데 정렬', + alignCenterV: '라벨에서 세로 가운데 정렬', + alignCenterBoth: '라벨 가운데 정렬', }, label: { diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 0e732121..33e07159 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -52,6 +52,9 @@ const lt = { multipleSelectedFmt: 'Pasirinkta objektų: {n}', multipleSelectedHint: 'rodyklėmis perkeli', visualApproxHint: 'Vaizdavimas apytikslis; matmenys atitinka ZPL spaudinį', + alignCenterH: 'Centruoti horizontaliai etiketėje', + alignCenterV: 'Centruoti vertikaliai etiketėje', + alignCenterBoth: 'Centruoti etiketėje', }, label: { diff --git a/src/locales/lv.ts b/src/locales/lv.ts index f3fe1f65..a9d65a35 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -52,6 +52,9 @@ const lv = { multipleSelectedFmt: 'Atlasīti {n} objekti', multipleSelectedHint: 'ar bultiņām pārvietot', visualApproxHint: 'Vizuālais attēlojums ir aptuvens; izmēri atbilst ZPL izdrukai', + alignCenterH: 'Centrēt horizontāli uz etiķetes', + alignCenterV: 'Centrēt vertikāli uz etiķetes', + alignCenterBoth: 'Centrēt uz etiķetes', }, label: { diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 5834c104..53db985c 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -52,6 +52,9 @@ const nl = { multipleSelectedFmt: '{n} objecten geselecteerd', multipleSelectedHint: 'pijltoetsen om te verplaatsen', visualApproxHint: 'Visuele weergave bij benadering; afmetingen komen overeen met de ZPL-afdruk', + alignCenterH: 'Horizontaal op label centreren', + alignCenterV: 'Verticaal op label centreren', + alignCenterBoth: 'Op label centreren', }, label: { diff --git a/src/locales/no.ts b/src/locales/no.ts index 2176f13c..8166f7ae 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -52,6 +52,9 @@ const no = { multipleSelectedFmt: '{n} objekter valgt', multipleSelectedHint: 'piltaster flytter', visualApproxHint: 'Visuell gjengivelse er omtrentlig; dimensjonene samsvarer med ZPL-utskriften', + alignCenterH: 'Sentrer horisontalt på etiketten', + alignCenterV: 'Sentrer vertikalt på etiketten', + alignCenterBoth: 'Sentrer på etiketten', }, label: { diff --git a/src/locales/pl.ts b/src/locales/pl.ts index e15e9d8d..d0c89ff5 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -52,6 +52,9 @@ const pl = { multipleSelectedFmt: 'Wybrano obiektów: {n}', multipleSelectedHint: 'strzałki przesuwają', visualApproxHint: 'Renderowanie wizualne jest przybliżone; wymiary odpowiadają wydrukowi ZPL', + alignCenterH: 'Wyśrodkuj poziomo na etykiecie', + alignCenterV: 'Wyśrodkuj pionowo na etykiecie', + alignCenterBoth: 'Wyśrodkuj na etykiecie', }, label: { diff --git a/src/locales/pt.ts b/src/locales/pt.ts index f5762cf8..849bb82b 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -52,6 +52,9 @@ const pt = { multipleSelectedFmt: '{n} objetos selecionados', multipleSelectedHint: 'setas para mover', visualApproxHint: 'Renderização visual aproximada; as dimensões correspondem à impressão ZPL', + alignCenterH: 'Centralizar horizontalmente no rótulo', + alignCenterV: 'Centralizar verticalmente no rótulo', + alignCenterBoth: 'Centralizar no rótulo', }, label: { diff --git a/src/locales/ro.ts b/src/locales/ro.ts index ffcb56b2..fd46accb 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -52,6 +52,9 @@ const ro = { multipleSelectedFmt: '{n} obiecte selectate', multipleSelectedHint: 'săgeți pentru mutare', visualApproxHint: 'Randarea vizuală este aproximativă; dimensiunile corespund tipăririi ZPL', + alignCenterH: 'Centrează orizontal pe etichetă', + alignCenterV: 'Centrează vertical pe etichetă', + alignCenterBoth: 'Centrează pe etichetă', }, label: { diff --git a/src/locales/sk.ts b/src/locales/sk.ts index 52f7e375..7114d5f4 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -52,6 +52,9 @@ const sk = { multipleSelectedFmt: 'Vybraných objektov: {n}', multipleSelectedHint: 'šípkami posuniete', visualApproxHint: 'Vizuálne zobrazenie je približné; rozmery zodpovedajú tlači ZPL', + alignCenterH: 'Vycentrovať vodorovne na štítku', + alignCenterV: 'Vycentrovať zvislo na štítku', + alignCenterBoth: 'Vycentrovať na štítku', }, label: { diff --git a/src/locales/sl.ts b/src/locales/sl.ts index 2b6e866f..e2080758 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -52,6 +52,9 @@ const sl = { multipleSelectedFmt: 'Izbranih objektov: {n}', multipleSelectedHint: 's puščicami premikaš', visualApproxHint: 'Vizualni prikaz je približen; mere se ujemajo s tiskom ZPL', + alignCenterH: 'Vodoravno sredinsko poravnaj na etiketi', + alignCenterV: 'Navpično sredinsko poravnaj na etiketi', + alignCenterBoth: 'Sredinsko poravnaj na etiketi', }, label: { diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 78d51cc8..2b6f54b0 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -52,6 +52,9 @@ const sr = { multipleSelectedFmt: 'Изабрано објеката: {n}', multipleSelectedHint: 'стрелицама померај', visualApproxHint: 'Визуелни приказ је приближан; димензије одговарају ZPL отиску', + alignCenterH: 'Хоризонтално центрирање на етикети', + alignCenterV: 'Вертикално центрирање на етикети', + alignCenterBoth: 'Центрирање на етикети', }, label: { diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 3fc5598d..9b6b2ca8 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -52,6 +52,9 @@ const sv = { multipleSelectedFmt: '{n} objekt markerade', multipleSelectedHint: 'pilar för att flytta', visualApproxHint: 'Visuell återgivning är ungefärlig; måtten matchar ZPL-utskriften', + alignCenterH: 'Centrera horisontellt på etikett', + alignCenterV: 'Centrera vertikalt på etikett', + alignCenterBoth: 'Centrera på etikett', }, label: { diff --git a/src/locales/tr.ts b/src/locales/tr.ts index 287dd9e8..f940b3e9 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -52,6 +52,9 @@ const tr = { multipleSelectedFmt: '{n} nesne seçildi', multipleSelectedHint: 'oklarla taşı', visualApproxHint: 'Görsel render yaklaşıktır; boyutlar ZPL çıktısıyla eşleşir', + alignCenterH: 'Etikette yatay ortala', + alignCenterV: 'Etikette dikey ortala', + alignCenterBoth: 'Etikete ortala', }, label: { diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index e5ffb499..06e5a3c6 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -52,6 +52,9 @@ const zhHans = { multipleSelectedFmt: '已选择 {n} 个对象', multipleSelectedHint: '方向键移动', visualApproxHint: '视觉渲染为近似值;尺寸与 ZPL 打印输出一致', + alignCenterH: '在标签上水平居中', + alignCenterV: '在标签上垂直居中', + alignCenterBoth: '在标签上居中', }, label: { diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index 72d6e39f..d0ff35f8 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -52,6 +52,9 @@ const zhHant = { multipleSelectedFmt: '已選擇 {n} 個物件', multipleSelectedHint: '方向鍵移動', visualApproxHint: '視覺呈現為近似值;尺寸與 ZPL 列印輸出一致', + alignCenterH: '於標籤水平置中', + alignCenterV: '於標籤垂直置中', + alignCenterBoth: '於標籤置中', }, label: {