@smartcat/sanity-plugin 1.0.0 → 1.0.1
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/README.md +294 -0
- package/dist/_chunks-cjs/constants.cjs +13 -0
- package/dist/_chunks-cjs/constants.cjs.map +1 -0
- package/dist/_chunks-cjs/index.cjs +168 -0
- package/dist/_chunks-cjs/index.cjs.map +1 -0
- package/dist/_chunks-cjs/index2.cjs +311 -0
- package/dist/_chunks-cjs/index2.cjs.map +1 -0
- package/dist/_chunks-cjs/workflow.cjs +16 -0
- package/dist/_chunks-cjs/workflow.cjs.map +1 -0
- package/dist/_chunks-es/constants.js +14 -0
- package/dist/_chunks-es/constants.js.map +1 -0
- package/dist/_chunks-es/index.js +169 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/index2.js +313 -0
- package/dist/_chunks-es/index2.js.map +1 -0
- package/dist/_chunks-es/workflow.js +17 -0
- package/dist/_chunks-es/workflow.js.map +1 -0
- package/dist/export.cjs +160 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +162 -0
- package/dist/export.d.ts +162 -0
- package/dist/export.js +162 -0
- package/dist/export.js.map +1 -0
- package/dist/import.cjs +169 -0
- package/dist/import.cjs.map +1 -0
- package/dist/import.d.cts +155 -0
- package/dist/import.d.ts +155 -0
- package/dist/import.js +169 -0
- package/dist/import.js.map +1 -0
- package/dist/index.cjs +2103 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2110 -0
- package/dist/index.js.map +1 -0
- package/dist/locjson.cjs +12 -0
- package/dist/locjson.cjs.map +1 -0
- package/dist/locjson.d.cts +287 -0
- package/dist/locjson.d.ts +287 -0
- package/dist/locjson.js +12 -0
- package/dist/locjson.js.map +1 -0
- package/dist/progress.cjs +16 -0
- package/dist/progress.cjs.map +1 -0
- package/dist/progress.d.cts +262 -0
- package/dist/progress.d.ts +262 -0
- package/dist/progress.js +16 -0
- package/dist/progress.js.map +1 -0
- package/dist/smartcat.cjs +287 -0
- package/dist/smartcat.cjs.map +1 -0
- package/dist/smartcat.d.cts +234 -0
- package/dist/smartcat.d.ts +234 -0
- package/dist/smartcat.js +287 -0
- package/dist/smartcat.js.map +1 -0
- package/dist/templates.cjs +12 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +52 -0
- package/dist/templates.d.ts +52 -0
- package/dist/templates.js +12 -0
- package/dist/templates.js.map +1 -0
- package/package.json +101 -15
- package/src/actions/AddToProjectAction.tsx +258 -0
- package/src/export/export.test.ts +537 -0
- package/src/export/index.ts +393 -0
- package/src/form/TranslationStatusInput.tsx +212 -0
- package/src/import/import.test.ts +346 -0
- package/src/import/index.ts +418 -0
- package/src/index.ts +63 -0
- package/src/lib/constants.ts +23 -0
- package/src/lib/documentTitle.test.ts +43 -0
- package/src/lib/documentTitle.ts +30 -0
- package/src/lib/languageMap.test.ts +56 -0
- package/src/lib/languageMap.ts +71 -0
- package/src/lib/linkedDocuments.test.ts +56 -0
- package/src/lib/linkedDocuments.ts +100 -0
- package/src/lib/locjson/deserialize.ts +60 -0
- package/src/lib/locjson/fields.ts +355 -0
- package/src/lib/locjson/filename.ts +27 -0
- package/src/lib/locjson/index.ts +10 -0
- package/src/lib/locjson/locjson.test.ts +545 -0
- package/src/lib/locjson/paths.test.ts +41 -0
- package/src/lib/locjson/paths.ts +73 -0
- package/src/lib/locjson/portableText.ts +98 -0
- package/src/lib/locjson/serialize.ts +124 -0
- package/src/lib/locjson/types.ts +106 -0
- package/src/lib/log.ts +33 -0
- package/src/lib/projectItems.ts +17 -0
- package/src/lib/resolveConfig.test.ts +62 -0
- package/src/lib/resolveConfig.ts +47 -0
- package/src/lib/workflow.test.ts +37 -0
- package/src/lib/workflow.ts +48 -0
- package/src/lib/zip.fixtures.ts +75 -0
- package/src/lib/zip.test.ts +110 -0
- package/src/lib/zip.ts +164 -0
- package/src/progress/core.ts +387 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/progress.test.ts +268 -0
- package/src/schema/settings.ts +55 -0
- package/src/schema/translatableOptions.ts +25 -0
- package/src/schema/translationProject.ts +264 -0
- package/src/smartcat/client.test.ts +222 -0
- package/src/smartcat/client.ts +351 -0
- package/src/smartcat/index.ts +10 -0
- package/src/smartcat/types.ts +99 -0
- package/src/templates/core.ts +55 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/templates.test.ts +50 -0
- package/src/tool/Dashboard.tsx +44 -0
- package/src/tool/ItemProgress.tsx +176 -0
- package/src/tool/LogPanel.tsx +207 -0
- package/src/tool/ProjectView.tsx +975 -0
- package/src/tool/ProjectsView.tsx +208 -0
- package/src/tool/StatusBadge.tsx +33 -0
- package/src/tool/StudioInit.tsx +42 -0
- package/src/tool/WorkflowSelect.tsx +41 -0
- package/src/tool/data.ts +79 -0
- package/src/tool/index.ts +19 -0
- package/src/tool/processing.test.ts +623 -0
- package/src/tool/processing.ts +766 -0
- package/src/tool/useTemplates.ts +84 -0
- package/src/tool/useWorkflowSelection.ts +65 -0
- package/src/types.ts +77 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
buildProgress,
|
|
4
|
+
buildStageNameMap,
|
|
5
|
+
isTargetComplete,
|
|
6
|
+
itemsFromProject,
|
|
7
|
+
runProgressSync,
|
|
8
|
+
sourceIdPrefix,
|
|
9
|
+
stagesFromDocument,
|
|
10
|
+
summarizeCompletion,
|
|
11
|
+
targetPercent,
|
|
12
|
+
type DocProgress,
|
|
13
|
+
} from './index'
|
|
14
|
+
import type {SmartcatProject} from '../smartcat/types'
|
|
15
|
+
import {SmartcatTimeoutError} from '../smartcat/client'
|
|
16
|
+
|
|
17
|
+
// Stage-id → name map mirroring Smartcat's project-level workflowStages.
|
|
18
|
+
const STAGE_NAMES = [
|
|
19
|
+
{id: 'st-tr', stageType: 'translation'},
|
|
20
|
+
{id: 'st-ed', stageType: 'editing'},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
describe('stage helpers', () => {
|
|
24
|
+
it('names document stages via the project-level stage map and clamps percentages', () => {
|
|
25
|
+
const names = buildStageNameMap({id: 'p', workflowStages: STAGE_NAMES})
|
|
26
|
+
const stages = stagesFromDocument(
|
|
27
|
+
{id: 'g_1', workflowStages: [{id: 'st-tr', progress: 100}, {id: 'st-ed', progress: 42.6}]},
|
|
28
|
+
names,
|
|
29
|
+
)
|
|
30
|
+
expect(stages).toEqual([
|
|
31
|
+
{_key: 'st-tr', name: 'Translation', percent: 100},
|
|
32
|
+
{_key: 'st-ed', name: 'Editing', percent: 43},
|
|
33
|
+
])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('treats missing/empty stages as not complete', () => {
|
|
37
|
+
expect(isTargetComplete([])).toBe(false)
|
|
38
|
+
expect(isTargetComplete([{name: 'T', percent: 100}])).toBe(true)
|
|
39
|
+
expect(isTargetComplete([{name: 'T', percent: 100}, {name: 'E', percent: 99}])).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('averages stage percentages', () => {
|
|
43
|
+
expect(targetPercent([{name: 'T', percent: 100}, {name: 'E', percent: 50}])).toBe(75)
|
|
44
|
+
expect(targetPercent([])).toBe(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('extracts the source-id prefix from a document filename', () => {
|
|
48
|
+
expect(sourceIdPrefix({id: 'g_1', name: 'About Us-f18f0d41', fullPath: 'page/About Us-f18f0d41'})).toBe('f18f0d41')
|
|
49
|
+
expect(sourceIdPrefix({id: 'g_1', filename: 'Contact-3f14f092.locjson'})).toBe('3f14f092')
|
|
50
|
+
// The prefix is the final 8 chars — robust to dashes in the title or the id
|
|
51
|
+
// prefix itself (e.g. a custom id `demo-pdpA` → prefix `demo-pdp`).
|
|
52
|
+
expect(sourceIdPrefix({id: 'g_1', name: 'Product A-demo-pdp', fullPath: 'pdpPage/Product A-demo-pdp'})).toBe('demo-pdp')
|
|
53
|
+
expect(sourceIdPrefix({id: 'g_1', filename: 'Some-Dashed-Title-3f14f092.locjson'})).toBe('3f14f092')
|
|
54
|
+
// DOTTED titles (e.g. micro-copy keys): only a literal `.locjson` may be
|
|
55
|
+
// stripped — a generic strip-after-last-dot would eat `.resetChat-11AKrV8G`
|
|
56
|
+
// and return the wrong prefix, so the document never correlates.
|
|
57
|
+
expect(sourceIdPrefix({id: 'g_1', name: 'aiAssistantModal.resetChat-11AKrV8G'})).toBe('11AKrV8G')
|
|
58
|
+
expect(sourceIdPrefix({id: 'g_1', filename: 'aiAssistantModal.resetChat-11AKrV8G.locjson'})).toBe('11AKrV8G')
|
|
59
|
+
expect(sourceIdPrefix({id: 'g_1', name: 'v2.5 Landing Page-ab12cd34'})).toBe('ab12cd34')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const scProject: SmartcatProject = {
|
|
64
|
+
id: 'p1',
|
|
65
|
+
workflowStages: STAGE_NAMES,
|
|
66
|
+
documents: [
|
|
67
|
+
{id: '817_12', name: 'About Us-f18f0d41', targetLanguage: 'fr', workflowStages: [{id: 'st-tr', progress: 100}, {id: 'st-ed', progress: 100}]},
|
|
68
|
+
{id: '817_10', name: 'About Us-f18f0d41', targetLanguage: 'de', workflowStages: [{id: 'st-tr', progress: 30}, {id: 'st-ed', progress: 0}]},
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const items = [{_id: 'f18f0d41-aaaa-bbbb', title: 'About Us'}]
|
|
73
|
+
|
|
74
|
+
describe('buildProgress', () => {
|
|
75
|
+
it('correlates documents to items by id prefix and groups by source doc', () => {
|
|
76
|
+
const progress = buildProgress(items, scProject, undefined, '2026-06-11T00:00:00Z')
|
|
77
|
+
expect(progress).toHaveLength(1)
|
|
78
|
+
expect(progress[0]).toMatchObject({sourceDocId: 'f18f0d41-aaaa-bbbb', title: 'About Us'})
|
|
79
|
+
const [de, fr] = progress[0].targets // sorted by language
|
|
80
|
+
expect(de).toMatchObject({language: 'de', smartcatDocumentId: '817_10', complete: false})
|
|
81
|
+
expect(fr).toMatchObject({language: 'fr', smartcatDocumentId: '817_12', complete: true})
|
|
82
|
+
expect(fr.stages).toEqual([
|
|
83
|
+
{_key: 'st-tr', name: 'Translation', percent: 100},
|
|
84
|
+
{_key: 'st-ed', name: 'Editing', percent: 100},
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('carries imported flags over from previous progress', () => {
|
|
89
|
+
const previous: DocProgress[] = [
|
|
90
|
+
{_key: 'd', sourceDocId: 'f18f0d41-aaaa-bbbb', targets: [{_key: 'k', language: 'fr', stages: [], complete: true, imported: true}]},
|
|
91
|
+
]
|
|
92
|
+
const progress = buildProgress(items, scProject, previous, 'now')
|
|
93
|
+
expect(progress[0].targets.find((t) => t.language === 'fr')?.imported).toBe(true)
|
|
94
|
+
expect(progress[0].targets.find((t) => t.language === 'de')?.imported).toBe(false)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('carries a previously-stored title forward when the item has none', () => {
|
|
98
|
+
// A progress sync only has the raw `doc.title` (absent for types whose title
|
|
99
|
+
// is computed via preview), so the title stored at export time must survive.
|
|
100
|
+
const titleless = [{_id: 'f18f0d41-aaaa-bbbb'}]
|
|
101
|
+
const previous: DocProgress[] = [
|
|
102
|
+
{_key: 'd', sourceDocId: 'f18f0d41-aaaa-bbbb', title: 'Computed Name', targets: []},
|
|
103
|
+
]
|
|
104
|
+
const progress = buildProgress(titleless, scProject, previous, 'now')
|
|
105
|
+
expect(progress[0].title).toBe('Computed Name')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('drops documents that match no item', () => {
|
|
109
|
+
const progress = buildProgress([{_id: 'zzzzzzzz-x', title: 'Other'}], scProject, undefined, 'now')
|
|
110
|
+
expect(progress).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('inverts Smartcat codes back to Sanity ids using the mapping', () => {
|
|
114
|
+
const scWithPolish: SmartcatProject = {
|
|
115
|
+
id: 'p1',
|
|
116
|
+
workflowStages: STAGE_NAMES,
|
|
117
|
+
documents: [
|
|
118
|
+
{id: '817_12', name: 'About Us-f18f0d41', targetLanguage: 'pl', workflowStages: [{id: 'st-tr', progress: 100}, {id: 'st-ed', progress: 100}]},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
const previous: DocProgress[] = [
|
|
122
|
+
{_key: 'd', sourceDocId: 'f18f0d41-aaaa-bbbb', targets: [{_key: 'k', language: 'PL-PL', stages: [], complete: true, imported: true}]},
|
|
123
|
+
]
|
|
124
|
+
const progress = buildProgress(items, scWithPolish, previous, 'now', [{sanityId: 'PL-PL', smartcatLanguage: 'pl'}])
|
|
125
|
+
const target = progress[0].targets[0]
|
|
126
|
+
expect(target.language).toBe('PL-PL') // not 'pl'
|
|
127
|
+
expect(target._key).toBe('f18f0d41-aaaa-bbbb__PL-PL') // keyed on the Sanity id
|
|
128
|
+
expect(target.imported).toBe(true) // imported flag matched on the Sanity id
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('resolves items from a project shape', () => {
|
|
132
|
+
expect(itemsFromProject({items: [{doc: {_id: 'a'}}, {doc: null}]})).toEqual([{_id: 'a'}])
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('summarizeCompletion', () => {
|
|
137
|
+
it('flags allDone only when every target is complete AND imported', () => {
|
|
138
|
+
const progress: DocProgress[] = [
|
|
139
|
+
{
|
|
140
|
+
_key: 'd1',
|
|
141
|
+
sourceDocId: 'doc1',
|
|
142
|
+
targets: [
|
|
143
|
+
{_key: 'a', language: 'fr', stages: [{name: 'T', percent: 100}], complete: true, imported: true},
|
|
144
|
+
{_key: 'b', language: 'de', stages: [{name: 'T', percent: 100}], complete: true, imported: false},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
expect(summarizeCompletion(progress)).toMatchObject({total: 2, complete: 2, imported: 1, allDone: false, anyProgress: true})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('runProgressSync', () => {
|
|
153
|
+
function makeSanity(project: unknown) {
|
|
154
|
+
const sets: Record<string, unknown>[] = []
|
|
155
|
+
const client = {
|
|
156
|
+
fetch: (async () => project) as never,
|
|
157
|
+
patch: () => ({set: (attrs: Record<string, unknown>) => { sets.push(attrs); return {commit: async () => null} }}),
|
|
158
|
+
}
|
|
159
|
+
return {client, sets}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
it('refreshes progress and nudges sent → translating once work starts', async () => {
|
|
163
|
+
const {client, sets} = makeSanity({
|
|
164
|
+
_id: 'p',
|
|
165
|
+
status: 'sent',
|
|
166
|
+
smartcatProjectId: 'sc1',
|
|
167
|
+
items: [{doc: {_id: 'f18f0d41-aaaa-bbbb'}}],
|
|
168
|
+
progress: [],
|
|
169
|
+
})
|
|
170
|
+
const smartcat = {getProject: async (): Promise<SmartcatProject> => scProject}
|
|
171
|
+
|
|
172
|
+
const {summary} = await runProgressSync({sanity: client, smartcat, projectId: 'p', now: () => 'T'})
|
|
173
|
+
expect(summary.total).toBe(2)
|
|
174
|
+
expect(summary.anyProgress).toBe(true)
|
|
175
|
+
expect(sets[0].status).toBe('translating')
|
|
176
|
+
expect(sets[0].progressSyncedAt).toBe('T')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('does not change status when there is no progress yet', async () => {
|
|
180
|
+
const zero: SmartcatProject = {
|
|
181
|
+
id: 'sc1',
|
|
182
|
+
workflowStages: STAGE_NAMES,
|
|
183
|
+
documents: [{id: '817_12', name: 'About Us-f18f0d41', targetLanguage: 'fr', workflowStages: [{id: 'st-tr', progress: 0}]}],
|
|
184
|
+
}
|
|
185
|
+
const {client, sets} = makeSanity({
|
|
186
|
+
_id: 'p',
|
|
187
|
+
status: 'sent',
|
|
188
|
+
smartcatProjectId: 'sc1',
|
|
189
|
+
items: [{doc: {_id: 'f18f0d41-aaaa-bbbb'}}],
|
|
190
|
+
progress: [],
|
|
191
|
+
})
|
|
192
|
+
const smartcat = {getProject: async (): Promise<SmartcatProject> => zero}
|
|
193
|
+
await runProgressSync({sanity: client, smartcat, projectId: 'p', now: () => 'T'})
|
|
194
|
+
expect(sets[0].status).toBeUndefined()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('clears lastError and does not write functionLog on success', async () => {
|
|
198
|
+
const {client, sets} = makeSanity({
|
|
199
|
+
_id: 'p',
|
|
200
|
+
status: 'sent',
|
|
201
|
+
smartcatProjectId: 'sc1',
|
|
202
|
+
items: [{doc: {_id: 'f18f0d41-aaaa-bbbb'}}],
|
|
203
|
+
progress: [],
|
|
204
|
+
})
|
|
205
|
+
const smartcat = {getProject: async (): Promise<SmartcatProject> => scProject, setRequestLogger: () => {}}
|
|
206
|
+
await runProgressSync({sanity: client, smartcat, projectId: 'p', now: () => 'T'})
|
|
207
|
+
expect(sets[0].lastError).toBeNull()
|
|
208
|
+
// functionLog is shared with export/import — a successful refresh must not touch it.
|
|
209
|
+
expect(sets[0].functionLog).toBeUndefined()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('records a Smartcat failure instead of throwing, preserving existing progress', async () => {
|
|
213
|
+
const existing: DocProgress[] = [
|
|
214
|
+
{_key: 'k', sourceDocId: 'd', title: 'T', targets: []},
|
|
215
|
+
]
|
|
216
|
+
const {client, sets} = makeSanity({
|
|
217
|
+
_id: 'p',
|
|
218
|
+
status: 'translating',
|
|
219
|
+
smartcatProjectId: 'sc1',
|
|
220
|
+
items: [{doc: {_id: 'd'}}],
|
|
221
|
+
progress: existing,
|
|
222
|
+
})
|
|
223
|
+
const smartcat = {
|
|
224
|
+
getProject: async (): Promise<SmartcatProject> => {
|
|
225
|
+
throw new SmartcatTimeoutError('GET', '/api/integration/v1/project/sc1', 570_000)
|
|
226
|
+
},
|
|
227
|
+
setRequestLogger: () => {},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const {error} = await runProgressSync({sanity: client, smartcat, projectId: 'p', now: () => 'T'})
|
|
231
|
+
|
|
232
|
+
expect(error).toContain('9m30s')
|
|
233
|
+
expect(sets[0].lastError).toContain('9m30s')
|
|
234
|
+
expect(sets[0].progressSyncedAt).toBe('T')
|
|
235
|
+
// The failure is captured in the log the dashboard renders...
|
|
236
|
+
const log = JSON.parse(sets[0].functionLog as string) as {level: string; message: string}[]
|
|
237
|
+
expect(log.some((l) => l.level === 'error' && l.message.includes('9m30s'))).toBe(true)
|
|
238
|
+
// ...and the existing progress is left untouched (not zeroed out).
|
|
239
|
+
expect(sets[0].progress).toBeUndefined()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('captures request-logger error lines into functionLog', async () => {
|
|
243
|
+
const {client, sets} = makeSanity({
|
|
244
|
+
_id: 'p',
|
|
245
|
+
status: 'sent',
|
|
246
|
+
smartcatProjectId: 'sc1',
|
|
247
|
+
items: [],
|
|
248
|
+
progress: [],
|
|
249
|
+
})
|
|
250
|
+
let logger: (info: {method: string; path: string; status: number; body: string}) => void = () => {}
|
|
251
|
+
const smartcat = {
|
|
252
|
+
getProject: async (): Promise<SmartcatProject> => {
|
|
253
|
+
// Simulate the client tracing a failed call before rejecting.
|
|
254
|
+
logger({method: 'GET', path: '/project/sc1', status: 0, body: 'timed out'})
|
|
255
|
+
throw new Error('boom')
|
|
256
|
+
},
|
|
257
|
+
setRequestLogger: (l: typeof logger) => {
|
|
258
|
+
logger = l
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await runProgressSync({sanity: client, smartcat, projectId: 'p', now: () => 'T'})
|
|
263
|
+
|
|
264
|
+
const log = JSON.parse(sets[0].functionLog as string) as {level: string; message: string; indent?: boolean}[]
|
|
265
|
+
expect(log).toContainEqual({level: 'error', message: 'GET /project/sc1 did not complete'})
|
|
266
|
+
expect(log).toContainEqual({level: 'error', indent: true, message: 'timed out'})
|
|
267
|
+
})
|
|
268
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {defineType, defineField, defineArrayMember, type SchemaTypeDefinition} from 'sanity'
|
|
2
|
+
import {CogIcon} from '@sanity/icons'
|
|
3
|
+
import {SETTINGS_TYPE} from '../lib/constants'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Workspace-wide Smartcat settings, stored as a single fixed-id document.
|
|
7
|
+
*
|
|
8
|
+
* Holds the cached project-template list. The browser bumps `templatesRequestedAt`
|
|
9
|
+
* (Studio init / create-dialog open); the `smartcat-templates` Function fetches
|
|
10
|
+
* the list from Smartcat and writes `templates` + `templatesSyncedAt` back.
|
|
11
|
+
* All fields are managed automatically — never hand-edited.
|
|
12
|
+
*/
|
|
13
|
+
export function createSettingsType(): SchemaTypeDefinition {
|
|
14
|
+
return defineType({
|
|
15
|
+
name: SETTINGS_TYPE,
|
|
16
|
+
title: 'Smartcat settings',
|
|
17
|
+
type: 'document',
|
|
18
|
+
icon: CogIcon,
|
|
19
|
+
fields: [
|
|
20
|
+
defineField({
|
|
21
|
+
name: 'templates',
|
|
22
|
+
title: 'Project templates',
|
|
23
|
+
type: 'array',
|
|
24
|
+
readOnly: true,
|
|
25
|
+
of: [
|
|
26
|
+
defineArrayMember({
|
|
27
|
+
type: 'object',
|
|
28
|
+
name: 'smartcatTemplate',
|
|
29
|
+
fields: [
|
|
30
|
+
defineField({name: 'id', type: 'string'}),
|
|
31
|
+
defineField({name: 'name', type: 'string'}),
|
|
32
|
+
],
|
|
33
|
+
preview: {select: {title: 'name', subtitle: 'id'}},
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
// Handshake fields for the on-demand template refresh: the browser bumps
|
|
38
|
+
// `templatesRequestedAt`; the smartcat-templates Function writes
|
|
39
|
+
// `templatesSyncedAt`. Distinct fields stop the Function re-triggering on
|
|
40
|
+
// its own write (filter is delta::changedAny(templatesRequestedAt)).
|
|
41
|
+
defineField({name: 'templatesRequestedAt', type: 'datetime', readOnly: true, hidden: true}),
|
|
42
|
+
defineField({name: 'templatesSyncedAt', type: 'datetime', readOnly: true, hidden: true}),
|
|
43
|
+
],
|
|
44
|
+
preview: {
|
|
45
|
+
select: {templates: 'templates', syncedAt: 'templatesSyncedAt'},
|
|
46
|
+
prepare({templates, syncedAt}) {
|
|
47
|
+
const count = Array.isArray(templates) ? templates.length : 0
|
|
48
|
+
return {
|
|
49
|
+
title: 'Smartcat settings',
|
|
50
|
+
subtitle: syncedAt ? `${count} template${count === 1 ? '' : 's'} cached` : 'Not synced yet',
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema option this plugin adds, mirroring how `@sanity/document-internationalization`
|
|
3
|
+
* contributes `options.documentInternationalization.exclude`.
|
|
4
|
+
*
|
|
5
|
+
* Set it on a field — or once on a custom type — to force translation of a type
|
|
6
|
+
* the default allowlist (`string`/`text`/Portable Text) would otherwise skip,
|
|
7
|
+
* e.g. a custom string alias. The kind is derived from the resolved `jsonType`.
|
|
8
|
+
*/
|
|
9
|
+
export interface SmartcatTranslationSchemaOptions {
|
|
10
|
+
smartcatTranslation?: {
|
|
11
|
+
/**
|
|
12
|
+
* Force this field/type into translation even when its base type isn't in the
|
|
13
|
+
* default allowlist. Overrides a `documentInternationalization.exclude` on the
|
|
14
|
+
* same field. Ignored if the type has no translatable representation.
|
|
15
|
+
*/
|
|
16
|
+
include?: boolean
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
declare module 'sanity' {
|
|
21
|
+
interface StringOptions extends SmartcatTranslationSchemaOptions {}
|
|
22
|
+
interface TextOptions extends SmartcatTranslationSchemaOptions {}
|
|
23
|
+
interface ArrayOptions extends SmartcatTranslationSchemaOptions {}
|
|
24
|
+
interface ObjectOptions extends SmartcatTranslationSchemaOptions {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {defineType, defineField, defineArrayMember, type SchemaTypeDefinition} from 'sanity'
|
|
2
|
+
import {TranslateIcon} from '@sanity/icons'
|
|
3
|
+
import {TRANSLATION_PROJECT_TYPE} from '../lib/constants'
|
|
4
|
+
import type {TranslationProjectStatus} from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Single source of truth for status display titles, used by both the schema's
|
|
8
|
+
* status dropdown (document editor) and the dashboard's StatusBadge. Titles are
|
|
9
|
+
* kept terse so they read well in both places.
|
|
10
|
+
*/
|
|
11
|
+
export const STATUS_OPTIONS: {title: string; value: TranslationProjectStatus}[] = [
|
|
12
|
+
{title: 'Draft', value: 'draft'},
|
|
13
|
+
{title: 'Queued', value: 'queued'},
|
|
14
|
+
{title: 'Sent', value: 'sent'},
|
|
15
|
+
{title: 'In progress', value: 'translating'},
|
|
16
|
+
{title: 'Importing', value: 'importing'},
|
|
17
|
+
{title: 'Downloaded', value: 'downloaded'},
|
|
18
|
+
{title: 'Completed', value: 'completed'},
|
|
19
|
+
{title: 'Error', value: 'error'},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds the translation-project document type.
|
|
24
|
+
*
|
|
25
|
+
* `items` holds `smartcat.projectItem` objects (a `docId`/`docType` pair), not
|
|
26
|
+
* Sanity references, so any document type can be added — including ones linked
|
|
27
|
+
* transitively — without the consumer having to enumerate every referenceable
|
|
28
|
+
* type in a reference `to` list. Reads dereference `docId` manually via GROQ.
|
|
29
|
+
*/
|
|
30
|
+
export function createTranslationProjectType(): SchemaTypeDefinition {
|
|
31
|
+
return defineType({
|
|
32
|
+
name: TRANSLATION_PROJECT_TYPE,
|
|
33
|
+
title: 'Translation project',
|
|
34
|
+
type: 'document',
|
|
35
|
+
icon: TranslateIcon,
|
|
36
|
+
fields: [
|
|
37
|
+
defineField({
|
|
38
|
+
name: 'name',
|
|
39
|
+
title: 'Project name',
|
|
40
|
+
type: 'string',
|
|
41
|
+
validation: (rule) => rule.required(),
|
|
42
|
+
}),
|
|
43
|
+
defineField({
|
|
44
|
+
name: 'status',
|
|
45
|
+
type: 'string',
|
|
46
|
+
// Driven by the dashboard and Functions, not edited by hand.
|
|
47
|
+
readOnly: true,
|
|
48
|
+
initialValue: 'draft',
|
|
49
|
+
options: {list: STATUS_OPTIONS, layout: 'radio'},
|
|
50
|
+
}),
|
|
51
|
+
defineField({
|
|
52
|
+
name: 'sourceLanguage',
|
|
53
|
+
title: 'Source language',
|
|
54
|
+
type: 'string',
|
|
55
|
+
}),
|
|
56
|
+
// Workflow chosen at creation: `template:<id>` or a standard preset.
|
|
57
|
+
// Read by the export Function to build Smartcat's createProject params.
|
|
58
|
+
defineField({name: 'workflow', type: 'string', readOnly: true, hidden: true}),
|
|
59
|
+
defineField({
|
|
60
|
+
name: 'targetLanguages',
|
|
61
|
+
title: 'Target languages',
|
|
62
|
+
type: 'array',
|
|
63
|
+
of: [defineArrayMember({type: 'string'})],
|
|
64
|
+
options: {layout: 'tags'},
|
|
65
|
+
}),
|
|
66
|
+
// Sanity-id → Smartcat-code mapping for this project's source + target
|
|
67
|
+
// languages, stamped by the browser at export time so the thin Functions
|
|
68
|
+
// (which can't see plugin config) can map to/from Smartcat codes.
|
|
69
|
+
defineField({
|
|
70
|
+
name: 'smartcatLanguages',
|
|
71
|
+
type: 'array',
|
|
72
|
+
readOnly: true,
|
|
73
|
+
hidden: true,
|
|
74
|
+
of: [
|
|
75
|
+
defineArrayMember({
|
|
76
|
+
type: 'object',
|
|
77
|
+
name: 'smartcatLanguageMapping',
|
|
78
|
+
fields: [
|
|
79
|
+
defineField({name: 'sanityId', type: 'string'}),
|
|
80
|
+
defineField({name: 'smartcatLanguage', type: 'string'}),
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
defineField({
|
|
86
|
+
name: 'items',
|
|
87
|
+
title: 'Items',
|
|
88
|
+
description: 'Content added to this project for translation.',
|
|
89
|
+
// Populated by the "Add to translation project" action, not by hand.
|
|
90
|
+
readOnly: true,
|
|
91
|
+
type: 'array',
|
|
92
|
+
of: [
|
|
93
|
+
defineArrayMember({
|
|
94
|
+
type: 'object',
|
|
95
|
+
name: 'smartcat.projectItem',
|
|
96
|
+
title: 'Project item',
|
|
97
|
+
fields: [
|
|
98
|
+
defineField({name: 'docId', title: 'Document ID', type: 'string'}),
|
|
99
|
+
defineField({name: 'docType', title: 'Document type', type: 'string'}),
|
|
100
|
+
],
|
|
101
|
+
preview: {select: {title: 'docId', subtitle: 'docType'}},
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
defineField({
|
|
106
|
+
name: 'smartcatProjectId',
|
|
107
|
+
title: 'Smartcat project ID',
|
|
108
|
+
type: 'string',
|
|
109
|
+
readOnly: true,
|
|
110
|
+
description: 'GUID of the corresponding project in Smartcat.',
|
|
111
|
+
}),
|
|
112
|
+
defineField({
|
|
113
|
+
name: 'smartcatProjectUrl',
|
|
114
|
+
title: 'Smartcat project URL',
|
|
115
|
+
type: 'url',
|
|
116
|
+
readOnly: true,
|
|
117
|
+
description: 'Link to the project in Smartcat (set by the export Function).',
|
|
118
|
+
}),
|
|
119
|
+
defineField({name: 'lastExportAt', type: 'datetime', readOnly: true}),
|
|
120
|
+
defineField({name: 'lastImportAt', type: 'datetime', readOnly: true}),
|
|
121
|
+
// Per-document, per-language, per-stage translation progress, mirrored from
|
|
122
|
+
// Smartcat. The export captures the target document ids; the progress/import
|
|
123
|
+
// Functions refresh the stage percentages; the browser sets `imported`.
|
|
124
|
+
defineField({
|
|
125
|
+
name: 'progress',
|
|
126
|
+
title: 'Translation progress',
|
|
127
|
+
type: 'array',
|
|
128
|
+
readOnly: true,
|
|
129
|
+
of: [
|
|
130
|
+
defineArrayMember({
|
|
131
|
+
type: 'object',
|
|
132
|
+
name: 'smartcatDocProgress',
|
|
133
|
+
fields: [
|
|
134
|
+
defineField({name: 'sourceDocId', type: 'string'}),
|
|
135
|
+
defineField({name: 'title', type: 'string'}),
|
|
136
|
+
defineField({
|
|
137
|
+
name: 'targets',
|
|
138
|
+
type: 'array',
|
|
139
|
+
of: [
|
|
140
|
+
defineArrayMember({
|
|
141
|
+
type: 'object',
|
|
142
|
+
name: 'smartcatTargetProgress',
|
|
143
|
+
fields: [
|
|
144
|
+
defineField({name: 'language', type: 'string'}),
|
|
145
|
+
defineField({name: 'smartcatDocumentId', type: 'string'}),
|
|
146
|
+
defineField({
|
|
147
|
+
name: 'stages',
|
|
148
|
+
type: 'array',
|
|
149
|
+
of: [
|
|
150
|
+
defineArrayMember({
|
|
151
|
+
type: 'object',
|
|
152
|
+
name: 'smartcatStageProgress',
|
|
153
|
+
fields: [
|
|
154
|
+
defineField({name: 'name', type: 'string'}),
|
|
155
|
+
defineField({name: 'percent', type: 'number'}),
|
|
156
|
+
],
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
}),
|
|
160
|
+
// All stages at 100% (set by the progress/import Functions).
|
|
161
|
+
defineField({name: 'complete', type: 'boolean'}),
|
|
162
|
+
// A locale variant has been built from this target (set by the browser).
|
|
163
|
+
defineField({name: 'imported', type: 'boolean'}),
|
|
164
|
+
defineField({name: 'syncedAt', type: 'datetime'}),
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
}),
|
|
173
|
+
// Handshake fields for the on-demand progress refresh: the browser bumps
|
|
174
|
+
// `progressRequestedAt`; the smartcat-progress Function reacts and writes
|
|
175
|
+
// `progressSyncedAt`. Keeping them distinct stops the Function re-triggering
|
|
176
|
+
// on its own write (filter is delta::changedAny(progressRequestedAt)).
|
|
177
|
+
defineField({name: 'progressRequestedAt', type: 'datetime', readOnly: true, hidden: true}),
|
|
178
|
+
defineField({name: 'progressSyncedAt', type: 'datetime', readOnly: true, hidden: true}),
|
|
179
|
+
defineField({
|
|
180
|
+
name: 'lastError',
|
|
181
|
+
title: 'Last error',
|
|
182
|
+
type: 'string',
|
|
183
|
+
readOnly: true,
|
|
184
|
+
description: 'Most recent export/import error, if any.',
|
|
185
|
+
}),
|
|
186
|
+
// JSON-encoded LogLine[] from the most recent Function run (export upload or
|
|
187
|
+
// import download). Transient diagnostic data the browser merges into its
|
|
188
|
+
// live Log panel; overwritten each run.
|
|
189
|
+
defineField({
|
|
190
|
+
name: 'functionLog',
|
|
191
|
+
type: 'text',
|
|
192
|
+
readOnly: true,
|
|
193
|
+
hidden: true,
|
|
194
|
+
}),
|
|
195
|
+
// Transient transport buffers. The browser (plugin) serializes content into
|
|
196
|
+
// `outbox`; the thin export Function uploads it. The thin import Function
|
|
197
|
+
// downloads translated files into `inbox`; the browser turns them into
|
|
198
|
+
// locale variants. Both are cleared after processing.
|
|
199
|
+
defineField({
|
|
200
|
+
name: 'outbox',
|
|
201
|
+
type: 'array',
|
|
202
|
+
readOnly: true,
|
|
203
|
+
hidden: true,
|
|
204
|
+
of: [
|
|
205
|
+
defineArrayMember({
|
|
206
|
+
type: 'object',
|
|
207
|
+
name: 'smartcatOutboxItem',
|
|
208
|
+
fields: [
|
|
209
|
+
defineField({name: 'filename', type: 'string'}),
|
|
210
|
+
defineField({name: 'locjson', type: 'text'}),
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
],
|
|
214
|
+
}),
|
|
215
|
+
defineField({
|
|
216
|
+
name: 'inbox',
|
|
217
|
+
type: 'array',
|
|
218
|
+
readOnly: true,
|
|
219
|
+
hidden: true,
|
|
220
|
+
of: [
|
|
221
|
+
defineArrayMember({
|
|
222
|
+
type: 'object',
|
|
223
|
+
name: 'smartcatInboxItem',
|
|
224
|
+
fields: [
|
|
225
|
+
defineField({name: 'sourceDocId', type: 'string'}),
|
|
226
|
+
defineField({name: 'targetLanguage', type: 'string'}),
|
|
227
|
+
defineField({name: 'smartcatDocumentId', type: 'string'}),
|
|
228
|
+
defineField({name: 'locjson', type: 'text'}),
|
|
229
|
+
],
|
|
230
|
+
}),
|
|
231
|
+
],
|
|
232
|
+
}),
|
|
233
|
+
// Transient: items the browser marked for deletion. On the next export the
|
|
234
|
+
// Function deletes the Smartcat document and removes the item + its progress.
|
|
235
|
+
defineField({
|
|
236
|
+
name: 'pendingDeletions',
|
|
237
|
+
type: 'array',
|
|
238
|
+
readOnly: true,
|
|
239
|
+
hidden: true,
|
|
240
|
+
of: [
|
|
241
|
+
defineArrayMember({
|
|
242
|
+
type: 'object',
|
|
243
|
+
name: 'smartcatPendingDeletion',
|
|
244
|
+
fields: [
|
|
245
|
+
defineField({name: 'itemKey', type: 'string'}),
|
|
246
|
+
defineField({name: 'sourceDocId', type: 'string'}),
|
|
247
|
+
defineField({name: 'smartcatDocumentId', type: 'string'}),
|
|
248
|
+
],
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
}),
|
|
252
|
+
],
|
|
253
|
+
preview: {
|
|
254
|
+
select: {name: 'name', status: 'status', items: 'items'},
|
|
255
|
+
prepare({name, status, items}) {
|
|
256
|
+
const count = Array.isArray(items) ? items.length : 0
|
|
257
|
+
return {
|
|
258
|
+
title: name || 'Untitled project',
|
|
259
|
+
subtitle: `${status || 'draft'} · ${count} item${count === 1 ? '' : 's'}`,
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
}
|