RSSAmplifier

John's Blog · Jun 12, 2024

Making "is it Easter" Multilingual

0
Sign in to vote or save

John · John's Blog

Introduction

In the world of software, no project is ever truly complete and “is it Easter” is no exception. After having the site live for a few weeks, I thought about what more I could do with it. There is still more I want to learn about web development and I decided to make the app support multiple languages. Which turned out to be straightforward and relatively easy. Exactly why I like working with Python.

One of my goals while working on this feature was only using internally stored translations. I did not want to use an online API that does auto machine translations on the fly. Pretty much every browser has this built in so I wanted to support true, application level, bundled translations.

Flask-Babel

I used Flask-Babel to add translation strings to the app and was pleasantly surprised when I found it also handles proper date and number formatting. The US is unique in using Month / Day / Year format and Flask-Babel will properly format dates from the python Date object for the selected locale.

Initialization

Once flask_babel was included I created the object and initialized it as part of the app object.

from flask_babel import Babel
babel = Babel()
...
babel.init_app(app, locale_selector=get_locale)

I used later initialization instead of adding the app in the constructor because I wanted to attach a selector object for the locale.

def get_locale():
    return request.accept_languages.best_match([ 'en', 'nl' ])

The locale function just lists the languages I’m supporting and instructs the application to use the best match. The default language is English for all languages that aren’t Dutch. The base strings are all in English so that get used if a match isn’t found.

Translation Strings

Previously, I had a mix of strings in the Jina2 templates and in the Python code. I standardized on having all strings in the templates. Which turned out to make it easer to understand the templates.

Translatable strings are enclosed with _('') to tell babel the string are translatable. For example:

{{ _('Easter is on') }} {{ easter_date|dateformat(format='long', rebase=False) }}

Additionally, Date objects passed into the template are handled by the dateformat function that’s added to Jina2 by Flask-Babel. I have rebase set to False because I don’t need automatic timezone conversion. I handled timezone previously when determining if it is or isn’t Easter in the Python code.

Creating Translation Files

Now that all of the code is in place, I can start translating the app.

Config file

The first step to creating translations is making a babel.cfg to instruct pybabel where to look for translatable strings.

[python: **.py]
[jinja2: **/templates/**.j2]

Generate List of Translatable Strings

Next, tell pybabel to read the files and generate a messages.pot file with a list of all translatable strings.

❯ pybabel extract -F babel.cfg -o messages.pot app

Failure

When I first ran this I got a nasty and unintuitive error.

 pybabel extract -F babel.cfg -o messages.pot app
extracting messages from app/__init__.py
extracting messages from app/routes/__init__.py
extracting messages from app/routes/bunny_picture.py
extracting messages from app/routes/favicon.py
extracting messages from app/routes/index.py
extracting messages from app/routes/error/e404.py
extracting messages from app/routes/error/e500.py
extracting messages from app/templates/404.j2
Traceback (most recent call last):
  File "/Users/john/git/isiteaster/.venv/bin/pybabel", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/frontend.py", line 979, in main
    return CommandLineInterface().run(sys.argv)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/frontend.py", line 905, in run
    return cmdinst.run()
           ^^^^^^^^^^^^^
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/frontend.py", line 516, in run
    for filename, lineno, message, comments, context in extracted:
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/extract.py", line 215, in extract_from_dir
    yield from check_and_call_extract_file(
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/extract.py", line 279, in check_and_call_extract_file
    for message_tuple in extract_from_file(
                         ^^^^^^^^^^^^^^^^^^
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/extract.py", line 321, in extract_from_file
    return list(extract(method, fileobj, keywords, comment_tags,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/john/git/isiteaster/.venv/lib/python3.12/site-packages/babel/messages/extract.py", line 445, in extract
    raise ValueError(f"Unknown extraction method {method!r}")
ValueError: Unknown extraction method 'jinja2'

Solution

After a lot of Googling that lead to dead ends I figured out the solution. Install setuptools. I’m not using setuptools with this project, so it wasn’t in my virtual environment. I have no idea why but after installing it, pybabel works fine.

❯ pip install setuptools

It Works!

Now running gives proper output and generates the messages.pot file.

 pybabel extract -F babel.cfg -o messages.pot app
extracting messages from app/__init__.py
extracting messages from app/routes/__init__.py
extracting messages from app/routes/bunny_picture.py
extracting messages from app/routes/favicon.py
extracting messages from app/routes/index.py
extracting messages from app/routes/error/e404.py
extracting messages from app/routes/error/e500.py
extracting messages from app/templates/404.j2
extracting messages from app/templates/500.j2
extracting messages from app/templates/base.j2
extracting messages from app/templates/index.j2
extracting messages from app/templates/partials/footer.j2
extracting messages from app/templates/partials/head.j2
extracting messages from app/utils/__init__.py
extracting messages from app/utils/cli.py
extracting messages from app/utils/crawler_detect.py
extracting messages from app/utils/easter.py
extracting messages from app/utils/locale.py
extracting messages from app/utils/tz.py
writing PO template file to messages.pot

The messages.pot file is used to generate the language specific messages.po files which will contain all translations for a given language.

Initializing a Language

To support a new language (or the first) you need to initialize it.

❯ pybabel init -i messages.pot -d app/translations -l <LANG_CODE>

Where <LANG_CODE> is the language code you’re creating translations for. This creates the following directories, app/translations/<LANG_CODE>/LC_MESSAGES/. Additionally, it create a messages.po file which is where translations are stored. Do this for every language you want to support

Don’t forget to edit the messages.po file(s) with the translation strings!

Compiling Translations

Flask-Babel doesn’t use the messages.po, instead it uses a complied version called messages.mo.

❯ pybabel compile -d app/translations

Updating Translations

Over time new strings will inevitably be added to your application. When that happens you need to generate a new messages.pot. From that have the new strings added to each messages.po. Update the messages.po. Finally compile the new translations.

❯ pybabel extract -F babel.cfg -o messages.pot app
❯ pybabel update -i messages.pot -d app/translations
❯ pybabel compile -d app/translations

Using messages.mo with Flask-Babel

Flask-Babel looks for translations in a directory named translations in the same directory as the main app. In the case of “is it Easter”, I have the app source in a directory called app. At least right now it’s called app.

Flask-Babel knows what languages there are, based on the directory path under translations of <LANG_CODE>/LC_MESSAGES/. Flask-Babel automatically looks for all messages.mo files and will load them at startup.

Other than creating the complied translation files in the correct directory, Flask-Babel transparently loads and uses the translations.

Testing Translations

Once I had my translations written and complied, I needed to see how they displayed. I could temporally hard code the locale returned by get_locale() but that is cumbersome and doesn’t allow me to easily flip between multiple languages.

Instead I used Google Chrome’s “Developer Tools” and set a Sensor location with the location and language code for what I wanted to test.

In the developer tools.

  1. Click the three verticals dots.
  2. Choose “More tools ->”
  3. Choose “Sensors”
  4. In the Sensor area, choose the location.
    • If the location is not listed
    • Click “Manage” -> “Add Location…”
    • Be sure to add the “Locale” which is the 2 character language code

Now to test a language.

  1. Choose the location
  2. Reload the page

You’ll see it switch between languages. Assuming you have a translation and it’s listed in the get_locale() function as supported by the application.

Languages in “is it Easter”

Besides English, I’m only supporting Dutch in “is it Easter” because it’s the only other language I know well enough to have comprehensible translations. Machine translations are really good these days but I don’t want to go that route in order to support more languages. Especially when this is a web site that is pretty much never going to be used. Much less by people who don’t speak English. The URL is an English phrase after all.

Also, the more translations I add, the more work it is to maintain. I don’t want future enhancements to take up more time updating translation files than whatever it is that I’m adding. This is a for fun project and I want to keep it that way.

Conclusion

It was a lot easier to add multilingual support to the application. There are quite a few setup steps but it’s a straight forward process. Overall, this project came out much better than I expected. It also made me look at my string handling and really clean up parts of the application.

Read the original on nachtimwald.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.