strapi-llm-translator 0.9.1 → 0.9.3

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.
@@ -128,6 +128,26 @@ const routes = {
128
128
  };
129
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
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 SYSTEM_PROMPT_FIX = `You are a JSON correction assistant. Only return valid, corrected JSON.`;
132
+ const USER_PROMPT_FIX_PREFIX = "Fix this invalid JSON and return ONLY the corrected JSON. No explanations allowed. The JSON is:";
133
+ const cleanJSONString = (content) => {
134
+ return content.replace(/^```json\s*\n/, "").replace(/^```\s*\n/, "").replace(/\n\s*```$/, "").replace(/\u200B/g, "").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').trim();
135
+ };
136
+ const balanceJSONBraces = (content) => {
137
+ const openBraces = (content.match(/{/g) || []).length;
138
+ const closeBraces = (content.match(/}/g) || []).length;
139
+ if (openBraces > closeBraces) {
140
+ return content + "}".repeat(openBraces - closeBraces);
141
+ }
142
+ return content;
143
+ };
144
+ const safeJSONParse = (content) => {
145
+ const parsed = JSON.parse(content);
146
+ if (typeof parsed === "object" && parsed !== null) {
147
+ return parsed;
148
+ }
149
+ throw new Error("Invalid response format - not an object");
150
+ };
131
151
  const openai = new openai$1.OpenAI({
132
152
  baseURL: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL,
133
153
  apiKey: process.env.LLM_TRANSLATOR_LLM_API_KEY
@@ -262,7 +282,7 @@ const llmService = ({ strapi: strapi2 }) => ({
262
282
  const prompt = buildPrompt(translationPayload, config2.targetLanguage);
263
283
  const systemPrompt = await buildSystemPrompt(userConfig);
264
284
  const response = await callLLMProvider(prompt, systemPrompt, model, userConfig);
265
- const translatedData = parseLLMResponse(response);
285
+ const translatedData = await parseLLMResponse(response);
266
286
  const mergedContent = mergeTranslatedContent(fields, translatedData, translatableFields);
267
287
  const uidFields = findUIDFields(contentType);
268
288
  const translatedUIDs = await generateUIDsForTranslatedFields(
@@ -306,8 +326,9 @@ IMPORTANT RULES:
306
326
  4. Keep HTML tags intact if present
307
327
  5. Preserve any special characters or placeholders
308
328
  6. Return ONLY the translated JSON object
309
- 7. Do not add any explanations or comments
310
- 8. Ensure professional and culturally appropriate translations
329
+ 7. Ensure the JSON is valid and well-formed, all values must be strings
330
+ 8. Do not add any explanations or comments
331
+ 9. Ensure professional and culturally appropriate translations
311
332
 
312
333
  SOURCE JSON:
313
334
  ${JSON.stringify(fields, null, 2)}`;
@@ -324,10 +345,16 @@ const getUserConfig = async () => {
324
345
  const buildSystemPrompt = async (config2) => {
325
346
  return `${config2.systemPrompt || DEFAULT_SYSTEM_PROMPT} ${SYSTEM_PROMPT_APPENDIX}`;
326
347
  };
348
+ const createLLMRequest = (messages, temperature = 0.1) => {
349
+ return openai.chat.completions.create({
350
+ model,
351
+ messages,
352
+ temperature
353
+ });
354
+ };
327
355
  const callLLMProvider = async (prompt, systemPrompt, model2, config2) => {
328
- const response = await openai.chat.completions.create({
329
- model: model2,
330
- messages: [
356
+ return createLLMRequest(
357
+ [
331
358
  {
332
359
  role: "system",
333
360
  content: systemPrompt
@@ -337,37 +364,39 @@ const callLLMProvider = async (prompt, systemPrompt, model2, config2) => {
337
364
  content: prompt
338
365
  }
339
366
  ],
340
- temperature: config2.temperature
341
- });
342
- return response;
367
+ config2.temperature
368
+ );
369
+ };
370
+ const requestJSONCorrection = async (invalidJson) => {
371
+ const response = await createLLMRequest([
372
+ {
373
+ role: "system",
374
+ content: SYSTEM_PROMPT_FIX
375
+ },
376
+ {
377
+ role: "user",
378
+ content: `${USER_PROMPT_FIX_PREFIX} ${invalidJson}`
379
+ }
380
+ ]);
381
+ const correctedContent = response.choices[0]?.message?.content;
382
+ if (!correctedContent) throw new Error("No content in correction response");
383
+ return safeJSONParse(correctedContent.trim());
343
384
  };
344
- const parseLLMResponse = (response) => {
385
+ const parseLLMResponse = async (response) => {
345
386
  try {
346
387
  const content = response.choices[0]?.message?.content;
347
388
  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();
389
+ const cleanContent = cleanJSONString(content);
349
390
  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");
391
+ return safeJSONParse(cleanContent);
355
392
  } 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
- }
393
+ const balancedContent = balanceJSONBraces(cleanContent);
394
+ try {
395
+ return safeJSONParse(balancedContent);
396
+ } catch (secondError) {
397
+ console.error("Second parse attempt failed:", secondError);
398
+ return await requestJSONCorrection(cleanContent);
369
399
  }
370
- throw new Error(`JSON parsing failed: ${parseError.message}`);
371
400
  }
372
401
  } catch (error) {
373
402
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -127,6 +127,26 @@ const routes = {
127
127
  };
128
128
  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.";
129
129
  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.`;
130
+ const SYSTEM_PROMPT_FIX = `You are a JSON correction assistant. Only return valid, corrected JSON.`;
131
+ const USER_PROMPT_FIX_PREFIX = "Fix this invalid JSON and return ONLY the corrected JSON. No explanations allowed. The JSON is:";
132
+ const cleanJSONString = (content) => {
133
+ return content.replace(/^```json\s*\n/, "").replace(/^```\s*\n/, "").replace(/\n\s*```$/, "").replace(/\u200B/g, "").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').trim();
134
+ };
135
+ const balanceJSONBraces = (content) => {
136
+ const openBraces = (content.match(/{/g) || []).length;
137
+ const closeBraces = (content.match(/}/g) || []).length;
138
+ if (openBraces > closeBraces) {
139
+ return content + "}".repeat(openBraces - closeBraces);
140
+ }
141
+ return content;
142
+ };
143
+ const safeJSONParse = (content) => {
144
+ const parsed = JSON.parse(content);
145
+ if (typeof parsed === "object" && parsed !== null) {
146
+ return parsed;
147
+ }
148
+ throw new Error("Invalid response format - not an object");
149
+ };
130
150
  const openai = new OpenAI({
131
151
  baseURL: process.env.STRAPI_ADMIN_LLM_TRANSLATOR_LLM_BASE_URL,
132
152
  apiKey: process.env.LLM_TRANSLATOR_LLM_API_KEY
@@ -261,7 +281,7 @@ const llmService = ({ strapi: strapi2 }) => ({
261
281
  const prompt = buildPrompt(translationPayload, config2.targetLanguage);
262
282
  const systemPrompt = await buildSystemPrompt(userConfig);
263
283
  const response = await callLLMProvider(prompt, systemPrompt, model, userConfig);
264
- const translatedData = parseLLMResponse(response);
284
+ const translatedData = await parseLLMResponse(response);
265
285
  const mergedContent = mergeTranslatedContent(fields, translatedData, translatableFields);
266
286
  const uidFields = findUIDFields(contentType);
267
287
  const translatedUIDs = await generateUIDsForTranslatedFields(
@@ -305,8 +325,9 @@ IMPORTANT RULES:
305
325
  4. Keep HTML tags intact if present
306
326
  5. Preserve any special characters or placeholders
307
327
  6. Return ONLY the translated JSON object
308
- 7. Do not add any explanations or comments
309
- 8. Ensure professional and culturally appropriate translations
328
+ 7. Ensure the JSON is valid and well-formed, all values must be strings
329
+ 8. Do not add any explanations or comments
330
+ 9. Ensure professional and culturally appropriate translations
310
331
 
311
332
  SOURCE JSON:
312
333
  ${JSON.stringify(fields, null, 2)}`;
@@ -323,10 +344,16 @@ const getUserConfig = async () => {
323
344
  const buildSystemPrompt = async (config2) => {
324
345
  return `${config2.systemPrompt || DEFAULT_SYSTEM_PROMPT} ${SYSTEM_PROMPT_APPENDIX}`;
325
346
  };
347
+ const createLLMRequest = (messages, temperature = 0.1) => {
348
+ return openai.chat.completions.create({
349
+ model,
350
+ messages,
351
+ temperature
352
+ });
353
+ };
326
354
  const callLLMProvider = async (prompt, systemPrompt, model2, config2) => {
327
- const response = await openai.chat.completions.create({
328
- model: model2,
329
- messages: [
355
+ return createLLMRequest(
356
+ [
330
357
  {
331
358
  role: "system",
332
359
  content: systemPrompt
@@ -336,37 +363,39 @@ const callLLMProvider = async (prompt, systemPrompt, model2, config2) => {
336
363
  content: prompt
337
364
  }
338
365
  ],
339
- temperature: config2.temperature
340
- });
341
- return response;
366
+ config2.temperature
367
+ );
368
+ };
369
+ const requestJSONCorrection = async (invalidJson) => {
370
+ const response = await createLLMRequest([
371
+ {
372
+ role: "system",
373
+ content: SYSTEM_PROMPT_FIX
374
+ },
375
+ {
376
+ role: "user",
377
+ content: `${USER_PROMPT_FIX_PREFIX} ${invalidJson}`
378
+ }
379
+ ]);
380
+ const correctedContent = response.choices[0]?.message?.content;
381
+ if (!correctedContent) throw new Error("No content in correction response");
382
+ return safeJSONParse(correctedContent.trim());
342
383
  };
343
- const parseLLMResponse = (response) => {
384
+ const parseLLMResponse = async (response) => {
344
385
  try {
345
386
  const content = response.choices[0]?.message?.content;
346
387
  if (!content) throw new Error("No content in response");
347
- 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();
388
+ const cleanContent = cleanJSONString(content);
348
389
  try {
349
- const parsed = JSON.parse(cleanContent);
350
- if (typeof parsed === "object" && parsed !== null) {
351
- return parsed;
352
- }
353
- throw new Error("Invalid response format - not an object");
390
+ return safeJSONParse(cleanContent);
354
391
  } catch (parseError) {
355
- const openBraces = (cleanContent.match(/{/g) || []).length;
356
- const closeBraces = (cleanContent.match(/}/g) || []).length;
357
- if (openBraces > closeBraces) {
358
- const missingBraces = openBraces - closeBraces;
359
- const fixedContent = cleanContent + "}".repeat(missingBraces);
360
- try {
361
- const parsed = JSON.parse(fixedContent);
362
- if (typeof parsed === "object" && parsed !== null) {
363
- return parsed;
364
- }
365
- } catch (secondError) {
366
- console.error("Second parse attempt failed:", secondError);
367
- }
392
+ const balancedContent = balanceJSONBraces(cleanContent);
393
+ try {
394
+ return safeJSONParse(balancedContent);
395
+ } catch (secondError) {
396
+ console.error("Second parse attempt failed:", secondError);
397
+ return await requestJSONCorrection(cleanContent);
368
398
  }
369
- throw new Error(`JSON parsing failed: ${parseError.message}`);
370
399
  }
371
400
  } catch (error) {
372
401
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -1,2 +1,4 @@
1
1
  export declare 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.";
2
2
  export declare 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.";
3
+ export declare const SYSTEM_PROMPT_FIX = "You are a JSON correction assistant. Only return valid, corrected JSON.";
4
+ export declare const USER_PROMPT_FIX_PREFIX = "Fix this invalid JSON and return ONLY the corrected JSON. No explanations allowed. The JSON is:";
@@ -0,0 +1,3 @@
1
+ export declare const cleanJSONString: (content: string) => string;
2
+ export declare const balanceJSONBraces: (content: string) => string;
3
+ export declare const safeJSONParse: (content: string) => Record<string, any>;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.1",
2
+ "version": "0.9.3",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "plugin",
@@ -72,5 +72,13 @@
72
72
  "name": "strapi-llm-translator",
73
73
  "description": "AI-Powered Content Translation for Strapi",
74
74
  "license": "MIT",
75
- "author": "grenzbotin <franziska@vulpis.dev>"
75
+ "author": "grenzbotin <franziska@vulpis.dev>",
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/grenzbotin/strapi-llm-translator.git"
79
+ },
80
+ "bugs": {
81
+ "url": "https://github.com/grenzbotin/strapi-llm-translator/issues"
82
+ },
83
+ "homepage": "https://github.com/grenzbotin/strapi-llm-translator#readme"
76
84
  }