@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
@@ -1,12 +1,8 @@
1
1
  import {useCallback, useEffect, useRef, useState} from 'react'
2
- import {useClient, useRelativeTime, useSchema, useValuePreview} from 'sanity'
2
+ import {useClient, useRelativeTime, useSchema} from 'sanity'
3
3
  import {applyImportedTranslations, prepareExport, type PendingDeletion} from './processing'
4
- import {getTranslatableFields, localizationMode} from '../lib/locjson/fields'
5
- import {serializeToLocjson} from '../lib/locjson/serialize'
6
- import {buildLocjsonFilename} from '../lib/locjson/filename'
7
- import {resolveDocumentTitle} from '../lib/documentTitle'
4
+ import {localizationMode} from '../lib/locjson/fields'
8
5
  import type {LocalizationMode} from '../lib/locjson/types'
9
- import {useIntentLink} from 'sanity/router'
10
6
  import {
11
7
  Badge,
12
8
  Box,
@@ -25,18 +21,18 @@ import {
25
21
  } from '@sanity/ui'
26
22
  import {
27
23
  ArrowLeftIcon,
28
- LaunchIcon,
29
24
  TrashIcon,
30
25
  PublishIcon,
31
26
  CloseIcon,
32
27
  DownloadIcon,
33
28
  SyncIcon,
34
29
  RemoveCircleIcon,
35
- JsonIcon,
36
30
  } from '@sanity/icons'
37
31
  import styled, {css, keyframes} from 'styled-components'
38
32
  import {API_VERSION} from '../lib/constants'
39
- import {UNTITLED} from '../lib/documentTitle'
33
+ import {DocTitle} from './DocTitle'
34
+ import {AddItemsSearch} from './AddItemsSearch'
35
+ import {OpenInStudioButton, ViewLocjsonButton} from './itemActions'
40
36
  import {PROJECT_DETAIL_QUERY, type ProjectDetail, type ProjectItemRef} from './data'
41
37
  import {StatusBadge, statusLabel} from './StatusBadge'
42
38
  import {ItemProgress, languageTitle} from './ItemProgress'
@@ -67,26 +63,6 @@ function languageMappingLabel(language: SmartcatLanguage): string {
67
63
  return code === language.id ? language.id : `${language.id} → ${code}`
68
64
  }
69
65
 
70
- /**
71
- * Resolve an item's title the way the Studio does — by running the schema type's
72
- * `preview.prepare` — so types without a literal `title` field (e.g. field-level
73
- * localized docs) show the same label as in the structure tree, not 'Untitled'.
74
- */
75
- function DocTitle({doc}: {doc: ProjectItemRef['doc']}) {
76
- const schema = useSchema()
77
- const schemaType = doc ? schema.get(doc._type) : undefined
78
- const preview = useValuePreview({
79
- enabled: Boolean(doc && schemaType),
80
- schemaType,
81
- value: doc ? {_id: doc._id, _type: doc._type} : undefined,
82
- })
83
- // String-only: never fall back to the raw projected value, which is an
84
- // internationalized-array object for field-level i18n types (renders as React #31).
85
- const title = typeof preview.value?.title === 'string' ? preview.value.title : undefined
86
- // Placeholder while the async preview resolves, so a real title isn't briefly mislabeled.
87
- return <>{title ?? (preview.isLoading ? '…' : UNTITLED)}</>
88
- }
89
-
90
66
  /**
91
67
  * Statuses during which a Function is actively running, so the project shouldn't
92
68
  * be re-sent until it settles. `translating`/`sent`/`completed` are resting
@@ -124,159 +100,6 @@ function LastUpdate({date}: {date: string}) {
124
100
  )
125
101
  }
126
102
 
127
- /** Opens the document in the Studio. */
128
- function OpenInStudioButton({id, type}: {id: string; type: string}) {
129
- const {onClick, href} = useIntentLink({intent: 'edit', params: {id, type}})
130
- return (
131
- <Button
132
- as="a"
133
- href={href}
134
- onClick={onClick}
135
- mode="bleed"
136
- icon={LaunchIcon}
137
- text="Open in Studio"
138
- fontSize={0}
139
- padding={2}
140
- />
141
- )
142
- }
143
-
144
- /** Read-only, monospace, full-height code view for the LocJSON modal. */
145
- const CodeArea = styled.textarea`
146
- width: 100%;
147
- height: 70vh;
148
- resize: none;
149
- border: none;
150
- outline: none;
151
- padding: 0;
152
- background: transparent;
153
- color: inherit;
154
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
155
- font-size: 12px;
156
- line-height: 1.5;
157
- white-space: pre;
158
- tab-size: 2;
159
- `
160
-
161
- /**
162
- * Builds the LocJSON the export would produce for an item — same field selection
163
- * and serialize options as {@link prepareExport} — and shows it (pretty-printed)
164
- * in a read-only modal, for inspecting exactly what gets sent to Smartcat.
165
- */
166
- function ViewLocjsonButton(props: {
167
- docId: string
168
- type: string
169
- /** Whether the export will translate the published doc (else prefer the draft). */
170
- sourceIsPublished: boolean | null
171
- client: {fetch: (query: string, params?: Record<string, unknown>) => Promise<unknown>}
172
- schema: {get: (typeName: string) => unknown}
173
- sourceLanguage: string
174
- documentI18nLanguageField: string
175
- fieldI18nLanguageField: string
176
- }) {
177
- const {docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField} = props
178
- const [open, setOpen] = useState(false)
179
- const [content, setContent] = useState<string | null>(null)
180
- const [filename, setFilename] = useState<string | null>(null)
181
- const [error, setError] = useState<string | null>(null)
182
-
183
- const build = useCallback(async () => {
184
- setOpen(true)
185
- setContent(null)
186
- setFilename(null)
187
- setError(null)
188
- try {
189
- // Resolve the same version the export will send: the draft (fallback to
190
- // published) unless the item was added from the published perspective.
191
- const raw = (await client.fetch(
192
- sourceIsPublished
193
- ? '*[_id == $id][0]'
194
- : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
195
- {id: docId},
196
- )) as (Record<string, unknown> & {_id: string}) | null
197
- if (!raw) throw new Error(`Document not found: ${docId}`)
198
- const isDraft = typeof raw._id === 'string' && raw._id.startsWith('drafts.')
199
- // Serialize under the canonical (bare) id, matching the export.
200
- const doc = isDraft ? {...raw, _id: docId} : raw
201
- const fields = getTranslatableFields(schema.get(type), {exclude: [documentI18nLanguageField]})
202
- const locjson = serializeToLocjson(doc as never, fields, {
203
- sourceLanguage,
204
- fieldLanguageKey: fieldI18nLanguageField,
205
- })
206
- // Same name the export sends to Smartcat; the last path segment is the
207
- // file's basename (the Smartcat "folder/" prefix can't be a local filename).
208
- const smartcatName = buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc as never), {draft: isDraft})
209
- setFilename(smartcatName.split('/').pop() || smartcatName)
210
- setContent(JSON.stringify(locjson, null, 2))
211
- } catch (err) {
212
- setError(err instanceof Error ? err.message : String(err))
213
- }
214
- }, [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField])
215
-
216
- const download = useCallback(() => {
217
- if (content == null || !filename) return
218
- const url = URL.createObjectURL(new Blob([content], {type: 'application/json'}))
219
- const anchor = document.createElement('a')
220
- anchor.href = url
221
- anchor.download = filename
222
- anchor.click()
223
- URL.revokeObjectURL(url)
224
- }, [content, filename])
225
-
226
- return (
227
- <>
228
- <Button
229
- mode="bleed"
230
- icon={JsonIcon}
231
- title="View LocJSON"
232
- fontSize={0}
233
- padding={2}
234
- onClick={build}
235
- />
236
- {open && (
237
- <Dialog
238
- id={`locjson-${docId}`}
239
- header={
240
- <Flex align="center" gap={3}>
241
- <Text weight="semibold">LocJSON · {type}</Text>
242
- <Button
243
- icon={DownloadIcon}
244
- text="Download"
245
- mode="ghost"
246
- fontSize={1}
247
- padding={2}
248
- disabled={content == null}
249
- onClick={download}
250
- />
251
- </Flex>
252
- }
253
- onClose={() => setOpen(false)}
254
- width={3}
255
- >
256
- <Box padding={4}>
257
- {error ? (
258
- <Text size={1} muted>
259
- {error}
260
- </Text>
261
- ) : content === null ? (
262
- <Flex align="center" justify="center" padding={5}>
263
- <Spinner muted />
264
- </Flex>
265
- ) : (
266
- <CodeArea
267
- readOnly
268
- spellCheck={false}
269
- value={content}
270
- onFocus={(e) => e.currentTarget.select()}
271
- />
272
- )}
273
- </Box>
274
- </Dialog>
275
- )}
276
- </>
277
- )
278
- }
279
-
280
103
  export function ProjectView({
281
104
  projectId,
282
105
  onBack,
@@ -912,6 +735,18 @@ export function ProjectView({
912
735
  {modeByType.get(item.doc._type) === 'field' ? 'fields' : 'document'}
913
736
  </Badge>
914
737
  )}
738
+ <Badge
739
+ tone={item.sendExistingTranslations ? 'primary' : 'default'}
740
+ fontSize={0}
741
+ paddingX={2}
742
+ title={
743
+ item.sendExistingTranslations
744
+ ? 'Existing translations are uploaded to Smartcat (one bilingual file per language) and marked confirmed'
745
+ : 'Only source text is sent; Smartcat translates from scratch'
746
+ }
747
+ >
748
+ {item.sendExistingTranslations ? 'source + translations' : 'source only'}
749
+ </Badge>
915
750
  {item.doc && <OpenInStudioButton id={item.doc._id} type={item.doc._type} />}
916
751
  {item.doc && (
917
752
  <ViewLocjsonButton
@@ -923,6 +758,8 @@ export function ProjectView({
923
758
  sourceLanguage={sourceLanguage}
924
759
  documentI18nLanguageField={documentI18nLanguageField}
925
760
  fieldI18nLanguageField={fieldI18nLanguageField}
761
+ targetLanguages={targets}
762
+ languages={languages}
926
763
  />
927
764
  )}
928
765
  </Flex>
@@ -970,11 +807,16 @@ export function ProjectView({
970
807
  </Stack>
971
808
  )}
972
809
 
973
- <Card padding={4} radius={2} tone="transparent" border>
974
- <Text muted align="center" size={1}>
975
- Add content from a document’s “⋯ → Add to translation project” menu.
976
- </Text>
977
- </Card>
810
+ <AddItemsSearch
811
+ projectId={projectId}
812
+ translatableTypes={translatableTypes}
813
+ existingDocIds={new Set(items.map((i) => i.docId))}
814
+ sourceLanguage={sourceLanguage}
815
+ languages={languages}
816
+ targetLanguages={targets}
817
+ documentI18nLanguageField={documentI18nLanguageField}
818
+ fieldI18nLanguageField={fieldI18nLanguageField}
819
+ />
978
820
  </Stack>
979
821
 
980
822
  {confirmDelete && (
package/src/tool/data.ts CHANGED
@@ -24,6 +24,12 @@ export interface ProjectItemRef {
24
24
  * and which version the export serializes. Null on legacy items (pre-flag).
25
25
  */
26
26
  sourceIsPublished: boolean | null
27
+ /**
28
+ * Whether the export sends this item's existing translations (one bilingual
29
+ * file per target language) or only its source text. Null/false on legacy
30
+ * items (pre-flag) ⇒ source-only, today's behaviour.
31
+ */
32
+ sendExistingTranslations: boolean | null
27
33
  doc: {_id: string; _type: string} | null
28
34
  /** Locale variants linked via translation.metadata: language -> variant doc id. */
29
35
  translations: {language: string; id: string}[] | null
@@ -73,6 +79,7 @@ export const PROJECT_DETAIL_QUERY = `*[_id == $id][0]{
73
79
  _key,
74
80
  "docId": ${ITEM_ID},
75
81
  sourceIsPublished,
82
+ sendExistingTranslations,
76
83
  "doc": ${itemDocDeref('{_id, _type}')},
77
84
  "translations": *[_type == "translation.metadata" && references(${ITEM_ID_FROM_SUBQUERY})][0]
78
85
  .translations[]{language, "id": value._ref}
@@ -0,0 +1,216 @@
1
+ import {useCallback, useState} from 'react'
2
+ import {useIntentLink} from 'sanity/router'
3
+ import {Box, Button, Dialog, Flex, Menu, MenuButton, MenuItem, Spinner, Text} from '@sanity/ui'
4
+ import {LaunchIcon, JsonIcon, ChevronDownIcon, DownloadIcon} from '@sanity/icons'
5
+ import styled from 'styled-components'
6
+ import {getTranslatableFields, localizationMode} from '../lib/locjson/fields'
7
+ import {serializeToLocjson} from '../lib/locjson/serialize'
8
+ import {buildLocjsonFilename} from '../lib/locjson/filename'
9
+ import {resolveDocumentTitle} from '../lib/documentTitle'
10
+ import {languageTitle} from './ItemProgress'
11
+ import type {SmartcatLanguage} from '../types'
12
+
13
+ /** Opens the document in the Studio. */
14
+ export function OpenInStudioButton({id, type}: {id: string; type: string}) {
15
+ const {onClick, href} = useIntentLink({intent: 'edit', params: {id, type}})
16
+ return (
17
+ <Button
18
+ as="a"
19
+ href={href}
20
+ onClick={onClick}
21
+ mode="bleed"
22
+ icon={LaunchIcon}
23
+ text="Open in Studio"
24
+ fontSize={0}
25
+ padding={2}
26
+ />
27
+ )
28
+ }
29
+
30
+ /** Read-only, monospace, full-height code view for the LocJSON modal. */
31
+ const CodeArea = styled.textarea`
32
+ width: 100%;
33
+ height: 70vh;
34
+ resize: none;
35
+ border: none;
36
+ outline: none;
37
+ padding: 0;
38
+ background: transparent;
39
+ color: inherit;
40
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
41
+ font-size: 12px;
42
+ line-height: 1.5;
43
+ white-space: pre;
44
+ tab-size: 2;
45
+ `
46
+
47
+ /**
48
+ * Target-locale siblings of a document-level item, via translation.metadata. The
49
+ * member's locale lives in its language field (v5+, uuid `_key`) or in `_key`
50
+ * itself (v4.x) — `coalesce` reads whichever applies, per {@link fieldLanguageKey}.
51
+ */
52
+ function siblingsQuery(fieldLanguageKey: string): string {
53
+ return `*[_type == "translation.metadata" && references($id)][0].translations[]{"language": coalesce(${fieldLanguageKey}, _key), "doc": value->}`
54
+ }
55
+
56
+ /**
57
+ * Dropdown that builds the LocJSON the export would produce for an item — same
58
+ * field selection and serialize options as {@link prepareExport} — and shows it
59
+ * (pretty-printed) in a read-only modal. Offers "Source only" (what a plain
60
+ * export sends) plus "Source + <language>" for each target language: the
61
+ * bilingual file the send-existing flow would upload for that language, so you
62
+ * can inspect exactly what goes to Smartcat per language.
63
+ */
64
+ export function ViewLocjsonButton(props: {
65
+ docId: string
66
+ type: string
67
+ /** Whether the export will translate the published doc (else prefer the draft). */
68
+ sourceIsPublished: boolean | null
69
+ client: {fetch: (query: string, params?: Record<string, unknown>) => Promise<unknown>}
70
+ schema: {get: (typeName: string) => unknown}
71
+ sourceLanguage: string
72
+ documentI18nLanguageField: string
73
+ fieldI18nLanguageField: string
74
+ /** Project target languages (Sanity locale ids). */
75
+ targetLanguages: string[]
76
+ /** Configured languages (for titles + Smartcat codes). */
77
+ languages: SmartcatLanguage[]
78
+ }) {
79
+ const {docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, targetLanguages, languages} = props
80
+ const [open, setOpen] = useState(false)
81
+ const [content, setContent] = useState<string | null>(null)
82
+ const [filename, setFilename] = useState<string | null>(null)
83
+ const [title, setTitle] = useState<string>('Source only')
84
+ const [error, setError] = useState<string | null>(null)
85
+
86
+ // Build the LocJSON for one variant: `targetLanguage` undefined ⇒ source-only
87
+ // (a plain export), otherwise the bilingual file for that Sanity locale.
88
+ const build = useCallback(
89
+ async (targetLanguage?: string) => {
90
+ setOpen(true)
91
+ setContent(null)
92
+ setFilename(null)
93
+ setError(null)
94
+ setTitle(targetLanguage ? `Source + ${languageTitle(languages, targetLanguage)}` : 'Source only')
95
+ try {
96
+ // Resolve the same version the export will send: the draft (fallback to
97
+ // published) unless the item was added from the published perspective.
98
+ const raw = (await client.fetch(
99
+ sourceIsPublished
100
+ ? '*[_id == $id][0]'
101
+ : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
102
+ {id: docId},
103
+ )) as (Record<string, unknown> & {_id: string}) | null
104
+ if (!raw) throw new Error(`Document not found: ${docId}`)
105
+ const isDraft = typeof raw._id === 'string' && raw._id.startsWith('drafts.')
106
+ // Serialize under the canonical (bare) id, matching the export.
107
+ const doc = isDraft ? {...raw, _id: docId} : raw
108
+ const fields = getTranslatableFields(schema.get(type), {exclude: [documentI18nLanguageField]})
109
+
110
+ // For a bilingual variant, resolve the existing translation the same way
111
+ // the export does: field-level from the inline member, document-level from
112
+ // the target-locale sibling document.
113
+ let target: {language: string; doc?: Record<string, unknown> & {_id: string; _type: string}} | undefined
114
+ if (targetLanguage) {
115
+ if (localizationMode(schema.get(type)) === 'document') {
116
+ const variants = (await client.fetch(siblingsQuery(fieldI18nLanguageField), {id: docId})) as
117
+ | {language: string; doc: (Record<string, unknown> & {_id: string; _type: string}) | null}[]
118
+ | null
119
+ const sibling = (variants ?? []).find((v) => v.language === targetLanguage)?.doc ?? undefined
120
+ target = {language: targetLanguage, doc: sibling}
121
+ } else {
122
+ target = {language: targetLanguage}
123
+ }
124
+ }
125
+
126
+ const locjson = serializeToLocjson(doc as never, fields, {
127
+ sourceLanguage,
128
+ fieldLanguageKey: fieldI18nLanguageField,
129
+ target,
130
+ })
131
+ // Same name the export sends to Smartcat; the last path segment is the
132
+ // file's basename (the Smartcat "folder/" prefix can't be a local filename).
133
+ const smartcatCode = targetLanguage
134
+ ? languages.find((l) => l.id === targetLanguage)?.smartcatLanguage || targetLanguage
135
+ : undefined
136
+ const smartcatName = buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc as never), {
137
+ draft: isDraft,
138
+ language: smartcatCode,
139
+ })
140
+ setFilename(smartcatName.split('/').pop() || smartcatName)
141
+ setContent(JSON.stringify(locjson, null, 2))
142
+ } catch (err) {
143
+ setError(err instanceof Error ? err.message : String(err))
144
+ }
145
+ },
146
+ [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, languages],
147
+ )
148
+
149
+ const download = useCallback(() => {
150
+ if (content == null || !filename) return
151
+ const url = URL.createObjectURL(new Blob([content], {type: 'application/json'}))
152
+ const anchor = document.createElement('a')
153
+ anchor.href = url
154
+ anchor.download = filename
155
+ anchor.click()
156
+ URL.revokeObjectURL(url)
157
+ }, [content, filename])
158
+
159
+ return (
160
+ <>
161
+ <MenuButton
162
+ id={`locjson-menu-${docId}`}
163
+ button={<Button mode="bleed" icon={JsonIcon} iconRight={ChevronDownIcon} title="View LocJSON" fontSize={0} padding={2} />}
164
+ menu={
165
+ <Menu>
166
+ <MenuItem text="Source only" onClick={() => build()} />
167
+ {targetLanguages.map((lang) => (
168
+ <MenuItem key={lang} text={`Source + ${languageTitle(languages, lang)}`} onClick={() => build(lang)} />
169
+ ))}
170
+ </Menu>
171
+ }
172
+ popover={{portal: true}}
173
+ />
174
+ {open && (
175
+ <Dialog
176
+ id={`locjson-${docId}`}
177
+ header={
178
+ <Flex align="center" gap={3}>
179
+ <Text weight="semibold">LocJSON · {type} · {title}</Text>
180
+ <Button
181
+ icon={DownloadIcon}
182
+ text="Download"
183
+ mode="ghost"
184
+ fontSize={1}
185
+ padding={2}
186
+ disabled={content == null}
187
+ onClick={download}
188
+ />
189
+ </Flex>
190
+ }
191
+ onClose={() => setOpen(false)}
192
+ width={3}
193
+ >
194
+ <Box padding={4}>
195
+ {error ? (
196
+ <Text size={1} muted>
197
+ {error}
198
+ </Text>
199
+ ) : content === null ? (
200
+ <Flex align="center" justify="center" padding={5}>
201
+ <Spinner muted />
202
+ </Flex>
203
+ ) : (
204
+ <CodeArea
205
+ readOnly
206
+ spellCheck={false}
207
+ value={content}
208
+ onFocus={(e) => e.currentTarget.select()}
209
+ />
210
+ )}
211
+ </Box>
212
+ </Dialog>
213
+ )}
214
+ </>
215
+ )
216
+ }
@@ -1,7 +1,7 @@
1
1
  import {beforeAll, describe, expect, it} from 'vitest'
2
2
  import {Schema} from '@sanity/schema'
3
3
  import {JSDOM} from 'jsdom'
4
- import {applyImportedTranslations, prepareExport, type ProcessingClient} from './processing'
4
+ import {applyImportedTranslations, prepareExport, siblingsQuery, type ProcessingClient} from './processing'
5
5
 
6
6
  // processing.ts uses the browser's global DOMParser; provide one via jsdom.
7
7
  beforeAll(() => {
@@ -696,3 +696,13 @@ describe('nested + array-member field-level translation', () => {
696
696
  ])
697
697
  })
698
698
  })
699
+
700
+ describe('siblingsQuery', () => {
701
+ // The v5+ translation.metadata member stores its locale in a language field
702
+ // with a uuid `_key`; reading only `_key` (the v4.x layout) missed every v5+
703
+ // sibling, so send-existing attached no target for document-level items.
704
+ it('reads the locale from the configured field, falling back to `_key` (v4.x)', () => {
705
+ expect(siblingsQuery('language')).toContain('"language": coalesce(language, _key)')
706
+ expect(siblingsQuery('_key')).toContain('"language": coalesce(_key, _key)')
707
+ })
708
+ })