gt-sanity 3.1.2 → 3.1.4

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 (32) hide show
  1. package/README.md +87 -1
  2. package/dist/index.cjs +553 -168
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +43 -0
  5. package/dist/index.d.ts +43 -0
  6. package/dist/index.js +555 -170
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/adapter/core.ts +31 -7
  10. package/src/adapter/types.ts +23 -1
  11. package/src/components/TranslationsProvider.tsx +149 -65
  12. package/src/components/page/DebugInfoDialog.tsx +142 -0
  13. package/src/components/page/TranslationsTable.tsx +7 -2
  14. package/src/components/page/TranslationsTool.tsx +29 -8
  15. package/src/components/shared/LanguageStatus.tsx +34 -15
  16. package/src/components/shared/SingleDocumentView.tsx +7 -2
  17. package/src/components/tab/TranslationView.tsx +88 -41
  18. package/src/configuration/baseDocumentLevelConfig/documentLevelPatch.test.ts +84 -0
  19. package/src/index.ts +40 -3
  20. package/src/sanity-api/resolveRefs.ts +39 -0
  21. package/src/structure/__tests__/localizedStructure.test.ts +149 -0
  22. package/src/structure/localizedStructure.ts +156 -0
  23. package/src/utils/__tests__/debugInfo.test.ts +114 -0
  24. package/src/utils/__tests__/languageStatusState.test.ts +61 -0
  25. package/src/utils/__tests__/translationPreferences.test.ts +102 -0
  26. package/src/utils/__tests__/translationSummary.test.ts +88 -0
  27. package/src/utils/debugInfo.ts +116 -0
  28. package/src/utils/importUtils.ts +15 -6
  29. package/src/utils/languageStatusState.ts +25 -0
  30. package/src/utils/translationPreferences.ts +54 -0
  31. package/src/utils/translationSummary.ts +140 -0
  32. package/src/components/shared/ProgressBar.tsx +0 -54
package/dist/index.cjs CHANGED
@@ -5,7 +5,16 @@ function _interopDefaultCompat(e) {
5
5
  return e && typeof e == "object" && "default" in e ? e : { default: e };
6
6
  }
7
7
  var React__default = /* @__PURE__ */ _interopDefaultCompat(React), JSONPointer__default = /* @__PURE__ */ _interopDefaultCompat(JSONPointer), merge__default = /* @__PURE__ */ _interopDefaultCompat(merge);
