Skip to content

Texts in several languages

A game shows its players texts: a score, a hint, the end screen's message, the labels of its menus. Written straight into the scripts, they are in one language. Kept in translation files, the same game shows them in English to one player and in Portuguese to another.

Translation files

They go in the game's locales/ folder, one per language, named for it: locales/en.json, locales/pt.json, locales/pt_BR.json. Each is a JSON object of keys and the texts they stand for:

{
  "hud.coins": "Coins {count}/{total}",
  "hud.hint": "Grab every coin, then reach the flag!",
  "over.win": "You win!"
}

A key is any name you like; words joined by dots keep them in order (hud.coins, menu.credits). A text names the values put into it in braces ({count}). A long text may be a list of lines, which are joined with line breaks: "credits": ["Made by Ana", "Music by Rui"].

English is the one every other language falls back to, so en.json has every key; a translation has the keys it translates, and a key it lacks shows in English.

The templates come with a locales/en.json, and the platformer's (games/platformer/locales/) is a full example.

Showing a text

I18n.t gives the text of a key in the language in use, with its values filled in:

UI.text(I18n.t("hud.coins", count: @coins, total: @total), x: 20, y: 40, size: 28, color: :gold)
GameMenu.instance.finish(I18n.t("over.win"))

Each value is shown as its to_s. A number with a set number of decimals is formatted first: I18n.t("hud.best", seconds: format("%.1f", @best)). Keep a whole sentence in one text, values and all, rather than gluing pieces together: another language may want the values elsewhere, or a different word for one and for many ("{place}: 1 other player" and "{place}: {count} other players" are two keys).

A key no file has a text for shows as itself (hud.coins), so a missing text is easy to spot on the screen.

The language

I18n.locale is the language texts are shown in, as a Symbol (:en). It starts as the player's: the language chosen in RedGem's settings, else the system's, matched with the game's files whatever their case and whether - or _ is written: the same language and region (pt_BR.json for pt-BR), else the language alone (pt.json), else another region of it. When the game has no file in the player's language, it starts in the language RedGem shows its own texts in (English when it has none in the player's). A text a region's file lacks is looked for in the language's own file (pt_BR, then pt). A game may offer a choice:

I18n.available_locales    # => [:en, :pt]: the languages the game has files for
I18n.locale = :pt         # from now on, in Portuguese

A text already looked up keeps its language. GameMenu looks its labels and headings up each time it draws them, so they follow I18n.locale; a game's own option or heading does too when it is given as a lookup rather than a text:

menu.screens[:pause].add_option(-> { I18n.t("menu.credits") }, index: 1) { menu.show(:credits) }
GameMenu::Screen.new(heading: -> { I18n.t("credits.heading") })

The engine's own texts

The labels of GameMenu (Play, Quit, Play again, Resume, Restart, Paused, and those of its settings screen and controls page) come from the engine, in the player's language. A game's files may change them or translate them, with the same keys:

{
  "game.menu.play": "Start",
  "game.menu.resume": "Continuar"
}

The keys are game.menu.play, game.menu.quit, game.menu.play_again, game.menu.resume, game.menu.restart and game.menu.paused; the settings screen's are game.settings.*, and the controls page's game.controls.* (its rows, and the names of keys and buttons: see The controls page). A text is looked for in the game's files in the language in use, then among the engine's texts in that language, then in the game's English, then in the engine's English; else the key itself is shown. (So a game whose English changes a label still shows the engine's translation of it to a player who reads another language.)

What is read, and what is not

Translation files travel with the game: builds, Android packages and dedicated servers carry the locales/ folder like the rest. They are read once, when the game's scripts start. Only files named <language>.json are read (a language's name is letters, digits, - and _), none larger than 1 MB and none reached through a symbolic link. A file that is not a translation file (broken JSON, or a value that is neither a text nor a list of lines) is left out whole: the log says which file and why, and the game goes on with the others.

On a dedicated server each room has its own I18n.locale. A text a server sends players, such as the reason a join was turned down, is in the server's language: a game that wants it in each player's sends a key and lets the player's machine look it up.