@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.
- package/README.md +294 -0
- package/dist/_chunks-cjs/constants.cjs +13 -0
- package/dist/_chunks-cjs/constants.cjs.map +1 -0
- package/dist/_chunks-cjs/index.cjs +174 -0
- package/dist/_chunks-cjs/index.cjs.map +1 -0
- package/dist/_chunks-cjs/index2.cjs +338 -0
- package/dist/_chunks-cjs/index2.cjs.map +1 -0
- package/dist/_chunks-cjs/workflow.cjs +16 -0
- package/dist/_chunks-cjs/workflow.cjs.map +1 -0
- package/dist/_chunks-es/constants.js +14 -0
- package/dist/_chunks-es/constants.js.map +1 -0
- package/dist/_chunks-es/index.js +175 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/index2.js +340 -0
- package/dist/_chunks-es/index2.js.map +1 -0
- package/dist/_chunks-es/workflow.js +17 -0
- package/dist/_chunks-es/workflow.js.map +1 -0
- package/dist/export.cjs +160 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +162 -0
- package/dist/export.d.ts +162 -0
- package/dist/export.js +162 -0
- package/dist/export.js.map +1 -0
- package/dist/import.cjs +169 -0
- package/dist/import.cjs.map +1 -0
- package/dist/import.d.cts +155 -0
- package/dist/import.d.ts +155 -0
- package/dist/import.js +169 -0
- package/dist/import.js.map +1 -0
- package/dist/index.cjs +2172 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2179 -0
- package/dist/index.js.map +1 -0
- package/dist/locjson.cjs +12 -0
- package/dist/locjson.cjs.map +1 -0
- package/dist/locjson.d.cts +291 -0
- package/dist/locjson.d.ts +291 -0
- package/dist/locjson.js +12 -0
- package/dist/locjson.js.map +1 -0
- package/dist/progress.cjs +16 -0
- package/dist/progress.cjs.map +1 -0
- package/dist/progress.d.cts +277 -0
- package/dist/progress.d.ts +277 -0
- package/dist/progress.js +16 -0
- package/dist/progress.js.map +1 -0
- package/dist/smartcat.cjs +287 -0
- package/dist/smartcat.cjs.map +1 -0
- package/dist/smartcat.d.cts +234 -0
- package/dist/smartcat.d.ts +234 -0
- package/dist/smartcat.js +287 -0
- package/dist/smartcat.js.map +1 -0
- package/dist/templates.cjs +12 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +52 -0
- package/dist/templates.d.ts +52 -0
- package/dist/templates.js +12 -0
- package/dist/templates.js.map +1 -0
- package/package.json +101 -15
- package/src/actions/AddToProjectAction.tsx +274 -0
- package/src/export/export.test.ts +537 -0
- package/src/export/index.ts +393 -0
- package/src/form/TranslationStatusInput.tsx +212 -0
- package/src/import/import.test.ts +346 -0
- package/src/import/index.ts +418 -0
- package/src/index.ts +63 -0
- package/src/lib/constants.ts +23 -0
- package/src/lib/documentTitle.test.ts +43 -0
- package/src/lib/documentTitle.ts +30 -0
- package/src/lib/languageMap.test.ts +56 -0
- package/src/lib/languageMap.ts +71 -0
- package/src/lib/linkedDocuments.test.ts +56 -0
- package/src/lib/linkedDocuments.ts +100 -0
- package/src/lib/locjson/deserialize.ts +60 -0
- package/src/lib/locjson/fields.ts +355 -0
- package/src/lib/locjson/filename.ts +34 -0
- package/src/lib/locjson/index.ts +10 -0
- package/src/lib/locjson/locjson.test.ts +615 -0
- package/src/lib/locjson/paths.test.ts +41 -0
- package/src/lib/locjson/paths.ts +73 -0
- package/src/lib/locjson/portableText.ts +157 -0
- package/src/lib/locjson/serialize.ts +124 -0
- package/src/lib/locjson/types.ts +106 -0
- package/src/lib/log.ts +33 -0
- package/src/lib/projectItems.ts +31 -0
- package/src/lib/resolveConfig.test.ts +62 -0
- package/src/lib/resolveConfig.ts +54 -0
- package/src/lib/workflow.test.ts +37 -0
- package/src/lib/workflow.ts +48 -0
- package/src/lib/zip.fixtures.ts +75 -0
- package/src/lib/zip.test.ts +110 -0
- package/src/lib/zip.ts +164 -0
- package/src/progress/core.ts +409 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/progress.test.ts +290 -0
- package/src/schema/settings.ts +55 -0
- package/src/schema/translatableOptions.ts +25 -0
- package/src/schema/translationProject.ts +268 -0
- package/src/smartcat/client.test.ts +222 -0
- package/src/smartcat/client.ts +351 -0
- package/src/smartcat/index.ts +10 -0
- package/src/smartcat/types.ts +99 -0
- package/src/templates/core.ts +55 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/templates.test.ts +50 -0
- package/src/tool/Dashboard.tsx +46 -0
- package/src/tool/ItemProgress.tsx +176 -0
- package/src/tool/LogPanel.tsx +207 -0
- package/src/tool/ProjectView.tsx +1004 -0
- package/src/tool/ProjectsView.tsx +208 -0
- package/src/tool/StatusBadge.tsx +33 -0
- package/src/tool/StudioInit.tsx +42 -0
- package/src/tool/WorkflowSelect.tsx +41 -0
- package/src/tool/data.ts +86 -0
- package/src/tool/index.ts +19 -0
- package/src/tool/processing.test.ts +698 -0
- package/src/tool/processing.ts +839 -0
- package/src/tool/useTemplates.ts +84 -0
- package/src/tool/useWorkflowSelection.ts +65 -0
- package/src/types.ts +77 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live, in-memory log lines for the export/import flows, streamed to the
|
|
3
|
+
* ProjectView Log panel as each document and field is processed. Never persisted
|
|
4
|
+
* — purely a session-scoped trace for troubleshooting (e.g. on client calls).
|
|
5
|
+
*/
|
|
6
|
+
declare type LogLevel = "info" | "success" | "skip" | "error" | "update";
|
|
7
|
+
|
|
8
|
+
declare interface LogLine {
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
/** Field-level lines are indented under their document header. */
|
|
12
|
+
indent?: boolean;
|
|
13
|
+
/** Optional value (source or translated text); revealed on click in the panel. */
|
|
14
|
+
value?: string;
|
|
15
|
+
/** Prior value; when set, the panel shows `before` and `value` side by side. */
|
|
16
|
+
before?: string;
|
|
17
|
+
/** Extra detail (e.g. a JSON parse error); shown in its own box on expand. */
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thin import step run by a Sanity Function: refreshes per-stage progress from
|
|
23
|
+
* Smartcat, then downloads the ready targets into the project's `inbox` and
|
|
24
|
+
* sets status to `downloaded`. The browser turns the inbox into locale
|
|
25
|
+
* variants and decides final completion.
|
|
26
|
+
*
|
|
27
|
+
* Downloads happen in **batches** (one Smartcat export task per ~50 documents,
|
|
28
|
+
* returned as a ZIP), so run time no longer scales with per-document polling —
|
|
29
|
+
* large projects fit comfortably inside the Function timeout. The inbox is
|
|
30
|
+
* size-capped per run; overflow targets are reported as `remaining` and the
|
|
31
|
+
* next import run picks them up (already-imported targets are skipped).
|
|
32
|
+
*
|
|
33
|
+
* Targets with nothing confirmed are skipped, so no untranslated variants are
|
|
34
|
+
* created. Does no content processing, so it depends only on the Smartcat client.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runImportDownload(
|
|
37
|
+
options: RunImportDownloadOptions,
|
|
38
|
+
): Promise<RunImportDownloadResult>;
|
|
39
|
+
|
|
40
|
+
export declare interface RunImportDownloadOptions {
|
|
41
|
+
sanity: SanityLikeClient;
|
|
42
|
+
smartcat: SmartcatImportClient;
|
|
43
|
+
/** `_id` of the `smartcat.translationProject` to import. */
|
|
44
|
+
projectId: string;
|
|
45
|
+
now?: () => string;
|
|
46
|
+
/** Documents per Smartcat batch-export task. */
|
|
47
|
+
batchSize?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Soft cap on the total LocJSON bytes stored in the project's `inbox` per
|
|
50
|
+
* run. The inbox lives on the Sanity project document, which has a practical
|
|
51
|
+
* size limit — targets beyond the cap are reported as `remaining` and picked
|
|
52
|
+
* up by the next import run.
|
|
53
|
+
*/
|
|
54
|
+
maxInboxBytes?: number;
|
|
55
|
+
/** How many batch-export tasks run against Smartcat concurrently. */
|
|
56
|
+
concurrency?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Soft time budget for the download phase, in ms. Once exceeded, batches not
|
|
59
|
+
* yet started are deferred as `remaining` and the run exits cleanly — so the
|
|
60
|
+
* Function always finishes well before its hard platform timeout instead of
|
|
61
|
+
* being killed mid-run (which would leave the project stuck in "importing").
|
|
62
|
+
*/
|
|
63
|
+
timeBudgetMs?: number;
|
|
64
|
+
/** Clock override for tests (ms). */
|
|
65
|
+
nowMs?: () => number;
|
|
66
|
+
/**
|
|
67
|
+
* Receives every log line as it is emitted (in addition to the functionLog
|
|
68
|
+
* stored on the project document) — the Function forwards these to console
|
|
69
|
+
* so `sanity functions logs` shows the run without reading the document.
|
|
70
|
+
*/
|
|
71
|
+
onLog?: (line: LogLine) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export declare interface RunImportDownloadResult {
|
|
75
|
+
/** Number of complete targets downloaded into the inbox. */
|
|
76
|
+
downloaded: number;
|
|
77
|
+
/** Number of targets skipped because they are not yet fully translated. */
|
|
78
|
+
skipped: number;
|
|
79
|
+
/** Targets ready to download but deferred to the next run (inbox size cap). */
|
|
80
|
+
remaining: number;
|
|
81
|
+
/** Targets already imported in a previous run and therefore not re-downloaded. */
|
|
82
|
+
alreadyImported: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Minimal structural subset of @sanity/client used by the import download. */
|
|
86
|
+
export declare interface SanityLikeClient {
|
|
87
|
+
fetch<T = unknown>(
|
|
88
|
+
query: string,
|
|
89
|
+
params?: Record<string, unknown>,
|
|
90
|
+
): Promise<T>;
|
|
91
|
+
patch(id: string): {
|
|
92
|
+
set(attrs: Record<string, unknown>): {
|
|
93
|
+
commit(): Promise<unknown>;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A document within a Smartcat project (one per source file × target language). */
|
|
99
|
+
declare interface SmartcatDocument {
|
|
100
|
+
/** Target document id, formatted `<fileGuid>_<languageNumber>`. */
|
|
101
|
+
id: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
fullPath?: string;
|
|
104
|
+
filename?: string;
|
|
105
|
+
/** Shared id of the uploaded source file (the `<fileGuid>` part of `id`). */
|
|
106
|
+
externalId?: string;
|
|
107
|
+
sourceLanguage?: string;
|
|
108
|
+
targetLanguage?: string;
|
|
109
|
+
/** Per-stage progress. On documents these carry only `{id, progress}`; the
|
|
110
|
+
* stage names live on the project-level `workflowStages` (keyed by `id`). */
|
|
111
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Structural subset of SmartcatClient the import download needs. */
|
|
115
|
+
export declare interface SmartcatImportClient {
|
|
116
|
+
getProject(projectId: string): Promise<SmartcatProject>;
|
|
117
|
+
exportDocument(documentId: string): Promise<string>;
|
|
118
|
+
/**
|
|
119
|
+
* Batch export: one Smartcat task for many documents (ZIP result). Optional
|
|
120
|
+
* so custom/legacy clients keep working — without it every document falls
|
|
121
|
+
* back to a single {@link exportDocument} round-trip.
|
|
122
|
+
*/
|
|
123
|
+
exportDocumentsBatch?(documentIds: string[]): Promise<
|
|
124
|
+
{
|
|
125
|
+
filename: string;
|
|
126
|
+
content: string;
|
|
127
|
+
}[]
|
|
128
|
+
>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A Smartcat project as returned by the API. */
|
|
132
|
+
declare interface SmartcatProject {
|
|
133
|
+
id: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
sourceLanguage?: string;
|
|
136
|
+
targetLanguages?: string[];
|
|
137
|
+
/** Project-level stages, where each `id` maps to a `stageType` (e.g. "translation"). */
|
|
138
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
139
|
+
documents?: SmartcatDocument[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
declare interface SmartcatWorkflowStage {
|
|
143
|
+
id?: string;
|
|
144
|
+
/** Stage type/name, e.g. "translation" / "editing" (casing varies by API). */
|
|
145
|
+
stageType?: string;
|
|
146
|
+
/** Human-readable stage name, when the API provides one. */
|
|
147
|
+
stageName?: string;
|
|
148
|
+
/** Completion percentage for this stage, 0–100. */
|
|
149
|
+
progress?: number;
|
|
150
|
+
/** Word counts, when present — useful for a weighted progress view. */
|
|
151
|
+
wordsTranslated?: number;
|
|
152
|
+
totalWordsCount?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {};
|
package/dist/import.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live, in-memory log lines for the export/import flows, streamed to the
|
|
3
|
+
* ProjectView Log panel as each document and field is processed. Never persisted
|
|
4
|
+
* — purely a session-scoped trace for troubleshooting (e.g. on client calls).
|
|
5
|
+
*/
|
|
6
|
+
declare type LogLevel = "info" | "success" | "skip" | "error" | "update";
|
|
7
|
+
|
|
8
|
+
declare interface LogLine {
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
/** Field-level lines are indented under their document header. */
|
|
12
|
+
indent?: boolean;
|
|
13
|
+
/** Optional value (source or translated text); revealed on click in the panel. */
|
|
14
|
+
value?: string;
|
|
15
|
+
/** Prior value; when set, the panel shows `before` and `value` side by side. */
|
|
16
|
+
before?: string;
|
|
17
|
+
/** Extra detail (e.g. a JSON parse error); shown in its own box on expand. */
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Thin import step run by a Sanity Function: refreshes per-stage progress from
|
|
23
|
+
* Smartcat, then downloads the ready targets into the project's `inbox` and
|
|
24
|
+
* sets status to `downloaded`. The browser turns the inbox into locale
|
|
25
|
+
* variants and decides final completion.
|
|
26
|
+
*
|
|
27
|
+
* Downloads happen in **batches** (one Smartcat export task per ~50 documents,
|
|
28
|
+
* returned as a ZIP), so run time no longer scales with per-document polling —
|
|
29
|
+
* large projects fit comfortably inside the Function timeout. The inbox is
|
|
30
|
+
* size-capped per run; overflow targets are reported as `remaining` and the
|
|
31
|
+
* next import run picks them up (already-imported targets are skipped).
|
|
32
|
+
*
|
|
33
|
+
* Targets with nothing confirmed are skipped, so no untranslated variants are
|
|
34
|
+
* created. Does no content processing, so it depends only on the Smartcat client.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runImportDownload(
|
|
37
|
+
options: RunImportDownloadOptions,
|
|
38
|
+
): Promise<RunImportDownloadResult>;
|
|
39
|
+
|
|
40
|
+
export declare interface RunImportDownloadOptions {
|
|
41
|
+
sanity: SanityLikeClient;
|
|
42
|
+
smartcat: SmartcatImportClient;
|
|
43
|
+
/** `_id` of the `smartcat.translationProject` to import. */
|
|
44
|
+
projectId: string;
|
|
45
|
+
now?: () => string;
|
|
46
|
+
/** Documents per Smartcat batch-export task. */
|
|
47
|
+
batchSize?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Soft cap on the total LocJSON bytes stored in the project's `inbox` per
|
|
50
|
+
* run. The inbox lives on the Sanity project document, which has a practical
|
|
51
|
+
* size limit — targets beyond the cap are reported as `remaining` and picked
|
|
52
|
+
* up by the next import run.
|
|
53
|
+
*/
|
|
54
|
+
maxInboxBytes?: number;
|
|
55
|
+
/** How many batch-export tasks run against Smartcat concurrently. */
|
|
56
|
+
concurrency?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Soft time budget for the download phase, in ms. Once exceeded, batches not
|
|
59
|
+
* yet started are deferred as `remaining` and the run exits cleanly — so the
|
|
60
|
+
* Function always finishes well before its hard platform timeout instead of
|
|
61
|
+
* being killed mid-run (which would leave the project stuck in "importing").
|
|
62
|
+
*/
|
|
63
|
+
timeBudgetMs?: number;
|
|
64
|
+
/** Clock override for tests (ms). */
|
|
65
|
+
nowMs?: () => number;
|
|
66
|
+
/**
|
|
67
|
+
* Receives every log line as it is emitted (in addition to the functionLog
|
|
68
|
+
* stored on the project document) — the Function forwards these to console
|
|
69
|
+
* so `sanity functions logs` shows the run without reading the document.
|
|
70
|
+
*/
|
|
71
|
+
onLog?: (line: LogLine) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export declare interface RunImportDownloadResult {
|
|
75
|
+
/** Number of complete targets downloaded into the inbox. */
|
|
76
|
+
downloaded: number;
|
|
77
|
+
/** Number of targets skipped because they are not yet fully translated. */
|
|
78
|
+
skipped: number;
|
|
79
|
+
/** Targets ready to download but deferred to the next run (inbox size cap). */
|
|
80
|
+
remaining: number;
|
|
81
|
+
/** Targets already imported in a previous run and therefore not re-downloaded. */
|
|
82
|
+
alreadyImported: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Minimal structural subset of @sanity/client used by the import download. */
|
|
86
|
+
export declare interface SanityLikeClient {
|
|
87
|
+
fetch<T = unknown>(
|
|
88
|
+
query: string,
|
|
89
|
+
params?: Record<string, unknown>,
|
|
90
|
+
): Promise<T>;
|
|
91
|
+
patch(id: string): {
|
|
92
|
+
set(attrs: Record<string, unknown>): {
|
|
93
|
+
commit(): Promise<unknown>;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A document within a Smartcat project (one per source file × target language). */
|
|
99
|
+
declare interface SmartcatDocument {
|
|
100
|
+
/** Target document id, formatted `<fileGuid>_<languageNumber>`. */
|
|
101
|
+
id: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
fullPath?: string;
|
|
104
|
+
filename?: string;
|
|
105
|
+
/** Shared id of the uploaded source file (the `<fileGuid>` part of `id`). */
|
|
106
|
+
externalId?: string;
|
|
107
|
+
sourceLanguage?: string;
|
|
108
|
+
targetLanguage?: string;
|
|
109
|
+
/** Per-stage progress. On documents these carry only `{id, progress}`; the
|
|
110
|
+
* stage names live on the project-level `workflowStages` (keyed by `id`). */
|
|
111
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Structural subset of SmartcatClient the import download needs. */
|
|
115
|
+
export declare interface SmartcatImportClient {
|
|
116
|
+
getProject(projectId: string): Promise<SmartcatProject>;
|
|
117
|
+
exportDocument(documentId: string): Promise<string>;
|
|
118
|
+
/**
|
|
119
|
+
* Batch export: one Smartcat task for many documents (ZIP result). Optional
|
|
120
|
+
* so custom/legacy clients keep working — without it every document falls
|
|
121
|
+
* back to a single {@link exportDocument} round-trip.
|
|
122
|
+
*/
|
|
123
|
+
exportDocumentsBatch?(documentIds: string[]): Promise<
|
|
124
|
+
{
|
|
125
|
+
filename: string;
|
|
126
|
+
content: string;
|
|
127
|
+
}[]
|
|
128
|
+
>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A Smartcat project as returned by the API. */
|
|
132
|
+
declare interface SmartcatProject {
|
|
133
|
+
id: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
sourceLanguage?: string;
|
|
136
|
+
targetLanguages?: string[];
|
|
137
|
+
/** Project-level stages, where each `id` maps to a `stageType` (e.g. "translation"). */
|
|
138
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
139
|
+
documents?: SmartcatDocument[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
declare interface SmartcatWorkflowStage {
|
|
143
|
+
id?: string;
|
|
144
|
+
/** Stage type/name, e.g. "translation" / "editing" (casing varies by API). */
|
|
145
|
+
stageType?: string;
|
|
146
|
+
/** Human-readable stage name, when the API provides one. */
|
|
147
|
+
stageName?: string;
|
|
148
|
+
/** Completion percentage for this stage, 0–100. */
|
|
149
|
+
progress?: number;
|
|
150
|
+
/** Word counts, when present — useful for a weighted progress view. */
|
|
151
|
+
wordsTranslated?: number;
|
|
152
|
+
totalWordsCount?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {};
|
package/dist/import.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ITEM_ID, buildProgress, itemsFromProject, hasConfirmedContent, itemDocDerefRespectingDraft } from "./_chunks-es/index.js";
|
|
2
|
+
function smartcatDocPath(scDoc) {
|
|
3
|
+
return (scDoc?.fullPath || scDoc?.name || scDoc?.filename || "").replace(/\.[^./]+$/, "");
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_BATCH_SIZE = 50, DEFAULT_MAX_INBOX_BYTES = 15e5, DEFAULT_CONCURRENCY = 4, DEFAULT_TIME_BUDGET_MS = 10 * 6e4, PROJECT_QUERY = `*[_id == $id][0]{
|
|
6
|
+
_id,
|
|
7
|
+
smartcatProjectId,
|
|
8
|
+
smartcatLanguages[]{sanityId, smartcatLanguage},
|
|
9
|
+
"items": items[]{ "_id": ${ITEM_ID}, "title": ${itemDocDerefRespectingDraft(".title")} },
|
|
10
|
+
progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
|
|
11
|
+
}`;
|
|
12
|
+
function stripDraft(id) {
|
|
13
|
+
return id.replace(/^drafts\./, "");
|
|
14
|
+
}
|
|
15
|
+
function sanityDocIdOf(locjson) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(locjson).properties?.["x-sanity"]?.documentId;
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function downloadBatch(smartcat, batch, emit) {
|
|
23
|
+
const contentByTarget = /* @__PURE__ */ new Map(), batchAttempted = !!(smartcat.exportDocumentsBatch && batch.length > 1);
|
|
24
|
+
let batchFailed = !1;
|
|
25
|
+
if (batchAttempted)
|
|
26
|
+
try {
|
|
27
|
+
const files2 = await smartcat.exportDocumentsBatch(batch.map((t) => t.smartcatDocumentId)), targetByDocId = new Map(batch.map((t) => [stripDraft(t.sourceDocId), t]));
|
|
28
|
+
for (const file of files2) {
|
|
29
|
+
const docId = sanityDocIdOf(file.content), target = docId ? targetByDocId.get(stripDraft(docId)) : void 0;
|
|
30
|
+
target && !contentByTarget.has(target) && contentByTarget.set(target, file.content);
|
|
31
|
+
}
|
|
32
|
+
contentByTarget.size < batch.length && emit({
|
|
33
|
+
level: "info",
|
|
34
|
+
message: `${batch.length - contentByTarget.size} of ${batch.length} file(s) missing from the batch archive \u2014 fetching them per-document`
|
|
35
|
+
});
|
|
36
|
+
} catch (err) {
|
|
37
|
+
batchFailed = !0, emit({
|
|
38
|
+
level: "error",
|
|
39
|
+
message: `Batch export of ${batch.length} document(s) failed \u2014 falling back to per-document export: ${err instanceof Error ? err.message : String(err)}`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const files = [];
|
|
43
|
+
let fallbackCount = 0;
|
|
44
|
+
for (const target of batch) {
|
|
45
|
+
let locjson = contentByTarget.get(target);
|
|
46
|
+
locjson === void 0 && (locjson = await smartcat.exportDocument(target.smartcatDocumentId), batchAttempted && fallbackCount++), files.push({ target, locjson });
|
|
47
|
+
}
|
|
48
|
+
return { files, fallbackCount, batchFailed };
|
|
49
|
+
}
|
|
50
|
+
async function runImportDownload(options) {
|
|
51
|
+
const {
|
|
52
|
+
sanity,
|
|
53
|
+
smartcat,
|
|
54
|
+
projectId,
|
|
55
|
+
now = () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
batchSize = DEFAULT_BATCH_SIZE,
|
|
57
|
+
maxInboxBytes = DEFAULT_MAX_INBOX_BYTES,
|
|
58
|
+
concurrency = DEFAULT_CONCURRENCY,
|
|
59
|
+
timeBudgetMs = DEFAULT_TIME_BUDGET_MS,
|
|
60
|
+
nowMs = Date.now,
|
|
61
|
+
onLog
|
|
62
|
+
} = options, log = [], emit = (line) => {
|
|
63
|
+
log.push(line), onLog?.(line);
|
|
64
|
+
}, project = await sanity.fetch(PROJECT_QUERY, { id: projectId });
|
|
65
|
+
if (!project) throw new Error(`Translation project ${projectId} not found`);
|
|
66
|
+
try {
|
|
67
|
+
if (!project.smartcatProjectId)
|
|
68
|
+
throw new Error("Project has no smartcatProjectId \u2014 export it first");
|
|
69
|
+
const timestamp = now(), scProject = await smartcat.getProject(project.smartcatProjectId);
|
|
70
|
+
emit({ level: "info", message: "Checking Smartcat for completed translations" });
|
|
71
|
+
const scDocById = new Map((scProject.documents ?? []).filter((d) => d.id).map((d) => [d.id, d])), progress = buildProgress(
|
|
72
|
+
itemsFromProject(project),
|
|
73
|
+
scProject,
|
|
74
|
+
project.progress,
|
|
75
|
+
timestamp,
|
|
76
|
+
project.smartcatLanguages
|
|
77
|
+
), ready = [];
|
|
78
|
+
let skipped = 0, alreadyImported = 0;
|
|
79
|
+
for (const docProgress of progress)
|
|
80
|
+
for (const target of docProgress.targets) {
|
|
81
|
+
const id = target.smartcatDocumentId, label = id && smartcatDocPath(scDocById.get(id)) || docProgress.title || docProgress.sourceDocId;
|
|
82
|
+
if (!id || !hasConfirmedContent(target.stages)) {
|
|
83
|
+
id && !target.imported && (skipped++, emit({ level: "skip", message: `${label} (Smartcat ID: ${id}) \u2192 ${target.language}: not ready, skipped` }));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (target.imported) {
|
|
87
|
+
alreadyImported++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
ready.push({
|
|
91
|
+
key: target._key,
|
|
92
|
+
sourceDocId: docProgress.sourceDocId,
|
|
93
|
+
language: target.language,
|
|
94
|
+
smartcatDocumentId: id,
|
|
95
|
+
label
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const inbox = [];
|
|
99
|
+
let inboxBytes = 0, remaining = 0;
|
|
100
|
+
const byLanguage = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const target of ready) {
|
|
102
|
+
const group = byLanguage.get(target.language) ?? [];
|
|
103
|
+
group.push(target), byLanguage.set(target.language, group);
|
|
104
|
+
}
|
|
105
|
+
const batches = [];
|
|
106
|
+
for (const group of byLanguage.values())
|
|
107
|
+
for (let i = 0; i < group.length; i += batchSize)
|
|
108
|
+
batches.push(group.slice(i, i + batchSize));
|
|
109
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, batches.length));
|
|
110
|
+
batches.length > 0 && emit({
|
|
111
|
+
level: "info",
|
|
112
|
+
message: `Downloading ${ready.length} target(s) in ${batches.length} batch(es) of up to ${batchSize}, ${effectiveConcurrency} in parallel`
|
|
113
|
+
});
|
|
114
|
+
const startedAtMs = nowMs();
|
|
115
|
+
let deferredByBytes = 0, deferredByTime = 0, fallbackDownloads = 0, failedBatches = 0, nextBatch = 0;
|
|
116
|
+
const worker = async () => {
|
|
117
|
+
for (; nextBatch < batches.length; ) {
|
|
118
|
+
const index = nextBatch++, batch = batches[index];
|
|
119
|
+
if (inboxBytes >= maxInboxBytes) {
|
|
120
|
+
deferredByBytes += batch.length;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (nowMs() - startedAtMs >= timeBudgetMs) {
|
|
124
|
+
deferredByTime += batch.length;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const batchStartMs = nowMs(), { files, fallbackCount, batchFailed } = await downloadBatch(smartcat, batch, emit);
|
|
128
|
+
fallbackDownloads += fallbackCount, batchFailed && failedBatches++;
|
|
129
|
+
for (const { target, locjson } of files)
|
|
130
|
+
inbox.push({
|
|
131
|
+
_key: target.key,
|
|
132
|
+
sourceDocId: target.sourceDocId,
|
|
133
|
+
targetLanguage: target.language,
|
|
134
|
+
smartcatDocumentId: target.smartcatDocumentId,
|
|
135
|
+
locjson
|
|
136
|
+
}), inboxBytes += locjson.length;
|
|
137
|
+
emit({
|
|
138
|
+
level: "success",
|
|
139
|
+
message: `Batch ${index + 1}/${batches.length} (${batch[0].language}): ${files.length} doc(s) in ${((nowMs() - batchStartMs) / 1e3).toFixed(1)}s${fallbackCount ? ` \u2014 ${fallbackCount} via per-document fallback` : ""}`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return await Promise.all(Array.from({ length: effectiveConcurrency }, worker)), remaining = deferredByBytes + deferredByTime, ready.length > 0 && emit({
|
|
144
|
+
level: failedBatches || fallbackDownloads ? "error" : "info",
|
|
145
|
+
message: `Download phase: ${((nowMs() - startedAtMs) / 1e3).toFixed(1)}s \u2014 ${batches.length} batch(es), ${failedBatches} failed, ${fallbackDownloads} per-document fallback download(s); deferred ${deferredByBytes} (inbox size cap) + ${deferredByTime} (time budget)`
|
|
146
|
+
}), emit({
|
|
147
|
+
level: "info",
|
|
148
|
+
message: `Downloaded ${inbox.length} target(s)${skipped ? `, skipped ${skipped} not ready` : ""}${alreadyImported ? `, ${alreadyImported} already imported` : ""}${remaining ? `, ${remaining} deferred \u2014 run Import again to continue` : ""}`
|
|
149
|
+
}), await sanity.patch(projectId).set({
|
|
150
|
+
inbox,
|
|
151
|
+
progress,
|
|
152
|
+
progressSyncedAt: timestamp,
|
|
153
|
+
status: "downloaded",
|
|
154
|
+
// Deferred-target count for the dashboard: after applying this batch it
|
|
155
|
+
// auto-continues the import instead of asking for another click.
|
|
156
|
+
importRemaining: remaining,
|
|
157
|
+
lastError: null,
|
|
158
|
+
functionLog: JSON.stringify(log)
|
|
159
|
+
}).commit(), { downloaded: inbox.length, skipped, remaining, alreadyImported };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
+
throw emit({ level: "error", message }), await sanity.patch(projectId).set({ status: "error", lastError: message, lastImportAt: now(), functionLog: JSON.stringify(log) }).commit().catch(() => {
|
|
163
|
+
}), err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
runImportDownload
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.js","sources":["../src/import/index.ts"],"sourcesContent":["import type {SmartcatDocument, SmartcatProject} from '../smartcat/types'\nimport {buildProgress, hasConfirmedContent, itemsFromProject, type DocProgress} from '../progress/core'\nimport {ITEM_ID, itemDocDerefRespectingDraft} from '../lib/projectItems'\nimport type {SmartcatLanguageMapping} from '../lib/languageMap'\nimport type {LogLine} from '../lib/log'\n\n/** The Smartcat document's full path (folder + filename), extension stripped. */\nfunction smartcatDocPath(scDoc: SmartcatDocument | undefined): string {\n const raw = scDoc?.fullPath || scDoc?.name || scDoc?.filename || ''\n return raw.replace(/\\.[^./]+$/, '')\n}\n\n/** Minimal structural subset of @sanity/client used by the import download. */\nexport interface SanityLikeClient {\n fetch<T = unknown>(query: string, params?: Record<string, unknown>): Promise<T>\n patch(id: string): {\n set(attrs: Record<string, unknown>): {commit(): Promise<unknown>}\n }\n}\n\n/** Structural subset of SmartcatClient the import download needs. */\nexport interface SmartcatImportClient {\n getProject(projectId: string): Promise<SmartcatProject>\n exportDocument(documentId: string): Promise<string>\n /**\n * Batch export: one Smartcat task for many documents (ZIP result). Optional\n * so custom/legacy clients keep working — without it every document falls\n * back to a single {@link exportDocument} round-trip.\n */\n exportDocumentsBatch?(documentIds: string[]): Promise<{filename: string; content: string}[]>\n}\n\nexport interface RunImportDownloadOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatImportClient\n /** `_id` of the `smartcat.translationProject` to import. */\n projectId: string\n now?: () => string\n /** Documents per Smartcat batch-export task. */\n batchSize?: number\n /**\n * Soft cap on the total LocJSON bytes stored in the project's `inbox` per\n * run. The inbox lives on the Sanity project document, which has a practical\n * size limit — targets beyond the cap are reported as `remaining` and picked\n * up by the next import run.\n */\n maxInboxBytes?: number\n /** How many batch-export tasks run against Smartcat concurrently. */\n concurrency?: number\n /**\n * Soft time budget for the download phase, in ms. Once exceeded, batches not\n * yet started are deferred as `remaining` and the run exits cleanly — so the\n * Function always finishes well before its hard platform timeout instead of\n * being killed mid-run (which would leave the project stuck in \"importing\").\n */\n timeBudgetMs?: number\n /** Clock override for tests (ms). */\n nowMs?: () => number\n /**\n * Receives every log line as it is emitted (in addition to the functionLog\n * stored on the project document) — the Function forwards these to console\n * so `sanity functions logs` shows the run without reading the document.\n */\n onLog?: (line: LogLine) => void\n}\n\nexport interface RunImportDownloadResult {\n /** Number of complete targets downloaded into the inbox. */\n downloaded: number\n /** Number of targets skipped because they are not yet fully translated. */\n skipped: number\n /** Targets ready to download but deferred to the next run (inbox size cap). */\n remaining: number\n /** Targets already imported in a previous run and therefore not re-downloaded. */\n alreadyImported: number\n}\n\nconst DEFAULT_BATCH_SIZE = 50\n/** Keeps the project document comfortably below Sanity's document size limits. */\nconst DEFAULT_MAX_INBOX_BYTES = 1_500_000\n/**\n * Parallel Smartcat export tasks. High enough to divide the download time ~4×,\n * low enough not to crowd the account's export-task queue.\n */\nconst DEFAULT_CONCURRENCY = 4\n/** Exit-early margin against the 15-minute Sanity Function timeout. */\nconst DEFAULT_TIME_BUDGET_MS = 10 * 60_000\n\nconst PROJECT_QUERY = `*[_id == $id][0]{\n _id,\n smartcatProjectId,\n smartcatLanguages[]{sanityId, smartcatLanguage},\n \"items\": items[]{ \"_id\": ${ITEM_ID}, \"title\": ${itemDocDerefRespectingDraft('.title')} },\n progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}\n}`\n\ninterface ProjectData {\n _id: string\n smartcatProjectId?: string\n smartcatLanguages?: SmartcatLanguageMapping[]\n items?: {_id?: string; title?: string}[]\n progress?: DocProgress[]\n}\n\ninterface InboxItem {\n _key: string\n sourceDocId: string\n targetLanguage: string\n smartcatDocumentId: string\n locjson: string\n}\n\n/** A (document × language) target selected for download this run. */\ninterface ReadyTarget {\n key: string\n sourceDocId: string\n language: string\n smartcatDocumentId: string\n label: string\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/** The Sanity source-document id a LocJSON carries in its `x-sanity` metadata. */\nfunction sanityDocIdOf(locjson: string): string | undefined {\n try {\n const parsed = JSON.parse(locjson) as {properties?: {'x-sanity'?: {documentId?: string}}}\n return parsed.properties?.['x-sanity']?.documentId\n } catch {\n return undefined\n }\n}\n\ninterface BatchDownloadResult {\n files: {target: ReadyTarget; locjson: string}[]\n /** Targets fetched one-by-one because the batched call failed or its archive missed them. */\n fallbackCount: number\n /** The batched export call itself failed (the error line is already emitted). */\n batchFailed: boolean\n}\n\n/**\n * Downloads a batch of same-language targets. Uses one batched export task\n * (ZIP) when the client supports it, correlating each returned file back to\n * its target via the LocJSON's own `x-sanity.documentId`. Targets the archive\n * didn't cover — and the whole batch when the batched call fails — fall back\n * to per-document exports, so a batch problem degrades to the old (slow but\n * proven) path instead of failing the run. Every degradation is logged loudly:\n * a silent fallback looks exactly like \"batching deployed but nothing faster\".\n */\nasync function downloadBatch(\n smartcat: SmartcatImportClient,\n batch: ReadyTarget[],\n emit: (line: LogLine) => void,\n): Promise<BatchDownloadResult> {\n const contentByTarget = new Map<ReadyTarget, string>()\n const batchAttempted = Boolean(smartcat.exportDocumentsBatch && batch.length > 1)\n let batchFailed = false\n\n if (batchAttempted) {\n try {\n const files = await smartcat.exportDocumentsBatch!(batch.map((t) => t.smartcatDocumentId))\n const targetByDocId = new Map(batch.map((t) => [stripDraft(t.sourceDocId), t]))\n for (const file of files) {\n const docId = sanityDocIdOf(file.content)\n const target = docId ? targetByDocId.get(stripDraft(docId)) : undefined\n if (target && !contentByTarget.has(target)) contentByTarget.set(target, file.content)\n }\n if (contentByTarget.size < batch.length) {\n emit({\n level: 'info',\n message: `${batch.length - contentByTarget.size} of ${batch.length} file(s) missing from the batch archive — fetching them per-document`,\n })\n }\n } catch (err) {\n batchFailed = true\n emit({\n level: 'error',\n message: `Batch export of ${batch.length} document(s) failed — falling back to per-document export: ${err instanceof Error ? err.message : String(err)}`,\n })\n }\n }\n\n const files: {target: ReadyTarget; locjson: string}[] = []\n let fallbackCount = 0\n for (const target of batch) {\n let locjson = contentByTarget.get(target)\n if (locjson === undefined) {\n locjson = await smartcat.exportDocument(target.smartcatDocumentId)\n if (batchAttempted) fallbackCount++\n }\n files.push({target, locjson})\n }\n return {files, fallbackCount, batchFailed}\n}\n\n/**\n * Thin import step run by a Sanity Function: refreshes per-stage progress from\n * Smartcat, then downloads the ready targets into the project's `inbox` and\n * sets status to `downloaded`. The browser turns the inbox into locale\n * variants and decides final completion.\n *\n * Downloads happen in **batches** (one Smartcat export task per ~50 documents,\n * returned as a ZIP), so run time no longer scales with per-document polling —\n * large projects fit comfortably inside the Function timeout. The inbox is\n * size-capped per run; overflow targets are reported as `remaining` and the\n * next import run picks them up (already-imported targets are skipped).\n *\n * Targets with nothing confirmed are skipped, so no untranslated variants are\n * created. Does no content processing, so it depends only on the Smartcat client.\n */\nexport async function runImportDownload(\n options: RunImportDownloadOptions,\n): Promise<RunImportDownloadResult> {\n const {\n sanity,\n smartcat,\n projectId,\n now = () => new Date().toISOString(),\n batchSize = DEFAULT_BATCH_SIZE,\n maxInboxBytes = DEFAULT_MAX_INBOX_BYTES,\n concurrency = DEFAULT_CONCURRENCY,\n timeBudgetMs = DEFAULT_TIME_BUDGET_MS,\n nowMs = Date.now,\n onLog,\n } = options\n const log: LogLine[] = []\n const emit = (line: LogLine) => {\n log.push(line)\n onLog?.(line)\n }\n\n const project = await sanity.fetch<ProjectData | null>(PROJECT_QUERY, {id: projectId})\n if (!project) throw new Error(`Translation project ${projectId} not found`)\n\n try {\n if (!project.smartcatProjectId) {\n throw new Error('Project has no smartcatProjectId — export it first')\n }\n\n const timestamp = now()\n const scProject = await smartcat.getProject(project.smartcatProjectId)\n emit({level: 'info', message: 'Checking Smartcat for completed translations'})\n // Look up each target's Smartcat document by its id, for full-path logging.\n const scDocById = new Map((scProject.documents ?? []).filter((d) => d.id).map((d) => [d.id, d]))\n\n // Rebuild progress from Smartcat + the project's items, so the dashboard\n // reflects the latest state even for targets we don't import this round.\n const progress = buildProgress(\n itemsFromProject(project),\n scProject,\n project.progress,\n timestamp,\n project.smartcatLanguages,\n )\n\n // Collect every target that has confirmed content (at any stage); targets\n // with nothing confirmed are skipped. Targets already imported in an\n // earlier run are not re-downloaded, so capped runs make forward progress.\n const ready: ReadyTarget[] = []\n let skipped = 0\n let alreadyImported = 0\n for (const docProgress of progress) {\n for (const target of docProgress.targets) {\n const id = target.smartcatDocumentId\n // Prefer the Smartcat full path; fall back to the resolved title / source id.\n const label =\n (id && smartcatDocPath(scDocById.get(id))) || docProgress.title || docProgress.sourceDocId\n if (!id || !hasConfirmedContent(target.stages)) {\n if (id && !target.imported) {\n skipped++\n emit({level: 'skip', message: `${label} (Smartcat ID: ${id}) → ${target.language}: not ready, skipped`})\n }\n continue\n }\n if (target.imported) {\n alreadyImported++\n continue\n }\n ready.push({\n key: target._key,\n sourceDocId: docProgress.sourceDocId,\n language: target.language,\n smartcatDocumentId: id,\n label,\n })\n }\n }\n\n // Download in batches: one Smartcat export task per batch (ZIP result)\n // instead of a request+poll round-trip per document. Batches are grouped\n // by language so each source document appears at most once per archive.\n const inbox: InboxItem[] = []\n let inboxBytes = 0\n let remaining = 0\n const byLanguage = new Map<string, ReadyTarget[]>()\n for (const target of ready) {\n const group = byLanguage.get(target.language) ?? []\n group.push(target)\n byLanguage.set(target.language, group)\n }\n\n const batches: ReadyTarget[][] = []\n for (const group of byLanguage.values()) {\n for (let i = 0; i < group.length; i += batchSize) {\n batches.push(group.slice(i, i + batchSize))\n }\n }\n\n // Worker pool: up to `concurrency` batch-export tasks in flight at once —\n // Smartcat assembles the archives in parallel, dividing the download time.\n // Each worker re-checks the caps before claiming the next batch:\n // - inbox byte cap: the inbox lives on the project document, which must\n // stay below Sanity's document size limit;\n // - time budget: exit cleanly before the platform kills the Function, so a\n // slow Smartcat day degrades to \"continue in the next run\", never to a\n // project stuck in \"importing\".\n const effectiveConcurrency = Math.max(1, Math.min(concurrency, batches.length))\n if (batches.length > 0) {\n emit({\n level: 'info',\n message: `Downloading ${ready.length} target(s) in ${batches.length} batch(es) of up to ${batchSize}, ${effectiveConcurrency} in parallel`,\n })\n }\n const startedAtMs = nowMs()\n let deferredByBytes = 0\n let deferredByTime = 0\n let fallbackDownloads = 0\n let failedBatches = 0\n let nextBatch = 0\n const worker = async () => {\n while (nextBatch < batches.length) {\n const index = nextBatch++\n const batch = batches[index]\n if (inboxBytes >= maxInboxBytes) {\n deferredByBytes += batch.length\n continue\n }\n if (nowMs() - startedAtMs >= timeBudgetMs) {\n deferredByTime += batch.length\n continue\n }\n const batchStartMs = nowMs()\n const {files, fallbackCount, batchFailed} = await downloadBatch(smartcat, batch, emit)\n fallbackDownloads += fallbackCount\n if (batchFailed) failedBatches++\n for (const {target, locjson} of files) {\n inbox.push({\n _key: target.key,\n sourceDocId: target.sourceDocId,\n targetLanguage: target.language,\n smartcatDocumentId: target.smartcatDocumentId,\n locjson,\n })\n inboxBytes += locjson.length\n }\n emit({\n level: 'success',\n message:\n `Batch ${index + 1}/${batches.length} (${batch[0].language}): ${files.length} doc(s) in ` +\n `${((nowMs() - batchStartMs) / 1000).toFixed(1)}s` +\n `${fallbackCount ? ` — ${fallbackCount} via per-document fallback` : ''}`,\n })\n }\n }\n await Promise.all(Array.from({length: effectiveConcurrency}, worker))\n remaining = deferredByBytes + deferredByTime\n\n // One line that answers \"where did the time go and why was anything left\n // behind\" — the silent-degradation postmortems start here.\n if (ready.length > 0) {\n emit({\n level: failedBatches || fallbackDownloads ? 'error' : 'info',\n message:\n `Download phase: ${((nowMs() - startedAtMs) / 1000).toFixed(1)}s — ` +\n `${batches.length} batch(es), ${failedBatches} failed, ${fallbackDownloads} per-document fallback download(s); ` +\n `deferred ${deferredByBytes} (inbox size cap) + ${deferredByTime} (time budget)`,\n })\n }\n\n emit({\n level: 'info',\n message:\n `Downloaded ${inbox.length} target(s)` +\n `${skipped ? `, skipped ${skipped} not ready` : ''}` +\n `${alreadyImported ? `, ${alreadyImported} already imported` : ''}` +\n `${remaining ? `, ${remaining} deferred — run Import again to continue` : ''}`,\n })\n\n await sanity\n .patch(projectId)\n .set({\n inbox,\n progress,\n progressSyncedAt: timestamp,\n status: 'downloaded',\n // Deferred-target count for the dashboard: after applying this batch it\n // auto-continues the import instead of asking for another click.\n importRemaining: remaining,\n lastError: null,\n functionLog: JSON.stringify(log),\n })\n .commit()\n\n return {downloaded: inbox.length, skipped, remaining, alreadyImported}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n emit({level: 'error', message})\n await sanity\n .patch(projectId)\n .set({status: 'error', lastError: message, lastImportAt: now(), functionLog: JSON.stringify(log)})\n .commit()\n .catch(() => {})\n throw err\n }\n}\n"],"names":["files"],"mappings":"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"}
|