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