sanity-plugin-image-resizer 1.0.3 → 1.1.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.
@@ -1,4 +1,70 @@
1
1
  "use strict";
2
- var resources = {};
2
+ var resources = {
3
+ // ── Tool ────────────────────────────────────────────────────────────────
4
+ /** Tool title shown in the Studio sidebar */
5
+ "tool.title": "Image Resizer",
6
+ // ── Header ──────────────────────────────────────────────────────────────
7
+ /** Main heading on the tool page */
8
+ "header.title": "Image Resizer",
9
+ /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */
10
+ "header.description": "Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.",
11
+ // ── Actions ─────────────────────────────────────────────────────────────
12
+ /** Refresh button label */
13
+ "action.refresh": "Refresh",
14
+ /** Process-all button label ({{count}} = number of pending assets) */
15
+ "action.process-all": "Process All ({{count}})",
16
+ /** Label for the button that lets the current in-flight tasks finish */
17
+ "action.finish-ongoing": "Finish ongoing tasks ({{count}})",
18
+ /** Label for the stop button that cancels the queue immediately */
19
+ "action.stop-all": "Stop All (possible data loss)",
20
+ // ── Status badges ───────────────────────────────────────────────────────
21
+ /** Pending badge ({{count}} = number) */
22
+ "status.pending": "{{count}} pending",
23
+ /** Processing badge */
24
+ "status.processing": "{{count}} processing",
25
+ /** Done badge */
26
+ "status.done": "{{count}} done",
27
+ /** Failed badge */
28
+ "status.failed": "{{count}} failed",
29
+ // ── Empty / loading states ──────────────────────────────────────────────
30
+ /** Shown while scanning assets */
31
+ "state.scanning": "Scanning assets\u2026",
32
+ /** Shown when no violating assets are found */
33
+ "state.all-good": "All images meet the requirements.",
34
+ // ── Settings dialog ─────────────────────────────────────────────────────
35
+ /** Settings dialog header */
36
+ "settings.title": "Conversion Settings",
37
+ /** PNG → WebP toggle label */
38
+ "settings.png-to-webp": "Convert PNG \u2192 WebP",
39
+ /** TIFF → JPG toggle label */
40
+ "settings.tiff-to-jpg": "Convert TIFF \u2192 JPG (instead of WebP)",
41
+ /** Hint below toggles */
42
+ "settings.apply-hint": "Changes apply on next Refresh.",
43
+ // ── Asset card ──────────────────────────────────────────────────────────
44
+ /** Violation badge: TIFF → WebP */
45
+ "violation.tiff-to-webp": "TIFF \u2192 WebP",
46
+ /** Violation badge: TIFF → JPG */
47
+ "violation.tiff-to-jpg": "TIFF \u2192 JPG",
48
+ /** Violation badge: PNG → WebP */
49
+ "violation.png-to-webp": "PNG \u2192 WebP",
50
+ /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */
51
+ "violation.width": "> {{maxWidth}}px",
52
+ /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */
53
+ "violation.size": "> {{maxSize}} MB",
54
+ /** Asset size summary ({{size}} MB — {{width}}px wide) */
55
+ "asset.summary": "{{size}} MB \u2014 {{width}}px wide",
56
+ /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */
57
+ "asset.summary-done": "{{oldSize}} MB \u2192 {{newSize}} MB{{reduction}} \u2014 {{width}}px wide",
58
+ /** Reduction percentage shown after size (e.g. " (−32%)") */
59
+ "asset.reduction": " (\u2212{{percent}}%)",
60
+ /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */
61
+ "asset.width-reduced": "{{oldWidth}}px \u2192 {{newWidth}}px",
62
+ /** Process button */
63
+ "asset.process": "Process",
64
+ /** Done badge */
65
+ "asset.done": "Done",
66
+ /** Retry button */
67
+ "asset.retry": "Retry"
68
+ };
3
69
  exports.default = resources;
4
70
  //# sourceMappingURL=resources.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resources.js","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {}\n"],"names":[],"mappings":";AAAA,IAAA,YAAe,CAAA;;"}
1
+ {"version":3,"file":"resources.js","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that lets the current in-flight tasks finish */\n 'action.finish-ongoing': 'Finish ongoing tasks ({{count}})',\n /** Label for the stop button that cancels the queue immediately */\n 'action.stop-all': 'Stop All (possible data loss)',\n\n // ── Status badges ───────────────────────────────────────────────────────\n /** Pending badge ({{count}} = number) */\n 'status.pending': '{{count}} pending',\n /** Processing badge */\n 'status.processing': '{{count}} processing',\n /** Done badge */\n 'status.done': '{{count}} done',\n /** Failed badge */\n 'status.failed': '{{count}} failed',\n\n // ── Empty / loading states ──────────────────────────────────────────────\n /** Shown while scanning assets */\n 'state.scanning': 'Scanning assets…',\n /** Shown when no violating assets are found */\n 'state.all-good': 'All images meet the requirements.',\n\n // ── Settings dialog ─────────────────────────────────────────────────────\n /** Settings dialog header */\n 'settings.title': 'Conversion Settings',\n /** PNG → WebP toggle label */\n 'settings.png-to-webp': 'Convert PNG → WebP',\n /** TIFF → JPG toggle label */\n 'settings.tiff-to-jpg': 'Convert TIFF → JPG (instead of WebP)',\n /** Hint below toggles */\n 'settings.apply-hint': 'Changes apply on next Refresh.',\n\n // ── Asset card ──────────────────────────────────────────────────────────\n /** Violation badge: TIFF → WebP */\n 'violation.tiff-to-webp': 'TIFF → WebP',\n /** Violation badge: TIFF → JPG */\n 'violation.tiff-to-jpg': 'TIFF → JPG',\n /** Violation badge: PNG → WebP */\n 'violation.png-to-webp': 'PNG → WebP',\n /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */\n 'violation.width': '> {{maxWidth}}px',\n /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */\n 'violation.size': '> {{maxSize}} MB',\n /** Asset size summary ({{size}} MB — {{width}}px wide) */\n 'asset.summary': '{{size}} MB — {{width}}px wide',\n /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */\n 'asset.summary-done': '{{oldSize}} MB → {{newSize}} MB{{reduction}} — {{width}}px wide',\n /** Reduction percentage shown after size (e.g. \" (−32%)\") */\n 'asset.reduction': ' (−{{percent}}%)',\n /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */\n 'asset.width-reduced': '{{oldWidth}}px → {{newWidth}}px',\n /** Process button */\n 'asset.process': 'Process',\n /** Done badge */\n 'asset.done': 'Done',\n /** Retry button */\n 'asset.retry': 'Retry',\n}\n"],"names":[],"mappings":";AAAA,IAAA,YAAe;AAAA;AAAA;AAAA,EAGX,cAAc;AAAA;AAAA;AAAA,EAId,gBAAgB;AAAA;AAAA,EAEhB,sBACI;AAAA;AAAA;AAAA,EAIJ,kBAAkB;AAAA;AAAA,EAElB,sBAAsB;AAAA;AAAA,EAEtB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA;AAAA,EAInB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA;AAAA,EAIjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,kBAAkB;AAAA;AAAA,EAElB,wBAAwB;AAAA;AAAA,EAExB,wBAAwB;AAAA;AAAA,EAExB,uBAAuB;AAAA;AAAA;AAAA,EAIvB,0BAA0B;AAAA;AAAA,EAE1B,yBAAyB;AAAA;AAAA,EAEzB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,iBAAiB;AAAA;AAAA,EAEjB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA,EAEvB,iBAAiB;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA,EAEd,eAAe;AACnB;;"}
@@ -1,4 +1,70 @@
1
- var resources = {};
1
+ var resources = {
2
+ // ── Tool ────────────────────────────────────────────────────────────────
3
+ /** Tool title shown in the Studio sidebar */
4
+ "tool.title": "Image Resizer",
5
+ // ── Header ──────────────────────────────────────────────────────────────
6
+ /** Main heading on the tool page */
7
+ "header.title": "Image Resizer",
8
+ /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */
9
+ "header.description": "Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.",
10
+ // ── Actions ─────────────────────────────────────────────────────────────
11
+ /** Refresh button label */
12
+ "action.refresh": "Refresh",
13
+ /** Process-all button label ({{count}} = number of pending assets) */
14
+ "action.process-all": "Process All ({{count}})",
15
+ /** Label for the button that lets the current in-flight tasks finish */
16
+ "action.finish-ongoing": "Finish ongoing tasks ({{count}})",
17
+ /** Label for the stop button that cancels the queue immediately */
18
+ "action.stop-all": "Stop All (possible data loss)",
19
+ // ── Status badges ───────────────────────────────────────────────────────
20
+ /** Pending badge ({{count}} = number) */
21
+ "status.pending": "{{count}} pending",
22
+ /** Processing badge */
23
+ "status.processing": "{{count}} processing",
24
+ /** Done badge */
25
+ "status.done": "{{count}} done",
26
+ /** Failed badge */
27
+ "status.failed": "{{count}} failed",
28
+ // ── Empty / loading states ──────────────────────────────────────────────
29
+ /** Shown while scanning assets */
30
+ "state.scanning": "Scanning assets\u2026",
31
+ /** Shown when no violating assets are found */
32
+ "state.all-good": "All images meet the requirements.",
33
+ // ── Settings dialog ─────────────────────────────────────────────────────
34
+ /** Settings dialog header */
35
+ "settings.title": "Conversion Settings",
36
+ /** PNG → WebP toggle label */
37
+ "settings.png-to-webp": "Convert PNG \u2192 WebP",
38
+ /** TIFF → JPG toggle label */
39
+ "settings.tiff-to-jpg": "Convert TIFF \u2192 JPG (instead of WebP)",
40
+ /** Hint below toggles */
41
+ "settings.apply-hint": "Changes apply on next Refresh.",
42
+ // ── Asset card ──────────────────────────────────────────────────────────
43
+ /** Violation badge: TIFF → WebP */
44
+ "violation.tiff-to-webp": "TIFF \u2192 WebP",
45
+ /** Violation badge: TIFF → JPG */
46
+ "violation.tiff-to-jpg": "TIFF \u2192 JPG",
47
+ /** Violation badge: PNG → WebP */
48
+ "violation.png-to-webp": "PNG \u2192 WebP",
49
+ /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */
50
+ "violation.width": "> {{maxWidth}}px",
51
+ /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */
52
+ "violation.size": "> {{maxSize}} MB",
53
+ /** Asset size summary ({{size}} MB — {{width}}px wide) */
54
+ "asset.summary": "{{size}} MB \u2014 {{width}}px wide",
55
+ /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */
56
+ "asset.summary-done": "{{oldSize}} MB \u2192 {{newSize}} MB{{reduction}} \u2014 {{width}}px wide",
57
+ /** Reduction percentage shown after size (e.g. " (−32%)") */
58
+ "asset.reduction": " (\u2212{{percent}}%)",
59
+ /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */
60
+ "asset.width-reduced": "{{oldWidth}}px \u2192 {{newWidth}}px",
61
+ /** Process button */
62
+ "asset.process": "Process",
63
+ /** Done badge */
64
+ "asset.done": "Done",
65
+ /** Retry button */
66
+ "asset.retry": "Retry"
67
+ };
2
68
  export {
3
69
  resources as default
4
70
  };
