#!/usr/bin/env python from __future__ import annotations import glob import json import os from babel.core import get_global from babel.dates import PATTERN_CHARS from babel.dates import tokenize_pattern from babel.localedata import LocaleDataDict from babel.localedata import load from babel.localedata import normalize_locale from babel.plural import PluralRule from babel.plural import _binary_compiler from babel.plural import _GettextCompiler from babel.plural import _unary_compiler from babel.plural import compile_zero from cleo.application import Application from cleo.commands.command import Command from cleo.helpers import argument from pendulum import __version__ class _LambdaCompiler(_GettextCompiler): """Compiles the expression to lambda function.""" compile_v = compile_zero compile_w = compile_zero compile_f = compile_zero compile_t = compile_zero compile_and = _binary_compiler("(%s and %s)") compile_or = _binary_compiler("(%s or %s)") compile_not = _unary_compiler("(not %s)") compile_mod = _binary_compiler("(%s %% %s)") def compile_relation(self, method, expr, range_list): code = _GettextCompiler.compile_relation(self, method, expr, range_list) code = code.replace("&&", "and") code = code.replace("||", "or") if method == "in": expr = self.compile(expr) code = f"({expr} == {expr} and {code})" return code class LocaleCreate(Command): name = "locale create" description = "Creates locale translations." arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)] TEMPLATE = """from .custom import translations as custom_translations \"\"\" {locale} locale file. It has been generated automatically and must not be modified directly. \"\"\" locale = {{ 'plural': {plural}, 'ordinal': {ordinal}, 'translations': {translations}, 'custom': custom_translations }} """ CUSTOM_TEMPLATE = """\"\"\" {locale} custom locale file. \"\"\" translations = {{}} """ LOCALE_DIR = os.path.join("pendulum", "locales") def handle(self): locales = self.argument("locales") if not locales: return for locale in locales: data = {} parts = locale.split("-") if len(parts) > 1: parts[1] = parts[1].upper() normalized = normalize_locale(locale.replace("-", "_")) if not normalized: self.line(f"Locale [{locale}] does not exist.") continue self.line(f"Generating {locale} locale.") content = LocaleDataDict(load(normalized)) # Pluralization rule rule = content["plural_form"] plural = self.plural_rule_to_lambda(rule) # Ordinal rule rule = content["ordinal_form"] ordinal = self.plural_rule_to_lambda(rule) # Getting days names days = content["days"]["format"] data["days"] = {} for fmt, names in days.items(): data["days"][fmt] = {} for value, name in sorted(names.items()): data["days"][fmt][value] = name # Getting months names months = content["months"]["format"] data["months"] = months # Units patterns = content["unit_patterns"] units = [ "year", "month", "week", "day", "hour", "minute", "second", "microsecond", ] data["units"] = {} for unit in units: pattern = patterns[f"duration-{unit}"]["long"] if "per" in pattern: del pattern["per"] data["units"][unit] = pattern # Relative data["relative"] = {} for key in content["date_fields"]: if key not in [ "year", "month", "week", "day", "hour", "minute", "second", ]: continue data["relative"][key] = content["date_fields"][key] # Day periods data["day_periods"] = content["day_periods"]["format"]["wide"] # Week data data["week_data"] = content["week_data"] result = self.TEMPLATE.format( locale=locale, plural=plural, ordinal=ordinal, translations=self.format_dict(data, tab=2), ) dest_dir = os.path.join(self.LOCALE_DIR, locale.replace("-", "_")) if not os.path.exists(dest_dir): os.mkdir(dest_dir) init = os.path.join(dest_dir, "__init__.py") main = os.path.join(dest_dir, "locale.py") custom = os.path.join(dest_dir, "custom.py") if not os.path.exists(init): with open(init, "w"): os.utime(init) with open(main, "w") as fw: fw.write(result) if not os.path.exists(custom): with open(custom, "w") as fw: fw.write(self.CUSTOM_TEMPLATE.format(locale=locale)) def format_dict(self, d, tab=1): s = ["{\n"] for k, v in d.items(): if isinstance(v, (dict, LocaleDataDict)): v = self.format_dict(v, tab + 1) else: v = repr(v) s.append(f"{' ' * tab}{k!r}: {v},\n") s.append(f'{" " * (tab - 1)}}}') return "".join(s) def plural_rule_to_lambda(self, rule): to_py = _LambdaCompiler().compile result = ["lambda n: "] for tag, ast in PluralRule.parse(rule).abstract: result.append(f"'{tag}' if {to_py(ast)} else ") result.append("'other'") return "".join(result) def convert_ldml_format(self, fmt): result = [] for tok_type, tok_value in tokenize_pattern(fmt): if tok_type == "chars": result.append(tok_value.replace("%", "%%")) elif tok_type == "field": fieldchar, fieldnum = tok_value limit = PATTERN_CHARS[fieldchar] if limit and fieldnum not in limit: raise ValueError( f"Invalid length for field: {(fieldchar * fieldnum)!r}" ) result.append( self.TOKENS_MAP.get(fieldchar * fieldnum, fieldchar * fieldnum) ) else: raise NotImplementedError(f"Unknown token type: {tok_type}") return "".join(result) class LocaleRecreate(Command): name = "locale recreate" description = "Recreate existing locales." def handle(self): # Listing locales locales_dir = os.path.join("pendulum", "locales") locales = glob.glob(os.path.join(locales_dir, "*", "locale.py")) locales = [os.path.basename(os.path.dirname(locale)) for locale in locales] self.call("locale create", "locales " + " ".join(locales)) class WindowsTzDump(Command): name = "windows dump-timezones" description = "Dumps the mapping of Windows timezones to IANA timezones." MAPPING_DIR = os.path.join("pendulum", "tz", "data") def handle(self): raw_tznames = get_global("windows_zone_mapping") sorted_names = sorted(raw_tznames.keys()) tznames = {} for name in sorted_names: tznames[name] = raw_tznames[name] mapping = json.dumps(tznames, indent=4).replace('"', "'") with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f: f.write(f"windows_timezones = {mapping}\n") app = Application("clock", __version__) app.add(LocaleCreate()) app.add(LocaleRecreate()) app.add(WindowsTzDump()) if __name__ == "__main__": app.run()