Django
Localhero.ai translates Django gettext .po files on every pull request. New and changed strings are translated and committed back to the same PR, so you always have on-brand translations ready to deploy with each PR.
Prerequisites
- A Django project with gettext catalogs (
locale/or similar) - Node 18 or later, to run the CLI through
npx - A Localhero.ai API key
Get an API key from your account.
Setup
1. Initialize the project
Run the CLI in your project root. No install step, and it detects Django from your manage.py:
npx @localheroai/cli init
For a detected Django project it proposes the **/*.po pattern and the django workflow, and looks for your catalog directory among translations/, locale/ and locales/, proposing the first one that exists. Per-app <app>/locale/ layouts are not found automatically, so point --path at them during init, or edit translationFiles.paths afterwards.
2. Check the generated config
init writes a localhero.json in your project root. For a Django project with catalogs in locale/ it comes out like this:
{
"schemaVersion": "1.0",
"projectId": "your-project-id",
"sourceLocale": "en",
"outputLocales": ["sv", "de", "es"],
"translationFiles": {
"paths": ["locale/"],
"pattern": "**/*.po",
"workflow": "django"
}
}
Commit this file. The django workflow applies its Django-specific handling automatically; an optional django block in the config can override those defaults. Fields are documented in Project Setup.
3. Add the GitHub Action workflow
init offers to create the workflow for you, running localheroai/localhero-action. Add your API key as the repository secret LOCALHERO_API_KEY on GitHub.
One thing to add yourself: the generated file has no extract step. Put makemessages before the Localhero.ai step, as below. Without it the job runs green and translates nothing, because only strings already in your catalogs are visible to it.
Adjust the paths: filter if your .po files live somewhere other than the config above:
name: Localhero.ai - Automatic I18n translation
on:
pull_request:
paths:
- "locale/**"
- "templates/**"
- "**/*.py"
- "localhero.json"
repository_dispatch:
types: [localhero-sync]
workflow_dispatch:
concurrency:
group: translate-${{ github.event.client_payload.branch || github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
translate:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.client_payload.branch || github.head_ref || github.ref_name }}
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Extract messages
run: |
sudo apt-get install -y -qq gettext
pip install -r requirements.txt
python manage.py makemessages --all
- uses: localheroai/localhero-action@v1
with:
api-key: ${{ secrets.LOCALHERO_API_KEY }}
fetch-depth: 0 is important: the Action diffs against the base branch, and a shallow checkout has nothing to compare. See the GitHub Actions guide for all inputs.
The extract step means a PR that only touches templates or Python code still gets its new strings translated. Adjust the makemessages invocation if your project wraps it in its own command. Prefer extracting locally before each PR? Drop that step and the source-file triggers.
How it works
- You open a pull request that touches your templates, Python code or catalogs.
- The workflow runs
makemessagesto extract new strings into the catalogs, then the GitHub Action diffs the branch against your base branch and translates only the messages that changed or are still untranslated. Updated and removed messages are synced to the Localhero.ai backend, so the web interface stays in sync with your codebase and PRs. - It commits the updated
.pofiles to the same PR, so the diff you review contains both the code and its translations. - You review and edit the translations in the dashboard, and edits sync back to the branch.
- The translated
.pofiles still needpython manage.py compilemessagesto become the.mofiles Django serves. Most projects already run it during build or deploy. If yours does not, add it to your deploy step or to this workflow after the translation step.
Nothing in your app has to know Localhero.ai exists. The .po catalog is the integration, so your gettext calls and template tags stay as they are.
Rather not automatically translate on PRs? Run makemessages then npx @localheroai/cli translate locally to fill in every missing message, and commit the result yourself. See the CLI reference.
Django and .po specifics
Locale directory layout
The gettext layout Django uses, <locale>/LC_MESSAGES/<domain>.po, works without configuration. The .po filename is the domain, not the language, so the directory above LC_MESSAGES is read as the locale.
Non-standard codes are trusted too: sv_FI_custom/LC_MESSAGES/django.po is read as sv_FI_custom, no localeRegex needed.
Excluding messages
Exclusions work per file rather than per message. Use translationFiles.ignore with a glob to leave a catalog alone. ignoreKeys, which targets individual keys, applies to YAML and JSON projects and has no effect on .po files.
For a specific set of messages, keep them in their own domain or directory and exclude that path.
Renaming and removing messages
In the pull request flow this takes care of itself: messages removed in the diff are reported to the backend along with the rest of the changes. The manual push command is more conservative and never deletes, so after rewording or removing messages outside a PR, run push --prune --force to make the remote side match your files.
This comes up more in Django than in key-based formats, because the msgid is the source string itself. Editing English copy creates a new message rather than updating an existing one.
Plurals
Translations come back in each target language's own CLDR plural categories rather than a copy of the source language's set. A language like Polish gets its extra plural forms even when your English source only distinguishes singular from plural.
Questions we get
Can I translate some languages myself?
Yes. Turn off Auto-translate for that language in your project settings, and Localhero.ai writes nothing for it. The CLI reports those languages as skipped, and you keep them under your own process while the rest are translated automatically.
Will it translate my whole catalog on every PR?
No, only messages that changed or are missing. On a pull request the diff is against the PR's base branch. Locally, --changed-only compares against main unless translationFiles.baseBranch says otherwise.
How do I stop it running on a specific PR?
Add the skip-translation label. The Action also skips draft PRs and its own commits. The details are in the GitHub Actions guide.
Longer walkthrough with a real codebase: localizing a Django app. For the CI side on its own, see translating .po files in CI.
Last updated
Ready to try it?
Get setup in a couple of minutes. No credit card required.