@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.
- package/dist/_chunks-cjs/index.cjs +18 -5
- package/dist/_chunks-cjs/index.cjs.map +1 -1
- package/dist/_chunks-cjs/index2.cjs +8 -6
- package/dist/_chunks-cjs/index2.cjs.map +1 -1
- package/dist/_chunks-es/index.js +18 -5
- package/dist/_chunks-es/index.js.map +1 -1
- package/dist/_chunks-es/index2.js +8 -6
- package/dist/_chunks-es/index2.js.map +1 -1
- package/dist/export.cjs +21 -4
- package/dist/export.cjs.map +1 -1
- package/dist/export.d.cts +2 -0
- package/dist/export.d.ts +2 -0
- package/dist/export.js +22 -5
- package/dist/export.js.map +1 -1
- package/dist/index.cjs +477 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +482 -129
- package/dist/index.js.map +1 -1
- package/dist/locjson.d.cts +15 -0
- package/dist/locjson.d.ts +15 -0
- package/dist/progress.d.cts +0 -19
- package/dist/progress.d.ts +0 -19
- package/dist/smartcat.cjs +14 -2
- package/dist/smartcat.cjs.map +1 -1
- package/dist/smartcat.d.cts +10 -1
- package/dist/smartcat.d.ts +10 -1
- package/dist/smartcat.js +14 -2
- package/dist/smartcat.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/AddToProjectAction.tsx +23 -2
- package/src/export/export.test.ts +42 -5
- package/src/export/index.ts +63 -8
- package/src/lib/locjson/filename.ts +7 -2
- package/src/lib/locjson/locjson.test.ts +64 -0
- package/src/lib/locjson/serialize.ts +38 -2
- package/src/progress/core.ts +44 -3
- package/src/progress/progress.test.ts +40 -0
- package/src/schema/translationProject.ts +19 -0
- package/src/smartcat/client.ts +15 -1
- package/src/tool/AddItemsSearch.tsx +462 -0
- package/src/tool/DocTitle.tsx +23 -0
- package/src/tool/ProjectView.tsx +29 -187
- package/src/tool/data.ts +7 -0
- package/src/tool/itemActions.tsx +216 -0
- package/src/tool/processing.test.ts +11 -1
- package/src/tool/processing.ts +116 -4
package/src/tool/processing.ts
CHANGED
|
@@ -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: {
|
|
232
|
-
|
|
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
|
-
|
|
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.
|