@smartcat/sanity-plugin 1.0.0 → 1.1.0

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