Skip to main content

Flask Internationalization

Introduction

Internationalization (often abbreviated as i18n - "i" followed by 18 letters, then "n") is the process of designing your application to be adaptable to different languages and regions without engineering changes. It's a crucial feature for applications intended for a global audience, allowing you to reach users in their native languages.

In this tutorial, we'll learn how to implement internationalization in Flask applications using the Flask-Babel extension. By the end, you'll be able to create Flask applications that can dynamically switch between languages based on user preferences.

Prerequisites

Before we begin, make sure you have:

  • Basic knowledge of Flask
  • Python 3.6 or higher installed
  • A Flask development environment set up

Understanding Internationalization Concepts

Before diving into code, let's clarify some key concepts:

  • Internationalization (i18n): The process of designing software to potentially support different languages
  • Localization (l10n): The process of adapting your software to a specific locale or language
  • Translation: Converting text from one language to another
  • Locale: A set of parameters that define language, region, and formatting preferences

Setting Up Flask-Babel

Flask-Babel is an extension to Flask that adds i18n and l10n support to any Flask application. Let's start by installing it:

bash
pip install Flask-Babel

Basic Setup

First, create a simple Flask application structure:

/myapp
/translations
/templates
/static
app.py
babel.cfg

Now, let's create our basic Flask application with internationalization support:

python
from flask import Flask, render_template, request, session, g
from flask_babel import Babel, gettext

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

babel = Babel(app)

# Define locale selector function
@babel.localeselector
def get_locale():
# Try to get the language from the session
if 'language' in session:
return session['language']
# Otherwise, try to detect it from the request
return request.accept_languages.best_match(['en', 'es', 'fr', 'de'])

@app.route('/')
def index():
return render_template('index.html')

if __name__ == '__main__':
app.run(debug=True)

In the above code:

  • We initialize the Babel extension with our Flask app
  • We define the default locale as English ('en')
  • We specify where translations will be stored
  • We create a localeselector function that determines which language to use for each request

Creating Templates with Translatable Text

Now, let's create our index.html template with text ready for translation:

html
<!DOCTYPE html>
<html>
<head>
<title>{{ _('Welcome to My Multilingual App') }}</title>
</head>
<body>
<h1>{{ _('Hello, World!') }}</h1>
<p>{{ _('This is a demonstration of Flask internationalization.') }}</p>

<p>{{ _('Today is %(date)s', date=current_date) }}</p>

<h2>{{ _('Change Language') }}</h2>
<ul>
<li><a href="{{ url_for('set_language', lang='en') }}">English</a></li>
<li><a href="{{ url_for('set_language', lang='es') }}">Español</a></li>
<li><a href="{{ url_for('set_language', lang='fr') }}">Français</a></li>
</ul>
</body>
</html>

Let's add a route to change the language:

python
@app.route('/language/<lang>')
def set_language(lang):
session['language'] = lang
return redirect(request.referrer or url_for('index'))

@app.context_processor
def inject_date():
return {'current_date': datetime.now().strftime('%Y-%m-%d')}

Extracting Strings for Translation

Next, we need to configure which files to extract translations from. Create a babel.cfg file:

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

Now, let's extract all translatable strings:

bash
pybabel extract -F babel.cfg -o messages.pot .

This command creates a messages.pot file that contains all the translatable strings found in your application.

Creating Translations

Let's create translation files for Spanish:

bash
pybabel init -i messages.pot -d translations -l es

This creates a translation directory for Spanish. Now, open the file translations/es/LC_MESSAGES/messages.po. It will look something like this:

msgid "Welcome to My Multilingual App"
msgstr ""

msgid "Hello, World!"
msgstr ""

msgid "This is a demonstration of Flask internationalization."
msgstr ""

msgid "Today is %(date)s"
msgstr ""

msgid "Change Language"
msgstr ""

Fill in the translations:

msgid "Welcome to My Multilingual App"
msgstr "Bienvenido a Mi Aplicación Multilingüe"

msgid "Hello, World!"
msgstr "¡Hola, Mundo!"

msgid "This is a demonstration of Flask internationalization."
msgstr "Esta es una demostración de internacionalización en Flask."

msgid "Today is %(date)s"
msgstr "Hoy es %(date)s"

msgid "Change Language"
msgstr "Cambiar Idioma"

Compiling Translations

After translating, compile the translation files:

bash
pybabel compile -d translations

Now, let's update our Flask application to use these translations:

python
from flask import Flask, render_template, request, session, g, redirect, url_for
from flask_babel import Babel, gettext as _
from datetime import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

babel = Babel(app)

@babel.localeselector
def get_locale():
if 'language' in session:
return session['language']
return request.accept_languages.best_match(['en', 'es', 'fr'])

@app.route('/')
def index():
return render_template('index.html')

@app.route('/language/<lang>')
def set_language(lang):
session['language'] = lang
return redirect(request.referrer or url_for('index'))

@app.context_processor
def inject_date():
return {'current_date': datetime.now().strftime('%Y-%m-%d')}

if __name__ == '__main__':
app.run(debug=True)

Lazy Translation

Sometimes you need to translate strings outside of a request context. For this, Flask-Babel provides lazy translations:

python
from flask_babel import lazy_gettext as _l

# This string will be translated when it's actually used
title = _l('My Application Title')

Pluralization

Different languages handle pluralization differently. Flask-Babel supports proper pluralization with the ngettext function:

python
from flask_babel import ngettext

@app.route('/items/<int:count>')
def items(count):
message = ngettext(
'You have %(num)d item',
'You have %(num)d items',
count) % {'num': count}
return render_template('items.html', message=message, count=count)

Dates and Numbers Formatting

