strapi-llm-translator 0.9.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/dist/_chunks/App-Boy_i56G.mjs +223 -0
  4. package/dist/_chunks/App-COSMdnpL.js +223 -0
  5. package/dist/_chunks/de-CvAR4QXO.js +29 -0
  6. package/dist/_chunks/de-D3VSS3Mq.mjs +29 -0
  7. package/dist/_chunks/en-BjVvIBv8.js +29 -0
  8. package/dist/_chunks/en-BnBUQVgF.mjs +29 -0
  9. package/dist/_chunks/index-BEhzLY5B.mjs +277 -0
  10. package/dist/_chunks/index-DuLkx3CK.js +276 -0
  11. package/dist/admin/index.js +3 -0
  12. package/dist/admin/index.mjs +4 -0
  13. package/dist/admin/src/components/Initializer.d.ts +5 -0
  14. package/dist/admin/src/components/LLMButton.d.ts +2 -0
  15. package/dist/admin/src/components/PluginIcon.d.ts +18 -0
  16. package/dist/admin/src/index.d.ts +11 -0
  17. package/dist/admin/src/pages/App.d.ts +2 -0
  18. package/dist/admin/src/pages/HomePage.d.ts +2 -0
  19. package/dist/admin/src/pluginId.d.ts +1 -0
  20. package/dist/admin/src/utils/constants.d.ts +4 -0
  21. package/dist/admin/src/utils/getLocaleFromUrl.d.ts +1 -0
  22. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  23. package/dist/server/index.js +392 -0
  24. package/dist/server/index.mjs +393 -0
  25. package/dist/server/src/bootstrap.d.ts +5 -0
  26. package/dist/server/src/config/constants.d.ts +2 -0
  27. package/dist/server/src/config/index.d.ts +9 -0
  28. package/dist/server/src/content-types/index.d.ts +2 -0
  29. package/dist/server/src/controllers/admin.controller.d.ts +11 -0
  30. package/dist/server/src/controllers/index.d.ts +23 -0
  31. package/dist/server/src/destroy.d.ts +5 -0
  32. package/dist/server/src/index.d.ts +63 -0
  33. package/dist/server/src/middlewares/index.d.ts +2 -0
  34. package/dist/server/src/policies/index.d.ts +2 -0
  35. package/dist/server/src/register.d.ts +5 -0
  36. package/dist/server/src/routes/admin.d.ts +9 -0
  37. package/dist/server/src/routes/index.d.ts +14 -0
  38. package/dist/server/src/services/index.d.ts +6 -0
  39. package/dist/server/src/services/llm-service.d.ts +6 -0
  40. package/dist/server/src/types/controllers.d.ts +22 -0
  41. package/dist/server/src/types/index.d.ts +41 -0
  42. package/package.json +70 -0
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ const openai$1 = require("openai");
3
+ const bootstrap = ({ strapi: strapi2 }) => {
4
+ };
5
+ const destroy = ({ strapi: strapi2 }) => {
6
+ };
7
+ const register = ({ strapi: strapi2 }) => {
8
+ };
9
+ const config = {
10
+ default: {
11
+ llmApiKey: process.env.LLM_TRANSLATOR_LLM_API_KEY,
12
+ llmEndpoint: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL || "https://api.openai.com/v1/chat/completions",
13
+ llmModel: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL || "gpt-4o"
14
+ },
15
+ validator() {
16
+ const {
17
+ LLM_TRANSLATOR_LLM_API_KEY,
18
+ STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL,
19
+ STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL
20
+ } = process.env;
21
+ if (!LLM_TRANSLATOR_LLM_API_KEY) {
22
+ throw new Error("LLM_TRANSLATOR_LLM_API_KEY is required");
23
+ }
24
+ if (!STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL) {
25
+ console.info(
26
+ "STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL is not set, using default: https://api.openai.com/v1/chat/completions"
27
+ );
28
+ } else {
29
+ console.info(
30
+ `STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL is set to: ${STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL}`
31
+ );
32
+ }
33
+ if (!STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL) {
34
+ console.info("STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL is not set, using default: gpt-4o");
35
+ } else {
36
+ console.info(
37
+ `STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL is set to: ${STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL}`
38
+ );
39
+ }
40
+ }
41
+ };
42
+ const contentTypes = {};
43
+ const controllers$1 = ({ strapi: strapi2 }) => ({
44
+ // Genertate translations
45
+ async generate(ctx) {
46
+ try {
47
+ const { fields, components, targetLanguage, contentType } = ctx.request.body;
48
+ const result = await strapi2.plugin("strapi-llm-translator").service("llm-service").generateWithLLM(contentType, fields, components, {
49
+ targetLanguage
50
+ });
51
+ ctx.status = result.meta.status;
52
+ ctx.body = result;
53
+ } catch (error) {
54
+ console.error("Error in generate controller:", error);
55
+ ctx.status = 500;
56
+ ctx.body = {
57
+ meta: {
58
+ ok: false,
59
+ status: 500,
60
+ message: "Internal server error"
61
+ }
62
+ };
63
+ }
64
+ },
65
+ // Get the configuration
66
+ async getConfig(ctx) {
67
+ const pluginStore = strapi2.store({
68
+ environment: strapi2.config.environment,
69
+ type: "plugin",
70
+ name: "strapi-llm-translator"
71
+ // replace with your plugin name
72
+ });
73
+ const config2 = await pluginStore.get({ key: "configuration" });
74
+ ctx.body = config2 || {};
75
+ },
76
+ // Save the configuration
77
+ async setConfig(ctx) {
78
+ const { body } = ctx.request;
79
+ const pluginStore = strapi2.store({
80
+ environment: strapi2.config.environment,
81
+ type: "plugin",
82
+ name: "strapi-llm-translator"
83
+ // replace with your plugin name
84
+ });
85
+ await pluginStore.set({
86
+ key: "configuration",
87
+ value: { ...body }
88
+ });
89
+ ctx.body = await pluginStore.get({ key: "configuration" });
90
+ }
91
+ });
92
+ const controllers = {
93
+ admin: controllers$1
94
+ };
95
+ const middlewares = {};
96
+ const policies = {};
97
+ const adminRoutes = [
98
+ {
99
+ method: "POST",
100
+ path: "/generate",
101
+ handler: "admin.generate",
102
+ config: {
103
+ policies: []
104
+ }
105
+ },
106
+ {
107
+ method: "GET",
108
+ path: "/config",
109
+ handler: "admin.getConfig",
110
+ config: {
111
+ policies: []
112
+ }
113
+ },
114
+ {
115
+ method: "POST",
116
+ path: "/config",
117
+ handler: "admin.setConfig",
118
+ config: {
119
+ policies: []
120
+ }
121
+ }
122
+ ];
123
+ const routes = {
124
+ admin: {
125
+ type: "admin",
126
+ routes: adminRoutes
127
+ }
128
+ };
129
+ 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.";
130
+ 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.`;
131
+ const openai = new openai$1.OpenAI({
132
+ baseURL: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL,
133
+ apiKey: process.env.LLM_TRANSLATOR_LLM_API_KEY
134
+ });
135
+ const model = process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_MODEL;
136
+ const isTranslatableField = (contentType, key, value) => {
137
+ if (typeof value !== "string") {
138
+ return false;
139
+ }
140
+ const fieldSchema = contentType?.attributes?.[key];
141
+ if (!fieldSchema) {
142
+ return false;
143
+ }
144
+ const isStringOrText = ["string", "text"].includes(fieldSchema.type);
145
+ const isNotUID = fieldSchema.type !== "uid";
146
+ const isLocalizable = fieldSchema.pluginOptions?.i18n?.localized !== false;
147
+ return isStringOrText && isNotUID && isLocalizable;
148
+ };
149
+ const extractTranslatableFields = (contentType, fields, components = {}) => {
150
+ const translatableFields = [];
151
+ Object.entries(fields).forEach(([key, value]) => {
152
+ if (isTranslatableField(contentType, key, value)) {
153
+ translatableFields.push({
154
+ path: [key],
155
+ value,
156
+ originalPath: [key]
157
+ });
158
+ }
159
+ });
160
+ if (fields.blocks && Array.isArray(fields.blocks)) {
161
+ fields.blocks.forEach((block, blockIndex) => {
162
+ if (block.__component && components[block.__component]) {
163
+ const componentSchema = components[block.__component];
164
+ Object.entries(componentSchema.attributes).forEach(([fieldName, schema]) => {
165
+ if (block[fieldName] && typeof block[fieldName] === "string" && ["string", "text", "richtext"].includes(schema.type)) {
166
+ translatableFields.push({
167
+ path: ["blocks", String(blockIndex), fieldName],
168
+ value: block[fieldName],
169
+ originalPath: ["blocks", String(blockIndex), fieldName]
170
+ });
171
+ }
172
+ });
173
+ }
174
+ });
175
+ }
176
+ return translatableFields;
177
+ };
178
+ const prepareTranslationPayload = (fields) => {
179
+ const payload = {};
180
+ fields.forEach((field) => {
181
+ let current = payload;
182
+ field.path.forEach((part, index2) => {
183
+ if (index2 === field.path.length - 1) {
184
+ current[part] = field.value;
185
+ } else {
186
+ current[part] = current[part] || {};
187
+ current = current[part];
188
+ }
189
+ });
190
+ });
191
+ return payload;
192
+ };
193
+ const mergeTranslatedContent = (originalData, translatedData, translatableFields) => {
194
+ const result = JSON.parse(JSON.stringify(originalData));
195
+ translatableFields.forEach((field) => {
196
+ let translatedValue = translatedData;
197
+ for (const part of field.path) {
198
+ translatedValue = translatedValue?.[part];
199
+ if (translatedValue === void 0) break;
200
+ }
201
+ if (translatedValue !== void 0) {
202
+ let current = result;
203
+ field.originalPath.forEach((part, index2) => {
204
+ if (index2 === field.originalPath.length - 1) {
205
+ current[part] = translatedValue;
206
+ } else {
207
+ current = current[part];
208
+ }
209
+ });
210
+ }
211
+ });
212
+ return result;
213
+ };
214
+ const generateSlug = async (data, field, contentTypeUID) => {
215
+ const uidService = strapi.service("plugin::content-manager.uid");
216
+ const slug = await uidService.generateUIDField({
217
+ contentTypeUID,
218
+ field,
219
+ data
220
+ });
221
+ return slug;
222
+ };
223
+ const findUIDFields = (contentType) => {
224
+ const uidFields = [];
225
+ Object.entries(contentType.attributes || {}).forEach(([fieldName, schema]) => {
226
+ if (schema.type === "uid" && schema.targetField) {
227
+ uidFields.push({
228
+ fieldName,
229
+ targetField: schema.targetField
230
+ });
231
+ }
232
+ });
233
+ return uidFields;
234
+ };
235
+ const generateUIDsForTranslatedFields = async (uidFields, translatedData, contentTypeUID, mergedContent) => {
236
+ const translatedUIDs = {};
237
+ for (const { fieldName, targetField } of uidFields) {
238
+ if (translatedData[targetField] !== void 0) {
239
+ try {
240
+ const newUID = await generateSlug(
241
+ {
242
+ ...mergedContent,
243
+ [targetField]: translatedData[targetField]
244
+ },
245
+ fieldName,
246
+ contentTypeUID
247
+ );
248
+ translatedUIDs[fieldName] = newUID;
249
+ } catch (error) {
250
+ console.error(`Failed to generate UID for field ${fieldName}:`, error);
251
+ }
252
+ }
253
+ }
254
+ return translatedUIDs;
255
+ };
256
+ const llmService = ({ strapi: strapi2 }) => ({
257
+ async generateWithLLM(contentType, fields, components, config2) {
258
+ try {
259
+ const userConfig = await getUserConfig();
260
+ const translatableFields = extractTranslatableFields(contentType, fields, components);
261
+ const translationPayload = prepareTranslationPayload(translatableFields);
262
+ const prompt = buildPrompt(translationPayload, config2.targetLanguage);
263
+ const systemPrompt = await buildSystemPrompt(userConfig);
264
+ const response = await callLLMProvider(prompt, systemPrompt, model, userConfig);
265
+ const translatedData = parseLLMResponse(response);
266
+ const mergedContent = mergeTranslatedContent(fields, translatedData, translatableFields);
267
+ const uidFields = findUIDFields(contentType);
268
+ const translatedUIDs = await generateUIDsForTranslatedFields(
269
+ uidFields,
270
+ translatedData,
271
+ contentType.uid,
272
+ mergedContent
273
+ );
274
+ return {
275
+ data: {
276
+ ...mergedContent,
277
+ ...translatedUIDs
278
+ },
279
+ meta: {
280
+ ok: true,
281
+ status: 200,
282
+ message: "Translation completed successfully"
283
+ }
284
+ };
285
+ } catch (error) {
286
+ strapi2.log.error("LLM translation error:", error);
287
+ return {
288
+ data: fields,
289
+ // Return original fields in case of error
290
+ meta: {
291
+ ok: false,
292
+ status: 500,
293
+ message: error instanceof Error ? error.message : "Translation failed"
294
+ }
295
+ };
296
+ }
297
+ }
298
+ });
299
+ const buildPrompt = (fields, targetLanguage) => {
300
+ return `You are translating content from a CMS. Please translate the following JSON data to ${targetLanguage}.
301
+
302
+ IMPORTANT RULES:
303
+ 1. Preserve all JSON structure and keys exactly as provided
304
+ 2. Only translate string values
305
+ 3. Maintain any markdown formatting within the text
306
+ 4. Keep HTML tags intact if present
307
+ 5. Preserve any special characters or placeholders
308
+ 6. Return ONLY the translated JSON object
309
+ 7. Do not add any explanations or comments
310
+ 8. Ensure professional and culturally appropriate translations
311
+
312
+ SOURCE JSON:
313
+ ${JSON.stringify(fields, null, 2)}`;
314
+ };
315
+ const getUserConfig = async () => {
316
+ const pluginStore = strapi.store({
317
+ environment: strapi.config.environment,
318
+ type: "plugin",
319
+ name: "strapi-llm-translator"
320
+ });
321
+ const config2 = await pluginStore.get({ key: "configuration" });
322
+ return config2;
323
+ };
324
+ const buildSystemPrompt = async (config2) => {
325
+ return `${config2.systemPrompt || DEFAULT_SYSTEM_PROMPT} ${SYSTEM_PROMPT_APPENDIX}`;
326
+ };
327
+ const callLLMProvider = async (prompt, systemPrompt, model2, config2) => {
328
+ const response = await openai.chat.completions.create({
329
+ model: model2,
330
+ messages: [
331
+ {
332
+ role: "system",
333
+ content: systemPrompt
334
+ },
335
+ {
336
+ role: "user",
337
+ content: prompt
338
+ }
339
+ ],
340
+ temperature: config2.temperature
341
+ });
342
+ return response;
343
+ };
344
+ const parseLLMResponse = (response) => {
345
+ try {
346
+ const content = response.choices[0]?.message?.content;
347
+ if (!content) throw new Error("No content in response");
348
+ const cleanContent = content.replace(/^```json\s*\n/, "").replace(/^```\s*\n/, "").replace(/\n\s*```$/, "").replace(/\u200B/g, "").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').trim();
349
+ try {
350
+ const parsed = JSON.parse(cleanContent);
351
+ if (typeof parsed === "object" && parsed !== null) {
352
+ return parsed;
353
+ }
354
+ throw new Error("Invalid response format - not an object");
355
+ } catch (parseError) {
356
+ const openBraces = (cleanContent.match(/{/g) || []).length;
357
+ const closeBraces = (cleanContent.match(/}/g) || []).length;
358
+ if (openBraces > closeBraces) {
359
+ const missingBraces = openBraces - closeBraces;
360
+ const fixedContent = cleanContent + "}".repeat(missingBraces);
361
+ try {
362
+ const parsed = JSON.parse(fixedContent);
363
+ if (typeof parsed === "object" && parsed !== null) {
364
+ return parsed;
365
+ }
366
+ } catch (secondError) {
367
+ console.error("Second parse attempt failed:", secondError);
368
+ }
369
+ }
370
+ throw new Error(`JSON parsing failed: ${parseError.message}`);
371
+ }
372
+ } catch (error) {
373
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
374
+ throw new Error(`Translation failed: ${errorMessage}`);
375
+ }
376
+ };
377
+ const services = {
378
+ "llm-service": llmService
379
+ };
380
+ const index = {
381
+ register,
382
+ bootstrap,
383
+ destroy,
384
+ config,
385
+ controllers,
386
+ routes,
387
+ services,
388
+ contentTypes,
389
+ policies,
390
+ middlewares
391
+ };
392
+ module.exports = index;