gt-sanity 1.0.1 → 1.0.3

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": "gt-sanity",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "General Translation integration with Sanity",
5
5
  "keywords": [
6
6
  "sanity",
@@ -39,20 +39,6 @@
39
39
  "src",
40
40
  "v2-incompatible.js"
41
41
  ],
42
- "scripts": {
43
- "release": "npm run build && npm publish --access public",
44
- "release:alpha": "npm run build && npm publish --access public --tag alpha",
45
- "release:beta": "npm run build && npm publish --access public --tag beta",
46
- "release:latest": "npm run build && npm publish --access public --tag latest",
47
- "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
48
- "format": "prettier --write --cache --ignore-unknown .",
49
- "link-watch": "plugin-kit link-watch",
50
- "lint": "eslint .",
51
- "lint:fix": "eslint . --fix",
52
- "prepublishOnly": "npm run build",
53
- "test": "vitest run",
54
- "watch": "pkg-utils watch --strict"
55
- },
56
42
  "dependencies": {
57
43
  "@portabletext/block-tools": "^3.5.5",
58
44
  "@portabletext/to-html": "^2.0.14",
@@ -62,12 +48,13 @@
62
48
  "@sanity/schema": "^3.98.1",
63
49
  "@sanity/ui": "^2.16.4",
64
50
  "@sanity/util": "^3.98.1",
65
- "generaltranslation": "^7.6.3",
66
51
  "jsonpath-plus": "^10.3.0",
67
52
  "jsonpointer": "^5.0.1",
68
- "lodash.merge": "^4.6.2"
53
+ "lodash.merge": "^4.6.2",
54
+ "generaltranslation": "7.6.4"
69
55
  },
70
56
  "devDependencies": {
57
+ "@portabletext/types": "^2.0.15",
71
58
  "@sanity/pkg-utils": "^8.1.6",
72
59
  "@sanity/plugin-kit": "^4.0.19",
73
60
  "@types/lodash.merge": "^4.6.9",
@@ -95,5 +82,18 @@
95
82
  },
96
83
  "engines": {
97
84
  "node": ">=18"
85
+ },
86
+ "scripts": {
87
+ "release": "pnpm run build && pnpm publish --access public",
88
+ "release:alpha": "pnpm run build && pnpm publish --access public --tag alpha",
89
+ "release:beta": "pnpm run build && pnpm publish --access public --tag beta",
90
+ "release:latest": "pnpm run build && pnpm publish --access public --tag latest",
91
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
92
+ "format": "prettier --write --cache --ignore-unknown .",
93
+ "link-watch": "plugin-kit link-watch",
94
+ "lint": "eslint .",
95
+ "lint:fix": "eslint . --fix",
96
+ "test": "vitest run",
97
+ "watch": "pkg-utils watch --strict"
98
98
  }
99
- }
99
+ }
@@ -0,0 +1,24 @@
1
+ import { DocumentActionComponent } from 'sanity';
2
+ import { useRouter } from 'sanity/router';
3
+ import { TranslateIcon } from '@sanity/icons';
4
+
5
+ export const translateAction: DocumentActionComponent = (props) => {
6
+ const router = useRouter();
7
+
8
+ return {
9
+ label: 'Translate',
10
+ icon: TranslateIcon,
11
+ tone: 'primary',
12
+ onHandle: () => {
13
+ // Switch to the translation tab using the document ID and type
14
+ const { id, type } = props;
15
+
16
+ // Navigate to the translation view for this document
17
+ router.navigateIntent('edit', {
18
+ id,
19
+ type,
20
+ view: 'general-translation',
21
+ });
22
+ },
23
+ };
24
+ };
@@ -0,0 +1,29 @@
1
+ import { Button } from '@sanity/ui';
2
+ import { TranslateIcon } from '@sanity/icons';
3
+ import { useRouter } from 'sanity/router';
4
+
5
+ export function TranslateButton() {
6
+ const router = useRouter();
7
+
8
+ const handleClick = () => {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const panes = router.state.panes as any[];
11
+ if (panes && panes.length > 0) {
12
+ const currentPane = panes[0];
13
+ router.navigateUrl({
14
+ ...currentPane,
15
+ view: 'general-translation',
16
+ });
17
+ }
18
+ };
19
+
20
+ return (
21
+ <Button
22
+ icon={TranslateIcon}
23
+ text='Translate'
24
+ onClick={handleClick}
25
+ mode='ghost'
26
+ tone='primary'
27
+ />
28
+ );
29
+ }
@@ -77,8 +77,8 @@ interface TranslationsContextType {
77
77
  handleImportMissing: () => Promise<void>;
78
78
  handleRefreshAll: () => Promise<void>;
79
79
  handleImportDocument: (documentId: string, localeId: string) => Promise<void>;
80
- handlePatchDocumentReferences: () => Promise<void>;
81
- handlePublishAllTranslations: () => Promise<void>;
80
+ handlePatchDocumentReferences: () => Promise<number>;
81
+ handlePublishAllTranslations: () => Promise<number>;
82
82
  }
