@smartcat/sanity-plugin 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +294 -0
  2. package/dist/_chunks-cjs/constants.cjs +13 -0
  3. package/dist/_chunks-cjs/constants.cjs.map +1 -0
  4. package/dist/_chunks-cjs/index.cjs +168 -0
  5. package/dist/_chunks-cjs/index.cjs.map +1 -0
  6. package/dist/_chunks-cjs/index2.cjs +311 -0
  7. package/dist/_chunks-cjs/index2.cjs.map +1 -0
  8. package/dist/_chunks-cjs/workflow.cjs +16 -0
  9. package/dist/_chunks-cjs/workflow.cjs.map +1 -0
  10. package/dist/_chunks-es/constants.js +14 -0
  11. package/dist/_chunks-es/constants.js.map +1 -0
  12. package/dist/_chunks-es/index.js +169 -0
  13. package/dist/_chunks-es/index.js.map +1 -0
  14. package/dist/_chunks-es/index2.js +313 -0
  15. package/dist/_chunks-es/index2.js.map +1 -0
  16. package/dist/_chunks-es/workflow.js +17 -0
  17. package/dist/_chunks-es/workflow.js.map +1 -0
  18. package/dist/export.cjs +160 -0
  19. package/dist/export.cjs.map +1 -0
  20. package/dist/export.d.cts +162 -0
  21. package/dist/export.d.ts +162 -0
  22. package/dist/export.js +162 -0
  23. package/dist/export.js.map +1 -0
  24. package/dist/import.cjs +169 -0
  25. package/dist/import.cjs.map +1 -0
  26. package/dist/import.d.cts +155 -0
  27. package/dist/import.d.ts +155 -0
  28. package/dist/import.js +169 -0
  29. package/dist/import.js.map +1 -0
  30. package/dist/index.cjs +2103 -0
  31. package/dist/index.cjs.map +1 -0
  32. package/dist/index.d.cts +122 -0
  33. package/dist/index.d.ts +122 -0
  34. package/dist/index.js +2110 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/locjson.cjs +12 -0
  37. package/dist/locjson.cjs.map +1 -0
  38. package/dist/locjson.d.cts +287 -0
  39. package/dist/locjson.d.ts +287 -0
  40. package/dist/locjson.js +12 -0
  41. package/dist/locjson.js.map +1 -0
  42. package/dist/progress.cjs +16 -0
  43. package/dist/progress.cjs.map +1 -0
  44. package/dist/progress.d.cts +262 -0
  45. package/dist/progress.d.ts +262 -0
  46. package/dist/progress.js +16 -0
  47. package/dist/progress.js.map +1 -0
  48. package/dist/smartcat.cjs +287 -0
  49. package/dist/smartcat.cjs.map +1 -0
  50. package/dist/smartcat.d.cts +234 -0
  51. package/dist/smartcat.d.ts +234 -0
  52. package/dist/smartcat.js +287 -0
  53. package/dist/smartcat.js.map +1 -0
  54. package/dist/templates.cjs +12 -0
  55. package/dist/templates.cjs.map +1 -0
  56. package/dist/templates.d.cts +52 -0
  57. package/dist/templates.d.ts +52 -0
  58. package/dist/templates.js +12 -0
  59. package/dist/templates.js.map +1 -0
  60. package/package.json +101 -15
  61. package/src/actions/AddToProjectAction.tsx +258 -0
  62. package/src/export/export.test.ts +537 -0
  63. package/src/export/index.ts +393 -0
  64. package/src/form/TranslationStatusInput.tsx +212 -0
  65. package/src/import/import.test.ts +346 -0
  66. package/src/import/index.ts +418 -0
  67. package/src/index.ts +63 -0
  68. package/src/lib/constants.ts +23 -0
  69. package/src/lib/documentTitle.test.ts +43 -0
  70. package/src/lib/documentTitle.ts +30 -0
  71. package/src/lib/languageMap.test.ts +56 -0
  72. package/src/lib/languageMap.ts +71 -0
  73. package/src/lib/linkedDocuments.test.ts +56 -0
  74. package/src/lib/linkedDocuments.ts +100 -0
  75. package/src/lib/locjson/deserialize.ts +60 -0
  76. package/src/lib/locjson/fields.ts +355 -0
  77. package/src/lib/locjson/filename.ts +27 -0
  78. package/src/lib/locjson/index.ts +10 -0
  79. package/src/lib/locjson/locjson.test.ts +545 -0
  80. package/src/lib/locjson/paths.test.ts +41 -0
  81. package/src/lib/locjson/paths.ts +73 -0
  82. package/src/lib/locjson/portableText.ts +98 -0
  83. package/src/lib/locjson/serialize.ts +124 -0
  84. package/src/lib/locjson/types.ts +106 -0
  85. package/src/lib/log.ts +33 -0
  86. package/src/lib/projectItems.ts +17 -0
  87. package/src/lib/resolveConfig.test.ts +62 -0
  88. package/src/lib/resolveConfig.ts +47 -0
  89. package/src/lib/workflow.test.ts +37 -0
  90. package/src/lib/workflow.ts +48 -0
  91. package/src/lib/zip.fixtures.ts +75 -0
  92. package/src/lib/zip.test.ts +110 -0
  93. package/src/lib/zip.ts +164 -0
  94. package/src/progress/core.ts +387 -0
  95. package/src/progress/index.ts +7 -0
  96. package/src/progress/progress.test.ts +268 -0
  97. package/src/schema/settings.ts +55 -0
  98. package/src/schema/translatableOptions.ts +25 -0
  99. package/src/schema/translationProject.ts +264 -0
  100. package/src/smartcat/client.test.ts +222 -0
  101. package/src/smartcat/client.ts +351 -0
  102. package/src/smartcat/index.ts +10 -0
  103. package/src/smartcat/types.ts +99 -0
  104. package/src/templates/core.ts +55 -0
  105. package/src/templates/index.ts +5 -0
  106. package/src/templates/templates.test.ts +50 -0
  107. package/src/tool/Dashboard.tsx +44 -0
  108. package/src/tool/ItemProgress.tsx +176 -0
  109. package/src/tool/LogPanel.tsx +207 -0
  110. package/src/tool/ProjectView.tsx +975 -0
  111. package/src/tool/ProjectsView.tsx +208 -0
  112. package/src/tool/StatusBadge.tsx +33 -0
  113. package/src/tool/StudioInit.tsx +42 -0
  114. package/src/tool/WorkflowSelect.tsx +41 -0
  115. package/src/tool/data.ts +79 -0
  116. package/src/tool/index.ts +19 -0
  117. package/src/tool/processing.test.ts +623 -0
  118. package/src/tool/processing.ts +766 -0
  119. package/src/tool/useTemplates.ts +84 -0
  120. package/src/tool/useWorkflowSelection.ts +65 -0
  121. package/src/types.ts +77 -0
