@smartcat/sanity-plugin 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-cjs/index.cjs +18 -5
- package/dist/_chunks-cjs/index.cjs.map +1 -1
- package/dist/_chunks-cjs/index2.cjs +8 -6
- package/dist/_chunks-cjs/index2.cjs.map +1 -1
- package/dist/_chunks-es/index.js +18 -5
- package/dist/_chunks-es/index.js.map +1 -1
- package/dist/_chunks-es/index2.js +8 -6
- package/dist/_chunks-es/index2.js.map +1 -1
- package/dist/export.cjs +21 -4
- package/dist/export.cjs.map +1 -1
- package/dist/export.d.cts +2 -0
- package/dist/export.d.ts +2 -0
- package/dist/export.js +22 -5
- package/dist/export.js.map +1 -1
- package/dist/index.cjs +477 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +482 -129
- package/dist/index.js.map +1 -1
- package/dist/locjson.d.cts +15 -0
- package/dist/locjson.d.ts +15 -0
- package/dist/progress.d.cts +0 -19
- package/dist/progress.d.ts +0 -19
- package/dist/smartcat.cjs +14 -2
- package/dist/smartcat.cjs.map +1 -1
- package/dist/smartcat.d.cts +10 -1
- package/dist/smartcat.d.ts +10 -1
- package/dist/smartcat.js +14 -2
- package/dist/smartcat.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/AddToProjectAction.tsx +23 -2
- package/src/export/export.test.ts +42 -5
- package/src/export/index.ts +63 -8
- package/src/lib/locjson/filename.ts +7 -2
- package/src/lib/locjson/locjson.test.ts +64 -0
- package/src/lib/locjson/serialize.ts +38 -2
- package/src/progress/core.ts +44 -3
- package/src/progress/progress.test.ts +40 -0
- package/src/schema/translationProject.ts +19 -0
- package/src/smartcat/client.ts +15 -1
- package/src/tool/AddItemsSearch.tsx +462 -0
- package/src/tool/DocTitle.tsx +23 -0
- package/src/tool/ProjectView.tsx +29 -187
- package/src/tool/data.ts +7 -0
- package/src/tool/itemActions.tsx +216 -0
- package/src/tool/processing.test.ts +11 -1
- package/src/tool/processing.ts +116 -4
package/dist/export.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.cjs","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":["sourceIdPrefix","smartcat","toSmartcatLanguage","resolveWorkflowCreateParams","SmartcatError","progress","buildProgress"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"export.cjs","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, toSanityLanguage, 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 /** `targetLanguage` (Smartcat code) restricts a bilingual file to one language. */\n uploadDocument(projectId: string, filename: string, content: string, targetLanguage?: 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, targetLanguage},\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 /** Present for per-language bilingual files (send-existing flow): the single\n * Smartcat target-language code this file carries. Absent ⇒ target all langs. */\n targetLanguage?: 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 // Per-language bilingual files (send-existing flow) upload one document per\n // (source × language); the rest target all languages with one file. They take\n // different upload/update paths, so split them here.\n const perLangBox = outbox.filter((item) => item.targetLanguage)\n const stdBox = outbox.filter((item) => !item.targetLanguage)\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. Only the all-language\n // files use this source-prefix map; per-language files resolve their existing\n // document id per (source × language) from stored progress below.\n const existing =\n isNew || stdBox.length === 0\n ? new Map<string, string>()\n : existingDocIdsBySource(await smartcat.getProject(scProjectId as string), stdBox)\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 = stdBox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId))\n const toCreate = stdBox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)))\n\n // Existing per-language document ids, keyed `${sourceDocId}__${sanityLocale}`\n // from stored progress — the target that re-sync updates in place. Progress\n // stores the Sanity locale, so map the outbox's Smartcat code back to it.\n const perLangDocId = new Map<string, string>()\n for (const doc of project.progress ?? []) {\n for (const t of doc.targets) {\n if (t.smartcatDocumentId) perLangDocId.set(`${doc.sourceDocId}__${t.language}`, t.smartcatDocumentId)\n }\n }\n const existingPerLangId = (item: OutboxItem): string | undefined => {\n if (!item.sourceDocId || !item.targetLanguage) return undefined\n const locale = toSanityLanguage(project.smartcatLanguages, item.targetLanguage)\n return perLangDocId.get(`${item.sourceDocId}__${locale}`)\n }\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 // Per-language bilingual files: one document per (source × language). Update in\n // place when a stored document id exists for that pair (rename-proof: keyed by\n // the stable Smartcat id, not the filename), otherwise create with the target\n // language pinned via the upload's `documentModel`. Tag written docs with the\n // filename so buildProgress correlates them, and capture the id for next time.\n await runWithConcurrency(perLangBox, UPLOAD_CHUNK_CONCURRENCY, async (item) => {\n const existingId = existingPerLangId(item)\n try {\n const returned = existingId\n ? await smartcat.updateDocument(existingId, item.filename, item.locjson)\n : await smartcat.uploadDocument(scProjectId as string, item.filename, item.locjson, item.targetLanguage)\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n if (existingId) updated++\n else created++\n log.push({level: 'success', message: `${existingId ? 'Updated' : 'Uploaded'} ${item.filename}`})\n } catch (e) {\n // A 409 means a prior attempt already created this file (HTTP error masking\n // a successful create) — it's synced, not failed.\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 // 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":["sourceIdPrefix","smartcat","toSmartcatLanguage","resolveWorkflowCreateParams","toSanityLanguage","SmartcatError","progress","buildProgress"],"mappings":"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"}
|
package/dist/export.d.cts
CHANGED
|
@@ -103,10 +103,12 @@ export declare interface SmartcatExportClient {
|
|
|
103
103
|
createProject(input: CreateProjectInput): Promise<{
|
|
104
104
|
id: string;
|
|
105
105
|
}>;
|
|
106
|
+
/** `targetLanguage` (Smartcat code) restricts a bilingual file to one language. */
|
|
106
107
|
uploadDocument(
|
|
107
108
|
projectId: string,
|
|
108
109
|
filename: string,
|
|
109
110
|
content: string,
|
|
111
|
+
targetLanguage?: string,
|
|
110
112
|
): Promise<SmartcatDocument[]>;
|
|
111
113
|
uploadDocuments(
|
|
112
114
|
projectId: string,
|
package/dist/export.d.ts
CHANGED
|
@@ -103,10 +103,12 @@ export declare interface SmartcatExportClient {
|
|
|
103
103
|
createProject(input: CreateProjectInput): Promise<{
|
|
104
104
|
id: string;
|
|
105
105
|
}>;
|
|
106
|
+
/** `targetLanguage` (Smartcat code) restricts a bilingual file to one language. */
|
|
106
107
|
uploadDocument(
|
|
107
108
|
projectId: string,
|
|
108
109
|
filename: string,
|
|
109
110
|
content: string,
|
|
111
|
+
targetLanguage?: string,
|
|
110
112
|
): Promise<SmartcatDocument[]>;
|
|
111
113
|
uploadDocuments(
|
|
112
114
|
projectId: string,
|
package/dist/export.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SmartcatError } from "./smartcat.js";
|
|
2
|
-
import { toSmartcatLanguage, buildProgress, sourceIdPrefix } from "./_chunks-es/index.js";
|
|
2
|
+
import { toSmartcatLanguage, toSanityLanguage, buildProgress, sourceIdPrefix } from "./_chunks-es/index.js";
|
|
3
3
|
import { resolveWorkflowCreateParams } from "./_chunks-es/workflow.js";
|
|
4
4
|
const UPLOAD_CHUNK_SIZE = 50, BATCH_RETRY_DELAY_MS = 1e3, UPLOAD_CHUNK_CONCURRENCY = 10;
|
|
5
5
|
function chunk(items, size) {
|
|
@@ -25,7 +25,7 @@ const PROJECT_QUERY = `*[_id == $id][0]{
|
|
|
25
25
|
smartcatProjectId,
|
|
26
26
|
smartcatProjectUrl,
|
|
27
27
|
smartcatLanguages[]{sanityId, smartcatLanguage},
|
|
28
|
-
outbox[]{sourceDocId, title, filename, locjson},
|
|
28
|
+
outbox[]{sourceDocId, title, filename, locjson, targetLanguage},
|
|
29
29
|
pendingDeletions[]{itemKey, sourceDocId, smartcatDocumentId},
|
|
30
30
|
progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
|
|
31
31
|
}`;
|
|
@@ -95,9 +95,17 @@ async function runExportUpload(options) {
|
|
|
95
95
|
} catch (e) {
|
|
96
96
|
log.push({ level: "error", message: `Failed to delete document ${del.smartcatDocumentId}: ${e instanceof Error ? e.message : String(e)}` });
|
|
97
97
|
}
|
|
98
|
-
const existing = isNew ||
|
|
98
|
+
const perLangBox = outbox.filter((item) => item.targetLanguage), stdBox = outbox.filter((item) => !item.targetLanguage), existing = isNew || stdBox.length === 0 ? /* @__PURE__ */ new Map() : existingDocIdsBySource(await smartcat.getProject(scProjectId), stdBox);
|
|
99
99
|
let created = 0, updated = 0, failed = 0;
|
|
100
|
-
const writtenDocs = [], toUpdate =
|
|
100
|
+
const writtenDocs = [], toUpdate = stdBox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId)), toCreate = stdBox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId))), perLangDocId = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const doc of project.progress ?? [])
|
|
102
|
+
for (const t of doc.targets)
|
|
103
|
+
t.smartcatDocumentId && perLangDocId.set(`${doc.sourceDocId}__${t.language}`, t.smartcatDocumentId);
|
|
104
|
+
const existingPerLangId = (item) => {
|
|
105
|
+
if (!item.sourceDocId || !item.targetLanguage) return;
|
|
106
|
+
const locale = toSanityLanguage(project.smartcatLanguages, item.targetLanguage);
|
|
107
|
+
return perLangDocId.get(`${item.sourceDocId}__${locale}`);
|
|
108
|
+
};
|
|
101
109
|
for (const item of toUpdate) {
|
|
102
110
|
const docId = existing.get(item.sourceDocId);
|
|
103
111
|
try {
|
|
@@ -131,7 +139,16 @@ async function runExportUpload(options) {
|
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
|
-
await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk)
|
|
142
|
+
await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk), await runWithConcurrency(perLangBox, UPLOAD_CHUNK_CONCURRENCY, async (item) => {
|
|
143
|
+
const existingId = existingPerLangId(item);
|
|
144
|
+
try {
|
|
145
|
+
const returned = existingId ? await smartcat.updateDocument(existingId, item.filename, item.locjson) : await smartcat.uploadDocument(scProjectId, item.filename, item.locjson, item.targetLanguage);
|
|
146
|
+
for (const d of returned) writtenDocs.push({ ...d, name: d.name || item.filename });
|
|
147
|
+
existingId ? updated++ : created++, log.push({ level: "success", message: `${existingId ? "Updated" : "Uploaded"} ${item.filename}` });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
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)` }));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
135
152
|
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
153
|
log.push({
|
|
137
154
|
level: failed ? "error" : "info",
|
package/dist/export.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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, toSanityLanguage, 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 /** `targetLanguage` (Smartcat code) restricts a bilingual file to one language. */\n uploadDocument(projectId: string, filename: string, content: string, targetLanguage?: 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, targetLanguage},\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 /** Present for per-language bilingual files (send-existing flow): the single\n * Smartcat target-language code this file carries. Absent ⇒ target all langs. */\n targetLanguage?: 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 // Per-language bilingual files (send-existing flow) upload one document per\n // (source × language); the rest target all languages with one file. They take\n // different upload/update paths, so split them here.\n const perLangBox = outbox.filter((item) => item.targetLanguage)\n const stdBox = outbox.filter((item) => !item.targetLanguage)\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. Only the all-language\n // files use this source-prefix map; per-language files resolve their existing\n // document id per (source × language) from stored progress below.\n const existing =\n isNew || stdBox.length === 0\n ? new Map<string, string>()\n : existingDocIdsBySource(await smartcat.getProject(scProjectId as string), stdBox)\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 = stdBox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId))\n const toCreate = stdBox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)))\n\n // Existing per-language document ids, keyed `${sourceDocId}__${sanityLocale}`\n // from stored progress — the target that re-sync updates in place. Progress\n // stores the Sanity locale, so map the outbox's Smartcat code back to it.\n const perLangDocId = new Map<string, string>()\n for (const doc of project.progress ?? []) {\n for (const t of doc.targets) {\n if (t.smartcatDocumentId) perLangDocId.set(`${doc.sourceDocId}__${t.language}`, t.smartcatDocumentId)\n }\n }\n const existingPerLangId = (item: OutboxItem): string | undefined => {\n if (!item.sourceDocId || !item.targetLanguage) return undefined\n const locale = toSanityLanguage(project.smartcatLanguages, item.targetLanguage)\n return perLangDocId.get(`${item.sourceDocId}__${locale}`)\n }\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 // Per-language bilingual files: one document per (source × language). Update in\n // place when a stored document id exists for that pair (rename-proof: keyed by\n // the stable Smartcat id, not the filename), otherwise create with the target\n // language pinned via the upload's `documentModel`. Tag written docs with the\n // filename so buildProgress correlates them, and capture the id for next time.\n await runWithConcurrency(perLangBox, UPLOAD_CHUNK_CONCURRENCY, async (item) => {\n const existingId = existingPerLangId(item)\n try {\n const returned = existingId\n ? await smartcat.updateDocument(existingId, item.filename, item.locjson)\n : await smartcat.uploadDocument(scProjectId as string, item.filename, item.locjson, item.targetLanguage)\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n if (existingId) updated++\n else created++\n log.push({level: 'success', message: `${existingId ? 'Updated' : 'Uploaded'} ${item.filename}`})\n } catch (e) {\n // A 409 means a prior attempt already created this file (HTTP error masking\n // a successful create) — it's synced, not failed.\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 // 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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"}
|