@smartcat/sanity-plugin 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-cjs/index.cjs +18 -5
- package/dist/_chunks-cjs/index.cjs.map +1 -1
- package/dist/_chunks-cjs/index2.cjs +8 -6
- package/dist/_chunks-cjs/index2.cjs.map +1 -1
- package/dist/_chunks-es/index.js +18 -5
- package/dist/_chunks-es/index.js.map +1 -1
- package/dist/_chunks-es/index2.js +8 -6
- package/dist/_chunks-es/index2.js.map +1 -1
- package/dist/export.cjs +21 -4
- package/dist/export.cjs.map +1 -1
- package/dist/export.d.cts +2 -0
- package/dist/export.d.ts +2 -0
- package/dist/export.js +22 -5
- package/dist/export.js.map +1 -1
- package/dist/index.cjs +477 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +482 -129
- package/dist/index.js.map +1 -1
- package/dist/locjson.d.cts +15 -0
- package/dist/locjson.d.ts +15 -0
- package/dist/progress.d.cts +0 -19
- package/dist/progress.d.ts +0 -19
- package/dist/smartcat.cjs +14 -2
- package/dist/smartcat.cjs.map +1 -1
- package/dist/smartcat.d.cts +10 -1
- package/dist/smartcat.d.ts +10 -1
- package/dist/smartcat.js +14 -2
- package/dist/smartcat.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/AddToProjectAction.tsx +23 -2
- package/src/export/export.test.ts +42 -5
- package/src/export/index.ts +63 -8
- package/src/lib/locjson/filename.ts +7 -2
- package/src/lib/locjson/locjson.test.ts +64 -0
- package/src/lib/locjson/serialize.ts +38 -2
- package/src/progress/core.ts +44 -3
- package/src/progress/progress.test.ts +40 -0
- package/src/schema/translationProject.ts +19 -0
- package/src/smartcat/client.ts +15 -1
- package/src/tool/AddItemsSearch.tsx +462 -0
- package/src/tool/DocTitle.tsx +23 -0
- package/src/tool/ProjectView.tsx +29 -187
- package/src/tool/data.ts +7 -0
- package/src/tool/itemActions.tsx +216 -0
- package/src/tool/processing.test.ts +11 -1
- package/src/tool/processing.ts +116 -4
package/dist/locjson.d.cts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function buildLocjsonFilename(
|
|
|
14
14
|
title?: string,
|
|
15
15
|
options?: {
|
|
16
16
|
draft?: boolean;
|
|
17
|
+
language?: string;
|
|
17
18
|
},
|
|
18
19
|
): string;
|
|
19
20
|
|
|
@@ -208,6 +209,20 @@ export declare interface SerializeOptions {
|
|
|
208
209
|
title?: string;
|
|
209
210
|
types: string[];
|
|
210
211
|
}) => void;
|
|
212
|
+
/**
|
|
213
|
+
* When set, produces a **bilingual** file for a single target language: each
|
|
214
|
+
* unit's `target` is populated from the existing translation, in parallel with
|
|
215
|
+
* its `source`. Used by the "send existing translations" flow. The target value
|
|
216
|
+
* of a field is read the same way as the source, but for the target language:
|
|
217
|
+
* - field-level: the target-language member's `value` on the same document;
|
|
218
|
+
* - document-level: the same field path on `doc` (the target-locale sibling).
|
|
219
|
+
* `language` is the Sanity locale (matches internationalized-array members).
|
|
220
|
+
*/
|
|
221
|
+
target?: {
|
|
222
|
+
language: string;
|
|
223
|
+
/** The target-locale sibling document (document-level only). */
|
|
224
|
+
doc?: SerializableDocument;
|
|
225
|
+
};
|
|
211
226
|
}
|
|
212
227
|
|
|
213
228
|
/**
|
package/dist/locjson.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function buildLocjsonFilename(
|
|
|
14
14
|
title?: string,
|
|
15
15
|
options?: {
|
|
16
16
|
draft?: boolean;
|
|
17
|
+
language?: string;
|
|
17
18
|
},
|
|
18
19
|
): string;
|
|
19
20
|
|
|
@@ -208,6 +209,20 @@ export declare interface SerializeOptions {
|
|
|
208
209
|
title?: string;
|
|
209
210
|
types: string[];
|
|
210
211
|
}) => void;
|
|
212
|
+
/**
|
|
213
|
+
* When set, produces a **bilingual** file for a single target language: each
|
|
214
|
+
* unit's `target` is populated from the existing translation, in parallel with
|
|
215
|
+
* its `source`. Used by the "send existing translations" flow. The target value
|
|
216
|
+
* of a field is read the same way as the source, but for the target language:
|
|
217
|
+
* - field-level: the target-language member's `value` on the same document;
|
|
218
|
+
* - document-level: the same field path on `doc` (the target-locale sibling).
|
|
219
|
+
* `language` is the Sanity locale (matches internationalized-array members).
|
|
220
|
+
*/
|
|
221
|
+
target?: {
|
|
222
|
+
language: string;
|
|
223
|
+
/** The target-locale sibling document (document-level only). */
|
|
224
|
+
doc?: SerializableDocument;
|
|
225
|
+
};
|
|
211
226
|
}
|
|
212
227
|
|
|
213
228
|
/**
|
package/dist/progress.d.cts
CHANGED
|
@@ -203,25 +203,6 @@ declare interface SmartcatWorkflowStage {
|
|
|
203
203
|
totalWordsCount?: number;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
/**
|
|
207
|
-
* Extracts the 8-char source-id prefix Smartcat preserves in a document's
|
|
208
|
-
* filename (built as `<title>-<idPrefix>.locjson`, or `<title>-<idPrefix>-draft.locjson`
|
|
209
|
-
* when the content came from the document's draft). Used to correlate a Smartcat
|
|
210
|
-
* document back to a Sanity source document.
|
|
211
|
-
*
|
|
212
|
-
* A trailing `-draft` marker is stripped first, so a draft-sourced file resolves
|
|
213
|
-
* to the same id as its published counterpart. The prefix is then always the
|
|
214
|
-
* final 8 characters before the (marker and) extension, so take the tail rather
|
|
215
|
-
* than splitting on a dash — the title or the id prefix itself can
|
|
216
|
-
* contain dashes (e.g. a custom id like `demo-pdpA` → prefix `demo-pdp`), which
|
|
217
|
-
* would make a last-dash split return the wrong fragment.
|
|
218
|
-
*
|
|
219
|
-
* Only a literal `.locjson` extension is stripped (Smartcat's API often returns
|
|
220
|
-
* the name without it). A generic strip-after-last-dot would eat the tail of any
|
|
221
|
-
* DOTTED title — e.g. micro-copy keys like `aiAssistantModal.resetChat-11AKrV8G`
|
|
222
|
-
* — leaving the wrong 8 chars, so those documents silently never correlate,
|
|
223
|
-
* never import, and re-syncs duplicate them instead of updating.
|
|
224
|
-
*/
|
|
225
206
|
export declare function sourceIdPrefix(doc: SmartcatDocument): string;
|
|
226
207
|
|
|
227
208
|
/**
|
package/dist/progress.d.ts
CHANGED
|
@@ -203,25 +203,6 @@ declare interface SmartcatWorkflowStage {
|
|
|
203
203
|
totalWordsCount?: number;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
/**
|
|
207
|
-
* Extracts the 8-char source-id prefix Smartcat preserves in a document's
|
|
208
|
-
* filename (built as `<title>-<idPrefix>.locjson`, or `<title>-<idPrefix>-draft.locjson`
|
|
209
|
-
* when the content came from the document's draft). Used to correlate a Smartcat
|
|
210
|
-
* document back to a Sanity source document.
|
|
211
|
-
*
|
|
212
|
-
* A trailing `-draft` marker is stripped first, so a draft-sourced file resolves
|
|
213
|
-
* to the same id as its published counterpart. The prefix is then always the
|
|
214
|
-
* final 8 characters before the (marker and) extension, so take the tail rather
|
|
215
|
-
* than splitting on a dash — the title or the id prefix itself can
|
|
216
|
-
* contain dashes (e.g. a custom id like `demo-pdpA` → prefix `demo-pdp`), which
|
|
217
|
-
* would make a last-dash split return the wrong fragment.
|
|
218
|
-
*
|
|
219
|
-
* Only a literal `.locjson` extension is stripped (Smartcat's API often returns
|
|
220
|
-
* the name without it). A generic strip-after-last-dot would eat the tail of any
|
|
221
|
-
* DOTTED title — e.g. micro-copy keys like `aiAssistantModal.resetChat-11AKrV8G`
|
|
222
|
-
* — leaving the wrong 8 chars, so those documents silently never correlate,
|
|
223
|
-
* never import, and re-syncs duplicate them instead of updating.
|
|
224
|
-
*/
|
|
225
206
|
export declare function sourceIdPrefix(doc: SmartcatDocument): string;
|
|
226
207
|
|
|
227
208
|
/**
|
package/dist/smartcat.cjs
CHANGED
|
@@ -116,9 +116,21 @@ class SmartcatClient {
|
|
|
116
116
|
async getTemplates() {
|
|
117
117
|
return this.request("GET", `${API_BASE}/template`);
|
|
118
118
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Uploads one source file into a project. `filename` may include a folder path.
|
|
121
|
+
*
|
|
122
|
+
* When `targetLanguage` is given (a Smartcat language code), the file is
|
|
123
|
+
* restricted to that single target language via the `documentModel` settings
|
|
124
|
+
* part — used by the "send existing translations" flow, which uploads one
|
|
125
|
+
* bilingual LocJSON per language. Omitted ⇒ the document targets every project
|
|
126
|
+
* language (today's source-only default).
|
|
127
|
+
*/
|
|
128
|
+
async uploadDocument(projectId, filename, content, targetLanguage) {
|
|
121
129
|
const form = new FormData();
|
|
130
|
+
if (targetLanguage) {
|
|
131
|
+
const model = [{ targetLanguages: [targetLanguage] }];
|
|
132
|
+
form.append("documentModel", new Blob([JSON.stringify(model)], { type: "application/json" }), "documentModel");
|
|
133
|
+
}
|
|
122
134
|
form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
|
|
123
135
|
const result = await this.request(
|
|
124
136
|
"POST",
|
package/dist/smartcat.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smartcat.cjs","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /** Uploads one source file into a project. `filename` may include a folder path. */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"smartcat.cjs","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /**\n * Uploads one source file into a project. `filename` may include a folder path.\n *\n * When `targetLanguage` is given (a Smartcat language code), the file is\n * restricted to that single target language via the `documentModel` settings\n * part — used by the \"send existing translations\" flow, which uploads one\n * bilingual LocJSON per language. Omitted ⇒ the document targets every project\n * language (today's source-only default).\n */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n targetLanguage?: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n if (targetLanguage) {\n // `documentModel` is a JSON array (one entry per file); a bare object 400s.\n const model = [{targetLanguages: [targetLanguage]}]\n form.append('documentModel', new Blob([JSON.stringify(model)], {type: 'application/json'}), 'documentModel')\n }\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
|
package/dist/smartcat.d.cts
CHANGED
|
@@ -57,11 +57,20 @@ export declare class SmartcatClient {
|
|
|
57
57
|
createProject(input: CreateProjectInput): Promise<SmartcatProject>;
|
|
58
58
|
/** Lists the workspace's project templates. */
|
|
59
59
|
getTemplates(): Promise<SmartcatTemplatesResponse>;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Uploads one source file into a project. `filename` may include a folder path.
|
|
62
|
+
*
|
|
63
|
+
* When `targetLanguage` is given (a Smartcat language code), the file is
|
|
64
|
+
* restricted to that single target language via the `documentModel` settings
|
|
65
|
+
* part — used by the "send existing translations" flow, which uploads one
|
|
66
|
+
* bilingual LocJSON per language. Omitted ⇒ the document targets every project
|
|
67
|
+
* language (today's source-only default).
|
|
68
|
+
*/
|
|
61
69
|
uploadDocument(
|
|
62
70
|
projectId: string,
|
|
63
71
|
filename: string,
|
|
64
72
|
content: string,
|
|
73
|
+
targetLanguage?: string,
|
|
65
74
|
): Promise<SmartcatDocument[]>;
|
|
66
75
|
/**
|
|
67
76
|
* Uploads many source files into a project in a single request — the
|
package/dist/smartcat.d.ts
CHANGED
|
@@ -57,11 +57,20 @@ export declare class SmartcatClient {
|
|
|
57
57
|
createProject(input: CreateProjectInput): Promise<SmartcatProject>;
|
|
58
58
|
/** Lists the workspace's project templates. */
|
|
59
59
|
getTemplates(): Promise<SmartcatTemplatesResponse>;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Uploads one source file into a project. `filename` may include a folder path.
|
|
62
|
+
*
|
|
63
|
+
* When `targetLanguage` is given (a Smartcat language code), the file is
|
|
64
|
+
* restricted to that single target language via the `documentModel` settings
|
|
65
|
+
* part — used by the "send existing translations" flow, which uploads one
|
|
66
|
+
* bilingual LocJSON per language. Omitted ⇒ the document targets every project
|
|
67
|
+
* language (today's source-only default).
|
|
68
|
+
*/
|
|
61
69
|
uploadDocument(
|
|
62
70
|
projectId: string,
|
|
63
71
|
filename: string,
|
|
64
72
|
content: string,
|
|
73
|
+
targetLanguage?: string,
|
|
65
74
|
): Promise<SmartcatDocument[]>;
|
|
66
75
|
/**
|
|
67
76
|
* Uploads many source files into a project in a single request — the
|
package/dist/smartcat.js
CHANGED
|
@@ -114,9 +114,21 @@ class SmartcatClient {
|
|
|
114
114
|
async getTemplates() {
|
|
115
115
|
return this.request("GET", `${API_BASE}/template`);
|
|
116
116
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Uploads one source file into a project. `filename` may include a folder path.
|
|
119
|
+
*
|
|
120
|
+
* When `targetLanguage` is given (a Smartcat language code), the file is
|
|
121
|
+
* restricted to that single target language via the `documentModel` settings
|
|
122
|
+
* part — used by the "send existing translations" flow, which uploads one
|
|
123
|
+
* bilingual LocJSON per language. Omitted ⇒ the document targets every project
|
|
124
|
+
* language (today's source-only default).
|
|
125
|
+
*/
|
|
126
|
+
async uploadDocument(projectId, filename, content, targetLanguage) {
|
|
119
127
|
const form = new FormData();
|
|
128
|
+
if (targetLanguage) {
|
|
129
|
+
const model = [{ targetLanguages: [targetLanguage] }];
|
|
130
|
+
form.append("documentModel", new Blob([JSON.stringify(model)], { type: "application/json" }), "documentModel");
|
|
131
|
+
}
|
|
120
132
|
form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
|
|
121
133
|
const result = await this.request(
|
|
122
134
|
"POST",
|
package/dist/smartcat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smartcat.js","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /** Uploads one source file into a project. `filename` may include a folder path. */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"smartcat.js","sources":["../src/lib/zip.ts","../src/smartcat/client.ts"],"sourcesContent":["/**\n * Minimal ZIP reader for Smartcat batch-export archives.\n *\n * Dependency-free by design (like the rest of the Function-side code): built on\n * `DataView` + the global `DecompressionStream` (Node 18+ / browsers). Supports\n * the two compression methods Smartcat archives use — stored (0) and deflate (8).\n *\n * ZIP64 is REQUIRED, not an edge case: Smartcat writes export archives with\n * SharpZipLib's `UseZip64 = On` (forced), so central-directory size/offset\n * fields hold `0xFFFFFFFF` placeholders with the real 64-bit values in each\n * entry's ZIP64 extra field — even for tiny archives. Large archives\n * additionally use the ZIP64 end-of-central-directory record.\n */\n\nexport interface ZipEntry {\n /** Entry path inside the archive (folders separated by `/`). */\n name: string\n data: Uint8Array\n}\n\nconst LOCAL_HEADER_SIG = 0x04034b50\nconst CENTRAL_HEADER_SIG = 0x02014b50\nconst EOCD_SIG = 0x06054b50\nconst ZIP64_EOCD_SIG = 0x06064b50\nconst ZIP64_EOCD_LOCATOR_SIG = 0x07064b50\n/** EOCD record is 22 bytes + an optional comment of up to 65535 bytes. */\nconst EOCD_MIN_SIZE = 22\n/** ZIP64 EOCD locator is a fixed 20 bytes, sitting right before the classic EOCD. */\nconst ZIP64_LOCATOR_SIZE = 20\n/** Placeholder meaning \"the real value is in the ZIP64 extra field / record\". */\nconst U32_MAX = 0xffffffff\nconst U16_MAX = 0xffff\n/** Extra-field header id of the ZIP64 extended information block. */\nconst ZIP64_EXTRA_ID = 0x0001\n\n/** Whether the payload looks like a ZIP archive (`PK\\x03\\x04` magic). */\nexport function isZip(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 4 &&\n bytes[0] === 0x50 &&\n bytes[1] === 0x4b &&\n bytes[2] === 0x03 &&\n bytes[3] === 0x04\n )\n}\n\n/** Extracts all file entries (directories are skipped). */\nexport async function unzip(bytes: Uint8Array): Promise<ZipEntry[]> {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const eocd = findEndOfCentralDirectory(view)\n const entries: ZipEntry[] = []\n\n let offset = eocd.centralDirectoryOffset\n for (let i = 0; i < eocd.entryCount; i++) {\n if (view.getUint32(offset, true) !== CENTRAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`)\n }\n const method = view.getUint16(offset + 10, true)\n let compressedSize: number = view.getUint32(offset + 20, true)\n let uncompressedSize: number = view.getUint32(offset + 24, true)\n const nameLength = view.getUint16(offset + 28, true)\n const extraLength = view.getUint16(offset + 30, true)\n const commentLength = view.getUint16(offset + 32, true)\n let localHeaderOffset: number = view.getUint32(offset + 42, true)\n const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength))\n\n // ZIP64: any field at its 32-bit max is a placeholder; the real value lives\n // in the entry's ZIP64 extra block, in a fixed order, holding ONLY the\n // fields that overflowed (Smartcat's forced-ZIP64 writer always defers the\n // sizes this way, even for small files).\n if (compressedSize === U32_MAX || uncompressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n const extraStart = offset + 46 + nameLength\n const extraEnd = extraStart + extraLength\n let p = extraStart\n while (p + 4 <= extraEnd) {\n const id = view.getUint16(p, true)\n const size = view.getUint16(p + 2, true)\n if (id === ZIP64_EXTRA_ID) {\n let q = p + 4\n if (uncompressedSize === U32_MAX) {\n uncompressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (compressedSize === U32_MAX) {\n compressedSize = Number(view.getBigUint64(q, true))\n q += 8\n }\n if (localHeaderOffset === U32_MAX) {\n localHeaderOffset = Number(view.getBigUint64(q, true))\n }\n break\n }\n p += 4 + size\n }\n if (compressedSize === U32_MAX || localHeaderOffset === U32_MAX) {\n throw new Error(`Invalid ZIP: \"${name}\" needs ZIP64 sizes but has no ZIP64 extra field`)\n }\n }\n offset += 46 + nameLength + extraLength + commentLength\n\n if (name.endsWith('/')) continue // directory entry\n\n // The local header repeats name/extra with its own lengths; the data\n // follows it. Sizes come from the central directory (authoritative even\n // when the local header deferred them to a data descriptor).\n if (view.getUint32(localHeaderOffset, true) !== LOCAL_HEADER_SIG) {\n throw new Error(`Invalid ZIP: bad local header for \"${name}\"`)\n }\n const localNameLength = view.getUint16(localHeaderOffset + 26, true)\n const localExtraLength = view.getUint16(localHeaderOffset + 28, true)\n const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength\n const compressed = bytes.subarray(dataStart, dataStart + compressedSize)\n\n entries.push({name, data: await decompress(method, compressed, name)})\n }\n return entries\n}\n\n/** Convenience: unzip and decode every file entry as UTF-8 text. */\nexport async function unzipText(bytes: Uint8Array): Promise<{name: string; content: string}[]> {\n const entries = await unzip(bytes)\n const decoder = new TextDecoder()\n return entries.map((e) => ({name: e.name, content: decoder.decode(e.data)}))\n}\n\nfunction findEndOfCentralDirectory(view: DataView): {\n entryCount: number\n centralDirectoryOffset: number\n} {\n const lowest = Math.max(0, view.byteLength - EOCD_MIN_SIZE - 0xffff)\n for (let i = view.byteLength - EOCD_MIN_SIZE; i >= lowest; i--) {\n if (view.getUint32(i, true) !== EOCD_SIG) continue\n\n let entryCount: number = view.getUint16(i + 10, true)\n let centralDirectoryOffset: number = view.getUint32(i + 16, true)\n\n // ZIP64: placeholder values defer to the ZIP64 EOCD record, found via the\n // fixed-size locator that sits immediately before the classic EOCD.\n if (entryCount === U16_MAX || centralDirectoryOffset === U32_MAX) {\n const locator = i - ZIP64_LOCATOR_SIZE\n if (locator < 0 || view.getUint32(locator, true) !== ZIP64_EOCD_LOCATOR_SIG) {\n throw new Error('Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator')\n }\n const zip64Eocd = Number(view.getBigUint64(locator + 8, true))\n if (view.getUint32(zip64Eocd, true) !== ZIP64_EOCD_SIG) {\n throw new Error('Invalid ZIP: bad ZIP64 end-of-central-directory record')\n }\n entryCount = Number(view.getBigUint64(zip64Eocd + 32, true))\n centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, true))\n }\n return {entryCount, centralDirectoryOffset}\n }\n throw new Error('Invalid ZIP: end-of-central-directory record not found')\n}\n\nasync function decompress(method: number, data: Uint8Array, name: string): Promise<Uint8Array> {\n if (method === 0) return data // stored\n if (method === 8) {\n // Copy into a fresh ArrayBuffer so the Blob sees exactly this entry's bytes.\n const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream('deflate-raw'))\n return new Uint8Array(await new Response(stream).arrayBuffer())\n }\n throw new Error(`Unsupported ZIP compression method ${method} for \"${name}\"`)\n}\n","import {isZip, unzipText} from '../lib/zip'\nimport type {\n CreateProjectInput,\n RequestLogger,\n SmartcatClientOptions,\n SmartcatDocument,\n SmartcatProject,\n SmartcatTemplatesResponse,\n} from './types'\n\nconst API_BASE = '/api/integration/v1'\n\n/** One file from a (batch) export: archive entry name + LocJSON text. */\nexport interface ExportedFile {\n /** Path inside the export archive; empty for a single-file response. */\n filename: string\n content: string\n}\n\n/** Error thrown when the Smartcat API returns a non-2xx response. */\nexport class SmartcatError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string,\n ) {\n super(`Smartcat API error ${status}: ${body}`)\n this.name = 'SmartcatError'\n }\n}\n\n/** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */\nexport class SmartcatTimeoutError extends Error {\n constructor(\n public readonly method: string,\n public readonly path: string,\n public readonly timeoutMs: number,\n ) {\n super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`)\n this.name = 'SmartcatTimeoutError'\n }\n}\n\n/** Human-readable duration, e.g. 570000 → \"9m30s\", 30000 → \"30s\". */\nfunction formatDuration(ms: number): string {\n const totalSeconds = Math.round(ms / 1000)\n const minutes = Math.floor(totalSeconds / 60)\n const seconds = totalSeconds % 60\n if (!minutes) return `${seconds}s`\n return seconds ? `${minutes}m${seconds}s` : `${minutes}m`\n}\n\n/** True for an abort/timeout rejection from `fetch` (DOMException-style). */\nfunction isAbortError(err: unknown): boolean {\n const name = (err as {name?: string} | undefined)?.name\n return name === 'TimeoutError' || name === 'AbortError'\n}\n\n/**\n * Minimal client for the Smartcat integration API.\n *\n * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,\n * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,\n * so it is portable. Secrets are required, so this only runs server-side.\n */\nexport class SmartcatClient {\n private readonly server: string\n private readonly authHeader: string\n private readonly fetchImpl: typeof fetch\n private readonly requestTimeoutMs?: number\n private requestLogger?: RequestLogger\n\n constructor(options: SmartcatClientOptions) {\n this.server = options.server.replace(/^https?:\\/\\//, '').replace(/\\/$/, '')\n this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`\n this.fetchImpl = options.fetchImpl ?? fetch\n this.requestTimeoutMs = options.requestTimeoutMs\n }\n\n /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */\n private timeoutSignal(): AbortSignal | undefined {\n return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : undefined\n }\n\n /** Attach a sink that receives every HTTP call (method, path, status, body). */\n setRequestLogger(logger: RequestLogger): void {\n this.requestLogger = logger\n }\n\n /** Creates a project and returns it (including its generated `id` GUID). */\n async createProject(input: CreateProjectInput): Promise<SmartcatProject> {\n const {templateId, ...rest} = input\n // A template fully owns its workflow, MT/TM and pretranslation config, so we\n // send only the project identity + template id. The standard flow sets the\n // MT/TM/pretranslation flags here; `workflowStages` is supplied by the caller\n // (it's what distinguishes AI+human from AI-only).\n const body = templateId\n ? {assignToVendor: false, externalTag: 'smartcat-sanity-plugin', templateId, ...rest}\n : {\n assignToVendor: false,\n externalTag: 'smartcat-sanity-plugin',\n useMT: true,\n useTranslationMemory: true,\n pretranslate: true,\n autoPropagateRepetitions: true,\n minTranslationAssuranceLevel: 'regular',\n ...rest,\n }\n const form = new FormData()\n form.append('body', JSON.stringify(body))\n return this.request('POST', `${API_BASE}/project/create`, form)\n }\n\n /** Lists the workspace's project templates. */\n async getTemplates(): Promise<SmartcatTemplatesResponse> {\n return this.request('GET', `${API_BASE}/template`)\n }\n\n /**\n * Uploads one source file into a project. `filename` may include a folder path.\n *\n * When `targetLanguage` is given (a Smartcat language code), the file is\n * restricted to that single target language via the `documentModel` settings\n * part — used by the \"send existing translations\" flow, which uploads one\n * bilingual LocJSON per language. Omitted ⇒ the document targets every project\n * language (today's source-only default).\n */\n async uploadDocument(\n projectId: string,\n filename: string,\n content: string,\n targetLanguage?: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n if (targetLanguage) {\n // `documentModel` is a JSON array (one entry per file); a bare object 400s.\n const model = [{targetLanguages: [targetLanguage]}]\n form.append('documentModel', new Blob([JSON.stringify(model)], {type: 'application/json'}), 'documentModel')\n }\n // Use a generic content type: declaring application/json makes Smartcat try\n // to map the file to an API model instead of importing it as a document.\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Uploads many source files into a project in a single request — the\n * multi-file sibling of {@link uploadDocument}. Mirrors the reference\n * `AddDocuments` multipart contract: one `model` part carrying a JSON array\n * with one (default-settings) entry per file, plus one `file` part per file.\n * Returns the created documents (files × target languages).\n */\n async uploadDocuments(\n projectId: string,\n files: {filename: string; content: string}[],\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n // One settings object per file, positional with the file parts (empty = default\n // disassembly, as the single-file path). Correlation never relies on this order.\n form.append('model', new Blob([JSON.stringify(files.map(() => ({})))], {type: 'application/json'}), 'model')\n for (const f of files) {\n form.append('file', new Blob([f.content], {type: 'application/octet-stream'}), f.filename)\n }\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'POST',\n `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /**\n * Updates an existing document's source content by re-uploading the file.\n * Smartcat re-imports and merges, preserving translations of unchanged\n * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).\n */\n async updateDocument(\n documentId: string,\n filename: string,\n content: string,\n ): Promise<SmartcatDocument[]> {\n const form = new FormData()\n form.append('file', new Blob([content], {type: 'application/octet-stream'}), filename)\n const result = await this.request<SmartcatDocument[] | SmartcatDocument>(\n 'PUT',\n `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,\n form,\n )\n return Array.isArray(result) ? result : result ? [result] : []\n }\n\n /** Fetches a project (including its documents and per-stage progress). */\n async getProject(projectId: string): Promise<SmartcatProject> {\n return this.request('GET', `${API_BASE}/project/${encodeURIComponent(projectId)}`)\n }\n\n /**\n * Deletes a document from its project. The id is a target document id\n * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every\n * target language — in a single call.\n */\n async deleteDocument(documentId: string): Promise<void> {\n await this.request('DELETE', `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`)\n }\n\n /** Builds the Smartcat web URL for a project's overview page. */\n projectUrl(projectId: string): string {\n return `https://${this.server}/projects/${projectId}/general`\n }\n\n /**\n * Exports a document's target (translated) content and returns it as text.\n * Two-step: request an export task, then poll until ready.\n *\n * `mode` defaults to `confirmed` — only segments confirmed at any workflow\n * stage are returned (unconfirmed/untranslated segments fall back to source),\n * so we never import unvetted machine-translation drafts. Use `current` to pull\n * the live state including unconfirmed edits.\n */\n async exportDocument(\n documentId: string,\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<string> {\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 20, sleep = defaultSleep} = options\n\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, text} = await this.requestRaw('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) return text\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, text)\n }\n throw new Error(`Smartcat export for ${documentId} timed out`)\n }\n\n /**\n * Exports many documents' target content in **one** export task. Smartcat\n * fans the work out server-side and (for more than one document) returns a\n * ZIP archive of the individual files, so the per-document request+poll\n * round-trip is paid once per batch instead of once per document.\n *\n * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry\n * name, so callers should correlate on the parsed content.\n */\n async exportDocumentsBatch(\n documentIds: string[],\n options: {\n mode?: 'confirmed' | 'current'\n pollIntervalMs?: number\n maxAttempts?: number\n sleep?: (ms: number) => Promise<void>\n } = {},\n ): Promise<ExportedFile[]> {\n if (documentIds.length === 0) return []\n // Bigger batches take longer to assemble server-side; poll longer, not faster.\n const {mode = 'confirmed', pollIntervalMs = 3000, maxAttempts = 60, sleep = defaultSleep} = options\n\n const query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join('&')\n const task = await this.request<{id: string}>(\n 'POST',\n `${API_BASE}/document/export?${query}&mode=${mode}&type=target`,\n )\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const {status, bytes} = await this.requestBytes('GET', `${API_BASE}/document/export/${task.id}`)\n if (status === 200) {\n // A single-document task returns the bare file; multi-document tasks\n // return a ZIP. Decide by content, not by count — Smartcat may drop\n // documents it can't export from the archive.\n if (isZip(bytes)) {\n return (await unzipText(bytes)).map(({name, content}) => ({filename: name, content}))\n }\n return [{filename: '', content: new TextDecoder().decode(bytes)}]\n }\n if (status === 204) {\n await sleep(pollIntervalMs)\n continue\n }\n throw new SmartcatError(status, new TextDecoder().decode(bytes))\n }\n throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`)\n }\n\n private async request<T = unknown>(method: string, path: string, body?: BodyInit): Promise<T> {\n const {status, text} = await this.requestRaw(method, path, body)\n if (status < 200 || status >= 300) throw new SmartcatError(status, text)\n return (text ? JSON.parse(text) : null) as T\n }\n\n /**\n * Normalizes a `fetch` rejection. A timeout abort becomes a\n * {@link SmartcatTimeoutError} — traced through the request logger first (with\n * a synthetic status 0) so it surfaces in the same log as HTTP failures — while\n * anything else is rethrown unchanged.\n */\n private onFetchError(method: string, path: string, err: unknown): unknown {\n if (isAbortError(err) && this.requestTimeoutMs) {\n const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs)\n this.requestLogger?.({method, path, status: 0, body: timeout.message})\n return timeout\n }\n return err\n }\n\n private async requestRaw(\n method: string,\n path: string,\n body?: BodyInit,\n ): Promise<{status: number; text: string}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n body,\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const text = await res.text()\n this.requestLogger?.({method, path, status: res.status, body: text})\n return {status: res.status, text}\n }\n\n /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */\n private async requestBytes(method: string, path: string): Promise<{status: number; bytes: Uint8Array}> {\n let res: Response\n try {\n res = await this.fetchImpl(`https://${this.server}${path}`, {\n method,\n headers: {Authorization: this.authHeader},\n signal: this.timeoutSignal(),\n })\n } catch (err) {\n throw this.onFetchError(method, path, err)\n }\n const bytes = new Uint8Array(await res.arrayBuffer())\n // Log a size placeholder for successful binary payloads; error bodies are\n // text (Smartcat's error detail) and stay readable in the trace.\n const logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes)\n this.requestLogger?.({method, path, status: res.status, body: logBody})\n return {status: res.status, bytes}\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -56,6 +56,10 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
|
|
|
56
56
|
const [newName, setNewName] = useState('')
|
|
57
57
|
const [busy, setBusy] = useState(false)
|
|
58
58
|
const [includeLinked, setIncludeLinked] = useState(true)
|
|
59
|
+
// Whether to also send the item's existing translations (one bilingual file
|
|
60
|
+
// per target language) rather than only its source text. Off by default:
|
|
61
|
+
// imported translations land confirmed at every stage, skipping human review.
|
|
62
|
+
const [sendExisting, setSendExisting] = useState(false)
|
|
59
63
|
const {templates, loaded, requestRefresh} = useTemplates()
|
|
60
64
|
const [workflow, setWorkflow] = useWorkflowSelection(open, templates, loaded)
|
|
61
65
|
|
|
@@ -103,6 +107,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
|
|
|
103
107
|
docId,
|
|
104
108
|
docType,
|
|
105
109
|
sourceIsPublished,
|
|
110
|
+
sendExistingTranslations: sendExisting,
|
|
106
111
|
})
|
|
107
112
|
|
|
108
113
|
// The root document plus, optionally, everything it links to — de-duped by id.
|
|
@@ -159,7 +164,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
|
|
|
159
164
|
} finally {
|
|
160
165
|
setBusy(false)
|
|
161
166
|
}
|
|
162
|
-
}, [selection, newName, workflow, publishedId, doc, includeLinked, sourceIsPublished, schema, client, projects, close])
|
|
167
|
+
}, [selection, newName, workflow, publishedId, doc, includeLinked, sendExisting, sourceIsPublished, schema, client, projects, close])
|
|
163
168
|
|
|
164
169
|
const content = useMemo(() => {
|
|
165
170
|
if (projects === null) {
|
|
@@ -239,6 +244,22 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
|
|
|
239
244
|
</Text>
|
|
240
245
|
</Stack>
|
|
241
246
|
</Flex>
|
|
247
|
+
<Flex align="flex-start" gap={3}>
|
|
248
|
+
<Switch
|
|
249
|
+
checked={sendExisting}
|
|
250
|
+
onChange={(e) => setSendExisting(e.currentTarget.checked)}
|
|
251
|
+
disabled={busy}
|
|
252
|
+
/>
|
|
253
|
+
<Stack space={2}>
|
|
254
|
+
<Text size={1} weight="medium">
|
|
255
|
+
Send existing translations
|
|
256
|
+
</Text>
|
|
257
|
+
<Text size={1} muted>
|
|
258
|
+
Upload current translations alongside the source, so Smartcat starts pre-filled.
|
|
259
|
+
Imported translations are marked confirmed at every stage — they skip human review.
|
|
260
|
+
</Text>
|
|
261
|
+
</Stack>
|
|
262
|
+
</Flex>
|
|
242
263
|
</Stack>
|
|
243
264
|
</Card>
|
|
244
265
|
|
|
@@ -255,7 +276,7 @@ export function createAddToProjectAction(config: ResolvedSmartcatConfig): Docume
|
|
|
255
276
|
</Flex>
|
|
256
277
|
</Stack>
|
|
257
278
|
)
|
|
258
|
-
}, [projects, selection, newName, workflow, templates, busy, includeLinked, docTitle, sourceIsPublished, close, handleConfirm])
|
|
279
|
+
}, [projects, selection, newName, workflow, templates, busy, includeLinked, sendExisting, docTitle, sourceIsPublished, close, handleConfirm])
|
|
259
280
|
|
|
260
281
|
return {
|
|
261
282
|
label: 'Add to translation project',
|