Software localization

The Ultimate Guide to Python Localization

Follow our step-by-step guide to preparing Python apps for localization and get to know common modules that can help you implement multilingual support for a global user base.
Python localization blog post featured image | Phrase

Localizing Python applications is a process with many moving parts. But when done right, it can improve accessibility, increase user engagement, and expand market reach.

To help you get started, this tutorial will outline best practices for preparing Python apps for localization using JSON or YAML files, and discuss common modules available to help.

It will provide valuable insights for both developers looking to improve global appeal and businesses looking to expand into new markets.

How do I internationalize UI strings with Python?

Let’s address some key questions on how best to prepare your Python app for localization.

Why do we use translation files for our UI?

There are a couple of benefits to using translation files instead of hard-coding translation texts in your application:

  • It is easier to edit and translate as most translators are not familiar with code
  • You can easily identify the wrong translation texts without searching through your code base
  • There is no need to rebuild your applications when there is a change to a translation (depending on programming languages and frameworks used)

At its core, translation files can be represented in different formats. The following section covers how to internationalize your Python application using:

  • Custom implementation with JSON / YAML as translation files
  • babel and gettext to work with PO and MO translation files

Installation

Before that, let’s install the following Python packages:

  • babel: utilities for internationalization and localization in Python
  • pyyaml: support reading and writing of YAML files

We can run the following commands from the command line to install our packages.

pip install pyyaml
pip install babel

How to load JSON translation files?

One of the simplest methods to store translation files is via JSON. For example, you can create a new file called en.json with the following items:

{
    "title": "Dialogstation",
    "ques-name": "Geben Sie Ihren Namen ein:",
    "ques-age": "Geben Sie Ihr Alter ein:",
    "ans-name": "Hallo, $name! Willkommen bei Phrase",
    "ans-age": {
        "one": "Du bist $count Jahr alt",
        "other": "Du bist $count Jahre alt"
    },
    "ques-dob": "Geben Sie Ihr Geburtsdatum ein (JJJJ-MM-TT):",
    "ans-dob": "Sie wurden am $dob geboren"
}Code language: JSON / JSON with Comments (json)

Then, simply load it using the built-in json package as follows:

import json

with open('filename.json', 'r', encoding='utf8') as f:
    data = json.load(f)Code language: JavaScript (javascript)

How to load YAML translation files?

We can use YAML files instead of JSON. We will need the pyyaml package to support YAML.

Given en.yaml as the translation file for English with the following content:

title: Interactive Terminal
ques-name: "Enter your name:"
ques-age: "Enter your age:"
ans-name: Hello, $name! Welcome to Phrase
ans-age:
  one: You are $count year old
  other: You are $count years old
ques-dob: "Enter your date of birth (YYYY-MM-DD):",
ans-dob: You were born on $dobCode language: PHP (php)

Likewise, the German translation file should be called de.yaml:

title: Dialogstation
ques-name: "Geben Sie Ihren Namen ein:"
ques-age: "Geben Sie Ihr Alter ein:"
ans-name: Hallo, $name! Willkommen bei Phrase
ans-age:
  one: Du bist $count Jahr alt
  other: Du bist $count Jahre alt
ques-dob: "Geben Sie Ihr Geburtsdatum ein (JJJJ-MM-TT):",
ans-dob: Sie wurden am $dob geborenCode language: PHP (php)

Load it using pyyaml package as follows:

import yaml

with open('filename.yml', 'r', encoding='utf8') as f:
    data = yaml.safe_load(f)import jsonCode language: JavaScript (javascript)

Translator class

Using what you have learned so far, let’s build a custom module with a Translator class with the following features:

  • Load translation files during startup
  • Set the currently active locale
  • Set the plural rules
  • Translate and format the translation text

In addition, the module should also include 2 additional functions to:

  • Convert a string to a datetime object
  • Format a datetime object to a string

Make sure that your file structure is as follows:

.
├── data (folder containing JSON or YAML translation files)/
│   ├── en.json
│   ├── de.json
│   ├── en.yaml
│   └── de.yaml
├── i18n.py (custom module)
└── main.py (main application)Code language: plaintext (plaintext)

Create a new Python file called i18n.py with the following code:

import json
import glob
import os
import yaml
from datetime import datetime
from babel.dates import format_datetime

supported_format = ['json', 'yaml']


class Translator():
    def __init__(self, translations_folder, file_format='json', default_locale='en'):
        # initialization
        self.data = {}
        self.locale = 'en'

        # check if format is supported
        if file_format in supported_format:
            # get list of files with specific extensions
            files = glob.glob(os.path.join(translations_folder, f'*.{file_format}'))
            for fil in files:
                # get the name of the file without extension, will be used as locale name
                loc = os.path.splitext(os.path.basename(fil))[0]
                with open(fil, 'r', encoding='utf8') as f:
                    if file_format == 'json':
                        self.data[loc] = json.load(f)
                    elif file_format == 'yaml':
                        self.data[loc] = yaml.safe_load(f)

    def set_locale(self, loc):
        if loc in self.data:
            self.locale = loc
        else:
            print('Invalid locale')

    def get_locale(self):
        return self.locale

    def translate(self, key):
        # return the key instead of translation text if locale is not supported
        if self.locale not in self.data:
            return key

        text = self.data[self.locale].get(key, key)
        
        return text


def str_to_datetime(dt_str, format='%Y-%m-%d'):
    return datetime.strptime(dt_str, format)


def datetime_to_str(dt, format='MMMM dd, yyyy', loc='en'):
    return format_datetime(dt, format=format, locale=loc)

The glob module is used to load the translation files dynamically depending on the file_format value. It accepts either json or yaml:

    def __init__(self, folder, file_format='json', default_locale='en', default_locale='en'):
        # ...

        # check if format is supported
        if file_format in supported_format:
            # get list of files with specific extensions
            files = glob.glob(os.path.join(folder, f'*.{file_format}'))
            for fil in files:
                # get the name of the file without extension, will be used as locale name
                loc = os.path.splitext(os.path.basename(fil))[0]
                with open(fil, 'r', encoding='utf8') as f:
                    if file_format == 'json':
                        self.data[loc] = json.load(f)
                    elif file_format == 'yaml':
                        self.data[loc] = yaml.safe_load(f)Code language: PHP (php)

The core feature is the translate function which returns the translation text based on the current active locale:

    def translate(self, key):
        # return the key instead
        if self.locale not in self.data:
            return key

        text = self.data[self.locale].get(key, key)

        return textCode language: PHP (php)

The module also contains 2 additional global functions for datetime formatting:

def str_to_datetime(dt_str, format='%Y-%m-%d'):
    return datetime.strptime(dt_str, format)

def datetime_to_str(dt, format='MMMM dd, yyyy', loc='en'):
    return format_datetime(dt, format=format, locale=loc)Code language: JavaScript (javascript)

Adding interpolation to the Translator class

For string interpolation, you can use the built-in template string module. Add the following import statement:

from string import TemplateCode language: JavaScript (javascript)

Under the translate function, instead of returning the text variable directly, instantiate it with the Template class and call the safe_substitute function as follows:

def translate(self, key, **kwargs):
    # return the key instead of translation text if locale is not supported
    if self.locale not in self.data:
        return key

    text = self.data[self.locale].get(key, key)
    
    # string interpolation
    return Template(text).safe_substitute(**kwargs)Code language: PHP (php)

Adding pluralization to the Translator class

You can import the babel.plural.PluralRule class for pluralization support:

from babel.plural import PluralRuleCode language: JavaScript (javascript)

