Self-Hosting Weblate: Continuous Localization Platform

Localization is one of those things that’s easy to ignore until it blocks a release. You’ve got translators waiting on developers to export strings, developers waiting on translators to finish, and a spreadsheet somewhere that nobody trusts anymore. Meanwhile your French translation is three versions behind and the German one has 47 fuzzy entries nobody’s reviewed.

Weblate is an open-source continuous localization platform that plugs directly into your Git workflow. Translators work in a web UI, changes get committed back to your repository automatically, and you can see translation status at a glance across every language. It’s used by over 2,500 projects including LibreOffice, phpMyAdmin, and Fedora — so it handles real scale.

The hosted Weblate service works well for open-source projects (it’s free for them), but self-hosting gives you unlimited projects, unlimited users, full control over your data, and no per-word pricing for commercial use. If you’re localizing proprietary software or need to keep translations on your own infrastructure, self-hosting is the way to go.

Weblate vs Other Translation Platforms

FeatureWeblateCrowdinTransifexPontoonPOEditor
Open source✅ GPLv3❌ Proprietary❌ Proprietary✅ BSD❌ Proprietary
Git integration✅ Native push/pull✅ Integrations✅ Integrations✅ Native⚠️ Import/export
Machine translation✅ 10+ engines✅ Built-in✅ Built-in✅ Google/MS✅ Google/MS
Translation memory✅ Shared + per-project⚠️ Basic
Quality checks✅ 50+ built-in✅ QA checks✅ QA checks⚠️ Basic
File formats✅ 40+ (PO, XLIFF, JSON, Android, iOS, etc.)✅ 30+✅ 20+⚠️ Limited✅ 20+
Self-hostable✅ Docker/bare metal
Glossary✅ Per-project + shared
Review workflow✅ Configurable✅ Suggestion-based⚠️ Basic
Resource usage~3 GB RAMN/A (cloud)N/A (cloud)~1 GB RAMN/A (cloud)
Pricing (commercial)Free (self-hosted)From $40/moFrom $125/moFree (self-hosted)From $15/mo

Weblate wins on format support, quality checks, and the fact that it commits translations directly to your repo. If your workflow is “translators submit, CI validates, PR merges,” Weblate fits like a glove.

Prerequisites

  • Docker and Docker Compose installed (Get Docker)
  • A domain name pointed at your server (e.g., translate.example.com)
  • At least 3 GB of RAM (4+ GB recommended for production)
  • 2 CPU cores minimum
  • SMTP credentials for email notifications
  • A reverse proxy for HTTPS termination

Quick Start with Docker Compose

Weblate’s official Docker setup uses three containers: the Weblate application, PostgreSQL for the database, and Valkey (Redis-compatible) for caching.

Create a project directory:

mkdir weblate && cd weblate

Create your environment file with configuration:

cat > environment <<'EOF'
# Domain and site settings
WEBLATE_SITE_DOMAIN=translate.example.com
WEBLATE_SITE_TITLE=My Translations

# Admin account (change these!)
[email protected]
WEBLATE_ADMIN_PASSWORD=change-me-immediately
WEBLATE_ADMIN_NAME=Admin

# Email configuration
WEBLATE_EMAIL_HOST=smtp.example.com
[email protected]
WEBLATE_EMAIL_HOST_PASSWORD=your-smtp-password
[email protected]
[email protected]
WEBLATE_EMAIL_PORT=587
WEBLATE_EMAIL_USE_TLS=true

# HTTPS behind reverse proxy
WEBLATE_ENABLE_HTTPS=1
WEBLATE_IP_PROXY_HEADER=HTTP_X_FORWARDED_FOR

# Security
WEBLATE_ALLOWED_HOSTS=translate.example.com

# PostgreSQL connection (matches database service)
POSTGRES_PASSWORD=weblate-db-password
POSTGRES_USER=weblate
POSTGRES_DATABASE=weblate
POSTGRES_HOST=database
POSTGRES_PORT=5432

# Registration settings
WEBLATE_REGISTRATION_OPEN=1
WEBLATE_REQUIRE_LOGIN=0

# Performance
WEBLATE_WORKERS=2
WEB_WORKERS=4

# Time zone
WEBLATE_TIME_ZONE=UTC
EOF

Create docker-compose.yml:

services:
  weblate:
    image: weblate/weblate:latest
    depends_on:
      - database
      - cache
    volumes:
      - weblate-data:/app/data
      - weblate-cache:/app/cache
    env_file:
      - ./environment
    ports:
      - "127.0.0.1:8080:8080"
    restart: always
    read_only: true
    tmpfs:
      - /run
      - /tmp

  database:
    image: postgres:16-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data
    env_file:
      - ./environment
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U weblate"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: valkey/valkey:9-alpine
    volumes:
      - cache-data:/data
    command: ["valkey-server", "--save", "60", "1", "--loglevel", "warning"]
    restart: always
    read_only: true

volumes:
  weblate-data: {}
  weblate-cache: {}
  postgres-data: {}
  cache-data: {}

Start everything:

docker compose up -d

First startup takes a few minutes — Weblate runs database migrations and compiles static assets. Watch the logs:

docker compose logs -f weblate

Once you see spawned uWSGI worker, you’re ready. Access the UI at http://your-server:8080 and log in with the admin credentials you set.

Important: Change your admin password immediately if you used a weak one in the environment file. After first login, you can remove WEBLATE_ADMIN_PASSWORD from the environment file — it’s only used during initial setup.

Adding Your First Translation Project

Weblate organizes translations into Projects (your application) and Components (individual translatable resources within that project).

Create a Project

  1. Go to Management → Projects → Add new
  2. Fill in the project name, URL slug, and website URL
  3. Set the source language (usually English)
  4. Save

Add a Component (Git Integration)

This is where the magic happens. A component connects directly to your Git repository:

  1. Inside your project, click Add new component
  2. Choose Version control system: Git
  3. Source code repository: https://github.com/your-org/your-repo.git
  4. Repository branch: main
  5. File mask: locales/*/messages.po (adjust to your file structure)
  6. New translation template: locales/en/messages.po
  7. File format: Auto-detect usually works, or choose explicitly (GNU gettext, JSON, Android strings, etc.)

Common file mask patterns:

FormatFile mask example
GNU gettext (.po)locales/*/LC_MESSAGES/messages.po
JSON (i18next)public/locales/*/translation.json
Android stringsapp/src/main/res/values-*/strings.xml
iOS strings*.lproj/Localizable.strings
XLIFFtranslations/*.xliff
Flutter ARBlib/l10n/app_*.arb
Ruby YAMLconfig/locales/*.yml

Push Configuration

For Weblate to commit translations back to your repository, you need push access:

For GitHub: Create a personal access token (or deploy key) and set the push URL:

https://oauth2:ghp_YOUR_TOKEN@github.com/your-org/your-repo.git

Or use SSH by mounting your SSH key:

# In docker-compose.yml, add to weblate volumes:
volumes:
  - weblate-data:/app/data
  - weblate-cache:/app/cache
  - ./ssh:/app/data/ssh:ro

Commit settings in the component configuration let you customize:

  • Commit message templates (include translator credit)
  • Whether to squash commits
  • Push branch (can push to a separate branch for review)
  • Auto-merge after push

Setting Up Machine Translation

Weblate integrates with multiple machine translation engines to suggest translations:

In Management → Machine Translation, enable your preferred engines:

DeepL (Best Quality for European Languages)

# Add to environment file:
WEBLATE_MT_DEEPL_KEY=your-deepl-api-key

LibreTranslate (Self-Hosted, Free)

# Add to environment file:
WEBLATE_MT_LIBRETRANSLATE_API_URL=http://libretranslate:5000

You can run LibreTranslate alongside Weblate. Add to docker-compose.yml:

  libretranslate:
    image: libretranslate/libretranslate:latest
    restart: always
    environment:
      - LT_LOAD_ONLY=en,de,fr,es,pt,it,nl,ja,zh,ko

OpenAI / Custom LLM

WEBLATE_MT_OPENAI_KEY=sk-your-key

Machine translations appear as suggestions — translators review and accept or modify them. This speeds up initial translations dramatically while maintaining quality through human review.

Review Workflow

Weblate supports configurable review workflows per component:

Suggestion-Based (Default)

Anonymous and non-privileged users submit suggestions. Trusted translators accept or reject them.

Review Required

Translations go through a two-stage process: translate → review. Enable in component settings under Translation flagsRequire review.

Dedicated Reviewers

Assign the “Review strings” permission to specific users. Translations stay in “needs review” state until a reviewer approves them.

Automatic Suggestions

Enable Auto-suggest to pre-fill translations from:

  • Translation memory (shared across projects)
  • Machine translation engines
  • Other components in the same project

Reverse Proxy Configuration

Caddy

t}ransr}leavteerh.seeeax_dapemrrpo_lxueyp.clXoo-mcFao{lrhwoasrtd:e8d0-8P0ro{to{scheme}

Nginx

server {
    listen 443 ssl http2;
    server_name translate.example.com;

    ssl_certificate /etc/letsencrypt/live/translate.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/translate.example.com/privkey.pem;

    client_max_body_size 100M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_read_timeout 3600s;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }
}

The long proxy_read_timeout is important — some Weblate operations (like importing large translation files) take time.

Backup and Restore

Backup

Weblate stores data in three places: the PostgreSQL database, the data volume (Git repos, SSH keys, translation memory), and the cache:

#!/bin/bash
# backup-weblate.sh
BACKUP_DIR="/backups/weblate/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# Database dump
docker compose exec -T database pg_dump -U weblate weblate | gzip > "$BACKUP_DIR/database.sql.gz"

# Data volume (repos, SSH keys, TM)
docker run --rm -v weblate_weblate-data:/data -v "$BACKUP_DIR":/backup alpine \
    tar czf /backup/weblate-data.tar.gz -C /data .

echo "Backup saved to $BACKUP_DIR"

Restore

# Stop Weblate
docker compose down

# Restore database
gunzip < "$BACKUP_DIR/database.sql.gz" | docker compose exec -T database psql -U weblate weblate

# Restore data volume
docker run --rm -v weblate_weblate-data:/data -v "$BACKUP_DIR":/backup alpine \
    sh -c "rm -rf /data/* && tar xzf /backup/weblate-data.tar.gz -C /data"

# Restart
docker compose up -d

Troubleshooting

Weblate Takes Forever to Start

First startup genuinely takes 2–5 minutes for migrations. Subsequent starts are faster. If it’s stuck, check the logs:

docker compose logs weblate | tail -50

Common causes: database not ready (add healthcheck to PostgreSQL), insufficient RAM (Weblate needs 3+ GB).

Git Push Fails

Check the push URL and credentials:

docker compose exec weblate weblate shell
# In Python shell:
from weblate.trans.models import Component
c = Component.objects.first()
print(c.push)

For SSH, ensure the key is in /app/data/ssh/ and the host is in known_hosts.

Translation File Format Errors

If Weblate can’t parse your translation files, check:

  • File mask matches your actual file structure
  • File format is set correctly (auto-detect sometimes guesses wrong)
  • Source files are valid (run msgfmt --check for PO files)

High Memory Usage

Weblate’s main memory consumers are the translation memory database and Git repositories. For large installations:

# Add to environment:
WEBLATE_WORKERS=2      # Reduce from default
WEB_WORKERS=2          # Reduce web workers
WEBLATE_SILENCED_SYSTEM_CHECKS=weblate.E019  # Suppress non-critical warnings

Emails Not Sending

Test email configuration:

docker compose exec weblate weblate sendtestemail [email protected]

If that fails, double-check SMTP settings. Most issues are wrong port, missing TLS setting, or authentication failures.

Power User Tips

Webhooks for CI/CD: Configure repository webhooks so Weblate pulls new strings automatically when developers push. Supported for GitHub, GitLab, Bitbucket, Gitea, and Pagure — just add the webhook URL shown in component settings.

Translation memory across projects: Enable shared translation memory in project settings. A string translated in one project auto-suggests for identical strings in others. Huge time saver for organizations with multiple apps.

Glossary management: Create a glossary component to enforce consistent terminology. Weblate flags translations that don’t use glossary terms.

Add-ons for automation: Weblate has 30+ add-ons. Key ones:

  • Automatic translation — pre-fill from TM and machine translation
  • Cleanup translation files — remove stale strings after source changes
  • Squash Git commits — keep translation branch history clean
  • Component discovery — auto-create components based on file patterns

API access: Weblate has a full REST API for automation:

curl -H "Authorization: Token YOUR_API_TOKEN" \
    https://translate.example.com/api/projects/

SSO integration: Configure SAML, LDAP, or social auth (GitHub, GitLab, Google) for team access. Add the appropriate environment variables — see the authentication docs.

Horizontal scaling: For large deployments, run multiple Weblate containers behind a load balancer with shared PostgreSQL, Valkey, and NFS/S3 for the data volume. Weblate supports Celery workers on separate nodes for background task processing.

Wrapping Up

Weblate eliminates the translation bottleneck by making localization a continuous process instead of a batch job. Translators work in a clean web UI, changes flow back to your repo via Git, and you get quality checks, translation memory, and machine translation suggestions out of the box.

The self-hosted setup is heavier than some tools we’ve covered (3 GB RAM minimum is no joke), but for any project serious about localization, it’s worth the resources. You get unlimited users, unlimited projects, and no per-word fees — just the cost of running a server.

For HTTPS, pair it with Caddy for automatic certificates. Back up regularly with Kopia or Duplicati. And if you’re managing team access, Authentik or Authelia handle SSO beautifully.