diff --git a/custom_components/cap_alerts/providers/eccc.py b/custom_components/cap_alerts/providers/eccc.py index 1b4d218..f139be8 100644 --- a/custom_components/cap_alerts/providers/eccc.py +++ b/custom_components/cap_alerts/providers/eccc.py @@ -357,6 +357,24 @@ def _pick_cap_link(entry: Element) -> tuple[str, str]: # --------------------------------------------------------------------------- +def _language_matches(info_lang: str, preferred: str) -> bool: + """Check language match with BCP 47 primary-subtag fallback. + + Exact match wins (``en-CA`` == ``en-CA``). When that fails, the primary + subtag (the part before the first ``-``) is compared so that a bare + ``en`` info block matches a preferred ``en-CA`` and vice-versa. This + hardens against CAP documents that declare a bare primary subtag + instead of the full region-tagged form the ECCC feed normally carries. + """ + if not info_lang or not preferred: + return False + if info_lang == preferred: + return True + return ( + preferred.split("-", 1)[0].casefold() == info_lang.split("-", 1)[0].casefold() + ) + + def _select_info(doc: CAPDoc, language: str) -> CAPInfoDoc: """Pick the block matching language; fall back to first. @@ -368,7 +386,7 @@ def _select_info(doc: CAPDoc, language: str) -> CAPInfoDoc: if not doc.infos: return CAPInfoDoc() for info in doc.infos: - if info.language == language: + if _language_matches(info.language, language): return info return doc.infos[0] @@ -423,7 +441,9 @@ def _select_region_info( province where the alert is still live would be the worse failure. """ candidates = ( - [info for info in doc.infos if info.language == language] if language else [] + [info for info in doc.infos if _language_matches(info.language, language)] + if language + else [] ) if not candidates: # No block declares this language (single-language document, or a @@ -731,7 +751,7 @@ def _merge_languages(variants: list[CAPAlert], preferred_lang: str) -> CAPAlert: primary = None alt = None for v in variants: - if v.language == preferred_lang: + if _language_matches(v.language, preferred_lang): primary = v else: alt = v diff --git a/tests/test_eccc_provider.py b/tests/test_eccc_provider.py index 599cd6f..0cb9a40 100644 --- a/tests/test_eccc_provider.py +++ b/tests/test_eccc_provider.py @@ -67,6 +67,7 @@ def _load_provider(name: str) -> types.ModuleType: _parse_cap_alert = _cap_mod.parse_cap_alert _select_info = _eccc_mod._select_info _select_region_info = _eccc_mod._select_region_info +_language_matches = _eccc_mod._language_matches _location_status = _eccc_mod._location_status _is_terminal_info = _eccc_mod._is_terminal_info _resolve_chain_leaves = _cap_mod.resolve_chain_leaves @@ -302,6 +303,39 @@ def test_parse_cap_alert_tolerates_newline_separated_references(): assert doc.references[1][1] == "urn:oid:2.49.0.1.124.test.2026.OLD.EN" +# --------------------------------------------------------------------------- +# _language_matches tests +# --------------------------------------------------------------------------- + + +def test_language_matches_exact_match(): + assert _language_matches("en-CA", "en-CA") is True + assert _language_matches("fr-CA", "fr-CA") is True + + +def test_language_matches_bare_primary_against_region(): + """Bare 'en' info block should match preferred 'en-CA'.""" + assert _language_matches("en", "en-CA") is True + assert _language_matches("en-CA", "en") is True + + +def test_language_matches_different_primary_subtags(): + """en-CA and fr-CA must not match each other.""" + assert _language_matches("en-CA", "fr-CA") is False + assert _language_matches("fr-CA", "en-CA") is False + + +def test_language_matches_empty_strings(): + assert _language_matches("", "en-CA") is False + assert _language_matches("en-CA", "") is False + assert _language_matches("", "") is False + + +def test_language_matches_case_insensitive(): + assert _language_matches("EN-CA", "en-ca") is True + assert _language_matches("en-CA", "EN-ca") is True + + # --------------------------------------------------------------------------- # _select_info tests # ---------------------------------------------------------------------------