gt-sanity 0.0.5

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.
Files changed (44) hide show
  1. package/LICENSE.md +138 -0
  2. package/README.md +100 -0
  3. package/dist/index.d.mts +241 -0
  4. package/dist/index.d.ts +241 -0
  5. package/dist/index.js +2119 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +2099 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +92 -0
  10. package/sanity.json +8 -0
  11. package/src/adapter/core.ts +44 -0
  12. package/src/adapter/createTask.ts +41 -0
  13. package/src/adapter/getLocales.ts +13 -0
  14. package/src/adapter/getTranslation.ts +20 -0
  15. package/src/adapter/getTranslationTask.ts +31 -0
  16. package/src/adapter/index.ts +13 -0
  17. package/src/components/LanguageStatus.tsx +65 -0
  18. package/src/components/NewTask.tsx +249 -0
  19. package/src/components/ProgressBar.tsx +38 -0
  20. package/src/components/TaskView.tsx +255 -0
  21. package/src/components/TranslationContext.tsx +19 -0
  22. package/src/components/TranslationView.tsx +82 -0
  23. package/src/components/TranslationsTab.tsx +177 -0
  24. package/src/configuration/README.md +8 -0
  25. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.ts +108 -0
  26. package/src/configuration/baseDocumentLevelConfig/helpers/createI18nDocAndPatchMetadata.ts +47 -0
  27. package/src/configuration/baseDocumentLevelConfig/helpers/createTranslationMetadata.ts +43 -0
  28. package/src/configuration/baseDocumentLevelConfig/helpers/getOrCreateTranslationMetadata.ts +77 -0
  29. package/src/configuration/baseDocumentLevelConfig/helpers/getTranslationMetadata.ts +15 -0
  30. package/src/configuration/baseDocumentLevelConfig/helpers/index.ts +5 -0
  31. package/src/configuration/baseDocumentLevelConfig/helpers/patchI18nDoc.ts +25 -0
  32. package/src/configuration/baseDocumentLevelConfig/index.ts +129 -0
  33. package/src/configuration/baseDocumentLevelConfig/legacyDocumentLevelPatch.ts +69 -0
  34. package/src/configuration/baseFieldLevelConfig.ts +118 -0
  35. package/src/configuration/index.ts +18 -0
  36. package/src/configuration/utils/checkSerializationVersion.ts +13 -0
  37. package/src/configuration/utils/findDocumentAtRevision.ts +22 -0
  38. package/src/configuration/utils/findLatestDraft.ts +16 -0
  39. package/src/configuration/utils/index.ts +3 -0
  40. package/src/hooks/useClient.ts +5 -0
  41. package/src/hooks/useSecrets.ts +33 -0
  42. package/src/index.ts +120 -0
  43. package/src/types.ts +124 -0
  44. package/v2-incompatible.js +11 -0
