Skip to content

blueprints, stages/authenticator_sms: translate before interpolating - #25993

Merged
BeryJu merged 2 commits into
goauthentik:mainfrom
Dev-next-gen:fix/i18n-format-before-gettext
Sep 10, 2026
Merged

BeryJu merged 2 commits into
goauthentik:mainfrom
Dev-next-gen:fix/i18n-format-before-gettext

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

Details

What does this PR change?

Two call sites interpolate a string and then hand the result to gettext:

# authentik/blueprints/api.py
_("User lacks permission to create {model}".format_map({"model": full_model}))

# authentik/stages/authenticator_sms/models.py
_("Use this code to authenticate in authentik: {token}".format_map({"token": token}))

The .format_map() runs before _(), so the string that reaches the catalog lookup is
"User lacks permission to create authentik_providers_saml.samlprovider", not the msgid
"User lacks permission to create {model}". It never matches, and the translations that
are already in locale/ for both of these strings are dead.

This moves the interpolation outside the _() call, which is what
authentik/core/api/applications.py and authentik/tenants/api/settings.py already do:

_("Value for flag {flag_key} needs to be of type {type}.").format(...)

Why is this change needed?

The msgids are extracted and translated in every locale, they just can't be reached. From
locale/fr_FR/LC_MESSAGES/django.po:

#: authentik/blueprints/api.py
msgid "User lacks permission to create {model}"
msgstr "L'utilisateur manque de permission pour créer {model}"

#: authentik/stages/authenticator_sms/models.py
msgid "Use this code to authenticate in authentik: {token}"
msgstr "Utilisez ce code pour s'authentifier à authentik : {token}"

The SMS one is the user-visible half: the OTP text message body always goes out in English,
whatever the brand/user locale is.

How was this tested?

I don't have a full dev environment on this machine, so I checked the thing that actually
breaks rather than the surrounding view code. gettext_lazy/gettext end up in
gettext.GNUTranslations.gettext() against the compiled locale/<lang>/LC_MESSAGES/django.mo,
so I pulled the real _() expression out of each file with ast, evaluated it with _ bound
to the repo's own compiled fr_FR catalog, and compared against the msgstr.

Before:

authentik/blueprints/api.py
  expression : _('User lacks permission to create {model}'.format_map({'model': full_model}))
  fr_FR gives: 'User lacks permission to create authentik_providers_saml.samlprovider'
  expected   : "L'utilisateur manque de permission pour créer authentik_providers_saml.samlprovider"
  -> NOT TRANSLATED

authentik/stages/authenticator_sms/models.py
  expression : _('Use this code to authenticate in authentik: {token}'.format_map({'token': token}))
  fr_FR gives: 'Use this code to authenticate in authentik: 123456'
  expected   : "Utilisez ce code pour s'authentifier à authentik : 123456"
  -> NOT TRANSLATED

AssertionError: 2 string(s) never reach the fr_FR catalog

After:

authentik/blueprints/api.py
  expression : _('User lacks permission to create {model}').format(model=full_model)
  fr_FR gives: "L'utilisateur manque de permission pour créer authentik_providers_saml.samlprovider"
  -> OK

authentik/stages/authenticator_sms/models.py
  expression : _('Use this code to authenticate in authentik: {token}').format(token=token)
  fr_FR gives: "Utilisez ce code pour s'authentifier à authentik : 123456"
  -> OK

all strings translated

For no-regression I ran the same comparison against the en catalog on both revisions:

authentik/blueprints/api.py
  en before: 'User lacks permission to create authentik_providers_saml.samlprovider'
  en after : 'User lacks permission to create authentik_providers_saml.samlprovider'
  identical: True
authentik/stages/authenticator_sms/models.py
  en before: 'Use this code to authenticate in authentik: 123456'
  en after : 'Use this code to authenticate in authentik: 123456'
  identical: True

That matters for authentik/core/tests/test_transactional_applications_api.py, which asserts
on the literal English text of the blueprints message — the output is byte-identical, so it
should still pass.

One note on blueprints/api.py: _ there is gettext_lazy, so .format() on the proxy
forces evaluation and returns a plain str instead of a lazy object. That's the same shape as
tenants/api/settings.py, and both messages are built inside a request, so the active language
is already set. In authenticator_sms/models.py the annotation is already -> str and both
callers do str(self.get_message(token)), so returning a real str is if anything more correct.

Linked issues

None.


While grepping for the pattern I found four more instances of it — policies/password/models.py,
providers/oauth2/api/providers.py, sources/ldap/api/sources.py, sources/oauth/views/callback.py
(and a handful of _(f"...") in admin/files/validation.py and enterprise/, which xgettext can't
extract at all). I left them out because open PRs are currently touching those files and I didn't
want to hand you conflicts over a one-line change. Happy to fold them in here or send a follow-up,
whichever you prefer.

Checklist

  • The project has been linted, built, and tested (make all) — I couldn't run this locally, see above for what I did run
  • The documentation has been updated and formatted (make docs) — no docs affected
  • I have read the AI usage policy.

AI tools used: Claude Code, to sweep the tree for the _("...".format(...)) pattern and to draft
the ast-based reproduction script. The diff, the verification against the real catalogs, and this
description are mine and I've re-run every output pasted above.

`_("...".format(...))` interpolates first and then asks gettext for the
result, so the msgid that gets extracted into locale/ can never be matched
at runtime and the translations already shipped for these two strings are
never used. Move the interpolation outside the gettext call, matching what
core/api/applications.py and tenants/api/settings.py already do.
@Dev-next-gen
Dev-next-gen requested a review from a team as a code owner September 9, 2026 20:33
@netlify

netlify Bot commented Sep 9, 2026

Copy link
Copy Markdown

✅ Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit 14acf38
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/6aa1c2ae47dd4b0008c33a5d
😎 Deploy Preview https://deploy-preview-25993--authentik-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@BeryJu
BeryJu enabled auto-merge (squash) September 10, 2026 11:01
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.82%. Comparing base (890d740) to head (60938a6).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25993      +/-   ##
==========================================
- Coverage   91.86%   91.82%   -0.04%     
==========================================
  Files        1169     1169              
  Lines       75451    75451              
  Branches     4054     4054              
==========================================
- Hits        69310    69285      -25     
- Misses       6099     6124      +25     
  Partials       42       42              
Flag Coverage Δ
conformance 33.75% <0.00%> (ø)
e2e 38.44% <0.00%> (+<0.01%) ⬆️
integration 30.01% <0.00%> (-0.45%) ⬇️
rust 42.92% <ø> (ø)
unit 93.41% <100.00%> (+<0.01%) ⬆️
unit-migrate 93.44% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@BeryJu
BeryJu merged commit 1dfefad into goauthentik:main Sep 10, 2026
137 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants