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