diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa8bc27..afc0059 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,15 @@ jobs: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] - django: ["4.2", "5.2"] + django: ["4.2", "5.2", "6.1"] + exclude: + # Django 6.x requires Python >=3.12. Without these, uv cannot + # resolve the combination and the leg fails on dependency + # resolution rather than on anything about this package. + - python-version: "3.10" + django: "6.1" + - python-version: "3.11" + django: "6.1" steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9b29a5c..7989ba6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -65,7 +65,7 @@ jobs: - python-version: "3.10" django: "4.2" - python-version: "3.13" - django: "5.2" + django: "6.1" steps: - uses: actions/checkout@v7 diff --git a/.gitignore b/.gitignore index 29370c6..6a906d1 100755 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,20 @@ coverage.xml *.mo *.pot +# ...except this package's own catalogs, which are source, not build output. +# This negation is load-bearing twice over: hatchling honours .gitignore when +# it builds, so an ignored catalog is also an UNSHIPPED one -- Django then +# finds no locale directory in the installed package and every string falls +# back to English with nothing in the logs to say why. django.pot hit exactly +# that and was caught by test_packaging.py. +# +# .mo is un-ignored for the same reason, ahead of need: no compiled catalog +# ships today (every translation is still a fuzzy machine draft), and +# test_i18n.py fails if one appears. When a language is reviewed and its +# fuzzy flags come off, its .mo has to be committed to reach users. +!django_mfa/locale/django.pot +!django_mfa/locale/*/LC_MESSAGES/*.mo + # Django stuff: *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 6280a72..bcc6942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,47 @@ Versions follow [PEP 440](https://peps.python.org/pep-0440/). The version in `pyproject.toml` is the only place it is written; the git tag and the GitHub Release are derived from it (see [docs/contributing.md](docs/contributing.md)). +## 4.3.0 + +### Added + +- **Translation catalogs.** `django_mfa/locale/` now ships `django.pot` (75 + entries) and machine-drafted `.po` files for `de`, `es`, `fr`, `pt_BR`, + `ja` and `zh_Hans`. Every entry is marked `fuzzy`, so gettext ignores it + and users still see English: **no language is live yet**, and a draft only + starts appearing after a human reviews it and removes the flags. See + [docs/translations.md](docs/translations.md). No compiled `.mo` files + ship, because a fully fuzzy catalog compiles to an empty one. +- The Python side is now translatable, matching the templates (which already + were): the verification error, the passkey sign-in error, each adapter's + `verbose_name`, and `Authenticator.Type`'s labels. Wrapping the `Type` + labels needs **no migration** — a `gettext_lazy` proxy compares equal to + the string it wraps, so the autodetector sees no change to `choices` + (verified on Django 4.2, 5.2 and 6.1). + +- **Django 6.1 support**, now claimed in the classifiers and exercised in CI + on Python 3.12 and 3.13. Django 6.x requires Python 3.12+, so the matrix + excludes it on 3.10/3.11; those interpreters keep Django 4.2 and 5.2, both + still LTS. No source change was needed — the suite already passed on 6.x. + (Django 6.0 passes too, but is not claimed or tested.) + +### Changed + +- The PyPI classifier is now `Development Status :: 5 - Production/Stable`, + up from `4 - Beta`. +- `publish.yml`'s pre-release smoke matrix now tests the newest supported + corner as Python 3.13 + Django 6.1, up from 3.13 + 5.2. The oldest corner + (3.10 + 4.2) is unchanged. + +### Fixed + +- `.gitignore`'s blanket `*.pot`/`*.mo` rules excluded the package's own + catalogs. Because hatchling honours `.gitignore` at build time, an ignored + catalog is also an unshipped one — Django would find no locale directory + in the installed package and silently fall back to English. Negations now + keep `django_mfa/locale/` tracked, and `test_packaging.py` asserts the + catalogs are in the built wheel. + ## 4.2.0 ### Added diff --git a/README.md b/README.md index 81521f3..39f7d5c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ PyPI CI Python versions - Django versions + Django versions Docs License

