@smartcat/sanity-plugin 1.2.0 → 1.3.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 (42) 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 +156 -39
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +160 -43
  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/ProjectView.tsx +108 -44
  41. package/src/tool/data.ts +7 -0
  42. package/src/tool/processing.ts +110 -4
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { defineType, defineField, defineArrayMember, useClient, prepareForPreview, useSchema, usePerspective, useRelativeTime, useValuePreview, useWorkspace, useFormValue, definePlugin } from "sanity";
2
- import { TranslateIcon, CogIcon, AddIcon, LaunchIcon, CheckmarkIcon, CopyIcon, SyncIcon, ArrowLeftIcon, TrashIcon, PublishIcon, DownloadIcon, CloseIcon, RemoveCircleIcon, JsonIcon } from "@sanity/icons";
2
+ import { TranslateIcon, CogIcon, AddIcon, LaunchIcon, CheckmarkIcon, CopyIcon, SyncIcon, ArrowLeftIcon, TrashIcon, PublishIcon, DownloadIcon, CloseIcon, RemoveCircleIcon, ChevronDownIcon, JsonIcon } from "@sanity/icons";
3
3
  import { TRANSLATION_PROJECT_TYPE, SETTINGS_TYPE, API_VERSION, SETTINGS_DOC_ID, requireSourceLanguage } from "./_chunks-es/constants.js";
4
4
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
5
5
  import { useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect, Fragment as Fragment$1 } from "react";
6
- import { Select, useToast, Flex, Spinner, Stack, Label, TextInput, Card, Text, Badge, Switch, Button, Heading, Dialog, Box, Inline, useTheme_v2, useTheme, Checkbox, Container } from "@sanity/ui";
6
+ import { Select, useToast, Flex, Spinner, Stack, Label, TextInput, Card, Text, Badge, Switch, Button, Heading, Dialog, Box, Inline, useTheme_v2, useTheme, Checkbox, MenuButton, Menu, MenuItem, Container } from "@sanity/ui";
7
7
  import { uuid } from "@sanity/uuid";
8
8
  import { TEMPLATE_PREFIX, STANDARD_WORKFLOW_OPTIONS, isWorkflowValid } from "./_chunks-es/workflow.js";
9
- import { ITEM_ID, ITEM_ID_FROM_SUBQUERY, itemDocDeref, summarizeCompletion, buildLanguageMappings, findDuplicateSmartcatLanguages, itemDocDerefRespectingDraft, targetPercent } from "./_chunks-es/index.js";
10
- import { getTranslatableFields, deserializeFromLocjson, serializeToLocjson, getAtPath, buildLocjsonFilename, localizationMode, enumerateLeaves, setAtPath, isInternationalizedArrayType, innerValueType } from "./_chunks-es/index2.js";
9
+ import { ITEM_ID, ITEM_ID_FROM_SUBQUERY, itemDocDeref, summarizeCompletion, buildLanguageMappings, findDuplicateSmartcatLanguages, itemDocDerefRespectingDraft, toSmartcatLanguage, targetPercent } from "./_chunks-es/index.js";
10
+ import { getTranslatableFields, deserializeFromLocjson, localizationMode, serializeToLocjson, getAtPath, buildLocjsonFilename, enumerateLeaves, setAtPath, isInternationalizedArrayType, innerValueType } from "./_chunks-es/index2.js";
11
11
  import { useIntentLink, useRouter, useRouterState, route } from "sanity/router";
12
12
  import styled, { keyframes, css } from "styled-components";
