Real-time in-game chat translation, on-screen overlay, and outgoing translated chat — all engine-side. Hosted backend at translate.pugbot.net.
Checksums: SHA256SUMS
Quake3-UrT.x86_64 / Quake3-UrT.exe, mark it executable on Linux (chmod +x ftw-linux-x86_64), then run it. No extra DLLs or shared libraries required — everything (libcurl, TLS via Schannel on Windows / OpenSSL on Linux) is statically linked.
ftwgl/ftwgl-quake3e (branch next-interp) and stock ec-/Quake3e are being prepared once we finish in-game testing. This page will be updated with links and a merge request reference when ready.
A chat-translation layer built directly into the FTWGL Quake3e client. It is not a
text-file localization table. The client sends chat sentences to a
LibreTranslate-compatible
HTTP endpoint asynchronously, caches completed translations, and prints them back as
[TR] … lines in the console and (optionally) as a chat-style on-screen overlay.
The same backend can also be used to translate your own outgoing chat before it
reaches the server.
say / say_team / tell.
The client expects a LibreTranslate-compatible /translate endpoint that accepts
application/x-www-form-urlencoded POST fields:
| Field | Description |
|---|---|
q | Sentence to translate. |
source | Source language, normally auto. |
target | Target language code, e.g. en, es, de, fr. |
format | Always text. |
api_key | Optional, only sent if tr_api_key is set. |
The default endpoint baked into the binary is this host:
https://translate.pugbot.net/translate
You can self-host with Docker:
docker volume create libretranslate-data
docker run -d \
--name libretranslate \
--restart unless-stopped \
-p 127.0.0.1:5000:5000 \
-v libretranslate-data:/home/libretranslate/.local \
libretranslate/libretranslate:latest \
--host 0.0.0.0 \
--port 5000 \
--load-only en,es,de,fr,it,pt,ru
Quick connectivity test:
curl -s https://translate.pugbot.net/translate \
-d 'q=hola amigo como estas' \
-d 'source=auto' \
-d 'target=en' \
-d 'format=text'
Expected response shape:
{"translatedText":"hello friend how are you"}
Once the client is connected and tr_enable 1 (the default), every chat / radio
line that contains a speaker prefix is translated into tr_language. Translated
output appears in the console as:
[TR] PlayerName: hello friend how are you
Repeated identical lines are served instantly from the in-memory cache.
With tr_overlay 1 (the default), translated lines also appear stacked on the
left side of the screen, fading out over tr_overlay_time seconds.
Setting cl_clean_tr 1:
>Player: hello)
is server-driven and outside the engine. To hide it, add
cg_chatHeight 0 to your config. Combined with cl_clean_tr 1, only the
translation overlay will be visible.
Two ways to send a translated message:
1. Generic, language from tr_say_lang:
/tr_say hola, ¿cómo estás?
/tr_say_team buena suerte
/tr_tell 3 gracias por la ayuda
2. Per-language shortcut commands (force the target regardless of the cvar):
/say_es hello world → sends Spanish to public chat
/say_de good luck → sends German
/say_team_pt vamos no spawn → sends Portuguese to team chat
While the translation request is in flight nothing is sent yet. When the response
arrives the client runs the corresponding say / say_team / tell
command with the translated text. If the backend fails or times out, the
original message is sent so you are never silenced.
| Cvar | Default | Description |
|---|---|---|
tr_enable | 1 | Master switch for incoming and outgoing translation. |
tr_language | en (auto-detected from $LANG) | Target language for incoming translation. |
tr_source | auto | Source language hint. auto lets the backend detect. |
tr_url | https://translate.pugbot.net/translate | LibreTranslate-compatible endpoint URL. |
tr_api_key | (empty) | Optional API key. Stored as CVAR_PROTECTED. |
tr_timeout | 2500 | HTTP request timeout in milliseconds (250–10000). |
tr_chat_only | 1 | If set, only chat-style lines are eligible for translation, not arbitrary console output. |
tr_debug | 0 | Print backend / queue diagnostics to the console. |
tr_hide_original | 0 | Suppress the original chat line and only show the translation. On failure the original is shown as a fallback. |
| Cvar | Default | Description |
|---|---|---|
tr_say_lang | en | Target language for the generic tr_say, tr_say_team, tr_tell commands. |
| Cvar | Default | Description |
|---|---|---|
tr_overlay | 1 | Show translated lines as an on-screen overlay (0 = console only). |
tr_overlay_time | 8 | Seconds a translated line stays visible (1–60). Last 25% fades. |
tr_overlay_y | 0 | Pixel Y position for the top of the overlay. 0 = auto. |
cl_clean_tr | 0 | Hide engine notify feed and use chat-sized text for the overlay. |
| Command | Description |
|---|---|
tr_language <code> | Set tr_language and clear the cache. |
tr_status | Print backend URL, current target language, and cache stats (ready / pending / failed). |
tr_clear | Clear the translation cache and any pending HTTP requests. |
tr_reload | Compatibility alias for tr_clear. No text files are loaded. |
| Command | Description |
|---|---|
tr_say <message> | Translate to tr_say_lang and forward via say. |
tr_say_team <message> | Translate to tr_say_lang and forward via say_team. |
tr_tell <client> <message> | Translate to tr_say_lang and forward via tell <client>. |
These ignore tr_say_lang and force the listed target language.
| Public | Team | Target language |
|---|---|---|
say_en | say_team_en | English |
say_es | say_team_es | Spanish |
say_de | say_team_de | German |
say_fr | say_team_fr | French |
say_it | say_team_it | Italian |
say_pt | say_team_pt | Portuguese |
say_ru | say_team_ru | Russian |
The full source patches will be published here once in-game testing is complete and the upstream MR has been opened. High-level layout:
| File | Role |
|---|---|
code/client/tr_system.h | Public API for the translation system. |
code/client/tr_system.c | Implementation: cache, HTTP, overlay, outgoing queue, commands. |
code/client/cl_console.c | Hook: route prints through translator; flush + draw overlay. |
code/client/cl_main.c | Hook: TR_Init, TR_Shutdown, TR_Frame, TR_UpdateCvars. |
Makefile | Adds tr_system.c to the client build with USE_TRANSLATION_CURL. |
"<source>><target>:<lower-cased stripped text>". Each entry stores
its own source/target so incoming and outgoing requests with different
languages do not collide. Identical repeated lines hit the cache instantly.TR_MAX_PENDING = 8) pumped from TR_Frame. Requests use
CURLOPT_NOSIGNAL, CURLOPT_TIMEOUT_MS = tr_timeout,
CURLOPT_CONNECTTIMEOUT_MS = 750. Response JSON is parsed with a small
bespoke parser (no external JSON dependency) supporting \uXXXX escapes.\n), translation only consumes the first line; the trailing remainder is
re-appended verbatim so kill feeds never get swallowed.say, say_team, tell <n>) per cache entry. When the
matching response arrives the client builds
Cbuf_AddText("<cmd> \"<sanitized>\"\n"). Sanitization strips
", \, ; and collapses newlines so the translation can never
break out of the quoted argument or inject extra commands.tr_hide_original falls back to printing the
original line.TR_EnqueueDisplay are also
pushed to an 8-line ring. TR_DrawOverlay renders oldest-first, fading
during the last 25% of tr_overlay_time. cl_clean_tr selects between
SMALLCHAR (native) and BIGCHAR (640×480 virtual) rendering and gates
Con_DrawNotify.[TR] lines through Com_Printf, an internal flag short-circuits the
translator so it does not try to translate its own output.| Symptom | Fix |
|---|---|
No [TR] lines appear. | Verify tr_enable 1, tr_status shows the URL, and the backend is reachable. Set tr_debug 1. |
Linux: "missing libcurl symbol" when launching an older build. | The current binaries on this page are statically linked — you should not see this. If you do, you are running an older build that did dlopen("libcurl.so.4"); install libcurl4 or download a fresh binary above. |
Outgoing tr_say sends original text. | Backend is unreachable or timing out. Increase tr_timeout, check tr_status, and confirm the URL with curl. |
Same-language say_en still hits the network. | Set tr_source en explicitly; with tr_source auto the client cannot prove the source matches the target. |
| cgame yellow chat still shows in clean mode. | Add cg_chatHeight 0 to your Urban Terror config. The cgame draws that overlay; the engine cannot suppress it. |
| Console feed still translates non-chat lines. | Set tr_chat_only 1 (default). |
^1, ^7, …) are sent to the backend as opaque tokens.
Most LibreTranslate models pass them through, but heavy reformatting can lose
them. Translated output is then re-prefixed with the speaker's original colored prefix.say /
say_team / tell commands. Anti-flood limits still apply.tr_say_lang <code> + tr_say.