@translationstudio/translationstudio-strapi-extension 1.1.1 → 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,50 +76,76 @@ 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
+ }
88
+ },
89
+ async setDevelopmentUrl(ctx) {
90
+ const url = ctx.request.body.url;
91
+ const result = await strapi2.plugin(APP_NAME$1).service("service").setDevelopmentUrl(url);
92
+ if (result) {
93
+ ctx.status = 200;
94
+ ctx.body = { success: true };
95
+ } else {
96
+ ctx.status = 500;
97
+ ctx.body = { success: false };
98
+ }
99
+ },
100
+ async getDevelopmentUrl(ctx) {
101
+ const url = await strapi2.plugin(APP_NAME$1).service("service").getDevelopmentUrl();
102
+ if (url) {
103
+ ctx.status = 200;
104
+ ctx.body = { url };
105
+ } else {
106
+ ctx.status = 404;
107
+ ctx.body = { url: "" };
108
+ }
82
109
  },
83
110
  async importData(ctx) {
84
111
  if (!await this.validateToken(ctx)) {
85
112
  ctx.status = 400;
86
113
  return;
87
114
  }
88
- const payload = JSON.parse(ctx.request.body);
89
- const result = await strapi2.plugin(APP_NAME$1).service("service").importData(payload);
90
- 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;
91
127
  },
92
128
  async ping(ctx) {
93
- const result = await strapi2.plugin(APP_NAME$1).service("service").ping();
129
+ await strapi2.plugin(APP_NAME$1).service("service").ping();
94
130
  ctx.status = 204;
95
- ctx.body = result;
96
131
  },
97
132
  async getLanguages(ctx) {
98
133
  if (!await this.validateToken(ctx)) {
99
134
  ctx.status = 400;
100
135
  return;
101
136
  }
102
- const result = await strapi2.plugin(APP_NAME$1).service("service").getLanguages();
103
- ctx.status = 200;
104
- 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
+ }
105
145
  },
106
146
  async getEmail(ctx) {
107
147
  const result = await strapi2.plugin(APP_NAME$1).service("service").getEmail(ctx);
108
148
  ctx.body = result;
109
- },
110
- async getEntryData(ctx) {
111
- const { uid, locale } = ctx.request.body;
112
- if (!uid) {
113
- return ctx.badRequest("Missing uid parameter");
114
- }
115
- try {
116
- const [contentTypeID, entryID] = uid.split("#");
117
- const entry = await strapi2.plugin(APP_NAME$1).service("service").getEntryData(contentTypeID, entryID, locale);
118
- return entry;
119
- } catch (error) {
120
- return ctx.badRequest("Failed to get entry data", { error: error.message });
121
- }
122
149
  }
123
150
  });
124
151
  const controllers = {
@@ -143,6 +170,22 @@ const routes = [
143
170
  policies: []
144
171
  }
145
172
  },
173
+ {
174
+ method: "GET",
175
+ path: "/devurl",
176
+ handler: "controller.getDevelopmentUrl",
177
+ config: {
178
+ policies: []
179
+ }
180
+ },
181
+ {
182
+ method: "POST",
183
+ path: "/devurl",
184
+ handler: "controller.setDevelopmentUrl",
185
+ config: {
186
+ policies: []
187
+ }
188
+ },
146
189
  {
147
190
  method: "GET",
148
191
  path: "/getToken",
@@ -218,23 +261,35 @@ const routes = [
218
261
  config: {
219
262
  policies: []
220
263
  }
221
- },
222
- {
223
- method: "POST",
224
- path: "/entrydata",
225
- handler: "controller.getEntryData",
226
- config: {
227
- policies: []
228
- }
229
264
  }
230
265
  ];
231
- const getContentType = async (contentTypeID) => {
232
- 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);
233
282
  if (!contentType?.attributes) {
234
- throw new Error(`Content type or schema not found: ${contentTypeID}`);
283
+ strapi.log.error(`Content type or schema not found: ${contentTypeID}`);
284
+ return null;
235
285
  }
236
- return contentType;
237
- };
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
+ }
238
293
  const parsePayload = (payload) => {
239
294
  const [contentTypeID, entryID] = payload.element.includes("#") ? payload.element.split("#") : [payload.element, void 0];
240
295
  const locale = payload.source.includes("-") ? payload.source.split("-")[0] : payload.source;
@@ -244,7 +299,7 @@ const getComponentSchema = async (componentName) => {
244
299
  try {
245
300
  return await strapi.components[componentName];
246
301
  } catch (error) {
247
- console.error(`Failed to get component schema for ${componentName}:`, error);
302
+ strapi.log.error(`Failed to get component schema for ${componentName}:`, error);
248
303
  return null;
249
304
  }
250
305
  };
@@ -267,27 +322,32 @@ const buildPopulateConfig = async (schema) => {
267
322
  }
268
323
  return populate;
269
324
  };
270
- const getEntry = async (contentTypeID, entryID, locale) => {
325
+ const getEntry = async (contentTypeID, entryID, locale, logError = true) => {
271
326
  try {
272
327
  const contentType = await strapi.contentTypes[contentTypeID];
273
328
  const populateConfig = await buildPopulateConfig(contentType);
274
329
  const query = {
275
330
  locale,
331
+ documentId: entryID,
276
332
  populate: populateConfig
277
333
  };
278
- if (entryID) {
279
- Object.assign(query, { documentId: entryID });
280
- }
281
334
  const entry = await strapi.documents(contentTypeID).findFirst(query);
282
- return entry;
335
+ if (entry) {
336
+ strapi.log.info("Obtained " + contentTypeID + "::" + entryID + " in " + locale);
337
+ return entry;
338
+ }
283
339
  } catch (error) {
284
- console.error("Entry fetch error:", error);
285
- return null;
340
+ strapi.log.error(error.message ?? error);
286
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;
287
350
  };
288
- const transformResponse = (data) => data.map(
289
- (item) => item.realType === "blocks" && Array.isArray(item.translatableValue[0]) ? { ...item, translatableValue: item.translatableValue[0] } : item
290
- );
291
351
  function jsonToHtml(json) {
292
352
  if (!json || !Array.isArray(json)) {
293
353
  return "";
@@ -336,43 +396,37 @@ function formatText(child) {
336
396
  }
337
397
  return text;
338
398
  }
339
- 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) {
340
407
  const contentFields = [];
341
- const componentSchema = await strapi.components[componentName];
342
- if (!componentSchema) {
343
- throw new Error(`Component schema not found for ${componentName}`);
344
- }
408
+ Logger$4.info("Processing dynamic field " + fieldName);
409
+ if (!componentSchema || !componentSchema.attributes)
410
+ return [];
345
411
  const schemaAttributes = componentSchema.attributes || {};
346
412
  const dataToProcess = value || {};
347
- if (Array.isArray(dataToProcess)) {
348
- for (const item of dataToProcess) {
349
- const processedFields = await processComponentFields$1(
350
- item,
351
- schemaAttributes,
352
- fieldName,
353
- componentName,
354
- schemaName,
355
- item.id
356
- );
357
- contentFields.push(...processedFields);
358
- }
359
- } else {
360
- const processedFields = await processComponentFields$1(
361
- dataToProcess,
413
+ const candidates = Array.isArray(dataToProcess) ? dataToProcess : [dataToProcess];
414
+ for (const item of candidates) {
415
+ const processedFields = await processComponentFields(
416
+ item,
362
417
  schemaAttributes,
363
418
  fieldName,
364
- componentName,
365
- schemaName,
366
- componentId
419
+ schemata
367
420
  );
368
- contentFields.push(...processedFields);
421
+ if (processedFields.length > 0)
422
+ contentFields.push(...processedFields);
369
423
  }
370
424
  return contentFields;
371
- };
425
+ }
372
426
  const shouldSkipField$1 = (key, fieldSchema) => {
373
427
  return key === "id" || fieldSchema.private;
374
428
  };
375
- const isTranslatableField$1 = (type) => {
429
+ const isTranslatableField = (type) => {
376
430
  return ["string", "text", "blocks", "richtext"].includes(type);
377
431
  };
378
432
  const getTranslatedValue = (type, value) => {
@@ -381,64 +435,61 @@ const getTranslatedValue = (type, value) => {
381
435
  }
382
436
  return value.toString();
383
437
  };
384
- const buildTranslatable = (key, fieldSchema, value, parentPath, componentId, schemaName) => {
438
+ const buildTranslatable = (key, fieldSchema, value, uuid = "") => {
385
439
  return {
386
440
  field: key,
387
441
  type: ["richtext", "blocks"].includes(fieldSchema.type) ? "html" : "text",
388
442
  translatableValue: [value],
389
443
  realType: fieldSchema.type,
390
- componentInfo: {
391
- namePath: parentPath,
392
- id: componentId,
393
- schemaName
394
- }
444
+ uuid
395
445
  };
396
446
  };
397
- const processComponentFields$1 = async (componentData, schema, parentField, componentName, schemaName, componentId) => {
447
+ const processComponentFields = async (componentData, schema, parentField, schemata) => {
398
448
  const contentFields = [];
399
- const parentPath = parentField.split(".");
449
+ const uuid = crypto.randomUUID();
400
450
  for (const [key, fieldSchema] of Object.entries(schema)) {
401
451
  if (shouldSkipField$1(key, fieldSchema)) continue;
402
452
  const value = componentData?.[key];
403
- const fieldPath = `${parentField}.${key}`;
453
+ if (!value)
454
+ continue;
404
455
  if (fieldSchema.type === "component") {
405
- if (!fieldSchema.component) continue;
456
+ if (!value.__component)
457
+ continue;
458
+ const targetSchema = schemata[value.__component];
459
+ if (!targetSchema)
460
+ continue;
406
461
  const nestedFields = await processComponent(
407
- fieldPath,
408
- fieldSchema.component,
409
- value || {},
410
- fieldSchema.component,
411
- value?.id
462
+ `${parentField}.${key}`,
463
+ value,
464
+ targetSchema,
465
+ schemata
412
466
  );
413
- contentFields.push(...nestedFields);
467
+ if (nestedFields.length > 0)
468
+ contentFields.push(...nestedFields);
414
469
  continue;
415
470
  }
416
- if (!isTranslatableField$1(fieldSchema.type)) continue;
471
+ if (!isTranslatableField(fieldSchema.type)) continue;
417
472
  if (value === null || value === void 0 || value === "") continue;
418
473
  const translatedValue = getTranslatedValue(fieldSchema.type, value);
419
474
  const translatable = buildTranslatable(
420
475
  key,
421
476
  fieldSchema,
422
477
  translatedValue,
423
- parentPath,
424
- componentId,
425
- schemaName
478
+ uuid
426
479
  );
480
+ componentData.__tsuid = uuid;
427
481
  contentFields.push(translatable);
428
482
  }
429
483
  return contentFields;
430
484
  };
431
- const isFieldLocalizable = (fieldSchema, parentSchema) => {
432
- if (fieldSchema.pluginOptions?.i18n?.localized !== void 0) {
433
- return fieldSchema.pluginOptions.i18n.localized;
434
- }
435
- if (parentSchema.pluginOptions?.i18n?.localized === true) {
436
- const localizableTypes = ["string", "text", "blocks", "richtext"];
437
- return localizableTypes.includes(fieldSchema.type);
438
- }
439
- 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)
440
491
  };
441
- const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
492
+ const DEFAULT_FIELDS$1 = /* @__PURE__ */ new Set([
442
493
  "id",
443
494
  "documentId",
444
495
  "createdAt",
@@ -450,45 +501,41 @@ const DEFAULT_FIELDS = /* @__PURE__ */ new Set([
450
501
  "createdBy"
451
502
  ]);
452
503
  const isEmpty = (value) => value === null || value === void 0 || value === "";
453
- const isTranslatableField = (fieldSchema) => ["string", "text", "blocks", "richtext"].includes(fieldSchema.type) && fieldSchema.pluginOptions?.i18n?.localized !== false;
454
- 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) => {
455
506
  const results = [];
456
507
  for (const component of value) {
457
508
  const componentName = component?.__component;
458
509
  if (!componentName) continue;
510
+ const schema = schemata[componentName];
511
+ if (!schema) continue;
459
512
  const fields = await processComponent(
460
513
  key,
461
- componentName,
462
514
  component,
463
- componentName,
464
- component.id
515
+ schema,
516
+ schemata
465
517
  );
466
- results.push(...fields);
518
+ if (fields.length > 0)
519
+ results.push(...fields);
467
520
  }
468
521
  return results;
469
522
  };
470
- const processComponentField = async (key, value, fieldSchema) => {
523
+ const processComponentField = async (key, value, fieldSchema, schemata) => {
471
524
  const results = [];
472
- if (fieldSchema.repeatable && Array.isArray(value)) {
473
- for (const component of value) {
474
- const fields = await processComponent(
475
- key,
476
- fieldSchema.component,
477
- component,
478
- fieldSchema.component,
479
- component.id
480
- );
481
- results.push(...fields);
482
- }
483
- } 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;
484
531
  const fields = await processComponent(
485
532
  key,
486
- fieldSchema.component,
487
- value,
488
- fieldSchema.component,
489
- value.id
533
+ component,
534
+ schema,
535
+ schemata
490
536
  );
491
- results.push(...fields);
537
+ if (fields.length > 0)
538
+ results.push(...fields);
492
539
  }
493
540
  return results;
494
541
  };
@@ -501,38 +548,148 @@ const processRegularField = (key, value, fieldSchema) => {
501
548
  realType: fieldSchema.type
502
549
  };
503
550
  };
504
- 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) => {
505
558
  const contentFields = [];
559
+ const staticContent = {};
560
+ const schema = schemaData.entry.attributes;
506
561
  for (const [key, value] of Object.entries(entry)) {
507
- if (shouldSkipField(key, value)) continue;
508
- const fieldSchema = schema[key];
509
- if (!fieldSchema) continue;
510
- if (isDynamicZone(fieldSchema, value, schema)) {
511
- const zoneFields = await processDynamicZone(key, value);
512
- contentFields.push(...zoneFields);
562
+ if (shouldSkipField(key, value))
513
563
  continue;
514
- }
515
- if (isComponent(fieldSchema, value, schema)) {
516
- const componentFields = await processComponentField(key, value, fieldSchema);
517
- contentFields.push(...componentFields);
564
+ const fieldSchema = schema[key];
565
+ if (!fieldSchema || !IsLocalisedField(fieldSchema)) {
566
+ Logger$3.debug("SKipping non-local field " + key);
518
567
  continue;
519
568
  }
520
- if (isTranslatableField(fieldSchema)) {
569
+ if (isSimpleTranslatableField(fieldSchema)) {
570
+ Logger$3.debug("Processing simple field " + key);
521
571
  const translatedField = processRegularField(key, value, fieldSchema);
522
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;
523
590
  }
524
591
  }
525
- return contentFields;
592
+ Logger$3.info("Process completed");
593
+ return {
594
+ fields: contentFields,
595
+ keep: staticContent
596
+ };
526
597
  };
527
598
  const shouldSkipField = (key, value) => {
528
- return DEFAULT_FIELDS.has(key) || isEmpty(value);
599
+ return DEFAULT_FIELDS$1.has(key) || isEmpty(value);
600
+ };
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
+ };
529
612
  };
530
- const isDynamicZone = (fieldSchema, value, schema) => {
531
- return fieldSchema.type === "dynamiczone" && isFieldLocalizable(fieldSchema, schema) && Array.isArray(value);
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)
532
619
  };
533
- const isComponent = (fieldSchema, value, schema) => {
534
- return fieldSchema.type === "component" && isFieldLocalizable(fieldSchema, schema);
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;
535
639
  };
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);
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
+ }
536
693
  function htmlToJson(html) {
537
694
  function parseHTML(html2) {
538
695
  const elements2 = [];
@@ -725,227 +882,171 @@ function htmlToJson(html) {
725
882
  }
726
883
  return blocks;
727
884
  }
728
- async function updateEntry(contentTypeID, entryID, sourceLocale, targetLocale, data, attributes) {
729
- if (!entryID) {
730
- const singleTypeData = await strapi.documents(contentTypeID).findFirst();
731
- entryID = singleTypeData.documentId;
732
- }
733
- const originalEntry = await strapi.documents(contentTypeID).findFirst({
734
- documentId: entryID,
735
- locale: sourceLocale
736
- });
737
- const processedData = processDataRecursively(data);
738
- for (const [key, value] of Object.entries(processedData)) {
739
- if (attributes[key]?.type === "blocks" && typeof value === "string") {
740
- console.warn(
741
- `Field ${key} is a blocks field but received string value. Converting to blocks format.`
742
- );
743
- 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);
744
903
  }
745
904
  }
746
- const localizedData = {};
747
- for (const field in processedData) {
748
- if (attributes[field] && (!attributes[field].pluginOptions?.i18n || attributes[field].pluginOptions?.i18n?.localized !== false)) {
749
- 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);
750
913
  }
751
914
  }
752
- await strapi.documents(contentTypeID).update({
753
- documentId: entryID,
754
- locale: targetLocale,
755
- data: localizedData
756
- });
757
- if (originalEntry.publishedAt !== null) {
758
- await strapi.documents(contentTypeID).publish({
759
- documentId: entryID,
760
- locale: sourceLocale
761
- });
762
- }
915
+ if (fields.length > 0)
916
+ Logger$1.info(fields.length + " dynamic fields/components replaced for later merge: " + fields.join(", "));
917
+ return fields;
763
918
  }
764
- function processDataRecursively(data, schema) {
765
- if (!data || typeof data !== "object") {
766
- 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;
767
926
  }
768
- if (Array.isArray(data)) {
769
- if (data[0]?.fields) {
770
- const processedFields = {};
771
- for (const fieldData of data[0].fields) {
772
- if (fieldData.realType === "blocks") {
773
- if (fieldData.translatableValue?.[0]) {
774
- processedFields[fieldData.field] = htmlToJson(fieldData.translatableValue[0]);
775
- }
776
- } else {
777
- processedFields[fieldData.field] = fieldData.translatableValue?.[0] || null;
778
- }
779
- }
780
- return processedFields;
781
- }
782
- 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;
783
930
  }
784
- const result = {};
785
- for (const key in data) {
786
- 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;
787
935
  }
788
- return result;
789
- }
790
- function organizeFields(fields) {
791
- const componentFieldsMap = /* @__PURE__ */ new Map();
792
- const dynamicZoneFields = /* @__PURE__ */ new Map();
793
- const regularFields = [];
794
- fields.forEach((field) => {
795
- if (!field.componentInfo) {
796
- regularFields.push(field);
797
- return;
798
- }
799
- const { namePath, id } = field.componentInfo;
800
- const pathString = namePath.join(".");
801
- if (namePath[0] === "dynamiczone") {
802
- if (!dynamicZoneFields.has(id)) {
803
- dynamicZoneFields.set(id, []);
804
- }
805
- dynamicZoneFields.get(id)?.push(field);
806
- } else {
807
- if (!componentFieldsMap.has(pathString)) {
808
- componentFieldsMap.set(pathString, []);
809
- }
810
- componentFieldsMap.get(pathString)?.push(field);
811
- }
812
- });
813
- return { regularFields, componentFieldsMap, dynamicZoneFields };
814
- }
815
- function processRegularFields(regularFields, acc) {
816
- regularFields.forEach((field) => {
817
- acc[field.field] = field.translatableValue[0];
818
- });
819
- return acc;
820
- }
821
- function processRepeatableComponents(fields, existingEntry, rootPath) {
822
- const existingComponents = existingEntry?.[rootPath] || [];
823
- const componentsById = /* @__PURE__ */ new Map();
824
- fields.forEach((field) => {
825
- if (!field.componentInfo) {
826
- console.warn(`Component info missing for field: ${field.field}`);
827
- return;
828
- }
829
- const componentId = field.componentInfo.id;
830
- if (!componentsById.has(componentId)) {
831
- const existingComponent = existingComponents.find((c) => c.id === componentId);
832
- componentsById.set(componentId, existingComponent ? { ...existingComponent } : {});
833
- }
834
- const component = componentsById.get(componentId);
835
- if (field.realType === "blocks") {
836
- component[field.field] = htmlToJson(field.translatableValue[0] || "");
837
- } else {
838
- component[field.field] = field.translatableValue[0];
839
- }
840
- });
841
- return Array.from(componentsById.values()).map((comp) => {
842
- if (!existingComponents.find((ec) => ec.id === comp.id)) {
843
- const { id, ...rest } = comp;
844
- return rest;
845
- }
846
- return comp;
847
- }).filter((comp) => Object.keys(comp).length > 0);
848
- }
849
- function processNestedComponents(fields, pathParts, existingEntry, acc) {
850
- let current = acc;
851
- let currentExisting = existingEntry;
852
- pathParts.forEach((part, index2) => {
853
- if (!current[part]) {
854
- current[part] = {};
855
- if (currentExisting?.[part]?.id) {
856
- current[part].id = currentExisting[part].id;
857
- }
858
- }
859
- if (index2 === pathParts.length - 1) {
860
- fields.forEach((field) => {
861
- if (field.realType === "blocks") {
862
- current[part][field.field] = htmlToJson(field.translatableValue[0] || "");
863
- } else {
864
- current[part][field.field] = field.translatableValue[0];
865
- }
866
- });
867
- } else {
868
- current = current[part];
869
- currentExisting = currentExisting?.[part];
870
- }
871
- });
872
- }
873
- function processComponentFields(componentFieldsMap, acc, existingEntry, targetSchema) {
874
- componentFieldsMap.forEach((fields, namePath) => {
875
- if (!fields.length) return;
876
- const pathParts = namePath.split(".");
877
- const rootPath = pathParts[0];
878
- const schema = targetSchema.attributes?.[rootPath];
879
- if (schema?.repeatable) {
880
- acc[rootPath] = processRepeatableComponents(fields, existingEntry, rootPath);
881
- } else {
882
- 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;
883
973
  }
884
- });
885
- return acc;
886
- }
887
- function transformFieldsToData(fields) {
888
- return fields.reduce((acc, field) => {
889
- if (!field.translatableValue) {
890
- acc[field.field] = "";
891
- 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);
892
981
  }
893
- if (field.realType === "blocks") {
894
- acc[field.field] = htmlToJson(field.translatableValue[0] || "");
895
- } else if (field.realType === "richtext") {
896
- acc[field.field] = field.translatableValue[0] || "";
897
- } else {
898
- 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;
899
1014
  }
900
- return acc;
901
- }, {});
902
- }
903
- function processDynamicZones(dynamicZoneFields, acc, existingEntry) {
904
- if (dynamicZoneFields.size > 0) {
905
- const existingDynamicZone = existingEntry?.dynamiczone || [];
906
- acc.dynamiczone = Array.from(dynamicZoneFields.entries()).sort(([a], [b]) => a - b).map(([_, fields]) => {
907
- if (!fields[0].componentInfo) {
908
- console.warn(
909
- `Component info missing for dynamic zone field: ${fields[0].field}`
910
- );
911
- return null;
912
- }
913
- const { schemaName } = fields[0].componentInfo;
914
- const componentData = transformFieldsToData(fields);
915
- const matchingComponent = existingDynamicZone.find(
916
- (comp) => comp.__component === schemaName
917
- );
918
- return {
919
- __component: schemaName,
920
- ...componentData,
921
- ...matchingComponent?.id ? { id: matchingComponent.id } : {}
922
- };
923
- }).filter(Boolean);
1015
+ if (mergeValue(translatable.field, translatable, schema, entry.entry))
1016
+ count++;
924
1017
  }
925
- return acc;
926
- }
927
- function prepareImportData(translatables, existingEntry, targetSchema) {
928
- return translatables.reduce((acc, doc) => {
929
- const { regularFields, componentFieldsMap, dynamicZoneFields } = organizeFields(translatables);
930
- const withRegularFields = processRegularFields(regularFields, acc);
931
- const withComponentFields = processComponentFields(
932
- componentFieldsMap,
933
- withRegularFields,
934
- existingEntry,
935
- targetSchema
936
- );
937
- const withDynamicZones = processDynamicZones(
938
- dynamicZoneFields,
939
- withComponentFields,
940
- existingEntry
941
- );
942
- return withDynamicZones;
943
- }, {});
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;
944
1040
  }
945
- const jwt = require("jsonwebtoken");
946
- const crypto = require("crypto");
947
- const TRANSLATIONTUDIO_URL = "https://cms-strapi-service-7866fdd79eab.herokuapp.com";
1041
+ const TRANSLATIONTUDIO_URL = "https://strapi.translationstudio.tech";
948
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
+ };
949
1050
  const service = ({ strapi: strapi2 }) => {
950
1051
  const pluginStore = strapi2.store({
951
1052
  type: "plugin",
@@ -957,6 +1058,16 @@ const service = ({ strapi: strapi2 }) => {
957
1058
  const result = await pluginStore.get({ key: "license" });
958
1059
  return { license: result };
959
1060
  },
1061
+ async getTranslationstudioUrl() {
1062
+ try {
1063
+ const result = await pluginStore.get({ key: "developurl" });
1064
+ if (typeof result === "string" && result !== "")
1065
+ return result;
1066
+ } catch (err) {
1067
+ strapi2.log.warn(err);
1068
+ }
1069
+ return TRANSLATIONTUDIO_URL;
1070
+ },
960
1071
  async setLicense(license) {
961
1072
  try {
962
1073
  await pluginStore.set({
@@ -968,6 +1079,26 @@ const service = ({ strapi: strapi2 }) => {
968
1079
  return { success: false };
969
1080
  }
970
1081
  },
1082
+ async setDevelopmentUrl(url) {
1083
+ try {
1084
+ await pluginStore.set({
1085
+ key: "developurl",
1086
+ value: url
1087
+ });
1088
+ return true;
1089
+ } catch (error) {
1090
+ return false;
1091
+ }
1092
+ },
1093
+ async getDevelopmentUrl() {
1094
+ try {
1095
+ const result = await pluginStore.get({ key: "developurl" });
1096
+ if (typeof result === "string")
1097
+ return result;
1098
+ } catch (error) {
1099
+ }
1100
+ return "";
1101
+ },
971
1102
  // Access Token
972
1103
  async getToken() {
973
1104
  try {
@@ -979,23 +1110,16 @@ const service = ({ strapi: strapi2 }) => {
979
1110
  },
980
1111
  async generateToken() {
981
1112
  const secretKey = crypto.randomBytes(64).toString("hex");
982
- const token = jwt.sign(
983
- {
984
- app: APP_NAME,
985
- iat: Math.floor(Date.now() / 1e3)
986
- },
987
- secretKey,
988
- { expiresIn: "10y" }
989
- );
990
1113
  await pluginStore.set({
991
1114
  key: "token",
992
- value: token
1115
+ value: secretKey
993
1116
  });
994
- return { token };
1117
+ return { token: secretKey };
995
1118
  },
996
1119
  async getLanguageOptions() {
997
1120
  const { license } = await this.getLicense();
998
- const response = await fetch(TRANSLATIONTUDIO_URL + "/mappings", {
1121
+ const url = await this.getTranslationstudioUrl();
1122
+ const response = await fetch(url + "/mappings", {
999
1123
  headers: { Authorization: `${license}` }
1000
1124
  });
1001
1125
  const responseData = await response.json();
@@ -1003,9 +1127,15 @@ const service = ({ strapi: strapi2 }) => {
1003
1127
  },
1004
1128
  async exportData(payload) {
1005
1129
  const { contentTypeID, entryID, locale } = parsePayload(payload);
1006
- 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
+ }
1007
1137
  const entry = await getEntry(contentTypeID, entryID, locale);
1008
- const contentFields = await processEntryFields(entry, contentType.attributes);
1138
+ const contentFields = await processEntryFields(entry, contentType);
1009
1139
  return transformResponse(contentFields);
1010
1140
  },
1011
1141
  async importData(payload) {
@@ -1013,27 +1143,37 @@ const service = ({ strapi: strapi2 }) => {
1013
1143
  const sourceLocale = payload.source;
1014
1144
  const targetLocale = payload.target;
1015
1145
  try {
1016
- const existingEntry = await getEntry(contentTypeID, entryID, targetLocale);
1017
- const targetSchema = await getContentType(contentTypeID);
1018
- const data = prepareImportData(payload.document[0].fields, existingEntry, targetSchema);
1019
- if (targetSchema.pluginOptions.i18n.localized === true) {
1020
- await updateEntry(
1021
- contentTypeID,
1022
- entryID,
1023
- sourceLocale,
1024
- targetLocale,
1025
- data,
1026
- targetSchema.attributes
1027
- );
1028
- }
1029
- 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;
1030
1168
  } catch (error) {
1031
- return { success: false };
1169
+ strapi2.log.error(error);
1032
1170
  }
1171
+ return false;
1033
1172
  },
1034
1173
  async requestTranslation(payload) {
1035
1174
  const { license } = await this.getLicense();
1036
- const response = await fetch(TRANSLATIONTUDIO_URL + "/translate", {
1175
+ const url = await this.getTranslationstudioUrl();
1176
+ const response = await fetch(url + "/translate", {
1037
1177
  method: "POST",
1038
1178
  headers: {
1039
1179
  Authorization: `${license}`,
@@ -1064,10 +1204,6 @@ const service = ({ strapi: strapi2 }) => {
1064
1204
  },
1065
1205
  async ping() {
1066
1206
  return;
1067
- },
1068
- async getEntryData(contentTypeID, entryID, locale) {
1069
- const entry = await getEntry(contentTypeID, entryID, locale);
1070
- return entry;
1071
1207
  }
1072
1208
  };
1073
1209
  };