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

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,18 @@ 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
+ if (!action.payload._id)
178
+ return devLog.warn("SET_TRANSLATION_JOB_STATUS action missing _id", {
179
+ ...logContext,
180
+ payload: action.payload
181
+ }), context;
182
+ const jobKey = `${undraftId(action.payload._id)}_${action.payload.language}`, newTranslationJobs = new Map(context.translationJobs), existingJob = newTranslationJobs.get(jobKey), mergedJob = defaults(action.payload, existingJob);
172
183
  return newTranslationJobs.set(jobKey, mergedJob), {
173
184
  ...context,
174
185
  translationJobs: newTranslationJobs
@@ -191,6 +202,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
191
202
  ...context,
192
203
  overallStatus: "idle",
193
204
  translationJobs: /* @__PURE__ */ new Map(),
205
+ selectedReferenceIds: /* @__PURE__ */ new Set(),
194
206
  // Clear any release conflicts from previous run
195
207
  releaseConfig: {
196
208
  ...context.releaseConfig,
@@ -218,6 +230,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
218
230
  shouldTranslateReferences: !1,
219
231
  maxReferenceDepth: 1,
220
232
  references: [],
233
+ selectedReferenceIds: /* @__PURE__ */ new Set(),
221
234
  // Restore jobs from active translation if available
222
235
  translationJobs: activeTranslation?.translationJobs ?? /* @__PURE__ */ new Map(),
223
236
  translationMode: config?.openai?.defaultMode || "sanity-only",
@@ -233,7 +246,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
233
246
  jobCount: activeTranslation.translationJobs.size,
234
247
  component: "TranslationContextProvider"
235
248
  });
236
- const [context, dispatch] = useReducer(translationReducer, initialContext);
249
+ const [context, dispatch] = useReducer(translationReducer, initialContext), lastProcessedRefsRef = useRef("");
237
250
  return useEffect(() => {
238
251
  const handleActiveTranslationUpdate = (state) => {
239
252
  dispatch({
@@ -273,7 +286,28 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
273
286
  });
274
287
  }
275
288
  })();
276
- }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), devLog.debug("Translation context state initialized", {
289
+ }, [config?.openai?.apiKey, config?.openai?.enabled, config?.openai?.model]), useEffect(() => {
290
+ const currentRefsKey = context.references.map((ref) => ref._id).sort().join(",");
291
+ if (currentRefsKey !== lastProcessedRefsRef.current) {
292
+ if (context.references.length > 0) {
293
+ const currentReferenceIds = new Set(context.references.map((ref) => ref._id)), previousReferenceIds = new Set(
294
+ lastProcessedRefsRef.current ? lastProcessedRefsRef.current.split(",") : []
295
+ ), hasNewReferences = Array.from(currentReferenceIds).some((id) => !previousReferenceIds.has(id)), currentSelection = context.selectedReferenceIds;
296
+ if (currentSelection.size === 0 || hasNewReferences) {
297
+ const newSelection = new Set(currentSelection);
298
+ currentReferenceIds.forEach((id) => {
299
+ newSelection.add(id);
300
+ });
301
+ const currentIds = Array.from(currentSelection).sort(), newIds = Array.from(newSelection).sort();
302
+ (currentIds.length !== newIds.length || !currentIds.every((id, i) => id === newIds[i])) && dispatch({
303
+ type: "SET_SELECTED_REFERENCE_IDS",
304
+ payload: newSelection
305
+ });
306
+ }
307
+ }
308
+ lastProcessedRefsRef.current = currentRefsKey;
309
+ }
310
+ }, [context.references, context.selectedReferenceIds]), devLog.debug("Translation context state initialized", {
277
311
  documentId: document._id,
278
312
  overallStatus: context.overallStatus,
279
313
  selectedLanguages: context.selectedTargetLanguages,
@@ -290,7 +324,7 @@ const isLoggingEnabled = () => process.env.NEXT_PUBLIC_SANITY_STUDIO_AI_TRANSLAT
290
324
  return context;
291
325
  }, getExistingTranslationId = (translationMetadata, targetLanguage) => translationMetadata?.translations?.length && translationMetadata.translations.find(
292
326
  (translation) => translation._key === targetLanguage
293
- )?.value._ref || null, undraftId = (id) => id.replace(/^drafts\./, ""), TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
327
+ )?.value._ref || null, TRANSLATION_METADATA_TYPE = "translation.metadata", getTranslationMetadata = async ({
294
328
  client,
295
329
  sourceDocument,
296
330
  sourceLanguage
@@ -2662,10 +2696,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2662
2696
  overallStatus,
2663
2697
  dispatch,
2664
2698
  references,
2699
+ selectedReferenceIds,
2665
2700
  selectedTargetLanguages,
2666
2701
  translationMode,
2667
2702
  releaseConfig
2668
- } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1);
2703
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [conflictContext, setConflictContext] = useState(null), [isCheckingConflicts, setIsCheckingConflicts] = useState(!1), filteredReferences = references.filter((ref) => selectedReferenceIds.has(ref._id));
2669
2704
  useEffect(() => {
2670
2705
  releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2671
2706
  }, [releaseConfig.mode, conflictContext]);
@@ -2678,7 +2713,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2678
2713
  dispatch,
2679
2714
  languageFieldName: config.languageFieldName,
2680
2715
  schemaId: config.schemaId,
2681
- references,
2716
+ references: filteredReferences,
2682
2717
  maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2683
2718
  maxReferenceDepth: config.maxReferenceDepth,
2684
2719
  translationMode,
@@ -2704,7 +2739,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2704
2739
  config.maxReferenceDepth,
2705
2740
  config.openai,
2706
2741
  config.excludeFields,
2707
- references,
2742
+ filteredReferences,
2708
2743
  translationMode,
2709
2744
  releaseConfig
2710
2745
  ]
@@ -2722,7 +2757,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2722
2757
  sourceDocument: document,
2723
2758
  targetLanguages: selectedTargetLanguages,
2724
2759
  languageFieldName: config.languageFieldName,
2725
- references
2760
+ references: filteredReferences
2726
2761
  });
2727
2762
  if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
2728
2763
  setConflictContext({
@@ -2733,7 +2768,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2733
2768
  }
2734
2769
  }
2735
2770
  startTranslation(!1);
2736
- }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
2771
+ }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
2737
2772
  setConflictContext(null);
2738
2773
  }, []), handleReplaceConflict = useCallback(() => {
2739
2774
  conflictContext && startTranslation(!0, conflictContext.releaseId);
@@ -3107,8 +3142,104 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3107
3142
  });
3108
3143
  }
3109
3144
  return result;
