HomeBuilding Real ProjectsProject: Multi-Language Translation App
intermediate20 min read· Module 8, Lesson 3

🌍Project: Multi-Language Translation App

Build a CLI tool that translates text between any languages with context awareness

Project: Multi-Language Translation App

In this project, you will build a complete CLI translation tool powered by Claude. This is not a simple translate-this-sentence app — it handles language detection, preserves formatting, manages technical terminology, supports batch file translation, and uses structured outputs for reliable results.

By the end, you will have a production-quality tool you can actually use every day.


What We Are Building

A Node.js command-line application called translator with these features:

FeatureDescription
Auto-detect source languageNo need to specify what language the input is in
Translate to any languageSupport every language Claude knows
Preserve formattingMarkdown, code blocks, lists, and tables stay intact
Technical term handlingKeep technical terms accurate with optional glossary
Multi-target translationTranslate to multiple languages at once
Batch file translationTranslate entire files or directories
Structured outputJSON output mode for integration with other tools
Context-awareUnderstand domain context for better translations

Step 1: Project Setup

Create the project structure:

Terminal
mkdir translator && cd translator npm init -y npm install @anthropic-ai/sdk commander chalk npm install -D typescript @types/node tsx

Update your package.json:

JSON
{ "name": "translator", "version": "1.0.0", "type": "module", "bin": { "translator": "./dist/cli.js" }, "scripts": { "build": "tsc", "dev": "tsx src/cli.ts", "start": "node dist/cli.js" } }

Create tsconfig.json:

JSON
{ "compilerOptions": { "target": "ES2022", "module": "ES2022", "moduleResolution": "node", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "declaration": true }, "include": ["src/**/*"] }

Step 2: Define Types

Create src/types.ts:

TypeScript
text: string; targetLanguage: string; sourceLanguage?: string; // Optional — auto-detect if not provided context?: string; // Domain context (e.g., "medical", "legal", "tech") preserveFormatting?: boolean; glossary?: Record<string, string>; // term -> preferred translation } originalText: string; translatedText: string; detectedLanguage: string; targetLanguage: string; confidence: number; // 0–1 confidence in the translation quality glossaryTermsUsed: string[]; warnings: string[]; } file: string; results: TranslationResult[]; errors: string[]; } language: string; languageCode: string; confidence: number; script: string; }

Step 3: Build the Translation Engine

Create src/translator.ts — this is the core of the application:

TypeScript
TranslationRequest, TranslationResult, LanguageDetection, } from "./types.js"; const client = new Anthropic(); text: string ): Promise<LanguageDetection> { const response = await client.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 256, messages: [ { role: "user", content: `Detect the language of the following text. Respond with ONLY valid JSON in this exact format: {"language": "English", "languageCode": "en", "confidence": 0.98, "script": "Latin"} Text to analyze: """${text.slice(0, 500)}"""`, }, ], }); const content = response.content[0].type === "text" ? response.content[0].text : ""; try { return JSON.parse(content) as LanguageDetection; } catch { return { language: "Unknown", languageCode: "unknown", confidence: 0, script: "Unknown", }; } } request: TranslationRequest ): Promise<TranslationResult> { // Step 1: Detect source language if not provided let sourceLanguage = request.sourceLanguage || ""; if (!sourceLanguage) { const detection = await detectLanguage(request.text); sourceLanguage = detection.language; } // Step 2: Build the translation prompt const glossarySection = request.glossary ? ` <glossary> Use these specific translations for technical terms: ${Object.entries(request.glossary) .map(([term, translation]) => `- "${term}" -> "${translation}"`) .join("\n")} </glossary>` : ""; const contextSection = request.context ? `\nDomain context: This text is from the "${request.context}" domain. Use appropriate terminology.` : ""; const formattingRule = request.preserveFormatting !== false ? "\nIMPORTANT: Preserve ALL formatting including markdown, code blocks, lists, tables, and whitespace structure." : ""; const systemPrompt = `You are an expert translator. Translate text accurately while preserving meaning, tone, and nuance. ${contextSection} ${formattingRule} ${glossarySection} Respond with ONLY valid JSON in this exact format: { "translatedText": "the translated text here", "confidence": 0.95, "glossaryTermsUsed": ["term1", "term2"], "warnings": ["any translation warnings or notes"] }`; const response = await client.messages.create({ model: "claude-sonnet-4-20250514", max_tokens: 4096, system: systemPrompt, messages: [ { role: "user", content: `Translate the following text from ${sourceLanguage} to ${request.targetLanguage}: """${request.text}"""`, }, ], }); const content = response.content[0].type === "text" ? response.content[0].text : "{}"; try { const parsed = JSON.parse(content); return { originalText: request.text, translatedText: parsed.translatedText || "", detectedLanguage: sourceLanguage, targetLanguage: request.targetLanguage, confidence: parsed.confidence || 0, glossaryTermsUsed: parsed.glossaryTermsUsed || [], warnings: parsed.warnings || [], }; } catch { // If JSON parsing fails, treat the whole response as the translation return { originalText: request.text, translatedText: content, detectedLanguage: sourceLanguage, targetLanguage: request.targetLanguage, confidence: 0.5, glossaryTermsUsed: [], warnings: ["Response was not structured JSON — raw text used"], }; } } text: string, targets: string[], options?: Partial<TranslationRequest> ): Promise<TranslationResult[]> { const results: TranslationResult[] = []; // Run translations in parallel for speed const promises = targets.map((target) => translate({ text, targetLanguage: target, ...options, }) ); const settled = await Promise.allSettled(promises); for (const result of settled) { if (result.status === "fulfilled") { results.push(result.value); } else { results.push({ originalText: text, translatedText: "", detectedLanguage: options?.sourceLanguage || "Unknown", targetLanguage: "Unknown", confidence: 0, glossaryTermsUsed: [], warnings: [`Translation failed: ${result.reason}`], }); } } return results; }

Step 4: Build the Batch Processor

Create src/batch.ts for translating files:

TypeScript
filePath: string, targetLanguage: string, options?: Partial<TranslationRequest> ): Promise<BatchResult> { const result: BatchResult = { file: filePath, results: [], errors: [], }; try { const content = await readFile(filePath, "utf-8"); // Split large files into chunks to stay within token limits const chunks = splitIntoChunks(content, 3000); for (let i = 0; i < chunks.length; i++) { try { const translated = await translate({ text: chunks[i], targetLanguage, preserveFormatting: true, ...options, }); result.results.push(translated); } catch (error) { result.errors.push( `Chunk ${i + 1}/${chunks.length} failed: ${error}` ); } } // Write translated file if (result.results.length > 0) { const translatedContent = result.results .map((r) => r.translatedText) .join("\n"); const ext = extname(filePath); const base = basename(filePath, ext); const outputPath = join( filePath, "..", `${base}.${targetLanguage}${ext}` ); await writeFile(outputPath, translatedContent, "utf-8"); } } catch (error) { result.errors.push(`Failed to read file: ${error}`); } return result; } dirPath: string, targetLanguage: string, extensions: string[] = [".md", ".txt", ".html"], options?: Partial<TranslationRequest> ): Promise<BatchResult[]> { const results: BatchResult[] = []; const entries = await readdir(dirPath); for (const entry of entries) { const fullPath = join(dirPath, entry); const stats = await stat(fullPath); if (stats.isFile() && extensions.includes(extname(entry))) { console.log(`Translating: ${entry}`); const result = await translateFile( fullPath, targetLanguage, options ); results.push(result); } } return results; } function splitIntoChunks(text: string, maxChars: number): string[] { const chunks: string[] = []; const paragraphs = text.split("\n\n"); let current = ""; for (const paragraph of paragraphs) { if (current.length + paragraph.length > maxChars && current.length > 0) { chunks.push(current.trim()); current = ""; } current += paragraph + "\n\n"; } if (current.trim().length > 0) { chunks.push(current.trim()); } return chunks; }

Step 5: Build the CLI Interface

Create src/cli.ts:

TypeScript
translate, detectLanguage, translateMultiTarget, } from "./translator.js"; const program = new Command(); program .name("translator") .description("AI-powered multi-language translation tool") .version("1.0.0"); // === Command: Translate text === program .command("text") .description("Translate text to a target language") .argument("<text>", "Text to translate") .requiredOption("-t, --target <language>", "Target language") .option("-s, --source <language>", "Source language (auto-detect if omitted)") .option("-c, --context <domain>", "Domain context (medical, legal, tech)") .option("-g, --glossary <file>", "Path to glossary JSON file") .option("--json", "Output raw JSON result") .action(async (text, opts) => { try { let glossary: Record<string, string> | undefined; if (opts.glossary) { const glossaryContent = await readFile(opts.glossary, "utf-8"); glossary = JSON.parse(glossaryContent); } console.log(chalk.blue("Translating...")); const result = await translate({ text, targetLanguage: opts.target, sourceLanguage: opts.source, context: opts.context, glossary, preserveFormatting: true, }); if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log(); console.log(chalk.gray(`Detected: ${result.detectedLanguage}`)); console.log(chalk.gray(`Target: ${result.targetLanguage}`)); console.log(chalk.gray(`Confidence: ${(result.confidence * 100).toFixed(0)}%`)); console.log(); console.log(chalk.green(result.translatedText)); if (result.warnings.length > 0) { console.log(); console.log(chalk.yellow("Warnings:")); result.warnings.forEach((w) => console.log(chalk.yellow(` - ${w}`))); } } } catch (error) { console.error(chalk.red(`Error: ${error}`)); process.exit(1); } }); // === Command: Detect language === program .command("detect") .description("Detect the language of text") .argument("<text>", "Text to analyze") .action(async (text) => { try { const result = await detectLanguage(text); console.log(); console.log(chalk.blue("Language Detection Result:")); console.log(` Language: ${chalk.green(result.language)}`); console.log(` Code: ${chalk.gray(result.languageCode)}`); console.log(` Script: ${chalk.gray(result.script)}`); console.log( ` Confidence: ${chalk.yellow((result.confidence * 100).toFixed(0) + "%")}` ); } catch (error) { console.error(chalk.red(`Error: ${error}`)); process.exit(1); } }); // === Command: Multi-target translation === program .command("multi") .description("Translate text to multiple languages at once") .argument("<text>", "Text to translate") .requiredOption( "-t, --targets <languages>", "Comma-separated target languages" ) .option("-c, --context <domain>", "Domain context") .option("--json", "Output raw JSON result") .action(async (text, opts) => { try { const targets = opts.targets.split(",").map((t: string) => t.trim()); console.log( chalk.blue(`Translating to ${targets.length} languages...`) ); const results = await translateMultiTarget(text, targets, { context: opts.context, }); if (opts.json) { console.log(JSON.stringify(results, null, 2)); } else { for (const result of results) { console.log(); console.log( chalk.cyan(`--- ${result.targetLanguage} ---`) ); console.log(chalk.green(result.translatedText)); console.log( chalk.gray( `Confidence: ${(result.confidence * 100).toFixed(0)}%` ) ); } } } catch (error) { console.error(chalk.red(`Error: ${error}`)); process.exit(1); } }); // === Command: Translate file === program .command("file") .description("Translate a file") .argument("<path>", "Path to file") .requiredOption("-t, --target <language>", "Target language") .option("-c, --context <domain>", "Domain context") .action(async (path, opts) => { try { console.log(chalk.blue(`Translating file: ${path}`)); const result = await translateFile(path, opts.target, { context: opts.context, }); console.log( chalk.green(`Translated ${result.results.length} chunk(s)`) ); if (result.errors.length > 0) { console.log(chalk.yellow("Errors:")); result.errors.forEach((e) => console.log(chalk.yellow(` - ${e}`)) ); } } catch (error) { console.error(chalk.red(`Error: ${error}`)); process.exit(1); } }); // === Command: Translate directory === program .command("dir") .description("Translate all files in a directory") .argument("<path>", "Path to directory") .requiredOption("-t, --target <language>", "Target language") .option( "-e, --extensions <exts>", "File extensions to include (comma-separated)", ".md,.txt,.html" ) .option("-c, --context <domain>", "Domain context") .action(async (path, opts) => { try { const extensions = opts.extensions .split(",") .map((e: string) => e.trim()); console.log(chalk.blue(`Translating directory: ${path}`)); console.log(chalk.gray(`Extensions: ${extensions.join(", ")}`)); const results = await translateDirectory( path, opts.target, extensions, { context: opts.context } ); let totalChunks = 0; let totalErrors = 0; for (const result of results) { totalChunks += result.results.length; totalErrors += result.errors.length; console.log( chalk.green( ` ${result.file}: ${result.results.length} chunk(s)` ) ); } console.log(); console.log( chalk.blue( `Done: ${results.length} files, ${totalChunks} chunks, ${totalErrors} errors` ) ); } catch (error) { console.error(chalk.red(`Error: ${error}`)); process.exit(1); } }); program.parse();

Step 6: Create a Glossary File

Create glossary.json for testing with technical terms:

JSON
{ "API": "API", "endpoint": "نقطة النهاية", "middleware": "البرمجية الوسيطة", "authentication": "المصادقة", "token": "رمز التوثيق", "deployment": "النشر", "repository": "المستودع", "pull request": "طلب السحب", "merge conflict": "تعارض الدمج", "CI/CD": "CI/CD" }

Step 7: Usage Examples

Try these commands once the project is built:

Terminal
# Simple translation npx tsx src/cli.ts text "Hello, how are you?" -t Arabic # Detect language npx tsx src/cli.ts detect "Bonjour, comment allez-vous?" # Translate with context npx tsx src/cli.ts text "The patient presented with acute myocardial infarction" \ -t Arabic -c medical # Translate with glossary npx tsx src/cli.ts text "Create a pull request after pushing to the repository" \ -t Arabic -g glossary.json # Multi-target translation npx tsx src/cli.ts multi "Welcome to our platform" \ -t "Arabic, French, Spanish, German, Japanese" # Translate a markdown file npx tsx src/cli.ts file README.md -t Arabic # Translate all markdown files in a directory npx tsx src/cli.ts dir ./docs -t Arabic -e ".md,.txt" # Get JSON output for programmatic use npx tsx src/cli.ts text "Hello world" -t French --json

Step 8: Markdown Preservation Test

Test that your tool preserves markdown formatting:

Terminal
npx tsx src/cli.ts text "# Main Title This is a paragraph with **bold text** and *italic text*. ## Code Example ```javascript const greeting = 'Hello World'; console.log(greeting);

Features

  • Feature one
  • Feature two
  • Feature three
Column AColumn B
Cell 1Cell 2

This is a blockquote with important information." -t Arabic

The translated output should preserve all markdown syntax — headings, bold, italic, code blocks, lists, tables, and blockquotes — while translating only the text content. --- ### How the Translation Flow Works Here is the complete data flow through the application: ```text User Input | v [CLI Parser] --> Validates arguments and options | v [Language Detection] --> Auto-detect source (if not specified) | v [Prompt Builder] --> Constructs system prompt with: | - Domain context | - Glossary terms | - Formatting rules v [Claude API Call] --> Sends structured request | v [JSON Parser] --> Extracts translation from structured response | v [Output Formatter] --> Displays result (pretty or JSON)

Adding Error Handling for Production

In a production app, add retry logic and proper error messages:

TypeScript
async function translateWithRetry( request: TranslationRequest, maxRetries: number = 3 ): Promise<TranslationResult> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await translate(request); } catch (error: unknown) { const isRateLimit = error instanceof Error && error.message.includes("rate_limit"); if (isRateLimit && attempt < maxRetries) { const waitMs = Math.pow(2, attempt) * 1000; console.log( `Rate limited. Waiting ${waitMs / 1000}s before retry ${attempt + 1}/${maxRetries}...` ); await new Promise((resolve) => setTimeout(resolve, waitMs)); continue; } throw error; } } throw new Error("Max retries exceeded"); }

Extending the Tool

Ideas to take this project further:

ExtensionHow
Translation memoryCache previous translations to avoid re-translating
Quality scoringCompare back-translation to original to score accuracy
Interactive modeREPL mode for continuous translation sessions
Watch modeAuto-translate files when they change
Config file.translatorrc for default settings per project
Plugin systemCustom pre/post-processing for specific file types

Key Takeaways

  1. Structure your outputs — JSON responses make translation results reliable and parseable
  2. Auto-detect language — Never force the user to specify the source language
  3. Preserve formatting — Explicit instructions in the system prompt keep markdown intact
  4. Use glossaries — Technical terms need consistent translation across a project
  5. Batch processing — Chunk large files to stay within token limits
  6. Parallel requests — Multi-target translation benefits from concurrent API calls
  7. Error handling — Retry logic with exponential backoff keeps the tool reliable
  8. CLI design — Commander.js makes it easy to build professional command-line tools
  9. Domain context — A single word in the prompt dramatically improves specialized translations
  10. Modular architecture — Separate engine, batch processor, and CLI for testability

Congratulations! You have built a real, usable translation tool. Try using it on your own projects.