@smartcat/sanity-plugin 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +294 -0
  2. package/dist/_chunks-cjs/constants.cjs +13 -0
  3. package/dist/_chunks-cjs/constants.cjs.map +1 -0
  4. package/dist/_chunks-cjs/index.cjs +174 -0
  5. package/dist/_chunks-cjs/index.cjs.map +1 -0
  6. package/dist/_chunks-cjs/index2.cjs +338 -0
  7. package/dist/_chunks-cjs/index2.cjs.map +1 -0
  8. package/dist/_chunks-cjs/workflow.cjs +16 -0
  9. package/dist/_chunks-cjs/workflow.cjs.map +1 -0
  10. package/dist/_chunks-es/constants.js +14 -0
  11. package/dist/_chunks-es/constants.js.map +1 -0
  12. package/dist/_chunks-es/index.js +175 -0
  13. package/dist/_chunks-es/index.js.map +1 -0
  14. package/dist/_chunks-es/index2.js +340 -0
  15. package/dist/_chunks-es/index2.js.map +1 -0
  16. package/dist/_chunks-es/workflow.js +17 -0
  17. package/dist/_chunks-es/workflow.js.map +1 -0
  18. package/dist/export.cjs +160 -0
  19. package/dist/export.cjs.map +1 -0
  20. package/dist/export.d.cts +162 -0
  21. package/dist/export.d.ts +162 -0
  22. package/dist/export.js +162 -0
  23. package/dist/export.js.map +1 -0
  24. package/dist/import.cjs +169 -0
  25. package/dist/import.cjs.map +1 -0
  26. package/dist/import.d.cts +155 -0
  27. package/dist/import.d.ts +155 -0
  28. package/dist/import.js +169 -0
  29. package/dist/import.js.map +1 -0
  30. package/dist/index.cjs +2172 -0
  31. package/dist/index.cjs.map +1 -0
  32. package/dist/index.d.cts +122 -0
  33. package/dist/index.d.ts +122 -0
  34. package/dist/index.js +2179 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/locjson.cjs +12 -0
  37. package/dist/locjson.cjs.map +1 -0
  38. package/dist/locjson.d.cts +291 -0
  39. package/dist/locjson.d.ts +291 -0
  40. package/dist/locjson.js +12 -0
  41. package/dist/locjson.js.map +1 -0
  42. package/dist/progress.cjs +16 -0
  43. package/dist/progress.cjs.map +1 -0
  44. package/dist/progress.d.cts +277 -0
  45. package/dist/progress.d.ts +277 -0
  46. package/dist/progress.js +16 -0
  47. package/dist/progress.js.map +1 -0
  48. package/dist/smartcat.cjs +287 -0
  49. package/dist/smartcat.cjs.map +1 -0
  50. package/dist/smartcat.d.cts +234 -0
  51. package/dist/smartcat.d.ts +234 -0
  52. package/dist/smartcat.js +287 -0
  53. package/dist/smartcat.js.map +1 -0
  54. package/dist/templates.cjs +12 -0
  55. package/dist/templates.cjs.map +1 -0
  56. package/dist/templates.d.cts +52 -0
  57. package/dist/templates.d.ts +52 -0
  58. package/dist/templates.js +12 -0
  59. package/dist/templates.js.map +1 -0
  60. package/package.json +101 -15
  61. package/src/actions/AddToProjectAction.tsx +274 -0
  62. package/src/export/export.test.ts +537 -0
  63. package/src/export/index.ts +393 -0
  64. package/src/form/TranslationStatusInput.tsx +212 -0
  65. package/src/import/import.test.ts +346 -0
  66. package/src/import/index.ts +418 -0
  67. package/src/index.ts +63 -0
  68. package/src/lib/constants.ts +23 -0
  69. package/src/lib/documentTitle.test.ts +43 -0
  70. package/src/lib/documentTitle.ts +30 -0
  71. package/src/lib/languageMap.test.ts +56 -0
  72. package/src/lib/languageMap.ts +71 -0
  73. package/src/lib/linkedDocuments.test.ts +56 -0
  74. package/src/lib/linkedDocuments.ts +100 -0
  75. package/src/lib/locjson/deserialize.ts +60 -0
  76. package/src/lib/locjson/fields.ts +355 -0
  77. package/src/lib/locjson/filename.ts +34 -0
  78. package/src/lib/locjson/index.ts +10 -0
  79. package/src/lib/locjson/locjson.test.ts +615 -0
  80. package/src/lib/locjson/paths.test.ts +41 -0
  81. package/src/lib/locjson/paths.ts +73 -0
  82. package/src/lib/locjson/portableText.ts +157 -0
  83. package/src/lib/locjson/serialize.ts +124 -0
  84. package/src/lib/locjson/types.ts +106 -0
  85. package/src/lib/log.ts +33 -0
  86. package/src/lib/projectItems.ts +31 -0
  87. package/src/lib/resolveConfig.test.ts +62 -0
  88. package/src/lib/resolveConfig.ts +54 -0
  89. package/src/lib/workflow.test.ts +37 -0
  90. package/src/lib/workflow.ts +48 -0
  91. package/src/lib/zip.fixtures.ts +75 -0
  92. package/src/lib/zip.test.ts +110 -0
  93. package/src/lib/zip.ts +164 -0
  94. package/src/progress/core.ts +409 -0
  95. package/src/progress/index.ts +7 -0
  96. package/src/progress/progress.test.ts +290 -0
  97. package/src/schema/settings.ts +55 -0
  98. package/src/schema/translatableOptions.ts +25 -0
  99. package/src/schema/translationProject.ts +268 -0
  100. package/src/smartcat/client.test.ts +222 -0
  101. package/src/smartcat/client.ts +351 -0
  102. package/src/smartcat/index.ts +10 -0
  103. package/src/smartcat/types.ts +99 -0
  104. package/src/templates/core.ts +55 -0
  105. package/src/templates/index.ts +5 -0
  106. package/src/templates/templates.test.ts +50 -0
  107. package/src/tool/Dashboard.tsx +46 -0
  108. package/src/tool/ItemProgress.tsx +176 -0
  109. package/src/tool/LogPanel.tsx +207 -0
  110. package/src/tool/ProjectView.tsx +1004 -0
  111. package/src/tool/ProjectsView.tsx +208 -0
  112. package/src/tool/StatusBadge.tsx +33 -0
  113. package/src/tool/StudioInit.tsx +42 -0
  114. package/src/tool/WorkflowSelect.tsx +41 -0
  115. package/src/tool/data.ts +86 -0
  116. package/src/tool/index.ts +19 -0
  117. package/src/tool/processing.test.ts +698 -0
  118. package/src/tool/processing.ts +839 -0
  119. package/src/tool/useTemplates.ts +84 -0
  120. package/src/tool/useWorkflowSelection.ts +65 -0
  121. package/src/types.ts +77 -0
