hazo_images 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGE_LOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # hazo_images Change Log
2
2
 
3
+ ## 1.11.0 — 2026-09-19
4
+
5
+ ### Changed
6
+
7
+ Migrated this package's i18n bridge onto the canonical `hazo_core/i18n`
8
+ (`createPackageT`/`resolvePackageLocale`), deleting the local
9
+ `src/lib/create_package_t.ts` copy. Same shape (flat, literal-dotted
10
+ `src/locales/en.json` keys, `{{var}}` mustache interpolation) — this is an
11
+ internal wiring change, not a behavior change for consumers, and the four
12
+ UI components (`ImageUploader`, `ImageViewer`, `ImageEditorDialog`,
13
+ `ImageAnnotator`) now each call `createPackageT('hazo_images', en)` from
14
+ `hazo_core/i18n` instead of the deleted local file. `hazo_i18n` peer range
15
+ bumped `^0.3.0` → `^0.5.0` (still optional). `hazo_core` peer/devDep bumped
16
+ `^2.3.0` → `^2.4.0` — `hazo_core/i18n` doesn't exist before 2.4.0, so the
17
+ old range was unsatisfiable for this migration.
18
+
19
+ Also newly exports `t`, `createPackageT`, `resolvePackageLocale`, and the
20
+ `TFunc` type from `hazo_images/ui` — previously the bridge wasn't reachable
21
+ from any public entry point, which blocked writing an autotest against the
22
+ real catalog. Consumers that want this package's own translator (e.g. to
23
+ extend/override strings) can now import it directly instead of re-deriving
24
+ it from `src/locales/en.json`.
25
+
26
+ ### Fixed
27
+
28
+ - `ImageEditorDialog`'s color-input hex-value `aria-label` concatenated a raw
29
+ English string (`` `${label} hex value` ``) instead of going through `t()`.
30
+ Extracted into a new catalog key, `editor.hex_value_label` (`"{{label}}
31
+ hex value"`), and wired through `t('editor.hex_value_label', { label })`.
32
+
33
+ ### Added
34
+
35
+ - `test-app` `/autotest` page: new `hazo_images_i18n` scenario group
36
+ (catalog loads, all keys resolve, interpolation, coverage threshold).
37
+
38
+ ## 1.10.0 — 2026-08-04
39
+
40
+ ### Fixed
41
+
42
+ Moved one package from optional `peerDependencies` to `dependencies`. It is
43
+ imported statically at the top level of this package's own `dist/`, but was
44
+ declared as an *optional* peer — and npm never auto-installs an optional peer,
45
+ under any `legacy-peer-deps` setting. The result was a package that could not
46
+ run as shipped: a consumer installed it, imported it, and the build failed with
47
+ module-not-found on a package it had never heard of and had no reason to
48
+ declare. It is an implementation detail of this package, so this package now
49
+ brings it.
50
+
51
+ - react-konva@^18.2.16 || ^19.2.5
52
+
53
+ Dropped the `optional` flag on `react` in `peerDependenciesMeta`. It is
54
+ imported statically and this package cannot render without it, so marking
55
+ it optional understated a hard requirement. It stays a peer — the host controls the
56
+ version and a single instance must be shared.
57
+
58
+
3
59
  ## 1.9.0 — 2026-07-21
4
60
 
5
61
  ### Added
@@ -1,3 +1,5 @@
1
+ import * as hazo_core_i18n from 'hazo_core/i18n';
2
+ export { TFunc, createPackageT, resolvePackageLocale } from 'hazo_core/i18n';
1
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
4
  import * as React from 'react';
3
5
 
@@ -406,4 +408,6 @@ declare function cn(...parts: Array<string | false | null | undefined>): string;
406
408
  */
407
409
  declare function canDecodeImage(file: File): Promise<boolean>;
408
410
 