83
83
 
84
84
  const TranslationsContext = createContext<TranslationsContextType | null>(null);
@@ -594,32 +594,32 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
594
594
  async (documentId: string, localeId: string) => {
595
595
  if (!secrets) return;
596
596
 
597
- try {
598
- const key = `${documentId}:${localeId}`;
599
- const status = translationStatuses.get(key);
597
+ const key = `${documentId}:${localeId}`;
598
+ const status = translationStatuses.get(key);
600
599
 
601
- if (!status?.isReady || !status.translationId) {
602
- toast.push({
603
- title: `Translation not ready for ${documentId} (${localeId})`,
604
- status: 'warning',
605
- closable: true,
606
- });
607
- return;
608
- }
600
+ if (!status?.isReady || !status.translationId) {
601
+ toast.push({
602
+ title: `Translation not ready for ${documentId} (${localeId})`,
603
+ status: 'warning',
604
+ closable: true,
605
+ });
606
+ return;
607
+ }
609
608
 
610
- const document = documents.find(
611
- (doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
612
- );
609
+ const document = documents.find(
610
+ (doc) => (doc._id?.replace('drafts.', '') || doc._id) === documentId
611
+ );
613
612
 
614
- if (!document) {
615
- toast.push({
616
- title: `Document ${documentId} not found`,
617
- status: 'error',
618
- closable: true,
619
- });
620
- return;
621
- }
613
+ if (!document) {
614
+ toast.push({
615
+ title: `Document ${documentId} not found`,
616
+ status: 'error',
617
+ closable: true,
618
+ });
619
+ return;
620
+ }
622
621
 
622
+ try {
623
623
  const downloadedFiles = await downloadTranslations(
624
624
  [
625
625
  {
@@ -682,11 +682,11 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
682
682
  });
683
683
  }
684
684
  },
685
- [secrets, documents, translationContext]
685
+ [secrets, documents, translationContext, translationStatuses]
686
686
  );
687
687
 
688
688
  const handlePatchDocumentReferences = useCallback(async () => {
689
- if (!secrets || documents.length === 0) return;
689
+ if (!secrets || documents.length === 0) return 0;
690
690
 
691
691
  setIsBusy(true);
692
692
 
@@ -775,6 +775,8 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
775
775
  patchedCount > 0 || result.failureCount === 0 ? 'success' : 'error',
776
776
  closable: true,
777
777
  });
778
+
779
+ return patchedCount;
778
780
  } catch (error) {
779
781
  console.error('Error patching document references:', error);
780
782
  toast.push({
@@ -782,6 +784,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
782
784
  status: 'error',
783
785
  closable: true,
784
786
  });
787
+ return 0;
785
788
  } finally {
786
789
  setIsBusy(false);
787
790
  setImportProgress({ current: 0, total: 0, isImporting: false });
@@ -789,7 +792,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
789
792
  }, [secrets, documents, locales, client]);
790
793
 
791
794
  const handlePublishAllTranslations = useCallback(async () => {
792
- if (!secrets || documents.length === 0) return;
795
+ if (!secrets || documents.length === 0) return 0;
793
796
 
794
797
  setIsBusy(true);
795
798
 
@@ -806,7 +809,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
806
809
  status: 'warning',
807
810
  closable: true,
808
811
  });
809
- return;
812
+ return 0;
810
813
  }
811
814
 