@@ -0,0 +1,43 @@
1
+ import {
2
+ KeyedObject,
3
+ Reference,
4
+ SanityClient,
5
+ SanityDocumentLike,
6
+ } from 'sanity';
7
+
8
+ type TranslationReference = KeyedObject & {
9
+ _type: 'internationalizedArrayReferenceValue';
10
+ value: Reference;
11
+ };
12
+
13
+ export const createTranslationMetadata = (
14
+ document: SanityDocumentLike,
15
+ client: SanityClient,
16
+ baseLanguage: string
17
+ ): Promise<SanityDocumentLike> => {
18
+ const baseLangEntry: TranslationReference = {
19
+ _key: baseLanguage,
20
+ _type: 'internationalizedArrayReferenceValue',
21
+ value: {
22
+ _type: 'reference',
23
+ _ref: document._id.replace('drafts.', ''),
24
+ },
25
+ };
26
+
27
+ if (document._id.startsWith('drafts.')) {
28
+ baseLangEntry.value = {
29
+ ...baseLangEntry.value,
30
+ _weak: true,
31
+ //this should reflect doc i18n config when this
32
+ //plugin is able to take that as a config option
33
+ _strengthenOnPublish: {
34
+ type: document._type,
35
+ },
36
+ };
37
+ }
38
+
39
+ return client.create({
40
+ _type: 'translation.metadata',
41
+ translations: [baseLangEntry],
42
+ });
43
+ };
@@ -0,0 +1,77 @@
1
+ import {
2
+ KeyedObject,
3
+ Reference,
4
+ SanityClient,
5
+ SanityDocumentLike,
6
+ } from 'sanity';
7
+
8
+ type TranslationReference = KeyedObject & {
9
+ _type: 'internationalizedArrayReferenceValue';
10
+ value: Reference;
11
+ };
12
+
13
+ export const getOrCreateTranslationMetadata = async (
14
+ documentId: string,
15
+ baseDocument: SanityDocumentLike,
16
+ client: SanityClient,
17
+ baseLanguage: string
18
+ ): Promise<SanityDocumentLike> => {
19
+ // First, try to get existing metadata
20
+ const existingMetadata = await client.fetch(
21
+ `*[
22
+ _type == 'translation.metadata' &&
23
+ translations[_key == $baseLanguage][0].value._ref == $id
24
+ ][0]`,
25
+ { baseLanguage, id: documentId.replace('drafts.', '') }
26
+ );
27
+
28
+ if (existingMetadata) {
29
+ return existingMetadata;
30
+ }
31
+
32
+ // If no metadata exists, create it atomically
33
+ const baseLangEntry: TranslationReference = {
34
+ _key: baseLanguage,
35
+ _type: 'internationalizedArrayReferenceValue',
36
+ value: {
37
+ _type: 'reference',
38
+ _ref: baseDocument._id.replace('drafts.', ''),
39
+ },
40
+ };
41
+
42
+ if (baseDocument._id.startsWith('drafts.')) {
43
+ baseLangEntry.value = {
44
+ ...baseLangEntry.value,
45
+ _weak: true,
46
+ //this should reflect doc i18n config when this
47
+ //plugin is able to take that as a config option
48
+ _strengthenOnPublish: {
49
+ type: baseDocument._type,
50
+ },
51
+ };
52
+ }
53
+
54
+ try {
55
+ // Use createIfNotExists to handle race conditions
56
+ return await client.createIfNotExists({
57
+ _id: `translation.metadata.${documentId.replace('drafts.', '')}`,
58
+ _type: 'translation.metadata',
59
+ translations: [baseLangEntry],
60
+ });
61
+ } catch (error) {
62
+ // If creation fails due to race condition, fetch the existing document
63
+ const metadata = await client.fetch(
64
+ `*[
65
+ _type == 'translation.metadata' &&
66
+ translations[_key == $baseLanguage][0].value._ref == $id
67
+ ][0]`,
68
+ { baseLanguage, id: documentId.replace('drafts.', '') }
69
+ );
70
+
71
+ if (metadata) {
72
+ return metadata;
73
+ }
74
+
75
+ throw error;
76
+ }
77
+ };
@@ -0,0 +1,15 @@
1
+ import { SanityClient, SanityDocumentLike } from 'sanity';
2
+
3
+ export const getTranslationMetadata = (
4
+ id: string,
5
+ client: SanityClient,
6
+ baseLanguage: string
7
+ ): Promise<SanityDocumentLike | null> => {
8
+ return client.fetch(
9
+ `*[
10
+ _type == 'translation.metadata' &&
11
+ translations[_key == $baseLanguage][0].value._ref == $id
12
+ ][0]`,
13
+ { baseLanguage, id: id.replace('drafts.', '') }
14
+ );
15
+ };
@@ -0,0 +1,5 @@
1
+ export { createI18nDocAndPatchMetadata } from './createI18nDocAndPatchMetadata';
2
+ export { createTranslationMetadata } from './createTranslationMetadata';
3
+ export { getTranslationMetadata } from './getTranslationMetadata';
4
+ export { getOrCreateTranslationMetadata } from './getOrCreateTranslationMetadata';
5
+ export { patchI18nDoc } from './patchI18nDoc';
@@ -0,0 +1,25 @@
1
+ import { SanityClient, SanityDocumentLike } from 'sanity';
2
+
3
+ export const patchI18nDoc = (
4
+ i18nDocId: string,
5
+ mergedDocument: SanityDocumentLike,
6
+ translatedFields: Record<string, any>,
7
+ client: SanityClient
8
+ ): void => {
9
+ const cleanedMerge: Record<string, any> = {};
10
+ Object.entries(mergedDocument).forEach(([key, value]) => {
11
+ if (
12
+ //only patch those fields that had translated strings
13
+ key in translatedFields &&
14
+ //don't overwrite any existing system values on the i18n doc
15
+ !['_id', '_rev', '_updatedAt', 'language'].includes(key)
16
+ ) {
17
+ cleanedMerge[key] = value;
18
+ }
19
+ });
20
+
21
+ client
22
+ .transaction()
23
+ .patch(i18nDocId, (p) => p.set(cleanedMerge))
24
+ .commit();
25
+ };
@@ -0,0 +1,129 @@
1
+ import {
2
+ ExportForTranslation,
3
+ GTSerializedDocument,
4
+ ImportTranslation,
5
+ } from '../../types';
6
+ import { SanityDocument } from 'sanity';
7
+ import { findLatestDraft } from '../utils';
8
+ import { documentLevelPatch } from './documentLevelPatch';
9
+ import { legacyDocumentLevelPatch } from './legacyDocumentLevelPatch';
10
+ import {
11
+ BaseDocumentDeserializer,
12
+ BaseDocumentSerializer,
13
+ defaultStopTypes,
14
+ customSerializers,
15
+ customBlockDeserializers,
16
+ } from 'sanity-naive-html-serializer';
17
+ import { gtConfig } from '../../adapter/core';
18
+
19
+ export const baseDocumentLevelConfig = {
20
+ exportForTranslation: async (
21
+ ...params: Parameters<ExportForTranslation>
22
+ ): Promise<GTSerializedDocument> => {
23
+ const [
24
+ docInfo,
25
+ context,
26
+ serializationOptions = {},
27
+ languageField = 'language',
28
+ ] = params;
29
+ const { client, schema } = context;
30
+ const stopTypes = [
31
+ ...(serializationOptions.additionalStopTypes ?? []),
32
+ ...defaultStopTypes,
33
+ ];
34
+ const serializers = {
35
+ ...customSerializers,
36
+ types: {
37
+ ...customSerializers.types,
38
+ ...(serializationOptions.additionalSerializers ?? {}),
39
+ },
40
+ };
41
+ const doc = await findLatestDraft(docInfo.documentId, client);
42
+ delete doc[languageField];
43
+ const baseLanguage = gtConfig.getSourceLocale();
44
+ const serialized = BaseDocumentSerializer(schema).serializeDocument(
45
+ doc,
46
+ 'document',
47
+ baseLanguage,
48
+ stopTypes,
49
+ serializers
50
+ );
51
+ return {
52
+ content: serialized.content,
53
+ documentId: docInfo.documentId,
54
+ versionId: docInfo.versionId,
55
+ };
56
+ },
57
+ importTranslation: (
58
+ ...params: Parameters<ImportTranslation>
59
+ ): Promise<void> => {
60
+ const [
61
+ docInfo,
62
+ localeId,
63
+ document,
64
+ context,
65
+ serializationOptions = {},
66
+ languageField = 'language',
67
+ mergeWithTargetLocale = false,
68
+ ] = params;
69
+ const { client } = context;
70
+ const deserializers = {
71
+ types: {
72
+ ...(serializationOptions.additionalDeserializers ?? {}),
73
+ },
74
+ };
75
+ const blockDeserializers = [
76
+ ...(serializationOptions.additionalBlockDeserializers ?? []),
77
+ ...customBlockDeserializers,
78
+ ];
79
+
80
+ const deserialized = BaseDocumentDeserializer.deserializeDocument(
81
+ document,
82
+ deserializers,
83
+ blockDeserializers
84
+ ) as SanityDocument;
85
+ return documentLevelPatch(
86
+ docInfo, // versionId is not used here, since we just use the _rev id in the deserialized HTML itself
87
+ deserialized,
88
+ localeId,
89
+ client,
90
+ languageField,
91
+ mergeWithTargetLocale
92
+ );
93
+ },
94
+ secretsNamespace: 'translationService',
95
+ };
96
+
97
+ export const legacyDocumentLevelConfig = {
98
+ ...baseDocumentLevelConfig,
99
+ importTranslation: (
100
+ ...params: Parameters<ImportTranslation>
101
+ ): Promise<void> => {
102
+ const [docInfo, localeId, document, context, serializationOptions = {}] =
103
+ params;
104
+ const { client } = context;
105
+ const deserializers = {
106
+ types: {
107
+ ...(serializationOptions.additionalDeserializers ?? {}),
108
+ },
109
+ };
110
+ const blockDeserializers = [
111
+ ...(serializationOptions.additionalBlockDeserializers ?? []),
112
+ ...customBlockDeserializers,
113
+ ];
114
+
115
+ const deserialized = BaseDocumentDeserializer.deserializeDocument(
116
+ document,
117
+ deserializers,
118
+ blockDeserializers
119
+ ) as SanityDocument;
120
+ return legacyDocumentLevelPatch(
121
+ docInfo, // versionId is not used here, since we just use the _rev id in the deserialized HTML itself
122
+ deserialized,
123
+ localeId,
124
+ client
125
+ );
126
+ },
127
+ };
128
+
129
+ export { documentLevelPatch, legacyDocumentLevelPatch };
@@ -0,0 +1,69 @@
1
+ import { SanityClient, SanityDocument, SanityDocumentLike } from 'sanity';
2
+ import { BaseDocumentMerger } from 'sanity-naive-html-serializer';
3
+
4
+ import { findLatestDraft, findDocumentAtRevision } from '../utils';
5
+ import type { GTFile } from '../../types';
6
+
7
+ export const legacyDocumentLevelPatch = async (
8
+ docInfo: GTFile,
9
+ translatedFields: SanityDocument,
10
+ localeId: string,
11
+ client: SanityClient
12
+ ): Promise<void> => {
13
+ let baseDoc: SanityDocument | null = null;
14
+
15
+ /*
16
+ * we send over the _rev with our translation file so we can
17
+ * accurately coalesce the translations in case something has
18
+ * changed in the base document since translating
19
+ */
20
+ if (docInfo.documentId && docInfo.versionId) {
21
+ baseDoc = await findDocumentAtRevision(
22
+ docInfo.documentId,
23
+ docInfo.versionId,
24
+ client
25
+ );
26
+ }
27
+ if (!baseDoc) {
28
+ baseDoc = await findLatestDraft(docInfo.documentId, client);
29
+ }
30
+
31
+ /*
32
+ * we then merge the translation with the base document
33
+ * to create a document that contains the translation and everything
34
+ * that wasn't sent over for translation
35
+ */
36
+ const merged = BaseDocumentMerger.documentLevelMerge(
37
+ translatedFields,
38
+ baseDoc
39
+ ) as SanityDocumentLike;
40
+
41
+ /* we now need to check if we have a translated document
42
+ * if not, we create it
43
+ */
44
+ const targetId = `drafts.${docInfo.documentId}__i18n_${localeId}`;
45
+ const i18nDoc = await findLatestDraft(targetId, client);
46
+ if (i18nDoc) {
47
+ const cleanedMerge: Record<string, any> = {};
48
+ //don't overwrite any existing system values on the i18n doc
49
+ Object.entries(merged).forEach(([key, value]) => {
50
+ if (
51
+ Object.keys(translatedFields).includes(key) &&
52
+ !['_id', '_rev', '_updatedAt'].includes(key)
53
+ ) {
54
+ cleanedMerge[key] = value;
55
+ }
56
+ });
57
+
58
+ await client
59
+ .transaction()
60
+ //@ts-ignore
61
+ .patch(i18nDoc._id, (p) => p.set(cleanedMerge))
62
+ .commit();
63
+ } else {
64
+ merged._id = targetId;
65
+
66
+ merged.__i18n_lang = localeId;
67
+ await client.create(merged);
68
+ }
69
+ };
@@ -0,0 +1,118 @@
1
+ import { SanityClient, SanityDocument } from 'sanity';
2
+ import {
3
+ BaseDocumentSerializer,
4
+ BaseDocumentDeserializer,
5
+ BaseDocumentMerger,
6
+ defaultStopTypes,
7
+ customSerializers,
8
+ customBlockDeserializers,
9
+ } from 'sanity-naive-html-serializer';
10
+
11
+ import type {
12
+ ExportForTranslation,
13
+ GTFile,
14
+ GTSerializedDocument,
15
+ ImportTranslation,
16
+ } from '../types';
17
+ import { findLatestDraft, findDocumentAtRevision } from './utils';
18
+ import { gtConfig } from '../adapter/core';
19
+
20
+ export const fieldLevelPatch = async (
21
+ docInfo: GTFile,
22
+ translatedFields: SanityDocument,
23
+ localeId: string,
24
+ client: SanityClient,
25
+ mergeWithTargetLocale: boolean = false
26
+ ): Promise<void> => {
27
+ let baseDoc: SanityDocument;
28
+ const baseLanguage = gtConfig.getSourceLocale();
29
+ if (docInfo.documentId && docInfo.versionId) {
30
+ baseDoc = await findDocumentAtRevision(
31
+ docInfo.documentId,
32
+ docInfo.versionId,
33
+ client
34
+ );
35
+ } else {
36
+ baseDoc = await findLatestDraft(docInfo.documentId, client);
37
+ }
38
+
39
+ const merged = BaseDocumentMerger.fieldLevelMerge(
40
+ translatedFields,
41
+ baseDoc,
42
+ localeId,
43
+ mergeWithTargetLocale ? baseLanguage : localeId
44
+ );
45
+
46
+ await client.patch(baseDoc._id).set(merged).commit();
47
+ };
48
+
49
+ export const baseFieldLevelConfig = {
50
+ exportForTranslation: async (
51
+ ...params: Parameters<ExportForTranslation>
52
+ ): Promise<GTSerializedDocument> => {
53
+ const [docInfo, context, serializationOptions = {}] = params;
54
+ const baseLanguage = gtConfig.getSourceLocale();
55
+ const { client, schema } = context;
56
+ const stopTypes = [
57
+ ...(serializationOptions.additionalStopTypes ?? []),
58
+ ...defaultStopTypes,
59
+ ];
60
+ const serializers = {
61
+ ...customSerializers,
62
+ types: {
63
+ ...customSerializers.types,
64
+ ...(serializationOptions.additionalSerializers ?? {}),
65
+ },
66
+ };
67
+ const doc = await findLatestDraft(docInfo.documentId, client);
68
+ const serialized = BaseDocumentSerializer(schema).serializeDocument(
69
+ doc,
70
+ 'field',
71
+ baseLanguage,
72
+ stopTypes,
73
+ serializers
74
+ );
75
+ return {
76
+ content: serialized.content,
77
+ documentId: docInfo.documentId,
78
+ versionId: docInfo.versionId,
79
+ };
80
+ },
81
+ importTranslation: (
82
+ ...params: Parameters<ImportTranslation>
83
+ ): Promise<void> => {
84
+ const [
85
+ docInfo,
86
+ localeId,
87
+ document,
88
+ context,
89
+ serializationOptions = {},
90
+ ,
91
+ mergeWithTargetLocale,
92
+ ] = params;
93
+ const { client } = context;
94
+ const deserializers = {
95
+ types: {
96
+ ...(serializationOptions.additionalDeserializers ?? {}),
97
+ },
98
+ };
99
+ const blockDeserializers = [
100
+ ...(serializationOptions.additionalBlockDeserializers ?? []),
101
+ ...customBlockDeserializers,
102
+ ];
103
+
104
+ const deserialized = BaseDocumentDeserializer.deserializeDocument(
105
+ document,
106
+ deserializers,
107
+ blockDeserializers
108
+ ) as SanityDocument;
109
+ return fieldLevelPatch(
110
+ docInfo,
111
+ deserialized,
112
+ localeId,
113
+ client,
114
+ mergeWithTargetLocale
115
+ );
116
+ },
117
+ secretsNamespace: 'translationService',
118
+ };
@@ -0,0 +1,18 @@
1
+ import {
2
+ baseDocumentLevelConfig,
3
+ documentLevelPatch,
4
+ legacyDocumentLevelConfig,
5
+ legacyDocumentLevelPatch,
6
+ } from './baseDocumentLevelConfig';
7
+ import { baseFieldLevelConfig, fieldLevelPatch } from './baseFieldLevelConfig';
8
+ import { findLatestDraft } from './utils';
9
+
10
+ export {
11
+ baseDocumentLevelConfig,
12
+ legacyDocumentLevelConfig,
13
+ baseFieldLevelConfig,
14
+ findLatestDraft,
15
+ legacyDocumentLevelPatch,
16
+ documentLevelPatch,
17
+ fieldLevelPatch,
18
+ };
@@ -0,0 +1,13 @@
1
+ export const checkSerializationVersion = (HTMLdoc: string): string | null => {
2
+ const parser = new DOMParser();
3
+ const node = parser.parseFromString(HTMLdoc, 'text/html');
4
+ const versionMetaTag = Array.from(node.head.children).find(
5
+ (metaTag) => metaTag.getAttribute('name') === 'version'
6
+ );
7
+ if (!versionMetaTag) {
8
+ return null;
9
+ }
10
+
11
+ const version = versionMetaTag.getAttribute('content');
12
+ return version;
13
+ };
@@ -0,0 +1,22 @@
1
+ import { SanityClient, SanityDocument } from 'sanity';
2
+
3
+ //revision fetch
4
+ export const findDocumentAtRevision = async (
5
+ documentId: string,
6
+ rev: string,
7
+ client: SanityClient
8
+ ): Promise<SanityDocument> => {
9
+ const dataset = client.config().dataset;
10
+ const baseUrl = `/data/history/${dataset}/documents/${documentId}?revision=${rev}`;
11
+ const url = client.getUrl(baseUrl);
12
+ const revisionDoc = await fetch(url, { credentials: 'include' })
13
+ .then((req) => req.json())
14
+ .then((req) => {
15
+ if (req.documents && req.documents.length) {
16
+ return req.documents[0];
17
+ }
18
+ return null;
19
+ });
20
+
21
+ return revisionDoc;
22
+ };
@@ -0,0 +1,16 @@
1
+ import { SanityClient, SanityDocument } from 'sanity';
2
+
3
+ //use perspectives in the future
4
+ export const findLatestDraft = (
5
+ documentId: string,
6
+ client: SanityClient
7
+ ): Promise<SanityDocument> => {
8
+ const query = `*[_id == $id || _id == $draftId]`;
9
+ const params = { id: documentId, draftId: `drafts.${documentId}` };
10
+ return client
11
+ .fetch(query, params)
12
+ .then(
13
+ (docs: SanityDocument[]) =>
14
+ docs.find((doc) => doc._id.startsWith('drafts.')) ?? docs[0]
15
+ );
16
+ };
@@ -0,0 +1,3 @@
1
+ export * from './findDocumentAtRevision';
2
+ export * from './findLatestDraft';
3
+ export * from './checkSerializationVersion';
@@ -0,0 +1,5 @@
1
+ import { SanityClient, useClient as useSanityClient } from 'sanity';
2
+
3
+ export const useClient = (): SanityClient => {
4
+ return useSanityClient({ apiVersion: '2022-12-07' });
5
+ };
@@ -0,0 +1,33 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { SanityDocumentLike } from 'sanity';
3
+ import { useClient } from './useClient';
4
+
5
+ interface ReturnProps<T> {
6
+ loading: boolean;
7
+ secrets: T | null;
8
+ }
9
+ export function useSecrets<T>(id: string): ReturnProps<T> {
10
+ const [loading, setLoading] = useState<boolean>(true);
11
+ const [secrets, setSecrets] = useState<T | null>(null);
12
+ const client = useClient();
13
+
14
+ useEffect(() => {
15
+ function fetchData() {
16
+ client
17
+ .fetch('* [_id == $id][0]', { id })
18
+ .then((doc: SanityDocumentLike) => {
19
+ const result: Record<string, any> = {};
20
+ for (const key in doc) {
21
+ if (key[0] !== '_') {
22
+ result[key] = doc[key];
23
+ }
24
+ }
25
+ setSecrets(result as T);
26
+ setLoading(false);
27
+ });
28
+ }
29
+ fetchData();
30
+ }, [id, client]);
31
+
32
+ return { loading, secrets };
33
+ }