409
- export { type AnnotatorShape, type AnnotatorTool, type ArrowShape, type CircleShape, DEFAULT_IMAGE_EDIT_CONTROLS, ImageAnnotator, type ImageAnnotatorProps, type ImageEditControls, type ImageEditTab, ImageEditorDialog, type ImageEditorDialogProps, type ImageProcessOptions, type ImageProcessResult, type ImageProcessThumbnail, ImageUploader, type ImageUploaderProps, ImageViewer, type ImageViewerProps, type InputOption, type PenShape, type ProcessFn, type ResizeBox, type ResizeHandle, ResizeHandleOverlay, type ResizeHandleOverlayProps, type TabElement, type ValidationError, type ValidationResult, addShape, canDecodeImage, clearShapes, cn, controlValuesToOptions, defaultControlValues, undoShape, validate_image_edit_controls };
411
+ declare const t: hazo_core_i18n.TFunc;
412
+
413
+ export { type AnnotatorShape, type AnnotatorTool, type ArrowShape, type CircleShape, DEFAULT_IMAGE_EDIT_CONTROLS, ImageAnnotator, type ImageAnnotatorProps, type ImageEditControls, type ImageEditTab, ImageEditorDialog, type ImageEditorDialogProps, type ImageProcessOptions, type ImageProcessResult, type ImageProcessThumbnail, ImageUploader, type ImageUploaderProps, ImageViewer, type ImageViewerProps, type InputOption, type PenShape, type ProcessFn, type ResizeBox, type ResizeHandle, ResizeHandleOverlay, type ResizeHandleOverlayProps, type TabElement, type ValidationError, type ValidationResult, addShape, canDecodeImage, clearShapes, cn, controlValuesToOptions, defaultControlValues, t, undoShape, validate_image_edit_controls };
package/dist/ui/index.js CHANGED
@@ -1,3 +1,67 @@
1
+ // src/locales/en.json
2
+ var en_default = {
3
+ "uploader.default_label": "Drop an image here, or click to browse",
4
+ "uploader.replace_hint": "Click or drag to replace",
5
+ "uploader.browse": "Browse files",
6
+ "viewer.before": "Before",
7
+ "viewer.after": "After",
8
+ "annotator.toolbar_aria": "Annotation tools",
9
+ "annotator.tool.pen.label": "Pen",
10
+ "annotator.tool.pen.hint": "Freehand pen \u2014 draw by dragging",
11
+ "annotator.tool.arrow.label": "Arrow",
12
+ "annotator.tool.arrow.hint": "Arrow \u2014 drag from tail to head",
13
+ "annotator.tool.circle.label": "Circle",
14
+ "annotator.tool.circle.hint": "Circle \u2014 drag to size an ellipse",
15
+ "annotator.tool.bubble.label": "Speech bubble",
16
+ "annotator.tool.bubble.hint": "Speech bubble \u2014 drag to size, then type",
17
+ "annotator.tool.text.label": "Text",
18
+ "annotator.tool.text.hint": "Text \u2014 click to place, then type",
19
+ "annotator.color": "Color",
20
+ "annotator.color_swatch_aria": "Color {{color}}",
21
+ "annotator.custom_color": "Custom",
22
+ "annotator.stroke_width": "Stroke width",
23
+ "annotator.size": "Size",
24
+ "annotator.width_px": "{{width}}px",
25
+ "annotator.undo": "Undo",
26
+ "annotator.redo": "Redo",
27
+ "annotator.clear": "Clear",
28
+ "annotator.clear_all_tooltip": "Clear all",
29
+ "annotator.save_tooltip": "Flatten image + markup to PNG",
30
+ "annotator.save": "Save",
31
+ "annotator.loading": "Loading image\u2026",
32
+ "annotator.text_placeholder": "Type\u2026",
33
+ "editor.title": "Edit image",
34
+ "editor.trigger": "Edit image",
35
+ "editor.save": "Save",
36
+ "editor.cancel": "Cancel",
37
+ "editor.processing": "Processing\u2026",
38
+ "editor.reset": "Reset",
39
+ "editor.crop": "Crop image",
40
+ "editor.crop_done": "Done",
41
+ "editor.color_clear": "Clear",
42
+ "editor.hex_value_default": "Hex value",
43
+ "editor.hex_value_label": "{{label}} hex value",
44
+ "editor.crop_drag_hint": "Drag the box to move \xB7 drag a handle to resize",
45
+ "editor.preview_alt": "Edited preview",
46
+ "editor.original_alt": "Original image",
47
+ "editor.preview_badge": "Preview",
48
+ "editor.original_badge": "Original",
49
+ "editor.no_image": "No image",
50
+ "editor.shape.rectangle": "Rectangle",
51
+ "editor.shape.square": "Square",
52
+ "editor.shape.circle": "Circle",
53
+ "editor.crop_section_label": "Crop",
54
+ "editor.crop_clear": "Clear",
55
+ "editor.shape_label": "Shape",
56
+ "editor.crop_draw_hint": "Click \u201C{{cropLabel}}\u201D to draw a crop region on the image.",
57
+ "editor.saved": "Saved: {{path}}",
58
+ "editor.crop_original_alt": "Crop original",
59
+ "editor.network_error": "Network error"
60
+ };
61
+
62
+ // src/ui/index.ts
63
+ import { createPackageT as createPackageT5 } from "hazo_core/i18n";
64
+
1
65
  // src/ui/image-uploader.tsx
2
66
  import * as React from "react";
3
67
  import { Button } from "hazo_ui";
@@ -8,12 +72,14 @@ function cn(...parts) {
8
72
  }
9
73
 
10
74
  // src/ui/image-uploader.tsx
