@smartcat/sanity-plugin 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"import.js","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":["files"],"mappings":"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"}
1
+ {"version":3,"file":"import.js","sources":["../src/import/index.ts"],"sourcesContent":["import type {SmartcatDocument, SmartcatProject} from '../smartcat/types'\nimport {buildProgress, hasConfirmedContent, itemsFromProject, type DocProgress} from '../progress/core'\nimport {ITEM_ID, itemDocDerefRespectingDraft} 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[]{ \"_id\": ${ITEM_ID}, \"title\": ${itemDocDerefRespectingDraft('.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?: {_id?: string; title?: string}[]\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":["files"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -84,7 +84,11 @@ function createTranslationProjectType() {
84
84
  title: "Project item",
85
85
  fields: [
86
86
  sanity.defineField({ name: "docId", title: "Document ID", type: "string" }),
87
- sanity.defineField({ name: "docType", title: "Document type", type: "string" })
87
+ sanity.defineField({ name: "docType", title: "Document type", type: "string" }),
88
+ // Set when the item was added from the published perspective, so the
89
+ // export translates the published document. Absent/false (incl. all
90
+ // legacy items) prefers the draft — see itemDocDerefRespectingDraft.
91
+ sanity.defineField({ name: "sourceIsPublished", title: "Source is published", type: "boolean" })
88
92
  ],
89
93
  preview: { select: { title: "docId", subtitle: "docType" } }
90
94
  })
@@ -430,7 +434,7 @@ const PROJECTS_QUERY$1 = `*[_type == $type] | order(name asc){
430
434
  function createAddToProjectAction(config) {
431
435
  const sourceLanguage = config.sourceLanguage;
432
436
  return (props) => {
433
- const { id, draft, published, onComplete } = props, client = sanity.useClient({ apiVersion: constants.API_VERSION }), schema = sanity.useSchema(), toast = ui.useToast(), [open, setOpen] = react.useState(!1), [projects, setProjects] = react.useState(null), [selection, setSelection] = react.useState(CREATE_NEW), [newName, setNewName] = react.useState(""), [busy, setBusy] = react.useState(!1), [includeLinked, setIncludeLinked] = react.useState(!0), { templates, loaded, requestRefresh } = useTemplates(), [workflow2, setWorkflow] = useWorkflowSelection(open, templates, loaded), publishedId2 = id.replace(/^drafts\./, ""), doc = draft ?? published, docTitle = doc && resolveDocumentTitle(schema, doc) || UNTITLED;
437
+ const { id, draft, published, onComplete } = props, client = sanity.useClient({ apiVersion: constants.API_VERSION }), schema = sanity.useSchema(), toast = ui.useToast(), { selectedPerspectiveName } = sanity.usePerspective(), sourceIsPublished = selectedPerspectiveName === "published" || !draft, [open, setOpen] = react.useState(!1), [projects, setProjects] = react.useState(null), [selection, setSelection] = react.useState(CREATE_NEW), [newName, setNewName] = react.useState(""), [busy, setBusy] = react.useState(!1), [includeLinked, setIncludeLinked] = react.useState(!0), { templates, loaded, requestRefresh } = useTemplates(), [workflow2, setWorkflow] = useWorkflowSelection(open, templates, loaded), publishedId2 = id.replace(/^drafts\./, ""), doc = draft ?? published, docTitle = doc && resolveDocumentTitle(schema, doc) || UNTITLED;
434
438
  react.useEffect(() => {
435
439
  if (!open) return;
436
440
  requestRefresh();
@@ -452,7 +456,8 @@ function createAddToProjectAction(config) {
452
456
  _type: "smartcat.projectItem",
453
457
  _key: uuid.uuid(),
454
458
  docId,
455
- docType
459
+ docType,
460
+ sourceIsPublished
456
461
  }), collected = /* @__PURE__ */ new Map([[publishedId2, doc?._type]]);
457
462
  if (includeLinked) {
458
463
  const linked = await gatherLinkedDocuments({ client, rootId: publishedId2, isRoot: (type) => config.rootTypes.includes(type), isTranslatable: (type) => locjson.getTranslatableFields(schema.get(type), { exclude: [config.documentI18nLanguageField] }).length > 0 });
@@ -494,7 +499,7 @@ function createAddToProjectAction(config) {
494
499
  } finally {
495
500
  setBusy(!1);
496
501
  }
497
- }, [selection, newName, workflow2, publishedId2, doc, includeLinked, schema, client, projects, close]), content = react.useMemo(() => projects === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
502
+ }, [selection, newName, workflow2, publishedId2, doc, includeLinked, sourceIsPublished, schema, client, projects, close]), content = react.useMemo(() => projects === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
498
503
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
499
504
  /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: "Project" }),
500
505
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -545,7 +550,20 @@ function createAddToProjectAction(config) {
545
550
  ] }),
546
551
  /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
547
552
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
548
- "Adding ",
553
+ "Adding",
554
+ " ",
555
+ /* @__PURE__ */ jsxRuntime.jsx(
556
+ ui.Badge,
557
+ {
558
+ tone: sourceIsPublished ? "positive" : "caution",
559
+ fontSize: 1,
560
+ paddingX: 2,
561
+ style: { verticalAlign: "middle" },
562
+ children: sourceIsPublished ? "Published" : "Draft"
563
+ }
564
+ ),
565
+ " ",
566
+ "version of ",
549
567
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: docTitle }),
550
568
  " to the selected project."
551
569
  ] }),
@@ -578,7 +596,7 @@ function createAddToProjectAction(config) {
578
596
  }
579
597
  )
580
598
  ] })
581
- ] }), [projects, selection, newName, workflow2, templates, busy, includeLinked, docTitle, close, handleConfirm]);
599
+ ] }), [projects, selection, newName, workflow2, templates, busy, includeLinked, docTitle, sourceIsPublished, close, handleConfirm]);
582
600
  return {
583
601
  label: "Add to translation project",
584
602
  icon: icons.TranslateIcon,
@@ -614,6 +632,7 @@ const PROJECTS_QUERY = `*[_type == "${constants.TRANSLATION_PROJECT_TYPE}"] | or
614
632
  items[]{
615
633
  _key,
616
634
  "docId": ${progress.ITEM_ID},
635
+ sourceIsPublished,
617
636
  "doc": ${progress.itemDocDeref("{_id, _type}")},
618
637
  "translations": *[_type == "translation.metadata" && references(${progress.ITEM_ID_FROM_SUBQUERY})][0]
619
638
  .translations[]{language, "id": value._ref}
@@ -789,6 +808,16 @@ function valuePreview(value) {
789
808
  function errorMessage(err) {
790
809
  return err instanceof Error ? err.message : String(err);
791
810
  }
811
+ function memberLocalesWithContent(members, languageKey) {
812
+ if (!Array.isArray(members)) return [];
813
+ const locales = [];
814
+ for (const member of members) {
815
+ if (!member || typeof member != "object") continue;
816
+ const locale = member[languageKey], value = member.value, hasContent = typeof value == "string" ? value.trim().length > 0 : Array.isArray(value) ? value.length > 0 : value != null;
817
+ typeof locale == "string" && hasContent && locales.push(locale);
818
+ }
819
+ return locales;
820
+ }
792
821
  function schemaFieldNames(schema, type) {
793
822
  return (schema.get(type)?.fields ?? []).map((f) => f?.name).filter((n) => typeof n == "string" && !n.startsWith("_"));
794
823
  }
@@ -796,7 +825,14 @@ function blockContentTypeFor(schema, type, fieldPath) {
796
825
  const fieldType = schema.get(type)?.fields?.find((f) => f.name === fieldPath)?.type;
797
826
  return locjson.isInternationalizedArrayType(fieldType) ? locjson.innerValueType(fieldType) : fieldType;
798
827
  }
799
- const ITEMS_QUERY = `*[_id == $id][0]{ sourceLanguage, items[]{ "doc": ${progress.itemDocDeref()} } }`;
828
+ const ITEMS_QUERY = `*[_id == $id][0]{
829
+ sourceLanguage,
830
+ items[]{
831
+ "docId": ${progress.ITEM_ID},
832
+ "docType": docType,
833
+ "doc": ${progress.itemDocDerefRespectingDraft()}
834
+ }
835
+ }`;
800
836
  async function prepareExport({
801
837
  client,
802
838
  schema,
@@ -804,6 +840,7 @@ async function prepareExport({
804
840
  targetLanguages,
805
841
  sourceLanguage,
806
842
  languages,
843
+ translatableTypes,
807
844
  exclude,
808
845
  fieldLanguageKey,
809
846
  deletions = [],
@@ -817,15 +854,22 @@ async function prepareExport({
817
854
  const detail = dups.map((d) => `${d.sanityIds.join(", ")} \u2192 ${d.smartcatLanguage}`).join("; ");
818
855
  throw new Error(`Multiple target languages map to the same Smartcat language: ${detail}`);
819
856
  }
820
- const deletedDocIds = new Set(deletions.map((d) => d.sourceDocId)), docs = (project?.items ?? []).map((i) => i?.doc).filter((d) => !!d).filter((d) => !deletedDocIds.has(d._id)), fieldsByType = /* @__PURE__ */ new Map(), fieldsFor = (type) => {
857
+ const deletedDocIds = new Set(deletions.map((d) => d.sourceDocId)), items = (project?.items ?? []).filter(
858
+ (item) => !(item.docId && deletedDocIds.has(item.docId))
859
+ ), unresolved = items.filter((item) => !item.doc), docs = items.map((item) => item.doc).filter((d) => !!d), fieldsByType = /* @__PURE__ */ new Map(), fieldsFor = (type) => {
821
860
  let fields = fieldsByType.get(type);
822
861
  return fields || (fields = locjson.getTranslatableFields(schema.get(type), { exclude }), fieldsByType.set(type, fields)), fields;
823
- };
824
- log({ level: "info", message: `Export \u2014 ${docs.length} document(s) \u2192 ${targetLanguages.join(", ") || "(no targets)"}` });
862
+ }, configuredLanguages = languages.map((l) => l.id).join(", ") || "(none)", configuredTypes = translatableTypes === void 0 ? "all types" : translatableTypes.join(", ") || "(none)";
863
+ log({ level: "info", message: `Config \u2014 source language: ${sourceLanguage}` }), log({ level: "info", indent: !0, message: `languages: ${configuredLanguages}` }), log({ level: "info", indent: !0, message: `translatable types: ${configuredTypes}` }), log({ level: "info", message: `Project \u2014 source: ${source} \u2192 targets: ${targetLanguages.join(", ") || "(no targets)"}` }), log({ level: "info", message: `Export \u2014 ${docs.length} document(s)` });
864
+ for (const item of unresolved)
865
+ log({
866
+ level: "error",
867
+ message: `${item.docId ?? "unknown document"} (${item.docType ?? "unknown type"}): no document found \u2014 it may be unpublished or deleted; skipped`
868
+ });
825
869
  const outbox = [];
826
- for (const doc of docs) {
827
- const name = resolveDocumentTitle(schema, doc);
828
- log({ level: "info", message: `${name || doc._id} \xB7 ${doc._type}` });
870
+ for (const rawDoc of docs) {
871
+ const isDraft = rawDoc._id.startsWith("drafts."), doc = isDraft ? { ...rawDoc, _id: publishedId(rawDoc._id) } : rawDoc, name = resolveDocumentTitle(schema, doc);
872
+ log({ level: "info", message: `${name || doc._id} \xB7 ${doc._type}${isDraft ? " (draft)" : ""}` });
829
873
  try {
830
874
  const fields = fieldsFor(doc._type), skippedBlockTypes = /* @__PURE__ */ new Map(), locjson$1 = locjson.serializeToLocjson(doc, fields, {
831
875
  sourceLanguage: source,
@@ -834,11 +878,18 @@ async function prepareExport({
834
878
  }), unitByKey = new Map(locjson$1.units.map((u) => [u.key, u]));
835
879
  for (const field of fields) {
836
880
  const unit = unitByKey.get(field.path), skippedTypes = skippedBlockTypes.get(field.path);
837
- log(unit ? { level: "success", indent: !0, message: `${field.title || field.path}: serialized (${field.kind})`, value: unit.source.join("") } : skippedTypes ? {
838
- level: "error",
839
- indent: !0,
840
- message: `${field.title || field.path}: not translated \u2014 contains block type(s) that can't be translated: ${skippedTypes.join(", ")}. The whole field was left untranslated to avoid losing this content.`
841
- } : { level: "skip", indent: !0, message: `${field.title || field.path}: skipped \u2014 no source content` });
881
+ if (unit)
882
+ log({ level: "success", indent: !0, message: `${field.title || field.path}: serialized (${field.kind})`, value: unit.source.join("") });
883
+ else if (skippedTypes)
884
+ log({
885
+ level: "error",
886
+ indent: !0,
887
+ message: `${field.title || field.path}: not translated \u2014 contains block type(s) that can't be translated: ${skippedTypes.join(", ")}. The whole field was left untranslated to avoid losing this content.`
888
+ });
889
+ else {
890
+ const otherLocales = field.localization === "field" ? memberLocalesWithContent(locjson.getAtPath(doc, field.path), fieldLanguageKey).filter((l) => l !== source) : [], reason = otherLocales.length ? `no '${source}' content (I see only: ${otherLocales.join(", ")})` : "no source content";
891
+ log({ level: "skip", indent: !0, message: `${field.title || field.path}: skipped \u2014 ${reason}` });
892
+ }
842
893
  }
843
894
  const selected = new Set(fields.map((f) => f.path)), excluded = new Set(exclude ?? []);
844
895
  for (const fieldName of schemaFieldNames(schema, doc._type))
@@ -855,7 +906,7 @@ async function prepareExport({
855
906
  _key: uuid.uuid(),
856
907
  sourceDocId: doc._id,
857
908
  title: name,
858
- filename: locjson.buildLocjsonFilename(doc._type, doc._id, name),
909
+ filename: locjson.buildLocjsonFilename(doc._type, doc._id, name, { draft: isDraft }),
859
910
  locjson: JSON.stringify(locjson$1)
860
911
  });
861
912
  } catch (err) {
@@ -1413,20 +1464,23 @@ const CodeArea = styled__default.default.textarea`
1413
1464
  tab-size: 2;
1414
1465
  `;
1415
1466
  function ViewLocjsonButton(props) {
1416
- const { docId, type, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField } = props, [open, setOpen] = react.useState(!1), [content, setContent] = react.useState(null), [filename, setFilename] = react.useState(null), [error, setError] = react.useState(null), build = react.useCallback(async () => {
1467
+ const { docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField } = props, [open, setOpen] = react.useState(!1), [content, setContent] = react.useState(null), [filename, setFilename] = react.useState(null), [error, setError] = react.useState(null), build = react.useCallback(async () => {
1417
1468
  setOpen(!0), setContent(null), setFilename(null), setError(null);
1418
1469
  try {
1419
- const doc = await client.fetch("*[_id == $id][0]", { id: docId });
1420
- if (!doc) throw new Error(`Document not found: ${docId}`);
1421
- const fields = locjson.getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] }), locjson$1 = locjson.serializeToLocjson(doc, fields, {
1470
+ const raw = await client.fetch(
1471
+ sourceIsPublished ? "*[_id == $id][0]" : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
1472
+ { id: docId }
1473
+ );
1474
+ if (!raw) throw new Error(`Document not found: ${docId}`);
1475
+ const isDraft = typeof raw._id == "string" && raw._id.startsWith("drafts."), doc = isDraft ? { ...raw, _id: docId } : raw, fields = locjson.getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] }), locjson$1 = locjson.serializeToLocjson(doc, fields, {
1422
1476
  sourceLanguage,
1423
1477
  fieldLanguageKey: fieldI18nLanguageField
1424
- }), smartcatName = locjson.buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc));
1478
+ }), smartcatName = locjson.buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc), { draft: isDraft });
1425
1479
  setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson$1, null, 2));
1426
1480
  } catch (err) {
1427
1481
  setError(err instanceof Error ? err.message : String(err));
1428
1482
  }
1429
- }, [client, schema, docId, type, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField]), download = react.useCallback(() => {
1483
+ }, [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField]), download = react.useCallback(() => {
1430
1484
  if (content == null || !filename) return;
1431
1485
  const url = URL.createObjectURL(new Blob([content], { type: "application/json" })), anchor = document.createElement("a");
1432
1486
  anchor.href = url, anchor.download = filename, anchor.click(), URL.revokeObjectURL(url);
@@ -1485,6 +1539,7 @@ function ProjectView({
1485
1539
  onBack,
1486
1540
  languages,
1487
1541
  sourceLanguage,
1542
+ translatableTypes,
1488
1543
  documentI18nLanguageField,
1489
1544
  fieldI18nLanguageField
1490
1545
  }) {
@@ -1630,6 +1685,7 @@ function ProjectView({
1630
1685
  targetLanguages: targets,
1631
1686
  sourceLanguage,
1632
1687
  languages,
1688
+ translatableTypes,
1633
1689
  exclude: [documentI18nLanguageField],
1634
1690
  fieldLanguageKey: fieldI18nLanguageField,
1635
1691
  deletions,
@@ -1841,13 +1897,23 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1841
1897
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1842
1898
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1843
1899
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1900
+ /* @__PURE__ */ jsxRuntime.jsx(
1901
+ ui.Badge,
1902
+ {
1903
+ tone: item.sourceIsPublished ? "positive" : "caution",
1904
+ paddingX: 2,
1905
+ title: item.sourceIsPublished ? "The published version is the translation source" : "The draft version is the translation source",
1906
+ children: item.sourceIsPublished ? "Published" : "Draft"
1907
+ }
1908
+ ),
1844
1909
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "medium", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: isMarked ? { textDecoration: "line-through" } : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(DocTitle, { doc: item.doc }) }) }),
1845
- item.doc?._type && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", children: item.doc._type }),
1910
+ item.doc?._type && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", paddingX: 2, children: item.doc._type }),
1846
1911
  item.doc?._type && modeByType.has(item.doc._type) && /* @__PURE__ */ jsxRuntime.jsx(
1847
1912
  ui.Badge,
1848
1913
  {
1849
1914
  tone: "default",
1850
1915
  fontSize: 0,
1916
+ paddingX: 2,
1851
1917
  title: modeByType.get(item.doc._type) === "field" ? "This item is translated in field mode; translations will be stored as language variants of each field" : "This item is translated in document mode; translations will be stored as linked documents",
1852
1918
  children: modeByType.get(item.doc._type) === "field" ? "fields" : "document"
1853
1919
  }
@@ -1858,6 +1924,7 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1858
1924
  {
1859
1925
  docId: item.doc._id,
1860
1926
  type: item.doc._type,
1927
+ sourceIsPublished: item.sourceIsPublished,
1861
1928
  client,
1862
1929
  schema,
1863
1930
  sourceLanguage,
@@ -1933,7 +2000,7 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1933
2000
  ] });
1934
2001
  }
1935
2002
  function createDashboardComponent(config) {
1936
- const { sourceLanguage, languages, documentI18nLanguageField, fieldI18nLanguageField } = config;
2003
+ const { sourceLanguage, languages, translatableTypes, documentI18nLanguageField, fieldI18nLanguageField } = config;
1937
2004
  return function() {
1938
2005
  const router$1 = router.useRouter(), projectId = router.useRouterState(
1939
2006
  react.useCallback((state) => state.projectId ?? null, [])
@@ -1945,6 +2012,7 @@ function createDashboardComponent(config) {
1945
2012
  onBack: goBack,
1946
2013
  languages,
1947
2014
  sourceLanguage,
2015
+ translatableTypes,
1948
2016
  documentI18nLanguageField,
1949
2017
  fieldI18nLanguageField
1950
2018
  }
@@ -2064,6 +2132,7 @@ function resolveConfig(config) {
2064
2132
  const translatableTypes = config.translatableTypes;
2065
2133
  return {
2066
2134
  isTranslatableType: translatableTypes ? (type) => translatableTypes.includes(type) : (type) => !PLUGIN_TYPES.has(type),
2135
+ translatableTypes,
2067
2136
  // `rootTypes` inherits the *explicitly-configured* translatableTypes (not the
2068
2137
  // resolved "everything") so an unconfigured Studio gets no crawl boundary
2069
2138
  // rather than every type being a root (which would stop the crawl at once).