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