Skip to content

Localization

The texts the server sends to players come from one language per server, chosen in moongate.toml. The texts live in TOML files under data/messages/ in the server root; C# code reads them through ILocalizationService in Moongate.Server.Ultima, and Lua scripts through the localization module.

Texts that the client already has in its own localized files (cliloc numbers) do not need this service: the server sends the number and the client shows it in the player’s language. Use ILocalizationService for texts that the server writes itself.

Set the language code in the [ultima.localization] section:

[ultima.localization]
language = "ita"

The code names the file data/messages/<language>.toml and the directory data/messages/<language>/; the default is eng. The server ships these languages:

Code Language
eng English
ita Italian
ger German
fre French
spa Spanish
por Portuguese
pol Polish
cze Czech

The language applies to the whole server, in game and standalone modes. A change takes effect at the next start. See the configuration reference.

Each file is a [messages] table of number = "text":

[messages]
0 = "Questo oggetto non ha più cariche."
691 = "{0} è stato ucciso da {1}! [Terremoto]\n"
1737 = "[{0:x} {1:x} {2:x} {3:x}]"
  • Number: the message id. The same number is the same message in every language.
  • Text: a .NET composite format. {0}, {1}, … are the values the code fills in, in order. {0,6} pads a value to 6 characters and {0:x} writes a number in hex. Write {{ and }} for a literal brace.

The files come from the UOX3 dictionaries (data/dictionaries/dictionary.*), with the UOX3 printf placeholders (%s, %i, %d) turned into {0}, {1}, …

English is the reference and must always exist. The server loads it first, then replaces each text with the one in the chosen language. A message missing from the chosen language stays in English, so a partial translation still works. The log reports how many messages fell back:

Found 5648 messages in ita, 3 of them in English

