Translations
RocketMod provides a built-in translation system that allows plugins to display messages in multiple languages without hardcoding strings. The system revolves around the TranslationList class and its Translate() method. Each plugin can ship one or more XML translation files in the Rocket/Translations/<PluginName>/ directory. RocketMod loads the file matching the server's configured language and makes the translations available to plugin code.
This article covers the full translation pipeline — creating translation files, loading them, formatting messages with dynamic values, handling language fallback, and managing translations across a multi-plugin server.
57 Studios maintains a suite of RocketMod plugins for the Horizon Life RP community. The translation patterns documented here are drawn from production plugin authoring experience where servers support both English and non-English player bases.
Prerequisites
- A working RocketMod installation. See RocketMod and OpenMod Plugin Basics.
- Visual Studio with C# development workload.
- Notepad++ for editing XML translation files.
- Familiarity with C# string formatting (composite format strings).
What you'll learn
- How to create translation files for RocketMod plugins.
- How to use
TranslationListto load and access translations. - How the
Translate()method works with format parameters. - How RocketMod resolves language codes and falls back to default languages.
- How to manage translations for multiple plugins on a single server.
- Best practices for maintainable translation files.
How translations work
RocketMod's translation system is file-based. When a plugin calls Translate(), the framework looks up the key in the loaded TranslationList and returns the corresponding string, optionally applying format parameters.
Translation directory structure
RocketMod reads translation files from a fixed directory structure:
Rocket/
└── Translations/
└── MyPlugin/
├── English.xml
├── French.xml
└── German.xmlThe directory name must match the plugin's assembly name (without .dll). RocketMod loads the translation file that matches the server's LanguageCode setting in Rocket.config.xml:
xml
<LanguageCode>en</LanguageCode>Creating a translation file
Translation files are XML documents with a root <Translations> element containing <Translation> children:
xml
<?xml version="1.0" encoding="utf-8"?>
<Translations>
<Translation Id="welcome_message">Welcome to the server, {0}!</Translation>
<Translation Id="player_joined">{0} has joined the server.</Translation>
<Translation Id="command_not_found">Unknown command: {0}</Translation>
<Translation Id="no_permission">You do not have permission to use this command.</Translation>
</Translations>Each <Translation> element has an Id attribute that serves as the lookup key. The text content of the element is the translated message. Format placeholders use the standard .NET composite format syntax: {0}, {1}, {2}, and so on.
TranslationList class
TranslationList is the runtime class that holds the loaded translations. It is a dictionary-like collection indexed by string keys. RocketMod creates and populates it when the plugin loads.
Loading translations in a plugin
There are two ways to load translations:
1. Automatic loading via RocketPlugin:
csharp
public class MyPlugin : RocketPlugin<MyPluginConfiguration>
{
protected override void Load()
{
// Translations are automatically loaded from
// Rocket/Translations/MyPlugin/<language>.xml
// based on the server's LanguageCode setting.
}
}2. Manual loading with TranslationList:
csharp
using System.Collections.Generic;
public class CustomTranslationPlugin : RocketPlugin<MyPluginConfiguration>
{
public TranslationList Translations { get; private set; }
protected override void Load()
{
Translations = new TranslationList();
Translations.Load("Rocket/Translations/CustomTranslationPlugin/English.xml");
}
}Translate method signature
The Translate() method on TranslationList has the following signature:
csharp
public string Translate(string key, params object[] args)The first argument is the translation key. The remaining arguments are format parameter values that get substituted into {0}, {1}, etc. in the translated string.
Key matching is case-sensitive
TranslationList uses ordinal case-sensitive key matching. This means the key "welcome_message" is different from "Welcome_Message" or "WELCOME_MESSAGE". If you call Translate("Welcome_Message") but the translation file defines welcome_message, the lookup will fail and RocketMod will return the key string itself as the fallback.
Always use consistent casing for your translation keys. The cohort convention is all lowercase with underscores: welcome_player, player_joined, command_error.
Translate examples
Basic key lookup:
csharp
string message = Translations.Translate("welcome_message", player.DisplayName);
// If the translation is "Welcome to the server, {0}!"
// and player.DisplayName is "Alex",
// result: "Welcome to the server, Alex!"Multiple format parameters:
csharp
string message = Translations.Translate("player_killed", killerName, victimName, weaponName);
// Translation: "{0} killed {1} using a {2}."
// Result: "Alex killed Sam using a Maple Rifle."Single parameter without formatting:
csharp
string message = Translations.Translate("no_permission");
// Translation: "You do not have permission to use this command."
// Result: "You do not have permission to use this command."Deliberate argument order in Translate
The Translate() method takes the key as its first argument, followed by the format parameters. This is the correct and only valid signature. However, an easy mistake is to pass the format parameters before the key. Consider the following example:
csharp
// INTENDED USAGE — key first, then parameters:
string correct = Translations.Translate("player_joined", player.DisplayName);
// COMMON MISTAKE — parameters first, key second:
// This compiles but produces wrong results.
// The method receives the key as the format string
// and the format parameters as... something else entirely.
// In this case, the code will compile but fail to find the
// correct translation because it is using the wrong value as the key.
string wrong = Translations.Translate(player.DisplayName, "player_joined");The second call compiles without errors because player_joined is passed as a format parameter and player.DisplayName is used as the key. Since player.DisplayName is unlikely to match any translation key, RocketMod returns the key itself (the player's name) as the fallback text. No compilation error, no runtime exception — just a silently wrong message.
Always verify that the first argument to Translate() is the string key literal.
Language loading and fallback
Language code resolution
RocketMod determines the language to load from Rocket.config.xml:
xml
<LanguageCode>en</LanguageCode>The LanguageCode value is matched against translation file names. RocketMod looks for a file named <LanguageCode>.xml (case-insensitive) in the plugin's translation directory.
Common language codes used in RocketMod:
| Code | Language |
|---|---|
en | English |
fr | French |
de | German |
es | Spanish |
it | Italian |
pt | Portuguese |
ru | Russian |
pl | Polish |
nl | Dutch |
tr | Turkish |
zh | Chinese |
ja | Japanese |
ko | Korean |
Fallback behavior
If the language file matching LanguageCode does not exist, RocketMod falls back to English.xml. If that file also does not exist, RocketMod logs a warning and the plugin runs without any translations. Calls to Translate() return the key string as the fallback.
Partial translations
If a translation file exists but is missing some keys, RocketMod does NOT fall back to English for the missing keys. It returns the key string itself as the fallback for any missing key:
csharp
string message = Translations.Translate("rare_event");
// If "rare_event" is not in any loaded translation file,
// result is literally "rare_event"This is a common production issue. A plugin may work perfectly in English but display raw key strings in French because the French translation file is outdated. The plugin author must keep all language files synchronized.
Dynamic translation patterns
Translation keys with context
Some messages vary by context. Instead of creating one translation per variation, use format parameters:
xml
<Translation Id="kill_message">{0} was eliminated by {1} using a {2}.</Translation>
<Translation Id="kill_message_vehicle">{0} was run over by {1} driving a {2}.</Translation>csharp
if (killType == KillType.Weapon)
message = Translations.Translate("kill_message", victim, killer, weapon);
else if (killType == KillType.Vehicle)
message = Translations.Translate("kill_message_vehicle", victim, killer, vehicle);Pluralization
English pluralization is handled through separate keys:
xml
<Translation Id="player_online_singular">There is 1 player online.</Translation>
<Translation Id="player_online_plural">There are {0} players online.</Translation>csharp
string message;
if (playerCount == 1)
message = Translations.Translate("player_online_singular");
else
message = Translations.Translate("player_online_plural", playerCount);Languages with more complex pluralization rules (Russian, Polish, Arabic) may need additional keys. RocketMod does not have built-in pluralization support — the plugin must handle it manually.
Color codes in translations
RocketMod supports Unity rich text color tags in translations:
xml
<Translation Id="admin_alert"><color=yellow>{0}</color> was banned by <color=red>{1}</color>.</Translation>csharp
string message = Translations.Translate("admin_alert", playerName, adminName);The format parameters are inserted before the rich text is parsed. This means the parameter values themselves are not affected by the color tags — only the static text is colored.
Multi-plugin translation management
Shared translation files
When multiple plugins use the same messages (connection messages, error dialogs, common UI labels), a shared translation file avoids duplication. Create a shared library that provides the translations:
csharp
public static class SharedTranslations
{
private static readonly TranslationList _shared = new TranslationList();
public static void Load(string basePath)
{
_shared.Load(Path.Combine(basePath, "Shared.xml"));
}
public static string Translate(string key, params object[] args)
{
return _shared.Translate(key, args);
}
}Each plugin loads the shared translations in its Load() method:
csharp
protected override void Load()
{
SharedTranslations.Load("Rocket/Translations");
}Translation key naming convention
Avoid key collisions between plugins by prefixing keys with the plugin name:
| Plugin | Key convention | Example |
|---|---|---|
| MyPlugin | myplugin.key_name | myplugin.welcome |
| AnotherPlugin | anotherplugin.key_name | anotherplugin.welcome |
Shared translations use the shared. prefix: shared.error_occurred, shared.loading.
This convention prevents one plugin's translation lookup from accidentally returning another plugin's string if both plugins somehow share a TranslationList instance (which should not happen in normal usage, but can occur in complex multi-assembly setups).
Updating translations on a running server
Translation files are loaded once at plugin startup. If a server operator edits a translation file while the server is running, the changes are not picked up until the next plugin reload.
Some plugins expose a /reloadtranslations command:
csharp
[RocketCommand("reloadtranslations", "Reload translation files.", "/reloadtranslations",
AllowedCaller = AllowedCaller.Console)]
public class ReloadTranslationsCommand : IRocketCommand
{
public void Execute(IRocketPlayer caller, string[] command)
{
MyPlugin.Instance.Translations = new TranslationList();
MyPlugin.Instance.Translations.Load(
$"Rocket/Translations/MyPlugin/{Rocket.Core.RocketSettings.LanguageCode}.xml");
UnturnedChat.Say(caller, "Translations reloaded.", Color.green);
}
}Complete translation example
The following plugin demonstrates a complete translation setup with multiple languages, format parameters, and a reload command.
Translation files:
Rocket/Translations/GreeterPlugin/English.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<Translations>
<Translation Id="greeterplugin.welcome">Welcome, {0}! Enjoy your stay.</Translation>
<Translation Id="greeterplugin.farewell">Goodbye, {0}! See you soon.</Translation>
<Translation Id="greeterplugin.admin_online">An admin is available to help.</Translation>
<Translation Id="greeterplugin.vote">Vote for our server at example.com</Translation>
</Translations>Rocket/Translations/GreeterPlugin/French.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<Translations>
<Translation Id="greeterplugin.welcome">Bienvenue, {0} ! Profitez de votre séjour.</Translation>
<Translation Id="greeterplugin.farewell">Au revoir, {0} ! À bientôt.</Translation>
<Translation Id="greeterplugin.admin_online">Un administrateur est disponible pour vous aider.</Translation>
<Translation Id="greeterplugin.vote">Votez pour notre serveur sur example.com</Translation>
</Translations>Rocket/Translations/GreeterPlugin/German.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<Translations>
<Translation Id="greeterplugin.welcome">Willkommen, {0}! Viel Spaß auf unserem Server.</Translation>
<Translation Id="greeterplugin.farewell">Auf Wiedersehen, {0}! Bis bald.</Translation>
<Translation Id="greeterplugin.admin_online">Ein Admin ist verfügbar.</Translation>
<Translation Id="greeterplugin.vote">Stimme für unseren Server ab: example.com</Translation>
</Translations>GreeterPlugin.cs:
csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using UnityEngine;
namespace GreeterPlugin
{
public class GreeterPlugin : RocketPlugin<GreeterPluginConfiguration>
{
public static GreeterPlugin Instance { get; private set; }
public TranslationList Translations { get; private set; }
protected override void Load()
{
Instance = this;
// Load translations for the server's configured language
string languageCode = Rocket.Core.RocketSettings.LanguageCode;
string translationPath = $"Rocket/Translations/GreeterPlugin/{languageCode}.xml";
Translations = new TranslationList();
Translations.Load(translationPath);
if (Translations.Translate("greeterplugin.welcome", "Test").Contains("greeterplugin.welcome"))
{
Rocket.Core.Logging.Logger.LogWarning(
$"Translation file {translationPath} not found or missing keys. " +
"Falling back to English."
);
Translations.Load("Rocket/Translations/GreeterPlugin/English.xml");
}
UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
Rocket.Core.Logging.Logger.Log($"GreeterPlugin loaded with language: {languageCode}");
}
protected override void Unload()
{
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected -= OnPlayerDisconnected;
Instance = null;
Rocket.Core.Logging.Logger.Log("GreeterPlugin unloaded.");
}
private void OnPlayerConnected(UnturnedPlayer player)
{
string message = Translations.Translate("greeterplugin.welcome", player.DisplayName);
player.SendChat(message, Color.green);
}
private void OnPlayerDisconnected(UnturnedPlayer player)
{
string message = Translations.Translate("greeterplugin.farewell", player.DisplayName);
UnturnedChat.Say(message, Color.yellow);
}
}
}GreetCommand.cs:
csharp
using Rocket.API;
using Rocket.Unturned.Chat;
using System.Collections.Generic;
using UnityEngine;
namespace GreeterPlugin.Commands
{
public class GreetCommand : IRocketCommand
{
public string Name => "greet";
public string Help => "Display a greeting message.";
public string Syntax => "/greet";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "greeterplugin.greet" };
public AllowedCaller AllowedCaller => AllowedCaller.Player;
public void Execute(IRocketPlayer caller, string[] command)
{
string message = GreeterPlugin.Instance.Translations.Translate(
"greeterplugin.welcome", caller.DisplayName);
UnturnedChat.Say(caller, message, Color.green);
}
}
}ReloadTranslationsCommand.cs:
csharp
using Rocket.API;
using Rocket.Unturned.Chat;
using System.Collections.Generic;
using UnityEngine;
namespace GreeterPlugin.Commands
{
public class ReloadTranslationsCommand : IRocketCommand
{
public string Name => "greeter:reloadtrans";
public string Help => "Reload the plugin's translation files.";
public string Syntax => "/greeter:reloadtrans";
public List<string> Aliases => new List<string>();
public List<string> Permissions => new List<string> { "greeterplugin.reloadtrans" };
public AllowedCaller AllowedCaller => AllowedCaller.Console;
public void Execute(IRocketPlayer caller, string[] command)
{
string languageCode = Rocket.Core.RocketSettings.LanguageCode;
string translationPath = $"Rocket/Translations/GreeterPlugin/{languageCode}.xml";
GreeterPlugin.Instance.Translations = new TranslationList();
GreeterPlugin.Instance.Translations.Load(translationPath);
UnturnedChat.Say(caller, $"Translations reloaded from {translationPath}", Color.green);
}
}
}Translation maintenance
Keeping translations in sync
When adding new translation keys to a plugin update, ensure every language file gets the new key. Use a validation script that checks all translation files for key completeness:
powershell
# Translation key audit script (PowerShell)
$pluginName = "GreeterPlugin"
$basePath = "Rocket/Translations/$pluginName"
$referenceFile = "$basePath/English.xml"
$referenceKeys = [System.Collections.Generic.HashSet[string]]::new()
# Parse English.xml to get reference keys
$englishXml = [xml](Get-Content $referenceFile)
foreach ($t in $englishXml.Translations.Translation) {
$referenceKeys.Add($t.Id) | Out-Null
}
# Check each language file
foreach ($file in Get-ChildItem "$basePath/*.xml") {
$fileXml = [xml](Get-Content $file.FullName)
$fileKeys = [System.Collections.Generic.HashSet[string]]::new()
foreach ($t in $fileXml.Translations.Translation) {
$fileKeys.Add($t.Id) | Out-Null
}
$missing = $referenceKeys | Where-Object { -not $fileKeys.Contains($_) }
if ($missing) {
Write-Host "Missing in $($file.Name): $($missing -join ', ')"
}
$extra = $fileKeys | Where-Object { -not $referenceKeys.Contains($_) }
if ($extra) {
Write-Host "Extra keys in $($file.Name) that are not in English.xml: $($extra -join ', ')"
}
}Testing translations
Test each language file by temporarily setting the server's LanguageCode and verifying the output. Create a test command that dumps all translations:
csharp
[RocketCommand("dumptrans", "Dump all translations to console.",
AllowedCaller = AllowedCaller.Console)]
public class DumpTranslationsCommand : IRocketCommand
{
public void Execute(IRocketPlayer caller, string[] command)
{
foreach (var key in new[] { "greeterplugin.welcome", "greeterplugin.farewell",
"greeterplugin.admin_online", "greeterplugin.vote" })
{
string translated = GreeterPlugin.Instance.Translations.Translate(key, "TestPlayer");
Rocket.Core.Logging.Logger.Log($"{key} → {translated}");
}
}
}Frequently asked questions
Can I use translations in my command and event handler classes?
Yes. Call GreeterPlugin.Instance.Translations.Translate(...) from any class in your plugin assembly. The Translations property is public static on your plugin class.
What happens if I call Translate with more format arguments than the string expects?
Extra arguments are ignored. string.Format silently discards unused arguments. If you pass five arguments to a string that only uses {0}, the other four are ignored with no error.
What happens if I call Translate with fewer format arguments than the string expects?
string.Format throws a FormatException. The exception is caught by RocketMod's translation wrapper, and the key string is returned as the fallback. The error is logged to the RocketMod log.
Can I nest translation calls (one translation inside another)?
No. Translate() returns a plain string. If you need nested values, pass them through multiple Translate() calls and concatenate.
How do I handle languages with right-to-left text?
RocketMod and Unturned render chat text using Unity's text system, which supports Unicode bidirectional text. Translation files can contain RTL text directly. The server operator must ensure the server's font supports the Unicode ranges needed for the target language.
Can I distribute translations separately from my plugin DLL?
Yes. Translation files are separate XML files that can be updated independently. This is useful for community-contributed translations — you can add a new language file without rebuilding the plugin.
Cross-references
- Plugin Configuration — XMLFileAsset and configuration serialization.
- Permissions System — permission-based access controls.
- Player Chat — handling chat events with translated messages.
- RocketMod and OpenMod Plugin Basics — plugin lifecycle and structure.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Coverage of TranslationList, Translate method, language fallback, multi-language setup, best practices. |