8
- const SECRETS_NAMESPACE = "generaltranslation.secrets", gt = new generaltranslation.GT();
8
+ const SECRETS_NAMESPACE = "generaltranslation.secrets", gt = new generaltranslation.GT(), DEFAULT_TRANSLATION_PREFERENCES = {
9
+ autoRefresh: !0,
10
+ autoImport: !0,
11
+ autoPatchReferences: !1,
12
+ autoPublish: !1,
13
+ // Opt-in: turning this on means Sanity overwrites whatever General
14
+ // Translation holds for that source version, including a completed
15
+ // translation that has not been imported yet.
16
+ preserveExistingTranslations: !1
17
+ };
9
18
  function overrideConfig(secrets) {
10
19
  gt.setConfig({
11
20
  ...secrets?.project && { projectId: secrets?.project },
@@ -29,10 +38,10 @@ class GTConfig {
29
38
  additionalBlockDeserializers;
30
39
  translationLevel;
31
40
  fieldLevelDocuments;
32
- preserveExistingTranslations;
41
+ preferences;
33
42
  static instance;
34
- constructor(secretsNamespace, languageField, sourceLocale, locales, singletons, singletonMapping, ignoreFields, dedupeFields, skipFields, translateDocuments, additionalStopTypes = [], additionalSerializers = {}, additionalDeserializers = { types: {} }, additionalBlockDeserializers = [], translationLevel = "document", fieldLevelDocuments = [], preserveExistingTranslations = !1) {
35
- this.secretsNamespace = secretsNamespace, this.languageField = languageField, this.sourceLocale = sourceLocale, this.locales = locales, this.singletons = singletons, this.singletonMapping = singletonMapping, this.ignoreFields = ignoreFields, this.dedupeFields = dedupeFields, this.skipFields = skipFields, this.translateDocuments = translateDocuments, this.additionalStopTypes = additionalStopTypes, this.additionalSerializers = additionalSerializers, this.additionalDeserializers = additionalDeserializers, this.additionalBlockDeserializers = additionalBlockDeserializers, this.translationLevel = translationLevel, this.fieldLevelDocuments = fieldLevelDocuments, this.preserveExistingTranslations = preserveExistingTranslations;
43
+ constructor(secretsNamespace, languageField, sourceLocale, locales, singletons, singletonMapping, ignoreFields, dedupeFields, skipFields, translateDocuments, additionalStopTypes = [], additionalSerializers = {}, additionalDeserializers = { types: {} }, additionalBlockDeserializers = [], translationLevel = "document", fieldLevelDocuments = [], preferences = DEFAULT_TRANSLATION_PREFERENCES) {
44
+ this.secretsNamespace = secretsNamespace, this.languageField = languageField, this.sourceLocale = sourceLocale, this.locales = locales, this.singletons = singletons, this.singletonMapping = singletonMapping, this.ignoreFields = ignoreFields, this.dedupeFields = dedupeFields, this.skipFields = skipFields, this.translateDocuments = translateDocuments, this.additionalStopTypes = additionalStopTypes, this.additionalSerializers = additionalSerializers, this.additionalDeserializers = additionalDeserializers, this.additionalBlockDeserializers = additionalBlockDeserializers, this.translationLevel = translationLevel, this.fieldLevelDocuments = fieldLevelDocuments, this.preferences = preferences;
36
45
  }
37
46
  static getInstance() {
38
47
  return this.instance || (this.instance = new GTConfig(
@@ -52,8 +61,8 @@ class GTConfig {
52
61
  []
53
62
  )), this.instance;
54
63
  }
55
- init(secretsNamespace, languageField, sourceLocale, locales, singletons, singletonMapping, ignoreFields, dedupeFields, skipFields, translateDocuments, additionalStopTypes = [], additionalSerializers = {}, additionalDeserializers = { types: {} }, additionalBlockDeserializers = [], translationLevel = "document", fieldLevelDocuments = [], preserveExistingTranslations = !1) {
56
- this.secretsNamespace = secretsNamespace, this.languageField = languageField, this.sourceLocale = sourceLocale, this.locales = locales, this.singletons = singletons, this.singletonMapping = singletonMapping, this.ignoreFields = ignoreFields, this.dedupeFields = dedupeFields, this.skipFields = skipFields, this.translateDocuments = translateDocuments, this.additionalStopTypes = additionalStopTypes, this.additionalSerializers = additionalSerializers, this.additionalDeserializers = additionalDeserializers, this.additionalBlockDeserializers = additionalBlockDeserializers, this.translationLevel = translationLevel, this.fieldLevelDocuments = fieldLevelDocuments, this.preserveExistingTranslations = preserveExistingTranslations;
64
+ init(secretsNamespace, languageField, sourceLocale, locales, singletons, singletonMapping, ignoreFields, dedupeFields, skipFields, translateDocuments, additionalStopTypes = [], additionalSerializers = {}, additionalDeserializers = { types: {} }, additionalBlockDeserializers = [], translationLevel = "document", fieldLevelDocuments = [], preferences = DEFAULT_TRANSLATION_PREFERENCES) {
65
+ this.secretsNamespace = secretsNamespace, this.languageField = languageField, this.sourceLocale = sourceLocale, this.locales = locales, this.singletons = singletons, this.singletonMapping = singletonMapping, this.ignoreFields = ignoreFields, this.dedupeFields = dedupeFields, this.skipFields = skipFields, this.translateDocuments = translateDocuments, this.additionalStopTypes = additionalStopTypes, this.additionalSerializers = additionalSerializers, this.additionalDeserializers = additionalDeserializers, this.additionalBlockDeserializers = additionalBlockDeserializers, this.translationLevel = translationLevel, this.fieldLevelDocuments = fieldLevelDocuments, this.preferences = preferences;
57
66
  }
58
67
  getSecretsNamespace() {
59
68
  return this.secretsNamespace;
@@ -103,8 +112,8 @@ class GTConfig {
103
112
  getFieldLevelDocuments() {
104
113
  return this.fieldLevelDocuments;
105
114
  }
106
- getPreserveExistingTranslations() {
107
- return this.preserveExistingTranslations;
115
+ getPreferences() {
116
+ return this.preferences;
108
117
  }
109
118
  }
110
119
  const pluginConfig = GTConfig.getInstance();
@@ -1564,6 +1573,21 @@ async function resolveRefs(doc, locale, client) {
1564
1573
  );
1565
1574
  return updateDocumentReferences(doc, translatedRefs);
1566
1575
  }
1576
+ async function commitResolvedRefs(translatedDoc, resolvedDoc, client) {
1577
+ const {
1578
+ _id: _resolvedId,
1579
+ _rev: _resolvedRev,
1580
+ _createdAt: _resolvedCreatedAt,
1581
+ _updatedAt: _resolvedUpdatedAt,
1582
+ ...changes
1583
+ } = resolvedDoc;
1584
+ if (translatedDoc._id.startsWith("drafts.")) {
1585
+ await client.patch(translatedDoc._id).set(changes).commit();
1586
+ return;
1587
+ }
1588
+ const draftId = `drafts.${getPublishedId(translatedDoc._id)}`;
1589
+ await client.transaction().createIfNotExists({ ...translatedDoc, _id: draftId }).patch(draftId, (patch) => patch.set(changes)).commit();
1590
+ }
1567
1591
  function findReferences(obj, path = []) {
1568
1592
  if (!obj || typeof obj != "object")
1569
1593
  return [];
@@ -1699,13 +1723,14 @@ async function processImportBatch(items, options = {}) {
1699
1723
  }
1700
1724
  async function getReadyFilesForImport(translationStatuses, options = {}) {
1701
1725
  const { filterReadyFiles = () => !0 } = options, readyFilesByDocumentLocale = /* @__PURE__ */ new Map();
1702
- for (const [key, status] of translationStatuses.entries())
1703
- if (status.isReady && status.fileData && filterReadyFiles(key, status)) {
1726
+ for (const [key, status] of translationStatuses.entries()) {
1727
+ const readyStatus = status.isReady && status.fileData ? { ...status, fileData: status.fileData } : null;
1728
+ if (readyStatus && filterReadyFiles(key, readyStatus)) {
1704
1729
  const fileData = {
1705
- fileId: getPublishedId(status.fileData.fileId),
1706
- versionId: status.fileData.versionId,
1707
- branchId: status.fileData.branchId,
1708
- locale: status.fileData.locale
1730
+ fileId: getPublishedId(readyStatus.fileData.fileId),
1731
+ versionId: readyStatus.fileData.versionId,
1732
+ branchId: readyStatus.fileData.branchId,
1733
+ locale: readyStatus.fileData.locale
1709
1734
  };
1710
1735
  readyFilesByDocumentLocale.set(
1711
1736
  createStableTranslationKey(
@@ -1716,6 +1741,7 @@ async function getReadyFilesForImport(translationStatuses, options = {}) {
1716
1741
  fileData
1717
1742
  );
1718
1743
  }
1744
+ }
1719
1745
  return Array.from(readyFilesByDocumentLocale.values());
1720
1746
  }
1721
1747
  async function importTranslations(readyFiles, secrets, translationContext, options = {}) {
@@ -1778,7 +1804,30 @@ const getLocales = async (_secrets) => pluginConfig.getLocales().map((locale) =>
1778
1804
  localeId: locale,
1779
1805
  description: gt.getLocaleProperties(locale).name,
1780
1806
  enabled: !0
1781
- })), TranslationsContext = React.createContext(null), getUploadedVersionsStorageKey = (projectId, dataset) => `gt-sanity:uploadedVersions:${projectId ?? ""}:${dataset ?? ""}`;
1807
+ })), getPreferencesStorageKey = (projectId, dataset) => `gt-sanity:preferences:${projectId ?? ""}:${dataset ?? ""}`, isPlainRecord$1 = (value) => typeof value == "object" && value !== null && !Array.isArray(value);
1808
+ function readPreferences(storageKey, defaults) {
1809
+ try {
1810
+ const raw = window.localStorage.getItem(storageKey);
1811
+ if (!raw) return defaults;
1812
+ const parsed = JSON.parse(raw);
1813
+ if (!isPlainRecord$1(parsed)) return defaults;
1814
+ const merged = { ...defaults }, keys = Object.keys(defaults);
1815
+ for (const key of keys) {
1816
+ const value = parsed[key];
1817
+ typeof value == "boolean" && (merged[key] = value);
1818
+ }
1819
+ return merged;
1820
+ } catch {
1821
+ return defaults;
1822
+ }
1823
+ }
1824
+ function writePreferences(storageKey, value) {
1825
+ try {
1826
+ window.localStorage.setItem(storageKey, JSON.stringify(value));
1827
+ } catch {
1828
+ }
1829
+ }
1830
+ const TranslationsContext = React.createContext(null), getUploadedVersionsStorageKey = (projectId, dataset) => `gt-sanity:uploadedVersions:${projectId ?? ""}:${dataset ?? ""}`;
1782
1831
  function readUploadedVersions(storageKey) {
1783
1832
  try {
1784
1833
  const raw = window.sessionStorage.getItem(storageKey);
@@ -1814,7 +1863,7 @@ const useTranslations = () => {
1814
1863
  children,
1815
1864
  singleDocument
1816
1865
  }) => {
1817
- const [isBusy, setIsBusy] = React.useState(!1), [documents, setDocuments] = React.useState([]), [locales, setLocales] = React.useState([]), [autoRefresh, setAutoRefresh] = React.useState(!0), [autoImport, setAutoImport] = React.useState(!1), [autoPatchReferences, setAutoPatchReferences] = React.useState(!1), [autoPublish, setAutoPublish] = React.useState(!1), [preserveExistingTranslations, setPreserveExistingTranslations] = React.useState(() => pluginConfig.getPreserveExistingTranslations()), [loadingDocuments, setLoadingDocuments] = React.useState(!1), [importProgress, setImportProgress] = React.useState({
1866
+ const [isBusy, setIsBusy] = React.useState(!1), [documents, setDocuments] = React.useState([]), [locales, setLocales] = React.useState([]), [loadingDocuments, setLoadingDocuments] = React.useState(!1), [importProgress, setImportProgress] = React.useState({
1818
1867
  current: 0,
1819
1868
  total: 0,
1820
1869
  isImporting: !1
@@ -1826,11 +1875,42 @@ const useTranslations = () => {
1826
1875
  downloaded: /* @__PURE__ */ new Set(),
1827
1876
  failed: /* @__PURE__ */ new Set(),
1828
1877
  skipped: /* @__PURE__ */ new Set()
1829
- }), downloadStatusRef = React.useRef(downloadStatus), [translationStatuses, setTranslationStatuses] = React.useState(/* @__PURE__ */ new Map()), [isRefreshing, setIsRefreshing] = React.useState(!1), client = useClient(), { projectId, dataset } = client.config(), uploadedVersionsStorageKey = getUploadedVersionsStorageKey(
1878
+ }), downloadStatusRef = React.useRef(downloadStatus), [translationStatuses, setTranslationStatuses] = React.useState(/* @__PURE__ */ new Map()), [pendingTranslations, setPendingTranslations] = React.useState(
1879
+ /* @__PURE__ */ new Set()
1880
+ ), [isRefreshing, setIsRefreshing] = React.useState(!1), client = useClient(), { projectId, dataset } = client.config(), uploadedVersionsStorageKey = getUploadedVersionsStorageKey(
1830
1881
  projectId,
1831
1882
  dataset
1832
1883
  ), [uploadedVersions, setUploadedVersions] = React.useState(
1833
1884
  () => readUploadedVersions(uploadedVersionsStorageKey)
1885
+ ), preferencesStorageKey = getPreferencesStorageKey(projectId, dataset), [preferences, setPreferences] = React.useState(
1886
+ () => readPreferences(preferencesStorageKey, pluginConfig.getPreferences())
1887
+ ), {
1888
+ autoRefresh,
1889
+ autoImport,
1890
+ autoPatchReferences,
1891
+ autoPublish,
1892
+ preserveExistingTranslations
1893
+ } = preferences, setPreference = React.useCallback(
1894
+ (key, value) => {
1895
+ const next = { ...preferences, [key]: value };
1896
+ setPreferences(next), writePreferences(preferencesStorageKey, next);
1897
+ },
1898
+ [preferences, preferencesStorageKey]
1899
+ ), setAutoRefresh = React.useCallback(
1900
+ (value) => setPreference("autoRefresh", value),
1901
+ [setPreference]
1902
+ ), setAutoImport = React.useCallback(
1903
+ (value) => setPreference("autoImport", value),
1904
+ [setPreference]
1905
+ ), setAutoPatchReferences = React.useCallback(
1906
+ (value) => setPreference("autoPatchReferences", value),
1907
+ [setPreference]
1908
+ ), setAutoPublish = React.useCallback(
1909
+ (value) => setPreference("autoPublish", value),
1910
+ [setPreference]
1911
+ ), setPreserveExistingTranslations = React.useCallback(
1912
+ (value) => setPreference("preserveExistingTranslations", value),
1913
+ [setPreference]
1834
1914
  ), schema2 = sanity.useSchema(), translationContext = { client, schema: schema2 }, toast = ui.useToast(), { loading: loadingSecrets, secrets } = useSecrets(
1835
1915
  pluginConfig.getSecretsNamespace()
1836
1916
  ), [branchId, setBranchId] = React.useState(void 0);
@@ -1962,7 +2042,20 @@ const useTranslations = () => {
1962
2042
  const nextUploadedVersions = new Map(uploadedVersions);
1963
2043
  for (const { info } of transformedDocuments)
1964
2044
  info.versionId && nextUploadedVersions.set(info.documentId, info.versionId);
1965
- writeUploadedVersions(uploadedVersionsStorageKey, nextUploadedVersions), setUploadedVersions(nextUploadedVersions), toast.push({
2045
+ writeUploadedVersions(uploadedVersionsStorageKey, nextUploadedVersions), setUploadedVersions(nextUploadedVersions), setPendingTranslations((prev) => {
2046
+ const next = new Set(prev);
2047
+ for (const { info } of transformedDocuments)
2048
+ for (const localeId of availableLocaleIds)
2049
+ next.add(
2050
+ createTranslationStatusKey(
2051
+ branchId,
2052
+ info.documentId,
2053
+ info.versionId ?? "",
2054
+ localeId
2055
+ )
2056
+ );
2057
+ return next;
2058
+ }), toast.push({
1966
2059
  title: force ? `Retranslating ${documents.length} documents from scratch` : `Translation tasks created for ${documents.length} documents`,
1967
2060
  status: "success",
1968
2061
  closable: !0
@@ -2196,28 +2289,27 @@ const useTranslations = () => {
2196
2289
  setBranchId(defaultBranch.branch.id);
2197
2290
  },
2198
2291
  [secrets]
2199
- ), handleRefreshAll = React.useCallback(async () => {
2200
- if (!(!secrets || documents.length === 0 || !branchId)) {
2201
- setIsRefreshing(!0);
2202
- try {
2203
- const availableLocaleIds = locales.filter((locale) => locale.enabled !== !1).map((locale) => locale.localeId), fileQueryData = [];
2204
- for (const doc of documents)
2205
- for (const localeId of availableLocaleIds) {
2206
- const documentId = getDocumentPublishedId(doc);
2207
- fileQueryData.push({
2208
- versionId: getVersionId(doc),
2209
- fileId: documentId,
2210
- branchId,
2211
- locale: localeId
2212
- });
2213
- }
2214
- const readyTranslations = await checkTranslationStatus(
2215
- fileQueryData,
2216
- downloadStatusRef.current,
2217
- secrets
2218
- );
2219
- setTranslationStatuses(() => {
2220
- const newStatuses = /* @__PURE__ */ new Map();
2292
+ ), handleRefreshAll = React.useCallback(
2293
+ async ({ silent = !1 } = {}) => {
2294
+ if (!(!secrets || documents.length === 0 || !branchId)) {
2295
+ setIsRefreshing(!0);
2296
+ try {
2297
+ const availableLocaleIds = locales.filter((locale) => locale.enabled !== !1).map((locale) => locale.localeId), fileQueryData = [];
2298
+ for (const doc of documents)
2299
+ for (const localeId of availableLocaleIds) {
2300
+ const documentId = getDocumentPublishedId(doc);
2301
+ fileQueryData.push({
2302
+ versionId: getVersionId(doc),
2303
+ fileId: documentId,
2304
+ branchId,
2305
+ locale: localeId
2306
+ });
2307
+ }
2308
+ const readyTranslations = await checkTranslationStatus(
2309
+ fileQueryData,
2310
+ downloadStatusRef.current,
2311
+ secrets
2312
+ ), newStatuses = /* @__PURE__ */ new Map();
2221
2313
  for (const doc of documents)
2222
2314
  for (const localeId of availableLocaleIds) {
2223
2315
  const documentId = getDocumentPublishedId(doc), versionId = getVersionId(doc), key = createTranslationStatusKey(
@@ -2247,23 +2339,30 @@ const useTranslations = () => {
2247
2339
  }
2248
2340
  });
2249
2341
  }
2250
- return newStatuses;
2251
- }), toast.push({
2252
- title: `Refreshed status for ${documents.length} document(s)`,
2253
- status: "success",
2254
- closable: !0
2255
- });
2256
- } catch (error) {
2257
- console.error("Error refreshing translation status:", error), toast.push({
2258
- title: "Translation status could not be refreshed. Try again before importing.",
2259
- status: "error",
2260
- closable: !0
2261
- });
2262
- } finally {
2263
- setIsRefreshing(!1);
2342
+ setTranslationStatuses(newStatuses), setPendingTranslations((prev) => {
2343
+ if (prev.size === 0) return prev;
2344
+ const next = new Set(prev);
2345
+ for (const [key, status] of newStatuses)
2346
+ status.isReady && next.delete(key);
2347
+ return next.size === prev.size ? prev : next;
2348
+ }), silent || toast.push({
2349
+ title: `Refreshed status for ${documents.length} document(s)`,
2350
+ status: "success",
2351
+ closable: !0
2352
+ });
2353
+ } catch (error) {
2354
+ console.error("Error refreshing translation status:", error), toast.push({
2355
+ title: "Translation status could not be refreshed. Try again before importing.",
2356
+ status: "error",
2357
+ closable: !0
2358
+ });
2359
+ } finally {
2360
+ setIsRefreshing(!1);
2361
+ }
2264
2362
  }
2265
- }
2266
- }, [secrets, documents, locales, branchId, getVersionId]), handleImportDocument = React.useCallback(
2363
+ },
2364
+ [secrets, documents, locales, branchId, getVersionId]
2365
+ ), handleImportDocument = React.useCallback(
2267
2366
  async (documentId, versionId, localeId) => {
2268
2367
  if (!secrets) return;
2269
2368
  const key = createTranslationStatusKey(
@@ -2381,16 +2480,7 @@ const useTranslations = () => {
2381
2480
  localeId,
2382
2481
  client
2383
2482
  );
2384
- if (resolvedDoc !== translatedDoc) {
2385
- const mutation = {
2386
- patch: {
2387
- id: translatedDoc._id,
2388
- set: resolvedDoc
2389
- }
2390
- };
2391
- return await client.mutate([mutation]), { patched: !0, doc: translatedDoc, localeId };
2392
- }
2393
- return { patched: !1, doc: translatedDoc, localeId };
2483
+ return resolvedDoc !== translatedDoc ? (await commitResolvedRefs(translatedDoc, resolvedDoc, client), { patched: !0, doc: translatedDoc, localeId }) : { patched: !1, doc: translatedDoc, localeId };
2394
2484
  },
2395
2485
  {
2396
2486
  onProgress: (current, total) => {
@@ -2487,7 +2577,7 @@ const useTranslations = () => {
2487
2577
  }, [fetchLocales, secrets]), React.useEffect(() => {
2488
2578
  documents.length > 0 && locales.length > 0 && fetchExistingTranslations();
2489
2579
  }, [fetchExistingTranslations]), React.useEffect(() => {
2490
- documents.length > 0 && locales.length > 0 && secrets && !loadingDocuments && branchId && handleRefreshAll();
2580
+ documents.length > 0 && locales.length > 0 && secrets && !loadingDocuments && branchId && handleRefreshAll({ silent: !0 });
2491
2581
  }, [
2492
2582
  documents,
2493
2583
  locales,
@@ -2498,7 +2588,7 @@ const useTranslations = () => {
2498
2588
  ]), React.useEffect(() => {
2499
2589
  if (!autoRefresh || documents.length === 0 || !secrets) return;
2500
2590
  const interval = setInterval(async () => {
2501
- await handleRefreshAll();
2591
+ await handleRefreshAll({ silent: !0 });
2502
2592
  }, 1e4);
2503
2593
  return () => clearInterval(interval);
2504
2594
  }, [autoRefresh, documents.length, secrets, handleRefreshAll]), React.useEffect(() => {
@@ -2522,6 +2612,7 @@ const useTranslations = () => {
2522
2612
  existingTranslations,
2523
2613
  downloadStatus,
2524
2614
  translationStatuses,
2615
+ pendingTranslations,
2525
2616
  isRefreshing,
2526
2617
  loadingSecrets,
2527
2618
  secrets,
@@ -2564,58 +2655,216 @@ const useTranslations = () => {
2564
2655
  ] }) })
2565
2656
  }
2566
2657
  ) : null;
2567
- function ProgressBar({ progress }) {
2568
- return typeof progress > "u" ? (console.warn("No progress prop passed to ProgressBar"), null) : /* @__PURE__ */ jsxRuntime.jsxs(
2569
- ui.Card,
2658
+ var version = "3.1.4";
2659
+ function buildDebugInfo({
2660
+ secrets,
2661
+ preferences,
2662
+ translations,
2663
+ sanityProjectId,
2664
+ sanityDataset,
2665
+ branchId
2666
+ }) {
2667
+ const deserializerTypes = pluginConfig.getAdditionalDeserializers()?.types ?? {};
2668
+ return {
2669
+ package: { name: "gt-sanity", version },
2670
+ sanity: { projectId: sanityProjectId, dataset: sanityDataset },
2671
+ generalTranslation: {
2672
+ projectId: secrets?.project,
2673
+ apiKeyConfigured: !!secrets?.secret,
2674
+ secretsNamespace: pluginConfig.getSecretsNamespace(),
2675
+ secretsDocumentFound: secrets !== null,
2676
+ branchId
2677
+ },
2678
+ locales: {
2679
+ sourceLocale: pluginConfig.getSourceLocale(),
2680
+ targetLocales: pluginConfig.getLocales()
2681
+ },
2682
+ documents: {
2683
+ translationLevel: pluginConfig.getTranslationLevel(),
2684
+ translateDocuments: pluginConfig.getTranslateDocuments(),
2685
+ fieldLevelDocuments: pluginConfig.getFieldLevelDocuments(),
2686
+ languageField: pluginConfig.getLanguageField(),
2687
+ singletons: pluginConfig.getSingletons()
2688
+ },
2689
+ fieldRules: {
2690
+ ignoreFields: pluginConfig.getIgnoreFields(),
2691
+ dedupeFields: pluginConfig.getDedupeFields(),
2692
+ skipFields: pluginConfig.getSkipFields(),
2693
+ additionalStopTypes: pluginConfig.getAdditionalStopTypes()
2694
+ },
2695
+ translations,
2696
+ preferences,
2697
+ customization: {
2698
+ additionalSerializers: Object.keys(
2699
+ pluginConfig.getAdditionalSerializers()?.types ?? {}
2700
+ ),
2701
+ additionalDeserializers: Object.keys(deserializerTypes),
2702
+ additionalBlockDeserializers: pluginConfig.getAdditionalBlockDeserializers()?.length ?? 0
2703
+ }
2704
+ };
2705
+ }
2706
+ function formatDebugInfo(info) {
2707
+ return JSON.stringify(info, null, 2);
2708
+ }
2709
+ const EMPTY_TRANSLATION_SUMMARY = {
2710
+ documentsChecked: 0,
2711
+ expected: 0,
2712
+ found: 0,
2713
+ draftOnly: 0,
2714
+ published: 0,
2715
+ missing: 0
2716
+ };
2717
+ function summarizeTranslations({
2718
+ documentCount,
2719
+ targetLocaleCount,
2720
+ translatedIds,
2721
+ publishedIds,
2722
+ draftIds
2723
+ }) {
2724
+ const unique = Array.from(new Set(translatedIds)), publishedSet = new Set(publishedIds), draftSet = new Set(draftIds), published = unique.filter((id) => publishedSet.has(id)).length, draftOnly = unique.filter(
2725
+ (id) => !publishedSet.has(id) && draftSet.has(`drafts.${id}`)
2726
+ ).length, expected = documentCount * targetLocaleCount;
2727
+ return {
2728
+ documentsChecked: documentCount,
2729
+ expected,
2730
+ found: unique.length,
2731
+ draftOnly,
2732
+ published,
2733
+ missing: Math.max(0, expected - unique.length)
2734
+ };
2735
+ }
2736
+ async function collectTranslationSummary(documents, client) {
2737
+ const documentIds = documents.map(getDocumentPublishedId), targetLocales = pluginConfig.getLocales();
2738
+ if (documentIds.length === 0)
2739
+ return EMPTY_TRANSLATION_SUMMARY;
2740
+ const translatedIds = (await client.fetch(
2741
+ `*[_type == 'translation.metadata' && references($documentIds)]{
2742
+ 'targets': translations[language in $targetLocales && defined(value._ref)]{
2743
+ language,
2744
+ 'id': value._ref
2745
+ }
2746
+ }`,
2747
+ { documentIds, targetLocales }
2748
+ )).flatMap((entry) => entry.targets ?? []).map((target) => target.id).filter((id) => !!id);
2749
+ if (translatedIds.length === 0)
2750
+ return summarizeTranslations({
2751
+ documentCount: documentIds.length,
2752
+ targetLocaleCount: targetLocales.length,
2753
+ translatedIds: [],
2754
+ publishedIds: [],
2755
+ draftIds: []
2756
+ });
2757
+ const unique = Array.from(new Set(translatedIds)), existence = await client.fetch(
2758
+ `{
2759
+ 'publishedIds': *[_id in $ids]._id,
2760
+ 'draftIds': *[_id in $draftIds]._id
2761
+ }`,
2762
+ { ids: unique, draftIds: unique.map((id) => `drafts.${id}`) }
2763
+ );
2764
+ return summarizeTranslations({
2765
+ documentCount: documentIds.length,
2766
+ targetLocaleCount: targetLocales.length,
2767
+ translatedIds: unique,
2768
+ publishedIds: existence?.publishedIds ?? [],
2769
+ draftIds: existence?.draftIds ?? []
2770
+ });
2771
+ }
2772
+ const DebugInfoDialog = ({
2773
+ isOpen,
2774
+ onClose
2775
+ }) => {
2776
+ const {
2777
+ secrets,
2778
+ documents,
2779
+ branchId,
2780
+ autoRefresh,
2781
+ autoImport,
2782
+ autoPatchReferences,
2783
+ autoPublish,
2784
+ preserveExistingTranslations
2785
+ } = useTranslations(), client = useClient(), [copied, setCopied] = React.useState(!1), [translations, setTranslations] = React.useState(
2786
+ EMPTY_TRANSLATION_SUMMARY
2787
+ ), { projectId, dataset } = client.config(), serialized = React.useMemo(
2788
+ () => formatDebugInfo(
2789
+ buildDebugInfo({
2790
+ secrets,
2791
+ preferences: {
2792
+ autoRefresh,
2793
+ autoImport,
2794
+ autoPatchReferences,
2795
+ autoPublish,
2796
+ preserveExistingTranslations
2797
+ },
2798
+ translations,
2799
+ sanityProjectId: projectId,
2800
+ sanityDataset: dataset,
2801
+ branchId
2802
+ })
2803
+ ),
2804
+ [
2805
+ secrets,
2806
+ translations,
2807
+ branchId,
2808
+ projectId,
2809
+ dataset,
2810
+ autoRefresh,
2811
+ autoImport,
2812
+ autoPatchReferences,
2813
+ autoPublish,
2814
+ preserveExistingTranslations
2815
+ ]
2816
+ );
2817
+ return React.useEffect(() => {
2818
+ if (!isOpen) return;
2819
+ let cancelled = !1;
2820
+ return collectTranslationSummary(documents, client).then((summary) => {
2821
+ cancelled || setTranslations(summary);
2822
+ }).catch(() => {
2823
+ cancelled || setTranslations(EMPTY_TRANSLATION_SUMMARY);
2824
+ }), () => {
2825
+ cancelled = !0;
2826
+ };
2827
+ }, [isOpen, documents, client]), isOpen ? /* @__PURE__ */ jsxRuntime.jsx(
2828
+ ui.Dialog,
2570
2829
  {
2571
- radius: 2,
2572
- overflow: "hidden",
2573
- style: { width: "100%", position: "relative" },
2574
- children: [
2575
- /* @__PURE__ */ jsxRuntime.jsx(
2576
- ui.Card,
2577
- {
2578
- tone: "positive",
2579
- style: {
2580
- position: "absolute",
2581
- inset: 0,
2582
- transform: `scaleX(${progress / 100})`,
2583
- transformOrigin: "left",
2584
- transition: "transform .2s ease"
2585
- }
2586
- }
2587
- ),
2830
+ header: "Debug info",
2831
+ id: "gt-debug-info-dialog",
2832
+ onClose,
2833
+ width: 2,
2834
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, justify: "space-between", align: "center", children: [
2588
2835
  /* @__PURE__ */ jsxRuntime.jsx(
2589
- "div",
2836
+ ui.Button,
2590
2837
  {
2591
- style: {
2592
- position: "absolute",
2593
- inset: 0,
2594
- borderRadius: "inherit",
2595
- boxShadow: "inset 0 0 0 1px var(--card-border-color)",
2596
- pointerEvents: "none",
2597
- zIndex: 2
2838
+ icon: icons.ClipboardIcon,
2839
+ text: copied ? "Copied" : "Copy",
2840
+ tone: copied ? "positive" : "default",
2841
+ mode: "ghost",
2842
+ onClick: async () => {
2843
+ try {
2844
+ await navigator.clipboard.writeText(serialized), setCopied(!0);
2845
+ } catch {
2846
+ setCopied(!1);
2847
+ }
2598
2848
  }
2599
2849
  }
2600
2850
  ),
2601
- /* @__PURE__ */ jsxRuntime.jsx(
2602
- ui.Flex,
2603
- {
2604
- align: "center",
2605
- justify: "center",
2606
- padding: 2,
2607
- style: { position: "relative", zIndex: 1 },
2608
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, weight: "medium", children: [
2609
- progress,
2610
- "%"
2611
- ] })
2612
- }
2613
- )
2614
- ]
2851
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Close", mode: "ghost", onClick: onClose })
2852
+ ] }) }),
2853
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
2854
+ ui.Card,
2855
+ {
2856
+ padding: 3,
2857
+ radius: 2,
2858
+ tone: "transparent",
2859
+ border: !0,
2860
+ overflow: "auto",
2861
+ style: { maxHeight: "50vh" },
2862
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 0, language: "json", children: serialized })
2863
+ }
2864
+ ) }) })
2615
2865
  }
2616
- );
2617
- }
2618
- const LocaleLabel = ({
2866
+ ) : null;
2867
+ }, LocaleLabel = ({
2619
2868
  localeId,
2620
2869
  size = 1,
2621
2870
  weight = "medium"
@@ -2626,24 +2875,31 @@ const LocaleLabel = ({
2626
2875
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size, weight, children: name }),
2627
2876
  !verbatim && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { fontSize: 0, radius: 2, children: code })
2628
2877
  ] });
2878
+ }, STATE_LABEL = {
2879
+ "not-translated": "Not translated",
2880
+ translating: "Translating\u2026",
2881
+ ready: "Ready to import",
2882
+ imported: "Imported"
2629
2883
  }, LanguageStatus = ({
2630
2884
  localeId,
2631
- progress,
2632
- importFile,
2633
- isImported = !1
2885
+ state,
2886
+ importFile
2634
2887
  }) => {
2635
- const [isBusy, setIsBusy] = React.useState(!1), displayedProgress = isImported && progress < 100 ? 100 : progress, handleImport = React.useCallback(async () => {
2888
+ const [isBusy, setIsBusy] = React.useState(!1), handleImport = React.useCallback(async () => {
2636
2889
  setIsBusy(!0);
2637
2890
  try {
2638
2891
  await importFile();
2639
2892
  } finally {
2640
2893
  setIsBusy(!1);
2641
2894
  }
2642
- }, [importFile, setIsBusy]);
2895
+ }, [importFile]);
2643
2896
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { columns: 5, gap: 3, paddingX: 3, paddingY: 2, children: [
2644
2897
  /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { columnStart: 1, columnEnd: 3, align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(LocaleLabel, { localeId }) }),
2645
- typeof displayedProgress == "number" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { columnStart: 3, columnEnd: 5, align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ProgressBar, { progress: displayedProgress }) }) : null,
2646
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { columnStart: 5, columnEnd: 6, align: "center", justify: "flex-end", children: isImported ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", fontSize: 0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 1, children: [
2898
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { columnStart: 3, columnEnd: 5, align: "center", gap: 2, children: [
2899
+ state === "translating" && /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { size: 1, muted: !0 }),
2900
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: state !== "ready", children: STATE_LABEL[state] })
2901
+ ] }),
2902
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { columnStart: 5, columnEnd: 6, align: "center", justify: "flex-end", children: state === "imported" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "positive", fontSize: 0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 1, children: [
2647
2903
  /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, {}),
2648
2904
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: "Imported" })
2649
2905
  ] }) }) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -2657,11 +2913,19 @@ const LocaleLabel = ({
2657
2913
  text: "Import",
2658
2914
  loading: isBusy,
2659
2915
  icon: icons.DownloadIcon,
2660
- disabled: isBusy || !progress || progress < 100
2916
+ disabled: isBusy || state !== "ready"
2661
2917
  }
2662
2918
  ) })
