Flask App Tutorial on Internationalization

Let your Python Flask app say hi to the world in any language! Our Flask app tutorial will walk you through the process from start to finish!

Here comes another great i18n guide in Python! Our complete Flask app tutorial will show you how to implement internationalization and localization in your Flask application. Thereby, we will be making full use of the locale (via the glob and json modules) to create a multilingual analytic dashboard. All of them are part of Python’s standard library, except for Flask, and the language files are stored in the nested JSON format. Ready to start?

The Setup

First and foremost, check out our GitHub repository for the complete code used in this tutorial. You can clone it via Git or download it directly as a zip file.
It is highly recommended to create a virtual environment before you continue with the installation. Run the following command in the terminal.

pip install flask

Language Files

German (de_DE)

In your project directory, create a new folder called language. Inside the folder, create a new JSON file called de_DE.json. Each JSON file represents a language that contains all the translations for your application. In this case, I have the translation for German. Append the following data inside it.

{
  "last_updated": "Zuletzt aktualisiert am",
  "dashboard_title": "Mein Dashboard",
  "message": "Nachrichten",
  "view": "Views",
  "share": "Aktien",
  "user": "Benutzer",
  "general_stat": "Allgemeine Statistiken",
  "new_visitor": "Neue Besucher",
  "new_user": "Neue Benutzer",
  "country": "Länder",
  "country_list": {"germany": "Deutschland", "united_kingdom": "Britien", "russia": "Russland", "spain": "Spanien", "india": "Indien", "france": "Frankreich"}
}

English/Singapore (en_SG)

Repeat the same process, and this time, I am going to use the following data and name it en_SG.json.

{
  "last_updated": "Last updated on",
  "dashboard_title": "My Dashboard",
  "message": "Messages",
  "view": "Views",
  "share": "Shares",
  "user": "Users",
  "general_stat": "General Stats",
  "new_visitor": "New Visitors",
  "new_user": "New Users",
  "country": "Countries",
  "country_list": {"germany": "Germany", "united_kingdom": "UK", "russia": "Russia", "spain": "Spain", "india": "India", "france": "France"}
}

The HTML File

To keep things simple, this tutorial will use a modified version of an analytical dashboard HTML template made by W3.CSS. This is how it looks like in full view. It will resize on its own depending on the screen resolution of your device.
Analytical dashboard HTML template made by W3.CSS | Phrase
We are going to make a lite version of it, displaying just the dashboard, without the navigation menu. In the root directory, create a new folder called templates. Inside the folder, create a new file called index.html. Append the following code to it.

<!DOCTYPE html>
<html>
  <title>APP</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <style>
    html,body,h1,h2,h3,h4,h5 {font-family: "Raleway", sans-serif}
  </style>
  <body class="w3-light-grey">
    <!-- Top container -->
    <div class="w3-bar w3-top w3-black w3-large" style="z-index:4">
      <button class="w3-bar-item w3-button w3-hide-large w3-hover-none w3-hover-text-light-grey">APP</button>
      <span class="w3-bar-item w3-right">{{ last_updated }} {{ update_time }}</span>
    </div>
    <!-- !PAGE CONTENT! -->
    <div class="w3-main" style="margin-top:43px;">
      <!-- Header -->
      <header class="w3-container" style="padding-top:22px">
        <h5><b><i class="fa fa-dashboard"></i> {{ dashboard_title }}</b></h5>
      </header>
      <div class="w3-row-padding w3-margin-bottom">
        <div class="w3-quarter">
          <div class="w3-container w3-red w3-padding-16">
            <div class="w3-left"><i class="fa fa-comment w3-xxxlarge"></i></div>
            <div class="w3-right">
              <h3>{{ stats.message }}</h3>
            </div>
            <div class="w3-clear"></div>
            <h4>{{ message }}</h4>
          </div>
        </div>
        <div class="w3-quarter">
          <div class="w3-container w3-blue w3-padding-16">
            <div class="w3-left"><i class="fa fa-eye w3-xxxlarge"></i></div>
            <div class="w3-right">
              <h3>{{ stats.view }}</h3>
            </div>
            <div class="w3-clear"></div>
            <h4>{{ view }}</h4>
          </div>
        </div>
        <div class="w3-quarter">
          <div class="w3-container w3-teal w3-padding-16">
            <div class="w3-left"><i class="fa fa-share-alt w3-xxxlarge"></i></div>
            <div class="w3-right">
              <h3>{{ stats.share }}</h3>
            </div>
            <div class="w3-clear"></div>
            <h4>{{ share }}</h4>
          </div>
        </div>
        <div class="w3-quarter">
          <div class="w3-container w3-orange w3-text-white w3-padding-16">
            <div class="w3-left"><i class="fa fa-users w3-xxxlarge"></i></div>
            <div class="w3-right">
              <h3>{{ stats.user }}</h3>
            </div>
            <div class="w3-clear"></div>
            <h4>{{ user }}</h4>
          </div>
        </div>
      </div>
      <div class="w3-container">
        <h5>{{ general_stat }}</h5>
        <p>{{ new_visitor }}</p>
        <div class="w3-grey">
          <div class="w3-container w3-center w3-padding w3-green" style="width:25%">+25%</div>
        </div>
        <p>{{ new_user }}</p>
        <div class="w3-grey">
          <div class="w3-container w3-center w3-padding w3-orange" style="width:50%">50%</div>
        </div>
      </div>
      <hr>
      <div class="w3-container">
        <h5>{{ country }}</h5>
        <table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
          <tr>
            <td>{{ country_list.germany }}</td>
            <td>{{ currencies.germany }}</td>
          </tr>
          <tr>
            <td>{{ country_list.united_kingdom }}</td>
            <td>{{ currencies.united_kingdom }}</td>
          </tr>
          <tr>
            <td>{{ country_list.russia }}</td>
            <td>{{ currencies.russia }}</td>
          </tr>
          <tr>
            <td>{{ country_list.spain }}</td>
            <td>{{ currencies.spain }}</td>
          </tr>
          <tr>
            <td>{{ country_list.india }}</td>
            <td>{{ currencies.india }}</td>
          </tr>
          <tr>
            <td>{{ country_list.france }}</td>
            <td>{{ currencies.france }}</td>
          </tr>
        </table>
        <br>
      </div>
    </div>
  </body>