@@ -1 +1 @@
1
- {"version":3,"file":"resources.mjs","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {}\n"],"names":[],"mappings":"AAAA,IAAA,YAAe,CAAA;"}
1
+ {"version":3,"file":"resources.mjs","sources":["../../src/i18n/resources.ts"],"sourcesContent":["export default {\n // ── Tool ────────────────────────────────────────────────────────────────\n /** Tool title shown in the Studio sidebar */\n 'tool.title': 'Image Resizer',\n\n // ── Header ──────────────────────────────────────────────────────────────\n /** Main heading on the tool page */\n 'header.title': 'Image Resizer',\n /** Description below the heading ({{maxWidth}} = pixel limit, {{maxSize}} = MB limit) */\n 'header.description':\n 'Converts TIFF images to WebP. Resizes/compresses all images to fit within {{maxWidth}}px / {{maxSize}} MB.',\n\n // ── Actions ─────────────────────────────────────────────────────────────\n /** Refresh button label */\n 'action.refresh': 'Refresh',\n /** Process-all button label ({{count}} = number of pending assets) */\n 'action.process-all': 'Process All ({{count}})',\n /** Label for the button that lets the current in-flight tasks finish */\n 'action.finish-ongoing': 'Finish ongoing tasks ({{count}})',\n /** Label for the stop button that cancels the queue immediately */\n 'action.stop-all': 'Stop All (possible data loss)',\n\n // ── Status badges ───────────────────────────────────────────────────────\n /** Pending badge ({{count}} = number) */\n 'status.pending': '{{count}} pending',\n /** Processing badge */\n 'status.processing': '{{count}} processing',\n /** Done badge */\n 'status.done': '{{count}} done',\n /** Failed badge */\n 'status.failed': '{{count}} failed',\n\n // ── Empty / loading states ──────────────────────────────────────────────\n /** Shown while scanning assets */\n 'state.scanning': 'Scanning assets…',\n /** Shown when no violating assets are found */\n 'state.all-good': 'All images meet the requirements.',\n\n // ── Settings dialog ─────────────────────────────────────────────────────\n /** Settings dialog header */\n 'settings.title': 'Conversion Settings',\n /** PNG → WebP toggle label */\n 'settings.png-to-webp': 'Convert PNG → WebP',\n /** TIFF → JPG toggle label */\n 'settings.tiff-to-jpg': 'Convert TIFF → JPG (instead of WebP)',\n /** Hint below toggles */\n 'settings.apply-hint': 'Changes apply on next Refresh.',\n\n // ── Asset card ──────────────────────────────────────────────────────────\n /** Violation badge: TIFF → WebP */\n 'violation.tiff-to-webp': 'TIFF → WebP',\n /** Violation badge: TIFF → JPG */\n 'violation.tiff-to-jpg': 'TIFF → JPG',\n /** Violation badge: PNG → WebP */\n 'violation.png-to-webp': 'PNG → WebP',\n /** Violation badge: exceeds max width ({{maxWidth}} = pixel limit) */\n 'violation.width': '> {{maxWidth}}px',\n /** Violation badge: exceeds max size ({{maxSize}} = MB limit) */\n 'violation.size': '> {{maxSize}} MB',\n /** Asset size summary ({{size}} MB — {{width}}px wide) */\n 'asset.summary': '{{size}} MB — {{width}}px wide',\n /** Asset done summary ({{oldSize}} MB → {{newSize}} MB — {{width}}px wide) */\n 'asset.summary-done': '{{oldSize}} MB → {{newSize}} MB{{reduction}} — {{width}}px wide',\n /** Reduction percentage shown after size (e.g. \" (−32%)\") */\n 'asset.reduction': ' (−{{percent}}%)',\n /** Width reduction badge ({{oldWidth}}px → {{newWidth}}px) */\n 'asset.width-reduced': '{{oldWidth}}px → {{newWidth}}px',\n /** Process button */\n 'asset.process': 'Process',\n /** Done badge */\n 'asset.done': 'Done',\n /** Retry button */\n 'asset.retry': 'Retry',\n}\n"],"names":[],"mappings":"AAAA,IAAA,YAAe;AAAA;AAAA;AAAA,EAGX,cAAc;AAAA;AAAA;AAAA,EAId,gBAAgB;AAAA;AAAA,EAEhB,sBACI;AAAA;AAAA;AAAA,EAIJ,kBAAkB;AAAA;AAAA,EAElB,sBAAsB;AAAA;AAAA,EAEtB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA;AAAA,EAInB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA;AAAA,EAIjB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,kBAAkB;AAAA;AAAA,EAElB,wBAAwB;AAAA;AAAA,EAExB,wBAAwB;AAAA;AAAA,EAExB,uBAAuB;AAAA;AAAA;AAAA,EAIvB,0BAA0B;AAAA;AAAA,EAE1B,yBAAyB;AAAA;AAAA,EAEzB,yBAAyB;AAAA;AAAA,EAEzB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,iBAAiB;AAAA;AAAA,EAEjB,sBAAsB;AAAA;AAAA,EAEtB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA,EAEvB,iBAAiB;AAAA;AAAA,EAEjB,cAAc;AAAA;AAAA,EAEd,eAAe;AACnB;"}
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
66
66
  {}
