Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions courlan/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ipaddress import ip_address
from urllib.parse import SplitResult, urlsplit

from babel import Locale, UnknownLocaleError
from .langcodes import ISO_LANGS, ISO_TERRS

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -79,7 +79,7 @@

# language filter
PATH_LANG_FILTER = re.compile(
r"(?:https?://[^/]+/)([a-z]{2})([_-][a-z]{2,3})?(?:/|$)", re.IGNORECASE
r"(?:https?://[^/]+/)([a-z]{2})([_-][a-z]{2})?(?:/|$)", re.IGNORECASE
)
ALL_PATH_LANGS = re.compile(r"(?:/)([a-z]{2})([_-][a-z]{2})?(?:/)", re.IGNORECASE)
ALL_PATH_LANGS_NO_TRAILING = re.compile(
Expand Down Expand Up @@ -178,17 +178,18 @@ def extension_filter(urlpath: str) -> bool:


@lru_cache(maxsize=1024)
def langcodes_score(language: str, segment: str, score: int) -> int:
"Use locale parser to assess the plausibility of the chosen URL segment."
def langcodes_score(language: str, segment: str) -> int:
"Score a URL segment as a language indicator: +1 if it is a plausible matching locale, -1 if a mismatching one, 0 if not a recognizable locale."
if not isinstance(segment, str):
return 0
delimiter = "_" if "_" in segment else "-"
try:
if Locale.parse(segment, sep=delimiter).language == language:
score += 1
else:
score -= 1
except (TypeError, UnknownLocaleError):
pass
return score
lang, _, territory = segment.partition(delimiter)
lang = lang.lower()
if lang not in ISO_LANGS:
return 0
if territory and territory.upper() not in ISO_TERRS:
return 0
return 1 if lang == language else -1


def lang_filter(
Expand All @@ -212,10 +213,10 @@ def lang_filter(
else:
occurrences = ALL_PATH_LANGS_NO_TRAILING.findall(url)
if len(occurrences) == 1:
score = langcodes_score(language, match[1], score)
score += langcodes_score(language, match[1] + (match[2] or ""))
elif len(occurrences) == 2:
for occurrence in occurrences:
score = langcodes_score(language, occurrence, score)
score += langcodes_score(language, occurrence[0] + occurrence[1])
# don't perform the test if there are too many candidates: > 2
# second test: prepended language cues
if strict:
Expand Down
313 changes: 313 additions & 0 deletions courlan/langcodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
"""
Language and territory code sets derived from CLDR via babel 2.18, used for
URL language detection. Replaces the babel runtime dependency.
"""

# ISO 639-1 language codes accepted by CLDR (167 codes).
# Generated by: all ab where Locale.parse(ab) succeeds in babel 2.18.
ISO_LANGS = frozenset(
(
"aa",
"ab",
"af",
"ak",
"am",
"an",
"ar",
"as",
"az",
"ba",
"be",
"bg",
"bh",
"bm",
"bn",
"bo",
"br",
"bs",
"ca",
"ce",
"co",
"cs",
"cu",
"cv",
"cy",
"da",
"de",
"dv",
"dz",
"ee",
"el",
"en",
"eo",
"es",
"et",
"eu",
"fa",
"ff",
"fi",
"fo",
"fr",
"fy",
"ga",
"gd",
"gl",
"gn",
"gu",
"gv",
"ha",
"he",
"hi",
"hr",
"ht",
"hu",
"hy",
"ia",
"id",
"ie",
"ig",
"ii",
"in",
"io",
"is",
"it",
"iu",
"iw",
"ja",
"ji",
"jv",
"jw",
"ka",
"ki",
"kk",
"kl",
"km",
"kn",
"ko",
"ks",
"ku",
"kw",
"ky",
"la",
"lb",
"lg",
"ln",
"lo",
"lt",
"lu",
"lv",
"mg",
"mi",
"mk",
"ml",
"mn",
"mo",
"mr",
"ms",
"mt",
"my",
"nb",
"nd",
"ne",
"nl",
"nn",
"no",
"nr",
"nv",
"ny",
"oc",
"om",
"or",
"os",
"pa",
"pl",
"ps",
"pt",
"qu",
"rm",
"rn",
"ro",
"ru",
"rw",
"sa",
"sc",
"sd",
"se",
"sg",
"si",
"sk",
"sl",
"sn",
"so",
"sq",
"sr",
"ss",
"st",
"su",
"sv",
"sw",
"ta",
"te",
"tg",
"th",
"ti",
"tk",
"tl",
"tn",
"to",
"tr",
"ts",
"tt",
"tw",
"ug",
"uk",
"ur",
"uz",
"ve",
"vi",
"vo",
"wa",
"wo",
"xh",
"yi",
"yo",
"za",
"zh",
"zu",
)
)

# ISO 3166-1 alpha-2 territory codes accepted by CLDR (128 codes, incl. historical).
# Generated by: all XX where Locale.parse("en_XX") succeeds in babel 2.18.
ISO_TERRS = frozenset(
(
"AE",
"AG",
"AI",
"AS",
"AT",
"AU",
"BB",
"BE",
"BI",
"BM",
"BS",
"BW",
"BZ",
"CA",
"CC",
"CH",
"CK",
"CM",
"CT",
"CX",
"CY",
"CZ",
"DD",
"DE",
"DG",
"DK",
"DM",
"ER",
"ES",
"FI",
"FJ",
"FK",
"FM",
"FR",
"FX",
"GB",
"GD",
"GG",
"GH",
"GI",
"GM",
"GS",
"GU",
"GY",
"HK",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IT",
"JE",
"JM",
"JT",
"KE",
"KI",
"KN",
"KY",
"LC",
"LR",
"LS",
"MG",
"MH",
"MI",
"MO",
"MP",
"MS",
"MT",
"MU",
"MV",
"MW",
"MY",
"NA",
"NF",
"NG",
"NH",
"NL",
"NO",
"NR",
"NU",
"NZ",
"PC",
"PG",
"PH",
"PK",
"PL",
"PN",
"PR",
"PT",
"PU",
"PW",
"RH",
"RO",
"RW",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SK",
"SL",
"SS",
"SX",
"SZ",
"TC",
"TK",
"TO",
"TT",
"TV",
"TZ",
"UG",
"UK",
"UM",
"US",
"VC",
"VG",
"VI",
"VU",
"WK",
"WS",
"ZA",
"ZM",
"ZW",
"ZZ",
)
)
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ license = "Apache-2.0"
dynamic = ["version"]
requires-python = ">=3.10"
authors = [
{name = "Adrien Barbaresi", email = "barbaresi@bbaw.de"}
{name = "Adrien Barbaresi"}
]
keywords=[
"cleaner",
Expand Down Expand Up @@ -49,7 +49,6 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"babel >= 2.16.0",
"tld >= 0.13",
"urllib3 >= 1.26, < 3",
]
Expand Down
Loading
Loading