75
+ import { createPackageT } from "hazo_core/i18n";
11
76
  import { jsx, jsxs } from "react/jsx-runtime";
77
+ var t = createPackageT("hazo_images", en_default);
12
78
  function ImageUploader({
13
79
  onSelect,
14
80
  accept = "image/*",
15
81
  className,
16
- label = "Drop an image here, or click to browse",
82
+ label = t("uploader.default_label"),
17
83
  disabled = false
18
84
  }) {
19
85
  const inputRef = React.useRef(null);
@@ -96,7 +162,7 @@ function ImageUploader({
96
162
  ),
97
163
  selectedFilename ? /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
98
164
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700", children: selectedFilename }),
99
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: "Click or drag to replace" })
165
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: t("uploader.replace_hint") })
100
166
  ] }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 text-center", children: label }),
101
167
  /* @__PURE__ */ jsx(
102
168
  Button,
@@ -109,7 +175,7 @@ function ImageUploader({
109
175
  e.stopPropagation();
110
176
  handleClick();
111
177
  },
112
- children: "Browse files"
178
+ children: t("uploader.browse")
113
179
  }
114
180
  )
115
181
  ]
@@ -120,7 +186,9 @@ function ImageUploader({
120
186
  // src/ui/image-viewer.tsx
121
187
  import * as React2 from "react";
122
188
  import { Card, CardContent, CardHeader, CardTitle } from "hazo_ui";
189
+ import { createPackageT as createPackageT2 } from "hazo_core/i18n";
123
190
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
191
+ var t2 = createPackageT2("hazo_images", en_default);
124
192
  function ZoomableImage({
125
193
  src,
126
194
  alt,
@@ -150,8 +218,8 @@ function ImageViewer({
150
218
  before,
151
219
  after,
152
220
  className,
153
- beforeLabel = "Before",
154
- afterLabel = "After",
221
+ beforeLabel = t2("viewer.before"),
222
+ afterLabel = t2("viewer.after"),
155
223
  zoomable = true
156
224
  }) {
157
225
  if (!before && !after) return null;
@@ -201,6 +269,7 @@ import {
201
269
  TabsTrigger,
202
270
  TabsContent
203
271
  } from "hazo_ui";
272
+ import { createPackageT as createPackageT3 } from "hazo_core/i18n";
204
273
 
205
274
  // src/ui/resize-handle-overlay.tsx
206
275
  import * as React3 from "react";
@@ -727,6 +796,7 @@ function controlValuesToOptions(values, controls) {
727
796
 
728
797
  // src/ui/image-editor-dialog.tsx
729
798
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
799
+ var t3 = createPackageT3("hazo_images", en_default);
730
800
  var MIN_BOX = 16;
731
801
  var CSS_RENDERED_KEYS = [
732
802
  "rotate",
@@ -826,10 +896,10 @@ function ControlInput({ element, value, onChange }) {
826
896
  placeholder: "#ff0000",
827
897
  onChange: (e) => onChange(name, e.target.value),
828
898
  className: "w-28",
829
- "aria-label": label ? `${label} hex value` : "Hex value"
899
+ "aria-label": label ? t3("editor.hex_value_label", { label }) : t3("editor.hex_value_default")
830
900
  }
831
901
  ),
832
- value.trim() !== "" && /* @__PURE__ */ jsx4(Button2, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange(name, ""), children: "Clear" })
902
+ value.trim() !== "" && /* @__PURE__ */ jsx4(Button2, { type: "button", variant: "ghost", size: "sm", onClick: () => onChange(name, ""), children: t3("editor.color_clear") })
833
903
  ] });
834
904
  }
835
905
  if (input_type === "textbox") {
@@ -933,7 +1003,7 @@ function CropBox({ src, crop, shape, onCropChange }) {
933
1003
  {
934
1004
  ref: imgRef,
935
1005
  src,
936
- alt: "Crop original",
1006
+ alt: t3("editor.crop_original_alt"),
937
1007
  className: "block h-auto w-auto max-h-[62vh] max-w-full object-contain",
938
1008
  draggable: false,
939
1009
  onLoad: measure
@@ -1035,7 +1105,7 @@ function ImageEditorDialog({
1035
1105
  onSave?.(res);
1036
1106
  }
1037
1107
  } catch (err) {
1038
- const msg = err instanceof Error ? err.message : "Network error";
1108
+ const msg = err instanceof Error ? err.message : t3("editor.network_error");
1039
1109
  setError(msg);
1040
1110
  setResult({ ok: false, error: msg, errorType: "NetworkError" });
1041
1111
  } finally {
@@ -1073,13 +1143,13 @@ function ImageEditorDialog({
1073
1143
  function handleOpenChange(nextOpen) {
1074
1144
  onOpenChange?.(nextOpen);
1075
1145
  }
1076
- const resolvedTitle = labels?.title ?? "Edit image";
1077
- const resolvedSaveLabel = labels?.save ?? "Save";
1078
- const resolvedCancelLabel = labels?.cancel ?? "Cancel";
1079
- const resolvedProcessingLabel = labels?.processing ?? "Processing\u2026";
1080
- const resolvedResetLabel = labels?.reset ?? "Reset";
1081
- const resolvedCropLabel = labels?.crop ?? "Crop image";
1082
- const resolvedCropDoneLabel = labels?.cropDone ?? "Done";
1146
+ const resolvedTitle = labels?.title ?? t3("editor.title");
1147
+ const resolvedSaveLabel = labels?.save ?? t3("editor.save");
1148
+ const resolvedCancelLabel = labels?.cancel ?? t3("editor.cancel");
1149
+ const resolvedProcessingLabel = labels?.processing ?? t3("editor.processing");
1150
+ const resolvedResetLabel = labels?.reset ?? t3("editor.reset");
1151
+ const resolvedCropLabel = labels?.crop ?? t3("editor.crop");
1152
+ const resolvedCropDoneLabel = labels?.cropDone ?? t3("editor.crop_done");
1083
1153
  const inCropMode = cropMode && Boolean(beforeUrl);
1084
1154
  const previewSrc = result?.ok ? result.preview : void 0;
1085
1155
  const stageSrc = previewSrc ?? beforeUrl ?? void 0;
@@ -1104,13 +1174,13 @@ function ImageEditorDialog({
1104
1174
  const stage = /* @__PURE__ */ jsxs3("div", { className: "relative flex min-h-[52vh] flex-1 items-center justify-center overflow-hidden rounded-lg bg-gray-900 p-4 lg:min-h-[64vh] lg:max-h-[72vh]", children: [
1105
1175
  inCropMode && beforeUrl ? /* @__PURE__ */ jsxs3("div", { className: "flex max-h-full flex-col items-center gap-2", children: [
1106
1176
  /* @__PURE__ */ jsx4(CropBox, { src: beforeUrl, crop, shape: cropShape, onCropChange: setCrop }),
1107
- /* @__PURE__ */ jsx4("span", { className: "text-xs text-gray-400", children: "Drag the box to move \xB7 drag a handle to resize" })
1177
+ /* @__PURE__ */ jsx4("span", { className: "text-xs text-gray-400", children: t3("editor.crop_drag_hint") })
1108
1178
  ] }) : stageSrc ? /* @__PURE__ */ jsxs3("div", { className: "relative inline-block", style: { transform: geometryTransform }, children: [
1109
1179
  /* @__PURE__ */ jsx4(
1110
1180
  "img",
1111
1181
  {
1112
1182
  src: stageSrc,
1113
- alt: stageIsPreview ? "Edited preview" : "Original image",
1183
+ alt: stageIsPreview ? t3("editor.preview_alt") : t3("editor.original_alt"),
1114
1184
  className: "block max-h-[68vh] max-w-full object-contain",
1115
1185
  style: { filter: cssFilter },
1116
1186
  draggable: false
@@ -1123,14 +1193,14 @@ function ImageEditorDialog({
1123
1193
  style: { backgroundColor: tintColor, mixBlendMode: "color" }
1124
1194
  }
1125
1195
  )
1126
- ] }) : /* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-500", children: "No image" }),
1127
- !inCropMode && stageSrc && /* @__PURE__ */ jsx4("span", { className: "absolute left-3 top-3 rounded-full bg-black/60 px-2.5 py-1 text-xs font-medium text-gray-200", children: stageIsPreview ? "Preview" : "Original" }),
1196
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-500", children: t3("editor.no_image") }),
1197
+ !inCropMode && stageSrc && /* @__PURE__ */ jsx4("span", { className: "absolute left-3 top-3 rounded-full bg-black/60 px-2.5 py-1 text-xs font-medium text-gray-200", children: stageIsPreview ? t3("editor.preview_badge") : t3("editor.original_badge") }),
1128
1198
  isProcessing && /* @__PURE__ */ jsx4("span", { className: "absolute right-3 top-3 rounded-full bg-black/60 px-2.5 py-1 text-xs font-medium text-gray-200", children: resolvedProcessingLabel })
1129
1199
  ] });
1130
1200
  const shapeOptions = [
1131
- { value: "rect", label: "Rectangle" },
1132
- { value: "square", label: "Square" },
1133
- { value: "circle", label: "Circle" }
1201
+ { value: "rect", label: t3("editor.shape.rectangle") },
1202
+ { value: "square", label: t3("editor.shape.square") },
1203
+ { value: "circle", label: t3("editor.shape.circle") }
1134
1204
  ];
1135
1205
  const rail = /* @__PURE__ */ jsxs3("div", { className: "flex w-full flex-col gap-4 px-1 lg:w-80 lg:shrink-0 lg:max-h-[72vh] lg:overflow-y-auto", children: [
1136
1206
  error && /* @__PURE__ */ jsx4("div", { className: "rounded-md bg-red-50 border border-red-200 px-4 py-2 text-sm text-red-700", children: error }),
@@ -1150,7 +1220,7 @@ function ImageEditorDialog({
1150
1220
  ] }, el.name)),
1151
1221
  tab.tab_name === "Geometry" && beforeUrl && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-3 mt-1 border-t border-gray-100 pt-3", children: [
1152
1222
  /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
1153
- /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium text-gray-700", children: "Crop" }),
1223
+ /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium text-gray-700", children: t3("editor.crop_section_label") }),
1154
1224
  /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
1155
1225
  crop && /* @__PURE__ */ jsx4(
1156
1226
  Button2,
@@ -1162,7 +1232,7 @@ function ImageEditorDialog({
1162
1232
  setCrop(null);
1163
1233
  setCropMode(false);
1164
1234
  },
1165
- children: "Clear"
1235
+ children: t3("editor.crop_clear")
1166
1236
  }
1167
1237
  ),
1168
1238
  /* @__PURE__ */ jsx4(
@@ -1178,7 +1248,7 @@ function ImageEditorDialog({
1178
1248
  ] })
1179
1249
  ] }),
1180
1250
  cropMode && /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-1.5", children: [
1181
- /* @__PURE__ */ jsx4(Label, { className: "text-sm text-gray-700", children: "Shape" }),
1251
+ /* @__PURE__ */ jsx4(Label, { className: "text-sm text-gray-700", children: t3("editor.shape_label") }),
1182
1252
  /* @__PURE__ */ jsx4("div", { className: "flex gap-1", children: shapeOptions.map((opt) => /* @__PURE__ */ jsx4(
1183
1253
  Button2,
1184
1254
  {
@@ -1203,18 +1273,11 @@ function ImageEditorDialog({
1203
1273
  "\xD7",
1204
1274
  crop.height,
1205
1275
  "px"
1206
- ] }) : /* @__PURE__ */ jsxs3("p", { className: "text-xs text-gray-500", children: [
1207
- "Click \u201C",
1208
- resolvedCropLabel,
1209
- "\u201D to draw a crop region on the image."
1210
- ] })
1276
+ ] }) : /* @__PURE__ */ jsx4("p", { className: "text-xs text-gray-500", children: t3("editor.crop_draw_hint", { cropLabel: resolvedCropLabel }) })
1211
1277
  ] })
1212
1278
  ] }) }, tab.tab_name))
1213
1279
  ] }),
1214
- result?.ok && result.saved && /* @__PURE__ */ jsxs3("p", { className: "text-xs text-green-700 bg-green-50 rounded p-2 break-all", children: [
1215
- "Saved: ",
1216
- result.saved.path ?? result.saved.url
1217
- ] }),
1280
+ result?.ok && result.saved && /* @__PURE__ */ jsx4("p", { className: "text-xs text-green-700 bg-green-50 rounded p-2 break-all", children: t3("editor.saved", { path: result.saved.path ?? result.saved.url ?? "" }) }),
1218
1281
  result?.saveError && /* @__PURE__ */ jsx4("p", { className: "text-xs text-red-600", children: result.saveError })
1219
1282
  ] });
1220
1283
  const dialogBody = /* @__PURE__ */ jsxs3(Fragment, { children: [
@@ -1267,7 +1330,7 @@ function ImageEditorDialog({
1267
1330
  return /* @__PURE__ */ jsx4(Dialog, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsx4(DialogContent, { className: cn("max-w-5xl", className), children: dialogBody }) });
1268
1331
  }
1269
1332
  return /* @__PURE__ */ jsxs3(Dialog, { onOpenChange: handleOpenChange, children: [
1270
- /* @__PURE__ */ jsx4(DialogTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx4(Button2, { type: "button", children: "Edit image" }) }),
1333
+ /* @__PURE__ */ jsx4(DialogTrigger, { asChild: true, children: trigger ?? /* @__PURE__ */ jsx4(Button2, { type: "button", children: t3("editor.trigger") }) }),
1271
1334
  /* @__PURE__ */ jsx4(DialogContent, { className: cn("max-w-5xl", className), children: dialogBody })
1272
1335
  ] });
1273
1336
  }
@@ -1336,7 +1399,9 @@ function redo(shapes, redoStack) {
1336
1399
  }
1337
1400
 
1338
1401
  // src/ui/image-annotator.tsx
1402
+ import { createPackageT as createPackageT4 } from "hazo_core/i18n";
1339
1403
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1404
+ var t4 = createPackageT4("hazo_images", en_default);
1340
1405
  var TOOL_IDS = ["pen", "arrow", "circle", "bubble", "text"];
1341
1406
  var DEFAULT_STROKE_COLOR = "#ef4444";
1342
1407
  var DEFAULT_STROKE_WIDTH = 4;
@@ -1355,11 +1420,11 @@ var PRESET_COLORS = [
1355
1420
  var BUBBLE_PADDING = 10;
1356
1421
  var fontSizeFor = (strokeWidth) => Math.max(12, Math.round(strokeWidth * 4));
1357
1422
  var TOOL_META = {
1358
- pen: { Icon: Pencil, label: "Pen", hint: "Freehand pen \u2014 draw by dragging" },
1359
- arrow: { Icon: MoveUpRight, label: "Arrow", hint: "Arrow \u2014 drag from tail to head" },
1360
- circle: { Icon: CircleIcon, label: "Circle", hint: "Circle \u2014 drag to size an ellipse" },
1361
- bubble: { Icon: MessageSquare, label: "Speech bubble", hint: "Speech bubble \u2014 drag to size, then type" },
1362
- text: { Icon: TypeIcon, label: "Text", hint: "Text \u2014 click to place, then type" }
1423
+ pen: { Icon: Pencil, label: t4("annotator.tool.pen.label"), hint: t4("annotator.tool.pen.hint") },
1424
+ arrow: { Icon: MoveUpRight, label: t4("annotator.tool.arrow.label"), hint: t4("annotator.tool.arrow.hint") },
1425
+ circle: { Icon: CircleIcon, label: t4("annotator.tool.circle.label"), hint: t4("annotator.tool.circle.hint") },
1426
+ bubble: { Icon: MessageSquare, label: t4("annotator.tool.bubble.label"), hint: t4("annotator.tool.bubble.hint") },
1427
+ text: { Icon: TypeIcon, label: t4("annotator.tool.text.label"), hint: t4("annotator.tool.text.hint") }
1363
1428
  };
1364
1429
  function bubbleTailPoints(width, height) {
1365
1430
  const baseX = Math.min(28, width * 0.5);
@@ -1672,12 +1737,12 @@ function ImageAnnotator({
1672
1737
  });
1673
1738
  }
1674
1739
  const resolvedLabels = {
1675
- save: labels?.save ?? "Save",
1676
- loading: labels?.loading ?? "Loading image\u2026"
1740
+ save: labels?.save ?? t4("annotator.save"),
1741
+ loading: labels?.loading ?? t4("annotator.loading")
1677
1742
  };
1678
1743
  const iconBtn = "h-10 w-10";
1679
1744
  return /* @__PURE__ */ jsx5(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs4("div", { className: cn("flex flex-col gap-3", className), children: [
1680
- /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1.5", role: "toolbar", "aria-label": "Annotation tools", children: [
1745
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1.5", role: "toolbar", "aria-label": t4("annotator.toolbar_aria"), children: [
1681
1746
  TOOL_IDS.map((id) => {
1682
1747
  const { Icon, label, hint } = TOOL_META[id];
1683
1748
  return /* @__PURE__ */ jsxs4(Tooltip, { children: [
@@ -1700,13 +1765,13 @@ function ImageAnnotator({
1700
1765
  /* @__PURE__ */ jsx5("span", { className: "mx-1 h-6 w-px self-center bg-gray-300", "aria-hidden": "true" }),
1701
1766
  /* @__PURE__ */ jsxs4(Tooltip, { children: [
1702
1767
  /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(Popover, { children: [
1703
- /* @__PURE__ */ jsx5(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(Button3, { type: "button", size: "icon", variant: "outline", className: iconBtn, "aria-label": "Color", children: /* @__PURE__ */ jsx5(Palette, { size: 18, style: { color } }) }) }),
1768
+ /* @__PURE__ */ jsx5(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(Button3, { type: "button", size: "icon", variant: "outline", className: iconBtn, "aria-label": t4("annotator.color"), children: /* @__PURE__ */ jsx5(Palette, { size: 18, style: { color } }) }) }),
1704
1769
  /* @__PURE__ */ jsx5(PopoverContent, { className: "w-auto p-3", children: /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-3", children: [
1705
1770
  /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-4 gap-2", children: PRESET_COLORS.map((c) => /* @__PURE__ */ jsx5(
1706
1771
  "button",
1707
1772
  {
1708
1773
  type: "button",
1709
- "aria-label": `Color ${c}`,
1774
+ "aria-label": t4("annotator.color_swatch_aria", { color: c }),
1710
1775
  onClick: () => setColor(c),
1711
1776
  className: cn(
1712
1777
  "h-7 w-7 rounded-full border border-gray-300",
@@ -1717,7 +1782,7 @@ function ImageAnnotator({
1717
1782
  c
1718
1783
  )) }),
1719
1784
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-sm", children: [
1720
- /* @__PURE__ */ jsx5("span", { className: "text-gray-600", children: "Custom" }),
1785
+ /* @__PURE__ */ jsx5("span", { className: "text-gray-600", children: t4("annotator.custom_color") }),
1721
1786
  /* @__PURE__ */ jsx5(
1722
1787
  "input",
1723
1788
  {
@@ -1730,18 +1795,15 @@ function ImageAnnotator({
1730
1795
  ] })
1731
1796
  ] }) })
1732
1797
  ] }) }),
1733
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Color" })
1798
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.color") })
1734
1799
  ] }),
1735
1800
  /* @__PURE__ */ jsxs4(Tooltip, { children: [
1736
1801
  /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(Popover, { children: [
1737
- /* @__PURE__ */ jsx5(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(Button3, { type: "button", size: "icon", variant: "outline", className: iconBtn, "aria-label": "Stroke width", children: /* @__PURE__ */ jsx5(SlidersHorizontal, { size: 18 }) }) }),
1802
+ /* @__PURE__ */ jsx5(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(Button3, { type: "button", size: "icon", variant: "outline", className: iconBtn, "aria-label": t4("annotator.stroke_width"), children: /* @__PURE__ */ jsx5(SlidersHorizontal, { size: 18 }) }) }),
1738
1803
  /* @__PURE__ */ jsx5(PopoverContent, { className: "w-56 p-4", children: /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-3", children: [
1739
1804
  /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between text-sm text-gray-600", children: [
1740
- /* @__PURE__ */ jsx5("span", { children: "Size" }),
1741
- /* @__PURE__ */ jsxs4("span", { className: "font-medium text-gray-900", children: [
1742
- width,
1743
- "px"
1744
- ] })
1805
+ /* @__PURE__ */ jsx5("span", { children: t4("annotator.size") }),
1806
+ /* @__PURE__ */ jsx5("span", { className: "font-medium text-gray-900", children: t4("annotator.width_px", { width: String(width) }) })
1745
1807
  ] }),
1746
1808
  /* @__PURE__ */ jsx5(
1747
1809
  Slider2,
@@ -1751,7 +1813,7 @@ function ImageAnnotator({
1751
1813
  step: 1,
1752
1814
  value: [width],
1753
1815
  onValueChange: ([v]) => setWidth(v),
1754
- "aria-label": "Stroke width"
1816
+ "aria-label": t4("annotator.stroke_width")
1755
1817
  }
1756
1818
  ),
1757
1819
  /* @__PURE__ */ jsx5("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx5(
@@ -1763,7 +1825,7 @@ function ImageAnnotator({
1763
1825
  ) })
1764
1826
  ] }) })
1765
1827
  ] }) }),
1766
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Stroke width" })
1828
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.stroke_width") })
1767
1829
  ] }),