Flask-Babel also helps with formatting dates, times, and numbers according to locale conventions:

python
from flask_babel import format_datetime, format_currency
from datetime import datetime

@app.route('/format_example')
def format_example():
now = datetime.now()
formatted_date = format_datetime(now)
price = format_currency(1234.56, 'USD')
return render_template('format.html', date=formatted_date, price=price)

In your template:

html
<p>{{ _('The current date is %(date)s', date=date) }}</p>
<p>{{ _('The price is %(price)s', price=price) }}</p>

Complete Example: Multilingual Blog

Let's create a more practical example of a simple multilingual blog:

python
from flask import Flask, render_template, request, session, g, redirect, url_for
from flask_babel import Babel, gettext as _, lazy_gettext as _l
from datetime import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

babel = Babel(app)

# Sample blog posts
blog_posts = [
{
'id': 1,
'title_en': 'First Post',
'title_es': 'Primer Post',
'content_en': 'This is my first blog post.',
'content_es': 'Este es mi primer post de blog.'
},
{
'id': 2,
'title_en': 'Flask Tutorial',
'title_es': 'Tutorial de Flask',
'content_en': 'Learn how to create web applications with Flask.',
'content_es': 'Aprende cómo crear aplicaciones web con Flask.'
}
]

@babel.localeselector
def get_locale():
if 'language' in session:
return session['language']
return request.accept_languages.best_match(['en', 'es'])

@app.route('/')
def index():
locale = get_locale()
localized_posts = []

for post in blog_posts:
localized_posts.append({
'id': post['id'],
'title': post[f'title_{locale}'],
'content': post[f'content_{locale}']
})

return render_template('blog.html', posts=localized_posts)

@app.route('/post/<int:post_id>')
def view_post(post_id):
locale = get_locale()
post = next((p for p in blog_posts if p['id'] == post_id), None)

if post:
localized_post = {
'id': post['id'],
'title': post[f'title_{locale}'],
'content': post[f'content_{locale}']
}
return render_template('post.html', post=localized_post)

return render_template('404.html')

@app.route('/language/<lang>')
def set_language(lang):
session['language'] = lang
return redirect(request.referrer or url_for('index'))

@app.context_processor
def inject_date():
return {'current_date': datetime.now().strftime('%Y-%m-%d')}

if __name__ == '__main__':
app.run(debug=True)

Templates:

blog.html:

html
<!DOCTYPE html>
<html>
<head>
<title>{{ _('My Multilingual Blog') }}</title>
</head>
<body>
<header>
<h1>{{ _('My Blog') }}</h1>
<div class="language-selector">
<a href="{{ url_for('set_language', lang='en') }}">English</a> |
<a href="{{ url_for('set_language', lang='es') }}">Español</a>
</div>
</header>

<main>
<h2>{{ _('Recent Posts') }}</h2>
{% for post in posts %}
<article>
<h3><a href="{{ url_for('view_post', post_id=post.id) }}">{{ post.title }}</a></h3>
<p>{{ post.content[:100] }}...</p>
</article>
{% endfor %}
</main>

<footer>
<p>{{ _('Today is %(date)s', date=current_date) }}</p>
</footer>
</body>
</html>

post.html:

html
<!DOCTYPE html>
<html>
<head>
<title>{{ post.title }} - {{ _('My Multilingual Blog') }}</title>
</head>
<body>
<header>
<h1>{{ _('My Blog') }}</h1>
<div class="language-selector">
<a href="{{ url_for('set_language', lang='en') }}">English</a> |
<a href="{{ url_for('set_language', lang='es') }}">Español</a>
</div>
</header>

<main>
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.content }}</p>
</article>

<a href="{{ url_for('index') }}">{{ _('Back to all posts') }}</a>
</main>

<footer>
<p>{{ _('Today is %(date)s', date=current_date) }}</p>
</footer>
</body>
</html>

Best Practices for Flask Internationalization

  1. Use translation functions consistently: Wrap all user-facing strings with gettext or its aliases.

  2. Keep translations organized: Use meaningful contexts and comments in your translation files.

  3. Update translations regularly: Run pybabel extract and pybabel update whenever you modify text.

  4. Test all languages: Manually verify that all translations make sense in context.

  5. Format dates, times, and numbers properly: Use Flask-Babel's formatting functions rather than hardcoding formats.

  6. Consider right-to-left languages: If supporting languages like Arabic or Hebrew, ensure your CSS handles RTL layouts.

  7. Use lazy translations for variables defined outside request context: Use lazy_gettext instead of gettext.

Updating Translations When Content Changes

When you update your application and add new translatable strings, you'll need to update your translations:

bash
# Extract updated strings
pybabel extract -F babel.cfg -o messages.pot .

# Update existing translation files with new strings
pybabel update -i messages.pot -d translations

# Compile translations after updating the .po files
pybabel compile -d translations

Summary

In this tutorial, we've covered:

  1. Setting up Flask-Babel for internationalization
  2. Creating and managing translation files
  3. Selecting the appropriate locale for each request
  4. Translating text in templates and Python code
  5. Handling plurals and dates correctly
  6. Building a practical multilingual blog application

Internationalization makes your application accessible to a global audience, which can significantly increase your user base. While it requires additional effort to implement and maintain translations, the benefits of reaching users in their native languages are substantial.

Additional Resources

Exercises

  1. Add another language (French or German) to the blog application.
  2. Implement a user profile page where users can set their preferred language permanently.
  3. Create a translation management interface for administrators to edit translations directly in the application.
  4. Extend the blog to support date formatting according to the selected locale.
  5. Add translation support for blog post comments.

By practicing these exercises, you'll gain a deeper understanding of Flask internationalization and be better prepared to implement it in your own applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)