MessagesLoader stops the server at startup when:

  • English or the chosen language has neither its file nor a toml file in its directory;
  • English has no messages;
  • a key is not a number;
  • a text is empty or is not a valid composite format, such as a lone {;
  • a text needs more than 16 values;
  • a translation needs more values than the English text, which would fail when the code passes the English number of values;
  • a translation has a number that English does not have;
  • the same number is in two files of one language, or twice in one file (1 and 01).

Besides data/messages/<language>.toml, the server reads every *.toml file in the directory data/messages/<language>/ and merges them all into one set of messages:

data/messages/
eng.toml # shipped: the standard texts, from UOX3
eng/
moongate.toml # shipped: Moongate's own texts, numbers from 30000
shard.toml # your own texts
quests.toml
ita.toml
ita/
moongate.toml
shard.toml

The server ships two files per language: <language>.toml with the standard texts and <language>/moongate.toml with Moongate’s own messages.

  • The file and the directory can both exist, or only one of them.
  • Every file has the same format: a [messages] table of number = "text".
  • The files of the directory are read in name order. Subdirectories and files that do not end in .toml are ignored.
  • Write the directory name in lower case: on Linux ENG/shard.toml is not read. The case of the file name and of the .toml extension does not matter.
  • A number can be in one file only. The same number in two files of one language stops the server and the error names both files.

Keep your shard’s texts in files of your own in data/messages/eng/ so that an update of the shipped eng.toml and eng/moongate.toml does not overwrite them.

Resolve ILocalizationService from the container, for example through a constructor:

public class BoatHandler
{
private readonly ILocalizationService _localization;
public BoatHandler(ILocalizationService localization)
{
_localization = localization;
}
public string BoardMessage()
{
return _localization.Get(1); // "Si sale a bordo della barca."
}
}
Member What it does
Language The configured code in lower case, such as ita.
Get(id, values...) The message with {0}, {1}, … replaced by values, and {{, }} turned into single braces. Throws KeyNotFoundException for an unknown id and FormatException when fewer values are given than the text needs.
TryGetText(id, out text) The text as written in the file, without filling in values; false for an unknown id.
_localization.Get(691, "Bob", "un drago"); // "Bob è stato ucciso da un drago! [Terremoto]\n"
if (_localization.TryGetText(691, out var text))
{
// text is "{0} è stato ucciso da {1}! [Terremoto]\n"
}

Values are formatted with the invariant culture, so numbers do not change with the host’s regional settings.

The service is registered by the Ultima plugin in game and standalone modes. It reads the messages the first time a text is asked for, after IDataLoaderService has run the loaders at startup; asking earlier fails because the messages are not loaded yet.

Scripts use the localization module, which calls ILocalizationService:

local text = localization.get(691, 'Bob', 'un drago')
-- "Bob è stato ucciso da un drago! [Terremoto]\n"
local raw = localization.text(691) -- "{0} è stato ucciso da {1}! [Terremoto]\n"
local missing = localization.text(99999) -- nil
log.info('Server language: {Language}', localization.language()) -- "ita"
Function What it does
localization.get(id, ...) The message with {0}, {1}, … replaced by the extra arguments. A whole Lua number is passed as an integer, so {0:x} works on it; nil is written as nil. An unknown id or too few arguments raise a Lua error, which a script can catch with pcall.
localization.text(id) The text as written in the file, or nil for an unknown id. Use it to check whether a message exists.
localization.language() The server language code, such as ita.
local ok, err = pcall(localization.get, 99999)
-- ok is false, err contains "No message has id 99999."

The Ultima plugin registers the module in game and standalone modes, with the other data services. definitions.lua declares it for editor completion, with localization.text returning string?.

The quests have their own file, data/messages/<language>/quests.toml, with numbers from 40000, so that whoever writes a quest never touches Moongate’s own messages. A text of a quest that starts with # is such a number (see Quests). It holds the lines of the quest engine and its gumps (40001–40045) and the texts of the example quests (40100–40114), translated in every shipped language.

The quests converted from ModernUO have theirs too, data/messages/<language>/ml_quests.toml, numbered as the client numbers its own texts (above one million), with the texts of the client’s cliloc as the converter reads them; German, Spanish and French hold only what the client has in those languages and the rest falls back to English.

Numbers from 30000 are Moongate’s, not UOX3’s, all translated in every shipped language. They live in data/messages/<language>/moongate.toml, apart from the standard texts:

Id Text Used by
30000–30004 Common, Uncommon, Rare, Epic, Legendary Tooltip rarity; an item shows it only above Common (30000 is kept for scripts and tools)
30005 [Cursed] Tooltip loot type
30006, 30007 Weight: 1 stone, Weight: {0} stones Tooltip weight
30008–30038, 30050–30052, most of 30055–30112, 30115–30120, 30122 and 30126 Target canceled., Unknown command: {0}, Usage: {0}, The world has been saved in {0} seconds., {0} now has {1} fame., … Command replies and broadcasts (CommandMessages)
30039–30049, 30053–30054, the rest up to 30113, 30121 and 30127 One per built-in command Command descriptions in help
30114 This moongate does not seem to go anywhere. Texts of the item scripts, read with localization.get
30123, 30124 You are hungry., You are starving: your wounds will not heal until you eat. What a player reads as its hunger drops
30128, 30129 You are thirsty., You are parched: your stamina will not come back until you drink. What a player reads as its thirst drops
30130–30132 You are simply too full to drink any more!, It is empty., You drink, and feel less thirsty. The texts of scripts/items/drink.lua
30133 You are overloaded: you carry {0} stones of {1}. What a player reads when it puts something down while carrying more than it may
30134–30137 You have entered {0}., You have left {0}., You are now under the protection of the guards of {0}., You have left the protection of the guards of {0}. What a player reads when it walks into or out of a named place, and when the protection of its guards changes
30138 Thou wilt regret thine actions, swine! What a guard says when it comes for a criminal
30148, 30149 You have been jailed for {0} days: {1}, Reason: {0} What a prisoner reads when a reason was given, and the reason on its release note
30151 the remains of {0} The name of the corpse an NPC leaves
30152 to 30154 Kills the NPC you target…, {0} is dead., {0} cannot die. The kill command
30164 to 30167 Raises the NPC whose corpse you target…, {0} is back., That is not a corpse., That corpse cannot be raised. The resurrect command
30155 to 30157 You must wait {0} seconds before posting again., That message is not yours., The board is busy: post again in a moment. What a player reads at a bulletin board
30168 You may not use skills in jail. What a prisoner reads when it uses a skill
30150 No character is named {0}. What jail <name> answers when no player has the name
30160 to 30163 Shows the version the server runs…, Moongate {0} “{1}” ({2}), built {3}., Shows how long the server has been running…, Up for {0}, since {1}. The version and uptime commands
30181 to 30184 No document {0} in templates/books., Document {0} is in your backpack., … The book command replies and help description
30192 to 30204 You cannot ask to be moved while you are in jail., You cannot ask to be moved while you are fighting., You already asked to be moved: stand still., You can ask to be moved again in {0} minutes., Stand still for {0} seconds and you will be taken to {1}., You moved: you stay where you are., You have been taken to {0}., There is no city to take you to., the rules of the server, and the four labels of the gump (Help, I am stuck, Useful commands, Server rules) What a player reads from the help gump: why “I am stuck” is refused, the wait, the move, the rules text and the labels of the menu
30205 to 30220 Call a game master, What is it about?, Question, Bug, Suggestion, Harassment, the typing prompt, the answers to a request, the alert to the staff, the answer line, the waiting count, and the description of the pages command What a player and a game master read from the help queue
30224 to 30229 The server will shut down in {0} minutes., the description of the event command and its replies What a player and an administrator read from the schedule
30230 to 30237 Oooooh, aren’t you cute!, TRICK!, You receive some candy., the announcements of the season What a shopkeeper says and a player reads in trick or treat
30238 to 30240 Merry Christmas!, The Christmas season is over., Happy Holidays! Gift items have been placed in your backpack. The announcements and the gift of Christmas
30297 to 30323 You invite {0} to join your party., {0} joined the party., the refusals of an invitation, the new leader, the party disbanded What a player reads from the party
30324 This character is {0} day(s) old. The footer of the own profile
30325 to 30328 You are not in a guild., Your guild has no allies., You cannot use the guild chat in jail., the answer to the chat button What a player reads from the guild and alliance chats
30329 That special move is not available. The refusal of a weapon special move no script does yet
30185, 30186 Opens the gump of the game master’s tools…, The gmtools gump is missing: templates/gumps/gmtools.xml. The gmtools command

The header of the command texts in eng/moongate.toml lists the ids of both sets.

Tooltips also use UOX3’s 9055 “[Blessed]”, and .account its 555 “An account by that name already exists!”. Polish and Czech write the plural weight abbreviated (“kam.”), since one text with {0} cannot follow their plural forms.

  1. Add the message with a number that is not used yet: a text of Moongate’s code to data/messages/eng/moongate.toml, a text of your shard to a file of your own in data/messages/eng/.
  2. Add the translation with the same number to the other files. A language without it shows the English text.
  3. Use the same values, in the same order, in every language.
  4. Run dotnet test --filter RepositoryDataFiles: it loads every shipped language with the real loader.

To add a language, copy eng.toml to data/messages/<code>.toml and eng/moongate.toml to data/messages/<code>/moongate.toml, translate the texts and set language = "<code>". The code may contain only ASCII letters.

Made with ❤️ by Tom

Buy me a coffee