1768
1830
  /* @__PURE__ */ jsx5("span", { className: "mx-1 h-6 w-px self-center bg-gray-300", "aria-hidden": "true" }),
1769
1831
  /* @__PURE__ */ jsxs4(Tooltip, { children: [
@@ -1774,13 +1836,13 @@ function ImageAnnotator({
1774
1836
  size: "icon",
1775
1837
  variant: "outline",
1776
1838
  className: iconBtn,
1777
- "aria-label": "Undo",
1839
+ "aria-label": t4("annotator.undo"),
1778
1840
  disabled: shapes.length === 0 && !editing,
1779
1841
  onClick: handleUndo,
1780
1842
  children: /* @__PURE__ */ jsx5(Undo2, { size: 18 })
1781
1843
  }
1782
1844
  ) }),
1783
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Undo" })
1845
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.undo") })
1784
1846
  ] }),
1785
1847
  /* @__PURE__ */ jsxs4(Tooltip, { children: [
1786
1848
  /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(
@@ -1790,13 +1852,13 @@ function ImageAnnotator({
1790
1852
  size: "icon",
1791
1853
  variant: "outline",
1792
1854
  className: iconBtn,
1793
- "aria-label": "Redo",
1855
+ "aria-label": t4("annotator.redo"),
1794
1856
  disabled: redoStack.length === 0,
1795
1857
  onClick: handleRedo,
1796
1858
  children: /* @__PURE__ */ jsx5(Redo2, { size: 18 })
1797
1859
  }
1798
1860
  ) }),
1799
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Redo" })
1861
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.redo") })
1800
1862
  ] }),
