@smartcat/sanity-plugin 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/_chunks-cjs/index.cjs +18 -5
  2. package/dist/_chunks-cjs/index.cjs.map +1 -1
  3. package/dist/_chunks-cjs/index2.cjs +8 -6
  4. package/dist/_chunks-cjs/index2.cjs.map +1 -1
  5. package/dist/_chunks-es/index.js +18 -5
  6. package/dist/_chunks-es/index.js.map +1 -1
  7. package/dist/_chunks-es/index2.js +8 -6
  8. package/dist/_chunks-es/index2.js.map +1 -1
  9. package/dist/export.cjs +21 -4
  10. package/dist/export.cjs.map +1 -1
  11. package/dist/export.d.cts +2 -0
  12. package/dist/export.d.ts +2 -0
  13. package/dist/export.js +22 -5
  14. package/dist/export.js.map +1 -1
  15. package/dist/index.cjs +477 -124
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +482 -129
  18. package/dist/index.js.map +1 -1
  19. package/dist/locjson.d.cts +15 -0
  20. package/dist/locjson.d.ts +15 -0
  21. package/dist/progress.d.cts +0 -19
  22. package/dist/progress.d.ts +0 -19
  23. package/dist/smartcat.cjs +14 -2
  24. package/dist/smartcat.cjs.map +1 -1
  25. package/dist/smartcat.d.cts +10 -1
  26. package/dist/smartcat.d.ts +10 -1
  27. package/dist/smartcat.js +14 -2
  28. package/dist/smartcat.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/actions/AddToProjectAction.tsx +23 -2
  31. package/src/export/export.test.ts +42 -5
  32. package/src/export/index.ts +63 -8
  33. package/src/lib/locjson/filename.ts +7 -2
  34. package/src/lib/locjson/locjson.test.ts +64 -0
  35. package/src/lib/locjson/serialize.ts +38 -2
  36. package/src/progress/core.ts +44 -3
  37. package/src/progress/progress.test.ts +40 -0
  38. package/src/schema/translationProject.ts +19 -0
  39. package/src/smartcat/client.ts +15 -1
  40. package/src/tool/AddItemsSearch.tsx +462 -0
  41. package/src/tool/DocTitle.tsx +23 -0
  42. package/src/tool/ProjectView.tsx +29 -187
  43. package/src/tool/data.ts +7 -0
  44. package/src/tool/itemActions.tsx +216 -0
  45. package/src/tool/processing.test.ts +11 -1
  46. package/src/tool/processing.ts +116 -4
@@ -14,7 +14,7 @@ import {serializeToLocjson} from '../lib/locjson/serialize'
14
14
  import {buildLocjsonFilename} from '../lib/locjson/filename'
15
15
  import {deserializeFromLocjson} from '../lib/locjson/deserialize'
16
16
  import {requireSourceLanguage} from '../lib/constants'
17
- import {buildLanguageMappings, findDuplicateSmartcatLanguages} from '../lib/languageMap'
17
+ import {buildLanguageMappings, findDuplicateSmartcatLanguages, toSmartcatLanguage} from '../lib/languageMap'
18
18
  import type {LocalizationMode, LocJSON} from '../lib/locjson/types'
19
19
  import {summarizeCompletion, type DocProgress} from '../progress/core'
20
20
  import type {SmartcatLanguage} from '../types'
@@ -142,10 +142,28 @@ const ITEMS_QUERY = `*[_id == $id][0]{
142
142
  items[]{
143
143
  "docId": ${ITEM_ID},
144
144
  "docType": docType,
145
+ sendExistingTranslations,
145
146
  "doc": ${itemDocDerefRespectingDraft()}
146
147
  }
147
148
  }`
148
149
 