</html>

The Flask Server

Import

Once you are done with it, create a new Python file called mydashboard.py in the root directory. Add the following import declaration at the top of your Python file.

from flask import Flask, render_template
import locale
import datetime
import glob
import json

Main

Next, declare the variable for Flask and the main function. Feel free to modify the port based on your preferences.

app = Flask(__name__)
if __name__ == '__main__':
  app.run('0.0.0.0', port=5000)

Inside the main function, declare and initialize the locale for our project. Set it to empty string if you intend to use the current locale of your machine.

app_language = 'en_SG'
locale.setlocale(locale.LC_ALL, app_language)

Variable Initialization

The next step is to initialize the following variables:

languages = {}
stats = {}
currencies = {}
date_format = "%d %b %Y %H:%M:%S %Z"
last_updated_time = ""

Let us explain their functions in greater detail:

  • languages – a dictionary to hold all the translated data from language files,
  • stats  – a dictionary to store relevant information, such as number of views, number of messages, etc.,
  • currencies – a dictionary that will contain information related to currency,
  • date_format – format for displaying the last updated datetime at the top of the web page,
  • last_updated_time – a datetime string that represent the last updated datetime.

Loading Language Files via glob and json

We are going to use glob to search the language folder for all the language files that we have created earlier. After that, we will use the json module to load the JSON data for all of the languages automatically. You just need to create a new JSON file in the language folder whenever you intend to add a new language to the application.

language_list = glob.glob("language/*.json")
  for lang in language_list:
    filename = lang.split('\\')
    lang_code = filename[1].split('.')[0]
    with open(lang, 'r', encoding='utf8') as file:
      languages[lang_code] = json.loads(file.read())

Data and Mapping

In the actual use case, you should have your own data for the application. For this tutorial, I am going to use some randomly generated data. Beforehand, you should create the following functions to help parsing our data based on the locale that we have set previously.

def get_stats(input):
  return locale.format_string('%d', input)
def get_currencies(input):
  return locale.currency(input, international=True)

Subsequently, add the following function that will parse the data and store them in their respective global variables.

def fetch_data():
  global stats
  global currencies
  global data_updated_time
  stat = {"message": 22450, "view": 578902, "share": 442, "user": 1824}
  currency = {"germany": 32.7, "united_kingdom": 16.5, "russia": 14.3, "spain": 10.8, "india": 7.6, "france": 4.9}
  stats = {k: get_stats(v) for k, v in stat.items()}
  currencies = {k: get_currencies(v) for k, v in currency.items()}
  data_updated_time = datetime.datetime.now().strftime(date_format)

Head over to the main function and call the fetch_data() function just right before the app.run() function:

if __name__ == '__main__':
  #... contains the rest of the code that we have defined earlier
  fetch_data()
  app.run('0.0.0.0', port=5000)

Route

Next, specify a new route for our dashboard with the respective global variables as parameters. If the user specified a non-supported language, we would default it to the language used by our app. In this case, it is en_SG.

@app.route('/dashboard/<language>')
def dashboard(language):
  if(language not in language_dict):
    language = app_language
  return render_template('index.html', **languagesen, stats = stats, currencies = currencies, update_time = data_updated_time)

One thing to note is that the variables are passed in two different ways:

  1. **languages[app_language] helps to unpack the dictionary into key = value pairs; given dashboard_title = “My Dashboard”, you can access them via {{ dashboard_title }} in the HTML file.
  2. stats = stats is a variable passed as a dictionary. Given message = “Message”, you need to access it via {{ stats.message }} in the HTML file.

The final code for mydashboard.py is as follows:

from flask import Flask, render_template
import locale
import datetime
import glob
import json
app = Flask(__name__)
def get_stats(input):
  return locale.format_string('%d', input)
def get_currencies(input):
  return locale.currency(input, international=True)
def fetch_data():
  global stats
  global currencies
  global last_updated_time
  stat = {"message": 22450, "view": 578902, "share": 442, "user": 1824}
  currency = {"germany": 32.7, "united_kingdom": 16.5, "russia": 14.3, "spain": 10.8, "india": 7.6, "france": 4.9}
  stats = {k: get_stats(v) for k, v in stat.items()}
  currencies = {k: get_currencies(v) for k, v in currency.items()}
  last_updated_time = datetime.datetime.now().strftime(date_format)
@app.route('/dashboard/<language>')
def dashboard(language):
  if(language not in language_dict):
    language = app_language
  return render_template('index.html', **languagesen, stats = stats, currencies = currencies, update_time = last_updated_time)
if __name__ == '__main__':
  app_language = 'en_SG'
  locale.setlocale(locale.LC_ALL, app_language)
  languages = {}
  stats = {}
  currencies = {}
  date_format = "%d %b %Y %H:%M:%S %Z"
  last_updated_time = ""
  language_list = glob.glob("language/*.json")
  for lang in language_list:
    filename = lang.split('\\')
    lang_code = filename[1].split('.')[0]
    with open(lang, 'r', encoding='utf8') as file:
      languages[lang_code] = json.loads(file.read())
  fetch_data()
  app.run('0.0.0.0', port=5000)

Once you are done with it, run the following command in the terminal to start your Flask application. Make sure that you are calling from the root directory.

python mydashboard.py

Open a browser and go to the URL below. Modify it accordingly if you had specified a different port.

localhost:5000/dashboard/en_SG

You should see the following web page:
analytical dashboard HTML template in English | Phrase
Let’s change the URL as follows:

localhost:5000/dashboard/de_DE

Here is the German version of our web page:
Analytical dashboard HTML template in German | Phrase

And This Is the End! Still Not Enough?

We are happy you were able to complete our Flask tutorial on internationalization. You should now be able to build a multilingual Flask application on your own.
As soon as you are ready to add even more languages to your app, think about streamlining your i18n process by signing up for Phrase, the leanest and most reliable translation management platform on the market.
Made for developers by developers, Phrase will equip 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 with more context for higher translation quality.

Last but not least, feel free to give the following Python guides a read as well:

Keep exploring

Photo-realistic sheet music featuring developer-style translation code in place of musical notes. The staff lines show snippets like t('auth.signin.button') and JSON structures, combining the aesthetics of musical notation with programming syntax to illustrate the idea of “composable localization.”

Blog post

Localization as code: a composable approach to localization

Why is localization still a manual, disconnected process in a world where everything else is already “as code”? Learn how a composable, developer-friendly approach brings localization into your CI/CD pipeline, with automation, observability, and Git-based workflows built in.

A woman in a light sweater sits in a home office, focused on her laptop, representing a developer or content manager working on WordPress localization tasks in a calm, professional environment.

Blog post

How to build a scalable WordPress i18n workflow

WordPress powers the web, but translating it well takes more than plugins. Discover how to build a scalable localization workflow using gettext, best practices, and the Phrase plugin.

Blog post

Localizing Unity games with the official Phrase plugin

Want to localize your Unity game without the CSV chaos? Discover how the official Phrase Strings Unity plugin simplifies your game’s localization workflow—from string table setup to pulling translations directly into your project. Whether you’re building for German, Serbian, or beyond, this guide shows how to get started fast and localize like a pro.

Blog post

Internationalization beyond code: A developer’s guide to real-world language challenges

Discover how language affects your UI. From text expansion to pluralization, this guide explores key i18n pitfalls and best practices for modern web developers.

A digital artwork featuring the Astro.js logo in bold yellow and purple tones, floating above Earth's horizon with a stunning cosmic nebula in the background. The vibrant space setting symbolizes the global and scalable nature of Astro’s localization capabilities, reinforcing the article’s focus on internationalization in web development.

Blog post

Astro.js localization part 2: dynamic content localization

Learn how to localize your Astro.js website with static and dynamic content translation. Explore Astro’s built-in i18n features and Paraglide for handling UI elements, navigation, and dynamic text seamlessly.