diff --git a/src/ComicInfoBuilder.py b/src/ComicInfoBuilder.py
index 759946b..9f03eb4 100644
--- a/src/ComicInfoBuilder.py
+++ b/src/ComicInfoBuilder.py
@@ -694,6 +694,63 @@ class ComicInfoBuilder:
return result
+ def _collect_all_alt_titles(self, md: dict) -> "dict[str, list[str]]":
+ """
+ Returns all known title variants grouped by language/script.
+
+ Groups collected (skipped when empty):
+ "en" – English (language = "en")
+ "de" – German (language = "de")
+ "ja" – Japanese native kanji (language = "ja")
+ "ja-romaji" – Japanese romanized (language = "ja-Latn" / "ja-romaji")
+ "ko" – Korean native (language = "ko")
+ "ko-romaji" – Korean romanized (language = "ko-Latn" / "ko-romaji")
+ "zh" – Chinese native (language = "zh" / "zh-hk" / "zh-tw" / …)
+ "zh-romaji" – Chinese romanized (language = "zh-Latn")
+
+ All variants are included (not just primary), preserving API order.
+ Duplicates within a group are removed.
+ """
+ _GROUPS: "dict[str, tuple]" = {
+ "en": ("en",),
+ "de": ("de",),
+ "ja": ("ja",),
+ "ja-romaji": ("ja-latn", "ja-romaji"),
+ "ko": ("ko",),
+ "ko-romaji": ("ko-latn", "ko-romaji"),
+ "zh": ("zh", "zh-hk", "zh-tw", "zh-hans", "zh-hant"),
+ "zh-romaji": ("zh-latn",),
+ }
+
+ # Pre-build a flat lang → group mapping for O(1) lookup
+ lang_to_group: "dict[str, str]" = {
+ lang: group
+ for group, langs in _GROUPS.items()
+ for lang in langs
+ }
+
+ result: "dict[str, list[str]]" = {}
+ seen: "dict[str, set[str]]" = {}
+
+ for entry in (md.get("titles") or md.get("alt_titles") or []):
+ if not isinstance(entry, dict):
+ continue
+ lang = (entry.get("language") or entry.get("lang") or "").lower()
+ group = lang_to_group.get(lang)
+ if not group:
+ continue
+ title = (entry.get("title") or "").strip()
+ if not title:
+ continue
+ if group not in result:
+ result[group] = []
+ seen[group] = set()
+ if title not in seen[group]:
+ result[group].append(title)
+ seen[group].add(title)
+
+ return result
+
# ======================================================================
# Summary / notes
# ======================================================================
@@ -706,10 +763,6 @@ class ComicInfoBuilder:
1. MAL statistics — HTML link + table with padded columns
2. Series description — Markdown converted to HTML
3. Alternate titles — HTML table
-
- IMPORTANT: no raw \\n characters anywhere in the output — Kavita
- renders every bare newline as a
. Sections are separated with
- an explicit
instead.
"""
# Inline style applied to label cells for readable column spacing.
_TD = 'style="padding-right:1.5em"'
@@ -744,16 +797,28 @@ class ComicInfoBuilder:
if desc_raw:
parts.append(_md_to_html(desc_raw))
- # 3. Alternate titles table (bottom) ------------------------------
- alt = self._collect_alt_titles(md)
- if alt:
- label_map = {"en": "EN", "de": "DE", "romaji": "Romaji", "jp": "JP (Kanji)"}
+ # 3. Alternate titles table (bottom) — all variants per language ------
+ all_alt = self._collect_all_alt_titles(md)
+ if all_alt:
+ label_map = {
+ "en": "EN",
+ "de": "DE",
+ "ja": "JA",
+ "ja-romaji": "JA Romaji",
+ "ko": "KO",
+ "ko-romaji": "KO Romaji",
+ "zh": "ZH",
+ "zh-romaji": "ZH Romaji",
+ }
alt_rows: list[str] = []
- for code in ("en", "de", "romaji", "jp"):
- if code in alt:
- alt_rows.append(
- f"