#!/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()