1801
1863
  /* @__PURE__ */ jsxs4(Tooltip, { children: [
1802
1864
  /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(
@@ -1806,13 +1868,13 @@ function ImageAnnotator({
1806
1868
  size: "icon",
1807
1869
  variant: "outline",
1808
1870
  className: iconBtn,
1809
- "aria-label": "Clear",
1871
+ "aria-label": t4("annotator.clear"),
1810
1872
  disabled: shapes.length === 0 && !editing,
1811
1873
  onClick: handleClear,
1812
1874
  children: /* @__PURE__ */ jsx5(Trash2, { size: 18 })
1813
1875
  }
1814
1876
  ) }),
1815
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Clear all" })
1877
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.clear_all_tooltip") })
1816
1878
  ] }),
1817
1879
  onSave && /* @__PURE__ */ jsxs4(Tooltip, { children: [
1818
1880
  /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs4(
@@ -1828,7 +1890,7 @@ function ImageAnnotator({
1828
1890
  ]
1829
1891
  }
1830
1892
  ) }),
1831
- /* @__PURE__ */ jsx5(TooltipContent, { children: "Flatten image + markup to PNG" })
1893
+ /* @__PURE__ */ jsx5(TooltipContent, { children: t4("annotator.save_tooltip") })
1832
1894
  ] })
