@braze/sanity-plugin-ai-translation 0.2.52 → 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
@@ -1253,7 +1282,9 @@ function restoreTransforms({
1253
1282
  schemaType,
1254
1283
  document: translatedDocument
1255
1284
  };
1256
- devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
1285
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType });
1286
+ const restoredValue = transform.fromTranslatable(value, originalValue, context);
1287
+ restoredObj[key] = restoredValue;
1257
1288
  } else
1258
1289
  restoredObj[key] = restoreTransforms({
1259
1290
  translatedDocument: value,
@@ -1491,9 +1522,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1491
1522
  }, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
1492
1523
  return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
1493
1524
  }, deserializeDocument = (originalDoc, reviewedText) => {
1494
- const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
1525
+ const parsed = parseReviewFormat(reviewedText, originalDoc), result = cloneDeep(originalDoc);
1495
1526
  return applyTextChanges(result, parsed), result;
1496
- }, parseReviewFormat = (text) => {
1527
+ }, parseReviewFormat = (text, originalDoc) => {
1497
1528
  const root = [], stack = [], lines = text.split(`
1498
1529
  `);
1499
1530
  for (let i = 0; i < lines.length; i++) {
@@ -1526,8 +1557,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1526
1557
  }
1527
1558
  const content = contentLines.join(`
1528
1559
  `);
1529
- let contentType = "translatable.vtt.captions";
1530
- (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
1560
+ let contentType;
1561
+ try {
1562
+ const fieldValue = get(originalDoc, [fieldName]);
1563
+ if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
1564
+ const fieldType = fieldValue._type;
1565
+ typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
1566
+ }
1567
+ } catch {
1568
+ }
1569
+ contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions");
1531
1570
  const node = {
1532
1571
  type: "field",
1533
1572
  name: fieldName,
@@ -1564,7 +1603,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1564
1603
  parent && parent.children.push(node);
1565
1604
  } else
1566
1605
  root.push(node);
1567
- else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
1606
+ else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1568
1607
  const contentLines = [];
1569
1608
  for (i++; i < lines.length; ) {
1570
1609
  const currentLine = lines[i];
@@ -1626,14 +1665,13 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1626
1665
  applyFieldChanges(doc, path, node);
1627
1666
  }
1628
1667
  }, applyFieldChanges = (doc, path, node) => {
1629
- if (node.attributes.type === "translatable.vtt.captions") {
1630
- const fieldValue = get(doc, path);
1631
- fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
1632
- return;
1633
- }
1634
- if (node.attributes.type === "translatable.richtext") {
1668
+ const nodeType = node.attributes.type;
1669
+ if (typeof nodeType == "string" && nodeType.startsWith("translatable.")) {
1635
1670
  const fieldValue = get(doc, path);
1636
- fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
1671
+ if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
1672
+ const fieldType = fieldValue._type;
1673
+ typeof fieldType == "string" && fieldType === nodeType && node.text && set(doc, [...path, "translationText"], node.text);
1674
+ }
1637
1675
  return;
1638
1676
  }
1639
1677
  if (node.attributes.type === "array") {
@@ -1721,12 +1759,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1721
1759
  return;
1722
1760
  }
1723
1761
  if (isPlainObject(value)) {
1724
- if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
1725
- lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1726
- return;
1727
- }
1728
- if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
1729
- lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1762
+ const valueObj = value, valueType = valueObj._type, translationText = valueObj.translationText;
1763
+ if (typeof valueType == "string" && valueType.startsWith("translatable.") && typeof translationText == "string") {
1764
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1730
1765
  return;
1731
1766
  }
1732
1767
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
@@ -2656,10 +2691,11 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2656
2691
  overallStatus,
2657
2692
  dispatch,
2658
2693
  references,
2694
+ selectedReferenceIds,
2659
2695
  selectedTargetLanguages,
2660
2696
  translationMode,
2661
2697
  releaseConfig
2662
- } = 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));
2663
2699
  useEffect(() => {
2664
2700
  releaseConfig.mode === "new" && conflictContext && setConflictContext(null);
2665
2701
  }, [releaseConfig.mode, conflictContext]);
@@ -2672,7 +2708,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2672
2708
  dispatch,
2673
2709
  languageFieldName: config.languageFieldName,
2674
2710
  schemaId: config.schemaId,
2675
- references,
2711
+ references: filteredReferences,
2676
2712
  maxConcurrentTranslationJobs: config.maxConcurrentTranslationJobs,
2677
2713
  maxReferenceDepth: config.maxReferenceDepth,
2678
2714
  translationMode,
@@ -2698,7 +2734,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2698
2734
  config.maxReferenceDepth,
2699
2735
  config.openai,
2700
2736
  config.excludeFields,
2701
- references,
2737
+ filteredReferences,
2702
2738
  translationMode,
2703
2739
  releaseConfig
2704
2740
  ]
@@ -2716,7 +2752,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2716
2752
  sourceDocument: document,
2717
2753
  targetLanguages: selectedTargetLanguages,
2718
2754
  languageFieldName: config.languageFieldName,
2719
- references
2755
+ references: filteredReferences
2720
2756
  });
2721
2757
  if (setIsCheckingConflicts(!1), foundConflicts.length > 0) {
2722
2758
  setConflictContext({
@@ -2727,7 +2763,7 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
2727
2763
  }
2728
2764
  }
2729
2765
  startTranslation(!1);
2730
- }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, references]), handleCancelConflict = useCallback(() => {
2766
+ }, [releaseConfig, client, document, selectedTargetLanguages, config.languageFieldName, startTranslation, filteredReferences]), handleCancelConflict = useCallback(() => {
2731
2767
  setConflictContext(null);
2732
2768
  }, []), handleReplaceConflict = useCallback(() => {
2733
2769
  conflictContext && startTranslation(!0, conflictContext.releaseId);
@@ -3101,8 +3137,104 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3101
3137
  });
3102
3138
  }
3103
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
+ ] }) });
3104
3228
  }, ReferenceConfigPanel = () => {
3105
- 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;
3106
3238
  useEffect(() => {
3107
3239
  (async () => {
3108
3240
  if (setIsLoadingReferences(!0), !shouldTranslateReferences) {
@@ -3172,7 +3304,24 @@ ${protectedPhrases.join(", ")}`, buildStyleGuideSection = (globalPrompt, localeP
3172
3304
  )
3173
3305
  ] }) : null
3174
3306
  ] }),
3175
- 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
3176
3325
  ] });
3177
3326
  }, WriteSettingsPanel = () => /* @__PURE__ */ jsx(ReferenceConfigPanel, {}), TranslationConfigView = ({ setIsDialogOpen }) => /* @__PURE__ */ jsxs(Stack, { space: 4, children: [
3178
3327
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
@@ -3984,7 +4133,14 @@ function parseSRTContent(content) {
3984
4133
  contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
3985
4134
  }), !content || content.trim() === "")
3986
4135
  return [];
3987
- const cues = [], cueBlocks = content.split(/\n\s*\n/);
4136
+ let normalizedContent = content;
4137
+ content.includes("\\n") && !content.includes(`
4138
+ `) && (normalizedContent = content.replace(/\\n/g, `
4139
+ `), devLog.debug("Unescaped newlines in SRT content", {
4140
+ originalLength: content.length,
4141
+ normalizedLength: normalizedContent.length
4142
+ }));
4143
+ const cues = [], cueBlocks = normalizedContent.split(/\n\s*\n/);
3988
4144
  for (const block of cueBlocks) {
3989
4145
  const lines = block.trim().split(`
3990
4146
  `);