added translate textadmin excel

This commit is contained in:
Arwid Thornström
2025-11-13 22:37:01 +01:00
parent bfad35a217
commit 0d79e2f2b5
6 changed files with 1179 additions and 7 deletions
@@ -0,0 +1,114 @@
You are a translation expert and copywriter working with translating text for the company Photowall.
Photowall is a company with a global e-commerce website selling custom printed wallpaper, canvas, posters and paint.
Photowall have two different types of wallpaper. One type is non repeating patterns (wall murals) and one is repeating patterns (wallpapers).
Photowall sell a highend wallpaper product of exclusive quality.
Translate to natural sounding language.
Make the text in the translated text some and flow naturally for that specific language.
Use the correct casing for the translated language.
The tone of voice should be premium and highend.
When translating the word "wallpaper" use these translations
"Danish (Denmark)": "tapet",
"German (Germany)": "tapete",
"English (Europe)": "wallpaper",
"English (United Kingdom)": "wallpaper",
"English (United States)": "wallpaper",
"Spanish (Spain)": "papel pintado",
"Finnish (Finland)": "tapetti",
"French (France)": "papier peint",
"Italian (Italy)": "carta da parati",
"Japanese": "壁紙",
"Dutch (Netherlands)": "behang",
"Norwegian Nynorsk (Norway)": "tapet",
"Polish (Poland)": "tapeta",
"Portuguese (Portugal)": "papel de parede",
"Romanian (Romania)": "tapet",
"Swedish (Sweden)": "tapet"
When translating the word "wallpapers" use these translations
"Danish (Denmark)": "tapeter",
"German (Germany)": "tapeten",
"English (Europe)": "wallpapers",
"English (United Kingdom)": "wallpapers",
"English (United States)": "wallpapers",
"Spanish (Spain)": "papeles pintados",
"Finnish (Finland)": "tapetit",
"French (France)": "papiers peints",
"Italian (Italy)": "carte da parati",
"Japanese": "壁紙",
"Dutch (Netherlands)": "behang",
"Norwegian Nynorsk (Norway)": "tapeter",
"Polish (Poland)": "tapety",
"Portuguese (Portugal)": "papéis de parede",
"Romanian (Romania)": "tapete",
"Swedish (Sweden)": "tapeter"
When translating the word "wall mural" use these translations
"Danish (Denmark)": "fototapet",
"German (Germany)": "fototapete",
"English (Europe)": "wall mural",
"English (United Kingdom)": "wall mural",
"English (United States)": "wall mural",
"Spanish (Spain)": "fotomural",
"Finnish (Finland)": "valokuvatapetti",
"French (France)": "décoration murale",
"Italian (Italy)": "decorazione murale",
"Japanese": "ウォールミューラル",
"Dutch (Netherlands)": "fotobehang",
"Norwegian Nynorsk (Norway)": "fototapet",
"Polish (Poland)": "fototapeta",
"Portuguese (Portugal)": "mural de parede",
"Romanian (Romania)": "fototapet",
"Swedish (Sweden)": "fototapet"
When translating the word "wall murals" use these translations
"Danish (Denmark)": "fototapeter",
"German (Germany)": "fototapeten",
"English (Europe)": "wall murals",
"English (United Kingdom)": "wall murals",
"English (United States)": "wall murals",
"Spanish (Spain)": "fotomurales",
"Finnish (Finland)": "valokuvatapetit",
"French (France)": "papiers peints panoramiques",
"Italian (Italy)": "fotomurali",
"Japanese": "ウォールミューラル",
"Dutch (Netherlands)": "fotobehang",
"Norwegian Nynorsk (Norway)": "fototapeter",
"Polish (Poland)": "fototapety",
"Portuguese (Portugal)": "murais de parede",
"Romanian (Romania)": "fototapete",
"Swedish (Sweden)": "fototapeter"
The languages are: Danish, German, English, Spanish, Finnish, French, Italian, Japanese, Dutch, Norwegian, Polish, Portuguese, Romanian, Swedish
The english input text for translation is
<INPUT START>
{input}
</INPUT END>
!IMPORTANT!
Return only the result in a JSON format defined within the <OUTPUT> and </OUTPUT>:
<OUTPUT>
{
"Danish (Denmark)": "string",
"German (Germany)": "string",
"English (Europe)": "string",
"English (United Kingdom)": "string",
"English (United States)": "string",
"Spanish (Spain)": "string",
"Finnish (Finland)": "string",
"French (France)": "string",
"Italian (Italy)": "string",
"Japanese": "string",
"Dutch (Netherlands)": "string",
"Norwegian Nynorsk (Norway)": "string",
"Polish (Poland)": "string",
"Portuguese (Portugal)": "string",
"Romanian (Romania)": "string",
"Swedish (Sweden)": "string"
}
</OUTPUT>
Output the JSON string directly without any additional formatting or delimiters.
@@ -0,0 +1,589 @@
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
import chalk = require('chalk');
import { getSettings } from '../../../globals';
const XLSX = require('xlsx');
const fs = require('fs');
const path = require('path');
const { Select } = require('enquirer');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const OpenAI = require('openai');
// Debug mode: set to true to save all AI model responses to JSON files
const DEBUG_MODE = false;
// Function to save debug response to JSON file
function saveDebugResponse(
response: string,
debugDir: string,
filename: string,
metadata: any = {},
): void {
if (!DEBUG_MODE) return;
try {
// Ensure debug directory exists
if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true });
}
const debugData = {
timestamp: new Date().toISOString(),
response: response,
...metadata,
};
const filePath = path.join(debugDir, filename);
fs.writeFileSync(filePath, JSON.stringify(debugData, null, 2), 'utf8');
} catch (error) {
console.error('Error saving debug response:', error.message);
}
}
// Function to extract JSON from text that might be wrapped in markdown code blocks
function extractJSON(text: string): string {
const jsonRegex = /```(?:json)?\s*([\s\S]*?)```/;
const match = text.match(jsonRegex);
if (match && match[1]) {
return match[1].trim();
}
return text.trim();
}
// Function to validate and fix JSON if needed
async function validateAndFixJSON(
response: string,
processorFunction: (
input: string,
prompt: string,
apiKey: string,
model: string,
debugDir?: string,
debugMetadata?: any,
) => Promise<string | null>,
apiKey: string,
model: string,
debugDir?: string,
debugMetadata?: any,
): Promise<{ isValid: boolean; json: any; rawText: string } | null> {
// Try to extract and parse JSON from the response
const extractedJSON = extractJSON(response);
try {
const parsed = JSON.parse(extractedJSON);
return { isValid: true, json: parsed, rawText: extractedJSON };
} catch (error) {
// JSON is invalid, try to fix it once
console.log(
chalk.yellow('⚠️ Invalid JSON detected, attempting to fix...'),
);
const fixPrompt = `The following text should be valid JSON but contains errors. Please fix it to be valid JSON format.
IMPORTANT INSTRUCTIONS:
- Output ONLY valid JSON, nothing else
- Do not add any explanations, comments, or markdown formatting
- Do not wrap the output in code blocks or backticks
- Preserve all the data from the original text
- Fix any syntax errors like missing commas, quotes, brackets, or braces
- Ensure all string values are properly quoted
- Ensure all keys are properly quoted
- Remove any trailing commas
- Make sure the JSON structure is complete and properly closed
BROKEN TEXT TO FIX:
${response}
OUTPUT ONLY THE FIXED JSON:`;
const fixedResponse = await processorFunction(
'',
fixPrompt,
apiKey,
model,
debugDir,
{ ...debugMetadata, isFixAttempt: true, originalResponse: response },
);
if (!fixedResponse) {
return null;
}
// Try to parse the fixed response
const fixedExtractedJSON = extractJSON(fixedResponse);
try {
const parsed = JSON.parse(fixedExtractedJSON);
console.log(chalk.green('✓ Successfully fixed invalid JSON'));
return { isValid: true, json: parsed, rawText: fixedExtractedJSON };
} catch (fixError) {
console.error(chalk.red('✗ Failed to fix JSON even after retry'));
return null;
}
}
}
// Process text with Gemini AI
async function processWithGemini(
inputText: string,
promptTemplate: string,
apiKey: string,
model: string,
debugDir?: string,
debugMetadata?: any,
) {
const generationConfig = {
temperature: 0.7, // Slightly lower for more consistent JSON output
topP: 0.95,
topK: 40,
maxOutputTokens: 32768, // Significantly increased for large translation responses (Gemini 2.5 Pro supports up to 65,536)
};
const genAI = new GoogleGenerativeAI(apiKey);
const geminiModel = genAI.getGenerativeModel({
model: model,
generationConfig,
});
try {
// If inputText is provided, replace {input} placeholder, otherwise use prompt as-is
const prompt = inputText
? promptTemplate.replace('{input}', inputText)
: promptTemplate;
const result = await geminiModel.generateContent(prompt);
const response = result.response;
const responseText = response.text();
// Save debug response if debug mode is enabled
if (DEBUG_MODE && debugDir) {
const filename = `gemini_${Date.now()}_${debugMetadata?.rowNumber || 'unknown'}.json`;
saveDebugResponse(responseText, debugDir, filename, {
provider: 'Gemini',
model: model,
inputText: inputText,
prompt: prompt,
...debugMetadata,
});
}
return responseText;
} catch (error) {
console.error('Error processing with Gemini:', error.message);
return null;
}
}
// Process text with OpenAI
async function processWithOpenAI(
inputText: string,
promptTemplate: string,
apiKey: string,
model: string,
debugDir?: string,
debugMetadata?: any,
) {
const openai = new OpenAI({ apiKey });
try {
// If inputText is provided, replace {input} placeholder, otherwise use prompt as-is
const prompt = inputText
? promptTemplate.replace('{input}', inputText)
: promptTemplate;
const response = await openai.chat.completions.create({
model: model,
messages: [{ role: 'user', content: prompt }],
});
const responseText = response.choices[0]?.message?.content || null;
// Save debug response if debug mode is enabled
if (DEBUG_MODE && debugDir && responseText) {
const filename = `openai_${Date.now()}_${debugMetadata?.rowNumber || 'unknown'}.json`;
saveDebugResponse(responseText, debugDir, filename, {
provider: 'OpenAI',
model: model,
inputText: inputText,
prompt: prompt,
...debugMetadata,
});
}
return responseText;
} catch (error) {
console.error('Error processing with OpenAI:', error.message);
return null;
}
}
module.exports = {
name: 'translate_excel',
alias: ['te'],
description: 'Translate Excel files using AI (te)',
hidden: false,
run: async (toolbox: GluegunMenuToolbox) => {
const { print, filesystem } = toolbox;
print.info(chalk.cyan('📊 Excel Translation Tool'));
print.info('');
// Get settings to check for API keys
const config = await getSettings(toolbox);
if (!config) {
print.error(
chalk.red('❌ No configuration found. Please run pwcli setup settings'),
);
return;
}
// Check which API keys are configured
const hasGemini = config.llm_api_keys?.gemini_api_key;
const hasOpenAI = config.llm_api_keys?.openai_api_key;
if (!hasGemini && !hasOpenAI) {
print.error(
chalk.red(
'❌ No AI API keys configured. Please run pwcli setup settings',
),
);
return;
}
// Step 1: Find Excel files in current directory
const currentDir = process.cwd();
const files = fs.readdirSync(currentDir);
const excelFiles = files.filter((file: string) =>
file.toLowerCase().endsWith('.xlsx'),
);
if (excelFiles.length === 0) {
print.error(
chalk.red('❌ No Excel (.xlsx) files found in current directory'),
);
return;
}
// Step 2: Let user select an Excel file
const filePrompt = new Select({
name: 'excelFile',
message: 'Select the Excel file to translate:',
choices: excelFiles,
});
const selectedFile = await filePrompt.run();
print.info(chalk.gray(`Selected file: ${selectedFile}`));
print.info('');
// Step 3: Find prompt .txt files in translate_excel folder
const promptDir = path.join(__dirname, '.');
const promptFiles = fs
.readdirSync(promptDir)
.filter((file: string) => file.toLowerCase().endsWith('.txt'));
if (promptFiles.length === 0) {
print.error(
chalk.red('❌ No prompt (.txt) files found in translate_excel folder'),
);
return;
}
// Create a mapping of display names to actual filenames
const promptDisplayNames = promptFiles.map((file: string) => {
// Remove .txt extension and convert to Title Case
const nameWithoutExt = file.replace(/\.txt$/i, '');
// Replace underscores and hyphens with spaces, then title case
return nameWithoutExt
.replace(/[_-]/g, ' ')
.split(' ')
.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(' ');
});
// Create mapping object for later use
const promptMapping = {};
promptFiles.forEach((file: string, index: number) => {
promptMapping[promptDisplayNames[index]] = file;
});
// Step 4: Let user select a prompt file
const promptPrompt = new Select({
name: 'promptFile',
message: 'Select the translation prompt to use:',
choices: promptDisplayNames,
});
const selectedPromptDisplay = await promptPrompt.run();
const selectedPromptFile = promptMapping[selectedPromptDisplay];
const promptPath = path.join(promptDir, selectedPromptFile);
const promptTemplate = fs.readFileSync(promptPath, 'utf8');
print.info(chalk.gray(`Selected prompt: ${selectedPromptDisplay}`));
print.info('');
// Step 5: Let user select AI provider
const providerChoices = [];
if (hasGemini) providerChoices.push('Gemini');
if (hasOpenAI) providerChoices.push('OpenAI');
const providerPrompt = new Select({
name: 'provider',
message: 'Select AI provider:',
choices: providerChoices,
});
const selectedProvider = await providerPrompt.run();
print.info(chalk.gray(`Selected provider: ${selectedProvider}`));
print.info('');
// Step 6: Let user select model based on provider
let modelChoices: string[] = [];
let selectedModel = '';
if (selectedProvider === 'Gemini') {
modelChoices = [
'gemini-flash-latest',
'gemini-flash-lite-latest',
'gemini-2.5-pro',
];
const modelPrompt = new Select({
name: 'model',
message: 'Select Gemini model:',
choices: modelChoices,
});
selectedModel = await modelPrompt.run();
} else if (selectedProvider === 'OpenAI') {
modelChoices = ['gpt-5-nano', 'gpt-5-mini', 'gpt-5.1'];
const modelPrompt = new Select({
name: 'model',
message: 'Select OpenAI model:',
choices: modelChoices,
});
selectedModel = await modelPrompt.run();
}
print.info(chalk.gray(`Selected model: ${selectedModel}`));
print.info('');
// Step 7: Read and process the Excel file
const filePath = path.join(currentDir, selectedFile);
const workbook = XLSX.readFile(filePath);
// Use first sheet
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// Convert to JSON
const data = XLSX.utils.sheet_to_json(worksheet);
if (data.length === 0) {
print.error(chalk.red('❌ Excel file is empty'));
return;
}
// Get original column order
const originalColumns = Object.keys(data[0]);
// Check if "English (Europe)" column exists
const inputColumn = 'English (Europe)';
if (!data[0].hasOwnProperty(inputColumn)) {
print.error(
chalk.red(
`❌ Column "${inputColumn}" not found. Available columns: ${originalColumns.join(', ')}`,
),
);
return;
}
print.info(
chalk.cyan(`Processing ${data.length} rows from sheet "${sheetName}"...`),
);
print.info('');
// Set up debug directory if debug mode is enabled
const debugDir = DEBUG_MODE
? path.join(currentDir, `${path.parse(selectedFile).name}_debug`)
: undefined;
if (DEBUG_MODE && debugDir) {
print.info(
chalk.gray(`Debug mode enabled. Saving responses to: ${debugDir}`),
);
print.info('');
}
// Step 8: Process each row with AI
for (let i = 0; i < data.length; i++) {
const row = data[i];
const inputText = row[inputColumn];
if (!inputText || inputText.trim() === '') {
print.info(
chalk.gray(`Skipping row ${i + 1}/${data.length} (empty input text)`),
);
continue;
}
print.info(
chalk.yellow(
`Processing row ${i + 1}/${data.length}: "${inputText.substring(0, 50)}${inputText.length > 50 ? '...' : ''}"`,
),
);
let aiResult = null;
let processorFunction = null;
let apiKey = null;
const debugMetadata = {
rowNumber: i + 1,
totalRows: data.length,
inputColumn: inputColumn,
selectedFile: selectedFile,
selectedPrompt: selectedPromptDisplay,
};
if (selectedProvider === 'Gemini') {
processorFunction = processWithGemini;
apiKey = config.llm_api_keys.gemini_api_key;
aiResult = await processWithGemini(
inputText,
promptTemplate,
apiKey,
selectedModel,
debugDir,
debugMetadata,
);
} else if (selectedProvider === 'OpenAI') {
processorFunction = processWithOpenAI;
apiKey = config.llm_api_keys.openai_api_key;
aiResult = await processWithOpenAI(
inputText,
promptTemplate,
apiKey,
selectedModel,
debugDir,
debugMetadata,
);
}
if (aiResult) {
// Validate and fix JSON if needed
const validationResult = await validateAndFixJSON(
aiResult,
processorFunction,
apiKey,
selectedModel,
debugDir,
debugMetadata,
);
if (validationResult && validationResult.json) {
const translationResult = validationResult.json;
// Update the row with translations from the LLM
for (const [country, translation] of Object.entries(
translationResult,
)) {
// Add translation if it's valid
if (translation !== null && translation !== undefined) {
row[country] = translation;
}
}
print.success(chalk.green(`✓ Translated row ${i + 1}`));
} else {
print.error(
chalk.red(
`❌ Error parsing AI response for row ${i + 1}: Invalid JSON even after retry attempt`,
),
);
console.error('AI response:', aiResult);
}
} else {
print.error(chalk.red(`❌ AI processing failed for row ${i + 1}`));
}
}
// Step 9: Write the translated data to a new Excel file
const parsedPath = path.parse(filePath);
const outputFilePath = path.join(
parsedPath.dir,
`${parsedPath.name}_translated${parsedPath.ext}`,
);
// Define the expected column order for output
const expectedColumnOrder = [
'name',
'category',
'Danish (Denmark)',
'German (Austria)',
'German (Switzerland)',
'German (Germany)',
'German (Luxembourg)',
'English (Australia)',
'English (Canada)',
'English (Estonia)',
'English (Europe)',
'English (United Kingdom)',
'English (Ireland)',
'English (New Zealand)',
'English (Singapore)',
'English (United States)',
'Spanish (Spain)',
'Finnish (Finland)',
'French (Belgium)',
'French (Canada)',
'French (Switzerland)',
'French (France)',
'French (Luxembourg)',
'Italian (Switzerland)',
'Italian (Italy)',
'Japanese',
'Dutch (Belgium)',
'Dutch (Netherlands)',
'Norwegian Nynorsk (Norway)',
'Polish (Poland)',
'Portuguese (Portugal)',
'Romanian (Romania)',
'Swedish (Sweden)',
];
// Build final column order: all expected columns + any extra columns from input
const finalColumnOrder = [
...expectedColumnOrder,
...originalColumns.filter((col) => !expectedColumnOrder.includes(col)),
];
// Ensure each row has all columns in the correct order (even if empty)
const normalizedData = data.map((row) => {
const normalizedRow = {};
for (const col of finalColumnOrder) {
normalizedRow[col] = row.hasOwnProperty(col) ? row[col] : '';
}
return normalizedRow;
});
// Write with the specified column order
const newWorksheet = XLSX.utils.json_to_sheet(normalizedData, {
header: finalColumnOrder,
});
const newWorkbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, sheetName);
XLSX.writeFile(newWorkbook, outputFilePath);
print.info('');
print.success(
chalk.green(
`✅ Successfully exported translated data to ${outputFilePath}`,
),
);
print.info(
chalk.gray(`Processed ${data.length} rows from sheet "${sheetName}"`),
);
print.info('');
// Return to AI menu
await toolbox.menu.showMenu('ai');
},
};
+22
View File
@@ -114,6 +114,18 @@ module.exports = {
localhost_url: '',
localhost_username: '',
localhost_password: '',
llm_api_keys: {
gemini_api_key: '',
openai_api_key: '',
},
};
}
// Ensure llm_api_keys exists in existing configs
if (!config.llm_api_keys) {
config.llm_api_keys = {
gemini_api_key: '',
openai_api_key: '',
};
}
@@ -149,6 +161,16 @@ module.exports = {
initial: config.localhost_password,
}).run();
config.llm_api_keys.gemini_api_key = await new Input({
message: `If you want to use Gemini AI features, please insert\nyour Gemini API key. Otherwise leave blank >`,
initial: config.llm_api_keys.gemini_api_key,
}).run();
config.llm_api_keys.openai_api_key = await new Input({
message: `If you want to use OpenAI features, please insert\nyour OpenAI API key. Otherwise leave blank >`,
initial: config.llm_api_keys.openai_api_key,
}).run();
print.info(`
${JSON.stringify(config)}
+4
View File
@@ -14,6 +14,10 @@ export type TConfig = {
localhost_url?: string;
localhost_username?: string;
localhost_password?: string;
llm_api_keys?: {
gemini_api_key: string;
openai_api_key: string;
};
};
export const getSettings = async (toolbox): Promise<TConfig | null> => {
const { filesystem, print } = toolbox;