67
67
  );
68
68
  const record = obj;
69
- return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
69
+ return record.asset?._ref === oldId ? { [path ? `${path}.asset` : "asset"]: { _type: "reference", _ref: newId } } : record._type === "reference" && record._ref === oldId ? { [path || "_ref"]: { _type: "reference", _ref: newId } } : Object.keys(record).filter((k) => !k.startsWith("_")).reduce(
70
70
  (acc, key) => {
71
71
  const childPath = path ? `${path}.${key}` : key;
72
72
  return Object.assign(
@@ -77,6 +77,25 @@ function buildReplacementPatch(obj, oldId, newId, path = "") {
77
77
  {}
78
78
  );
79
79
  }
80
+ const ASSET_METADATA_FIELDS = [
81
+ "title",
82
+ "description",
83
+ "altText",
84
+ "creditLine",
85
+ "source",
86
+ "opt"
87
+ ];
88
+ async function copyAssetMetadata(client, oldAssetId, newAssetId) {
89
+ const projection = ASSET_METADATA_FIELDS.join(", "), oldMeta = await client.fetch(
90
+ `*[_id == $id][0]{ ${projection} }`,
91
+ { id: oldAssetId }
92
+ );
93
+ if (!oldMeta) return;
94
+ const patch = {};
95
+ for (const field of ASSET_METADATA_FIELDS)
96
+ oldMeta[field] !== void 0 && oldMeta[field] !== null && (patch[field] = oldMeta[field]);
97
+ Object.keys(patch).length > 0 && await client.patch(newAssetId).set(patch).commit();
98
+ }
80
99
  const validateImageSize = async (value, context) => {
81
100
  if (!value?.asset?._ref) return !0;
82
101
  const asset = await context.getClient({ apiVersion: "2025-02-19" }).fetch(
@@ -92,21 +111,20 @@ const validateImageSize = async (value, context) => {
92
111
  return `Image size (${sizeMB}MB) exceeds the maximum of ${maxMB}MB`;
93
112
  }
94
113
  return asset.width && asset.width > exports.IMAGE_MAX_WIDTH ? `Image width (${asset.width}px) exceeds the maximum of ${exports.IMAGE_MAX_WIDTH}px` : !0;
95
- }, MAX_SIZE_MB$1 = exports.IMAGE_MAX_SIZE / 1024 / 1024, VIOLATION_LABELS = {
96
- format: "TIFF \u2192 WebP",
97
- width: `> ${exports.IMAGE_MAX_WIDTH}px`,
98
- size: `> ${MAX_SIZE_MB$1} MB`
99
- };
100
- function formatViolationLabel(settings) {
114
+ }, MAX_SIZE_MB$1 = exports.IMAGE_MAX_SIZE / 1024 / 1024;
115
+ function formatViolationLabel(settings, t) {
101
116
  const parts = [];
102
- return parts.push(settings.tiffToJpg ? "TIFF \u2192 JPG" : "TIFF \u2192 WebP"), settings.pngToWebp && parts.push("PNG \u2192 WebP"), parts.join(", ");
117
+ return parts.push(
118
+ settings.tiffToJpg ? t("violation.tiff-to-jpg") : t("violation.tiff-to-webp")
119
+ ), settings.pngToWebp && parts.push(t("violation.png-to-webp")), parts.join(", ");
103
120
  }
104
121
  function ViolationBadge({
105
122
  type,
106
- settings
123
+ settings,
124
+ t
107
125
  }) {
108
- const label = type === "format" ? formatViolationLabel(settings) : VIOLATION_LABELS[type];
109
- return /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "caution", size: 1, children: label });
126
+ let label;
127
+ return type === "format" ? label = formatViolationLabel(settings, t) : type === "width" ? label = t("violation.width", { maxWidth: exports.IMAGE_MAX_WIDTH }) : label = t("violation.size", { maxSize: MAX_SIZE_MB$1 }), /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "caution", size: 1, children: label });
110
128
  }