2663
2919
  ] });
2664
- }, LocaleCheckbox = ({
2920
+ };
2921
+ function resolveLanguageStatusState({
2922
+ status,
2923
+ isImported,
2924
+ isPending
2925
+ }) {
2926
+ return isImported ? "imported" : status?.isReady ? "ready" : isPending ? "translating" : "not-translated";
2927
+ }
2928
+ const LocaleCheckbox = ({
2665
2929
  locale,
2666
2930
  toggle,
2667
2931
  checked
@@ -2695,6 +2959,7 @@ const LocaleLabel = ({
2695
2959
  documents,
2696
2960
  locales,
2697
2961
  translationStatuses,
2962
+ pendingTranslations,
2698
2963
  branchId,
2699
2964
  isBusy,
2700
2965
  handleTranslateAll,
@@ -2717,7 +2982,7 @@ const LocaleLabel = ({
2717
2982
  preserveExistingTranslations,
2718
2983
  setPreserveExistingTranslations,
2719
2984
  getVersionId
2720
- } = useTranslations(), [isImporting, setIsImporting] = React.useState(!1), [isPublishing, setIsPublishing] = React.useState(!1), [isUploadingExisting, setIsUploadingExisting] = React.useState(!1), [isSaveLocalDialogOpen, setIsSaveLocalDialogOpen] = React.useState(!1), toast = ui.useToast(), document2 = documents[0], currentDocumentLanguage = React.useMemo(() => {
2985
+ } = useTranslations(), [isImporting, setIsImporting] = React.useState(!1), [isPublishing, setIsPublishing] = React.useState(!1), [isUploadingExisting, setIsUploadingExisting] = React.useState(!1), [isSaveLocalDialogOpen, setIsSaveLocalDialogOpen] = React.useState(!1), [isDebugInfoDialogOpen, setIsDebugInfoDialogOpen] = React.useState(!1), toast = ui.useToast(), document2 = documents[0], currentDocumentLanguage = React.useMemo(() => {
2721
2986
  if (!document2) return null;
2722
2987
  const languageField = pluginConfig.getLanguageField();
2723
2988
  return document2[languageField] || pluginConfig.getSourceLocale();
@@ -2729,7 +2994,21 @@ const LocaleLabel = ({
2729
2994
  }, [locales]), statusLocales = React.useMemo(() => {
2730
2995
  const sourceLocale = pluginConfig.getSourceLocale();
2731
2996
  return locales.filter((locale) => locale.localeId !== sourceLocale);
2732
- }, [locales]), documentId = React.useMemo(() => document2 ? getDocumentPublishedId(document2) : null, [document2]), versionId = React.useMemo(() => document2 ? getVersionId(document2) : null, [document2, getVersionId]), handleImportTranslations = React.useCallback(
2997
+ }, [locales]), documentId = React.useMemo(() => document2 ? getDocumentPublishedId(document2) : null, [document2]), versionId = React.useMemo(() => document2 ? getVersionId(document2) : null, [document2, getVersionId]), alreadyCompleteRef = React.useRef(null), statusKeyFor = React.useCallback(
2998
+ (localeId) => createTranslationStatusKey(
2999
+ branchId,
3000
+ documentId ?? "",
3001
+ versionId ?? "",
3002
+ localeId
3003
+ ),
3004
+ [branchId, documentId, versionId]
3005
+ );
3006
+ alreadyCompleteRef.current === null && documentId && versionId && translationStatuses.size > 0 && (alreadyCompleteRef.current = new Set(
3007
+ statusLocales.map((locale) => statusKeyFor(locale.localeId)).filter((key) => translationStatuses.get(key)?.isReady)
3008
+ ));
3009
+ const isWaitingOnTranslations = statusLocales.some(
3010
+ (locale) => pendingTranslations.has(statusKeyFor(locale.localeId))
3011
+ ), handleImportTranslations = React.useCallback(
2733
3012
  async (options = {}) => {
2734
3013
  const { autoOnly = !1 } = options;
2735
3014
  if (isImporting || !documentId || !versionId || autoOnly && !autoImport) return;
@@ -2740,7 +3019,7 @@ const LocaleLabel = ({
2740
3019
  versionId,
2741
3020
  locale.localeId
2742
3021
  );
2743
- return translationStatuses.get(key)?.isReady && !importedTranslations.has(key);
3022
+ return !translationStatuses.get(key)?.isReady || importedTranslations.has(key) ? !1 : !(autoOnly && alreadyCompleteRef.current?.has(key));
2744
3023
  });
2745
3024
  if (readyTranslations.length !== 0) {
2746
3025
  setIsImporting(!0);
@@ -2771,9 +3050,7 @@ const LocaleLabel = ({
2771
3050
  );
2772
3051
  React.useEffect(() => {
2773
3052
  handleImportTranslations({ autoOnly: !0 });
2774
- }, [handleImportTranslations]), React.useEffect(() => {
2775
- setAutoRefresh(!0), setAutoPatchReferences(!0), setAutoPublish(!0);
2776
- }, [setAutoRefresh, setAutoPatchReferences, setAutoPublish]);
3053
+ }, [handleImportTranslations]);
2777
3054
  const toggleLocale = React.useCallback(
2778
3055
  (localeId, shouldEnable) => {
2779
3056
  const updatedLocales = locales.map(
@@ -2825,12 +3102,12 @@ const LocaleLabel = ({
2825
3102
  ui.Button,
2826
3103
  {
2827
3104
  onClick: () => {
2828
- setAutoImport(!0), handleTranslateAll();
3105
+ alreadyCompleteRef.current = /* @__PURE__ */ new Set(), handleTranslateAll();
2829
3106
  },
2830
- disabled: isBusy || !availableLocales.length,
3107
+ disabled: isBusy || isWaitingOnTranslations || !availableLocales.length,
2831
3108
  icon: icons.TranslateIcon,
2832
- text: "Translate",
2833
- loading: isBusy && !isUploadingExisting
3109
+ text: isWaitingOnTranslations ? "Translating\u2026" : "Translate",
3110
+ loading: isBusy && !isUploadingExisting || isWaitingOnTranslations
2834
3111
  }
2835
3112
  )
2836
3113
  ] }),
@@ -2867,7 +3144,7 @@ const LocaleLabel = ({
2867
3144
  icon: icons.RefreshIcon,
2868
3145
  text: "Refresh",
2869
3146
  loading: isRefreshing,
2870
- onClick: handleRefreshAll,
3147
+ onClick: () => handleRefreshAll(),
2871
3148
  disabled: isRefreshing || isBusy
2872
3149
  }
2873
3150
  )
@@ -2879,13 +3156,16 @@ const LocaleLabel = ({
2879
3156
  documentId,
2880
3157
  versionId,
2881
3158
  locale.localeId
2882
- ), status = translationStatuses.get(key), progress = status?.progress || 0, isImported = importedTranslations.has(key);
3159
+ ), status = translationStatuses.get(key), isImported = importedTranslations.has(key);
2883
3160
  return /* @__PURE__ */ jsxRuntime.jsx(
2884
3161
  LanguageStatus,
2885
3162
  {
2886
3163
  localeId: locale.localeId,
2887
- progress,
2888
- isImported,
3164
+ state: resolveLanguageStatusState({
3165
+ status,
3166
+ isImported,
3167
+ isPending: pendingTranslations.has(key)
3168
+ }),
2889
3169
  importFile: async () => {
2890
3170
  !isImported && status?.isReady && await handleImportDocument(
2891
3171
  documentId,
@@ -2966,27 +3246,13 @@ const LocaleLabel = ({
2966
3246
  ] })
2967
3247
  ] }),
2968
3248
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
2969
- "Imported",
3249
+ statusLocales.filter(
3250
+ (locale) => importedTranslations.has(statusKeyFor(locale.localeId))
3251
+ ).length,
2970
3252
  " ",
2971
- statusLocales.filter((locale) => {
2972
- const key = createTranslationStatusKey(
2973
- branchId,
2974
- documentId,
2975
- versionId,
2976
- locale.localeId
2977
- );
2978
- return importedTranslations.has(key);
2979
- }).length,
2980
- "/",
2981
- statusLocales.filter((locale) => {
2982
- const key = createTranslationStatusKey(
2983
- branchId,
2984
- documentId,
2985
- versionId,
2986
- locale.localeId
2987
- );
2988
- return translationStatuses.get(key)?.isReady;
2989
- }).length
3253
+ "of ",
3254
+ statusLocales.length,
3255
+ " imported"
2990
3256
  ] })
2991
3257
  ] }),
2992
3258
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", justify: "flex-start", children: [
@@ -3063,7 +3329,30 @@ const LocaleLabel = ({
3063
3329
  ] })
3064
3330
  ] })
3065
3331
  ] })
3066
- ] })
3332
+ ] }),
3333
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { borderTop: !0, paddingTop: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "flex-end", gap: 2, children: [
3334
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
3335
+ "gt-sanity v",
3336
+ version
3337
+ ] }),
3338
+ /* @__PURE__ */ jsxRuntime.jsx(
3339
+ ui.Button,
3340
+ {
3341
+ fontSize: 1,
3342
+ padding: 2,
3343
+ mode: "bleed",
3344
+ text: "Debug info",
3345
+ onClick: () => setIsDebugInfoDialogOpen(!0)
3346
+ }
3347
+ )
3348
+ ] }) }),
3349
+ /* @__PURE__ */ jsxRuntime.jsx(
3350
+ DebugInfoDialog,
3351
+ {
3352
+ isOpen: isDebugInfoDialogOpen,
3353
+ onClose: () => setIsDebugInfoDialogOpen(!1)
3354
+ }
3355
+ )
3067
3356
  ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, tone: "neutral", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
3068
3357
  "Translation tools are only available for",
3069
3358
  " ",
@@ -3125,6 +3414,7 @@ const DocumentRow = ({ document: document2 }) => {
3125
3414
  const {
3126
3415
  locales,
3127
3416
  translationStatuses,
3417
+ pendingTranslations,
3128
3418
  downloadStatus,
3129
3419
  importedTranslations,
3130
3420
  handleImportDocument,
@@ -3163,8 +3453,11 @@ const DocumentRow = ({ document: document2 }) => {
3163
3453
  LanguageStatus,
3164
3454
  {
3165
3455
  localeId: locale.localeId,
3166
- progress: status?.progress || 0,
3167
- isImported: isImported || isDownloaded,
3456
+ state: resolveLanguageStatusState({
3457
+ status,
3458
+ isImported: isImported || isDownloaded,
3459
+ isPending: pendingTranslations.has(key)
3460
+ }),
3168
3461
  importFile: async () => {
3169
3462
  await handleImportDocument(
3170
3463
  publishedId,
@@ -3337,7 +3630,7 @@ const DocumentRow = ({ document: document2 }) => {
3337
3630
  "..."
3338
3631
  ] })
3339
3632
  ] }) : null, TranslationsToolContent = () => {
3340
- const [isTranslateAllDialogOpen, setIsTranslateAllDialogOpen] = React.useState(!1), [isImportAllDialogOpen, setIsImportAllDialogOpen] = React.useState(!1), [isImportMissingDialogOpen, setIsImportMissingDialogOpen] = React.useState(!1), [isUploadExistingDialogOpen, setIsUploadExistingDialogOpen] = React.useState(!1), [isSaveLocalDialogOpen, setIsSaveLocalDialogOpen] = React.useState(!1), {
3633
+ const [isTranslateAllDialogOpen, setIsTranslateAllDialogOpen] = React.useState(!1), [isImportAllDialogOpen, setIsImportAllDialogOpen] = React.useState(!1), [isImportMissingDialogOpen, setIsImportMissingDialogOpen] = React.useState(!1), [isUploadExistingDialogOpen, setIsUploadExistingDialogOpen] = React.useState(!1), [isSaveLocalDialogOpen, setIsSaveLocalDialogOpen] = React.useState(!1), [isDebugInfoDialogOpen, setIsDebugInfoDialogOpen] = React.useState(!1), {
3341
3634
  isBusy,
3342
3635
  documents,
3343
3636
  locales,
@@ -3427,7 +3720,7 @@ const DocumentRow = ({ document: document2 }) => {
3427
3720
  icon: icons.RefreshIcon,
3428
3721
  text: "Refresh",
3429
3722
  loading: isRefreshing,
3430
- onClick: handleRefreshAll,
3723
+ onClick: () => handleRefreshAll(),
3431
3724
  disabled: isRefreshing || actionsDisabled
3432
3725
  }
3433
3726
  )
@@ -3565,11 +3858,29 @@ const DocumentRow = ({ document: document2 }) => {
3565
3858
  }
3566
3859
  )
3567
3860
  ] }),