Under the __init__ function, initialize a new plural_rule variable as follows:

    def __init__(self, folder, file_format='json', default_locale='en'):
        self.data = {}
        self.locale = default_locale
        self.plural_rule = PluralRule({'one': 'n is Code language: PHP (php)

🗒 Note » The module uses {'one': 'n is 1'} as the plural rule, which represents simple pluralization. The key should be one of the following:

  • 0
  • 1
  • 2
  • a few
  • many
  • other

🔗 Resource » The pluralization syntax is based on the CDLR rules.

Next, implement the setter and getter function for the plural rule in the Translator class:

class Translator():
    ...

    def set_locale(self, loc):
        if loc in self.data:
            self.locale = loc
        else:
            print('Invalid locale')

    def get_locale(self):
        return self.locale

    def set_plural_rule(self, rule):
        try:
            self.plural_rule = PluralRule(rule)
        except Exception:
            print('Invalid plural rule')

    def get_plural_rule(self):
        return self.plural_rule

Modify the code under the translate function to include a check for pluralization:

class Translator():
    ...

    def translate(self, key, **kwargs):
        # return the key instead of translation text if locale is not supported
        if self.locale not in self.data:
            return key

        text = self.data[self.locale].get(key, key)

        # type dict represents key with plural form
        if type(text) == dict:
            count = kwargs.get('count', 1)
        # parse count to int
        try:
            count = int(count)
        except Exception:
            print('Invalid count')
            return key

        text = text.get(self.plural_rule(count), key)
        return Template(text).safe_substitute(**kwargs)

The complete code for the Translator class is as follows:

from babel.plural import PluralRule
import json
from string import Template
import glob
import os
import yaml
from datetime import datetime
from babel.dates import format_datetime

supported_format = ['json', 'yaml']


class Translator():
    def __init__(self, translations_folder, file_format='json', default_locale='en'):
        # initialization
        self.data = {}
        self.locale = 'en'
        self.plural_rule = PluralRule({'one': 'n is 1'})

        # check if format is supported
        if file_format in supported_format:
            # get list of files with specific extensions
            files = glob.glob(os.path.join(translations_folder, f'*.{file_format}'))
            for fil in files:
                # get the name of the file without extension, will be used as locale name
                loc = os.path.splitext(os.path.basename(fil))[0]
                with open(fil, 'r', encoding='utf8') as f:
                    if file_format == 'json':
                        self.data[loc] = json.load(f)
                    elif file_format == 'yaml':
                        self.data[loc] = yaml.safe_load(f)

    def set_locale(self, loc):
        if loc in self.data:
            self.locale = loc
        else:
            print('Invalid locale')

    def get_locale(self):
        return self.locale

    def set_plural_rule(self, rule):
        try:
            self.plural_rule = PluralRule(rule)
        except Exception:
            print('Invalid plural rule')

    def get_plural_rule(self):
        return self.plural_rule

    def translate(self, key, **kwargs):
        # return the key instead of translation text if locale is not supported
        if self.locale not in self.data:
            return key

        text = self.data[self.locale].get(key, key)
        # type dict represents key with plural form
        if type(text) == dict:
            count = kwargs.get('count', 1)
            # parse count to int
            try:
                count = int(count)
            except Exception:
                print('Invalid count')
                return key
            text = text.get(self.plural_rule(count), key)
        return Template(text).safe_substitute(**kwargs)


def parse_datetime(dt, input_format='%Y-%m-%d', output_format='MMMM dd, yyyy', output_locale='en'):
    dt = datetime.strptime(dt, input_format)
    return format_datetime(dt, format=output_format, locale=output_locale)

Using the Translator module in your application

Now, you can use the Translator class as a module in your application. Have a look at the following code snippet as a reference for the basic usage of the custom Translator module:

# import the module
import i18n

# instantiate a new Translator class with the path to the data
translator = i18n.Translator('data/')Code language: PHP (php)
name = 'John Doe'
print(translator.translate('ans-name', name=name))
# Hello, John Doe! Welcome to Phrase

# change the active locale to de
translator.set_locale('de')
print(translator.translate('ans-name', name=name))
# Hallo, John Doe! Willkommen bei Phrase

age = 30
print(translator.translate('ans-age', count=age))
# Du bist 30 Jahre alt

dob = '1992-01-01'
dob = i18n.parse_datetime(dob)

print(translator.translate('ans-dob', dob=dob))
# Sie wurden am January 01, 1992 geborenCode language: PHP (php)

You can easily extend the functionality of the i18n module to support more features based on your requirements.

How do I internationalize my Python UI with gettext?

Alternatively, you can utilize the gettext module to internationalize Python applications. gettext uses PO (also known as POT) and MO message catalog files.

🗒 Note » PO files represent the human-editable translation files, while MO files are machine-readable for consumption by gettext.

Fortunately, the babel package complements nicely with the gettext module. babel provides the following utility functions for working with Message Catalogs:

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

Basic translation messages

The first phase is to mark strings as translatable in the source files. Simply enclose translatable strings with the _() function:

# unmarked strings
print('Interactive Terminal')
print('title')

# strings marked as translatable
print(_('Interactive Terminal'))
print(_('title'))Code language: PHP (php)

🗒 Note » _() is a short-hand alias of gettext.gettext() function. You can pass in the key for translation or the actual translation string as long as it is unique across the application. It will return the translated string based on the current locale.

For example, given that the code above resides in a Python file called main_babel.py, the command to extract messages from source files is as follows:

pybabel extract -o data/messages.pot main_babel.py

The output file, data/messages.pot, should contain the following content:

    msgid ""
    msgstr ""
    "Project-Id-Version: PROJECT VERSION\n"
    "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
    "POT-Creation-Date: 2022-06-17 23:04+0800\n"
    "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    "Language-Team: LANGUAGE <LL@li.org>\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=utf-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Generated-By: Babel 2.10.1\n"

    #: main_babel.py:11
    msgid "title"
    msgstr ""

    #: main_babel.py:14
    msgid "ques-name"
    msgstr ""

    #Code language: PHP (php)

It represents the base translation file. msgid is the unique identifier for each message to be translated, while msgstr represents the translation text. You can leave msgstr empty for now as the actual translation text should be in the PO files.

The babel module will generate the relevant files for each locale automatically for you during the initialization phase. Let’s test it by running the following command for the en locale.

pybabel init -l en -i data/messages.pot -d data/

🗒 Note » -l sets the locale for the generated PO files.

Run another command again for de locale.

pybabel init -l de -i data/messages.pot -d data/

The content inside each messages.po file should be the same as the base POT file. The next step is to fill in the corresponding translation text inside the PO files:

.
├── messages.pot
├── de/
│   └── LC_MESSAGES/
│       └── messages.po
└── en/
    └── LC_MESSAGES/
        └── messages.poCode language: plaintext (plaintext)
#: main_babel.py:11
msgid "title"
msgstr "Interactive Terminal"

#: main_babel.py:14
msgid "ques-name"
msgstr "Enter your name:"

#: main_babel.py:16
msgid "ans-name"
msgstr "Hello, {name}! Welcome to Phrase"

#: main_babel.py:19
msgid "ques-age"
msgstr "Enter your age:"

#: main_babel.py:21
msgid "ans-age"
msgid_plural "ans-age-plural"
msgstr[0] "You are {count} year old"
msgstr[1] "You are {count} years old"Code language: PHP (php)

Finally, run the following command to compile PO files into MO files:

pybabel compile -d data/

The data folder should be as follows:

.
├── messages.pot
├── de/
│   └── LC_MESSAGES/
│       ├── messages.mo
│       └── messages.po
└── en/
    └── LC_MESSAGES/
        ├── messages.mo
        └── messages.poCode language: plaintext (plaintext)

Once you are done with it, you can utilize the gettext.translation function to load the translation files.

For example, you can call the install function to set it as the currently active locale and get the translation text as follows:

import gettext

# initialization
lang_en = gettext.translation('messages', localedir='data', languages=['en'])

# set current locale to en
lang_en.install()

print(_('ans-name'))
# Hello, John! Welcome to PhraseCode language: PHP (php)

🗒 Note » The install function will import the _() alias internally. Hence, there is no need to import _ manually.

Adding string interpolation via gettext

For string interpolation, you can use a variable name and mark it with curly brackets. For example, given the following translation text:

#: main_babel.py:16
msgid "ans-name"
msgstr "Hello, {name}! Welcome to Phrase"Code language: PHP (php)

🗒 Note » Using curly brackets {variable_name} in the translation text allows string interpolation with the format function.

You can easily interpolate in the translation text as follows:

import gettext

# initialization
lang_en = gettext.translation('messages', localedir='data', languages=['en'])

# set current locale to en
lang_en.install()

print(_('ans-name').format(name='John'))
# Hello, John! Welcome to Phrase

print(_('ans-name').format(name='Kelly'))
# Hello, Kelly! Welcome to PhraseCode language: PHP (php)

Adding pluralization support via ngettext

For pluralization support, use the ngettext function instead. It accepts 3 input arguments:

  • singular: id for singular form
  • plural: id for plural form
  • n: plural determiner
print(ngettext('ans-age', 'ans-age-plural', age))Code language: PHP (php)

Just like the gettext function, ngettext will return the translated string. You can easily interpolate in the output string with a function like format:

print(ngettext('ans-age', 'ans-age-plural', age).format(count=age))Code language: PHP (php)

Upon extraction, you should see a different syntax in the base translation file (POT):

#: main_babel.py:21
msgid "ans-age"
msgid_plural "ans-age-plural"
msgstr[0] ""
msgstr[1] ""Code language: PHP (php)

The number of msgstr is based on the number of plural forms specified for the locale. Different locales have different amounts of plural forms: While English has 2 plural forms, Arabic has 6.

A comment at the top of the file shows how a locale’s plural form for a message is resolved. For example:

"Plural-Forms: nplurals=2; plural=(n != 1);\n"Code language: JSON / JSON with Comments (json)

🔗 Resource » The pluralization syntax is based on the CDLR rules.

Have a look at the following examples:

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

# 3 plural forms
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"

# 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"Code language: HTML, XML (xml)

You can get the pluralized text as follows:

import gettext

# initialization
lang_en = gettext.translation('messages', localedir='data', languages=['en'])

# set current locale to en
lang_en.install(names=['gettext''ngettext'])

print(ngettext('ans-age', 'ans-age-plural', age).format(count=1))
# You are 1 year old

print(ngettext('ans-age', 'ans-age-plural', age).format(count=12))
# You are 12 years oldCode language: PHP (php)

Using gettext in your application

Instead of loading each locale one by one, you can load the translation files dynamically and store them in a dict object as follows:

import gettext

translations = {}
supported_langs = ['en', 'de']

# load translation files dynamically
for lang in supported_langs:
    translations[lang] = gettext.translation('messages', localedir='data', languages=[lang])Code language: PHP (php)

Now, simply set the desired locale and call either the _ or ngettext functions to get the desired translation texts:

import gettext

translations = {}
supported_langs = ['en', 'de']

# load translation files dynamically
for lang in supported_langs:
    translations[lang] = gettext.translation('messages', localedir='data', languages=[lang])

# set active locale to en
translations['en'].install(names=['gettext', 'ngettext'])

name = 'John Doe'
print(_('ans-name').format(name=name))
# Hello, John Doe! Welcome to Phrase

# change the active locale to de
translations['de'].install(names=['gettext', 'ngettext'])
print(_('ans-name').format(name=name))
# Hallo, John Doe! Willkommen bei Phrase

age = 30
text = ngettext('ans-age', 'ans-age-plural', age)
print(text)
# Du bist 30 Jahre alt

text = text.format(count=age)
print(text)
# Du bist 30 Jahre altCode language: PHP (php)

Simply run the file as usual and you should get the same output as what we did previously using JSON or YAML.

🤿 Go deeper » Check out our guide to translating Python applications with the GNU gettext module to learn more about the gettext module.

How can a Python app support right-to-left languages?

Python does not come with an out-of-the-box implementation for displaying text meant for right-to-left languages, but you can utilize the python-bidi module for such use cases. python-bidi is a Python implementation of a bi-directional (BiDi) layout. It provides a convenient function to display right-to-left languages. Run the following command to install it:

pip install python-bidi

It comes with the get_display function to display a string bi-directionally:

from bidi.algorithm import get_display

get_display('اَلْعَرَبِيَّةُ')Code language: JavaScript (javascript)

You should get the following output when you print the result:

اَلْعَرَبِيَّةُ

How do I format localized dates and times in Python?

Python has its own built-in datetime module for manipulating dates and times. It supports arithmetic operations on objects, making it extremely useful for localization and internationalization.

How do I work with date and time duration?

A timedelta object represents the difference between 2 dates or times. You can think of it as a duration. It accepts the following input arguments:

datetime.timedelta(milliseconds=0, microseconds=0, seconds=0, minutes=0, hours=0, days=0, weeks=0)

You can create a new timedelta as follows:

from datetime import timedelta

delta = timedelta(
    seconds=27,
    minutes=5,
    hours=8,
    days=50,
    weeks=2
)

# 64 days, 8:05:27Code language: PHP (php)

🤿 Go deeper » Check out the Timedelta objects documentation to learn more about the Timedelta class.

How can I use datetimes?

A time object represents a local time of the day regardless of the date, while a date represents a date in a calendar. Often the ideal choice is to use the datetime object. It contains all the relevant information related to time and date. The datetime object can be categorized as naive or aware, depending on whether it contains information related to timezone.

🤿 Go deeper » Check out the Aware and Naive Objects documentation to learn more about them.

from datetime import datetime, timedelta, timezone

# naive datetime obj
dt = datetime.now()
# 2022-06-12 15:48:40.014838

# aware datetime obj (GMT+8)
tz = timezone(timedelta(hours=8))
dt = datetime.now(tz=tz)
# 2022-06-12 15:48:40.014838+08:00Code language: PHP (php)

A datetime object supports the following methods for datetime conversion:

  • strftime: convert a datetime object to a string according to a given format
  • strptime: parse a string into a datetime object given a corresponding format

The strftime function accepts a string, which indicates the corresponding date and time format. This comes in handy when localizing the content of an application.

🤿 Go deeper » Check out the strftime and strptime documentation to learn more about them.

Have a look at the following code snippet which displays different string output depending on the input format string.

from datetime import datetime, timezone

dt = datetime(year=2022, month=6, day=12, hour=16, minute=32, second=45, tzinfo=timezone.utc)

dt.strftime('%Y-%m-%d %H:%M:%S')
# 2022-06-12 16:32:45

dt.strftime('%b %d, %Y')
# Jun 12, 2022

dt.strftime('%A (%I.%M %p)')
# Sunday (04.32 PM)

dt.strftime('%c')
# Sun Jun 12 16:32:45 2022Code language: PHP (php)

The format depends on the existing locale of the application. You can use the built-in locale module to change it.

import locale

# get current locale
locale.getlocale()
# ('English_Singapore', '1252')

locale.setlocale(locale.LC_ALL, 'de_DE')
# ('de_DE', 'ISO8859-1')Code language: PHP (php)

🤿 Go deeper » Check out our beginner’s guide to Python’s locale module to learn more about the locale module.

Let’s test it again using de_DE (German in Germany) as the current locale instead:

from datetime import datetime, timezone
import locale

locale.setlocale(locale.LC_ALL, 'de_DE')

dt = datetime(year=2022, month=6, day=12, hour=16, minute=32, second=45, tzinfo=timezone.utc)

dt.strftime('%Y-%m-%d %H:%M:%S')
# 2022-06-12 16:32:45

dt.strftime('%b %d, %Y')
# Jun 12, 2022

dt.strftime('%A (%I.%M %p)')
# Sonntag (04.32 )

dt.strftime('%c')
# 12.06.2022 16:32:45Code language: PHP (php)

On the other hand, the strptime function takes in 2 input arguments:

  • date_string: a string representation of a datetime based on 1989 C standard.
  • format: the format to parse the input date_string

🗒 Note » The strptime function cares about the current locale when parsing the input.

from datetime import datetime

text = '2022-06-12 16:32:45'
format = '%Y-%m-%d %H:%M:%S'

datetime.strptime(text, format)
# 2022-06-12 16:32:45Code language: PHP (php)

How do I format localized dates with Babel?

Alternatively, you can utilize the babel.dates module to format date and time.

A note on Babel

The babel module is a collection of utilities for l10n and i18n in Python. It is actively maintained and offers the following features:

  • Datetime formatting
  • Number formatting
  • Currency formatting
  • Generating message catalogs (translation files)

🤿 Go deeper » Check out Babel’s i18n advantages for multilingual apps to learn more about the babel module.

OK, back to date formatting. babel.dates comes with the following functions:

  • format_time
  • format_date
  • format_datetime

Have a look at the following code snippet for the usage of Babel’s formatting functions:

from datetime import datetime, timezone
from babel.dates import format_time, format_date, format_datetime

dt = datetime(year=2022, month=6, day=12, hour=16, minute=32, second=45, tzinfo=timezone.utc)

# using the default medium format
format_time(dt, locale='en_US')
# 4:32:45 PM

format_date(dt, locale='en_US')
# Jun 12, 2022

format_datetime(dt, locale='en_US')
# Jun 12, 2022, 4:32:45 PM

# using full format
format_time(dt, format='full', locale='en_US')
# 4:32:45 PM Coordinated Universal Time

format_date(dt, format='full', locale='en_US')
# Sunday, June 12, 2022

format_datetime(dt, format='full', locale='en_US')
# Sunday, June 12, 2022 at 4:32:45 PM Coordinated Universal Time

# using German locale
format_time(dt, format='full', locale='de_DE')
# 16:32:45 Koordinierte Weltzeit

format_date(dt, format='full', locale='de_DE')
# Sonntag, 12. Juni 2022

format_datetime(dt, format='full', locale='de_DE')
# Sonntag, 12. Juni 2022 um 16:32:45 Koordinierte WeltzeitCode language: PHP (php)

The format argument is optional and can be one of the following choices:

  • Short
  • Medium (the default value)
  • Long
  • Full

🗒 Note » The final output is based on the input locale argument.

🤿 Go deeper » Check out the babel.dates fields documentation to learn more about custom patterns.

How do I use babel to work with time zones?

You can utilize the get_timezone function to create a new timezone object based on timezone names such as US/Eastern or Europe/Berlin. Then, pass the object as input for tzinfo argument:

from datetime import datetime, timezone
from babel.dates import get_timezone, format_datetime

dt = datetime(year=2022, month=6, day=12, hour=16, minute=32, second=45, tzinfo=timezone.utc)

eastern = get_timezone('US/Eastern')
berlin = get_timezone('Europe/Berlin')

# using eastern timezone
format_datetime(dt, format='full', locale='en_US', tzinfo=eastern)
# Sunday, June 12, 2022 at 12:32:45 PM Eastern Daylight Time

# using berlin timezone
format_datetime(dt, format=format, locale='en_US', tzinfo=berlin)
# Sunday, June 12, 2022 at 6:32:45 PM Central European Summer TimeCode language: PHP (php)

How do I format localized numbers in Python?

Number formatting can be tricky when it comes to internationalization. For example, the text 12,345 conveys different meanings for English US (en_US) and German (de_DE). This is mainly because different languages use different symbols for decimal points and thousand separators.

🤿 Go deeper » Our concise guide to number localization covers grouping, separators, numeral systems, and more.

How do I use the built-in locale module to format numbers?

For conversion from string to integer or floating point, the locale module is a good option. It comes with the following built-in function:

  • atoi: convert a string to an integer using the current locale numeric conventions
  • atof: convert a string to a floating point using the current locale numeric conventions

Given 12,345 as the input string, the results of atof for both languages are as follows:

import locale

# English
locale.setlocale(locale.LC_ALL, 'en_US')
locale.atof('12,345')
# 12345.0

# German
locale.setlocale(locale.LC_ALL, 'de_DE')
locale.atof('12,345')
# 12.345

# English
locale.setlocale(locale.LC_ALL, 'en_US')
locale.atof('12.345')
# 12.345

# German
locale.setlocale(locale.LC_ALL, 'de_DE')
locale.atof('12.345')
# 12345.0Code language: PHP (php)

On the other hand, you can utilize the format_string function to convert a number into a localized string. It accepts the following input argument:

  • format: a string representing the format specification
  • val: a number
  • grouping: whether to take grouping into account. Grouping refers to a sequence of numbers specifying which relative positions the thousand separator is expected. It is False by default.

Have a look at the following code snippet:

import locale

locale.setlocale(locale.LC_ALL, 'en_US')
locale.format_string('%10.2f', 123456.78)
# 123456.78

locale.format_string('%10.2f', 123456.78, grouping=True)
# 123,456.78

locale.setlocale(locale.LC_ALL, 'de_DE')
locale.format_string('%10.2f', 123456.78)
# 123456,78

locale.format_string('%10.2f', 123456.78, grouping=True)
# 123.456,78Code language: PHP (php)

How do I use babel to format localized numbers?

Alternatively, you can utilize the following locale-specific formatting functions provided by the babel.numbers module:

  • format_decimal: format a given number based on the input locale
  • format_percent: format a given number to percentage based on the input locale
  • format_scientific: format a given number to scientific notation based on the input locale. It uses E as the notation for power of 10

The following code snippet illustrates the output for format_decimal when using different locales:

from babel.numbers import format_decimal, format_percent, format_scientific

format_decimal(12345, locale='en_US')
# 12,345
format_decimal(12345.67, locale='en_US')
# 12,345.67
format_decimal(12345, locale='de_DE')
# 12.345
format_decimal(12345.67, locale='de_DE')
# 12.345,67

format_percent(0.34, locale='en_US')
# 34%
format_percent(0.34, locale='de_DE')
# 34 %

format_scientific(1234567, locale='en_US')
# 1.234567E6
format_scientific(1234567, locale='de_DE')
# 1,234567E6Code language: PHP (php)

🗒 Note » Unlike the locale.format_string function, the babel.numbers.format_decimal function allows parsing of custom patterns.

🤿 Go deeper » Check out the Custom Pattern Syntax documentation to learn more about custom patterns.

How do I format localized currency in Python?

Currency formatting takes into account the currency symbol and locale. For example, the symbols and EUR represent euros and can be used interchangeably.

How do I use the built-in locale module to format currency?

The built-in locale module comes with the currency function to format currency. There is an optional international argument to display the currency using the international name instead of the currency symbol.

How do I use Babel to format localized currency?

The babel.numbers module comes with the format_currency function that considers both the locale and currency. It means that you can format a number using de_DE locale for USD.

Have a look at the following code snippet to understand more:

import locale
from babel.numbers import format_currency

locale.setlocale(locale.LC_ALL, 'en_US')
locale.currency(1234.56)
# $1234.56
locale.currency(1234.56, international=True)
# USD1234.56

format_currency(1234.56, 'USD', locale='en_US')
# $1,234.56
# set the currency to EURO
format_currency(1234.56, 'EUR', locale='en_US')
# €1,234.56
# set the currency to Japan YEN
format_currency(1234.56, 'JYP', locale='en_US')
# JYP1,234.56



locale.setlocale(locale.LC_ALL, 'de_DE')
locale.currency(1234.56)
# 1234,56 €
locale.currency(1234.56, international=True)
# 1234,56 EUR

format_currency(1234.56, 'USD', locale='de_DE')
# 1.234,56 $
format_currency(1234.56, 'EUR', locale='de_DE')
# 1.234,56 €
format_currency(1234.56, 'JYP', locale='de_DE')
# 1.234,56 JYPCode language: PHP (php)

🗒 Note » Unlike the locale.format_currency function, the babel.numbers.format_currency function allows you to specify the currency explicitly. Instead of having USD bound to en_US locale, you can display the desired currency via the currency argument.

Wrapping up

We are done! By now, you should have all the knowledge required to localize and internationalize Python applications.  If you are building web applications in Python and looking for built-in localization methods, consider checking out the following articles:

If you want to further streamline your localization process, consider signing up for Phrase, the most connective and customiziable suite of translation automation technology available.

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.