Localization
Localization means your game speaks the player’s language: “Play” for English players, “Jouer” for French ones, “Spielen” for German ones. You write every piece of text once per language in a translation table, and put keys in your scenes and scripts instead of the text itself. The game shows each key’s text in the current language.
- Translation tables
- Adding a table to the project
- Keys in labels, buttons and menus
- Keys in scripts: Tr
- Which language the game uses
- Changing the language while playing
- When a text is missing
- Questions
Translation tables
Section titled “Translation tables”A translation table is a CSV file (comma-separated values, the kind a spreadsheet saves), laid out like Godot’s:
key,en,fr,demenu.play,Play,Jouer,Spielenmenu.quit,Quit,Quitter,Beenden# The HUDhud.coins,"Coins: %d","Pièces : %d","Münzen: %d"- The first row names the languages. The first column is the keys (its title can be anything:
key,id); each other column is a language, by its code:en,fr,de,pt_BR,fr_CA. - Each row after it is a key and its text in each language.
- The first language is the fallback: text missing in another language shows in that one.
- Rows with an empty key, or a key starting with
#, are comments. - Put a text in double quotes when it has commas or line breaks. A quote inside quoted text is
written twice (
""). - Keys are up to you. Dotted names grouped by place (
menu.play,hud.coins,shop.buy) are easy to keep tidy.
Any spreadsheet program edits these files: save as CSV, in UTF-8 so accents and other alphabets come through.
Adding a table to the project
Section titled “Adding a table to the project”- File > Import Asset… (Ctrl+I) and choose the
.csvfile. It’s copied intoassets/and listed in the browser’s Translations folder. - Select it there. The preview shows how many keys it has, its languages, and the first keys
with their text in the first language. If the file can’t be read as a table, the preview
says so: the first row must be
key,<language>,<language>....
A project can have several tables (one for menus, one for dialogue…). The game loads them all, in the order they’re listed; a key that’s in two tables takes the later table’s text, and a language only one table has is still offered.
The game reads the tables each time it starts, so edit the copy in assets/ in your
spreadsheet, save it, and Play: no need to import it again.
Keys in labels, buttons and menus
Section titled “Keys in labels, buttons and menus”Write a key with @ in front where the text would go:
| Node | Where |
|---|---|
| Label | The inspector’s Text row: @menu.play |
| Button | Its Text row |
| RichText | Its Text row |
| TextField | Its Placeholder row |
| Menu | Its items’ labels and a select item’s options (in the scene file: <item label="@menu.play" action="scene:PlayScene"/>; the inspector doesn’t edit menu items yet) |
The game shows the key’s text in the current language, and changes it at once when the language
changes. To show text that really starts with @, write @@ (@@home shows @home).
The viewport doesn’t load the tables, so it shows the key itself (menu.play). Press Play to see
the translations.
Keys in scripts: Tr
Section titled “Keys in scripts: Tr”e.Tr(key) gives a key’s text in the current language:
Title :: proc(scene: ^e.Scene) { // "@" text follows the language as it changes e.CreateLabel(scene, "@menu.play", {320, 200}, 24, {255, 255, 255, 255}) // Tr gives the text once, in the language of the moment log.info(e.Tr("menu.quit"))}For text with numbers in it, put a %d (a whole number) or %s (text) in the table’s text and
fill it in with fmt.tprintf. Different languages can put it in different places:
CoinCounter :: struct { using script: e.Script, label: ^e.Label `export:"default=."`, coins: int,}
@(init)register_CoinCounter :: proc "contextless" () { e.Script_Register(CoinCounter, { update = proc(self: ^CoinCounter, events: []sdl.Event, dt: f64) { if self.label == nil do return // "Coins: %d" in English, "Pièces : %d" in French... self.label.text = fmt.tprintf(e.Tr("hud.coins"), self.coins) }, })}(Put the script on the Label: its label field’s path . is the node itself. Setting the text
every frame keeps it in the current language. fmt is import "core:fmt".)
Translation tables don’t get constants in scripts/assets.odin: scripts use the keys.
Which language the game uses
Section titled “Which language the game uses”File > Project Settings…, tab General, Language:
| Setting | The game starts in |
|---|---|
| Empty (the default) | The player’s system language, if a table has it; otherwise the tables’ first language |
A language code (fr) |
That language, whatever the player’s system says (useful to test a translation) |
Languages match loosely: fr picks a table’s fr_FR or fr_CA when there’s no plain fr,
and a player whose system says fr_CA gets fr. An exact match wins, and case doesn’t matter.
If the project’s Language isn’t in any table, the Console warns and the game uses the first
language.
Apply makes it an undoable step; it’s saved in project.xml as locale="fr".
Changing the language while playing
Section titled “Changing the language while playing”An options menu can switch the language. Everything written with @ changes at once:
LanguageButton :: struct { using script: e.Script,}
@(init)register_LanguageButton :: proc "contextless" () { e.Script_Register(LanguageButton, { update = proc(self: ^LanguageButton, events: []sdl.Event, dt: f64) { if !e.Input_IsKeyJustPressed(.L) do return // The next language the tables have, round and round locales := e.Locale_List(self.game) if len(locales) == 0 do return current := e.Locale_Get(self.game) next := 0 for name, i in locales do if name == current do next = (i + 1) % len(locales) e.Locale_Set(self.game, locales[next]) log.infof("Language: %s", e.Locale_Get(self.game)) }, })}| Code | What it does |
|---|---|
e.Locale_Set(game, "fr") |
Switches language (matching loosely, as above). Returns false, and keeps the language, when no table has it |
e.Locale_Get(game) |
The language in use, as the tables name it |
e.Locale_List(game) |
Every language the tables have, the fallback first (for a language menu) |
e.Locale_System() |
The player’s system language (fr_CA, or fr); "" when unknown |
e.Locale_LoadFile(game, path) |
Loads one more table while playing (a file of the project) |
Languages :: proc(game: ^e.Game) { if !e.Locale_Set(game, "fr") do log.warn("no French text") log.infof("the player's system: %s", e.Locale_System()) e.Locale_LoadFile(game, "assets/extra.csv")}The language isn’t remembered between runs by itself. To keep the player’s choice, store it in a
saved field of a global script and call e.Locale_Set with it after
e.Load_Game().
When a text is missing
Section titled “When a text is missing”The game never shows nothing:
- The key’s text in the current language,
- else its text in the first language (the fallback column),
- else the key itself (
hud.coins), so missing text is easy to spot while playing.
How it’s stored
Section titled “How it’s stored”A table is an asset of kind Translation in project.xml, and the language is on its root:
<project name="My Game" locale="fr"> <asset path="assets/strings.csv" kind="Translation"/></project>Questions
Section titled “Questions”My label shows menu.play instead of “Play”. In the viewport that’s normal: press Play. In
the game, check the key is spelled the same in the table, and that the table is in the
Translations folder.
My label shows @menu.play with the @. It’s a node that doesn’t translate: only Labels,
Buttons, RichText, TextField placeholders and Menu items do. Use e.Tr and set the text from a
script.
Accented letters come out wrong. Save the CSV as UTF-8, and use a font that has those letters.
The game is in English on my French computer. The project’s Language is set (it wins over the system); empty it to follow the system.
My text has a comma and the columns are off. Put the text in double quotes.
