diff --git a/src/metaglot/langid.py b/src/metaglot/langid.py new file mode 100644 index 0000000..f9c1057 --- /dev/null +++ b/src/metaglot/langid.py @@ -0,0 +1,219 @@ +import re +from typing import ClassVar, Optional +import pycountry + + +class PoLang: + """ + Represents a valid Gettext PO file Language field value. + + Supported formats: + ll - ISO 639 language code (2 or 3 letters, lowercase) + ll_CC - language code + ISO 3166 country code (uppercase) + ll_CC@variant - language code + country code + variant (lowercase) + + Examples: + PoLang("en") # valid + PoLang("zh_CN") # valid + PoLang("sr_Cyrl") # valid (3-letter language code) + PoLang("de_DE@euro") # invalid (@euro is not a valid variant) + PoLang("EN") # invalid (language code must be lowercase) + """ + + __PATTERN: ClassVar[re.Pattern] = re.compile( + r"^(?P[a-z]{2,3})" + r"(?:_(?P[A-Z]{2})" + r"(?:@(?P[a-z][a-z0-9-]*))?" + r")?$" + ) + + __language_code: str + """ISO 639 two-letter or three-letter language code (lowercase).""" + __country_code: Optional[str] + """ISO 3166 two-letter country code (uppercase) or no presented.""" + __variant: Optional[str] + """The variant designator. + The variant designator (lowercase) can be a script designator, + such as ‘latin’ or ‘cyrillic’. + """ + + def __init__(self, value: str): + match = self.__PATTERN.match(value) + if not match: + raise ValueError( + f"Invalid Language field format: {value!r}. " + f"Expected 'll', 'll_CC' or 'll_CC@variant'" + ) + + self.__language_code = match.group("language") + self.__country_code = match.group("country") # may be None + self.__variant = match.group("variant") # may be None + + self.__validate_language(self.__language_code) + if self.__country_code: + self.__validate_country(self.__country_code) + + # region: Internal validation helpers + + @staticmethod + def __validate_language(language_code: str) -> None: + """Validate the language code as an ISO 639 code using pycountry.""" + lang = pycountry.languages.get(alpha_2=language_code) + if lang is None: + lang = pycountry.languages.get(alpha_3=language_code) + if lang is None: + raise ValueError(f"Invalid language code: {language_code!r}") + + @staticmethod + def __validate_country(country_code: str) -> None: + """Validate the country code as an ISO 3166 alpha-2 code using pycountry.""" + country = pycountry.countries.get(alpha_2=country_code) + if country is None: + raise ValueError(f"Invalid country code: {country_code!r}") + + # endregion + + # region: Properties + + @property + def language(self) -> str: + """The lowercase language code.""" + return self.__language_code + + @property + def country(self) -> str | None: + """The uppercase country code, or None if not specified.""" + return self.__country_code + + @property + def variant(self) -> str | None: + """The lowercase variant identifier, or None if not specified.""" + return self.__variant + + @property + def value(self) -> str: + """The canonical string representation, rebuilt from the components.""" + result = self.__language_code + if self.__country_code is not None: + result += f"_{self.__country_code}" + if self.__variant is not None: + result += f"@{self.__variant}" + return result + + # endregion + + # region: Object methods + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"PoLang({self.value!r})" + + def __eq__(self, other) -> bool: + if self is other: + return True + if not isinstance(other, PoLang): + return NotImplemented + return ( + self.__language_code == other.__language_code + and self.__country_code == other.__country_code + and self.__variant == other.__variant + ) + + def __hash__(self) -> int: + return hash((self.__language_code, self.__country_code, self.__variant)) + + # endregion + + +class WinLcid: + """ + Represents a Windows language identifier (LANGID), the 2-byte language + component of a Windows LCID (see [MS-LCID] section 2.2). + + A LANGID combines a 10-bit primary language identifier and a 6-bit + sublanguage identifier. This is the value used by the LANGUAGE statement of + RC STRINGTABLE resources and the "Translation" value of VERSIONINFO + resources, so unlike a full 4-byte LCID it carries no sort identifier. + + The value is only loosely validated: it must fit into 2 bytes, but it is + not checked against the identifiers actually assigned by Microsoft. + + Examples: + WinLcid(0x0409) # valid + WinLcid(0x7C14) # valid + WinLcid(0x10000) # invalid (exceeds the 2-byte range) + """ + + __value: int + """The LANGID value, from 0x0000 to 0xFFFF.""" + + def __init__(self, value: int): + if not 0 <= value <= 0xFFFF: + raise ValueError( + f"Invalid language identifier value: {value:#x}. " + f"Expected a 2-byte integer between 0x0000 and 0xFFFF" + ) + self.__value = value + + @classmethod + def from_parts(cls, primary: int, sublanguage: int) -> "WinLcid": + """ + Create a language identifier from a primary/sublanguage pair, as used + by the two-number form of the RC LANGUAGE statement. + + :param primary: The primary language identifier (10 bits). + :param sublanguage: The sublanguage identifier (6 bits). + :return: The combined language identifier. + """ + if not 0 <= primary <= 0x3FF: + raise ValueError( + f"Invalid primary language identifier: {primary:#x}. " + f"Expected a 10-bit integer between 0x000 and 0x3FF" + ) + if not 0 <= sublanguage <= 0x3F: + raise ValueError( + f"Invalid sublanguage identifier: {sublanguage:#x}. " + f"Expected a 6-bit integer between 0x00 and 0x3F" + ) + return cls((sublanguage << 10) | primary) + + # region: Properties + + @property + def value(self) -> int: + """The LANGID value as an integer.""" + return self.__value + + @property + def primary(self) -> int: + """The primary language identifier (the low-order 10 bits).""" + return self.__value & 0x3FF + + @property + def sublanguage(self) -> int: + """The sublanguage identifier (the high-order 6 bits).""" + return self.__value >> 10 + + # endregion + + # region: Object methods + + def __str__(self) -> str: + return f"0x{self.__value:04X}" + + def __repr__(self) -> str: + return f"WinLcid(0x{self.__value:04X})" + + def __eq__(self, other) -> bool: + if self is other: + return True + if not isinstance(other, WinLcid): + return NotImplemented + return self.__value == other.__value + + def __hash__(self) -> int: + return hash(self.__value) + + # endregion diff --git a/src/metaglot/langmap/__init__.py b/src/metaglot/langmap/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/metaglot/langmap/win_lcid_map.py b/src/metaglot/langmap/win_lcid_map.py new file mode 100644 index 0000000..fb24304 --- /dev/null +++ b/src/metaglot/langmap/win_lcid_map.py @@ -0,0 +1,524 @@ +from ..langid import PoLang, WinLcid + +# YYC MARK: The table below is transcribed from the 'Language ID' table in +# [MS-LCID] section 2.2 (revision 2024-04-23). Underscore separators in the +# original language tags have been normalized to hyphens +# (e.g. 'es-ES_tradnl' -> 'es-ES-tradnl'). +# YYC MARK: Rows whose tags are marked 'reserved' in the document are kept +# for transcription fidelity but commented out with a leading '#', so they +# carry no runtime effect and lookups on them fail. +# YYC MARK: Rows without a usable language tag are omitted entirely: values +# that are neither defined nor reserved, the invariant-locale value 0x007F, +# the user-default and unspecified placeholders 0x0C00/0x1000 (harmful +# process-local values that must never be persisted into RC resources), and +# the transient LCIDs 0x2000-0x4C00. +# YYC MARK: Rows carrying multiple tags for one value (e.g. +# 'ff-NG, ff-Latn-NG') are expanded into consecutive entries sharing the +# same WinLcid. +# YYC MARK: The alternate-sort table of section 2.2 (full 4-byte LCIDs with +# sort identifiers, e.g. 'de-DE_phoneb' 0x00010407) is not transcribed: +# WinLcid only models the 2-byte language identifier used by RC resources. +# YYC MARK: Known contradiction inside the document: the appendix assigns +# 'quc' -> 0x0086 and 'quc-Latn-GT' -> 0x0486, while the section 2.2 registry +# has 'qut' -> 0x0086 and marks 'quc' (0x0093), 'qut-GT' (0x0486) and +# 'quc-CO' (0x0493) as reserved. The section 2.2 registry wins here. + +_LCID_TAGS: dict[str, WinLcid] = { + "ar": WinLcid(0x0001), + "bg": WinLcid(0x0002), + "ca": WinLcid(0x0003), + "zh-Hans": WinLcid(0x0004), + "cs": WinLcid(0x0005), + "da": WinLcid(0x0006), + "de": WinLcid(0x0007), + "el": WinLcid(0x0008), + "en": WinLcid(0x0009), + "es": WinLcid(0x000A), + "fi": WinLcid(0x000B), + "fr": WinLcid(0x000C), + "he": WinLcid(0x000D), + "hu": WinLcid(0x000E), + "is": WinLcid(0x000F), + "it": WinLcid(0x0010), + "ja": WinLcid(0x0011), + "ko": WinLcid(0x0012), + "nl": WinLcid(0x0013), + "no": WinLcid(0x0014), + "pl": WinLcid(0x0015), + "pt": WinLcid(0x0016), + "rm": WinLcid(0x0017), + "ro": WinLcid(0x0018), + "ru": WinLcid(0x0019), + "hr": WinLcid(0x001A), + "sk": WinLcid(0x001B), + "sq": WinLcid(0x001C), + "sv": WinLcid(0x001D), + "th": WinLcid(0x001E), + "tr": WinLcid(0x001F), + "ur": WinLcid(0x0020), + "id": WinLcid(0x0021), + "uk": WinLcid(0x0022), + "be": WinLcid(0x0023), + "sl": WinLcid(0x0024), + "et": WinLcid(0x0025), + "lv": WinLcid(0x0026), + "lt": WinLcid(0x0027), + "tg": WinLcid(0x0028), + "fa": WinLcid(0x0029), + "vi": WinLcid(0x002A), + "hy": WinLcid(0x002B), + "az": WinLcid(0x002C), + "eu": WinLcid(0x002D), + "hsb": WinLcid(0x002E), + "mk": WinLcid(0x002F), + "st": WinLcid(0x0030), + "ts": WinLcid(0x0031), + "tn": WinLcid(0x0032), + "ve": WinLcid(0x0033), + "xh": WinLcid(0x0034), + "zu": WinLcid(0x0035), + "af": WinLcid(0x0036), + "ka": WinLcid(0x0037), + "fo": WinLcid(0x0038), + "hi": WinLcid(0x0039), + "mt": WinLcid(0x003A), + "se": WinLcid(0x003B), + "ga": WinLcid(0x003C), + # "yi": WinLcid(0x003D), # reserved + "ms": WinLcid(0x003E), + "kk": WinLcid(0x003F), + "ky": WinLcid(0x0040), + "sw": WinLcid(0x0041), + "tk": WinLcid(0x0042), + "uz": WinLcid(0x0043), + "tt": WinLcid(0x0044), + "bn": WinLcid(0x0045), + "pa": WinLcid(0x0046), + "gu": WinLcid(0x0047), + "or": WinLcid(0x0048), + "ta": WinLcid(0x0049), + "te": WinLcid(0x004A), + "kn": WinLcid(0x004B), + "ml": WinLcid(0x004C), + "as": WinLcid(0x004D), + "mr": WinLcid(0x004E), + "sa": WinLcid(0x004F), + "mn": WinLcid(0x0050), + "bo": WinLcid(0x0051), + "cy": WinLcid(0x0052), + "km": WinLcid(0x0053), + "lo": WinLcid(0x0054), + "my": WinLcid(0x0055), + "gl": WinLcid(0x0056), + "kok": WinLcid(0x0057), + # "mni": WinLcid(0x0058), # reserved + "sd": WinLcid(0x0059), + "syr": WinLcid(0x005A), + "si": WinLcid(0x005B), + "chr": WinLcid(0x005C), + "iu": WinLcid(0x005D), + "am": WinLcid(0x005E), + "tzm": WinLcid(0x005F), + "ks": WinLcid(0x0060), + "ne": WinLcid(0x0061), + "fy": WinLcid(0x0062), + "ps": WinLcid(0x0063), + "fil": WinLcid(0x0064), + "dv": WinLcid(0x0065), + # "bin": WinLcid(0x0066), # reserved + "ff": WinLcid(0x0067), + "ha": WinLcid(0x0068), + # "ibb": WinLcid(0x0069), # reserved + "yo": WinLcid(0x006A), + "quz": WinLcid(0x006B), + "nso": WinLcid(0x006C), + "ba": WinLcid(0x006D), + "lb": WinLcid(0x006E), + "kl": WinLcid(0x006F), + "ig": WinLcid(0x0070), + # "kr": WinLcid(0x0071), # reserved + "om": WinLcid(0x0072), + "ti": WinLcid(0x0073), + "gn": WinLcid(0x0074), + "haw": WinLcid(0x0075), + # "la": WinLcid(0x0076), # reserved + # "so": WinLcid(0x0077), # reserved + "ii": WinLcid(0x0078), + # "pap": WinLcid(0x0079), # reserved + "arn": WinLcid(0x007A), + "moh": WinLcid(0x007C), + "br": WinLcid(0x007E), + "ug": WinLcid(0x0080), + "mi": WinLcid(0x0081), + "oc": WinLcid(0x0082), + "co": WinLcid(0x0083), + "gsw": WinLcid(0x0084), + "sah": WinLcid(0x0085), + "qut": WinLcid(0x0086), + "rw": WinLcid(0x0087), + "wo": WinLcid(0x0088), + "prs": WinLcid(0x008C), + "gd": WinLcid(0x0091), + "ku": WinLcid(0x0092), + # "quc": WinLcid(0x0093), # reserved + "ar-SA": WinLcid(0x0401), + "bg-BG": WinLcid(0x0402), + "ca-ES": WinLcid(0x0403), + "zh-TW": WinLcid(0x0404), + "cs-CZ": WinLcid(0x0405), + "da-DK": WinLcid(0x0406), + "de-DE": WinLcid(0x0407), + "el-GR": WinLcid(0x0408), + "en-US": WinLcid(0x0409), + "es-ES-tradnl": WinLcid(0x040A), + "fi-FI": WinLcid(0x040B), + "fr-FR": WinLcid(0x040C), + "he-IL": WinLcid(0x040D), + "hu-HU": WinLcid(0x040E), + "is-IS": WinLcid(0x040F), + "it-IT": WinLcid(0x0410), + "ja-JP": WinLcid(0x0411), + "ko-KR": WinLcid(0x0412), + "nl-NL": WinLcid(0x0413), + "nb-NO": WinLcid(0x0414), + "pl-PL": WinLcid(0x0415), + "pt-BR": WinLcid(0x0416), + "rm-CH": WinLcid(0x0417), + "ro-RO": WinLcid(0x0418), + "ru-RU": WinLcid(0x0419), + "hr-HR": WinLcid(0x041A), + "sk-SK": WinLcid(0x041B), + "sq-AL": WinLcid(0x041C), + "sv-SE": WinLcid(0x041D), + "th-TH": WinLcid(0x041E), + "tr-TR": WinLcid(0x041F), + "ur-PK": WinLcid(0x0420), + "id-ID": WinLcid(0x0421), + "uk-UA": WinLcid(0x0422), + "be-BY": WinLcid(0x0423), + "sl-SI": WinLcid(0x0424), + "et-EE": WinLcid(0x0425), + "lv-LV": WinLcid(0x0426), + "lt-LT": WinLcid(0x0427), + "tg-Cyrl-TJ": WinLcid(0x0428), + "fa-IR": WinLcid(0x0429), + "vi-VN": WinLcid(0x042A), + "hy-AM": WinLcid(0x042B), + "az-Latn-AZ": WinLcid(0x042C), + "eu-ES": WinLcid(0x042D), + "hsb-DE": WinLcid(0x042E), + "mk-MK": WinLcid(0x042F), + "st-ZA": WinLcid(0x0430), + "ts-ZA": WinLcid(0x0431), + "tn-ZA": WinLcid(0x0432), + "ve-ZA": WinLcid(0x0433), + "xh-ZA": WinLcid(0x0434), + "zu-ZA": WinLcid(0x0435), + "af-ZA": WinLcid(0x0436), + "ka-GE": WinLcid(0x0437), + "fo-FO": WinLcid(0x0438), + "hi-IN": WinLcid(0x0439), + "mt-MT": WinLcid(0x043A), + "se-NO": WinLcid(0x043B), + "yi-001": WinLcid(0x043D), + "ms-MY": WinLcid(0x043E), + "kk-KZ": WinLcid(0x043F), + "ky-KG": WinLcid(0x0440), + "sw-KE": WinLcid(0x0441), + "tk-TM": WinLcid(0x0442), + "uz-Latn-UZ": WinLcid(0x0443), + "tt-RU": WinLcid(0x0444), + "bn-IN": WinLcid(0x0445), + "pa-IN": WinLcid(0x0446), + "gu-IN": WinLcid(0x0447), + "or-IN": WinLcid(0x0448), + "ta-IN": WinLcid(0x0449), + "te-IN": WinLcid(0x044A), + "kn-IN": WinLcid(0x044B), + "ml-IN": WinLcid(0x044C), + "as-IN": WinLcid(0x044D), + "mr-IN": WinLcid(0x044E), + "sa-IN": WinLcid(0x044F), + "mn-MN": WinLcid(0x0450), + "bo-CN": WinLcid(0x0451), + "cy-GB": WinLcid(0x0452), + "km-KH": WinLcid(0x0453), + "lo-LA": WinLcid(0x0454), + "my-MM": WinLcid(0x0455), + "gl-ES": WinLcid(0x0456), + "kok-IN": WinLcid(0x0457), + # "mni-IN": WinLcid(0x0458), # reserved + # "sd-Deva-IN": WinLcid(0x0459), # reserved + "syr-SY": WinLcid(0x045A), + "si-LK": WinLcid(0x045B), + "chr-Cher-US": WinLcid(0x045C), + "iu-Cans-CA": WinLcid(0x045D), + "am-ET": WinLcid(0x045E), + "tzm-Arab-MA": WinLcid(0x045F), + "ks-Arab": WinLcid(0x0460), + "ne-NP": WinLcid(0x0461), + "fy-NL": WinLcid(0x0462), + "ps-AF": WinLcid(0x0463), + "fil-PH": WinLcid(0x0464), + "dv-MV": WinLcid(0x0465), + # "bin-NG": WinLcid(0x0466), # reserved + "ff-NG": WinLcid(0x0467), + "ff-Latn-NG": WinLcid(0x0467), + "ha-Latn-NG": WinLcid(0x0468), + # "ibb-NG": WinLcid(0x0469), # reserved + "yo-NG": WinLcid(0x046A), + "quz-BO": WinLcid(0x046B), + "nso-ZA": WinLcid(0x046C), + "ba-RU": WinLcid(0x046D), + "lb-LU": WinLcid(0x046E), + "kl-GL": WinLcid(0x046F), + "ig-NG": WinLcid(0x0470), + "kr-Latn-NG": WinLcid(0x0471), + "om-ET": WinLcid(0x0472), + "ti-ET": WinLcid(0x0473), + "gn-PY": WinLcid(0x0474), + "haw-US": WinLcid(0x0475), + "la-VA": WinLcid(0x0476), + "so-SO": WinLcid(0x0477), + "ii-CN": WinLcid(0x0478), + # "pap-029": WinLcid(0x0479), # reserved + "arn-CL": WinLcid(0x047A), + "moh-CA": WinLcid(0x047C), + "br-FR": WinLcid(0x047E), + "ug-CN": WinLcid(0x0480), + "mi-NZ": WinLcid(0x0481), + "oc-FR": WinLcid(0x0482), + "co-FR": WinLcid(0x0483), + "gsw-FR": WinLcid(0x0484), + "sah-RU": WinLcid(0x0485), + # "qut-GT": WinLcid(0x0486), # reserved + "rw-RW": WinLcid(0x0487), + "wo-SN": WinLcid(0x0488), + "prs-AF": WinLcid(0x048C), + # "plt-MG": WinLcid(0x048D), # reserved + # "zh-yue-HK": WinLcid(0x048E), # reserved + # "tdd-Tale-CN": WinLcid(0x048F), # reserved + # "khb-Talu-CN": WinLcid(0x0490), # reserved + "gd-GB": WinLcid(0x0491), + "ku-Arab-IQ": WinLcid(0x0492), + # "quc-CO": WinLcid(0x0493), # reserved + "qps-ploc": WinLcid(0x0501), + "qps-ploca": WinLcid(0x05FE), + "ar-IQ": WinLcid(0x0801), + "ca-ES-valencia": WinLcid(0x0803), + "zh-CN": WinLcid(0x0804), + "de-CH": WinLcid(0x0807), + "en-GB": WinLcid(0x0809), + "es-MX": WinLcid(0x080A), + "fr-BE": WinLcid(0x080C), + "it-CH": WinLcid(0x0810), + # "ja-Ploc-JP": WinLcid(0x0811), # reserved + "nl-BE": WinLcid(0x0813), + "nn-NO": WinLcid(0x0814), + "pt-PT": WinLcid(0x0816), + "ro-MD": WinLcid(0x0818), + "ru-MD": WinLcid(0x0819), + "sr-Latn-CS": WinLcid(0x081A), + "sv-FI": WinLcid(0x081D), + "ur-IN": WinLcid(0x0820), + # "az-Cyrl-AZ": WinLcid(0x082C), # reserved + "dsb-DE": WinLcid(0x082E), + "tn-BW": WinLcid(0x0832), + "se-SE": WinLcid(0x083B), + "ga-IE": WinLcid(0x083C), + "ms-BN": WinLcid(0x083E), + # "kk-Latn-KZ": WinLcid(0x083F), # reserved + # "uz-Cyrl-UZ": WinLcid(0x0843), # reserved + "bn-BD": WinLcid(0x0845), + "pa-Arab-PK": WinLcid(0x0846), + "ta-LK": WinLcid(0x0849), + # "mn-Mong-CN": WinLcid(0x0850), # reserved + # "bo-BT": WinLcid(0x0851), # reserved + "sd-Arab-PK": WinLcid(0x0859), + "iu-Latn-CA": WinLcid(0x085D), + "tzm-Latn-DZ": WinLcid(0x085F), + "ks-Deva-IN": WinLcid(0x0860), + "ne-IN": WinLcid(0x0861), + "ff-Latn-SN": WinLcid(0x0867), + "quz-EC": WinLcid(0x086B), + "ti-ER": WinLcid(0x0873), + "qps-plocm": WinLcid(0x09FF), + "ar-EG": WinLcid(0x0C01), + "zh-HK": WinLcid(0x0C04), + "de-AT": WinLcid(0x0C07), + "en-AU": WinLcid(0x0C09), + "es-ES": WinLcid(0x0C0A), + "fr-CA": WinLcid(0x0C0C), + "sr-Cyrl-CS": WinLcid(0x0C1A), + "se-FI": WinLcid(0x0C3B), + "mn-Mong-MN": WinLcid(0x0C50), + "dz-BT": WinLcid(0x0C51), + # "tmz-MA": WinLcid(0x0C5F), # reserved + "quz-PE": WinLcid(0x0C6B), + "ar-LY": WinLcid(0x1001), + "zh-SG": WinLcid(0x1004), + "de-LU": WinLcid(0x1007), + "en-CA": WinLcid(0x1009), + "es-GT": WinLcid(0x100A), + "fr-CH": WinLcid(0x100C), + "hr-BA": WinLcid(0x101A), + "smj-NO": WinLcid(0x103B), + "tzm-Tfng-MA": WinLcid(0x105F), + "ar-DZ": WinLcid(0x1401), + "zh-MO": WinLcid(0x1404), + "de-LI": WinLcid(0x1407), + "en-NZ": WinLcid(0x1409), + "es-CR": WinLcid(0x140A), + "fr-LU": WinLcid(0x140C), + "bs-Latn-BA": WinLcid(0x141A), + "smj-SE": WinLcid(0x143B), + "ar-MA": WinLcid(0x1801), + "en-IE": WinLcid(0x1809), + "es-PA": WinLcid(0x180A), + "fr-MC": WinLcid(0x180C), + "sr-Latn-BA": WinLcid(0x181A), + "sma-NO": WinLcid(0x183B), + "ar-TN": WinLcid(0x1C01), + "en-ZA": WinLcid(0x1C09), + "es-DO": WinLcid(0x1C0A), + "fr-029": WinLcid(0x1C0C), + "sr-Cyrl-BA": WinLcid(0x1C1A), + "sma-SE": WinLcid(0x1C3B), + "ar-OM": WinLcid(0x2001), + "en-JM": WinLcid(0x2009), + "es-VE": WinLcid(0x200A), + "fr-RE": WinLcid(0x200C), + "bs-Cyrl-BA": WinLcid(0x201A), + "sms-FI": WinLcid(0x203B), + "ar-YE": WinLcid(0x2401), + # "en-029": WinLcid(0x2409), # reserved + "es-CO": WinLcid(0x240A), + "fr-CD": WinLcid(0x240C), + "sr-Latn-RS": WinLcid(0x241A), + "smn-FI": WinLcid(0x243B), + "ar-SY": WinLcid(0x2801), + "en-BZ": WinLcid(0x2809), + "es-PE": WinLcid(0x280A), + "fr-SN": WinLcid(0x280C), + "sr-Cyrl-RS": WinLcid(0x281A), + "ar-JO": WinLcid(0x2C01), + "en-TT": WinLcid(0x2C09), + "es-AR": WinLcid(0x2C0A), + "fr-CM": WinLcid(0x2C0C), + "sr-Latn-ME": WinLcid(0x2C1A), + "ar-LB": WinLcid(0x3001), + "en-ZW": WinLcid(0x3009), + "es-EC": WinLcid(0x300A), + "fr-CI": WinLcid(0x300C), + "sr-Cyrl-ME": WinLcid(0x301A), + "ar-KW": WinLcid(0x3401), + "en-PH": WinLcid(0x3409), + "es-CL": WinLcid(0x340A), + "fr-ML": WinLcid(0x340C), + "ar-AE": WinLcid(0x3801), + # "en-ID": WinLcid(0x3809), # reserved + "es-UY": WinLcid(0x380A), + "fr-MA": WinLcid(0x380C), + "ar-BH": WinLcid(0x3C01), + "en-HK": WinLcid(0x3C09), + "es-PY": WinLcid(0x3C0A), + "fr-HT": WinLcid(0x3C0C), + "ar-QA": WinLcid(0x4001), + "en-IN": WinLcid(0x4009), + "es-BO": WinLcid(0x400A), + # "ar-Ploc-SA": WinLcid(0x4401), # reserved + "en-MY": WinLcid(0x4409), + "es-SV": WinLcid(0x440A), + # "ar-145": WinLcid(0x4801), # reserved + "en-SG": WinLcid(0x4809), + "es-HN": WinLcid(0x480A), + "en-AE": WinLcid(0x4C09), + "es-NI": WinLcid(0x4C0A), + # "en-BH": WinLcid(0x5009), # reserved + "es-PR": WinLcid(0x500A), + # "en-EG": WinLcid(0x5409), # reserved + "es-US": WinLcid(0x540A), + # "en-JO": WinLcid(0x5809), # reserved + # "es-419": WinLcid(0x580A), # reserved + # "en-KW": WinLcid(0x5C09), # reserved + "es-CU": WinLcid(0x5C0A), + # "en-TR": WinLcid(0x6009), # reserved + # "en-YE": WinLcid(0x6409), # reserved + "bs-Cyrl": WinLcid(0x641A), + "bs-Latn": WinLcid(0x681A), + "sr-Cyrl": WinLcid(0x6C1A), + "sr-Latn": WinLcid(0x701A), + "smn": WinLcid(0x703B), + "az-Cyrl": WinLcid(0x742C), + "sms": WinLcid(0x743B), + "zh": WinLcid(0x7804), + "nn": WinLcid(0x7814), + "bs": WinLcid(0x781A), + "az-Latn": WinLcid(0x782C), + "sma": WinLcid(0x783B), + # "kk-Cyrl": WinLcid(0x783F), # reserved + "uz-Cyrl": WinLcid(0x7843), + "mn-Cyrl": WinLcid(0x7850), + "iu-Cans": WinLcid(0x785D), + "tzm-Tfng": WinLcid(0x785F), + "zh-Hant": WinLcid(0x7C04), + "nb": WinLcid(0x7C14), + "sr": WinLcid(0x7C1A), + "tg-Cyrl": WinLcid(0x7C28), + "dsb": WinLcid(0x7C2E), + "smj": WinLcid(0x7C3B), + # "kk-Latn": WinLcid(0x7C3F), # reserved + "uz-Latn": WinLcid(0x7C43), + "pa-Arab": WinLcid(0x7C46), + "mn-Mong": WinLcid(0x7C50), + "sd-Arab": WinLcid(0x7C59), + "chr-Cher": WinLcid(0x7C5C), + "iu-Latn": WinLcid(0x7C5D), + "tzm-Latn": WinLcid(0x7C5F), + "ff-Latn": WinLcid(0x7C67), + "ha-Latn": WinLcid(0x7C68), + "ku-Arab": WinLcid(0x7C92), + # "fr-015": WinLcid(0xE40C), # reserved +} + + +# Gettext-style script designator words mapped to ISO 15924 script codes. +# This is not the full ISO 15924 table - it only contains the words that can +# hit entries in _LCID_TAGS above. Extend it as needed. +_SCRIPT_WORDS: dict[str, str] = { + "latin": "Latn", + "cyrillic": "Cyrl", + "arabic": "Arab", +} + + +def lookup(lang: PoLang) -> WinLcid: + """ + Resolve a PO file language to its Windows language identifier. + + :param lang: The PO file language. + :return: The Windows language identifier for the language. + :raises KeyError: If the language has no transcribed mapping. + """ + if lang.variant is None: + segments = [lang.language] + if lang.country is not None: + segments.append(lang.country) + return _LCID_TAGS["-".join(segments)] + + # YYC MARK: PoLang's variant slot may hold either a script designator or a + # genuine variant (Gettext semantics), while MS-LCID language tags place + # scripts and variants at different positions with different vocabularies + # ('cyrillic' != 'Cyrl'), so the conversion cannot be done mechanically. + # Candidate matching is done here as a workaround: the variant position is + # tried first, then the script position via _SCRIPT_WORDS (which is not + # the full ISO 15924 table, only the words that hit entries above). + key = f"{lang.language}-{lang.country}-{lang.variant}" + if key in _LCID_TAGS: + return _LCID_TAGS[key] + script = _SCRIPT_WORDS.get(lang.variant) + if script is not None: + key = f"{lang.language}-{script}-{lang.country}" + if key in _LCID_TAGS: + return _LCID_TAGS[key] + raise KeyError(str(lang))