Engineeringこの記事を日本語で読む

GameMaker localization: building the system the engine does not give you

If you are looking for GameMaker's localization system, you will not find one. There is no string table asset, no translation import, no locale switcher. What the engine gives you is a way to read files, a way to store key-value data, and a way to draw text — and localization is what you build out of those three.

That is less bad than it sounds. A working localization layer in GameMaker is a couple of hundred lines, and because you wrote it, you understand exactly what it does. The risk is not complexity. The risk is that the pieces the engine does not hand you are also the pieces you do not think about until they break: fonts that do not contain the characters you need, word wrapping that assumes spaces, a legacy character that turns into a line break, and text scattered across objects that never went through the lookup at all.

This article covers the design decisions that are hard to reverse later, in roughly the order you will face them.

One lookup function, and no drawn literals

The whole system rests on one function that takes a key and returns the string for the current language. Give it a short name, because you are going to type it thousands of times, and make it the only path by which player-facing text reaches the screen.

The discipline that matters is the negative one: no literal text in a draw call, ever. Not a placeholder, not a debug label, not the one-word button you were going to fix later. Once literals are allowed anywhere, finding them again means reading every object in the project, and there is nothing that will flag them for you. Deciding on day one that a drawn literal is a bug costs nothing; deciding it in month six costs a full audit.

Make the lookup loud when it fails. Returning the key itself for a missing entry is the standard choice and it works well, because a key on screen is obviously wrong in a way that empty text is not. In development builds, also log the miss. A missing key that only appears in one rare state gets noticed in the log long before anyone plays that state in the target language.

Assemble sentences from a template, never by concatenating translated fragments. Joining a translated noun to a translated verb produces something grammatical only in the language you designed it for. One key holding the whole sentence with a placeholder in it lets the translator move the placeholder where their language needs it.

// One entry point. _key is a string; returns the current language's text.
function L(_key) {
    var _s = global.strings;
    if (variable_struct_exists(_s, _key)) return variable_struct_get(_s, _key);
    if (GM_build_type == "run") show_debug_message("MISSING KEY: " + _key);
    return _key;
}

// Templates, not concatenation
// strings: "msg_found_item" -> "You found {0}!" / "{0}を見つけた!"
draw_text(x, y, string_replace(L("msg_found_item"), "{0}", item_name));

Where the text lives and how to load it without corrupting it

Text files go in your included files so they ship with the build. Whether you use CSV or JSON matters less than being consistent, and each has a real trade-off. CSV puts every language in one grid, which makes missing translations visible as empty cells and makes a spreadsheet a usable editor. JSON handles multi-line text, quotes, and nested structure without the escaping arguments that CSV always eventually produces, and GameMaker can parse it into a struct in one call.

A common middle path is one JSON file per language, keyed identically, loaded on startup or on language change. That keeps files small, makes it obvious which file a translator receives, and means a broken translation file can only break one language.

Read those files through buffers rather than the line-based text file functions. The text functions have line-oriented semantics that interact badly with embedded line breaks, and buffer reading is the dependable way to get UTF-8 content in intact. This is the single most common cause of a Japanese or Russian translation that loads as garbage while the English file loads fine — the file was correct and the reader was not.

Make sure your files are saved as UTF-8 without a byte order mark. A BOM at the start of the file becomes invisible characters attached to your very first key, so that key never matches and the failure looks like a mysterious single missing string. If your team edits CSV in a spreadsheet application, this is worth checking on every round trip, because spreadsheet applications have opinions about encoding that they do not always announce.

// Load a language file from Included Files as UTF-8
function lang_load(_code) {
    var _file = "lang/" + _code + ".json";
    if (!file_exists(_file)) { _file = "lang/en.json"; }
    var _buf  = buffer_load(_file);
    var _text = buffer_read(_buf, buffer_text);
    buffer_delete(_buf);
    global.strings  = json_parse(_text);
    global.language = _code;
}

// Startup: OS language as the default, saved preference wins
var _pref = /* read from your save/config */ undefined;
lang_load(is_undefined(_pref) ? os_get_language() : _pref);

