added translate textadmin excel
This commit is contained in:
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user