Repository: BoboTiG/ebook-reader-dict

Issue:
[CA] Add transliteration in 'etim-lang' template
- Wiktionary page: https://ca.wiktionary.org/wiki/feocromocitoma

Wikicode:
```
{{etim-lang|grc|ca|φαιός|trad=gris}}
```

Output:
```
Del grec antic <i>φαιός</i> («gris»)
```

Expected:
```
Del grec antic <i>φαιός</i> (<i>phaiós</i>, «gris»)
```

---

Model link, if any: https://ca.wiktionary.org/wiki/Plantilla:etim-lang


Discussion:
BoboTiG: I guess we can say all linked issues will be fixed. We will open speific ticket when we hit a unhandled case (I edited the description to manage the autoclose action).

Patch:
diff --git a/wikidict/check_word.py b/wikidict/check_word.py
index 38f78362..c87694c4 100644
--- a/wikidict/check_word.py
+++ b/wikidict/check_word.py
@@ -93,7 +93,7 @@ def filter_html(html: str, locale: str) -> str:
                 i.decompose()
         # Filter out anchors as they are ignored from templates
         for a in bs.find_all("a", href=True):
-            if a["href"].startswith("#"):
+            if a["href"].startswith("#") and "mw-selflink-fragment" not in a.get("class", []):
                 a.decompose()
 
     elif locale == "de":
diff --git a/wikidict/lang/ca/__init__.py b/wikidict/lang/ca/__init__.py
index 551a572e..c51bb875 100644
--- a/wikidict/lang/ca/__init__.py
+++ b/wikidict/lang/ca/__init__.py
@@ -3,6 +3,7 @@
 from typing import List, Pattern, Tuple
 
 from ...user_functions import uniq
+from .grc_trans import transliterate
 
 # Float number separator
 float_separator = ","
@@ -87,8 +88,6 @@
     "def-meta": "italic(parts[-1])",
     # {{doblet|ca|Castellar}}
     "doblet": "italic(parts[-1])",
-    # {{e|la|lupus}}
-    "e": "parts[-1]",
     # {{e-propi|ca|grèvol}}
     "e-propi": "strong(parts[-1])",
     # {{forma-f|ca|halloweenià}}
@@ -172,6 +171,9 @@ def last_template_handler(template: Tuple[str, ...], locale: str, word: str = ""
         >>> last_template_handler(["calc semàntic", "es", "ca", "pueblo"], "ca")
         'calc semàntic del castellà <i>pueblo</i>'
 
+        >>> last_template_handler(["e", "grc", "υ", "tr=-"], "ca")
+        'υ'
+
         >>> last_template_handler(["epònim", "ca", "w=Niels Henrik Abel"], "ca")
         'Niels Henrik Abel'
         >>> last_template_handler(["epònim", "ca", "André-Marie Ampère", "w=Niels Henrik Abel"], "ca")
@@ -183,11 +185,17 @@ def last_template_handler(template: Tuple[str, ...], locale: str, word: str = ""
         'Del llatí <i>verba</i>'
         >>> last_template_handler(["etim-lang", "en", "ca"], "ca")
         "De l'anglès"
+        >>> last_template_handler(["etim-lang", "grc", "ca", "φαιός", "trad=gris"], "ca")
+        'Del grec antic <i>φαιός</i> (<i>phaiós</i>, «gris»)'
 
         >>> last_template_handler(["del-lang", "la", "ca", "verba"], "ca")
         'del llatí <i>verba</i>'
         >>> last_template_handler(["Del-lang", "xib", "ca", "baitolo"], "ca")
         "De l'ibèric <i>baitolo</i>"
+        >>> last_template_handler(["Del-lang", "grc", "ca", "ῡ̔οειδής", "trad=en forma d’ípsilon"], "ca")
+        'Del grec antic <i>ῡ̔οειδής</i> (<i>hȳoeidḗs</i>, «en forma d’ípsilon»)'
+        >>> last_template_handler(["del-lang", "la", "ca"], "ca")
+        ''
 
         >>> last_template_handler(["Fals tall", "ca", "Far", "el Far"], "ca")
         'Fals tall sil·làbic de <i>el Far</i>'
@@ -199,6 +207,8 @@ def last_template_handler(template: Tuple[str, ...], locale: str, word: str = ""
 
         >>> last_template_handler(["m", "ca", "tardanies", "t=fruits tardans"], "ca")
         '<i>tardanies</i> («fruits tardans»)'
+        >>> last_template_handler(["m", "grc", "ὖ"], "ca")
+        '<i>ὖ</i> (<i>ŷ</i>)'
 
         >>> last_template_handler(["lleng", "la", "√ⵎⵣⵖ"], "ca")
         '√ⵎⵣⵖ'
@@ -245,16 +255,20 @@ def last_template_handler(template: Tuple[str, ...], locale: str, word: str = ""
     if lookup_template(template[0]):
         return render_template(template)
 
+    from .general import cal_apostrofar
+
     tpl, *parts = template
     data = extract_keywords_from(parts)
     phrase = ""
 
-    def parse_other_parameters() -> str:
+    def parse_other_parameters(lang: str = "", word: str = "") -> str:
         toadd = []
         if data["trans"]:
             toadd.append(italic(data["trans"]))
-        if data["tr"]:
+        elif data["tr"] and data["tr"] != "-":
             toadd.append(italic(data["tr"]))
+        elif lang == "grc" and word and data["tr"] != "-":
+            toadd.append(italic(transliterate(word)))
         if data["t"]:
             toadd.append(f"«{data['t']}»")
         if data["glossa"]:
@@ -270,7 +284,7 @@ def parse_other_parameters() -> str:
     if tpl == "calc semàntic":
         phrase = "calc semàntic "
         lang = langs[parts[0]]
-        phrase += "de l'" if lang.startswith(("a", "i", "o", "u", "h")) else "del "
+        phrase += "de l'" if cal_apostrofar(lang) else "del "
         phrase += f"{lang} "
         phrase += f"{italic(parts[-1])}{parse_other_parameters()}"
         return phrase
@@ -278,16 +292,26 @@ def parse_other_parameters() -> str:
     if tpl == "epònim":
         return parts[1] if len(parts) > 1 else (data["w"] if "w" in data else "")
 
+    if tpl == "e":
+        return f"{parts[-1]}{parse_other_parameters()}"
+
+    if tpl in ("del-lang", "Del-lang") and len(parts) == 2:
+        return ""
     if tpl in ("etim-lang", "del-lang", "Del-lang"):
         if parts[0] in langs:
             lang = langs[parts[0]]
-            phrase += "De l'" if lang.startswith(("a", "i", "o", "u", "h")) else "Del "
+            phrase += "De l'" if cal_apostrofar(lang) else "Del "
             if tpl == "del-lang" and phrase:
                 phrase = phrase.lower()
             phrase += f"{lang}"
+            word = ""
             if len(parts) > 2:
-                phrase += f" {italic(parts[2])}"
-        phrase += parse_other_parameters()
+                word = parts[2]
+                if len(parts) > 3:
+                    phrase += f" {italic(parts[3])}"
+                else:
+                    phrase += f" {italic(word)}"
+        phrase += parse_other_parameters(parts[0], word)
         return phrase
 
     if tpl in ("fals tall", "Fals tall"):
@@ -302,7 +326,7 @@ def parse_other_parameters() -> str:
         return phrase
 
     if tpl in ("m", "terme", "term", "calc"):
-        return f"{italic(parts[-1])}{parse_other_parameters()}"
+        return f"{italic(parts[-1])}{parse_other_parameters(parts[0], parts[-1])}"
 
     if tpl == "trad":
         src = data["sc"] or parts.pop(0)
diff --git a/wikidict/lang/ca/general.py b/wikidict/lang/ca/general.py
new file mode 100644
index 00000000..0312708f
--- /dev/null
+++ b/wikidict/lang/ca/general.py
@@ -0,0 +1,58 @@
+# From https://ca.wiktionary.org/w/index.php?title=M%C3%B2dul:ca-general&oldid=2255269 24/02/2024
+
+
+def cal_apostrofar(text: str) -> bool:
+    apostrophize = {
+        "hakk": False,
+        "haus": False,
+        "hawa": False,  # h consonant (hakka, haussa, hawaià)
+        "hia": False,
+        "hie": False,
+        "hio": False,
+        "hui": False,  # vocal consonant
+        "uig": True,
+        "uix": True,  # excepció per u vocal
+        "ha": True,
+        "he": True,
+        "hi": True,
+        "hí": True,
+        "ho": True,
+        "hu": True,
+        "hy": True,  # excepte anteriors
+        "ia": False,
+        "ià": False,
+        "ie": False,
+        "io": False,
+        "iu": False,  # i consonant
+        "ua": False,
+        "ue": False,
+        "ui": False,
+        "uí": False,
+        "uï": False,
+        "uo": False,  # u consonant
+        "ya": False,
+        "ye": False,
+        "yi": False,
+        "yo": False,
+        "yu": False,  # y consonant
+        "a": True,
+        "à": True,
+        "e": True,
+        "è": True,
+        "é": True,
+        "i": True,
+        "í": True,
+        "ï": True,
+        "y": True,
+        "o": True,
+        "ò": True,
+        "ó": True,
+        "u": True,
+        "ú": True,
+        "ü": True,  # excepte anteriors
+    }
+    for i in range(4, 0, -1):
+        apostrophized = apostrophize.get(text[:i])
+        if apostrophized is not None:
+            return True
+    return False
diff --git a/wikidict/lang/ca/grc_trans.py b/wikidict/lang/ca/grc_trans.py
new file mode 100644
index 00000000..359d1a44
--- /dev/null
+++ b/wikidict/lang/ca/grc_trans.py
@@ -0,0 +1,296 @@
+import re
+import unicodedata
+from typing import Dict, List, Union
+
+data = {}
+
+macron = chr(0x304)
+spacing_macron = chr(0xAF)
+modifier_macron = chr(0x2C9)
+breve = chr(0x306)
+spacing_breve = chr(0x2D8)
+rough = chr(0x314)
+smooth = chr(0x313)
+diaeresis = chr(0x308)
+acute = chr(0x301)
+grave = chr(0x300)
+circum = chr(0x342)
+Latin_circum = chr(0x302)
+coronis = chr(0x343)
+subscript = chr(0x345)
+undertie = chr(0x35C)
+
+data["consonants"] = "ΒβΓγΔδΖζΘθΚκΛλΜμΝνΞξΠπΡρΣσςΤτΦφΧχΨψ"
+data["consonant"] = "[" + data["consonants"] + "]"
+data["vowels"] = "ΑαΕεΗηΙιΟοΥυΩω"
+data["vowel"] = "[" + data["vowels"] + "]"
+data["combining_diacritics"] = "".join([macron, breve, rough, smooth, diaeresis, acute, grave, circum, subscript])
+data["combining_diacritic"] = "[" + data["combining_diacritics"] + "]"
+
+# Basic letters with and without diacritics
+letters_with_diacritics = "ΆΈ-ώϜϝἀ-ᾼῂ-ῌῐ-" + chr(0x1FDB) + "Ὶῠ-Ῥῲ-ῼ"
+data["word_characters"] = letters_with_diacritics + data["combining_diacritics"] + undertie
+data["word_character"] = "[" + data["word_characters"] + "]"
+
+UTF8_char = r"[\u0001-\u007F\u00C2-\u00F4][\u0080-\u00BF]*"
+basic_Greek = r"[\u0384-\u03FF]"  # excluding first line of Greek and Coptic block
+
+info = {}
+vowel_t = {"vowel": True}
+iota_t = {"vowel": True, "offglide": True}
+upsilon_t = {"vowel": True, "offglide": True}
+rho_t: Dict[str, bool] = {}
+diacritic_t = {"diacritic": True}
+breathing_t = {"diacritic": True}
+
+
+def add_info(characters: Union[str, List[str]], t: Dict[str, bool]) -> None:
+    if isinstance(characters, str):
+        for character in characters:  # TODO filter utf-8 chars ?
+            info[character] = t
+    else:
+        for character in characters:
+            info[character] = t
+
+
+add_info([macron, breve, diaeresis, acute, grave, circum, subscript], diacritic_t)
+add_info([rough, smooth], breathing_t)
+add_info("ΑΕΗΟΩαεηοω", vowel_t)
+add_info("Ιι", iota_t)
+add_info("Υυ", upsilon_t)
+add_info("Ρρ", rho_t)
+
+
+def decompose(text: str) -> str:
+    return unicodedata.normalize("NFD", text)
+
+
+def set_list(li: List[str], i: int, v: str) -> None:
+    try:
+        li[i] = v
+    except IndexError:
+        for _ in range(i - len(li) + 1):
+            li.append("")
+        li[i] = v
+
+
+def make_tokens(text: str) -> List[str]:
+    tokens: List[str] = []
+    prev_info: Dict[str, bool] = {}
+    token_i, vowel_count = 0, 0
+    prev = None
+    for character in decompose(text):  # TODO filter non UTF8 ?
+        curr_info = info.get(character, {})
+        # print(character)
+        # print(curr_info)
+        # print(prev_info)
+        if curr_info.get("vowel"):
+            vowel_count += 1
+            if prev and (
+                not (vowel_count == 2 and curr_info.get("offglide") and prev_info.get("vowel"))
+                or prev_info.get("offglide")
+                and curr_info == upsilon_t
+                or curr_info == prev_info
+            ):
+                token_i += 1
+                if prev_info.get("vowel"):
+                    vowel_count = 1
+            elif vowel_count == 2:
+                vowel_count = 0
+            set_list(tokens, token_i, (tokens[token_i] if token_i < len(tokens) else "") + character)
+        elif curr_info.get("diacritic"):
+            vowel_count = 0
+            set_list(tokens, token_i, (tokens[token_i] if token_i < len(tokens) else "") + character)
+            if prev_info.get("diacritic") or prev_info.get("vowel"):
+                if character == diaeresis:
+                    previous_vowel = ""
+                    vowel_with_diaeresis = ""
+                    # print("jere = " + tokens[token_i])
+                    if matches := re.match("^(" + basic_Greek + ")(" + basic_Greek + ".+)", tokens[token_i]):
+                        previous_vowel, vowel_with_diaeresis = matches.groups()
+                    if previous_vowel:
+                        set_list(tokens, token_i, previous_vowel)
+                        set_list(tokens, token_i + 1, vowel_with_diaeresis)
+                        token_i += 1
+            elif prev_info == rho_t:
+                if curr_info != breathing_t:
+                    print(f"The character {prev} in {text} should not have the accent {character} on it.")
+            else:
+                print(f"The character {prev} cannot have a diacritic on it.")
+        else:
+            vowel_count = 0
+            if prev:
+                token_i += 1
+            set_list(tokens, token_i, (tokens[token_i] if token_i < len(tokens) else "") + character)
+        prev = character
+        prev_info = curr_info
+    # print(tokens)
+    return tokens
+
+
+macron_diaeresis = macron + diaeresis + "?" + Latin_circum
+a_subscript = r"^[αΑ].*" + subscript + r"$"
+velar = "κγχξ"
+
+tt = {
+    "α": "a",
+    "ε": "e",
+    "η": "e" + macron,
+    "ι": "i",
+    "ο": "o",
+    "υ": "y",
+    "ω": "o" + macron,
+    "β": "b",
+    "γ": "g",
+    "δ": "d",
+    "ζ": "z",
+    "θ": "th",
+    "κ": "k",
+    "λ": "l",
+    "μ": "m",
+    "ν": "n",
+    "ξ": "x",
+    "π": "p",
+    "ρ": "r",
+    "σ": "s",
+    "ς": "s",
+    "τ": "t",
+    "φ": "ph",
+    "χ": "kh",
+    "ψ": "ps",
+    "ϝ": "w",
+    "ϻ": "ś",
+    "ϙ": "q",
+    "ϡ": "š",
+    "ͷ": "v",
+    "ϐ": "b",
+    "ϑ": "th",
+    "ϰ": "k",
+    "ϱ": "r",
+    "ϲ": "s",
+    "ϕ": "ph",
+    breve: "",
+    smooth: "",
+    rough: "",
+    circum: Latin_circum,
+    subscript: "i",
+}
+
+
+def gsub(pattern: str, replacements: Dict[str, str], string: str) -> str:
+    def replace(match: re.Match[str]) -> str:
+        return replacements.get(match.group(0), match.group(0))
+
+    return re.sub(pattern, replace, string)
+
+
+# see https://en.wiktionary.org/wiki/Module:grc-translit/testcases for test cases
+def transliterate(text: str) -> str:
+    """
+    >>> transliterate("λόγος")
+    'lógos'
+    >>> transliterate("σφίγξ")
+    'sphínx'
+    >>> transliterate("ϝάναξ")
+    'wánax'
+    >>> transliterate("οἷαι")
+    'hoîai'
+    >>> transliterate("ΙΧΘΥΣ")
+    'IKhThYS'
+    >>> transliterate("Υἱός")
+    'Yhiós'
+    >>> transliterate("ταῦρος")
+    'taûros'
+    >>> transliterate("νηῦς")
+    'nēŷs'
+    >>> transliterate("σῦς")
+    'sŷs'
+    >>> transliterate("ὗς")
+    'hŷs'
+    >>> transliterate("γυῖον")
+    'gyîon'
+    >>> transliterate("ἀναῡ̈τέω")
+    'anaȳ̈téō'
+    >>> transliterate("δαΐφρων")
+    'daḯphrōn'
+    >>> transliterate("τῶν")
+    'tôn'
+    >>> transliterate("τοὶ")
+    'toì'
+    >>> transliterate("τῷ")
+    'tôi'
+    >>> transliterate("τούτῳ")
+    'toútōi'
+    >>> transliterate("σοφίᾳ")
+    'sophíāi'
+    >>> transliterate("μᾱ̆νός")
+    'mānós'
+    >>> transliterate("ὁ")
+    'ho'
+    >>> transliterate("οἱ")
+    'hoi'
+    >>> transliterate("εὕρισκε")
+    'heúriske'
+    >>> transliterate("ὑϊκός")
+    'hyïkós'
+    >>> transliterate("πυρρός")
+    'pyrrhós'
+    >>> transliterate("ῥέω")
+    'rhéō'
+    >>> transliterate("σάἁμον")
+    'sáhamon'
+    >>> transliterate("Ὀδυσσεύς")
+    'Odysseús'
+    >>> transliterate("Εἵλως")
+    'Heílōs'
+    >>> transliterate("ᾍδης")
+    'Hā́idēs'
+    >>> transliterate("ἡ Ἑλήνη")
+    'hē Helḗnē'
+    >>> transliterate("ἔχεις μοι εἰπεῖν, ὦ Σώκρατες, ἆρα διδακτὸν ἡ ἀρετή?")
+    'ékheis moi eipeîn, ô Sṓkrates, âra didaktòn hē aretḗ?'
+    >>> transliterate("τί τηνικάδε ἀφῖξαι, ὦ Κρίτων? ἢ οὐ πρῲ ἔτι ἐστίν?")
+    'tí tēnikáde aphîxai, ô Krítōn? ḕ ou prṑi éti estín?'
+    >>> transliterate("τούτων φωνήεντα μέν ἐστιν ἑπτά· α ε η ι ο υ ω.")
+    'toútōn phōnḗenta mén estin heptá; a e ē i o y ō.'
+    >>> transliterate("πήγ(νῡμῐ)")
+    'pḗg(nȳmi)'
+    >>> transliterate("καλός&nbsp;καὶ&nbsp;ἀγαθός")
+    'kalós&nbsp;kaì&nbsp;agathós'
+    >>> transliterate("καλός&#32;καὶ&#32;ἀγαθός")
+    'kalós&#32;kaì&#32;agathós'
+    """
+    if text == "῾":
+        return "h"
+    text = re.sub(r"([^A-Za-z0-9])[;" + "\u037E" + r"]", r"\1?", text)
+    text = text.replace("·", ";")
+    tokens = make_tokens(text)
+    output = []
+    for i in range(len(tokens)):
+        token = tokens[i]
+        # print("token = " + token)
+        translit = gsub(UTF8_char, tt, token.lower())
+        for char, repl in tt.items():
+            translit = translit.replace(char, repl)
+        next_token = tokens[i + 1] if i + 1 < len(tokens) else None
+        if token == "γ" and next_token and next_token in velar:
+            translit = "n"
+        elif token == "ρ" and tokens[i - 1] == "ρ":
+            translit = "rh"
+        elif re.search(r"[αεο][υὐ]", token):
+            translit = translit.replace("y", "u")
+        elif re.match(a_subscript, token):
+            translit = re.sub(r"([aA])", r"\1" + macron, translit)
+        if rough in token:
+            if token.startswith("Ρ") or token.startswith("ρ"):
+                translit += "h"
+            else:
+                translit = "h" + translit
+
+        if re.search(macron_diaeresis, translit):
+            translit = translit.replace(macron, "")
+        if token != token.lower():
+            translit = translit[0].upper() + translit[1:]
+        output.append(translit)
+
+    return unicodedata.normalize("NFC", "".join(output))
diff --git a/wikidict/lang/ca/template_handlers.py b/wikidict/lang/ca/template_handlers.py
index c71f1b44..5a6c97ac 100644
--- a/wikidict/lang/ca/template_handlers.py
+++ b/wikidict/lang/ca/template_handlers.py
@@ -2,6 +2,7 @@
 from typing import DefaultDict, List, Tuple
 
 from ...user_functions import concat, extract_keywords_from, italic, strong, term
+from .general import cal_apostrofar
 from .labels import label_syntaxes, labels
 from .langs import langs
 
@@ -37,6 +38,8 @@ def render_comp(tpl: str, parts: List[str], data: DefaultDict[str, str]) -> str:
     'prefix <i>metro-</i> («mesura») i el sufix <i>-nom</i>'
     >>> render_comp("comp", ["ca", "mini-", "pequenas"], {"lang2": "es", "t2": "PIMER"})
     'prefix <i>mini-</i> i el castellà <i>pequenas</i> («PIMER»)'
+    >>> render_comp("comp", ["ca", "Birma", "-ia"], {"lang1": "en"})
+    'anglès <i>Birma</i> i el sufix <i>-ia</i>'
     """
 
     def value(word: str, standalone: bool = False) -> str:
@@ -66,12 +69,15 @@ def value(word: str, standalone: bool = False) -> str:
 
     word2 = parts.pop(0)
     if not parts:
-        phrase = value(word1)
+        phrase = ""
+        if "lang1" in data:
+            phrase = f"{langs[data['lang1']]} "
+        phrase += value(word1)
         if others := parse_index_parameters(data, 1):
             phrase += others
         if "lang2" in data:
             lang2 = langs[data["lang2"]]
-            phrase += " i l'" if lang2.startswith(("a", "i", "o", "u", "h")) else " i el "
+            phrase += " i l'" if cal_apostrofar(lang2) else " i el "
             phrase += f"{lang2} {value(word2)}"
         else:
             phrase += f" i {value(word2)}"
diff --git a/wikidict/lang/en/labels.py b/wikidict/lang/en/labels.py
index 4cbb075e..4632524b 100644
--- a/wikidict/lang/en/labels.py
+++ b/wikidict/lang/en/labels.py
@@ -3957,7 +3957,6 @@
     "Central Assam": "Central Assam",
     "Central Assamese": "Central Assam",
     "Central Calabria": "central Calabria",
-    "Central Dravidian": "Central Dravidian",
     "Central Finland": "Central Finland",
     "Central Finnish": "Central Finland",
     "Central German": "central Germany",
@@ -4383,7 +4382,8 @@
     "Gujarra": "Gujarra",
     "Gujwa": "Gujwa",
     "Gujwa-eup": "Gujwa",
-    "Gulf of Finland": "Gulf of Finland",
+    "Gulf of Finland": "Gulf of Finland islands",
+    "Gulf of Finland islands": "Gulf of Finland islands",
     "Gulpen": "Gulpen",
     "Gungjung": "speech of the royal court",
     "Gunma": "Gunma",
@@ -4446,10 +4446,10 @@
     "Hamshen": "Hemşin",
     "Hangyeong": "Hangyeong",
     "Hangyeong-myeon": "Hangyeong",
-    "Hangzhou Mandarin": "Hangzhounese",
     "Hangzhou Wu": "Hangzhounese",
     "Hangzhou dialect": "Hangzhounese",
     "Hangzhounese": "Hangzhounese",
+    "Hangzhounese Wu": "Hangzhounese",
     "Hankou": "Wuhan",
     "Hankow": "Wuhan",
     "Hanmuntu": "archaic Literary Chinese-style Korean",
@@ -5167,6 +5167,7 @@
     "Ningbo Wu": "Ningbonese",
     "Ningbo dialect": "Ningbonese",
     "Ningbonese": "Ningbonese",
+    "Ningbonese Wu": "Ningbonese",
     "Niotika": "Oinoe",
     "Nishnaabemwin": "Ottawa",
     "Nohyeong": "Nohyeong",
@@ -5185,7 +5186,6 @@
     "North Azerbaijani": "North Azerbaijani",
     "North Central Vietnam": "North Central Vietnam",
     "North Central Vietnamese": "North Central Vietnam",
-    "North Dravidian": "North Dravidian",
     "North German": "Northern Germany",
     "North Germanic": "North Germanic",
     "North Germany": "Northern Germany",
@@ -5354,7 +5354,6 @@
     "PIbR": "Proto-Ibero-Romance",
     "PItR": "Proto-Italo-Romance",
     "PN": "personal names",
-    "PNI": "Proto-Northern Iroquoian",
     "PR": "Proto-Romance",
     "PWR": "Proto-Western-Romance",
     "Pahang": "Pahang",
@@ -5781,7 +5780,6 @@
     "South Calabria": "southern Calabria",
     "South Coastal": "South Coastal Andhra",
     "South Coastal Andhra": "South Coastal Andhra",
-    "South Dravidian": "South Dravidian",
     "South German": "Southern German",
     "South Germany": "Southern German",
     "South Hesse": "South Hessian",
@@ -5805,7 +5803,6 @@
     "South Vietnamese": "Southern Vietnam",
     "South Western Vietnamese": "Mekong Delta",
     "South of Russia": "Southern Russia",
-    "South-Central Dravidian": "South-Central Dravidian",
     "SouthPS": "South Slavic",
     "Southeast Central Scots": "Southeast Central",
     "Southeast Limburgish": "Southeast Limburgish",
@@ -5890,7 +5887,7 @@
     "Sumatra": "Sumatra",
     "Sumut": "North Sumatra",
     "Sunnmøre": "Sunnmøre",
-    "Suomenlahti": "Gulf of Finland",
+    "Suomenlahti": "Gulf of Finland islands",
     "Surat": "Surati",
     "Surati": "Surati",
     "Surgut": "Surgut",
@@ -5904,8 +5901,9 @@
     "Sussex": "Sussex",
     "Sutsilvan": "Sutsilvan",
     "Suzhou Wu": "Suzhounese",
-    "Suzhou dialect": "Suzhounese",
     "Suzhounese": "Suzhounese",
+    "Suzhounese Wu": "Suzhounese",
+    "Suzhounese dialect": "Suzhounese",
     "Suçatı": "Apso",
     "Swiss German": "Switzerland",
     "Switzerland German": "Switzerland",
@@ -6971,5 +6969,5 @@
     "한림읍": "Hallim",
     "화순": "Hwasun",
     "화순리": "Hwasun",
-}  # 3,442
+}  # 3,440
 # END
diff --git a/wikidict/lang/en/langs.py b/wikidict/lang/en/langs.py
index 56e8b9e2..0c22d048 100644
--- a/wikidict/lang/en/langs.py
+++ b/wikidict/lang/en/langs.py
@@ -894,7 +894,7 @@
     "bez": "Kibena",
     "bfa": "Bari",
     "bfb": "Pauri Bareli",
-    "bfc": "Northern Bai",
+    "bfc": "Panyi Bai",
     "bfd": "Bafut",
     "bfe": "Betaf",
     "bff": "Bofi",
@@ -7416,6 +7416,7 @@
     "sit-aao": "Ao",
     "sit-alm": "Almora",
     "sit-bai": "Bai",
+    "sit-bai-pro": "Proto-Bai",
     "sit-bdi": "Bodish",
     "sit-bok": "Bokar",
     "sit-cai": "Caijia",
@@ -7455,12 +7456,14 @@
     "sit-mru": "Mruic",
     "sit-nas": "Naish",
     "sit-nax": "Naic",
+    "sit-nba": "Northern Bai",
     "sit-new": "Newaric",
     "sit-nng": "Nungish",
     "sit-prn": "Puiron",
     "sit-pro": "Proto-Sino-Tibetan",
     "sit-qia": "Qiangic",
     "sit-rgy": "Rgyalrongic",
+    "sit-sba": "Sino-Bai",
     "sit-sit": "Situ",
     "sit-tam": "Tamangic",
     "sit-tan": "Tani",
@@ -8894,6 +8897,8 @@
     "wur": "Wurrugu",
     "wut": "Wutung",
     "wuu": "Wu",
+    "wuu-hzh": "Hangzhounese",
+    "wuu-ngb": "Ningbonese",
     "wuu-sha": "Shanghainese",
     "wuu-szh": "Suzhounese",
     "wuv": "Wuvulu-Aua",
@@ -9713,5 +9718,5 @@
     "zyp": "Zyphe",
     "zza": "Zazaki",
     "zzj": "Zuojiang Zhuang",
-}  # 9,708
+}  # 9,713
 # END
diff --git a/wikidict/lang/fr/domain_templates.py b/wikidict/lang/fr/domain_templates.py
index e94f7b07..e06229f2 100644
--- a/wikidict/lang/fr/domain_templates.py
+++ b/wikidict/lang/fr/domain_templates.py
@@ -250,6 +250,7 @@
     "pâtes alimentaires": "Cuisine",
     "pâtisseries": "Pâtisserie",
     "pélicans": "Ornithologie",
+    "péninsules": "Géographie",
     "périodes": "Géologie",
     "pêches": "Botanique",
     "quartiers": "Toponyme",
@@ -327,5 +328,5 @@
     "états": "État",
     "étoiles": "Astronomie",
     "îles": "Géographie",
-}  # 322
+}  # 323
 # END


Test patch:
diff --git a/tests/test_ca.py b/tests/test_ca.py
index acd2e55b..95fa2ebd 100644
--- a/tests/test_ca.py
+++ b/tests/test_ca.py
@@ -22,7 +22,7 @@
             "-itzar",
             [],
             [],
-            ["Del llatí <i>-izare</i>, del grec antic <i>-ίζειν</i>."],
+            ["Del llatí <i>-izare</i>, del grec antic <i>-ίζειν</i> (<i>-ízein</i>)."],
             [
                 "<i>Aplicat a un substantiu o adjectiu forma un verb que expressa la seva realització o convertir-se'n.</i>",  # noqa
             ],