13
13
  const STATUS_OPTIONS = [
@@ -93,7 +93,19 @@ function createTranslationProjectType() {
93
93
  // Set when the item was added from the published perspective, so the
94
94
  // export translates the published document. Absent/false (incl. all
95
95
  // legacy items) prefers the draft — see itemDocDerefRespectingDraft.
96
- defineField({ name: "sourceIsPublished", title: "Source is published", type: "boolean" })
96
+ defineField({ name: "sourceIsPublished", title: "Source is published", type: "boolean" }),
97
+ // When true, the export sends the item's *existing* translations to
98
+ // Smartcat: one bilingual LocJSON per target language (populated from
99
+ // field/linked-doc translations), grouped via `{name}_{lang}.locjson`.
100
+ // Absent/false (incl. all legacy items) ⇒ today's behaviour: a single
101
+ // source-only file targeting all languages. Note: imported bilingual
102
+ // targets land confirmed at every workflow stage (Smartcat ignores
103
+ // per-segment status on LocJSON import), i.e. they skip human review.
104
+ defineField({
105
+ name: "sendExistingTranslations",
106
+ title: "Send existing translations",
107
+ type: "boolean"
108
+ })
97
109
  ],
98
110
  preview: { select: { title: "docId", subtitle: "docType" } }
99
111
  })
@@ -203,8 +215,15 @@ function createTranslationProjectType() {
203
215
  type: "object",
204
216
  name: "smartcatOutboxItem",
205
217
  fields: [
218
+ defineField({ name: "sourceDocId", type: "string" }),
219
+ defineField({ name: "title", type: "string" }),
206
220
  defineField({ name: "filename", type: "string" }),
207
- defineField({ name: "locjson", type: "text" })
221
+ defineField({ name: "locjson", type: "text" }),
222
+ // Present only for per-language bilingual files (send-existing flow):
223
+ // the single target language this file carries, so the export Function
224
+ // uploads it with `documentModel targetLanguages:[lang]`. Absent ⇒
225
+ // upload targeting all project languages (the source-only default).
226
+ defineField({ name: "targetLanguage", type: "string" })
208
227
  ]
209
228
  })
210
229
  ]
