Automate AI Translation with n8n + Ollama (Self-Hosted Content Localization Workflow)
Expanding into international markets is one of the highest-ROI moves a business can make. Harvard Business Review research shows that 72% of consumers spend most of their time on websites in their own language, and 56% say the ability to read product information in their native language is more important than price.
But translation is expensive. Professional human translation costs $0.10–0.30 per word. A 2,000-word product page translated into 5 languages runs $1,000–3,000. An e-commerce store with 500 products? That’s $50,000–150,000 just for the initial translation — before you factor in ongoing updates every time you change a description, launch a new product, or update your marketing copy.
Generic machine translation (Google Translate, DeepL) is cheaper but misses the mark on brand voice, technical terminology, and cultural context. “Our cloud-native platform enables teams to ship faster” becomes awkward and unnatural in most languages when run through a generic translator.
With n8n and Ollama, you can build a self-hosted AI translation pipeline that delivers context-aware, brand-consistent translations — running entirely on your own hardware, with zero per-word costs and full data privacy.
Why Self-Hosted Translation Beats Cloud APIs
Before building, here’s why running your own Ollama translation pipeline makes sense compared to paid translation APIs:
| Feature | Google Translate API | DeepL API | n8n + Ollama (This Workflow) |
|---|---|---|---|
| Cost per 1M characters | $20 | $25 | $0 (local) |
| 500 products × 5 languages | ~$500 | ~$625 | $0 |
| Custom glossary/terminology | Limited (500 terms max) | Basic (paid tiers only) | Unlimited, full control |
| Context-awareness | Sentence-level | Paragraph-level | Document-level + custom context |
| Tone/style control | None | Formal/Informal only | Any style via prompt |
| Data privacy | Sent to Google servers | Sent to DeepL servers | 100% on your hardware |
| Rate limits | Yes (600K chars/min) | Yes (varies by plan) | None — limited only by your CPU/GPU |
| Works offline | No | No | Yes |
Prerequisites
- n8n — self-hosted instance (Docker or npm)
- Ollama — running locally with a translation-capable model
- ~8GB RAM for 7B models, ~16GB for larger models with better multilingual support
Pull a translation model:
# Best all-around for European + major Asian languages:
ollama pull mistral:7b
# Best for Chinese, Japanese, Korean:
ollama pull qwen2:7b
# Best quality (needs 20GB+ RAM):
ollama pull command-r:35b
Architecture Overview
Content Source (Webhook / CMS API / File / Database)
↓
[Pre-Process] — Detect language, segment into translatable chunks
↓
[Load Glossary] — Fetch brand terminology from Google Sheets / JSON
↓
[Translate] — Ollama generates context-aware translation per segment
↓
[Validate] — Second AI pass checks accuracy, glossary compliance, completeness
↓ ↓ (if fails)
↓ [Re-translate with feedback]
↓
[Reassemble] — Merge segments, format output
↓
[Output] — Update CMS, write i18n files, export CSV, send via webhook
Step-by-Step: Build the Translation Workflow
Set up a webhook trigger to accept translation requests
Create a Webhook node in n8n that accepts POST requests with the content to translate, target languages, and optional context:
// Webhook payload structure
POST /webhook/translate
{
"source_text": "Our cloud-native platform enables teams to ship faster with built-in CI/CD pipelines and automated testing frameworks.",
"source_language": "en",
"target_languages": ["de", "fr", "es", "ja", "pt-BR"],
"context": "SaaS marketing website - hero section",
"tone": "professional but approachable",
"content_type": "marketing",
"glossary": {
"cloud-native": {"de": "Cloud-nativ", "fr": "cloud-native", "es": "nativo en la nube", "ja": "クラウドネイティブ"},
"CI/CD": {"all": "CI/CD"},
"pipelines": {"de": "Pipelines", "fr": "pipelines", "ja": "パイプライン"}
}
}
The context and tone fields are what separate this from generic machine translation. Telling the model that this is a “SaaS marketing hero section” produces dramatically different output than “technical documentation” or “casual blog post.”
Split content into optimal chunks for translation quality
Long texts need to be segmented before translation. Too-long inputs reduce quality; too-short inputs lose context. The sweet spot is 200–500 words per segment.
// n8n Function node: segment content for translation
const text = $input.first().json.source_text;
const targetLangs = $input.first().json.target_languages;
const segments = [];
// Split by paragraphs first, then by sentences if too long
const paragraphs = text.split(/\n\n+/);
for (const para of paragraphs) {
if (para.length > 500) {
// Split long paragraphs into sentence groups
const sentences = para.match(/[^.!?]+[.!?]+/g) || [para];
let chunk = '';
for (const sent of sentences) {
if ((chunk + sent).length > 500 && chunk) {
segments.push(chunk.trim());
chunk = sent;
} else {
chunk += sent;
}
}
if (chunk) segments.push(chunk.trim());
} else {
segments.push(para);
}
}
// Create one item per segment per language
const items = [];
for (const lang of targetLangs) {
for (let i = 0; i < segments.length; i++) {
items.push({
json: {
segment: segments[i],
segment_index: i,
total_segments: segments.length,
target_language: lang,
source_language: $input.first().json.source_language,
context: $input.first().json.context,
tone: $input.first().json.tone,
glossary: $input.first().json.glossary || {}
}
});
}
}
return items;
This creates one work item per segment per language. For a 3-paragraph text going into 5 languages, that’s 15 items — each processed independently, which enables parallel translation.
Build language-specific glossary instructions for each segment
The glossary is the secret weapon for consistent brand translation. This node formats glossary entries for the target language and embeds them in the prompt:
// n8n Function node: prepare glossary for prompt
const glossary = $json.glossary;
const targetLang = $json.target_language;
const entries = [];
for (const [term, translations] of Object.entries(glossary)) {
const translation = translations[targetLang] || translations['all'] || null;
if (translation) {
entries.push(`"${term}" → "${translation}"`);
}
}
const glossaryText = entries.length > 0
? `\n\nGLOSSARY (use these exact terms in your translation):\n${entries.join('\n')}`
: '';
return [{
json: {
...$json,
glossary_prompt: glossaryText
}
}];
For large glossaries (100+ terms), you can fetch them from a Google Sheet or database at the start of the workflow instead of passing them in every request. Use an HTTP Request node to hit the Google Sheets API or a Postgres/MySQL node to query your terminology database.
The core translation node with context-aware prompting
This is where the actual translation happens. The prompt includes context, tone, glossary, and specific rules to preserve formatting:
// HTTP Request node to Ollama
POST http://localhost:11434/api/generate
{
"model": "mistral:7b",
"prompt": "You are an expert human translator specializing in {{ $json.content_type || 'general' }} content. Translate the following text from {{ $json.source_language }} to {{ $json.target_language }}.\n\nCONTEXT: {{ $json.context }}\nTONE: {{ $json.tone }}\n{{ $json.glossary_prompt }}\n\nRULES:\n1. Preserve all HTML tags, URLs, email addresses, and code snippets exactly as-is\n2. Use glossary terms exactly as specified — do not paraphrase them\n3. Match the tone and register described above\n4. Preserve paragraph structure, bullet points, and formatting\n5. Adapt idioms and cultural references naturally — do not translate them literally\n6. Keep proper nouns, brand names, and product names in their original form unless glossary specifies otherwise\n7. Output ONLY the translated text — no explanations, no notes, no alternatives\n\nSOURCE TEXT:\n{{ $json.segment }}\n\nTRANSLATION:",
"stream": false,
"options": {
"temperature": 0.3,
"num_predict": 2048,
"top_p": 0.9
}
}
temperature: 0.3? Translation needs to be accurate and consistent, not creative. Low temperature reduces randomness, meaning the same input produces nearly identical output each time. This is critical for brand consistency — you don’t want “Sign Up” translated differently on every page. For marketing copy where you want more natural-sounding variations, you can bump this to 0.4–0.5.
Automatically catch translation errors before they reach production
A separate Ollama call reviews each translation for accuracy, completeness, and glossary compliance. This catches the errors that would otherwise require a human reviewer:
// HTTP Request node: quality validation
POST http://localhost:11434/api/generate
{
"model": "mistral:7b",
"prompt": "You are a professional translation quality reviewer. Compare the source text and its translation below.\n\nSource ({{ $json.source_language }}):\n{{ $json.segment }}\n\nTranslation ({{ $json.target_language }}):\n{{ $json.translated_text }}\n\nRequired glossary terms:\n{{ $json.glossary_prompt }}\n\nExpected tone: {{ $json.tone }}\n\nEvaluate the translation on these criteria (score 0-100 each):\n1. COMPLETENESS: Is all source content present in the translation?\n2. ACCURACY: Does the translation preserve the original meaning?\n3. GLOSSARY: Are all required glossary terms used correctly?\n4. NATURALNESS: Does it read naturally in the target language?\n5. TONE: Does it match the expected tone/register?\n\nRespond in valid JSON only:\n{\"completeness\": N, \"accuracy\": N, \"glossary\": N, \"naturalness\": N, \"tone\": N, \"overall\": N, \"issues\": [\"description of any problems\"], \"pass\": true_or_false}",
"stream": false,
"options": {
"temperature": 0.1,
"num_predict": 512
}
}
After the validation, a Function node parses the JSON response and routes the result:
// Parse validation result and decide next step
const response = JSON.parse($json.response);
let quality;
try {
quality = JSON.parse(response);
} catch (e) {
// If model output isn't valid JSON, flag for re-translation
quality = { pass: false, overall: 0, issues: ['Invalid validation response'] };
}
const result = {
...$input.first().json,
quality_score: quality.overall,
quality_details: quality,
needs_retranslation: !quality.pass || quality.overall < 75
};
return [{ json: result }];
If needs_retranslation is true, the workflow loops back to Step 4 with the issues appended to the prompt as additional context. This self-correction catches 90%+ of quality issues automatically.
Merge translated segments and deliver results
// n8n Function node: reassemble translated segments
const items = $input.all();
const translations = {};
for (const item of items) {
const lang = item.json.target_language;
if (!translations[lang]) {
translations[lang] = {
segments: [],
quality_scores: []
};
}
translations[lang].segments[item.json.segment_index] = item.json.translated_text;
translations[lang].quality_scores.push(item.json.quality_score);
}
// Build final output
const output = {};
for (const [lang, data] of Object.entries(translations)) {
const avgQuality = data.quality_scores.reduce((a, b) => a + b, 0) / data.quality_scores.length;
output[lang] = {
translated_text: data.segments.join('\n\n'),
average_quality_score: Math.round(avgQuality),
word_count: data.segments.join(' ').split(/\s+/).length,
segment_count: data.segments.length
};
}
return [{
json: {
source_language: items[0].json.source_language,
translations: output,
translated_at: new Date().toISOString()
}
}];
Real-World Use Cases
E-Commerce Product Localization
Translate product titles, descriptions, specifications, and category pages for international storefronts. Connect to Shopify (Admin API + Markets), WooCommerce (REST API + WPML), or Magento to automatically translate new products as they’re added. A store with 500 products can have all 5 target languages completed overnight.
SaaS App Localization (i18n Files)
Feed your en.json, .po, .xliff, or .strings files through the workflow. The translation preserves JSON structure, placeholder tokens ({{username}}, %d items), and HTML tags. Output files are ready to deploy directly into your app’s locale directory.
// Example: translating i18n JSON keys
// Input (en.json):
{
"nav.dashboard": "Dashboard",
"nav.settings": "Settings",
"welcome.title": "Welcome back, {{name}}!",
"items.count": "You have {{count}} items in your cart"
}
// Output (de.json) - preserves keys and placeholders:
{
"nav.dashboard": "Dashboard",
"nav.settings": "Einstellungen",
"welcome.title": "Willkommen zurück, {{name}}!",
"items.count": "Sie haben {{count}} Artikel in Ihrem Warenkorb"
}
Marketing Email Campaigns
Automatically translate email templates before each campaign send. The workflow preserves HTML formatting, personalization tokens, CTA buttons, and tracking links. Schedule translations to run 24 hours before send time so your team can review before launch.
Documentation and Knowledge Base
Translate help articles, API documentation, and knowledge base content. The context-awareness handles technical terminology that generic translators consistently get wrong — “endpoint” stays “Endpoint” in German (not “Endpunkt”), “deploy” becomes the correct localized term in each market.
Automated Content Pipeline
Combine the translation workflow with the Social Media Content Generator — write content once in English, then automatically translate and publish to localized social accounts across all markets.
Best Ollama Models for Translation
| Model | Size | Best For | Strong Languages | Speed (words/min) |
|---|---|---|---|---|
mistral:7b | 4.1 GB | European languages, marketing content | EN, DE, FR, ES, IT, PT, NL | ~200 |
llama3:8b | 4.7 GB | Broad language support, technical docs | 30+ languages, strong EN/ES/DE/FR | ~180 |
qwen2:7b | 4.4 GB | Chinese, Japanese, Korean content | ZH, JA, KO + EN | ~170 |
command-r:35b | 20 GB | Highest quality, complex content | 50+ languages including low-resource | ~80 |
gemma2:9b | 5.4 GB | Good balance of quality and speed | EN, DE, FR, ES, IT, JA, KO, ZH | ~160 |
mistral:7b delivers the best quality-to-speed ratio. For CJK languages (Chinese, Japanese, Korean), qwen2:7b is significantly better than Western-focused models. If you need the highest quality across all languages and have 20GB+ RAM, command-r:35b is the clear choice.
Performance and Throughput
mistral:7b: ~200 words/minute per languagellama3:8b: ~180 words/minute per language- With quality validation (2 passes): ~120 words/minute
- 5 languages sequentially: ~600 words/minute total throughput
Real-world estimates:
- 2,000-word blog post → 5 languages: ~15 minutes
- 500 product descriptions (50 words each) → 5 languages: ~3.5 hours
- Full SaaS app (2,000 i18n strings) → 5 languages: ~2 hours
- 10,000-word documentation site → 5 languages: ~75 minutes
With a GPU (RTX 3060+), multiply throughput by 3–5x.
Advanced: Centralized Glossary Management
For organizations translating consistently across multiple projects, maintain a centralized glossary in Google Sheets or a database. The workflow fetches the latest terms at the start of each run:
// Fetch glossary from Google Sheets (first node in workflow)
// Sheet columns: term_en | term_de | term_fr | term_es | term_ja | notes
GET https://sheets.googleapis.com/v4/spreadsheets/{{sheet_id}}/values/Glossary!A:G
// Or maintain a local glossary.json file:
{
"platform": {"de": "Plattform", "fr": "plateforme", "es": "plataforma", "ja": "プラットフォーム"},
"dashboard": {"de": "Dashboard", "fr": "tableau de bord", "es": "panel de control"},
"workflow": {"de": "Workflow", "fr": "flux de travail", "es": "flujo de trabajo"},
"deploy": {"de": "bereitstellen", "fr": "déployer", "es": "desplegar"},
"Sign Up": {"de": "Registrieren", "fr": "S'inscrire", "es": "Regístrate"}
}
This ensures that “Sign Up” is always “Registrieren” in German across every page, email, and app screen — something generic translation APIs cannot guarantee.
Integration with CMS Platforms
Connect the translation workflow to your content management system for fully automated localization on content publish:
- WordPress + WPML/Polylang — Trigger on
publish_postwebhook, translate all text fields, create translated post versions via REST API. Works with WooCommerce products too. - Shopify + Markets — Use Shopify Admin API (
POST /translations/register) to automatically translate products, collections, and pages when they’re created or updated. - Strapi / Contentful / Sanity — Webhook on content publish, translate all locale fields, update translations via the CMS API. Headless CMS platforms make this particularly clean.
- GitHub (docs sites) — Watch for pushes to
docs/en/, translate modified files, open a PR with translations intodocs/de/,docs/fr/, etc. Human reviewers approve the PR. - Notion / Confluence — Periodic sync: fetch pages tagged “needs-translation,” translate, create localized copies, update the status tag.
Get the Complete Translation Workflow + 10 More AI Templates
This n8n translation automation workflow is included in our Self-Hosted AI Workflow Pack with 11 production-ready n8n templates — all powered by Ollama, no API keys needed.
Includes: translation pipeline, email responder, blog writer, social content generator, document summarizer, meeting notes, lead scoring, code review, and more.
Get All 11 Workflows — $39One-time payment. No subscription. 30-day money-back guarantee.
Free Starter Workflow (Import into n8n)
Here’s a simplified version of the automated content localization workflow you can import directly into n8n. It handles single-language translation with basic quality validation:
Click to expand free workflow JSON
{
"name": "AI Translation Pipeline - Ollama (Free Starter)",
"nodes": [
{
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [250, 300],
"parameters": {
"path": "translate",
"httpMethod": "POST",
"responseMode": "lastNode"
}
},
{
"name": "Prepare Translation",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [450, 300],
"parameters": {
"assignments": {
"assignments": [
{"name": "source_text", "value": "={{ $json.body.source_text }}", "type": "string"},
{"name": "source_language", "value": "={{ $json.body.source_language || 'en' }}", "type": "string"},
{"name": "target_language", "value": "={{ $json.body.target_language || 'de' }}", "type": "string"},
{"name": "context", "value": "={{ $json.body.context || 'general content' }}", "type": "string"},
{"name": "tone", "value": "={{ $json.body.tone || 'professional' }}", "type": "string"}
]
}
}
},
{
"name": "Translate with Ollama",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [650, 300],
"parameters": {
"url": "http://localhost:11434/api/generate",
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({model: 'mistral:7b', prompt: 'You are an expert translator. Translate the following from ' + $json.source_language + ' to ' + $json.target_language + '.\\n\\nContext: ' + $json.context + '\\nTone: ' + $json.tone + '\\n\\nRules: Preserve all HTML/URLs/code. Output ONLY the translation.\\n\\nText: ' + $json.source_text + '\\n\\nTranslation:', stream: false, options: {temperature: 0.3, num_predict: 2048}}) }}",
"options": {"timeout": 120000}
}
},
{
"name": "Extract Translation",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [850, 300],
"parameters": {
"assignments": {
"assignments": [
{"name": "translated_text", "value": "={{ JSON.parse($json.data).response.trim() }}", "type": "string"},
{"name": "source_text", "value": "={{ $('Prepare Translation').item.json.source_text }}", "type": "string"},
{"name": "source_language", "value": "={{ $('Prepare Translation').item.json.source_language }}", "type": "string"},
{"name": "target_language", "value": "={{ $('Prepare Translation').item.json.target_language }}", "type": "string"}
]
}
}
}
],
"connections": {
"Webhook Trigger": {"main": [[{"node": "Prepare Translation", "type": "main", "index": 0}]]},
"Prepare Translation": {"main": [[{"node": "Translate with Ollama", "type": "main", "index": 0}]]},
"Translate with Ollama": {"main": [[{"node": "Extract Translation", "type": "main", "index": 0}]]}
},
"settings": {"executionOrder": "v1"},
"tags": [{"name": "AI"}, {"name": "Ollama"}, {"name": "Translation"}]
}
How to Use the Free Workflow
- Copy the JSON above and import via n8n → Workflows → Import from JSON
- Make sure Ollama is running:
ollama serve - Pull the model:
ollama pull mistral:7b - If n8n runs in Docker, replace
localhostwithhost.docker.internalin the Ollama URL - Activate the workflow and test with a POST request:
curl -X POST http://localhost:5678/webhook/translate \ -H "Content-Type: application/json" \ -d '{ "source_text": "Welcome to our platform. Get started in minutes.", "source_language": "en", "target_language": "de", "context": "SaaS onboarding page", "tone": "friendly and professional" }'
Tips for Best Translation Quality
- Always provide context — “marketing hero section” vs “API documentation” vs “customer support email” produces very different translations from the same source text. This single change improves output quality more than any model upgrade.
- Use glossaries for brand consistency — Without explicit glossary terms, the model will translate “Dashboard” differently each time (German: “Dashboard,” “Übersicht,” “Instrumententafel”). Pin your terms.
- Keep segments at 200–500 words — Too short (under 50 words) loses context. Too long (over 1,000 words) increases errors and hallucination. The segmentation in Step 2 handles this automatically.
- Always validate with a second pass — The quality check in Step 5 catches missing content, wrong glossary terms, and tone mismatches before they reach production. Skip this only for low-stakes content.
- Match models to language pairs — Use
qwen2for CJK,mistralfor European languages. A specialized 7B model outperforms a general 13B model for specific language pairs. - Test with professional translators first — Have a native speaker review the first batch of translations. Use their feedback to refine your prompts and glossary. The initial calibration investment pays off across thousands of future translations.
Troubleshooting
Translations sound unnatural or robotic
- Add
"tone": "natural and conversational"to the prompt context - Include an example of the desired style in the prompt
- Increase temperature slightly (0.4–0.5) for marketing copy
- Try a larger model —
command-r:35bproduces noticeably more natural output
Model outputs explanations instead of just the translation
- Emphasize in the prompt: “Output ONLY the translated text. No explanations, notes, or alternatives.”
- Use
num_predictto limit output length proportional to input length - Add a post-processing Function node that strips common prefixes like “Here is the translation:”
Glossary terms are being ignored
- Move glossary terms to the end of the prompt (models pay more attention to recent context)
- Use ALL CAPS for glossary section headers:
GLOSSARY (MANDATORY): - Add reinforcement: “You MUST use these exact terms. Do not paraphrase or substitute.”
Ollama connection issues from n8n Docker
- Replace
http://localhost:11434withhttp://host.docker.internal:11434 - Set
OLLAMA_HOST=0.0.0.0in Ollama’s environment to allow network connections - Verify:
curl http://host.docker.internal:11434/api/tagsfrom inside the n8n container
Need More AI Workflow Templates?
Check out our full collection of 11 self-hosted AI workflows for n8n + Ollama. Translation, email automation, document processing, content generation, lead scoring, and more — all running locally on your hardware.
Get the Full Pack — $39