Fonts are the hard part in GameMaker

A GameMaker font asset bakes a specified range of characters into a texture at a specified size. That design is fast and predictable, and it is also why adding Japanese, Chinese, Korean, or even a full Cyrillic set is not a small change. A default font asset covers a basic Latin range; characters outside it do not draw.

You have three realistic strategies. Include a large character range and accept a large texture — workable for Cyrillic or Greek, painful for CJK where the useful set runs into the thousands of characters. Generate a font that contains only the characters your translated text actually uses, which keeps the texture small but must be regenerated every time the text changes, so it belongs in your build process rather than in someone's memory. Or draw text through a runtime font-rendering extension, trading setup complexity for not having to decide the character set in advance.

Whichever you choose, make the font a property of the language rather than a global constant. Latin and CJK rarely look right at the same pixel size or the same line spacing, so your language data should carry which font asset to use, what line height to draw at, and any size adjustment — and your drawing code should read those instead of hardcoding a font.

Check the exact ranges and generation options in the font editor documentation for your GameMaker version, since the interface here has changed across releases and the details matter more than the general advice.

Drawing text that was not written in English

Three GML-specific behaviours will bite you in this order.

Word wrapping breaks on spaces. The wrapping variants of the text drawing functions take a pixel width and break the line at word boundaries, which means a Japanese or Chinese sentence — containing no spaces at all — is treated as one enormous word and runs straight off the edge of your box. You need to insert breaks yourself for those languages: measure character by character against the available width and insert a line break when the next character would overflow. That is more manageable than it sounds because CJK characters in a typical font all advance the same width, so the measurement is nearly arithmetic. Applying basic line-break rules on top of it — not starting a line with a closing bracket or a full stop — is a small amount of extra code that makes the result look deliberate rather than mechanical.

The hash character has legacy meaning. In GameMaker's text drawing, an unescaped hash has historically acted as a line break rather than printing as a character. Your English text probably never contains one; a translation can, especially in text about numbers, rankings, or channels. Confirm the behaviour in your version and, if it applies, escape it when loading translations rather than trusting every translator to know an engine detail.

Measure, do not assume. The functions that return the drawn width and height of a string are the only reliable way to know whether a translation fits, and they account for the actual font in use. Build a debug view that draws the bounding box of every text element in the current language: overflow becomes visible at a glance instead of one screen at a time. Deciding what to do when text does not fit — shrink, wrap, scroll, or abbreviate — is a design decision, and picking one per text element in advance is faster than reacting to each overflow report individually.

Everything that is not drawn text

The lookup function covers strings. A game contains more language than that, and these are the pieces that reliably ship untranslated because no part of your system tracks them:

  • Sprites with words drawn into them — title logos, signage, tutorial images, button prompts with labels. Either produce a sprite per language and swap by language code, or redesign so the word is drawn over the image at runtime
  • Audio: voice lines, and any sound effect that is really speech
  • The window title, set once at startup and almost never revisited
  • Save file text, achievement names, and anything written to a file or shown by the platform rather than by your own draw code
  • Number, date, and currency formatting, which differ by locale independently of the words around them
  • Store page text, screenshots, and the description — outside the project entirely, and the first thing a player in another language reads

A short checklist before adding your second language

Most of the cost of localizing a GameMaker project is paid before any translation arrives, in whether the project can accept one at all. Running through this list while you still have one language is dramatically cheaper than after you have three:

  • Every drawn string comes from the lookup function, with no literals anywhere in draw code
  • Sentences are built from templates with placeholders, not by concatenating pieces
  • Language files load through buffers, are UTF-8 without a byte order mark, and fail loudly if a file is missing
  • The font, line height, and any size adjustment are part of the language data, not constants in the drawing code
  • Wrapping works for a language with no spaces, tested with real text rather than repeated placeholder characters
  • Missing keys are logged in development builds, and a debug overlay can show text bounds for the current language
  • The chosen language is saved and restored, with the operating system language only used as the initial default

Related articles