package/dist/export.js ADDED
@@ -0,0 +1,162 @@
1
+ import { SmartcatError } from "./smartcat.js";
2
+ import { toSmartcatLanguage, buildProgress, sourceIdPrefix } from "./_chunks-es/index.js";
3
+ import { resolveWorkflowCreateParams } from "./_chunks-es/workflow.js";
4
+ const UPLOAD_CHUNK_SIZE = 50, BATCH_RETRY_DELAY_MS = 1e3, UPLOAD_CHUNK_CONCURRENCY = 10;
5
+ function chunk(items, size) {
6
+ const out = [];
7
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
8
+ return out;
9
+ }
10
+ async function runWithConcurrency(items, limit, worker) {
11
+ let next = 0;
12
+ async function runNext() {
13
+ const i = next++;
14
+ if (!(i >= items.length))
15
+ return await worker(items[i]), runNext();
16
+ }
17
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => runNext()));
18
+ }
19
+ const PROJECT_QUERY = `*[_id == $id][0]{
20
+ _id,
21
+ name,
22
+ sourceLanguage,
23
+ targetLanguages,
24
+ workflow,
25
+ smartcatProjectId,
26
+ smartcatProjectUrl,
27
+ smartcatLanguages[]{sanityId, smartcatLanguage},
28
+ outbox[]{sourceDocId, title, filename, locjson},
29
+ pendingDeletions[]{itemKey, sourceDocId, smartcatDocumentId},
30
+ progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
31
+ }`;
32
+ function existingDocIdsBySource(scProject, outbox) {
33
+ const idByPrefix = /* @__PURE__ */ new Map();
34
+ for (const doc of scProject.documents ?? []) {
35
+ const prefix = sourceIdPrefix(doc);
36
+ prefix && doc.id && !idByPrefix.has(prefix) && idByPrefix.set(prefix, doc.id);
37
+ }
38
+ const result = /* @__PURE__ */ new Map();
39
+ for (const item of outbox) {
40
+ if (!item.sourceDocId) continue;
41
+ const id = idByPrefix.get(stripDraft(item.sourceDocId).slice(0, 8));
42
+ id && result.set(item.sourceDocId, id);
43
+ }
44
+ return result;
45
+ }
46
+ function stripDraft(id) {
47
+ return id.replace(/^drafts\./, "");
48
+ }
49
+ function mergeWrittenDocs(authoritative, written) {
50
+ const byId = new Map(authoritative.filter((d) => d.id).map((d) => [d.id, d]));
51
+ for (const d of written) d.id && !byId.has(d.id) && byId.set(d.id, d);
52
+ return [...byId.values()];
53
+ }
54
+ async function runExportUpload(options) {
55
+ const {
56
+ sanity,
57
+ smartcat,
58
+ projectId,
59
+ now = () => (/* @__PURE__ */ new Date()).toISOString(),
60
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms))
61
+ } = options, log = [];
62
+ smartcat.setRequestLogger?.(({ method, path, status, body }) => {
63
+ status >= 200 && status < 300 ? log.push({ level: "info", message: `${method} ${path} \u2192 ${status}` }) : (log.push({ level: "error", message: `Error ${status} while performing ${method} ${path}` }), body && log.push({ level: "error", indent: !0, message: body }));
64
+ });
65
+ const project = await sanity.fetch(PROJECT_QUERY, { id: projectId });
66
+ if (!project) throw new Error(`Translation project ${projectId} not found`);
67
+ try {
68
+ const outbox = project.outbox ?? [], deletions = project.pendingDeletions ?? [];
69
+ if (outbox.length === 0 && deletions.length === 0) {
70
+ const status = project.smartcatProjectId ? "sent" : "draft";
71
+ return log.push({ level: "skip", message: "Nothing to sync \u2014 no documents with translatable content" }), await sanity.patch(projectId).set({ status, lastExportAt: now(), lastError: null, functionLog: JSON.stringify(log) }).unset(["outbox", "pendingDeletions"]).commit(), { smartcatProjectId: project.smartcatProjectId ?? "", created: 0, updated: 0, deleted: 0, failed: 0 };
72
+ }
73
+ log.push({ level: "info", message: `Syncing ${outbox.length} file(s)${deletions.length ? ` \xB7 ${deletions.length} deletion(s)` : ""} to Smartcat` });
74
+ const isNew = !project.smartcatProjectId;
75
+ let scProjectId = project.smartcatProjectId, scProjectUrl = project.smartcatProjectUrl;
76
+ if (isNew) {
77
+ const sourceLanguage = project.sourceLanguage;
78
+ if (!sourceLanguage) throw new Error("No source language set on the project");
79
+ const targetLanguages = project.targetLanguages ?? [];
80
+ if (targetLanguages.length === 0) throw new Error("No target languages set on the project");
81
+ const map = project.smartcatLanguages;
82
+ log.push({ level: "info", message: `Creating Smartcat project "${project.name || "Sanity translation project"}"` });
83
+ const createdProject = await smartcat.createProject({
84
+ name: project.name || "Sanity translation project",
85
+ sourceLanguage: toSmartcatLanguage(map, sourceLanguage),
86
+ targetLanguages: targetLanguages.map((id) => toSmartcatLanguage(map, id)),
87
+ ...resolveWorkflowCreateParams(project.workflow)
88
+ });
89
+ scProjectId = createdProject.id, scProjectUrl = smartcat.projectUrl(createdProject.id), log.push({ level: "success", message: `Created Smartcat project ${scProjectId}` }), await sanity.patch(projectId).set({ smartcatProjectId: scProjectId, smartcatProjectUrl: scProjectUrl }).commit();
90
+ }
91
+ const deletedItemKeys = [];
92
+ for (const del of deletions)
93
+ try {
94
+ del.smartcatDocumentId && (await smartcat.deleteDocument(del.smartcatDocumentId), log.push({ level: "success", message: `Deleted document ${del.smartcatDocumentId}` })), del.itemKey && deletedItemKeys.push(del.itemKey);
95
+ } catch (e) {
96
+ log.push({ level: "error", message: `Failed to delete document ${del.smartcatDocumentId}: ${e instanceof Error ? e.message : String(e)}` });
97
+ }
98
+ const existing = isNew || outbox.length === 0 ? /* @__PURE__ */ new Map() : existingDocIdsBySource(await smartcat.getProject(scProjectId), outbox);
99
+ let created = 0, updated = 0, failed = 0;
100
+ const writtenDocs = [], toUpdate = outbox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId)), toCreate = outbox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)));
101
+ for (const item of toUpdate) {
102
+ const docId = existing.get(item.sourceDocId);
103
+ try {
104
+ const returned = await smartcat.updateDocument(docId, item.filename, item.locjson);
105
+ for (const d of returned) writtenDocs.push({ ...d, name: d.name || item.filename });
106
+ updated++, log.push({ level: "success", message: `Updated ${item.filename}` });
107
+ } catch {
108
+ failed++, log.push({ level: "error", message: `Skipped ${item.filename} \u2014 update failed (see error above)` });
109
+ }
110
+ }
111
+ async function processChunk(group) {
112
+ const payload = group.map((i) => ({ filename: i.filename, content: i.locjson }));
113
+ try {
114
+ const returned = await smartcat.uploadDocuments(scProjectId, payload);
115
+ writtenDocs.push(...returned), created += group.length, log.push({ level: "success", message: `Uploaded ${group.length} file(s)` });
116
+ } catch {
117
+ await sleep(BATCH_RETRY_DELAY_MS);
118
+ try {
119
+ const returned = await smartcat.uploadDocuments(scProjectId, payload);
120
+ writtenDocs.push(...returned), created += group.length, log.push({ level: "success", message: `Uploaded ${group.length} file(s) (retry)` });
121
+ } catch {
122
+ log.push({ level: "info", message: `Batch upload failed, retrying ${group.length} file(s) individually` });
123
+ for (const item of group)
124
+ try {
125
+ const returned = await smartcat.uploadDocument(scProjectId, item.filename, item.locjson);
126
+ for (const d of returned) writtenDocs.push({ ...d, name: d.name || item.filename });
127
+ created++, log.push({ level: "success", message: `Uploaded ${item.filename}` });
128
+ } catch (e) {
129
+ e instanceof SmartcatError && e.status === 409 ? (created++, log.push({ level: "success", message: `Already synced ${item.filename}` })) : (failed++, log.push({ level: "error", message: `Skipped ${item.filename} \u2014 upload failed (see error above)` }));
130
+ }
131
+ }
132
+ }
133
+ }
134
+ await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk);
135
+ const scProject = await smartcat.getProject(scProjectId), documents = mergeWrittenDocs(scProject.documents ?? [], writtenDocs), items = outbox.map((o) => ({ _id: o.sourceDocId || o.filename, title: o.title })), progress = buildProgress(items, { ...scProject, documents }, project.progress, now(), project.smartcatLanguages);
136
+ log.push({
137
+ level: failed ? "error" : "info",
138
+ message: `Synced ${created} created, ${updated} updated${failed ? `, ${failed} failed` : ""}${deletedItemKeys.length ? `, ${deletedItemKeys.length} deleted` : ""}`
139
+ });
140
+ const patch = {
141
+ smartcatProjectId: scProjectId,
142
+ smartcatProjectUrl: scProjectUrl,
143
+ status: "sent",
144
+ progress,
145
+ lastExportAt: now(),
146
+ lastError: null,
147
+ functionLog: JSON.stringify(log)
148
+ };
149
+ scProject.name && (patch.name = scProject.name);
150
+ const unset = ["outbox", "pendingDeletions", ...deletedItemKeys.map((k) => `items[_key=="${k}"]`)];
151
+ return await sanity.patch(projectId).set(patch).unset(unset).commit(), { smartcatProjectId: scProjectId, created, updated, deleted: deletedItemKeys.length, failed };
152
+ } catch (err) {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ throw log.push({ level: "error", message }), await sanity.patch(projectId).set({ status: "error", lastError: message, lastExportAt: now(), functionLog: JSON.stringify(log) }).commit().catch(() => {
155
+ }), err;
156
+ }
157
+ }
158
+ export {
159
+ UPLOAD_CHUNK_CONCURRENCY,
160
+ runExportUpload
161
+ };
162
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sources":["../src/export/index.ts"],"sourcesContent":["import type {CreateProjectInput, RequestLogger, SmartcatDocument, SmartcatProject} from '../smartcat/types'\nimport {SmartcatError} from '../smartcat/client'\nimport {buildProgress, sourceIdPrefix, type DocProgress, type ProjectItem} from '../progress/core'\nimport {toSmartcatLanguage, type SmartcatLanguageMapping} from '../lib/languageMap'\nimport {resolveWorkflowCreateParams} from '../lib/workflow'\nimport type {LogLine} from '../lib/log'\n\n/** Source files per multi-file upload request (first-sync create path). */\nconst UPLOAD_CHUNK_SIZE = 50\n/** Delay before the single batch retry, ms. */\nconst BATCH_RETRY_DELAY_MS = 1000\n/** Create-path chunks uploaded concurrently. A saturation study found upload\n * throughput scales ~linearly with concurrency only up to ~C=40, where the\n * Smartcat API's per-workspace rate limit trips (HTTP 429) and effective\n * throughput collapses. Raising this above ~30 is pointless — the gains are\n * erased by rate-limiting, and it eats into the shared rate budget of any other\n * exports running concurrently. 10 is a safe, near-linear default with headroom. */\nexport const UPLOAD_CHUNK_CONCURRENCY = 10\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\n/** Runs `worker` over `items` with at most `limit` in flight concurrently. */\nasync function runWithConcurrency<T>(items: T[], limit: number, worker: (item: T) => Promise<void>): Promise<void> {\n let next = 0\n async function runNext(): Promise<void> {\n const i = next++\n if (i >= items.length) return\n await worker(items[i])\n return runNext()\n }\n await Promise.all(Array.from({length: Math.min(limit, items.length)}, () => runNext()))\n}\n\n/** Minimal structural subset of @sanity/client used by the export. */\nexport interface SanityLikeClient {\n fetch<T = unknown>(query: string, params?: Record<string, unknown>): Promise<T>\n patch(id: string): {\n set(attrs: Record<string, unknown>): {\n unset(keys: string[]): {commit(): Promise<unknown>}\n commit(): Promise<unknown>\n }\n }\n}\n\n/** Structural subset of SmartcatClient the export needs. */\nexport interface SmartcatExportClient {\n createProject(input: CreateProjectInput): Promise<{id: string}>\n uploadDocument(projectId: string, filename: string, content: string): Promise<SmartcatDocument[]>\n uploadDocuments(projectId: string, files: {filename: string; content: string}[]): Promise<SmartcatDocument[]>\n updateDocument(documentId: string, filename: string, content: string): Promise<SmartcatDocument[]>\n deleteDocument(documentId: string): Promise<void>\n getProject(projectId: string): Promise<SmartcatProject>\n projectUrl(projectId: string): string\n /** Optional: receive a trace of every HTTP call for the client-facing log. */\n setRequestLogger?(logger: RequestLogger): void\n}\n\ninterface PendingDeletion {\n itemKey?: string\n sourceDocId?: string\n smartcatDocumentId?: string\n}\n\nexport interface RunExportUploadOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatExportClient\n /** `_id` of the `smartcat.translationProject` to export. */\n projectId: string\n now?: () => string\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface RunExportUploadResult {\n smartcatProjectId: string\n /** Source files newly created in Smartcat this run. */\n created: number\n /** Source files updated in place this run. */\n updated: number\n /** Documents deleted from Smartcat this run. */\n deleted: number\n /** Documents whose upload/update failed (e.g. Smartcat lock); the rest still synced. */\n failed: number\n}\n\nconst PROJECT_QUERY = `*[_id == $id][0]{\n _id,\n name,\n sourceLanguage,\n targetLanguages,\n workflow,\n smartcatProjectId,\n smartcatProjectUrl,\n smartcatLanguages[]{sanityId, smartcatLanguage},\n outbox[]{sourceDocId, title, filename, locjson},\n pendingDeletions[]{itemKey, sourceDocId, smartcatDocumentId},\n progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}\n}`\n\ninterface OutboxItem {\n sourceDocId?: string\n title?: string\n filename: string\n locjson: string\n}\n\ninterface ProjectData {\n _id: string\n name?: string\n sourceLanguage?: string\n targetLanguages?: string[]\n workflow?: string\n smartcatProjectId?: string\n smartcatProjectUrl?: string\n smartcatLanguages?: SmartcatLanguageMapping[]\n outbox?: OutboxItem[]\n pendingDeletions?: PendingDeletion[]\n progress?: DocProgress[]\n}\n\n/** A source file maps to one Smartcat document id we can update in place. */\nfunction existingDocIdsBySource(scProject: SmartcatProject, outbox: OutboxItem[]): Map<string, string> {\n // Index Smartcat documents by the source-id prefix in their filename, keeping\n // one representative id per source file (all target-language docs share a source).\n const idByPrefix = new Map<string, string>()\n for (const doc of scProject.documents ?? []) {\n const prefix = sourceIdPrefix(doc)\n if (prefix && doc.id && !idByPrefix.has(prefix)) idByPrefix.set(prefix, doc.id)\n }\n const result = new Map<string, string>()\n for (const item of outbox) {\n if (!item.sourceDocId) continue\n const id = idByPrefix.get(stripDraft(item.sourceDocId).slice(0, 8))\n if (id) result.set(item.sourceDocId, id)\n }\n return result\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/**\n * Merges the documents Smartcat returned from this run's writes into the\n * authoritative `getProject` list. The fetched docs win (they carry workflow\n * stages); written docs are added only when the fetch hasn't indexed them yet,\n * so a freshly-created document — and its title — isn't dropped this run.\n */\nfunction mergeWrittenDocs(authoritative: SmartcatDocument[], written: SmartcatDocument[]): SmartcatDocument[] {\n const byId = new Map(authoritative.filter((d) => d.id).map((d) => [d.id, d]))\n for (const d of written) if (d.id && !byId.has(d.id)) byId.set(d.id, d)\n return [...byId.values()]\n}\n\n/**\n * Thin export step run by a Sanity Function: takes the LocJSON files the browser\n * prepared (in `outbox`) and syncs them to Smartcat.\n *\n * - **First sync** (no `smartcatProjectId`): creates the Smartcat project and\n * uploads each file, then mirrors Smartcat's (possibly de-duplicated) project\n * name back onto the Sanity project.\n * - **Re-sync** (project already exists): reuses it — updating documents that are\n * already there (Smartcat merges changes, preserving existing translations) and\n * creating any newly-added items. Never creates a second Smartcat project.\n *\n * Does no content processing, so it depends only on the Smartcat client.\n */\nexport async function runExportUpload(options: RunExportUploadOptions): Promise<RunExportUploadResult> {\n const {\n sanity,\n smartcat,\n projectId,\n now = () => new Date().toISOString(),\n sleep = (ms) => new Promise((r) => setTimeout(r, ms)),\n } = options\n const log: LogLine[] = []\n\n // Trace every Smartcat HTTP call into the client-facing log. Successful calls\n // are a single info line; failures show \"Error <status> while performing …\"\n // (red) with the raw response body on the line below.\n smartcat.setRequestLogger?.(({method, path, status, body}) => {\n if (status >= 200 && status < 300) {\n log.push({level: 'info', message: `${method} ${path} → ${status}`})\n } else {\n log.push({level: 'error', message: `Error ${status} while performing ${method} ${path}`})\n if (body) log.push({level: 'error', indent: true, message: body})\n }\n })\n\n const project = await sanity.fetch<ProjectData | null>(PROJECT_QUERY, {id: projectId})\n if (!project) throw new Error(`Translation project ${projectId} not found`)\n\n try {\n const outbox = project.outbox ?? []\n const deletions = project.pendingDeletions ?? []\n if (outbox.length === 0 && deletions.length === 0) {\n // Not an error: every queued document resolved to zero translatable units\n // (e.g. empty source content), so there's nothing to send. Settle back to a\n // resting status and explain why, rather than failing the project.\n const status = project.smartcatProjectId ? 'sent' : 'draft'\n log.push({level: 'skip', message: 'Nothing to sync — no documents with translatable content'})\n await sanity\n .patch(projectId)\n .set({status, lastExportAt: now(), lastError: null, functionLog: JSON.stringify(log)})\n .unset(['outbox', 'pendingDeletions'])\n .commit()\n return {smartcatProjectId: project.smartcatProjectId ?? '', created: 0, updated: 0, deleted: 0, failed: 0}\n }\n log.push({level: 'info', message: `Syncing ${outbox.length} file(s)${deletions.length ? ` · ${deletions.length} deletion(s)` : ''} to Smartcat`})\n\n const isNew = !project.smartcatProjectId\n let scProjectId = project.smartcatProjectId\n let scProjectUrl = project.smartcatProjectUrl\n\n if (isNew) {\n const sourceLanguage = project.sourceLanguage\n if (!sourceLanguage) throw new Error('No source language set on the project')\n const targetLanguages = project.targetLanguages ?? []\n if (targetLanguages.length === 0) throw new Error('No target languages set on the project')\n // Send Smartcat its own language codes (the project carries the mapping).\n const map = project.smartcatLanguages\n log.push({level: 'info', message: `Creating Smartcat project \"${project.name || 'Sanity translation project'}\"`})\n const createdProject = await smartcat.createProject({\n name: project.name || 'Sanity translation project',\n sourceLanguage: toSmartcatLanguage(map, sourceLanguage),\n targetLanguages: targetLanguages.map((id) => toSmartcatLanguage(map, id)),\n ...resolveWorkflowCreateParams(project.workflow),\n })\n scProjectId = createdProject.id\n scProjectUrl = smartcat.projectUrl(createdProject.id)\n log.push({level: 'success', message: `Created Smartcat project ${scProjectId}`})\n // Persist the new project id immediately, before uploads. If a later step\n // fails (or the event is re-run), the next run reuses this project instead\n // of creating a duplicate in Smartcat.\n await sanity.patch(projectId).set({smartcatProjectId: scProjectId, smartcatProjectUrl: scProjectUrl}).commit()\n }\n\n // Delete marked documents first (one id removes the whole document). Best-effort:\n // an item is removed from the project only if its Smartcat delete succeeded (or\n // it had no linked document).\n const deletedItemKeys: string[] = []\n for (const del of deletions) {\n try {\n if (del.smartcatDocumentId) {\n await smartcat.deleteDocument(del.smartcatDocumentId)\n log.push({level: 'success', message: `Deleted document ${del.smartcatDocumentId}`})\n }\n if (del.itemKey) deletedItemKeys.push(del.itemKey)\n } catch (e) {\n // Leave the item in place; the mark is cleared so the editor can retry.\n log.push({level: 'error', message: `Failed to delete document ${del.smartcatDocumentId}: ${e instanceof Error ? e.message : String(e)}`})\n }\n }\n\n // For an existing project, find which source files are already in Smartcat so\n // we update them in place rather than creating duplicates.\n const existing =\n isNew || outbox.length === 0\n ? new Map<string, string>()\n : existingDocIdsBySource(await smartcat.getProject(scProjectId as string), outbox)\n\n let created = 0\n let updated = 0\n let failed = 0\n // Capture the documents Smartcat returns from each write. The post-upload\n // getProject can lag on freshly-created docs (indexing delay); merging these\n // in guarantees buildProgress sees them this run, so the title — which lives\n // only on the in-memory outbox — is captured now instead of being lost until\n // a re-export (see the field-level \"Untitled\" progress bug).\n const writtenDocs: SmartcatDocument[] = []\n\n const toUpdate = outbox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId))\n const toCreate = outbox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)))\n\n // Updates stay per-item (no multi-file equivalent) — today's behavior.\n for (const item of toUpdate) {\n const docId = existing.get(item.sourceDocId as string) as string\n try {\n const returned = await smartcat.updateDocument(docId, item.filename, item.locjson)\n // Tag with the filename we uploaded so correlation works even when the\n // write response omits the document name.\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n updated++\n log.push({level: 'success', message: `Updated ${item.filename}`})\n } catch {\n // The HTTP error + body were already logged in red by the request logger.\n // One document failing (e.g. a Smartcat \"document locked\" / update-in-\n // progress 400) must not abort the batch or fail the whole project — record\n // it and move on so the rest still sync.\n failed++\n log.push({level: 'error', message: `Skipped ${item.filename} — update failed (see error above)`})\n }\n }\n\n // Per-chunk upload: batch → retry once as batch → per-file fallback. Mutates the\n // enclosing `created`/`failed`/`writtenDocs`/`log` state; never throws for a\n // normal upload failure (the per-file fallback swallows those), so it's safe to\n // run many of these concurrently below.\n async function processChunk(group: OutboxItem[]): Promise<void> {\n const payload = group.map((i) => ({filename: i.filename, content: i.locjson}))\n try {\n const returned = await smartcat.uploadDocuments(scProjectId as string, payload)\n // Batch correlation relies on the response carrying a per-doc name/fullPath (spike-confirmed).\n writtenDocs.push(...returned)\n created += group.length\n log.push({level: 'success', message: `Uploaded ${group.length} file(s)`})\n } catch {\n await sleep(BATCH_RETRY_DELAY_MS)\n try {\n const returned = await smartcat.uploadDocuments(scProjectId as string, payload)\n writtenDocs.push(...returned)\n created += group.length\n log.push({level: 'success', message: `Uploaded ${group.length} file(s) (retry)`})\n } catch {\n log.push({level: 'info', message: `Batch upload failed, retrying ${group.length} file(s) individually`})\n for (const item of group) {\n try {\n const returned = await smartcat.uploadDocument(scProjectId as string, item.filename, item.locjson)\n // Tag with the filename we uploaded so correlation works even when the\n // write response omits the document name.\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n created++\n log.push({level: 'success', message: `Uploaded ${item.filename}`})\n } catch (e) {\n // A 409 here means the batch attempt(s) above actually committed this\n // file before throwing (HTTP error masking a successful create); the\n // re-POST fails because Smartcat rejects a duplicate filename. The\n // document exists in the project, so it's synced, not failed —\n // getProject/buildProgress remain authoritative for its content.\n if (e instanceof SmartcatError && e.status === 409) {\n created++\n log.push({level: 'success', message: `Already synced ${item.filename}`})\n } else {\n failed++\n log.push({level: 'error', message: `Skipped ${item.filename} — upload failed (see error above)`})\n }\n }\n }\n }\n }\n }\n\n // Creates are uploaded in chunks, with bounded concurrency across chunks —\n // log-line order becomes non-deterministic, but the mutations above are safe\n // under JS's single-threaded async interleaving.\n await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk)\n\n // Re-fetch for authoritative ids/stages, and mirror the name back on first\n // creation (Smartcat may have de-duplicated it, e.g. \"My test (1)\"). Then add\n // any just-written docs it hasn't indexed yet so none are dropped this run.\n const scProject = await smartcat.getProject(scProjectId as string)\n const documents = mergeWrittenDocs(scProject.documents ?? [], writtenDocs)\n const items: ProjectItem[] = outbox.map((o) => ({_id: o.sourceDocId || o.filename, title: o.title}))\n const progress = buildProgress(items, {...scProject, documents}, project.progress, now(), project.smartcatLanguages)\n\n log.push({\n level: failed ? 'error' : 'info',\n message: `Synced ${created} created, ${updated} updated${failed ? `, ${failed} failed` : ''}${deletedItemKeys.length ? `, ${deletedItemKeys.length} deleted` : ''}`,\n })\n\n const patch: Record<string, unknown> = {\n smartcatProjectId: scProjectId,\n smartcatProjectUrl: scProjectUrl,\n status: 'sent',\n progress,\n lastExportAt: now(),\n lastError: null,\n functionLog: JSON.stringify(log),\n }\n // Mirror Smartcat's project name back on every sync, so the two stay in step\n // (Smartcat may de-duplicate it, e.g. \"My test (1)\"). Note: this means a local\n // rename in the Studio is overwritten on the next sync unless it's also pushed\n // to Smartcat — Smartcat is the source of truth for the linked project's name.\n if (scProject.name) patch.name = scProject.name\n\n const unset = ['outbox', 'pendingDeletions', ...deletedItemKeys.map((k) => `items[_key==\"${k}\"]`)]\n await sanity.patch(projectId).set(patch).unset(unset).commit()\n\n return {smartcatProjectId: scProjectId as string, created, updated, deleted: deletedItemKeys.length, failed}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log.push({level: 'error', message})\n await sanity\n .patch(projectId)\n .set({status: 'error', lastError: message, lastExportAt: now(), functionLog: JSON.stringify(log)})\n .commit()\n .catch(() => {})\n throw err\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var progress = require("./_chunks-cjs/index.cjs");
4
+ function smartcatDocPath(scDoc) {
5
+ return (scDoc?.fullPath || scDoc?.name || scDoc?.filename || "").replace(/\.[^./]+$/, "");
6
+ }
7
+ const DEFAULT_BATCH_SIZE = 50, DEFAULT_MAX_INBOX_BYTES = 15e5, DEFAULT_CONCURRENCY = 4, DEFAULT_TIME_BUDGET_MS = 10 * 6e4, PROJECT_QUERY = `*[_id == $id][0]{
8
+ _id,
9
+ smartcatProjectId,
10
+ smartcatLanguages[]{sanityId, smartcatLanguage},
11
+ "items": items[]{ "doc": ${progress.itemDocDeref("{_id, title}")} },
12
+ progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
13
+ }`;
14
+ function stripDraft(id) {
15
+ return id.replace(/^drafts\./, "");
16
+ }
17
+ function sanityDocIdOf(locjson) {
18
+ try {
19
+ return JSON.parse(locjson).properties?.["x-sanity"]?.documentId;
20
+ } catch {
21
+ return;
22
+ }
23
+ }
24
+ async function downloadBatch(smartcat, batch, emit) {
25
+ const contentByTarget = /* @__PURE__ */ new Map(), batchAttempted = !!(smartcat.exportDocumentsBatch && batch.length > 1);
26
+ let batchFailed = !1;
27
+ if (batchAttempted)
28
+ try {
29
+ const files2 = await smartcat.exportDocumentsBatch(batch.map((t) => t.smartcatDocumentId)), targetByDocId = new Map(batch.map((t) => [stripDraft(t.sourceDocId), t]));
30
+ for (const file of files2) {
31
+ const docId = sanityDocIdOf(file.content), target = docId ? targetByDocId.get(stripDraft(docId)) : void 0;
32
+ target && !contentByTarget.has(target) && contentByTarget.set(target, file.content);
33
+ }
34
+ contentByTarget.size < batch.length && emit({
35
+ level: "info",
36
+ message: `${batch.length - contentByTarget.size} of ${batch.length} file(s) missing from the batch archive \u2014 fetching them per-document`
37
+ });
38
+ } catch (err) {
39
+ batchFailed = !0, emit({
40
+ level: "error",
41
+ message: `Batch export of ${batch.length} document(s) failed \u2014 falling back to per-document export: ${err instanceof Error ? err.message : String(err)}`
42
+ });
43
+ }
44
+ const files = [];
45
+ let fallbackCount = 0;
46
+ for (const target of batch) {
47
+ let locjson = contentByTarget.get(target);
48
+ locjson === void 0 && (locjson = await smartcat.exportDocument(target.smartcatDocumentId), batchAttempted && fallbackCount++), files.push({ target, locjson });
49
+ }
50
+ return { files, fallbackCount, batchFailed };
51
+ }
52
+ async function runImportDownload(options) {
53
+ const {
54
+ sanity,
55
+ smartcat,
56
+ projectId,
57
+ now = () => (/* @__PURE__ */ new Date()).toISOString(),
58
+ batchSize = DEFAULT_BATCH_SIZE,
59
+ maxInboxBytes = DEFAULT_MAX_INBOX_BYTES,
60
+ concurrency = DEFAULT_CONCURRENCY,
61
+ timeBudgetMs = DEFAULT_TIME_BUDGET_MS,
62
+ nowMs = Date.now,
63
+ onLog
64
+ } = options, log = [], emit = (line) => {
65
+ log.push(line), onLog?.(line);
66
+ }, project = await sanity.fetch(PROJECT_QUERY, { id: projectId });
67
+ if (!project) throw new Error(`Translation project ${projectId} not found`);
68
+ try {
69
+ if (!project.smartcatProjectId)
70
+ throw new Error("Project has no smartcatProjectId \u2014 export it first");
71
+ const timestamp = now(), scProject = await smartcat.getProject(project.smartcatProjectId);
72
+ emit({ level: "info", message: "Checking Smartcat for completed translations" });
73
+ const scDocById = new Map((scProject.documents ?? []).filter((d) => d.id).map((d) => [d.id, d])), progress$1 = progress.buildProgress(
74
+ progress.itemsFromProject(project),
75
+ scProject,
76
+ project.progress,
77
+ timestamp,
78
+ project.smartcatLanguages
79
+ ), ready = [];
80
+ let skipped = 0, alreadyImported = 0;
81
+ for (const docProgress of progress$1)
82
+ for (const target of docProgress.targets) {
83
+ const id = target.smartcatDocumentId, label = id && smartcatDocPath(scDocById.get(id)) || docProgress.title || docProgress.sourceDocId;
84
+ if (!id || !progress.hasConfirmedContent(target.stages)) {
85
+ id && !target.imported && (skipped++, emit({ level: "skip", message: `${label} (Smartcat ID: ${id}) \u2192 ${target.language}: not ready, skipped` }));
86
+ continue;
87
+ }
88
+ if (target.imported) {
89
+ alreadyImported++;
90
+ continue;
91
+ }
92
+ ready.push({
93
+ key: target._key,
94
+ sourceDocId: docProgress.sourceDocId,
95
+ language: target.language,
96
+ smartcatDocumentId: id,
97
+ label
98
+ });
99
+ }
100
+ const inbox = [];
101
+ let inboxBytes = 0, remaining = 0;
102
+ const byLanguage = /* @__PURE__ */ new Map();
103
+ for (const target of ready) {
104
+ const group = byLanguage.get(target.language) ?? [];
105
+ group.push(target), byLanguage.set(target.language, group);
106
+ }
107
+ const batches = [];
108
+ for (const group of byLanguage.values())
109
+ for (let i = 0; i < group.length; i += batchSize)
110
+ batches.push(group.slice(i, i + batchSize));
111
+ const effectiveConcurrency = Math.max(1, Math.min(concurrency, batches.length));
112
+ batches.length > 0 && emit({
113
+ level: "info",
114
+ message: `Downloading ${ready.length} target(s) in ${batches.length} batch(es) of up to ${batchSize}, ${effectiveConcurrency} in parallel`
115
+ });
116
+ const startedAtMs = nowMs();
117
+ let deferredByBytes = 0, deferredByTime = 0, fallbackDownloads = 0, failedBatches = 0, nextBatch = 0;
118
+ const worker = async () => {
119
+ for (; nextBatch < batches.length; ) {
120
+ const index = nextBatch++, batch = batches[index];
121
+ if (inboxBytes >= maxInboxBytes) {
122
+ deferredByBytes += batch.length;
123
+ continue;
124
+ }
125
+ if (nowMs() - startedAtMs >= timeBudgetMs) {
126
+ deferredByTime += batch.length;
127
+ continue;
128
+ }
129
+ const batchStartMs = nowMs(), { files, fallbackCount, batchFailed } = await downloadBatch(smartcat, batch, emit);
130
+ fallbackDownloads += fallbackCount, batchFailed && failedBatches++;
131
+ for (const { target, locjson } of files)
132
+ inbox.push({
133
+ _key: target.key,
134
+ sourceDocId: target.sourceDocId,
135
+ targetLanguage: target.language,
136
+ smartcatDocumentId: target.smartcatDocumentId,
137
+ locjson
138
+ }), inboxBytes += locjson.length;
139
+ emit({
140
+ level: "success",
141
+ message: `Batch ${index + 1}/${batches.length} (${batch[0].language}): ${files.length} doc(s) in ${((nowMs() - batchStartMs) / 1e3).toFixed(1)}s${fallbackCount ? ` \u2014 ${fallbackCount} via per-document fallback` : ""}`
142
+ });
143
+ }
144
+ };
145
+ return await Promise.all(Array.from({ length: effectiveConcurrency }, worker)), remaining = deferredByBytes + deferredByTime, ready.length > 0 && emit({
146
+ level: failedBatches || fallbackDownloads ? "error" : "info",
147
+ message: `Download phase: ${((nowMs() - startedAtMs) / 1e3).toFixed(1)}s \u2014 ${batches.length} batch(es), ${failedBatches} failed, ${fallbackDownloads} per-document fallback download(s); deferred ${deferredByBytes} (inbox size cap) + ${deferredByTime} (time budget)`
148
+ }), emit({
149
+ level: "info",
150
+ message: `Downloaded ${inbox.length} target(s)${skipped ? `, skipped ${skipped} not ready` : ""}${alreadyImported ? `, ${alreadyImported} already imported` : ""}${remaining ? `, ${remaining} deferred \u2014 run Import again to continue` : ""}`
151
+ }), await sanity.patch(projectId).set({
152
+ inbox,
153
+ progress: progress$1,
154
+ progressSyncedAt: timestamp,
155
+ status: "downloaded",
156
+ // Deferred-target count for the dashboard: after applying this batch it
157
+ // auto-continues the import instead of asking for another click.
158
+ importRemaining: remaining,
159
+ lastError: null,
160
+ functionLog: JSON.stringify(log)
161
+ }).commit(), { downloaded: inbox.length, skipped, remaining, alreadyImported };
162
+ } catch (err) {
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ throw emit({ level: "error", message }), await sanity.patch(projectId).set({ status: "error", lastError: message, lastImportAt: now(), functionLog: JSON.stringify(log) }).commit().catch(() => {
165
+ }), err;
166
+ }
167
+ }
168
+ exports.runImportDownload = runImportDownload;
169
+ //# sourceMappingURL=import.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.cjs","sources":["../src/import/index.ts"],"sourcesContent":["import type {SmartcatDocument, SmartcatProject} from '../smartcat/types'\nimport {buildProgress, hasConfirmedContent, itemsFromProject, type DocProgress} from '../progress/core'\nimport {itemDocDeref} from '../lib/projectItems'\nimport type {SmartcatLanguageMapping} from '../lib/languageMap'\nimport type {LogLine} from '../lib/log'\n\n/** The Smartcat document's full path (folder + filename), extension stripped. */\nfunction smartcatDocPath(scDoc: SmartcatDocument | undefined): string {\n const raw = scDoc?.fullPath || scDoc?.name || scDoc?.filename || ''\n return raw.replace(/\\.[^./]+$/, '')\n}\n\n/** Minimal structural subset of @sanity/client used by the import download. */\nexport interface SanityLikeClient {\n fetch<T = unknown>(query: string, params?: Record<string, unknown>): Promise<T>\n patch(id: string): {\n set(attrs: Record<string, unknown>): {commit(): Promise<unknown>}\n }\n}\n\n/** Structural subset of SmartcatClient the import download needs. */\nexport interface SmartcatImportClient {\n getProject(projectId: string): Promise<SmartcatProject>\n exportDocument(documentId: string): Promise<string>\n /**\n * Batch export: one Smartcat task for many documents (ZIP result). Optional\n * so custom/legacy clients keep working — without it every document falls\n * back to a single {@link exportDocument} round-trip.\n */\n exportDocumentsBatch?(documentIds: string[]): Promise<{filename: string; content: string}[]>\n}\n\nexport interface RunImportDownloadOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatImportClient\n /** `_id` of the `smartcat.translationProject` to import. */\n projectId: string\n now?: () => string\n /** Documents per Smartcat batch-export task. */\n batchSize?: number\n /**\n * Soft cap on the total LocJSON bytes stored in the project's `inbox` per\n * run. The inbox lives on the Sanity project document, which has a practical\n * size limit — targets beyond the cap are reported as `remaining` and picked\n * up by the next import run.\n */\n maxInboxBytes?: number\n /** How many batch-export tasks run against Smartcat concurrently. */\n concurrency?: number\n /**\n * Soft time budget for the download phase, in ms. Once exceeded, batches not\n * yet started are deferred as `remaining` and the run exits cleanly — so the\n * Function always finishes well before its hard platform timeout instead of\n * being killed mid-run (which would leave the project stuck in \"importing\").\n */\n timeBudgetMs?: number\n /** Clock override for tests (ms). */\n nowMs?: () => number\n /**\n * Receives every log line as it is emitted (in addition to the functionLog\n * stored on the project document) — the Function forwards these to console\n * so `sanity functions logs` shows the run without reading the document.\n */\n onLog?: (line: LogLine) => void\n}\n\nexport interface RunImportDownloadResult {\n /** Number of complete targets downloaded into the inbox. */\n downloaded: number\n /** Number of targets skipped because they are not yet fully translated. */\n skipped: number\n /** Targets ready to download but deferred to the next run (inbox size cap). */\n remaining: number\n /** Targets already imported in a previous run and therefore not re-downloaded. */\n alreadyImported: number\n}\n\nconst DEFAULT_BATCH_SIZE = 50\n/** Keeps the project document comfortably below Sanity's document size limits. */\nconst DEFAULT_MAX_INBOX_BYTES = 1_500_000\n/**\n * Parallel Smartcat export tasks. High enough to divide the download time ~4×,\n * low enough not to crowd the account's export-task queue.\n */\nconst DEFAULT_CONCURRENCY = 4\n/** Exit-early margin against the 15-minute Sanity Function timeout. */\nconst DEFAULT_TIME_BUDGET_MS = 10 * 60_000\n\nconst PROJECT_QUERY = `*[_id == $id][0]{\n _id,\n smartcatProjectId,\n smartcatLanguages[]{sanityId, smartcatLanguage},\n \"items\": items[]{ \"doc\": ${itemDocDeref('{_id, title}')} },\n progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}\n}`\n\ninterface ProjectData {\n _id: string\n smartcatProjectId?: string\n smartcatLanguages?: SmartcatLanguageMapping[]\n items?: {doc: {_id: string; title?: string} | null}[]\n progress?: DocProgress[]\n}\n\ninterface InboxItem {\n _key: string\n sourceDocId: string\n targetLanguage: string\n smartcatDocumentId: string\n locjson: string\n}\n\n/** A (document × language) target selected for download this run. */\ninterface ReadyTarget {\n key: string\n sourceDocId: string\n language: string\n smartcatDocumentId: string\n label: string\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/** The Sanity source-document id a LocJSON carries in its `x-sanity` metadata. */\nfunction sanityDocIdOf(locjson: string): string | undefined {\n try {\n const parsed = JSON.parse(locjson) as {properties?: {'x-sanity'?: {documentId?: string}}}\n return parsed.properties?.['x-sanity']?.documentId\n } catch {\n return undefined\n }\n}\n\ninterface BatchDownloadResult {\n files: {target: ReadyTarget; locjson: string}[]\n /** Targets fetched one-by-one because the batched call failed or its archive missed them. */\n fallbackCount: number\n /** The batched export call itself failed (the error line is already emitted). */\n batchFailed: boolean\n}\n\n/**\n * Downloads a batch of same-language targets. Uses one batched export task\n * (ZIP) when the client supports it, correlating each returned file back to\n * its target via the LocJSON's own `x-sanity.documentId`. Targets the archive\n * didn't cover — and the whole batch when the batched call fails — fall back\n * to per-document exports, so a batch problem degrades to the old (slow but\n * proven) path instead of failing the run. Every degradation is logged loudly:\n * a silent fallback looks exactly like \"batching deployed but nothing faster\".\n */\nasync function downloadBatch(\n smartcat: SmartcatImportClient,\n batch: ReadyTarget[],\n emit: (line: LogLine) => void,\n): Promise<BatchDownloadResult> {\n const contentByTarget = new Map<ReadyTarget, string>()\n const batchAttempted = Boolean(smartcat.exportDocumentsBatch && batch.length > 1)\n let batchFailed = false\n\n if (batchAttempted) {\n try {\n const files = await smartcat.exportDocumentsBatch!(batch.map((t) => t.smartcatDocumentId))\n const targetByDocId = new Map(batch.map((t) => [stripDraft(t.sourceDocId), t]))\n for (const file of files) {\n const docId = sanityDocIdOf(file.content)\n const target = docId ? targetByDocId.get(stripDraft(docId)) : undefined\n if (target && !contentByTarget.has(target)) contentByTarget.set(target, file.content)\n }\n if (contentByTarget.size < batch.length) {\n emit({\n level: 'info',\n message: `${batch.length - contentByTarget.size} of ${batch.length} file(s) missing from the batch archive — fetching them per-document`,\n })\n }\n } catch (err) {\n batchFailed = true\n emit({\n level: 'error',\n message: `Batch export of ${batch.length} document(s) failed — falling back to per-document export: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n }\n\n const files: {target: ReadyTarget; locjson: string}[] = []\n let fallbackCount = 0\n for (const target of batch) {\n let locjson = contentByTarget.get(target)\n if (locjson === undefined) {\n locjson = await smartcat.exportDocument(target.smartcatDocumentId)\n if (batchAttempted) fallbackCount++\n }\n files.push({target, locjson})\n }\n return {files, fallbackCount, batchFailed}\n}\n\n/**\n * Thin import step run by a Sanity Function: refreshes per-stage progress from\n * Smartcat, then downloads the ready targets into the project's `inbox` and\n * sets status to `downloaded`. The browser turns the inbox into locale\n * variants and decides final completion.\n *\n * Downloads happen in **batches** (one Smartcat export task per ~50 documents,\n * returned as a ZIP), so run time no longer scales with per-document polling —\n * large projects fit comfortably inside the Function timeout. The inbox is\n * size-capped per run; overflow targets are reported as `remaining` and the\n * next import run picks them up (already-imported targets are skipped).\n *\n * Targets with nothing confirmed are skipped, so no untranslated variants are\n * created. Does no content processing, so it depends only on the Smartcat client.\n */\nexport async function runImportDownload(\n options: RunImportDownloadOptions,\n): Promise<RunImportDownloadResult> {\n const {\n sanity,\n smartcat,\n projectId,\n now = () => new Date().toISOString(),\n batchSize = DEFAULT_BATCH_SIZE,\n maxInboxBytes = DEFAULT_MAX_INBOX_BYTES,\n concurrency = DEFAULT_CONCURRENCY,\n timeBudgetMs = DEFAULT_TIME_BUDGET_MS,\n nowMs = Date.now,\n onLog,\n } = options\n const log: LogLine[] = []\n const emit = (line: LogLine) => {\n log.push(line)\n onLog?.(line)\n }\n\n const project = await sanity.fetch<ProjectData | null>(PROJECT_QUERY, {id: projectId})\n if (!project) throw new Error(`Translation project ${projectId} not found`)\n\n try {\n if (!project.smartcatProjectId) {\n throw new Error('Project has no smartcatProjectId — export it first')\n }\n\n const timestamp = now()\n const scProject = await smartcat.getProject(project.smartcatProjectId)\n emit({level: 'info', message: 'Checking Smartcat for completed translations'})\n // Look up each target's Smartcat document by its id, for full-path logging.\n const scDocById = new Map((scProject.documents ?? []).filter((d) => d.id).map((d) => [d.id, d]))\n\n // Rebuild progress from Smartcat + the project's items, so the dashboard\n // reflects the latest state even for targets we don't import this round.\n const progress = buildProgress(\n itemsFromProject(project),\n scProject,\n project.progress,\n timestamp,\n project.smartcatLanguages,\n )\n\n // Collect every target that has confirmed content (at any stage); targets\n // with nothing confirmed are skipped. Targets already imported in an\n // earlier run are not re-downloaded, so capped runs make forward progress.\n const ready: ReadyTarget[] = []\n let skipped = 0\n let alreadyImported = 0\n for (const docProgress of progress) {\n for (const target of docProgress.targets) {\n const id = target.smartcatDocumentId\n // Prefer the Smartcat full path; fall back to the resolved title / source id.\n const label =\n (id && smartcatDocPath(scDocById.get(id))) || docProgress.title || docProgress.sourceDocId\n if (!id || !hasConfirmedContent(target.stages)) {\n if (id && !target.imported) {\n skipped++\n emit({level: 'skip', message: `${label} (Smartcat ID: ${id}) → ${target.language}: not ready, skipped`})\n }\n continue\n }\n if (target.imported) {\n alreadyImported++\n continue\n }\n ready.push({\n key: target._key,\n sourceDocId: docProgress.sourceDocId,\n language: target.language,\n smartcatDocumentId: id,\n label,\n })\n }\n }\n\n // Download in batches: one Smartcat export task per batch (ZIP result)\n // instead of a request+poll round-trip per document. Batches are grouped\n // by language so each source document appears at most once per archive.\n const inbox: InboxItem[] = []\n let inboxBytes = 0\n let remaining = 0\n const byLanguage = new Map<string, ReadyTarget[]>()\n for (const target of ready) {\n const group = byLanguage.get(target.language) ?? []\n group.push(target)\n byLanguage.set(target.language, group)\n }\n\n const batches: ReadyTarget[][] = []\n for (const group of byLanguage.values()) {\n for (let i = 0; i < group.length; i += batchSize) {\n batches.push(group.slice(i, i + batchSize))\n }\n }\n\n // Worker pool: up to `concurrency` batch-export tasks in flight at once —\n // Smartcat assembles the archives in parallel, dividing the download time.\n // Each worker re-checks the caps before claiming the next batch:\n // - inbox byte cap: the inbox lives on the project document, which must\n // stay below Sanity's document size limit;\n // - time budget: exit cleanly before the platform kills the Function, so a\n // slow Smartcat day degrades to \"continue in the next run\", never to a\n // project stuck in \"importing\".\n const effectiveConcurrency = Math.max(1, Math.min(concurrency, batches.length))\n if (batches.length > 0) {\n emit({\n level: 'info',\n message: `Downloading ${ready.length} target(s) in ${batches.length} batch(es) of up to ${batchSize}, ${effectiveConcurrency} in parallel`,\n })\n }\n const startedAtMs = nowMs()\n let deferredByBytes = 0\n let deferredByTime = 0\n let fallbackDownloads = 0\n let failedBatches = 0\n let nextBatch = 0\n const worker = async () => {\n while (nextBatch < batches.length) {\n const index = nextBatch++\n const batch = batches[index]\n if (inboxBytes >= maxInboxBytes) {\n deferredByBytes += batch.length\n continue\n }\n if (nowMs() - startedAtMs >= timeBudgetMs) {\n deferredByTime += batch.length\n continue\n }\n const batchStartMs = nowMs()\n const {files, fallbackCount, batchFailed} = await downloadBatch(smartcat, batch, emit)\n fallbackDownloads += fallbackCount\n if (batchFailed) failedBatches++\n for (const {target, locjson} of files) {\n inbox.push({\n _key: target.key,\n sourceDocId: target.sourceDocId,\n targetLanguage: target.language,\n smartcatDocumentId: target.smartcatDocumentId,\n locjson,\n })\n inboxBytes += locjson.length\n }\n emit({\n level: 'success',\n message:\n `Batch ${index + 1}/${batches.length} (${batch[0].language}): ${files.length} doc(s) in ` +\n `${((nowMs() - batchStartMs) / 1000).toFixed(1)}s` +\n `${fallbackCount ? ` — ${fallbackCount} via per-document fallback` : ''}`,\n })\n }\n }\n await Promise.all(Array.from({length: effectiveConcurrency}, worker))\n remaining = deferredByBytes + deferredByTime\n\n // One line that answers \"where did the time go and why was anything left\n // behind\" — the silent-degradation postmortems start here.\n if (ready.length > 0) {\n emit({\n level: failedBatches || fallbackDownloads ? 'error' : 'info',\n message:\n `Download phase: ${((nowMs() - startedAtMs) / 1000).toFixed(1)}s — ` +\n `${batches.length} batch(es), ${failedBatches} failed, ${fallbackDownloads} per-document fallback download(s); ` +\n `deferred ${deferredByBytes} (inbox size cap) + ${deferredByTime} (time budget)`,\n })\n }\n\n emit({\n level: 'info',\n message:\n `Downloaded ${inbox.length} target(s)` +\n `${skipped ? `, skipped ${skipped} not ready` : ''}` +\n `${alreadyImported ? `, ${alreadyImported} already imported` : ''}` +\n `${remaining ? `, ${remaining} deferred — run Import again to continue` : ''}`,\n })\n\n await sanity\n .patch(projectId)\n .set({\n inbox,\n progress,\n progressSyncedAt: timestamp,\n status: 'downloaded',\n // Deferred-target count for the dashboard: after applying this batch it\n // auto-continues the import instead of asking for another click.\n importRemaining: remaining,\n lastError: null,\n functionLog: JSON.stringify(log),\n })\n .commit()\n\n return {downloaded: inbox.length, skipped, remaining, alreadyImported}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n emit({level: 'error', message})\n await sanity\n .patch(projectId)\n .set({status: 'error', lastError: message, lastImportAt: now(), functionLog: JSON.stringify(log)})\n .commit()\n .catch(() => {})\n throw err\n }\n}\n"],"names":["itemDocDeref","files","progress","buildProgress","itemsFromProject","hasConfirmedContent"],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Live, in-memory log lines for the export/import flows, streamed to the
3
+ * ProjectView Log panel as each document and field is processed. Never persisted
4
+ * — purely a session-scoped trace for troubleshooting (e.g. on client calls).
5
+ */
6
+ declare type LogLevel = "info" | "success" | "skip" | "error" | "update";
7
+
8
+ declare interface LogLine {
9
+ level: LogLevel;
10
+ message: string;
11
+ /** Field-level lines are indented under their document header. */
12
+ indent?: boolean;
13
+ /** Optional value (source or translated text); revealed on click in the panel. */
14
+ value?: string;
15
+ /** Prior value; when set, the panel shows `before` and `value` side by side. */
16
+ before?: string;
17
+ /** Extra detail (e.g. a JSON parse error); shown in its own box on expand. */
18
+ error?: string;
19
+ }
20
+
21
+ /**
22
+ * Thin import step run by a Sanity Function: refreshes per-stage progress from
23
+ * Smartcat, then downloads the ready targets into the project's `inbox` and
24
+ * sets status to `downloaded`. The browser turns the inbox into locale
25
+ * variants and decides final completion.
26
+ *
27
+ * Downloads happen in **batches** (one Smartcat export task per ~50 documents,
28
+ * returned as a ZIP), so run time no longer scales with per-document polling —
29
+ * large projects fit comfortably inside the Function timeout. The inbox is
30
+ * size-capped per run; overflow targets are reported as `remaining` and the
31
+ * next import run picks them up (already-imported targets are skipped).
32
+ *
33
+ * Targets with nothing confirmed are skipped, so no untranslated variants are
34
+ * created. Does no content processing, so it depends only on the Smartcat client.
35
+ */
36
+ export declare function runImportDownload(
37
+ options: RunImportDownloadOptions,
38
+ ): Promise<RunImportDownloadResult>;
39
+
40
+ export declare interface RunImportDownloadOptions {
41
+ sanity: SanityLikeClient;
42
+ smartcat: SmartcatImportClient;
43
+ /** `_id` of the `smartcat.translationProject` to import. */
44
+ projectId: string;
45
+ now?: () => string;
46
+ /** Documents per Smartcat batch-export task. */
47
+ batchSize?: number;
48
+ /**
49
+ * Soft cap on the total LocJSON bytes stored in the project's `inbox` per
50
+ * run. The inbox lives on the Sanity project document, which has a practical
51
+ * size limit — targets beyond the cap are reported as `remaining` and picked
52
+ * up by the next import run.
53
+ */
54
+ maxInboxBytes?: number;
55
+ /** How many batch-export tasks run against Smartcat concurrently. */
56
+ concurrency?: number;
57
+ /**
58
+ * Soft time budget for the download phase, in ms. Once exceeded, batches not
59
+ * yet started are deferred as `remaining` and the run exits cleanly — so the
60
+ * Function always finishes well before its hard platform timeout instead of
61
+ * being killed mid-run (which would leave the project stuck in "importing").
62
+ */
63
+ timeBudgetMs?: number;
64
+ /** Clock override for tests (ms). */
65
+ nowMs?: () => number;
66
+ /**
67
+ * Receives every log line as it is emitted (in addition to the functionLog
68
+ * stored on the project document) — the Function forwards these to console
69
+ * so `sanity functions logs` shows the run without reading the document.
70
+ */
71
+ onLog?: (line: LogLine) => void;
72
+ }
73
+
74
+ export declare interface RunImportDownloadResult {
75
+ /** Number of complete targets downloaded into the inbox. */
76
+ downloaded: number;
77
+ /** Number of targets skipped because they are not yet fully translated. */
78
+ skipped: number;
79
+ /** Targets ready to download but deferred to the next run (inbox size cap). */
80
+ remaining: number;
81
+ /** Targets already imported in a previous run and therefore not re-downloaded. */
82
+ alreadyImported: number;
83
+ }
84
+
85
+ /** Minimal structural subset of @sanity/client used by the import download. */
86
+ export declare interface SanityLikeClient {
87
+ fetch<T = unknown>(
88
+ query: string,
89
+ params?: Record<string, unknown>,
90
+ ): Promise<T>;
91
+ patch(id: string): {
92
+ set(attrs: Record<string, unknown>): {
93
+ commit(): Promise<unknown>;
94
+ };
95
+ };
96
+ }
97
+
98
+ /** A document within a Smartcat project (one per source file × target language). */
99
+ declare interface SmartcatDocument {
100
+ /** Target document id, formatted `<fileGuid>_<languageNumber>`. */
101
+ id: string;
102
+ name?: string;
103
+ fullPath?: string;
104
+ filename?: string;
105
+ /** Shared id of the uploaded source file (the `<fileGuid>` part of `id`). */
106
+ externalId?: string;
107
+ sourceLanguage?: string;
108
+ targetLanguage?: string;
109
+ /** Per-stage progress. On documents these carry only `{id, progress}`; the
110
+ * stage names live on the project-level `workflowStages` (keyed by `id`). */
111
+ workflowStages?: SmartcatWorkflowStage[];
112
+ }
113
+
114
+ /** Structural subset of SmartcatClient the import download needs. */
115
+ export declare interface SmartcatImportClient {
116
+ getProject(projectId: string): Promise<SmartcatProject>;
117
+ exportDocument(documentId: string): Promise<string>;
118
+ /**
119
+ * Batch export: one Smartcat task for many documents (ZIP result). Optional
120
+ * so custom/legacy clients keep working — without it every document falls
121
+ * back to a single {@link exportDocument} round-trip.
122
+ */
123
+ exportDocumentsBatch?(documentIds: string[]): Promise<
124
+ {
125
+ filename: string;
126
+ content: string;
127
+ }[]
128
+ >;
129
+ }
130
+
131
+ /** A Smartcat project as returned by the API. */
132
+ declare interface SmartcatProject {
133
+ id: string;
134
+ name?: string;
135
+ sourceLanguage?: string;
136
+ targetLanguages?: string[];
137
+ /** Project-level stages, where each `id` maps to a `stageType` (e.g. "translation"). */
138
+ workflowStages?: SmartcatWorkflowStage[];
139
+ documents?: SmartcatDocument[];
140
+ }
141
+
142
+ declare interface SmartcatWorkflowStage {
143
+ id?: string;
144
+ /** Stage type/name, e.g. "translation" / "editing" (casing varies by API). */
145
+ stageType?: string;
146
+ /** Human-readable stage name, when the API provides one. */
147
+ stageName?: string;
148
+ /** Completion percentage for this stage, 0–100. */
149
+ progress?: number;
150
+ /** Word counts, when present — useful for a weighted progress view. */
151
+ wordsTranslated?: number;
152
+ totalWordsCount?: number;
153
+ }
154
+
155
+ export {};