# How to Translate a Flask App With Flask-Babel | Vitra.ai

> Flask ships with nothing, which means every decision is yours. Here is the conventional setup and the three places it usually leaks for most banks.

**Canonical URL**: https://www.vitra.ai/general/translate-a-flask-app
**Source**: This is the Markdown rendering of https://www.vitra.ai/general/translate-a-flask-app, generated at build time from that page.

---

4 min read

# How to Translate a Flask App With Flask-Babel

Flask ships with nothing, which means every decision is yours. Here is the conventional setup and the three places it usually leaks for most banks.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Senior Product Manager , Vitra.ai
Updated Aug 17, 2026

![How to Translate a Flask App With Flask-Babel](https://www.vitra.ai/static/images/blog/translate-a-flask-app.jpg)

Table of contents

[Nothing built in, which is the Flask philosophy](#nothing-built-in-which-is-the-flask-philosophy)

[The standard setup](#the-standard-setup)

[Lazy strings matter here too](#lazy-strings-matter-here-too)

[Choose the locale from the URL, not the header](#choose-the-locale-from-the-url-not-the-header)

[What Flask-Babel does not cover](#what-flask-babel-does-not-cover)

[Numbers, dates and currency](#numbers-dates-and-currency)

[Keeping it running](#keeping-it-running)

[Blueprints and larger apps](#blueprints-and-larger-apps)

[Testing the locale selector](#testing-the-locale-selector)

[Where to start](#where-to-start)

[FAQ](#faq)

Contributors

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Senior Product Manager

Subscribe to our newsletter

Subscribe

> **Quick answer —** Flask has no built-in internationalisation, so the standard route is Flask-Babel: gettext under the hood, a locale selector, and extraction through Babel's configuration. Everything beyond templates and view code is left to you.

## Nothing built in, which is the Flask philosophy

[Django](https://www.vitra.ai/general/translate-a-django-site) decides for you. Flask does not, so the first step is choosing Flask-Babel and wiring it, rather than switching on a framework feature. That means the conventions are yours to hold. Nothing stops a developer hard-coding a string, because nothing in the framework is watching.

## The standard setup

Initialise Babel with the app, define a locale selector that reads the URL, session or `Accept-Language` header, wrap strings in `gettext` or `_()`, and use the same in Jinja templates.

Extraction runs through `babel.cfg`, which tells Babel which files to scan and how. A misconfigured `babel.cfg` that skips your templates directory produces a catalogue containing only Python strings, and nobody notices until the pages render in English.

## Lazy strings matter here too

Anything defined at module import — form field labels in a WTForms class, a constant, an enum — needs `lazy_gettext`. The eager version resolves before a request exists and pins the language for the process.

This is the same trap as Django's and it is easier to hit in Flask, because there is no framework guidance nudging you toward the lazy version.

## Choose the locale from the URL, not the header

`Accept-Language` is convenient and invisible to search engines. If the site needs to rank, the language belongs in the path, with the header as a fallback for the first visit only.

That is a routing decision made early or an expensive retrofit made late.

## What Flask-Babel does not cover

Database content. Emails sent from the app, which need the recipient's language rather than the request's — often no request exists at all when a background job sends them. API error messages consumed by a separate front end. Anything in static JavaScript.

The email case is the one that bites: a Celery task has no request context, so the locale selector returns the default and every notification goes out in English.

## Numbers, dates and currency

Babel handles formatting as well as translation, and using it is worth the small effort. Hand-formatted dates are the other half of a localized interface that still feels foreign.

## Keeping it running

Add extraction and compilation to CI. A catalogue that is only updated when someone remembers is a catalogue that is permanently a release behind.

[translation memory](https://www.vitra.ai/features/translation-memory) keeps the recurring cost proportional to the diff, and [quality control](https://www.vitra.ai/features/quality-control) verifies placeholders across the round trip.

Configuration details are on the [Flask integration](https://www.vitra.ai/integration/flask) page.

## Blueprints and larger apps

Once an app is split into blueprints, extraction has to cover every blueprint's templates and modules. A `babel.cfg` written when the app had one package silently stops covering the ones added later.

Re-check the extraction config whenever the app's structure changes. The failure is silent and looks like a translation gap rather than a configuration one.

## Testing the locale selector

The selector is ordinary Python and deserves a test. Assert that a URL prefix wins over a session value, that the session wins over the header, and that an unsupported language falls back rather than erroring.

Three small tests prevent the class of bug where one user's language leaks into another's response under load.

## Where to start

Check that `babel.cfg` actually includes your templates directory. A surprising share of half-translated Flask apps come down to that one line.

## FAQ

**Does Flask have built-in internationalisation?** No. Flask ships with nothing, so the conventional route is Flask-Babel, which provides gettext, a locale selector and extraction through a babel.cfg configuration file.

**Why are my Flask templates untranslated?** Usually because babel.cfg does not include the templates directory, so extraction produces a catalogue containing only Python strings. Nothing errors, and the pages just render in English.

**Why do emails from my Flask app go out in English?** Because a background job has no request context, so the locale selector falls back to the default. The recipient's language has to be passed explicitly to the task rather than read from the request.

**Should locale come from the URL or the Accept-Language header?** From the URL if the site needs to rank. Header-based selection is invisible to search engines because every language shares one address, so the header is best used only as a first-visit fallback.

Our blog

## Lastest blog posts

Tool and strategies modern teams need to help their companies grow.

Automotive

[Automotive Brochure Localization by Market](https://www.vitra.ai/automotive/automotive-brochure-localization)
A car brochure is a spec grid, a legal footer and a photo library, all market-specific. What actually has to change, and why the layout decides the schedule.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

Automotive

[Automotive Campaign Localization Across Markets](https://www.vitra.ai/automotive/automotive-campaign-localization)
Campaigns run through national companies and dealer networks, so one master becomes hundreds of files. Where the offer text and the disclaimers actually break.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

Automotive

[Car Service Manual Translation for Technicians](https://www.vitra.ai/automotive/automotive-service-manual-translation)
A workshop manual is read mid-repair by someone with the car on a lift. What that demands of procedures, torque figures and fault codes, in every language.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

[View all posts](https://www.vitra.ai/blog/page/1)

---

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "Article",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://www.vitra.ai/general/translate-a-flask-app"
  },
  "headline": "How to Translate a Flask App With Flask-Babel",
  "image": [
    {
      "@type": "ImageObject",
      "url": "https://www.vitra.ai/static/images/blog/translate-a-flask-app.jpg"
    }
  ],
  "datePublished": "2026-08-17T00:00:00.000Z",
  "dateModified": "2026-08-17T00:00:00.000Z",
  "author": [
    {
      "@type": "Person",
      "name": "Samhitha J Bhatt"
    }
  ],
  "publisher": {
    "@type": "Organization",
    "name": "Vitra.ai",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.vitra.ai/static/images/vitra-v-logo.png"
    }
  },
  "description": "Flask ships with nothing, which means every decision is yours. Here is the conventional setup and the three places it usually leaks for most banks."
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://www.vitra.ai"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "General",
      "item": "https://www.vitra.ai/general"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "How to Translate a Flask App With Flask-Babel",
      "item": "https://www.vitra.ai/general/translate-a-flask-app"
    }
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does Flask have built-in internationalisation?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. Flask ships with nothing, so the conventional route is Flask-Babel, which provides gettext, a locale selector and extraction through a babel.cfg configuration file."
      }
    },
    {
      "@type": "Question",
      "name": "Why are my Flask templates untranslated?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Usually because babel.cfg does not include the templates directory, so extraction produces a catalogue containing only Python strings. Nothing errors, and the pages just render in English."
      }
    },
    {
      "@type": "Question",
      "name": "Why do emails from my Flask app go out in English?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Because a background job has no request context, so the locale selector falls back to the default. The recipient's language has to be passed explicitly to the task rather than read from the request."
      }
    },
    {
      "@type": "Question",
      "name": "Should locale come from the URL or the Accept-Language header?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "From the URL if the site needs to rank. Header-based selection is invisible to search engines because every language shares one address, so the header is best used only as a first-visit fallback."
      }
    }
  ]
}
```
