Created on 17 Jan 2021 ;    Modified on 18 Jan 2021

How create a minimal flask project (part 2: to show a page with two languages)


This is the second part of an article about Flask, as follows:

Objective

This is the second part of Flask project to show a single page in two different versions:

  • single language;
  • two languages.

Here we approach how render a page with two (or more) languages. This characteristic usually is said internationalization.

Methodology

We have seen how display an html page with any characteristic about its used language.

Now we work on the second blueprint: single_page/twoels

Development

About the environment: we need the Flask-Babel app. So, let us install it.

As usual, in shell:

1  # installation
2  >cd flask_single_page
3  >venv\Scripts\activate                   # activate python's virtual environment
4  (venv) >pip install Flask-Babel          # install flask-babel
5  ...

Our structure changes as follow (remember: ▼ means directory; it isn't part of directory's name):

▼ flask_single_page
    run.py
    babel.cfg                 # +  babel configuration
    ▼ single_page
        __init__.py
        ▼ oneel
            __init__.py
            views.py
            ▼ static
                ▼ css
                    style.css
            ▼ templates
                base.html
                index.html
        ▼ twoels
            __init__.py
            views.py
            ▼ static           # + this dir holds static resources
                ▼ css
                    style.css
            ▼ templates        # + blueprint templates are here
                2lbase.html
                2lindex.html
    ▼ docs
    ▼ tests
    ▼ venv

File single_page/twoels/static/css/style.css is a copy of that present in single_page/oneel.

But, before to work with our target blueprint (single_page/twoels) we take a look at Babel.

We need to configure it using the file flask_single_page/babel.cfg

