@braze/sanity-plugin-ai-translation 0.2.53 → 0.2.54

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getPublishedId, pathToString, useClient, defineType, defineField, definePlugin } from "sanity";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import { WarningOutlineIcon, SearchIcon, CheckmarkIcon, ErrorOutlineIcon, CircleIcon, ArrowRightIcon, ResetIcon, PanelRightIcon, TranslateIcon } from "@sanity/icons";
4
- import { createContext, useReducer, useEffect, useContext, useMemo, useState, useCallback } from "react";
4
+ import { createContext, useReducer, useRef, useEffect, useContext, useMemo, useState, useCallback } from "react";
5
5
  import defaults from "lodash.defaults";
6
6
  import { Card, Stack, Flex, Text, Box, Heading, Button, Spinner, Inline, Checkbox, Grid, Radio, TextInput, Badge, Select, Label, Code, Tooltip } from "@sanity/ui";
7
7
  import { toPlainText } from "@portabletext/react";
@@ -62,7 +62,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
62
62
  error: error instanceof Error ? error.message : "Network error"
63
63
  };
64
64
  }
65
- }, activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
65
+ }, undraftId = (id) => id.replace(/^drafts\./, ""), activeTranslations = /* @__PURE__ */ new Map(), registerActiveTranslation = (documentId, releaseId) => {
66
66
  const entry = {
67
67
  state: {
68
68
  documentId,
@@ -80,7 +80,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
80
80
  }, updateActiveTranslationJob = (documentId, job) => {
81
81
  const entry = activeTranslations.get(documentId);
82
82
  if (entry) {
83
- const jobKey = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
83
+ const jobKey = `${undraftId(job._id)}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
84
84
  Object.entries(job).filter(([_, value]) => value !== void 0)
85
85
  ), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
86
86
  entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
@@ -114,6 +114,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
114
114
  shouldTranslateReferences: !1,
115
115
  maxReferenceDepth: 1,
116
116
  references: [],
117
+ selectedReferenceIds: /* @__PURE__ */ new Set(),
117
118
  translationMetadata: null,
118
119
  translationMode: "sanity-only",
119
120
  openaiValidated: !1,
@@ -167,8 +168,13 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
167
168
  ...context,
168
169
  references: action.payload
169
170
  };
171
+ case "SET_SELECTED_REFERENCE_IDS":
172
+ return {
173
+ ...context,
174
+ selectedReferenceIds: action.payload
175
+ };
170
176
  case "SET_TRANSLATION_JOB_STATUS": {
171
- const jobKey = `${action.payload._id}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
177
+ const jobKey = `${undraftId(action.payload._id)}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
172
178
  return newTranslationJobs.set(jobKey, mergedJob), {
173
179
  ...context,
174
180
  translationJobs: newTranslationJobs
@@ -191,6 +197,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
191
197
  ...context,
192
198
  overallStatus: "idle",
193
199
  translationJobs: /* @__PURE__ */ new Map(),
200
+ selectedReferenceIds: /* @__PURE__ */ new Set(),
194
201
  // Clear any release conflicts from previous run
195
202
  releaseConfig: {
196
203
  ...context.releaseConfig,
@@ -218,6 +225,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
218
225
  shouldTranslateReferences: !1,
219
226
  maxReferenceDepth: 1,
220
227
  references: [],
228
+ selectedReferenceIds: /* @__PURE__ */ new Set(),
221
229
  // Restore jobs from active translation if available
222
230
  translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
223
231
  translationMode: config?.openai?.defaultMode || "sanity-only",
@@ -233,7 +241,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
233
241
  jobCount: activeTranslation.translationJobs.size,
234
242
  component: "TranslationContextProvider"
235
243
  });
236
- const [context, dispatch] = useReducer(translationReducer, initialContext);
244
+ const [context, dispatch] = useReducer(translationReducer, initialContext), lastProcessedRefsRef = useRef("");
237
245
  return useEffect(() => {
238
246
  const handleActiveTranslationUpdate = (state) => {
239
247
  dispatch({
@@ -273,7 +281,28 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
273
281
  });
274
282
  }
275
283
  })();
276
- }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
284
+ }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
285
+ const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
286
+ if (currentRefsKey !== lastProcessedRefsRef.current) {
287
+ if (context.references.length > 0) {
288
+ const currentReferenceIds = new Set(context.references.map((ref) => ref._id)), previousReferenceIds = new Set(
289
+ lastProcessedRefsRef.current ? lastProcessedRefsRef.current.split(",") : []
290
+ ), hasNewReferences = Array.from(currentReferenceIds).some((id) => !previousReferenceIds.has(id)), currentSelection = context.selectedReferenceIds;
291
+ if (currentSelection.size === 0 || hasNewReferences) {
292
+ const newSelection = new Set(currentSelection);
293
+ currentReferenceIds.forEach((id) => {
294
+ newSelection.add(id);
295
+ });
296
+ const currentIds = Array.from(currentSelection).sort(), newIds = Array.from(newSelection).sort();
297
+ (currentIds.length !== newIds.length || !currentIds.every((id, i) => id === newIds[i])) && dispatch({
298
+ type: "SET_SELECTED_REFERENCE_IDS",
299
+ payload: newSelection
300
+ });
301
+ }
302
+ }
303
+ lastProcessedRefsRef.current = currentRefsKey;
304
+ }
305
+ }, [context.references, context.selectedReferenceIds]), devLog.debug("Translation context state initialized", {
277
306
  documentId: document._id,
278
307
  overallStatus: context.overallStatus,
279
308
  selectedLanguages: context.selectedTargetLanguages,
@@ -290,7 +319,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
290
319
  return context;
291
320
  }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
292
321
  (translation) => translation._key === targetLanguage
293
- )?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
322
+ )?.value._ref || null, TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
294
323
  client,
295
324
  sourceDocument,
296
325
  sourceLanguage
@@ -2662,10 +2691,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2662
2691
  overallStatus,
2663
2692
  dispatch,
2664
2693
  references,
2694
+ selectedReferenceIds,
2665
2695
  selectedTargetLanguages,
2666
2696
  translationMode,
2667
2697
  releaseConfig
2668
- } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
2698
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
2669
2699
  useEffect(() => {
2670
2700
  releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2671
2701
  }, [releaseConfig.mode, conflictContext]);
@@ -2678,7 +2708,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2678
2708
  dispatch,
2679
2709
  languageFieldName: config.languageFieldName,
2680
2710
  schemaId: config.schemaId,
2681
- references,
2711
+ references: filteredReferences,
2682
2712
  maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2683
2713
  maxReferenceDepth: config.maxReferenceDepth,
2684
2714
  translationMode,
@@ -2704,7 +2734,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2704
2734
  config.maxReferenceDepth,
2705
2735
  config.openai,
2706
2736
  config.excludeFields,
2707
- references,
2737
+ filteredReferences,
2708
2738
  translationMode,
2709
2739
  releaseConfig
2710
2740
  ]
@@ -2722,7 +2752,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2722
2752
  sourceDocument: document,
2723
2753
  targetLanguages: selectedTargetLanguages,
2724
2754
  languageFieldName: config.languageFieldName,
2725
- references
2755
+ references: filteredReferences
2726
2756
  });
2727
2757
  if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
2728
2758
  setConflictContext({
@@ -2733,7 +2763,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2733
2763
  }
2734
2764
  }
2735
2765
  startTranslation(!1);
2736
- }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
2766
+ }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
2737
2767
  setConflictContext(null);
2738
2768
  }, []), handleReplaceConflict = useCallback(() => {
2739
2769
  conflictContext && startTranslation(!0, conflictContext.releaseId);
@@ -3107,8 +3137,104 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3107
3137
  });
3108
3138
  }
3109
3139
  return result;
3140
+ }, ReferenceSelectionPanel = () => {
3141
+ const { references, selectedReferenceIds, dispatch } = useTranslationContext(), [typeFilter, setTypeFilter] = useState("all"), documentTypes = useMemo(() => {
3142
+ const types = new Set(references.map((ref) => ref._type));
3143
+ return Array.from(types).sort();
3144
+ }, [references]), filteredReferences = useMemo(() => typeFilter === "all" ? references : references.filter((ref) => ref._type === typeFilter), [references, typeFilter]), selectedCount = useMemo(() => filteredReferences.filter((ref) => selectedReferenceIds.has(ref._id)).length, [filteredReferences, selectedReferenceIds]), handleToggleReference = (referenceId) => {
3145
+ const newSelection = new Set(selectedReferenceIds);
3146
+ newSelection.has(referenceId) ? newSelection.delete(referenceId) : newSelection.add(referenceId), dispatch({
3147
+ type: "SET_SELECTED_REFERENCE_IDS",
3148
+ payload: newSelection
3149
+ });
3150
+ }, handleSelectAll = () => {
3151
+ const newSelection = new Set(selectedReferenceIds);
3152
+ filteredReferences.forEach((ref) => {
3153
+ newSelection.add(ref._id);
3154
+ }), dispatch({
3155
+ type: "SET_SELECTED_REFERENCE_IDS",
3156
+ payload: newSelection
3157
+ });
3158
+ }, handleSelectNone = () => {
3159
+ const newSelection = new Set(selectedReferenceIds);
3160
+ filteredReferences.forEach((ref) => {
3161
+ newSelection.delete(ref._id);
3162
+ }), dispatch({
3163
+ type: "SET_SELECTED_REFERENCE_IDS",
3164
+ payload: newSelection
3165
+ });
3166
+ };
3167
+ return references.length === 0 ? null : /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3168
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", gap: 2, children: [
3169
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
3170
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Select References" }),
3171
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3172
+ "(",
3173
+ selectedCount,
3174
+ " of ",
3175
+ filteredReferences.length,
3176
+ " selected)"
3177
+ ] })
3178
+ ] }),
3179
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
3180
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectAll, text: "Select All" }),
3181
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectNone, text: "Select None" })
3182
+ ] })
3183
+ ] }),
3184
+ documentTypes.length > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
3185
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Filter by type:" }),
3186
+ /* @__PURE__ */ jsxs(
3187
+ Select,
3188
+ {
3189
+ value: typeFilter,
3190
+ onChange: (event) => setTypeFilter(event.target.value),
3191
+ style: { minWidth: "150px" },
3192
+ children: [
3193
+ /* @__PURE__ */ jsxs("option", { value: "all", children: [
3194
+ "All types (",
3195
+ references.length,
3196
+ ")"
3197
+ ] }),
3198
+ documentTypes.map((type) => {
3199
+ const count = references.filter((ref) => ref._type === type).length;
3200
+ return /* @__PURE__ */ jsxs("option", { value: type, children: [
3201
+ type,
3202
+ " (",
3203
+ count,
3204
+ ")"
3205
+ ] }, type);
3206
+ })
3207
+ ]
3208
+ }
3209
+ )
3210
+ ] }),
3211
+ /* @__PURE__ */ jsx(Box, { style: { maxHeight: "400px", overflowY: "auto" }, children: /* @__PURE__ */ jsx(Stack, { space: 2, children: filteredReferences.length === 0 ? /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "No references match the selected filter." }) : filteredReferences.map((reference) => {
3212
+ const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
3213
+ return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
3214
+ /* @__PURE__ */ jsx(
3215
+ Checkbox,
3216
+ {
3217
+ checked: isSelected,
3218
+ onChange: () => handleToggleReference(reference._id)
3219
+ }
3220
+ ),
3221
+ /* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
3222
+ /* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
3223
+ /* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
3224
+ ] })
3225
+ ] }) }, reference._id);
3226
+ }) }) })
3227
+ ] }) });
3110
3228
  }, ReferenceConfigPanel = () => {
3111
- const { dispatch, document, shouldTranslateReferences, maxReferenceDepth, config, references } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
3229
+ const {
3230
+ dispatch,
3231
+ document,
3232
+ shouldTranslateReferences,
3233
+ maxReferenceDepth,
3234
+ config,
3235
+ references,
3236
+ selectedReferenceIds
3237
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
3112
3238
  useEffect(() => {
3113
3239
  (async () => {
3114
3240
  if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
@@ -3178,7 +3304,24 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3178
3304
  )
3179
3305
  ] }) : null
3180
3306
  ] }),
3181
- shouldTranslateReferences ? /* @__PURE__ */ jsx(Flex, { padding: 2, justify: "flex-end", children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : `${references.length} translatable references found.` }) }) : null
3307
+ shouldTranslateReferences ? /* @__PURE__ */ jsxs(Fragment, { children: [
3308
+ /* @__PURE__ */ jsxs(Flex, { padding: 2, justify: "space-between", align: "center", children: [
3309
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : (() => {
3310
+ const selectedCount = references.filter((ref) => selectedReferenceIds.has(ref._id)).length, totalCount = references.length;
3311
+ return selectedCount === totalCount ? `${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.` : `${selectedCount} of ${totalCount} reference${totalCount === 1 ? "" : "s"} will be translated along with this document.`;
3312
+ })() }),
3313
+ !isLoadingReferences && references.length > 0 && /* @__PURE__ */ jsx(
3314
+ Button,
3315
+ {
3316
+ mode: "ghost",
3317
+ size: 1,
3318
+ text: showSelectionPanel ? "Hide selection" : "Select specific references...",
3319
+ onClick: () => setShowSelectionPanel(!showSelectionPanel)
3320
+ }
3321
+ )
3322
+ ] }),
3323
+ !isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
3324
+ ] }) : null
3182
3325
  ] });
3183
3326
  }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3184
3327
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [