@translationstudio/translationstudio-strapi-extension 1.1.3 → 2.0.0

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,40 +112,40 @@ 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);
129
148
  ctx.body = result;
130
- },
131
- async getEntryData(ctx) {
132
- const { uid, locale } = ctx.request.body;
133
- if (!uid) {
134
- return ctx.badRequest("Missing uid parameter");
135
- }
136
- try {
137
- const [contentTypeID, entryID] = uid.split("#");
138
- const entry = await strapi2.plugin(APP_NAME$1).service("service").getEntryData(contentTypeID, entryID, locale);
139
- return entry;
140
- } catch (error) {
141
- return ctx.badRequest("Failed to get entry data", { error: error.message });
142
- }
143
149
  }
144
150
  });
145
151
  const controllers = {
@@ -255,23 +261,35 @@ const routes = [
255
261
  config: {
256
262
  policies: []
257
263
  }
258
- },
259
- {
260
- method: "POST",
261
- path: "/entrydata",
262
- handler: "controller.getEntryData",
263
- config: {
264
- policies: []
265
- }
266
264
  }
267
265
  ];
268
- const getContentType = async (contentTypeID) => {
269
- const contentType = await strapi.contentType(contentTypeID);
266
+ function getComponentSchemata(schema) {
267
+ const res = {};
268
+ for (let fieldName in schema.attributes) {
269
+ const field = schema.attributes[fieldName];
270
+ if (!field.components || !Array.isArray(field.components) || field.components.length === 0)
271
+ continue;
272
+ for (let type of field.components) {
273
+ const schema2 = strapi.components[type];
274
+ if (schema2 && schema2.attributes)
275
+ res[type] = schema2;
276
+ }
277
+ }
278
+ return res;
279
+ }
280
+ function getContentType(contentTypeID) {
281
+ const contentType = strapi.contentType(contentTypeID);
270
282
  if (!contentType?.attributes) {
271
- throw new Error(`Content type or schema not found: ${contentTypeID}`);
283
+ strapi.log.error(`Content type or schema not found: ${contentTypeID}`);
284
+ return null;
272
285
  }
273
- return contentType;
274
- };
286
+ const res = {
287
+ entry: contentType,
288
+ components: getComponentSchemata(contentType)
289
+ };
290
+ strapi.log.info("SChema loaded for " + contentTypeID + ". Component schemata loaded: " + Object.keys(res.components).length);
291
+ return res;
292
+ }
275
293
  const parsePayload = (payload) => {
276
294
  const [contentTypeID, entryID] = payload.element.includes("#") ? payload.element.split("#") : [payload.element, void 0];
277
295
  const locale = payload.source.includes("-") ? payload.source.split("-")[0] : payload.source;
@@ -281,7 +299,7 @@ const getComponentSchema = async (componentName) => {
281
299
  try {
282
300
  return await strapi.components[componentName];
283
301
  } catch (error) {
284
- console.error(`Failed to get component schema for ${componentName}:`, error);
302
+ strapi.log.error(`Failed to get component schema for ${componentName}:`, error);
285
303
  return null;
286
304
  }
287
305
  };
@@ -304,27 +322,32 @@ const buildPopulateConfig = async (schema) => {
304
322
  }
305
323
  return populate;
306
324
  };
307
- const getEntry = async (contentTypeID, entryID, locale) => {
325
+ const getEntry = async (contentTypeID, entryID, locale, logError = true) => {
308
326
  try {
309
327
  const contentType = await strapi.contentTypes[contentTypeID];
310
328
  const populateConfig = await buildPopulateConfig(contentType);
311
329
  const query = {
312
330
  locale,
331
+ documentId: entryID,
313
332
  populate: populateConfig
314
333
  };
315
- if (entryID) {
316
- Object.assign(query, { documentId: entryID });
317
- }
318
334
  const entry = await strapi.documents(contentTypeID).findFirst(query);
319
- return entry;
335
+ if (entry) {
336
+ strapi.log.info("Obtained " + contentTypeID + "::" + entryID + " in " + locale);
337
+ return entry;
338
+ }
320
339
  } catch (error) {
321
- console.error("Entry fetch error:", error);
322
- return null;
340
+ strapi.log.error(error.message ?? error);
323
341
  }
342
+ strapi.log.warn("Could not find entry " + entryID + " in locale " + locale);
343
+ return null;
344
+ };
345
+ const transformResponse = function(data) {
346
+ data.fields = data.fields.map(
347
+ (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
348
+ );
349
+ return data;
324
350
  };
325
- const transformResponse = (data) => data.map(
326
- (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
327
- );
328
351
  function jsonToHtml(json) {
329
352
  if (!json || !Array.isArray(json)) {
330
353
  return "";
@@ -373,43 +396,37 @@ function formatText(child) {
373
396
  }
374
397
  return text;
375
398
  }
376
- const processComponent = async (fieldName, componentName, value, schemaName, componentId) => {
399
+ const Logger$4 = {
400
+ log: typeof strapi !== "undefined" ? strapi.log : console,
401
+ info: (val) => Logger$4.log.info(val),
402
+ warn: (val) => Logger$4.log.warn(val),
403
+ error: (val) => Logger$4.log.error(val),
404
+ debug: (val) => Logger$4.log.debug(val)
405
+ };
406
+ async function processComponent(fieldName, value, componentSchema, schemata) {
377
407
  const contentFields = [];
378
- const componentSchema = await strapi.components[componentName];
379
- if (!componentSchema) {
380
- throw new Error(`Component schema not found for ${componentName}`);
381
- }
408
+ Logger$4.info("Processing dynamic field " + fieldName);
409
+ if (!componentSchema || !componentSchema.attributes)
410
+ return [];
382
411
  const schemaAttributes = componentSchema.attributes || {};
383
412
  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,
413
+ const candidates = Array.isArray(dataToProcess) ? dataToProcess : [dataToProcess];
414
+ for (const item of candidates) {
415
+ const processedFields = await processComponentFields(
416
+ item,
399
417
  schemaAttributes,
400
418
  fieldName,
401
- componentName,
402
- schemaName,
403
- componentId
419
+ schemata
404
420
  );
405
- contentFields.push(...processedFields);
421
+ if (processedFields.length > 0)
422
+ contentFields.push(...processedFields);
406
423
  }
407
424
  return contentFields;
408
- };
425
+ }
409
426
  const shouldSkipField$1 = (key, fieldSchema) => {
410
427
  return key === "id" || fieldSchema.private;
411
428
  };
412
- const isTranslatableField$1 = (type) => {
429
+ const isTranslatableField = (type) => {
413
430
  return ["string", "text", "blocks", "richtext"].includes(type);
414
431
  };
415
432
  const getTranslatedValue = (type, value) => {
@@ -418,64 +435,61 @@ const getTranslatedValue = (type, value) => {
418
435
  }
419
436
  return value.toString();
420
437
  };
421
- const buildTranslatable = (key, fieldSchema, value, parentPath, componentId, schemaName) => {
438
+ const buildTranslatable = (key, fieldSchema, value, uuid = "") => {
422
439
  return {
423
440
  field: key,
424
441
  type: ["richtext", "blocks"].includes(fieldSchema.type) ? "html" : "text",
425
442
  translatableValue: [value],
426
443
  realType: fieldSchema.type,
427
- componentInfo: {
428
- namePath: parentPath,
429
- id: componentId,
430
- schemaName
431
- }
444
+ uuid
432
445
  };
433
446
  };
434
- const processComponentFields$1 = async (componentData, schema, parentField, componentName, schemaName, componentId) => {
447
+ const processComponentFields = async (componentData, schema, parentField, schemata) => {
435
448
  const contentFields = [];
436
- const parentPath = parentField.split(".");
449
+ const uuid = crypto.randomUUID();
437
450
  for (const [key, fieldSchema] of Object.entries(schema)) {
438
451
  if (shouldSkipField$1(key, fieldSchema)) continue;
439
452
  const value = componentData?.[key];
440
- const fieldPath = `${parentField}.${key}`;
453
+ if (!value)
454
+ continue;
441
455
  if (fieldSchema.type === "component") {
442
- if (!fieldSchema.component) continue;
456
+ if (!value.__component)
457
+ continue;
458
+ const targetSchema = schemata[value.__component];
459
+ if (!targetSchema)
460
+ continue;
443
461
  const nestedFields = await processComponent(
444
- fieldPath,
445
- fieldSchema.component,
446
- value || {},
447
- fieldSchema.component,
448
- value?.id
462
+ `${parentField}.${key}`,
463
+ value,
464
+ targetSchema,
465
+ schemata
449
466
  );
450
- contentFields.push(...nestedFields);
467
+ if (nestedFields.length > 0)
468
+ contentFields.push(...nestedFields);
451
469
  continue;
452
470
  }
453
- if (!isTranslatableField$1(fieldSchema.type)) continue;
471
+ if (!isTranslatableField(fieldSchema.type)) continue;
454
472
  if (value === null || value === void 0 || value === "") continue;
455
473
  const translatedValue = getTranslatedValue(fieldSchema.type, value);
456
474
  const translatable = buildTranslatable(
457
475
  key,
458
476
  fieldSchema,
459
477
  translatedValue,
460
- parentPath,
461
- componentId,
462
- schemaName
478
+ uuid
463
479
  );
480
+ componentData.__tsuid = uuid;
464
481
  contentFields.push(translatable);
465
482
  }
466
483
  return contentFields;
467
484
  };
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;
485
+ const Logger$3 = {
486
+ log: typeof strapi !== "undefined" ? strapi.log : console,
487
+ info: (val) => Logger$3.log.info(val),
488
+ warn: (val) => Logger$3.log.warn(val),
489
+ error: (val) => Logger$3.log.error(val),
490
+ debug: (val) => Logger$3.log.debug(val)
477
491
  };
478
- const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
492
+ const DEFAULT_FIELDS$1 = /* @__PURE__ */ new Set([
479
493
  "id",
480
494
  "documentId",
481
495
  "createdAt",
@@ -487,45 +501,41 @@ const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
487
501
  "createdBy"
488
502
  ]);
489
503
  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) => {
504
+ const isSimpleTranslatableField = (fieldSchema) => ["string", "text", "blocks", "richtext"].includes(fieldSchema.type) && fieldSchema.pluginOptions?.i18n?.localized === true;
505
+ const processDynamicZone = async (key, value, schemata) => {
492
506
  const results = [];
493
507
  for (const component of value) {
494
508
  const componentName = component?.__component;
495
509
  if (!componentName) continue;
510
+ const schema = schemata[componentName];
511
+ if (!schema) continue;
496
512
  const fields = await processComponent(
497
513
  key,
498
- componentName,
499
514
  component,
500
- componentName,
501
- component.id
515
+ schema,
516
+ schemata
502
517
  );
503
- results.push(...fields);
518
+ if (fields.length > 0)
519
+ results.push(...fields);
504
520
  }
505
521
  return results;
506
522
  };
507
- const processComponentField = async (key, value, fieldSchema) => {
523
+ const processComponentField = async (key, value, fieldSchema, schemata) => {
508
524
  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 {
525
+ const candidates = Array.isArray(value) ? value : [value];
526
+ for (const component of candidates) {
527
+ const componentName = component?.__component;
528
+ if (!componentName) continue;
529
+ const schema = schemata[componentName];
530
+ if (!schema) continue;
521
531
  const fields = await processComponent(
522
532
  key,
523
- fieldSchema.component,
524
- value,
525
- fieldSchema.component,
526
- value.id
533
+ component,
534
+ schema,
535
+ schemata
527
536
  );
528
- results.push(...fields);
537
+ if (fields.length > 0)
538
+ results.push(...fields);
529
539
  }
530
540
  return results;
531
541
  };
@@ -538,38 +548,148 @@ const processRegularField = (key, value, fieldSchema) => {
538
548
  realType: fieldSchema.type
539
549
  };
540
550
  };
541
- const processEntryFields = async (entry, schema, locale) => {
551
+ function IsLocalisableSchema(schema) {
552
+ return schema.pluginOptions?.i18n?.localized === true;
553
+ }
554
+ function IsLocalisedField(field) {
555
+ return field.pluginOptions?.i18n?.localized === true;
556
+ }
557
+ const processEntryFields = async (entry, schemaData, _locale) => {
542
558
  const contentFields = [];
559
+ const staticContent = {};
560
+ const schema = schemaData.entry.attributes;
543
561
  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);
562
+ if (shouldSkipField(key, value))
550
563
  continue;
551
- }
552
- if (isComponent(fieldSchema, value, schema)) {
553
- const componentFields = await processComponentField(key, value, fieldSchema);
554
- contentFields.push(...componentFields);
564
+ const fieldSchema = schema[key];
565
+ if (!fieldSchema || !IsLocalisedField(fieldSchema)) {
566
+ Logger$3.debug("SKipping non-local field " + key);
555
567
  continue;
556
568
  }
557
- if (isTranslatableField(fieldSchema)) {
569
+ if (isSimpleTranslatableField(fieldSchema)) {
570
+ Logger$3.debug("Processing simple field " + key);
558
571
  const translatedField = processRegularField(key, value, fieldSchema);
559
572
  contentFields.push(translatedField);
573
+ continue;
574
+ }
575
+ const zoneInfo = isDynamicZone(fieldSchema, value);
576
+ if (zoneInfo.isZone) {
577
+ if (zoneInfo.hasContent) {
578
+ Logger$3.debug("Processing dynamic zone field " + key);
579
+ const zoneFields = await processDynamicZone(key, value, schemaData.components);
580
+ contentFields.push(...zoneFields);
581
+ staticContent[key] = value;
582
+ }
583
+ continue;
584
+ }
585
+ const componentInfo = isComponent(fieldSchema);
586
+ if (componentInfo.isZone) {
587
+ const componentFields = await processComponentField(key, value, fieldSchema, schemaData.components);
588
+ contentFields.push(...componentFields);
589
+ staticContent[key] = value;
560
590
  }
561
591
  }
562
- return contentFields;
592
+ Logger$3.info("Process completed");
593
+ return {
594
+ fields: contentFields,
595
+ keep: staticContent
596
+ };
563
597
  };
564
598
  const shouldSkipField = (key, value) => {
565
- return DEFAULT_FIELDS.has(key) || isEmpty(value);
599
+ return DEFAULT_FIELDS$1.has(key) || isEmpty(value);
566
600
  };
567
- const isDynamicZone = (fieldSchema, value, schema) => {
568
- return fieldSchema.type === "dynamiczone" && isFieldLocalizable(fieldSchema, schema) && Array.isArray(value);
601
+ const isDynamicZone = (fieldSchema, value) => {
602
+ return {
603
+ isZone: fieldSchema.type === "dynamiczone",
604
+ hasContent: Array.isArray(value) && value.length > 0
605
+ };
606
+ };
607
+ const isComponent = (fieldSchema) => {
608
+ return {
609
+ isZone: fieldSchema.type === "dynamiczone",
610
+ hasContent: true
611
+ };
612
+ };
613
+ const Logger$2 = {
614
+ log: typeof strapi !== "undefined" ? strapi.log : console,
615
+ info: (val) => Logger$2.log.info(val),
616
+ warn: (val) => Logger$2.log.warn(val),
617
+ error: (val) => Logger$2.log.error(val),
618
+ debug: (val) => Logger$2.log.debug(val)
619
+ };
620
+ const DEFAULT_FIELDS = [
621
+ "id",
622
+ "documentId",
623
+ "createdAt",
624
+ "updatedAt",
625
+ ,
626
+ "createdBy",
627
+ "updatedBy",
628
+ "publishedAt",
629
+ "locale",
630
+ "localizations"
631
+ ];
632
+ const getContentFields = function(schema) {
633
+ const nullFields = [];
634
+ for (let field in schema) {
635
+ if (!DEFAULT_FIELDS.includes(field))
636
+ nullFields.push(field);
637
+ }
638
+ return nullFields;
569
639
  };
570
- const isComponent = (fieldSchema, value, schema) => {
571
- return fieldSchema.type === "component" && isFieldLocalizable(fieldSchema, schema);
640
+ const getInvalidOrNullFields = function(document, schema) {
641
+ if (!document)
642
+ return getContentFields(schema);
643
+ const nullFields = [];
644
+ let fieldsValid = 0;
645
+ for (let field in document) {
646
+ if (DEFAULT_FIELDS.includes(field))
647
+ continue;
648
+ if (document[field] === null)
649
+ nullFields.push(field);
650
+ else
651
+ fieldsValid++;
652
+ }
653
+ if (nullFields.length > 0 || fieldsValid > 0)
654
+ return nullFields;
655
+ return getContentFields(schema);
572
656
  };
657
+ function appendMissingFields(data, sourceEntry, targetSchema, targetEntry) {
658
+ const nullFields = getInvalidOrNullFields(targetEntry, targetSchema.entry.attributes);
659
+ if (nullFields.length === 0)
660
+ return;
661
+ let count = 0;
662
+ Logger$2.info("Adding missing fields to new locale: " + nullFields.join(", "));
663
+ for (let field of nullFields) {
664
+ if (data[field]) {
665
+ Logger$2.info("Field already present: " + field);
666
+ continue;
667
+ }
668
+ if (!sourceEntry[field]) {
669
+ Logger$2.info("No valid source langauge field value for " + field + " - skipping it.");
670
+ continue;
671
+ }
672
+ if (!targetSchema.entry.attributes[field]) {
673
+ Logger$2.warn("Schema does not contain field " + field);
674
+ continue;
675
+ }
676
+ Logger$2.info("Adding missing field and value for " + field);
677
+ data[field] = sourceEntry[field];
678
+ count++;
679
+ }
680
+ if (count > 0)
681
+ Logger$2.info(count + " missing fields added.");
682
+ }
683
+ async function updateEntry(contentTypeID, entryID, targetLocale, data) {
684
+ strapi.log.info("Updating target entry " + contentTypeID + "::" + entryID + " in locale " + targetLocale);
685
+ const newEntry = await strapi.documents(contentTypeID).update({
686
+ documentId: entryID,
687
+ locale: targetLocale,
688
+ data
689
+ });
690
+ if (!newEntry)
691
+ throw new Error("Cannot update target entry " + contentTypeID + "::" + entryID + " in locale " + targetLocale);
692
+ }
573
693
  function htmlToJson(html) {
574
694
  function parseHTML(html2) {
575
695
  const elements2 = [];
@@ -762,227 +882,171 @@ function htmlToJson(html) {
762
882
  }
763
883
  return blocks;
764
884
  }
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);
885
+ const Logger$1 = {
886
+ log: typeof strapi !== "undefined" ? strapi.log : console,
887
+ info: (val) => Logger$1.log.info(val),
888
+ warn: (val) => Logger$1.log.warn(val),
889
+ error: (val) => Logger$1.log.error(val),
890
+ debug: (val) => Logger$1.log.debug(val)
891
+ };
892
+ const removeComponentIds = function(elem) {
893
+ const list = Array.isArray(elem) ? elem : [elem];
894
+ for (let obj of list) {
895
+ if (obj.__component && obj.id)
896
+ delete obj.id;
897
+ for (let key in obj) {
898
+ const child = obj[key];
899
+ if (Array.isArray(child) && Array.length > 0)
900
+ removeComponentIds(child);
901
+ else if (typeof child === "object")
902
+ removeComponentIds(child);
781
903
  }
782
904
  }
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];
905
+ };
906
+ function replaceDynamicZones(strapiEntry, replacableFields) {
907
+ const fields = [];
908
+ for (let key in strapiEntry) {
909
+ if (replacableFields[key]) {
910
+ removeComponentIds(replacableFields[key]);
911
+ strapiEntry[key] = replacableFields[key];
912
+ fields.push(key);
787
913
  }
788
914
  }
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
- }
915
+ if (fields.length > 0)
916
+ Logger$1.info(fields.length + " dynamic fields/components replaced for later merge: " + fields.join(", "));
917
+ return fields;
800
918
  }
801
- function processDataRecursively(data, schema) {
802
- if (!data || typeof data !== "object") {
803
- return data;
919
+ const mergeValue = function(field, translatable, targetSchema, map) {
920
+ if (translatable.translatableValue.length === 0)
921
+ return false;
922
+ if (targetSchema.attributes[field] === void 0) {
923
+ Logger$1.info("Field " + field + " does not exist in schema. Skipping it");
924
+ Logger$1.info(targetSchema);
925
+ return false;
804
926
  }
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));
927
+ if (translatable.translatableValue[0] === "") {
928
+ Logger$1.info("Skipping empty translated content for field " + field);
929
+ return false;
820
930
  }
821
- const result = {};
822
- for (const key in data) {
823
- result[key] = processDataRecursively(data[key]);
931
+ if (translatable.realType === "blocks") {
932
+ Logger$1.info("Merge block field " + field);
933
+ map[field] = htmlToJson(translatable.translatableValue[0] || "");
934
+ return true;
824
935
  }
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);
936
+ if (translatable.type === "text") {
937
+ Logger$1.info("Merge text field " + field);
938
+ map[field] = translatable.translatableValue[0];
939
+ return true;
940
+ }
941
+ Logger$1.warn("Did not process " + field);
942
+ return false;
943
+ };
944
+ const mergeSimpleFields = function(translatables, existingEntry, targetSchema, map) {
945
+ let count = 0;
946
+ for (const candidate of translatables) {
947
+ const field = candidate.field;
948
+ if (!candidate.uuid && mergeValue(field, candidate, targetSchema.entry, map))
949
+ count++;
950
+ }
951
+ if (count > 0) {
952
+ Logger$1.info("Updated " + count + " simple text fields");
953
+ return true;
954
+ }
955
+ return false;
956
+ };
957
+ const buildMapOfUuids = function(existingEntry, schemaMap, map) {
958
+ if (typeof existingEntry !== "object")
959
+ return map;
960
+ const componentName = existingEntry["__component"] ?? "";
961
+ const schema = componentName && schemaMap.components[componentName] ? schemaMap.components[componentName] : null;
962
+ if (componentName && !schema)
963
+ Logger$1.warn("Cannot find component schema " + componentName);
964
+ for (const key of Object.keys(existingEntry)) {
965
+ if (key === "__component")
966
+ continue;
967
+ if (schema !== null && key === "__tsuid") {
968
+ map[existingEntry[key]] = {
969
+ entry: existingEntry,
970
+ schema
971
+ };
972
+ continue;
920
973
  }
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;
974
+ const child = existingEntry[key];
975
+ if (child) {
976
+ if (Array.isArray(child)) {
977
+ for (let e of child)
978
+ buildMapOfUuids(e, schemaMap, map);
979
+ } else
980
+ buildMapOfUuids(child, schemaMap, map);
929
981
  }
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] || "";
982
+ }
983
+ if (existingEntry["__tsuid"]) {
984
+ delete existingEntry["__tsuid"];
985
+ Logger$1.info("Removed cusom property __tsuid");
986
+ }
987
+ return map;
988
+ };
989
+ const mergeDynamicZones = function(translatables, schemaMap, existingEntry) {
990
+ if (translatables.length === 0) {
991
+ Logger$1.info("Skipping merging of dynamic zones, because none are present.");
992
+ return;
993
+ }
994
+ const map = {};
995
+ buildMapOfUuids(existingEntry, schemaMap, map);
996
+ const mapSize = Object.keys(map).length;
997
+ if (mapSize === 0) {
998
+ Logger$1.warn("Could not create a uuid map");
999
+ return false;
1000
+ }
1001
+ Logger$1.info("Built uuid map with " + mapSize + " entry(s)");
1002
+ let count = 0;
1003
+ for (const translatable of translatables) {
1004
+ if (!translatable.uuid)
1005
+ continue;
1006
+ const uuid = translatable.uuid;
1007
+ if (!map[uuid])
1008
+ continue;
1009
+ const entry = map[uuid];
1010
+ const schema = entry.schema;
1011
+ if (!schema) {
1012
+ Logger$1.warn("Cannot find schema by uuid #" + uuid);
1013
+ continue;
936
1014
  }
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);
1015
+ if (mergeValue(translatable.field, translatable, schema, entry.entry))
1016
+ count++;
961
1017
  }
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
- }, {});
1018
+ if (count > 0) {
1019
+ Logger$1.info("Updated " + count + " entries in dynamic zones/content blocks");
1020
+ return true;
1021
+ }
1022
+ return false;
1023
+ };
1024
+ function prepareImportData(translatables, keepData, existingEntry, targetSchema) {
1025
+ const result = {};
1026
+ const simpleUpdated = mergeSimpleFields(translatables, existingEntry, targetSchema, result);
1027
+ let otherUpdated = false;
1028
+ const vsFields = replaceDynamicZones(existingEntry, keepData);
1029
+ if (vsFields.length > 0) {
1030
+ if (mergeDynamicZones(translatables, targetSchema, existingEntry)) {
1031
+ vsFields.forEach((field) => result[field] = existingEntry[field]);
1032
+ otherUpdated = true;
1033
+ } else
1034
+ Logger$1.warn("Could not merge dynamic fields");
1035
+ }
1036
+ if (simpleUpdated || otherUpdated)
1037
+ return result;
1038
+ else
1039
+ return null;
981
1040
  }
982
- require("jsonwebtoken");
983
- const crypto = require("crypto");
984
1041
  const TRANSLATIONTUDIO_URL = "https://strapi.translationstudio.tech";
985
1042
  const APP_NAME = "translationstudio";
1043
+ const Logger = {
1044
+ log: typeof strapi !== "undefined" ? strapi.log : console,
1045
+ info: (val) => Logger.log.info(val),
1046
+ warn: (val) => Logger.log.warn(val),
1047
+ error: (val) => Logger.log.error(val),
1048
+ debug: (val) => Logger.log.debug(val)
1049
+ };
986
1050
  const service = ({ strapi: strapi2 }) => {
987
1051
  const pluginStore = strapi2.store({
988
1052
  type: "plugin",
@@ -1000,7 +1064,7 @@ const service = ({ strapi: strapi2 }) => {
1000
1064
  if (typeof result === "string" && result !== "")
1001
1065
  return result;
1002
1066
  } catch (err) {
1003
- console.warn(err);
1067
+ strapi2.log.warn(err);
1004
1068
  }
1005
1069
  return TRANSLATIONTUDIO_URL;
1006
1070
  },
@@ -1063,9 +1127,15 @@ const service = ({ strapi: strapi2 }) => {
1063
1127
  },
1064
1128
  async exportData(payload) {
1065
1129
  const { contentTypeID, entryID, locale } = parsePayload(payload);
1066
- const contentType = await getContentType(contentTypeID);
1130
+ const contentType = getContentType(contentTypeID);
1131
+ if (contentType === null || !IsLocalisableSchema(contentType.entry)) {
1132
+ return {
1133
+ fields: [],
1134
+ keep: {}
1135
+ };
1136
+ }
1067
1137
  const entry = await getEntry(contentTypeID, entryID, locale);
1068
- const contentFields = await processEntryFields(entry, contentType.attributes);
1138
+ const contentFields = await processEntryFields(entry, contentType);
1069
1139
  return transformResponse(contentFields);
1070
1140
  },
1071
1141
  async importData(payload) {
@@ -1073,23 +1143,32 @@ const service = ({ strapi: strapi2 }) => {
1073
1143
  const sourceLocale = payload.source;
1074
1144
  const targetLocale = payload.target;
1075
1145
  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 };
1146
+ const sourceEntry = await getEntry(contentTypeID, entryID, sourceLocale);
1147
+ if (sourceEntry == null)
1148
+ throw new Error("Cannot find source entry " + contentTypeID + "::" + entryID + " in " + sourceLocale);
1149
+ const targetSchema = getContentType(contentTypeID);
1150
+ if (targetSchema === null || !IsLocalisableSchema(targetSchema.entry))
1151
+ throw new Error("Cannot find schema");
1152
+ const data = prepareImportData(
1153
+ payload.document[0].fields,
1154
+ payload.document[0].keep ?? {},
1155
+ sourceEntry,
1156
+ targetSchema
1157
+ );
1158
+ strapi2.log.info("Loading target language entry");
1159
+ const targetLocaleEntry = await getEntry(contentTypeID, entryID, targetLocale);
1160
+ appendMissingFields(data, sourceEntry, targetSchema, targetLocaleEntry);
1161
+ await updateEntry(
1162
+ contentTypeID,
1163
+ entryID,
1164
+ targetLocale,
1165
+ data
1166
+ );
1167
+ return true;
1090
1168
  } catch (error) {
1091
- return { success: false };
1169
+ strapi2.log.error(error);
1092
1170
  }
1171
+ return false;
1093
1172
  },
1094
1173
  async requestTranslation(payload) {
1095
1174
  const { license } = await this.getLicense();
@@ -1125,10 +1204,6 @@ const service = ({ strapi: strapi2 }) => {
1125
1204
  },
1126
1205
  async ping() {
1127
1206
  return;
1128
- },
1129
- async getEntryData(contentTypeID, entryID, locale) {
1130
- const entry = await getEntry(contentTypeID, entryID, locale);
1131
- return entry;
1132
1207
  }
1133
1208
  };
1134
1209
  };