🌍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:
| Feature | Description |
|---|---|
| Auto-detect source language | No need to specify what language the input is in |
| Translate to any language | Support every language Claude knows |
| Preserve formatting | Markdown, code blocks, lists, and tables stay intact |
| Technical term handling | Keep technical terms accurate with optional glossary |
| Multi-target translation | Translate to multiple languages at once |
| Batch file translation | Translate entire files or directories |
| Structured output | JSON output mode for integration with other tools |
| Context-aware | Understand domain context for better translations |
Step 1: Project Setup
Create the project structure:
mkdir translator && cd translator
npm init -y
npm install @anthropic-ai/sdk commander chalk
npm install -D typescript @types/node tsxUpdate your package.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:
{
"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:
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:
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:
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:
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:
{
"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:
# 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 --jsonStep 8: Markdown Preservation Test
Test that your tool preserves markdown formatting:
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 A | Column B |
|---|---|
| Cell 1 | Cell 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:
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:
| Extension | How |
|---|---|
| Translation memory | Cache previous translations to avoid re-translating |
| Quality scoring | Compare back-translation to original to score accuracy |
| Interactive mode | REPL mode for continuous translation sessions |
| Watch mode | Auto-translate files when they change |
| Config file | .translatorrc for default settings per project |
| Plugin system | Custom pre/post-processing for specific file types |
Key Takeaways
- Structure your outputs — JSON responses make translation results reliable and parseable
- Auto-detect language — Never force the user to specify the source language
- Preserve formatting — Explicit instructions in the system prompt keep markdown intact
- Use glossaries — Technical terms need consistent translation across a project
- Batch processing — Chunk large files to stay within token limits
- Parallel requests — Multi-target translation benefits from concurrent API calls
- Error handling — Retry logic with exponential backoff keeps the tool reliable
- CLI design — Commander.js makes it easy to build professional command-line tools
- Domain context — A single word in the prompt dramatically improves specialized translations
- 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.