Skip to content

Warehouse: l10n skeleton #6535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 47 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
dc77e90
Add language switcher UI
nlhkabu Sep 11, 2019
41c996b
[WIP] Makefile, warehouse: l10n skeleton
woodruffw Aug 27, 2019
9477fce
Makefile: Add compile-pos target
woodruffw Aug 27, 2019
b317335
Makefile, warehouse: Add update-po target
woodruffw Aug 27, 2019
fbfad64
warehouse: Fix Accept-Language handling
woodruffw Aug 27, 2019
56e0fa8
warehouse: Move TranslationStringFactory to module scope
woodruffw Aug 27, 2019
98ea80d
warehouse: More l10n work
woodruffw Aug 27, 2019
eb57a6a
warehouse: Language selection form skeleton
woodruffw Aug 27, 2019
b8d811a
warehouse: Expose locale form on account mgmt page
woodruffw Aug 28, 2019
b7131a6
warehouse: Remove unnecessary error boilerplate
woodruffw Aug 29, 2019
55ca1ca
warehouse: Auto-format
woodruffw Aug 29, 2019
a5da547
tests: Remove old locales static view
woodruffw Aug 29, 2019
a94d42b
tests: Update i18n config stub
woodruffw Aug 29, 2019
5a95134
tests: Update default_response
woodruffw Aug 29, 2019
00d7f0f
tests: Auto-format
woodruffw Aug 29, 2019
3afaa8b
warehouse: Revert to functional localize
woodruffw Aug 29, 2019
e01cec8
warehouse: Use IDs for messages
woodruffw Sep 9, 2019
3135725
warehouse: Tag all account view strings
woodruffw Sep 9, 2019
772310c
warehouse: Formatting
woodruffw Sep 12, 2019
3228111
warehouse: Error nits
woodruffw Sep 12, 2019
e42fcbb
tests: Add l10n fixtures, fix account view tests
woodruffw Sep 12, 2019
e8bf88d
warehouse: Move locale route, form out of management
woodruffw Sep 12, 2019
7be37dc
tests: Update manage views, routes tests
woodruffw Sep 12, 2019
8bfd142
tests: Add /locale test
woodruffw Sep 12, 2019
ffd17f3
tests: Fill in remaining i18n tests
woodruffw Sep 12, 2019
7945bed
warehouse: isort
woodruffw Sep 12, 2019
5a94df8
warehouse/accounts: Revert to english copy for IDs
woodruffw Sep 13, 2019
c74a7f9
warehouse/locale: Update master POT
woodruffw Sep 13, 2019
54b61c2
warehouse: Initial lazy string impl
woodruffw Sep 16, 2019
f3d04db
warehouse: Simplify LazyString a bit
woodruffw Sep 16, 2019
1749565
tests: Fix forms tests
woodruffw Sep 16, 2019
3fdec6c
tests: Update i18n, manage forms tests
woodruffw Sep 16, 2019
f417c4a
warehouse: Add language selector functionality
woodruffw Sep 16, 2019
d407342
warehouse: Re-add selected indicator
woodruffw Sep 16, 2019
5287750
tests: Update i18n tests
woodruffw Sep 16, 2019
e8a291b
warehouse: Remove stub langs
woodruffw Sep 16, 2019
7eb8016
Merge branch 'master' into tob-l10n
woodruffw Sep 16, 2019
e1f5061
docker-compose: Run hupper with MO watching rule
woodruffw Sep 16, 2019
23c6964
Makefile, pybabel: Use config file
woodruffw Sep 16, 2019
30e3a2a
locale: Update messages to include HTML
woodruffw Sep 16, 2019
018226b
warehouse/config: Tell Jinja2 our localization domain
woodruffw Sep 18, 2019
9ec8737
tests: Update config tests
woodruffw Sep 19, 2019
6421fd9
warehouse/i18n: Use _ as locale separator
woodruffw Sep 19, 2019
2b1249d
tests/i18n: Fix test_sets_locale
woodruffw Sep 19, 2019
0313b5f
warehouse: Handle LazyString session serialization
woodruffw Sep 19, 2019
066dd6c
tests, warehouse: Fix tests, lint
woodruffw Sep 19, 2019
7dc195d
tests: Add msgpack utils tests
woodruffw Sep 19, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ PR := $(shell echo "$${TRAVIS_PULL_REQUEST:-false}")
BRANCH := $(shell echo "$${TRAVIS_BRANCH:-master}")
DB := example
IPYTHON := no
LOCALES := $(shell find warehouse/locale -type d -depth 1 -exec basename {} \;)

# set environment variable WAREHOUSE_IPYTHON_SHELL=1 if IPython
# needed in development environment
Expand Down Expand Up @@ -168,4 +169,39 @@ purge: stop clean
stop:
docker-compose down -v

.PHONY: default build serve initdb shell tests docs deps travis-deps clean purge debug stop
compile-pot:
$(BINDIR)/pybabel extract \
-F babel.cfg \
--copyright-holder="PyPA" \
--msgid-bugs-address="https://github.com/pypa/warehouse/issues/new" \
--project="Warehouse" \
--output="warehouse/locale/messages.pot" \
warehouse

init-po:
$(BINDIR)/pybabel init \
--input-file="warehouse/locale/messages.pot" \
--output-dir="warehouse/locale/" \
--locale="$(L)"

update-po:
$(BINDIR)/pybabel update \
--input-file="warehouse/locale/messages.pot" \
--output-file="warehouse/locale/$(L)/LC_MESSAGES/messages.po" \
--locale="$(L)"

compile-po:
$(BINDIR)/pybabel compile \
--input-file="warehouse/locale/$(L)/LC_MESSAGES/messages.po" \
--directory="warehouse/locale/" \
--locale="$(L)"

build-mos: compile-pot
for LOCALE in $(LOCALES) ; do \
if [[ -f warehouse/locale/$$LOCALE/LC_MESSAGES/messages.mo ]]; then \
L=$$LOCALE $(MAKE) update-po ; \
fi ; \
L=$$LOCALE $(MAKE) compile-po ; \
done

.PHONY: default build serve initdb shell tests docs deps travis-deps clean purge debug stop compile-pot
4 changes: 4 additions & 0 deletions babel.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[python: **.py]
[jinja2: **.html]
encoding = utf-8
extensions=jinja2.ext.autoescape,jinja2.ext.with_
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ services:
# working on pypi-theme, this is a private repository due to the fact
# that other people's IP is contained in it.
#THEME_REPO:
command: hupper -m gunicorn.app.wsgiapp -b 0.0.0.0:8000 -c gunicorn.conf warehouse.wsgi:application
command: hupper -w 'warehouse/locale/**/*.mo' -m gunicorn.app.wsgiapp -b 0.0.0.0:8000 -c gunicorn.conf warehouse.wsgi:application
env_file: dev/environment
volumes:
# We specify all of these directories instead of just . because we want to
Expand Down
28 changes: 27 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import pytest
import webtest as _webtest

from pyramid.i18n import TranslationString
from pyramid.static import ManifestCacheBuster
from pytest_postgresql.factories import (
drop_postgresql_database,
Expand All @@ -33,12 +34,15 @@
from sqlalchemy import event

from warehouse import admin, config, static
from warehouse.accounts import services as account_services
from warehouse.accounts import services as account_services, views as account_views
from warehouse.macaroons import services as macaroon_services
from warehouse.manage import views as manage_views
from warehouse.metrics import IMetricsService

from .common.db import Session

L10N_TAGGED_MODULES = [account_views, manage_views]


def pytest_collection_modifyitems(items):
for item in items:
Expand Down Expand Up @@ -313,3 +317,25 @@ def pytest_runtest_makereport(item, call):
)
if data:
rep.sections.append(("Captured {} log".format(log_type), data))


@pytest.fixture(scope="session")
def monkeypatch_session():
# NOTE: This is a minor hack to avoid duplicate monkeypatching
# on every function scope for dummy_localize.
# https://github.com/pytest-dev/pytest/issues/1872#issuecomment-375108891
from _pytest.monkeypatch import MonkeyPatch

m = MonkeyPatch()
yield m
m.undo()


@pytest.fixture(scope="session", autouse=True)
def dummy_localize(monkeypatch_session):
def localize(message, **kwargs):
ts = TranslationString(message, **kwargs)
return ts.interpolate()

for mod in L10N_TAGGED_MODULES:
monkeypatch_session.setattr(mod, "_", localize)
47 changes: 26 additions & 21 deletions tests/unit/accounts/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def test_password_confirm_required_error(self):
assert not form.validate()
assert form.password_confirm.errors.pop() == "This field is required."

def test_passwords_mismatch_error(self):
def test_passwords_mismatch_error(self, pyramid_config):
user_service = pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
)
Expand All @@ -266,7 +266,7 @@ def test_passwords_mismatch_error(self):

assert not form.validate()
assert (
form.password_confirm.errors.pop()
str(form.password_confirm.errors.pop())
== "Your passwords don't match. Try again."
)

Expand Down Expand Up @@ -299,7 +299,7 @@ def test_email_required_error(self):
assert not form.validate()
assert form.email.errors.pop() == "This field is required."

def test_invalid_email_error(self):
def test_invalid_email_error(self, pyramid_config):
form = forms.RegistrationForm(
data={"email": "bad"},
user_service=pretend.stub(
Expand All @@ -309,7 +309,9 @@ def test_invalid_email_error(self):
)

assert not form.validate()
assert form.email.errors.pop() == "The email address isn't valid. Try again."
assert (
str(form.email.errors.pop()) == "The email address isn't valid. Try again."
)

def test_exotic_email_success(self):
form = forms.RegistrationForm(
Expand All @@ -323,7 +325,7 @@ def test_exotic_email_success(self):
form.validate()
assert len(form.email.errors) == 0

def test_email_exists_error(self):
def test_email_exists_error(self, pyramid_config):
form = forms.RegistrationForm(
data={"email": "foo@bar.com"},
user_service=pretend.stub(
Expand All @@ -334,12 +336,12 @@ def test_email_exists_error(self):

assert not form.validate()
assert (
form.email.errors.pop()
str(form.email.errors.pop())
== "This email address is already being used by another account. "
"Use a different email."
)

def test_blacklisted_email_error(self):
def test_blacklisted_email_error(self, pyramid_config):
form = forms.RegistrationForm(
data={"email": "foo@bearsarefuzzy.com"},
user_service=pretend.stub(
Expand All @@ -350,12 +352,12 @@ def test_blacklisted_email_error(self):

assert not form.validate()
assert (
form.email.errors.pop()
str(form.email.errors.pop())
== "You can't use an email address from this domain. Use a "
"different email."
)

def test_username_exists(self):
def test_username_exists(self, pyramid_config):
form = forms.RegistrationForm(
data={"username": "foo"},
user_service=pretend.stub(
Expand All @@ -365,13 +367,13 @@ def test_username_exists(self):
)
assert not form.validate()
assert (
form.username.errors.pop()
str(form.username.errors.pop())
== "This username is already being used by another account. "
"Choose a different username."
)

@pytest.mark.parametrize("username", ["_foo", "bar_", "foo^bar"])
def test_username_is_valid(self, username):
def test_username_is_valid(self, username, pyramid_config):
form = forms.RegistrationForm(
data={"username": username},
user_service=pretend.stub(
Expand All @@ -381,7 +383,7 @@ def test_username_is_valid(self, username):
)
assert not form.validate()
assert (
form.username.errors.pop() == "The username is invalid. Usernames "
str(form.username.errors.pop()) == "The username is invalid. Usernames "
"must be composed of letters, numbers, "
"dots, hyphens and underscores. And must "
"also start and finish with a letter or number. "
Expand Down Expand Up @@ -423,7 +425,7 @@ def test_password_breached(self):
"compromised and cannot be used."
)

def test_name_too_long(self):
def test_name_too_long(self, pyramid_config):
form = forms.RegistrationForm(
data={"full_name": "hello " * 50},
user_service=pretend.stub(
Expand All @@ -433,7 +435,7 @@ def test_name_too_long(self):
)
assert not form.validate()
assert (
form.full_name.errors.pop()
str(form.full_name.errors.pop())
== "The name is too long. Choose a name with 100 characters or less."
)

Expand Down Expand Up @@ -493,7 +495,7 @@ def test_password_confirm_required_error(self):
assert not form.validate()
assert form.password_confirm.errors.pop() == "This field is required."

def test_passwords_mismatch_error(self):
def test_passwords_mismatch_error(self, pyramid_config):
form = forms.ResetPasswordForm(
data={
"new_password": "password",
Expand All @@ -507,7 +509,7 @@ def test_passwords_mismatch_error(self):

assert not form.validate()
assert (
form.password_confirm.errors.pop()
str(form.password_confirm.errors.pop())
== "Your passwords don't match. Try again."
)

Expand Down Expand Up @@ -578,7 +580,7 @@ def test_creation(self):

assert form.user_service is user_service

def test_totp_secret_exists(self):
def test_totp_secret_exists(self, pyramid_config):
form = forms.TOTPAuthenticationForm(
data={"totp_value": ""}, user_id=pretend.stub(), user_service=pretend.stub()
)
Expand All @@ -591,15 +593,15 @@ def test_totp_secret_exists(self):
user_service=pretend.stub(check_totp_value=lambda *a: True),
)
assert not form.validate()
assert form.totp_value.errors.pop() == "TOTP code must be 6 digits."
assert str(form.totp_value.errors.pop()) == "TOTP code must be 6 digits."

form = forms.TOTPAuthenticationForm(
data={"totp_value": "123456"},
user_id=pretend.stub(),
user_service=pretend.stub(check_totp_value=lambda *a: False),
)
assert not form.validate()
assert form.totp_value.errors.pop() == "Invalid TOTP code."
assert str(form.totp_value.errors.pop()) == "Invalid TOTP code."

form = forms.TOTPAuthenticationForm(
data={"totp_value": "123456"},
Expand Down Expand Up @@ -627,7 +629,7 @@ def test_creation(self):

assert form.challenge is challenge

def test_credential_bad_payload(self):
def test_credential_bad_payload(self, pyramid_config):
form = forms.WebAuthnAuthenticationForm(
credential="not valid json",
user_id=pretend.stub(),
Expand All @@ -637,7 +639,10 @@ def test_credential_bad_payload(self):
rp_id=pretend.stub(),
)
assert not form.validate()
assert form.credential.errors.pop() == "Invalid WebAuthn assertion: Bad payload"
assert (
str(form.credential.errors.pop())
== "Invalid WebAuthn assertion: Bad payload"
)

def test_credential_invalid(self):
form = forms.WebAuthnAuthenticationForm(
Expand Down
31 changes: 15 additions & 16 deletions tests/unit/accounts/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@


class TestFailedLoginView:
exc = TooManyFailedLogins(resets_in=datetime.timedelta(seconds=600))
request = pretend.stub()
def test_too_many_failed_logins(self):
exc = TooManyFailedLogins(resets_in=datetime.timedelta(seconds=600))
request = pretend.stub(localizer=pretend.stub(translate=lambda tsf: tsf()))

resp = views.failed_logins(exc, request)
resp = views.failed_logins(exc, request)

assert resp.status == "429 Too Many Failed Login Attempts"
assert resp.detail == (
"There have been too many unsuccessful login attempts. " "Try again later."
)
assert dict(resp.headers).get("Retry-After") == "600"
assert resp.status == "429 Too Many Failed Login Attempts"
assert resp.detail == (
"There have been too many unsuccessful login attempts. Try again later."
)
assert dict(resp.headers).get("Retry-After") == "600"


class TestUserProfile:
Expand Down Expand Up @@ -595,7 +596,7 @@ def test_webauthn_get_options_invalid_token(self, monkeypatch):
assert request.session.flash.calls == [
pretend.call("Invalid or expired two factor login.", queue="error")
]
assert result == {"fail": {"errors": ["Invalid two factor token"]}}
assert result == {"fail": {"errors": ["Invalid or expired two factor login."]}}

def test_webauthn_get_options(self, monkeypatch):
_get_two_factor_data = pretend.call_recorder(
Expand Down Expand Up @@ -642,7 +643,7 @@ def test_webauthn_validate_invalid_token(self, monkeypatch):
assert request.session.flash.calls == [
pretend.call("Invalid or expired two factor login.", queue="error")
]
assert result == {"fail": {"errors": ["Invalid two factor token"]}}
assert result == {"fail": {"errors": ["Invalid or expired two factor login."]}}

def test_webauthn_validate_invalid_form(self, monkeypatch):
_get_two_factor_data = pretend.call_recorder(
Expand Down Expand Up @@ -929,10 +930,8 @@ def test_register_fails_with_admin_flag_set(self, db_request):
assert isinstance(result, HTTPSeeOther)
assert db_request.session.flash.calls == [
pretend.call(
(
"New user registration temporarily disabled. "
"See https://pypi.org/help#admin-intervention for details."
),
"New user registration temporarily disabled. "
"See https://pypi.org/help#admin-intervention for details.",
queue="error",
)
]
Expand Down Expand Up @@ -1450,8 +1449,8 @@ def test_verify_email(
@pytest.mark.parametrize(
("exception", "message"),
[
(TokenInvalid, "Invalid token: request a new verification link"),
(TokenExpired, "Expired token: request a new verification link"),
(TokenInvalid, "Invalid token: request a new email verification link"),
(TokenExpired, "Expired token: request a new email verification link"),
(TokenMissing, "Invalid token: no token supplied"),
],
)
Expand Down
Loading