Build a Local AI Translation Tool with Ollama โ No Google Translate Needed
Google Translate is convenient, but every sentence you type gets sent to Googleโs servers. If youโre translating contracts, medical notes, internal docs, or anything remotely sensitive โ thatโs a problem.
In this tutorial, youโll build a fully local translation tool powered by Ollama. A Python CLI for quick terminal translations, plus a Flask web UI for anyone on your team who prefers a browser. No API keys, no cloud calls, no data leaving your machine.
This is entry #14 in the Build It With AI series.
Why Local Translation Matters
Cloud translation services process your text on remote servers. For personal use thatโs usually fine. For business use โ especially under GDPR or similar regulations โ itโs a liability. A local tool gives you:
- Full privacy โ text never leaves your network
- No rate limits โ translate as much as you want
- No API costs โ zero ongoing fees
- Offline support โ works without internet after model download
Prerequisites
- Python 3.10+
- Ollama installed and running
- A capable multilingual model pulled locally
For the model, Qwen3 is an excellent choice โ it handles 50+ languages well. Pull it:
ollama pull qwen3:8b
Any strong coding/instruction model with multilingual training will work. Llama 3 and Gemma 2 are solid alternatives.
Project Setup
mkdir local-translator && cd local-translator
python -m venv venv && source venv/bin/activate
pip install requests flask
Create the project structure:
local-translator/
โโโ translator.py # Core translation logic
โโโ cli.py # CLI interface
โโโ app.py # Flask web UI
โโโ templates/
โโโ index.html
Core Translation Module
translator.py handles all communication with Ollama:
import requests
import json
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "qwen3:8b"
def translate(text, target_lang, source_lang=None):
"""Translate text to target language using Ollama."""
if source_lang:
prompt = f"Translate the following {source_lang} text to {target_lang}. Return ONLY the translation, nothing else.\n\n{text}"
else:
prompt = f"Translate the following text to {target_lang}. Return ONLY the translation, nothing else.\n\n{text}"
resp = requests.post(OLLAMA_URL, json={
"model": MODEL,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.1}
})
resp.raise_for_status()
return resp.json()["response"].strip()
def detect_language(text):
"""Detect the language of the input text."""
resp = requests.post(OLLAMA_URL, json={
"model": MODEL,
"prompt": f"What language is this text written in? Reply with ONLY the language name, nothing else.\n\n{text}",
"stream": False,
"options": {"temperature": 0.0}
})
resp.raise_for_status()
return resp.json()["response"].strip()
def translate_batch(items, target_lang):
"""Translate a list of strings, returning list of translations."""
return [translate(item, target_lang) for item in items]
def translate_file(input_path, output_path, target_lang):
"""Translate a text file line by line."""
with open(input_path, "r") as f:
lines = [l.strip() for l in f if l.strip()]
translated = translate_batch(lines, target_lang)
with open(output_path, "w") as f:
f.write("\n".join(translated))
return len(translated)
Low temperature (0.1) keeps translations consistent and literal. The prompts are deliberately simple โ LLMs follow โreturn ONLY the translationโ instructions reliably.
CLI Tool
cli.py gives you fast terminal access:
import argparse
from translator import translate, detect_language, translate_file
def main():
parser = argparse.ArgumentParser(description="Local AI Translator")
sub = parser.add_subparsers(dest="command")
# Translate text
tr = sub.add_parser("translate", help="Translate text")
tr.add_argument("text", help="Text to translate")
tr.add_argument("-t", "--to", required=True, help="Target language")
tr.add_argument("-f", "--from-lang", help="Source language (auto-detected if omitted)")
# Detect language
det = sub.add_parser("detect", help="Detect language")
det.add_argument("text", help="Text to analyze")
# Translate file
fi = sub.add_parser("file", help="Translate a file")
fi.add_argument("input", help="Input file path")
fi.add_argument("output", help="Output file path")
fi.add_argument("-t", "--to", required=True, help="Target language")
args = parser.parse_args()
if args.command == "translate":
result = translate(args.text, args.to, args.from_lang)
print(result)
elif args.command == "detect":
print(detect_language(args.text))
elif args.command == "file":
count = translate_file(args.input, args.output, args.to)
print(f"Translated {count} lines โ {args.output}")
else:
parser.print_help()
if __name__ == "__main__":
main()
Usage examples:
# Simple translation
python cli.py translate "The meeting is at 3pm tomorrow" -t Spanish
# With source language specified
python cli.py translate "Bonjour le monde" -t English -f French
# Detect language
python cli.py detect "ใใใฏๆฅๆฌ่ชใฎใใญในใใงใ"
# Translate an entire file
python cli.py file notes.txt notes_de.txt -t German
Flask Web UI
app.py โ a minimal web interface:
from flask import Flask, render_template, request, jsonify
from translator import translate, detect_language
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/translate", methods=["POST"])
def do_translate():
data = request.json
text = data.get("text", "")
target = data.get("target", "English")
source = data.get("source") or None
try:
result = translate(text, target, source)
return jsonify({"translation": result})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/detect", methods=["POST"])
def do_detect():
text = request.json.get("text", "")
try:
lang = detect_language(text)
return jsonify({"language": lang})
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(debug=True, port=5000)
templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Local Translator</title>
<style>
* { box-sizing: border-box; font-family: system-ui, sans-serif; }
body { max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
h1 { text-align: center; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
textarea { width: 100%; height: 200px; padding: .75rem; border: 1px solid #ccc; border-radius: 6px; resize: vertical; }
select, button { padding: .5rem 1rem; border-radius: 6px; border: 1px solid #ccc; }
button { background: #2563eb; color: #fff; border: none; cursor: pointer; }
button:hover { background: #1d4ed8; }
.controls { display: flex; gap: .5rem; margin: 1rem 0; align-items: center; }
#output { background: #f9fafb; }
.status { color: #666; font-size: .875rem; margin-top: .5rem; }
</style>
</head>
<body>
<h1>๐ Local Translator</h1>
<div class="controls">
<button onclick="detectLang()">Detect Language</button>
<span id="detected"></span>
<select id="target">
<option>Spanish</option><option>French</option><option>German</option>
<option>Italian</option><option>Portuguese</option><option>Dutch</option>
<option>Russian</option><option>Chinese</option><option>Japanese</option>
<option>Korean</option><option>Arabic</option><option>Hindi</option>
<option>English</option><option>Polish</option><option>Turkish</option>
<option>Vietnamese</option><option>Thai</option><option>Swedish</option>
</select>
<button onclick="doTranslate()">Translate</button>
</div>
<div class="grid">
<textarea id="input" placeholder="Enter text to translate..."></textarea>
<textarea id="output" readonly placeholder="Translation appears here..."></textarea>
</div>
<div class="status" id="status"></div>
<script>
const $ = id => document.getElementById(id);
const status = msg => $("status").textContent = msg;
async function doTranslate() {
status("Translating...");
const res = await fetch("/translate", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({text: $("input").value, target: $("target").value})
});
const data = await res.json();
$("output").value = data.translation || data.error;
status(data.error ? "Error" : "Done");
}
async function detectLang() {
status("Detecting...");
const res = await fetch("/detect", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({text: $("input").value})
});
const data = await res.json();
$("detected").textContent = data.language ? `Detected: ${data.language}` : data.error;
status("");
}
</script>
</body>
</html>
Run it:
python app.py
Open http://localhost:5000 โ paste text, pick a language, hit Translate.
Tips for Better Results
Model choice matters. Qwen3 8B handles most European and Asian languages well. For less common languages (Swahili, Tagalog, etc.), larger models like Qwen3 32B or Llama 3 70B produce noticeably better output.
Keep inputs short. LLMs translate best in chunks of 1-3 paragraphs. For long documents, the file translation feature already handles this by splitting on lines.
Add context when needed. If a word is ambiguous, include a hint in the source language field: python cli.py translate "bank" -t Spanish -f "English (financial context)" โ the model will pick the right meaning.
Temperature tuning. We use 0.1 for consistency. Bump it to 0.3-0.4 if you want more natural-sounding (but less literal) translations.
What Youโve Built
A fully private translation stack:
- CLI tool โ fast terminal translations, language detection, and file processing
- Web UI โ browser-based interface anyone on your team can use
- Zero cloud dependency โ everything runs on your hardware
The entire thing is under 150 lines of Python. No API keys to manage, no usage limits, no privacy concerns.
From here you could add translation memory (cache repeated phrases), support for PDF/DOCX input, or hook it into a larger document processing pipeline. The Ollama API is the same either way โ just prompt and go.