@translationstudio/translationstudio-strapi-extension 1.1.0 → 1.1.1

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.
@@ -289,49 +289,54 @@ const getEntry = async (contentTypeID, entryID, locale) => {
289
289
  const transformResponse = (data) => data.map(
290
290
  (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
291
291
  );
292
+ function jsonToHtml(json) {
293
+ if (!json || !Array.isArray(json)) {
294
+ return "";
295
+ }
296
+ return json.map((node) => processNode(node)).join("");
297
+ }
298
+ function processNode(node) {
299
+ if (!node) return "";
300
+ switch (node.type) {
301
+ case "paragraph":
302
+ return `<p>${node.children.map(processNode).join("")}</p>`;
303
+ case "heading":
304
+ const level = node.level || 1;
305
+ return `<h${level}>${node.children.map(processNode).join("")}</h${level}>`;
306
+ case "link":
307
+ const url = node.url || "#";
308
+ return `<a href="${url}">${node.children.map(processNode).join("")}</a>`;
309
+ case "list":
310
+ const listType = node.format === "ordered" ? "ol" : "ul";
311
+ return `<${listType}>${node.children.map(processNode).join("")}</${listType}>`;
312
+ case "list-item":
313
+ return `<li>${node.children.map(processNode).join("")}</li>`;
314
+ case "quote":
315
+ return `<blockquote>${node.children.map(processNode).join("")}</blockquote>`;
316
+ case "code":
317
+ return `<pre><code>${node.children.map((child) => child.text).join("")}</code></pre>`;
318
+ case "image":
319
+ return `<img src="${node.url}" alt="${node.alt || ""}" />`;
320
+ case "text":
321
+ return formatText(node);
322
+ default:
323
+ return node.children && Array.isArray(node.children) ? node.children.map(processNode).join("") : node.text || "";
324
+ }
325
+ }
292
326
  function formatText(child) {
293
327
  if (child.type === "link") {
294
- return `<a href="${child.url}">${child.children.map((sub) => sub.text).join("")}</a>`;
328
+ return `<a href="${child.url}">${child.children.map((sub) => formatText(sub)).join("")}</a>`;
295
329
  }
296
330
  let text = child.text || "";
331
+ if (child.code) text = `<code>${text}</code>`;
297
332
  if (child.bold) text = `<strong>${text}</strong>`;
298
333
  if (child.italic) text = `<em>${text}</em>`;
299
334
  if (child.underline) text = `<u>${text}</u>`;
300
- if (child.strikethrough) text = `<del>${text}</del>`;
335
+ if (child.strikethrough) {
336
+ text = `~~${text}~~`;
337
+ }
301
338
  return text;
302
339
  }
303
- function renderChildren(children) {
304
- return children.map(formatText).join("");
305
- }
306
- function renderHeading(element) {
307
- return `<h${element.level}>${renderChildren(element.children)}</h${element.level}>`;
308
- }
309
- function renderParagraph(element) {
310
- return `<p>${renderChildren(element.children)}</p>`;
311
- }
312
- function renderList(element) {
313
- const tag = element.format === "unordered" ? "ul" : "ol";
314
- const items = element.children.map(renderListItem).join("");
315
- return `<${tag}>${items}</${tag}>`;
316
- }
317
- function renderListItem(item) {
318
- const content = item.children.map(formatText).join("");
319
- return `<li>${content}</li>`;
320
- }
321
- function jsonToHtml(jsonData) {
322
- return jsonData.map((element) => {
323
- switch (element.type) {
324
- case "heading":
325
- return renderHeading(element);
326
- case "paragraph":
327
- return renderParagraph(element);
328
- case "list":
329
- return renderList(element);
330
- default:
331
- return "";
332
- }
333
- }).join("");
334
- }
335
340
  const processComponent = async (fieldName, componentName, value, schemaName, componentId) => {
336
341
  const contentFields = [];
337
342
  const componentSchema = await strapi.components[componentName];
@@ -529,149 +534,197 @@ const isDynamicZone = (fieldSchema, value, schema) => {
529
534
  const isComponent = (fieldSchema, value, schema) => {
530
535
  return fieldSchema.type === "component" && isFieldLocalizable(fieldSchema, schema);
531
536
  };
532
- function parseInlineElements(text) {
533
- if (!text.includes("<")) {
534
- return [{ type: "text", text }];
537
+ function htmlToJson(html) {
538
+ function parseHTML(html2) {
539
+ const elements2 = [];
540
+ const tagRegex = /<([a-z0-9]+)((?:\s+[a-z-]+="[^"]*")*)\s*>([\s\S]*?)<\/\1>/gi;
541
+ let match;
542
+ while ((match = tagRegex.exec(html2)) !== null) {
543
+ const [, tag, attributes, content] = match;
544
+ const attrs = {};
545
+ const attrRegex = /([a-z-]+)="([^"]*)"/gi;
546
+ let attrMatch;
547
+ while ((attrMatch = attrRegex.exec(attributes)) !== null) {
548
+ attrs[attrMatch[1]] = attrMatch[2];
549
+ }
550
+ elements2.push({ tag, attrs, content });
551
+ }
552
+ return elements2;
535
553
  }
536
- const linkRegex = /<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
537
- if (text.includes("<a ")) {
538
- let linkMatch;
554
+ function parseInlineContent(content) {
555
+ const segments = [];
556
+ let currentText = "";
557
+ let formatStack = [];
558
+ let currentFormat = {
559
+ bold: false,
560
+ italic: false,
561
+ underline: false,
562
+ code: false,
563
+ strikethrough: false
564
+ };
565
+ const pushSegment = () => {
566
+ if (currentText) {
567
+ segments.push({
568
+ type: "text",
569
+ text: currentText,
570
+ ...Object.fromEntries(Object.entries(currentFormat).filter(([_, value]) => value))
571
+ });
572
+ currentText = "";
573
+ }
574
+ };
575
+ const tags = content.split(/(<[^>]+>|~~)/);
576
+ for (const tag of tags) {
577
+ if (!tag) continue;
578
+ if (tag === "~~") {
579
+ pushSegment();
580
+ currentFormat.strikethrough = !currentFormat.strikethrough;
581
+ continue;
582
+ }
583
+ if (tag.startsWith("<")) {
584
+ pushSegment();
585
+ if (tag.startsWith("</")) {
586
+ const tagName = tag.slice(2, -1).toLowerCase();
587
+ const lastTag = formatStack.pop();
588
+ if (lastTag && lastTag.type === tagName) {
589
+ switch (tagName) {
590
+ case "strong":
591
+ currentFormat.bold = false;
592
+ break;
593
+ case "em":
594
+ currentFormat.italic = false;
595
+ break;
596
+ case "u":
597
+ currentFormat.underline = false;
598
+ break;
599
+ case "code":
600
+ currentFormat.code = false;
601
+ break;
602
+ case "del":
603
+ currentFormat.strikethrough = false;
604
+ break;
605
+ }
606
+ }
607
+ } else {
608
+ const tagName = tag.slice(1, -1).toLowerCase();
609
+ formatStack.push({ type: tagName, index: segments.length });
610
+ switch (tagName) {
611
+ case "strong":
612
+ currentFormat.bold = true;
613
+ break;
614
+ case "em":
615
+ currentFormat.italic = true;
616
+ break;
617
+ case "u":
618
+ currentFormat.underline = true;
619
+ break;
620
+ case "code":
621
+ currentFormat.code = true;
622
+ break;
623
+ case "del":
624
+ currentFormat.strikethrough = true;
625
+ break;
626
+ }
627
+ }
628
+ } else {
629
+ currentText += tag;
630
+ }
631
+ }
632
+ pushSegment();
633
+ return segments.filter((segment) => segment.text.length > 0);
634
+ }
635
+ function parseList(html2, format) {
636
+ const listItems = html2.match(/<li>([\s\S]*?)<\/li>/g) || [];
637
+ return {
638
+ type: "list",
639
+ format,
640
+ children: listItems.map((item) => ({
641
+ type: "list-item",
642
+ children: parseListContent(item.replace(/<li>|<\/li>/g, ""))
643
+ }))
644
+ };
645
+ }
646
+ function parseListContent(content) {
647
+ const children = [];
648
+ const linkRegex = /<a\s+href="([^"]+)">([\s\S]*?)<\/a>/g;
539
649
  let lastIndex = 0;
540
- const elements = [];
541
- while ((linkMatch = linkRegex.exec(text)) !== null) {
542
- const [fullMatch, url, linkContent] = linkMatch;
543
- if (linkMatch.index > lastIndex) {
544
- const beforeText = text.substring(lastIndex, linkMatch.index);
545
- if (beforeText) {
546
- elements.push(...parseInlineElements(beforeText));
650
+ let match;
651
+ while ((match = linkRegex.exec(content)) !== null) {
652
+ const [fullMatch, href, linkText] = match;
653
+ if (match.index > lastIndex) {
654
+ const textBefore = content.slice(lastIndex, match.index);
655
+ if (textBefore.trim()) {
656
+ children.push(...parseInlineContent(textBefore));
547
657
  }
548
658
  }
549
- elements.push({
659
+ children.push({
550
660
  type: "link",
551
- url,
552
- children: parseInlineElements(linkContent)
661
+ url: href,
662
+ children: parseInlineContent(linkText)
553
663
  });
554
- lastIndex = linkMatch.index + fullMatch.length;
664
+ lastIndex = match.index + fullMatch.length;
555
665
  }
556
- if (lastIndex < text.length) {
557
- const afterText = text.substring(lastIndex);
558
- if (afterText) {
559
- elements.push(...parseInlineElements(afterText));
666
+ if (lastIndex < content.length) {
667
+ const remainingText = content.slice(lastIndex);
668
+ if (remainingText.trim()) {
669
+ children.push(...parseInlineContent(remainingText));
560
670
  }
561
671
  }
562
- return elements;
672
+ return children;
563
673
  }
564
- const formatRegex = /<(strong|em|u|del)>([\s\S]*?)<\/\1>/;
565
- const match = formatRegex.exec(text);
566
- if (match) {
567
- const [fullMatch, tag, content] = match;
568
- const beforeText = text.substring(0, match.index);
569
- const afterText = text.substring(match.index + fullMatch.length);
570
- const elements = [];
571
- if (beforeText) {
572
- elements.push(...parseInlineElements(beforeText));
573
- }
574
- const nestedElements = parseInlineElements(content);
575
- nestedElements.forEach((element) => {
576
- if (tag === "strong") element.bold = true;
577
- else if (tag === "em") element.italic = true;
578
- else if (tag === "u") element.underline = true;
579
- else if (tag === "del") element.strikethrough = true;
580
- });
581
- elements.push(...nestedElements);
582
- if (afterText) {
583
- elements.push(...parseInlineElements(afterText));
584
- }
585
- return elements;
674
+ function parseParagraph(content) {
675
+ return {
676
+ type: "paragraph",
677
+ children: parseListContent(content)
678
+ };
586
679
  }
587
- return [{ type: "text", text }];
588
- }
589
- function parseHeading(tag, innerText) {
590
- const level = parseInt(tag[1]);
591
- return {
592
- type: "heading",
593
- level,
594
- children: [{ type: "text", text: innerText.trim() }]
595
- };
596
- }
597
- function parseParagraph(innerText) {
598
- return {
599
- type: "paragraph",
600
- children: parseInlineElements(innerText)
601
- };
602
- }
603
- function parseList(tag, innerText) {
604
- const listType = tag === "ul" ? "unordered" : "ordered";
605
- const listItems = [];
606
- const listItemRegex = /<li>(.*?)<\/li>/g;
607
- let itemMatch;
608
- while ((itemMatch = listItemRegex.exec(innerText)) !== null) {
609
- listItems.push({
610
- type: "list-item",
611
- children: parseInlineElements(itemMatch[1])
612
- });
680
+ const blocks = [];
681
+ const elements = parseHTML(html);
682
+ if (elements.length === 0 && html.trim()) {
683
+ blocks.push(parseParagraph(html));
684
+ return blocks;
613
685
  }
614
- return {
615
- type: "list",
616
- format: listType,
617
- children: listItems
618
- };
619
- }
620
- function htmlToJson(htmlData) {
621
- const jsonData = [];
622
- const blockRegex = /<(h[1-3]|p|ul|ol)(?:[^>]*?)>([\s\S]*?)<\/\1>/g;
623
- let match;
624
- while ((match = blockRegex.exec(htmlData)) !== null) {
625
- const [, tag, content] = match;
626
- switch (tag) {
686
+ for (const element of elements) {
687
+ switch (element.tag.toLowerCase()) {
627
688
  case "h1":
628
689
  case "h2":
629
690
  case "h3":
630
- jsonData.push(parseHeading(tag, content));
691
+ case "h4":
692
+ case "h5":
693
+ case "h6":
694
+ blocks.push({
695
+ type: "heading",
696
+ level: parseInt(element.tag.slice(1)),
697
+ children: parseListContent(element.content)
698
+ });
631
699
  break;
632
700
  case "p":
633
- if (content.includes("<a ")) {
634
- const linkRegex = /<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
635
- let linkMatch;
636
- let lastIndex = 0;
637
- const children = [];
638
- children.push({ type: "text", text: "" });
639
- while ((linkMatch = linkRegex.exec(content)) !== null) {
640
- const [fullMatch, url, linkText] = linkMatch;
641
- if (linkMatch.index > lastIndex) {
642
- const beforeLinkText = content.substring(lastIndex, linkMatch.index);
643
- if (beforeLinkText) {
644
- children.push({ type: "text", text: beforeLinkText });
645
- }
646
- }
647
- children.push({
648
- type: "link",
649
- url,
650
- children: [{ type: "text", text: linkText }]
651
- });
652
- lastIndex = linkMatch.index + fullMatch.length;
653
- }
654
- const afterLastLink = content.substring(lastIndex);
655
- if (afterLastLink) {
656
- children.push({ type: "text", text: afterLastLink });
657
- } else {
658
- children.push({ type: "text", text: "" });
659
- }
660
- jsonData.push({
661
- type: "paragraph",
662
- children
663
- });
664
- } else {
665
- jsonData.push(parseParagraph(content));
666
- }
701
+ blocks.push(parseParagraph(element.content));
667
702
  break;
668
703
  case "ul":
704
+ blocks.push(parseList(element.content, "unordered"));
705
+ break;
669
706
  case "ol":
670
- jsonData.push(parseList(tag, content));
707
+ blocks.push(parseList(element.content, "ordered"));
671
708
  break;
709
+ case "blockquote":
710
+ blocks.push({
711
+ type: "quote",
712
+ children: [parseParagraph(element.content)]
713
+ });
714
+ break;
715
+ case "pre":
716
+ if (element.content.includes("<code>")) {
717
+ blocks.push({
718
+ type: "code",
719
+ children: parseInlineContent(element.content)
720
+ });
721
+ }
722
+ break;
723
+ default:
724
+ blocks.push(parseParagraph(element.content));
672
725
  }
673
726
  }
674
- return jsonData;
727
+ return blocks;
675
728
  }
676
729
  async function updateEntry(contentTypeID, entryID, sourceLocale, targetLocale, data, attributes) {
677
730
  if (!entryID) {
@@ -683,6 +736,14 @@ async function updateEntry(contentTypeID, entryID, sourceLocale, targetLocale, d
683
736
  locale: sourceLocale
684
737
  });
685
738
  const processedData = processDataRecursively(data);
739
+ for (const [key, value] of Object.entries(processedData)) {
740
+ if (attributes[key]?.type === "blocks" && typeof value === "string") {
741
+ console.warn(
742
+ `Field ${key} is a blocks field but received string value. Converting to blocks format.`
743
+ );
744
+ processedData[key] = htmlToJson(value);
745
+ }
746
+ }
686
747
  const localizedData = {};
687
748
  for (const field in processedData) {
688
749
  if (attributes[field] && (!attributes[field].pluginOptions?.i18n || attributes[field].pluginOptions?.i18n?.localized !== false)) {
@@ -701,23 +762,29 @@ async function updateEntry(contentTypeID, entryID, sourceLocale, targetLocale, d
701
762
  });
702
763
  }
703
764
  }
704
- function processDataRecursively(data) {
765
+ function processDataRecursively(data, schema) {
705
766
  if (!data || typeof data !== "object") {
706
767
  return data;
707
768
  }
708
769
  if (Array.isArray(data)) {
770
+ if (data[0]?.fields) {
771
+ const processedFields = {};
772
+ for (const fieldData of data[0].fields) {
773
+ if (fieldData.realType === "blocks") {
774
+ if (fieldData.translatableValue?.[0]) {
775
+ processedFields[fieldData.field] = htmlToJson(fieldData.translatableValue[0]);
776
+ }
777
+ } else {
778
+ processedFields[fieldData.field] = fieldData.translatableValue?.[0] || null;
779
+ }
780
+ }
781
+ return processedFields;
782
+ }
709
783
  return data.map((item) => processDataRecursively(item));
710
784
  }
711
785
  const result = {};
712
786
  for (const key in data) {
713
- const value = data[key];
714
- if (typeof value === "string" && (key.includes("Blocks") || key.includes("RTBlocks") || key === "blocks")) {
715
- result[key] = htmlToJson(value);
716
- } else if (value && typeof value === "object") {
717
- result[key] = processDataRecursively(value);
718
- } else {
719
- result[key] = value;
720
- }
787
+ result[key] = processDataRecursively(data[key]);
721
788
  }
722
789
  return result;
723
790
  }
@@ -762,16 +829,15 @@ function processRepeatableComponents(fields, existingEntry, rootPath) {
762
829
  }
763
830
  const componentId = field.componentInfo.id;
764
831
  if (!componentsById.has(componentId)) {
765
- const existingComponent = existingComponents.find(
766
- (c) => c.id === componentId
767
- );
768
- componentsById.set(
769
- componentId,
770
- existingComponent ? { ...existingComponent } : {}
771
- );
832
+ const existingComponent = existingComponents.find((c) => c.id === componentId);
833
+ componentsById.set(componentId, existingComponent ? { ...existingComponent } : {});
772
834
  }
773
835
  const component = componentsById.get(componentId);
774
- component[field.field] = field.translatableValue[0];
836
+ if (field.realType === "blocks") {
837
+ component[field.field] = htmlToJson(field.translatableValue[0] || "");
838
+ } else {
839
+ component[field.field] = field.translatableValue[0];
840
+ }
775
841
  });
776
842
  return Array.from(componentsById.values()).map((comp) => {
777
843
  if (!existingComponents.find((ec) => ec.id === comp.id)) {
@@ -793,7 +859,11 @@ function processNestedComponents(fields, pathParts, existingEntry, acc) {
793
859
  }
794
860
  if (index2 === pathParts.length - 1) {
795
861
  fields.forEach((field) => {
796
- current[part][field.field] = field.translatableValue[0];
862
+ if (field.realType === "blocks") {
863
+ current[part][field.field] = htmlToJson(field.translatableValue[0] || "");
864
+ } else {
865
+ current[part][field.field] = field.translatableValue[0];
866
+ }
797
867
  });
798
868
  } else {
799
869
  current = current[part];
@@ -808,11 +878,7 @@ function processComponentFields(componentFieldsMap, acc, existingEntry, targetSc
808
878
  const rootPath = pathParts[0];
809
879
  const schema = targetSchema.attributes?.[rootPath];
810
880
  if (schema?.repeatable) {
811
- acc[rootPath] = processRepeatableComponents(
812
- fields,
813
- existingEntry,
814
- rootPath
815
- );
881
+ acc[rootPath] = processRepeatableComponents(fields, existingEntry, rootPath);
816
882
  } else {
817
883
  processNestedComponents(fields, pathParts, existingEntry, acc);
818
884
  }
@@ -930,12 +996,9 @@ const service = ({ strapi: strapi2 }) => {
930
996
  },
931
997
  async getLanguageOptions() {
932
998
  const { license } = await this.getLicense();
933
- const response = await fetch(
934
- TRANSLATIONTUDIO_URL + "/mappings",
935
- {
936
- headers: { Authorization: `${license}` }
937
- }
938
- );
999
+ const response = await fetch(TRANSLATIONTUDIO_URL + "/mappings", {
1000
+ headers: { Authorization: `${license}` }
1001
+ });
939
1002
  const responseData = await response.json();
940
1003
  return responseData;
941
1004
  },
@@ -943,10 +1006,7 @@ const service = ({ strapi: strapi2 }) => {
943
1006
  const { contentTypeID, entryID, locale } = parsePayload(payload);
944
1007
  const contentType = await getContentType(contentTypeID);
945
1008
  const entry = await getEntry(contentTypeID, entryID, locale);
946
- const contentFields = await processEntryFields(
947
- entry,
948
- contentType.attributes
949
- );
1009
+ const contentFields = await processEntryFields(entry, contentType.attributes);
950
1010
  return transformResponse(contentFields);
951
1011
  },
952
1012
  async importData(payload) {
@@ -954,17 +1014,9 @@ const service = ({ strapi: strapi2 }) => {
954
1014
  const sourceLocale = payload.source;
955
1015
  const targetLocale = payload.target;
956
1016
  try {
957
- const existingEntry = await getEntry(
958
- contentTypeID,
959
- entryID,
960
- targetLocale
961
- );
1017
+ const existingEntry = await getEntry(contentTypeID, entryID, targetLocale);
962
1018
  const targetSchema = await getContentType(contentTypeID);
963
- const data = prepareImportData(
964
- payload.document[0].fields,
965
- existingEntry,
966
- targetSchema
967
- );
1019
+ const data = prepareImportData(payload.document[0].fields, existingEntry, targetSchema);
968
1020
  if (targetSchema.pluginOptions.i18n.localized === true) {
969
1021
  await updateEntry(
970
1022
  contentTypeID,
@@ -982,17 +1034,14 @@ const service = ({ strapi: strapi2 }) => {
982
1034
  },
983
1035
  async requestTranslation(payload) {
984
1036
  const { license } = await this.getLicense();
985
- const response = await fetch(
986
- TRANSLATIONTUDIO_URL + "/translate",
987
- {
988
- method: "POST",
989
- headers: {
990
- Authorization: `${license}`,
991
- "Content-Type": "application/json"
992
- },
993
- body: JSON.stringify(payload)
994
- }
995
- );
1037
+ const response = await fetch(TRANSLATIONTUDIO_URL + "/translate", {
1038
+ method: "POST",
1039
+ headers: {
1040
+ Authorization: `${license}`,
1041
+ "Content-Type": "application/json"
1042
+ },
1043
+ body: JSON.stringify(payload)
1044
+ });
996
1045
  if (response.status === 204) return true;
997
1046
  },
998
1047
  async getEmail(ctx) {