3568
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { borderTop: !0, paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
3569
- "For more information, see the",
3570
- " ",
3571
- /* @__PURE__ */ jsxRuntime.jsx(router.Link, { href: "https://dash.generaltranslation.com", children: "General Translation Dashboard" }),
3572
- "."
3861
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { borderTop: !0, paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", gap: 3, wrap: "wrap", children: [
3862
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
3863
+ "For more information, see the",
3864
+ " ",
3865
+ /* @__PURE__ */ jsxRuntime.jsx(router.Link, { href: "https://dash.generaltranslation.com", children: "General Translation Dashboard" }),
3866
+ "."
3867
+ ] }),
3868
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3869
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
3870
+ "gt-sanity v",
3871
+ version
3872
+ ] }),
3873
+ /* @__PURE__ */ jsxRuntime.jsx(
3874
+ ui.Button,
3875
+ {
3876
+ fontSize: 1,
3877
+ padding: 2,
3878
+ mode: "bleed",
3879
+ text: "Debug info",
3880
+ onClick: () => setIsDebugInfoDialogOpen(!0)
3881
+ }
3882
+ )
3883
+ ] })
3573
3884
  ] }) })
3574
3885
  ] }) }),
3575
3886
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3609,6 +3920,13 @@ const DocumentRow = ({ document: document2 }) => {
3609
3920
  isOpen: isUploadExistingDialogOpen,
3610
3921
  onClose: () => setIsUploadExistingDialogOpen(!1)
3611
3922
  }
