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
| Feature | Weblate | Crowdin | Transifex | Pontoon | POEditor |
|---|---|---|---|---|---|
| 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 RAM | N/A (cloud) | N/A (cloud) | ~1 GB RAM | N/A (cloud) |
| Pricing (commercial) | Free (self-hosted) | From $40/mo | From $125/mo | Free (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_PASSWORDfrom 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
- Go to Management → Projects → Add new
- Fill in the project name, URL slug, and website URL
- Set the source language (usually English)
- Save
Add a Component (Git Integration)
This is where the magic happens. A component connects directly to your Git repository:
- Inside your project, click Add new component
- Choose Version control system: Git
- Source code repository:
https://github.com/your-org/your-repo.git - Repository branch:
main - File mask:
locales/*/messages.po(adjust to your file structure) - New translation template:
locales/en/messages.po - File format: Auto-detect usually works, or choose explicitly (GNU gettext, JSON, Android strings, etc.)
Common file mask patterns:
| Format | File mask example |
|---|---|
| GNU gettext (.po) | locales/*/LC_MESSAGES/messages.po |
| JSON (i18next) | public/locales/*/translation.json |
| Android strings | app/src/main/res/values-*/strings.xml |
| iOS strings | *.lproj/Localizable.strings |
| XLIFF | translations/*.xliff |
| Flutter ARB | lib/l10n/app_*.arb |
| Ruby YAML | config/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:
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 flags → Require 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
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 --checkfor 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.