#!/usr/bin/env python # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/.
Target "langtags":
This script extracts information about 1) mappings between deprecated and
current Unicode BCP 47 locale identifiers, and2) deprecated and current
BCP 47 Unicode extension value from CLDR, and converts it to C++ mapping
code in intl/components/LocaleGenerated.cpp. The code is used in
intl/components/Locale.cpp.
Target "tzdata":
This script computes which time zone informations are not up-to-date in ICU and provides the necessary mappings to workaround this problem. https://ssl.icu-project.org/trac/ticket/12044
Target "currency":
Generates the mapping from currency codes to decimal digits used for them.
Target "units":
Generate source and test files using the list of so-called "sanctioned unit
identifiers" and verifies that the ICU data filter includes these units.
Target "numbering":
Generate source and test files using the list of numbering systems with
simple digit mappings and verifies that it's in sync with ICU/CLDR. """
import io import json import os import re import tarfile import tempfile from contextlib import closing from functools import partial, total_ordering from itertools import chain, filterfalse, groupby, tee, zip_longest from operator import attrgetter, itemgetter from urllib.parse import urlsplit from urllib.request import Request as UrlRequest from urllib.request import urlopen from zipfile import ZipFile
import yaml
# From https://docs.python.org/3/library/itertools.html def grouper(iterable, n, fillvalue=None): "Collect data into fixed-length chunks or blocks" # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue)
def writeMappingHeader(println, description, source, url): if type(description) isnot list:
description = [description] for desc in description:
println(f"// {desc}")
println(f"// Derived from {source}.")
println(f"// {url}")
def writeMappingsVar(println, mapping, name, description, source, url): """Writes a variable definition with a mapping table.
Writes the contents of dictionary |mapping| through the |println|
function with the given variable name and a comment with description,
fileDate, and URL. """
println("")
writeMappingHeader(println, description, source, url)
println(f"var {name} = {{") for key, value in sorted(mapping.items(), key=itemgetter(0)):
println(f' "{key}": "{value}",')
println("};")
def writeMappingsBinarySearch(
println,
fn_name,
type_name,
name,
validate_fn,
validate_case_fn,
mappings,
tag_maxlength,
description,
source,
url,
): """Emit code to perform a binary search on language tag subtags.
Uses the contents of |mapping|, which can either be a dictionary or set,
to emit a mapping function to find subtag replacements. """
println("")
writeMappingHeader(println, description, source, url)
println(
f"""
bool mozilla::intl::Locale::{fn_name}({type_name} {name}) {{
MOZ_ASSERT({validate_fn}({name}.Span()));
MOZ_ASSERT({validate_case_fn}({name}.Span())); """.strip()
)
writeMappingsBinarySearchBody(println, name, name, mappings, tag_maxlength)
# Group in pairs of ten to not exceed the 80 line column limit. for entries in grouper(subtags, 10):
entries = (
f'"{tag}"'.rjust(length + 2) for tag in entries if tag isnotNone
)
println(" {},".format(", ".join(entries)))
println(" };")
trailing_return = True
# Sort the subtags by length. That enables using an optimized comparator # for the binary search, which only performs a single |memcmp| for multiple # of two subtag lengths.
mappings_keys = mappings.keys() if type(mappings) is dict else mappings for length, subtags in groupby(sorted(mappings_keys, key=len), len): # Omit the length check if the current length is the maximum length. if length != tag_maxlength:
println(
f""" if ({source_name}.Length() == {length}) {{ """.rstrip("\n")
) else:
trailing_return = False
println( """
{ """.rstrip("\n")
)
# The subtags need to be sorted for binary search to work.
subtags = sorted(subtags)
# Don't emit a binary search for short lists. if len(subtags) == 1: if type(mappings) is dict:
println(
f""" if ({equals(subtags[0])}) {{
{target_name}.Set(mozilla::MakeStringSpan("{mappings[subtags[0]]}")); returntrue;
}} returnfalse; """.strip("\n")
) else:
println(
f""" return {equals(subtags[0])}; """.strip("\n")
) elif len(subtags) <= 4: if type(mappings) is dict: for subtag in subtags:
println(
f""" if ({equals(subtag)}) {{
{target_name}.Set("{mappings[subtag]}"); returntrue;
}} """.strip("\n")
)
# |non_default_replacements| is a list and hence not hashable. Convert it # to a string to get a proper hashable value. def hash_key(default, non_default_replacements): return (default, str(sorted(str(v) for v in non_default_replacements)))
# Merge duplicate region entries.
region_aliases = {} for deprecated_region, (default, non_default_replacements) in sorted(
complex_region_mappings.items(), key=itemgetter(0)
):
key = hash_key(default, non_default_replacements) if key notin region_aliases:
region_aliases[key] = [] else:
region_aliases[key].append(deprecated_region)
first_region = True for deprecated_region, (default, non_default_replacements) in sorted(
complex_region_mappings.items(), key=itemgetter(0)
):
key = hash_key(default, non_default_replacements) if deprecated_region in region_aliases[key]: continue
replacement_regions = sorted({
region for (_, _, region) in non_default_replacements
})
first_case = True for replacement_region in replacement_regions:
replacement_language_script = sorted(
(language, script) for (language, script, region) in (non_default_replacements) if region == replacement_region
)
def writeLegacyMappingsFunction(println, legacy_mappings, description, source, url): """Writes a function definition that maps legacy language tags."""
println("")
writeMappingHeader(println, description, source, url)
println( """\
bool mozilla::intl::Locale::UpdateLegacyMappings() {
// We're mapping legacy tags to non-legacy form here.
// Other tags remain unchanged.
//
// Legacy tags are either sign language tags ("sgn") or have one or multiple
// variant subtags. Therefore we can quickly exclude most tags by checking
// these two subtags.
// The variant subtags need to be sorted for binary search.
MOZ_ASSERT(std::is_sorted(mVariants.begin(), mVariants.end(),
IsLessThan<decltype(mVariants)::ElementType>));
auto findVariant = [this](mozilla::Span<const char> variant) {
auto* p = std::lower_bound(mVariants.begin(), mVariants.end(), variant,
IsLessThan<decltype(mVariants)::ElementType,
decltype(variant)>);
# Helper class for pattern matching. class AnyClass: def __eq__(self, obj): return obj isnotNone
Any = AnyClass()
# Group the mappings by language.
legacy_mappings_by_language = {} for type, replacement in legacy_mappings.items():
(language, _, _, _) = type
legacy_mappings_by_language.setdefault(language, {})[type] = replacement
# Handle the empty language case first. ifNonein legacy_mappings_by_language: # Get the mappings and remove them from the dict.
mappings = legacy_mappings_by_language.pop(None)
# This case only applies for the "hepburn-heploc" -> "alalc97" # mapping, so just inline it here.
from_tag = (None, None, None, "hepburn-heploc")
to_tag = (None, None, None, "alalc97")
println( """ if (mVariants.length() >= 2) { if (auto* hepburn = findVariant(mozilla::MakeStringSpan("hepburn"))) { if (auto* heploc = findVariant(mozilla::MakeStringSpan("heploc"))) {
removeVariants(hepburn, heploc);
if (!insertVariantSortedIfNotPresent(mozilla::MakeStringSpan("alalc97"))) { returnfalse;
}
}
}
} """
)
# Handle sign languages next. if"sgn"in legacy_mappings_by_language:
mappings = legacy_mappings_by_language.pop("sgn")
# Legacy sign language mappings have the form "sgn-XX" where "XX" is # some region code. assert all(type == ("sgn", None, Any, None) for type in mappings.keys())
# Legacy sign languages are mapped to a single language subtag. assert all(
replacement == (Any, None, None, None) for replacement in mappings.values()
)
println( """ if (Language().EqualTo("sgn")) { if (Region().Present() && SignLanguageMapping(mLanguage, Region())) {
mRegion.Set(mozilla::MakeStringSpan(""));
}
} """.rstrip().lstrip("\n")
)
# Finally handle all remaining cases.
# The remaining mappings have neither script nor region subtags in the source locale. assert all(
type == (Any, None, None, Any) for mappings in legacy_mappings_by_language.values() for type in mappings.keys()
)
# And they have neither script nor region nor variant subtags in the target locale. assert all(
replacement == (Any, None, None, None) for mappings in legacy_mappings_by_language.values() for replacement in mappings.values()
)
# Compact the mappings table by removing empty fields.
legacy_mappings_by_language = {
lang: {
variants: r_language for ((_, _, _, variants), (r_language, _, _, _)) in mappings.items()
} for (lang, mappings) in legacy_mappings_by_language.items()
}
# Try to combine the remaining cases.
legacy_mappings_compact = {}
# Python can't hash dicts or lists, so use the string representation as the hash key. def hash_key(mappings): return str(sorted(mappings.items(), key=itemgetter(0)))
for lang, mappings in sorted(
legacy_mappings_by_language.items(), key=itemgetter(0)
):
key = hash_key(mappings)
legacy_mappings_compact.setdefault(key, []).append(lang)
for langs in legacy_mappings_compact.values():
language_equal_to = (
f"""Language().EqualTo("{lang}")"""for lang in sorted(langs)
)
cond = f""" ||\n{"" * len("elseif (")}""".join(language_equal_to)
# Count the variant subtags to determine the sort order. def variant_size(m):
(k, _) = m return len(k.split("-"))
# Alias rules are applied by largest union size first. for size, mappings_by_size in groupby(
sorted(mappings.items(), key=variant_size, reverse=True), key=variant_size
): # Convert grouper object to dict.
mappings_by_size = dict(mappings_by_size)
is_first = True
chain_if = size == 1
# Alias rules are applied in alphabetical order for variants, r_language in sorted(
mappings_by_size.items(), key=itemgetter(0)
):
sorted_variants = sorted(variants.split("-"))
len_variants = len(sorted_variants)
def readSupplementalData(core_file): """Reads CLDR Supplemental Data and extracts information for Intl.js.
Information extracted:
- legacyMappings: mappings from legacy tags to preferred complete language tags
- languageMappings: mappings from language subtags to preferred subtags
- complexLanguageMappings: mappings from language subtags with complex rules
- regionMappings: mappings from region subtags to preferred subtags
- complexRegionMappings: mappings from region subtags with complex rules
- variantMappings: mappings from variant subtags to preferred subtags
- likelySubtags: likely subtags used for generating test data only
Returns these mappings as dictionaries. """ import xml.etree.ElementTree as ET
# CLDR uses "_" as the separator for some elements. Replace it with "-". def bcp47_id(cldr_id): return cldr_id.replace("_", "-")
# Return the tuple (language, script, region, variants) and assert all # subtags are in canonical case. def bcp47_canonical(language, script, region, variants): # Canonical case for language subtags is lower case. assert language isNoneor language.lower() == language
# Canonical case for script subtags is title case. assert script isNoneor script.title() == script
# Canonical case for region subtags is upper case. assert region isNoneor region.upper() == region
# Canonical case for variant subtags is lower case. assert variants isNoneor variants.lower() == variants
return (language, script, region, variants[1:] if variants elseNone)
# Language ids are interpreted as multi-maps in # <https://www.unicode.org/reports/tr35/#LocaleId_Canonicalization>. # # See UTS35, §Annex C, Definitions - 1. Multimap interpretation. def language_id_to_multimap(language_id):
match = re_unicode_language_id.match(language_id) assert match isnotNone, (
f"{language_id} invalid Unicode BCP 47 locale identifier"
)
# Normalize "und" language to None, but keep the rest as is. return (language if language != "und"elseNone,) + canonical_language_id[1:]
rules = {}
territory_exception_rules = {}
tree = ET.parse(core_file.open("common/supplemental/supplementalMetadata.xml"))
# Load the rules from supplementalMetadata.xml. # # See UTS35, §Annex C, Definitions - 2. Alias elements. # See UTS35, §Annex C, Preprocessing. for alias_name in [ "languageAlias", "scriptAlias", "territoryAlias", "variantAlias",
]: for alias in tree.iterfind(".//" + alias_name): # Replace '_' by '-'.
type = bcp47_id(alias.get("type"))
replacement = bcp47_id(alias.get("replacement"))
# Prefix with "und-". if alias_name != "languageAlias":
type = "und-" + type
# Discard all rules where the type is an invalid languageId. if re_unicode_language_id.match(type) isNone: continue
type = language_id_to_multimap(type)
# Multiple, whitespace-separated territory replacements may be present. if alias_name == "territoryAlias"and" "in replacement:
replacements = replacement.split(" ")
replacement_list = [
language_id_to_multimap("und-" + r) for r in replacements
]
assert type notin territory_exception_rules, (
f"Duplicate alias rule: {type}"
)
assert type notin rules, f"Duplicate alias rule: {type}"
rules[type] = replacement
# Helper class for pattern matching. class AnyClass: def __eq__(self, obj): return obj isnotNone
Any = AnyClass()
modified_rules = True
loop_count = 0
while modified_rules:
modified_rules = False
loop_count += 1
# UTS 35 defines that canonicalization is applied until a fixed point has # been reached. This iterative application of the canonicalization algorithm # is only needed for a relatively small set of rules, so we can precompute # the transitive closure of all rules here and then perform a single pass # when canonicalizing language tags at runtime.
transitive_rules = {}
# Compute the transitive closure. # Any case which currently doesn't occur in the CLDR sources isn't supported # and will lead to throwing an error. for type, replacement in rules.items():
(language, script, region, variants) = type
(r_language, r_script, r_region, r_variants) = replacement
for i_type, i_replacement in rules.items():
(i_language, i_script, i_region, i_variants) = i_type
(i_r_language, i_r_script, i_r_region, i_r_variants) = i_replacement
if i_language isnotNoneand i_language == r_language: # This case currently only occurs when neither script nor region # subtags are present. A single variant subtags may be present # in |type|. And |i_type| definitely has a single variant subtag. # Should this ever change, update this code accordingly. assert type in (
(Any, None, None, None),
(Any, None, None, Any),
) assert replacement == (Any, None, None, None) assert i_type == (Any, None, None, Any) assert i_replacement == (Any, None, None, None)
# This case happens for the rules # "zh-guoyu -> zh", # "zh-hakka -> hak", and # "und-hakka -> und". # Given the possible input "zh-guoyu-hakka", the first rule will # change it to "zh-hakka", and then the second rule can be # applied. (The third rule isn't applied ever.) # # Let's assume there's a hypothetical rule # "zh-aaaaa" -> "en" # And we have the input "zh-aaaaa-hakka", then "zh-aaaaa -> en" # is applied before "zh-hakka -> hak", because rules are sorted # alphabetically. That means the overall result is "en": # "zh-aaaaa-hakka" is first canonicalized to "en-hakka" and then # "hakka" is removed through the third rule. # # No current rule requires to handle this special case, so we # don't yet support it. assert variants isNoneor variants <= i_variants
# Combine all variants and remove duplicates.
vars = set(
i_variants.split("-")
+ (variants.split("-") if variants else [])
)
if i_script isnotNoneand i_script == r_script: # This case currently doesn't occur, so we don't yet support it. raise ValueError(
f"{type} -> {replacement} :: {i_type} -> {i_replacement}"
) if i_region isnotNoneand i_region == r_region: # This case currently only applies for sign language # replacements. Similar to the language subtag case any other # combination isn't currently supported. assert type == (None, None, Any, None) assert replacement == (None, None, Any, None) assert i_type == ("sgn", None, Any, None) assert i_replacement == (Any, None, None, None)
if i_variants isnotNoneand i_variants == r_variants: # This case currently doesn't occur, so we don't yet support it. raise ValueError(
f"{type} -> {replacement} :: {i_type} -> {i_replacement}"
)
# Ensure there are no contradicting rules. assert all(
rules[type] == replacement for (type, replacement) in transitive_rules.items() if type in rules
)
# If |transitive_rules| is not a subset of |rules|, new rules will be added.
modified_rules = not (transitive_rules.keys() <= rules.keys())
# Ensure we only have to iterate more than once for the "guoyo-{hakka,xiang}" # case. Failing this assertion means either there's a bug when computing the # stop condition of this loop or a new kind of legacy language tags was added. if modified_rules and loop_count > 1:
new_rules = {k for k in transitive_rules.keys() if k notin rules} for k in new_rules: assert k in (
(Any, None, None, "guoyu-hakka"),
(Any, None, None, "guoyu-xiang"),
)
# Merge the transitive rules.
rules.update(transitive_rules)
# Computes the size of the union of all field value sets. def multi_map_size(locale_id):
(language, script, region, variants) = locale_id
return (
(1if language isnotNoneelse0)
+ (1if script isnotNoneelse0)
+ (1if region isnotNoneelse0)
+ (len(variants.split("-")) if variants isnotNoneelse0)
)
# Dictionary of legacy mappings, contains raw rules, e.g. # (None, None, None, "hepburn-heploc") -> (None, None, None, "alalc97").
legacy_mappings = {}
# Dictionary of simple language subtag mappings, e.g. "in" -> "id".
language_mappings = {}
# Dictionary of complex language subtag mappings, modifying more than one # subtag, e.g. "sh" -> ("sr", "Latn", None) and "cnr" -> ("sr", None, "ME").
complex_language_mappings = {}
# Dictionary of simple script subtag mappings, e.g. "Qaai" -> "Zinh".
script_mappings = {}
# Dictionary of simple region subtag mappings, e.g. "DD" -> "DE".
region_mappings = {}
# Dictionary of complex region subtag mappings, containing more than one # replacement, e.g. "SU" -> ("RU", ["AM", "AZ", "BY", ...]).
complex_region_mappings = {}
# Dictionary of aliased variant subtags to a tuple of preferred replacement # type and replacement, e.g. "arevela" -> ("language", "hy") or # "aaland" -> ("region", "AX") or "heploc" -> ("variant", "alalc97").
variant_mappings = {}
# Preprocess all rules so we can perform a single lookup per subtag at runtime. for type, replacement in rules.items():
(language, script, region, variants) = type
(r_language, r_script, r_region, r_variants) = replacement
type_map_size = multi_map_size(type)
# Most mappings are one-to-one and can be encoded through lookup tables. if type_map_size == 1: if language isnotNone: assert r_language isnotNone, "Can't remove a language subtag"
# We don't yet support this case. assert r_variants isNone, (
f"Unhandled variant replacement in language alias: {replacement}"
)
if replacement == (Any, None, None, None):
language_mappings[language] = r_language else:
complex_language_mappings[language] = replacement[:-1] elif script isnotNone: # We don't support removing script subtags. assert r_script isnotNone, (
f"Can't remove a script subtag: {replacement}"
)
# We only support one-to-one script mappings for now. assert replacement == ( None,
Any, None, None,
), f"Unhandled replacement in script alias: {replacement}"
script_mappings[script] = r_script elif region isnotNone: # We don't support removing region subtags. assert r_region isnotNone, (
f"Can't remove a region subtag: {replacement}"
)
# We only support one-to-one region mappings for now. assert replacement == ( None, None,
Any, None,
), f"Unhandled replacement in region alias: {replacement}"
if type notin territory_exception_rules:
region_mappings[region] = r_region else:
complex_region_mappings[region] = [
r_region for (_, _, r_region, _) in territory_exception_rules[type]
] else: assert variants isnotNone assert len(variants.split("-")) == 1
# We only support one-to-one variant mappings for now. assert multi_map_size(replacement) <= 1, (
f"Unhandled replacement in variant alias: {replacement}"
)
if r_language isnotNone:
variant_mappings[variants] = ("language", r_language) elif r_script isnotNone:
variant_mappings[variants] = ("script", r_script) elif r_region isnotNone:
variant_mappings[variants] = ("region", r_region) elif r_variants isnotNone: assert len(r_variants.split("-")) == 1
variant_mappings[variants] = ("variant", r_variants) else:
variant_mappings[variants] = None else: # Alias rules which have multiple input fields must be processed # first. This applies only to a handful of rules, so our generated # code adds fast paths to skip these rules in the common case.
# Case 1: Language and at least one variant subtag. if language isnotNoneand variants isnotNone: pass
# Case 2: Sign language and a region subtag. elif language == "sgn"and region isnotNone: pass
# Case 3: "hepburn-heploc" to "alalc97" canonicalization. elif (
language isNone and variants isnotNone and len(variants.split("-")) == 2
): pass
# Any other combination is currently unsupported. else: raise ValueError(f"{type} -> {replacement}")
legacy_mappings[type] = replacement
tree = ET.parse(core_file.open("common/supplemental/likelySubtags.xml"))
for deprecated_region, replacements in complex_region_mappings.items(): # Find all likely subtag entries which don't already contain a region # subtag and whose target region is in the list of replacement regions.
region_likely_subtags = [
(from_language, from_script, to_region) for (
(from_language, from_script, from_region),
(_, _, to_region),
) in likely_subtags.items() if from_region isNoneand to_region in replacements
]
# The first replacement entry is the default region.
default = replacements[0]
# Find all likely subtag entries whose region matches the default region.
default_replacements = {
(language, script) for (language, script, region) in region_likely_subtags if region == default
}
# And finally find those entries which don't use the default region. # These are the entries we're actually interested in, because those need # to be handled specially when selecting the correct preferred region.
non_default_replacements = [
(language, script, region) for (language, script, region) in region_likely_subtags if (language, script) notin default_replacements
]
# Remove redundant mappings. # # For example starting with CLDR 43, the deprecated region "SU" has the # following non-default replacement entries for "GE": # - ('sva', None, 'GE') # - ('sva', 'Cyrl', 'GE') # - ('sva', 'Latn', 'GE') # # The latter two entries are redundant, because they're already handled # by the first entry.
non_default_replacements = [
(language, script, region) for (language, script, region) in non_default_replacements if script isNone or (language, None, region) notin non_default_replacements
]
# If there are no non-default replacements, we can handle the region as # part of the simple region mapping. if non_default_replacements:
complex_region_mappings_final[deprecated_region] = (
default,
non_default_replacements,
) else:
region_mappings[deprecated_region] = default
# Mapping from Unicode extension types to dict of deprecated to # preferred values.
mapping = { # Unicode BCP 47 U Extension "u": {}, # Unicode BCP 47 T Extension "t": {},
}
def readBCP47File(file):
tree = ET.parse(file) for keyword in tree.iterfind(".//keyword/key"):
extension = keyword.get("extension", "u") assert extension in {"u", "t"}, f"unknown extension type: {extension}"
extension_name = keyword.get("name")
for type in keyword.iterfind("type"): # <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>: # # The key or type name used by Unicode locale extension with 'u' extension # syntax or the 't' extensions syntax. When alias below is absent, this name # can be also used with the old style "@key=type" syntax.
name = type.get("name")
# All other names should match the 'type' production. assert typeRE.match(name) isnotNone, (
f"{name} matches the 'type' production"
)
# <https://unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files>: # # The preferred value of the deprecated key, type or attribute element. # When a key, type or attribute element is deprecated, this attribute is # used for specifying a new canonical form if available.
preferred = type.get("preferred")
# <https://unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers> # # Use the bcp47 data to replace keys, types, tfields, and tvalues by their # canonical forms. See Section 3.6.4 U Extension Data Files) and Section # 3.7.1 T Extension Data Files. The aliases are in the alias attribute # value, while the canonical is in the name attribute value.
# 'preferred' contains the new preferred name, 'alias' the compatibility # name, but then there's this entry where 'preferred' and 'alias' are the # same. So which one to choose? Assume 'preferred' is the actual canonical # name. # # <type name="islamicc" # description="Civil (algorithmic) Arabic calendar" # deprecated="true" # preferred="islamic-civil" # alias="islamic-civil"/>
if preferred isnotNone: assert typeRE.match(preferred), preferred
mapping[extension].setdefault(extension_name, {})[name] = preferred
if alias isnotNone: for alias_name in alias.lower().split(" "): # Ignore alias entries which don't match the 'type' production. if typeRE.match(alias_name) isNone: continue
# See comment above when 'alias' and 'preferred' are both present. if (
preferred isnotNone and name in mapping[extension][extension_name]
): continue
# Skip over entries where 'name' and 'alias' are equal. # # <type name="pst8pdt" # description="POSIX style time zone for US Pacific Time" # alias="PST8PDT" # since="1.8"/> if name == alias_name: continue
mapping[extension].setdefault(extension_name, {})[
alias_name
] = name
def readSupplementalMetadata(file): # Find subdivision and region replacements. # # <https://www.unicode.org/reports/tr35/#Canonical_Unicode_Locale_Identifiers> # # Replace aliases in special key values: # - If there is an 'sd' or 'rg' key, replace any subdivision alias # in its value in the same way, using subdivisionAlias data.
tree = ET.parse(file) for alias in tree.iterfind(".//subdivisionAlias"):
type = alias.get("type") assert typeRE.match(type) isnotNone, (
f"{type} matches the 'type' production"
)
# Take the first replacement when multiple ones are present.
replacement = alias.get("replacement").split(" ")[0].lower()
# Append "zzzz" if the replacement is a two-letter region code. if alphaRegionRE.match(replacement) isnotNone:
replacement += "zzzz"
# Assert the replacement is syntactically correct. assert typeRE.match(replacement) isnotNone, (
f"replacement {replacement} matches the 'type' production"
)
# 'subdivisionAlias' applies to 'rg' and 'sd' keys.
mapping["u"].setdefault("rg", {})[type] = replacement
mapping["u"].setdefault("sd", {})[type] = replacement
for name in core_file.namelist(): if bcpFileRE.match(name):
readBCP47File(core_file.open(name))
template <size_t Length, size_t TagLength, size_t SubtagLength>
static inline bool HasReplacement(
const char (&subtags)[Length][TagLength],
const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) {
MOZ_ASSERT(subtag.Length() == TagLength - 1, "subtag must have the same length as the list of subtags");
template <size_t Length, size_t TagLength, size_t SubtagLength>
static inline const char* SearchReplacement(
const char (&subtags)[Length][TagLength], const char* (&aliases)[Length],
const mozilla::intl::LanguageTagSubtag<SubtagLength>& subtag) {
MOZ_ASSERT(subtag.Length() == TagLength - 1, "subtag must have the same length as the list of subtags");
const char* ptr = subtag.Span().data();
auto p = std::lower_bound(std::begin(subtags), std::end(subtags), ptr,
[](const char* a, const char* b) { return memcmp(a, b, TagLength - 1) < 0;
}); if (p != std::end(subtags) && memcmp(*p, ptr, TagLength - 1) == 0) { return aliases[std::distance(std::begin(subtags), p)];
} return nullptr;
}
def bcp47(tag):
(language, script, region) = tag return"{}{}{}".format(
language, "-" + script if script else"", "-" + region if region else""
)
def canonical(tag):
(language, script, region) = tag
# Map deprecated language subtags. if language in language_mappings:
language = language_mappings[language] elif language in complex_language_mappings:
(language2, script2, region2) = complex_language_mappings[language]
(language, script, region) = (
language2,
script if script else script2,
region if region else region2,
)
# Map deprecated script subtags. if script in script_mappings:
script = script_mappings[script]
# Map deprecated region subtags. if region in region_mappings:
region = region_mappings[region] else: # Assume no complex region mappings are needed for now. assert region notin complex_region_mappings, (
f"unexpected region with complex mappings: {region}"
)
# Step 3: Return. return (
language if language != language_s else language_m,
script if script != script_s else script_m,
region if region != region_s else region_m,
)
# Update the expected result if necessary. if from_tag in likely_subtags:
to_tag = likely_subtags[from_tag]
# Canonicalize the expected output.
to_canonical = canonical(to_tag)
# Sanity check: This should match the result of |addLikelySubtags|. assert to_canonical == addLikelySubtags(from_tag)
return to_canonical
# |likely_subtags| contains non-canonicalized tags, so canonicalize it first.
likely_subtags_canonical = {
k: likely_canonical(k, v) for (k, v) in likely_subtags.items()
}
# Add test data for |Intl.Locale.prototype.maximize()|.
writeMappingsVar(
println,
{bcp47(k): bcp47(v) for (k, v) in likely_subtags_canonical.items()}, "maxLikelySubtags", "Extracted from likelySubtags.xml.",
source,
url,
)
# Use the maximalized tags as the input for the remove likely-subtags test.
minimized = {
tag: removeLikelySubtags(tag) for tag in likely_subtags_canonical.values()
}
# Add test data for |Intl.Locale.prototype.minimize()|.
writeMappingsVar(
println,
{bcp47(k): bcp47(v) for (k, v) in minimized.items()}, "minLikelySubtags", "Extracted from likelySubtags.xml.",
source,
url,
)
println( """ for (let [tag, maximal] of Object.entries(maxLikelySubtags)) {
assertEq(new Intl.Locale(tag).maximize().toString(), maximal);
}"""
)
println( """ for (let [tag, minimal] of Object.entries(minLikelySubtags)) {
assertEq(new Intl.Locale(tag).minimize().toString(), minimal);
}"""
)
println( """ if (typeof reportCompare === "function")
reportCompare(0, 0);"""
)
def writeAllLocalesSupportedTest(topsrcdir): """Writes the supported locales test files."""
all_locales = [] for line in flines(os.path.join(topsrcdir, "browser/locales/all-locales")):
line = line.strip() if line == "": continue
# Special case for the legacy locale id "ja-JP-mac", which is not a valid # BCP 47 locale identifier.
locale = line if line != "ja-JP-mac"else"ja-JP-macos"
all_locales.append(locale)
# List of Intl service constructors.
intl_constructors = [ "Collator", "DateTimeFormat", "DisplayNames", "DurationFormat", "ListFormat", "NumberFormat", "PluralRules", "RelativeTimeFormat", "Segmenter",
]
// Ensure all Firefox locales are supported by Intl.{intl_constructor}, except
// for the known unsupported locales.
assertEqArray(
[...new Set(allLocales).difference(new Set(supported))].sort(),
unsupported
);
if (typeof reportCompare === "function")
reportCompare(0, 0, "ok"); """.rstrip()
)
def readFiles(cldr_file): with ZipFile(cldr_file) as zip_file:
data.update(readSupplementalData(zip_file))
data.update(readUnicodeExtensions(zip_file))
print("Processing CLDR data...") if filename isnotNone:
print("Always make sure you have the newest CLDR common.zip!") with open(filename, "rb") as cldr_file:
readFiles(cldr_file) else:
print("Downloading CLDR common.zip...") with closing(urlopen(url)) as cldr_file:
cldr_data = io.BytesIO(cldr_file.read())
readFiles(cldr_data)
print("Writing Intl data...") with open(out, mode="w", encoding="utf-8", newline="") as f:
println = partial(print, file=f)
writeCLDRLanguageTagData(println, data, url)
print("Writing Intl test data...")
js_src_builtin_intl_dir = os.path.dirname(os.path.abspath(__file__))
test_file = os.path.join(
js_src_builtin_intl_dir, "../../tests/non262/Intl/Locale/likely-subtags-generated.js",
) with open(test_file, mode="w", encoding="utf-8", newline="") as f:
println = partial(print, file=f)
def _tarlines(self, tar, m): with closing(tar.extractfile(m)) as f: for line in f: yield line.decode("utf-8")
def validateTimeZones(zones, links): """Validate the zone and link entries."""
linkZones = set(links.keys())
intersect = linkZones.intersection(zones) if intersect: raise RuntimeError("Links also present in zones: %s" % intersect)
zoneNames = {z.name for z in zones}
linkTargets = set(links.values()) ifnot linkTargets.issubset(zoneNames): raise RuntimeError( "Link targets not found: %s" % linkTargets.difference(zoneNames)
)
def readIANAFiles(tzdataDir, files): """Read all IANA time zone files from the given iterable."""
nameSyntax = r"[\w/+\-]+"
pZone = re.compile(r"Zone\s+(?P<name>%s)\s+.*" % nameSyntax)
pLink = re.compile(
r"(#PACKRATLIST\s+zone.tab\s+)?Link\s+(?P<target>%s)\s+(?P<name>%s)(?:\s+#.*)?"
% (nameSyntax, nameSyntax)
)
def createZone(line, fname):
match = pZone.match(line)
name = match.group("name") return Zone(name, fname)
def parseValue(value):
m = reNumberVector.match(value) if m: return [int(v) for v in reNumberValue.findall(value)]
m = reStringVector.match(value) if m: return [v[1:-1] for v in reStringValue.findall(value)] raise RuntimeError("unknown value type: %s" % value)
def extractValue(values): if len(values) == 0: returnNone if len(values) == 1: return values[0] return values
values = [] for line in flines(filename, "utf-8-sig"):
line = line.strip() if line == "": continue
m = reEmptyLine.match(line) if m: continue
m = reStartTable.match(line) if m: assert len(values) == 0
tables.append(m.group("name")) continue
m = reEndTable.match(line) if m: yield (currentTable(), extractValue(values))
tables.pop()
values = [] continue
m = reCompactTable.match(line) if m: assert len(values) == 0
tables.append(m.group("name")) yield (currentTable(), extractValue(parseValue(m.group("value"))))
tables.pop() continue
m = reSingleValue.match(line) if m and tables:
values.extend(parseValue(m.group("value"))) continue
raise RuntimeError("unknown entry: %s" % line)
def readICUTimeZonesFromTimezoneTypes(icuTzDir): """Read the ICU time zone information from `icuTzDir`/timezoneTypes.txt and returns the tuple (zones, links). """
typeMapTimeZoneKey = "timezoneTypes:table(nofallback)|typeMap|timezone|"
typeAliasTimeZoneKey = "timezoneTypes:table(nofallback)|typeAlias|timezone|"
for name, value in readICUResourceFile(os.path.join(icuTzDir, "timezoneTypes.txt")): if name.startswith(typeMapTimeZoneKey):
zones.add(toTimeZone(name[len(typeMapTimeZoneKey) :])) if name.startswith(typeAliasTimeZoneKey):
links[toTimeZone(name[len(typeAliasTimeZoneKey) :])] = value
validateTimeZones(zones, links)
return (zones, links)
def readICUTimeZonesFromZoneInfo(icuTzDir): """Read the ICU time zone information from `icuTzDir`/zoneinfo64.txt and returns the tuple (zones, links). """
zoneKey = "zoneinfo64:table(nofallback)|Zones:array|:table"
linkKey = "zoneinfo64:table(nofallback)|Zones:array|:int"
namesKey = "zoneinfo64:table(nofallback)|Names"
tzId = 0
tzLinks = dict()
tzNames = []
for name, value in readICUResourceFile(os.path.join(icuTzDir, "zoneinfo64.txt")): if name == zoneKey:
tzId += 1 elif name == linkKey:
tzLinks[tzId] = int(value)
tzId += 1 elif name == namesKey:
tzNames.extend(value)
links = {Zone(tzNames[zone]): tzNames[target] for (zone, target) in tzLinks.items()}
zones = {Zone(v) for v in tzNames if Zone(v) notin links}
validateTimeZones(zones, links)
return (zones, links)
def readICUTimeZones(icuDir, icuTzDir, ignoreFactory): # zoneinfo64.txt contains the supported time zones by ICU. This data is # generated from tzdata files, it doesn't include "backzone" in stock ICU.
(zoneinfoZones, zoneinfoLinks) = readICUTimeZonesFromZoneInfo(icuTzDir)
# timezoneTypes.txt contains the canonicalization information for ICU. This # data is generated from CLDR files. It includes data about time zones from # tzdata's "backzone" file.
(typesZones, typesLinks) = readICUTimeZonesFromTimezoneTypes(icuTzDir)
# Remove the placeholder time zone "Factory". # See also <https://github.com/eggert/tz/blob/master/factory>. if ignoreFactory: assert Zone("Factory") in zoneinfoZones assert Zone("Factory") notin zoneinfoLinks assert Zone("Factory") notin typesZones assert Zone("Factory") in typesLinks
zoneinfoZones.remove(Zone("Factory")) del typesLinks[Zone("Factory")]
# Remove any outdated ICU links. for links in (zoneinfoLinks, typesLinks): for zone in otherICULegacyLinks().keys(): if zone notin links: raise KeyError(f"Can't remove non-existent link from '{zone}'") del links[zone]
# Information in zoneinfo64 should be a superset of timezoneTypes. def inZoneInfo64(zone): return zone in zoneinfoZones or zone in zoneinfoLinks
notFoundInZoneInfo64 = [zone for zone in typesZones ifnot inZoneInfo64(zone)] if notFoundInZoneInfo64: raise RuntimeError( "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64
)
notFoundInZoneInfo64 = [
zone for zone in typesLinks.keys() ifnot inZoneInfo64(zone)
] if notFoundInZoneInfo64: raise RuntimeError( "Missing time zones in zoneinfo64.txt: %s" % notFoundInZoneInfo64
)
# zoneinfo64.txt only defines the supported time zones by ICU, the canonicalization # rules are defined through timezoneTypes.txt. Merge both to get the actual zones # and links used by ICU.
icuZones = set(
chain(
(zone for zone in zoneinfoZones if zone notin typesLinks),
(zone for zone in typesZones),
)
)
icuLinks = dict(
chain(
(
(zone, target) for (zone, target) in zoneinfoLinks.items() if zone notin typesZones
),
((zone, target) for (zone, target) in typesLinks.items()),
)
)
return (icuZones, icuLinks)
def readICULegacyZones(icuDir): """Read the ICU legacy time zones from `icuTzDir`/tools/tzcode/icuzones and returns the tuple (zones, links). """
tzdir = TzDataDir(os.path.join(icuDir, "tools/tzcode"))
# Per spec we must recognize only IANA time zones and links, but ICU # recognizes various legacy, non-IANA time zones and links. Compute these # non-IANA time zones and links.
# Most legacy, non-IANA time zones and links are in the icuzones file.
(zones, links, _) = readIANAFiles(tzdir, ["icuzones"])
# A handful of non-IANA zones/links are not in icuzones and must be added # manually so that we won't invoke ICU with them. for zone, target in otherICULegacyLinks().items(): if zone in links: if links[zone] != target: raise KeyError(
f"Can't overwrite link '{zone} -> {links[zone]}' with '{target}'"
) else:
print(
f"Info: Link '{zone} -> {target}' can be removed from otherICULegacyLinks()"
)
links[zone] = target
return (zones, links)
def otherICULegacyLinks(): """The file `icuTzDir`/tools/tzcode/icuzones contains all ICU legacy time
zones with the exception of time zones which are removed by IANA after an
ICU release.
For example ICU 67 uses tzdata2018i, but tzdata2020b removed the link from "US/Pacific-New" to "America/Los_Angeles". ICU standalone tzdata updates
don't include modified icuzones files, so we must manually record any IANA
modifications here.
After an ICU update, we can remove any no longer needed entries from this
function by checking if the relevant entries are now included in icuzones. """
return { # Current ICU is up-to-date with IANA, so this dict is empty.
}
def icuTzDataVersion(icuTzDir): """Read the ICU time zone version from `icuTzDir`/zoneinfo64.txt."""
def searchInFile(pattern, f):
p = re.compile(pattern) for line in flines(f, "utf-8-sig"):
m = p.search(line) if m: return m.group(1) returnNone
zoneinfo = os.path.join(icuTzDir, "zoneinfo64.txt") ifnot os.path.isfile(zoneinfo): raise RuntimeError("file not found: %s" % zoneinfo)
version = searchInFile(r"^//\s+tz version:\s+([0-9]{4}[a-z])$", zoneinfo) if version isNone: raise RuntimeError( "%s does not contain a valid tzdata version string" % zoneinfo
) return version
def findIncorrectICUZones(ianaZones, ianaLinks, icuZones, icuLinks): """Find incorrect ICU zone entries."""
def isIANATimeZone(zone): return zone in ianaZones or zone in ianaLinks
def isICUTimeZone(zone): return zone in icuZones or zone in icuLinks
def isICULink(zone): return zone in icuLinks
# All IANA zones should be present in ICU.
missingTimeZones = [zone for zone in ianaZones ifnot isICUTimeZone(zone)] if missingTimeZones: raise RuntimeError( "Not all zones are present in ICU, did you forget " "to run intl/update-tzdata.sh? %s" % missingTimeZones
)
# Zones which are only present in ICU?
additionalTimeZones = [zone for zone in icuZones ifnot isIANATimeZone(zone)] if additionalTimeZones: raise RuntimeError( "Additional zones present in ICU, did you forget " "to run intl/update-tzdata.sh? %s" % additionalTimeZones
)
# Zones which are marked as links in ICU.
result = ((zone, icuLinks[zone]) for zone in ianaZones if isICULink(zone))
# Remove unnecessary UTC mappings.
utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"]
result = ((zone, target) for (zone, target) in result if zone.name notin utcnames)
return sorted(result, key=itemgetter(0))
def findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks): """Find incorrect ICU link entries."""
def isIANATimeZone(zone): return zone in ianaZones or zone in ianaLinks
def isICUTimeZone(zone): return zone in icuZones or zone in icuLinks
def isICULink(zone): return zone in icuLinks
def isICUZone(zone): return zone in icuZones
# All links should be present in ICU.
missingTimeZones = [zone for zone in ianaLinks.keys() ifnot isICUTimeZone(zone)] if missingTimeZones: raise RuntimeError( "Not all zones are present in ICU, did you forget " "to run intl/update-tzdata.sh? %s" % missingTimeZones
)
# Links which are only present in ICU?
additionalTimeZones = [zone for zone in icuLinks.keys() ifnot isIANATimeZone(zone)] if additionalTimeZones: raise RuntimeError( "Additional links present in ICU, did you forget " "to run intl/update-tzdata.sh? %s" % additionalTimeZones
)
result = chain( # IANA links which have a different target in ICU.
(
(zone, target, icuLinks[zone]) for (zone, target) in ianaLinks.items() if isICULink(zone) and target != icuLinks[zone]
), # IANA links which are zones in ICU.
(
(zone, target, zone.name) for (zone, target) in ianaLinks.items() if isICUZone(zone)
),
)
# Remove unnecessary UTC mappings.
utcnames = ["Etc/UTC", "Etc/UCT", "Etc/GMT"]
result = (
(zone, target, icuTarget) for (zone, target, icuTarget) in result if target notin utcnames or icuTarget notin utcnames
)
return sorted(result, key=itemgetter(0))
def readZoneTab(tzdataDir):
zone_country = dict()
zonetab_path = tzdataDir.resolve("zone.tab") for line in tzdataDir.readlines(zonetab_path): if line.startswith("#"): continue
(country, coords, zone, *comments) = line.strip().split("\t") assert zone notin zone_country
zone_country[zone] = country
# Step 5.c.
if primary in ["Etc/UTC", "Etc/GMT", "GMT"]:
primary = "UTC"
# Step 5.d. (Not applicable)
# Steps 5.e-f.
if primary == identifier:
assert zone not in zones
zones.add(zone)
else:
assert zone not in links
links[zone] = primary
# Ensure all zones and links are valid.
validateTimeZones(zones, links)
# Step 6.
assert Zone("UTC") in zones
# Step 7.
return (zones, links)
generatedFileWarning = "// Generated by make_intl_data.py. DO NOT EDIT."
tzdataVersionComment = "// tzdata version = {0}"
def processTimeZones(tzdataDir, icuDir, icuTzDir, version, ignoreFactory, out):
"""Read the time zone info and create a new time zone cpp file."""
print("Processing tzdata mapping...")
(ianaZones, ianaLinks) = availableNamedTimeZoneIdentifiers(tzdataDir, ignoreFactory)
(icuZones, icuLinks) = readICUTimeZones(icuDir, icuTzDir, ignoreFactory)
(legacyZones, legacyLinks) = readICULegacyZones(icuDir)
if ignoreFactory:
legacyZones.add(Zone("Factory"))
# Remove all legacy ICU time zones.
icuZones = {zone for zone in icuZones if zone not in legacyZones}
icuLinks = {
zone: target for (zone, target) in icuLinks.items() if zone not in legacyLinks
}
incorrectZones = findIncorrectICUZones(ianaZones, ianaLinks, icuZones, icuLinks)
if not incorrectZones:
print("<<< No incorrect ICU time zones found, please update Intl.js! >>>")
print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>")
incorrectLinks = findIncorrectICULinks(ianaZones, ianaLinks, icuZones, icuLinks)
if not incorrectLinks:
print("<<< No incorrect ICU time zone links found, please update Intl.js! >>>")
print("<<< Maybe https://ssl.icu-project.org/trac/ticket/12044 was fixed? >>>")
print("Writing Intl tzdata file...")
with open(out, mode="w", encoding="utf-8", newline="") as f:
println = partial(print, file=f)
println(
"// Legacy ICU time zones, these are not valid IANA time zone names. We also"
)
println("// disallow the old and deprecated System V time zones.")
println(
"// https://ssl.icu-project.org/repos/icu/trunk/icu4c/source/tools/tzcode/icuzones"
) # NOQA: E501
println("const char* const legacyICUTimeZones[] = {")
for zone in chain(sorted(legacyLinks.keys()), sorted(legacyZones)):
println(' "%s",' % zone)
println("};")
println("")
# Read zone and link infos.
(_, links) = availableNamedTimeZoneIdentifiers(tzdataDir, ignoreFactory)
with open(
os.path.join(testDir, fileName), mode="w", encoding="utf-8", newline=""
) as f:
println = partial(print, file=f)
println("")
println(generatedFileWarning)
println(tzdataVersionComment.format(version))
println(
"""
const tzMapper = [
x => x,
x => x.toUpperCase(),
x => x.toLowerCase(),
];
"""
)
println("// Link names derived from IANA Time Zone Database.")
println("const links = {")
for zone, target in sorted(links.items(), key=itemgetter(0)):
println(' "%s": "%s",' % (zone, target))
println("};")
println(
"""
for (let [linkName, target] of Object.entries(links)) {
if (target === "Etc/UTC" || target === "Etc/GMT")
target = "UTC";
for (let map of tzMapper) {
let dtf = new Intl.DateTimeFormat(undefined, {timeZone: map(linkName)});
let resolvedTimeZone = dtf.resolvedOptions().timeZone;
assertEq(resolvedTimeZone, target, `${linkName} -> ${target}`);
}
}
"""
)
println(
"""
if (typeof reportCompare === "function")
reportCompare(0, 0, "ok");
"""
)
println("const zones = [")
for zone in sorted(zones):
println(f' "{zone}",')
println("];")
println("const links = {")
for link, target in sorted(links.items(), key=itemgetter(0)):
println(f' "{link}": "{target}",')
println("};")
println(
"""
let epochNanoseconds = [
new Temporal.PlainDate(1900, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(1950, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(1960, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(1970, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(1980, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(1990, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(2000, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(2010, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(2020, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
new Temporal.PlainDate(2030, 1, 1).toZonedDateTime("UTC").epochNanoseconds,
];
function timeZoneId(zdt) {
let str = zdt.toString();
let m = str.match(/(?<=\\[)[\\w\\/_+-]+(?=\\])/);
assertEq(m !== null, true, str);
return m[0];
}
for (let zone of zones) {
let zdt = new Temporal.ZonedDateTime(0n, zone);
for (let epochNs of epochNanoseconds) {
assertEq(
new Temporal.ZonedDateTime(epochNs, link).offsetNanoseconds,
new Temporal.ZonedDateTime(epochNs, zone).offsetNanoseconds,
`link=${link}, zone=${zone}, epochNs=${epochNs}`
);
}
}
if (typeof reportCompare === "function")
reportCompare(0, 0, "ok");
"""
)
def generateTzDataTests(tzdataDir, version, ignoreFactory, testDir):
dtfTestDir = os.path.join(testDir, "DateTimeFormat")
if not os.path.isdir(dtfTestDir):
raise RuntimeError("not a directory: %s" % dtfTestDir)
zdtTestDir = os.path.join(testDir, "../Temporal/ZonedDateTime")
if not os.path.isdir(zdtTestDir):
raise RuntimeError("not a directory: %s" % zdtTestDir)
def updateTzdata(topsrcdir, args):
"""Update the time zone cpp file."""
icuDir = os.path.join(topsrcdir, "intl/icu/source")
if not os.path.isdir(icuDir):
raise RuntimeError("not a directory: %s" % icuDir)
icuTzDir = os.path.join(topsrcdir, "intl/tzdata/source")
if not os.path.isdir(icuTzDir):
raise RuntimeError("not a directory: %s" % icuTzDir)
intlTestDir = os.path.join(topsrcdir, "js/src/tests/non262/Intl")
if not os.path.isdir(intlTestDir):
raise RuntimeError("not a directory: %s" % intlTestDir)
tzDir = args.tz
if tzDir is not None and not (os.path.isdir(tzDir) or os.path.isfile(tzDir)):
raise RuntimeError("not a directory or file: %s" % tzDir)
out = args.out
# Ignore the placeholder time zone "Factory".
ignoreFactory = True
if tzDir is None:
print("Downloading tzdata file...")
with closing(urlopen(url)) as tzfile:
fname = urlsplit(tzfile.geturl()).path.split("/")[-1]
with tempfile.NamedTemporaryFile(suffix=fname) as tztmpfile:
print("File stored in %s" % tztmpfile.name)
tztmpfile.write(tzfile.read())
tztmpfile.flush()
updateFrom(tztmpfile.name)
else:
updateFrom(tzDir)
for country in tree.iterfind(".//CcyNtry"):
# Skip entry if no currency information is available.
currency = country.findtext("Ccy")
if currency is None:
continue
assert reCurrency.match(currency)
minorUnits = country.findtext("CcyMnrUnts")
assert minorUnits is not None
# Skip all entries without minorUnits or which use the default minorUnits.
if reIntMinorUnits.match(minorUnits) and int(minorUnits) != 2:
currencyName = country.findtext("CcyNm")
countryName = country.findtext("CtryNm")
yield (currency, int(minorUnits), currencyName, countryName)
def writeCurrencyFile(published, currencies, out):
with open(out, mode="w", encoding="utf-8", newline="") as f:
println = partial(print, file=f)
println(
"""
/**
* Mapping from currency codes to the number of decimal digits used for them.
* Default is 2 digits.
*
* Spec: ISO 4217 Currency and Funds Code List.
* http://www.currency-iso.org/en/home/tables/table-a1.html
*/
if filename is not None:
print("Always make sure you have the newest currency code list file!")
updateFrom(filename)
else:
print("Downloading currency & funds code list...")
request = UrlRequest(url)
request.add_header(
"User-agent",
"Mozilla/5.0 (Mobile; rv:{0}.0) Gecko/{0}.0 Firefox/{0}.0".format(
randint(1, 999)
),
)
with closing(urlopen(request)) as currencyFile:
fname = urlsplit(currencyFile.geturl()).path.split("/")[-1]
with tempfile.NamedTemporaryFile(suffix=fname) as currencyTmpFile:
print("File stored in %s" % currencyTmpFile.name)
currencyTmpFile.write(currencyFile.read())
currencyTmpFile.flush()
updateFrom(currencyTmpFile.name)
needs_binary_search = any(
len(replacements.items()) > linear_search_max_length
for replacements in mapping.values()
)
if needs_binary_search:
println(
f"""
static int32_t Compare{extension}Type(const char* a, mozilla::Span<const char> b) {{
MOZ_ASSERT(!std::char_traits<char>::find(b.data(), b.size(), '\\0'),
"unexpected null-character in string");
using UnsignedChar = unsigned char;
for (size_t i = 0; i < b.size(); i++) {{
// |a| is zero-terminated and |b| doesn't contain a null-terminator. So if
// we've reached the end of |a|, the below if-statement will always be true.
// That ensures we don't read past the end of |a|.
if (int32_t r = UnsignedChar(a[i]) - UnsignedChar(b[i])) {{
return r;
}}
}}
// Return zero if both strings are equal or a positive number if |b| is a
// prefix of |a|.
return int32_t(UnsignedChar(a[b.size()]));
}}
for entries in grouper(subtags, max_entries):
entries = (
f'"{tag}"'.center(length + 2) for tag in entries if tag is not None
)
println(" {},".format(", ".join(entries)))
println(" };")
# Merge duplicate keys.
key_aliases = {}
for key, replacements in sorted(mapping.items(), key=itemgetter(0)):
hash_key = to_hash_key(replacements)
if hash_key not in key_aliases:
key_aliases[hash_key] = []
else:
key_aliases[hash_key].append(key)
first_key = True
for key, replacements in sorted(mapping.items(), key=itemgetter(0)):
hash_key = to_hash_key(replacements)
if key in key_aliases[hash_key]:
continue
cond = (f'Is{extension}Key(key, "{k}")' for k in [key] + key_aliases[hash_key])
if len(replacements) > linear_search_max_length:
types = [t for (t, _) in replacements]
preferred = [r for (_, r) in replacements]
max_len = max(len(k) for k in types + preferred)
write_array(types, "types", max_len)
write_array(preferred, "aliases", max_len)
println(
f"""
return Search{extension}Replacement(types, aliases, type);
""".strip("\n")
)
else:
for type, replacement in replacements:
println(
f"""
if (Is{extension}Type(type, "{type}")) {{
return "{replacement}";
}}""".strip("\n")
)
println(
"""
}""".lstrip("\n")
)
println(
"""
return nullptr;
}
""".strip("\n")
)
def readICUUnitResourceFile(filepath):
"""Return a set of unit descriptor pairs where the first entry denotes the unit type and the
second entry the unit name.
for line in flines(filepath, "utf-8-sig"):
# Remove leading and trailing whitespace.
line = line.strip()
# Skip over comments.
if in_multiline_comment:
if line.endswith("*/"):
in_multiline_comment = False
continue
if line.startswith("//"):
continue
if line.startswith("/*"):
in_multiline_comment = True
continue
# Try to match the start of a table, e.g. `length{` or `meter{`.
match = start_table_re.match(line)
if match:
parents.append(table)
table_name = match.group(1)
new_table = {}
table[table_name] = new_table
table = new_table
continue
# Try to match the end of a table.
match = end_table_re.match(line)
if match:
table = parents.pop()
continue
# Try to match a table entry, e.g. `dnam{"meter"}`.
match = table_entry_re.match(line)
if match:
entry_key = match.group(1)
entry_value = match.group(2)
table[entry_key] = entry_value
continue
raise Exception(f"unexpected line: '{line}' in {filepath}")
assert len(parents) == 0, "Not all tables closed"
assert len(table) == 1, "More than one root table"
# Remove the top-level language identifier table.
(_, unit_table) = table.popitem()
# Add all units for the three display formats "units", "unitsNarrow", and "unitsShort".
# But exclude the pseudo-units "compound" and "ccoordinate".
return {
(unit_type, unit_name if not unit_name.endswith(":alias") else unit_name[:-6])
for unit_display in ("units", "unitsNarrow", "unitsShort")
if unit_display in unit_table
for (unit_type, unit_names) in unit_table[unit_display].items()
if unit_type not in {"compound", "coordinate"}
for unit_name in unit_names.keys()
}
def computeSupportedUnits(all_units, sanctioned_units):
"""Given the set of all possible ICU unit identifiers and the set of sanctioned unit
identifiers, compute the set of effectively supported ICU unit identifiers.
"""
def find_match(unit):
unit_match = [
(unit_type, unit_name)
for (unit_type, unit_name) in all_units
if unit_name == unit
]
if unit_match:
assert len(unit_match) == 1
return unit_match[0]
return None
def compound_unit_identifiers():
for numerator in sanctioned_units:
for denominator in sanctioned_units:
yield f"{numerator}-per-{denominator}"
supported_simple_units = {find_match(unit) for unit in sanctioned_units}
assert None not in supported_simple_units
supported_compound_units = {
unit_match
for unit_match in (find_match(unit) for unit in compound_unit_identifiers())
if unit_match
}
def readICUDataFilterForUnits(data_filter_file):
with open(data_filter_file, encoding="utf-8") as f:
data_filter = json.load(f)
# Find the rule set for the "unit_tree".
unit_tree_rules = [
entry["rules"]
for entry in data_filter["resourceFilters"]
if entry["categories"] == ["unit_tree"]
]
assert len(unit_tree_rules) == 1
# Compute the list of included units from that rule set. The regular expression must match
# "+/*/length/meter" and mustn't match either "-/*" or "+/*/compound".
included_unit_re = re.compile(r"^\+/\*/(.+?)/(.+)$")
filtered_units = (included_unit_re.match(unit) for unit in unit_tree_rules[0])
return {(unit.group(1), unit.group(2)) for unit in filtered_units if unit}
/**
* The list of currently supported simple unit identifiers.
*
* The list must be kept in alphabetical order.
*/
inline constexpr SimpleMeasureUnit simpleMeasureUnits[] = {
// clang-format off"""
)
for unit_name in sorted(sanctioned_units):
println(f' {{"{unit_name}"}},')
println(
"""
// clang-format on
};
} // namespace js::intl
#endif
""".strip("\n")
)
writeUnitTestFiles(all_units, sanctioned_units)
def writeUnitTestFiles(all_units, sanctioned_units):
"""Generate test files for unit number formatters."""
write_test(
"unit-compound-combinations.js",
"""
// Test all simple unit identifier combinations are allowed.
for (const numerator of sanctionedSimpleUnitIdentifiers) {
for (const denominator of sanctionedSimpleUnitIdentifiers) {
const unit = `${numerator}-per-${denominator}`;
const nf = new Intl.NumberFormat("en", {style: "unit", unit});
if (allowed) {
const nf = new Intl.NumberFormat("en", {style: "unit", unit});
assertEq(nf.format(1), nf.formatToParts(1).map(p => p.value).join(""));
} else {
assertThrowsInstanceOf(() => new Intl.NumberFormat("en", {style: "unit", unit}),
RangeError, `Missing error for "${typeAndUnit}"`);
}
}""",
)
write_test(
"unit-formatToParts-has-unit-field.js",
"""
// Test only English and Chinese to keep the overall runtime reasonable.
//
// Chinese is included because it contains more than one "unit" element for
// certain unit combinations.
const locales = ["en", "zh"];
// Plural rules for English only differentiate between "one" and "other". Plural
// rules for Chinese only use "other". That means we only need to test two values
// per unit.
const values = [0, 1];
// Ensure unit formatters contain at least one "unit" element.
for (const locale of locales) {
for (const unit of sanctionedSimpleUnitIdentifiers) {
const nf = new Intl.NumberFormat(locale, {style: "unit", unit});
for (const value of values) {
assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true,
`locale=${locale}, unit=${unit}`);
}
}
for (const numerator of sanctionedSimpleUnitIdentifiers) {
for (const denominator of sanctionedSimpleUnitIdentifiers) {
const unit = `${numerator}-per-${denominator}`;
const nf = new Intl.NumberFormat(locale, {style: "unit", unit});
for (const value of values) {
assertEq(nf.formatToParts(value).some(e => e.type === "unit"), true,
`locale=${locale}, unit=${unit}`);
}
}
}
}""",
indent=2,
)
with open(
os.path.join(js_src_builtin_intl_dir, "SanctionedSimpleUnitIdentifiers.yaml"),
encoding="utf-8",
) as f:
sanctioned_units = yaml.safe_load(f)
# Read all possible ICU unit identifiers from the "unit/root.txt" resource.
unit_root_file = os.path.join(icu_unit_path, "root.txt")
all_units = readICUUnitResourceFile(unit_root_file)
# Compute the set of effectively supported ICU unit identifiers.
supported_units = computeSupportedUnits(all_units, sanctioned_units)
# Read the list of units we're including into the ICU data file.
data_filter_file = os.path.join(icu_path, "data_filter.json")
filtered_units = readICUDataFilterForUnits(data_filter_file)
# Both sets must match to avoid resource loading errors at runtime.
if supported_units != filtered_units:
def units_to_string(units):
return ", ".join("/".join(u) for u in units)
# Not exactly an error, but we currently don't have a use case where we need to support
# more units than required by ECMA-402.
extra = filtered_units - supported_units
if extra:
raise RuntimeError(f"Unnecessary units: {units_to_string(extra)}")
def readICUNumberingSystemsResourceFile(filepath):
"""Returns a dictionary of numbering systems where the key denotes the numbering system name
and the value a dictionary with additional numbering system data.
for line in flines(filepath, "utf-8-sig"):
# Remove leading and trailing whitespace.
line = line.strip()
# Skip over comments.
if in_multiline_comment:
if line.endswith("*/"):
in_multiline_comment = False
continue
if line.startswith("//"):
continue
if line.startswith("/*"):
in_multiline_comment = True
continue
# Try to match the start of a table, e.g. `latn{`.
match = start_table_re.match(line)
if match:
parents.append(table)
table_name = match.group(1)
new_table = {}
table[table_name] = new_table
table = new_table
continue
# Try to match the end of a table.
match = end_table_re.match(line)
if match:
table = parents.pop()
continue
# Try to match a table entry, e.g. `desc{"0123456789"}`.
match = table_entry_re.match(line)
if match:
entry_key = match.group(1)
entry_value = (
match.group(2) if match.group(2) is not None else int(match.group(3))
)
table[entry_key] = entry_value
continue
raise Exception(f"unexpected line: '{line}' in {filepath}")
assert len(parents) == 0, "Not all tables closed"
assert len(table) == 1, "More than one root table"
# Remove the two top-level "numberingSystems" tables.
(_, numbering_systems) = table.popitem()
(_, numbering_systems) = numbering_systems.popitem()
# Assert all numbering systems use base 10.
assert all(ns["radix"] == 10 for ns in numbering_systems.values())
# Return the numbering systems.
return {
key: (
{"digits": value["desc"], "algorithmic": False}
if not bool(value["algorithmic"])
else {"algorithmic": True}
)
for (key, value) in numbering_systems.items()
}
with open(
os.path.join(js_src_builtin_intl_dir, "NumberingSystems.yaml"),
encoding="utf-8",
) as f:
numbering_systems = yaml.safe_load(f)
# Read all possible ICU unit identifiers from the "misc/numberingSystems.txt" resource.
misc_ns_file = os.path.join(icu_misc_path, "numberingSystems.txt")
all_numbering_systems = readICUNumberingSystemsResourceFile(misc_ns_file)
all_numbering_systems_simple_digits = {
name
for (name, value) in all_numbering_systems.items()
if not value["algorithmic"]
}
# Assert ICU includes support for all required numbering systems. If this assertion fails,
# something is broken in ICU.
assert all_numbering_systems_simple_digits.issuperset(numbering_systems), (
f"{numbering_systems.difference(all_numbering_systems_simple_digits)}"
)
# Assert the spec requires support for all numbering systems with simple digit mappings. If
# this assertion fails, file a PR at <https://github.com/tc39/ecma402> to include any new
# numbering systems.
assert all_numbering_systems_simple_digits.issubset(numbering_systems), (
f"{all_numbering_systems_simple_digits.difference(numbering_systems)}"
)
writeNumberingSystemFiles(all_numbering_systems)
if __name__ == "__main__":
import argparse
# This script must reside in js/src/builtin/intl to work correctly.
(thisDir, thisFile) = os.path.split(os.path.abspath(__file__))
dirPaths = os.path.normpath(thisDir).split(os.sep)
if "/".join(dirPaths[-4:]) != "js/src/builtin/intl":
raise RuntimeError("%s must reside in js/src/builtin/intl" % __file__)
topsrcdir = "/".join(dirPaths[:-4])
def EnsureHttps(v):
if not v.startswith("https:"):
raise argparse.ArgumentTypeError(f"URL protocol must be https: {v}")
return v
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.