@@ -0,0 +1,234 @@
1
+ /** Input for creating a Smartcat project. */
2
+ export declare interface CreateProjectInput {
3
+ name: string;
4
+ description?: string;
5
+ /** Source language code (e.g. "en"). */
6
+ sourceLanguage: string;
7
+ /** Target language codes (e.g. ["fr", "zh-Hans"]). */
8
+ targetLanguages: string[];
9
+ assignToVendor?: boolean;
10
+ pretranslate?: boolean;
11
+ autoPropagateRepetitions?: boolean;
12
+ /** Workflow stages, e.g. ["translation", "postediting"]. */
13
+ workflowStages?: string[];
14
+ /** Create from a Smartcat project template; the template defines the workflow. */
15
+ templateId?: string;
16
+ }
17
+
18
+ /** One file from a (batch) export: archive entry name + LocJSON text. */
19
+ declare interface ExportedFile {
20
+ /** Path inside the export archive; empty for a single-file response. */
21
+ filename: string;
22
+ content: string;
23
+ }
24
+
25
+ /** Sink for per-request tracing; attach with `SmartcatClient.setRequestLogger`. */
26
+ declare type RequestLogger = (info: RequestLogInfo) => void;
27
+
28
+ /** One Smartcat HTTP call, reported to a {@link RequestLogger} for tracing. */
29
+ declare interface RequestLogInfo {
30
+ method: string;
31
+ /** Request path incl. query string (no host, no secrets). */
32
+ path: string;
33
+ status: number;
34
+ /** Raw response body text — carries Smartcat's error detail on failures. */
35
+ body: string;
36
+ }
37
+
38
+ /**
39
+ * Minimal client for the Smartcat integration API.
40
+ *
41
+ * Uses Basic auth (`base64(workspaceId:apiKey)`). Built on global `fetch`,
42
+ * `FormData` and `Blob` (Node 18+ / the Functions runtime); no Node-only APIs,
43
+ * so it is portable. Secrets are required, so this only runs server-side.
44
+ */
45
+ export declare class SmartcatClient {
46
+ private readonly server;
47
+ private readonly authHeader;
48
+ private readonly fetchImpl;
49
+ private readonly requestTimeoutMs?;
50
+ private requestLogger?;
51
+ constructor(options: SmartcatClientOptions);
52
+ /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */
53
+ private timeoutSignal;
54
+ /** Attach a sink that receives every HTTP call (method, path, status, body). */
55
+ setRequestLogger(logger: RequestLogger): void;
56
+ /** Creates a project and returns it (including its generated `id` GUID). */
57
+ createProject(input: CreateProjectInput): Promise<SmartcatProject>;
58
+ /** Lists the workspace's project templates. */
59
+ getTemplates(): Promise<SmartcatTemplatesResponse>;
60
+ /** Uploads one source file into a project. `filename` may include a folder path. */
61
+ uploadDocument(
62
+ projectId: string,
63
+ filename: string,
64
+ content: string,
65
+ ): Promise<SmartcatDocument[]>;
66
+ /**
67
+ * Uploads many source files into a project in a single request — the
68
+ * multi-file sibling of {@link uploadDocument}. Mirrors the reference
69
+ * `AddDocuments` multipart contract: one `model` part carrying a JSON array
70
+ * with one (default-settings) entry per file, plus one `file` part per file.
71
+ * Returns the created documents (files × target languages).
72
+ */
73
+ uploadDocuments(
74
+ projectId: string,
75
+ files: {
76
+ filename: string;
77
+ content: string;
78
+ }[],
79
+ ): Promise<SmartcatDocument[]>;
80
+ /**
81
+ * Updates an existing document's source content by re-uploading the file.
82
+ * Smartcat re-imports and merges, preserving translations of unchanged
83
+ * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).
84
+ */
85
+ updateDocument(
86
+ documentId: string,
87
+ filename: string,
88
+ content: string,
89
+ ): Promise<SmartcatDocument[]>;
90
+ /** Fetches a project (including its documents and per-stage progress). */
91
+ getProject(projectId: string): Promise<SmartcatProject>;
92
+ /**
93
+ * Deletes a document from its project. The id is a target document id
94
+ * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every
95
+ * target language — in a single call.
96
+ */
97
+ deleteDocument(documentId: string): Promise<void>;
98
+ /** Builds the Smartcat web URL for a project's overview page. */
99
+ projectUrl(projectId: string): string;
100
+ /**
101
+ * Exports a document's target (translated) content and returns it as text.
102
+ * Two-step: request an export task, then poll until ready.
103
+ *
104
+ * `mode` defaults to `confirmed` — only segments confirmed at any workflow
105
+ * stage are returned (unconfirmed/untranslated segments fall back to source),
106
+ * so we never import unvetted machine-translation drafts. Use `current` to pull
107
+ * the live state including unconfirmed edits.
108
+ */
109
+ exportDocument(
110
+ documentId: string,
111
+ options?: {
112
+ mode?: "confirmed" | "current";
113
+ pollIntervalMs?: number;
114
+ maxAttempts?: number;
115
+ sleep?: (ms: number) => Promise<void>;
116
+ },
117
+ ): Promise<string>;
118
+ /**
119
+ * Exports many documents' target content in **one** export task. Smartcat
120
+ * fans the work out server-side and (for more than one document) returns a
121
+ * ZIP archive of the individual files, so the per-document request+poll
122
+ * round-trip is paid once per batch instead of once per document.
123
+ *
124
+ * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry
125
+ * name, so callers should correlate on the parsed content.
126
+ */
127
+ exportDocumentsBatch(
128
+ documentIds: string[],
129
+ options?: {
130
+ mode?: "confirmed" | "current";
131
+ pollIntervalMs?: number;
132
+ maxAttempts?: number;
133
+ sleep?: (ms: number) => Promise<void>;
134
+ },
135
+ ): Promise<ExportedFile[]>;
136
+ private request;
137
+ /**
138
+ * Normalizes a `fetch` rejection. A timeout abort becomes a
139
+ * {@link SmartcatTimeoutError} — traced through the request logger first (with
140
+ * a synthetic status 0) so it surfaces in the same log as HTTP failures — while
141
+ * anything else is rethrown unchanged.
142
+ */
143
+ private onFetchError;
144
+ private requestRaw;
145
+ /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */
146
+ private requestBytes;
147
+ }
148
+
149
+ /** Credentials + server for a Smartcat workspace. */
150
+ export declare interface SmartcatClientOptions {
151
+ /** API server hostname, e.g. "us.smartcat.com". */
152
+ server: string;
153
+ /** Workspace ID (GUID). */
154
+ workspaceId: string;
155
+ /** API token. */
156
+ apiKey: string;
157
+ /** Override fetch (defaults to global fetch). Useful for tests. */
158
+ fetchImpl?: typeof fetch;
159
+ /**
160
+ * Per-request timeout in ms. When set, each HTTP call is aborted after this
161
+ * long (via `AbortSignal.timeout`) and rejected as a {@link SmartcatTimeoutError},
162
+ * so a stalled Smartcat response fails fast with a clear, loggable error instead
163
+ * of the whole Sanity Function being platform-killed. Unset ⇒ no timeout.
164
+ */
165
+ requestTimeoutMs?: number;
166
+ }
167
+
168
+ /** A document within a Smartcat project (one per source file × target language). */
169
+ export declare interface SmartcatDocument {
170
+ /** Target document id, formatted `<fileGuid>_<languageNumber>`. */
171
+ id: string;
172
+ name?: string;
173
+ fullPath?: string;
174
+ filename?: string;
175
+ /** Shared id of the uploaded source file (the `<fileGuid>` part of `id`). */
176
+ externalId?: string;
177
+ sourceLanguage?: string;
178
+ targetLanguage?: string;
179
+ /** Per-stage progress. On documents these carry only `{id, progress}`; the
180
+ * stage names live on the project-level `workflowStages` (keyed by `id`). */
181
+ workflowStages?: SmartcatWorkflowStage[];
182
+ }
183
+
184
+ /** Error thrown when the Smartcat API returns a non-2xx response. */
185
+ export declare class SmartcatError extends Error {
186
+ readonly status: number;
187
+ readonly body: string;
188
+ constructor(status: number, body: string);
189
+ }
190
+
191
+ /** A Smartcat project as returned by the API. */
192
+ export declare interface SmartcatProject {
193
+ id: string;
194
+ name?: string;
195
+ sourceLanguage?: string;
196
+ targetLanguages?: string[];
197
+ /** Project-level stages, where each `id` maps to a `stageType` (e.g. "translation"). */
198
+ workflowStages?: SmartcatWorkflowStage[];
199
+ documents?: SmartcatDocument[];
200
+ }
201
+
202
+ /** A Smartcat project template (from `GET /template`). */
203
+ export declare interface SmartcatTemplate {
204
+ id: string;
205
+ name: string;
206
+ }
207
+
208
+ /** Response shape of `GET /template`. */
209
+ export declare interface SmartcatTemplatesResponse {
210
+ templates: SmartcatTemplate[];
211
+ }
212
+
213
+ /** Error thrown when a request is aborted by the configured `requestTimeoutMs`. */
214
+ export declare class SmartcatTimeoutError extends Error {
215
+ readonly method: string;
216
+ readonly path: string;
217
+ readonly timeoutMs: number;
218
+ constructor(method: string, path: string, timeoutMs: number);
219
+ }
220
+
221
+ export declare interface SmartcatWorkflowStage {
222
+ id?: string;
223
+ /** Stage type/name, e.g. "translation" / "editing" (casing varies by API). */
224
+ stageType?: string;
225
+ /** Human-readable stage name, when the API provides one. */
226
+ stageName?: string;
227
+ /** Completion percentage for this stage, 0–100. */
228
+ progress?: number;
229
+ /** Word counts, when present — useful for a weighted progress view. */
230
+ wordsTranslated?: number;
231
+ totalWordsCount?: number;
232
+ }
233
+
234
+ export {};
@@ -0,0 +1,287 @@
1
+ function isZip(bytes) {
2
+ return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
3
+ }
4
+ async function unzip(bytes) {
5
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), eocd = findEndOfCentralDirectory(view), entries = [];
6
+ let offset = eocd.centralDirectoryOffset;
7
+ for (let i = 0; i < eocd.entryCount; i++) {
8
+ if (view.getUint32(offset, !0) !== 33639248)
9
+ throw new Error(`Invalid ZIP: bad central directory entry at ${offset}`);
10
+ const method = view.getUint16(offset + 10, !0);
11
+ let compressedSize = view.getUint32(offset + 20, !0), uncompressedSize = view.getUint32(offset + 24, !0);
12
+ const nameLength = view.getUint16(offset + 28, !0), extraLength = view.getUint16(offset + 30, !0), commentLength = view.getUint16(offset + 32, !0);
13
+ let localHeaderOffset = view.getUint32(offset + 42, !0);
14
+ const name = new TextDecoder().decode(bytes.subarray(offset + 46, offset + 46 + nameLength));
15
+ if (compressedSize === 4294967295 || uncompressedSize === 4294967295 || localHeaderOffset === 4294967295) {
16
+ const extraStart = offset + 46 + nameLength, extraEnd = extraStart + extraLength;
17
+ let p = extraStart;
18
+ for (; p + 4 <= extraEnd; ) {
19
+ const id = view.getUint16(p, !0), size = view.getUint16(p + 2, !0);
20
+ if (id === 1) {
21
+ let q = p + 4;
22
+ uncompressedSize === 4294967295 && (uncompressedSize = Number(view.getBigUint64(q, !0)), q += 8), compressedSize === 4294967295 && (compressedSize = Number(view.getBigUint64(q, !0)), q += 8), localHeaderOffset === 4294967295 && (localHeaderOffset = Number(view.getBigUint64(q, !0)));
23
+ break;
24
+ }
25
+ p += 4 + size;
26
+ }
27
+ if (compressedSize === 4294967295 || localHeaderOffset === 4294967295)
28
+ throw new Error(`Invalid ZIP: "${name}" needs ZIP64 sizes but has no ZIP64 extra field`);
29
+ }
30
+ if (offset += 46 + nameLength + extraLength + commentLength, name.endsWith("/")) continue;
31
+ if (view.getUint32(localHeaderOffset, !0) !== 67324752)
32
+ throw new Error(`Invalid ZIP: bad local header for "${name}"`);
33
+ const localNameLength = view.getUint16(localHeaderOffset + 26, !0), localExtraLength = view.getUint16(localHeaderOffset + 28, !0), dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength, compressed = bytes.subarray(dataStart, dataStart + compressedSize);
34
+ entries.push({ name, data: await decompress(method, compressed, name) });
35
+ }
36
+ return entries;
37
+ }
38
+ async function unzipText(bytes) {
39
+ const entries = await unzip(bytes), decoder = new TextDecoder();
40
+ return entries.map((e) => ({ name: e.name, content: decoder.decode(e.data) }));
41
+ }
42
+ function findEndOfCentralDirectory(view) {
43
+ const lowest = Math.max(0, view.byteLength - 22 - 65535);
44
+ for (let i = view.byteLength - 22; i >= lowest; i--) {
45
+ if (view.getUint32(i, !0) !== 101010256) continue;
46
+ let entryCount = view.getUint16(i + 10, !0), centralDirectoryOffset = view.getUint32(i + 16, !0);
47
+ if (entryCount === 65535 || centralDirectoryOffset === 4294967295) {
48
+ const locator = i - 20;
49
+ if (locator < 0 || view.getUint32(locator, !0) !== 117853008)
50
+ throw new Error("Invalid ZIP: ZIP64 values without a ZIP64 end-of-central-directory locator");
51
+ const zip64Eocd = Number(view.getBigUint64(locator + 8, !0));
52
+ if (view.getUint32(zip64Eocd, !0) !== 101075792)
53
+ throw new Error("Invalid ZIP: bad ZIP64 end-of-central-directory record");
54
+ entryCount = Number(view.getBigUint64(zip64Eocd + 32, !0)), centralDirectoryOffset = Number(view.getBigUint64(zip64Eocd + 48, !0));
55
+ }
56
+ return { entryCount, centralDirectoryOffset };
57
+ }
58
+ throw new Error("Invalid ZIP: end-of-central-directory record not found");
59
+ }
60
+ async function decompress(method, data, name) {
61
+ if (method === 0) return data;
62
+ if (method === 8) {
63
+ const stream = new Blob([data.slice()]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
64
+ return new Uint8Array(await new Response(stream).arrayBuffer());
65
+ }
66
+ throw new Error(`Unsupported ZIP compression method ${method} for "${name}"`);
67
+ }
68
+ const API_BASE = "/api/integration/v1";
69
+ class SmartcatError extends Error {
70
+ constructor(status, body) {
71
+ super(`Smartcat API error ${status}: ${body}`), this.status = status, this.body = body, this.name = "SmartcatError";
72
+ }
73
+ }
74
+ class SmartcatTimeoutError extends Error {
75
+ constructor(method, path, timeoutMs) {
76
+ super(`Smartcat did not respond within ${formatDuration(timeoutMs)} for ${method} ${path}`), this.method = method, this.path = path, this.timeoutMs = timeoutMs, this.name = "SmartcatTimeoutError";
77
+ }
78
+ }
79
+ function formatDuration(ms) {
80
+ const totalSeconds = Math.round(ms / 1e3), minutes = Math.floor(totalSeconds / 60), seconds = totalSeconds % 60;
81
+ return minutes ? seconds ? `${minutes}m${seconds}s` : `${minutes}m` : `${seconds}s`;
82
+ }
83
+ function isAbortError(err) {
84
+ const name = err?.name;
85
+ return name === "TimeoutError" || name === "AbortError";
86
+ }
87
+ class SmartcatClient {
88
+ constructor(options) {
89
+ this.server = options.server.replace(/^https?:\/\//, "").replace(/\/$/, ""), this.authHeader = `Basic ${btoa(`${options.workspaceId}:${options.apiKey}`)}`, this.fetchImpl = options.fetchImpl ?? fetch, this.requestTimeoutMs = options.requestTimeoutMs;
90
+ }
91
+ /** An `AbortSignal` that fires after `requestTimeoutMs`, or undefined if unset. */
92
+ timeoutSignal() {
93
+ return this.requestTimeoutMs ? AbortSignal.timeout(this.requestTimeoutMs) : void 0;
94
+ }
95
+ /** Attach a sink that receives every HTTP call (method, path, status, body). */
96
+ setRequestLogger(logger) {
97
+ this.requestLogger = logger;
98
+ }
99
+ /** Creates a project and returns it (including its generated `id` GUID). */
100
+ async createProject(input) {
101
+ const { templateId, ...rest } = input, body = templateId ? { assignToVendor: !1, externalTag: "smartcat-sanity-plugin", templateId, ...rest } : {
102
+ assignToVendor: !1,
103
+ externalTag: "smartcat-sanity-plugin",
104
+ useMT: !0,
105
+ useTranslationMemory: !0,
106
+ pretranslate: !0,
107
+ autoPropagateRepetitions: !0,
108
+ minTranslationAssuranceLevel: "regular",
109
+ ...rest
110
+ }, form = new FormData();
111
+ return form.append("body", JSON.stringify(body)), this.request("POST", `${API_BASE}/project/create`, form);
112
+ }
113
+ /** Lists the workspace's project templates. */
114
+ async getTemplates() {
115
+ return this.request("GET", `${API_BASE}/template`);
116
+ }
117
+ /** Uploads one source file into a project. `filename` may include a folder path. */
118
+ async uploadDocument(projectId, filename, content) {
119
+ const form = new FormData();
120
+ form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
121
+ const result = await this.request(
122
+ "POST",
123
+ `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,
124
+ form
125
+ );
126
+ return Array.isArray(result) ? result : result ? [result] : [];
127
+ }
128
+ /**
129
+ * Uploads many source files into a project in a single request — the
130
+ * multi-file sibling of {@link uploadDocument}. Mirrors the reference
131
+ * `AddDocuments` multipart contract: one `model` part carrying a JSON array
132
+ * with one (default-settings) entry per file, plus one `file` part per file.
133
+ * Returns the created documents (files × target languages).
134
+ */
135
+ async uploadDocuments(projectId, files) {
136
+ const form = new FormData();
137
+ form.append("model", new Blob([JSON.stringify(files.map(() => ({})))], { type: "application/json" }), "model");
138
+ for (const f of files)
139
+ form.append("file", new Blob([f.content], { type: "application/octet-stream" }), f.filename);
140
+ const result = await this.request(
141
+ "POST",
142
+ `${API_BASE}/project/document?projectId=${encodeURIComponent(projectId)}`,
143
+ form
144
+ );
145
+ return Array.isArray(result) ? result : result ? [result] : [];
146
+ }
147
+ /**
148
+ * Updates an existing document's source content by re-uploading the file.
149
+ * Smartcat re-imports and merges, preserving translations of unchanged
150
+ * segments. `documentId` is a target document id (`<fileGuid>_<langNum>`).
151
+ */
152
+ async updateDocument(documentId, filename, content) {
153
+ const form = new FormData();
154
+ form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
155
+ const result = await this.request(
156
+ "PUT",
157
+ `${API_BASE}/document/update?documentId=${encodeURIComponent(documentId)}`,
158
+ form
159
+ );
160
+ return Array.isArray(result) ? result : result ? [result] : [];
161
+ }
162
+ /** Fetches a project (including its documents and per-stage progress). */
163
+ async getProject(projectId) {
164
+ return this.request("GET", `${API_BASE}/project/${encodeURIComponent(projectId)}`);
165
+ }
166
+ /**
167
+ * Deletes a document from its project. The id is a target document id
168
+ * (`<fileGuid>_<langNum>`), but Smartcat removes the whole document — every
169
+ * target language — in a single call.
170
+ */
171
+ async deleteDocument(documentId) {
172
+ await this.request("DELETE", `${API_BASE}/document?documentIds=${encodeURIComponent(documentId)}`);
173
+ }
174
+ /** Builds the Smartcat web URL for a project's overview page. */
175
+ projectUrl(projectId) {
176
+ return `https://${this.server}/projects/${projectId}/general`;
177
+ }
178
+ /**
179
+ * Exports a document's target (translated) content and returns it as text.
180
+ * Two-step: request an export task, then poll until ready.
181
+ *
182
+ * `mode` defaults to `confirmed` — only segments confirmed at any workflow
183
+ * stage are returned (unconfirmed/untranslated segments fall back to source),
184
+ * so we never import unvetted machine-translation drafts. Use `current` to pull
185
+ * the live state including unconfirmed edits.
186
+ */
187
+ async exportDocument(documentId, options = {}) {
188
+ const { mode = "confirmed", pollIntervalMs = 3e3, maxAttempts = 20, sleep = defaultSleep } = options, task = await this.request(
189
+ "POST",
190
+ `${API_BASE}/document/export?documentIds=${encodeURIComponent(documentId)}&mode=${mode}&type=target`
191
+ );
192
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
193
+ const { status, text } = await this.requestRaw("GET", `${API_BASE}/document/export/${task.id}`);
194
+ if (status === 200) return text;
195
+ if (status === 204) {
196
+ await sleep(pollIntervalMs);
197
+ continue;
198
+ }
199
+ throw new SmartcatError(status, text);
200
+ }
201
+ throw new Error(`Smartcat export for ${documentId} timed out`);
202
+ }
203
+ /**
204
+ * Exports many documents' target content in **one** export task. Smartcat
205
+ * fans the work out server-side and (for more than one document) returns a
206
+ * ZIP archive of the individual files, so the per-document request+poll
207
+ * round-trip is paid once per batch instead of once per document.
208
+ *
209
+ * Identity comes from each LocJSON's own `x-sanity` metadata, not the entry
210
+ * name, so callers should correlate on the parsed content.
211
+ */
212
+ async exportDocumentsBatch(documentIds, options = {}) {
213
+ if (documentIds.length === 0) return [];
214
+ const { mode = "confirmed", pollIntervalMs = 3e3, maxAttempts = 60, sleep = defaultSleep } = options, query = documentIds.map((id) => `documentIds=${encodeURIComponent(id)}`).join("&"), task = await this.request(
215
+ "POST",
216
+ `${API_BASE}/document/export?${query}&mode=${mode}&type=target`
217
+ );
218
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
219
+ const { status, bytes } = await this.requestBytes("GET", `${API_BASE}/document/export/${task.id}`);
220
+ if (status === 200)
221
+ return isZip(bytes) ? (await unzipText(bytes)).map(({ name, content }) => ({ filename: name, content })) : [{ filename: "", content: new TextDecoder().decode(bytes) }];
222
+ if (status === 204) {
223
+ await sleep(pollIntervalMs);
224
+ continue;
225
+ }
226
+ throw new SmartcatError(status, new TextDecoder().decode(bytes));
227
+ }
228
+ throw new Error(`Smartcat batch export of ${documentIds.length} document(s) timed out`);
229
+ }
230
+ async request(method, path, body) {
231
+ const { status, text } = await this.requestRaw(method, path, body);
232
+ if (status < 200 || status >= 300) throw new SmartcatError(status, text);
233
+ return text ? JSON.parse(text) : null;
234
+ }
235
+ /**
236
+ * Normalizes a `fetch` rejection. A timeout abort becomes a
237
+ * {@link SmartcatTimeoutError} — traced through the request logger first (with
238
+ * a synthetic status 0) so it surfaces in the same log as HTTP failures — while
239
+ * anything else is rethrown unchanged.
240
+ */
241
+ onFetchError(method, path, err) {
242
+ if (isAbortError(err) && this.requestTimeoutMs) {
243
+ const timeout = new SmartcatTimeoutError(method, path, this.requestTimeoutMs);
244
+ return this.requestLogger?.({ method, path, status: 0, body: timeout.message }), timeout;
245
+ }
246
+ return err;
247
+ }
248
+ async requestRaw(method, path, body) {
249
+ let res;
250
+ try {
251
+ res = await this.fetchImpl(`https://${this.server}${path}`, {
252
+ method,
253
+ headers: { Authorization: this.authHeader },
254
+ body,
255
+ signal: this.timeoutSignal()
256
+ });
257
+ } catch (err) {
258
+ throw this.onFetchError(method, path, err);
259
+ }
260
+ const text = await res.text();
261
+ return this.requestLogger?.({ method, path, status: res.status, body: text }), { status: res.status, text };
262
+ }
263
+ /** Like {@link requestRaw} but preserves the body as bytes (ZIP downloads). */
264
+ async requestBytes(method, path) {
265
+ let res;
266
+ try {
267
+ res = await this.fetchImpl(`https://${this.server}${path}`, {
268
+ method,
269
+ headers: { Authorization: this.authHeader },
270
+ signal: this.timeoutSignal()
271
+ });
272
+ } catch (err) {
273
+ throw this.onFetchError(method, path, err);
274
+ }
275
+ const bytes = new Uint8Array(await res.arrayBuffer()), logBody = res.status >= 200 && res.status < 300 ? `<${bytes.length} bytes>` : new TextDecoder().decode(bytes);
276
+ return this.requestLogger?.({ method, path, status: res.status, body: logBody }), { status: res.status, bytes };
277
+ }
278
+ }
279
+ function defaultSleep(ms) {
280
+ return new Promise((resolve) => setTimeout(resolve, ms));
281
+ }
282
+ export {
283
+ SmartcatClient,
284
+ SmartcatError,
285
+ SmartcatTimeoutError
286
+ };
287
+ //# sourceMappingURL=smartcat.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var constants = require("./_chunks-cjs/constants.cjs");
4
+ function keyify(value) {
5
+ return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "x";
6
+ }
7
+ async function runTemplatesSync(options) {
8
+ const { sanity, smartcat, settingsId = constants.SETTINGS_DOC_ID, now = () => (/* @__PURE__ */ new Date()).toISOString() } = options, { templates } = await smartcat.getTemplates(), value = [...templates ?? []].sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({ _key: keyify(t.id), id: t.id, name: t.name }));
9
+ return await sanity.patch(settingsId).set({ templates: value, templatesSyncedAt: now() }).commit(), { count: value.length };
10
+ }
11
+ exports.runTemplatesSync = runTemplatesSync;
12
+ //# sourceMappingURL=templates.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.cjs","sources":["../src/templates/core.ts"],"sourcesContent":["import type {SmartcatTemplatesResponse} from '../smartcat/types'\nimport {SETTINGS_DOC_ID} from '../lib/constants'\n\n/**\n * Refreshes the cached Smartcat project-template list onto the `smartcat.settings`\n * singleton. Run by the thin `smartcat-templates` Function when the browser bumps\n * `templatesRequestedAt` (on Studio init or when a create dialog opens).\n *\n * Dependency-free (no DOM, no @sanity/* runtime) so it bundles into the Function.\n */\n\n/** Minimal structural subset of @sanity/client used by the templates refresh. */\nexport interface SanityLikeClient {\n patch(id: string): {\n set(attrs: Record<string, unknown>): {commit(): Promise<unknown>}\n }\n}\n\n/** Structural subset of SmartcatClient the templates refresh needs. */\nexport interface SmartcatTemplatesClient {\n getTemplates(): Promise<SmartcatTemplatesResponse>\n}\n\nexport interface RunTemplatesSyncOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatTemplatesClient\n /** `_id` of the settings singleton (defaults to the fixed id). */\n settingsId?: string\n now?: () => string\n}\n\n/** Sanitizes a template id into a stable, Sanity-safe array `_key`. */\nfunction keyify(value: string): string {\n return value.replace(/[^a-zA-Z0-9_-]/g, '-') || 'x'\n}\n\n/**\n * Fetches templates from Smartcat and writes them (sorted by name) to the\n * settings singleton, stamping `templatesSyncedAt`. Always writes the timestamp —\n * even for an empty list — so the cache reads as initialized.\n */\nexport async function runTemplatesSync(\n options: RunTemplatesSyncOptions,\n): Promise<{count: number}> {\n const {sanity, smartcat, settingsId = SETTINGS_DOC_ID, now = () => new Date().toISOString()} = options\n\n const {templates} = await smartcat.getTemplates()\n const value = [...(templates ?? [])]\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((t) => ({_key: keyify(t.id), id: t.id, name: t.name}))\n\n await sanity.patch(settingsId).set({templates: value, templatesSyncedAt: now()}).commit()\n\n return {count: value.length}\n}\n"],"names":["SETTINGS_DOC_ID"],"mappings":";;;AAgCA,SAAS,OAAO,OAAuB;AACrC,SAAO,MAAM,QAAQ,mBAAmB,GAAG,KAAK;AAClD;AAOA,eAAsB,iBACpB,SAC0B;AAC1B,QAAM,EAAC,QAAQ,UAAU,aAAaA,UAAAA,iBAAiB,MAAM,OAAM,oBAAI,KAAA,GAAO,YAAA,EAAY,IAAK,SAEzF,EAAC,UAAA,IAAa,MAAM,SAAS,aAAA,GAC7B,QAAQ,CAAC,GAAI,aAAa,CAAA,CAAG,EAChC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,EAC3C,IAAI,CAAC,OAAO,EAAC,MAAM,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,MAAM,EAAE,KAAA,EAAM;AAE5D,SAAA,MAAM,OAAO,MAAM,UAAU,EAAE,IAAI,EAAC,WAAW,OAAO,mBAAmB,IAAA,EAAI,CAAE,EAAE,OAAA,GAE1E,EAAC,OAAO,MAAM,OAAA;AACvB;;"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Fetches templates from Smartcat and writes them (sorted by name) to the
3
+ * settings singleton, stamping `templatesSyncedAt`. Always writes the timestamp —
4
+ * even for an empty list — so the cache reads as initialized.
5
+ */
6
+ export declare function runTemplatesSync(
7
+ options: RunTemplatesSyncOptions,
8
+ ): Promise<{
9
+ count: number;
10
+ }>;
11
+
12
+ export declare interface RunTemplatesSyncOptions {
13
+ sanity: SanityLikeClient;
14
+ smartcat: SmartcatTemplatesClient;
15
+ /** `_id` of the settings singleton (defaults to the fixed id). */
16
+ settingsId?: string;
17
+ now?: () => string;
18
+ }
19
+
20
+ /**
21
+ * Refreshes the cached Smartcat project-template list onto the `smartcat.settings`
22
+ * singleton. Run by the thin `smartcat-templates` Function when the browser bumps
23
+ * `templatesRequestedAt` (on Studio init or when a create dialog opens).
24
+ *
25
+ * Dependency-free (no DOM, no @sanity/* runtime) so it bundles into the Function.
26
+ */
27
+ /** Minimal structural subset of @sanity/client used by the templates refresh. */
28
+ export declare interface SanityLikeClient {
29
+ patch(id: string): {
30
+ set(attrs: Record<string, unknown>): {
31
+ commit(): Promise<unknown>;
32
+ };
33
+ };
34
+ }
35
+
36
+ /** A Smartcat project template (from `GET /template`). */
37
+ declare interface SmartcatTemplate {
38
+ id: string;
39
+ name: string;
40
+ }
41
+
42
+ /** Structural subset of SmartcatClient the templates refresh needs. */
43
+ export declare interface SmartcatTemplatesClient {
44
+ getTemplates(): Promise<SmartcatTemplatesResponse>;
45
+ }
46
+
47
+ /** Response shape of `GET /template`. */
48
+ declare interface SmartcatTemplatesResponse {
49
+ templates: SmartcatTemplate[];
50
+ }
51
+
52
+ export {};