Fix pluralization placeholders across the UI (v0.6.2)

Replaced "episode(s)"/"show(s)"/"topic(s)" text with real
pluralization everywhere it appeared.
This commit is contained in:
flan
2026-07-11 07:53:53 +00:00
parent 1d636219b8
commit 3715117d5b
7 changed files with 42 additions and 12 deletions
+1
View File
@@ -11,3 +11,4 @@ hark.db
hark.db-*
auth.db
data/
.scratch/
+16
View File
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.6.2] - 2026-07-11
### Fixed
- Proper pluralization ("1 episode" / "2 episodes") across the UI, replacing
the placeholder "episode(s)"/"show(s)"/"topic(s)" text everywhere it
appeared (home page status banner, topic/show/search pages).
- Topic 730's label was a mis-extracted book citation ("Jerome Jacobson
(ed.). Studies in the archaeology of India and Pakistan...") that had also
been canonicalized to the wrong Wikidata entity — a same-name collision
with an unrelated archaeology book editor, not the actual McDonald's
Monopoly fraud perpetrator these two episodes cover. Relabeled to
"McDonald's Monopoly fraud" and pointed at Q16997479 (the closest real
entity available; no dedicated fraud-specific Wikidata item exists).
Data-only fix (topics table), not yet re-synced to the deployed instance.
## [0.6.1] - 2026-07-11
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "hark"
version = "0.6.1"
version = "0.6.2"
description = "Cross-podcast topic index and discovery service"
readme = "README.md"
requires-python = ">=3.12"
+1 -1
View File
@@ -1,3 +1,3 @@
"""hark — cross-podcast topic index and discovery service."""
__version__ = "0.6.1"
__version__ = "0.6.2"
+14 -7
View File
@@ -245,6 +245,10 @@ def esc(value) -> str:
return html.escape(str(value if value is not None else ""))
def plural(n: int, word: str) -> str:
return f"{n} {word}" if n == 1 else f"{n} {word}s"
def page(title: str, body: str, user: str | None = None) -> str:
header = HEADER.format(user=esc(user)) if user else ""
return PAGE.format(title=esc(title), header=header, body=body)
@@ -437,7 +441,7 @@ class App:
)
body = (
f"<h1>{esc(topic['label'])}{qid}</h1><p>{pills}</p>"
f"<h2>covered by {len(shows)} show(s), {len(episodes)} episode(s)</h2>"
f"<h2>covered by {plural(len(shows), 'show')}, {plural(len(episodes), 'episode')}</h2>"
f"<p>{show_pills}</p>"
f'<table><tr><th>show</th><th>episode</th><th>date</th>'
f'<th title="extractor\'s confidence this episode is really about this topic">conf</th></tr>'
@@ -483,7 +487,7 @@ class App:
if q:
topics_pager = pagination_html("/search", {"q": q}, page_num, topic_total, "topics")
no_match = f"No topics match “{q}”."
body += (f"<h2>{topic_total} topic(s)</h2>"
body += (f"<h2>{plural(topic_total, 'topic')}</h2>"
+ topic_table(topics, empty=no_match) + topics_pager)
if episodes:
eps = "".join(
@@ -497,7 +501,8 @@ class App:
eps_table = f'<p class="dim">No episode titles match “{q}”.</p>'
note = (f'<p class="dim">showing the 50 most recent of {episode_total}'
f"narrow your search to see the rest.</p>") if episode_total > 50 else ""
body += f"<h2>{episode_total} episode title match(es)</h2>{eps_table}{note}"
match_word = "match" if episode_total == 1 else "matches"
body += f"<h2>{episode_total} episode title {match_word}</h2>{eps_table}{note}"
return page("search", body, user["username"])
def view_shows(self, user) -> str:
@@ -597,7 +602,7 @@ class App:
)
body = (
f"<h1>{esc(show['name'])}</h1>"
f"<h2>{total_episodes} episode(s), {topic_count} topic(s) covered</h2>"
f"<h2>{plural(total_episodes, 'episode')}, {plural(topic_count, 'topic')} covered</h2>"
f"{adblock_section}"
f"<table><tr><th>episode</th><th>date</th><th>topics</th></tr>{rows_html}</table>{pager}"
)
@@ -779,14 +784,16 @@ def index_status_html(pending_episodes: int, pending_canon: int, last_extracted_
active = last_dt is not None and utcnow() - last_dt < ACTIVE_WINDOW
if active:
lines.append(
f"<p>Indexing in progress — {pending_episodes} episode(s) queued, "
f"<p>Indexing in progress — {plural(pending_episodes, 'episode')} queued, "
f"last processed {relative_time(last_dt)}.</p>"
)
else:
when = f", last activity {relative_time(last_dt)}" if last_dt else ""
lines.append(f"<p>{pending_episodes} episode(s) not yet indexed{when}.</p>")
lines.append(f"<p>{plural(pending_episodes, 'episode')} not yet indexed{when}.</p>")
if pending_canon:
lines.append(f"<p class=\"dim\">{pending_canon} topic(s) awaiting Wikidata canonicalization.</p>")
lines.append(
f"<p class=\"dim\">{plural(pending_canon, 'topic')} awaiting Wikidata canonicalization.</p>"
)
if not lines:
return ""
cls = "status active" if active else "status"
+8 -2
View File
@@ -145,7 +145,7 @@ def test_index_status_shows_pending_episodes(tmp_path):
resp, _ = request(srv, "POST", "/login", body={"username": "admin", "password": "t"})
cookie = resp.getheader("Set-Cookie").split(";")[0]
resp, body = request(srv, "GET", "/", cookie=cookie)
assert "1 episode(s) not yet indexed" in body
assert "1 episode not yet indexed" in body
assert "Indexing in progress" not in body # last activity is months old, not active
finally:
srv.shutdown()
@@ -232,7 +232,7 @@ def test_show_page_paginates_episodes(tmp_path):
resp, body = request(srv, "GET", "/show/1", cookie=cookie)
assert resp.status == 200
assert "60 episode(s)" in body # total is the real count, not the page size
assert "60 episodes" in body # total is the real count, not the page size
assert body.count("<tr><td>") == web.PAGE_SIZE # only one page's worth rendered
assert "page 1 of 2" in body
@@ -354,6 +354,12 @@ def test_adblock_toggle_requires_login(server):
assert resp.getheader("Location") == "/login"
def test_plural():
assert web.plural(0, "episode") == "0 episodes"
assert web.plural(1, "episode") == "1 episode"
assert web.plural(2, "episode") == "2 episodes"
def test_episode_page_404_for_missing_episode(server):
cookie = login(server)
resp, _ = request(server, "GET", "/episode/999", cookie=cookie)
Generated
+1 -1
View File
@@ -257,7 +257,7 @@ wheels = [
[[package]]
name = "hark"
version = "0.6.1"
version = "0.6.2"
source = { editable = "." }
dependencies = [
{ name = "adscrub" },