Pygame Tutorial: Localizing Video Game UI and Text

Pygame Tutorial on Localizing Video Game UI and Text

Writing video games for a global user base? Learn how to get ahead of the game in localization with Pygame and Babel.

Pygame is a strong set of Python modules for writing video games. Nevertheless, localizing the user interface (UI) and text in Pygame can often be a time-consuming process: You need to load the font beforehand, render it with the desired text, and draw it on your screen. On top of that, you need to handle all mouse and keypress events when building the menu interface. In this tutorial, you will learn how to localize the UI and text of both the menu and in-game interface seamlessly. Let’s start by installing the necessary modules.

Our setup for localizing with Pygame and Babel

For a smooth start, it is highly recommended to create a new virtual environment and activate it before the installation process.

Pygame

Run the following command to install Pygame:

pip install pygame

Pygame-menu

Let us install another complementing package called pygame-menu. It is extremely useful for creating a menu for your game:

pip install pygame-menu

Babel

Last but not least, Babel is the right choice for localizing Python applications. Run the following command to install it:

pip install babel

Babel provides a command-line interface for working with message catalogs.

🗒 Note » Check out the following tutorials if you want to learn more about using Babel:

Message catalogs

Let us break down the basic workflow for localizing with message catalogs:

  • Mark strings for translation in the Python scripts.
  • Extract translations into a base POT file.
  • Create a new message catalog for each supported language.
  • Translate the content in each message catalog.
  • Compile the translation files into MO files.

🔗 Resource » Here’s how to use Google Translate in your Python app to automatically translate the content in your message catalogs.

Marking translation strings

First and foremost, you need to mark all the text that will need to get translated. You can do so by wrapping the text in a call to the gettext.gettext() function. Alternatively, you can use the preferred alias form _() as well. Have a look at the following example:

# print example
print(_('app_title'))

# set the title for your application
pygame.display.set_caption(_('window_title'))

app_title and window_title represent the unique identifier for the translation string.

Babel provides the following useful commands for generating message catalogs for your game:

  • extract—extract messages from source files and generate a POT file
  • init—create new message catalogs from a POT file
  • update—update existing message catalogs in a POT file
  • compile—compile message catalogs to MO files

Extracting translations into a base POT file

Once you have marked all translation strings, you can use the extract command to create a POT file. The generated file contains all the strings that are meant for translation. You have to pass in the following arguments:

  • a directory or files path
  • an output path

To test the command, create a new folder called locale in your working directory. Then, run the following command (replace mygame.py with the filename of your Python file):

pybabel extract mygame.py -o locale/messages.pot

You should see the following output:

extracting messages from mygame.py
writing PO template file to locale/messages.pot

Inside the locale folder, you can find the newly generated messages.pot file. It serves as the base translation file for your project. You will use it to generate the corresponding translation file for each of the locales that your game supports.

...

#: test.py:110
msgid "widget_font"
msgstr ""

#: test.py:116
msgid "app_title"
msgstr ""

#: test.py:120
msgid "name"
msgstr ""

...

Each translation string contains two important items, msgid and msgstr:

  • msgid—represents the unique identifier for the translation
  • msgstr—represents the translation text

You can leave the msgstr as an empty string in the base file.

Creating new message catalogs from a POT file

The next step is to initialize the corresponding locales that will be supported by your game. Run the init command with the following arguments:

  • locale (-l)
  • input file (-i)
  • output directory (-d)
pybabel init -l en_US -i locale/messages.pot -d locale

Your console should display the following output:

creating catalog locale\en_US\LC_MESSAGES\messages.po based on locale/messages.pot

You need to repeat the command for the other locales as well. For example:

# German
pybabel init -l de_DE -i locale/messages.pot -d locale

# Simplified Chinese
pybabel init -l zh_CN -i locale/messages.pot -d locale

Each command will create a new folder based on the locale you have specified. There will be a LC_MESSAGES folder with a messages.po file inside each locale. Open the messages.po file and fill in the corresponding translation texts as follows:

...

#: test.py:110
msgid "widget_font"
msgstr "arial"

#: test.py:116
msgid "app_title"
msgstr "Welcome"

#: test.py:120
msgid "name"
msgstr "Name"

...

Updating existing message catalogs in a POT file

Subsequently, when there are new translation strings or changes to the existing translations, simply run the extract command and then execute the update command as shown below. Let us say you have added a new translation string called last_updated in your Python file:

...

text = _('last_updated')
f_text = font.render(text, True, (255, 255, 255))

...

All you need to do is run the following command to update the translation files:

pybabel extract mygame.py -o locale/messages.pot
pybabel update -i locale/messages.pot -d locale

The messages.po file should now look like this:

#: test.py:110
msgid "widget_font"
msgstr "arial"

#: test.py:116
msgid "app_title"
msgstr "Welcome"

#: test.py:120
msgid "name"
msgstr "Name"

...

#: mygame.py:89
msgid "last_updated"
msgstr ""

It retains all old translations and appends new ones to all messages.po files inside each locale folder. Simply fill in the desired translation and save the file. Here is an example for en_US locale:

#: mygame.py:89
msgid "last_updated"
msgstr "Last updated on"

Once you have filled in the translation text for all supported locales, you are ready to execute the compile command.

Compiling message catalogs to MO files

Lastly, let us compile the PO into MO files by running the compile command:

pybabel compile -d locale

You should get the following output:

compiling catalog locale\de_DE\LC_MESSAGES\messages.po to locale\de_DE\LC_MESSAGES\messages.mo
compiling catalog locale\en_US\LC_MESSAGES\messages.po to locale\en_US\LC_MESSAGES\messages.mo
compiling catalog locale\zh_CN\LC_MESSAGES\messages.po to locale\zh_CN\LC_MESSAGES\messages.mo

It will generate the machine-readable messages.mo file side by side with the messages.po in each locale folder.

Localizing your game

We are now all set up to move on with the localization of our game with the help of Babel. We will have a detailed look at interpolation, pluralization, loading locales dynamically, using a predefined theme to localize the UI and text of your menu, as well as formatting numbers and dates.

Interpolation

Given the following translation string…

game_name = "Tic Tac Toe"
text = _("press_start") % game_name

…you can easily interpolate the output inside your messages.po file of each locale:

msgid "press_start"
msgstr "Press start to play %s"

You need to retain the %s placeholder since it represents string interpolation. For integer variables, you need to use %d instead.

msgid "score_board"
msgstr "Score: %d"

Pluralization

Babel comes with its own pluralization support via the gettext.ngettext() function. It takes in three positional parameters:

  • singular—unique identifier for singular translation
  • plural—unique identifier for plural translation
  • n—plural determiner

You can use it as follows:

import gettext

...

difficulty = 2
text = gettext.ngettext('game_message', 'game_message_plural', difficulty )

...

Based on the value of the difficulty parameter, it will return either the output string from game_message or game_message_plural. You can interpolate it as follows:

gettext.ngettext('game_message', 'game_message_plural', difficulty) % difficulty 

After you run the extract or update command, you can find the corresponding plural formula inside each messages.po file. By default, Babel will generate the plural forms based on the locale you have set during the init command.

# example for two plural forms
"Plural-Forms: nplurals=2; plural=(n != 1)\n"

# example for 6 plural forms
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : "
"n%100>=3 && n%100<=10 ? 3 : n%100>=0 && n%100<=2 ? 4 : 5)\n"

Here is an example of how the translation string will appear in message.po file for two plural forms:

#: mygame.py:67
#, python-format
msgid "game_message"
msgid_plural "game_message_plural"
msgstr[0] "You will gain %d point per coin collected."
msgstr[1] "You will gain %d points per coin collected."

Some languages have several plural forms. Arabic, for example, has 6 plural forms in total, so before filling in the translations, make sure its messages.po is as follows:

#: mygame.py:67
#, python-format
msgid "game_message"
msgid_plural "game_message_plural"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
msgstr[4] ""
msgstr[5] ""

Compile now all messages.po into messages.mo files.

Loading locales dynamically

Next, append the following code in your main file to load all locales dynamically via the glob package:

import glob
import os

...

locales = [x.split('\\')[1] for x in glob.glob('locale/*') if os.path.isdir(x)]
active_locale = 'en_US'
translations = {}

for locale in locales:
    translations[locale] = gettext.translation('messages', localedir='locale', languages=[locale])

translations[active_locale].install()

Using a theme

pygame-menu provides a few predefined themes for you to localize the UI and text of your menu. You can use a theme directly this way:

menu = pygame_menu.Menu(
    height=300,
    theme=pygame_menu.themes.THEME_BLUE,
    title=_('app_title'),
    width=400
)

Besides that, you can customize the theme as well by copying it and modifying the underlying attributes:

mytheme = pygame_menu.themes.THEME_ORANGE.copy()

# change background color of title
mytheme.title_background_color=(0, 0, 0)

...
menu = pygame_menu.Menu(
    height=300,
    theme=mytheme,
    title=_('app_title'),
    width=400
)

You can find the full list of predefined fonts here. Other than that, you can use any system fonts by specifying the corresponding name. Run the following command to get the complete list of the names for your system fonts:

print(pygame.font.get_fonts())

It is recommended to have a specific font for each locale else all translation text will not be rendered properly. You can use the same font for locales that share the same character set:

# using predefined fonts for widgets
theme.widget_font = pygame_menu.font.FONT_DIGITAL

# using system fonts for widgets
theme.widget_font = 'arial'

Here is an overview of available text alignments for widgets:

  •  pygame_menu.locals.ALIGN_LEFT
  • pygame_menu.locals.ALIGN_CENTER
  • pygame_menu.locals.ALIGN_RIGHT

You can use it to set a system-wide text alignment for all your widgets. This is extremely useful for localizing right-to-left locales.

# set text alignment for widgets to align right
theme.widget_alignment = pygame_menu.locals.ALIGN_RIGHT

Changing a locale

There are multiple ways to let users change the locale of a game. One of the simplest methods is to let users select the desired language while they use the menu interface. You can implement that by creating a selector widget:

...
def update_widget_ui():
    pass


def set_locale(selected: Tuple, value: Any) -> None:
    global locale, translations

    locale = value
    translations[locale].install()
    update_widget_ui()


LANGUAGES = [('English', 'en_US'), ('Deutsch', 'de_DE'), ('简体中文', 'zh_CN')]

language_selector = main_menu.add.selector(_('language') + ': ', LANGUAGES, onchange=set_locale)

The main_menu.add.selector widget accepts the following input parameters:

  • title
  • list of tuple representing the display text and the underlying value
  • callback function when there are changes to the current value

You still need to implement the corresponding code in the update_widget_ui function to update all the widgets manually via the set_title or update_font function. Take the following code snippet as a reference:

def update_widget_ui():
    active_font = _('widget_font')
    # update window title and font
    pygame.display.set_caption(_('window_title'))
    main_menu._menubar.update_font({'name': active_font})

    # update widget title
    main_menu.set_title(_('app_title'))
    name_input.set_title(_('name') + ': ')
    difficulty_selector.set_title(_('difficulty') + ': ')
    language_selector.set_title(_('language') + ': ')
    play_button.set_title(_('play'))
    quit_button.set_title(_('quit'))

    # update widget font
    name_input.update_font({'name': active_font})
    difficulty_selector.update_font({'name': active_font})
    language_selector.update_font({'name': active_font})
    play_button.update_font({'name': active_font})
    quit_button.update_font({'name': active_font})

    # update default value and items in selector
    name_input.set_default_value(_('name_default'))
    difficulty_selector.update_items([(_('easy'), 1), (_('medium'), 2), (_('hard'), 3)])

As we can see, the font name is localized via the translation string in this case. It is recommended to use a different font for each of the locales to prevent pygame from rendering it incorrectly. Here is a code snippet example of how to localize the font inside your messages.po files.

# en_US
#: mygame.py:140
msgid "widget_font"
msgstr "arial"

# de_DE
#: mygame.py:140
msgid "widget_font"
msgstr "arial"

# zh_CN
#: mygame.py:140
msgid "widget_font"
msgstr "dengxian"

In-game font rendering

The previous section covered localizing the UI and text of the game. For in-game text, you need to create the font beforehand and draw it on the surface of your main window:

surface = create_example_window(_('window_title'), (600, 400))

# load font via path
font = pygame.font.Font('C:\WINDOWS\FONTS\SIMHEI.TTF', 20)

# OR load font via system font name
# (use this line or the last but not both)
font = pygame.font.SysFont('simhei', 20)

# render the font, set antialias to True and tuple representing the RGB color
f_text = font.render("Hello world!", True, (255, 255, 255))

# draw on surface, tuple of x, y coordinates
surface.blit(f_text, (10, 10))

Formatting numbers

Babel provides a few powerful built-in functions for formatting numbers as well:

  • datetime
  • number and decimal
  • currency

The most useful function for a game project is the format_decimal function that will parse the given integer based on the input locale. For example:

from babel.numbers import format_decimal

...

text = format_decimal(12345.6, locale='en_US')
# 12,345.6

text = format_decimal(12345.6, locale='de_DE')
# 12.345,6

You can also use the format_decimal function for both whole and float numbers. There is another function called format_number, but it has been deprecated in the latest version of Babel.

Formatting dates

You can easily format dates via the built-in format_date function. The function accepts a datetime object and the target locale:

from babel.dates import format_date
import datetime

...

now = datetime.datetime.now()
text = format_date(now, locale='en_US')
# Aug 5, 2021

text = format_date(now, locale='de_DE')
# 05.08.2021

text = format_date(now, locale='zh_CN')
# 2021年8月5日

Example Code

Have a look at this repository for the reference code. It comes with the following message catalogs:

  • en_US
  • de_DE
  • zh_CN

Once you have cloned it, you can run it as follows:

python mygame.py

You should now see the following screen:

Our game's main menu UI in English

Navigate to the Language selector and change it to the next locale by:

  • hitting the right arrow button
  • clicking on the <English> text

The menu interface will change from English to German as follows:

Our game's main menu UI in German

Repeat the same process and you should see the user interface for Simplified Chinese:

Our game's main menu UI in Simplified Chinese

Likewise, you can play around with the difficulty-level selector and click the “Play” button to start the game. You should see the following simple mockup interface for English:

Game user UI in English

This user interface aims to showcase pluralization, number, and date formatting. Try going back to the main menu and changing both the level of difficulty and language. You will see the following output when you hit again the “Play” button in German:

Game UI in German

As for Simplified Chinese, you should see the following user interface:

Game UI in Simplified Chinese

Conclusion

We are done! You have localized the UI and text of your pygame project using pygame-menu and the Babel package. If you want to further streamline your localization process, consider signing up for Phrase, the most reliable localization, and translation management platform.

Phrase comes with a 14-day free trial and can provide you with everything you need to:

  • Build production-ready integrations with your development workflow,
  • Invite as many users as you wish to collaborate on your projects,
  • Edit and convert localization files online with more context for higher translation quality.
Comments