Add translations
Problem. Your building's name shows up as my-mod.cutter.title in-game.
Solution. Everything player-facing is a translation key, not a string. Ship a
translations.json next to your DLL and resolve keys with .T().
The file
{
"en-US":
{
"building-variant.cutter-diagonal.title": "Diagonal Destroyer",
"building-variant.cutter-diagonal.description": "<gl>Destroys</gl> the <gl>Even Parts</gl> of a shape."
},
"pt-BR":
{
"building-variant.cutter-diagonal.title": "Eliminadora de diagonais"
}
}
Top level is language code, then flat key → text. Languages may be partial — the
pt-BR block above translates only the title, and the description falls back to
en-US. Always provide a complete en-US block as the fallback.
Copy it to the output on every build:
<None Update="translations.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Using a key
using Core.Localization;
IText title = "my-mod.cutter.title".T();
.T() turns a key into an IText, which is what every builder wants:
BuildingGroup.Create(groupId)
.WithTitle("my-mod.cutter.title".T())
.WithDescription("my-mod.cutter.description".T())
IText is resolved lazily at display time, so it follows the player's language setting
and updates if they change it. Never pass a literal string where an IText is
expected — you would hard-code English.
Language codes
The codes the game recognises, from BuiltinLanguagesExtensions.Code():
| Language | Code | Language | Code |
|---|---|---|---|
| English | en-US |
Polish | pl |
| German | de-DE |
Brazilian Portuguese | pt-BR |
| Spanish | es-ES |
Russian | ru |
| French | fr-FR |
Thai | th |
| Japanese | ja |
Turkish | tr |
| Korean | ko |
Traditional Chinese | zh-Hant |
| Simplified Chinese | zh-Hans |
Note the inconsistency — some are language-region (en-US, pt-BR), some are bare
(ja, ru). Use exactly what the table says.
Naming keys
Vanilla uses dotted, kebab-cased paths:
building-variant.cutter-diagonal.title. Two workable conventions for a mod:
- Mimic vanilla (
building-variant.my-cutter.title) — fits in, but risks colliding with a future vanilla key. - Namespace with your mod (
my-mod.cutter.title) — collision-free, and obvious in a translator's diff which keys are yours. Preferred.
Some keys are not yours to name. Keybinding titles are looked up as
keybinding.<id> and keybinding.<id>.description from the binding's own id, so the
key follows whatever id you registered.
Markup
Descriptions accept the game's inline tags — <gl>…</gl> in the sample highlights a
term. Copy the tags vanilla uses for the same kind of emphasis rather than inventing
formatting.
Placeholders: bind values with RawText, never .T()
A key with a self-closing placeholder is filled in with Bind:
"my-mod.store.layer": "Layer <layer/>"
"my-mod.store.layer".T().Bind("layer", new RawText(n.ToString()))
Bind takes an IText, and the obvious-looking way to make one is .T() — which is
wrong. .T() builds a LazyLocalizedText: it treats the string as a translation id.
So n.ToString().T() asks the resolver for a key named "1", the lookup fails, and
LazyLocalizedText.Build emits its miss marker:
sb.Append("?");
sb.Append(Id.Id); // renders "?1"
The symptom is a stray ? in front of the value — Layer ?1. Use RawText, the
plain-text IText, for anything that is already literal: numbers, names, values read
from simulation state.
A ? prefix anywhere in the UI means the same thing — something was treated as a
translation id and not found.
Gotchas
- A missing key renders as the raw key. That is your signal the file did not copy, the language block is missing, or the key is misspelled — check the output folder first.
- Keys are flat strings, not nested objects. Dots are part of the name, not structure.
translations.jsonis loaded by the game's mod loader, not by ShapezShifter, so the filename and location are fixed by convention: next to your DLL.