@translationstudio/translationstudio-strapi-extension 1.1.3 → 2.0.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.
@@ -1,3 +1,4 @@
1
+ import * as crypto from "crypto";
1
2
  const bootstrap = ({ strapi: strapi2 }) => {
2
3
  };
3
4
  const destroy = ({ strapi: strapi2 }) => {
@@ -75,10 +76,15 @@ const controller = ({ strapi: strapi2 }) => ({
75
76
  ctx.status = 400;
76
77
  return;
77
78
  }
78
- const payload = JSON.parse(ctx.request.body);
79
- const result = await strapi2.plugin(APP_NAME$1).service("service").exportData(payload);
80
- ctx.status = 200;
81
- ctx.body = [{ fields: result }];
79
+ try {
80
+ const payload = typeof ctx.request.body === "string" ? JSON.parse(ctx.request.body) : ctx.request.body;
81
+ const result = await strapi2.plugin(APP_NAME$1).service("service").exportData(payload);
82
+ ctx.status = 200;
83
+ ctx.body = [{ fields: result }];
84
+ } catch (ex) {
85
+ ctx.status = 500;
86
+ ctx.body = { error: ex.message ?? "Generic error" };
87
+ }
82
88
  },
83
89
  async setDevelopmentUrl(ctx) {
84
90
  const url = ctx.request.body.url;
@@ -106,23 +112,36 @@ const controller = ({ strapi: strapi2 }) => ({
106
112
  ctx.status = 400;
107
113
  return;
108
114
  }
109
- const payload = JSON.parse(ctx.request.body);
110
- const result = await strapi2.plugin(APP_NAME$1).service("service").importData(payload);
111
- ctx.body = result;
115
+ try {
116
+ const payload = typeof ctx.request.body === "object" ? ctx.request.body : JSON.parse(ctx.request.body);
117
+ strapi2.log.info("Importing");
118
+ const result = await strapi2.plugin(APP_NAME$1).service("service").importData(payload);
119
+ ctx.body = { success: result };
120
+ ctx.status = 200;
121
+ return;
122
+ } catch (err) {
123
+ strapi2.log.error(err.message ?? err);
124
+ }
125
+ ctx.body = { message: "Could not perform import" };
126
+ ctx.status = 500;
112
127
  },
113
128
  async ping(ctx) {
114
- const result = await strapi2.plugin(APP_NAME$1).service("service").ping();
129
+ await strapi2.plugin(APP_NAME$1).service("service").ping();
115
130
  ctx.status = 204;
116
- ctx.body = result;
117
131
  },
118
132
  async getLanguages(ctx) {
119
133
  if (!await this.validateToken(ctx)) {
120
134
  ctx.status = 400;
121
135
  return;
122
136
  }
123
- const result = await strapi2.plugin(APP_NAME$1).service("service").getLanguages();
124
- ctx.status = 200;
125
- ctx.body = result;
137
+ try {
138
+ const result = await strapi2.plugin(APP_NAME$1).service("service").getLanguages();
139
+ ctx.status = 200;
140
+ ctx.body = result;
141
+ } catch (err) {
142
+ ctx.status = 400;
143
+ ctx.body = {};
144
+ }
126
145
  },
127
146
  async getEmail(ctx) {
128
147
  const result = await strapi2.plugin(APP_NAME$1).service("service").getEmail(ctx);
@@ -256,6 +275,7 @@ const routes = [
256
275
  policies: []
257
276
  }
258
277
  },
278
+ /* get entry title */
259
279
  {
260
280
  method: "POST",
261
281
  path: "/entrydata",
@@ -265,13 +285,33 @@ const routes = [
265
285
  }
266
286
  }
267
287
  ];
268
- const getContentType = async (contentTypeID) => {
269
- const contentType = await strapi.contentType(contentTypeID);
288
+ function getComponentSchemata(schema) {
289
+ const res = {};
290
+ for (let fieldName in schema.attributes) {
291
+ const field = schema.attributes[fieldName];
292
+ if (!field.components || !Array.isArray(field.components) || field.components.length === 0)
293
+ continue;
294
+ for (let type of field.components) {
295
+ const schema2 = strapi.components[type];
296
+ if (schema2 && schema2.attributes)
297
+ res[type] = schema2;
298
+ }
299
+ }
300
+ return res;
301
+ }
302
+ function getContentType(contentTypeID) {
303
+ const contentType = strapi.contentType(contentTypeID);
270
304
  if (!contentType?.attributes) {
271
- throw new Error(`Content type or schema not found: ${contentTypeID}`);
305
+ strapi.log.error(`Content type or schema not found: ${contentTypeID}`);
306
+ return null;
272
307
  }
273
- return contentType;
274
- };
308
+ const res = {
309
+ entry: contentType,
310
+ components: getComponentSchemata(contentType)
311
+ };
312
+ strapi.log.info("SChema loaded for " + contentTypeID + ". Component schemata loaded: " + Object.keys(res.components).length);
313
+ return res;
314
+ }
275
315
  const parsePayload = (payload) => {
276
316
  const [contentTypeID, entryID] = payload.element.includes("#") ? payload.element.split("#") : [payload.element, void 0];
277
317
  const locale = payload.source.includes("-") ? payload.source.split("-")[0] : payload.source;
@@ -281,7 +321,7 @@ const getComponentSchema = async (componentName) => {
281
321
  try {
282
322
  return await strapi.components[componentName];
283
323
  } catch (error) {
284
- console.error(`Failed to get component schema for ${componentName}:`, error);
324
+ strapi.log.error(`Failed to get component schema for ${componentName}:`, error);
285
325
  return null;
286
326
  }
287
327
  };
@@ -304,27 +344,32 @@ const buildPopulateConfig = async (schema) => {
304
344
  }
305
345
  return populate;
306
346
  };
307
- const getEntry = async (contentTypeID, entryID, locale) => {
347
+ const getEntry = async (contentTypeID, entryID, locale, logError = true) => {
308
348
  try {
309
349
  const contentType = await strapi.contentTypes[contentTypeID];
310
350
  const populateConfig = await buildPopulateConfig(contentType);
311
351
  const query = {
312
352
  locale,
353
+ documentId: entryID,
313
354
  populate: populateConfig
314
355
  };
315
- if (entryID) {
316
- Object.assign(query, { documentId: entryID });
317
- }
318
356
  const entry = await strapi.documents(contentTypeID).findFirst(query);
319
- return entry;
357
+ if (entry) {
358
+ strapi.log.info("Obtained " + contentTypeID + "::" + entryID + " in " + locale);
359
+ return entry;
360
+ }
320
361
  } catch (error) {
321
- console.error("Entry fetch error:", error);
322
- return null;
362
+ strapi.log.error(error.message ?? error);
323
363
  }
364
+ strapi.log.warn("Could not find entry " + entryID + " in locale " + locale);
365
+ return null;
366
+ };
367
+ const transformResponse = function(data) {
368
+ data.fields = data.fields.map(
369
+ (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
370
+ );
371
+ return data;
324
372
  };
325
- const transformResponse = (data) => data.map(
326
- (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
327
- );
328
373
  function jsonToHtml(json) {
329
374
  if (!json || !Array.isArray(json)) {
330
375
  return "";
@@ -373,43 +418,37 @@ function formatText(child) {
373
418
  }
374
419
  return text;
375
420
  }
376
- const processComponent = async (fieldName, componentName, value, schemaName, componentId) => {
421
+ const Logger$4 = {
422
+ log: typeof strapi !== "undefined" ? strapi.log : console,
423
+ info: (val) => Logger$4.log.info(val),
424
+ warn: (val) => Logger$4.log.warn(val),
425
+ error: (val) => Logger$4.log.error(val),
426
+ debug: (val) => Logger$4.log.debug(val)
427
+ };
428
+ async function processComponent(fieldName, value, componentSchema, schemata) {
377
429
  const contentFields = [];
378
- const componentSchema = await strapi.components[componentName];
379
- if (!componentSchema) {
380
- throw new Error(`Component schema not found for ${componentName}`);
381
- }
430
+ Logger$4.info("Processing dynamic field " + fieldName);
431
+ if (!componentSchema || !componentSchema.attributes)
432
+ return [];
382
433
  const schemaAttributes = componentSchema.attributes || {};
383
434
  const dataToProcess = value || {};
384
- if (Array.isArray(dataToProcess)) {
385
- for (const item of dataToProcess) {
386
- const processedFields = await processComponentFields$1(
387
- item,
388
- schemaAttributes,
389
- fieldName,
390
- componentName,
391
- schemaName,
392
- item.id
393
- );
394
- contentFields.push(...processedFields);
395
- }
396
- } else {
397
- const processedFields = await processComponentFields$1(
398
- dataToProcess,
435
+ const candidates = Array.isArray(dataToProcess) ? dataToProcess : [dataToProcess];
436
+ for (const item of candidates) {
437
+ const processedFields = await processComponentFields(
438
+ item,
399
439
  schemaAttributes,
400
440
  fieldName,
401
- componentName,
402
- schemaName,
403
- componentId
441
+ schemata
404
442
  );
405
- contentFields.push(...processedFields);
443
+ if (processedFields.length > 0)
444
+ contentFields.push(...processedFields);
406
445
  }
407
446
  return contentFields;
408
- };
447
+ }
409
448
  const shouldSkipField$1 = (key, fieldSchema) => {
410
449
  return key === "id" || fieldSchema.private;
411
450
  };
412
- const isTranslatableField$1 = (type) => {
451
+ const isTranslatableField = (type) => {
413
452
  return ["string", "text", "blocks", "richtext"].includes(type);
414
453
  };
415
454
  const getTranslatedValue = (type, value) => {
@@ -418,64 +457,61 @@ const getTranslatedValue = (type, value) => {
418
457
  }
419
458
  return value.toString();
420
459
  };
421
- const buildTranslatable = (key, fieldSchema, value, parentPath, componentId, schemaName) => {
460
+ const buildTranslatable = (key, fieldSchema, value, uuid = "") => {
422
461
  return {
423
462
  field: key,
424
463
  type: ["richtext", "blocks"].includes(fieldSchema.type) ? "html" : "text",
425
464
  translatableValue: [value],
426
465
  realType: fieldSchema.type,
427
- componentInfo: {
428
- namePath: parentPath,
429
- id: componentId,
430
- schemaName
431
- }
466
+ uuid
432
467
  };
433
468
  };
434
- const processComponentFields$1 = async (componentData, schema, parentField, componentName, schemaName, componentId) => {
469
+ const processComponentFields = async (componentData, schema, parentField, schemata) => {
435
470
  const contentFields = [];
436
- const parentPath = parentField.split(".");
471
+ const uuid = crypto.randomUUID();
437
472
  for (const [key, fieldSchema] of Object.entries(schema)) {
438
473
  if (shouldSkipField$1(key, fieldSchema)) continue;
439
474
  const value = componentData?.[key];
440
- const fieldPath = `${parentField}.${key}`;
475
+ if (!value)
476
+ continue;
441
477
  if (fieldSchema.type === "component") {
442
- if (!fieldSchema.component) continue;
478
+ if (!value.__component)
479
+ continue;
480
+ const targetSchema = schemata[value.__component];
481
+ if (!targetSchema)
482
+ continue;
443
483
  const nestedFields = await processComponent(
444
- fieldPath,
445
- fieldSchema.component,
446
- value || {},
447
- fieldSchema.component,
448
- value?.id
484
+ `${parentField}.${key}`,
485
+ value,
486
+ targetSchema,
487
+ schemata
449
488
  );
450
- contentFields.push(...nestedFields);
489
+ if (nestedFields.length > 0)
490
+ contentFields.push(...nestedFields);
451
491
  continue;
452
492
  }
453
- if (!isTranslatableField$1(fieldSchema.type)) continue;
493
+ if (!isTranslatableField(fieldSchema.type)) continue;
454
494
  if (value === null || value === void 0 || value === "") continue;
455
495
  const translatedValue = getTranslatedValue(fieldSchema.type, value);
456
496
  const translatable = buildTranslatable(
457
497
  key,
458
498
  fieldSchema,
459
499
  translatedValue,
460
- parentPath,
461
- componentId,
462
- schemaName
500
+ uuid
463
501
  );
502
+ componentData.__tsuid = uuid;
464
503
  contentFields.push(translatable);
465
504
  }
466
505
  return contentFields;
467
506
  };
468
- const isFieldLocalizable = (fieldSchema, parentSchema) => {
469
- if (fieldSchema.pluginOptions?.i18n?.localized !== void 0) {
470
- return fieldSchema.pluginOptions.i18n.localized;
471
- }
472
- if (parentSchema.pluginOptions?.i18n?.localized === true) {
473
- const localizableTypes = ["string", "text", "blocks", "richtext"];
474
- return localizableTypes.includes(fieldSchema.type);
475
- }
476
- return false;
507
+ const Logger$3 = {
508
+ log: typeof strapi !== "undefined" ? strapi.log : console,
509
+ info: (val) => Logger$3.log.info(val),
510
+ warn: (val) => Logger$3.log.warn(val),
511
+ error: (val) => Logger$3.log.error(val),
512
+ debug: (val) => Logger$3.log.debug(val)
477
513
  };
478
- const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
514
+ const DEFAULT_FIELDS$1 = /* @__PURE__ */ new Set([
479
515
  "id",
480
516
  "documentId",
481
517
  "createdAt",
@@ -487,45 +523,41 @@ const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
487
523
  "createdBy"
488
524
  ]);
489
525
  const isEmpty = (value) => value === null || value === void 0 || value === "";
490
- const isTranslatableField = (fieldSchema) => ["string", "text", "blocks", "richtext"].includes(fieldSchema.type) && fieldSchema.pluginOptions?.i18n?.localized !== false;
491
- const processDynamicZone = async (key, value, schema) => {
526
+ const isSimpleTranslatableField = (fieldSchema) => ["string", "text", "blocks", "richtext"].includes(fieldSchema.type) && fieldSchema.pluginOptions?.i18n?.localized === true;
527
+ const processDynamicZone = async (key, value, schemata) => {
492
528
  const results = [];
493
529
  for (const component of value) {
494
530
  const componentName = component?.__component;
495
531
  if (!componentName) continue;
532
+ const schema = schemata[componentName];
533
+ if (!schema) continue;
496
534
  const fields = await processComponent(
497
535
  key,
498
- componentName,
499
536
  component,
500
- componentName,
501
- component.id
537
+ schema,
538
+ schemata
502
539
  );
503
- results.push(...fields);
540
+ if (fields.length > 0)
541
+ results.push(...fields);
504
542
  }
505
543
  return results;
506
544
  };
507
- const processComponentField = async (key, value, fieldSchema) => {
545
+ const processComponentField = async (key, value, fieldSchema, schemata) => {
508
546
  const results = [];
509
- if (fieldSchema.repeatable && Array.isArray(value)) {
510
- for (const component of value) {
511
- const fields = await processComponent(
512
- key,
513
- fieldSchema.component,
514
- component,
515
- fieldSchema.component,
516
- component.id
517
- );
518
- results.push(...fields);
519
- }
520
- } else {
547
+ const candidates = Array.isArray(value) ? value : [value];
548
+ for (const component of candidates) {
549
+ const componentName = component?.__component;
550
+ if (!componentName) continue;
551
+ const schema = schemata[componentName];
552
+ if (!schema) continue;
521
553
  const fields = await processComponent(
522
554
  key,
523
- fieldSchema.component,
524
- value,
525
- fieldSchema.component,
526
- value.id
555
+ component,
556
+ schema,
557
+ schemata
527
558
  );
528
- results.push(...fields);
559
+ if (fields.length > 0)
560
+ results.push(...fields);
529
561
  }
530
562
  return results;
531
563
  };
@@ -538,38 +570,148 @@ const processRegularField = (key, value, fieldSchema) => {
538
570
  realType: fieldSchema.type
539
571
  };
540
572
  };
541
- const processEntryFields = async (entry, schema, locale) => {
573
+ function IsLocalisableSchema(schema) {
574
+ return schema.pluginOptions?.i18n?.localized === true;
575
+ }
576
+ function IsLocalisedField(field) {
577
+ return field.pluginOptions?.i18n?.localized === true;
578
+ }
579
+ const processEntryFields = async (entry, schemaData, _locale) => {
542
580
  const contentFields = [];
581
+ const staticContent = {};
582
+ const schema = schemaData.entry.attributes;
543
583
  for (const [key, value] of Object.entries(entry)) {
544
- if (shouldSkipField(key, value)) continue;
545
- const fieldSchema = schema[key];
546
- if (!fieldSchema) continue;
547
- if (isDynamicZone(fieldSchema, value, schema)) {
548
- const zoneFields = await processDynamicZone(key, value);
549
- contentFields.push(...zoneFields);
584
+ if (shouldSkipField(key, value))
550
585
  continue;
551
- }
552
- if (isComponent(fieldSchema, value, schema)) {
553
- const componentFields = await processComponentField(key, value, fieldSchema);
554
- contentFields.push(...componentFields);
586
+ const fieldSchema = schema[key];
587
+ if (!fieldSchema || !IsLocalisedField(fieldSchema)) {
588
+ Logger$3.debug("SKipping non-local field " + key);
555
589
  continue;
556
590
  }
557
- if (isTranslatableField(fieldSchema)) {
591
+ if (isSimpleTranslatableField(fieldSchema)) {
592
+ Logger$3.debug("Processing simple field " + key);
558
593
  const translatedField = processRegularField(key, value, fieldSchema);
559
594
  contentFields.push(translatedField);
595
+ continue;
596
+ }
597
+ const zoneInfo = isDynamicZone(fieldSchema, value);
598
+ if (zoneInfo.isZone) {
599
+ if (zoneInfo.hasContent) {
600
+ Logger$3.debug("Processing dynamic zone field " + key);
601
+ const zoneFields = await processDynamicZone(key, value, schemaData.components);
602
+ contentFields.push(...zoneFields);
603
+ staticContent[key] = value;
604
+ }
605
+ continue;
606
+ }
607
+ const componentInfo = isComponent(fieldSchema);
608
+ if (componentInfo.isZone) {
609
+ const componentFields = await processComponentField(key, value, fieldSchema, schemaData.components);
610
+ contentFields.push(...componentFields);
611
+ staticContent[key] = value;
560
612
  }
561
613
  }
562
- return contentFields;
614
+ Logger$3.info("Process completed");
615
+ return {
616
+ fields: contentFields,
617
+ keep: staticContent
618
+ };
563
619
  };
564
620
  const shouldSkipField = (key, value) => {
565
- return DEFAULT_FIELDS.has(key) || isEmpty(value);
621
+ return DEFAULT_FIELDS$1.has(key) || isEmpty(value);
622
+ };
623
+ const isDynamicZone = (fieldSchema, value) => {
624
+ return {
625
+ isZone: fieldSchema.type === "dynamiczone",
626
+ hasContent: Array.isArray(value) && value.length > 0
627
+ };
628
+ };
629
+ const isComponent = (fieldSchema) => {
630
+ return {
631
+ isZone: fieldSchema.type === "dynamiczone",
632
+ hasContent: true
633
+ };
634
+ };
635
+ const Logger$2 = {
636
+ log: typeof strapi !== "undefined" ? strapi.log : console,
637
+ info: (val) => Logger$2.log.info(val),
638
+ warn: (val) => Logger$2.log.warn(val),
639
+ error: (val) => Logger$2.log.error(val),
640
+ debug: (val) => Logger$2.log.debug(val)
566
641
  };
567
- const isDynamicZone = (fieldSchema, value, schema) => {
568
- return fieldSchema.type === "dynamiczone" && isFieldLocalizable(fieldSchema, schema) && Array.isArray(value);
642
+ const DEFAULT_FIELDS = [
643
+ "id",
644
+ "documentId",
645
+ "createdAt",
646
+ "updatedAt",
647
+ ,
648
+ "createdBy",
649
+ "updatedBy",
650
+ "publishedAt",
651
+ "locale",
652
+ "localizations"
653
+ ];
654
+ const getContentFields = function(schema) {
655
+ const nullFields = [];
656
+ for (let field in schema) {
657
+ if (!DEFAULT_FIELDS.includes(field))
658
+ nullFields.push(field);
659
+ }
660
+ return nullFields;
569
661
  };
570
- const isComponent = (fieldSchema, value, schema) => {
571
- return fieldSchema.type === "component" && isFieldLocalizable(fieldSchema, schema);
662
+ const getInvalidOrNullFields = function(document, schema) {
663
+ if (!document)
664
+ return getContentFields(schema);
665
+ const nullFields = [];
666
+ let fieldsValid = 0;
667
+ for (let field in document) {
668
+ if (DEFAULT_FIELDS.includes(field))
669
+ continue;
670
+ if (document[field] === null)
671
+ nullFields.push(field);
672
+ else
673
+ fieldsValid++;
674
+ }
675
+ if (nullFields.length > 0 || fieldsValid > 0)
676
+ return nullFields;
677
+ return getContentFields(schema);
572
678
  };
679
+ function appendMissingFields(data, sourceEntry, targetSchema, targetEntry) {
680
+ const nullFields = getInvalidOrNullFields(targetEntry, targetSchema.entry.attributes);
681
+ if (nullFields.length === 0)
682
+ return;
683
+ let count = 0;
684
+ Logger$2.info("Adding missing fields to new locale: " + nullFields.join(", "));
685
+ for (let field of nullFields) {
686
+ if (data[field]) {
687
+ Logger$2.info("Field already present: " + field);
688
+ continue;
689
+ }
690
+ if (!sourceEntry[field]) {
691
+ Logger$2.info("No valid source langauge field value for " + field + " - skipping it.");
692
+ continue;
693
+ }
694
+ if (!targetSchema.entry.attributes[field]) {
695
+ Logger$2.warn("Schema does not contain field " + field);
696
+ continue;
697
+ }
698
+ Logger$2.info("Adding missing field and value for " + field);
699
+ data[field] = sourceEntry[field];
700
+ count++;
701
+ }
702
+ if (count > 0)
703
+ Logger$2.info(count + " missing fields added.");
704
+ }
705
+ async function updateEntry(contentTypeID, entryID, targetLocale, data) {
706
+ strapi.log.info("Updating target entry " + contentTypeID + "::" + entryID + " in locale " + targetLocale);
707
+ const newEntry = await strapi.documents(contentTypeID).update({
708
+ documentId: entryID,
709
+ locale: targetLocale,
710
+ data
711
+ });
712
+ if (!newEntry)
713
+ throw new Error("Cannot update target entry " + contentTypeID + "::" + entryID + " in locale " + targetLocale);
714
+ }
573
715
  function htmlToJson(html) {
574
716
  function parseHTML(html2) {
575
717
  const elements2 = [];
@@ -762,227 +904,171 @@ function htmlToJson(html) {
762
904
  }
763
905
  return blocks;
764
906
  }
765
- async function updateEntry(contentTypeID, entryID, sourceLocale, targetLocale, data, attributes) {
766
- if (!entryID) {
767
- const singleTypeData = await strapi.documents(contentTypeID).findFirst();
768
- entryID = singleTypeData.documentId;
769
- }
770
- const originalEntry = await strapi.documents(contentTypeID).findFirst({
771
- documentId: entryID,
772
- locale: sourceLocale
773
- });
774
- const processedData = processDataRecursively(data);
775
- for (const [key, value] of Object.entries(processedData)) {
776
- if (attributes[key]?.type === "blocks" && typeof value === "string") {
777
- console.warn(
778
- `Field ${key} is a blocks field but received string value. Converting to blocks format.`
779
- );
780
- processedData[key] = htmlToJson(value);
907
+ const Logger$1 = {
908
+ log: typeof strapi !== "undefined" ? strapi.log : console,
909
+ info: (val) => Logger$1.log.info(val),
910
+ warn: (val) => Logger$1.log.warn(val),
911
+ error: (val) => Logger$1.log.error(val),
912
+ debug: (val) => Logger$1.log.debug(val)
913
+ };
914
+ const removeComponentIds = function(elem) {
915
+ const list = Array.isArray(elem) ? elem : [elem];
916
+ for (let obj of list) {
917
+ if (obj.__component && obj.id)
918
+ delete obj.id;
919
+ for (let key in obj) {
920
+ const child = obj[key];
921
+ if (Array.isArray(child) && Array.length > 0)
922
+ removeComponentIds(child);
923
+ else if (typeof child === "object")
924
+ removeComponentIds(child);
781
925
  }
782
926
  }
783
- const localizedData = {};
784
- for (const field in processedData) {
785
- if (attributes[field] && (!attributes[field].pluginOptions?.i18n || attributes[field].pluginOptions?.i18n?.localized !== false)) {
786
- localizedData[field] = processedData[field];
927
+ };
928
+ function replaceDynamicZones(strapiEntry, replacableFields) {
929
+ const fields = [];
930
+ for (let key in strapiEntry) {
931
+ if (replacableFields[key]) {
932
+ removeComponentIds(replacableFields[key]);
933
+ strapiEntry[key] = replacableFields[key];
934
+ fields.push(key);
787
935
  }
788
936
  }
789
- await strapi.documents(contentTypeID).update({
790
- documentId: entryID,
791
- locale: targetLocale,
792
- data: localizedData
793
- });
794
- if (originalEntry.publishedAt !== null) {
795
- await strapi.documents(contentTypeID).publish({
796
- documentId: entryID,
797
- locale: sourceLocale
798
- });
799
- }
937
+ if (fields.length > 0)
938
+ Logger$1.info(fields.length + " dynamic fields/components replaced for later merge: " + fields.join(", "));
939
+ return fields;
800
940
  }
801
- function processDataRecursively(data, schema) {
802
- if (!data || typeof data !== "object") {
803
- return data;
941
+ const mergeValue = function(field, translatable, targetSchema, map) {
942
+ if (translatable.translatableValue.length === 0)
943
+ return false;
944
+ if (targetSchema.attributes[field] === void 0) {
945
+ Logger$1.info("Field " + field + " does not exist in schema. Skipping it");
946
+ Logger$1.info(targetSchema);
947
+ return false;
804
948
  }
805
- if (Array.isArray(data)) {
806
- if (data[0]?.fields) {
807
- const processedFields = {};
808
- for (const fieldData of data[0].fields) {
809
- if (fieldData.realType === "blocks") {
810
- if (fieldData.translatableValue?.[0]) {
811
- processedFields[fieldData.field] = htmlToJson(fieldData.translatableValue[0]);
812
- }
813
- } else {
814
- processedFields[fieldData.field] = fieldData.translatableValue?.[0] || null;
815
- }
816
- }
817
- return processedFields;
818
- }
819
- return data.map((item) => processDataRecursively(item));
949
+ if (translatable.translatableValue[0] === "") {
950
+ Logger$1.info("Skipping empty translated content for field " + field);
951
+ return false;
820
952
  }
821
- const result = {};
822
- for (const key in data) {
823
- result[key] = processDataRecursively(data[key]);
953
+ if (translatable.realType === "blocks") {
954
+ Logger$1.info("Merge block field " + field);
955
+ map[field] = htmlToJson(translatable.translatableValue[0] || "");
956
+ return true;
824
957
  }
825
- return result;
826
- }
827
- function organizeFields(fields) {
828
- const componentFieldsMap = /* @__PURE__ */ new Map();
829
- const dynamicZoneFields = /* @__PURE__ */ new Map();
830
- const regularFields = [];
831
- fields.forEach((field) => {
832
- if (!field.componentInfo) {
833
- regularFields.push(field);
834
- return;
835
- }
836
- const { namePath, id } = field.componentInfo;
837
- const pathString = namePath.join(".");
838
- if (namePath[0] === "dynamiczone") {
839
- if (!dynamicZoneFields.has(id)) {
840
- dynamicZoneFields.set(id, []);
841
- }
842
- dynamicZoneFields.get(id)?.push(field);
843
- } else {
844
- if (!componentFieldsMap.has(pathString)) {
845
- componentFieldsMap.set(pathString, []);
846
- }
847
- componentFieldsMap.get(pathString)?.push(field);
848
- }
849
- });
850
- return { regularFields, componentFieldsMap, dynamicZoneFields };
851
- }
852
- function processRegularFields(regularFields, acc) {
853
- regularFields.forEach((field) => {
854
- acc[field.field] = field.translatableValue[0];
855
- });
856
- return acc;
857
- }
858
- function processRepeatableComponents(fields, existingEntry, rootPath) {
859
- const existingComponents = existingEntry?.[rootPath] || [];
860
- const componentsById = /* @__PURE__ */ new Map();
861
- fields.forEach((field) => {
862
- if (!field.componentInfo) {
863
- console.warn(`Component info missing for field: ${field.field}`);
864
- return;
865
- }
866
- const componentId = field.componentInfo.id;
867
- if (!componentsById.has(componentId)) {
868
- const existingComponent = existingComponents.find((c) => c.id === componentId);
869
- componentsById.set(componentId, existingComponent ? { ...existingComponent } : {});
870
- }
871
- const component = componentsById.get(componentId);
872
- if (field.realType === "blocks") {
873
- component[field.field] = htmlToJson(field.translatableValue[0] || "");
874
- } else {
875
- component[field.field] = field.translatableValue[0];
876
- }
877
- });
878
- return Array.from(componentsById.values()).map((comp) => {
879
- if (!existingComponents.find((ec) => ec.id === comp.id)) {
880
- const { id, ...rest } = comp;
881
- return rest;
882
- }
883
- return comp;
884
- }).filter((comp) => Object.keys(comp).length > 0);
885
- }
886
- function processNestedComponents(fields, pathParts, existingEntry, acc) {
887
- let current = acc;
888
- let currentExisting = existingEntry;
889
- pathParts.forEach((part, index2) => {
890
- if (!current[part]) {
891
- current[part] = {};
892
- if (currentExisting?.[part]?.id) {
893
- current[part].id = currentExisting[part].id;
894
- }
895
- }
896
- if (index2 === pathParts.length - 1) {
897
- fields.forEach((field) => {
898
- if (field.realType === "blocks") {
899
- current[part][field.field] = htmlToJson(field.translatableValue[0] || "");
900
- } else {
901
- current[part][field.field] = field.translatableValue[0];
902
- }
903
- });
904
- } else {
905
- current = current[part];
906
- currentExisting = currentExisting?.[part];
907
- }
908
- });
909
- }
910
- function processComponentFields(componentFieldsMap, acc, existingEntry, targetSchema) {
911
- componentFieldsMap.forEach((fields, namePath) => {
912
- if (!fields.length) return;
913
- const pathParts = namePath.split(".");
914
- const rootPath = pathParts[0];
915
- const schema = targetSchema.attributes?.[rootPath];
916
- if (schema?.repeatable) {
917
- acc[rootPath] = processRepeatableComponents(fields, existingEntry, rootPath);
918
- } else {
919
- processNestedComponents(fields, pathParts, existingEntry, acc);
958
+ if (translatable.type === "text") {
959
+ Logger$1.info("Merge text field " + field);
960
+ map[field] = translatable.translatableValue[0];
961
+ return true;
962
+ }
963
+ Logger$1.warn("Did not process " + field);
964
+ return false;
965
+ };
966
+ const mergeSimpleFields = function(translatables, existingEntry, targetSchema, map) {
967
+ let count = 0;
968
+ for (const candidate of translatables) {
969
+ const field = candidate.field;
970
+ if (!candidate.uuid && mergeValue(field, candidate, targetSchema.entry, map))
971
+ count++;
972
+ }
973
+ if (count > 0) {
974
+ Logger$1.info("Updated " + count + " simple text fields");
975
+ return true;
976
+ }
977
+ return false;
978
+ };
979
+ const buildMapOfUuids = function(existingEntry, schemaMap, map) {
980
+ if (typeof existingEntry !== "object")
981
+ return map;
982
+ const componentName = existingEntry["__component"] ?? "";
983
+ const schema = componentName && schemaMap.components[componentName] ? schemaMap.components[componentName] : null;
984
+ if (componentName && !schema)
985
+ Logger$1.warn("Cannot find component schema " + componentName);
986
+ for (const key of Object.keys(existingEntry)) {
987
+ if (key === "__component")
988
+ continue;
989
+ if (schema !== null && key === "__tsuid") {
990
+ map[existingEntry[key]] = {
991
+ entry: existingEntry,
992
+ schema
993
+ };
994
+ continue;
920
995
  }
921
- });
922
- return acc;
923
- }
924
- function transformFieldsToData(fields) {
925
- return fields.reduce((acc, field) => {
926
- if (!field.translatableValue) {
927
- acc[field.field] = "";
928
- return acc;
996
+ const child = existingEntry[key];
997
+ if (child) {
998
+ if (Array.isArray(child)) {
999
+ for (let e of child)
1000
+ buildMapOfUuids(e, schemaMap, map);
1001
+ } else
1002
+ buildMapOfUuids(child, schemaMap, map);
929
1003
  }
930
- if (field.realType === "blocks") {
931
- acc[field.field] = htmlToJson(field.translatableValue[0] || "");
932
- } else if (field.realType === "richtext") {
933
- acc[field.field] = field.translatableValue[0] || "";
934
- } else {
935
- acc[field.field] = Array.isArray(field.translatableValue) && field.translatableValue.length > 1 ? field.translatableValue.join(" ") : field.translatableValue[0] || "";
1004
+ }
1005
+ if (existingEntry["__tsuid"]) {
1006
+ delete existingEntry["__tsuid"];
1007
+ Logger$1.info("Removed cusom property __tsuid");
1008
+ }
1009
+ return map;
1010
+ };
1011
+ const mergeDynamicZones = function(translatables, schemaMap, existingEntry) {
1012
+ if (translatables.length === 0) {
1013
+ Logger$1.info("Skipping merging of dynamic zones, because none are present.");
1014
+ return;
1015
+ }
1016
+ const map = {};
1017
+ buildMapOfUuids(existingEntry, schemaMap, map);
1018
+ const mapSize = Object.keys(map).length;
1019
+ if (mapSize === 0) {
1020
+ Logger$1.warn("Could not create a uuid map");
1021
+ return false;
1022
+ }
1023
+ Logger$1.info("Built uuid map with " + mapSize + " entry(s)");
1024
+ let count = 0;
1025
+ for (const translatable of translatables) {
1026
+ if (!translatable.uuid)
1027
+ continue;
1028
+ const uuid = translatable.uuid;
1029
+ if (!map[uuid])
1030
+ continue;
1031
+ const entry = map[uuid];
1032
+ const schema = entry.schema;
1033
+ if (!schema) {
1034
+ Logger$1.warn("Cannot find schema by uuid #" + uuid);
1035
+ continue;
936
1036
  }
937
- return acc;
938
- }, {});
939
- }
940
- function processDynamicZones(dynamicZoneFields, acc, existingEntry) {
941
- if (dynamicZoneFields.size > 0) {
942
- const existingDynamicZone = existingEntry?.dynamiczone || [];
943
- acc.dynamiczone = Array.from(dynamicZoneFields.entries()).sort(([a], [b]) => a - b).map(([_, fields]) => {
944
- if (!fields[0].componentInfo) {
945
- console.warn(
946
- `Component info missing for dynamic zone field: ${fields[0].field}`
947
- );
948
- return null;
949
- }
950
- const { schemaName } = fields[0].componentInfo;
951
- const componentData = transformFieldsToData(fields);
952
- const matchingComponent = existingDynamicZone.find(
953
- (comp) => comp.__component === schemaName
954
- );
955
- return {
956
- __component: schemaName,
957
- ...componentData,
958
- ...matchingComponent?.id ? { id: matchingComponent.id } : {}
959
- };
960
- }).filter(Boolean);
1037
+ if (mergeValue(translatable.field, translatable, schema, entry.entry))
1038
+ count++;
961
1039
  }
962
- return acc;
963
- }
964
- function prepareImportData(translatables, existingEntry, targetSchema) {
965
- return translatables.reduce((acc, doc) => {
966
- const { regularFields, componentFieldsMap, dynamicZoneFields } = organizeFields(translatables);
967
- const withRegularFields = processRegularFields(regularFields, acc);
968
- const withComponentFields = processComponentFields(
969
- componentFieldsMap,
970
- withRegularFields,
971
- existingEntry,
972
- targetSchema
973
- );
974
- const withDynamicZones = processDynamicZones(
975
- dynamicZoneFields,
976
- withComponentFields,
977
- existingEntry
978
- );
979
- return withDynamicZones;
980
- }, {});
1040
+ if (count > 0) {
1041
+ Logger$1.info("Updated " + count + " entries in dynamic zones/content blocks");
1042
+ return true;
1043
+ }
1044
+ return false;
1045
+ };
1046
+ function prepareImportData(translatables, keepData, existingEntry, targetSchema) {
1047
+ const result = {};
1048
+ const simpleUpdated = mergeSimpleFields(translatables, existingEntry, targetSchema, result);
1049
+ let otherUpdated = false;
1050
+ const vsFields = replaceDynamicZones(existingEntry, keepData);
1051
+ if (vsFields.length > 0) {
1052
+ if (mergeDynamicZones(translatables, targetSchema, existingEntry)) {
1053
+ vsFields.forEach((field) => result[field] = existingEntry[field]);
1054
+ otherUpdated = true;
1055
+ } else
1056
+ Logger$1.warn("Could not merge dynamic fields");
1057
+ }
1058
+ if (simpleUpdated || otherUpdated)
1059
+ return result;
1060
+ else
1061
+ return null;
981
1062
  }
982
- require("jsonwebtoken");
983
- const crypto = require("crypto");
984
1063
  const TRANSLATIONTUDIO_URL = "https://strapi.translationstudio.tech";
985
1064
  const APP_NAME = "translationstudio";
1065
+ const Logger = {
1066
+ log: typeof strapi !== "undefined" ? strapi.log : console,
1067
+ info: (val) => Logger.log.info(val),
1068
+ warn: (val) => Logger.log.warn(val),
1069
+ error: (val) => Logger.log.error(val),
1070
+ debug: (val) => Logger.log.debug(val)
1071
+ };
986
1072
  const service = ({ strapi: strapi2 }) => {
987
1073
  const pluginStore = strapi2.store({
988
1074
  type: "plugin",
@@ -1000,7 +1086,7 @@ const service = ({ strapi: strapi2 }) => {
1000
1086
  if (typeof result === "string" && result !== "")
1001
1087
  return result;
1002
1088
  } catch (err) {
1003
- console.warn(err);
1089
+ strapi2.log.warn(err);
1004
1090
  }
1005
1091
  return TRANSLATIONTUDIO_URL;
1006
1092
  },
@@ -1063,9 +1149,15 @@ const service = ({ strapi: strapi2 }) => {
1063
1149
  },
1064
1150
  async exportData(payload) {
1065
1151
  const { contentTypeID, entryID, locale } = parsePayload(payload);
1066
- const contentType = await getContentType(contentTypeID);
1152
+ const contentType = getContentType(contentTypeID);
1153
+ if (contentType === null || !IsLocalisableSchema(contentType.entry)) {
1154
+ return {
1155
+ fields: [],
1156
+ keep: {}
1157
+ };
1158
+ }
1067
1159
  const entry = await getEntry(contentTypeID, entryID, locale);
1068
- const contentFields = await processEntryFields(entry, contentType.attributes);
1160
+ const contentFields = await processEntryFields(entry, contentType);
1069
1161
  return transformResponse(contentFields);
1070
1162
  },
1071
1163
  async importData(payload) {
@@ -1073,23 +1165,32 @@ const service = ({ strapi: strapi2 }) => {
1073
1165
  const sourceLocale = payload.source;
1074
1166
  const targetLocale = payload.target;
1075
1167
  try {
1076
- const existingEntry = await getEntry(contentTypeID, entryID, targetLocale);
1077
- const targetSchema = await getContentType(contentTypeID);
1078
- const data = prepareImportData(payload.document[0].fields, existingEntry, targetSchema);
1079
- if (targetSchema.pluginOptions.i18n.localized === true) {
1080
- await updateEntry(
1081
- contentTypeID,
1082
- entryID,
1083
- sourceLocale,
1084
- targetLocale,
1085
- data,
1086
- targetSchema.attributes
1087
- );
1088
- }
1089
- return { success: true };
1168
+ const sourceEntry = await getEntry(contentTypeID, entryID, sourceLocale);
1169
+ if (sourceEntry == null)
1170
+ throw new Error("Cannot find source entry " + contentTypeID + "::" + entryID + " in " + sourceLocale);
1171
+ const targetSchema = getContentType(contentTypeID);
1172
+ if (targetSchema === null || !IsLocalisableSchema(targetSchema.entry))
1173
+ throw new Error("Cannot find schema");
1174
+ const data = prepareImportData(
1175
+ payload.document[0].fields,
1176
+ payload.document[0].keep ?? {},
1177
+ sourceEntry,
1178
+ targetSchema
1179
+ );
1180
+ strapi2.log.info("Loading target language entry");
1181
+ const targetLocaleEntry = await getEntry(contentTypeID, entryID, targetLocale);
1182
+ appendMissingFields(data, sourceEntry, targetSchema, targetLocaleEntry);
1183
+ await updateEntry(
1184
+ contentTypeID,
1185
+ entryID,
1186
+ targetLocale,
1187
+ data
1188
+ );
1189
+ return true;
1090
1190
  } catch (error) {
1091
- return { success: false };
1191
+ strapi2.log.error(error);
1092
1192
  }
1193
+ return false;
1093
1194
  },
1094
1195
  async requestTranslation(payload) {
1095
1196
  const { license } = await this.getLicense();