3145
+ }, ReferenceSelectionPanel = () => {
3146
+ const { references, selectedReferenceIds, dispatch } = useTranslationContext(), [typeFilter, setTypeFilter] = useState("all"), documentTypes = useMemo(() => {
3147
+ const types = new Set(references.map((ref) => ref._type));
3148
+ return Array.from(types).sort();
3149
+ }, [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) => {
3150
+ const newSelection = new Set(selectedReferenceIds);
3151
+ newSelection.has(referenceId) ? newSelection.delete(referenceId) : newSelection.add(referenceId), dispatch({
3152
+ type: "SET_SELECTED_REFERENCE_IDS",
3153
+ payload: newSelection
3154
+ });
3155
+ }, handleSelectAll = () => {
3156
+ const newSelection = new Set(selectedReferenceIds);
3157
+ filteredReferences.forEach((ref) => {
3158
+ newSelection.add(ref._id);
3159
+ }), dispatch({
3160
+ type: "SET_SELECTED_REFERENCE_IDS",
3161
+ payload: newSelection
3162
+ });
3163
+ }, handleSelectNone = () => {
3164
+ const newSelection = new Set(selectedReferenceIds);
3165
+ filteredReferences.forEach((ref) => {
3166
+ newSelection.delete(ref._id);
3167
+ }), dispatch({
3168
+ type: "SET_SELECTED_REFERENCE_IDS",
3169
+ payload: newSelection
3170
+ });
3171
+ };
3172
+ return references.length === 0 ? null : /* @__PURE__ */ jsx(Card, { padding: 4, border: !0, muted: !0, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
3173
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", gap: 2, children: [
3174
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
3175
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Select References" }),
3176
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
3177
+ "(",
3178
+ selectedCount,
3179
+ " of ",
3180
+ filteredReferences.length,
3181
+ " selected)"
3182
+ ] })
3183
+ ] }),
3184
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
3185
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectAll, text: "Select All" }),
3186
+ /* @__PURE__ */ jsx(Button, { mode: "ghost", size: 1, onClick: handleSelectNone, text: "Select None" })
3187
+ ] })
3188
+ ] }),
3189
+ documentTypes.length > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
3190
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Filter by type:" }),
3191
+ /* @__PURE__ */ jsxs(
3192
+ Select,
3193
+ {
3194
+ value: typeFilter,
3195
+ onChange: (event) => setTypeFilter(event.target.value),
3196
+ style: { minWidth: "150px" },
3197
+ children: [
3198
+ /* @__PURE__ */ jsxs("option", { value: "all", children: [
3199
+ "All types (",
3200
+ references.length,
3201
+ ")"
3202
+ ] }),
3203
+ documentTypes.map((type) => {
3204
+ const count = references.filter((ref) => ref._type === type).length;
3205
+ return /* @__PURE__ */ jsxs("option", { value: type, children: [
3206
+ type,
3207
+ " (",
3208
+ count,
3209
+ ")"
3210
+ ] }, type);
3211
+ })
3212
+ ]
3213
+ }
3214
+ )
3215
+ ] }),
3216
+ /* @__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) => {
3217
+ const isSelected = selectedReferenceIds.has(reference._id), displayTitle = reference.title || reference._id;
3218
+ return /* @__PURE__ */ jsx(Card, { padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
3219
+ /* @__PURE__ */ jsx(
3220
+ Checkbox,
3221
+ {
3222
+ checked: isSelected,
3223
+ onChange: () => handleToggleReference(reference._id)
3224
+ }
3225
+ ),
3226
+ /* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 2, children: [
3227
+ /* @__PURE__ */ jsx(Text, { size: 1, children: displayTitle }),
3228
+ /* @__PURE__ */ jsx(Badge, { tone: "primary", fontSize: 0, children: reference._type })
3229
+ ] })
3230
+ ] }) }, reference._id);
3231
+ }) }) })
3232
+ ] }) });
3110
3233
  }, ReferenceConfigPanel = () => {
3111
- const { dispatch, document, shouldTranslateReferences, maxReferenceDepth, config, references } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
3234
+ const {
3235
+ dispatch,
3236
+ document,
3237
+ shouldTranslateReferences,
3238
+ maxReferenceDepth,
3239
+ config,
3240
+ references,
3241
+ selectedReferenceIds
3242
+ } = useTranslationContext(), client = useClient({ apiVersion: "vX" }), [isLoadingReferences, setIsLoadingReferences] = useState(!1), [showSelectionPanel, setShowSelectionPanel] = useState(!1), maxDepth = config.maxReferenceDepth || 3;
3112
3243
  useEffect(() => {
3113
3244
  (async () => {
3114
3245
  if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
@@ -3178,7 +3309,24 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3178
3309
  )
3179
3310
  ] }) : null
3180
3311
  ] }),
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
3312
+ shouldTranslateReferences ? /* @__PURE__ */ jsxs(Fragment, { children: [
3313
+ /* @__PURE__ */ jsxs(Flex, { padding: 2, justify: "space-between", align: "center", children: [
3314
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: isLoadingReferences ? "Loading references..." : references.length === 0 ? "No translatable references found." : (() => {
3315
+ const selectedCount = references.filter((ref) => selectedReferenceIds.has(ref._id)).length, totalCount = references.length;
3316
+ 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.`;
3317
+ })() }),
3318
+ !isLoadingReferences && references.length > 0 && /* @__PURE__ */ jsx(
3319
+ Button,
3320
+ {
3321
+ mode: "ghost",
3322
+ size: 1,
3323
+ text: showSelectionPanel ? "Hide selection" : "Select specific references...",
3324
+ onClick: () => setShowSelectionPanel(!showSelectionPanel)
3325
+ }
3326
+ )
3327
+ ] }),
3328
+ !isLoadingReferences && references.length > 0 && showSelectionPanel && /* @__PURE__ */ jsx(ReferenceSelectionPanel, {})
3329
+ ] }) : null
3182
3330
  ] });
3183
3331
  }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3184
3332
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [