@goenhance/strapi-plugins-translate 0.11.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/dist/_chunks/App-DvJ8tPer.js +674 -0
  4. package/dist/_chunks/App-uB_CPrcd.mjs +674 -0
  5. package/dist/_chunks/en-BOBGCAB6.mjs +65 -0
  6. package/dist/_chunks/en-BaJyCZ_c.js +65 -0
  7. package/dist/_chunks/index-B8MBOdIV.js +276 -0
  8. package/dist/_chunks/index-DkZZ45sW.mjs +277 -0
  9. package/dist/_chunks/zh-CtGwhmjc.mjs +65 -0
  10. package/dist/_chunks/zh-DUCnaWvE.js +65 -0
  11. package/dist/_chunks/zh-Hans-CJW3RUKL.js +65 -0
  12. package/dist/_chunks/zh-Hans-pTiPqgIS.mjs +65 -0
  13. package/dist/admin/index.js +3 -0
  14. package/dist/admin/index.mjs +4 -0
  15. package/dist/admin/src/components/Initializer.d.ts +5 -0
  16. package/dist/admin/src/components/LLMButton.d.ts +2 -0
  17. package/dist/admin/src/components/PluginIcon.d.ts +18 -0
  18. package/dist/admin/src/index.d.ts +11 -0
  19. package/dist/admin/src/pages/App.d.ts +2 -0
  20. package/dist/admin/src/pages/BatchTranslatePage.d.ts +2 -0
  21. package/dist/admin/src/pages/SettingsPage.d.ts +2 -0
  22. package/dist/admin/src/pluginId.d.ts +1 -0
  23. package/dist/admin/src/utils/constants.d.ts +4 -0
  24. package/dist/admin/src/utils/getLocaleFromUrl.d.ts +1 -0
  25. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  26. package/dist/server/index.js +754 -0
  27. package/dist/server/index.mjs +755 -0
  28. package/dist/server/src/bootstrap.d.ts +5 -0
  29. package/dist/server/src/config/constants.d.ts +7 -0
  30. package/dist/server/src/config/index.d.ts +11 -0
  31. package/dist/server/src/content-types/index.d.ts +2 -0
  32. package/dist/server/src/controllers/admin.controller.d.ts +21 -0
  33. package/dist/server/src/controllers/index.d.ts +43 -0
  34. package/dist/server/src/destroy.d.ts +5 -0
  35. package/dist/server/src/index.d.ts +2 -0
  36. package/dist/server/src/middlewares/index.d.ts +2 -0
  37. package/dist/server/src/policies/index.d.ts +2 -0
  38. package/dist/server/src/register.d.ts +5 -0
  39. package/dist/server/src/routes/admin.d.ts +9 -0
  40. package/dist/server/src/routes/index.d.ts +14 -0
  41. package/dist/server/src/services/index.d.ts +2 -0
  42. package/dist/server/src/services/llm-service.d.ts +6 -0
  43. package/dist/server/src/types/controllers.d.ts +22 -0
  44. package/dist/server/src/types/index.d.ts +53 -0
  45. package/dist/server/src/utils/json-utils.d.ts +4 -0
  46. package/package.json +88 -0
@@ -0,0 +1,754 @@
1
+ "use strict";
2
+ const openai = require("openai");
3
+ const bootstrap = ({ strapi: strapi2 }) => {
4
+ };
5
+ const destroy = ({ strapi: strapi2 }) => {
6
+ };
7
+ const register = ({ strapi: strapi2 }) => {
8
+ };
9
+ const DEFAULT_SYSTEM_PROMPT = "You are a professional translator. Your task is to translate the provided content accurately while preserving the original meaning and tone.";
10
+ const SYSTEM_PROMPT_APPENDIX = `The user asks you to translate the text to a specific language, the language is provided via short code like "en", "fr", "de", etc.`;
11
+ const DEFAULT_LLM_TEMPERATURE = 0.3;
12
+ const DEFAULT_LLM_MODEL = "gpt-4o";
13
+ const DEFAULT_LLM_BASE_URL = "https://api.openai.com/v1";
14
+ const SYSTEM_PROMPT_FIX = `You are a JSON correction assistant. Only return valid, corrected JSON.`;
15
+ const USER_PROMPT_FIX_PREFIX = "Fix this invalid JSON and return ONLY the corrected JSON. No explanations allowed. The JSON is:";
16
+ const config = {
17
+ default: ({ env }) => ({
18
+ llmApiKey: env("LLM_TRANSLATOR_LLM_API_KEY"),
19
+ llmEndpoint: env("STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL"),
20
+ llmModel: env("STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL")
21
+ }),
22
+ validator(config2) {
23
+ const PLUGIN_NAME = "Strapi LLM Translator";
24
+ console.info(`
25
+ ==== ${PLUGIN_NAME} Configuration Validation ====`);
26
+ if (!config2?.llmApiKey) {
27
+ console.warn("⚠️ LLM API Key: Missing");
28
+ console.info(" → Translation features requiring API keys will be disabled");
29
+ } else {
30
+ console.info("✅ LLM API Key: Configured");
31
+ }
32
+ const endpoint = config2?.llmEndpoint || DEFAULT_LLM_BASE_URL;
33
+ if (!config2?.llmEndpoint) {
34
+ console.warn(`⚠️ API Endpoint: Using default (${DEFAULT_LLM_BASE_URL})`);
35
+ } else {
36
+ console.info(`✅ API Endpoint: Configured (${endpoint})`);
37
+ }
38
+ const model = config2?.llmModel || DEFAULT_LLM_MODEL;
39
+ if (!config2?.llmModel) {
40
+ console.warn(`⚠️ LLM Model: Using default (${DEFAULT_LLM_MODEL})`);
41
+ } else {
42
+ console.info(`✅ LLM Model: Configured (${model})`);
43
+ }
44
+ console.info("========================================================\n");
45
+ }
46
+ };
47
+ const contentTypes = {};
48
+ const controllers$1 = ({ strapi: strapi2 }) => ({
49
+ // Genertate translations
50
+ async generate(ctx) {
51
+ try {
52
+ const { fields, components, targetLanguage, contentType } = ctx.request.body;
53
+ const result = await strapi2.plugin("strapi-plugins-translate").service("llm-service").generateWithLLM(contentType, fields, components, {
54
+ targetLanguage
55
+ });
56
+ ctx.status = result.meta.status;
57
+ ctx.body = result;
58
+ } catch (error) {
59
+ console.error("Error in generate controller:", error);
60
+ ctx.status = 500;
61
+ ctx.body = {
62
+ meta: {
63
+ ok: false,
64
+ status: 500,
65
+ message: "Internal server error"
66
+ }
67
+ };
68
+ }
69
+ },
70
+ // Get the configuration
71
+ async getConfig(ctx) {
72
+ const pluginStore = strapi2.store({
73
+ environment: strapi2.config.environment,
74
+ type: "plugin",
75
+ name: "strapi-plugins-translate"
76
+ // replace with your plugin name
77
+ });
78
+ const config2 = await pluginStore.get({ key: "configuration" });
79
+ ctx.body = config2 || {};
80
+ },
81
+ // Save the configuration
82
+ async setConfig(ctx) {
83
+ const { body } = ctx.request;
84
+ const pluginStore = strapi2.store({
85
+ environment: strapi2.config.environment,
86
+ type: "plugin",
87
+ name: "strapi-plugins-translate"
88
+ // replace with your plugin name
89
+ });
90
+ await pluginStore.set({
91
+ key: "configuration",
92
+ value: { ...body }
93
+ });
94
+ ctx.body = await pluginStore.get({ key: "configuration" });
95
+ },
96
+ // Get all content types that support i18n
97
+ async getContentTypes(ctx) {
98
+ try {
99
+ const contentTypes2 = strapi2.contentTypes;
100
+ const i18nContentTypes = [];
101
+ Object.entries(contentTypes2).forEach(([uid, contentType]) => {
102
+ const isI18nEnabled = contentType.pluginOptions?.i18n?.localized === true;
103
+ const isApiContentType = uid.startsWith("api::");
104
+ if (isI18nEnabled && isApiContentType) {
105
+ i18nContentTypes.push({
106
+ uid,
107
+ displayName: contentType.info?.displayName || uid,
108
+ apiID: contentType.info?.singularName || uid.split(".").pop() || uid,
109
+ kind: contentType.kind || "collectionType"
110
+ });
111
+ }
112
+ });
113
+ ctx.body = {
114
+ data: i18nContentTypes,
115
+ meta: { ok: true }
116
+ };
117
+ } catch (error) {
118
+ console.error("Error getting content types:", error);
119
+ ctx.status = 500;
120
+ ctx.body = {
121
+ data: [],
122
+ meta: { ok: false, message: "Failed to get content types" }
123
+ };
124
+ }
125
+ },
126
+ // Get all available locales
127
+ async getLocales(ctx) {
128
+ try {
129
+ const locales = await strapi2.plugin("i18n").service("locales").find();
130
+ ctx.body = {
131
+ data: locales,
132
+ meta: { ok: true }
133
+ };
134
+ } catch (error) {
135
+ console.error("Error getting locales:", error);
136
+ ctx.status = 500;
137
+ ctx.body = {
138
+ data: [],
139
+ meta: { ok: false, message: "Failed to get locales" }
140
+ };
141
+ }
142
+ },
143
+ // Get entries for a specific content type and locale
144
+ async getEntries(ctx) {
145
+ try {
146
+ const { contentType } = ctx.params;
147
+ const { locale, page = "1", pageSize = "25" } = ctx.query;
148
+ const decodedContentType = decodeURIComponent(contentType);
149
+ const contentTypeSchema = strapi2.contentTypes[decodedContentType];
150
+ if (!contentTypeSchema) {
151
+ ctx.status = 404;
152
+ ctx.body = {
153
+ data: [],
154
+ meta: { ok: false, message: "Content type not found" }
155
+ };
156
+ return;
157
+ }
158
+ const mainField = contentTypeSchema.info?.mainField || Object.keys(contentTypeSchema.attributes).find(
159
+ (key) => contentTypeSchema.attributes[key].type === "string" && ["title", "name", "label"].includes(key.toLowerCase())
160
+ ) || "id";
161
+ const idField = Object.keys(contentTypeSchema.attributes).find(
162
+ (key) => (key.endsWith("_id") || key.endsWith("Id") || contentTypeSchema.attributes[key].type === "uid" && key !== "slug") && key != "category_id"
163
+ );
164
+ const query = {
165
+ page: parseInt(page, 10),
166
+ pageSize: parseInt(pageSize, 10),
167
+ populate: "*"
168
+ };
169
+ if (locale) {
170
+ query.locale = locale;
171
+ }
172
+ const entries = await strapi2.documents(decodedContentType).findMany(query);
173
+ const count = await strapi2.documents(decodedContentType).count(
174
+ locale ? { locale } : {}
175
+ );
176
+ const getDisplayValue = (value) => {
177
+ if (value === null || value === void 0) {
178
+ return "";
179
+ }
180
+ if (typeof value === "string" || typeof value === "number") {
181
+ return String(value);
182
+ }
183
+ if (typeof value === "object") {
184
+ return value.title || value.name || value.label || value.id || JSON.stringify(value);
185
+ }
186
+ return String(value);
187
+ };
188
+ const entriesWithLocaleInfo = await Promise.all(
189
+ entries.map(async (entry) => {
190
+ const localizations = entry.localizations || [];
191
+ const existingLocales = [entry.locale, ...localizations.map((l) => l.locale)];
192
+ const titleValue = entry[mainField];
193
+ const displayTitle = getDisplayValue(titleValue) || getDisplayValue(entry.id);
194
+ const customIdValue = idField ? entry[idField] : null;
195
+ const displayCustomId = customIdValue ? getDisplayValue(customIdValue) : null;
196
+ return {
197
+ id: entry.id,
198
+ documentId: entry.documentId,
199
+ customId: displayCustomId,
200
+ title: displayTitle,
201
+ locale: entry.locale,
202
+ publishedAt: entry.publishedAt,
203
+ existingLocales,
204
+ createdAt: entry.createdAt,
205
+ updatedAt: entry.updatedAt
206
+ };
207
+ })
208
+ );
209
+ ctx.body = {
210
+ data: entriesWithLocaleInfo,
211
+ meta: {
212
+ ok: true,
213
+ pagination: {
214
+ page: parseInt(page, 10),
215
+ pageSize: parseInt(pageSize, 10),
216
+ total: count,
217
+ pageCount: Math.ceil(count / parseInt(pageSize, 10))
218
+ },
219
+ mainField,
220
+ idField
221
+ }
222
+ };
223
+ } catch (error) {
224
+ console.error("Error getting entries:", error);
225
+ ctx.status = 500;
226
+ ctx.body = {
227
+ data: [],
228
+ meta: { ok: false, message: "Failed to get entries" }
229
+ };
230
+ }
231
+ },
232
+ // Batch translate entries
233
+ async batchTranslate(ctx) {
234
+ try {
235
+ const { contentType, entries, sourceLocale, targetLocales, publish = false } = ctx.request.body;
236
+ const results = [];
237
+ const contentTypeSchema = strapi2.contentTypes[contentType];
238
+ if (!contentTypeSchema) {
239
+ ctx.status = 404;
240
+ ctx.body = {
241
+ data: results,
242
+ meta: { ok: false, message: "Content type not found" }
243
+ };
244
+ return;
245
+ }
246
+ const components = {};
247
+ Object.entries(contentTypeSchema.attributes || {}).forEach(
248
+ ([, attr]) => {
249
+ if (attr.type === "component" && attr.component) {
250
+ components[attr.component] = strapi2.components[attr.component];
251
+ } else if (attr.type === "dynamiczone" && attr.components) {
252
+ attr.components.forEach((comp) => {
253
+ components[comp] = strapi2.components[comp];
254
+ });
255
+ }
256
+ }
257
+ );
258
+ for (const entryId of entries) {
259
+ const sourceEntry = await strapi2.documents(contentType).findOne({
260
+ documentId: entryId,
261
+ locale: sourceLocale,
262
+ populate: "*"
263
+ });
264
+ if (!sourceEntry) {
265
+ results.push({
266
+ documentId: entryId,
267
+ locale: sourceLocale,
268
+ success: false,
269
+ message: "Source entry not found"
270
+ });
271
+ continue;
272
+ }
273
+ for (const targetLocale of targetLocales) {
274
+ try {
275
+ const existingTranslation = await strapi2.documents(contentType).findOne({
276
+ documentId: entryId,
277
+ locale: targetLocale
278
+ });
279
+ if (existingTranslation) {
280
+ results.push({
281
+ documentId: entryId,
282
+ locale: targetLocale,
283
+ success: false,
284
+ message: "Translation already exists"
285
+ });
286
+ continue;
287
+ }
288
+ const translationResult = await strapi2.plugin("strapi-plugins-translate").service("llm-service").generateWithLLM(contentTypeSchema, sourceEntry, components, {
289
+ targetLanguage: targetLocale
290
+ });
291
+ if (!translationResult.meta.ok) {
292
+ results.push({
293
+ documentId: entryId,
294
+ locale: targetLocale,
295
+ success: false,
296
+ message: translationResult.meta.message || "Translation failed"
297
+ });
298
+ continue;
299
+ }
300
+ const translatedData = { ...translationResult.data };
301
+ delete translatedData.id;
302
+ delete translatedData.documentId;
303
+ delete translatedData.createdAt;
304
+ delete translatedData.updatedAt;
305
+ delete translatedData.publishedAt;
306
+ delete translatedData.locale;
307
+ delete translatedData.localizations;
308
+ delete translatedData.createdBy;
309
+ delete translatedData.updatedBy;
310
+ await strapi2.documents(contentType).update({
311
+ documentId: entryId,
312
+ locale: targetLocale,
313
+ data: translatedData,
314
+ status: publish ? "published" : "draft"
315
+ });
316
+ results.push({
317
+ documentId: entryId,
318
+ locale: targetLocale,
319
+ success: true
320
+ });
321
+ } catch (error) {
322
+ console.error(`Error translating entry ${entryId} to ${targetLocale}:`, error);
323
+ results.push({
324
+ documentId: entryId,
325
+ locale: targetLocale,
326
+ success: false,
327
+ message: error instanceof Error ? error.message : "Unknown error"
328
+ });
329
+ }
330
+ }
331
+ }
332
+ const successCount = results.filter((r) => r.success).length;
333
+ const failureCount = results.filter((r) => !r.success).length;
334
+ ctx.body = {
335
+ data: results,
336
+ meta: {
337
+ ok: true,
338
+ successCount,
339
+ failureCount,
340
+ message: `Translated ${successCount} entries, ${failureCount} failed`
341
+ }
342
+ };
343
+ } catch (error) {
344
+ console.error("Error in batch translate:", error);
345
+ ctx.status = 500;
346
+ ctx.body = {
347
+ data: [],
348
+ meta: {
349
+ ok: false,
350
+ message: error instanceof Error ? error.message : "Batch translation failed"
351
+ }
352
+ };
353
+ }
354
+ }
355
+ });
356
+ const controllers = {
357
+ admin: controllers$1
358
+ };
359
+ const middlewares = {};
360
+ const policies = {};
361
+ const adminRoutes = [
362
+ {
363
+ method: "POST",
364
+ path: "/generate",
365
+ handler: "admin.generate",
366
+ config: {
367
+ policies: []
368
+ }
369
+ },
370
+ {
371
+ method: "GET",
372
+ path: "/config",
373
+ handler: "admin.getConfig",
374
+ config: {
375
+ policies: []
376
+ }
377
+ },
378
+ {
379
+ method: "POST",
380
+ path: "/config",
381
+ handler: "admin.setConfig",
382
+ config: {
383
+ policies: []
384
+ }
385
+ },
386
+ {
387
+ method: "GET",
388
+ path: "/content-types",
389
+ handler: "admin.getContentTypes",
390
+ config: {
391
+ policies: []
392
+ }
393
+ },
394
+ {
395
+ method: "GET",
396
+ path: "/locales",
397
+ handler: "admin.getLocales",
398
+ config: {
399
+ policies: []
400
+ }
401
+ },
402
+ {
403
+ method: "GET",
404
+ path: "/entries/:contentType",
405
+ handler: "admin.getEntries",
406
+ config: {
407
+ policies: []
408
+ }
409
+ },
410
+ {
411
+ method: "POST",
412
+ path: "/batch-translate",
413
+ handler: "admin.batchTranslate",
414
+ config: {
415
+ policies: []
416
+ }
417
+ }
418
+ ];
419
+ const routes = {
420
+ admin: {
421
+ type: "admin",
422
+ routes: adminRoutes
423
+ }
424
+ };
425
+ const cleanJSONString = (content) => {
426
+ return content.replace(/^```json\s*\n/, "").replace(/^```\s*\n/, "").replace(/\n\s*```$/, "").replace(/\u200B/g, "").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').trim();
427
+ };
428
+ const balanceJSONBraces = (content) => {
429
+ let openBraces = 0;
430
+ let closeBraces = 0;
431
+ let inString = false;
432
+ for (let i = 0; i < content.length; i += 1) {
433
+ const char = content[i];
434
+ if (char === '"' && content[i - 1] !== "\\") {
435
+ inString = !inString;
436
+ }
437
+ if (!inString) {
438
+ if (char === "{") openBraces += 1;
439
+ if (char === "}") closeBraces += 1;
440
+ }
441
+ }
442
+ if (openBraces > closeBraces) {
443
+ return content + "}".repeat(openBraces - closeBraces);
444
+ }
445
+ return content;
446
+ };
447
+ const extractJSONObject = (content) => {
448
+ const firstBrace = content.indexOf("{");
449
+ const lastBrace = content.lastIndexOf("}");
450
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
451
+ return content.slice(firstBrace, lastBrace + 1);
452
+ }
453
+ return content;
454
+ };
455
+ const safeJSONParse = (content) => {
456
+ const parsed = JSON.parse(content);
457
+ if (typeof parsed === "object" && parsed !== null) {
458
+ return parsed;
459
+ }
460
+ throw new Error("Invalid response format - not an object");
461
+ };
462
+ const llmClient = new openai.OpenAI({
463
+ baseURL: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL ?? DEFAULT_LLM_BASE_URL,
464
+ apiKey: process.env.LLM_TRANSLATOR_LLM_API_KEY ?? "not_set"
465
+ });
466
+ const LLM_MODEL = process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL ?? DEFAULT_LLM_MODEL;
467
+ const extractTranslatableFields = (contentType, fields, components = {}) => {
468
+ const translatableFields = [];
469
+ const isTranslatableFieldSchema = (schema, value) => {
470
+ if (!schema) {
471
+ return false;
472
+ }
473
+ const { type } = schema;
474
+ const isStringType = ["string", "text"].includes(type) && typeof value === "string";
475
+ const isRichTextType = ["richtext", "richText", "blocks"].includes(type) && (typeof value === "string" || typeof value === "object");
476
+ const isJSONType = type === "json" && typeof value === "object";
477
+ const isNotUID = type !== "uid";
478
+ const isLocalizable = schema.pluginOptions?.i18n?.localized !== false;
479
+ return (isStringType || isRichTextType || isJSONType) && isNotUID && isLocalizable;
480
+ };
481
+ const traverse = (schema, data, path = [], originalPath = []) => {
482
+ Object.entries(schema.attributes || {}).forEach(([fieldName, fieldSchemaRaw]) => {
483
+ const fieldSchema = fieldSchemaRaw;
484
+ const value = data?.[fieldName];
485
+ if (value === void 0 || value === null) {
486
+ return;
487
+ }
488
+ if (isTranslatableFieldSchema(fieldSchema, value)) {
489
+ translatableFields.push({
490
+ path: [...path, fieldName],
491
+ value,
492
+ originalPath: [...originalPath, fieldName]
493
+ });
494
+ return;
495
+ }
496
+ if (fieldSchema.type === "component") {
497
+ const componentSchema = components[fieldSchema.component];
498
+ if (!componentSchema) return;
499
+ if (fieldSchema.repeatable && Array.isArray(value)) {
500
+ value.forEach(
501
+ (item, index) => traverse(
502
+ componentSchema,
503
+ item,
504
+ [...path, fieldName, String(index)],
505
+ [...originalPath, fieldName, String(index)]
506
+ )
507
+ );
508
+ } else if (typeof value === "object") {
509
+ traverse(componentSchema, value, [...path, fieldName], [...originalPath, fieldName]);
510
+ }
511
+ } else if (fieldSchema.type === "dynamiczone" && Array.isArray(value)) {
512
+ value.forEach((item, index) => {
513
+ const compSchema = components[item.__component];
514
+ if (compSchema) {
515
+ traverse(
516
+ compSchema,
517
+ item,
518
+ [...path, fieldName, String(index)],
519
+ [...originalPath, fieldName, String(index)]
520
+ );
521
+ }
522
+ });
523
+ }
524
+ });
525
+ };
526
+ traverse(contentType, fields, [], []);
527
+ return translatableFields;
528
+ };
529
+ const prepareTranslationPayload = (fields) => {
530
+ const payload = {};
531
+ fields.forEach((field) => {
532
+ let current = payload;
533
+ field.path.forEach((part, index) => {
534
+ if (index === field.path.length - 1) {
535
+ current[part] = field.value;
536
+ } else {
537
+ current[part] = current[part] || {};
538
+ current = current[part];
539
+ }
540
+ });
541
+ });
542
+ return payload;
543
+ };
544
+ const mergeTranslatedContent = (originalData, translatedData, translatableFields) => {
545
+ const result = JSON.parse(JSON.stringify(originalData));
546
+ translatableFields.forEach((field) => {
547
+ let translatedValue = translatedData;
548
+ for (const part of field.path) {
549
+ translatedValue = translatedValue?.[part];
550
+ if (translatedValue === void 0) break;
551
+ }
552
+ if (translatedValue !== void 0) {
553
+ let current = result;
554
+ field.originalPath.forEach((part, index) => {
555
+ if (index === field.originalPath.length - 1) {
556
+ current[part] = translatedValue;
557
+ } else {
558
+ current = current[part];
559
+ }
560
+ });
561
+ }
562
+ });
563
+ return result;
564
+ };
565
+ const generateSlug = async (data, field, contentTypeUID) => {
566
+ const uidService = strapi.service("plugin::content-manager.uid");
567
+ const slug = await uidService.generateUIDField({
568
+ contentTypeUID,
569
+ field,
570
+ data
571
+ });
572
+ return slug;
573
+ };
574
+ const findUIDFields = (contentType) => {
575
+ const uidFields = [];
576
+ Object.entries(contentType.attributes || {}).forEach(([fieldName, schema]) => {
577
+ if (schema.type === "uid" && schema.targetField) {
578
+ uidFields.push({
579
+ fieldName,
580
+ targetField: schema.targetField
581
+ });
582
+ }
583
+ });
584
+ return uidFields;
585
+ };
586
+ const generateUIDsForTranslatedFields = async (uidFields, translatedData, contentTypeUID, mergedContent) => {
587
+ const translatedUIDs = {};
588
+ for (const { fieldName, targetField } of uidFields) {
589
+ if (translatedData[targetField] !== void 0) {
590
+ try {
591
+ const newUID = await generateSlug(
592
+ {
593
+ ...mergedContent,
594
+ [targetField]: translatedData[targetField]
595
+ },
596
+ fieldName,
597
+ contentTypeUID
598
+ );
599
+ translatedUIDs[fieldName] = newUID;
600
+ } catch (error) {
601
+ console.error(`Failed to generate UID for field ${fieldName}:`, error);
602
+ }
603
+ }
604
+ }
605
+ return translatedUIDs;
606
+ };
607
+ const llmService = ({ strapi: strapi2 }) => ({
608
+ async generateWithLLM(contentType, fields, components, config2) {
609
+ try {
610
+ const userConfig = await getUserConfig();
611
+ const translatableFields = extractTranslatableFields(contentType, fields, components);
612
+ const translationPayload = prepareTranslationPayload(translatableFields);
613
+ const prompt = buildPrompt(translationPayload, config2.targetLanguage);
614
+ const systemPrompt = await buildSystemPrompt(userConfig);
615
+ const response = await callLLMProvider(prompt, systemPrompt, userConfig);
616
+ const translatedData = await parseLLMResponse(response);
617
+ const mergedContent = mergeTranslatedContent(fields, translatedData, translatableFields);
618
+ const uidFields = findUIDFields(contentType);
619
+ const translatedUIDs = await generateUIDsForTranslatedFields(
620
+ uidFields,
621
+ translatedData,
622
+ contentType.uid,
623
+ mergedContent
624
+ );
625
+ return {
626
+ data: {
627
+ ...mergedContent,
628
+ ...translatedUIDs
629
+ },
630
+ meta: {
631
+ ok: true,
632
+ status: 200,
633
+ message: "Translation completed successfully"
634
+ }
635
+ };
636
+ } catch (error) {
637
+ strapi2.log.error("LLM translation error:", error);
638
+ return {
639
+ data: fields,
640
+ // Return original fields in case of error
641
+ meta: {
642
+ ok: false,
643
+ status: 500,
644
+ message: error instanceof Error ? error.message : "Translation failed"
645
+ }
646
+ };
647
+ }
648
+ }
649
+ });
650
+ const buildPrompt = (fields, targetLanguage) => {
651
+ return `You are translating content from a CMS. Please translate the following JSON data to ${targetLanguage}.
652
+
653
+ IMPORTANT RULES:
654
+ 1. Preserve all JSON structure and keys exactly as provided
655
+ 2. Only translate string values
656
+ 3. Maintain any markdown formatting within the text
657
+ 4. Keep HTML tags intact if present
658
+ 5. Preserve any special characters or placeholders
659
+ 6. Return ONLY the translated JSON object
660
+ 7. Ensure the JSON is valid and well-formed. Keep arrays and nested objects intact
661
+ 8. Do not add any explanations or comments
662
+ 9. Ensure professional and culturally appropriate translations
663
+
664
+ SOURCE JSON:
665
+ ${JSON.stringify(fields, null, 2)}`;
666
+ };
667
+ const getUserConfig = async () => {
668
+ const pluginStore = strapi.store({
669
+ environment: strapi.config.environment,
670
+ type: "plugin",
671
+ name: "strapi-plugins-translate"
672
+ });
673
+ const config2 = await pluginStore.get({ key: "configuration" });
674
+ return config2;
675
+ };
676
+ const buildSystemPrompt = async (userConfig) => {
677
+ return `${userConfig?.systemPrompt || DEFAULT_SYSTEM_PROMPT} ${SYSTEM_PROMPT_APPENDIX}`;
678
+ };
679
+ const createLLMRequest = (messages, temperature = 0.1) => {
680
+ return llmClient.chat.completions.create({
681
+ model: LLM_MODEL,
682
+ messages,
683
+ temperature,
684
+ response_format: { type: "json_object" }
685
+ });
686
+ };
687
+ const callLLMProvider = async (prompt, systemPrompt, userConfig) => {
688
+ return createLLMRequest(
689
+ [
690
+ {
691
+ role: "system",
692
+ content: systemPrompt
693
+ },
694
+ {
695
+ role: "user",
696
+ content: prompt
697
+ }
698
+ ],
699
+ userConfig?.temperature ?? DEFAULT_LLM_TEMPERATURE
700
+ );
701
+ };
702
+ const requestJSONCorrection = async (invalidJson) => {
703
+ const response = await createLLMRequest([
704
+ {
705
+ role: "system",
706
+ content: SYSTEM_PROMPT_FIX
707
+ },
708
+ {
709
+ role: "user",
710
+ content: `${USER_PROMPT_FIX_PREFIX} ${invalidJson}`
711
+ }
712
+ ]);
713
+ const correctedContent = response.choices[0]?.message?.content;
714
+ if (!correctedContent) throw new Error("No content in correction response");
715
+ return safeJSONParse(correctedContent.trim());
716
+ };
717
+ const parseLLMResponse = async (response) => {
718
+ try {
719
+ const content = response.choices[0]?.message?.content;
720
+ if (!content) throw new Error("No content in response");
721
+ const cleanContent = cleanJSONString(content);
722
+ const jsonContent = extractJSONObject(cleanContent);
723
+ try {
724
+ return safeJSONParse(jsonContent);
725
+ } catch (parseError) {
726
+ const balancedContent = balanceJSONBraces(jsonContent);
727
+ try {
728
+ return safeJSONParse(balancedContent);
729
+ } catch (secondError) {
730
+ console.error("Second parse attempt failed:", secondError);
731
+ return await requestJSONCorrection(cleanContent);
732
+ }
733
+ }
734
+ } catch (error) {
735
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
736
+ throw new Error(`Translation failed: ${errorMessage}`);
737
+ }
738
+ };
739
+ const services = {
740
+ "llm-service": llmService
741
+ };
742
+ const plugin = {
743
+ register,
744
+ bootstrap,
745
+ destroy,
746
+ config,
747
+ controllers,
748
+ routes,
749
+ services,
750
+ contentTypes,
751
+ policies,
752
+ middlewares
753
+ };
754
+ module.exports = plugin;