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
13 from pathlib import Path
15 from collections import defaultdict
16 from argparse import ArgumentParser
22 from BeautifulSoup import BeautifulSoup
24 from bs4 import BeautifulSoup
27 LANGUAGE_RE = re.compile(r'^"Language:\s+(.*)\\n"$')
28 MAPFILE = "po/po-to-mozilla.toml"
31 def get_language(pofile: Path) -> str:
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).
38 for line in pofile.open():
39 match = LANGUAGE_RE.match(line)
42 raise ValueError(f"Could not extract language from file {pofile}")
45 def locale_to_mozilla(locale: str) -> str:
47 >>> locale_to_mozilla('ar_EG')
49 >>> locale_to_mozilla('it')
50 Traceback (most recent call last):
52 ValueError: country not specified in 'it'
53 >>> locale_to_mozilla('ar_EG:XX')
54 Traceback (most recent call last):
56 ValueError: The input format is invalid; you can't both have underscores and colons
57 >>> locale_to_mozilla('ar:EG')
63 "The input format is invalid; you can't both have underscores and colons"
67 raise ValueError("country not specified in '%s'" % locale)
68 lang, country = locale.split("_")
69 return f"{lang}-{country.upper()}:{country.upper()}"
74 This class fetches a list of all possible locale.
77 self.locales = self.parse_table(
78 requests.get("https://lh.2xlibre.net/locales/").text
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")
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())
95 class LocaleDescriptions:
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)
117 # It's probably a new language
120 f"Could not find {moz_locale} (from {po}), "
121 f"please add it to {MAPFILE}",
125 self.languages_not_found.add(moz_locale)
127 def get_suggestions(self):
129 This encodes contribute/release_process/update_locale_descriptions#no-underscore
131 if not self.languages_not_found:
133 valid_locales = ValidLocales()
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
141 details = ", ".join(valid_locales.locales[locale])
142 suggested_add += f'{lang}="{locale_to_mozilla(locale)}" # {details}\n'
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'
153 "\n\n## You can add the following block as-is,"
154 " but please verify it first!\n"
156 text += suggested_add
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")
176 parser = get_parser()
177 args = parser.parse_args()
178 helper = LocaleDescriptions()
181 print(parser.error("You need to specify a subcommand"))
184 sorted(helper.get_all_mozlocales(warnings=(args.mode == "generate")))
186 if args.mode == "generate":
187 for out in mozlocales:
191 elif args.mode == "suggest":
192 if not helper.n_errors:
194 suggestion = helper.get_suggestions()
195 print(suggestion, file=sys.stderr)
198 elif args.mode == "doctest":
201 doctest.testmod(verbose=args.verbose)
204 if __name__ == "__main__":