Software localization
Pygame Tutorial: Localizing Video Game UI and Text

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:
- How to Translate Python Applications with The GNU gettext Module
- Learning gettext tools for Internationalization (i18n)
- Explore All i18n Advantages of Babel for Your Python App
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 fileinit
—create new message catalogs from a POT fileupdate
—update existing message catalogs in a POT filecompile
—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 translationmsgstr
—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:
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:
Repeat the same process and you should see the user interface for 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:
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:
As for Simplified Chinese, you should see the following user interface:
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.