812
815
  const query = `*[
@@ -840,7 +843,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
840
843
  status: 'warning',
841
844
  closable: true,
842
845
  });
843
- return;
846
+ return 0;
844
847
  }
845
848
 
846
849
  const translatedDocumentIds = await publishTranslations(
@@ -853,6 +856,8 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
853
856
  status: 'success',
854
857
  closable: true,
855
858
  });
859
+
860
+ return translatedDocumentIds.length;
856
861
  } catch (error) {
857
862
  console.error('Error publishing translations:', error);
858
863
  toast.push({
@@ -860,6 +865,7 @@ export const TranslationsProvider: React.FC<TranslationsProviderProps> = ({
860
865
  status: 'error',
861
866
  closable: true,
862
867
  });
868
+ return 0;
863
869
  } finally {
864
870
  setIsBusy(false);
865
871
  }
@@ -15,12 +15,13 @@ import {
15
15
  Flex,
16
16
  Switch,
17
17
  Tooltip,
18
+ useToast,
18
19
  } from '@sanity/ui';
19
20
  import { pluginConfig } from '../../adapter/core';
20
21
  import { useTranslations } from '../TranslationsProvider';
21
22
  import { LanguageStatus } from '../shared/LanguageStatus';
22
23
  import { LocaleCheckbox } from '../shared/LocaleCheckbox';
23
- import { DownloadIcon, LinkIcon } from '@sanity/icons';
24
+ import { DownloadIcon, LinkIcon, PublishIcon } from '@sanity/icons';
24
25
 
25
26
  export const TranslationView = () => {
26
27
  const {
@@ -35,11 +36,17 @@ export const TranslationView = () => {
35
36
  importedTranslations,
36
37
  setLocales,
37
38
  handlePatchDocumentReferences,
39
+ handlePublishAllTranslations,
38
40
  } = useTranslations();
39
41
 
40
42
  const [autoImport, setAutoImport] = useState(false);
41
43
  const [isImporting, setIsImporting] = useState(false);
42
44
  const [autoRefresh, setAutoRefresh] = useState(true);
45
+ const [autoPatchReferences, setAutoPatchReferences] = useState(true);
46
+ const [autoPublish, setAutoPublish] = useState(true);
47
+ const [isPublishing, setIsPublishing] = useState(false);
48
+
49
+ const toast = useToast();
43
50
 
44
51
  // Get the single document (first document in single document mode)
45
52
  const document = documents[0];
@@ -76,70 +83,66 @@ export const TranslationView = () => {
76
83
  return document._id?.replace('drafts.', '') || document._id;
77
84
  }, [document]);
78
85
 
79
- // Auto import functionality
80
- const checkAndImportCompletedTranslations = useCallback(async () => {
81
- if (!autoImport || isImporting || !documentId) return;
82
-
83
- const completedTranslations = availableLocales.filter((locale) => {
84
- const key = `${documentId}:${locale.localeId}`;
85
- const status = translationStatuses.get(key);
86
- return (
87
- (status?.progress || 0) >= 100 &&
88
- status?.isReady &&
89
- !importedTranslations.has(key)
90
- );
91
- });
92
-
93
- if (completedTranslations.length === 0) return;
94
-
95
- setIsImporting(true);
96
- try {
97
- for (const locale of completedTranslations) {
98
- await handleImportDocument(documentId, locale.localeId);
99
- }
100
- } finally {
101
- setIsImporting(false);
102
- }
103
- }, [
104
- autoImport,
105
- isImporting,
106
- documentId,
107
- availableLocales,
108
- translationStatuses,
109
- importedTranslations,
110
- handleImportDocument,
111
- ]);
86
+ // Unified import functionality
87
+ const handleImportTranslations = useCallback(
88
+ async (options: { autoOnly?: boolean } = {}) => {
89
+ const { autoOnly = false } = options;
112
90
 
113
- const handleImportAll = useCallback(async () => {
114
- if (isImporting || !documentId) return;
91
+ // Check preconditions
92
+ if (isImporting || !documentId) return;
93
+ if (autoOnly && !autoImport) return;
115
94
 
116
- setIsImporting(true);
117
- try {
95
+ // Find translations ready to import
118
96
  const readyTranslations = availableLocales.filter((locale) => {
119
97
  const key = `${documentId}:${locale.localeId}`;
120
98
  const status = translationStatuses.get(key);
121
99
  return status?.isReady && !importedTranslations.has(key);
122
100
  });
123
101
 
124
- for (const locale of readyTranslations) {
125
- await handleImportDocument(documentId, locale.localeId);
102
+ if (readyTranslations.length === 0) return;
103
+
104
+ setIsImporting(true);
105
+ try {
106
+ // Import all ready translations
107
+ await Promise.all(
108
+ readyTranslations.map((locale) =>
109
+ handleImportDocument(documentId, locale.localeId)
110
+ )
111
+ );
112
+
113
+ // Auto patch document references if enabled
114
+ if (autoPatchReferences) {
115
+ await handlePatchDocumentReferences();
116
+ }
117
+
118
+ // Auto publish translations if enabled
119
+ if (autoPublish) {
120
+ await handlePublishAllTranslations();
121
+ }
122
+ } finally {
123
+ setIsImporting(false);
126
124
  }
127
- } finally {
128
- setIsImporting(false);
129
- }
130
- }, [
131
- isImporting,
132
- documentId,
133
- availableLocales,
134
- translationStatuses,
135
- importedTranslations,
136
- handleImportDocument,
137
- ]);
125
+ },
126
+ [
127
+ autoImport,
128
+ isImporting,
129
+ documentId,
130
+ availableLocales,
131
+ translationStatuses,
132
+ importedTranslations,
133
+ handleImportDocument,
134
+ autoPatchReferences,
135
+ handlePatchDocumentReferences,
136
+ autoPublish,
137
+ handlePublishAllTranslations,
138
+ toast,
139
+ ]
140
+ );
138
141
 
139
- // Check for completed translations on status updates
142
+ // Check for completed translations on status updates (auto-import)
140
143
  useEffect(() => {
141
- checkAndImportCompletedTranslations();
142
- }, [checkAndImportCompletedTranslations]);
144
+ handleImportTranslations({ autoOnly: true });
145
+ }, [handleImportTranslations]);
143
146
 
144
147
  // Auto refresh functionality
145
148
  useEffect(() => {
@@ -147,7 +150,7 @@ export const TranslationView = () => {
147
150
 
148
151
  const interval = setInterval(async () => {
149
152
  await handleRefreshAll();
150
- await checkAndImportCompletedTranslations();
153
+ await handleImportTranslations({ autoOnly: true });
151
154
  }, 10000);
152
155
 
153
156
  return () => clearInterval(interval);
@@ -156,13 +159,13 @@ export const TranslationView = () => {
156
159
  documentId,
157
160
  availableLocales.length,
158
161
  handleRefreshAll,
159
- checkAndImportCompletedTranslations,
162
+ handleImportTranslations,
160
163
  ]);
161
164
 
162
165
  useEffect(() => {
163
166
  const initialRefresh = async () => {
164
167
  await handleRefreshAll();
165
- await checkAndImportCompletedTranslations();
168
+ await handleImportTranslations({ autoOnly: true });
166
169
  };
167
170
  initialRefresh();
168
171
  }, []);
@@ -316,7 +319,8 @@ export const TranslationView = () => {
316
319
  <Flex gap={2} align='center'>
317
320
  <Button
318
321
  mode='ghost'
319
- onClick={handleImportAll}
322
+ tone='primary'
323
+ onClick={() => handleImportTranslations()}
320
324
  text={isImporting ? 'Importing...' : 'Import All'}
321
325
  icon={DownloadIcon}
322
326
  disabled={
@@ -327,36 +331,37 @@ export const TranslationView = () => {
327
331
  return !status?.isReady || importedTranslations.has(key);
328
332
  })
329
333
  }
334
+ style={{ minWidth: '180px' }}
330
335
  />
331
- <Text size={1} muted>
332
- Imported{' '}
333
- {
334
- availableLocales.filter((locale) => {
335
- const key = `${documentId}:${locale.localeId}`;
336
- return importedTranslations.has(key);
337
- }).length
338
- }
339
- /
340
- {
341
- availableLocales.filter((locale) => {
342
- const key = `${documentId}:${locale.localeId}`;
343
- const status = translationStatuses.get(key);
344
- return status?.isReady;
345
- }).length
346
- }
347
- </Text>
348
- </Flex>
349
- <Flex gap={2} align='center' style={{ whiteSpace: 'nowrap' }}>
350
- <Text size={1}>Auto-import when complete</Text>
351
- <Switch
352
- checked={autoImport}
353
- onChange={() => setAutoImport(!autoImport)}
354
- disabled={isImporting}
355
- />
336
+ <Flex gap={2} align='center'>
337
+ <Switch
338
+ checked={autoImport}
339
+ onChange={() => setAutoImport(!autoImport)}
340
+ disabled={isImporting}
341
+ />
342
+ <Text size={1}>Auto-import when complete</Text>
343
+ </Flex>
356
344
  </Flex>
345
+ <Text size={1} muted>
346
+ Imported{' '}
347
+ {
348
+ availableLocales.filter((locale) => {
349
+ const key = `${documentId}:${locale.localeId}`;
350
+ return importedTranslations.has(key);
351
+ }).length
352
+ }
353
+ /
354
+ {
355
+ availableLocales.filter((locale) => {
356
+ const key = `${documentId}:${locale.localeId}`;
357
+ const status = translationStatuses.get(key);
358
+ return status?.isReady;
359
+ }).length
360
+ }
361
+ </Text>
357
362
  </Flex>
358
363
 
359
- <Flex justify='flex-start'>
364
+ <Flex gap={2} align='center' justify='flex-start'>
360
365
  <Tooltip
361
366
  placement='top'
362
367
  content={`Replaces references to ${pluginConfig.getSourceLocale()} documents in this document with the corresponding translated document reference`}
@@ -364,12 +369,55 @@ export const TranslationView = () => {
364
369
  <Button
365
370
  mode='ghost'
366
371
  tone='caution'
367
- onClick={handlePatchDocumentReferences}
368
- text={isBusy ? 'Patching...' : 'Patch Document References'}
372
+ onClick={async () => {
373
+ await handlePatchDocumentReferences();
374
+ }}
375
+ text={isBusy ? 'Patching...' : 'Patch References'}
369
376
  icon={isBusy ? null : LinkIcon}
370
- disabled={isBusy}
377
+ disabled={isBusy || isImporting}
378
+ style={{ minWidth: '180px' }}
371
379
  />
372
380
  </Tooltip>
381
+ <Flex gap={2} align='center'>
382
+ <Switch
383
+ checked={autoPatchReferences}
384
+ onChange={() => setAutoPatchReferences(!autoPatchReferences)}
385
+ disabled={isImporting || isBusy}
386
+ />
387
+ <Text size={1}>Auto-patch after import</Text>
388
+ </Flex>
389
+ </Flex>
390
+
391
+ <Flex gap={2} align='center' justify='flex-start'>
392
+ <Tooltip
393
+ placement='top'
394
+ content='Publishes all translations (if the source document is published)'
395
+ >
396
+ <Button
397
+ mode='ghost'
398
+ tone='positive'
399
+ onClick={async () => {
400
+ setIsPublishing(true);
401
+ try {
402
+ await handlePublishAllTranslations();
403
+ } finally {
404
+ setIsPublishing(false);
405
+ }
406
+ }}
407
+ text={isPublishing ? 'Publishing...' : 'Publish Translations'}
408
+ icon={isPublishing ? null : PublishIcon}
409
+ disabled={isBusy || isPublishing || isImporting}
410
+ style={{ minWidth: '180px' }}
411
+ />
412
+ </Tooltip>
413
+ <Flex gap={2} align='center'>
414
+ <Switch
415
+ checked={autoPublish}
416
+ onChange={() => setAutoPublish(!autoPublish)}
417
+ disabled={isPublishing || isImporting || isBusy}
418
+ />
419
+ <Text size={1}>Auto-publish after import</Text>
420
+ </Flex>
373
421
  </Flex>
374
422
  </Stack>
375
423
  </Stack>
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  customSerializers,
17
17
  SerializedDocument,
18
18
  } from './serialization';
19
+ import { translateAction } from './actions/translateAction';
19
20
 
20
21
  export type {
21
22
  Secrets,
@@ -28,6 +29,7 @@ export type {
28
29
  };
29
30
  export {
30
31
  TranslationsTab,
32
+ translateAction,
31
33
  //helpers for end developers who may need to customize serialization
32
34
  findLatestDraft,
33
35
  BaseDocumentSerializer,
@@ -139,6 +141,19 @@ export const gtPlugin = definePlugin<GTPluginConfig>(
139
141
  router: route.create('/*'),
140
142
  },
141
143
  ],
144
+ document: {
145
+ views: [
146
+ {
147
+ id: 'general-translation',
148
+ title: 'Translations',
149
+ component: TranslationsTab,
150
+ },
151
+ ],
152
+ actions: (prev) => {
153
+ // Move translateAction to the beginning so it appears as a prominent button
154
+ return [...prev, translateAction];
155
+ },
156
+ },
142
157
  };
143
158
  }
144
159
  );
@@ -1,6 +1,6 @@
1
1
  // Adapted from https://github.com/sanity-io/sanity-naive-html-serializer
2
2
 
3
- import { PortableTextBlockStyle } from '@portabletext/types';
3
+ import type { PortableTextBlockStyle } from '@portabletext/types';
4
4
 
5
5
  import {
6
6
  PortableTextBlockComponent,
@@ -8,7 +8,7 @@ import {
8
8
  } from '../serialization/';
9
9
  import { PortableTextHtmlComponents } from '@portabletext/to-html';
10
10
  import { pluginConfig } from '../adapter/core';
11
- import { merge } from 'lodash';
11
+ import merge from 'lodash.merge';
12
12
 
13
13
  export function deserializeDocument(document: string) {
14
14
  const deserializers = merge(