summaryrefslogtreecommitdiffstats
path: root/clock
blob: acd8c558c9a8263e7fb194c4202ba154f6bce7d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/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"<error>Locale [{locale}] does not exist.</error>")
                continue

            self.line(f"<info>Generating <comment>{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()