Server Admin

Scuffed Translation

Requirements, the translation pipeline and backends (Gemini, keyless Google, LibreTranslate), staff commands, every config key with its default, data files and the API for other mods.

Gameplay GuideView the players side of Scuffed Translation

Last updated

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

ItemRequirement
Hytale server>=0.7.0-pre.0 <0.8.0
Required pluginHytale:AssetModule (always present on a server)
Optional pluginsNone declared. EndlessLink is detected at runtime (see Integrations)
NetworkOutbound 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.

  1. 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.
  2. Shorthand. Gamer abbreviations (wru, brb, lfg, idk and others) are expanded to full words.
  3. Phrasebook. About 40 common phrases are answered instantly in English, Spanish, Portuguese and French, with no network call (usePhrasebook).
  4. Cache. An in-memory LRU cache, saved to disk, holds every language translated for a message. A phrase is only translated once.
  5. Masking. Text in double quotes and known item names are swapped for placeholders before the backend sees them and restored afterwards.
  6. Backend call. One batched call covers all missing languages. Identical messages that arrive at the same moment share one call.
  7. 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 valueProviderNotes
gemini (default)Google Gemini, model from geminiModelOne 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 endpointsNo 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 libreUrlOne request per language, sent in parallel. No outside quota

Routing rules:

  • Messages longer than largeMessageWords words skip the primary backend and go to google_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 quotaBackoffMinutes and fallbackBackend takes 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 to quotaBackoffMinutes, and resets on the first success. Messages in that window are delivered untranslated.
  • Requests against the primary are metered by a token bucket. For gemini the bucket is requestsPerMinute. For google_free it is the larger of requestsPerMinute and 60, and for libretranslate the larger of requestsPerMinute and 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.

CommandDescription
/translate global onEnable translation server-wide again. Players with it switched off stay off
/translate global offDisable 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 reloadRe-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.

NodeEffect
scuffedtranslation.adminGranted to a user, it counts as operator for global, test and reload
Group hytale:AdminMembers 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.

KeyDefaultDescription
backendgeminigemini, google_free or libretranslate
geminiApiKeyemptyGoogle AI Studio API key for Gemini. If empty, the mod reads a geminiApiKey from mods/AIPrefab/config.json when that file exists
geminiModelgemini-2.5-flash-liteGemini model name
libreUrlhttp://localhost:5000Base URL of your LibreTranslate server
libreApiKeyemptyLibreTranslate API key, if your instance requires one
fallbackBackendgoogle_freeBackend used when the primary is over quota or failing. none disables the fallback
quotaBackoffMinutes10How long the primary is skipped after a quota error (minimum 1)
eventPriorityLATEPriority of the chat listener. Use an engine priority name; an invalid value falls back to LATE. Needs a restart
builtinChatListenerautoauto: active, but steps aside when EndlessLink drives chat. on: always active. off: never active (translation only through the API)
useContextfalseSend the last few chat lines to the backend to resolve pronouns. Bypasses the cache and costs more requests
contextLines3Number of recent lines used when useContext is on (minimum 1)
skipIfAlreadyInTargetLanguagetrueSkip readers when the text already looks like their language (guess works for English, Spanish, Portuguese and French only)
autoDetectClientLanguagetrueTranslate into each player's game client language. Off means translation is opt-in through /translate set
defaultLanguageenLanguage for players whose client language is unknown or unsupported. Empty means those players are not translated. Only used when autoDetectClientLanguage is on
showSourceTagtrueAppend a faint (code) tag to translated lines
usePhrasebooktrueUse the built-in phrasebook before any backend call
protectItemNamestrueKeep item and glossary names out of translation
requestsPerMinute14Request budget for the primary backend (minimum 1)
cacheSize4000Maximum cached messages (minimum 64)
maxMessageWords100Longer messages are not translated (minimum 1)
largeMessageWords25Longer messages go to google_free (minimum 1)
perPlayerMessagesPerMinute15Spam guard: translated messages per sender per minute, with a burst of the same size. Extra messages are delivered untranslated (minimum 1)
requestTimeoutSeconds8Timeout for each backend HTTP call (minimum 2)
translationThreads4Size of the background pool, 1 to 16. Needs a restart
debugfalseLog 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.
  • largeMessageWords keeps long messages off the metered backend.
  • maxMessageWords drops very long messages entirely.
  • useContext raises request counts; leave it off unless you need it.

Data & storage

All files are in mods/ScuffedTranslation/ (relative to the server's working directory).

FileContents
config.jsonSettings, created on first start
prefs.jsonPer-player choices keyed by UUID (lang for /translate set, enabled for `/translate on
cache.jsonPersistent translation cache. Written after every 200 new entries and at shutdown, and loaded on start
glossary.jsonOptional, 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

PluginTypeUse
EndlessLinkDetected at runtimeWhen 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
AIPrefabOptional config sourceIts 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.

MethodPurpose
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

SymptomCheck
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 notAnother chat mod owns delivery. Check the "Chat delivered by" line in /translate status
Gemini selected but the log says it is using google_freegeminiApiKey is empty and no AIPrefab key was found
Messages arrive untranslated in burstsThe 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 translatedSet defaultLanguage to a supported code