Skip to content
Open
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
27 changes: 27 additions & 0 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'get_cldr_version',
'get_global',
'get_locale_identifier',
'get_official_territories',
'negotiate_locale',
'parse_locale',
]
Expand All @@ -42,6 +43,7 @@
"parent_exceptions",
"script_aliases",
"territory_aliases",
"territory_codes",
"territory_currencies",
"territory_languages",
"territory_zones",
Expand Down Expand Up @@ -89,6 +91,7 @@ def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]:
- ``parent_exceptions``
- ``script_aliases``
- ``territory_aliases``
- ``territory_codes``
- ``territory_currencies``
- ``territory_languages``
- ``territory_zones``
Expand Down Expand Up @@ -1379,3 +1382,27 @@ def get_cldr_version() -> str:
:rtype: str
"""
return str(get_global("cldr")["version"])


def get_official_territories() -> frozenset[str]:
"""Return the ISO 3166-1 alpha-2 territory codes known to Babel.

This is the set of two-letter territory (country) codes that the CLDR
provides, with deprecated codes and the ISO user-assigned ranges removed.

>>> codes = get_official_territories()
>>> 'US' in codes
True
>>> 'ZZ' in codes # reserved for private use
False

Since the data comes from the CLDR, the returned set does not match the
official ISO 3166-1 list exactly: a few territories that the CLDR omits
are missing, and a handful of exceptionally reserved codes (such as
``EU``) are included.

.. versionadded:: 2.19

:rtype: frozenset[str]
"""
return frozenset(get_global('territory_codes'))
2 changes: 2 additions & 0 deletions docs/api/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ Utility Functions
.. autofunction:: get_locale_identifier

.. autofunction:: get_cldr_version

.. autofunction:: get_official_territories
25 changes: 25 additions & 0 deletions scripts/import_cldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ def _parse_currency_date(s):
return tuple(map(int, parts + [1] * (3 - len(parts))))


def _is_user_assigned_territory(code):
# ISO 3166-1 reserves these code ranges for private use, so they never
# denote an actual territory.
return code in ('AA', 'ZZ') or 'QM' <= code <= 'QZ' or 'XA' <= code <= 'XZ'


def _currency_sort_key(tup):
code, start, end, tender = tup
return int(not tender), start or (1, 1, 1)
Expand Down Expand Up @@ -359,6 +365,25 @@ def parse_global(srcdir, sup):
'official_status': language.attrib.get('officialStatus'),
}
territory_languages[territory.attrib['type']] = languages

# ISO 3166-1 alpha-2 territory codes (see GH #904).
# CLDR's supplemental code mappings list every territory code alongside its
# ISO numeric and alpha-3 equivalents. We keep the two-letter codes that
# have a numeric ISO code assigned (this drops exceptionally reserved codes
# such as EA or IC), and skip the ISO user-assigned ranges as well as any
# code CLDR records as a deprecated alias.
territory_codes = set()
for mapping in sup.findall('.//codeMappings/territoryCodes'):
code = mapping.attrib['type']
if len(code) != 2 or not code.isalpha():
continue
if 'numeric' not in mapping.attrib:
continue
if code in territory_aliases or _is_user_assigned_territory(code):
continue
territory_codes.add(code)
global_data['territory_codes'] = tuple(sorted(territory_codes))

return global_data


Expand Down
15 changes: 15 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,18 @@ def test_locale_parse_empty():

def test_get_cldr_version():
assert core.get_cldr_version() == "48"


def test_get_official_territories():
codes = core.get_official_territories()
assert isinstance(codes, frozenset)
# A good spread of everyday country codes should be present.
assert {'US', 'DE', 'FR', 'JP', 'ZA', 'BR'} <= codes
# All codes are two uppercase letters.
assert all(len(code) == 2 and code.isalpha() and code.isupper() for code in codes)
# The ISO user-assigned ranges are excluded.
assert 'ZZ' not in codes
assert not any('XA' <= code <= 'XZ' for code in codes)
# Deprecated codes such as Yugoslavia or the USSR are excluded.
assert 'YU' not in codes
assert 'SU' not in codes