Merge branch 'stable' into devel
[tails.git] / bin / locale-descriptions
blob7f76074c9ce7f2d66ad64650a42bb0f11943c041
1 #!/usr/bin/python3
2 """
3 This script helps the RM to maintain config/chroot_local-includes/usr/share/tails/browser-localization/descriptions
5 It does so in two ways:
6  - the 'generate' subcommand will generate content for the
7    descriptions file, based on information in po/po-to-mozilla.toml
8  - the 'suggest' subcommand will help the RM adding more lines
9    to po/po-to-mozilla.toml
10 """
12 import sys
13 from pathlib import Path
14 import re
15 from collections import defaultdict
16 from argparse import ArgumentParser
18 import toml
19 import requests
21 try:
22     from BeautifulSoup import BeautifulSoup
23 except ImportError:
24     from bs4 import BeautifulSoup
26 ERRORS = 0
27 LANGUAGE_RE = re.compile(r'^"Language:\s+(.*)\\n"$')
28 MAPFILE = "po/po-to-mozilla.toml"
31 def get_language(pofile: Path) -> str:
32     """
33     Get language name associated with the pofile.
35     Please note that this might be a language name (ie: `it` for Italian)
36     or a full locale (ie: `pt_BR` for Brazilian Portuguese).
37     """
38     for line in pofile.open():
39         match = LANGUAGE_RE.match(line)
40         if match is not None:
41             return match.group(1)
42     raise ValueError(f"Could not extract language from file {pofile}")
45 def locale_to_mozilla(locale: str) -> str:
46     """
47     >>> locale_to_mozilla('ar_EG')
48     'ar-EG:EG'
49     >>> locale_to_mozilla('it')
50     Traceback (most recent call last):
51     ...
52     ValueError: country not specified in 'it'
53     >>> locale_to_mozilla('ar_EG:XX')
54     Traceback (most recent call last):
55     ...
56     ValueError: The input format is invalid; you can't both have underscores and colons
57     >>> locale_to_mozilla('ar:EG')
58     'ar:EG'
59     """
60     if ":" in locale:
61         if "_" in locale:
62             raise ValueError(
63                 "The input format is invalid; you can't both have underscores and colons"
64             )
65         return locale
66     if "_" not in locale:
67         raise ValueError("country not specified in '%s'" % locale)
68     lang, country = locale.split("_")
69     return f"{lang}-{country.upper()}:{country.upper()}"
72 class ValidLocales:
73     """
74     This class fetches a list of all possible locale.
75     """
76     def __init__(self):
77         self.locales = self.parse_table(
78             requests.get("https://lh.2xlibre.net/locales/").text
79         )
80         self.languages = defaultdict(list)
81         for locale in self.locales:
82             self.languages[locale.split("_")[0]].append(locale)
84     def parse_table(self, body):
85         dom = BeautifulSoup(body, features="lxml")
86         ret = {}
87         for row in dom.find_all("tr"):
88             locale = row.select("td:first-child > a")[0].string
89             language = row.select("td:nth-child(2)")[0].string.strip("— ")
90             country = row.select("td:nth-child(3)")[0].string or ""
91             ret[locale] = (language, country.title())
92         return ret
95 class LocaleDescriptions:
96     def __init__(self):
97         self.n_errors = 0
98         self.languages_not_found = set()
99         self.po_to_mozilla = toml.load(open(MAPFILE))
101     def get_all_mozlocales(self, warnings=True):
102         yield from self.po_to_mozilla.get("extra", {}).get("extra_languages", [])
103         for po in sorted(Path("po/").glob("*.po")):
104             moz_locale = get_language(po)
106             if "_" in moz_locale:
107                 # See contribute/release_process/update_locale_descriptions#with-underscore
108                 lang, sub = moz_locale.split("_", maxsplit=1)
109                 yield f"{lang}-{sub}:{sub}"
110             elif moz_locale in self.po_to_mozilla["map"]:
111                 # We've already met this, and encoded it in po-to-mozilla.toml
112                 value = self.po_to_mozilla["map"][moz_locale]
113                 values = [value] if type(value) is str else value
114                 for locale in values:
115                     yield locale_to_mozilla(locale)
116             else:
117                 # It's probably a new language
118                 if warnings:
119                     print(
120                         f"Could not find {moz_locale} (from {po}), "
121                         f"please add it to {MAPFILE}",
122                         file=sys.stderr,
123                     )
124                 self.n_errors += 1
125                 self.languages_not_found.add(moz_locale)
127     def get_suggestions(self):
128         """
129         This encodes contribute/release_process/update_locale_descriptions#no-underscore
130         """
131         if not self.languages_not_found:
132             return
133         valid_locales = ValidLocales()
134         suggested_add = ""
135         others = ""
136         for lang in sorted(self.languages_not_found):
137             locales = valid_locales.languages[lang]
138             if len(locales) == 1:
139                 # If there is a single locale for this language, then it's a no-brainer
140                 locale = locales[0]
141                 details = ", ".join(valid_locales.locales[locale])
142                 suggested_add += f'{lang}="{locale_to_mozilla(locale)}"   # {details}\n'
143             else:
144                 # Otherwise, the RM must manually follow the process detailed in 
145                 # in contribute/release_process/update_locale_descriptions.mdwn
146                 others += f"{lang}: pick between\n"
147                 for locale in locales:
148                     details = ", ".join(valid_locales.locales[locale])
149                     others += f'  {lang}="{locale_to_mozilla(locale)}"   # {details}\n'
151         text = others
152         text += (
153             "\n\n## You can add the following block as-is,"
154             " but please verify it first!\n"
155         )
156         text += suggested_add
157         return text
160 def get_parser():
161     p = ArgumentParser()
162     p.set_defaults(mode="")
163     sub = p.add_subparsers()
164     generate = sub.add_parser("generate")
165     generate.set_defaults(mode="generate")
166     suggest = sub.add_parser("suggest")
167     suggest.set_defaults(mode="suggest")
168     doctest = sub.add_parser("doctest")
169     doctest.add_argument("-v", "--verbose", action="store_true", default=False)
170     doctest.set_defaults(mode="doctest")
172     return p
175 def main():
176     parser = get_parser()
177     args = parser.parse_args()
178     helper = LocaleDescriptions()
180     if args.mode == "":
181         print(parser.error("You need to specify a subcommand"))
183     mozlocales = list(
184         sorted(helper.get_all_mozlocales(warnings=(args.mode == "generate")))
185     )
186     if args.mode == "generate":
187         for out in mozlocales:
188             print(out)
189         sys.exit(0)
191     elif args.mode == "suggest":
192         if not helper.n_errors:
193             sys.exit(0)
194         suggestion = helper.get_suggestions()
195         print(suggestion, file=sys.stderr)
196         sys.exit(1)
198     elif args.mode == "doctest":
199         import doctest
201         doctest.testmod(verbose=args.verbose)
204 if __name__ == "__main__":
205     main()