3923
+ ),
3924
+ /* @__PURE__ */ jsxRuntime.jsx(
3925
+ DebugInfoDialog,
3926
+ {
3927
+ isOpen: isDebugInfoDialogOpen,
3928
+ onClose: () => setIsDebugInfoDialogOpen(!1)
3929
+ }
3612
3930
  )
3613
3931
  ] });
3614
3932
  }, TranslationsTool = () => /* @__PURE__ */ jsxRuntime.jsx(BaseTranslationWrapper, { showContainer: !1, children: /* @__PURE__ */ jsxRuntime.jsx(TranslationsProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(TranslationsToolContent, {}) }) });
@@ -3617,6 +3935,61 @@ function resolveTargetLocales(sourceLocale, locales) {
3617
3935
  (locale) => locale !== sourceLocale
3618
3936
  );
3619
3937
  }
3938
+ function documentLevelTypes() {
3939
+ const level = pluginConfig.getTranslationLevel(), types = pluginConfig.getTranslateDocuments().map((filter) => filter.type).filter((type) => !!type);
3940
+ if (level === "internationalizedArray") return [];
3941
+ if (level === "mixed") {
3942
+ const fieldLevel = new Set(
3943
+ pluginConfig.getFieldLevelDocuments().map((filter) => filter.type)
3944
+ );
3945
+ return types.filter((type) => !fieldLevel.has(type));
3946
+ }
3947
+ return types;
3948
+ }
3949
+ function localeLabel(locale) {
3950
+ const sourceLocale = pluginConfig.getSourceLocale();
3951
+ return formatLocalePropertiesLabel(
3952
+ locale,
3953
+ generaltranslation.getLocaleProperties(locale, sourceLocale)
3954
+ );
3955
+ }
3956
+ function typeTitle(type, context) {
3957
+ return context?.schema?.get(type)?.title ?? type;
3958
+ }
3959
+ function gtStructureItems(S, context, options = {}) {
3960
+ const types = options.types ?? documentLevelTypes();
3961
+ if (types.length === 0)
3962
+ return console.warn(
3963
+ internal.createDiagnosticMessage({
3964
+ source: "gt-sanity",
3965
+ severity: "Warning",
3966
+ whatHappened: "No document types to group by locale",
3967
+ why: "the structure helper groups per document type, and none were resolved from the plugin configuration",
3968
+ fix: "List the types in `translateDocuments`, or pass them to the structure helper as `types`"
3969
+ })
3970
+ ), [];
3971
+ const sourceLocale = pluginConfig.getSourceLocale(), languageField = pluginConfig.getLanguageField(), targetLocales = pluginConfig.getLocales(), titleFor = options.localeTitle ?? ((title, _locale, label) => `${title} in ${label}`);
3972
+ return types.map((type) => {
3973
+ const title = typeTitle(type, context), sourceFilter = `_type == $type && (!defined(${languageField}) || ${languageField} == $locale)`, localeFilter = `_type == $type && ${languageField} == $locale`;
3974
+ return S.listItem().id(`gt-${type}`).title(title).child(
3975
+ S.list().id(`gt-${type}-locales`).title(title).items([
3976
+ S.listItem().id(`gt-${type}-${sourceLocale}`).title(options.sourceTitle ?? localeLabel(sourceLocale)).child(
3977
+ S.documentList().id(`gt-${type}-${sourceLocale}-list`).title(options.sourceTitle ?? localeLabel(sourceLocale)).schemaType(type).filter(sourceFilter).params({ type, locale: sourceLocale })
3978
+ ),
3979
+ S.divider(),
3980
+ ...targetLocales.map((locale) => {
3981
+ const paneTitle = titleFor(title, locale, localeLabel(locale));
3982
+ return S.listItem().id(`gt-${type}-${locale}`).title(paneTitle).child(
3983
+ S.documentList().id(`gt-${type}-${locale}-list`).title(paneTitle).schemaType(type).filter(localeFilter).params({ type, locale })
3984
+ );
3985
+ })
3986
+ ])
3987
+ );
3988
+ });
3989
+ }
3990
+ function gtStructure(options = {}) {
3991
+ return (S, context) => S.list().title("Content").items(gtStructureItems(S, context, options));
3992
+ }
3620
3993
  const TranslationTab = (props) => {
3621
3994
  const { displayed } = props.document;
3622
3995
  return /* @__PURE__ */ jsxRuntime.jsx(BaseTranslationWrapper, { showContainer: !1, children: /* @__PURE__ */ jsxRuntime.jsx(TranslationsProvider, { singleDocument: displayed, children: /* @__PURE__ */ jsxRuntime.jsx(TranslationView, {}) }) });
@@ -3645,7 +4018,11 @@ const TranslationTab = (props) => {
3645
4018
  fieldLevelLocalization,
3646
4019
  translationLevel = "document",
3647
4020
  fieldLevelDocuments,
3648
- preserveExistingTranslations = !1
4021
+ preserveExistingTranslations = DEFAULT_TRANSLATION_PREFERENCES.preserveExistingTranslations,
4022
+ autoRefresh = DEFAULT_TRANSLATION_PREFERENCES.autoRefresh,
4023
+ autoImport = DEFAULT_TRANSLATION_PREFERENCES.autoImport,
4024
+ autoPatchReferences = DEFAULT_TRANSLATION_PREFERENCES.autoPatchReferences,
4025
+ autoPublish = DEFAULT_TRANSLATION_PREFERENCES.autoPublish
3649
4026
  }) => {
3650
4027
  const resolvedSourceLocale = sourceLocale ?? defaultLocale ?? internal.libraryDefaultLocale, targetLocales = resolveTargetLocales(resolvedSourceLocale, locales), normalizeFilters = (entries) => entries?.map((entry) => typeof entry == "string" ? { type: entry } : entry).filter((filter) => filter.documentId || filter.type), normalizedTranslateDocuments = normalizeFilters(translateDocuments), normalizedFieldLevelDocuments = normalizeFilters(fieldLevelDocuments) ?? [], rawFieldLevelConfig = internationalizedArray2 ?? fieldLevelLocalization;
3651
4028
  warnOnRemovedFieldLevelOptions(rawFieldLevelConfig);
@@ -3668,7 +4045,13 @@ const TranslationTab = (props) => {
3668
4045
  additionalBlockDeserializers,
3669
4046
  translationLevel,
3670
4047
  normalizedFieldLevelDocuments,
3671
- preserveExistingTranslations
4048
+ {
4049
+ autoRefresh,
4050
+ autoImport,
4051
+ autoPatchReferences,
4052
+ autoPublish,
4053
+ preserveExistingTranslations
4054
+ }
3672
4055
  ), gt.setConfig({
3673
4056
  sourceLocale: resolvedSourceLocale,
3674
4057
  customMapping,
@@ -3807,5 +4190,7 @@ exports.defaultStopTypes = defaultStopTypes;
3807
4190
  exports.detachGTData = detachGTData;
3808
4191
  exports.findLatestDraft = findLatestDraft;
3809
4192
  exports.gtPlugin = gtPlugin;
4193
+ exports.gtStructure = gtStructure;
4194
+ exports.gtStructureItems = gtStructureItems;
3810
4195
  exports.translateAction = translateAction;
3811
4196
  //# sourceMappingURL=index.cjs.map