[python: twoels/**.py]
[jinja2: twoels/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
silent=False

This file tells to Babel commands where and how search text to translate: Python and html files. And what extensions to use beyond the basic logic.

But we need to configure even our application. So our single_page/__init__.py changes:

 1  from flask import Flask
 2  from flask_babel import Babel                                # +
 3 
 4  babel = Babel()                                              # +
 5 
 6  def create_app():
 7      '''create and configure the app'''
 8      app = Flask(__name__)
 9 
10      babel.init_app(app)                                      # +
11 
12      app.config.from_mapping(
13          SECRET_KEY='leave-hope-to-enter',
14          LANGUAGES = {'en': 'english', 'it': 'italiano',},    # +
15      )
16 
17      # a simple page that says hello
18      @app.route('/hello')
19      def hello():
20          return 'Hello, World!'
21 
22      from .oneel import views as views1
23      app.register_blueprint(views1.oneel)
24 
25      from .twoels import views as views2
26      app.register_blueprint(views2.twoels)
27 
28      return app

In Python programs we tell to Babel what strings we are going to translate using the function gettext() shortened as _(). So single_page/twoels/views.py become:

 1  from flask import Blueprint, current_app, render_template , request          # +-
 2  from flask_babel import _                                                    # +
 3  from single_page import babel                                                # +
 4 
 5  # this app will respond to srv/2l/... URLs
 6  twoels = Blueprint('twoels',                                                 # +-
 7                     __name__,
 8                     static_folder='static',
 9                     template_folder='templates',
10                     url_prefix='/2l')
11 
12  @twoels.route('/')                 # index URLs
13  @twoels.route('/index')
14  @twoels.route('/index.html')
15  def index():
16      return render_template('2lindex.html', title=_('two languages title'))   # +-
17 
18  @babel.localeselector                                        # +
19  def get_locale():                                            # +
20      #<! to test Italian language: configure web browser OR ...
21      #   ... decomment the following line of code
22      #return 'it'
23      return request.accept_languages.best_match(current_app.config['LANGUAGES'].keys())

Here the key point is the decorator @babel.localeselector and the following function get_locale. These are called before the dispatch of an URL, setting babel.default_locale, used to select the desired language.

In html templates we mark text to translate using {{ _() }}. So our templates now are :

file single_page/twoels/templates/2lbase.html

 1  <!doctype html>
 2 
 3  <title>{% block title %}{% endblock %} - {{ _('Single Page, two languages') }}</title>   <!--  +- -->
 4  <link rel="stylesheet" href="{{ url_for('oneel.static', filename='css/style.css') }}">
 5  <nav>
 6    <h1>{{ _('Single Page - single language') }}</h1>                                      <!--  +- -->
 7    <ul>
 8        <li><a href="{{ url_for('oneel.index') }}">{{ _('single language index') }}</a>    <!--  +- -->
 9        <li><a href="{{ url_for('twoels.index') }}">{{ _('two languages index') }}</a>     <!--  +  -->
10    </ul>
11  </nav>
12  <section class="content">
13    <header>
14      {% block header %}{% endblock %}
15    </header>
16 
17    {% for message in get_flashed_messages() %}
18      <div class="flash">{{ message }}</div>
19    {% endfor %}
20 
21    {% block content %}{% endblock %}
22 
23  </section>

file single_page/twoels/templates/2lindex.html

 1  {% extends '2lbase.html' %}
 2 
 3  {% block header %}
 4    <h1>{% block title %}Home{% endblock %}</h1>
 5  {% endblock %}
 6 
 7  {% block content %}
 8    <p>{{ _('Single page, single language, content') }}</p>     <!--  +- -->
 9  {% endblock %}

A note about template filenames. We changed them compared to equivalent filenames in single_page/oneel to avoid collision between each other. Flask's search strategy for template files does not ensure the distinction between templates with the same name belonging to different blueprints.

Next steps are:

  • extract and store text to translate in file messages.pot;
  • initialize directories and data file to hold translations, creating catalog messages.po;
  • translate text hold in catalog messages.po;
  • compile catalog messages.po to file messages.mo.

To do these tasks we use pybabel.exe and poedit.exe in shell as follow (current directory is flask_single_page; operating system is MS Windows 10, in Linux you have to use venv/bin/pybabel):

(venv) >venv\Scripts\pybabel.exe extract -F babel.cfg -o messages.pot single_page
...writing PO template file to messages.pot

(venv) >venv\Scripts\pybabel.exe init -i messages.pot -d single_page\translations -l it      # init italian language using "-l it"
creating catalog single_page\translations\it\LC_MESSAGES\messages.po based on messages.pot

(venv) >\bin\poedit\poedit.exe           # GUI of poedit to translate messags in catalog messages.po ...
                                         # ... we have to write translations in GUI and save them

(venv) >venv\Scripts\pybabel.exe compile -d single_page\twoels\translations
compiling catalog single_page\translations\it\LC_MESSAGES\messages.po to single_page\translations\it\LC_MESSAGES\messages.mo

A note about poedit.exe. This isn't the only tool to write text translations in messages.po. Feel free to choose another one if you like.

In case we need to update messages, we skip the creation of translations\<language>\LC_MESSAGES\messages.po but we have to do again the tasks:

  • message extraction,
  • update of messages.po,
  • translation and
  • compilation,

as follow:

(venv) >venv\Scripts\pybabel.exe extract -F babel.cfg -o messages.pot single_page
...writing PO template file to messages.pot

(venv) >venv\Scripts\pybabel.exe update -i messages.pot -d single_page\translations
updating catalog single_page\translations\it\LC_MESSAGES\messages.po based on messages.pot

(venv) >\bin\poedit\poedit.exe           # GUI of poedit to translate messags in catalog messages.po

(venv) >venv\Scripts\pybabel.exe compile -d single_page\translations
compiling catalog single_page\translations\it\LC_MESSAGES\messages.po to single_page\translations\it\LC_MESSAGES\messages.mo

Now, executing python run.py, if web browser configuration tells we like English as preferred language, browsing url http://localhost:5000/2l/ we get the page content in English language:

single page, two languages, english language option

But if we configure our browser to use Italian as our preferred language (e.g. in Firefox as follow)

firefox configuration to use italian language to display pages

we get the page content in Italian language:

single page, two languages, italian language option

Good. It's working.

So, have we done? No, not yet. We need another little piece to complete the puzzle. Please read part 3 :-)

Enjoy, ldfa