@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 @@
|
|
|
1
|
+
{"version":3,"file":"index2.js","sources":["../../src/lib/locjson/fields.ts","../../src/lib/locjson/paths.ts","../../src/lib/locjson/portableText.ts","../../src/lib/locjson/serialize.ts","../../src/lib/locjson/filename.ts","../../src/lib/locjson/deserialize.ts"],"sourcesContent":["import type {LocalizationMode, TranslatableField, TranslatableLeaf, ValueShape} from './types'\n\nexport interface GetTranslatableFieldsOptions {\n /**\n * Field names to exclude (e.g. the i18n language field, passed by the caller\n * as the configured `documentI18nPluginLanguageField`). Defaults to none.\n */\n exclude?: string[]\n}\n\ninterface CompiledType {\n jsonType?: string\n name?: string\n title?: string\n of?: CompiledType[]\n fields?: CompiledField[]\n options?: {\n documentInternationalization?: {exclude?: boolean}\n smartcatTranslation?: {include?: boolean}\n }\n}\n\n/**\n * True when a field is marked non-translatable via document-internationalization's\n * own field option (`options.documentInternationalization.exclude`). Honoring it\n * means consumers exclude fields per type using the i18n plugin's native marker —\n * no separate config here.\n */\nfunction isExcludedByDocumentI18n(field: CompiledField): boolean {\n return field.type?.options?.documentInternationalization?.exclude === true\n}\n\ninterface CompiledField {\n name: string\n type?: CompiledType\n}\n\n/**\n * True for a Sanity **system** type — the reserved `sanity.` namespace, notably\n * the `sanity.imageAsset` / `sanity.fileAsset` documents that image/file fields\n * reference. Their string fields (`originalFilename`, `path`, `url`, …) look\n * translatable but must never be sent: assets are shared across locales, and\n * translating a filename or CDN path corrupts the reference. Excluding them here\n * keeps them out of both the linked-document crawl and export serialization,\n * since both decide translatability via {@link getTranslatableFields}.\n */\nexport function isSystemType(typeName: unknown): boolean {\n return typeof typeName === 'string' && typeName.startsWith('sanity.')\n}\n\n/**\n * True for a `sanity-plugin-internationalized-array` array type (e.g.\n * `internationalizedArrayString`). Detected structurally by naming convention so\n * we never need a dependency on that plugin.\n */\nexport function isInternationalizedArrayType(type?: CompiledType): boolean {\n return (\n type?.jsonType === 'array' &&\n typeof type.name === 'string' &&\n type.name.startsWith('internationalizedArray')\n )\n}\n\n/** True for an array member `_type` like `internationalizedArrayStringValue`. */\nexport function isInternationalizedArrayMemberType(typeName: unknown): boolean {\n return typeof typeName === 'string' && /^internationalizedArray.+Value$/.test(typeName)\n}\n\n/** The inner value type of an internationalized-array field (its member's `value` field). */\nexport function innerValueType(arrayType?: CompiledType): CompiledType | undefined {\n return arrayType?.of?.[0]?.fields?.find((f) => f.name === 'value')?.type\n}\n\n/**\n * Localization mode of a **compiled** schema type: `field` if it declares any\n * internationalized-array field (in which case only those fields are\n * translatable), otherwise `document`. A pure function of the type, so callers\n * should compute it once per type.\n */\nexport function localizationMode(compiledType: unknown): LocalizationMode {\n const fields = (compiledType as CompiledType)?.fields\n if (!Array.isArray(fields)) return 'document'\n return fields.some((f) => isInternationalizedArrayType(f.type)) ? 'field' : 'document'\n}\n\n/**\n * Selects translatable fields from a **compiled** Sanity schema type.\n *\n * Document-mode types (default policy, installation-agnostic):\n * - `string` and `text` fields -> plain units\n * - Portable Text (arrays containing `block` members) -> HTML units\n * - everything else (slug, url, datetime, number, boolean, reference, image,\n * non-block arrays, objects, system `_` fields, excluded names) is excluded.\n *\n * Field-mode types (any internationalized-array field present): only the\n * internationalized-array fields are translatable; each is classified by its\n * inner `value` type (string/text -> plain, Portable Text -> HTML).\n *\n * The compiled schema shape isn't strongly typed publicly, so this reads it\n * defensively. Callers that need overrides can post-filter the result.\n */\nexport function getTranslatableFields(\n compiledType: unknown,\n options: GetTranslatableFieldsOptions = {},\n): TranslatableField[] {\n const rootType = compiledType as CompiledType\n if (isSystemType(rootType?.name)) return []\n if (!Array.isArray(rootType?.fields)) return []\n const result: TranslatableField[] = []\n walkFields(rootType, '', localizationMode(rootType), new Set(options.exclude ?? []), result)\n return result\n}\n\n/**\n * Recursively selects translatable fields. In field-mode it descends through\n * plain object fields (so nested internationalized-array fields, e.g.\n * `benefits.items`, are reached) and classifies each i18n field's member value\n * shape. Document-mode keeps the flat top-level behavior.\n */\nfunction walkFields(\n compiledType: CompiledType | undefined,\n prefix: string,\n mode: LocalizationMode,\n exclude: Set<string>,\n out: TranslatableField[],\n): void {\n for (const field of compiledType?.fields ?? []) {\n // `exclude` (the i18n language field) only ever appears at the top level.\n if (!field?.name || field.name.startsWith('_') || (prefix === '' && exclude.has(field.name))) continue\n const forceInclude = field.type?.options?.smartcatTranslation?.include === true\n if (isExcludedByDocumentI18n(field) && !forceInclude) continue\n const path = prefix ? `${prefix}.${field.name}` : field.name\n const title = field.type?.title || titleCase(field.name)\n\n if (mode === 'field') {\n if (isInternationalizedArrayType(field.type)) {\n const inner = innerValueType(field.type)\n const shape = classifyValueShape(inner) ?? (forceInclude ? classifyValueShapeByJsonType(inner) : null)\n if (shape) out.push({path, localization: 'field', kind: shape.container === 'scalar' ? shape.kind : 'plain', title, value: shape})\n else if (forceInclude) warnUntranslatableInclude(field.name, inner)\n } else if (isPlainObject(field.type)) {\n walkFields(field.type, path, mode, exclude, out)\n }\n } else {\n const kind = classifyField(field.type) ?? (forceInclude ? classifyByJsonType(field.type) : null)\n if (kind) out.push({path, localization: 'document', kind, title, value: {container: 'scalar', kind}})\n else if (forceInclude) warnUntranslatableInclude(field.name, field.type)\n }\n }\n}\n\n/** One translatable leaf of a field, expanded against a concrete source value. */\nexport interface FieldLeaf {\n /** Unique unit key / x-sanity fieldPath. */\n fullKey: string\n /** Path within the member value to the leaf (empty = the value itself). */\n valuePath: string\n kind: TranslatableField['kind']\n title?: string\n}\n\n/**\n * Expand a field into its translatable leaves against a concrete source value\n * (the source-language member value for field-mode). Serialize and import both\n * call this with the SAME source value, so the unit keys they compute match.\n */\nexport function enumerateLeaves(field: TranslatableField, sourceValue: unknown): FieldLeaf[] {\n const base = field.path\n const shape: ValueShape = field.value ?? {container: 'scalar', kind: field.kind}\n if (shape.container === 'scalar') {\n return [{fullKey: base, valuePath: '', kind: shape.kind, title: field.title}]\n }\n if (shape.container === 'arrayOfScalar') {\n const arr = Array.isArray(sourceValue) ? sourceValue : []\n return arr.map((_, i) => ({fullKey: `${base}[${i}]`, valuePath: `[${i}]`, kind: shape.kind, title: field.title}))\n }\n if (shape.container === 'object') {\n return shape.leaves.map((l) => ({fullKey: `${base}.${l.subPath}`, valuePath: l.subPath, kind: l.kind, title: l.title ?? field.title}))\n }\n const arr = Array.isArray(sourceValue) ? sourceValue : []\n const out: FieldLeaf[] = []\n for (const item of arr) {\n const key = (item as Record<string, unknown>)?._key\n if (typeof key !== 'string') continue\n for (const l of shape.leaves) {\n const valuePath = l.subPath ? `[${key}].${l.subPath}` : `[${key}]`\n out.push({fullKey: `${base}${valuePath}`, valuePath, kind: l.kind, title: l.title ?? field.title})\n }\n }\n return out\n}\n\n/** A recursable plain object (not a reference/image/file/slug/geopoint). */\nfunction isPlainObject(type?: CompiledType): boolean {\n if (type?.jsonType !== 'object' || !Array.isArray(type.fields)) return false\n if (type.fields.some((f) => f.name === '_ref')) return false\n return !['reference', 'image', 'file', 'slug', 'geopoint'].includes(type.name ?? '')\n}\n\n/** Classify an internationalized-array member's `value` type into a ValueShape. */\nfunction classifyValueShape(inner?: CompiledType): ValueShape | null {\n if (!inner) return null\n const scalar = classifyField(inner)\n if (scalar) return {container: 'scalar', kind: scalar}\n if (inner.jsonType === 'array' && Array.isArray(inner.of)) {\n const member = inner.of[0]\n if (member?.jsonType === 'string' && (member.name === 'string' || member.name === 'text')) {\n return {container: 'arrayOfScalar', kind: 'plain'}\n }\n if (isPlainObject(member)) {\n const leaves = collectLeaves(member, '')\n if (leaves.length) return {container: 'arrayOfObject', leaves}\n }\n return null\n }\n if (isPlainObject(inner)) {\n const leaves = collectLeaves(inner, '')\n if (leaves.length) return {container: 'object', leaves}\n }\n return null\n}\n\n/** ValueShape by jsonType only (for `smartcatTranslation.include` overrides). */\nfunction classifyValueShapeByJsonType(inner?: CompiledType): ValueShape | null {\n const kind = classifyByJsonType(inner)\n return kind ? {container: 'scalar', kind} : null\n}\n\n/** Translatable string/text/Portable-Text leaves within an object (recursing plain objects). */\nfunction collectLeaves(objType: CompiledType | undefined, prefix: string): TranslatableLeaf[] {\n const leaves: TranslatableLeaf[] = []\n for (const f of objType?.fields ?? []) {\n if (!f?.name || f.name.startsWith('_')) continue\n if (isExcludedByDocumentI18n(f) && f.type?.options?.smartcatTranslation?.include !== true) continue\n const subPath = prefix ? `${prefix}.${f.name}` : f.name\n const kind = classifyField(f.type)\n if (kind) leaves.push({subPath, kind, title: f.type?.title || titleCase(f.name)})\n else if (isPlainObject(f.type)) leaves.push(...collectLeaves(f.type, subPath))\n }\n return leaves\n}\n\n/** True for an array whose members include a Portable Text `block` type. */\nfunction isPortableTextArray(type: CompiledType): boolean {\n return (\n type.jsonType === 'array' &&\n Array.isArray(type.of) &&\n type.of.some((member) => member?.name === 'block' || member?.jsonType === 'block')\n )\n}\n\nfunction classifyField(type?: CompiledType): TranslatableField['kind'] | null {\n if (!type) return null\n\n // Plain text: only the `string` and `text` base types (not url, slug, datetime…).\n if (type.jsonType === 'string' && (type.name === 'string' || type.name === 'text')) {\n return 'plain'\n }\n\n if (isPortableTextArray(type)) return 'portableText'\n\n return null\n}\n\n/**\n * Classify by base `jsonType` only, ignoring the type name. Used for fields\n * explicitly opted in with `smartcatTranslation.include`, where the name isn't in\n * the default allowlist (e.g. a custom string alias) but the underlying shape is\n * still translatable.\n */\nfunction classifyByJsonType(type?: CompiledType): TranslatableField['kind'] | null {\n if (!type) return null\n if (type.jsonType === 'string') return 'plain'\n if (isPortableTextArray(type)) return 'portableText'\n return null\n}\n\nfunction warnUntranslatableInclude(fieldName: string, type?: CompiledType): void {\n // eslint-disable-next-line no-console\n console.warn(\n `[smartcat-translation] Field \"${fieldName}\" is marked smartcatTranslation.include ` +\n `but its type (name: ${JSON.stringify(type?.name)}, jsonType: ${JSON.stringify(type?.jsonType)}) ` +\n `has no translatable representation; skipping.`,\n )\n}\n\nfunction titleCase(name: string): string {\n return name.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase())\n}\n\n/**\n * Selects translatable fields from a document's **data** (no schema needed) —\n * for use in Sanity Functions, which run server-side without the Studio schema.\n *\n * Applies the same homogeneity rule as {@link getTranslatableFields}: if any\n * field holds internationalized-array members, the document is field-mode and\n * only those fields are selected (classified by a sample member's `value`);\n * otherwise top-level `string`/Portable Text fields are selected (document-mode).\n */\nexport function inferTranslatableFields(\n doc: Record<string, unknown>,\n options: GetTranslatableFieldsOptions = {},\n): TranslatableField[] {\n if (isSystemType(doc._type)) return []\n const exclude = new Set(options.exclude ?? [])\n\n const isI18nArray = (value: unknown): value is Array<{value?: unknown}> =>\n Array.isArray(value) &&\n value.some((m) => m && typeof m === 'object' && isInternationalizedArrayMemberType((m as {_type?: unknown})._type))\n\n const entries = Object.entries(doc).filter(([key]) => !key.startsWith('_') && !exclude.has(key))\n const fieldMode = entries.some(([, value]) => isI18nArray(value))\n\n const result: TranslatableField[] = []\n for (const [key, value] of entries) {\n if (fieldMode) {\n if (!isI18nArray(value)) continue\n const sample = value.find((m) => m && typeof m === 'object')?.value\n const kind = sampleKind(sample)\n if (kind) result.push({path: key, kind, localization: 'field', title: titleCase(key)})\n } else if (typeof value === 'string') {\n if (value.length > 0) result.push({path: key, kind: 'plain', localization: 'document', title: titleCase(key)})\n } else if (Array.isArray(value) && value.some(isPortableTextBlock)) {\n result.push({path: key, kind: 'portableText', localization: 'document', title: titleCase(key)})\n }\n }\n return result\n}\n\nfunction sampleKind(value: unknown): TranslatableField['kind'] | null {\n if (typeof value === 'string') return 'plain'\n if (Array.isArray(value) && value.some(isPortableTextBlock)) return 'portableText'\n return null\n}\n\nfunction isPortableTextBlock(value: unknown): boolean {\n return Boolean(value && typeof value === 'object' && (value as {_type?: string})._type === 'block')\n}\n\n/**\n * Reorders fields to match `orderedPaths` (e.g. a schema's field order). Fields\n * not listed keep their original relative order and go last. Stable.\n */\nexport function orderFieldsBy(\n fields: TranslatableField[],\n orderedPaths: string[] | undefined,\n): TranslatableField[] {\n if (!orderedPaths || orderedPaths.length === 0) return fields\n const rank = new Map(orderedPaths.map((path, i) => [path, i]))\n const at = (path: string) => (rank.has(path) ? (rank.get(path) as number) : Number.MAX_SAFE_INTEGER)\n return fields\n .map((field, i) => ({field, i}))\n .sort((a, b) => at(a.field.path) - at(b.field.path) || a.i - b.i)\n .map((x) => x.field)\n}\n","/**\n * Tiny path grammar for addressing translatable leaves nested inside a field's\n * value: `.field` for object properties, `[key]` for array members by `_key`,\n * `[n]` for array members by index. Used to expand array/object member values\n * into per-leaf LocJSON units and to overlay translations back onto a cloned\n * source value.\n *\n * e.g. `items[b1].value`, `tabs[2]`, `` (empty = the value itself).\n */\nexport type PathSeg = {field: string} | {key: string} | {index: number}\n\nexport function parsePath(path: string): PathSeg[] {\n const segs: PathSeg[] = []\n const re = /\\.?([^.[\\]]+)|\\[([^\\]]+)\\]/g\n let m: RegExpExecArray | null\n while ((m = re.exec(path)) !== null) {\n if (m[1] !== undefined) segs.push({field: m[1]})\n else if (/^\\d+$/.test(m[2])) segs.push({index: Number(m[2])})\n else segs.push({key: m[2]})\n }\n return segs\n}\n\n/** Append a segment to a base path string, producing a valid path. */\nexport function joinPath(base: string, seg: PathSeg): string {\n if ('field' in seg) return base ? `${base}.${seg.field}` : seg.field\n if ('index' in seg) return `${base}[${seg.index}]`\n return `${base}[${seg.key}]`\n}\n\nfunction child(container: unknown, seg: PathSeg): unknown {\n if (container == null || typeof container !== 'object') return undefined\n if ('field' in seg) return (container as Record<string, unknown>)[seg.field]\n if ('index' in seg) return Array.isArray(container) ? container[seg.index] : undefined\n return Array.isArray(container)\n ? container.find((m) => m && typeof m === 'object' && (m as Record<string, unknown>)._key === seg.key)\n : undefined\n}\n\n/** Read the value at a path from a root value (undefined if any segment misses). */\nexport function getAtPath(root: unknown, path: string): unknown {\n let cur = root\n for (const seg of parsePath(path)) {\n cur = child(cur, seg)\n if (cur === undefined) return undefined\n }\n return cur\n}\n\n/**\n * Return a deep clone of `root` with the leaf at `path` replaced by `value`.\n * An empty path replaces the whole value. Missing intermediate segments are a\n * no-op (the clone is returned unchanged), so a template that lacks a leaf can't\n * be corrupted.\n */\nexport function setAtPath<T>(root: T, path: string, value: unknown): T {\n const segs = parsePath(path)\n if (segs.length === 0) return value as T\n const clone = structuredClone(root)\n let cur: unknown = clone\n for (let i = 0; i < segs.length - 1; i++) {\n cur = child(cur, segs[i])\n if (cur == null || typeof cur !== 'object') return clone\n }\n const last = segs[segs.length - 1]\n if ('field' in last) (cur as Record<string, unknown>)[last.field] = value\n else if ('index' in last && Array.isArray(cur)) cur[last.index] = value\n else if ('key' in last && Array.isArray(cur)) {\n const idx = cur.findIndex((m) => m && typeof m === 'object' && (m as Record<string, unknown>)._key === last.key)\n if (idx >= 0) cur[idx] = value\n }\n return clone\n}\n","import {escapeHTML, toHTML} from '@portabletext/to-html'\nimport {htmlToBlocks, type DeserializerRule} from '@sanity/block-tools'\n\n/** A Portable Text block (loosely typed — shape varies by schema). */\nexport type PortableTextBlock = Record<string, unknown>\n\n/**\n * Custom (non-`block`) top-level Portable Text types that we *can* round-trip\n * through HTML, and so must NOT treat as unserializable. Each needs both a\n * `toHTML` component (below) and an `htmlToBlocks` rule (see {@link tableRule})\n * so the block survives export → translation → import intact.\n *\n * Currently just `@sanity/table`: `{_type:'table', rows:[{_type:'tableRow',\n * cells: string[]}]}` — plain-string cells, no header row, no col/row spans.\n */\nconst SERIALIZABLE_CUSTOM_TYPES = new Set(['table'])\n\n/** Renders a `@sanity/table` block to an HTML `<table>` for translation. */\nfunction tableToHtml(value: unknown): string {\n const rows = Array.isArray((value as {rows?: unknown})?.rows) ? (value as {rows: unknown[]}).rows : []\n const trs = rows\n .map((row) => {\n const cells = Array.isArray((row as {cells?: unknown})?.cells) ? (row as {cells: unknown[]}).cells : []\n const tds = cells.map((cell) => `<td>${escapeHTML(typeof cell === 'string' ? cell : '')}</td>`).join('')\n return `<tr>${tds}</tr>`\n })\n .join('')\n return `<table><tbody>${trs}</tbody></table>`\n}\n\n/**\n * An `htmlToBlocks` rule that turns a translated `<table>` back into a\n * `@sanity/table` block. Rows get deterministic index-based `_key`s (the whole\n * field value is replaced on import, so keys need only be unique within the\n * table); cells are plain strings, matching the schema.\n */\nconst tableRule: DeserializerRule = {\n deserialize(el, _next, createBlock) {\n if ((el as Element).tagName?.toLowerCase() !== 'table') return undefined\n const rows = Array.from((el as Element).querySelectorAll('tr')).map((tr, i) => ({\n _type: 'tableRow',\n _key: `row${i}`,\n cells: Array.from(tr.querySelectorAll('td, th')).map((cell) => cell.textContent ?? ''),\n }))\n return createBlock({_type: 'table', rows}) as never\n },\n}\n\n/** Parses an HTML string into a DOM Document. Provided by the environment. */\nexport type ParseHtml = (html: string) => Document\n\n/**\n * Renders Portable Text to an HTML string for translation.\n * Whole-field: the entire block array becomes one HTML document fragment.\n */\nexport function portableTextToHtml(blocks: PortableTextBlock[] | undefined | null): string {\n if (!blocks || blocks.length === 0) return ''\n return toHTML(blocks as never, {\n // Fallback only. Serialization skips any field containing an unserializable\n // block up front (see findUnserializableBlockTypes), so to-html should never\n // meet an unknown type here. If one slips through, render it as empty rather\n // than let @portabletext/to-html emit its default placeholder — a hidden\n // `<div style=\"display:none\">Unknown block type \"…\"</div>` whose English text\n // would otherwise be sent to Smartcat and translated into every locale.\n // (`onMissingComponent: false` only silences the warning; it does NOT stop the\n // placeholder — the `unknownType` override is what suppresses it.)\n onMissingComponent: false,\n components: {\n unknownType: () => '',\n // Serializable custom block types get real markup so their text is\n // translatable and can be parsed back (see SERIALIZABLE_CUSTOM_TYPES).\n types: {table: ({value}) => tableToHtml(value)},\n },\n })\n}\n\n/**\n * Distinct `_type`s in a Portable Text value that `@portabletext/to-html` cannot\n * serialize: any top-level member that isn't a standard `block` (and isn't in\n * {@link SERIALIZABLE_CUSTOM_TYPES}, e.g. `table`, which we render explicitly),\n * or any inline child that isn't a `span` (e.g. an inline object). Such nodes\n * render to nothing, so a field containing them can't survive the HTML\n * round-trip — the caller skips the whole field and warns rather than silently\n * drop or corrupt it. Marks/annotations are intentionally not included: their\n * text survives (only the annotation would be lost).\n */\nexport function findUnserializableBlockTypes(\n blocks: PortableTextBlock[] | undefined | null,\n): string[] {\n if (!Array.isArray(blocks)) return []\n const types = new Set<string>()\n for (const block of blocks) {\n if (!block || typeof block !== 'object') continue\n const type = (block as {_type?: unknown})._type\n if (type !== 'block') {\n if (typeof type === 'string' && !SERIALIZABLE_CUSTOM_TYPES.has(type)) types.add(type)\n continue\n }\n const children = (block as {children?: unknown}).children\n if (!Array.isArray(children)) continue\n for (const child of children) {\n const childType = (child as {_type?: unknown} | null)?._type\n if (typeof childType === 'string' && childType !== 'span') types.add(childType)\n }\n }\n return [...types].sort()\n}\n\n/**\n * Extracts the visible text of Portable Text — the concatenation of every span's\n * `text` across all blocks (recursing into `children`). Used to decide whether a\n * field carries anything translatable: structural-but-textless blocks render to\n * non-empty HTML (`<p></p>`, `<br/>`, `<ul><li></li></ul>`), so emptiness must be\n * judged from the source blocks, not from stripping tags/entities out of the HTML\n * (which would mishandle ` ` and void elements).\n */\nexport function portableTextToPlainText(blocks: PortableTextBlock[] | undefined | null): string {\n if (!Array.isArray(blocks)) return ''\n let text = ''\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n node.forEach(visit)\n } else if (node && typeof node === 'object') {\n const value = node as {text?: unknown; children?: unknown; rows?: unknown}\n if (typeof value.text === 'string') text += value.text\n if (Array.isArray(value.children)) visit(value.children)\n // `table` blocks carry text in `rows[].cells[]` (plain strings), not spans;\n // count it so a table-only field isn't judged empty and skipped on export.\n if (Array.isArray(value.rows)) {\n for (const row of value.rows) {\n const cells = (row as {cells?: unknown})?.cells\n if (Array.isArray(cells)) for (const cell of cells) if (typeof cell === 'string') text += cell\n }\n }\n }\n }\n visit(blocks)\n return text\n}\n\n/**\n * Parses translated HTML back into Portable Text using the field's compiled\n * block content type. `parseHtml` supplies a DOM parser:\n * - Studio: (html) => new DOMParser().parseFromString(html, 'text/html')\n * - Functions: (html) => new JSDOM(html).window.document\n */\nexport function htmlToPortableText(\n html: string,\n blockContentType: unknown,\n parseHtml: ParseHtml,\n): PortableTextBlock[] {\n if (!html.trim()) return []\n return htmlToBlocks(html, blockContentType as never, {\n parseHtml,\n rules: [tableRule],\n }) as unknown as PortableTextBlock[]\n}\n","import {\n findUnserializableBlockTypes,\n portableTextToHtml,\n portableTextToPlainText,\n type PortableTextBlock,\n} from './portableText'\nimport {enumerateLeaves} from './fields'\nimport {getAtPath} from './paths'\nimport type {LocJSON, LocUnit, TranslatableField} from './types'\n\nexport interface SerializeOptions {\n sourceLanguage: string\n /**\n * How internationalized-array members identify their locale: a field name\n * (e.g. `'language'`) or `'_key'` (v4.x and lower). Resolved by the caller (see\n * `resolveConfig`); no default is assumed here.\n */\n fieldLanguageKey: string\n /**\n * Called when a Portable Text field is skipped because it contains block types\n * that can't be serialized (see {@link findUnserializableBlockTypes}). The\n * whole field is left out so its content isn't dropped or corrupted on the\n * round-trip; the caller surfaces this to the user's log.\n */\n onSkippedField?: (info: {fieldPath: string; title?: string; types: string[]}) => void\n}\n\n/** A minimal view of a Sanity document needed for serialization. */\nexport interface SerializableDocument {\n _id: string\n _type: string\n _rev?: string\n [key: string]: unknown\n}\n\n/**\n * Serializes a Sanity document into a LocJSON file: one unit per translatable\n * field. Plain fields become plain units (segments split on newlines); Portable\n * Text fields become whole-field HTML units (Smartcat segments them internally).\n *\n * Fields with no value are skipped, so the file only contains real source text.\n */\nexport function serializeToLocjson(\n doc: SerializableDocument,\n fields: TranslatableField[],\n options: SerializeOptions,\n): LocJSON {\n const units: LocUnit[] = []\n\n for (const field of fields) {\n // For field-level (internationalized-array) fields the source content is the\n // source-language member's `value` (the field may be nested through objects).\n const source =\n field.localization === 'field'\n ? memberValue(getAtPath(doc, field.path), options.sourceLanguage, options.fieldLanguageKey)\n : getAtPath(doc, field.path)\n\n // A scalar field yields one leaf; array/object member values yield one leaf\n // per translatable string inside (addressed by valuePath).\n for (const leaf of enumerateLeaves(field, source)) {\n const value = leaf.valuePath ? getAtPath(source, leaf.valuePath) : source\n const comments = leaf.title ? [leaf.title] : undefined\n\n if (leaf.kind === 'plain') {\n // Skip when there's no translatable text. Trim only for the check — the\n // stored source keeps its original whitespace so the round-trip is exact.\n if (typeof value !== 'string' || value.trim().length === 0) continue\n units.push({\n key: leaf.fullKey,\n properties: {comments, 'x-smartcat-split': 'full', 'x-sanity': {fieldPath: leaf.fullKey}},\n // Split into line segments but keep the trailing newline on each, so\n // concatenating the segments (LocJSON's '' join) reconstructs the original.\n source: value.split(/(?<=\\n)/),\n target: [],\n })\n } else {\n const blocks = value as PortableTextBlock[] | undefined\n // A field with a block to-html can't serialize (e.g. a custom `table`)\n // is skipped whole: partial HTML would drop that block on the round-trip.\n const unserializable = findUnserializableBlockTypes(blocks)\n if (unserializable.length > 0) {\n options.onSkippedField?.({fieldPath: field.path, title: field.title, types: unserializable})\n continue\n }\n // `html` is non-empty for structural-but-textless blocks (e.g. `<p></p>`,\n // `<br/>`), so also require actual visible text before emitting a unit.\n const html = portableTextToHtml(blocks)\n if (!html || portableTextToPlainText(blocks).trim().length === 0) continue\n units.push({\n key: leaf.fullKey,\n properties: {comments, 'x-smartcat-format': 'html', 'x-smartcat-split': 'full', 'x-sanity': {fieldPath: leaf.fullKey}},\n source: [html],\n target: [],\n })\n }\n }\n }\n\n return {\n properties: {\n version: 1,\n 'x-sanity': {\n documentId: doc._id,\n documentType: doc._type,\n rev: doc._rev,\n sourceLanguage: options.sourceLanguage,\n },\n },\n units,\n }\n}\n\n/**\n * Reads the `value` of an internationalized-array field's member for a given\n * language. Members are identified strictly by `languageKey` — a field name\n * (`'language'`, v5+) or `'_key'` (v4.x and lower).\n */\nfunction memberValue(field: unknown, language: string, languageKey: string): unknown {\n if (!Array.isArray(field)) return undefined\n const member = field.find(\n (m) => m && typeof m === 'object' && (m as Record<string, unknown>)[languageKey] === language,\n )\n return (member as {value?: unknown} | undefined)?.value\n}\n","/**\n * Builds the Smartcat document filename for a Sanity document:\n * \"<type>/<sanitized title>-<id prefix>.locjson\"\n * e.g. \"page/About Us-3f14f092.locjson\".\n *\n * The type acts as a folder; the id prefix guarantees uniqueness. Identity on\n * import comes from the file's `x-sanity` metadata, not this name, so renames\n * are safe. When the exported content came from the document's draft, a `-draft`\n * marker is appended so it's visible in Smartcat (`…-3f14f092-draft.locjson`).\n */\nexport function buildLocjsonFilename(\n documentType: string,\n documentId: string,\n title?: string,\n options: {draft?: boolean} = {},\n): string {\n const idPrefix = stripDraft(documentId).slice(0, 8) || 'unknown'\n const safeTitle = sanitizeSegment(title || 'Untitled')\n const safeType = sanitizeSegment(documentType) || 'document'\n const draftMarker = options.draft ? '-draft' : ''\n return `${safeType}/${safeTitle}-${idPrefix}${draftMarker}.locjson`\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/** Removes characters unsafe for file paths while keeping it human-readable. */\nfunction sanitizeSegment(value: string): string {\n return value\n .replace(/[/\\\\?%*:|\"<>]/g, ' ') // path-unsafe characters\n .replace(/\\s+/g, ' ')\n .trim()\n}\n","import {htmlToPortableText, type ParseHtml} from './portableText'\nimport type {LocJSON} from './types'\n\nexport interface DeserializeOptions {\n /** DOM parser for HTML units (DOMParser in Studio, jsdom in Functions). */\n parseHtml: ParseHtml\n /** Returns the compiled block content type for a Portable Text field path. */\n getBlockContentType: (fieldPath: string) => unknown\n}\n\nexport interface DeserializedDocument {\n documentId?: string\n documentType?: string\n sourceLanguage?: string\n /** Translated field values, keyed by field path. */\n fields: Record<string, unknown>\n /**\n * Source (pre-translation) text per field path, as the raw joined string. Used\n * to decide whether a field is JSON (so its translation can be validated before\n * import). Always the string form, even for HTML units.\n */\n sources: Record<string, string>\n}\n\n/**\n * Parses a (translated) LocJSON file back into Sanity field values.\n *\n * Uses each unit's `target` (falling back to `source` when a unit hasn't been\n * translated yet, so the result is always coherent). HTML units are converted\n * back to Portable Text via the field's compiled block content type.\n */\nexport function deserializeFromLocjson(locjson: LocJSON, options: DeserializeOptions): DeserializedDocument {\n const fileMeta = locjson.properties?.['x-sanity']\n const fields: Record<string, unknown> = {}\n const sources: Record<string, string> = {}\n\n for (const unit of locjson.units ?? []) {\n const fieldPath = unit.properties?.['x-sanity']?.fieldPath ?? unit.key\n const text = (unit.target?.length ? unit.target : unit.source) ?? []\n\n // LocJSON splits a string into arbitrary chunks with no implied delimiter,\n // so the original is always reconstructed by concatenating the segments.\n const value = text.join('')\n sources[fieldPath] = (unit.source ?? []).join('')\n\n if (unit.properties?.['x-smartcat-format'] === 'html') {\n fields[fieldPath] = htmlToPortableText(value, options.getBlockContentType(fieldPath), options.parseHtml)\n } else {\n fields[fieldPath] = value\n }\n }\n\n return {\n documentId: fileMeta?.documentId,\n documentType: fileMeta?.documentType,\n sourceLanguage: fileMeta?.sourceLanguage,\n fields,\n sources,\n }\n}\n"],"names":["child"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const TEMPLATE_PREFIX = "template:", STANDARD_AI_HUMAN = "standard:ai-human", STANDARD_AI = "standard:ai", STANDARD_WORKFLOW_OPTIONS = [
|
|
2
|
+
{ value: STANDARD_AI_HUMAN, label: "AI Translation + Human Review" },
|
|
3
|
+
{ value: STANDARD_AI, label: "AI Translation" }
|
|
4
|
+
];
|
|
5
|
+
function isWorkflowValid(workflow, templateIds) {
|
|
6
|
+
return workflow === STANDARD_AI_HUMAN || workflow === STANDARD_AI ? !0 : workflow.startsWith(TEMPLATE_PREFIX) ? templateIds.includes(workflow.slice(TEMPLATE_PREFIX.length)) : !1;
|
|
7
|
+
}
|
|
8
|
+
function resolveWorkflowCreateParams(workflow) {
|
|
9
|
+
return workflow && workflow.startsWith(TEMPLATE_PREFIX) ? { templateId: workflow.slice(TEMPLATE_PREFIX.length) } : workflow === STANDARD_AI ? { workflowStages: ["translation"] } : { workflowStages: ["translation", "postediting"] };
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
STANDARD_WORKFLOW_OPTIONS,
|
|
13
|
+
TEMPLATE_PREFIX,
|
|
14
|
+
isWorkflowValid,
|
|
15
|
+
resolveWorkflowCreateParams
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.js","sources":["../../src/lib/workflow.ts"],"sourcesContent":["/**\n * Workflow selection for new projects. A project stores a single `workflow`\n * string: either `template:<smartcatTemplateId>` or one of the standard presets.\n * The browser sets it at creation; the export Function decodes it into the\n * Smartcat `createProject` params (a template id, or explicit workflow stages).\n *\n * Dependency-free so it bundles into the thin export Function.\n */\n\nexport const TEMPLATE_PREFIX = 'template:'\nexport const STANDARD_AI_HUMAN = 'standard:ai-human'\nexport const STANDARD_AI = 'standard:ai'\n\n/** Backwards-compatible default: matches the previous hard-coded behavior. */\nexport const DEFAULT_WORKFLOW = STANDARD_AI_HUMAN\n\n/** Labels for the two standard presets (rendered under the \"Standard workflows\" group). */\nexport const STANDARD_WORKFLOW_OPTIONS: {value: string; label: string}[] = [\n {value: STANDARD_AI_HUMAN, label: 'AI Translation + Human Review'},\n {value: STANDARD_AI, label: 'AI Translation'},\n]\n\nexport interface WorkflowCreateParams {\n templateId?: string\n workflowStages?: string[]\n}\n\n/**\n * Whether a `workflow` value is currently selectable: the standard presets always\n * are; a `template:<id>` is valid only while that id still exists in the cached\n * template list. Empty / unknown values are invalid (no selection).\n */\nexport function isWorkflowValid(workflow: string, templateIds: string[]): boolean {\n if (workflow === STANDARD_AI_HUMAN || workflow === STANDARD_AI) return true\n if (workflow.startsWith(TEMPLATE_PREFIX)) {\n return templateIds.includes(workflow.slice(TEMPLATE_PREFIX.length))\n }\n return false\n}\n\n/** Decodes a stored `workflow` value into Smartcat create-project params. */\nexport function resolveWorkflowCreateParams(workflow?: string): WorkflowCreateParams {\n if (workflow && workflow.startsWith(TEMPLATE_PREFIX)) {\n return {templateId: workflow.slice(TEMPLATE_PREFIX.length)}\n }\n if (workflow === STANDARD_AI) return {workflowStages: ['translation']}\n return {workflowStages: ['translation', 'postediting']}\n}\n"],"names":[],"mappings":"AASO,MAAM,kBAAkB,aAClB,oBAAoB,qBACpB,cAAc,eAMd,4BAA8D;AAAA,EACzE,EAAC,OAAO,mBAAmB,OAAO,gCAAA;AAAA,EAClC,EAAC,OAAO,aAAa,OAAO,iBAAA;AAC9B;AAYO,SAAS,gBAAgB,UAAkB,aAAgC;AAChF,SAAI,aAAa,qBAAqB,aAAa,cAAoB,KACnE,SAAS,WAAW,eAAe,IAC9B,YAAY,SAAS,SAAS,MAAM,gBAAgB,MAAM,CAAC,IAE7D;AACT;AAGO,SAAS,4BAA4B,UAAyC;AACnF,SAAI,YAAY,SAAS,WAAW,eAAe,IAC1C,EAAC,YAAY,SAAS,MAAM,gBAAgB,MAAM,MAEvD,aAAa,cAAoB,EAAC,gBAAgB,CAAC,aAAa,MAC7D,EAAC,gBAAgB,CAAC,eAAe,aAAa,EAAA;AACvD;"}
|
package/dist/export.cjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var smartcat = require("./smartcat.cjs"), progress = require("./_chunks-cjs/index.cjs"), workflow = require("./_chunks-cjs/workflow.cjs");
|
|
4
|
+
const UPLOAD_CHUNK_SIZE = 50, BATCH_RETRY_DELAY_MS = 1e3, UPLOAD_CHUNK_CONCURRENCY = 10;
|
|
5
|
+
function chunk(items, size) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
async function runWithConcurrency(items, limit, worker) {
|
|
11
|
+
let next = 0;
|
|
12
|
+
async function runNext() {
|
|
13
|
+
const i = next++;
|
|
14
|
+
if (!(i >= items.length))
|
|
15
|
+
return await worker(items[i]), runNext();
|
|
16
|
+
}
|
|
17
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => runNext()));
|
|
18
|
+
}
|
|
19
|
+
const PROJECT_QUERY = `*[_id == $id][0]{
|
|
20
|
+
_id,
|
|
21
|
+
name,
|
|
22
|
+
sourceLanguage,
|
|
23
|
+
targetLanguages,
|
|
24
|
+
workflow,
|
|
25
|
+
smartcatProjectId,
|
|
26
|
+
smartcatProjectUrl,
|
|
27
|
+
smartcatLanguages[]{sanityId, smartcatLanguage},
|
|
28
|
+
outbox[]{sourceDocId, title, filename, locjson},
|
|
29
|
+
pendingDeletions[]{itemKey, sourceDocId, smartcatDocumentId},
|
|
30
|
+
progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}
|
|
31
|
+
}`;
|
|
32
|
+
function existingDocIdsBySource(scProject, outbox) {
|
|
33
|
+
const idByPrefix = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const doc of scProject.documents ?? []) {
|
|
35
|
+
const prefix = progress.sourceIdPrefix(doc);
|
|
36
|
+
prefix && doc.id && !idByPrefix.has(prefix) && idByPrefix.set(prefix, doc.id);
|
|
37
|
+
}
|
|
38
|
+
const result = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const item of outbox) {
|
|
40
|
+
if (!item.sourceDocId) continue;
|
|
41
|
+
const id = idByPrefix.get(stripDraft(item.sourceDocId).slice(0, 8));
|
|
42
|
+
id && result.set(item.sourceDocId, id);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function stripDraft(id) {
|
|
47
|
+
return id.replace(/^drafts\./, "");
|
|
48
|
+
}
|
|
49
|
+
function mergeWrittenDocs(authoritative, written) {
|
|
50
|
+
const byId = new Map(authoritative.filter((d) => d.id).map((d) => [d.id, d]));
|
|
51
|
+
for (const d of written) d.id && !byId.has(d.id) && byId.set(d.id, d);
|
|
52
|
+
return [...byId.values()];
|
|
53
|
+
}
|
|
54
|
+
async function runExportUpload(options) {
|
|
55
|
+
const {
|
|
56
|
+
sanity,
|
|
57
|
+
smartcat: smartcat$1,
|
|
58
|
+
projectId,
|
|
59
|
+
now = () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
61
|
+
} = options, log = [];
|
|
62
|
+
smartcat$1.setRequestLogger?.(({ method, path, status, body }) => {
|
|
63
|
+
status >= 200 && status < 300 ? log.push({ level: "info", message: `${method} ${path} \u2192 ${status}` }) : (log.push({ level: "error", message: `Error ${status} while performing ${method} ${path}` }), body && log.push({ level: "error", indent: !0, message: body }));
|
|
64
|
+
});
|
|
65
|
+
const project = await sanity.fetch(PROJECT_QUERY, { id: projectId });
|
|
66
|
+
if (!project) throw new Error(`Translation project ${projectId} not found`);
|
|
67
|
+
try {
|
|
68
|
+
const outbox = project.outbox ?? [], deletions = project.pendingDeletions ?? [];
|
|
69
|
+
if (outbox.length === 0 && deletions.length === 0) {
|
|
70
|
+
const status = project.smartcatProjectId ? "sent" : "draft";
|
|
71
|
+
return log.push({ level: "skip", message: "Nothing to sync \u2014 no documents with translatable content" }), await sanity.patch(projectId).set({ status, lastExportAt: now(), lastError: null, functionLog: JSON.stringify(log) }).unset(["outbox", "pendingDeletions"]).commit(), { smartcatProjectId: project.smartcatProjectId ?? "", created: 0, updated: 0, deleted: 0, failed: 0 };
|
|
72
|
+
}
|
|
73
|
+
log.push({ level: "info", message: `Syncing ${outbox.length} file(s)${deletions.length ? ` \xB7 ${deletions.length} deletion(s)` : ""} to Smartcat` });
|
|
74
|
+
const isNew = !project.smartcatProjectId;
|
|
75
|
+
let scProjectId = project.smartcatProjectId, scProjectUrl = project.smartcatProjectUrl;
|
|
76
|
+
if (isNew) {
|
|
77
|
+
const sourceLanguage = project.sourceLanguage;
|
|
78
|
+
if (!sourceLanguage) throw new Error("No source language set on the project");
|
|
79
|
+
const targetLanguages = project.targetLanguages ?? [];
|
|
80
|
+
if (targetLanguages.length === 0) throw new Error("No target languages set on the project");
|
|
81
|
+
const map = project.smartcatLanguages;
|
|
82
|
+
log.push({ level: "info", message: `Creating Smartcat project "${project.name || "Sanity translation project"}"` });
|
|
83
|
+
const createdProject = await smartcat$1.createProject({
|
|
84
|
+
name: project.name || "Sanity translation project",
|
|
85
|
+
sourceLanguage: progress.toSmartcatLanguage(map, sourceLanguage),
|
|
86
|
+
targetLanguages: targetLanguages.map((id) => progress.toSmartcatLanguage(map, id)),
|
|
87
|
+
...workflow.resolveWorkflowCreateParams(project.workflow)
|
|
88
|
+
});
|
|
89
|
+
scProjectId = createdProject.id, scProjectUrl = smartcat$1.projectUrl(createdProject.id), log.push({ level: "success", message: `Created Smartcat project ${scProjectId}` }), await sanity.patch(projectId).set({ smartcatProjectId: scProjectId, smartcatProjectUrl: scProjectUrl }).commit();
|
|
90
|
+
}
|
|
91
|
+
const deletedItemKeys = [];
|
|
92
|
+
for (const del of deletions)
|
|
93
|
+
try {
|
|
94
|
+
del.smartcatDocumentId && (await smartcat$1.deleteDocument(del.smartcatDocumentId), log.push({ level: "success", message: `Deleted document ${del.smartcatDocumentId}` })), del.itemKey && deletedItemKeys.push(del.itemKey);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log.push({ level: "error", message: `Failed to delete document ${del.smartcatDocumentId}: ${e instanceof Error ? e.message : String(e)}` });
|
|
97
|
+
}
|
|
98
|
+
const existing = isNew || outbox.length === 0 ? /* @__PURE__ */ new Map() : existingDocIdsBySource(await smartcat$1.getProject(scProjectId), outbox);
|
|
99
|
+
let created = 0, updated = 0, failed = 0;
|
|
100
|
+
const writtenDocs = [], toUpdate = outbox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId)), toCreate = outbox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)));
|
|
101
|
+
for (const item of toUpdate) {
|
|
102
|
+
const docId = existing.get(item.sourceDocId);
|
|
103
|
+
try {
|
|
104
|
+
const returned = await smartcat$1.updateDocument(docId, item.filename, item.locjson);
|
|
105
|
+
for (const d of returned) writtenDocs.push({ ...d, name: d.name || item.filename });
|
|
106
|
+
updated++, log.push({ level: "success", message: `Updated ${item.filename}` });
|
|
107
|
+
} catch {
|
|
108
|
+
failed++, log.push({ level: "error", message: `Skipped ${item.filename} \u2014 update failed (see error above)` });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function processChunk(group) {
|
|
112
|
+
const payload = group.map((i) => ({ filename: i.filename, content: i.locjson }));
|
|
113
|
+
try {
|
|
114
|
+
const returned = await smartcat$1.uploadDocuments(scProjectId, payload);
|
|
115
|
+
writtenDocs.push(...returned), created += group.length, log.push({ level: "success", message: `Uploaded ${group.length} file(s)` });
|
|
116
|
+
} catch {
|
|
117
|
+
await sleep(BATCH_RETRY_DELAY_MS);
|
|
118
|
+
try {
|
|
119
|
+
const returned = await smartcat$1.uploadDocuments(scProjectId, payload);
|
|
120
|
+
writtenDocs.push(...returned), created += group.length, log.push({ level: "success", message: `Uploaded ${group.length} file(s) (retry)` });
|
|
121
|
+
} catch {
|
|
122
|
+
log.push({ level: "info", message: `Batch upload failed, retrying ${group.length} file(s) individually` });
|
|
123
|
+
for (const item of group)
|
|
124
|
+
try {
|
|
125
|
+
const returned = await smartcat$1.uploadDocument(scProjectId, item.filename, item.locjson);
|
|
126
|
+
for (const d of returned) writtenDocs.push({ ...d, name: d.name || item.filename });
|
|
127
|
+
created++, log.push({ level: "success", message: `Uploaded ${item.filename}` });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
e instanceof smartcat.SmartcatError && e.status === 409 ? (created++, log.push({ level: "success", message: `Already synced ${item.filename}` })) : (failed++, log.push({ level: "error", message: `Skipped ${item.filename} \u2014 upload failed (see error above)` }));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk);
|
|
135
|
+
const scProject = await smartcat$1.getProject(scProjectId), documents = mergeWrittenDocs(scProject.documents ?? [], writtenDocs), items = outbox.map((o) => ({ _id: o.sourceDocId || o.filename, title: o.title })), progress$1 = progress.buildProgress(items, { ...scProject, documents }, project.progress, now(), project.smartcatLanguages);
|
|
136
|
+
log.push({
|
|
137
|
+
level: failed ? "error" : "info",
|
|
138
|
+
message: `Synced ${created} created, ${updated} updated${failed ? `, ${failed} failed` : ""}${deletedItemKeys.length ? `, ${deletedItemKeys.length} deleted` : ""}`
|
|
139
|
+
});
|
|
140
|
+
const patch = {
|
|
141
|
+
smartcatProjectId: scProjectId,
|
|
142
|
+
smartcatProjectUrl: scProjectUrl,
|
|
143
|
+
status: "sent",
|
|
144
|
+
progress: progress$1,
|
|
145
|
+
lastExportAt: now(),
|
|
146
|
+
lastError: null,
|
|
147
|
+
functionLog: JSON.stringify(log)
|
|
148
|
+
};
|
|
149
|
+
scProject.name && (patch.name = scProject.name);
|
|
150
|
+
const unset = ["outbox", "pendingDeletions", ...deletedItemKeys.map((k) => `items[_key=="${k}"]`)];
|
|
151
|
+
return await sanity.patch(projectId).set(patch).unset(unset).commit(), { smartcatProjectId: scProjectId, created, updated, deleted: deletedItemKeys.length, failed };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
throw log.push({ level: "error", message }), await sanity.patch(projectId).set({ status: "error", lastError: message, lastExportAt: now(), functionLog: JSON.stringify(log) }).commit().catch(() => {
|
|
155
|
+
}), err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.UPLOAD_CHUNK_CONCURRENCY = UPLOAD_CHUNK_CONCURRENCY;
|
|
159
|
+
exports.runExportUpload = runExportUpload;
|
|
160
|
+
//# sourceMappingURL=export.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.cjs","sources":["../src/export/index.ts"],"sourcesContent":["import type {CreateProjectInput, RequestLogger, SmartcatDocument, SmartcatProject} from '../smartcat/types'\nimport {SmartcatError} from '../smartcat/client'\nimport {buildProgress, sourceIdPrefix, type DocProgress, type ProjectItem} from '../progress/core'\nimport {toSmartcatLanguage, type SmartcatLanguageMapping} from '../lib/languageMap'\nimport {resolveWorkflowCreateParams} from '../lib/workflow'\nimport type {LogLine} from '../lib/log'\n\n/** Source files per multi-file upload request (first-sync create path). */\nconst UPLOAD_CHUNK_SIZE = 50\n/** Delay before the single batch retry, ms. */\nconst BATCH_RETRY_DELAY_MS = 1000\n/** Create-path chunks uploaded concurrently. A saturation study found upload\n * throughput scales ~linearly with concurrency only up to ~C=40, where the\n * Smartcat API's per-workspace rate limit trips (HTTP 429) and effective\n * throughput collapses. Raising this above ~30 is pointless — the gains are\n * erased by rate-limiting, and it eats into the shared rate budget of any other\n * exports running concurrently. 10 is a safe, near-linear default with headroom. */\nexport const UPLOAD_CHUNK_CONCURRENCY = 10\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const out: T[][] = []\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))\n return out\n}\n\n/** Runs `worker` over `items` with at most `limit` in flight concurrently. */\nasync function runWithConcurrency<T>(items: T[], limit: number, worker: (item: T) => Promise<void>): Promise<void> {\n let next = 0\n async function runNext(): Promise<void> {\n const i = next++\n if (i >= items.length) return\n await worker(items[i])\n return runNext()\n }\n await Promise.all(Array.from({length: Math.min(limit, items.length)}, () => runNext()))\n}\n\n/** Minimal structural subset of @sanity/client used by the export. */\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>): {\n unset(keys: string[]): {commit(): Promise<unknown>}\n commit(): Promise<unknown>\n }\n }\n}\n\n/** Structural subset of SmartcatClient the export needs. */\nexport interface SmartcatExportClient {\n createProject(input: CreateProjectInput): Promise<{id: string}>\n uploadDocument(projectId: string, filename: string, content: string): Promise<SmartcatDocument[]>\n uploadDocuments(projectId: string, files: {filename: string; content: string}[]): Promise<SmartcatDocument[]>\n updateDocument(documentId: string, filename: string, content: string): Promise<SmartcatDocument[]>\n deleteDocument(documentId: string): Promise<void>\n getProject(projectId: string): Promise<SmartcatProject>\n projectUrl(projectId: string): string\n /** Optional: receive a trace of every HTTP call for the client-facing log. */\n setRequestLogger?(logger: RequestLogger): void\n}\n\ninterface PendingDeletion {\n itemKey?: string\n sourceDocId?: string\n smartcatDocumentId?: string\n}\n\nexport interface RunExportUploadOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatExportClient\n /** `_id` of the `smartcat.translationProject` to export. */\n projectId: string\n now?: () => string\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface RunExportUploadResult {\n smartcatProjectId: string\n /** Source files newly created in Smartcat this run. */\n created: number\n /** Source files updated in place this run. */\n updated: number\n /** Documents deleted from Smartcat this run. */\n deleted: number\n /** Documents whose upload/update failed (e.g. Smartcat lock); the rest still synced. */\n failed: number\n}\n\nconst PROJECT_QUERY = `*[_id == $id][0]{\n _id,\n name,\n sourceLanguage,\n targetLanguages,\n workflow,\n smartcatProjectId,\n smartcatProjectUrl,\n smartcatLanguages[]{sanityId, smartcatLanguage},\n outbox[]{sourceDocId, title, filename, locjson},\n pendingDeletions[]{itemKey, sourceDocId, smartcatDocumentId},\n progress[]{_key, sourceDocId, title, targets[]{_key, language, smartcatDocumentId, stages, complete, imported, syncedAt}}\n}`\n\ninterface OutboxItem {\n sourceDocId?: string\n title?: string\n filename: string\n locjson: string\n}\n\ninterface ProjectData {\n _id: string\n name?: string\n sourceLanguage?: string\n targetLanguages?: string[]\n workflow?: string\n smartcatProjectId?: string\n smartcatProjectUrl?: string\n smartcatLanguages?: SmartcatLanguageMapping[]\n outbox?: OutboxItem[]\n pendingDeletions?: PendingDeletion[]\n progress?: DocProgress[]\n}\n\n/** A source file maps to one Smartcat document id we can update in place. */\nfunction existingDocIdsBySource(scProject: SmartcatProject, outbox: OutboxItem[]): Map<string, string> {\n // Index Smartcat documents by the source-id prefix in their filename, keeping\n // one representative id per source file (all target-language docs share a source).\n const idByPrefix = new Map<string, string>()\n for (const doc of scProject.documents ?? []) {\n const prefix = sourceIdPrefix(doc)\n if (prefix && doc.id && !idByPrefix.has(prefix)) idByPrefix.set(prefix, doc.id)\n }\n const result = new Map<string, string>()\n for (const item of outbox) {\n if (!item.sourceDocId) continue\n const id = idByPrefix.get(stripDraft(item.sourceDocId).slice(0, 8))\n if (id) result.set(item.sourceDocId, id)\n }\n return result\n}\n\nfunction stripDraft(id: string): string {\n return id.replace(/^drafts\\./, '')\n}\n\n/**\n * Merges the documents Smartcat returned from this run's writes into the\n * authoritative `getProject` list. The fetched docs win (they carry workflow\n * stages); written docs are added only when the fetch hasn't indexed them yet,\n * so a freshly-created document — and its title — isn't dropped this run.\n */\nfunction mergeWrittenDocs(authoritative: SmartcatDocument[], written: SmartcatDocument[]): SmartcatDocument[] {\n const byId = new Map(authoritative.filter((d) => d.id).map((d) => [d.id, d]))\n for (const d of written) if (d.id && !byId.has(d.id)) byId.set(d.id, d)\n return [...byId.values()]\n}\n\n/**\n * Thin export step run by a Sanity Function: takes the LocJSON files the browser\n * prepared (in `outbox`) and syncs them to Smartcat.\n *\n * - **First sync** (no `smartcatProjectId`): creates the Smartcat project and\n * uploads each file, then mirrors Smartcat's (possibly de-duplicated) project\n * name back onto the Sanity project.\n * - **Re-sync** (project already exists): reuses it — updating documents that are\n * already there (Smartcat merges changes, preserving existing translations) and\n * creating any newly-added items. Never creates a second Smartcat project.\n *\n * Does no content processing, so it depends only on the Smartcat client.\n */\nexport async function runExportUpload(options: RunExportUploadOptions): Promise<RunExportUploadResult> {\n const {\n sanity,\n smartcat,\n projectId,\n now = () => new Date().toISOString(),\n sleep = (ms) => new Promise((r) => setTimeout(r, ms)),\n } = options\n const log: LogLine[] = []\n\n // Trace every Smartcat HTTP call into the client-facing log. Successful calls\n // are a single info line; failures show \"Error <status> while performing …\"\n // (red) with the raw response body on the line below.\n smartcat.setRequestLogger?.(({method, path, status, body}) => {\n if (status >= 200 && status < 300) {\n log.push({level: 'info', message: `${method} ${path} → ${status}`})\n } else {\n log.push({level: 'error', message: `Error ${status} while performing ${method} ${path}`})\n if (body) log.push({level: 'error', indent: true, message: body})\n }\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 const outbox = project.outbox ?? []\n const deletions = project.pendingDeletions ?? []\n if (outbox.length === 0 && deletions.length === 0) {\n // Not an error: every queued document resolved to zero translatable units\n // (e.g. empty source content), so there's nothing to send. Settle back to a\n // resting status and explain why, rather than failing the project.\n const status = project.smartcatProjectId ? 'sent' : 'draft'\n log.push({level: 'skip', message: 'Nothing to sync — no documents with translatable content'})\n await sanity\n .patch(projectId)\n .set({status, lastExportAt: now(), lastError: null, functionLog: JSON.stringify(log)})\n .unset(['outbox', 'pendingDeletions'])\n .commit()\n return {smartcatProjectId: project.smartcatProjectId ?? '', created: 0, updated: 0, deleted: 0, failed: 0}\n }\n log.push({level: 'info', message: `Syncing ${outbox.length} file(s)${deletions.length ? ` · ${deletions.length} deletion(s)` : ''} to Smartcat`})\n\n const isNew = !project.smartcatProjectId\n let scProjectId = project.smartcatProjectId\n let scProjectUrl = project.smartcatProjectUrl\n\n if (isNew) {\n const sourceLanguage = project.sourceLanguage\n if (!sourceLanguage) throw new Error('No source language set on the project')\n const targetLanguages = project.targetLanguages ?? []\n if (targetLanguages.length === 0) throw new Error('No target languages set on the project')\n // Send Smartcat its own language codes (the project carries the mapping).\n const map = project.smartcatLanguages\n log.push({level: 'info', message: `Creating Smartcat project \"${project.name || 'Sanity translation project'}\"`})\n const createdProject = await smartcat.createProject({\n name: project.name || 'Sanity translation project',\n sourceLanguage: toSmartcatLanguage(map, sourceLanguage),\n targetLanguages: targetLanguages.map((id) => toSmartcatLanguage(map, id)),\n ...resolveWorkflowCreateParams(project.workflow),\n })\n scProjectId = createdProject.id\n scProjectUrl = smartcat.projectUrl(createdProject.id)\n log.push({level: 'success', message: `Created Smartcat project ${scProjectId}`})\n // Persist the new project id immediately, before uploads. If a later step\n // fails (or the event is re-run), the next run reuses this project instead\n // of creating a duplicate in Smartcat.\n await sanity.patch(projectId).set({smartcatProjectId: scProjectId, smartcatProjectUrl: scProjectUrl}).commit()\n }\n\n // Delete marked documents first (one id removes the whole document). Best-effort:\n // an item is removed from the project only if its Smartcat delete succeeded (or\n // it had no linked document).\n const deletedItemKeys: string[] = []\n for (const del of deletions) {\n try {\n if (del.smartcatDocumentId) {\n await smartcat.deleteDocument(del.smartcatDocumentId)\n log.push({level: 'success', message: `Deleted document ${del.smartcatDocumentId}`})\n }\n if (del.itemKey) deletedItemKeys.push(del.itemKey)\n } catch (e) {\n // Leave the item in place; the mark is cleared so the editor can retry.\n log.push({level: 'error', message: `Failed to delete document ${del.smartcatDocumentId}: ${e instanceof Error ? e.message : String(e)}`})\n }\n }\n\n // For an existing project, find which source files are already in Smartcat so\n // we update them in place rather than creating duplicates.\n const existing =\n isNew || outbox.length === 0\n ? new Map<string, string>()\n : existingDocIdsBySource(await smartcat.getProject(scProjectId as string), outbox)\n\n let created = 0\n let updated = 0\n let failed = 0\n // Capture the documents Smartcat returns from each write. The post-upload\n // getProject can lag on freshly-created docs (indexing delay); merging these\n // in guarantees buildProgress sees them this run, so the title — which lives\n // only on the in-memory outbox — is captured now instead of being lost until\n // a re-export (see the field-level \"Untitled\" progress bug).\n const writtenDocs: SmartcatDocument[] = []\n\n const toUpdate = outbox.filter((item) => item.sourceDocId && existing.get(item.sourceDocId))\n const toCreate = outbox.filter((item) => !(item.sourceDocId && existing.get(item.sourceDocId)))\n\n // Updates stay per-item (no multi-file equivalent) — today's behavior.\n for (const item of toUpdate) {\n const docId = existing.get(item.sourceDocId as string) as string\n try {\n const returned = await smartcat.updateDocument(docId, item.filename, item.locjson)\n // Tag with the filename we uploaded so correlation works even when the\n // write response omits the document name.\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n updated++\n log.push({level: 'success', message: `Updated ${item.filename}`})\n } catch {\n // The HTTP error + body were already logged in red by the request logger.\n // One document failing (e.g. a Smartcat \"document locked\" / update-in-\n // progress 400) must not abort the batch or fail the whole project — record\n // it and move on so the rest still sync.\n failed++\n log.push({level: 'error', message: `Skipped ${item.filename} — update failed (see error above)`})\n }\n }\n\n // Per-chunk upload: batch → retry once as batch → per-file fallback. Mutates the\n // enclosing `created`/`failed`/`writtenDocs`/`log` state; never throws for a\n // normal upload failure (the per-file fallback swallows those), so it's safe to\n // run many of these concurrently below.\n async function processChunk(group: OutboxItem[]): Promise<void> {\n const payload = group.map((i) => ({filename: i.filename, content: i.locjson}))\n try {\n const returned = await smartcat.uploadDocuments(scProjectId as string, payload)\n // Batch correlation relies on the response carrying a per-doc name/fullPath (spike-confirmed).\n writtenDocs.push(...returned)\n created += group.length\n log.push({level: 'success', message: `Uploaded ${group.length} file(s)`})\n } catch {\n await sleep(BATCH_RETRY_DELAY_MS)\n try {\n const returned = await smartcat.uploadDocuments(scProjectId as string, payload)\n writtenDocs.push(...returned)\n created += group.length\n log.push({level: 'success', message: `Uploaded ${group.length} file(s) (retry)`})\n } catch {\n log.push({level: 'info', message: `Batch upload failed, retrying ${group.length} file(s) individually`})\n for (const item of group) {\n try {\n const returned = await smartcat.uploadDocument(scProjectId as string, item.filename, item.locjson)\n // Tag with the filename we uploaded so correlation works even when the\n // write response omits the document name.\n for (const d of returned) writtenDocs.push({...d, name: d.name || item.filename})\n created++\n log.push({level: 'success', message: `Uploaded ${item.filename}`})\n } catch (e) {\n // A 409 here means the batch attempt(s) above actually committed this\n // file before throwing (HTTP error masking a successful create); the\n // re-POST fails because Smartcat rejects a duplicate filename. The\n // document exists in the project, so it's synced, not failed —\n // getProject/buildProgress remain authoritative for its content.\n if (e instanceof SmartcatError && e.status === 409) {\n created++\n log.push({level: 'success', message: `Already synced ${item.filename}`})\n } else {\n failed++\n log.push({level: 'error', message: `Skipped ${item.filename} — upload failed (see error above)`})\n }\n }\n }\n }\n }\n }\n\n // Creates are uploaded in chunks, with bounded concurrency across chunks —\n // log-line order becomes non-deterministic, but the mutations above are safe\n // under JS's single-threaded async interleaving.\n await runWithConcurrency(chunk(toCreate, UPLOAD_CHUNK_SIZE), UPLOAD_CHUNK_CONCURRENCY, processChunk)\n\n // Re-fetch for authoritative ids/stages, and mirror the name back on first\n // creation (Smartcat may have de-duplicated it, e.g. \"My test (1)\"). Then add\n // any just-written docs it hasn't indexed yet so none are dropped this run.\n const scProject = await smartcat.getProject(scProjectId as string)\n const documents = mergeWrittenDocs(scProject.documents ?? [], writtenDocs)\n const items: ProjectItem[] = outbox.map((o) => ({_id: o.sourceDocId || o.filename, title: o.title}))\n const progress = buildProgress(items, {...scProject, documents}, project.progress, now(), project.smartcatLanguages)\n\n log.push({\n level: failed ? 'error' : 'info',\n message: `Synced ${created} created, ${updated} updated${failed ? `, ${failed} failed` : ''}${deletedItemKeys.length ? `, ${deletedItemKeys.length} deleted` : ''}`,\n })\n\n const patch: Record<string, unknown> = {\n smartcatProjectId: scProjectId,\n smartcatProjectUrl: scProjectUrl,\n status: 'sent',\n progress,\n lastExportAt: now(),\n lastError: null,\n functionLog: JSON.stringify(log),\n }\n // Mirror Smartcat's project name back on every sync, so the two stay in step\n // (Smartcat may de-duplicate it, e.g. \"My test (1)\"). Note: this means a local\n // rename in the Studio is overwritten on the next sync unless it's also pushed\n // to Smartcat — Smartcat is the source of truth for the linked project's name.\n if (scProject.name) patch.name = scProject.name\n\n const unset = ['outbox', 'pendingDeletions', ...deletedItemKeys.map((k) => `items[_key==\"${k}\"]`)]\n await sanity.patch(projectId).set(patch).unset(unset).commit()\n\n return {smartcatProjectId: scProjectId as string, created, updated, deleted: deletedItemKeys.length, failed}\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log.push({level: 'error', message})\n await sanity\n .patch(projectId)\n .set({status: 'error', lastError: message, lastExportAt: now(), functionLog: JSON.stringify(log)})\n .commit()\n .catch(() => {})\n throw err\n }\n}\n"],"names":["sourceIdPrefix","smartcat","toSmartcatLanguage","resolveWorkflowCreateParams","SmartcatError","progress","buildProgress"],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Input for creating a Smartcat project. */
|
|
2
|
+
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
|
+
/** Sink for per-request tracing; attach with `SmartcatClient.setRequestLogger`. */
|
|
19
|
+
declare type RequestLogger = (info: RequestLogInfo) => void;
|
|
20
|
+
|
|
21
|
+
/** One Smartcat HTTP call, reported to a {@link RequestLogger} for tracing. */
|
|
22
|
+
declare interface RequestLogInfo {
|
|
23
|
+
method: string;
|
|
24
|
+
/** Request path incl. query string (no host, no secrets). */
|
|
25
|
+
path: string;
|
|
26
|
+
status: number;
|
|
27
|
+
/** Raw response body text — carries Smartcat's error detail on failures. */
|
|
28
|
+
body: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thin export step run by a Sanity Function: takes the LocJSON files the browser
|
|
33
|
+
* prepared (in `outbox`) and syncs them to Smartcat.
|
|
34
|
+
*
|
|
35
|
+
* - **First sync** (no `smartcatProjectId`): creates the Smartcat project and
|
|
36
|
+
* uploads each file, then mirrors Smartcat's (possibly de-duplicated) project
|
|
37
|
+
* name back onto the Sanity project.
|
|
38
|
+
* - **Re-sync** (project already exists): reuses it — updating documents that are
|
|
39
|
+
* already there (Smartcat merges changes, preserving existing translations) and
|
|
40
|
+
* creating any newly-added items. Never creates a second Smartcat project.
|
|
41
|
+
*
|
|
42
|
+
* Does no content processing, so it depends only on the Smartcat client.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runExportUpload(
|
|
45
|
+
options: RunExportUploadOptions,
|
|
46
|
+
): Promise<RunExportUploadResult>;
|
|
47
|
+
|
|
48
|
+
export declare interface RunExportUploadOptions {
|
|
49
|
+
sanity: SanityLikeClient;
|
|
50
|
+
smartcat: SmartcatExportClient;
|
|
51
|
+
/** `_id` of the `smartcat.translationProject` to export. */
|
|
52
|
+
projectId: string;
|
|
53
|
+
now?: () => string;
|
|
54
|
+
sleep?: (ms: number) => Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export declare interface RunExportUploadResult {
|
|
58
|
+
smartcatProjectId: string;
|
|
59
|
+
/** Source files newly created in Smartcat this run. */
|
|
60
|
+
created: number;
|
|
61
|
+
/** Source files updated in place this run. */
|
|
62
|
+
updated: number;
|
|
63
|
+
/** Documents deleted from Smartcat this run. */
|
|
64
|
+
deleted: number;
|
|
65
|
+
/** Documents whose upload/update failed (e.g. Smartcat lock); the rest still synced. */
|
|
66
|
+
failed: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Minimal structural subset of @sanity/client used by the export. */
|
|
70
|
+
export declare interface SanityLikeClient {
|
|
71
|
+
fetch<T = unknown>(
|
|
72
|
+
query: string,
|
|
73
|
+
params?: Record<string, unknown>,
|
|
74
|
+
): Promise<T>;
|
|
75
|
+
patch(id: string): {
|
|
76
|
+
set(attrs: Record<string, unknown>): {
|
|
77
|
+
unset(keys: string[]): {
|
|
78
|
+
commit(): Promise<unknown>;
|
|
79
|
+
};
|
|
80
|
+
commit(): Promise<unknown>;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A document within a Smartcat project (one per source file × target language). */
|
|
86
|
+
declare interface SmartcatDocument {
|
|
87
|
+
/** Target document id, formatted `<fileGuid>_<languageNumber>`. */
|
|
88
|
+
id: string;
|
|
89
|
+
name?: string;
|
|
90
|
+
fullPath?: string;
|
|
91
|
+
filename?: string;
|
|
92
|
+
/** Shared id of the uploaded source file (the `<fileGuid>` part of `id`). */
|
|
93
|
+
externalId?: string;
|
|
94
|
+
sourceLanguage?: string;
|
|
95
|
+
targetLanguage?: string;
|
|
96
|
+
/** Per-stage progress. On documents these carry only `{id, progress}`; the
|
|
97
|
+
* stage names live on the project-level `workflowStages` (keyed by `id`). */
|
|
98
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Structural subset of SmartcatClient the export needs. */
|
|
102
|
+
export declare interface SmartcatExportClient {
|
|
103
|
+
createProject(input: CreateProjectInput): Promise<{
|
|
104
|
+
id: string;
|
|
105
|
+
}>;
|
|
106
|
+
uploadDocument(
|
|
107
|
+
projectId: string,
|
|
108
|
+
filename: string,
|
|
109
|
+
content: string,
|
|
110
|
+
): Promise<SmartcatDocument[]>;
|
|
111
|
+
uploadDocuments(
|
|
112
|
+
projectId: string,
|
|
113
|
+
files: {
|
|
114
|
+
filename: string;
|
|
115
|
+
content: string;
|
|
116
|
+
}[],
|
|
117
|
+
): Promise<SmartcatDocument[]>;
|
|
118
|
+
updateDocument(
|
|
119
|
+
documentId: string,
|
|
120
|
+
filename: string,
|
|
121
|
+
content: string,
|
|
122
|
+
): Promise<SmartcatDocument[]>;
|
|
123
|
+
deleteDocument(documentId: string): Promise<void>;
|
|
124
|
+
getProject(projectId: string): Promise<SmartcatProject>;
|
|
125
|
+
projectUrl(projectId: string): string;
|
|
126
|
+
/** Optional: receive a trace of every HTTP call for the client-facing log. */
|
|
127
|
+
setRequestLogger?(logger: RequestLogger): void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A Smartcat project as returned by the API. */
|
|
131
|
+
declare interface SmartcatProject {
|
|
132
|
+
id: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
sourceLanguage?: string;
|
|
135
|
+
targetLanguages?: string[];
|
|
136
|
+
/** Project-level stages, where each `id` maps to a `stageType` (e.g. "translation"). */
|
|
137
|
+
workflowStages?: SmartcatWorkflowStage[];
|
|
138
|
+
documents?: SmartcatDocument[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
declare interface SmartcatWorkflowStage {
|
|
142
|
+
id?: string;
|
|
143
|
+
/** Stage type/name, e.g. "translation" / "editing" (casing varies by API). */
|
|
144
|
+
stageType?: string;
|
|
145
|
+
/** Human-readable stage name, when the API provides one. */
|
|
146
|
+
stageName?: string;
|
|
147
|
+
/** Completion percentage for this stage, 0–100. */
|
|
148
|
+
progress?: number;
|
|
149
|
+
/** Word counts, when present — useful for a weighted progress view. */
|
|
150
|
+
wordsTranslated?: number;
|
|
151
|
+
totalWordsCount?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Create-path chunks uploaded concurrently. A saturation study found upload
|
|
155
|
+
* throughput scales ~linearly with concurrency only up to ~C=40, where the
|
|
156
|
+
* Smartcat API's per-workspace rate limit trips (HTTP 429) and effective
|
|
157
|
+
* throughput collapses. Raising this above ~30 is pointless — the gains are
|
|
158
|
+
* erased by rate-limiting, and it eats into the shared rate budget of any other
|
|
159
|
+
* exports running concurrently. 10 is a safe, near-linear default with headroom. */
|
|
160
|
+
export declare const UPLOAD_CHUNK_CONCURRENCY = 10;
|
|
161
|
+
|
|
162
|
+
export {};
|