@smartcat/sanity-plugin 1.0.0 → 1.0.1

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.
Files changed (121) hide show
  1. package/README.md +294 -0
  2. package/dist/_chunks-cjs/constants.cjs +13 -0
  3. package/dist/_chunks-cjs/constants.cjs.map +1 -0
  4. package/dist/_chunks-cjs/index.cjs +168 -0
  5. package/dist/_chunks-cjs/index.cjs.map +1 -0
  6. package/dist/_chunks-cjs/index2.cjs +311 -0
  7. package/dist/_chunks-cjs/index2.cjs.map +1 -0
  8. package/dist/_chunks-cjs/workflow.cjs +16 -0
  9. package/dist/_chunks-cjs/workflow.cjs.map +1 -0
  10. package/dist/_chunks-es/constants.js +14 -0
  11. package/dist/_chunks-es/constants.js.map +1 -0
  12. package/dist/_chunks-es/index.js +169 -0
  13. package/dist/_chunks-es/index.js.map +1 -0
  14. package/dist/_chunks-es/index2.js +313 -0
  15. package/dist/_chunks-es/index2.js.map +1 -0
  16. package/dist/_chunks-es/workflow.js +17 -0
  17. package/dist/_chunks-es/workflow.js.map +1 -0
  18. package/dist/export.cjs +160 -0
  19. package/dist/export.cjs.map +1 -0
  20. package/dist/export.d.cts +162 -0
  21. package/dist/export.d.ts +162 -0
  22. package/dist/export.js +162 -0
  23. package/dist/export.js.map +1 -0
  24. package/dist/import.cjs +169 -0
  25. package/dist/import.cjs.map +1 -0
  26. package/dist/import.d.cts +155 -0
  27. package/dist/import.d.ts +155 -0
  28. package/dist/import.js +169 -0
  29. package/dist/import.js.map +1 -0
  30. package/dist/index.cjs +2103 -0
  31. package/dist/index.cjs.map +1 -0
  32. package/dist/index.d.cts +122 -0
  33. package/dist/index.d.ts +122 -0
  34. package/dist/index.js +2110 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/locjson.cjs +12 -0
  37. package/dist/locjson.cjs.map +1 -0
  38. package/dist/locjson.d.cts +287 -0
  39. package/dist/locjson.d.ts +287 -0
  40. package/dist/locjson.js +12 -0
  41. package/dist/locjson.js.map +1 -0
  42. package/dist/progress.cjs +16 -0
  43. package/dist/progress.cjs.map +1 -0
  44. package/dist/progress.d.cts +262 -0
  45. package/dist/progress.d.ts +262 -0
  46. package/dist/progress.js +16 -0
  47. package/dist/progress.js.map +1 -0
  48. package/dist/smartcat.cjs +287 -0
  49. package/dist/smartcat.cjs.map +1 -0
  50. package/dist/smartcat.d.cts +234 -0
  51. package/dist/smartcat.d.ts +234 -0
  52. package/dist/smartcat.js +287 -0
  53. package/dist/smartcat.js.map +1 -0
  54. package/dist/templates.cjs +12 -0
  55. package/dist/templates.cjs.map +1 -0
  56. package/dist/templates.d.cts +52 -0
  57. package/dist/templates.d.ts +52 -0
  58. package/dist/templates.js +12 -0
  59. package/dist/templates.js.map +1 -0
  60. package/package.json +101 -15
  61. package/src/actions/AddToProjectAction.tsx +258 -0
  62. package/src/export/export.test.ts +537 -0
  63. package/src/export/index.ts +393 -0
  64. package/src/form/TranslationStatusInput.tsx +212 -0
  65. package/src/import/import.test.ts +346 -0
  66. package/src/import/index.ts +418 -0
  67. package/src/index.ts +63 -0
  68. package/src/lib/constants.ts +23 -0
  69. package/src/lib/documentTitle.test.ts +43 -0
  70. package/src/lib/documentTitle.ts +30 -0
  71. package/src/lib/languageMap.test.ts +56 -0
  72. package/src/lib/languageMap.ts +71 -0
  73. package/src/lib/linkedDocuments.test.ts +56 -0
  74. package/src/lib/linkedDocuments.ts +100 -0
  75. package/src/lib/locjson/deserialize.ts +60 -0
  76. package/src/lib/locjson/fields.ts +355 -0
  77. package/src/lib/locjson/filename.ts +27 -0
  78. package/src/lib/locjson/index.ts +10 -0
  79. package/src/lib/locjson/locjson.test.ts +545 -0
  80. package/src/lib/locjson/paths.test.ts +41 -0
  81. package/src/lib/locjson/paths.ts +73 -0
  82. package/src/lib/locjson/portableText.ts +98 -0
  83. package/src/lib/locjson/serialize.ts +124 -0
  84. package/src/lib/locjson/types.ts +106 -0
  85. package/src/lib/log.ts +33 -0
  86. package/src/lib/projectItems.ts +17 -0
  87. package/src/lib/resolveConfig.test.ts +62 -0
  88. package/src/lib/resolveConfig.ts +47 -0
  89. package/src/lib/workflow.test.ts +37 -0
  90. package/src/lib/workflow.ts +48 -0
  91. package/src/lib/zip.fixtures.ts +75 -0
  92. package/src/lib/zip.test.ts +110 -0
  93. package/src/lib/zip.ts +164 -0
  94. package/src/progress/core.ts +387 -0
  95. package/src/progress/index.ts +7 -0
  96. package/src/progress/progress.test.ts +268 -0
  97. package/src/schema/settings.ts +55 -0
  98. package/src/schema/translatableOptions.ts +25 -0
  99. package/src/schema/translationProject.ts +264 -0
  100. package/src/smartcat/client.test.ts +222 -0
  101. package/src/smartcat/client.ts +351 -0
  102. package/src/smartcat/index.ts +10 -0
  103. package/src/smartcat/types.ts +99 -0
  104. package/src/templates/core.ts +55 -0
  105. package/src/templates/index.ts +5 -0
  106. package/src/templates/templates.test.ts +50 -0
  107. package/src/tool/Dashboard.tsx +44 -0
  108. package/src/tool/ItemProgress.tsx +176 -0
  109. package/src/tool/LogPanel.tsx +207 -0
  110. package/src/tool/ProjectView.tsx +975 -0
  111. package/src/tool/ProjectsView.tsx +208 -0
  112. package/src/tool/StatusBadge.tsx +33 -0
  113. package/src/tool/StudioInit.tsx +42 -0
  114. package/src/tool/WorkflowSelect.tsx +41 -0
  115. package/src/tool/data.ts +79 -0
  116. package/src/tool/index.ts +19 -0
  117. package/src/tool/processing.test.ts +623 -0
  118. package/src/tool/processing.ts +766 -0
  119. package/src/tool/useTemplates.ts +84 -0
  120. package/src/tool/useWorkflowSelection.ts +65 -0
  121. package/src/types.ts +77 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var sanity = require("sanity"), icons = require("@sanity/icons"), constants = require("./_chunks-cjs/constants.cjs"), jsxRuntime = require("react/jsx-runtime"), react = require("react"), ui = require("@sanity/ui"), uuid = require("@sanity/uuid"), workflow = require("./_chunks-cjs/workflow.cjs"), progress = require("./_chunks-cjs/index.cjs"), locjson = require("./_chunks-cjs/index2.cjs"), router = require("sanity/router"), styled = require("styled-components");
