In a Django project, how do LOCALE_PATHS and each app's locale/ directory differ, and which wins when both translate the same string?
answer
- project catalogs vs reusable-app catalogs
- an ordered list, first wins
- then INSTALLED_APPS order
- Django's own catalog last
basics
~10 sAn app's locale/ directory ships translations with that app, while LOCALE_PATHS lists project-level directories. At runtime LOCALE_PATHS wins, earlier entries first, then apps' locale/ directories in INSTALLED_APPS order, then Django's own catalogs.
solid answer
~40 sDjango merges all `.mo` catalogs for the active language into one in-memory catalog. When several define the same msgid, precedence decides: directories in `LOCALE_PATHS` come first, in list order; then each installed app's `locale/` directory, in `INSTALLED_APPS` order; then Django's built-in `django/conf/locale`. That makes `LOCALE_PATHS` the place for project-wide translations, and for **overriding** a third-party app's or Django's own wording. App `locale/` directories suit reusable apps that carry their own translations. When `makemessages` runs from the project root, a string from an app that has a `locale/` directory goes into that app's `.po`. A string from an app without one goes into the first `LOCALE_PATHS` entry, or the command fails if `LOCALE_PATHS` is empty.
code
python · 15 lines# settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
INSTALLED_APPS = [
"exhibits", # exhibits/locale/de/... wins over tours for the same msgid
"tours",
"django.contrib.admin",
# ...
]
LOCALE_PATHS = [
BASE_DIR / "locale", # project-wide strings and overrides: highest precedence
]go deeper
Recall that translations live in locale/<lang>/LC_MESSAGES and that LOCALE_PATHS adds project-level directories.
Explain the precedence order (LOCALE_PATHS, then apps in INSTALLED_APPS order, then Django) and where makemessages writes each string.
Use LOCALE_PATHS to override third-party wording, and make sure compilemessages sees those paths in the deploy build.
Choose per-app catalogs or one project catalog based on team ownership and how the translation agency works.
## Two kinds of catalog location Every Django catalog lives in the same layout, `<base>/<locale name>/LC_MESSAGES/django.po|mo` (plus `djangojs.*` for JavaScript). What differs is the **base**: - **An app's `locale/` directory**, e.g. `exhibits/locale/de/LC_MESSAGES/django.po`. It travels with the app. This is how reusable and third-party apps ship translations, and how a large project can keep each app's strings next to its code. - **`LOCALE_PATHS`**, a setting that lists extra base directories, typically `[BASE_DIR / "locale"]`. It is for translations that belong to the **project** as a whole: shared templates, the project-level URLconf, or overrides. - **Django's own catalogs** in `django/conf/locale`, which translate the admin, form errors, date names and so on. `LOCALE_PATHS` defaults to an empty list. ## Runtime precedence For the active language, Django builds one merged in-memory catalog. When the same msgid is translated in more than one place, the order decides: | Rank | Source | Tie-break inside the rank | |---|---|---| | 1 (highest) | directories in `LOCALE_PATHS` | earlier entries win | | 2 | each installed app's `locale/` directory | apps earlier in `INSTALLED_APPS` win | | 3 (fallback) | `django/conf/locale` | — | Consequences worth knowing: 1. **Overriding a third-party translation**: put your preferred `msgstr` for that msgid in a catalog under `LOCALE_PATHS`. You never edit the package in `site-packages`. 2. **App order matters**: two apps translating "Collection" differently resolve in favour of the app listed first. 3. **Fallbacks**: a `pt_BR` string with no translation uses the generic `pt` catalog. A string the active language lacks entirely falls back to the `LANGUAGE_CODE` language's translation, and only then to the source text. Directory names use **locale name** notation: `de`, `pt_BR`, `zh_Hans`. That is not the language-code form `pt-br` used in URLs and `LANGUAGES`. `makemessages -l pt-br` doesn't create a catalog; it answers "invalid locale pt-br, did you mean pt_BR?". ## How the merged catalog is built and cached For each language, Django builds one translation object the first time that language is needed in a process. It loads Django's own catalog, then merges in every installed app's `locale/` catalog and finally the `LOCALE_PATHS` catalogs, each later layer overwriting entries from earlier ones. That is how the precedence table above comes about. The object is cached for the life of the process, which is why new `.mo` files need a worker restart in production. The development server's autoreloader watches `.mo` files and clears the cache for you. If the default language (`LANGUAGE_CODE`) has no catalog at all, Django raises an error when building it, so an English-default site relies on Django's own `en` catalog being present. ## Where `makemessages` writes Run from the project root, `makemessages` distributes strings automatically: - a string found under an app that has a `locale/` directory goes into **that app's** `.po` file; - a string from any other file goes into the **first** directory in `LOCALE_PATHS`; - if there is no such directory, it fails with "Unable to find a locale path to store translations… Make sure the 'locale' directory exists in an app or LOCALE_PATHS setting is set." Run from inside an app's root, it writes to that app's `locale/` directory if one exists. It only scans the current directory tree. Packages installed elsewhere are not re-extracted. ## Choosing a layout for a museum guide | Situation | Recommended location | |---|---| | A reusable "audio tours" app you publish separately | the app's own `locale/` | | Exhibit texts in several in-house apps, translated by one agency | either per-app `locale/`, or one `LOCALE_PATHS` catalog for a single hand-off | | Changing the wording of a third-party app or of Django's admin | `LOCALE_PATHS` (it outranks apps and Django) | | Shared base templates at the project level | `LOCALE_PATHS` | A single project catalog gives translators **one file per language**. Per-app catalogs keep ownership clear and avoid merge conflicts between teams. Both are fine, and many projects mix them. ## Compiling and deploying `compilemessages` run from the project root compiles every `locale/` directory it finds in the tree. It also compiles the `LOCALE_PATHS` directories when `DJANGO_SETTINGS_MODULE` is set, which `manage.py` and `--settings` take care of. Run as a bare `django-admin compilemessages` without settings, a `LOCALE_PATHS` directory outside the project tree is silently skipped. That is a classic cause of "the overrides don't apply in production".
- How do you change the German wording of a string that comes from a third-party app?Don't edit the package. Add the same msgid with your `msgstr` to a `.po` file under a directory in `LOCALE_PATHS`, for example `locale/de/LC_MESSAGES/django.po`, and compile it. `LOCALE_PATHS` has the highest precedence, so your translation wins over the app's own catalog and over Django's.
- makemessages fails with "Unable to find a locale path to store translations". What does it mean?It found marked strings in a file that belongs to no directory with a `locale/` folder, and `LOCALE_PATHS` is empty, so there is nowhere to write them. Create a `locale/` directory in that app, or set `LOCALE_PATHS` so strings from such files go into its first entry.
Think of a museum's label desk with three trays: the curator's corrections tray (LOCALE_PATHS), each gallery's own tray (app locale/ directories, checked in gallery order), and the publisher's standard labels (Django's catalog). For any label, the first tray that has a card wins.
saying these in an interview costs you the question
- App locale/ directories override LOCALE_PATHS.
- The last entry in LOCALE_PATHS has the highest precedence.
- Locale directories are named with language codes like pt-br.
- makemessages also re-extracts third-party packages in site-packages.
- To fix a third-party translation you edit the package's .po file.