111
129
  function statusTone(status) {
112
130
  return {
@@ -121,7 +139,7 @@ function AssetCard({
121
139
  onProcess,
122
140
  settings
123
141
  }) {
124
- const isDone = asset.status === "done" && asset.newUrl, thumbUrl = isDone ? asset.newUrl : asset.url, sizeReduction = isDone && asset.newSize != null ? Math.round((1 - asset.newSize / asset.size) * 100) : null;
142
+ const { t } = sanity.useTranslation(imageResizerLocaleNamespace), isDone = asset.status === "done" && asset.newUrl, thumbUrl = isDone ? asset.newUrl : asset.url, sizeReduction = isDone && asset.newSize != null ? Math.round((1 - asset.newSize / asset.size) * 100) : null;
125
143
  return /* @__PURE__ */ jsxRuntime.jsx(
126
144
  ui.Card,
127
145
  {
@@ -145,32 +163,22 @@ function AssetCard({
145
163
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { flex: 1, minWidth: 0 }, children: [
146
164
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: "100%", minWidth: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: isDone ? asset.newFilename || asset.originalFilename || asset._id : asset.originalFilename || asset._id }) }),
147
165
  isDone ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
148
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, style: { wordBreak: "break-word" }, children: [
149
- (asset.size / 1024 / 1024).toFixed(1),
150
- " MB \u2192",
151
- " ",
152
- (asset.newSize / 1024 / 1024).toFixed(1),
153
- " MB",
154
- sizeReduction !== null && sizeReduction > 0 ? ` (\u2212${sizeReduction}%)` : "",
155
- " ",
156
- "\u2014 ",
157
- asset.newWidth,
158
- "px wide"
159
- ] }),
160
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: asset.newWidth != null && asset.newWidth < asset.width && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { tone: "positive", size: 1, children: [
161
- asset.width,
162
- "px \u2192 ",
163
- asset.newWidth,
164
- "px"
165
- ] }) })
166
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, style: { wordBreak: "break-word" }, children: t("asset.summary-done", {
167
+ oldSize: (asset.size / 1024 / 1024).toFixed(1),
168
+ newSize: (asset.newSize / 1024 / 1024).toFixed(1),
169
+ reduction: sizeReduction !== null && sizeReduction > 0 ? t("asset.reduction", { percent: sizeReduction }) : "",
170
+ width: asset.newWidth
171
+ }) }),
172
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: asset.newWidth != null && asset.newWidth < asset.width && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", size: 1, children: t("asset.width-reduced", {
173
+ oldWidth: asset.width,
174
+ newWidth: asset.newWidth
175
+ }) }) })
166
176
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
167
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: asset.violations.map((v) => /* @__PURE__ */ jsxRuntime.jsx(ViolationBadge, { type: v, settings }, v)) }),
168
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, style: { wordBreak: "break-word" }, children: [
169
- (asset.size / 1024 / 1024).toFixed(1),
170
- " MB \u2014 ",
171
- asset.width,
172
- "px wide"
173
- ] })
177
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: asset.violations.map((v) => /* @__PURE__ */ jsxRuntime.jsx(ViolationBadge, { type: v, settings, t }, v)) }),
178
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, style: { wordBreak: "break-word" }, children: t("asset.summary", {
179
+ size: (asset.size / 1024 / 1024).toFixed(1),
180
+ width: asset.width
181
+ }) })
174
182
  ] }),
175
183
  asset.status === "error" && /* @__PURE__ */ jsxRuntime.jsx(
176
184
  ui.Text,
@@ -188,18 +196,18 @@ function AssetCard({
188
196
  asset.status === "idle" && /* @__PURE__ */ jsxRuntime.jsx(
189
197
  ui.Button,
190
198
  {
191
- text: "Process",
199
+ text: t("asset.process"),
192
200
  mode: "ghost",
193
201
  tone: "primary",
194
202
  onClick: () => onProcess(asset)
195
203
  }
196
204
  ),
197
205
  asset.status === "processing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
198
- asset.status === "done" && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", children: "Done" }),
206
+ asset.status === "done" && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", children: t("asset.done") }),
199
207
  asset.status === "error" && /* @__PURE__ */ jsxRuntime.jsx(
200
208
  ui.Button,
201
209
  {
202
- text: "Retry",
210
+ text: t("asset.retry"),
203
211
  mode: "ghost",
204
212
  tone: "critical",
205
213
  onClick: () => onProcess(asset)
@@ -227,7 +235,7 @@ function loadSettings() {
227
235
  return DEFAULT_SETTINGS;
228
236
  }
229
237
  function ImageResizerView() {
230
- const client = sanity.useClient({ apiVersion: "2025-02-19" }), [assets, setAssets] = react.useState([]), [loading, setLoading] = react.useState(!0), [processingAll, setProcessingAll] = react.useState(!1), [settings, setSettings] = react.useState(loadSettings), [showSettings, setShowSettings] = react.useState(!1), updateSettings = react.useCallback(
238
+ const client = sanity.useClient({ apiVersion: "2025-02-19" }), { t } = sanity.useTranslation(imageResizerLocaleNamespace), [assets, setAssets] = react.useState([]), [loading, setLoading] = react.useState(!0), [processingAll, setProcessingAll] = react.useState(!1), [settings, setSettings] = react.useState(loadSettings), [showSettings, setShowSettings] = react.useState(!1), updateSettings = react.useCallback(
231
239
  (updater) => {
232
240
  setSettings((prev) => {
233
241
  const next = updater(prev);
@@ -239,7 +247,7 @@ function ImageResizerView() {
239
247
  react.useEffect(() => {
240
248
  assetsRef.current = assets;
241
249
  }, [assets]);
242
- const fetchAssets = react.useCallback(async () => {
250
+ const cancelRef = react.useRef(!1), fetchAssets = react.useCallback(async () => {
243
251
  setLoading(!0);
244
252
  try {
245
253
  const raw = await client.fetch(
@@ -291,7 +299,9 @@ function ImageResizerView() {
291
299
  const baseName = asset.originalFilename?.replace(/\.[^.]+$/, "") || "image", newAsset = await client.assets.upload("image", blob, {
292
300
  filename: `${baseName}.${outFormat}`,
293
301
  contentType: `image/${outFormat}`
294
- }), refs = await client.fetch(
302
+ });
303
+ await copyAssetMetadata(client, asset._id, newAsset._id);
304
+ const refs = await client.fetch(
295
305
  "*[references($id)]{ _id }",
296
306
  { id: asset._id }
297
307
  );
@@ -320,19 +330,31 @@ function ImageResizerView() {
320
330
  },
321
331
  [client, updateAsset, settings]
322
332
  ), processAll = react.useCallback(async () => {
323
- setProcessingAll(!0);
333
+ cancelRef.current = !1, setProcessingAll(!0);
324
334
  const pending = assetsRef.current.filter(
325
335
  (a) => a.status === "idle" || a.status === "error"
326
336
  );
327
337
  let idx = 0;
328
338
  const next = async () => {
329
- for (; idx < pending.length; ) {
339
+ for (; idx < pending.length && !cancelRef.current; ) {
330
340
  const asset = pending[idx++];
331
341
  await processAsset(asset);
332
342
  }
333
343
  };
334
344
  await Promise.all(Array.from({ length: CONCURRENCY }, () => next())), setProcessingAll(!1);
335
- }, [processAsset]), counts = react.useMemo(
345
+ }, [processAsset]);
346
+ react.useCallback(() => {
347
+ cancelRef.current = !0;
348
+ }, []), react.useEffect(() => {
349
+ if (!processingAll) return;
350
+ const handler = (e) => {
351
+ e.preventDefault();
352
+ };
353
+ return window.addEventListener("beforeunload", handler), () => window.removeEventListener("beforeunload", handler);
354
+ }, [processingAll]), react.useEffect(() => () => {
355
+ cancelRef.current = !0;
356
+ }, []);
357
+ const counts = react.useMemo(
336
358
  () => ({
337
359
  pending: assets.filter((a) => a.status === "idle").length,
338
360
  processing: assets.filter((a) => a.status === "processing").length,
@@ -351,20 +373,17 @@ function ImageResizerView() {
351
373
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
352
374
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [
353
375
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, style: { flex: 1, minWidth: 0 }, children: [
354
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Image Resizer" }),
355
- /* @__PURE__ */ jsxRuntime.jsxs(
376
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: t("header.title") }),
377
+ /* @__PURE__ */ jsxRuntime.jsx(
356
378
  ui.Card,
357
379
  {
358
380
  size: 1,
359
381
  tone: "transparent",
360
382
  style: { wordBreak: "break-word" },
361
- children: [
362
- "Converts TIFF images to WebP. Resizes/compresses all images to fit within ",
363
- exports.IMAGE_MAX_WIDTH,
364
- "px / ",
365
- MAX_SIZE_MB,
366
- " MB."
367
- ]
383
+ children: t("header.description", {
384
+ maxWidth: exports.IMAGE_MAX_WIDTH,
385
+ maxSize: MAX_SIZE_MB
386
+ })
368
387
  }
369
388
  )
370
389
  ] }),
@@ -381,46 +400,44 @@ function ImageResizerView() {
381
400
  /* @__PURE__ */ jsxRuntime.jsx(
382
401
  ui.Button,
383
402
  {
384
- text: "Refresh",
403
+ text: t("action.refresh"),
385
404
  mode: "ghost",
386
405
  onClick: fetchAssets,
387
406
  disabled: loading || processingAll
388
407
  }
389
408
  ),
390
- counts.pending > 0 && /* @__PURE__ */ jsxRuntime.jsx(
409
+ counts.pending > 0 && !processingAll && /* @__PURE__ */ jsxRuntime.jsx(
391
410
  ui.Button,
392
411
  {
393
- text: processingAll ? "Processing\u2026" : `Process All (${counts.pending})`,
412
+ text: t("action.process-all", { count: counts.pending }),
394
413
  tone: "primary",
395
414
  onClick: processAll,
396
- disabled: processingAll || loading,
397
- icon: processingAll ? ui.Spinner : void 0
415
+ disabled: loading
398
416
  }
399
- )
417
+ ),
418
+ processingAll && /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, children: /* @__PURE__ */ jsxRuntime.jsx(
419
+ ui.Button,
420
+ {
421
+ text: t("action.finish-ongoing", {
422
+ count: counts.processing
423
+ }),
424
+ tone: "caution",
425
+ mode: "ghost",
426
+ disabled: !0
427
+ }
428
+ ) })
400
429
  ] })
401
430
  ] }),
402
431
  !loading && assets.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, wrap: "wrap", children: [
403
- counts.pending > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { tone: "caution", children: [
404
- counts.pending,
405
- " pending"
406
- ] }),
407
- counts.processing > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { tone: "primary", children: [
408
- counts.processing,
409
- " processing"
410
- ] }),
411
- counts.done > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { tone: "positive", children: [
412
- counts.done,
413
- " done"
414
- ] }),
415
- counts.error > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { tone: "critical", children: [
416
- counts.error,
417
- " failed"
418
- ] })
432
+ counts.pending > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "caution", children: t("status.pending", { count: counts.pending }) }),
433
+ counts.processing > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", children: t("status.processing", { count: counts.processing }) }),
434
+ counts.done > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", children: t("status.done", { count: counts.done }) }),
435
+ counts.error > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "critical", children: t("status.failed", { count: counts.error }) })
419
436
  ] }),
420
437
  loading ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { padding: 6, justify: "center", align: "center", gap: 3, children: [
421
438
  /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}),
422
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, children: "Scanning assets\u2026" })
423
- ] }) : assets.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 2, tone: "positive", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", children: "All images meet the requirements." }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
439
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, children: t("state.scanning") })
440
+ ] }) : assets.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 2, tone: "positive", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", children: t("state.all-good") }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
424
441
  AssetCard,