4
+ function _interopDefaultCompat(e) {
5
+ return e && typeof e == "object" && "default" in e ? e : { default: e };
6
+ }
7
+ var styled__default = /* @__PURE__ */ _interopDefaultCompat(styled);
8
+ const STATUS_OPTIONS = [
9
+ { title: "Draft", value: "draft" },
10
+ { title: "Queued", value: "queued" },
11
+ { title: "Sent", value: "sent" },
12
+ { title: "In progress", value: "translating" },
13
+ { title: "Importing", value: "importing" },
14
+ { title: "Downloaded", value: "downloaded" },
15
+ { title: "Completed", value: "completed" },
16
+ { title: "Error", value: "error" }
17
+ ];
18
+ function createTranslationProjectType() {
19
+ return sanity.defineType({
20
+ name: constants.TRANSLATION_PROJECT_TYPE,
21
+ title: "Translation project",
22
+ type: "document",
23
+ icon: icons.TranslateIcon,
24
+ fields: [
25
+ sanity.defineField({
26
+ name: "name",
27
+ title: "Project name",
28
+ type: "string",
29
+ validation: (rule) => rule.required()
30
+ }),
31
+ sanity.defineField({
32
+ name: "status",
33
+ type: "string",
34
+ // Driven by the dashboard and Functions, not edited by hand.
35
+ readOnly: !0,
36
+ initialValue: "draft",
37
+ options: { list: STATUS_OPTIONS, layout: "radio" }
38
+ }),
39
+ sanity.defineField({
40
+ name: "sourceLanguage",
41
+ title: "Source language",
42
+ type: "string"
43
+ }),
44
+ // Workflow chosen at creation: `template:<id>` or a standard preset.
45
+ // Read by the export Function to build Smartcat's createProject params.
46
+ sanity.defineField({ name: "workflow", type: "string", readOnly: !0, hidden: !0 }),
47
+ sanity.defineField({
48
+ name: "targetLanguages",
49
+ title: "Target languages",
50
+ type: "array",
51
+ of: [sanity.defineArrayMember({ type: "string" })],
52
+ options: { layout: "tags" }
53
+ }),
54
+ // Sanity-id → Smartcat-code mapping for this project's source + target
55
+ // languages, stamped by the browser at export time so the thin Functions
56
+ // (which can't see plugin config) can map to/from Smartcat codes.
57
+ sanity.defineField({
58
+ name: "smartcatLanguages",
59
+ type: "array",
60
+ readOnly: !0,
61
+ hidden: !0,
62
+ of: [
63
+ sanity.defineArrayMember({
64
+ type: "object",
65
+ name: "smartcatLanguageMapping",
66
+ fields: [
67
+ sanity.defineField({ name: "sanityId", type: "string" }),
68
+ sanity.defineField({ name: "smartcatLanguage", type: "string" })
69
+ ]
70
+ })
71
+ ]
72
+ }),
73
+ sanity.defineField({
74
+ name: "items",
75
+ title: "Items",
76
+ description: "Content added to this project for translation.",
77
+ // Populated by the "Add to translation project" action, not by hand.
78
+ readOnly: !0,
79
+ type: "array",
80
+ of: [
81
+ sanity.defineArrayMember({
82
+ type: "object",
83
+ name: "smartcat.projectItem",
84
+ title: "Project item",
85
+ fields: [
86
+ sanity.defineField({ name: "docId", title: "Document ID", type: "string" }),
87
+ sanity.defineField({ name: "docType", title: "Document type", type: "string" })
88
+ ],
89
+ preview: { select: { title: "docId", subtitle: "docType" } }
90
+ })
91
+ ]
92
+ }),
93
+ sanity.defineField({
94
+ name: "smartcatProjectId",
95
+ title: "Smartcat project ID",
96
+ type: "string",
97
+ readOnly: !0,
98
+ description: "GUID of the corresponding project in Smartcat."
99
+ }),
100
+ sanity.defineField({
101
+ name: "smartcatProjectUrl",
102
+ title: "Smartcat project URL",
103
+ type: "url",
104
+ readOnly: !0,
105
+ description: "Link to the project in Smartcat (set by the export Function)."
106
+ }),
107
+ sanity.defineField({ name: "lastExportAt", type: "datetime", readOnly: !0 }),
108
+ sanity.defineField({ name: "lastImportAt", type: "datetime", readOnly: !0 }),
109
+ // Per-document, per-language, per-stage translation progress, mirrored from
110
+ // Smartcat. The export captures the target document ids; the progress/import
111
+ // Functions refresh the stage percentages; the browser sets `imported`.
112
+ sanity.defineField({
113
+ name: "progress",
114
+ title: "Translation progress",
115
+ type: "array",
116
+ readOnly: !0,
117
+ of: [
118
+ sanity.defineArrayMember({
119
+ type: "object",
120
+ name: "smartcatDocProgress",
121
+ fields: [
122
+ sanity.defineField({ name: "sourceDocId", type: "string" }),
123
+ sanity.defineField({ name: "title", type: "string" }),
124
+ sanity.defineField({
125
+ name: "targets",
126
+ type: "array",
127
+ of: [
128
+ sanity.defineArrayMember({
129
+ type: "object",
130
+ name: "smartcatTargetProgress",
131
+ fields: [
132
+ sanity.defineField({ name: "language", type: "string" }),
133
+ sanity.defineField({ name: "smartcatDocumentId", type: "string" }),
134
+ sanity.defineField({
135
+ name: "stages",
136
+ type: "array",
137
+ of: [
138
+ sanity.defineArrayMember({
139
+ type: "object",
140
+ name: "smartcatStageProgress",
141
+ fields: [
142
+ sanity.defineField({ name: "name", type: "string" }),
143
+ sanity.defineField({ name: "percent", type: "number" })
144
+ ]
145
+ })
146
+ ]
147
+ }),
148
+ // All stages at 100% (set by the progress/import Functions).
149
+ sanity.defineField({ name: "complete", type: "boolean" }),
150
+ // A locale variant has been built from this target (set by the browser).
151
+ sanity.defineField({ name: "imported", type: "boolean" }),
152
+ sanity.defineField({ name: "syncedAt", type: "datetime" })
153
+ ]
154
+ })
155
+ ]
156
+ })
157
+ ]
158
+ })
159
+ ]
160
+ }),
161
+ // Handshake fields for the on-demand progress refresh: the browser bumps
162
+ // `progressRequestedAt`; the smartcat-progress Function reacts and writes
163
+ // `progressSyncedAt`. Keeping them distinct stops the Function re-triggering
164
+ // on its own write (filter is delta::changedAny(progressRequestedAt)).
165
+ sanity.defineField({ name: "progressRequestedAt", type: "datetime", readOnly: !0, hidden: !0 }),
166
+ sanity.defineField({ name: "progressSyncedAt", type: "datetime", readOnly: !0, hidden: !0 }),
167
+ sanity.defineField({
168
+ name: "lastError",
169
+ title: "Last error",
170
+ type: "string",
171
+ readOnly: !0,
172
+ description: "Most recent export/import error, if any."
173
+ }),
174
+ // JSON-encoded LogLine[] from the most recent Function run (export upload or
175
+ // import download). Transient diagnostic data the browser merges into its
176
+ // live Log panel; overwritten each run.
177
+ sanity.defineField({
178
+ name: "functionLog",
179
+ type: "text",
180
+ readOnly: !0,
181
+ hidden: !0
182
+ }),
183
+ // Transient transport buffers. The browser (plugin) serializes content into
184
+ // `outbox`; the thin export Function uploads it. The thin import Function
185
+ // downloads translated files into `inbox`; the browser turns them into
186
+ // locale variants. Both are cleared after processing.
187
+ sanity.defineField({
188
+ name: "outbox",
189
+ type: "array",
190
+ readOnly: !0,
191
+ hidden: !0,
192
+ of: [
193
+ sanity.defineArrayMember({
194
+ type: "object",
195
+ name: "smartcatOutboxItem",
196
+ fields: [
197
+ sanity.defineField({ name: "filename", type: "string" }),
198
+ sanity.defineField({ name: "locjson", type: "text" })
199
+ ]
200
+ })
201
+ ]
202
+ }),
203
+ sanity.defineField({
204
+ name: "inbox",
205
+ type: "array",
206
+ readOnly: !0,
207
+ hidden: !0,
208
+ of: [
209
+ sanity.defineArrayMember({
210
+ type: "object",
211
+ name: "smartcatInboxItem",
212
+ fields: [
213
+ sanity.defineField({ name: "sourceDocId", type: "string" }),
214
+ sanity.defineField({ name: "targetLanguage", type: "string" }),
215
+ sanity.defineField({ name: "smartcatDocumentId", type: "string" }),
216
+ sanity.defineField({ name: "locjson", type: "text" })
217
+ ]
218
+ })
219
+ ]
220
+ }),
221
+ // Transient: items the browser marked for deletion. On the next export the
222
+ // Function deletes the Smartcat document and removes the item + its progress.
223
+ sanity.defineField({
224
+ name: "pendingDeletions",
225
+ type: "array",
226
+ readOnly: !0,
227
+ hidden: !0,
228
+ of: [
229
+ sanity.defineArrayMember({
230
+ type: "object",
231
+ name: "smartcatPendingDeletion",
232
+ fields: [
233
+ sanity.defineField({ name: "itemKey", type: "string" }),
234
+ sanity.defineField({ name: "sourceDocId", type: "string" }),
235
+ sanity.defineField({ name: "smartcatDocumentId", type: "string" })
236
+ ]
237
+ })
238
+ ]
239
+ })
240
+ ],
241
+ preview: {
242
+ select: { name: "name", status: "status", items: "items" },
243
+ prepare({ name, status, items }) {
244
+ const count = Array.isArray(items) ? items.length : 0;
245
+ return {
246
+ title: name || "Untitled project",
247
+ subtitle: `${status || "draft"} \xB7 ${count} item${count === 1 ? "" : "s"}`
248
+ };
249
+ }
250
+ }
251
+ });
252
+ }
253
+ function createSettingsType() {
254
+ return sanity.defineType({
255
+ name: constants.SETTINGS_TYPE,
256
+ title: "Smartcat settings",
257
+ type: "document",
258
+ icon: icons.CogIcon,
259
+ fields: [
260
+ sanity.defineField({
261
+ name: "templates",
262
+ title: "Project templates",
263
+ type: "array",
264
+ readOnly: !0,
265
+ of: [
266
+ sanity.defineArrayMember({
267
+ type: "object",
268
+ name: "smartcatTemplate",
269
+ fields: [
270
+ sanity.defineField({ name: "id", type: "string" }),
271
+ sanity.defineField({ name: "name", type: "string" })
272
+ ],
273
+ preview: { select: { title: "name", subtitle: "id" } }
274
+ })
275
+ ]
276
+ }),
277
+ // Handshake fields for the on-demand template refresh: the browser bumps
278
+ // `templatesRequestedAt`; the smartcat-templates Function writes
279
+ // `templatesSyncedAt`. Distinct fields stop the Function re-triggering on
280
+ // its own write (filter is delta::changedAny(templatesRequestedAt)).
281
+ sanity.defineField({ name: "templatesRequestedAt", type: "datetime", readOnly: !0, hidden: !0 }),
282
+ sanity.defineField({ name: "templatesSyncedAt", type: "datetime", readOnly: !0, hidden: !0 })
283
+ ],
284
+ preview: {
285
+ select: { templates: "templates", syncedAt: "templatesSyncedAt" },
286
+ prepare({ templates, syncedAt }) {
287
+ const count = Array.isArray(templates) ? templates.length : 0;
288
+ return {
289
+ title: "Smartcat settings",
290
+ subtitle: syncedAt ? `${count} template${count === 1 ? "" : "s"} cached` : "Not synced yet"
291
+ };
292
+ }
293
+ }
294
+ });
295
+ }
296
+ function WorkflowSelect({ value, onChange, templates, disabled }) {
297
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value, onChange: (e) => onChange(e.currentTarget.value), disabled, children: [
298
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: !0, children: "Select a workflow\u2026" }),
299
+ templates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: "Templates", children: templates.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: `${workflow.TEMPLATE_PREFIX}${t.id}`, children: t.name }, t.id)) }),
300
+ /* @__PURE__ */ jsxRuntime.jsx("optgroup", { label: "Standard workflows", children: workflow.STANDARD_WORKFLOW_OPTIONS.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.value, children: o.label }, o.value)) })
301
+ ] });
302
+ }
303
+ const SETTINGS_QUERY = "*[_id == $id][0]{templates, templatesRequestedAt, templatesSyncedAt}", REQUEST_STALE_MS = 3e4;
304
+ function isRefreshInFlight(s) {
305
+ const requested = s?.templatesRequestedAt;
306
+ return !requested || !(!s?.templatesSyncedAt || requested > s.templatesSyncedAt) ? !1 : Date.now() - Date.parse(requested) < REQUEST_STALE_MS;
307
+ }
308
+ function useTemplates() {
309
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), [settings, setSettings] = react.useState(null), [loaded, setLoaded] = react.useState(!1), settingsRef = react.useRef(null);
310
+ settingsRef.current = settings, react.useEffect(() => {
311
+ let cancelled = !1;
312
+ const load = () => client.fetch(SETTINGS_QUERY, { id: constants.SETTINGS_DOC_ID }).then((s) => {
313
+ cancelled || (setSettings(s), setLoaded(!0));
314
+ }).catch(() => {
315
+ });
316
+ load();
317
+ const sub = client.listen(SETTINGS_QUERY, { id: constants.SETTINGS_DOC_ID }, { visibility: "query" }).subscribe({ next: load, error: () => {
318
+ } });
319
+ return () => {
320
+ cancelled = !0, sub.unsubscribe();
321
+ };
322
+ }, [client]);
323
+ const requestRefresh = react.useCallback(() => {
324
+ isRefreshInFlight(settingsRef.current) || client.transaction().createIfNotExists({ _id: constants.SETTINGS_DOC_ID, _type: constants.SETTINGS_TYPE }).patch(constants.SETTINGS_DOC_ID, (p) => p.set({ templatesRequestedAt: (/* @__PURE__ */ new Date()).toISOString() })).commit({ visibility: "async" }).catch(() => {
325
+ });
326
+ }, [client]);
327
+ return { templates: settings?.templates ?? [], loaded, requestRefresh };
328
+ }
329
+ const STORAGE_KEY = "smartcat:lastWorkflow";
330
+ function readLastWorkflow() {
331
+ try {
332
+ return localStorage.getItem(STORAGE_KEY) ?? "";
333
+ } catch {
334
+ return "";
335
+ }
336
+ }
337
+ function writeLastWorkflow(value) {
338
+ try {
339
+ localStorage.setItem(STORAGE_KEY, value);
340
+ } catch {
341
+ }
342
+ }
343
+ function useWorkflowSelection(active, templates, loaded) {
344
+ const [workflow$1, setWorkflowState] = react.useState(""), initialized = react.useRef(!1), templateIds = react.useMemo(() => templates.map((t) => t.id), [templates]);
345
+ react.useEffect(() => {
346
+ if (!active) {
347
+ initialized.current = !1;
348
+ return;
349
+ }
350
+ if (initialized.current || !loaded) return;
351
+ initialized.current = !0;
352
+ const saved = readLastWorkflow();
353
+ setWorkflowState(workflow.isWorkflowValid(saved, templateIds) ? saved : "");
354
+ }, [active, loaded, templateIds]), react.useEffect(() => {
355
+ !active || !loaded || !workflow$1 || workflow.isWorkflowValid(workflow$1, templateIds) || setWorkflowState("");
356
+ }, [active, loaded, workflow$1, templateIds]);
357
+ const setWorkflow = react.useCallback((value) => {
358
+ setWorkflowState(value), value && writeLastWorkflow(value);
359
+ }, []);
360
+ return [workflow$1, setWorkflow];
361
+ }
362
+ const UNTITLED = "Untitled";
363
+ function resolveDocumentTitle(schema, doc) {
364
+ const type = schema.get(doc._type);
365
+ if (type)
366
+ try {
367
+ const preview = sanity.prepareForPreview(doc, type);
368
+ return typeof preview?.title == "string" ? preview.title : void 0;
369
+ } catch {
370
+ return;
371
+ }
372
+ }
373
+ const bareId = (id) => id.replace(/^drafts\./, "");
374
+ function collectRefs(node, out = /* @__PURE__ */ new Set()) {
375
+ if (!node || typeof node != "object") return out;
376
+ if (Array.isArray(node)) {
377
+ for (const item of node) collectRefs(item, out);
378
+ return out;
379
+ }
380
+ const obj = node;
381
+ typeof obj._ref == "string" && out.add(bareId(obj._ref));
382
+ for (const [key, value] of Object.entries(obj))
383
+ key !== "_ref" && collectRefs(value, out);
384
+ return out;
385
+ }
386
+ function pickPreferDraft(docs) {
387
+ const chosen = /* @__PURE__ */ new Map();
388
+ for (const doc of docs ?? []) {
389
+ const bare = bareId(doc._id);
390
+ (!chosen.has(bare) || doc._id.startsWith("drafts.")) && chosen.set(bare, doc);
391
+ }
392
+ return chosen;
393
+ }
394
+ async function gatherLinkedDocuments({
395
+ client,
396
+ rootId,
397
+ isRoot,
398
+ isTranslatable,
399
+ batchSize = 50
400
+ }) {
401
+ const start = bareId(rootId), seen = /* @__PURE__ */ new Set([start]), result = [];
402
+ let frontier = [start];
403
+ for (; frontier.length; ) {
404
+ const batch = frontier.splice(0, batchSize), bodies = await client.fetch(
405
+ "*[_id in $ids]",
406
+ { ids: batch.flatMap((id) => [id, `drafts.${id}`]) }
407
+ ), candidates = /* @__PURE__ */ new Set();
408
+ for (const doc of pickPreferDraft(bodies).values())
409
+ for (const ref of collectRefs(doc)) candidates.add(ref);
410
+ const fresh = [...candidates].filter((id) => !seen.has(id));
411
+ if (!fresh.length) continue;
412
+ fresh.forEach((id) => seen.add(id));
413
+ const typed = await client.fetch(
414
+ "*[_id in $ids]{_id, _type}",
415
+ { ids: fresh.flatMap((id) => [id, `drafts.${id}`]) }
416
+ );
417
+ for (const [id, doc] of pickPreferDraft(typed)) {
418
+ const type = doc._type;
419
+ !type || isRoot(type) || !isTranslatable(type) || (result.push({ docId: id, docType: type }), frontier.push(id));
420
+ }
421
+ }
422
+ return result;
423
+ }
424
+ const PROJECTS_QUERY$1 = `*[_type == $type] | order(name asc){
425
+ _id,
426
+ name,
427
+ status,
428
+ "count": count(items)
429
+ }`, CREATE_NEW = "__create_new__";
430
+ function createAddToProjectAction(config) {
431
+ const sourceLanguage = config.sourceLanguage;
432
+ return (props) => {
433
+ const { id, draft, published, onComplete } = props, client = sanity.useClient({ apiVersion: constants.API_VERSION }), schema = sanity.useSchema(), toast = ui.useToast(), [open, setOpen] = react.useState(!1), [projects, setProjects] = react.useState(null), [selection, setSelection] = react.useState(CREATE_NEW), [newName, setNewName] = react.useState(""), [busy, setBusy] = react.useState(!1), [includeLinked, setIncludeLinked] = react.useState(!0), { templates, loaded, requestRefresh } = useTemplates(), [workflow2, setWorkflow] = useWorkflowSelection(open, templates, loaded), publishedId2 = id.replace(/^drafts\./, ""), doc = draft ?? published, docTitle = doc && resolveDocumentTitle(schema, doc) || UNTITLED;
434
+ react.useEffect(() => {
435
+ if (!open) return;
436
+ requestRefresh();
437
+ let cancelled = !1;
438
+ return setProjects(null), client.fetch(PROJECTS_QUERY$1, { type: constants.TRANSLATION_PROJECT_TYPE }).then((res) => {
439
+ cancelled || (setProjects(res), setSelection(res.length > 0 ? res[0]._id : CREATE_NEW));
440
+ }).catch((err) => {
441
+ cancelled || toast.push({ status: "error", title: "Failed to load projects", description: String(err) });
442
+ }), () => {
443
+ cancelled = !0;
444
+ };
445
+ }, [open, client, toast, requestRefresh]);
446
+ const close = react.useCallback(() => {
447
+ setOpen(!1), setNewName(""), onComplete();
448
+ }, [onComplete]), handleConfirm = react.useCallback(async () => {
449
+ setBusy(!0);
450
+ try {
451
+ const makeItem = (docId, docType) => ({
452
+ _type: "smartcat.projectItem",
453
+ _key: uuid.uuid(),
454
+ docId,
455
+ docType
456
+ }), collected = /* @__PURE__ */ new Map([[publishedId2, doc?._type]]);
457
+ if (includeLinked) {
458
+ const linked = await gatherLinkedDocuments({ client, rootId: publishedId2, isRoot: (type) => config.rootTypes.includes(type), isTranslatable: (type) => locjson.getTranslatableFields(schema.get(type), { exclude: [config.documentI18nLanguageField] }).length > 0 });
459
+ for (const { docId, docType } of linked) collected.has(docId) || collected.set(docId, docType);
460
+ }
461
+ if (selection === CREATE_NEW) {
462
+ const name = newName.trim() || `Translation project ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`, items = [...collected].map(([docId, docType]) => makeItem(docId, docType));
463
+ await client.create({
464
+ _type: constants.TRANSLATION_PROJECT_TYPE,
465
+ name,
466
+ status: "draft",
467
+ sourceLanguage,
468
+ workflow: workflow2,
469
+ items
470
+ }), toast.push({
471
+ status: "success",
472
+ title: `Created \u201C${name}\u201D with ${items.length} document${items.length === 1 ? "" : "s"}`
473
+ });
474
+ } else {
475
+ const existing = await client.fetch(
476
+ `*[_id == $pid][0].items[]{"id": ${progress.ITEM_ID}}`,
477
+ { pid: selection }
478
+ ), existingIds = new Set((existing ?? []).map((e) => e.id)), newItems = [...collected].filter(([docId]) => !existingIds.has(docId)).map(([docId, docType]) => makeItem(docId, docType)), projName = projects?.find((p) => p._id === selection)?.name ?? "project";
479
+ if (newItems.length === 0)
480
+ toast.push({ status: "info", title: `Already in \u201C${projName}\u201D` });
481
+ else {
482
+ await client.patch(selection).setIfMissing({ items: [] }).insert("after", "items[-1]", newItems).commit();
483
+ const skipped = collected.size - newItems.length;
484
+ toast.push({
485
+ status: "success",
486
+ title: `Added ${newItems.length} document${newItems.length === 1 ? "" : "s"} to \u201C${projName}\u201D`,
487
+ description: skipped > 0 ? `${skipped} already in the project` : void 0
488
+ });
489
+ }
490
+ }
491
+ close();
492
+ } catch (err) {
493
+ toast.push({ status: "error", title: "Failed to add to project", description: String(err) });
494
+ } finally {
495
+ setBusy(!1);
496
+ }
497
+ }, [selection, newName, workflow2, publishedId2, doc, includeLinked, schema, client, projects, close]), content = react.useMemo(() => projects === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
498
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
499
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "Project" }),
500
+ /* @__PURE__ */ jsxRuntime.jsxs(
501
+ ui.Select,
502
+ {
503
+ value: selection,
504
+ onChange: (e) => setSelection(e.currentTarget.value),
505
+ disabled: busy,
506
+ children: [
507
+ projects.map((p) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: p._id, children: [
508
+ p.name,
509
+ " (",
510
+ p.count,
511
+ " item",
512
+ p.count === 1 ? "" : "s",
513
+ ")"
514
+ ] }, p._id)),
515
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: CREATE_NEW, children: "\u2795 Create new project\u2026" })
516
+ ]
517
+ }
518
+ )
519
+ ] }),
520
+ selection === CREATE_NEW && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
521
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
522
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "New project name" }),
523
+ /* @__PURE__ */ jsxRuntime.jsx(
524
+ ui.TextInput,
525
+ {
526
+ value: newName,
527
+ placeholder: "e.g. Spring campaign",
528
+ onChange: (e) => setNewName(e.currentTarget.value),
529
+ disabled: busy
530
+ }
531
+ )
532
+ ] }),
533
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
534
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "Workflow" }),
535
+ /* @__PURE__ */ jsxRuntime.jsx(
536
+ WorkflowSelect,
537
+ {
538
+ value: workflow2,
539
+ onChange: setWorkflow,
540
+ templates,
541
+ disabled: busy
542
+ }
543
+ )
544
+ ] })
545
+ ] }),
546
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
547
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
548
+ "Adding ",
549
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: docTitle }),
550
+ " to the selected project."
551
+ ] }),
552
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "flex-start", gap: 3, children: [
553
+ /* @__PURE__ */ jsxRuntime.jsx(
554
+ ui.Switch,
555
+ {
556
+ checked: includeLinked,
557
+ onChange: (e) => setIncludeLinked(e.currentTarget.checked),
558
+ disabled: busy
559
+ }
560
+ ),
561
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
562
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: "Include linked content" }),
563
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Also add documents this one references, recursively \u2014 skipping any already in the project." })
564
+ ] })
565
+ ] })
566
+ ] }) }),
567
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "flex-end", gap: 2, children: [
568
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Cancel", onClick: close, disabled: busy }),
569
+ /* @__PURE__ */ jsxRuntime.jsx(
570
+ ui.Button,
571
+ {
572
+ tone: "primary",
573
+ text: "Add to project",
574
+ icon: icons.AddIcon,
575
+ onClick: handleConfirm,
576
+ loading: busy,
577
+ disabled: selection === CREATE_NEW && !workflow2
578
+ }
579
+ )
580
+ ] })
581
+ ] }), [projects, selection, newName, workflow2, templates, busy, includeLinked, docTitle, close, handleConfirm]);
582
+ return {
583
+ label: "Add to translation project",
584
+ icon: icons.TranslateIcon,
585
+ onHandle: () => setOpen(!0),
586
+ dialog: open && {
587
+ type: "dialog",
588
+ header: "Add to translation project",
589
+ onClose: close,
590
+ content
591
+ }
592
+ };
593
+ };
594
+ }
595
+ const PROJECTS_QUERY = `*[_type == "${constants.TRANSLATION_PROJECT_TYPE}"] | order(_createdAt desc){
596
+ _id,
597
+ name,
598
+ status,
599
+ _createdAt,
600
+ _updatedAt,
601
+ "count": count(items)
602
+ }`, PROJECT_DETAIL_QUERY = `*[_id == $id][0]{
603
+ _id,
604
+ name,
605
+ status,
606
+ sourceLanguage,
607
+ targetLanguages,
608
+ smartcatProjectId,
609
+ smartcatProjectUrl,
610
+ lastError,
611
+ functionLog,
612
+ importRemaining,
613
+ progressSyncedAt,
614
+ items[]{
615
+ _key,
616
+ "docId": ${progress.ITEM_ID},
617
+ "doc": ${progress.itemDocDeref("{_id, _type}")},
618
+ "translations": *[_type == "translation.metadata" && references(${progress.ITEM_ID_FROM_SUBQUERY})][0]
619
+ .translations[]{language, "id": value._ref}
620
+ },
621
+ progress[]{
622
+ _key,
623
+ sourceDocId,
624
+ title,
625
+ targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}
626
+ }
627
+ }`, TONES = {
628
+ draft: "default",
629
+ queued: "caution",
630
+ sent: "primary",
631
+ translating: "primary",
632
+ importing: "caution",
633
+ downloaded: "primary",
634
+ completed: "positive",
635
+ error: "critical"
636
+ }, LABELS = Object.fromEntries(
637
+ STATUS_OPTIONS.map((o) => [o.value, o.title])
638
+ );
639
+ function statusLabel(status) {
640
+ return LABELS[status] ?? status;
641
+ }
642
+ function StatusBadge({ status }) {
643
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: TONES[status] ?? "default", fontSize: 0, padding: 2, radius: 2, children: statusLabel(status) });
644
+ }
645
+ function ProjectRow({ project: p, onOpen }) {
646
+ const createdAgo = sanity.useRelativeTime(p._createdAt, { useTemporalPhrase: !0 });
647
+ return /* @__PURE__ */ jsxRuntime.jsx(
648
+ ui.Card,
649
+ {
650
+ padding: 3,
651
+ radius: 2,
652
+ shadow: 1,
653
+ as: "button",
654
+ onClick: () => onOpen(p._id),
655
+ style: { cursor: "pointer", textAlign: "left", width: "100%" },
656
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
657
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 110 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, title: new Date(p._createdAt).toLocaleString(), children: createdAgo }) }),
658
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 100 }, children: /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: p.status }) }),
659
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.TranslateIcon, {}) }),
660
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "semibold", children: p.name }) }),
661
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
662
+ p.count,
663
+ " item",
664
+ p.count === 1 ? "" : "s"
665
+ ] })
666
+ ] })
667
+ }
668
+ );
669
+ }
670
+ function ProjectsView({ sourceLanguage, onOpenProject }) {
671
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), toast = ui.useToast(), [projects, setProjects] = react.useState(null), [creating, setCreating] = react.useState(!1), [newName, setNewName] = react.useState(""), [busy, setBusy] = react.useState(!1), { templates, loaded, requestRefresh } = useTemplates(), [workflow2, setWorkflow] = useWorkflowSelection(creating, templates, loaded);
672
+ react.useEffect(() => {
673
+ creating && requestRefresh();
674
+ }, [creating, requestRefresh]);
675
+ const load = react.useCallback(() => client.fetch(PROJECTS_QUERY).then(setProjects).catch((err) => toast.push({ status: "error", title: "Failed to load projects", description: String(err) })), [client, toast]);
676
+ react.useEffect(() => {
677
+ load();
678
+ const sub = client.listen(PROJECTS_QUERY, {}, { visibility: "query" }).subscribe({ next: () => load(), error: () => {
679
+ } });
680
+ return () => sub.unsubscribe();
681
+ }, [client, load]);
682
+ const handleCreate = react.useCallback(async () => {
683
+ const name = newName.trim();
684
+ if (name) {
685
+ setBusy(!0);
686
+ try {
687
+ const created = await client.create({
688
+ _type: constants.TRANSLATION_PROJECT_TYPE,
689
+ name,
690
+ status: "draft",
691
+ sourceLanguage,
692
+ workflow: workflow2,
693
+ items: []
694
+ });
695
+ toast.push({ status: "success", title: `Created \u201C${name}\u201D` }), setCreating(!1), setNewName(""), onOpenProject(created._id);
696
+ } catch (err) {
697
+ toast.push({ status: "error", title: "Failed to create project", description: String(err) });
698
+ } finally {
699
+ setBusy(!1);
700
+ }
701
+ }
702
+ }, [newName, workflow2, client, sourceLanguage, toast, onOpenProject]);
703
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
704
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", children: [
705
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Translation projects" }),
706
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { icon: icons.AddIcon, text: "New project", tone: "primary", onClick: () => setCreating(!0) })
707
+ ] }),
708
+ projects === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : projects.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
709
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: "No translation projects yet." }),
710
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { icon: icons.AddIcon, text: "Create your first project", onClick: () => setCreating(!0) }) })
711
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: projects.map((p) => /* @__PURE__ */ jsxRuntime.jsx(ProjectRow, { project: p, onOpen: onOpenProject }, p._id)) }),
712
+ creating && /* @__PURE__ */ jsxRuntime.jsx(
713
+ ui.Dialog,
714
+ {
715
+ id: "create-project",
716
+ header: "New translation project",
717
+ onClose: () => {
718
+ setCreating(!1), setNewName("");
719
+ },
720
+ width: 1,
721
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
722
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
723
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "Project name" }),
724
+ /* @__PURE__ */ jsxRuntime.jsx(
725
+ ui.TextInput,
726
+ {
727
+ value: newName,
728
+ placeholder: "e.g. Spring campaign",
729
+ autoFocus: !0,
730
+ onChange: (e) => setNewName(e.currentTarget.value),
731
+ onKeyDown: (e) => {
732
+ e.key === "Enter" && handleCreate();
733
+ },
734
+ disabled: busy
735
+ }
736
+ )
737
+ ] }),
738
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
739
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "Workflow" }),
740
+ /* @__PURE__ */ jsxRuntime.jsx(
741
+ WorkflowSelect,
742
+ {
743
+ value: workflow2,
744
+ onChange: setWorkflow,
745
+ templates,
746
+ disabled: busy
747
+ }
748
+ )
749
+ ] }),
750
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "flex-end", gap: 2, children: [
751
+ /* @__PURE__ */ jsxRuntime.jsx(
752
+ ui.Button,
753
+ {
754
+ mode: "bleed",
755
+ text: "Cancel",
756
+ onClick: () => {
757
+ setCreating(!1), setNewName("");
758
+ },
759
+ disabled: busy
760
+ }
761
+ ),
762
+ /* @__PURE__ */ jsxRuntime.jsx(
763
+ ui.Button,
764
+ {
765
+ tone: "primary",
766
+ text: "Create",
767
+ onClick: handleCreate,
768
+ loading: busy,
769
+ disabled: !newName.trim() || !workflow2
770
+ }
771
+ )
772
+ ] })
773
+ ] }) })
774
+ }
775
+ )
776
+ ] });
777
+ }
778
+ const METADATA_TYPE = "translation.metadata", REFERENCE_VALUE_TYPE = "internationalizedArrayReferenceValue";
779
+ function parseHtml(html) {
780
+ return new DOMParser().parseFromString(html, "text/html");
781
+ }
782
+ const VALUE_PREVIEW_CAP = 2e3;
783
+ function valuePreview(value) {
784
+ if (value == null) return;
785
+ const text = typeof value == "string" ? value : JSON.stringify(value, null, 2);
786
+ if (text)
787
+ return text.length > VALUE_PREVIEW_CAP ? `${text.slice(0, VALUE_PREVIEW_CAP)}\u2026` : text;
788
+ }
789
+ function errorMessage(err) {
790
+ return err instanceof Error ? err.message : String(err);
791
+ }
792
+ function schemaFieldNames(schema, type) {
793
+ return (schema.get(type)?.fields ?? []).map((f) => f?.name).filter((n) => typeof n == "string" && !n.startsWith("_"));
794
+ }
795
+ function blockContentTypeFor(schema, type, fieldPath) {
796
+ const fieldType = schema.get(type)?.fields?.find((f) => f.name === fieldPath)?.type;
797
+ return locjson.isInternationalizedArrayType(fieldType) ? locjson.innerValueType(fieldType) : fieldType;
798
+ }
799
+ const ITEMS_QUERY = `*[_id == $id][0]{ sourceLanguage, items[]{ "doc": ${progress.itemDocDeref()} } }`;
800
+ async function prepareExport({
801
+ client,
802
+ schema,
803
+ projectId,
804
+ targetLanguages,
805
+ sourceLanguage,
806
+ languages,
807
+ exclude,
808
+ fieldLanguageKey,
809
+ deletions = [],
810
+ onLog
811
+ }) {
812
+ const log = onLog ?? (() => {
813
+ }), project = await client.fetch(ITEMS_QUERY, { id: projectId }), source = project?.sourceLanguage || sourceLanguage, smartcatLanguages = progress.buildLanguageMappings(languages, [source, ...targetLanguages]), dups = progress.findDuplicateSmartcatLanguages(
814
+ smartcatLanguages.filter((m) => targetLanguages.includes(m.sanityId))
815
+ );
816
+ if (dups.length > 0) {
817
+ const detail = dups.map((d) => `${d.sanityIds.join(", ")} \u2192 ${d.smartcatLanguage}`).join("; ");
818
+ throw new Error(`Multiple target languages map to the same Smartcat language: ${detail}`);
819
+ }
820
+ const deletedDocIds = new Set(deletions.map((d) => d.sourceDocId)), docs = (project?.items ?? []).map((i) => i?.doc).filter((d) => !!d).filter((d) => !deletedDocIds.has(d._id)), fieldsByType = /* @__PURE__ */ new Map(), fieldsFor = (type) => {
821
+ let fields = fieldsByType.get(type);
822
+ return fields || (fields = locjson.getTranslatableFields(schema.get(type), { exclude }), fieldsByType.set(type, fields)), fields;
823
+ };
824
+ log({ level: "info", message: `Export \u2014 ${docs.length} document(s) \u2192 ${targetLanguages.join(", ") || "(no targets)"}` });
825
+ const outbox = [];
826
+ for (const doc of docs) {
827
+ const name = resolveDocumentTitle(schema, doc);
828
+ log({ level: "info", message: `${name || doc._id} \xB7 ${doc._type}` });
829
+ try {
830
+ const fields = fieldsFor(doc._type), skippedBlockTypes = /* @__PURE__ */ new Map(), locjson$1 = locjson.serializeToLocjson(doc, fields, {
831
+ sourceLanguage: source,
832
+ fieldLanguageKey,
833
+ onSkippedField: ({ fieldPath, types }) => skippedBlockTypes.set(fieldPath, types)
834
+ }), unitByKey = new Map(locjson$1.units.map((u) => [u.key, u]));
835
+ for (const field of fields) {
836
+ const unit = unitByKey.get(field.path), skippedTypes = skippedBlockTypes.get(field.path);
837
+ log(unit ? { level: "success", indent: !0, message: `${field.title || field.path}: serialized (${field.kind})`, value: unit.source.join("") } : skippedTypes ? {
838
+ level: "error",
839
+ indent: !0,
840
+ message: `${field.title || field.path}: not translated \u2014 contains block type(s) that can't be translated: ${skippedTypes.join(", ")}. The whole field was left untranslated to avoid losing this content.`
841
+ } : { level: "skip", indent: !0, message: `${field.title || field.path}: skipped \u2014 no source content` });
842
+ }
843
+ const selected = new Set(fields.map((f) => f.path)), excluded = new Set(exclude ?? []);
844
+ for (const fieldName of schemaFieldNames(schema, doc._type))
845
+ selected.has(fieldName) || excluded.has(fieldName) || log({ level: "skip", indent: !0, message: `${fieldName}: skipped \u2014 not translatable` });
846
+ if (locjson$1.units.length === 0) {
847
+ skippedBlockTypes.size > 0 ? log({
848
+ level: "error",
849
+ indent: !0,
850
+ message: "not sent \u2014 its only translatable content is in field(s) with block type(s) that can\u2019t be translated (see above)"
851
+ }) : log({ level: "skip", indent: !0, message: "no translatable content \u2014 not sent" });
852
+ continue;
853
+ }
854
+ outbox.push({
855
+ _key: uuid.uuid(),
856
+ sourceDocId: doc._id,
857
+ title: name,
858
+ filename: locjson.buildLocjsonFilename(doc._type, doc._id, name),
859
+ locjson: JSON.stringify(locjson$1)
860
+ });
861
+ } catch (err) {
862
+ log({ level: "error", indent: !0, message: `failed to serialize: ${errorMessage(err)}` });
863
+ }
864
+ }
865
+ return log({ level: "success", message: `Queued ${outbox.length} file(s)${deletions.length ? ` \xB7 ${deletions.length} for deletion` : ""}` }), await client.patch(projectId).set({
866
+ outbox,
867
+ pendingDeletions: deletions,
868
+ targetLanguages,
869
+ sourceLanguage: source,
870
+ smartcatLanguages,
871
+ status: "queued",
872
+ lastError: null
873
+ }).commit(), { prepared: outbox.length, deleting: deletions.length };
874
+ }
875
+ const INBOX_QUERY = `*[_id == $id][0]{
876
+ sourceLanguage,
877
+ inbox[]{ sourceDocId, targetLanguage, smartcatDocumentId, locjson },
878
+ progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
879
+ }`;
880
+ async function applyImportedTranslations({
881
+ client,
882
+ schema,
883
+ projectId,
884
+ documentLanguageField,
885
+ fieldLanguageKey,
886
+ onLog
887
+ }) {
888
+ const log = onLog ?? (() => {
889
+ }), project = await client.fetch(INBOX_QUERY, { id: projectId }), sourceLanguage = constants.requireSourceLanguage(project?.sourceLanguage), inbox = project?.inbox ?? [], titleByDoc = /* @__PURE__ */ new Map();
890
+ for (const p of project?.progress ?? [])
891
+ p.sourceDocId && p.title && titleByDoc.set(p.sourceDocId, p.title);
892
+ let imported = 0;
893
+ const importedKeys = /* @__PURE__ */ new Set(), modeByType = /* @__PURE__ */ new Map(), modeFor = (type) => {
894
+ let mode = modeByType.get(type);
895
+ return mode || (mode = locjson.localizationMode(schema.get(type)), modeByType.set(type, mode)), mode;
896
+ }, descriptorsByType = /* @__PURE__ */ new Map(), descriptorsFor = (type) => {
897
+ let d = descriptorsByType.get(type);
898
+ return d || (d = locjson.getTranslatableFields(schema.get(type), { exclude: [documentLanguageField] }), descriptorsByType.set(type, d)), d;
899
+ };
900
+ log({ level: "info", message: `Import \u2014 ${inbox.length} translated file(s)` });
901
+ for (const item of inbox) {
902
+ let parsed;
903
+ try {
904
+ parsed = JSON.parse(item.locjson);
905
+ } catch {
906
+ log({ level: "error", message: `${item.sourceDocId || "unknown"} \u2192 ${item.targetLanguage}: invalid LocJSON, skipped` });
907
+ continue;
908
+ }
909
+ const type = parsed.properties?.["x-sanity"]?.documentType, result = locjson.deserializeFromLocjson(parsed, {
910
+ parseHtml,
911
+ getBlockContentType: (fieldPath) => type ? blockContentTypeFor(schema, type, fieldPath) : void 0
912
+ });
913
+ if (!result.documentId || !result.documentType) {
914
+ log({ level: "error", message: `${item.sourceDocId || "unknown"} \u2192 ${item.targetLanguage}: missing document id/type, skipped` });
915
+ continue;
916
+ }
917
+ const mode = modeFor(result.documentType), title = titleByDoc.get(result.documentId), label = title ? `${title} (Sanity ID: ${result.documentId})` : result.documentId;
918
+ log({ level: "info", message: `${label} \u2192 ${item.targetLanguage} \xB7 ${result.documentType} (${mode} mode)` });
919
+ let ok = !1;
920
+ try {
921
+ ok = mode === "field" ? await applyFieldLevelTranslations(client, {
922
+ schema,
923
+ type: result.documentType,
924
+ docId: result.documentId,
925
+ locale: item.targetLanguage,
926
+ sourceLanguage: result.sourceLanguage || sourceLanguage,
927
+ descriptors: descriptorsFor(result.documentType),
928
+ translations: result.fields,
929
+ sources: result.sources,
930
+ languageKey: fieldLanguageKey
931
+ }, log) : await upsertVariant(client, {
932
+ sourceId: result.documentId,
933
+ type: result.documentType,
934
+ sourceLanguage,
935
+ locale: item.targetLanguage,
936
+ fields: result.fields,
937
+ sources: result.sources,
938
+ languageField: documentLanguageField
939
+ }, log);
940
+ } catch (err) {
941
+ log({ level: "error", indent: !0, message: `failed: ${errorMessage(err)}` });
942
+ }
943
+ ok && (imported++, importedKeys.add(`${result.documentId}__${item.targetLanguage}`));
944
+ }
945
+ const progress$1 = (project?.progress ?? []).map((doc) => ({
946
+ ...doc,
947
+ targets: doc.targets.map(
948
+ (target) => importedKeys.has(`${doc.sourceDocId}__${target.language}`) ? { ...target, imported: !0 } : target
949
+ )
950
+ })), status = progress.summarizeCompletion(progress$1).allDone ? "completed" : "translating";
951
+ return await client.patch(projectId).set({ progress: progress$1, status, lastImportAt: (/* @__PURE__ */ new Date()).toISOString() }).unset(["inbox"]).commit(), { imported, status };
952
+ }
953
+ function isPotentialJson(value) {
954
+ return typeof value == "string" && value.trimStart().startsWith("{");
955
+ }
956
+ function malformedJsonError(source, translated) {
957
+ if (!isPotentialJson(source)) return null;
958
+ try {
959
+ JSON.parse(source);
960
+ } catch {
961
+ return null;
962
+ }
963
+ if (typeof translated != "string") return "translated value is not a string";
964
+ try {
965
+ return JSON.parse(translated), null;
966
+ } catch (err) {
967
+ return err instanceof Error ? err.message : String(err);
968
+ }
969
+ }
970
+ const DRAFTS_PREFIX = "drafts.";
971
+ function publishedId(id) {
972
+ return id.startsWith(DRAFTS_PREFIX) ? id.slice(DRAFTS_PREFIX.length) : id;
973
+ }
974
+ function draftId(id) {
975
+ return id.startsWith(DRAFTS_PREFIX) ? id : DRAFTS_PREFIX + id;
976
+ }
977
+ async function loadEditableDraft(client, id) {
978
+ const dId = draftId(id), draft = await client.fetch("*[_id == $id][0]", { id: dId });
979
+ if (draft) return { draftId: dId, base: draft, exists: !0 };
980
+ const published = await client.fetch("*[_id == $id][0]", { id: publishedId(id) });
981
+ return published ? { draftId: dId, base: published, exists: !1 } : null;
982
+ }
983
+ async function commitToDraft(client, editable, attrs) {
984
+ editable.exists || await client.createIfNotExists({ ...stripSystemFields(editable.base), _id: editable.draftId, _type: editable.base._type }), await client.patch(editable.draftId).set(attrs).commit();
985
+ }
986
+ async function applyFieldLevelTranslations(client, { docId, locale, sourceLanguage, descriptors, translations, sources, languageKey }, log) {
987
+ const editable = await loadEditableDraft(client, docId);
988
+ if (!editable)
989
+ return log({ level: "error", indent: !0, message: `source document not found: ${docId}` }), !1;
990
+ const doc = editable.base, byKey = languageKey === "_key", patchSet = {};
991
+ for (const field of descriptors) {
992
+ if (field.localization !== "field") continue;
993
+ const existing = Array.isArray(locjson.getAtPath(doc, field.path)) ? locjson.getAtPath(doc, field.path) : [], template = (existing.find((m) => m?.[languageKey] === sourceLanguage) ?? existing.find((m) => m?.value !== void 0))?.value;
994
+ let target = structuredClone(template), changed = !1;
995
+ for (const leaf of locjson.enumerateLeaves(field, template)) {
996
+ let value = translations[leaf.fullKey];
997
+ if (value === void 0) continue;
998
+ leaf.kind === "portableText" && Array.isArray(value) && (value = value.map((b) => keyBlock(b)));
999
+ const jsonError = malformedJsonError(sources[leaf.fullKey], value);
1000
+ if (jsonError) {
1001
+ log({ level: "error", indent: !0, message: `${leaf.fullKey}: '${locale}' translation is not valid JSON \u2014 skipped`, value: valuePreview(value), error: jsonError });
1002
+ continue;
1003
+ }
1004
+ target = leaf.valuePath ? locjson.setAtPath(target, leaf.valuePath, value) : value, changed = !0;
1005
+ }
1006
+ if (!changed) continue;
1007
+ const idx = existing.findIndex((m) => m?.[languageKey] === locale);
1008
+ if (idx >= 0 && sameValue(existing[idx]?.value, target)) {
1009
+ log({ level: "skip", indent: !0, message: `${field.path}: '${locale}' unchanged \u2014 skipped` });
1010
+ continue;
1011
+ }
1012
+ if (idx >= 0)
1013
+ patchSet[field.path] = existing.map((m, i) => i === idx ? { ...m, value: target } : m), log({ level: "update", indent: !0, message: `${field.path}: updating '${locale}' field variant`, before: valuePreview(existing[idx]?.value), value: valuePreview(target) });
1014
+ else {
1015
+ const _type = existing.find((m) => m?._type)?._type;
1016
+ patchSet[field.path] = [
1017
+ ...existing,
1018
+ byKey ? { _key: locale, _type, value: target } : { _key: uuid.uuid(), _type, [languageKey]: locale, value: target }
1019
+ ], log({ level: "info", indent: !0, message: `${field.path}: creating '${locale}' field variant`, value: valuePreview(target) });
1020
+ }
1021
+ }
1022
+ return Object.keys(patchSet).length === 0 ? (log({ level: "success", indent: !0, message: `${docId}: no field changes` }), !0) : (await commitToDraft(client, editable, patchSet), log({ level: "success", indent: !0, message: `saved ${Object.keys(patchSet).length} field(s) on draft of ${docId}` }), !0);
1023
+ }
1024
+ async function upsertVariant(client, { sourceId, type, sourceLanguage, locale, fields, sources, languageField }, log) {
1025
+ const source = await client.fetch("*[_id == $id][0]", { id: sourceId });
1026
+ if (!source)
1027
+ return log({ level: "error", indent: !0, message: `source document not found: ${sourceId}` }), !1;
1028
+ const translatedFields = keyPortableText(fields), metadata = await client.fetch(
1029
+ "*[_type == $metaType && references($sourceId)][0]{_id, translations}",
1030
+ { metaType: METADATA_TYPE, sourceId }
1031
+ ), existingVariantId = metadata?.translations?.find((t) => t.language === locale)?.value?._ref;
1032
+ let variantId;
1033
+ if (existingVariantId) {
1034
+ const editable = await loadEditableDraft(client, existingVariantId);
1035
+ if (!editable)
1036
+ return log({ level: "error", indent: !0, message: `'${locale}' document variant not found: ${existingVariantId}` }), !1;
1037
+ const variant = editable.base, changed = {};
1038
+ for (const [path, value] of Object.entries(translatedFields)) {
1039
+ if (sameValue(variant[path], value)) {
1040
+ log({ level: "skip", indent: !0, message: `${path}: unchanged \u2014 skipped` });
1041
+ continue;
1042
+ }
1043
+ const jsonError = malformedJsonError(sources[path], value);
1044
+ if (jsonError) {
1045
+ log({
1046
+ level: "error",
1047
+ indent: !0,
1048
+ message: `${path}: translation is not valid JSON \u2014 skipped`,
1049
+ before: valuePreview(variant[path]),
1050
+ value: valuePreview(value),
1051
+ error: jsonError
1052
+ });
1053
+ continue;
1054
+ }
1055
+ changed[path] = value, log({
1056
+ level: "update",
1057
+ indent: !0,
1058
+ message: `${path}: updating`,
1059
+ before: valuePreview(variant[path]),
1060
+ value: valuePreview(value)
1061
+ });
1062
+ }
1063
+ Object.keys(changed).length > 0 ? (await commitToDraft(client, editable, changed), log({ level: "success", indent: !0, message: `updated ${Object.keys(changed).length} field(s) on '${locale}' document variant draft` })) : log({ level: "success", indent: !0, message: `'${locale}' document variant already up to date` }), variantId = publishedId(existingVariantId);
1064
+ } else {
1065
+ const writableFields = {};
1066
+ for (const [path, value] of Object.entries(translatedFields)) {
1067
+ const jsonError = malformedJsonError(sources[path], value);
1068
+ if (jsonError) {
1069
+ log({
1070
+ level: "error",
1071
+ indent: !0,
1072
+ message: `${path}: translation is not valid JSON \u2014 skipped`,
1073
+ value: valuePreview(value),
1074
+ error: jsonError
1075
+ });
1076
+ continue;
1077
+ }
1078
+ writableFields[path] = value, log({ level: "info", indent: !0, message: `${path}`, value: valuePreview(value) });
1079
+ }
1080
+ log({ level: "info", indent: !0, message: `creating new '${locale}' document variant draft` }), variantId = uuid.uuid(), await client.create({ ...stripSystemFields(source), _id: draftId(variantId), [languageField]: locale, ...writableFields });
1081
+ }
1082
+ return metadata?._id ? existingVariantId || (log({ level: "info", indent: !0, message: "linking variant into translation metadata" }), await client.patch(metadata._id).setIfMissing({ translations: [] }).insert("after", "translations[-1]", [translationEntry(locale, variantId)]).commit()) : (log({ level: "info", indent: !0, message: "creating translation metadata" }), await client.create({
1083
+ _type: METADATA_TYPE,
1084
+ schemaTypes: [type],
1085
+ translations: [translationEntry(sourceLanguage, sourceId), translationEntry(locale, variantId)]
1086
+ })), log({ level: "success", indent: !0, message: `'${locale}' document variant ready: ${variantId}` }), !0;
1087
+ }
1088
+ function translationEntry(language, ref) {
1089
+ return {
1090
+ _key: uuid.uuid(),
1091
+ _type: REFERENCE_VALUE_TYPE,
1092
+ language,
1093
+ value: { _type: "reference", _ref: ref, _weak: !0 }
1094
+ };
1095
+ }
1096
+ function stripSystemFields(doc) {
1097
+ const { _id, _rev, _createdAt, _updatedAt, ...rest } = doc;
1098
+ return rest;
1099
+ }
1100
+ function keyPortableText(fields) {
1101
+ const out = {};
1102
+ for (const [key, value] of Object.entries(fields))
1103
+ Array.isArray(value) && value.some((v) => v && typeof v == "object" && "_type" in v) ? out[key] = value.map((b) => keyBlock(b)) : out[key] = value;
1104
+ return out;
1105
+ }
1106
+ function keyBlock(block) {
1107
+ const keyed = { _key: block._key || uuid.uuid(), ...block };
1108
+ return Array.isArray(keyed.children) && (keyed.children = keyed.children.map((child) => ({
1109
+ _key: child._key || uuid.uuid(),
1110
+ ...child
1111
+ }))), keyed;
1112
+ }
1113
+ function sameValue(a, b) {
1114
+ return stableString(a) === stableString(b);
1115
+ }
1116
+ function stableString(value) {
1117
+ return Array.isArray(value) ? `[${value.map(stableString).join(",")}]` : value && typeof value == "object" ? `{${Object.entries(value).filter(([k]) => k !== "_key").sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableString(v)}`).join(",")}}` : JSON.stringify(value) ?? "undefined";
1118
+ }
1119
+ function languageTitle(languages, id) {
1120
+ return languages.find((l) => l.id === id)?.title || id;
1121
+ }
1122
+ function OpenVariantInStudio({ id, type }) {
1123
+ const { onClick, href } = router.useIntentLink({ intent: "edit", params: { id, type } });
1124
+ return /* @__PURE__ */ jsxRuntime.jsx(
1125
+ ui.Button,
1126
+ {
1127
+ as: "a",
1128
+ href,
1129
+ onClick,
1130
+ mode: "bleed",
1131
+ icon: icons.LaunchIcon,
1132
+ title: "Open this language version in Studio",
1133
+ fontSize: 0,
1134
+ padding: 1
1135
+ }
1136
+ );
1137
+ }
1138
+ function editorUrl(projectUrl, smartcatDocumentId) {
1139
+ if (!projectUrl || !smartcatDocumentId) return null;
1140
+ let origin;
1141
+ try {
1142
+ origin = new URL(projectUrl).origin;
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ const sep = smartcatDocumentId.lastIndexOf("_");
1147
+ if (sep < 0) return null;
1148
+ const fileGuid = smartcatDocumentId.slice(0, sep), langNum = smartcatDocumentId.slice(sep + 1);
1149
+ return `${origin}/open-editor/${fileGuid}?targetLanguageId=${langNum}`;
1150
+ }
1151
+ function ProgressBar({ percent, complete }) {
1152
+ const { color } = ui.useTheme_v2(), pct = Math.max(0, Math.min(100, percent)), fill = (complete ? color.button.default.positive : color.button.default.primary).enabled.bg;
1153
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "transparent", border: !0, radius: 3, style: { height: 10, overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1154
+ "div",
1155
+ {
1156
+ style: {
1157
+ width: `${pct}%`,
1158
+ height: "100%",
1159
+ background: fill,
1160
+ borderRadius: "inherit",
1161
+ transition: "width 0.3s ease"
1162
+ }
1163
+ }
1164
+ ) });
1165
+ }
1166
+ function TargetRow({
1167
+ target,
1168
+ languages,
1169
+ smartcatProjectUrl,
1170
+ variants
1171
+ }) {
1172
+ const percent = progress.targetPercent(target.stages), href = editorUrl(smartcatProjectUrl, target.smartcatDocumentId), label = languageTitle(languages, target.language), variantId = variants?.byLanguage[target.language];
1173
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1174
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1175
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 150, flex: "none" }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 1, children: [
1176
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "medium", children: href ? /* @__PURE__ */ jsxRuntime.jsx(
1177
+ "a",
1178
+ {
1179
+ href,
1180
+ target: "_blank",
1181
+ rel: "noopener noreferrer",
1182
+ title: "Open document in Smartcat editor",
1183
+ children: label
1184
+ }
1185
+ ) : label }),
1186
+ variantId && variants && /* @__PURE__ */ jsxRuntime.jsx(OpenVariantInStudio, { id: variantId, type: variants.type })
1187
+ ] }) }),
1188
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ProgressBar, { percent, complete: target.complete }) }),
1189
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 38, flex: "none" }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, align: "right", children: [
1190
+ percent,
1191
+ "%"
1192
+ ] }) }),
1193
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 72, flex: "none" }, children: target.imported ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", fontSize: 0, padding: 2, children: "imported" }) : target.complete ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", fontSize: 0, padding: 2, children: "ready" }) : null })
1194
+ ] }),
1195
+ target.stages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Inline, { space: 3, style: { paddingLeft: 162 }, children: target.stages.map((stage) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 0, muted: !0, children: [
1196
+ stage.name,
1197
+ ": ",
1198
+ stage.percent,
1199
+ "%"
1200
+ ] }, stage.name)) })
1201
+ ] });
1202
+ }
1203
+ function ItemProgress({
1204
+ doc,
1205
+ languages,
1206
+ smartcatProjectUrl,
1207
+ variants
1208
+ }) {
1209
+ return !doc.targets || doc.targets.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, paddingTop: 1, children: doc.targets.map((target) => /* @__PURE__ */ jsxRuntime.jsx(
1210
+ TargetRow,
1211
+ {
1212
+ target,
1213
+ languages,
1214
+ smartcatProjectUrl,
1215
+ variants
1216
+ },
1217
+ target._key
1218
+ )) });
1219
+ }
1220
+ function logsToText(logs) {
1221
+ return logs.map((l) => {
1222
+ const prefix = l.indent ? " " : "", detail = [
1223
+ l.before !== void 0 ? `before: ${l.before}` : null,
1224
+ l.value !== void 0 ? `${l.before !== void 0 ? "after" : "value"}: ${l.value}` : null,
1225
+ l.error !== void 0 ? `error: ${l.error}` : null
1226
+ ].filter(Boolean).map((d) => ` ${d}`).join(`
1227
+ `);
1228
+ return `${prefix}${l.message}${detail ? `
1229
+ ${detail}` : ""}`;
1230
+ }).join(`
1231
+ `);
1232
+ }
1233
+ const MONO = "var(--font-family-mono, monospace)", VALUE_BG = "var(--card-code-bg-color, rgba(127,127,127,0.12))", GLYPH = { info: "", success: "\u2713", skip: "\u2013", error: "\u2717", update: "" };
1234
+ function levelColor(theme, level) {
1235
+ const muted = theme.sanity.color.muted;
1236
+ if (level === "success") return muted.positive.enabled.fg;
1237
+ if (level === "error") return muted.critical.enabled.fg;
1238
+ if (level === "update") return muted.primary.enabled.fg;
1239
+ }
1240
+ function valueBg(theme, level) {
1241
+ return level === "error" ? theme.sanity.color.muted.critical.enabled.bg : VALUE_BG;
1242
+ }
1243
+ function ValueBox({ label, text, bg }) {
1244
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, padding: 2, style: { background: bg, borderRadius: 3, minWidth: 0 }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1245
+ label && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, weight: "semibold", children: label }),
1246
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, style: { fontFamily: MONO, whiteSpace: "pre-wrap" }, children: text })
1247
+ ] }) });
1248
+ }
1249
+ function LogRow({ line }) {
1250
+ const theme = ui.useTheme(), [open, setOpen] = react.useState(!1), hasDetail = line.value !== void 0 || line.before !== void 0 || line.error !== void 0, toggle = hasDetail ? () => setOpen((v) => !v) : void 0;
1251
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { paddingLeft: line.indent ? 4 : 0, children: [
1252
+ /* @__PURE__ */ jsxRuntime.jsxs(
1253
+ ui.Text,
1254
+ {
1255
+ size: 1,
1256
+ muted: line.level === "skip",
1257
+ onClick: toggle,
1258
+ style: {
1259
+ fontFamily: MONO,
1260
+ whiteSpace: "pre-wrap",
1261
+ cursor: hasDetail ? "pointer" : "default",
1262
+ color: levelColor(theme, line.level)
1263
+ },
1264
+ children: [
1265
+ hasDetail && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { opacity: 0.6, userSelect: "none" }, children: [
1266
+ open ? "\u25BE" : "\u25B8",
1267
+ " "
1268
+ ] }),
1269
+ GLYPH[line.level] ? /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { opacity: 0.7 }, children: [
1270
+ GLYPH[line.level],
1271
+ " "
1272
+ ] }) : null,
1273
+ line.message
1274
+ ]
1275
+ }
1276
+ ),
1277
+ hasDetail && open && /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1278
+ line.before !== void 0 ? (
1279
+ // Before/after side by side, two boxes with a gap.
1280
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, align: "stretch", children: [
1281
+ /* @__PURE__ */ jsxRuntime.jsx(ValueBox, { label: "Before", text: line.before, bg: valueBg(theme, line.level) }),
1282
+ /* @__PURE__ */ jsxRuntime.jsx(ValueBox, { label: "After", text: line.value ?? "", bg: valueBg(theme, line.level) })
1283
+ ] })
1284
+ ) : line.value !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(ValueBox, { text: line.value, bg: valueBg(theme, line.level) }) : null,
1285
+ line.error !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(ValueBox, { label: "JSON error", text: line.error, bg: valueBg(theme, line.level) })
1286
+ ] }) })
1287
+ ] });
1288
+ }
1289
+ function LogPanel({
1290
+ logs,
1291
+ expanded: expandedProp,
1292
+ onExpandedChange
1293
+ }) {
1294
+ const [internalExpanded, setInternalExpanded] = react.useState(!1), expanded = expandedProp ?? internalExpanded, setExpanded = (next) => {
1295
+ onExpandedChange?.(next), expandedProp === void 0 && setInternalExpanded(next);
1296
+ }, [copied, setCopied] = react.useState(!1), scrollRef = react.useRef(null), stickToBottom = react.useRef(!0), copyTimeout = react.useRef(null);
1297
+ react.useEffect(() => () => {
1298
+ copyTimeout.current && clearTimeout(copyTimeout.current);
1299
+ }, []);
1300
+ const handleCopy = () => {
1301
+ navigator.clipboard?.writeText(logsToText(logs)), setCopied(!0), copyTimeout.current && clearTimeout(copyTimeout.current), copyTimeout.current = setTimeout(() => setCopied(!1), 1500);
1302
+ }, handleScroll = () => {
1303
+ const el = scrollRef.current;
1304
+ el && (stickToBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 8);
1305
+ };
1306
+ return react.useLayoutEffect(() => {
1307
+ const el = scrollRef.current;
1308
+ el && stickToBottom.current && (el.scrollTop = el.scrollHeight);
1309
+ }, [logs, expanded]), /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1310
+ /* @__PURE__ */ jsxRuntime.jsx(
1311
+ ui.Flex,
1312
+ {
1313
+ align: "center",
1314
+ gap: 2,
1315
+ onClick: () => setExpanded(!expanded),
1316
+ style: { cursor: "pointer", userSelect: "none" },
1317
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
1318
+ expanded ? "\u25BE" : "\u25B8",
1319
+ " ",
1320
+ logs.length > 0 ? `Log (${logs.length})` : "Log"
1321
+ ] })
1322
+ }
1323
+ ),
1324
+ expanded && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { radius: 2, border: !0, style: { position: "relative" }, children: logs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "The log is empty \u2014 it will show the status of any send or import operation." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1325
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { position: "absolute", top: 6, right: 6, zIndex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1326
+ ui.Button,
1327
+ {
1328
+ mode: "ghost",
1329
+ padding: 2,
1330
+ fontSize: 1,
1331
+ icon: copied ? icons.CheckmarkIcon : icons.CopyIcon,
1332
+ tone: copied ? "positive" : "default",
1333
+ onClick: handleCopy,
1334
+ title: copied ? "Copied" : "Copy log"
1335
+ }
1336
+ ) }),
1337
+ /* @__PURE__ */ jsxRuntime.jsx(
1338
+ "div",
1339
+ {
1340
+ ref: scrollRef,
1341
+ onScroll: handleScroll,
1342
+ style: { maxHeight: 320, overflowY: "auto", padding: 12 },
1343
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: logs.map((line, i) => /* @__PURE__ */ jsxRuntime.jsx(LogRow, { line }, i)) })
1344
+ }
1345
+ )
1346
+ ] }) })
1347
+ ] });
1348
+ }
1349
+ function parseFunctionLog(raw) {
1350
+ if (!raw) return [];
1351
+ try {
1352
+ const parsed = JSON.parse(raw);
1353
+ return Array.isArray(parsed) ? parsed : [];
1354
+ } catch {
1355
+ return [];
1356
+ }
1357
+ }
1358
+ function languageMappingLabel(language) {
1359
+ const code = language.smartcatLanguage || language.id;
1360
+ return code === language.id ? language.id : `${language.id} \u2192 ${code}`;
1361
+ }
1362
+ function DocTitle({ doc }) {
1363
+ const schema = sanity.useSchema(), schemaType = doc ? schema.get(doc._type) : void 0, preview = sanity.useValuePreview({
1364
+ enabled: !!(doc && schemaType),
1365
+ schemaType,
1366
+ value: doc ? { _id: doc._id, _type: doc._type } : void 0
1367
+ }), title = typeof preview.value?.title == "string" ? preview.value.title : void 0;
1368
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: title ?? (preview.isLoading ? "\u2026" : UNTITLED) });
1369
+ }
1370
+ const IN_FLIGHT = ["queued", "importing", "downloaded"], CAN_IMPORT = ["sent", "translating", "completed"], rotate = styled.keyframes`
1371
+ to { transform: rotate(360deg); }
1372
+ `, SpinningSyncIcon = styled__default.default(icons.SyncIcon)`
1373
+ ${({ $spinning }) => $spinning && styled.css`
1374
+ animation: ${rotate} 0.8s linear infinite;
1375
+ `}
1376
+ `;
1377
+ function LastUpdate({ date }) {
1378
+ const relative = sanity.useRelativeTime(date, { useTemporalPhrase: !0 });
1379
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, title: new Date(date).toLocaleString(), children: [
1380
+ "Last update: ",
1381
+ relative
1382
+ ] });
1383
+ }
1384
+ function OpenInStudioButton({ id, type }) {
1385
+ const { onClick, href } = router.useIntentLink({ intent: "edit", params: { id, type } });
1386
+ return /* @__PURE__ */ jsxRuntime.jsx(
1387
+ ui.Button,
1388
+ {
1389
+ as: "a",
1390
+ href,
1391
+ onClick,
1392
+ mode: "bleed",
1393
+ icon: icons.LaunchIcon,
1394
+ text: "Open in Studio",
1395
+ fontSize: 0,
1396
+ padding: 2
1397
+ }
1398
+ );
1399
+ }
1400
+ const CodeArea = styled__default.default.textarea`
1401
+ width: 100%;
1402
+ height: 70vh;
1403
+ resize: none;
1404
+ border: none;
1405
+ outline: none;
1406
+ padding: 0;
1407
+ background: transparent;
1408
+ color: inherit;
1409
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1410
+ font-size: 12px;
1411
+ line-height: 1.5;
1412
+ white-space: pre;
1413
+ tab-size: 2;
1414
+ `;
1415
+ function ViewLocjsonButton(props) {
1416
+ const { docId, type, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField } = props, [open, setOpen] = react.useState(!1), [content, setContent] = react.useState(null), [filename, setFilename] = react.useState(null), [error, setError] = react.useState(null), build = react.useCallback(async () => {
1417
+ setOpen(!0), setContent(null), setFilename(null), setError(null);
1418
+ try {
1419
+ const doc = await client.fetch("*[_id == $id][0]", { id: docId });
1420
+ if (!doc) throw new Error(`Document not found: ${docId}`);
1421
+ const fields = locjson.getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] }), locjson$1 = locjson.serializeToLocjson(doc, fields, {
1422
+ sourceLanguage,
1423
+ fieldLanguageKey: fieldI18nLanguageField
1424
+ }), smartcatName = locjson.buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc));
1425
+ setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson$1, null, 2));
1426
+ } catch (err) {
1427
+ setError(err instanceof Error ? err.message : String(err));
1428
+ }
1429
+ }, [client, schema, docId, type, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField]), download = react.useCallback(() => {
1430
+ if (content == null || !filename) return;
1431
+ const url = URL.createObjectURL(new Blob([content], { type: "application/json" })), anchor = document.createElement("a");
1432
+ anchor.href = url, anchor.download = filename, anchor.click(), URL.revokeObjectURL(url);
1433
+ }, [content, filename]);
1434
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1435
+ /* @__PURE__ */ jsxRuntime.jsx(
1436
+ ui.Button,
1437
+ {
1438
+ mode: "bleed",
1439
+ icon: icons.JsonIcon,
1440
+ title: "View LocJSON",
1441
+ fontSize: 0,
1442
+ padding: 2,
1443
+ onClick: build
1444
+ }
1445
+ ),
1446
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1447
+ ui.Dialog,
1448
+ {
1449
+ id: `locjson-${docId}`,
1450
+ header: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1451
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { weight: "semibold", children: [
1452
+ "LocJSON \xB7 ",
1453
+ type
1454
+ ] }),
1455
+ /* @__PURE__ */ jsxRuntime.jsx(
1456
+ ui.Button,
1457
+ {
1458
+ icon: icons.DownloadIcon,
1459
+ text: "Download",
1460
+ mode: "ghost",
1461
+ fontSize: 1,
1462
+ padding: 2,
1463
+ disabled: content == null,
1464
+ onClick: download
1465
+ }
1466
+ )
1467
+ ] }),
1468
+ onClose: () => setOpen(!1),
1469
+ width: 3,
1470
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: error ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: error }) : content === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsx(
1471
+ CodeArea,
1472
+ {
1473
+ readOnly: !0,
1474
+ spellCheck: !1,
1475
+ value: content,
1476
+ onFocus: (e) => e.currentTarget.select()
1477
+ }
1478
+ ) })
1479
+ }
1480
+ )
1481
+ ] });
1482
+ }
1483
+ function ProjectView({
1484
+ projectId,
1485
+ onBack,
1486
+ languages,
1487
+ sourceLanguage,
1488
+ documentI18nLanguageField,
1489
+ fieldI18nLanguageField
1490
+ }) {
1491
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), schema = sanity.useSchema(), toast = ui.useToast(), [project, setProject] = react.useState(void 0), [name, setName] = react.useState(""), [busy, setBusy] = react.useState(!1), [sending, setSending] = react.useState(!1), [importing, setImporting] = react.useState(!1), [cancelling, setCancelling] = react.useState(!1), [refreshing, setRefreshing] = react.useState(!1), [targets, setTargets] = react.useState([]), [confirmDelete, setConfirmDelete] = react.useState(!1), [markedForDeletion, setMarkedForDeletion] = react.useState(/* @__PURE__ */ new Set()), [logs, setLogs] = react.useState([]), [logExpanded, setLogExpanded] = react.useState(!1), appendLog = react.useCallback((line) => setLogs((prev) => [...prev, line]), []), appendServerLog = react.useCallback(
1492
+ (raw, header) => {
1493
+ const lines = parseFunctionLog(raw);
1494
+ setLogs(
1495
+ (prev) => lines.length === 0 ? [...prev, { level: "info", message: `${header}: no server log returned \u2014 deploy the latest Functions to see server-side steps` }] : [...prev, { level: "info", message: header }, ...lines.map((l) => ({ ...l, indent: !0 }))]
1496
+ );
1497
+ },
1498
+ []
1499
+ ), seeded = react.useRef(!1), toggleMark = react.useCallback((key) => {
1500
+ setMarkedForDeletion((prev) => {
1501
+ const next = new Set(prev);
1502
+ return next.has(key) ? next.delete(key) : next.add(key), next;
1503
+ });
1504
+ }, []), load = react.useCallback(() => client.fetch(PROJECT_DETAIL_QUERY, { id: projectId }).then((res) => {
1505
+ setProject(res), res && setName(res.name), res && !seeded.current && (setTargets(res.targetLanguages ?? []), seeded.current = !0);
1506
+ }).catch((err) => toast.push({ status: "error", title: "Failed to load project", description: String(err) })), [client, projectId, toast]);
1507
+ react.useEffect(() => {
1508
+ load();
1509
+ const sub = client.listen(PROJECT_DETAIL_QUERY, { id: projectId }, { visibility: "query" }).subscribe({ next: () => load(), error: () => {
1510
+ } });
1511
+ return () => sub.unsubscribe();
1512
+ }, [client, projectId, load]);
1513
+ const processingDownload = react.useRef(!1);
1514
+ react.useEffect(() => {
1515
+ if (project?.status === "importing" && (processingDownload.current = !1), project?.status !== "downloaded" || processingDownload.current) return;
1516
+ processingDownload.current = !0;
1517
+ const remaining = project.importRemaining ?? 0;
1518
+ appendLog({ level: "info", message: "Translations downloaded \u2014 applying to documents" }), appendServerLog(project.functionLog, "Import function"), applyImportedTranslations({
1519
+ client,
1520
+ schema,
1521
+ projectId,
1522
+ documentLanguageField: documentI18nLanguageField,
1523
+ fieldLanguageKey: fieldI18nLanguageField,
1524
+ onLog: appendLog
1525
+ }).then(async ({ imported, status }) => {
1526
+ if (remaining > 0 && imported > 0) {
1527
+ appendLog({ level: "success", message: `Imported ${imported} localized version(s)` }), appendLog({ level: "info", message: `${remaining} target(s) still on Smartcat \u2014 continuing import automatically` }), processingDownload.current = !1, await client.patch(projectId).set({ status: "importing", lastError: null }).commit();
1528
+ return;
1529
+ }
1530
+ const description = status === "completed" ? "All targets are fully translated \u2014 project completed." : "Some targets are still translating \u2014 import again later to pull the rest.";
1531
+ appendLog({ level: "success", message: `Imported ${imported} localized version(s)` }), appendLog({ level: status === "completed" ? "success" : "info", message: description }), toast.push({ status: "success", title: `Imported ${imported} localized version(s)`, description });
1532
+ }).catch((err) => {
1533
+ appendLog({ level: "error", message: `Failed to apply translations: ${String(err)}` }), toast.push({ status: "error", title: "Failed to apply translations", description: String(err) });
1534
+ });
1535
+ }, [project?.status, client, schema, projectId, toast, documentI18nLanguageField, fieldI18nLanguageField, appendLog, appendServerLog]);
1536
+ const exportInFlight = react.useRef(!1);
1537
+ react.useEffect(() => {
1538
+ exportInFlight.current && (project?.status === "sent" ? (appendServerLog(project.functionLog, "Export function"), appendLog({ level: "success", message: "Sent to Smartcat" }), project.smartcatProjectUrl && appendLog({ level: "info", message: `Smartcat project: ${project.smartcatProjectUrl}` }), parseFunctionLog(project.functionLog).some((l) => l.level === "error") && (toast.push({ status: "warning", title: "There were errors sending content into Smartcat \u2014 check the log" }), setLogExpanded(!0)), exportInFlight.current = !1) : project?.status === "error" && (appendServerLog(project.functionLog, "Export function"), appendLog({ level: "error", message: `Export failed: ${project.lastError ?? "unknown error"}` }), exportInFlight.current = !1));
1539
+ }, [project?.status, project?.smartcatProjectUrl, project?.lastError, project?.functionLog, appendLog, appendServerLog, toast]);
1540
+ const requestProgress = react.useCallback(() => client.patch(projectId).set({ progressRequestedAt: (/* @__PURE__ */ new Date()).toISOString() }).commit(), [client, projectId]), syncedAtRef = react.useRef(void 0);
1541
+ react.useEffect(() => {
1542
+ syncedAtRef.current = project?.progressSyncedAt;
1543
+ }, [project?.progressSyncedAt]);
1544
+ const refreshBaseline = react.useRef(void 0), refreshTimeout = react.useRef(null);
1545
+ react.useEffect(
1546
+ () => () => {
1547
+ refreshTimeout.current && clearTimeout(refreshTimeout.current);
1548
+ },
1549
+ []
1550
+ );
1551
+ const handleRefreshProgress = react.useCallback(async () => {
1552
+ refreshBaseline.current = syncedAtRef.current, setRefreshing(!0), refreshTimeout.current && clearTimeout(refreshTimeout.current), refreshTimeout.current = setTimeout(() => setRefreshing(!1), 3e4);
1553
+ try {
1554
+ await requestProgress();
1555
+ } catch (err) {
1556
+ setRefreshing(!1), refreshTimeout.current && clearTimeout(refreshTimeout.current), toast.push({ status: "error", title: "Failed to refresh progress", description: String(err) });
1557
+ }
1558
+ }, [requestProgress, toast]);
1559
+ react.useEffect(() => {
1560
+ refreshing && project?.progressSyncedAt !== refreshBaseline.current && (setRefreshing(!1), refreshTimeout.current && clearTimeout(refreshTimeout.current), project?.lastError && (appendServerLog(project.functionLog, "Progress function"), appendLog({ level: "error", message: `Progress refresh failed: ${project.lastError}` }), toast.push({ status: "error", title: "Failed to refresh progress", description: project.lastError }), setLogExpanded(!0)));
1561
+ }, [project?.progressSyncedAt, project?.lastError, project?.functionLog, refreshing, appendServerLog, appendLog, toast]);
1562
+ const progressRequested = react.useRef(!1);
1563
+ react.useEffect(() => {
1564
+ progressRequested.current || !project?.smartcatProjectId || (progressRequested.current = !0, handleRefreshProgress());
1565
+ }, [project?.smartcatProjectId, handleRefreshProgress]);
1566
+ const handleRename = react.useCallback(async () => {
1567
+ const trimmed = name.trim();
1568
+ if (!(!trimmed || trimmed === project?.name)) {
1569
+ setBusy(!0);
1570
+ try {
1571
+ await client.patch(projectId).set({ name: trimmed }).commit(), toast.push({ status: "success", title: "Renamed project" });
1572
+ } catch (err) {
1573
+ toast.push({ status: "error", title: "Failed to rename", description: String(err) });
1574
+ } finally {
1575
+ setBusy(!1);
1576
+ }
1577
+ }
1578
+ }, [name, project, client, projectId, toast]), handleDelete = react.useCallback(async () => {
1579
+ setBusy(!0);
1580
+ try {
1581
+ await client.delete(projectId), toast.push({ status: "success", title: "Deleted project" }), onBack();
1582
+ } catch (err) {
1583
+ toast.push({ status: "error", title: "Failed to delete", description: String(err) }), setBusy(!1);
1584
+ }
1585
+ }, [client, projectId, toast, onBack]), toggleTarget = react.useCallback((id) => {
1586
+ setTargets((prev) => prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]);
1587
+ }, []), handleImport = react.useCallback(async () => {
1588
+ setImporting(!0);
1589
+ try {
1590
+ processingDownload.current = !1, await client.patch(projectId).set({ status: "importing", lastError: null }).commit(), setLogs([]), appendLog({ level: "info", message: "Importing from Smartcat" }), appendLog({ level: "info", message: "The import function will download translations and create localized versions." }), toast.push({
1591
+ status: "success",
1592
+ title: "Importing from Smartcat",
1593
+ description: "The import function will download translations and create localized versions."
1594
+ });
1595
+ } catch (err) {
1596
+ toast.push({ status: "error", title: "Failed to start import", description: String(err) });
1597
+ } finally {
1598
+ setImporting(!1);
1599
+ }
1600
+ }, [client, projectId, toast, appendLog]), handleCancel = react.useCallback(async () => {
1601
+ setCancelling(!0);
1602
+ try {
1603
+ await client.patch(projectId).set({ status: "draft" }).commit(), toast.push({ status: "success", title: "Cancelled \u2014 project reset to draft" });
1604
+ } catch (err) {
1605
+ toast.push({ status: "error", title: "Failed to cancel", description: String(err) });
1606
+ } finally {
1607
+ setCancelling(!1);
1608
+ }
1609
+ }, [client, projectId, toast]), handleSend = react.useCallback(async () => {
1610
+ if (targets.length === 0) {
1611
+ toast.push({ status: "warning", title: "Select at least one target language" });
1612
+ return;
1613
+ }
1614
+ setSending(!0), setLogs([]);
1615
+ try {
1616
+ const deletions = [];
1617
+ for (const item of project?.items ?? []) {
1618
+ if (!markedForDeletion.has(item._key) || !item.doc?._id) continue;
1619
+ const smartcatDocumentId = (project?.progress ?? []).find((p) => p.sourceDocId === item.doc._id)?.targets?.find((t) => t.smartcatDocumentId)?.smartcatDocumentId;
1620
+ deletions.push({
1621
+ itemKey: item._key,
1622
+ sourceDocId: item.doc._id,
1623
+ smartcatDocumentId
1624
+ });
1625
+ }
1626
+ const { prepared, deleting } = await prepareExport({
1627
+ client,
1628
+ schema,
1629
+ projectId,
1630
+ targetLanguages: targets,
1631
+ sourceLanguage,
1632
+ languages,
1633
+ exclude: [documentI18nLanguageField],
1634
+ fieldLanguageKey: fieldI18nLanguageField,
1635
+ deletions,
1636
+ onLog: appendLog
1637
+ });
1638
+ setMarkedForDeletion(/* @__PURE__ */ new Set()), exportInFlight.current = !0, appendLog({ level: "info", message: "Uploading to Smartcat via the export function\u2026" }), toast.push({
1639
+ status: "success",
1640
+ title: "Queued for Smartcat",
1641
+ description: `Prepared ${prepared} file(s)` + (deleting ? ` and ${deleting} for deletion` : "") + "; the export function will sync them."
1642
+ });
1643
+ } catch (err) {
1644
+ appendLog({ level: "error", message: `Failed to queue: ${String(err)}` }), toast.push({ status: "error", title: "Failed to queue", description: String(err) });
1645
+ } finally {
1646
+ setSending(!1);
1647
+ }
1648
+ }, [targets, client, projectId, toast, schema, sourceLanguage, languages, documentI18nLanguageField, fieldI18nLanguageField, project, markedForDeletion, appendLog]), handleRemoveItem = react.useCallback(
1649
+ async (item) => {
1650
+ try {
1651
+ await client.patch(projectId).unset([`items[_key=="${item._key}"]`]).commit(), toast.push({ status: "success", title: "Removed item" });
1652
+ } catch (err) {
1653
+ toast.push({ status: "error", title: "Failed to remove item", description: String(err) });
1654
+ }
1655
+ },
1656
+ [client, projectId, toast]
1657
+ );
1658
+ if (project === void 0)
1659
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) });
1660
+ if (project === null)
1661
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1662
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { icon: icons.ArrowLeftIcon, text: "Back", mode: "bleed", onClick: onBack }),
1663
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 2, tone: "critical", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "This project no longer exists." }) })
1664
+ ] });
1665
+ const items = project.items ?? [], nameChanged = name.trim() !== project.name && name.trim().length > 0, languagesLocked = !!project.smartcatProjectId, progress2 = project.progress ?? [], progressByDoc = /* @__PURE__ */ new Map();
1666
+ for (const p of progress2)
1667
+ p.sourceDocId && progressByDoc.set(p.sourceDocId, p);
1668
+ const markedCount = items.filter((i) => markedForDeletion.has(i._key)).length, modeByType = /* @__PURE__ */ new Map();
1669
+ for (const item of items) {
1670
+ const t = item.doc?._type;
1671
+ t && !modeByType.has(t) && modeByType.set(t, locjson.localizationMode(schema.get(t)));
1672
+ }
1673
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
1674
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1675
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { icon: icons.ArrowLeftIcon, text: "Projects", mode: "bleed", onClick: onBack }),
1676
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1 }),
1677
+ /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: project.status }),
1678
+ /* @__PURE__ */ jsxRuntime.jsx(
1679
+ ui.Button,
1680
+ {
1681
+ icon: icons.TrashIcon,
1682
+ title: "Delete project\u2026",
1683
+ tone: "critical",
1684
+ mode: "bleed",
1685
+ fontSize: 1,
1686
+ padding: 2,
1687
+ onClick: () => setConfirmDelete(!0),
1688
+ disabled: busy
1689
+ }
1690
+ )
1691
+ ] }),
1692
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1693
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "flex-end", children: [
1694
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1695
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", muted: !0, children: "Project name" }),
1696
+ /* @__PURE__ */ jsxRuntime.jsx(
1697
+ ui.TextInput,
1698
+ {
1699
+ value: name,
1700
+ onChange: (e) => setName(e.currentTarget.value),
1701
+ onKeyDown: (e) => {
1702
+ e.key === "Enter" && handleRename();
1703
+ },
1704
+ disabled: busy
1705
+ }
1706
+ )
1707
+ ] }) }),
1708
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Save", onClick: handleRename, disabled: !nameChanged || busy, loading: busy })
1709
+ ] }),
1710
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1711
+ "Source: ",
1712
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: languageTitle(languages, project.sourceLanguage || sourceLanguage) })
1713
+ ] }),
1714
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1715
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Target languages" }),
1716
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 6, align: "center", children: [
1717
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, padding: 3, children: languages.filter((l) => l.id !== (project.sourceLanguage || sourceLanguage)).map((l) => {
1718
+ const selected = targets.includes(l.id), disabled = sending || languagesLocked;
1719
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, as: "label", style: { cursor: disabled ? "default" : "pointer" }, children: [
1720
+ /* @__PURE__ */ jsxRuntime.jsx(
1721
+ ui.Checkbox,
1722
+ {
1723
+ checked: selected,
1724
+ disabled,
1725
+ onChange: () => toggleTarget(l.id)
1726
+ }
1727
+ ),
1728
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
1729
+ l.title,
1730
+ /* @__PURE__ */ jsxRuntime.jsx(
1731
+ "span",
1732
+ {
1733
+ title: `Sanity language code: '${l.id}'
1734
+ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1735
+ style: { color: "var(--card-muted-fg-color)", opacity: 0.6, marginLeft: "1em" },
1736
+ children: languageMappingLabel(l)
1737
+ }
1738
+ )
1739
+ ] })
1740
+ ] }, l.id);
1741
+ }) }),
1742
+ project.smartcatProjectUrl ? /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, style: { maxWidth: 500 }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1743
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1744
+ "Smartcat project:",
1745
+ " ",
1746
+ /* @__PURE__ */ jsxRuntime.jsx(
1747
+ "a",
1748
+ {
1749
+ href: project.smartcatProjectUrl,
1750
+ target: "_blank",
1751
+ rel: "noopener noreferrer",
1752
+ title: "Open project in Smartcat",
1753
+ children: project.name
1754
+ }
1755
+ )
1756
+ ] }),
1757
+ languagesLocked && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Target languages are locked once the project has been sent to Smartcat." })
1758
+ ] }) }) : targets.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, style: { maxWidth: 500 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Select one or more languages to translate your content into before you can send it to Smartcat for translation." }) })
1759
+ ] })
1760
+ ] })
1761
+ ] }) }),
1762
+ project.lastError && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, tone: "critical", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: project.lastError }) }),
1763
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, wrap: "wrap", children: [
1764
+ /* @__PURE__ */ jsxRuntime.jsx(
1765
+ ui.Button,
1766
+ {
1767
+ icon: icons.PublishIcon,
1768
+ text: "Send to Smartcat",
1769
+ mode: !languagesLocked && items.length > 0 ? void 0 : "ghost",
1770
+ tone: !languagesLocked && items.length > 0 ? "primary" : void 0,
1771
+ onClick: handleSend,
1772
+ loading: sending,
1773
+ disabled: sending || items.length === 0 || targets.length === 0 || IN_FLIGHT.includes(project.status)
1774
+ }
1775
+ ),
1776
+ /* @__PURE__ */ jsxRuntime.jsx(
1777
+ ui.Button,
1778
+ {
1779
+ icon: icons.DownloadIcon,
1780
+ text: "Import translations",
1781
+ mode: languagesLocked ? void 0 : "ghost",
1782
+ tone: languagesLocked ? "primary" : void 0,
1783
+ onClick: handleImport,
1784
+ loading: importing,
1785
+ disabled: importing || !project.smartcatProjectId || !CAN_IMPORT.includes(project.status)
1786
+ }
1787
+ ),
1788
+ project.smartcatProjectId && /* @__PURE__ */ jsxRuntime.jsx(
1789
+ ui.Button,
1790
+ {
1791
+ icon: /* @__PURE__ */ jsxRuntime.jsx(SpinningSyncIcon, { $spinning: refreshing }),
1792
+ text: "Refresh progress",
1793
+ mode: "bleed",
1794
+ onClick: handleRefreshProgress,
1795
+ disabled: refreshing
1796
+ }
1797
+ ),
1798
+ project.progressSyncedAt && /* @__PURE__ */ jsxRuntime.jsx(LastUpdate, { date: project.progressSyncedAt }),
1799
+ markedCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1800
+ markedCount,
1801
+ " marked for deletion \u2014 applied on re-sync"
1802
+ ] }),
1803
+ IN_FLIGHT.includes(project.status) && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
1804
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1805
+ "Project is ",
1806
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: statusLabel(project.status) }),
1807
+ " \u2014 wait for it to finish before re-sending."
1808
+ ] }),
1809
+ /* @__PURE__ */ jsxRuntime.jsx(
1810
+ ui.Button,
1811
+ {
1812
+ icon: icons.CloseIcon,
1813
+ text: "Cancel",
1814
+ mode: "bleed",
1815
+ tone: "critical",
1816
+ fontSize: 1,
1817
+ padding: 2,
1818
+ onClick: handleCancel,
1819
+ loading: cancelling,
1820
+ disabled: cancelling
1821
+ }
1822
+ )
1823
+ ] })
1824
+ ] }),
1825
+ /* @__PURE__ */ jsxRuntime.jsx(LogPanel, { logs, expanded: logExpanded, onExpandedChange: setLogExpanded }),
1826
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1827
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
1828
+ items.length,
1829
+ " ",
1830
+ items.length === 1 ? "Item" : "Items"
1831
+ ] }) }),
1832
+ items.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: items.map((item) => {
1833
+ const docProgress = item.doc ? progressByDoc.get(item.doc._id) : void 0, isLinked = !!docProgress?.targets?.some((t) => t.smartcatDocumentId), isMarked = markedForDeletion.has(item._key);
1834
+ return /* @__PURE__ */ jsxRuntime.jsx(
1835
+ ui.Card,
1836
+ {
1837
+ padding: 3,
1838
+ radius: 2,
1839
+ shadow: 1,
1840
+ tone: isMarked ? "critical" : void 0,
1841
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1842
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1843
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1844
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "medium", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: isMarked ? { textDecoration: "line-through" } : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(DocTitle, { doc: item.doc }) }) }),
1845
+ item.doc?._type && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", children: item.doc._type }),
1846
+ item.doc?._type && modeByType.has(item.doc._type) && /* @__PURE__ */ jsxRuntime.jsx(
1847
+ ui.Badge,
1848
+ {
1849
+ tone: "default",
1850
+ fontSize: 0,
1851
+ title: modeByType.get(item.doc._type) === "field" ? "This item is translated in field mode; translations will be stored as language variants of each field" : "This item is translated in document mode; translations will be stored as linked documents",
1852
+ children: modeByType.get(item.doc._type) === "field" ? "fields" : "document"
1853
+ }
1854
+ ),
1855
+ item.doc && /* @__PURE__ */ jsxRuntime.jsx(OpenInStudioButton, { id: item.doc._id, type: item.doc._type }),
1856
+ item.doc && /* @__PURE__ */ jsxRuntime.jsx(
1857
+ ViewLocjsonButton,
1858
+ {
1859
+ docId: item.doc._id,
1860
+ type: item.doc._type,
1861
+ client,
1862
+ schema,
1863
+ sourceLanguage,
1864
+ documentI18nLanguageField,
1865
+ fieldI18nLanguageField
1866
+ }
1867
+ )
1868
+ ] }),
1869
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1 }),
1870
+ isLinked ? /* @__PURE__ */ jsxRuntime.jsx(
1871
+ ui.Button,
1872
+ {
1873
+ mode: isMarked ? "default" : "bleed",
1874
+ tone: "critical",
1875
+ icon: icons.RemoveCircleIcon,
1876
+ title: isMarked ? "Unmark for deletion" : "Mark for deletion",
1877
+ padding: 2,
1878
+ onClick: () => toggleMark(item._key)
1879
+ }
1880
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1881
+ ui.Button,
1882
+ {
1883
+ mode: "bleed",
1884
+ tone: "critical",
1885
+ icon: icons.TrashIcon,
1886
+ title: "Remove from project",
1887
+ onClick: () => handleRemoveItem(item)
1888
+ }
1889
+ )
1890
+ ] }),
1891
+ docProgress && /* @__PURE__ */ jsxRuntime.jsx(
1892
+ ItemProgress,
1893
+ {
1894
+ doc: docProgress,
1895
+ languages,
1896
+ smartcatProjectUrl: project.smartcatProjectUrl,
1897
+ variants: item.doc ? {
1898
+ type: item.doc._type,
1899
+ byLanguage: Object.fromEntries(
1900
+ (item.translations ?? []).map((t) => [t.language, t.id])
1901
+ )
1902
+ } : void 0
1903
+ }
1904
+ )
1905
+ ] })
1906
+ },
1907
+ item._key
1908
+ );
1909
+ }) }),
1910
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", size: 1, children: "Add content from a document\u2019s \u201C\u22EF \u2192 Add to translation project\u201D menu." }) })
1911
+ ] }),
1912
+ confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
1913
+ ui.Dialog,
1914
+ {
1915
+ id: "confirm-delete",
1916
+ header: "Delete translation project?",
1917
+ onClose: () => setConfirmDelete(!1),
1918
+ width: 1,
1919
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1920
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { children: [
1921
+ "Delete ",
1922
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: project.name }),
1923
+ "? This removes the project only \u2014 the referenced content documents are not deleted.",
1924
+ project.smartcatProjectId && " The Smartcat project is not deleted either and will remain in your Smartcat workspace."
1925
+ ] }),
1926
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "flex-end", gap: 2, children: [
1927
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Cancel", onClick: () => setConfirmDelete(!1), disabled: busy }),
1928
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { tone: "critical", text: "Delete", icon: icons.TrashIcon, onClick: handleDelete, loading: busy })
1929
+ ] })
1930
+ ] }) })
1931
+ }
1932
+ )
1933
+ ] });
1934
+ }
1935
+ function createDashboardComponent(config) {
1936
+ const { sourceLanguage, languages, documentI18nLanguageField, fieldI18nLanguageField } = config;
1937
+ return function() {
1938
+ const router$1 = router.useRouter(), projectId = router.useRouterState(
1939
+ react.useCallback((state) => state.projectId ?? null, [])
1940
+ ), openProject = react.useCallback((id) => router$1.navigate({ projectId: id }), [router$1]), goBack = react.useCallback(() => router$1.navigate({}), [router$1]);
1941
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, height: "fill", overflow: "auto", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { width: 2, children: projectId ? /* @__PURE__ */ jsxRuntime.jsx(
1942
+ ProjectView,
1943
+ {
1944
+ projectId,
1945
+ onBack: goBack,
1946
+ languages,
1947
+ sourceLanguage,
1948
+ documentI18nLanguageField,
1949
+ fieldI18nLanguageField
1950
+ }
1951
+ ) : /* @__PURE__ */ jsxRuntime.jsx(ProjectsView, { sourceLanguage, onOpenProject: openProject }) }) });
1952
+ };
1953
+ }
1954
+ const TRANSLATION_TOOL_NAME = "translation-projects";
1955
+ function createTranslationsTool(config) {
1956
+ return {
1957
+ name: TRANSLATION_TOOL_NAME,
1958
+ title: "Translation Projects",
1959
+ icon: icons.TranslateIcon,
1960
+ component: createDashboardComponent(config),
1961
+ router: router.route.create("/", [router.route.create("/project/:projectId")])
1962
+ };
1963
+ }
1964
+ const QUERY = `*[_type == "${constants.TRANSLATION_PROJECT_TYPE}" && count(items[
1965
+ ${progress.ITEM_ID} in [$id] + *[_type == "translation.metadata" && references($id)].translations[].value._ref
1966
+ ]) > 0] | order(_updatedAt desc){
1967
+ _id,
1968
+ name,
1969
+ "isSource": count(items[${progress.ITEM_ID} == $id]) > 0,
1970
+ "targetsThisLanguage": $language in targetLanguages
1971
+ }`;
1972
+ function ProjectLink({ basePath, project }) {
1973
+ const router$1 = router.useRouter(), href = `${basePath.replace(/\/$/, "")}/${TRANSLATION_TOOL_NAME}/project/${project._id}`, onClick = react.useCallback(
1974
+ (e) => {
1975
+ e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || (e.preventDefault(), router$1.navigateUrl({ path: href }));
1976
+ },
1977
+ [router$1, href]
1978
+ );
1979
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { href, onClick, children: project.name || "Untitled project" });
1980
+ }
1981
+ function RoleCard({
1982
+ lead,
1983
+ tone = "primary",
1984
+ warning,
1985
+ basePath,
1986
+ projects
1987
+ }) {
1988
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone, padding: 3, radius: 2, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1989
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
1990
+ lead,
1991
+ projects.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1992
+ i > 0 && ", ",
1993
+ /* @__PURE__ */ jsxRuntime.jsx(ProjectLink, { basePath, project: p })
1994
+ ] }, p._id))
1995
+ ] }),
1996
+ warning && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: warning })
1997
+ ] }) });
1998
+ }
1999
+ function TranslationStatusLine({ mode }) {
2000
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), { basePath } = sanity.useWorkspace(), docId = sanity.useFormValue(["_id"])?.replace(/^drafts\./, ""), language = sanity.useFormValue(["language"]) ?? null, [projects, setProjects] = react.useState([]);
2001
+ if (react.useEffect(() => {
2002
+ if (!docId) return;
2003
+ let cancelled = !1;
2004
+ const params = { id: docId, language }, load = () => client.fetch(QUERY, params).then((res) => !cancelled && setProjects(res)).catch(() => !cancelled && setProjects([]));
2005
+ load();
2006
+ const sub = client.listen(QUERY, params, { visibility: "query" }).subscribe({ next: load, error: () => {
2007
+ } });
2008
+ return () => {
2009
+ cancelled = !0, sub.unsubscribe();
2010
+ };
2011
+ }, [client, docId, language]), projects.length === 0) return null;
2012
+ if (mode === "field")
2013
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
2014
+ RoleCard,
2015
+ {
2016
+ lead: projects.length === 1 ? "This document is part of a translation project: " : "This document is part of multiple translation projects: ",
2017
+ basePath,
2018
+ projects
2019
+ }
2020
+ ) });
2021
+ const asSource = projects.filter((p) => p.isSource), asTarget = projects.filter((p) => !p.isSource && p.targetsThisLanguage);
2022
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2023
+ asSource.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2024
+ RoleCard,
2025
+ {
2026
+ lead: asSource.length === 1 ? "This source document is part of a translation project: " : "This source document is part of multiple translation projects: ",
2027
+ basePath,
2028
+ projects: asSource
2029
+ }
2030
+ ),
2031
+ asTarget.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2032
+ RoleCard,
2033
+ {
2034
+ tone: asTarget.length > 1 ? "critical" : "primary",
2035
+ lead: asTarget.length === 1 ? "This document variant is part of a translation project: " : "This document variant is part of multiple translation projects: ",
2036
+ warning: asTarget.length > 1 ? "Importing translations from more than one project can overwrite each other on this document." : void 0,
2037
+ basePath,
2038
+ projects: asTarget
2039
+ }
2040
+ )
2041
+ ] });
2042
+ }
2043
+ function createTranslationStatusInput(config) {
2044
+ return function(props) {
2045
+ return props.path.length === 0 && config.isTranslatableType(props.schemaType.name) ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
2046
+ /* @__PURE__ */ jsxRuntime.jsx(TranslationStatusLine, { mode: locjson.localizationMode(props.schemaType) }),
2047
+ props.renderDefault(props)
2048
+ ] }) : props.renderDefault(props);
2049
+ };
2050
+ }
2051
+ const INIT_QUERY = "*[_id == $id][0]{templatesRequestedAt, templatesSyncedAt}";
2052
+ function StudioInit(props) {
2053
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), didRun = react.useRef(!1);
2054
+ return react.useEffect(() => {
2055
+ didRun.current || (didRun.current = !0, client.fetch(INIT_QUERY, { id: constants.SETTINGS_DOC_ID }).then((s) => {
2056
+ if (!(s?.templatesSyncedAt || isRefreshInFlight(s)))
2057
+ return client.transaction().createIfNotExists({ _id: constants.SETTINGS_DOC_ID, _type: constants.SETTINGS_TYPE }).patch(constants.SETTINGS_DOC_ID, (p) => p.set({ templatesRequestedAt: (/* @__PURE__ */ new Date()).toISOString() })).commit({ visibility: "async" });
2058
+ }).catch(() => {
2059
+ }));
2060
+ }, [client]), props.renderDefault(props);
2061
+ }
2062
+ const PLUGIN_TYPES = /* @__PURE__ */ new Set([constants.TRANSLATION_PROJECT_TYPE, constants.SETTINGS_TYPE]);
2063
+ function resolveConfig(config) {
2064
+ const translatableTypes = config.translatableTypes;
2065
+ return {
2066
+ isTranslatableType: translatableTypes ? (type) => translatableTypes.includes(type) : (type) => !PLUGIN_TYPES.has(type),
2067
+ // `rootTypes` inherits the *explicitly-configured* translatableTypes (not the
2068
+ // resolved "everything") so an unconfigured Studio gets no crawl boundary
2069
+ // rather than every type being a root (which would stop the crawl at once).
2070
+ rootTypes: config.rootTypes ?? translatableTypes ?? [],
2071
+ sourceLanguage: constants.requireSourceLanguage(config.sourceLanguage),
2072
+ languages: config.languages ?? [],
2073
+ documentI18nLanguageField: config.documentI18nPluginLanguageField ?? "language",
2074
+ fieldI18nLanguageField: config.fieldI18nPluginLanguageField ?? "language"
2075
+ };
2076
+ }
2077
+ const smartcatTranslation = sanity.definePlugin((config) => {
2078
+ const resolved = resolveConfig(config), projectType = createTranslationProjectType(), settingsType = createSettingsType(), addToProjectAction = createAddToProjectAction(resolved), translationStatusInput = createTranslationStatusInput(resolved), translationsTool = createTranslationsTool(resolved), isTranslatable = resolved.isTranslatableType;
2079
+ return {
2080
+ name: "sanity-plugin-smartcat-translation",
2081
+ schema: {
2082
+ types: [projectType, settingsType]
2083
+ },
2084
+ studio: {
2085
+ components: {
2086
+ // Bootstraps the template cache on first Studio load.
2087
+ layout: StudioInit
2088
+ }
2089
+ },
2090
+ tools: [translationsTool],
2091
+ document: {
2092
+ actions: (prev, context) => isTranslatable(context.schemaType) ? [...prev, addToProjectAction] : prev
2093
+ },
2094
+ form: {
2095
+ components: {
2096
+ input: translationStatusInput
2097
+ }
2098
+ }
2099
+ };
2100
+ });
2101
+ exports.TRANSLATION_PROJECT_TYPE = constants.TRANSLATION_PROJECT_TYPE;
2102
+ exports.smartcatTranslation = smartcatTranslation;
2103
+ //# sourceMappingURL=index.cjs.map