@@ -209,13 +209,14 @@ this same API — there's no privileged path. | | | |---|---| | **Python** | 3.10 · 3.11 · 3.12 · 3.13 | -| **Django** | 4.2 LTS · 5.2 LTS | +| **Django** | 4.2 LTS · 5.2 LTS · 6.1 | | **Database** | Anything Django supports (state is a `JSONField`) | | **Dependencies** | `fido2`, `qrcode`. TOTP is implemented in-package, not pulled in. | -Every combination in that grid runs the full suite in CI, along with a job that builds -the wheel, installs it into a clean environment, and starts Django against it from -outside the source tree. +Every combination runs the full suite in CI — except Django 6.1 on Python 3.10 or +3.11, which Django itself doesn't support (6.x requires Python 3.12+). Alongside it, +a job builds the wheel, installs it into a clean environment, and starts Django +against it from outside the source tree. ## Documentation diff --git a/django_mfa/adapters/email.py b/django_mfa/adapters/email.py index 73ad293..edb03ef 100644 --- a/django_mfa/adapters/email.py +++ b/django_mfa/adapters/email.py @@ -17,6 +17,7 @@ from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils.crypto import salted_hmac +from django.utils.translation import gettext_lazy as _ from django_mfa import ratelimit from django_mfa.conf import settings as mfa_settings @@ -100,7 +101,7 @@ def _is_fresh(state): class EmailAdapter(Adapter): type = Authenticator.Type.EMAIL - verbose_name = "Emailed code" + verbose_name = _("Emailed code") supports_multiple = False supports_enroll = True counts_as_primary_factor = True diff --git a/django_mfa/adapters/recovery_codes.py b/django_mfa/adapters/recovery_codes.py index ac7f27c..2f7ec1e 100644 --- a/django_mfa/adapters/recovery_codes.py +++ b/django_mfa/adapters/recovery_codes.py @@ -3,6 +3,7 @@ import string from django.contrib.auth.hashers import check_password, make_password +from django.utils.translation import gettext_lazy as _ from django_mfa import events from django_mfa.atomic import update_data @@ -17,7 +18,7 @@ class RecoveryCodesAdapter(Adapter): type = Authenticator.Type.RECOVERY_CODES - verbose_name = "Recovery codes" + verbose_name = _("Recovery codes") # Recovery codes are exhaustible and must never be a user's sole second # factor. They still appear in the verification picker (you can verify with # one), but must not make primary_enabled_for() non-empty. diff --git a/django_mfa/adapters/totp.py b/django_mfa/adapters/totp.py index 6dcac0c..0fe6a09 100644 --- a/django_mfa/adapters/totp.py +++ b/django_mfa/adapters/totp.py @@ -2,6 +2,8 @@ import re import secrets +from django.utils.translation import gettext_lazy as _ + from django_mfa import totp as totp_mod from django_mfa.atomic import update_data from django_mfa.conf import settings as mfa_settings @@ -42,7 +44,7 @@ def generate_secret(): class TOTPAdapter(Adapter): type = Authenticator.Type.TOTP - verbose_name = "Authenticator app" + verbose_name = _("Authenticator app") def begin_enroll(self, request): secret = generate_secret() diff --git a/django_mfa/adapters/webauthn.py b/django_mfa/adapters/webauthn.py index 1872e23..784a705 100644 --- a/django_mfa/adapters/webauthn.py +++ b/django_mfa/adapters/webauthn.py @@ -11,6 +11,7 @@ # every place this deviates from the brief's hypothesised calls. import json +from django.utils.translation import gettext_lazy as _ from fido2.server import Fido2Server from fido2.webauthn import ( AttestedCredentialData, @@ -68,7 +69,7 @@ def user_entity(user): class WebAuthnAdapter(Adapter): type = Authenticator.Type.WEBAUTHN - verbose_name = "Security key or passkey" + verbose_name = _("Security key or passkey") supports_multiple = True def _existing_credentials(self, user): diff --git a/django_mfa/locale/de/LC_MESSAGES/django.po b/django_mfa/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..0e3d11f --- /dev/null +++ b/django_mfa/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,419 @@ +# German translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "%(code_length)s-stelliger Code" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "Noch %(remaining)s Code übrig." +msgstr[1] "Noch %(remaining)s Codes übrig." + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "Ihrem Konto wurde eine neue Zwei-Faktor-Methode hinzugefügt" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "Ihrem Konto wurde eine neue Zwei-Faktor-Methode hinzugefügt: %(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "Soeben wurde ein Wiederherstellungscode zur Anmeldung bei Ihrem Konto verwendet." + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "Ein Wiederherstellungscode wurde zur Anmeldung verwendet" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "Eine Zwei-Faktor-Methode wurde von Ihrem Konto entfernt" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "Methode hinzufügen" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "Fügen Sie unten eine Methode hinzu, um fortzufahren. Bis dahin ist der Rest der Website nicht verfügbar." + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "Fügen Sie Ihrer Anmeldung einen zweiten Schritt hinzu, damit ein gestohlenes Passwort allein nicht ausreicht." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "Sicherheitsschlüssel oder Passkey hinzufügen" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "Hinzugefügt am %(created)s" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "Authenticator-App" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "Zurück" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "Können Sie ihn nicht scannen?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "Abbrechen" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "Wählen Sie, wie Sie Ihre Identität bestätigen möchten." + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "Fertig" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "Herunterladen" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "Per E-Mail gesendeter Code" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "Wiederherstellungscode eingeben" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "Geben Sie Ihren Code ein" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "Falls Sie das nicht waren, ändern Sie Ihr Passwort und erzeugen Sie neue Codes." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "Falls Sie das nicht waren, ändern Sie sofort Ihr Passwort und richten Sie die Zwei-Faktor-Authentifizierung erneut ein." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "Falls Sie das nicht waren, ändern Sie sofort Ihr Passwort." + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "Falls Sie das nicht waren, entfernen Sie sie und ändern Sie sofort Ihr Passwort." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "Wenn Sie sich nicht anmelden wollten, kennt möglicherweise jemand Ihr Passwort. Ändern Sie es." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "Er läuft in %(validity_minutes)s Minuten ab und kann einmal verwendet werden." + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "Von Ihrer Organisation verwaltet" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "Diesen Schlüssel benennen" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "Öffnen Sie Ihre Authenticator-App und geben Sie den angezeigten sechsstelligen Code ein." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "Optional. Hilft Ihnen später, Ihre Schlüssel zu unterscheiden." + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "Passkey-Anmeldung fehlgeschlagen." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "Wiederherstellungscode" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "Wiederherstellungscodes" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "Registrieren" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "Entfernen" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "Diese Methode entfernen?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "Bewahren Sie diese an einem sicheren Ort auf. Jeder Code funktioniert einmal, und sie werden nur dieses eine Mal angezeigt." + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "Scannen Sie diesen Code mit einer Authenticator-App und geben Sie dann den angezeigten sechsstelligen Code ein." + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "Sicherheitsschlüssel oder Passkey" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "Wählen Sie die Schaltfläche unten und folgen Sie der Aufforderung Ihres Browsers, um Ihren Sicherheitsschlüssel oder Passkey zu verwenden." + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "Authenticator-App einrichten" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "E-Mail-Codes einrichten" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "Sechsstelliger Code" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "Es gibt nichts mehr hinzuzufügen." + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "Für dieses Konto gibt es keinen zu prüfenden E-Mail-Code. Verwenden Sie eine andere Methode oder wenden Sie sich an den Support, wenn Sie glauben, dass dies ein Fehler ist." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "Dieser Browser unterstützt keine Sicherheitsschlüssel oder Passkeys. Versuchen Sie einen anderen Browser oder wählen Sie eine andere Methode." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "Diese Zwei-Faktor-Methode wurde von Ihrem Konto entfernt: %(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "E-Mail-Codes aktivieren" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "Zwei-Faktor-Authentifizierung aktivieren" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "Zwei-Faktor-Authentifizierung" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "Die Zwei-Faktor-Authentifizierung ist für Ihr Konto deaktiviert" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "Für Ihr Konto ist die Zwei-Faktor-Authentifizierung erforderlich." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "Verwenden Sie einen Hardware-Sicherheitsschlüssel oder einen in dieses Gerät integrierten Passkey wie Touch ID oder Windows Hello." + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "Andere Methode verwenden" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "Verwenden Sie einen davon zur Anmeldung, wenn Sie den Zugriff auf Ihre anderen Methoden verlieren." + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "Verwenden Sie Ihren Sicherheitsschlüssel" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "Bestätigen" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "Bestätigen Sie, dass Sie es sind" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "Wiederherstellungscodes anzeigen" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "Wir haben einen Code an %(address)s gesendet. Geben Sie ihn unten ein, um E-Mail-Codes zu aktivieren." + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "Wir haben einen Code an %(address)s gesendet. Er läuft in Kürze ab." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "Sie haben noch %(remaining)s Wiederherstellungscode. Jeder funktioniert nur einmal." +msgstr[1] "Sie haben noch %(remaining)s Wiederherstellungscodes. Jeder funktioniert nur einmal." + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "Sie haben noch %(remaining)s Wiederherstellungscodes." + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "Sie haben die Zwei-Faktor-Authentifizierung noch nicht eingerichtet." + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "Ihr Konto hat keine E-Mail-Adresse, daher können keine Codes zugestellt werden. Fügen Sie zuerst eine zu Ihrem Profil hinzu." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "Ihr Konto ist nicht mehr durch Zwei-Faktor-Authentifizierung geschützt. Zum Anmelden genügt jetzt ein Passwort." + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "Ihr Code ist abgelaufen oder ungültig." + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "Ihre Methoden" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "Ihre Wiederherstellungscodes wurden bereits erzeugt und können nicht erneut angezeigt werden. Wenn Sie sie verloren haben, entfernen Sie die Zwei-Faktor-Authentifizierung und richten Sie sie erneut ein, um neue Codes zu erhalten." + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "Ihr Anmeldecode" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "Ihr Anmeldecode lautet %(code)s." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "z. B. YubiKey, Arbeitslaptop" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "zuletzt verwendet %(used)s" diff --git a/django_mfa/locale/django.pot b/django_mfa/locale/django.pot new file mode 100644 index 0000000..4aa7cbf --- /dev/null +++ b/django_mfa/locale/django.pot @@ -0,0 +1,338 @@ +# Translation template for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +msgid "%(code_length)s-digit code" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:78 +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "" +msgstr[1] "" + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +msgid "A new two-factor method was added to your account" +msgstr "" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +msgid "A recovery code was just used to sign in to your account." +msgstr "" + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +msgid "A recovery code was used to sign in" +msgstr "" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +msgid "A two-factor method was removed from your account" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:59 +msgid "Add a method" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:11 +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:7 +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +msgid "Add a security key or passkey" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:30 +msgid "Added %(created)s" +msgstr "" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +msgid "Authenticator app" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +msgid "Back" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +msgid "Can't scan it?" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +msgid "Cancel" +msgstr "" + +#: django_mfa/templates/django_mfa/picker.html:6 +msgid "Choose how you'd like to confirm your identity." +msgstr "" + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +msgid "Done" +msgstr "" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +msgid "Download" +msgstr "" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +msgid "Emailed code" +msgstr "" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +msgid "Enter a recovery code" +msgstr "" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +msgid "Enter your code" +msgstr "" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +msgid "If this wasn't you, change your password immediately." +msgstr "" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:46 +msgid "Managed by your organization" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +msgid "Name this key" +msgstr "" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +msgid "Optional. Helps you tell your keys apart later." +msgstr "" + +#: django_mfa/views/verify.py:54 +msgid "Passkey sign-in failed." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +msgid "Recovery code" +msgstr "" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +msgid "Recovery codes" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +msgid "Register" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:42 +msgid "Remove" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:41 +msgid "Remove this method?" +msgstr "" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "" + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +msgid "Security key or passkey" +msgstr "" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +msgid "Set up an authenticator app" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +msgid "Set up email codes" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +msgid "Six-digit code" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:69 +msgid "There is nothing left to add." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_email.html:30 +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +msgid "Turn on email codes" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +msgid "Turn on two-factor authentication" +msgstr "" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +msgid "Two-factor authentication" +msgstr "" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +msgid "Two-factor authentication is off for your account" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:10 +msgid "Two-factor authentication is required for your account." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +msgid "Use another method" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:79 +msgid "Use one to sign in if you lose access to your other methods." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +msgid "Use your security key" +msgstr "" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +msgid "Verify" +msgstr "" + +#: django_mfa/templates/django_mfa/picker.html:5 +msgid "Verify it's you" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:82 +msgid "View recovery codes" +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_email.html:8 +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "" +msgstr[1] "" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +msgid "You have %(remaining)s recovery codes left." +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:53 +msgid "You haven't set up two-factor authentication yet." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "" + +#: django_mfa/views/verify.py:20 +msgid "Your code is expired or invalid." +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:17 +msgid "Your methods" +msgstr "" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "" + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +msgid "Your sign-in code" +msgstr "" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +msgid "Your sign-in code is %(code)s." +msgstr "" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +msgid "e.g. YubiKey, work laptop" +msgstr "" + +#: django_mfa/templates/django_mfa/security.html:32 +msgid "last used %(used)s" +msgstr "" diff --git a/django_mfa/locale/es/LC_MESSAGES/django.po b/django_mfa/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..80c4ae0 --- /dev/null +++ b/django_mfa/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,419 @@ +# Spanish translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "Código de %(code_length)s dígitos" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "Queda %(remaining)s código." +msgstr[1] "Quedan %(remaining)s códigos." + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "Se añadió un nuevo método de doble factor a tu cuenta" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "Se añadió un nuevo método de doble factor a tu cuenta: %(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "Se acaba de usar un código de recuperación para iniciar sesión en tu cuenta." + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "Se usó un código de recuperación para iniciar sesión" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "Se eliminó un método de doble factor de tu cuenta" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "Añadir un método" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "Añade un método abajo para continuar. Hasta entonces, el resto del sitio no está disponible." + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "Añade un segundo paso al inicio de sesión para que una contraseña robada no baste por sí sola." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "Añadir una llave de seguridad o passkey" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "Añadido el %(created)s" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "Aplicación de autenticación" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "Atrás" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "¿No puedes escanearlo?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "Cancelar" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "Elige cómo quieres confirmar tu identidad." + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "Listo" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "Descargar" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "Código por correo electrónico" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "Introduce un código de recuperación" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "Introduce tu código" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "Si no fuiste tú, cambia tu contraseña y genera un nuevo conjunto de códigos." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "Si no fuiste tú, cambia tu contraseña y vuelve a configurar la autenticación de doble factor de inmediato." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "Si no fuiste tú, cambia tu contraseña de inmediato." + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "Si no fuiste tú, elimínalo y cambia tu contraseña de inmediato." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "Si no intentaste iniciar sesión, puede que alguien conozca tu contraseña. Cámbiala." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "Caduca en %(validity_minutes)s minutos y solo se puede usar una vez." + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "Gestionado por tu organización" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "Nombra esta llave" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "Abre tu aplicación de autenticación e introduce el código de seis dígitos que muestra." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "Opcional. Te ayuda a distinguir tus llaves más adelante." + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "Error al iniciar sesión con passkey." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "Código de recuperación" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "Códigos de recuperación" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "Registrar" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "Eliminar" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "¿Eliminar este método?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "Guárdalos en un lugar seguro. Cada código funciona una vez y esta es la única vez que se mostrarán." + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "Escanea este código con una aplicación de autenticación y luego introduce el código de seis dígitos que muestra." + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "Llave de seguridad o passkey" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "Selecciona el botón de abajo y sigue las indicaciones de tu navegador para usar tu llave de seguridad o passkey." + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "Configurar una aplicación de autenticación" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "Configurar códigos por correo electrónico" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "Código de seis dígitos" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "No queda nada por añadir." + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "Esta cuenta no tiene ningún código por correo electrónico que verificar. Usa otro método o contacta con soporte si crees que es un error." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "Este navegador no admite llaves de seguridad ni passkeys. Prueba con otro navegador o elige un método distinto." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "Se eliminó este método de doble factor de tu cuenta: %(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "Activar códigos por correo electrónico" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "Activar la autenticación de doble factor" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "Autenticación de doble factor" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "La autenticación de doble factor está desactivada en tu cuenta" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "Tu cuenta requiere autenticación de doble factor." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "Usa una llave de seguridad física o una passkey integrada en este dispositivo, como Touch ID o Windows Hello." + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "Usar otro método" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "Usa uno para iniciar sesión si pierdes el acceso a tus otros métodos." + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "Usa tu llave de seguridad" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "Verificar" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "Verifica tu identidad" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "Ver códigos de recuperación" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "Hemos enviado un código a %(address)s. Introdúcelo abajo para activar los códigos por correo electrónico." + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "Hemos enviado un código a %(address)s. Caduca en breve." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "Te queda %(remaining)s código de recuperación. Cada uno funciona una sola vez." +msgstr[1] "Te quedan %(remaining)s códigos de recuperación. Cada uno funciona una sola vez." + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "Te quedan %(remaining)s códigos de recuperación." + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "Aún no has configurado la autenticación de doble factor." + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "Tu cuenta no tiene dirección de correo electrónico, así que no se pueden entregar los códigos. Añade una a tu perfil primero." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "Tu cuenta ya no está protegida con autenticación de doble factor. Ahora basta una contraseña para iniciar sesión." + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "Tu código ha caducado o no es válido." + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "Tus métodos" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "Tus códigos de recuperación se generaron antes y no se pueden volver a mostrar. Si los has perdido, elimina y vuelve a añadir la autenticación de doble factor para obtener un conjunto nuevo." + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "Tu código de inicio de sesión" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "Tu código de inicio de sesión es %(code)s." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "p. ej., YubiKey, portátil del trabajo" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "usado por última vez el %(used)s" diff --git a/django_mfa/locale/fr/LC_MESSAGES/django.po b/django_mfa/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..00542ba --- /dev/null +++ b/django_mfa/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,419 @@ +# French translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "Code à %(code_length)s chiffres" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "Il reste %(remaining)s code." +msgstr[1] "Il reste %(remaining)s codes." + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "Une nouvelle méthode à deux facteurs a été ajoutée à votre compte" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "Une nouvelle méthode à deux facteurs a été ajoutée à votre compte : %(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "Un code de récupération vient d’être utilisé pour se connecter à votre compte." + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "Un code de récupération a été utilisé pour se connecter" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "Une méthode à deux facteurs a été supprimée de votre compte" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "Ajouter une méthode" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "Ajoutez une méthode ci-dessous pour continuer. En attendant, le reste du site est indisponible." + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "Ajoutez une deuxième étape à votre connexion pour qu’un mot de passe volé ne suffise pas à lui seul." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "Ajouter une clé de sécurité ou une passkey" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "Ajouté le %(created)s" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "Application d’authentification" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "Retour" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "Impossible de le scanner ?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "Annuler" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "Choisissez comment confirmer votre identité." + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "Terminé" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "Télécharger" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "Code envoyé par e-mail" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "Saisissez un code de récupération" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "Saisissez votre code" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "Si ce n’était pas vous, changez votre mot de passe et générez de nouveaux codes." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "Si ce n’était pas vous, changez immédiatement votre mot de passe et reconfigurez l’authentification à deux facteurs." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "Si ce n’était pas vous, changez immédiatement votre mot de passe." + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "Si ce n’était pas vous, supprimez-la et changez immédiatement votre mot de passe." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "Si vous n’avez pas tenté de vous connecter, quelqu’un connaît peut-être votre mot de passe. Changez-le." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "Il expire dans %(validity_minutes)s minutes et ne peut être utilisé qu’une fois." + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "Géré par votre organisation" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "Nommez cette clé" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "Ouvrez votre application d’authentification et saisissez le code à six chiffres affiché." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "Facultatif. Vous aide à distinguer vos clés plus tard." + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "Échec de la connexion par passkey." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "Code de récupération" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "Codes de récupération" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "Enregistrer" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "Supprimer" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "Supprimer cette méthode ?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "Conservez-les en lieu sûr. Chaque code ne fonctionne qu’une fois, et c’est la seule fois qu’ils seront affichés." + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "Scannez ce code avec une application d’authentification, puis saisissez le code à six chiffres affiché." + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "Clé de sécurité ou passkey" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "Sélectionnez le bouton ci-dessous, puis suivez l’invite de votre navigateur pour utiliser votre clé de sécurité ou votre passkey." + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "Configurer une application d’authentification" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "Configurer les codes par e-mail" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "Code à six chiffres" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "Il n’y a plus rien à ajouter." + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "Ce compte n’a aucun code par e-mail à vérifier. Utilisez une autre méthode ou contactez l’assistance si vous pensez qu’il s’agit d’une erreur." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "Ce navigateur ne prend pas en charge les clés de sécurité ni les passkeys. Essayez un autre navigateur ou choisissez une autre méthode." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "Cette méthode à deux facteurs a été supprimée de votre compte : %(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "Activer les codes par e-mail" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "Activer l’authentification à deux facteurs" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "Authentification à deux facteurs" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "L’authentification à deux facteurs est désactivée pour votre compte" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "L’authentification à deux facteurs est requise pour votre compte." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "Utilisez une clé de sécurité matérielle ou une passkey intégrée à cet appareil, comme Touch ID ou Windows Hello." + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "Utiliser une autre méthode" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "Utilisez-en un pour vous connecter si vous perdez l’accès à vos autres méthodes." + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "Utilisez votre clé de sécurité" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "Vérifier" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "Confirmez votre identité" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "Afficher les codes de récupération" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "Nous avons envoyé un code à %(address)s. Saisissez-le ci-dessous pour activer les codes par e-mail." + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "Nous avons envoyé un code à %(address)s. Il expire bientôt." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "Il vous reste %(remaining)s code de récupération. Chacun ne fonctionne qu’une seule fois." +msgstr[1] "Il vous reste %(remaining)s codes de récupération. Chacun ne fonctionne qu’une seule fois." + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "Il vous reste %(remaining)s codes de récupération." + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "Vous n’avez pas encore configuré l’authentification à deux facteurs." + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "Votre compte n’a pas d’adresse e-mail, les codes ne peuvent donc pas être envoyés. Ajoutez-en une à votre profil d’abord." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "Votre compte n’est plus protégé par l’authentification à deux facteurs. Un mot de passe suffit désormais pour se connecter." + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "Votre code est expiré ou invalide." + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "Vos méthodes" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "Vos codes de récupération ont été générés précédemment et ne peuvent plus être affichés. Si vous les avez perdus, supprimez puis reconfigurez l’authentification à deux facteurs pour en obtenir de nouveaux." + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "Votre code de connexion" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "Votre code de connexion est %(code)s." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "p. ex. YubiKey, ordinateur portable professionnel" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "dernière utilisation %(used)s" diff --git a/django_mfa/locale/ja/LC_MESSAGES/django.po b/django_mfa/locale/ja/LC_MESSAGES/django.po new file mode 100644 index 0000000..5cb3341 --- /dev/null +++ b/django_mfa/locale/ja/LC_MESSAGES/django.po @@ -0,0 +1,417 @@ +# Japanese translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ja\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "%(code_length)s桁のコード" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "残り %(remaining)s 個のコードがあります。" + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "アカウントに新しい二要素認証の方法が追加されました" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "アカウントに新しい二要素認証の方法が追加されました: %(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "アカウントへのサインインに復旧コードが使用されました。" + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "復旧コードでサインインしました" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "アカウントから二要素認証の方法が削除されました" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "方法を追加" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "続行するには、以下から方法を追加してください。追加するまでサイトの他の部分は利用できません。" + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "サインインに 2 段階目を追加すると、パスワードが盗まれただけでは不十分になります。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "セキュリティキーまたはパスキーを追加" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "%(created)s に追加" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "認証アプリ" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "戻る" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "スキャンできませんか?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "キャンセル" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "本人確認の方法を選択してください。" + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "完了" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "ダウンロード" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "メールで届くコード" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "復旧コードを入力" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "コードを入力" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "心当たりがない場合は、パスワードを変更し、新しいコードを生成してください。" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "心当たりがない場合は、直ちにパスワードを変更し、二要素認証を再設定してください。" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "心当たりがない場合は、直ちにパスワードを変更してください。" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "心当たりがない場合は、それを削除し、直ちにパスワードを変更してください。" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "サインインを試みていない場合、誰かがパスワードを知っている可能性があります。変更してください。" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "%(validity_minutes)s 分で期限切れになり、一度だけ使用できます。" + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "組織によって管理されています" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "このキーに名前を付ける" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "認証アプリを開き、表示された 6 桁のコードを入力してください。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "任意です。後でキーを見分けるのに役立ちます。" + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "パスキーでのサインインに失敗しました。" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "復旧コード" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "復旧コード" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "登録" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "削除" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "この方法を削除しますか?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "安全な場所に保管してください。各コードは一度だけ使用でき、表示されるのはこの一度きりです。" + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "このコードを認証アプリでスキャンし、表示された 6 桁のコードを入力してください。" + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "セキュリティキーまたはパスキー" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "下のボタンを選択し、ブラウザの指示に従ってセキュリティキーまたはパスキーを使用してください。" + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "認証アプリを設定" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "メールコードを設定" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "6 桁のコード" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "追加できるものはありません。" + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "このアカウントには確認できるメールコードがありません。別の方法を使用するか、誤りだと思われる場合はサポートにお問い合わせください。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "このブラウザはセキュリティキーやパスキーに対応していません。別のブラウザを試すか、他の方法を選択してください。" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "この二要素認証の方法がアカウントから削除されました: %(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "メールコードを有効にする" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "二要素認証を有効にする" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "二要素認証" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "アカウントの二要素認証が無効になっています" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "このアカウントには二要素認証が必要です。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "ハードウェアセキュリティキー、または Touch ID や Windows Hello などこの端末に組み込まれたパスキーを使用してください。" + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "別の方法を使う" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "他の方法が使えなくなった場合、いずれか 1 つを使ってサインインできます。" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "セキュリティキーを使用" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "確認" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "本人確認" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "復旧コードを表示" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "%(address)s にコードを送信しました。以下に入力してメールコードを有効にしてください。" + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "%(address)s にコードを送信しました。まもなく期限切れになります。" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "復旧コードが残り %(remaining)s 個あります。各コードは一度だけ使用できます。" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "復旧コードが残り %(remaining)s 個あります。" + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "まだ二要素認証を設定していません。" + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "アカウントにメールアドレスが登録されていないため、コードを配信できません。先にプロフィールに追加してください。" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "アカウントは二要素認証で保護されなくなりました。今はパスワードだけでサインインできます。" + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "コードの有効期限が切れているか、無効です。" + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "あなたの方法" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "復旧コードは以前に生成されたもので、再表示はできません。紛失した場合は、二要素認証を削除して再度追加すると新しいコードを取得できます。" + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "サインインコード" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "サインインコードは %(code)s です。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "例: YubiKey、仕事用ノート PC" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "最終使用 %(used)s" diff --git a/django_mfa/locale/pt_BR/LC_MESSAGES/django.po b/django_mfa/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000..14d4cad --- /dev/null +++ b/django_mfa/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,419 @@ +# Brazilian Portuguese translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "Código de %(code_length)s dígitos" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "Resta %(remaining)s código." +msgstr[1] "Restam %(remaining)s códigos." + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "Um novo método de dois fatores foi adicionado à sua conta" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "Um novo método de dois fatores foi adicionado à sua conta: %(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "Um código de recuperação acabou de ser usado para entrar na sua conta." + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "Um código de recuperação foi usado para entrar" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "Um método de dois fatores foi removido da sua conta" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "Adicionar um método" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "Adicione um método abaixo para continuar. Até lá, o restante do site fica indisponível." + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "Adicione uma segunda etapa ao seu login para que uma senha roubada não baste sozinha." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "Adicionar uma chave de segurança ou passkey" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "Adicionado em %(created)s" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "Aplicativo autenticador" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "Voltar" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "Não consegue escanear?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "Cancelar" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "Escolha como deseja confirmar sua identidade." + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "Concluído" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "Baixar" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "Código por e-mail" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "Digite um código de recuperação" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "Digite seu código" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "Se não foi você, troque sua senha e gere um novo conjunto de códigos." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "Se não foi você, troque sua senha e configure a autenticação de dois fatores novamente imediatamente." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "Se não foi você, troque sua senha imediatamente." + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "Se não foi você, remova-o e troque sua senha imediatamente." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "Se você não tentou entrar, alguém pode saber sua senha. Troque-a." + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "Ele expira em %(validity_minutes)s minutos e pode ser usado uma vez." + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "Gerenciado pela sua organização" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "Dê um nome a esta chave" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "Abra seu aplicativo autenticador e digite o código de seis dígitos exibido." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "Opcional. Ajuda você a diferenciar suas chaves depois." + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "Falha ao entrar com passkey." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "Código de recuperação" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "Códigos de recuperação" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "Registrar" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "Remover" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "Remover este método?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "Guarde-os em um lugar seguro. Cada código funciona uma vez, e esta é a única vez que serão exibidos." + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "Escaneie este código com um aplicativo autenticador e digite o código de seis dígitos exibido." + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "Chave de segurança ou passkey" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "Selecione o botão abaixo e siga as instruções do navegador para usar sua chave de segurança ou passkey." + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "Configurar um aplicativo autenticador" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "Configurar códigos por e-mail" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "Código de seis dígitos" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "Não há mais nada a adicionar." + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "Esta conta não tem código por e-mail para verificar. Use outro método ou contate o suporte se achar que isso é um engano." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "Este navegador não oferece suporte a chaves de segurança ou passkeys. Tente outro navegador ou escolha um método diferente." + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "Este método de dois fatores foi removido da sua conta: %(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "Ativar códigos por e-mail" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "Ativar a autenticação de dois fatores" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "Autenticação de dois fatores" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "A autenticação de dois fatores está desativada na sua conta" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "A autenticação de dois fatores é obrigatória para sua conta." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "Use uma chave de segurança física ou uma passkey integrada a este dispositivo, como Touch ID ou Windows Hello." + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "Usar outro método" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "Use um deles para entrar caso perca o acesso aos seus outros métodos." + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "Use sua chave de segurança" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "Verificar" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "Confirme que é você" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "Ver códigos de recuperação" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "Enviamos um código para %(address)s. Digite-o abaixo para ativar os códigos por e-mail." + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "Enviamos um código para %(address)s. Ele expira em breve." + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "Resta %(remaining)s código de recuperação. Cada um funciona apenas uma vez." +msgstr[1] "Restam %(remaining)s códigos de recuperação. Cada um funciona apenas uma vez." + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "Restam %(remaining)s códigos de recuperação." + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "Você ainda não configurou a autenticação de dois fatores." + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "Sua conta não tem endereço de e-mail, então os códigos não podem ser entregues. Adicione um ao seu perfil primeiro." + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "Sua conta não está mais protegida por autenticação de dois fatores. Agora basta uma senha para entrar." + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "Seu código expirou ou é inválido." + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "Seus métodos" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "Seus códigos de recuperação foram gerados antes e não podem ser exibidos novamente. Se você os perdeu, remova e adicione novamente a autenticação de dois fatores para obter um novo conjunto." + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "Seu código de acesso" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "Seu código de acesso é %(code)s." + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "ex.: YubiKey, notebook do trabalho" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "usado pela última vez em %(used)s" diff --git a/django_mfa/locale/zh_Hans/LC_MESSAGES/django.po b/django_mfa/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..6c932b9 --- /dev/null +++ b/django_mfa/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,417 @@ +# Simplified Chinese translation for django-mfa. +# Copyright (C) MicroPyramid +# This file is distributed under the same licence as the django-mfa package. +# +# MACHINE-DRAFTED, NOT REVIEWED. Every entry below is marked "fuzzy", which +# means gettext ignores it and users see the English source instead. Nothing +# here reaches a user until a human reviews an entry and removes its fuzzy +# flag. See docs/translations.md. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-mfa\n" +"Report-Msgid-Bugs-To: https://github.com/MicroPyramid/django-mfa/issues\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_Hans\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: django_mfa/templates/django_mfa/enroll_email.html:14 +#: django_mfa/templates/django_mfa/verify_email.html:15 +#, fuzzy +msgid "%(code_length)s-digit code" +msgstr "%(code_length)s 位数字代码" + +#: django_mfa/templates/django_mfa/security.html:78 +#, fuzzy +msgid "%(remaining)s code remaining." +msgid_plural "%(remaining)s codes remaining." +msgstr[0] "还剩 %(remaining)s 个代码。" + +#: django_mfa/templates/django_mfa/email/factor_added_subject.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account" +msgstr "您的账户已添加新的双重验证方式" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:1 +#, fuzzy +msgid "A new two-factor method was added to your account: %(factor)s" +msgstr "您的账户已添加新的双重验证方式:%(factor)s" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:1 +#, fuzzy +msgid "A recovery code was just used to sign in to your account." +msgstr "刚刚有人使用恢复代码登录了您的账户。" + +#: django_mfa/templates/django_mfa/email/recovery_code_used_subject.txt:1 +#, fuzzy +msgid "A recovery code was used to sign in" +msgstr "已使用恢复代码登录" + +#: django_mfa/templates/django_mfa/email/factor_removed_subject.txt:1 +#, fuzzy +msgid "A two-factor method was removed from your account" +msgstr "您的账户已移除一种双重验证方式" + +#: django_mfa/templates/django_mfa/security.html:59 +#, fuzzy +msgid "Add a method" +msgstr "添加方式" + +#: django_mfa/templates/django_mfa/security.html:11 +#, fuzzy +msgid "Add a method below to continue. Until you do, the rest of the site is unavailable." +msgstr "请在下方添加一种方式以继续。在此之前,网站其余部分不可用。" + +#: django_mfa/templates/django_mfa/security.html:7 +#, fuzzy +msgid "Add a second step to your sign-in so a stolen password isn't enough on its own." +msgstr "为登录添加第二个步骤,这样仅凭被盗密码就不足以登录。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:5 +#, fuzzy +msgid "Add a security key or passkey" +msgstr "添加安全密钥或通行密钥" + +#: django_mfa/templates/django_mfa/security.html:30 +#, fuzzy +msgid "Added %(created)s" +msgstr "添加于 %(created)s" + +#: django_mfa/adapters/totp.py:47 +#: django_mfa/models.py:34 +#, fuzzy +msgid "Authenticator app" +msgstr "身份验证器应用" + +#: django_mfa/templates/django_mfa/enroll_email.html:31 +#, fuzzy +msgid "Back" +msgstr "返回" + +#: django_mfa/templates/django_mfa/enroll_totp.html:11 +#, fuzzy +msgid "Can't scan it?" +msgstr "无法扫描?" + +#: django_mfa/templates/django_mfa/enroll_email.html:25 +#: django_mfa/templates/django_mfa/enroll_totp.html:31 +#: django_mfa/templates/django_mfa/enroll_webauthn.html:42 +#, fuzzy +msgid "Cancel" +msgstr "取消" + +#: django_mfa/templates/django_mfa/picker.html:6 +#, fuzzy +msgid "Choose how you'd like to confirm your identity." +msgstr "请选择确认身份的方式。" + +#: django_mfa/templates/django_mfa/recovery_codes.html:23 +#: django_mfa/templates/django_mfa/recovery_codes.html:41 +#, fuzzy +msgid "Done" +msgstr "完成" + +#: django_mfa/templates/django_mfa/recovery_codes.html:22 +#, fuzzy +msgid "Download" +msgstr "下载" + +#: django_mfa/adapters/email.py:104 +#: django_mfa/models.py:37 +#, fuzzy +msgid "Emailed code" +msgstr "邮件验证码" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:5 +#, fuzzy +msgid "Enter a recovery code" +msgstr "输入恢复代码" + +#: django_mfa/templates/django_mfa/verify_email.html:5 +#: django_mfa/templates/django_mfa/verify_totp.html:5 +#, fuzzy +msgid "Enter your code" +msgstr "输入您的代码" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:5 +#, fuzzy +msgid "If this wasn't you, change your password and generate a fresh set of codes." +msgstr "如果这不是您本人操作,请修改密码并重新生成一组代码。" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password and set up two-factor authentication again immediately." +msgstr "如果这不是您本人操作,请立即修改密码并重新设置双重验证。" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:3 +#, fuzzy +msgid "If this wasn't you, change your password immediately." +msgstr "如果这不是您本人操作,请立即修改密码。" + +#: django_mfa/templates/django_mfa/email/factor_added.txt:3 +#, fuzzy +msgid "If this wasn't you, remove it and change your password immediately." +msgstr "如果这不是您本人操作,请将其移除并立即修改密码。" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:5 +#, fuzzy +msgid "If you didn't try to sign in, someone may know your password. Change it." +msgstr "如果您没有尝试登录,可能有人知道了您的密码。请立即修改。" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:3 +#, fuzzy +msgid "It expires in %(validity_minutes)s minutes and can be used once." +msgstr "该代码将在 %(validity_minutes)s 分钟后失效,且仅可使用一次。" + +#: django_mfa/templates/django_mfa/security.html:46 +#, fuzzy +msgid "Managed by your organization" +msgstr "由您的组织管理" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:29 +#, fuzzy +msgid "Name this key" +msgstr "为该密钥命名" + +#: django_mfa/templates/django_mfa/verify_totp.html:6 +#, fuzzy +msgid "Open your authenticator app and enter the six-digit code it shows." +msgstr "打开身份验证器应用,输入其中显示的六位数字代码。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:36 +#, fuzzy +msgid "Optional. Helps you tell your keys apart later." +msgstr "可选。便于您以后区分不同的密钥。" + +#: django_mfa/views/verify.py:54 +#, fuzzy +msgid "Passkey sign-in failed." +msgstr "通行密钥登录失败。" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:14 +#, fuzzy +msgid "Recovery code" +msgstr "恢复代码" + +#: django_mfa/adapters/recovery_codes.py:21 +#: django_mfa/models.py:36 +#: django_mfa/templates/django_mfa/recovery_codes.html:5 +#: django_mfa/templates/django_mfa/security.html:75 +#, fuzzy +msgid "Recovery codes" +msgstr "恢复代码" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:41 +#, fuzzy +msgid "Register" +msgstr "注册" + +#: django_mfa/templates/django_mfa/security.html:42 +#, fuzzy +msgid "Remove" +msgstr "移除" + +#: django_mfa/templates/django_mfa/security.html:41 +#, fuzzy +msgid "Remove this method?" +msgstr "要移除此方式吗?" + +#: django_mfa/templates/django_mfa/recovery_codes.html:8 +#, fuzzy +msgid "Save these somewhere safe. Each code works once, and this is the only time they'll be shown." +msgstr "请妥善保存。每个代码仅可使用一次,且仅此一次显示。" + +#: django_mfa/templates/django_mfa/enroll_totp.html:6 +#, fuzzy +msgid "Scan this code with an authenticator app, then enter the six-digit code it shows." +msgstr "使用身份验证器应用扫描此码,然后输入其中显示的六位数字代码。" + +#: django_mfa/adapters/webauthn.py:72 +#: django_mfa/models.py:35 +#, fuzzy +msgid "Security key or passkey" +msgstr "安全密钥或通行密钥" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:6 +#, fuzzy +msgid "Select the button below, then follow your browser's prompt to use your security key or passkey." +msgstr "请点击下方按钮,然后按照浏览器提示使用您的安全密钥或通行密钥。" + +#: django_mfa/templates/django_mfa/enroll_totp.html:5 +#, fuzzy +msgid "Set up an authenticator app" +msgstr "设置身份验证器应用" + +#: django_mfa/templates/django_mfa/enroll_email.html:5 +#, fuzzy +msgid "Set up email codes" +msgstr "设置邮件验证码" + +#: django_mfa/templates/django_mfa/enroll_totp.html:20 +#: django_mfa/templates/django_mfa/verify_totp.html:12 +#, fuzzy +msgid "Six-digit code" +msgstr "六位数字代码" + +#: django_mfa/templates/django_mfa/security.html:69 +#, fuzzy +msgid "There is nothing left to add." +msgstr "没有可添加的项了。" + +#: django_mfa/templates/django_mfa/verify_email.html:30 +#, fuzzy +msgid "This account has no email code to verify. Use another method, or contact support if you believe this is a mistake." +msgstr "此账户没有可验证的邮件验证码。请使用其他方式,若您认为这是错误,请联系支持人员。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:12 +#: django_mfa/templates/django_mfa/verify_webauthn.html:12 +#, fuzzy +msgid "This browser doesn't support security keys or passkeys. Try another browser, or choose a different method." +msgstr "此浏览器不支持安全密钥或通行密钥。请更换浏览器,或选择其他方式。" + +#: django_mfa/templates/django_mfa/email/factor_removed.txt:1 +#, fuzzy +msgid "This two-factor method was removed from your account: %(factor)s" +msgstr "您的账户已移除此双重验证方式:%(factor)s" + +#: django_mfa/templates/django_mfa/enroll_email.html:24 +#, fuzzy +msgid "Turn on email codes" +msgstr "启用邮件验证码" + +#: django_mfa/templates/django_mfa/enroll_totp.html:30 +#, fuzzy +msgid "Turn on two-factor authentication" +msgstr "启用双重验证" + +#: django_mfa/templates/django_mfa/base.html:19 +#: django_mfa/templates/django_mfa/security.html:6 +#, fuzzy +msgid "Two-factor authentication" +msgstr "双重验证" + +#: django_mfa/templates/django_mfa/email/mfa_disabled_subject.txt:1 +#, fuzzy +msgid "Two-factor authentication is off for your account" +msgstr "您的账户已关闭双重验证" + +#: django_mfa/templates/django_mfa/security.html:10 +#, fuzzy +msgid "Two-factor authentication is required for your account." +msgstr "您的账户必须启用双重验证。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:6 +#, fuzzy +msgid "Use a hardware security key, or a passkey built into this device such as Touch ID or Windows Hello." +msgstr "请使用硬件安全密钥,或本设备内置的通行密钥(如 Touch ID 或 Windows Hello)。" + +#: django_mfa/templates/django_mfa/verify_email.html:26 +#: django_mfa/templates/django_mfa/verify_email.html:32 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:25 +#: django_mfa/templates/django_mfa/verify_totp.html:23 +#: django_mfa/templates/django_mfa/verify_webauthn.html:29 +#, fuzzy +msgid "Use another method" +msgstr "使用其他方式" + +#: django_mfa/templates/django_mfa/security.html:79 +#, fuzzy +msgid "Use one to sign in if you lose access to your other methods." +msgstr "若您无法使用其他方式,可用其中一个登录。" + +#: django_mfa/templates/django_mfa/verify_webauthn.html:5 +#, fuzzy +msgid "Use your security key" +msgstr "使用您的安全密钥" + +#: django_mfa/templates/django_mfa/verify_email.html:25 +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:24 +#: django_mfa/templates/django_mfa/verify_totp.html:22 +#: django_mfa/templates/django_mfa/verify_webauthn.html:28 +#, fuzzy +msgid "Verify" +msgstr "验证" + +#: django_mfa/templates/django_mfa/picker.html:5 +#, fuzzy +msgid "Verify it's you" +msgstr "验证您的身份" + +#: django_mfa/templates/django_mfa/security.html:82 +#, fuzzy +msgid "View recovery codes" +msgstr "查看恢复代码" + +#: django_mfa/templates/django_mfa/enroll_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. Enter it below to turn on email codes." +msgstr "我们已向 %(address)s 发送验证码。请在下方输入以启用邮件验证码。" + +#: django_mfa/templates/django_mfa/verify_email.html:8 +#, fuzzy +msgid "We've emailed a code to %(address)s. It expires shortly." +msgstr "我们已向 %(address)s 发送验证码。该验证码即将失效。" + +#: django_mfa/templates/django_mfa/verify_recovery_codes.html:7 +#, fuzzy +msgid "You have %(remaining)s recovery code left. Each one works only once." +msgid_plural "You have %(remaining)s recovery codes left. Each one works only once." +msgstr[0] "您还剩 %(remaining)s 个恢复代码。每个仅可使用一次。" + +#: django_mfa/templates/django_mfa/email/recovery_code_used.txt:3 +#, fuzzy +msgid "You have %(remaining)s recovery codes left." +msgstr "您还剩 %(remaining)s 个恢复代码。" + +#: django_mfa/templates/django_mfa/security.html:53 +#, fuzzy +msgid "You haven't set up two-factor authentication yet." +msgstr "您尚未设置双重验证。" + +#: django_mfa/templates/django_mfa/enroll_email.html:29 +#, fuzzy +msgid "Your account has no email address, so codes can't be delivered. Add one to your profile first." +msgstr "您的账户未设置电子邮件地址,无法发送验证码。请先在个人资料中添加。" + +#: django_mfa/templates/django_mfa/email/mfa_disabled.txt:1 +#, fuzzy +msgid "Your account is no longer protected by two-factor authentication. A password is now all that's needed to sign in." +msgstr "您的账户不再受双重验证保护。现在仅凭密码即可登录。" + +#: django_mfa/views/verify.py:20 +#, fuzzy +msgid "Your code is expired or invalid." +msgstr "您的代码已过期或无效。" + +#: django_mfa/templates/django_mfa/security.html:17 +#, fuzzy +msgid "Your methods" +msgstr "您的验证方式" + +#: django_mfa/templates/django_mfa/recovery_codes.html:38 +#, fuzzy +msgid "Your recovery codes were generated earlier and can't be shown again. If you've lost them, remove and re-add two-factor authentication to get a fresh set." +msgstr "您的恢复代码此前已生成,无法再次显示。如已丢失,请移除并重新添加双重验证以获取新的一组代码。" + +#: django_mfa/templates/django_mfa/email/otp_code_subject.txt:1 +#, fuzzy +msgid "Your sign-in code" +msgstr "您的登录代码" + +#: django_mfa/templates/django_mfa/email/otp_code.txt:1 +#, fuzzy +msgid "Your sign-in code is %(code)s." +msgstr "您的登录代码是 %(code)s。" + +#: django_mfa/templates/django_mfa/enroll_webauthn.html:32 +#, fuzzy +msgid "e.g. YubiKey, work laptop" +msgstr "例如:YubiKey、工作笔记本电脑" + +#: django_mfa/templates/django_mfa/security.html:32 +#, fuzzy +msgid "last used %(used)s" +msgstr "上次使用 %(used)s" diff --git a/django_mfa/models.py b/django_mfa/models.py index 6ef0dc9..cf8da54 100644 --- a/django_mfa/models.py +++ b/django_mfa/models.py @@ -1,6 +1,7 @@ from django.conf import settings from django.db import models from django.utils import timezone +from django.utils.translation import gettext_lazy as _ # Whether a factor type counts as "primary" (protects a user on its own) is # NOT decided here. It used to be a second, independent definition @@ -19,10 +20,21 @@ def for_user(self, user): class Authenticator(models.Model): class Type(models.TextChoices): - TOTP = "totp", "Authenticator app" - WEBAUTHN = "webauthn", "Security key or passkey" - RECOVERY_CODES = "recovery_codes", "Recovery codes" - EMAIL = "email", "Emailed code" + # Labels are lazily translated: get_type_display() feeds the + # notification emails (django_mfa/notifications.py) and the admin + # changelist, so leaving them in English would put an untranslated + # factor name inside an otherwise translated message. + # + # This needs NO migration, which is not obvious: `choices` is part of + # a field's deconstruction, so changing it normally provokes an + # AlterField. A gettext_lazy proxy compares equal to the string it + # wraps, though, so the autodetector sees the same choices it already + # had. Verified against Django 4.2, 5.2 and 6.1; test_migrations.py's + # makemigrations --check test is what would catch it regressing. + TOTP = "totp", _("Authenticator app") + WEBAUTHN = "webauthn", _("Security key or passkey") + RECOVERY_CODES = "recovery_codes", _("Recovery codes") + EMAIL = "email", _("Emailed code") user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="mfa_authenticators", diff --git a/django_mfa/tests/support/i18n.py b/django_mfa/tests/support/i18n.py new file mode 100644 index 0000000..7acf631 --- /dev/null +++ b/django_mfa/tests/support/i18n.py @@ -0,0 +1,287 @@ +"""Extract translatable strings and read PO catalogs, without gettext. + +`makemessages` shells out to xgettext, which is a system package this test +suite cannot assume: it is absent from this project's CI images and from at +least one maintainer's machine. A catalog guard that silently skips wherever +gettext is missing guards nothing -- it would have been skipped in every +environment that actually runs the suite, which is the same shape of failure +`test_packaging.py` exists to prevent (a green tick over a check that never +ran). + +So extraction happens in pure Python here instead: + +* Templates go through ``django.utils.translation.template.templatize()``, + the exact function `makemessages` itself feeds to xgettext. It rewrites a + Django template into a Python-ish source where every translatable string + appears as a ``gettext(...)`` call and *all* other content -- including + quote characters -- is replaced by filler letters, preserving line and + column positions. That output is deliberately not valid Python (``ast`` + chokes on it), which is why the calls are found by regex, the way + xgettext's own lenient lexer does. The filler guarantee is what makes the + regex safe: a string literal in that output can only have come from a real + translation tag. + +* Python modules are parsed with ``ast``, which is exact. + +This module is test support, not shipped API. Nothing in django_mfa imports +it. +""" + +import ast +import re +from pathlib import Path + +from django.utils.translation.template import templatize + +PACKAGE = Path(__file__).resolve().parents[2] +REPO_ROOT = PACKAGE.parent +LOCALE_DIR = PACKAGE / "locale" + +#: The gettext family Django's template lexer emits, plus the aliases the +#: Python side uses. `_` is included because that is how django_mfa imports +#: gettext_lazy. +GETTEXT_NAMES = frozenset({ + "_", "gettext", "gettext_lazy", "gettext_noop", + "ngettext", "ngettext_lazy", + "pgettext", "pgettext_lazy", "npgettext", "npgettext_lazy", +}) + +_CALL_RE = re.compile(r"\b(?Pn?p?gettext(?:_lazy|_noop)?)\s*\(") +_STR_RE = re.compile( + r"""[ubUB]*(?P'''|\"\"\"|'|")(?P(?:\\.|(?!(?P=q))[\s\S])*)(?P=q)""") + + +def _string_run(source, pos): + """Consecutive comma-separated string literals starting at ``pos``. + + Stops at the first argument that is not a literal, which is how the + count argument of ngettext() and the context of pgettext() are handled: + a context is a literal and so is collected, a count is not and so ends + the run. + """ + literals = [] + while True: + match = _STR_RE.match(source, pos) + if not match: + break + literals.append(ast.literal_eval(match.group(0))) + pos = match.end() + while pos < len(source) and source[pos] in " \t": + pos += 1 + if pos >= len(source) or source[pos] != ",": + break + pos += 1 + while pos < len(source) and source[pos] in " \t": + pos += 1 + return literals + + +def _key_from_call(name, literals): + """Turn one gettext-family call into a catalog key. + + The key is ``(msgctxt, msgid, msgid_plural)`` with None for the parts a + given call shape doesn't carry. Getting this right is the difference + between a usable catalog and a broken one: an ``ngettext`` pair recorded + as two independent singular entries produces msgids gettext will never + look up at runtime, because a plural lookup is keyed on the singular AND + the plural together. + + Returns None for a call with no literal msgid -- ``_(variable)`` is + legal Python that xgettext also declines to extract. + """ + base = name.removesuffix("_lazy").removesuffix("_noop") + has_context = base.startswith("p") or base.startswith("np") + is_plural = base.startswith("n") + + context = None + if has_context: + if not literals: + return None + context, literals = literals[0], literals[1:] + if not literals: + return None + if is_plural: + if len(literals) < 2: + return None + return (context, literals[0], literals[1]) + return (context, literals[0], None) + + +def strings_in_template(path): + """[(key, line number)] for one template file.""" + source = templatize(path.read_text(encoding="utf-8"), origin=str(path)) + found = [] + for match in _CALL_RE.finditer(source): + line = source.count("\n", 0, match.start()) + 1 + key = _key_from_call(match.group("fn"), _string_run(source, match.end())) + if key is not None: + found.append((key, line)) + return found + + +def strings_in_python(path): + """[(key, line number)] for one Python module.""" + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + found = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + func = node.func + name = getattr(func, "id", None) or getattr(func, "attr", None) + if name not in GETTEXT_NAMES: + continue + literals = [] + for arg in node.args: + if isinstance(arg, ast.Constant) and isinstance(arg.value, str): + literals.append(arg.value) + else: + # A non-literal argument ends the run for the same reason as + # in _string_run: it is a count or an interpolation target, + # not something a translator can be handed. + break + # `_` is an alias for gettext_lazy throughout this package; the + # regex-driven template path never sees it, so it is normalised here. + key = _key_from_call("gettext" if name == "_" else name, literals) + if key is not None: + found.append((key, node.lineno)) + return found + + +def source_files(): + """Every file in the package that can hold a translatable string. + + Tests are excluded: their strings are fixtures, and a translator being + asked to translate an assertion message would be a bug in this list. + """ + templates = sorted( + p for p in (PACKAGE / "templates").rglob("*") if p.is_file()) + modules = sorted( + p for p in PACKAGE.rglob("*.py") + if "tests" not in p.relative_to(PACKAGE).parts + ) + return templates, modules + + +def extract(): + """Every translatable string in the package. + + Returns ``{(msgctxt, msgid, msgid_plural): [(repo-relative path, line)]}`` + with keys and references sorted, so the result is stable enough both to + write a .pot from and to compare a committed .pot against. + """ + templates, modules = source_files() + catalog = {} + for path, reader in ( + *((p, strings_in_template) for p in templates), + *((p, strings_in_python) for p in modules), + ): + for key, line in reader(path): + rel = path.relative_to(REPO_ROOT).as_posix() + catalog.setdefault(key, []).append((rel, line)) + return { + key: sorted(set(refs)) + # Sort on the string parts only: None is not orderable against str, + # so a plain sorted() on the keys breaks the moment a context or a + # plural appears alongside an entry without one. + for key, refs in sorted(catalog.items(), key=lambda kv: tuple( + part or "" for part in kv[0])) + } + + +# --- PO files --------------------------------------------------------------- + +def po_escape(value): + return (value.replace("\\", "\\\\").replace('"', '\\"') + .replace("\n", "\\n").replace("\t", "\\t")) + + +def po_unescape(value): + out, i = [], 0 + simple = {"n": "\n", "t": "\t", '"': '"', "\\": "\\"} + while i < len(value): + char = value[i] + if char == "\\" and i + 1 < len(value): + out.append(simple.get(value[i + 1], value[i + 1])) + i += 2 + else: + out.append(char) + i += 1 + return "".join(out) + + +def _blank_entry(): + return {"msgctxt": None, "msgid": None, "msgid_plural": None, + "msgstrs": [], "flags": set()} + + +def parse_po(path): + """Minimal PO reader, returning one dict per entry with the header dropped. + + Each entry carries ``msgctxt``, ``msgid``, ``msgid_plural``, ``msgstrs`` + (a list -- one item for a singular entry, nplurals items for a plural + one) and ``flags``. + + Deliberately not a general-purpose PO parser: it understands exactly the + subset this project's catalogs use, which is also the subset the writer + in tools/ emits. It judges nothing -- the tests do that. + """ + entries, current, field = [], _blank_entry(), None + # Flags precede the entry they describe ("#, fuzzy" sits above its + # msgid), so they are buffered and attached to the NEXT entry. Adding + # them to `current` as they are read would file every flag against the + # preceding entry instead -- which, for a catalog whose central claim is + # "every entry is fuzzy", would be a guard that reads the wrong rows. + pending_flags = set() + + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line: + continue + if line.startswith("#,"): + pending_flags |= {f.strip() for f in line[2:].split(",")} + continue + if line.startswith("#"): + continue + + match = re.match(r"(msgctxt|msgid_plural|msgid|msgstr(?:\[(\d+)\])?)\s+" + r'"(.*)"$', line) + if match: + keyword, _index, value = match.groups() + value = po_unescape(value) + if keyword == "msgid": + if current["msgid"]: + entries.append(current) + current = _blank_entry() + current["flags"], pending_flags = pending_flags, set() + if keyword.startswith("msgstr"): + current["msgstrs"].append(value) + field = ("msgstrs", len(current["msgstrs"]) - 1) + else: + current[keyword] = value + field = (keyword, None) + elif line.startswith('"') and field: + name, index = field + chunk = po_unescape(line[1:-1]) + if index is None: + current[name] = (current[name] or "") + chunk + else: + current[name][index] += chunk + + if current["msgid"]: + entries.append(current) + return entries + + +def po_key(entry): + """The ``extract()`` key an entry corresponds to.""" + return (entry["msgctxt"], entry["msgid"], entry["msgid_plural"]) + + +def locale_files(): + """Every committed catalog: ``{language code: Path}``.""" + if not LOCALE_DIR.is_dir(): + return {} + return { + po.parent.parent.name: po + for po in sorted(LOCALE_DIR.glob("*/LC_MESSAGES/django.po")) + } diff --git a/django_mfa/tests/test_i18n.py b/django_mfa/tests/test_i18n.py new file mode 100644 index 0000000..68048f7 --- /dev/null +++ b/django_mfa/tests/test_i18n.py @@ -0,0 +1,179 @@ +"""Guard the translation catalog and the Python strings that feed it. + +Three distinct failure modes live here, none of which any other test or the +docs build would notice: + +1. **A catalog that has silently rotted.** Someone adds a template string, + never regenerates the catalog, and every translator's file is quietly + missing an entry. Nothing errors -- gettext falls back to the English + source -- so a half-translated page is the only symptom, and only in a + language nobody on the team reads. + +2. **An unreviewed machine draft going live.** The shipped .po files were + machine-drafted and are marked fuzzy, which is exactly what keeps them + inert. Strip a fuzzy flag by accident (a bulk edit, an over-eager + `msgattrib`) and unreviewed text starts appearing in a security UI. + +3. **A lazy string reaching somewhere that can't take one.** gettext_lazy + returns a proxy, not a str. Templates resolve it, and so does + JsonResponse -- but only because it encodes with DjangoJSONEncoder, which + special-cases Promise. A bare json.dumps raises TypeError instead. + +Extraction runs in pure Python (see tests/support/i18n.py) rather than via +`makemessages`, so these run wherever the suite runs -- gettext is not +installed in CI. +""" + +import json +import re +import unittest + +from django.test import SimpleTestCase +from django.utils.functional import Promise + +from django_mfa.adapters.email import EmailAdapter +from django_mfa.adapters.recovery_codes import RecoveryCodesAdapter +from django_mfa.adapters.totp import TOTPAdapter +from django_mfa.adapters.webauthn import WebAuthnAdapter +from django_mfa.models import Authenticator +from django_mfa.tests.support import i18n +from django_mfa.views.verify import GENERIC_ERROR, _passkey_failure + +#: Placeholders are the one thing a translator can break that crashes at +#: runtime rather than merely reading oddly: "%(code)s" renamed or dropped +#: makes the interpolation raise KeyError, in the middle of a sign-in. +PLACEHOLDER_RE = re.compile(r"%\((\w+)\)[sd]") + + +class CatalogCurrentTests(unittest.TestCase): + """The committed catalog matches what the package actually contains.""" + + @classmethod + def setUpClass(cls): + cls.extracted = i18n.extract() + cls.pot = i18n.parse_po(i18n.LOCALE_DIR / "django.pot") + cls.locales = i18n.locale_files() + + def test_pot_exists_and_is_not_empty(self): + self.assertTrue(self.pot, "django.pot has no entries") + + def test_pot_matches_the_source_tree(self): + source_keys = set(self.extracted) + pot_keys = {i18n.po_key(e) for e in self.pot} + + missing = source_keys - pot_keys + stale = pot_keys - source_keys + self.assertEqual( + (missing, stale), (set(), set()), + "django.pot is out of date. Strings in the code but not in the " + f"catalog: {sorted(m[1] for m in missing)}. Entries in the " + f"catalog with no string left in the code: " + f"{sorted(s[1] for s in stale)}. Regenerate with `makemessages`.", + ) + + def test_every_language_covers_the_whole_template(self): + pot_keys = {i18n.po_key(e) for e in self.pot} + self.assertTrue(self.locales, "no .po files found under locale/") + for code, path in self.locales.items(): + with self.subTest(language=code): + keys = {i18n.po_key(e) for e in i18n.parse_po(path)} + self.assertEqual( + keys, pot_keys, + f"{code} does not match django.pot. Missing: " + f"{sorted(k[1] for k in pot_keys - keys)}. Extra: " + f"{sorted(k[1] for k in keys - pot_keys)}.", + ) + + +class DraftsStayInertTests(unittest.TestCase): + """Nothing unreviewed reaches a user. + + Every shipped translation is machine-drafted. `fuzzy` is what makes that + safe: gettext skips a fuzzy entry entirely and falls back to the English + source. A language graduates by a human reviewing its entries and + removing the flags -- at which point this test is the thing that has to + be updated deliberately, which is the point. + """ + + def test_every_translated_entry_is_marked_fuzzy(self): + for code, path in i18n.locale_files().items(): + with self.subTest(language=code): + live = [e["msgid"] for e in i18n.parse_po(path) + if any(e["msgstrs"]) and "fuzzy" not in e["flags"]] + self.assertEqual( + live, [], + f"{code} has non-fuzzy translations, which means they are " + f"live for users: {live}. Either mark them fuzzy, or -- if " + f"a human really has reviewed this language -- update this " + f"test and docs/translations.md together.", + ) + + def test_no_compiled_catalogs_are_committed(self): + """A fully fuzzy catalog compiles to an empty one, so a .mo here is + either dead weight or evidence something was compiled from unreviewed + drafts. Either way it should not ship.""" + found = sorted(p.name for p in i18n.LOCALE_DIR.rglob("*.mo")) + self.assertEqual(found, [], f"unexpected compiled catalogs: {found}") + + +class PlaceholderTests(unittest.TestCase): + def test_translations_keep_every_placeholder(self): + for code, path in i18n.locale_files().items(): + for entry in i18n.parse_po(path): + expected = set(PLACEHOLDER_RE.findall(entry["msgid"])) + if entry["msgid_plural"]: + expected |= set(PLACEHOLDER_RE.findall(entry["msgid_plural"])) + for index, translated in enumerate(entry["msgstrs"]): + if not translated: + continue + with self.subTest(language=code, msgid=entry["msgid"], + form=index): + self.assertEqual( + set(PLACEHOLDER_RE.findall(translated)), expected, + "placeholders differ from the source string, which " + "raises KeyError at interpolation time", + ) + + +class LazyStringTests(SimpleTestCase): + """The Python strings are translatable, and survive where they are used.""" + + def test_user_facing_strings_are_lazy(self): + lazies = { + "GENERIC_ERROR": GENERIC_ERROR, + "TOTPAdapter.verbose_name": TOTPAdapter.verbose_name, + "WebAuthnAdapter.verbose_name": WebAuthnAdapter.verbose_name, + "RecoveryCodesAdapter.verbose_name": RecoveryCodesAdapter.verbose_name, + "EmailAdapter.verbose_name": EmailAdapter.verbose_name, + } + for name, value in lazies.items(): + with self.subTest(string=name): + self.assertIsInstance( + value, Promise, + f"{name} is a plain str, so it is not translatable") + + def test_factor_type_labels_are_lazy(self): + for choice in Authenticator.Type: + with self.subTest(factor=choice.value): + self.assertIsInstance(choice.label, Promise) + + def test_passkey_failure_response_serialises(self): + """The lazy error string still reaches the client as text. + + This works because JsonResponse encodes with DjangoJSONEncoder, + which resolves a Promise; plain json.dumps would raise TypeError and + turn every passkey failure -- the ordinary wrong-credential path, + not an edge case -- into a 500 instead of the deliberately uniform + 400. Asserting the decoded body rather than the encoder keeps this + honest if the response is ever built a different way. + """ + response = _passkey_failure() + self.assertEqual(response.status_code, 400) + self.assertEqual(json.loads(response.content), + {"error": "Passkey sign-in failed."}) + + def test_factor_labels_still_render_as_text(self): + """get_type_display() feeds email templates; a proxy must format.""" + authenticator = Authenticator(type=Authenticator.Type.TOTP) + self.assertEqual(f"{authenticator.get_type_display()}", + "Authenticator app") diff --git a/django_mfa/tests/test_packaging.py b/django_mfa/tests/test_packaging.py index d85bc2e..0d9be53 100644 --- a/django_mfa/tests/test_packaging.py +++ b/django_mfa/tests/test_packaging.py @@ -102,6 +102,33 @@ def test_templates_and_static_files_ship(self): "django_mfa/static/django_mfa/style.css"): self.assertIn(needle, self.names, f"{needle} missing from the wheel") + def test_translation_catalogs_ship(self): + """Catalogs ride on file inclusion too, and their absence is silent. + + Django looks for /locale//LC_MESSAGES/ inside the + *installed* package. A wheel without it doesn't error -- gettext + simply finds no catalog and every string falls back to the English + source, so a fully translated install looks untranslated with + nothing in the logs to say why. + + Asserted on the .po files and the .pot: no .mo ships yet, because + every entry is still a fuzzy machine draft (see + django_mfa/tests/test_i18n.py and docs/translations.md). When a + language is reviewed and starts shipping compiled catalogs, add the + .mo check here -- it is the file gettext actually reads at runtime. + """ + self.assertIn("django_mfa/locale/django.pot", self.names) + catalogs = [n for n in self.names + if n.startswith("django_mfa/locale/") + and n.endswith("/LC_MESSAGES/django.po")] + on_disk = sorted( + p.relative_to(REPO_ROOT).as_posix() + for p in (REPO_ROOT / "django_mfa" / "locale").glob( + "*/LC_MESSAGES/django.po")) + self.assertEqual( + sorted(catalogs), on_disk, + "the locale directory on disk and in the wheel disagree") + def test_migrations_ship(self): """A Django app whose migrations don't ship leaves `migrate` with nothing to apply and no error -- the tables simply never exist. diff --git a/django_mfa/views/verify.py b/django_mfa/views/verify.py index 8e1ca3a..cc1cc57 100644 --- a/django_mfa/views/verify.py +++ b/django_mfa/views/verify.py @@ -7,6 +7,7 @@ from django.http import Http404, HttpResponseNotAllowed, JsonResponse from django.shortcuts import redirect, render, resolve_url from django.utils.http import url_has_allowed_host_and_scheme +from django.utils.translation import gettext_lazy as _ from fido2.webauthn import AuthenticationResponse from django_mfa import events, ratelimit, session @@ -16,7 +17,7 @@ from django_mfa.models import Authenticator from django_mfa.registry import registry -GENERIC_ERROR = "Your code is expired or invalid." +GENERIC_ERROR = _("Your code is expired or invalid.") #: Session key the in-progress *passwordless* authentication #: challenge/state is stashed under between passkey_begin() and @@ -37,12 +38,22 @@ def _passkey_failure(): Deliberately generic and byte-for-byte identical across all of them (same status code, same body, constructed the same way every time): see - passkey_complete()'s docstring for why. A fresh JsonResponse is built on - every call rather than a module-level singleton being reused, since a - response object is mutated as it's rendered/sent and must not be shared - across requests. + passkey_complete()'s docstring for why. Translating it does not weaken + that -- the active language is a property of the request, not of which + failure occurred, so any given caller still gets one identical response + for every failure mode. A fresh JsonResponse is built on every call + rather than a module-level singleton being reused, since a response + object is mutated as it's rendered/sent and must not be shared across + requests. + + The lazy string survives here only because JsonResponse encodes with + DjangoJSONEncoder by default, which resolves a translation proxy; a bare + json.dumps would raise TypeError ("Object of type __proxy__ is not JSON + serializable") and turn every passkey failure into a 500. Swap the + encoder or hand-roll the serialisation and that stops being true -- + test_i18n.py pins the behaviour rather than the implementation. """ - return JsonResponse({"error": "Passkey sign-in failed."}, status=400) + return JsonResponse({"error": _("Passkey sign-in failed.")}, status=400) def _adapter_or_404(factor_type): diff --git a/docs/contributing.md b/docs/contributing.md index f809551..9963cf8 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -14,7 +14,7 @@ To run a single test, pass a label -- `test_runner.py` falls back to the whole s uv run python test_runner.py django_mfa.tests.test_conf -To check one Python/Django combination locally (CI runs the full grid of Python 3.10-3.13 against Django 4.2 and 5.2): +To check one Python/Django combination locally (CI runs the grid of Python 3.10-3.13 against Django 4.2, 5.2 and 6.1, minus the two combinations Django 6.1 doesn't support — it needs Python 3.12+): uv run --python 3.12 --with "django~=4.2.0" python test_runner.py diff --git a/docs/index.md b/docs/index.md index 24ca45e..1d2c16f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,7 @@ mfa_flow :caption: Guides customizing +translations enforcement recipes custom_factors diff --git a/docs/installation_setup.md b/docs/installation_setup.md index 04f09d3..fa2e354 100644 --- a/docs/installation_setup.md +++ b/docs/installation_setup.md @@ -5,7 +5,10 @@ | | | |--------|------------------------| | Python | 3.10, 3.11, 3.12, 3.13 | -| Django | 4.2, 5.2 | +| Django | 4.2, 5.2, 6.1 | + +Django 6.1 needs Python 3.12 or newer — that is Django's own requirement, not +django-mfa's. On Python 3.10 or 3.11, use Django 4.2 or 5.2 (both LTS). Any database Django supports works: factor state lives in a `JSONField`, and no backend-specific features are used. diff --git a/docs/translations.md b/docs/translations.md new file mode 100644 index 0000000..d9502f5 --- /dev/null +++ b/docs/translations.md @@ -0,0 +1,92 @@ +# Translations + +Every string django-mfa shows a user is translatable. What ships today is the +machinery plus six machine-drafted catalogs; **no translation is live yet**, +by design. This page covers what you get out of the box, what your project +has to do to use it, and how to review a language so it starts appearing. + +## What ships + + django_mfa/locale/django.pot the template, 75 entries + django_mfa/locale//LC_MESSAGES/django.po + +Six languages have draft catalogs: `de`, `es`, `fr`, `pt_BR`, `ja`, `zh_Hans`. + +Every entry in every one of them is marked `#, fuzzy`. That is not an +oversight — it is the whole safety model. gettext skips a fuzzy entry and +falls back to the English source, so a machine draft nobody has read cannot +put words in your product's mouth on a sign-in screen. `django_mfa/tests/` +`test_i18n.py` fails the build if a fuzzy flag disappears without the +deliberate steps below. + +For the same reason **no compiled `.mo` files ship**: a fully fuzzy catalog +compiles to an empty one, so shipping it would add bytes and change nothing. + +## Using them in your project + +Django's i18n has to be switched on in the host project — django-mfa can't +do it for you: + + USE_I18N = True + + MIDDLEWARE = [ + ..., + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", # after sessions + "django.middleware.common.CommonMiddleware", + ] + +`LocaleMiddleware` is what picks a language per request. Without it every +request uses `LANGUAGE_CODE` and per-user language selection does nothing. + +:::{warning} +Shadowing a template drops its translations with it. A copy of +`django_mfa/templates/django_mfa/verify_totp.html` in your own app replaces +the shipped file *and* its `{% trans %}` tags — the strings in your copy are +yours to translate, in your project's own catalog. See {doc}`customizing`. +::: + +## Reviewing a language so it goes live + +A draft becomes a real translation when a human who reads the language has +checked it. The steps, in order: + +1. Read `django_mfa/locale//LC_MESSAGES/django.po` end to end. Fix + what's wrong. Pay particular attention to `%(name)s` placeholders: they + must appear in the translation exactly as in the source, or interpolation + raises `KeyError` in the middle of somebody's sign-in. A test enforces + this, but understanding why matters more than the test. +2. Remove the `#, fuzzy` line above each entry you have reviewed. An entry + keeps falling back to English until you do. +3. Compile it: `django-admin compilemessages -l `. This needs the + `gettext` tools installed (`apt install gettext`, `brew install gettext`). +4. Commit the `.mo` alongside the `.po`. It is deliberately un-ignored in + `.gitignore` — the `.mo` is the file gettext reads at runtime, and + hatchling won't put an ignored file in the wheel. +5. Update `test_i18n.py`'s `DraftsStayInertTests`, which asserts nothing is + live. Make it assert what's now true — that this language is reviewed and + the others are not. Changing that test should feel deliberate. + +## Adding a language + + django-admin makemessages -l # run from django_mfa/ + +Then follow the review steps above. There is no draft to start from, which +is fine: an empty `msgstr` falls back to English exactly as a fuzzy one does. + +## Adding or changing a string + +Wrap it — `{% trans %}` / `{% blocktrans %}` in a template, +`gettext_lazy as _` in Python — then regenerate: + + django-admin makemessages -a --keep-pot # from django_mfa/ + +`test_i18n.py` fails if the catalog and the code disagree, so a forgotten +regeneration is caught in CI rather than discovered by a translator months +later. That check runs a pure-Python extractor (`tests/support/i18n.py`) +rather than shelling out to `xgettext`, so it works on machines and CI +images that have no gettext installed. + +Two things are deliberately **not** translated: management-command output +and system-check messages. Both are read by operators and developers, not +end users, and both are matched against by scripts. diff --git a/docs/upgrading.md b/docs/upgrading.md index 38211ea..4c62178 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -4,7 +4,7 @@ This release is a ground-up rewrite: U2F support is gone, TOTP and WebAuthn/pass ## 1. Django 4.2+ and Python 3.10+ are now required -Earlier releases supported Django 2.2-3.2 and Python 3.6-3.10. This release requires Django 4.2 or 5.2, and Python 3.10-3.13 (see `pyproject.toml`). The old code could not run on a newer Django at all -- it imported `django.utils.http.is_safe_url` and `django.utils.translation.ugettext`, both removed in Django 4.0 -- so there is no supported path that runs both the old and new code against the same Django version; the Django/Python upgrade and the django-mfa upgrade have to happen together. +Earlier releases supported Django 2.2-3.2 and Python 3.6-3.10. This release requires Django 4.2, 5.2 or 6.1, and Python 3.10-3.13 (see `pyproject.toml`; Django 6.1 additionally needs Python 3.12+, which is Django's own floor). The old code could not run on a newer Django at all -- it imported `django.utils.http.is_safe_url` and `django.utils.translation.ugettext`, both removed in Django 4.0 -- so there is no supported path that runs both the old and new code against the same Django version; the Django/Python upgrade and the django-mfa upgrade have to happen together. ## 2. U2F support is removed entirely diff --git a/pyproject.toml b/pyproject.toml index 2205ed3..e9c051c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "django-mfa" -version = "4.2.0" +version = "4.3.0" description = "Second-factor authentication for Django: authenticator apps (TOTP), security keys and passkeys (WebAuthn), and recovery codes." readme = "README.md" requires-python = ">=3.10" @@ -23,11 +23,12 @@ dependencies = [ # SPDX `license` expression above (PEP 639). The expression is the source of # truth. classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.2", + "Framework :: Django :: 6.1", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", diff --git a/uv.lock b/uv.lock index 5a95f78..b4d5bc8 100644 --- a/uv.lock +++ b/uv.lock @@ -566,7 +566,7 @@ wheels = [ [[package]] name = "django-mfa" -version = "4.2.0" +version = "4.3.0" source = { editable = "." } dependencies = [ { name = "django", version = "5.2.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },