Install, staff-command, configuration and integration reference for server operators. For how translation looks to players see the Scuffed Translation gameplay guide.
- Plugin version: 0.14.7-pre.1 (plugin name
ScuffedTranslation)
Requirements
| Item | Requirement |
|---|---|
| Hytale server | >=0.7.0-pre.0 <0.8.0 |
| Required plugin | Hytale:AssetModule (always present on a server) |
| Optional plugins | None declared. EndlessLink is detected at runtime (see Integrations) |
| Network | Outbound HTTPS from the server to the translation provider you choose |
The mod ships no assets. Translation needs a working backend: the default setup works with no key and no extra software.
Chat text is sent to the backend you configure. With gemini or google_free that is a third-party service, so players' chat leaves your server. Use libretranslate on your own host if that matters for your community.
How it works
For each chat message, the mod works out which readers need a translation and then resolves all of their languages together.
- Who needs it. A reader is skipped if they are the sender, have translation off, read the same language the sender writes, or the message already looks like their language. Messages with no letters, or over
maxMessageWords, are left to normal chat. - Shorthand. Gamer abbreviations (
wru,brb,lfg,idkand others) are expanded to full words. - Phrasebook. About 40 common phrases are answered instantly in English, Spanish, Portuguese and French, with no network call (
usePhrasebook). - Cache. An in-memory LRU cache, saved to disk, holds every language translated for a message. A phrase is only translated once.
- Masking. Text in double quotes and known item names are swapped for placeholders before the backend sees them and restored afterwards.
- Backend call. One batched call covers all missing languages. Identical messages that arrive at the same moment share one call.
- Delivery. The translated line is rendered with the same formatter as a native chat line, with an optional
(code)tag, and sent only to the readers of that language. Everyone else keeps normal chat.
If anything fails, the original text is delivered. Translation runs on a small background pool, never on the world thread.
Backends
backend value | Provider | Notes |
|---|---|---|
gemini (default) | Google Gemini, model from geminiModel | One request returns every needed language as JSON. Needs geminiApiKey. With no key the mod logs a warning and runs google_free instead |
google_free (alias google) | Keyless Google Translate web endpoints | No setup. One request per language. Unofficial, so it can be rate limited per server IP; the mod rotates between three endpoint variants if one is refused |
libretranslate (alias libre) | Your own LibreTranslate server at libreUrl | One request per language, sent in parallel. No outside quota |
Routing rules:
- Messages longer than
largeMessageWordswords skip the primary backend and go togoogle_free, which saves Gemini requests. - If the primary reports a quota or rate error (HTTP 429, "quota", "resource exhausted" and similar), it is skipped for
quotaBackoffMinutesandfallbackBackendtakes over. One warning is logged when this starts and one info line when the primary returns. - When primary and fallback are both
google_free(the keyless setup), a 429 backs off for 15 seconds, doubling on each repeat up toquotaBackoffMinutes, and resets on the first success. Messages in that window are delivered untranslated. - Requests against the primary are metered by a token bucket. For
geminithe bucket isrequestsPerMinute. Forgoogle_freeit is the larger ofrequestsPerMinuteand 60, and forlibretranslatethe larger ofrequestsPerMinuteand 240. Each language of a per-language backend costs one request. The fallback and long-message backends are not metered by this bucket. - The background pool queue holds 256 tasks. When it is full, the message is delivered untranslated.
Admin & Staff Commands
All commands are player commands (they cannot be run from the console). The root command is /translate, alias /tr. Every sub-command is visible to the default player group; the staff ones check for operator status themselves and reply with an error otherwise.
| Command | Description |
|---|---|
/translate global on | Enable translation server-wide again. Players with it switched off stay off |
/translate global off | Disable translation for everyone. The setting is saved in prefs.json and survives restarts. /translate status tells players it is disabled |
/translate test <lang> <text> | Translate a sample line into a supported language code and print the result, the backend name and the time taken. Use it to check keys and connectivity with one player online |
/translate reload | Re-read config.json, rebuild the backends and the per-player throttle |
Player commands (on, off, set <code>, status, langs) are listed in the gameplay guide. /translate status also prints a "Chat delivered by" line that names who is sending translated chat: the built-in listener, EndlessLink, or "disabled" when builtinChatListener is off. When /translate test works but chat does not, this line shows which component owns chat.
What /translate reload does not apply
translationThreads (the pool is built at startup) and eventPriority (the listener is registered at startup) need a server restart. Everything else in config.json takes effect on reload. On 0.14.7-pre.1 a reload re-reads cache.json without saving first, so translations not yet written to it (fewer than 200) are lost. Later builds save the cache before rebuilding.
Permissions
The mod has one permission node, and it is not a command node.
| Node | Effect |
|---|---|
scuffedtranslation.admin | Granted to a user, it counts as operator for global, test and reload |
Group hytale:Admin | Members of the engine admin (operator) group can use the same sub-commands |
The /translate command tree itself is assigned to the hytale:Adventurer default group, so every player can use the player sub-commands without any permission setup.
Configuration
File: mods/ScuffedTranslation/config.json. It is written with the defaults below on first start. Edit it, then run /translate reload. Values below a minimum are raised to the minimum.
| Key | Default | Description |
|---|---|---|
backend | gemini | gemini, google_free or libretranslate |
geminiApiKey | empty | Google AI Studio API key for Gemini. If empty, the mod reads a geminiApiKey from mods/AIPrefab/config.json when that file exists |
geminiModel | gemini-2.5-flash-lite | Gemini model name |
libreUrl | http://localhost:5000 | Base URL of your LibreTranslate server |
libreApiKey | empty | LibreTranslate API key, if your instance requires one |
fallbackBackend | google_free | Backend used when the primary is over quota or failing. none disables the fallback |
quotaBackoffMinutes | 10 | How long the primary is skipped after a quota error (minimum 1) |
eventPriority | LATE | Priority of the chat listener. Use an engine priority name; an invalid value falls back to LATE. Needs a restart |
builtinChatListener | auto | auto: active, but steps aside when EndlessLink drives chat. on: always active. off: never active (translation only through the API) |
useContext | false | Send the last few chat lines to the backend to resolve pronouns. Bypasses the cache and costs more requests |
contextLines | 3 | Number of recent lines used when useContext is on (minimum 1) |
skipIfAlreadyInTargetLanguage | true | Skip readers when the text already looks like their language (guess works for English, Spanish, Portuguese and French only) |
autoDetectClientLanguage | true | Translate into each player's game client language. Off means translation is opt-in through /translate set |
defaultLanguage | en | Language for players whose client language is unknown or unsupported. Empty means those players are not translated. Only used when autoDetectClientLanguage is on |
showSourceTag | true | Append a faint (code) tag to translated lines |
usePhrasebook | true | Use the built-in phrasebook before any backend call |
protectItemNames | true | Keep item and glossary names out of translation |
requestsPerMinute | 14 | Request budget for the primary backend (minimum 1) |
cacheSize | 4000 | Maximum cached messages (minimum 64) |
maxMessageWords | 100 | Longer messages are not translated (minimum 1) |
largeMessageWords | 25 | Longer messages go to google_free (minimum 1) |
perPlayerMessagesPerMinute | 15 | Spam guard: translated messages per sender per minute, with a burst of the same size. Extra messages are delivered untranslated (minimum 1) |
requestTimeoutSeconds | 8 | Timeout for each backend HTTP call (minimum 2) |
translationThreads | 4 | Size of the background pool, 1 to 16. Needs a restart |
debug | false | Log each primary call and backend errors |
The file also contains a _comment field that is ignored. An old supportedLanguages key is ignored and logs a line; the language list is fixed in the mod (18 languages, see the gameplay guide) because the in-game font renders Latin and Cyrillic scripts only.
API key setup
Translation works with no key. To use Gemini, create an API key in Google AI Studio and put it in geminiApiKey, then run /translate reload. Keep the key out of shared copies of the file. The key is sent only to Google's Gemini endpoint. If you already use the AIPrefab plugin, its geminiApiKey is reused automatically when the field here is empty.
Cost and rate controls
- The token bucket (
requestsPerMinute) and the per-sender limit (perPlayerMessagesPerMinute) cap request volume. - The phrasebook, the persistent cache and duplicate merging remove most repeat calls.
largeMessageWordskeeps long messages off the metered backend.maxMessageWordsdrops very long messages entirely.useContextraises request counts; leave it off unless you need it.
Data & storage
All files are in mods/ScuffedTranslation/ (relative to the server's working directory).
| File | Contents |
|---|---|
config.json | Settings, created on first start |
prefs.json | Per-player choices keyed by UUID (lang for /translate set, enabled for `/translate on |
cache.json | Persistent translation cache. Written after every 200 new entries and at shutdown, and loaded on start |
glossary.json | Optional, you create it. {"terms": ["Crystal Sword", "Iron Pickaxe"]} adds names to protect. Only entries of 2 to 4 words are used |
The glossary also harvests every item id from the server registry, including modded items, the first time a player joins. Only multi-word names (2 to 4 words, for example Copper Bar) are indexed.
To clear all translations, stop the server and delete cache.json. To reset one player, remove their UUID from prefs.json.
Integrations
| Plugin | Type | Use |
|---|---|---|
| EndlessLink | Detected at runtime | When an EndlessLink build with the ScuffedTranslation bridge is installed and its chat.modifyChatMessages is not false, the built-in listener stands down and EndlessLink renders each viewer's line using this mod's API. Otherwise the built-in listener stays active |
| AIPrefab | Optional config source | Its geminiApiKey is reused when yours is empty |
With builtinChatListener set to auto, the built-in listener also steps aside as soon as any chat mod calls the API below. Set it to on to force the built-in listener or off to disable it.
API for other mods
Chat mods that deliver chat themselves can call com.zbeve.scuffedtranslation.ScuffedTranslationApi. It uses only engine types, so it can be reached by reflection without a hard dependency. Get the instance with the static get() (null when the mod is not loaded). Calls can block on a backend request, so make them off the tick thread.
| Method | Purpose |
|---|---|
claimChat() | Declare that your mod delivers chat. The built-in listener steps aside (with builtinChatListener at auto) |
viewersNeedingTranslation(sender, body, viewers) | Fast check with no network. Returns the viewer UUIDs who will get a translation. Deliver the original to everyone else immediately |
translateForViewers(sender, body, viewers) | Returns viewerUuid -> translated body for viewers who need one. Viewers missing from the map get the original |
viewersNeedingRemoteTranslation(senderUuid, senderLang, body, viewers) | Same pre-check for a message relayed from another server; there is no local sender |
translateForRemoteViewers(senderUuid, senderLang, body, viewers) | Translation for a relayed message. A missing sender UUID shares one spam-guard bucket |
languageOf(player) | The base language code the player writes in, or null. Send it with relayed messages so the receiving server does not guess |
All calls apply the global switch, word limits, per-player throttle, cache and fallback rules described above. All methods except languageOf also mark chat as externally driven.
Troubleshooting
| Symptom | Check |
|---|---|
| Nobody gets translated chat | /translate status for the "Chat delivered by" line. builtinChatListener may be off, or /translate global off may be active |
/translate test works but chat does not | Another chat mod owns delivery. Check the "Chat delivered by" line in /translate status |
Gemini selected but the log says it is using google_free | geminiApiKey is empty and no AIPrefab key was found |
| Messages arrive untranslated in bursts | The backend is rate limiting. Look for the quota warning in the log, lower traffic, set a Gemini key, or point backend at your own LibreTranslate |
| Players with unsupported clients are not translated | Set defaultLanguage to a supported code |