diff --git a/homeassistant/util/language.py b/homeassistant/util/language.py index 9fa46b6a5d6..8324293e136 100644 --- a/homeassistant/util/language.py +++ b/homeassistant/util/language.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass +import math import operator import re @@ -15,7 +16,7 @@ def preferred_regions( language: str, country: str | None = None, code: str | None = None, -) -> Iterable[str | None]: +) -> Iterable[str]: """Yield an ordered list of regions for a language based on country/code hints. Regions should be checked for support in the returned order if no other @@ -70,7 +71,7 @@ class Dialect: # Regions are upper-cased self.region = self.region.upper() - def score(self, dialect: Dialect, country: str | None = None) -> int: + def score(self, dialect: Dialect, country: str | None = None) -> float: """Return score for match with another dialect where higher is better. Score < 0 indicates a failure to match. @@ -79,27 +80,46 @@ class Dialect: # Not a match return -1 - if self.region == dialect.region: - # Language + region match + if (self.region is None) and (dialect.region is None): + # Weak match with no region constraint return 1 - pref_regions: set[str | None] = set() - if (self.region is None) or (dialect.region is None): - # Generate a set of preferred regions - pref_regions = set( - preferred_regions( - self.language, - country=country, - code=self.code, - ) + if (self.region is not None) and (dialect.region is not None): + if self.region == dialect.region: + # Exact language + region match + return math.inf + + # Regions are both set, but don't match + return 0 + + # Generate ordered list of preferred regions + pref_regions = list( + preferred_regions( + self.language, + country=country, + code=self.code, ) + ) - # Replace missing regions with preferred - regions = pref_regions if self.region is None else {self.region} - other_regions = pref_regions if dialect.region is None else {dialect.region} + try: + # Determine score based on position in the preferred regions list. + if self.region is not None: + region_idx = pref_regions.index(self.region) + elif dialect.region is not None: + region_idx = pref_regions.index(dialect.region) + else: + # Can't happen, but mypy is not smart enough + raise ValueError() - # Better match if there is overlap in regions - return 2 if regions.intersection(other_regions) else 0 + # More preferred regions are at the front. + # Add 1 to boost above a weak match where no regions are set. + return 1 + (len(pref_regions) - region_idx) + except ValueError: + # Region was not in preferred list + pass + + # Not a preferred region + return 0 @staticmethod def parse(tag: str) -> Dialect: diff --git a/tests/util/test_language.py b/tests/util/test_language.py index 87f54ee882e..70c38a38f00 100644 --- a/tests/util/test_language.py +++ b/tests/util/test_language.py @@ -1,6 +1,8 @@ """Test Home Assistant language util methods.""" from __future__ import annotations +import pytest + from homeassistant.const import MATCH_ALL from homeassistant.util import language @@ -95,26 +97,54 @@ def test_language_as_region() -> None: def test_zh_hant() -> None: - """Test that the zh-Hant matches HK or TW first.""" + """Test that the zh-Hant matches HK or TW.""" assert language.matches( "zh-Hant", - ["en-US", "en-GB", "zh-CN", "zh-HK", "zh-TW"], + ["en-US", "en-GB", "zh-CN", "zh-HK"], ) == [ "zh-HK", - "zh-TW", "zh-CN", ] assert language.matches( "zh-Hant", - ["en-US", "en-GB", "zh-CN", "zh-TW", "zh-HK"], + ["en-US", "en-GB", "zh-CN", "zh-TW"], ) == [ "zh-TW", - "zh-HK", "zh-CN", ] +@pytest.mark.parametrize("target", ["zh-Hant", "zh-Hans"]) +def test_zh_with_country(target: str) -> None: + """Test that the zh-Hant/zh-Hans still matches country when provided.""" + supported = ["en-US", "en-GB", "zh-CN", "zh-HK", "zh-TW"] + assert ( + language.matches( + target, + supported, + country="TW", + )[0] + == "zh-TW" + ) + assert ( + language.matches( + target, + supported, + country="HK", + )[0] + == "zh-HK" + ) + assert ( + language.matches( + target, + supported, + country="CN", + )[0] + == "zh-CN" + ) + + def test_zh_hans() -> None: """Test that the zh-Hans matches CN first.""" assert language.matches(