150
+ /**
151
+ * Target-locale sibling documents for a set of source ids, via
152
+ * document-internationalization's `translation.metadata`. Returns, per metadata
153
+ * group, every locale variant's `{language, doc}` — the send-existing flow reads
154
+ * each target language's sibling as the translation to upload. Document-level
155
+ * only; field-level types keep all languages inline on the same document.
156
+ *
157
+ * The variant's locale lives in its language field (v5+, with a uuid `_key`) or
158
+ * in `_key` itself (v4.x); `coalesce` reads whichever applies per the configured
159
+ * {@link fieldLanguageKey}. Reading only `_key` misses v5+ siblings entirely.
160
+ */
161
+ export function siblingsQuery(fieldLanguageKey: string): string {
162
+ return `*[_type == "translation.metadata" && references($ids)]{
163
+ "variants": translations[]{ "language": coalesce(${fieldLanguageKey}, _key), "doc": value-> }
164
+ }`
165
+ }
166
+
149
167
  export async function prepareExport({
150
168
  client,
151
169
  schema,
@@ -165,6 +183,7 @@ export async function prepareExport({
165
183
  items?: {
166
184
  docId?: string
167
185
  docType?: string
186
+ sendExistingTranslations?: boolean
168
187
  doc: (Record<string, unknown> & {_id: string; _type: string}) | null
169
188
  }[]
170
189
  } | null>(ITEMS_QUERY, {id: projectId})
@@ -195,6 +214,37 @@ export async function prepareExport({
195
214
  .map((item) => item.doc)
196
215
  .filter((d): d is Record<string, unknown> & {_id: string; _type: string} => Boolean(d))
197
216
 
217
+ // For "send existing translations" document-level items, fetch each source's
218
+ // target-locale sibling documents up front (one query), keyed by
219
+ // bare-source-id → (Sanity locale → sibling doc). Field-level types need none —
220
+ // their translations live inline on the same document.
221
+ type TargetDoc = Record<string, unknown> & {_id: string; _type: string}
222
+ const siblingsByDocId = new Map<string, Map<string, TargetDoc>>()
223
+ const flaggedDocLevelIds = items
224
+ .filter(
225
+ (item) =>
226
+ item.doc && item.sendExistingTranslations === true && localizationMode(schema.get(item.doc._type)) === 'document',
227
+ )
228
+ .map((item) => publishedId((item.doc as {_id: string})._id))
229
+ if (flaggedDocLevelIds.length > 0) {
230
+ const groups = await client.fetch<{variants: {language: string; doc: TargetDoc | null}[]}[]>(
231
+ siblingsQuery(fieldLanguageKey),
232
+ {ids: flaggedDocLevelIds},
233
+ )
234
+ for (const group of groups ?? []) {
235
+ const byLang = new Map<string, TargetDoc>()
236
+ const groupSourceIds: string[] = []
237
+ for (const variant of group.variants ?? []) {
238
+ if (!variant.doc) continue
239
+ byLang.set(variant.language, variant.doc)
240
+ groupSourceIds.push(publishedId(variant.doc._id))
241
+ }
242
+ // Every member of a translation group can reach the same sibling set, so key
243
+ // the map by each variant's id — the source doc looks up its own group.
244
+ for (const sid of groupSourceIds) siblingsByDocId.set(sid, byLang)
245
+ }
246
+ }
247
+
198
248
  // The translatable field list is per-type, so compute it once per type even
199
249
  // when a project holds many documents of the same type.
200
250
  const fieldsByType = new Map<string, ReturnType<typeof getTranslatableFields>>()
@@ -228,17 +278,79 @@ export async function prepareExport({
228
278
  })
229
279
  }
230
280
 
231
- const outbox: {_key: string; sourceDocId: string; title?: string; filename: string; locjson: string}[] = []
232
- for (const rawDoc of docs) {
281
+ const outbox: {
282
+ _key: string
283
+ sourceDocId: string
284
+ title?: string
285
+ filename: string
286
+ locjson: string
287
+ /** Present for per-language bilingual files (send-existing flow). */
288
+ targetLanguage?: string
289
+ }[] = []
290
+
291
+ /**
292
+ * Queue one bilingual LocJSON per target language for a "send existing
293
+ * translations" item. Each file carries the source plus that language's
294
+ * existing translation (field-level: the inline member; document-level: the
295
+ * locale-sibling document), is named `…_{smartcatCode}.locjson` so Smartcat
296
+ * groups the languages as one document, and records its single target language.
297
+ */
298
+ const queueBilingual = (
299
+ doc: Record<string, unknown> & {_id: string; _type: string},
300
+ name: string | undefined,
301
+ fields: TranslatableField[],
302
+ isDraft: boolean,
303
+ ): void => {
304
+ const mode = localizationMode(schema.get(doc._type))
305
+ const byLang = mode === 'document' ? siblingsByDocId.get(doc._id) : undefined
306
+ let queued = 0
307
+ for (const targetSanityId of targetLanguages) {
308
+ const smartcatCode = toSmartcatLanguage(smartcatLanguages, targetSanityId)
309
+ const target =
310
+ mode === 'field' ? {language: targetSanityId} : {language: targetSanityId, doc: byLang?.get(targetSanityId)}
311
+ const locjson = serializeToLocjson(doc as never, fields, {sourceLanguage: source, fieldLanguageKey, target})
312
+ if (locjson.units.length === 0) {
313
+ log({level: 'skip', indent: true, message: `${targetSanityId}: no translatable content — not sent`})
314
+ continue
315
+ }
316
+ const prefilled = locjson.units.filter((u) => u.target.length > 0).length
317
+ outbox.push({
318
+ _key: uuid(),
319
+ sourceDocId: doc._id,
320
+ title: name,
321
+ filename: buildLocjsonFilename(doc._type, doc._id, name, {draft: isDraft, language: smartcatCode}),
322
+ locjson: JSON.stringify(locjson),
323
+ targetLanguage: smartcatCode,
324
+ })
325
+ queued++
326
+ log({
327
+ level: 'success',
328
+ indent: true,
329
+ message: `${targetSanityId} → ${smartcatCode}: queued ${locjson.units.length} unit(s), ${prefilled} with existing translation`,
330
+ })
331
+ }
332
+ if (queued === 0) log({level: 'skip', indent: true, message: 'no translatable content — not sent'})
333
+ }
334
+ for (const item of items) {
335
+ const rawDoc = item.doc
336
+ if (!rawDoc) continue
233
337
  // Serialize under the canonical (bare) id so identity, metadata and progress
234
338
  // stay stable, even when the *content* came from the draft. Mark draft-sourced
235
339
  // files so they're recognizable in Smartcat.
236
340
  const isDraft = rawDoc._id.startsWith('drafts.')
237
341
  const doc = isDraft ? {...rawDoc, _id: publishedId(rawDoc._id)} : rawDoc
238
342
  const name = resolveDocumentTitle(schema, doc)
239
- log({level: 'info', message: `${name || doc._id} · ${doc._type}${isDraft ? ' (draft)' : ''}`})
343
+ const sendExisting = item.sendExistingTranslations === true
344
+ log({
345
+ level: 'info',
346
+ message: `${name || doc._id} · ${doc._type}${isDraft ? ' (draft)' : ''}${sendExisting ? ' · sending existing translations' : ''}`,
347
+ })
240
348
  try {
241
349
  const fields = fieldsFor(doc._type)
350
+ if (sendExisting) {
351
+ queueBilingual(doc, name, fields, isDraft)
352
+ continue
353
+ }
242
354
  // Fields skipped because they hold a block type that can't be serialized
243
355
  // (e.g. a custom `table`): left untranslated on purpose, reported as errors
244
356
  // below so the editor knows to act rather than lose content silently.