@smartcat/sanity-plugin 1.3.0 → 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.
@@ -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,
@@ -17,9 +13,6 @@ import {
17
13
  Flex,
18
14
  Heading,
19
15
  Inline,
20
- Menu,
21
- MenuButton,
22
- MenuItem,
23
16
  Spinner,
24
17
  Stack,
25
18
  Text,
@@ -28,19 +21,18 @@ import {
28
21
  } from '@sanity/ui'
29
22
  import {
30
23
  ArrowLeftIcon,
31
- LaunchIcon,
32
24
  TrashIcon,
33
25
  PublishIcon,
34
26
  CloseIcon,
35
27
  DownloadIcon,
36
28
  SyncIcon,
37
29
  RemoveCircleIcon,
38
- JsonIcon,
39
- ChevronDownIcon,
40
30
  } from '@sanity/icons'
41
31
  import styled, {css, keyframes} from 'styled-components'
42
32
  import {API_VERSION} from '../lib/constants'
43
- import {UNTITLED} from '../lib/documentTitle'
33
+ import {DocTitle} from './DocTitle'
34
+ import {AddItemsSearch} from './AddItemsSearch'
35
+ import {OpenInStudioButton, ViewLocjsonButton} from './itemActions'
44
36
  import {PROJECT_DETAIL_QUERY, type ProjectDetail, type ProjectItemRef} from './data'
45
37
  import {StatusBadge, statusLabel} from './StatusBadge'
46
38
  import {ItemProgress, languageTitle} from './ItemProgress'
@@ -71,26 +63,6 @@ function languageMappingLabel(language: SmartcatLanguage): string {
71
63
  return code === language.id ? language.id : `${language.id} → ${code}`
72
64
  }
73
65
 
74
- /**
75
- * Resolve an item's title the way the Studio does — by running the schema type's
76
- * `preview.prepare` — so types without a literal `title` field (e.g. field-level
77
- * localized docs) show the same label as in the structure tree, not 'Untitled'.
78
- */
79
- function DocTitle({doc}: {doc: ProjectItemRef['doc']}) {
80
- const schema = useSchema()
81
- const schemaType = doc ? schema.get(doc._type) : undefined
82
- const preview = useValuePreview({
83
- enabled: Boolean(doc && schemaType),
84
- schemaType,
85
- value: doc ? {_id: doc._id, _type: doc._type} : undefined,
86
- })
87
- // String-only: never fall back to the raw projected value, which is an
88
- // internationalized-array object for field-level i18n types (renders as React #31).
89
- const title = typeof preview.value?.title === 'string' ? preview.value.title : undefined
90
- // Placeholder while the async preview resolves, so a real title isn't briefly mislabeled.
91
- return <>{title ?? (preview.isLoading ? '…' : UNTITLED)}</>
92
- }
93
-
94
66
  /**
95
67
  * Statuses during which a Function is actively running, so the project shouldn't
96
68
  * be re-sent until it settles. `translating`/`sent`/`completed` are resting
@@ -128,205 +100,6 @@ function LastUpdate({date}: {date: string}) {
128
100
  )
129
101
  }
130
102
 
131
- /** Opens the document in the Studio. */
132
- function OpenInStudioButton({id, type}: {id: string; type: string}) {
133
- const {onClick, href} = useIntentLink({intent: 'edit', params: {id, type}})
134
- return (
135
- <Button
136
- as="a"
137
- href={href}
138
- onClick={onClick}
139
- mode="bleed"
140
- icon={LaunchIcon}
141
- text="Open in Studio"
142
- fontSize={0}
143
- padding={2}
144
- />
145
- )
146
- }
147
-
148
- /** Read-only, monospace, full-height code view for the LocJSON modal. */
149
- const CodeArea = styled.textarea`
150
- width: 100%;
151
- height: 70vh;
152
- resize: none;
153
- border: none;
154
- outline: none;
155
- padding: 0;
156
- background: transparent;
157
- color: inherit;
158
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
159
- font-size: 12px;
160
- line-height: 1.5;
161
- white-space: pre;
162
- tab-size: 2;
163
- `
164
-
165
- /** Target-locale siblings of a document-level item, via translation.metadata. */
166
- const VIEW_SIBLINGS_QUERY = `*[_type == "translation.metadata" && references($id)][0].translations[]{"language": _key, "doc": value->}`
167
-
168
- /**
169
- * Dropdown that builds the LocJSON the export would produce for an item — same
170
- * field selection and serialize options as {@link prepareExport} — and shows it
171
- * (pretty-printed) in a read-only modal. Offers "Source only" (what a plain
172
- * export sends) plus "Source + <language>" for each target language: the
173
- * bilingual file the send-existing flow would upload for that language, so you
174
- * can inspect exactly what goes to Smartcat per language.
175
- */
176
- function ViewLocjsonButton(props: {
177
- docId: string
178
- type: string
179
- /** Whether the export will translate the published doc (else prefer the draft). */
180
- sourceIsPublished: boolean | null
181
- client: {fetch: (query: string, params?: Record<string, unknown>) => Promise<unknown>}
182
- schema: {get: (typeName: string) => unknown}
183
- sourceLanguage: string
184
- documentI18nLanguageField: string
185
- fieldI18nLanguageField: string
186
- /** Project target languages (Sanity locale ids). */
187
- targetLanguages: string[]
188
- /** Configured languages (for titles + Smartcat codes). */
189
- languages: SmartcatLanguage[]
190
- }) {
191
- const {docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, targetLanguages, languages} = props
192
- const [open, setOpen] = useState(false)
193
- const [content, setContent] = useState<string | null>(null)
194
- const [filename, setFilename] = useState<string | null>(null)
195
- const [title, setTitle] = useState<string>('Source only')
196
- const [error, setError] = useState<string | null>(null)
197
-
198
- // Build the LocJSON for one variant: `targetLanguage` undefined ⇒ source-only
199
- // (a plain export), otherwise the bilingual file for that Sanity locale.
200
- const build = useCallback(
201
- async (targetLanguage?: string) => {
202
- setOpen(true)
203
- setContent(null)
204
- setFilename(null)
205
- setError(null)
206
- setTitle(targetLanguage ? `Source + ${languageTitle(languages, targetLanguage)}` : 'Source only')
207
- try {
208
- // Resolve the same version the export will send: the draft (fallback to
209
- // published) unless the item was added from the published perspective.
210
- const raw = (await client.fetch(
211
- sourceIsPublished
212
- ? '*[_id == $id][0]'
213
- : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
214
- {id: docId},
215
- )) as (Record<string, unknown> & {_id: string}) | null
216
- if (!raw) throw new Error(`Document not found: ${docId}`)
217
- const isDraft = typeof raw._id === 'string' && raw._id.startsWith('drafts.')
218
- // Serialize under the canonical (bare) id, matching the export.
219
- const doc = isDraft ? {...raw, _id: docId} : raw
220
- const fields = getTranslatableFields(schema.get(type), {exclude: [documentI18nLanguageField]})
221
-
222
- // For a bilingual variant, resolve the existing translation the same way
223
- // the export does: field-level from the inline member, document-level from
224
- // the target-locale sibling document.
225
- let target: {language: string; doc?: Record<string, unknown> & {_id: string; _type: string}} | undefined
226
- if (targetLanguage) {
227
- if (localizationMode(schema.get(type)) === 'document') {
228
- const variants = (await client.fetch(VIEW_SIBLINGS_QUERY, {id: docId})) as
229
- | {language: string; doc: (Record<string, unknown> & {_id: string; _type: string}) | null}[]
230
- | null
231
- const sibling = (variants ?? []).find((v) => v.language === targetLanguage)?.doc ?? undefined
232
- target = {language: targetLanguage, doc: sibling}
233
- } else {
234
- target = {language: targetLanguage}
235
- }
236
- }
237
-
238
- const locjson = serializeToLocjson(doc as never, fields, {
239
- sourceLanguage,
240
- fieldLanguageKey: fieldI18nLanguageField,
241
- target,
242
- })
243
- // Same name the export sends to Smartcat; the last path segment is the
244
- // file's basename (the Smartcat "folder/" prefix can't be a local filename).
245
- const smartcatCode = targetLanguage
246
- ? languages.find((l) => l.id === targetLanguage)?.smartcatLanguage || targetLanguage
247
- : undefined
248
- const smartcatName = buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc as never), {
249
- draft: isDraft,
250
- language: smartcatCode,
251
- })
252
- setFilename(smartcatName.split('/').pop() || smartcatName)
253
- setContent(JSON.stringify(locjson, null, 2))
254
- } catch (err) {
255
- setError(err instanceof Error ? err.message : String(err))
256
- }
257
- },
258
- [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, languages],
259
- )
260
-
261
- const download = useCallback(() => {
262
- if (content == null || !filename) return
263
- const url = URL.createObjectURL(new Blob([content], {type: 'application/json'}))
264
- const anchor = document.createElement('a')
265
- anchor.href = url
266
- anchor.download = filename
267
- anchor.click()
268
- URL.revokeObjectURL(url)
269
- }, [content, filename])
270
-
271
- return (
272
- <>
273
- <MenuButton
274
- id={`locjson-menu-${docId}`}
275
- button={<Button mode="bleed" icon={JsonIcon} iconRight={ChevronDownIcon} title="View LocJSON" fontSize={0} padding={2} />}
276
- menu={
277
- <Menu>
278
- <MenuItem text="Source only" onClick={() => build()} />
279
- {targetLanguages.map((lang) => (
280
- <MenuItem key={lang} text={`Source + ${languageTitle(languages, lang)}`} onClick={() => build(lang)} />
281
- ))}
282
- </Menu>
283
- }
284
- popover={{portal: true}}
285
- />
286
- {open && (
287
- <Dialog
288
- id={`locjson-${docId}`}
289
- header={
290
- <Flex align="center" gap={3}>
291
- <Text weight="semibold">LocJSON · {type} · {title}</Text>
292
- <Button
293
- icon={DownloadIcon}
294
- text="Download"
295
- mode="ghost"
296
- fontSize={1}
297
- padding={2}
298
- disabled={content == null}
299
- onClick={download}
300
- />
301
- </Flex>
302
- }
303
- onClose={() => setOpen(false)}
304
- width={3}
305
- >
306
- <Box padding={4}>
307
- {error ? (
308
- <Text size={1} muted>
309
- {error}
310
- </Text>
311
- ) : content === null ? (
312
- <Flex align="center" justify="center" padding={5}>
313
- <Spinner muted />
314
- </Flex>
315
- ) : (
316
- <CodeArea
317
- readOnly
318
- spellCheck={false}
319
- value={content}
320
- onFocus={(e) => e.currentTarget.select()}
321
- />
322
- )}
323
- </Box>
324
- </Dialog>
325
- )}
326
- </>
327
- )
328
- }
329
-
330
103
  export function ProjectView({
331
104
  projectId,
332
105
  onBack,
@@ -1034,11 +807,16 @@ export function ProjectView({
1034
807
  </Stack>
1035
808
  )}
1036
809
 
1037
- <Card padding={4} radius={2} tone="transparent" border>
1038
- <Text muted align="center" size={1}>
1039
- Add content from a document’s “⋯ → Add to translation project” menu.
1040
- </Text>
1041
- </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
+ />
1042
820
  </Stack>
1043
821
 
1044
822
  {confirmDelete && (
@@ -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
+ })
@@ -153,10 +153,16 @@ const ITEMS_QUERY = `*[_id == $id][0]{
153
153
  * group, every locale variant's `{language, doc}` — the send-existing flow reads
154
154
  * each target language's sibling as the translation to upload. Document-level
155
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.
156
160
  */
157
- const SIBLINGS_QUERY = `*[_type == "translation.metadata" && references($ids)]{
158
- "variants": translations[]{ "language": _key, "doc": value-> }
161
+ export function siblingsQuery(fieldLanguageKey: string): string {
162
+ return `*[_type == "translation.metadata" && references($ids)]{
163
+ "variants": translations[]{ "language": coalesce(${fieldLanguageKey}, _key), "doc": value-> }
159
164
  }`
165
+ }
160
166
 
161
167
  export async function prepareExport({
162
168
  client,
@@ -222,7 +228,7 @@ export async function prepareExport({
222
228
  .map((item) => publishedId((item.doc as {_id: string})._id))
223
229
  if (flaggedDocLevelIds.length > 0) {
224
230
  const groups = await client.fetch<{variants: {language: string; doc: TargetDoc | null}[]}[]>(
225
- SIBLINGS_QUERY,
231
+ siblingsQuery(fieldLanguageKey),
226
232
  {ids: flaggedDocLevelIds},
227
233
  )
228
234
  for (const group of groups ?? []) {