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:
@@ -11,3 +11,4 @@ hark.db
|
||||
hark.db-*
|
||||
auth.db
|
||||
data/
|
||||
.scratch/
|
||||
|
||||
@@ -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
@@ -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,3 +1,3 @@
|
||||
"""hark — cross-podcast topic index and discovery service."""
|
||||
|
||||
__version__ = "0.6.1"
|
||||
__version__ = "0.6.2"
|
||||
|
||||
+14
-7
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user