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

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.d.ts CHANGED
@@ -446,6 +446,21 @@ declare interface TranslatableVTTCaptions {
446
446
  _originalMetadata: VTTCaptionMetadata[];
447
447
  }
448
448
 
449
+ /**
450
+ * Special type to track transformed Wistia captions.
451
+ * This combines metadata separation for token efficiency with compact text format for translation.
452
+ */
453
+ declare interface TranslatableWistiaCaptions {
454
+ _type: "translatable.wistia.captions";
455
+ translationText: string;
456
+ _originalMetadata: WistiaCaptionsMetadata;
457
+ _srtCues: Array<{
458
+ index: number;
459
+ startTime: string;
460
+ endTime: string;
461
+ }>;
462
+ }
463
+
449
464
  /**
450
465
  * Schema for translation history
451
466
  *
@@ -497,4 +512,40 @@ export declare const vttCaptionTransform: FieldTransform<
497
512
  TranslatableVTTCaptions
498
513
  >;
499
514
 
515
+ /**
516
+ * Represents a Wistia captions object with SRT content and metadata.
517
+ */
518
+ declare interface WistiaCaptions {
519
+ captionContent?: string;
520
+ captionFileName?: string;
521
+ isoLanguage?: string;
522
+ captionUploadStatus?: string;
523
+ captionUploadMessage?: string;
524
+ [key: string]: string | undefined;
525
+ }
526
+
527
+ /**
528
+ * Wistia captions metadata without captionContent, used for preservation during translation.
529
+ */
530
+ declare interface WistiaCaptionsMetadata {
531
+ captionFileName?: string;
532
+ isoLanguage?: string;
533
+ captionUploadStatus?: string;
534
+ captionUploadMessage?: string;
535
+ _originalIndex: number;
536
+ [key: string]: string | number | undefined;
537
+ }
538
+
539
+ /**
540
+ * Transform for handling Wistia captions in documents using SRT format.
541
+ * This stores metadata separately for token efficiency while formatting
542
+ * text content in a compact indexed format for better translation results.
543
+ *
544
+ * @public
545
+ */
546
+ export declare const wistiaCaptionTransform: FieldTransform<
547
+ WistiaCaptions,
548
+ TranslatableWistiaCaptions
549
+ >;
550
+
500
551
  export {};
package/dist/index.js CHANGED
@@ -80,7 +80,9 @@ 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), mergedJob = existingJob ? { ...existingJob, ...job } : job;
83
+ const jobKey = `${job._id}_${job.language}`, existingJob = entry.state.translationJobs.get(jobKey), jobWithDefinedValues = Object.fromEntries(
84
+ Object.entries(job).filter(([_, value]) => value !== void 0)
85
+ ), mergedJob = existingJob ? { ...existingJob, ...jobWithDefinedValues } : jobWithDefinedValues;
84
86
  entry.state.translationJobs.set(jobKey, mergedJob), notifySubscribers(entry);
85
87
  }
86
88
  }, getActiveTranslation = (documentId) => activeTranslations.get(documentId)?.state ?? null, subscribeToActiveTranslation = (documentId, callback) => {
@@ -1251,7 +1253,9 @@ function restoreTransforms({
1251
1253
  schemaType,
1252
1254
  document: translatedDocument
1253
1255
  };
1254
- devLog.debug("Restoring transform for field", { fieldPath, schemaType }), restoredObj[key] = transform.fromTranslatable(value, originalValue, context);
1256
+ devLog.debug("Restoring transform for field", { fieldPath, schemaType });
1257
+ const restoredValue = transform.fromTranslatable(value, originalValue, context);
1258
+ restoredObj[key] = restoredValue;
1255
1259
  } else
1256
1260
  restoredObj[key] = restoreTransforms({
1257
1261
  translatedDocument: value,
@@ -1489,9 +1493,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1489
1493
  }, modelPricing = pricing[model] || pricing["gpt-5"], cachedInputRate = model === "gpt-5.1" ? 0.125 / 1e6 : modelPricing.input;
1490
1494
  return Math.max(0, tokens.prompt - tokens.cached) * modelPricing.input + tokens.cached * cachedInputRate + tokens.completion * modelPricing.output + tokens.reasoning * modelPricing.reasoning;
1491
1495
  }, deserializeDocument = (originalDoc, reviewedText) => {
1492
- const parsed = parseReviewFormat(reviewedText), result = cloneDeep(originalDoc);
1496
+ const parsed = parseReviewFormat(reviewedText, originalDoc), result = cloneDeep(originalDoc);
1493
1497
  return applyTextChanges(result, parsed), result;
1494
- }, parseReviewFormat = (text) => {
1498
+ }, parseReviewFormat = (text, originalDoc) => {
1495
1499
  const root = [], stack = [], lines = text.split(`
1496
1500
  `);
1497
1501
  for (let i = 0; i < lines.length; i++) {
@@ -1524,8 +1528,16 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1524
1528
  }
1525
1529
  const content = contentLines.join(`
1526
1530
  `);
1527
- let contentType = "translatable.vtt.captions";
1528
- (content.includes("<block") || content.includes("<span")) && (contentType = "translatable.richtext");
1531
+ let contentType;
1532
+ try {
1533
+ const fieldValue = get(originalDoc, [fieldName]);
1534
+ if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
1535
+ const fieldType = fieldValue._type;
1536
+ typeof fieldType == "string" && fieldType.startsWith("translatable.") && (contentType = fieldType);
1537
+ }
1538
+ } catch {
1539
+ }
1540
+ contentType || (content.includes("<block") || content.includes("<span") ? contentType = "translatable.richtext" : contentType = "translatable.vtt.captions");
1529
1541
  const node = {
1530
1542
  type: "field",
1531
1543
  name: fieldName,
@@ -1562,7 +1574,7 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1562
1574
  parent && parent.children.push(node);
1563
1575
  } else
1564
1576
  root.push(node);
1565
- else if (attributes.type === "translatable.vtt.captions" || attributes.type === "translatable.richtext") {
1577
+ else if (attributes.type && typeof attributes.type == "string" && attributes.type.startsWith("translatable.")) {
1566
1578
  const contentLines = [];
1567
1579
  for (i++; i < lines.length; ) {
1568
1580
  const currentLine = lines[i];
@@ -1624,14 +1636,13 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1624
1636
  applyFieldChanges(doc, path, node);
1625
1637
  }
1626
1638
  }, applyFieldChanges = (doc, path, node) => {
1627
- if (node.attributes.type === "translatable.vtt.captions") {
1628
- const fieldValue = get(doc, path);
1629
- fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.vtt.captions" && node.text && set(doc, [...path, "translationText"], node.text);
1630
- return;
1631
- }
1632
- if (node.attributes.type === "translatable.richtext") {
1639
+ const nodeType = node.attributes.type;
1640
+ if (typeof nodeType == "string" && nodeType.startsWith("translatable.")) {
1633
1641
  const fieldValue = get(doc, path);
1634
- fieldValue && typeof fieldValue == "object" && fieldValue._type === "translatable.richtext" && node.text && set(doc, [...path, "translationText"], node.text);
1642
+ if (fieldValue && typeof fieldValue == "object" && fieldValue !== null) {
1643
+ const fieldType = fieldValue._type;
1644
+ typeof fieldType == "string" && fieldType === nodeType && node.text && set(doc, [...path, "translationText"], node.text);
1645
+ }
1635
1646
  return;
1636
1647
  }
1637
1648
  if (node.attributes.type === "array") {
@@ -1719,12 +1730,9 @@ const globalTransformRegistry = new TransformRegistry(), callGPT5 = async (apiKe
1719
1730
  return;
1720
1731
  }
1721
1732
  if (isPlainObject(value)) {
1722
- if (value._type === "translatable.vtt.captions" && typeof value.translationText == "string") {
1723
- lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1724
- return;
1725
- }
1726
- if (value._type === "translatable.richtext" && typeof value.translationText == "string") {
1727
- lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(value.translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1733
+ const valueObj = value, valueType = valueObj._type, translationText = valueObj.translationText;
1734
+ if (typeof valueType == "string" && valueType.startsWith("translatable.") && typeof translationText == "string") {
1735
+ lines.push(`${indentStr}--- ${fieldName} ---`), lines.push(translationText), lines.push(`${indentStr}--- /${fieldName} ---`);
1728
1736
  return;
1729
1737
  }
1730
1738
  lines.push(`${indentStr}<field name="${fieldName}" type="object">`);
@@ -3799,6 +3807,284 @@ function deserializeVTTCaptionText(translationText, originalMetadata) {
3799
3807
  resultExample: result.length > 0 ? result[0] : null
3800
3808
  }), result;
3801
3809
  }
3810
+ const LOCALE_TO_ISO = {
3811
+ // English variants
3812
+ "en-us": "eng",
3813
+ "en-US": "eng",
3814
+ "en-gb": "eng",
3815
+ "en-GB": "eng",
3816
+ "en-ca": "eng",
3817
+ "en-CA": "eng",
3818
+ "en-au": "eng",
3819
+ "en-AU": "eng",
3820
+ // Spanish variants
3821
+ es: "spa",
3822
+ "es-es": "spa",
3823
+ "es-ES": "spa",
3824
+ "es-mx": "spa",
3825
+ "es-MX": "spa",
3826
+ // French variants
3827
+ fr: "fra",
3828
+ "fr-fr": "fra",
3829
+ "fr-FR": "fra",
3830
+ "fr-ca": "fra",
3831
+ "fr-CA": "fra",
3832
+ // German variants
3833
+ de: "ger",
3834
+ "de-de": "ger",
3835
+ "de-DE": "ger",
3836
+ "de-at": "ger",
3837
+ "de-AT": "ger",
3838
+ // Italian variants
3839
+ it: "ita",
3840
+ "it-it": "ita",
3841
+ "it-IT": "ita",
3842
+ // Portuguese variants
3843
+ pt: "por",
3844
+ "pt-br": "por",
3845
+ "pt-BR": "por",
3846
+ "pt-pt": "por",
3847
+ "pt-PT": "por",
3848
+ // Russian variants
3849
+ ru: "rus",
3850
+ "ru-ru": "rus",
3851
+ "ru-RU": "rus",
3852
+ // Japanese variants
3853
+ ja: "jpn",
3854
+ "ja-jp": "jpn",
3855
+ "ja-JP": "jpn",
3856
+ // Korean variants
3857
+ ko: "kor",
3858
+ "ko-kr": "kor",
3859
+ "ko-KR": "kor",
3860
+ // Chinese variants
3861
+ zh: "chi",
3862
+ "zh-cn": "chi",
3863
+ "zh-CN": "chi",
3864
+ "zh-tw": "chi",
3865
+ "zh-TW": "chi",
3866
+ "zh-hk": "chi",
3867
+ "zh-HK": "chi"
3868
+ }, normalizeToIsoLanguageCode = (code) => {
3869
+ const normalizedCode = code.toLowerCase();
3870
+ return LOCALE_TO_ISO[normalizedCode];
3871
+ }, wistiaCaptionTransform = {
3872
+ matcher: {
3873
+ fieldName: "wistiaCaptions"
3874
+ },
3875
+ toTranslatable: (wistiaCaptions, context) => {
3876
+ if (devLog.info("Wistia captions toTranslatable - input:", {
3877
+ fieldPath: context.fieldPath,
3878
+ hasCaptionContent: !!wistiaCaptions?.captionContent,
3879
+ captionContentLength: wistiaCaptions?.captionContent?.length ?? 0,
3880
+ isoLanguage: wistiaCaptions?.isoLanguage
3881
+ }), !wistiaCaptions || !wistiaCaptions.captionContent)
3882
+ return devLog.debug("Wistia caption field is missing or has no content, returning empty transform", {
3883
+ fieldPath: context.fieldPath,
3884
+ value: wistiaCaptions
3885
+ }), {
3886
+ _type: "translatable.wistia.captions",
3887
+ translationText: "",
3888
+ _originalMetadata: {
3889
+ captionFileName: wistiaCaptions?.captionFileName,
3890
+ isoLanguage: wistiaCaptions?.isoLanguage,
3891
+ captionUploadStatus: wistiaCaptions?.captionUploadStatus,
3892
+ captionUploadMessage: wistiaCaptions?.captionUploadMessage,
3893
+ _originalIndex: 0
3894
+ },
3895
+ _srtCues: []
3896
+ };
3897
+ devLog.info("Applying Wistia caption transform", {
3898
+ fieldPath: context.fieldPath,
3899
+ captionContentPreview: wistiaCaptions.captionContent.substring(0, 200)
3900
+ });
3901
+ const srtCues = parseSRTContent(wistiaCaptions.captionContent);
3902
+ devLog.debug("Parsed SRT cues:", {
3903
+ fieldPath: context.fieldPath,
3904
+ cueCount: srtCues.length,
3905
+ firstCue: srtCues.length > 0 ? srtCues[0] : null
3906
+ });
3907
+ const translationText = serializeWistiaCaptionText(srtCues), originalMetadata = {
3908
+ captionFileName: wistiaCaptions.captionFileName,
3909
+ isoLanguage: wistiaCaptions.isoLanguage,
3910
+ captionUploadStatus: wistiaCaptions.captionUploadStatus,
3911
+ captionUploadMessage: wistiaCaptions.captionUploadMessage,
3912
+ _originalIndex: 0
3913
+ }, srtCuesMetadata = srtCues.map((cue) => ({
3914
+ index: cue.index,
3915
+ startTime: cue.startTime,
3916
+ endTime: cue.endTime
3917
+ }));
3918
+ return devLog.info("Wistia captions serialized:", {
3919
+ fieldPath: context.fieldPath,
3920
+ translationTextLength: translationText.length,
3921
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : ""),
3922
+ cueCount: srtCues.length
3923
+ }), {
3924
+ _type: "translatable.wistia.captions",
3925
+ translationText,
3926
+ _originalMetadata: originalMetadata,
3927
+ _srtCues: srtCuesMetadata
3928
+ };
3929
+ },
3930
+ fromTranslatable: (translatedValue, originalValue, context) => {
3931
+ if (devLog.info("Wistia captions fromTranslatable - input:", {
3932
+ fieldPath: context.fieldPath,
3933
+ valueType: typeof translatedValue,
3934
+ isCorrectType: translatedValue._type === "translatable.wistia.captions",
3935
+ hasText: !!translatedValue.translationText,
3936
+ hasMetadata: !!translatedValue._originalMetadata,
3937
+ hasCues: Array.isArray(translatedValue._srtCues)
3938
+ }), !translatedValue || translatedValue._type !== "translatable.wistia.captions" || !translatedValue._originalMetadata || !Array.isArray(translatedValue._srtCues))
3939
+ return devLog.debug("Value is not a translatable Wistia caption object, returning original", {
3940
+ fieldPath: context.fieldPath,
3941
+ valueType: typeof translatedValue,
3942
+ translatedValue
3943
+ }), originalValue || {};
3944
+ devLog.info("Processing translated Wistia captions", {
3945
+ fieldPath: context.fieldPath,
3946
+ cueCount: translatedValue._srtCues.length
3947
+ });
3948
+ const { translationText, _originalMetadata, _srtCues } = translatedValue;
3949
+ devLog.info("Wistia captions translationText:", {
3950
+ fieldPath: context.fieldPath,
3951
+ translationTextLength: translationText.length,
3952
+ translationTextPreview: translationText.substring(0, 200) + (translationText.length > 200 ? "..." : "")
3953
+ });
3954
+ const reconstructedCaptionContent = deserializeWistiaCaptionText(translationText, _srtCues), documentLanguage = getDocumentLanguage(context.document);
3955
+ let updatedIsoLanguage = _originalMetadata.isoLanguage;
3956
+ if (documentLanguage) {
3957
+ const isoCode = normalizeToIsoLanguageCode(documentLanguage);
3958
+ isoCode ? (updatedIsoLanguage = isoCode, devLog.debug("Updated isoLanguage based on document language", {
3959
+ fieldPath: context.fieldPath,
3960
+ documentLanguage,
3961
+ isoCode
3962
+ })) : devLog.debug("Could not convert document language to ISO code, preserving original", {
3963
+ fieldPath: context.fieldPath,
3964
+ documentLanguage,
3965
+ originalIsoLanguage: _originalMetadata.isoLanguage
3966
+ });
3967
+ } else
3968
+ devLog.debug("Document language field not found, preserving original isoLanguage", {
3969
+ fieldPath: context.fieldPath,
3970
+ originalIsoLanguage: _originalMetadata.isoLanguage
3971
+ });
3972
+ const result = {
3973
+ captionContent: reconstructedCaptionContent,
3974
+ captionFileName: _originalMetadata.captionFileName,
3975
+ isoLanguage: updatedIsoLanguage,
3976
+ captionUploadStatus: _originalMetadata.captionUploadStatus,
3977
+ captionUploadMessage: _originalMetadata.captionUploadMessage
3978
+ };
3979
+ return devLog.info("Wistia captions final result:", {
3980
+ fieldPath: context.fieldPath,
3981
+ hasCaptionContent: !!result.captionContent,
3982
+ captionContentLength: result.captionContent?.length ?? 0,
3983
+ isoLanguage: result.isoLanguage
3984
+ }), result;
3985
+ }
3986
+ };
3987
+ function parseSRTContent(content) {
3988
+ if (devLog.debug("parseSRTContent - input:", {
3989
+ contentLength: content.length,
3990
+ contentPreview: content.substring(0, 100) + (content.length > 100 ? "..." : "")
3991
+ }), !content || content.trim() === "")
3992
+ return [];
3993
+ let normalizedContent = content;
3994
+ content.includes("\\n") && !content.includes(`
3995
+ `) && (normalizedContent = content.replace(/\\n/g, `
3996
+ `), devLog.debug("Unescaped newlines in SRT content", {
3997
+ originalLength: content.length,
3998
+ normalizedLength: normalizedContent.length
3999
+ }));
4000
+ const cues = [], cueBlocks = normalizedContent.split(/\n\s*\n/);
4001
+ for (const block of cueBlocks) {
4002
+ const lines = block.trim().split(`
4003
+ `);
4004
+ if (lines.length < 3)
4005
+ continue;
4006
+ const cueNumber = Number.parseInt(lines[0].trim(), 10);
4007
+ if (Number.isNaN(cueNumber)) {
4008
+ devLog.warn("Invalid SRT cue number", { line: lines[0] });
4009
+ continue;
4010
+ }
4011
+ const timestampLine = lines[1].trim(), timestampMatch = timestampLine.match(/^(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})$/);
4012
+ if (!timestampMatch) {
4013
+ devLog.warn("Invalid SRT timestamp format", { line: timestampLine });
4014
+ continue;
4015
+ }
4016
+ const startTime = timestampMatch[1].replace(".", ","), endTime = timestampMatch[2].replace(".", ","), text = lines.slice(2).join(`
4017
+ `).trim();
4018
+ text && cues.push({
4019
+ index: cueNumber,
4020
+ startTime,
4021
+ endTime,
4022
+ text
4023
+ });
4024
+ }
4025
+ return devLog.debug("parseSRTContent - output:", {
4026
+ cueCount: cues.length,
4027
+ firstCue: cues.length > 0 ? cues[0] : null
4028
+ }), cues;
4029
+ }
4030
+ function serializeWistiaCaptionText(cues) {
4031
+ if (devLog.debug("serializeWistiaCaptionText - input:", {
4032
+ cuesCount: cues.length,
4033
+ firstCue: cues.length > 0 ? cues[0] : null
4034
+ }), !Array.isArray(cues) || cues.length === 0)
4035
+ return "";
4036
+ const result = cues.map((cue) => (devLog.debug(`Processing cue ${cue.index}:`, {
4037
+ text: cue.text,
4038
+ hasText: typeof cue.text == "string"
4039
+ }), `[${cue.index}] ${cue.text}`)).join(`
4040
+ `);
4041
+ return devLog.debug("serializeWistiaCaptionText - output:", {
4042
+ resultLength: result.length,
4043
+ resultPreview: result.substring(0, 100) + (result.length > 100 ? "..." : "")
4044
+ }), result;
4045
+ }
4046
+ function deserializeWistiaCaptionText(translationText, srtCuesMetadata) {
4047
+ if (devLog.debug("deserializeWistiaCaptionText - input:", {
4048
+ translationTextLength: translationText.length,
4049
+ translationTextPreview: translationText.substring(0, 100) + (translationText.length > 100 ? "..." : ""),
4050
+ metadataCount: srtCuesMetadata.length,
4051
+ metadataExample: srtCuesMetadata.length > 0 ? srtCuesMetadata[0] : null
4052
+ }), !translationText || translationText.trim() === "")
4053
+ return "";
4054
+ const result = [], lines = translationText.split(`
4055
+ `), lineRegex = /^\[(\d+)\]\s*(.*)$/, metadataByIndex = /* @__PURE__ */ new Map();
4056
+ for (const metadata of srtCuesMetadata)
4057
+ metadataByIndex.set(metadata.index, { startTime: metadata.startTime, endTime: metadata.endTime });
4058
+ for (const line of lines) {
4059
+ const match = line.match(lineRegex);
4060
+ if (!match) {
4061
+ devLog.warn("Failed to parse caption line", { line });
4062
+ continue;
4063
+ }
4064
+ const index = Number.parseInt(match[1], 10), text = match[2];
4065
+ devLog.debug("Found caption:", {
4066
+ index,
4067
+ text
4068
+ });
4069
+ const metadata = metadataByIndex.get(index);
4070
+ metadata ? (devLog.debug(`Found matching metadata for index ${index}`), result.push(`${index}`), result.push(`${metadata.startTime} --> ${metadata.endTime}`), result.push(text), result.push("")) : devLog.warn("Missing metadata for caption", { index });
4071
+ }
4072
+ result.length > 0 && result[result.length - 1] === "" && result.pop();
4073
+ const reconstructed = result.join(`
4074
+ `);
4075
+ return devLog.debug("deserializeWistiaCaptionText - output:", {
4076
+ resultLength: reconstructed.length,
4077
+ resultPreview: reconstructed.substring(0, 200) + (reconstructed.length > 200 ? "..." : "")
4078
+ }), reconstructed;
4079
+ }
4080
+ function getDocumentLanguage(document) {
4081
+ if (!document || typeof document != "object")
4082
+ return;
4083
+ const languageFieldNames = ["language", "lang", "locale"];
4084
+ for (const fieldName of languageFieldNames)
4085
+ if (fieldName in document && typeof document[fieldName] == "string")
4086
+ return document[fieldName];
4087
+ }
3802
4088
  const aiTranslationPlugin = definePlugin((config) => (config.fieldTransforms?.length && (devLog.info(`Registering ${config.fieldTransforms.length} custom field transforms`), config.fieldTransforms.forEach((transform) => {
3803
4089
  globalTransformRegistry.register(transform);
3804
4090
  })), {
@@ -3818,6 +4104,7 @@ export {
3818
4104
  restoreExcludedFields,
3819
4105
  richTextTransform,
3820
4106
  translationHistorySchema,
3821
- vttCaptionTransform
4107
+ vttCaptionTransform,
4108
+ wistiaCaptionTransform
3822
4109
  };
3823
4110
  //# sourceMappingURL=index.js.map