Skip to content

Commit 63b0fb5

Browse files
authored
Merge pull request #92 from MicroPyramid/dev
feat: add translation support for user-facing strings
2 parents c9c36ca + a6281dc commit 63b0fb5

28 files changed

Lines changed: 3557 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ jobs:
2121
fail-fast: false
2222
matrix:
2323
python-version: ["3.10", "3.11", "3.12", "3.13"]
24-
django: ["4.2", "5.2"]
24+
django: ["4.2", "5.2", "6.1"]
25+
exclude:
26+
# Django 6.x requires Python >=3.12. Without these, uv cannot
27+
# resolve the combination and the leg fails on dependency
28+
# resolution rather than on anything about this package.
29+
- python-version: "3.10"
30+
django: "6.1"
31+
- python-version: "3.11"
32+
django: "6.1"
2533

2634
steps:
2735
- uses: actions/checkout@v7

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
- python-version: "3.10"
6666
django: "4.2"
6767
- python-version: "3.13"
68-
django: "5.2"
68+
django: "6.1"
6969
steps:
7070
- uses: actions/checkout@v7
7171

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ coverage.xml
4949
*.mo
5050
*.pot
5151

52+
# ...except this package's own catalogs, which are source, not build output.
53+
# This negation is load-bearing twice over: hatchling honours .gitignore when
54+
# it builds, so an ignored catalog is also an UNSHIPPED one -- Django then
55+
# finds no locale directory in the installed package and every string falls
56+
# back to English with nothing in the logs to say why. django.pot hit exactly
57+
# that and was caught by test_packaging.py.
58+
#
59+
# .mo is un-ignored for the same reason, ahead of need: no compiled catalog
60+
# ships today (every translation is still a fuzzy machine draft), and
61+
# test_i18n.py fails if one appears. When a language is reviewed and its
62+
# fuzzy flags come off, its .mo has to be committed to reach users.
63+
!django_mfa/locale/django.pot
64+
!django_mfa/locale/*/LC_MESSAGES/*.mo
65+
5266
# Django stuff:
5367
*.log
5468

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,47 @@ Versions follow [PEP 440](https://peps.python.org/pep-0440/). The version in
1212
`pyproject.toml` is the only place it is written; the git tag and the GitHub
1313
Release are derived from it (see [docs/contributing.md](docs/contributing.md)).
1414

15+
## 4.3.0
16+
17+
### Added
18+
19+
- **Translation catalogs.** `django_mfa/locale/` now ships `django.pot` (75
20+
entries) and machine-drafted `.po` files for `de`, `es`, `fr`, `pt_BR`,
21+
`ja` and `zh_Hans`. Every entry is marked `fuzzy`, so gettext ignores it
22+
and users still see English: **no language is live yet**, and a draft only
23+
starts appearing after a human reviews it and removes the flags. See
24+
[docs/translations.md](docs/translations.md). No compiled `.mo` files
25+
ship, because a fully fuzzy catalog compiles to an empty one.
26+
- The Python side is now translatable, matching the templates (which already
27+
were): the verification error, the passkey sign-in error, each adapter's
28+
`verbose_name`, and `Authenticator.Type`'s labels. Wrapping the `Type`
29+
labels needs **no migration** — a `gettext_lazy` proxy compares equal to
30+
the string it wraps, so the autodetector sees no change to `choices`
31+
(verified on Django 4.2, 5.2 and 6.1).
32+
33+
- **Django 6.1 support**, now claimed in the classifiers and exercised in CI
34+
on Python 3.12 and 3.13. Django 6.x requires Python 3.12+, so the matrix
35+
excludes it on 3.10/3.11; those interpreters keep Django 4.2 and 5.2, both
36+
still LTS. No source change was needed — the suite already passed on 6.x.
37+
(Django 6.0 passes too, but is not claimed or tested.)
38+
39+
### Changed
40+
41+
- The PyPI classifier is now `Development Status :: 5 - Production/Stable`,
42+
up from `4 - Beta`.
43+
- `publish.yml`'s pre-release smoke matrix now tests the newest supported
44+
corner as Python 3.13 + Django 6.1, up from 3.13 + 5.2. The oldest corner
45+
(3.10 + 4.2) is unchanged.
46+
47+
### Fixed
48+
49+
- `.gitignore`'s blanket `*.pot`/`*.mo` rules excluded the package's own
50+
catalogs. Because hatchling honours `.gitignore` at build time, an ignored
51+
catalog is also an unshipped one — Django would find no locale directory
52+
in the installed package and silently fall back to English. Negations now
53+
keep `django_mfa/locale/` tracked, and `test_packaging.py` asserts the
54+
catalogs are in the built wheel.
55+
1556
## 4.2.0
1657

1758
### Added

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="PyPI" src="https://img.shields.io/pypi/v/django-mfa.svg"></a>
1111
<a href="https://github.com/MicroPyramid/django-mfa/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/MicroPyramid/django-mfa/actions/workflows/ci.yml/badge.svg"></a>
1212
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Python versions" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue"></a>
13-
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Django versions" src="https://img.shields.io/badge/django-4.2%20%7C%205.2-0C4B33"></a>
13+
<a href="https://pypi.python.org/pypi/django-mfa"><img alt="Django versions" src="https://img.shields.io/badge/django-4.2%20%7C%205.2%20%7C%206.1-0C4B33"></a>
1414
<a href="http://django-mfa.readthedocs.io/en/latest/"><img alt="Docs" src="https://readthedocs.org/projects/django-mfa/badge/?version=latest"></a>
1515
<a href="https://github.com/MicroPyramid/django-mfa/blob/master/LICENSE"><img alt="License" src="https://img.shields.io/github/license/micropyramid/django-mfa.svg"></a>
1616
</p>
@@ -209,13 +209,14 @@ this same API — there's no privileged path.
209209
| | |
210210
|---|---|
211211
| **Python** | 3.10 · 3.11 · 3.12 · 3.13 |
212-
| **Django** | 4.2 LTS · 5.2 LTS |
212+
| **Django** | 4.2 LTS · 5.2 LTS · 6.1 |
213213
| **Database** | Anything Django supports (state is a `JSONField`) |
214214
| **Dependencies** | `fido2`, `qrcode`. TOTP is implemented in-package, not pulled in. |
215215

216-
Every combination in that grid runs the full suite in CI, along with a job that builds
217-
the wheel, installs it into a clean environment, and starts Django against it from
218-
outside the source tree.
216+
Every combination runs the full suite in CI — except Django 6.1 on Python 3.10 or
217+
3.11, which Django itself doesn't support (6.x requires Python 3.12+). Alongside it,
218+
a job builds the wheel, installs it into a clean environment, and starts Django
219+
against it from outside the source tree.
219220

220221
## Documentation
221222

django_mfa/adapters/email.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from django.core.mail import send_mail
1818
from django.template.loader import render_to_string
1919
from django.utils.crypto import salted_hmac
20+
from django.utils.translation import gettext_lazy as _
2021

2122
from django_mfa import ratelimit
2223
from django_mfa.conf import settings as mfa_settings
@@ -100,7 +101,7 @@ def _is_fresh(state):
100101

101102
class EmailAdapter(Adapter):
102103
type = Authenticator.Type.EMAIL
103-
verbose_name = "Emailed code"
104+
verbose_name = _("Emailed code")
104105
supports_multiple = False
105106
supports_enroll = True
106107
counts_as_primary_factor = True

django_mfa/adapters/recovery_codes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import string
44

55
from django.contrib.auth.hashers import check_password, make_password
6+
from django.utils.translation import gettext_lazy as _
67

78
from django_mfa import events
89
from django_mfa.atomic import update_data
@@ -17,7 +18,7 @@
1718

1819
class RecoveryCodesAdapter(Adapter):
1920
type = Authenticator.Type.RECOVERY_CODES
20-
verbose_name = "Recovery codes"
21+
verbose_name = _("Recovery codes")
2122
# Recovery codes are exhaustible and must never be a user's sole second
2223
# factor. They still appear in the verification picker (you can verify with
2324
# one), but must not make primary_enabled_for() non-empty.

django_mfa/adapters/totp.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import re
33
import secrets
44

5+
from django.utils.translation import gettext_lazy as _
6+
57
from django_mfa import totp as totp_mod
68
from django_mfa.atomic import update_data
79
from django_mfa.conf import settings as mfa_settings
@@ -42,7 +44,7 @@ def generate_secret():
4244

4345
class TOTPAdapter(Adapter):
4446
type = Authenticator.Type.TOTP
45-
verbose_name = "Authenticator app"
47+
verbose_name = _("Authenticator app")
4648

4749
def begin_enroll(self, request):
4850
secret = generate_secret()

django_mfa/adapters/webauthn.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# every place this deviates from the brief's hypothesised calls.
1212
import json
1313

14+
from django.utils.translation import gettext_lazy as _
1415
from fido2.server import Fido2Server
1516
from fido2.webauthn import (
1617
AttestedCredentialData,
@@ -68,7 +69,7 @@ def user_entity(user):
6869

6970
class WebAuthnAdapter(Adapter):
7071
type = Authenticator.Type.WEBAUTHN
71-
verbose_name = "Security key or passkey"
72+
verbose_name = _("Security key or passkey")
7273
supports_multiple = True
7374

7475
def _existing_credentials(self, user):

0 commit comments

Comments
 (0)