Translate the whole site to English: English routes with 301 redirects from the Dutch paths, English UI, docs, seeds and captions, one-time live content migration

This commit is contained in:
2026-10-05 11:25:41 +02:00
parent bbdecdc95b
commit ff547e1677
153 changed files with 1095 additions and 774 deletions
+10 -10
View File
@@ -3,7 +3,7 @@ JSON = {"Accept": "application/json"}
def _create(admin, **overrides):
payload = {"title": "Een echte notitie", "body": "Tekst met **nadruk** en `code`.", "topic": "rol", "status": "draft"}
payload = {"title": "A real note", "body": "Text with **emphasis** and `code`.", "topic": "roll", "status": "draft"}
payload.update(overrides)
response = admin.post("/admin/posts", json=payload, headers=JSON)
assert response.status_code == 200, response.text
@@ -11,24 +11,24 @@ def _create(admin, **overrides):
def test_create_draft_is_hidden_then_published(admin, anon):
data = _create(admin, title="Concept dat later live gaat")
data = _create(admin, title="Draft that goes live later")
assert anon.get(f"/posts/{data['slug']}").status_code == 404
assert admin.post(f"/admin/posts/{data['uid']}/publish", headers=JSON).status_code == 200
page = anon.get(f"/posts/{data['slug']}")
assert page.status_code == 200
assert "<strong>nadruk</strong>" in page.text
assert "<strong>emphasis</strong>" in page.text
def test_edit_post(admin, anon):
data = _create(admin, title="Te bewerken notitie", status="published")
response = admin.post(f"/admin/posts/{data['uid']}", json={"title": "Bewerkte notitie", "body": "Nieuwe tekst hier.", "topic": "standaard", "status": "published"}, headers=JSON)
data = _create(admin, title="Note to edit", status="published")
response = admin.post(f"/admin/posts/{data['uid']}", json={"title": "Edited note", "body": "New text right here.", "topic": "standard", "status": "published"}, headers=JSON)
assert response.status_code == 200, response.text
posts = anon.get("/standaard", headers=JSON).json()["posts"]
assert any(post["title"] == "Bewerkte notitie" for post in posts)
posts = anon.get("/standard", headers=JSON).json()["posts"]
assert any(post["title"] == "Edited note" for post in posts)
def test_delete_goes_to_trash(admin, anon):
data = _create(admin, title="Weg ermee notitie", status="published")
data = _create(admin, title="Gone with it note", status="published")
assert admin.post(f"/admin/posts/{data['uid']}/delete", headers=JSON).status_code == 200
assert anon.get(f"/posts/{data['slug']}").status_code == 404
trash = admin.get("/admin/trash", headers=JSON).json()["items"]
@@ -41,10 +41,10 @@ def test_validation_error(admin):
def test_html_form_post(admin):
response = admin.post("/admin/posts", data={"title": "Via formulier", "body": "Formuliertekst hier.", "topic": "rol", "status": "draft"})
response = admin.post("/admin/posts", data={"title": "Via the form", "body": "Form text right here.", "topic": "roll", "status": "draft"})
assert response.status_code in (302, 303)
def test_member_cannot_write(member):
response = member.post("/admin/posts", json={"title": "Niet toegestaan", "body": "Tekst hier.", "topic": "rol"}, headers=JSON)
response = member.post("/admin/posts", json={"title": "Not allowed", "body": "Text right here.", "topic": "roll"}, headers=JSON)
assert response.status_code == 403
+7 -7
View File
@@ -4,15 +4,15 @@
def test_home_is_molodetz(anon):
html = anon.get("/").text
assert "Molodetz" in html
for label in ("Rol", "Standaard", "Flyers", "Memes", "Mensen", "Binnen"):
for label in ("Roll", "Standard", "Flyers", "Memes", "People", "Join"):
assert f">{label}</a>" in html
assert "wa.me" not in html and "whatsapp" not in html.lower()
def test_unknown_page_is_404(anon):
response = anon.get("/bestaat-niet-xyz")
response = anon.get("/does-not-exist-xyz")
assert response.status_code == 404
assert anon.get("/bestaat-niet-xyz", headers={"Accept": "application/json"}).json()["error"]
assert anon.get("/does-not-exist-xyz", headers={"Accept": "application/json"}).json()["error"]
def _intro_section(html):
@@ -47,15 +47,15 @@ def test_intro_json_face(anon):
def test_admin_can_edit_intro(admin, anon):
original = admin.get("/admin/settings", headers={"Accept": "application/json"}).json()["settings"]["site_intro"]
try:
response = admin.post("/admin/settings", json={"site_intro": "Eerste regel.\n\nTweede alinea\nmet een regel erbij."}, headers={"Accept": "application/json"})
response = admin.post("/admin/settings", json={"site_intro": "First line.\n\nSecond paragraph\nwith an extra line."}, headers={"Accept": "application/json"})
assert response.status_code == 200
intro = _intro_section(anon.get("/").text)
assert intro.find("p").get_text() == "Eerste regel."
assert intro.find("p").get_text() == "First line."
assert intro.find_all("p")[1].find("br") is not None
form = admin.post("/admin/settings", data={"site_intro": "Via formulier.\r\n\r\nRegel een\r\nregel twee"})
form = admin.post("/admin/settings", data={"site_intro": "Via the form.\r\n\r\nLine one\r\nline two"})
assert form.status_code in (302, 303)
stored = admin.get("/admin/settings", headers={"Accept": "application/json"}).json()["settings"]["site_intro"]
assert stored == "Via formulier.\n\nRegel een\nregel twee"
assert stored == "Via the form.\n\nLine one\nline two"
finally:
admin.post("/admin/settings", json={"site_intro": original}, headers={"Accept": "application/json"})
assert _intro_section(anon.get("/").text).find("p").get_text(" ", strip=True).startswith("I wrote more code than you.")
+5 -5
View File
@@ -3,11 +3,11 @@ JSON = {"Accept": "application/json"}
def test_join_request_is_stored_and_visible_to_admin(anon, admin):
response = anon.post("/binnen", data={"name": "Schrijver Een", "contact": "schrijver@example.invalid", "repo_url": "https://example.com/repo", "message": "Ik wil meeschrijven.", "website": ""})
response = anon.post("/join", data={"name": "Writer One", "contact": "schrijver@example.invalid", "repo_url": "https://example.com/repo", "message": "I want to write along.", "website": ""})
assert response.status_code in (302, 303)
assert "ok=1" in response.headers["location"]
rows = admin.get("/admin/joins", headers=JSON).json()["requests"]
row = next(row for row in rows if row["name"] == "Schrijver Een")
row = next(row for row in rows if row["name"] == "Writer One")
assert row["contact"] == "schrijver@example.invalid"
assert row["repo_url"].startswith("https://example.com/repo")
status = admin.post(f"/admin/joins/{row['uid']}/status", json={"status": "contacted"}, headers=JSON)
@@ -15,19 +15,19 @@ def test_join_request_is_stored_and_visible_to_admin(anon, admin):
def test_join_json_api(anon):
response = anon.post("/binnen", json={"name": "Schrijver Twee", "contact": "@twee"}, headers=JSON)
response = anon.post("/join", json={"name": "Writer Two", "contact": "@twee"}, headers=JSON)
assert response.status_code == 200
assert response.json()["ok"] is True
def test_honeypot_is_not_stored(anon, admin):
anon.post("/binnen", data={"name": "Robot Bot", "contact": "bot@example.invalid", "website": "spam"})
anon.post("/join", data={"name": "Robot Bot", "contact": "bot@example.invalid", "website": "spam"})
rows = admin.get("/admin/joins", headers=JSON).json()["requests"]
assert not any(row["name"] == "Robot Bot" for row in rows)
def test_join_validation(anon):
response = anon.post("/binnen", json={"name": "x", "contact": ""}, headers=JSON)
response = anon.post("/join", json={"name": "x", "contact": ""}, headers=JSON)
assert response.status_code == 422
+57
View File
@@ -0,0 +1,57 @@
# retoor <retoor@molodetz.nl>
import re
import pytest
PAGES = ["/", "/roll", "/standard", "/flyers", "/memes", "/people", "/people/retoor", "/join", "/terms", "/privacy", "/auth/login", "/docs", "/docs/welcome", "/docs/api/content"]
DUTCH = ("Ik schrijf mee", "Mensen", "Binnen", "Voorwaarden", "Inloggen", "Meldingen", "Beheer", "Naar inhoud", "Nog niets", "Intro bewerken", "Een zes is", "Eenendertig", "Gezet in serif", "Verder lezen")
@pytest.mark.parametrize("path", PAGES)
def test_pages_are_english(anon, path):
html = anon.get(path).text
assert '<html lang="en">' in html
assert 'content="en_US"' in html
visible = re.sub(r"<script.*?</script>", "", html, flags=re.S)
for word in DUTCH:
assert word not in visible, (path, word)
def test_nav_uses_english_paths(anon):
html = anon.get("/").text
for href, label in (("/roll", "Roll"), ("/standard", "Standard"), ("/people", "People"), ("/join", "Join")):
assert f'href="{href}"' in html and f">{label}</a>" in html
for old in ('href="/rol"', 'href="/standaard"', 'href="/mensen"', 'href="/binnen"', 'href="/voorwaarden"'):
assert old not in html
assert ">I'm in</a>" in html or ">I&#39;m in</a>" in html
def test_admin_sees_edit_intro(admin):
html = admin.get("/").text
assert "Edit intro" in html
assert "Intro bewerken" not in html
def test_sitemap_lists_english_paths(anon):
xml = anon.get("/sitemap.xml").text
for path in ("/roll<", "/standard<", "/people<", "/join<", "/terms<", "/docs/welcome<"):
assert path in xml, path
for old in ("/rol<", "/mensen", "/binnen", "/voorwaarden", "/docs/welkom"):
assert old not in xml, old
def test_gallery_captions_are_english(anon):
flyers = anon.get("/flyers", headers={"Accept": "application/json"}).json()["items"]
captions = {item["caption"] for item in flyers}
assert "Thirty-one choices. You have zero." in captions
assert not any(" geen " in f" {caption.lower()} " or " een " in f" {caption.lower()} " for caption in captions)
def test_english_migration_helpers():
from molodetz.bootstrap import _translate_notification
assert _translate_notification("Ada wil meeschrijven") == "Ada wants to write along"
assert _translate_notification("Gepubliceerd: Geen cursus.") == "Published: No course."
assert _translate_notification("Back-up database klaar") == "Backup database finished"
assert _translate_notification("Je API-sleutel is vernieuwd.") == "Your API key was renewed."
assert _translate_notification("Already English") == "Already English"
+68
View File
@@ -0,0 +1,68 @@
# retoor <retoor@molodetz.nl>
import pytest
from molodetz.routers.legacy import LEGACY_PATHS
REDIRECTS = [
("/rol", "/roll"),
("/standaard", "/standard"),
("/mensen", "/people"),
("/mensen/retoor", "/people/retoor"),
("/binnen", "/join"),
("/voorwaarden", "/terms"),
("/docs/welkom", "/docs/welcome"),
("/docs/schrijven", "/docs/writing"),
("/docs/binnen", "/docs/join"),
("/docs/juridisch", "/docs/legal"),
("/docs/architectuur", "/docs/architecture"),
("/docs/api/inhoud", "/docs/api/content"),
("/docs/api/binnen", "/docs/api/join"),
]
@pytest.mark.parametrize("old,new", REDIRECTS)
def test_old_dutch_paths_redirect_permanently(anon, old, new):
response = anon.get(old)
assert response.status_code == 301, old
assert response.headers["location"] == new
assert anon.get(new).status_code == 200, new
def test_redirect_keeps_query_string(anon):
response = anon.get("/rol?before=2026-01-01")
assert response.status_code == 301
assert response.headers["location"] == "/roll?before=2026-01-01"
def test_old_join_post_keeps_method(anon):
response = anon.post("/binnen", data={"name": "x"})
assert response.status_code == 308
assert response.headers["location"] == "/join"
def test_legacy_paths_are_hidden_from_schema():
from molodetz.main import app
paths = app.openapi()["paths"]
assert not any(old in paths for old in LEGACY_PATHS)
assert "/mensen/{username}" not in paths
def test_old_post_slug_redirects_to_english_slug(anon, admin):
from molodetz.content import translate_seeded_posts
from molodetz.database import db, get_user_by_username, insert_post, update_post_row
author = get_user_by_username("retoor")
row = insert_post(author["uid"], "Af is een eigenschap.", "Een project dat open blijft, is een mening.", "rol", status="published", is_placeholder=True)
try:
assert translate_seeded_posts() >= 1
migrated = db["posts"].find_one(uid=row["uid"])
assert migrated["title"] == "Finished is a feature."
assert migrated["topic"] == "roll"
assert migrated["slug"].startswith("finished-is-a-feature-")
assert "Carried over from the old Molodetz page" in migrated["body"]
response = anon.get(f"/posts/{row['slug']}")
assert response.status_code == 301
assert response.headers["location"] == f"/posts/{migrated['slug']}"
finally:
update_post_row(row["uid"], deleted_at="2026-01-01T00:00:00+00:00", deleted_by="test")
+2 -2
View File
@@ -1,8 +1,8 @@
# retoor <retoor@molodetz.nl>
def test_mensen_lists_real_accounts_only(anon):
people = anon.get("/mensen", headers={"Accept": "application/json"}).json()
def test_people_lists_real_accounts_only(anon):
people = anon.get("/people", headers={"Accept": "application/json"}).json()
names = [person["username"] for person in people.get("people", [])]
assert names[0] == "retoor"
assert set(names) <= {"retoor", "lezer"}
+2 -2
View File
@@ -2,8 +2,8 @@
def test_post_page(anon):
posts = anon.get("/rol", headers={"Accept": "application/json"}).json()["posts"]
post = next(post for post in posts if post["title"] == "Geen cursus.")
posts = anon.get("/roll", headers={"Accept": "application/json"}).json()["posts"]
post = next(post for post in posts if post["title"] == "No course.")
response = anon.get(f"/posts/{post['slug']}")
assert response.status_code == 200
assert "Placeholder" in response.text
-11
View File
@@ -1,11 +0,0 @@
# retoor <retoor@molodetz.nl>
def test_seeded_posts_and_placeholders(anon):
rol = anon.get("/rol", headers={"Accept": "application/json"}).json()["posts"]
seeds = {"Een zes is geen ambitie.", "Af is een eigenschap.", "De assistent is geen senior.", "Geen cursus."}
assert seeds <= {post["title"] for post in rol}
assert all(post["is_placeholder"] for post in rol if post["title"] in seeds)
standaard = anon.get("/standaard", headers={"Accept": "application/json"}).json()["posts"]
keuzes = next(post for post in standaard if post["title"] == "Eenendertig keuzes. Jij hebt er nul.")
assert keuzes["is_placeholder"] is False
+33
View File
@@ -0,0 +1,33 @@
# retoor <retoor@molodetz.nl>
from molodetz.content import PLACEHOLDER_NOTE, SEED_POSTS
ROLL_SEEDS = {"A pass is not an ambition.", "Finished is a feature.", "The assistant is not a senior.", "No course."}
def test_seeded_posts_and_placeholders(anon):
roll = anon.get("/roll", headers={"Accept": "application/json"}).json()["posts"]
assert ROLL_SEEDS <= {post["title"] for post in roll}
assert all(post["is_placeholder"] for post in roll if post["title"] in ROLL_SEEDS)
assert all(post["topic"] == "roll" for post in roll)
standard = anon.get("/standard", headers={"Accept": "application/json"}).json()["posts"]
choices = next(post for post in standard if post["title"] == "Thirty-one choices. You have zero.")
assert choices["is_placeholder"] is False
assert choices["slug"].startswith("thirty-one-choices-you-have-zero-")
def test_seed_texts_are_english_and_keep_placeholder_marker():
assert {title for _, title, _ in SEED_POSTS} >= ROLL_SEEDS | {"Thirty-one choices. You have zero."}
assert PLACEHOLDER_NOTE.strip().startswith("> Placeholder.")
assert "retoor will replace it" in PLACEHOLDER_NOTE
for _, title, body in SEED_POSTS:
for dutch in (" een ", " het ", " niet ", " geen ", " wie "):
assert dutch not in f" {title.lower()} {body.lower()} ", (title, dutch)
assert chr(0x2014) not in body and chr(0x2013) not in body
def test_placeholder_note_renders_on_roll_post(anon):
roll = anon.get("/roll", headers={"Accept": "application/json"}).json()["posts"]
post = next(post for post in roll if post["title"] == "Finished is a feature.")
html = anon.get(f"/posts/{post['slug']}").text
assert "Carried over from the old Molodetz page" in html
assert "A project that stays open is an opinion." in html
+3 -3
View File
@@ -3,8 +3,8 @@ import re
import pytest
PUBLIC_PAGES = ["/", "/rol", "/standaard", "/flyers", "/memes", "/mensen", "/mensen/retoor", "/binnen", "/docs", "/docs/welkom", "/docs/api/inhoud", "/docs/search?q=markdown", "/voorwaarden", "/privacy", "/auth/login"]
JSON_PAGES = ["/", "/rol", "/standaard", "/flyers", "/memes", "/mensen", "/binnen"]
PUBLIC_PAGES = ["/", "/roll", "/standard", "/flyers", "/memes", "/people", "/people/retoor", "/join", "/docs", "/docs/welcome", "/docs/api/content", "/docs/search?q=markdown", "/terms", "/privacy", "/auth/login"]
JSON_PAGES = ["/", "/roll", "/standard", "/flyers", "/memes", "/people", "/join"]
@pytest.mark.parametrize("path", PUBLIC_PAGES)
@@ -23,7 +23,7 @@ def test_json_faces(anon, path):
def test_no_bare_static_urls_in_rendered_html(anon):
for path in ("/", "/flyers", "/memes", "/rol"):
for path in ("/", "/flyers", "/memes", "/roll"):
html = anon.get(path).text
assert not re.search(r"""(src|href)="/static/(?!v|uploads/)""", html), path