Status: Roadmap. The Weblate integration, namespaced JSON structure (
locales/{es,en,...}/), and CI validation workflow described in this document represent the target translation infrastructure. Currently (alpha, v0.1.0), translations are managed via the monolithiccoffeepie_website/public/translations.jsonfile. SeeCONTRIBUTING.mdfor the current workflow. The migration to Weblate and namespaced locales is planned —scripts/migrate_to_namespaces.pyand.github/workflows/validate-translations.ymlwill be created as part of this migration.
Weblate is the canonical Translation Management System for Coffee Pie. It provides a web-based GUI that allows translators (technical and non-technical) to contribute without touching code or JSON directly. Weblate syncs bidirectionally with this GitHub repository — translations approved in Weblate are committed as pull requests automatically.
Why Weblate over manual JSON editing:
- No syntax errors (Weblate validates JSON on save)
- No merge conflicts from concurrent translators
- Glossary enforcement prevents mistranslation of brand terms
- Machine translation pre-fill with mandatory human review (voting)
- Non-technical translators don't need to know Git or JSON
- Go to https://hosted.weblate.org
- Sign in with your GitHub account
- Add this repository (
avolink/coffeepie) as a new translation project - Point it at the translation files under
coffeepie_website/public/locales/ - Configure: source language = Spanish (
es), file format = JSON with nested structure - Done — translators get a web UI immediately
Create weblate/docker-compose.yml:
version: '3'
services:
weblate:
image: weblate/weblate
ports:
- "8080:80"
environment:
WEBLATE_SITE_DOMAIN: "translate.coffeepie.co"
WEBLATE_ADMIN_EMAIL: "admin@coffeepie.co"
WEBLATE_ADMIN_PASSWORD: "${WEBLATE_ADMIN_PASSWORD}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
REDIS_PASSWORD: "${REDIS_PASSWORD}"
volumes:
- weblate_data:/app/data
restart: always
database:
image: postgres:15
environment:
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_USER: weblate
POSTGRES_DB: weblate
volumes:
- postgres_data:/var/lib/postgresql/data
restart: always
cache:
image: redis:7-alpine
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
volumes:
- redis_data:/data
restart: always
volumes:
weblate_data:
postgres_data:
redis_data:Start with:
cd weblate
echo "WEBLATE_ADMIN_PASSWORD=changeme" > .env
echo "POSTGRES_PASSWORD=changeme" >> .env
echo "REDIS_PASSWORD=changeme" >> .env
docker compose up -dThen configure Weblate's admin UI to connect to https://github.com/avolink/coffeepie.git using a bot account's Personal Access Token.
coffeepie_website/public/translations.json (1.88 MB, 1,289 entries, 12 languages)
Keys are Spanish source text, values are objects with language codes:
{
"PRECIOS": {
"en": "PRICES",
"es": "PRECIOS",
"pt": "PREÇOS"
}
}coffeepie_website/public/locales/
es/
common.json ← navigation, labels, short text
pages.json ← page content, sections, long paragraphs
products.json ← product descriptions, specs, store
legal.json ← privacy policy, terms of service
faq.json ← Q&A entries
brand.json ← brand terms (should be identical across all languages)
en/
common.json
...
pt/ fr/ de/ ja/ ko/ zh/ ru/ ar/ hi/ el/
Each file uses ICU MessageFormat for complex plurals, genders, and variables:
{
"slices.count": "{count, plural, =0 {No slices} one {# slice} other {# slices}}",
"credits.remaining": "{credits, number} credits remaining",
"date.expires": "Expires {date, date, long}"
}Run scripts/migrate_to_namespaces.py (to be created) which:
- Reads
translations.json - Classifies each entry into a namespace based on content patterns
- Generates short semantic keys (e.g.,
pages.hero.modularidad) - Writes one file per language per namespace
- Generates
key_map.jsonfor backward compatibility
The following must remain identical across all 12 languages in any translation file:
| Category | Examples |
|---|---|
| Email addresses | accesibility@coffeepie.co |
| Physical addresses | Cr 46 #56-11, La Candelaria, Medellín, Antioquia. |
| Brand names with registered trademarks | Coffee Pie®, Commanders™, Sentinels™, Rangers™ |
| Company/project names | QFDM, OpenUDS, Sunshine, Moonlight, Proxmox |
| URLs and API endpoints | https://api.coffeepie.co, www.coffeepie.co |
| Technical specs and units | 1 Wh, 8 GB, 1 Core, 3 TOPS, 15 vMPX/s |
| Social media handles | Instagram, Facebook, TikTok, Youtube |
| Trademark/Copyright symbols | ™, ®, © — never replace with plain text TM, (R), (C) |
These identifiers should store the same value for all language codes (en, es, pt, fr, de, ru, hi, ja, zh, ko, ar, el).
Never run automated translation tools (LibreTranslate, Google Translate API, etc.) on the entire translations.json file. Automated translation tools:
- Mistranslate proper nouns (e.g.,
INICIObecameINITIOinstead ofHOME) - Corrupt HTML/special characters (e.g.,
|||paragraph separators becameh.124;fragments) - Produce misleading output in non-Latin scripts (e.g., Japanese
ホームbecameインティオ, Chinese首页became印度)
Instead, use Weblate which supports machine translation pre-fill with mandatory human review, glossary enforcement, and voting workflows.
All LibreTranslate/AI batch-translation scripts have been deleted. Weblate is the only supported translation workflow for production.
- Source language: Spanish (
es) — the canonical, correct version - Machine pre-fill: Weblate can use DeepL, Google Translate, or an LLM API to generate draft translations for new keys. These drafts are marked as "Needs review."
- Human review: At least one reviewer must approve each machine-generated translation before it is merged
- Glossary enforcement: Weblate warns translators when a term has a canonical translation defined in the glossary
- Voting: Community members can upvote/downvote translations. A minimum vote threshold can be configured
- CI validation: GitHub Actions validate JSON syntax, key consistency, and no empty translations on every PR (see
.github/workflows/validate-translations.yml)
| Spanish (source) | English | Portuguese | Notes |
|---|---|---|---|
| Terminales Codec | Codec Terminals | Terminais Codec | NOT "Codec terminals" (inconsistent caps) |
| Terminales Codec Modulares | Modular Codec Terminals | Terminais Codec Modulares | |
| Commanders™ | Commanders™ | Commanders™ | ™ is Unicode, not plain TM |
| Sentinels™ | Sentinels™ | Sentinels™ | |
| Rangers™ | Rangers™ | Rangers™ | |
| Coffee Pie® | Coffee Pie® | Coffee Pie® | ® is Unicode, not plain (R) |
| QFDM | QFDM | QFDM | Do not translate acronym |
| Sostenibilidad | Sustainability | Sustentabilidade | |
| Modularidad | Modularity | Modularidade | |
| Precios | Prices | Preços | |
| Tienda | Store | Loja | |
| Inicio | Home | Início | NOT "INITIO" (machine translation error) |
| Acerca de | About | Sobre | |
| Panel de Usuario | User Panel | Painel do Usuário | NOT "BAN OF USER" (machine translation error) |
| Anunciantes | Advertisers | Anunciantes | NOT "Announcers" |
| Fabricantes | Manufacturers | Fabricantes | |
| Consumidores Directos | Direct Consumers | Consumidores Diretos | |
| Proveedor de Internet (ISP) | Internet Service Provider (ISP) | Provedor de Internet (ISP) |
Create .github/workflows/validate-translations.yml:
name: Validate Translations
on:
pull_request:
paths:
- 'coffeepie_website/public/locales/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Validate JSON syntax
run: |
for f in coffeepie_website/public/locales/*/*.json; do
node -e "JSON.parse(require('fs').readFileSync('$f','utf8'))" || exit 1
done
- name: Check all languages have same keys
run: |
node -e "
const fs = require('fs');
const dirs = fs.readdirSync('coffeepie_website/public/locales');
const keysets = {};
let errors = 0;
dirs.forEach(lang => {
const files = fs.readdirSync('coffeepie_website/public/locales/' + lang);
files.forEach(f => {
const data = JSON.parse(fs.readFileSync('coffeepie_website/public/locales/' + lang + '/' + f, 'utf8'));
keysets[f] = keysets[f] || [];
keysets[f].push({lang, keys: Object.keys(data).sort()});
});
});
for (const [ns, entries] of Object.entries(keysets)) {
const refKeys = entries[0].keys.join(',');
entries.forEach(e => {
if (e.keys.join(',') !== refKeys) {
const missing = entries[0].keys.filter(k => !e.keys.includes(k));
const extra = e.keys.filter(k => !entries[0].keys.includes(k));
if (missing.length) console.error('MISSING KEYS in', ns, e.lang + ':', missing.join(', '));
if (extra.length) console.error('EXTRA KEYS in', ns, e.lang + ':', extra.join(', '));
errors++;
}
});
}
process.exit(errors > 0 ? 1 : 0);
"
- name: Check no empty translations
run: |
node -e "
const fs = require('fs');
const dirs = fs.readdirSync('coffeepie_website/public/locales');
let errors = 0;
dirs.forEach(lang => {
const files = fs.readdirSync('coffeepie_website/public/locales/' + lang);
files.forEach(f => {
const data = JSON.parse(fs.readFileSync('coffeepie_website/public/locales/' + lang + '/' + f, 'utf8'));
Object.entries(data).forEach(([k,v]) => {
if (!v || v.trim() === '') { console.error('EMPTY:', lang, f, k); errors++; }
});
});
});
process.exit(errors > 0 ? 1 : 0);
"| Code | Language | Script | Status |
|---|---|---|---|
es |
Spanish | Latin | Canonical source |
en |
English | Latin | Needs review |
pt |
Portuguese | Latin | Needs review |
fr |
French | Latin | Needs review |
de |
German | Latin | Needs review |
el |
Greek | Greek | Needs review |
ru |
Russian | Cyrillic | Needs review |
hi |
Hindi | Devanagari | Needs review |
ja |
Japanese | Hiragana/Katakana/Kanji | Needs review |
zh |
Chinese (Simplified) | Hanzi | Needs review |
ko |
Korean | Hangul | Needs review |
ar |
Arabic | Arabic | Needs review |
Note: For non-Latin script languages (ru, hi, ja, zh, ko, ar, el), translations must contain characters from their native script. Latin-only text in these columns indicates an untranslated or machine-translated entry that needs human review.
| Task | Command / Action |
|---|---|
| Translate a string | Open Weblate at translate.coffeepie.co, find the key, enter translation |
| Add a new key | Add it to locales/es/<namespace>.json, then git push — Weblate auto-detects it |
| Review machine translations | Weblate shows "Needs review" badge — click Approve or Edit |
| Validate translations | PRs auto-validate via GitHub Actions |
| Check translation coverage | See /TRANSLATIONS_AUDIT_REPORT.md (generated periodically) |