425
442
  {
426
443
  asset,
@@ -434,7 +451,7 @@ function ImageResizerView() {
434
451
  ui.Dialog,
435
452
  {
436
453
  id: "image-resizer-settings",
437
- header: "Conversion Settings",
454
+ header: t("settings.title"),
438
455
  onClose: () => setShowSettings(!1),
439
456
  width: 1,
440
457
  children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
@@ -450,7 +467,7 @@ function ImageResizerView() {
450
467
  }
451
468
  }
452
469
  ),
453
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "png-to-webp", style: { cursor: "pointer" }, children: "Convert PNG \u2192 WebP" })
470
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "png-to-webp", style: { cursor: "pointer" }, children: t("settings.png-to-webp") })
454
471
  ] }),
455
472
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
456
473
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -464,9 +481,9 @@ function ImageResizerView() {
464
481
  }
465
482
  }
466
483
  ),
467
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "tiff-to-jpg", style: { cursor: "pointer" }, children: "Convert TIFF \u2192 JPG (instead of WebP)" })
484
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "tiff-to-jpg", style: { cursor: "pointer" }, children: t("settings.tiff-to-jpg") })
468
485
  ] }),
469
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Changes apply on next Refresh." })
486
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: t("settings.apply-hint") })
470
487
  ] }) })
471
488
  }
472
489
  )
@@ -482,7 +499,13 @@ const imageResizerPlugin = sanity.definePlugin(
482
499
  {
483
500
  name: "image-resizer",
484
501
  title: "Image Resizer",
485
- component: ImageResizerView
502
+ component: ImageResizerView,
503
+ i18n: {
504
+ title: {
505
+ key: "tool.title",
506
+ ns: imageResizerLocaleNamespace
507
+ }
508
+ }
486
509
  }
487
510
  ],
488
511
  i18n: {