Translations & Localization
Hytale has a built-in translation and localization (i18n) system that lets mods provide text in multiple languages. You define translations in .lang files, reference them with Message.translation(), and format parameters with Message.param(). Translation keys are used throughout the API — command descriptions, UI text, item names, and player messages all support translated strings.
.lang File Format
Section titled “.lang File Format”Translations are stored in .lang files with a simple key = value format:
# comments start with #heal.desc = Heal a playerheal.success = Healed {name} for {amount} HP
# values can be quotedwelcome = "Welcome to the server!"
# multiline values use \ at end of linerules = These are the server rules. \Please read them carefully.Escape sequences: \n (newline), \t (tab). Duplicate keys and empty values within a file cause a parse error.
Directory Structure
Section titled “Directory Structure”Place .lang files under Server/Languages/<locale>/ in your pack root. No code registration is needed — I18nModule automatically discovers and loads all .lang files from every asset pack on LoadAssetEvent, which fires after all mods finish setup() but before start(). If you need to generate files programmatically, see Programmatic Translations.
Set IncludesAssetPack: true in your manifest. Place .lang files in src/main/resources/ so they end up at the JAR root — the server opens the JAR as a ZIP filesystem.
src/main/resources/├── manifest.json└── Server/ └── Languages/ ├── en-US/ │ ├── commands.lang │ └── items.lang └── fr-FR/ ├── commands.lang └── items.langPlace files directly in your pack folder under mods/:
mods/MyMod/├── manifest.json└── Server/ └── Languages/ ├── en-US/ │ ├── commands.lang │ └── items.lang └── fr-FR/ ├── commands.lang └── items.langEach subdirectory of Languages/ is a locale code (e.g. en-US, fr-FR, pt-BR). The default language is en-US.
Key Prefixing
Section titled “Key Prefixing”The file path becomes a prefix for all keys inside it. For a file at Server/Languages/en-US/commands.lang containing heal.desc = Heal a player, the full translation key is:
commands.heal.descFor nested directories like Server/Languages/en-US/ui/menus.lang containing shop.title = Shop, the key becomes:
ui.menus.shop.titleThe prefix is: directory path (relative to locale root, with / replaced by .) + filename without .lang.
Using Translations
Section titled “Using Translations”Message API
Section titled “Message API”Use Message.translation() to send translated text. The key is resolved client-side based on the player’s language.
// translated message - client resolves the keyplayerRef.sendMessage(Message.translation("mymod.healed.success"));
// raw text - sent as-is, no translationplayerRef.sendMessage(Message.raw("Debug: something happened"));Use Message.raw() only for debug output or text that genuinely shouldn’t be translated. For anything player-facing, use Message.translation().
| Factory | Description |
|---|---|
Message.translation(key) | Translated text, resolved client-side |
Message.raw(text) | Literal text, sent as-is |
Message.empty() | Blank message, used as an accumulator with .insert() |
Message.join(msg, ...) | Concatenates multiple messages |
Message.parse(json) | Decodes a JSON-formatted message (used by /say and /notify for structured input) |
Parameters
Section titled “Parameters”Translation values support parameter substitution with {paramName}:
mymod.healed.success = Healed {name} for {amount} HPplayerRef.sendMessage(Message.translation("mymod.healed.success") .param("name", targetName) .param("amount", healAmount));param() accepts several value types directly — no need to convert to String:
| Value Type | Example |
|---|---|
String | .param("name", playerName) |
int | .param("count", 5) |
long | .param("id", entityId) |
float | .param("speed", 1.5f) |
double | .param("x", position.getX()) |
boolean | .param("enabled", true) |
Message | .param("target", Message.translation("...")) |
Formatting
Section titled “Formatting”Parameters support format specifiers:
| Syntax | Description |
|---|---|
{name} | Simple substitution |
{name, upper} | Uppercase |
{name, lower} | Lowercase |
{count, number} | Number formatting |
{count, number, integer} | Integer formatting |
{count, number, decimal} | Decimal formatting |
{count, plural, ...} | ICU plural rules |
Literal braces use {{ and }}.
Plural Rules
Section titled “Plural Rules”The plural format uses ICU syntax to select text based on a numeric value. The # symbol is replaced with the formatted number by the client:
mymod.items.count = You have {count, plural, one {# item} other {# items}}mymod.mail.unread = {count, plural, one {# unread message} other {# unread messages}}mymod.lives.remaining = {lives, plural, one {# life remaining} other {# lives remaining}}Results for mymod.items.count (English):
count=1-> “You have 1 item”count=5-> “You have 5 items”
Languages with more plural forms use additional categories. Polish example:
mymod.items.count = Masz {count, plural, one {# przedmiot} few {# przedmioty} many {# przedmiotów}}count=1-> “Masz 1 przedmiot”count=3-> “Masz 3 przedmioty”count=5-> “Masz 5 przedmiotów”
Each language defines its own plural categories. English has two (one and other), but other languages have more:
| Language | Categories | Notes |
|---|---|---|
| English, German, Spanish, Turkish, Italian, Dutch, Danish, Finnish, Norwegian, Portuguese | one, other | 1 is one, everything else other |
| French, Portuguese (BR) | one, other | 0 and 1 are one |
| Russian, Ukrainian, Polish | one, few, many | e.g. 1 przedmiot, 2 przedmioty, 5 przedmiotów |
| Chinese, Japanese, Korean | other | no plural distinction |
Message Formatting
Section titled “Message Formatting”For the full Message API reference — styling (bold, italic, colors, links), composition (insert, join), mutability, and delivery — see Messages.
Where Translation Keys Are Used
Section titled “Where Translation Keys Are Used”Command Descriptions
Section titled “Command Descriptions”Command constructors take a translation key, not a raw string:
public class HealCommand extends CommandBase { public HealCommand() { // "mymod.commands.heal.desc" is a translation key super("heal", "mymod.commands.heal.desc"); }}The server resolves this key when searching commands. If the key doesn’t exist in any .lang file, I18nModule.getMessage() returns null and the command becomes unsearchable by description in the command list UI. The command still works and is findable by name, but description-based search silently fails.
See Creating Commands for the full command API.
Argument Descriptions
Section titled “Argument Descriptions”Command argument descriptions are also translation keys:
this.nameArg = this.withRequiredArg("name", "mymod.commands.heal.name.desc", ArgTypes.STRING);UI Text
Section titled “UI Text”UI components use LocalizableString which can be either a raw string or a translation reference in JSON assets:
{ "MessageId": "mymod.ui.shop.title", "MessageParams": { "playerName": "..." }}Item Names
Section titled “Item Names”Items use convention-based keys derived from their asset ID:
- Name:
server.items.{assetId}.name - Description:
server.items.{assetId}.description
Language Fallback
Section titled “Language Fallback”If a key is missing in a non-English locale, the system falls back to a configured fallback language (ultimately en-US). Configure fallbacks in Server/Languages/fallback.lang:
fr-FR=en-USde-DE=en-USpt-BR=pt-PTHot Reload
Section titled “Hot Reload”Translation files support hot reload during development. When you modify a .lang file, the server detects the change and pushes updated translations to connected clients in real-time via UpdateTranslations packets. No server restart required.
Server-Side Resolution
Section titled “Server-Side Resolution”While Message.translation() is resolved client-side, the server also resolves keys in specific cases:
- Console output: Messages displayed in the server console use
I18nModule.get().getMessage("en-US", key) - Command search: The command list UI searches resolved descriptions server-side
Programmatic Translations
Section titled “Programmatic Translations”If your mod needs to generate .lang files at runtime (e.g. unpacking from a resource, generating from data), write them during setup(). Translation loading happens on LoadAssetEvent, which fires after all mods finish setup() but before start() — so files written in setup() are picked up automatically.
Some built-in translations skip .lang files entirely and inject directly into the in-memory en-US map via the private addDefaultMessages() method (e.g. crafting bench category names, fieldcraft category names). Mods cannot use this mechanism — write .lang files instead.
/lang gen
Section titled “/lang gen”The /lang gen command lets mods scaffold .lang files from game data. It fires GenerateDefaultLanguageEvent — mods listen to this event, loop their registries, and contribute key-value pairs:
@Overrideprotected void setup() { this.getEventRegistry().registerGlobal( GenerateDefaultLanguageEvent.class, this::onGenerateLanguage);}
private void onGenerateLanguage(GenerateDefaultLanguageEvent event) { TranslationMap map = new TranslationMap(); for (MyItem item : myItemRegistry) { map.put("mymod.items." + item.getId() + ".name", item.getDefaultName()); map.put("mymod.items." + item.getId() + ".desc", item.getDefaultDesc()); } event.putTranslationFile("mymod_items", map);}Running /lang gen writes the result to <base asset pack>/Server/Languages/en-US/mymod_items.lang. The base asset pack is the first pack loaded (via --assets or the first directory in mods/). It must be a mutable directory (not a .zip or official asset bundle). All mods’ generated keys are written to this single location.
Existing values on disk are preserved — only missing keys are filled in. Use --clean to remove keys that are no longer contributed by any listener.