@smartcat/sanity-plugin 1.0.1 → 1.2.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 +10 -4
- package/dist/_chunks-cjs/index.cjs.map +1 -1
- package/dist/_chunks-cjs/index2.cjs +34 -7
- package/dist/_chunks-cjs/index2.cjs.map +1 -1
- package/dist/_chunks-es/index.js +10 -4
- package/dist/_chunks-es/index.js.map +1 -1
- package/dist/_chunks-es/index2.js +35 -8
- package/dist/_chunks-es/index2.js.map +1 -1
- package/dist/import.cjs +1 -1
- package/dist/import.cjs.map +1 -1
- package/dist/import.js +2 -2
- package/dist/import.js.map +1 -1
- package/dist/index.cjs +96 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +100 -31
- package/dist/index.js.map +1 -1
- package/dist/locjson.d.cts +5 -1
- package/dist/locjson.d.ts +5 -1
- package/dist/progress.d.cts +24 -9
- package/dist/progress.d.ts +24 -9
- package/package.json +15 -1
- package/src/actions/AddToProjectAction.tsx +21 -5
- package/src/import/index.ts +3 -3
- package/src/lib/locjson/filename.ts +10 -3
- package/src/lib/locjson/locjson.test.ts +78 -8
- package/src/lib/locjson/portableText.ts +71 -12
- package/src/lib/projectItems.ts +14 -0
- package/src/lib/resolveConfig.ts +7 -0
- package/src/package-manifest.test.ts +15 -0
- package/src/progress/core.ts +37 -15
- package/src/progress/progress.test.ts +22 -0
- package/src/schema/translationProject.ts +4 -0
- package/src/tool/Dashboard.tsx +3 -1
- package/src/tool/ProjectView.tsx +35 -6
- package/src/tool/data.ts +7 -0
- package/src/tool/processing.test.ts +79 -4
- package/src/tool/processing.ts +84 -11
package/dist/import.js.map
CHANGED
|
@@ -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]{
|
|
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)),
|
|
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: `
|
|
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
|
|
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
|
-
|
|
838
|
-
level: "
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
|
1420
|
-
|
|
1421
|
-
|
|
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).
|