@@ -439,7 +458,7 @@ const PROJECTS_QUERY$1 = `*[_type == $type] | order(name asc){
439
458
  function createAddToProjectAction(config) {
440
459
  const sourceLanguage = config.sourceLanguage;
441
460
  return (props) => {
442
- const { id, draft, published, onComplete } = props, client = useClient({ apiVersion: API_VERSION }), schema = useSchema(), toast = useToast(), { selectedPerspectiveName } = usePerspective(), sourceIsPublished = selectedPerspectiveName === "published" || !draft, [open, setOpen] = useState(!1), [projects, setProjects] = useState(null), [selection, setSelection] = useState(CREATE_NEW), [newName, setNewName] = useState(""), [busy, setBusy] = useState(!1), [includeLinked, setIncludeLinked] = useState(!0), { templates, loaded, requestRefresh } = useTemplates(), [workflow, setWorkflow] = useWorkflowSelection(open, templates, loaded), publishedId2 = id.replace(/^drafts\./, ""), doc = draft ?? published, docTitle = doc && resolveDocumentTitle(schema, doc) || UNTITLED;
461
+ const { id, draft, published, onComplete } = props, client = useClient({ apiVersion: API_VERSION }), schema = useSchema(), toast = useToast(), { selectedPerspectiveName } = usePerspective(), sourceIsPublished = selectedPerspectiveName === "published" || !draft, [open, setOpen] = useState(!1), [projects, setProjects] = useState(null), [selection, setSelection] = useState(CREATE_NEW), [newName, setNewName] = useState(""), [busy, setBusy] = useState(!1), [includeLinked, setIncludeLinked] = useState(!0), [sendExisting, setSendExisting] = useState(!1), { templates, loaded, requestRefresh } = useTemplates(), [workflow, setWorkflow] = useWorkflowSelection(open, templates, loaded), publishedId2 = id.replace(/^drafts\./, ""), doc = draft ?? published, docTitle = doc && resolveDocumentTitle(schema, doc) || UNTITLED;
443
462
  useEffect(() => {
444
463
  if (!open) return;
445
464
  requestRefresh();
@@ -462,7 +481,8 @@ function createAddToProjectAction(config) {
462
481
  _key: uuid(),
463
482
  docId,
464
483
  docType,
465
- sourceIsPublished
484
+ sourceIsPublished,
485
+ sendExistingTranslations: sendExisting
466
486
  }), collected = /* @__PURE__ */ new Map([[publishedId2, doc?._type]]);
467
487
  if (includeLinked) {
468
488
  const linked = await gatherLinkedDocuments({ client, rootId: publishedId2, isRoot: (type) => config.rootTypes.includes(type), isTranslatable: (type) => getTranslatableFields(schema.get(type), { exclude: [config.documentI18nLanguageField] }).length > 0 });
@@ -504,7 +524,7 @@ function createAddToProjectAction(config) {
504
524
  } finally {
505
525
  setBusy(!1);
506
526
  }
507
- }, [selection, newName, workflow, publishedId2, doc, includeLinked, sourceIsPublished, schema, client, projects, close]), content = useMemo(() => projects === null ? /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsx(Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
527
+ }, [selection, newName, workflow, publishedId2, doc, includeLinked, sendExisting, sourceIsPublished, schema, client, projects, close]), content = useMemo(() => projects === null ? /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsx(Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
508
528
  /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
509
529
  /* @__PURE__ */ jsx(Label, { size: 1, children: "Project" }),
510
530
  /* @__PURE__ */ jsxs(
@@ -585,6 +605,20 @@ function createAddToProjectAction(config) {
585
605
  /* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Include linked content" }),
586
606
  /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Also add documents this one references, recursively \u2014 skipping any already in the project." })
587
607
  ] })
608
+ ] }),
609
+ /* @__PURE__ */ jsxs(Flex, { align: "flex-start", gap: 3, children: [
610
+ /* @__PURE__ */ jsx(
611
+ Switch,
612
+ {
613
+ checked: sendExisting,
614
+ onChange: (e) => setSendExisting(e.currentTarget.checked),
615
+ disabled: busy
616
+ }
617
+ ),
618
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
619
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "medium", children: "Send existing translations" }),
620
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Upload current translations alongside the source, so Smartcat starts pre-filled. Imported translations are marked confirmed at every stage \u2014 they skip human review." })
621
+ ] })
588
622
  ] })
589
623
  ] }) }),
590
624
  /* @__PURE__ */ jsxs(Flex, { justify: "flex-end", gap: 2, children: [
@@ -601,7 +635,7 @@ function createAddToProjectAction(config) {
601
635
  }
602
636
  )
603
637
  ] })
604
- ] }), [projects, selection, newName, workflow, templates, busy, includeLinked, docTitle, sourceIsPublished, close, handleConfirm]);
638
+ ] }), [projects, selection, newName, workflow, templates, busy, includeLinked, sendExisting, docTitle, sourceIsPublished, close, handleConfirm]);
605
639
  return {
606
640
  label: "Add to translation project",
607
641
  icon: TranslateIcon,
@@ -638,6 +672,7 @@ const PROJECTS_QUERY = `*[_type == "${TRANSLATION_PROJECT_TYPE}"] | order(_creat
638
672
  _key,
639
673
  "docId": ${ITEM_ID},
640
674
  sourceIsPublished,
675
+ sendExistingTranslations,
641
676
  "doc": ${itemDocDeref("{_id, _type}")},
642
677
  "translations": *[_type == "translation.metadata" && references(${ITEM_ID_FROM_SUBQUERY})][0]
643
678
  .translations[]{language, "id": value._ref}
@@ -835,8 +870,11 @@ const ITEMS_QUERY = `*[_id == $id][0]{
835
870
  items[]{
836
871
  "docId": ${ITEM_ID},
837
872
  "docType": docType,
873
+ sendExistingTranslations,
838
874
  "doc": ${itemDocDerefRespectingDraft()}
839
875
  }
876
+ }`, SIBLINGS_QUERY = `*[_type == "translation.metadata" && references($ids)]{
877
+ "variants": translations[]{ "language": _key, "doc": value-> }
840
878
  }`;
841
879
  async function prepareExport({
842
880
  client,
@@ -861,7 +899,22 @@ async function prepareExport({
861
899
  }
862
900
  const deletedDocIds = new Set(deletions.map((d) => d.sourceDocId)), items = (project?.items ?? []).filter(
863
901
  (item) => !(item.docId && deletedDocIds.has(item.docId))
864
- ), unresolved = items.filter((item) => !item.doc), docs = items.map((item) => item.doc).filter((d) => !!d), fieldsByType = /* @__PURE__ */ new Map(), fieldsFor = (type) => {
902
+ ), unresolved = items.filter((item) => !item.doc), docs = items.map((item) => item.doc).filter((d) => !!d), siblingsByDocId = /* @__PURE__ */ new Map(), flaggedDocLevelIds = items.filter(
903
+ (item) => item.doc && item.sendExistingTranslations === !0 && localizationMode(schema.get(item.doc._type)) === "document"
904
+ ).map((item) => publishedId(item.doc._id));
905
+ if (flaggedDocLevelIds.length > 0) {
906
+ const groups = await client.fetch(
907
+ SIBLINGS_QUERY,
908
+ { ids: flaggedDocLevelIds }
909
+ );
910
+ for (const group of groups ?? []) {
911
+ const byLang = /* @__PURE__ */ new Map(), groupSourceIds = [];
912
+ for (const variant of group.variants ?? [])
913
+ variant.doc && (byLang.set(variant.language, variant.doc), groupSourceIds.push(publishedId(variant.doc._id)));
914
+ for (const sid of groupSourceIds) siblingsByDocId.set(sid, byLang);
915
+ }
916
+ }
917
+ const fieldsByType = /* @__PURE__ */ new Map(), fieldsFor = (type) => {
865
918
  let fields = fieldsByType.get(type);
866
919
  return fields || (fields = getTranslatableFields(schema.get(type), { exclude }), fieldsByType.set(type, fields)), fields;
867
920
  }, configuredLanguages = languages.map((l) => l.id).join(", ") || "(none)", configuredTypes = translatableTypes === void 0 ? "all types" : translatableTypes.join(", ") || "(none)";
@@ -871,12 +924,46 @@ async function prepareExport({
871
924
  level: "error",
872
925
  message: `${item.docId ?? "unknown document"} (${item.docType ?? "unknown type"}): no document found \u2014 it may be unpublished or deleted; skipped`
873
926
  });
874
- const outbox = [];
875
- for (const rawDoc of docs) {
876
- const isDraft = rawDoc._id.startsWith("drafts."), doc = isDraft ? { ...rawDoc, _id: publishedId(rawDoc._id) } : rawDoc, name = resolveDocumentTitle(schema, doc);
877
- log({ level: "info", message: `${name || doc._id} \xB7 ${doc._type}${isDraft ? " (draft)" : ""}` });
927
+ const outbox = [], queueBilingual = (doc, name, fields, isDraft) => {
928
+ const mode = localizationMode(schema.get(doc._type)), byLang = mode === "document" ? siblingsByDocId.get(doc._id) : void 0;
929
+ let queued = 0;
930
+ for (const targetSanityId of targetLanguages) {
931
+ const smartcatCode = toSmartcatLanguage(smartcatLanguages, targetSanityId), target = mode === "field" ? { language: targetSanityId } : { language: targetSanityId, doc: byLang?.get(targetSanityId) }, locjson = serializeToLocjson(doc, fields, { sourceLanguage: source, fieldLanguageKey, target });
932
+ if (locjson.units.length === 0) {
933
+ log({ level: "skip", indent: !0, message: `${targetSanityId}: no translatable content \u2014 not sent` });
934
+ continue;
935
+ }
936
+ const prefilled = locjson.units.filter((u) => u.target.length > 0).length;
937
+ outbox.push({
938
+ _key: uuid(),
939
+ sourceDocId: doc._id,
940
+ title: name,
941
+ filename: buildLocjsonFilename(doc._type, doc._id, name, { draft: isDraft, language: smartcatCode }),
942
+ locjson: JSON.stringify(locjson),
943
+ targetLanguage: smartcatCode
944
+ }), queued++, log({
945
+ level: "success",
946
+ indent: !0,
947
+ message: `${targetSanityId} \u2192 ${smartcatCode}: queued ${locjson.units.length} unit(s), ${prefilled} with existing translation`
948
+ });
949
+ }
950
+ queued === 0 && log({ level: "skip", indent: !0, message: "no translatable content \u2014 not sent" });
951
+ };
952
+ for (const item of items) {
953
+ const rawDoc = item.doc;
954
+ if (!rawDoc) continue;
955
+ const isDraft = rawDoc._id.startsWith("drafts."), doc = isDraft ? { ...rawDoc, _id: publishedId(rawDoc._id) } : rawDoc, name = resolveDocumentTitle(schema, doc), sendExisting = item.sendExistingTranslations === !0;
956
+ log({
957
+ level: "info",
958
+ message: `${name || doc._id} \xB7 ${doc._type}${isDraft ? " (draft)" : ""}${sendExisting ? " \xB7 sending existing translations" : ""}`
959
+ });
878
960
  try {
879
- const fields = fieldsFor(doc._type), skippedBlockTypes = /* @__PURE__ */ new Map(), locjson = serializeToLocjson(doc, fields, {
961
+ const fields = fieldsFor(doc._type);
962
+ if (sendExisting) {
963
+ queueBilingual(doc, name, fields, isDraft);
964
+ continue;
965
+ }
966
+ const skippedBlockTypes = /* @__PURE__ */ new Map(), locjson = serializeToLocjson(doc, fields, {
880
967
  sourceLanguage: source,
881
968
  fieldLanguageKey,
882
969
  onSkippedField: ({ fieldPath, types }) => skippedBlockTypes.set(fieldPath, types)
@@ -1467,39 +1554,55 @@ const CodeArea = styled.textarea`
1467
1554
  line-height: 1.5;
1468
1555
  white-space: pre;
1469
1556
  tab-size: 2;
1470
- `;
1557
+ `, VIEW_SIBLINGS_QUERY = '*[_type == "translation.metadata" && references($id)][0].translations[]{"language": _key, "doc": value->}';
1471
1558
  function ViewLocjsonButton(props) {
1472
- const { docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField } = props, [open, setOpen] = useState(!1), [content, setContent] = useState(null), [filename, setFilename] = useState(null), [error, setError] = useState(null), build = useCallback(async () => {
1473
- setOpen(!0), setContent(null), setFilename(null), setError(null);
1474
- try {
1475
- const raw = await client.fetch(
1476
- sourceIsPublished ? "*[_id == $id][0]" : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
1477
- { id: docId }
1478
- );
1479
- if (!raw) throw new Error(`Document not found: ${docId}`);
1480
- const isDraft = typeof raw._id == "string" && raw._id.startsWith("drafts."), doc = isDraft ? { ...raw, _id: docId } : raw, fields = getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] }), locjson = serializeToLocjson(doc, fields, {
1481
- sourceLanguage,
1482
- fieldLanguageKey: fieldI18nLanguageField
1483
- }), smartcatName = buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc), { draft: isDraft });
1484
- setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson, null, 2));
1485
- } catch (err) {
1486
- setError(err instanceof Error ? err.message : String(err));
1487
- }
1488
- }, [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField]), download = useCallback(() => {
1559
+ const { docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, targetLanguages, languages } = props, [open, setOpen] = useState(!1), [content, setContent] = useState(null), [filename, setFilename] = useState(null), [title, setTitle] = useState("Source only"), [error, setError] = useState(null), build = useCallback(
1560
+ async (targetLanguage) => {
1561
+ setOpen(!0), setContent(null), setFilename(null), setError(null), setTitle(targetLanguage ? `Source + ${languageTitle(languages, targetLanguage)}` : "Source only");
1562
+ try {
1563
+ const raw = await client.fetch(
1564
+ sourceIsPublished ? "*[_id == $id][0]" : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
1565
+ { id: docId }
1566
+ );
1567
+ if (!raw) throw new Error(`Document not found: ${docId}`);
1568
+ const isDraft = typeof raw._id == "string" && raw._id.startsWith("drafts."), doc = isDraft ? { ...raw, _id: docId } : raw, fields = getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] });
1569
+ let target;
1570
+ if (targetLanguage)
1571
+ if (localizationMode(schema.get(type)) === "document") {
1572
+ const sibling = (await client.fetch(VIEW_SIBLINGS_QUERY, { id: docId }) ?? []).find((v) => v.language === targetLanguage)?.doc ?? void 0;
1573
+ target = { language: targetLanguage, doc: sibling };
1574
+ } else
1575
+ target = { language: targetLanguage };
1576
+ const locjson = serializeToLocjson(doc, fields, {
1577
+ sourceLanguage,
1578
+ fieldLanguageKey: fieldI18nLanguageField,
1579
+ target
1580
+ }), smartcatCode = targetLanguage ? languages.find((l) => l.id === targetLanguage)?.smartcatLanguage || targetLanguage : void 0, smartcatName = buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc), {
1581
+ draft: isDraft,
1582
+ language: smartcatCode
1583
+ });
1584
+ setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson, null, 2));
1585
+ } catch (err) {
1586
+ setError(err instanceof Error ? err.message : String(err));
1587
+ }
1588
+ },
1589
+ [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, languages]
1590
+ ), download = useCallback(() => {
1489
1591
  if (content == null || !filename) return;
1490
1592
  const url = URL.createObjectURL(new Blob([content], { type: "application/json" })), anchor = document.createElement("a");
1491
1593
  anchor.href = url, anchor.download = filename, anchor.click(), URL.revokeObjectURL(url);
1492
1594
  }, [content, filename]);
1493
1595
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1494
1596
  /* @__PURE__ */ jsx(
1495
- Button,
1597
+ MenuButton,
1496
1598
  {
1497
- mode: "bleed",
1498
- icon: JsonIcon,
1499
- title: "View LocJSON",
1500
- fontSize: 0,
1501
- padding: 2,
1502
- onClick: build
1599
+ id: `locjson-menu-${docId}`,
1600
+ button: /* @__PURE__ */ jsx(Button, { mode: "bleed", icon: JsonIcon, iconRight: ChevronDownIcon, title: "View LocJSON", fontSize: 0, padding: 2 }),
1601
+ menu: /* @__PURE__ */ jsxs(Menu, { children: [
1602
+ /* @__PURE__ */ jsx(MenuItem, { text: "Source only", onClick: () => build() }),
1603
+ targetLanguages.map((lang) => /* @__PURE__ */ jsx(MenuItem, { text: `Source + ${languageTitle(languages, lang)}`, onClick: () => build(lang) }, lang))
1604
+ ] }),
1605
+ popover: { portal: !0 }
1503
1606
  }
1504
1607
  ),
1505
1608
  open && /* @__PURE__ */ jsx(
@@ -1509,7 +1612,9 @@ function ViewLocjsonButton(props) {
1509
1612
  header: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
1510
1613
  /* @__PURE__ */ jsxs(Text, { weight: "semibold", children: [
1511
1614
  "LocJSON \xB7 ",
1512
- type
1615
+ type,
1616
+ " \xB7 ",
1617
+ title
1513
1618
  ] }),
1514
1619
  /* @__PURE__ */ jsx(
1515
1620
  Button,
@@ -1923,6 +2028,16 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1923
2028
  children: modeByType.get(item.doc._type) === "field" ? "fields" : "document"
1924
2029
  }
1925
2030
  ),
2031
+ /* @__PURE__ */ jsx(
2032
+ Badge,
2033
+ {
2034
+ tone: item.sendExistingTranslations ? "primary" : "default",
2035
+ fontSize: 0,
2036
+ paddingX: 2,
2037
+ title: item.sendExistingTranslations ? "Existing translations are uploaded to Smartcat (one bilingual file per language) and marked confirmed" : "Only source text is sent; Smartcat translates from scratch",
2038
+ children: item.sendExistingTranslations ? "source + translations" : "source only"
2039
+ }
2040
+ ),
1926
2041
  item.doc && /* @__PURE__ */ jsx(OpenInStudioButton, { id: item.doc._id, type: item.doc._type }),
1927
2042
  item.doc && /* @__PURE__ */ jsx(
1928
2043
  ViewLocjsonButton,
@@ -1934,7 +2049,9 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
1934
2049
  schema,
1935
2050
  sourceLanguage,
1936
2051
  documentI18nLanguageField,
1937
- fieldI18nLanguageField
2052
+ fieldI18nLanguageField,
2053
+ targetLanguages: targets,
2054
+ languages
1938
2055
  }
1939
2056
  )
1940
2057
  ] }),