@sanity/document-internationalization 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/document-internationalization",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Create unique translations of a document based on its language, joined by a shared reference document.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -0,0 +1,252 @@
1
+ import {CopyIcon} from '@sanity/icons'
2
+ import {useToast} from '@sanity/ui'
3
+ import {uuid} from '@sanity/uuid'
4
+ import {useCallback, useMemo, useState} from 'react'
5
+ import {filter, firstValueFrom} from 'rxjs'
6
+ import {
7
+ DEFAULT_STUDIO_CLIENT_OPTIONS,
8
+ type DocumentActionComponent,
9
+ type Id,
10
+ InsufficientPermissionsMessage,
11
+ type PatchOperations,
12
+ useClient,
13
+ useCurrentUser,
14
+ useDocumentOperation,
15
+ useDocumentPairPermissions,
16
+ useDocumentStore,
17
+ useTranslation,
18
+ } from 'sanity'
19
+ import {useRouter} from 'sanity/router'
20
+ import {structureLocaleNamespace} from 'sanity/structure'
21
+
22
+ import {METADATA_SCHEMA_NAME, TRANSLATIONS_ARRAY_NAME} from '../constants'
23
+ import {useTranslationMetadata} from '../hooks/useLanguageMetadata'
24
+ import {documenti18nLocaleNamespace} from '../i18n'
25
+
26
+ const DISABLED_REASON_KEY = {
27
+ METADATA_NOT_FOUND: 'action.duplicate.disabled.missing-metadata',
28
+ MULTIPLE_METADATA: 'action.duplicate.disabled.multiple-metadata',
29
+ NOTHING_TO_DUPLICATE: 'action.duplicate.disabled.nothing-to-duplicate',
30
+ NOT_READY: 'action.duplicate.disabled.not-ready',
31
+ }
32
+
33
+ export const DuplicateWithTranslationsAction: DocumentActionComponent = ({
34
+ id,
35
+ type,
36
+ onComplete,
37
+ }) => {
38
+ const documentStore = useDocumentStore()
39
+ const {duplicate} = useDocumentOperation(id, type)
40
+ const {navigateIntent} = useRouter()
41
+ const [isDuplicating, setDuplicating] = useState(false)
42
+ const [permissions, isPermissionsLoading] = useDocumentPairPermissions({
43
+ id,
44
+ type,
45
+ permission: 'duplicate',
46
+ })
47
+ const {data, loading: isMetadataDocumentLoading} = useTranslationMetadata(id)
48
+ const hasOneMetadataDocument = useMemo(() => {
49
+ return Array.isArray(data) && data.length <= 1
50
+ }, [data])
51
+ const metadataDocument = Array.isArray(data) && data.length ? data[0] : null
52
+ const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
53
+ const toast = useToast()
54
+ const {t: s} = useTranslation(structureLocaleNamespace)
55
+ const {t: d} = useTranslation(documenti18nLocaleNamespace)
56
+ const currentUser = useCurrentUser()
57
+
58
+ const handle = useCallback(async () => {
59
+ setDuplicating(true)
60
+
61
+ try {
62
+ if (!metadataDocument) {
63
+ throw new Error('Metadata document not found')
64
+ }
65
+
66
+ // 1. Duplicate the document and its localized versions
67
+ const translations = new Map<string, Id>()
68
+ await Promise.all(
69
+ metadataDocument[TRANSLATIONS_ARRAY_NAME].map(async (translation) => {
70
+ const dupeId = uuid()
71
+ const locale = translation._key
72
+ const docId = translation.value?._ref
73
+
74
+ if (!docId) {
75
+ throw new Error('Translation document not found')
76
+ }
77
+
78
+ const {duplicate: duplicateTranslation} = await firstValueFrom(
79
+ documentStore.pair
80
+ .editOperations(docId, type)
81
+ .pipe(filter((op) => op.duplicate.disabled !== 'NOT_READY'))
82
+ )
83
+
84
+ if (duplicateTranslation.disabled) {
85
+ throw new Error('Cannot duplicate document')
86
+ }
87
+
88
+ const duplicateTranslationSuccess = firstValueFrom(
89
+ documentStore.pair
90
+ .operationEvents(docId, type)
91
+ .pipe(filter((e) => e.op === 'duplicate' && e.type === 'success'))
92
+ )
93
+ duplicateTranslation.execute(dupeId)
94
+ await duplicateTranslationSuccess
95
+
96
+ translations.set(locale, dupeId)
97
+ })
98
+ )
99
+
100
+ // 2. Duplicate the metadata document
101
+ const {duplicate: duplicateMetadata} = await firstValueFrom(
102
+ documentStore.pair
103
+ .editOperations(metadataDocument._id, METADATA_SCHEMA_NAME)
104
+ .pipe(filter((op) => op.duplicate.disabled !== 'NOT_READY'))
105
+ )
106
+
107
+ if (duplicateMetadata.disabled) {
108
+ throw new Error('Cannot duplicate document')
109
+ }
110
+
111
+ const duplicateMetadataSuccess = firstValueFrom(
112
+ documentStore.pair
113
+ .operationEvents(metadataDocument._id, METADATA_SCHEMA_NAME)
114
+ .pipe(filter((e) => e.op === 'duplicate' && e.type === 'success'))
115
+ )
116
+ const dupeId = uuid()
117
+ duplicateMetadata.execute(dupeId)
118
+ await duplicateMetadataSuccess
119
+
120
+ // 3. Patch the duplicated metadata document to update the references
121
+ // TODO: use document store
122
+ // const {patch: patchMetadata} = await firstValueFrom(
123
+ // documentStore.pair
124
+ // .editOperations(dupeId, METADATA_SCHEMA_NAME)
125
+ // .pipe(filter((op) => op.patch.disabled !== 'NOT_READY'))
126
+ // )
127
+
128
+ // if (patchMetadata.disabled) {
129
+ // throw new Error('Cannot patch document')
130
+ // }
131
+
132
+ // await firstValueFrom(
133
+ // documentStore.pair
134
+ // .consistencyStatus(dupeId, METADATA_SCHEMA_NAME)
135
+ // .pipe(filter((isConsistant) => isConsistant))
136
+ // )
137
+
138
+ // const patchMetadataSuccess = firstValueFrom(
139
+ // documentStore.pair
140
+ // .operationEvents(dupeId, METADATA_SCHEMA_NAME)
141
+ // .pipe(filter((e) => e.op === 'patch' && e.type === 'success'))
142
+ // )
143
+
144
+ const patch: PatchOperations = {
145
+ set: Object.fromEntries(
146
+ Array.from(translations.entries()).map(([locale, documentId]) => [
147
+ `${TRANSLATIONS_ARRAY_NAME}[_key == "${locale}"].value._ref`,
148
+ documentId,
149
+ ])
150
+ ),
151
+ }
152
+
153
+ // patchMetadata.execute([patch])
154
+ // await patchMetadataSuccess
155
+ await client.transaction().patch(dupeId, patch).commit()
156
+
157
+ // 4. Navigate to the duplicated document
158
+ navigateIntent('edit', {
159
+ id: Array.from(translations.values()).at(0),
160
+ type,
161
+ })
162
+
163
+ onComplete()
164
+ } catch (error) {
165
+ console.error(error)
166
+ toast.push({
167
+ status: 'error',
168
+ title: 'Error duplicating document',
169
+ description:
170
+ error instanceof Error
171
+ ? error.message
172
+ : 'Failed to duplicate document',
173
+ })
174
+ } finally {
175
+ setDuplicating(false)
176
+ }
177
+ }, [
178
+ client,
179
+ documentStore.pair,
180
+ metadataDocument,
181
+ navigateIntent,
182
+ onComplete,
183
+ toast,
184
+ type,
185
+ ])
186
+
187
+ return useMemo(() => {
188
+ if (!isPermissionsLoading && !permissions?.granted) {
189
+ return {
190
+ icon: CopyIcon,
191
+ disabled: true,
192
+ label: d('action.duplicate.label'),
193
+ title: (
194
+ <InsufficientPermissionsMessage
195
+ context="duplicate-document"
196
+ currentUser={currentUser}
197
+ />
198
+ ),
199
+ }
200
+ }
201
+
202
+ if (!isMetadataDocumentLoading && !metadataDocument) {
203
+ return {
204
+ icon: CopyIcon,
205
+ disabled: true,
206
+ label: d('action.duplicate.label'),
207
+ title: d(DISABLED_REASON_KEY.METADATA_NOT_FOUND),
208
+ }
209
+ }
210
+
211
+ if (!hasOneMetadataDocument) {
212
+ return {
213
+ icon: CopyIcon,
214
+ disabled: true,
215
+ label: d('action.duplicate.label'),
216
+ title: d(DISABLED_REASON_KEY.MULTIPLE_METADATA),
217
+ }
218
+ }
219
+
220
+ return {
221
+ icon: CopyIcon,
222
+ disabled:
223
+ isDuplicating ||
224
+ Boolean(duplicate.disabled) ||
225
+ isPermissionsLoading ||
226
+ isMetadataDocumentLoading,
227
+ label: isDuplicating
228
+ ? s('action.duplicate.running.label')
229
+ : d('action.duplicate.label'),
230
+ title: duplicate.disabled
231
+ ? s(DISABLED_REASON_KEY[duplicate.disabled])
232
+ : '',
233
+ onHandle: handle,
234
+ }
235
+ }, [
236
+ currentUser,
237
+ duplicate.disabled,
238
+ handle,
239
+ hasOneMetadataDocument,
240
+ isDuplicating,
241
+ isMetadataDocumentLoading,
242
+ isPermissionsLoading,
243
+ metadataDocument,
244
+ permissions?.granted,
245
+ s,
246
+ d,
247
+ ])
248
+ }
249
+
250
+ DuplicateWithTranslationsAction.action = 'duplicate'
251
+ // @ts-expect-error `displayName` is used by React DevTools
252
+ DuplicateWithTranslationsAction.displayName = 'DuplicateWithTranslationsAction'
@@ -0,0 +1,21 @@
1
+ import {defineLocaleResourceBundle} from 'sanity'
2
+
3
+ /**
4
+ * The locale namespace for the document internationalization plugin.
5
+ *
6
+ * @public
7
+ */
8
+ export const documenti18nLocaleNamespace =
9
+ 'document-internationalization' as const
10
+
11
+ /**
12
+ * The default locale bundle for the document internationalization plugin, which is US English.
13
+ *
14
+ * @internal
15
+ */
16
+ export const documentInternationalizationUsEnglishLocaleBundle =
17
+ defineLocaleResourceBundle({
18
+ locale: 'en-US',
19
+ namespace: documenti18nLocaleNamespace,
20
+ resources: () => import('./resources'),
21
+ })
@@ -0,0 +1,7 @@
1
+ export default {
2
+ 'action.duplicate.label': 'Duplicate with translations',
3
+ 'action.duplicate.disabled.missing-metadata':
4
+ 'The document cannot be duplicated because the metadata document is missing',
5
+ 'action.duplicate.disabled.multiple-metadata':
6
+ 'The document cannot be duplicated because there are multiple metadata documents',
7
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {DeleteTranslationAction} from './actions/DeleteTranslationAction'
2
+ export {DuplicateWithTranslationsAction} from './actions/DuplicateWithTranslationsAction'
2
3
  export {useDocumentInternationalizationContext} from './components/DocumentInternationalizationContext'
3
4
  export {DocumentInternationalizationMenu} from './components/DocumentInternationalizationMenu'
4
5
  export {documentInternationalization} from './plugin'
package/src/plugin.tsx CHANGED
@@ -9,6 +9,7 @@ import {DocumentInternationalizationProvider} from './components/DocumentInterna
9
9
  import {DocumentInternationalizationMenu} from './components/DocumentInternationalizationMenu'
10
10
  import OptimisticallyStrengthen from './components/OptimisticallyStrengthen'
11
11
  import {API_VERSION, DEFAULT_CONFIG, METADATA_SCHEMA_NAME} from './constants'
12
+ import {documentInternationalizationUsEnglishLocaleBundle} from './i18n'
12
13
  import metadata from './schema/translation/metadata'
13
14
  import type {PluginConfig, TranslationReference} from './types'
14
15
 
@@ -39,6 +40,10 @@ export const documentInternationalization = definePlugin<PluginConfig>(
39
40
  },
40
41
  },
41
42
 
43
+ i18n: {
44
+ bundles: [documentInternationalizationUsEnglishLocaleBundle],
45
+ },
46
+
42
47
  // Adds:
43
48
  // - A bulk-publishing UI component to the form
44
49
  // - Will only work for projects on a compatible plan