๐Ÿ“ Tutorials
ยท 5 min read

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

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.

๐Ÿ“˜