1833
1895
  ] }),
1834
1896
  /* @__PURE__ */ jsx5(
@@ -1877,7 +1939,7 @@ function ImageAnnotator({
1877
1939
  cancelEditing();
1878
1940
  }
1879
1941
  },
1880
- placeholder: "Type\u2026",
1942
+ placeholder: t4("annotator.text_placeholder"),
1881
1943
  className: "absolute resize-none rounded border border-dashed border-gray-400 bg-white/90 p-1 leading-tight shadow-sm outline-none",
1882
1944
  style: {
1883
1945
  left: editing.x + (editing.kind === "bubble" ? BUBBLE_PADDING : 0),
@@ -1911,6 +1973,10 @@ function canDecodeImage(file) {
1911
1973
  img.src = url;
1912
1974
  });
1913
1975
  }
1976
+
1977
+ // src/ui/index.ts
1978
+ import { createPackageT as createPackageT6, resolvePackageLocale } from "hazo_core/i18n";
1979
+ var t5 = createPackageT5("hazo_images", en_default);
1914
1980
  export {
1915
1981
  DEFAULT_IMAGE_EDIT_CONTROLS,
1916
1982
  ImageAnnotator,
@@ -1923,7 +1989,10 @@ export {
1923
1989
  clearShapes,
1924
1990
  cn,
1925
1991
  controlValuesToOptions,
1992
+ createPackageT6 as createPackageT,
1926
1993
  defaultControlValues,
1994
+ resolvePackageLocale,
1995
+ t5 as t,
1927
1996
  undoShape,
1928
1997
  validate_image_edit_controls
1929
1998
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_images",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,24 +48,28 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "ini": "^4.1.0",
51
+ "react-konva": "^18.2.16 || ^19.2.5",
51
52
  "zod": "^4.1.12"
52
53
  },
53
54
  "peerDependencies": {
54
- "hazo_core": "^1.3.0",
55
- "hazo_files": "^3.1.1",
56
- "hazo_theme": "^1.0.1",
57
- "hazo_ui": "^6.3.1",
55
+ "hazo_core": "^2.4.0",
56
+ "hazo_files": "^3.4.0",
57
+ "hazo_i18n": "^0.5.0",
58
+ "hazo_theme": "^1.4.0",
59
+ "hazo_ui": "^6.12.0",
58
60
  "konva": "^10.3.0",
59
61
  "lucide-react": "^0.553.0",
60
62
  "react": "^18.0.0 || ^19.0.0",
61
63
  "react-dom": "^18.0.0 || ^19.0.0",
62
- "react-konva": "^18.2.16 || ^19.2.5",
63
64
  "sharp": "^0.33.0"
64
65
  },
65
66
  "peerDependenciesMeta": {
66
67
  "hazo_files": {
67
68
  "optional": true
68
69
  },
70
+ "hazo_i18n": {
71
+ "optional": true
72
+ },
69
73
  "lucide-react": {
70
74
  "optional": true
71
75
  },
@@ -75,15 +79,9 @@
75
79
  "konva": {
76
80
  "optional": true
77
81
  },
78
- "react": {
79
- "optional": true
80
- },
81
82
  "react-dom": {
82
83
  "optional": true
83
84
  },
84
- "react-konva": {
85
- "optional": true
86
- },
87
85
  "sharp": {
88
86
  "optional": true
89
87
  },
@@ -95,7 +93,7 @@
95
93
  "@types/jest": "^30.0.0",
96
94
  "@types/node": "^22.10.0",
97
95
  "@types/react": "^18.3.3",
98
- "hazo_core": "^1.3.0",
96
+ "hazo_core": "^2.4.0",
99
97
  "jest": "^30.2.0",
100
98
  "jest-environment-node": "^30.2.0",
101
99
  "konva": "^10.3.0",