polyglot-python 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polyglot_python/__init__.py +24 -0
- polyglot_python/__main__.py +5 -0
- polyglot_python/cli.py +225 -0
- polyglot_python/locales.json +410 -0
- polyglot_python/translator.py +284 -0
- polyglot_python-0.2.0.dist-info/METADATA +177 -0
- polyglot_python-0.2.0.dist-info/RECORD +11 -0
- polyglot_python-0.2.0.dist-info/WHEEL +5 -0
- polyglot_python-0.2.0.dist-info/entry_points.txt +2 -0
- polyglot_python-0.2.0.dist-info/licenses/LICENSE +21 -0
- polyglot_python-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""polyglot-python: deterministic multilingual Python keyword transpiler."""
|
|
2
|
+
|
|
3
|
+
from .translator import (
|
|
4
|
+
Locale,
|
|
5
|
+
LocaleError,
|
|
6
|
+
TokenizeFailure,
|
|
7
|
+
Translation,
|
|
8
|
+
load_locales,
|
|
9
|
+
resolve_locale,
|
|
10
|
+
translate,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.2.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Locale",
|
|
17
|
+
"LocaleError",
|
|
18
|
+
"TokenizeFailure",
|
|
19
|
+
"Translation",
|
|
20
|
+
"__version__",
|
|
21
|
+
"load_locales",
|
|
22
|
+
"resolve_locale",
|
|
23
|
+
"translate",
|
|
24
|
+
]
|
polyglot_python/cli.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Command line interface for polyglot-python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from types import CodeType
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .translator import (
|
|
14
|
+
Locale,
|
|
15
|
+
TokenizeFailure,
|
|
16
|
+
Translation,
|
|
17
|
+
load_locales,
|
|
18
|
+
resolve_locale,
|
|
19
|
+
translate,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Failure(Exception):
|
|
24
|
+
def __init__(self, message: str) -> None:
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _configure_console() -> None:
|
|
30
|
+
for stream in (sys.stdout, sys.stderr):
|
|
31
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
32
|
+
if reconfigure is None:
|
|
33
|
+
continue
|
|
34
|
+
try:
|
|
35
|
+
reconfigure(encoding="utf-8", newline="\n")
|
|
36
|
+
except (ValueError, OSError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _message(locale: Locale | None, key: str, fallback: str) -> str:
|
|
41
|
+
if locale is None:
|
|
42
|
+
return fallback
|
|
43
|
+
return locale.messages.get(key, fallback)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_source(path: str) -> str:
|
|
47
|
+
try:
|
|
48
|
+
with open(path, "r", encoding="utf-8-sig", newline="") as handle:
|
|
49
|
+
return handle.read()
|
|
50
|
+
except UnicodeDecodeError as exc:
|
|
51
|
+
raise _Failure(
|
|
52
|
+
f"cannot decode {path} as UTF-8: {exc.reason} at byte {exc.start}"
|
|
53
|
+
) from exc
|
|
54
|
+
except OSError as exc:
|
|
55
|
+
reason = exc.strerror or str(exc)
|
|
56
|
+
raise _Failure(f"cannot read {path}: {reason}") from exc
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prepare(path: str, lang: str | None) -> tuple[Locale | None, Translation]:
|
|
60
|
+
text = _read_source(path)
|
|
61
|
+
code = resolve_locale(lang, text, path)
|
|
62
|
+
locale: Locale | None = None
|
|
63
|
+
if code is not None:
|
|
64
|
+
locales = load_locales()
|
|
65
|
+
locale = locales.get(code)
|
|
66
|
+
if locale is None:
|
|
67
|
+
available = ", ".join(sorted(locales))
|
|
68
|
+
raise _Failure(f"unknown locale {code!r}; available: {available}")
|
|
69
|
+
try:
|
|
70
|
+
translation = translate(text, locale)
|
|
71
|
+
except TokenizeFailure as exc:
|
|
72
|
+
prefix = _message(locale, "token_error", "Tokenization error")
|
|
73
|
+
raise _Failure(
|
|
74
|
+
f"{prefix}: {path}:{exc.line}:{exc.column}: {exc.detail}"
|
|
75
|
+
) from exc
|
|
76
|
+
return locale, translation
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _compile(
|
|
80
|
+
path: str, locale: Locale | None, translation: Translation
|
|
81
|
+
) -> CodeType | None:
|
|
82
|
+
try:
|
|
83
|
+
return compile(translation.python, path, "exec")
|
|
84
|
+
except SyntaxError as exc:
|
|
85
|
+
lineno = exc.lineno or 1
|
|
86
|
+
column = translation.original_column(lineno, exc.offset or 1)
|
|
87
|
+
prefix = _message(locale, "syntax_error", "Syntax error")
|
|
88
|
+
print(
|
|
89
|
+
f"{prefix}: {path}:{lineno}:{column}: {exc.msg}",
|
|
90
|
+
file=sys.stderr,
|
|
91
|
+
)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _run(namespace: argparse.Namespace) -> int:
|
|
96
|
+
try:
|
|
97
|
+
locale, translation = _prepare(namespace.file, namespace.lang)
|
|
98
|
+
except _Failure as exc:
|
|
99
|
+
print(f"polyglot-python: {exc.message}", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
code = _compile(namespace.file, locale, translation)
|
|
103
|
+
if code is None:
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
scope = {
|
|
107
|
+
"__name__": "__main__",
|
|
108
|
+
"__file__": os.path.abspath(namespace.file),
|
|
109
|
+
}
|
|
110
|
+
saved_argv = sys.argv
|
|
111
|
+
sys.argv = [namespace.file, *namespace.args]
|
|
112
|
+
try:
|
|
113
|
+
exec(code, scope)
|
|
114
|
+
except SystemExit as exc:
|
|
115
|
+
if exc.code is None:
|
|
116
|
+
return 0
|
|
117
|
+
if isinstance(exc.code, int):
|
|
118
|
+
return exc.code
|
|
119
|
+
print(exc.code, file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
raise
|
|
123
|
+
except Exception:
|
|
124
|
+
prefix = _message(locale, "runtime_error", "Runtime error")
|
|
125
|
+
print(f"{prefix}: {namespace.file}", file=sys.stderr)
|
|
126
|
+
traceback.print_exc()
|
|
127
|
+
return 1
|
|
128
|
+
finally:
|
|
129
|
+
sys.argv = saved_argv
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _check(namespace: argparse.Namespace) -> int:
|
|
134
|
+
try:
|
|
135
|
+
locale, translation = _prepare(namespace.file, namespace.lang)
|
|
136
|
+
except _Failure as exc:
|
|
137
|
+
print(f"polyglot-python: {exc.message}", file=sys.stderr)
|
|
138
|
+
return 1
|
|
139
|
+
return 0 if _compile(namespace.file, locale, translation) is not None else 1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _translate(namespace: argparse.Namespace) -> int:
|
|
143
|
+
try:
|
|
144
|
+
_, translation = _prepare(namespace.file, namespace.lang)
|
|
145
|
+
except _Failure as exc:
|
|
146
|
+
print(f"polyglot-python: {exc.message}", file=sys.stderr)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
if namespace.output:
|
|
150
|
+
try:
|
|
151
|
+
with open(namespace.output, "w", encoding="utf-8", newline="") as handle:
|
|
152
|
+
handle.write(translation.python)
|
|
153
|
+
except OSError as exc:
|
|
154
|
+
reason = exc.strerror or str(exc)
|
|
155
|
+
print(
|
|
156
|
+
f"polyglot-python: cannot write {namespace.output}: {reason}",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
159
|
+
return 1
|
|
160
|
+
else:
|
|
161
|
+
sys.stdout.write(translation.python)
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _locales(_namespace: argparse.Namespace) -> int:
|
|
166
|
+
locales = load_locales()
|
|
167
|
+
print(f"{'code':<6}{'name':<16}keywords")
|
|
168
|
+
for code in sorted(locales):
|
|
169
|
+
locale = locales[code]
|
|
170
|
+
print(f"{code:<6}{locale.name:<16}{len(locale.keywords)}")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
175
|
+
parser = argparse.ArgumentParser(
|
|
176
|
+
prog="polyglot-python",
|
|
177
|
+
description="Write Python in your own language: deterministic keyword translation.",
|
|
178
|
+
)
|
|
179
|
+
parser.add_argument(
|
|
180
|
+
"--version", action="version", version=f"polyglot-python {__version__}"
|
|
181
|
+
)
|
|
182
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
183
|
+
|
|
184
|
+
run = subparsers.add_parser("run", help="translate and execute a program")
|
|
185
|
+
run.add_argument("file", help="localized source file")
|
|
186
|
+
run.add_argument("--lang", metavar="CODE", help="locale code, e.g. tr")
|
|
187
|
+
run.add_argument(
|
|
188
|
+
"args",
|
|
189
|
+
nargs=argparse.REMAINDER,
|
|
190
|
+
help="arguments passed to the program",
|
|
191
|
+
)
|
|
192
|
+
run.set_defaults(handler=_run)
|
|
193
|
+
|
|
194
|
+
transform = subparsers.add_parser(
|
|
195
|
+
"translate", help="print the Python translation"
|
|
196
|
+
)
|
|
197
|
+
transform.add_argument("file", help="localized source file")
|
|
198
|
+
transform.add_argument("--lang", metavar="CODE", help="locale code, e.g. tr")
|
|
199
|
+
transform.add_argument(
|
|
200
|
+
"-o", "--output", metavar="PATH", help="write the translation to PATH"
|
|
201
|
+
)
|
|
202
|
+
transform.set_defaults(handler=_translate)
|
|
203
|
+
|
|
204
|
+
check = subparsers.add_parser(
|
|
205
|
+
"check", help="translate and compile a program without running it"
|
|
206
|
+
)
|
|
207
|
+
check.add_argument("file", help="localized source file")
|
|
208
|
+
check.add_argument("--lang", metavar="CODE", help="locale code, e.g. tr")
|
|
209
|
+
check.set_defaults(handler=_check)
|
|
210
|
+
|
|
211
|
+
listing = subparsers.add_parser("locales", help="list bundled locales")
|
|
212
|
+
listing.set_defaults(handler=_locales)
|
|
213
|
+
|
|
214
|
+
return parser
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
218
|
+
_configure_console()
|
|
219
|
+
parser = build_parser()
|
|
220
|
+
namespace = parser.parse_args(argv)
|
|
221
|
+
return namespace.handler(namespace)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
sys.exit(main())
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"locales": {
|
|
4
|
+
"tr": {
|
|
5
|
+
"name": "Türkçe",
|
|
6
|
+
"keywords": {
|
|
7
|
+
"False": "Yanlış",
|
|
8
|
+
"None": "Hiçbiri",
|
|
9
|
+
"True": "Doğru",
|
|
10
|
+
"and": "ve",
|
|
11
|
+
"as": "olarak",
|
|
12
|
+
"assert": "doğrula",
|
|
13
|
+
"async": "eşzamansız",
|
|
14
|
+
"await": "bekle",
|
|
15
|
+
"break": "kır",
|
|
16
|
+
"class": "sınıf",
|
|
17
|
+
"continue": "sürdür",
|
|
18
|
+
"def": "fonksiyon",
|
|
19
|
+
"del": "sil",
|
|
20
|
+
"elif": "değilse_eğer",
|
|
21
|
+
"else": "değilse",
|
|
22
|
+
"except": "yakala",
|
|
23
|
+
"finally": "sonunda",
|
|
24
|
+
"for": "için",
|
|
25
|
+
"from": "dan",
|
|
26
|
+
"global": "global",
|
|
27
|
+
"if": "eğer",
|
|
28
|
+
"import": "içe_aktar",
|
|
29
|
+
"in": "içinde",
|
|
30
|
+
"is": "aynı",
|
|
31
|
+
"lambda": "lambda",
|
|
32
|
+
"nonlocal": "dış",
|
|
33
|
+
"not": "değil",
|
|
34
|
+
"or": "veya",
|
|
35
|
+
"pass": "geç",
|
|
36
|
+
"raise": "yükselt",
|
|
37
|
+
"return": "döndür",
|
|
38
|
+
"try": "dene",
|
|
39
|
+
"while": "iken",
|
|
40
|
+
"with": "ile",
|
|
41
|
+
"yield": "üret"
|
|
42
|
+
},
|
|
43
|
+
"messages": {
|
|
44
|
+
"syntax_error": "Sözdizimi hatası",
|
|
45
|
+
"runtime_error": "Çalışma zamanı hatası",
|
|
46
|
+
"token_error": "Belirteç hatası"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"es": {
|
|
50
|
+
"name": "Español",
|
|
51
|
+
"keywords": {
|
|
52
|
+
"False": "Falso",
|
|
53
|
+
"None": "Ninguno",
|
|
54
|
+
"True": "Verdadero",
|
|
55
|
+
"and": "y",
|
|
56
|
+
"as": "como",
|
|
57
|
+
"assert": "afirmar",
|
|
58
|
+
"async": "asíncrono",
|
|
59
|
+
"await": "esperar",
|
|
60
|
+
"break": "romper",
|
|
61
|
+
"class": "clase",
|
|
62
|
+
"continue": "continuar",
|
|
63
|
+
"def": "función",
|
|
64
|
+
"del": "eliminar",
|
|
65
|
+
"elif": "sino_si",
|
|
66
|
+
"else": "sino",
|
|
67
|
+
"except": "excepto",
|
|
68
|
+
"finally": "finalmente",
|
|
69
|
+
"for": "para",
|
|
70
|
+
"from": "desde",
|
|
71
|
+
"global": "global",
|
|
72
|
+
"if": "si",
|
|
73
|
+
"import": "importar",
|
|
74
|
+
"in": "en",
|
|
75
|
+
"is": "es",
|
|
76
|
+
"lambda": "lambda",
|
|
77
|
+
"nonlocal": "no_local",
|
|
78
|
+
"not": "no",
|
|
79
|
+
"or": "o",
|
|
80
|
+
"pass": "pasar",
|
|
81
|
+
"raise": "lanzar",
|
|
82
|
+
"return": "devolver",
|
|
83
|
+
"try": "intentar",
|
|
84
|
+
"while": "mientras",
|
|
85
|
+
"with": "con",
|
|
86
|
+
"yield": "generar"
|
|
87
|
+
},
|
|
88
|
+
"messages": {
|
|
89
|
+
"syntax_error": "Error de sintaxis",
|
|
90
|
+
"runtime_error": "Error en tiempo de ejecución",
|
|
91
|
+
"token_error": "Error de tokenización"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"pt": {
|
|
95
|
+
"name": "Português",
|
|
96
|
+
"keywords": {
|
|
97
|
+
"False": "Falso",
|
|
98
|
+
"None": "Nenhum",
|
|
99
|
+
"True": "Verdadeiro",
|
|
100
|
+
"and": "e",
|
|
101
|
+
"as": "como",
|
|
102
|
+
"assert": "afirmar",
|
|
103
|
+
"async": "assíncrono",
|
|
104
|
+
"await": "aguardar",
|
|
105
|
+
"break": "quebrar",
|
|
106
|
+
"class": "classe",
|
|
107
|
+
"continue": "continuar",
|
|
108
|
+
"def": "função",
|
|
109
|
+
"del": "apagar",
|
|
110
|
+
"elif": "senão_se",
|
|
111
|
+
"else": "senão",
|
|
112
|
+
"except": "exceto",
|
|
113
|
+
"finally": "finalmente",
|
|
114
|
+
"for": "para",
|
|
115
|
+
"from": "de",
|
|
116
|
+
"global": "global",
|
|
117
|
+
"if": "se",
|
|
118
|
+
"import": "importar",
|
|
119
|
+
"in": "em",
|
|
120
|
+
"is": "é",
|
|
121
|
+
"lambda": "lambda",
|
|
122
|
+
"nonlocal": "não_local",
|
|
123
|
+
"not": "não",
|
|
124
|
+
"or": "ou",
|
|
125
|
+
"pass": "passar",
|
|
126
|
+
"raise": "levantar",
|
|
127
|
+
"return": "retornar",
|
|
128
|
+
"try": "tentar",
|
|
129
|
+
"while": "enquanto",
|
|
130
|
+
"with": "com",
|
|
131
|
+
"yield": "gerar"
|
|
132
|
+
},
|
|
133
|
+
"messages": {
|
|
134
|
+
"syntax_error": "Erro de sintaxe",
|
|
135
|
+
"runtime_error": "Erro de execução",
|
|
136
|
+
"token_error": "Erro de tokenização"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"de": {
|
|
140
|
+
"name": "Deutsch",
|
|
141
|
+
"keywords": {
|
|
142
|
+
"False": "Falsch",
|
|
143
|
+
"None": "Nichts",
|
|
144
|
+
"True": "Wahr",
|
|
145
|
+
"and": "und",
|
|
146
|
+
"as": "als",
|
|
147
|
+
"assert": "behaupten",
|
|
148
|
+
"async": "asynchron",
|
|
149
|
+
"await": "warten",
|
|
150
|
+
"break": "abbrechen",
|
|
151
|
+
"class": "klasse",
|
|
152
|
+
"continue": "fortsetzen",
|
|
153
|
+
"def": "funktion",
|
|
154
|
+
"del": "löschen",
|
|
155
|
+
"elif": "sonst_wenn",
|
|
156
|
+
"else": "sonst",
|
|
157
|
+
"except": "außer",
|
|
158
|
+
"finally": "schließlich",
|
|
159
|
+
"for": "für",
|
|
160
|
+
"from": "von",
|
|
161
|
+
"global": "global",
|
|
162
|
+
"if": "wenn",
|
|
163
|
+
"import": "importieren",
|
|
164
|
+
"in": "in",
|
|
165
|
+
"is": "ist",
|
|
166
|
+
"lambda": "lambda",
|
|
167
|
+
"nonlocal": "nichtlokal",
|
|
168
|
+
"not": "nicht",
|
|
169
|
+
"or": "oder",
|
|
170
|
+
"pass": "passieren",
|
|
171
|
+
"raise": "auslösen",
|
|
172
|
+
"return": "zurückgeben",
|
|
173
|
+
"try": "versuchen",
|
|
174
|
+
"while": "solange",
|
|
175
|
+
"with": "mit",
|
|
176
|
+
"yield": "erzeugen"
|
|
177
|
+
},
|
|
178
|
+
"messages": {
|
|
179
|
+
"syntax_error": "Syntaxfehler",
|
|
180
|
+
"runtime_error": "Laufzeitfehler",
|
|
181
|
+
"token_error": "Tokenisierungsfehler"
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"fr": {
|
|
185
|
+
"name": "Français",
|
|
186
|
+
"keywords": {
|
|
187
|
+
"False": "Faux",
|
|
188
|
+
"None": "Aucun",
|
|
189
|
+
"True": "Vrai",
|
|
190
|
+
"and": "et",
|
|
191
|
+
"as": "comme",
|
|
192
|
+
"assert": "affirmer",
|
|
193
|
+
"async": "asynchrone",
|
|
194
|
+
"await": "attendre",
|
|
195
|
+
"break": "casser",
|
|
196
|
+
"class": "classe",
|
|
197
|
+
"continue": "continuer",
|
|
198
|
+
"def": "fonction",
|
|
199
|
+
"del": "supprimer",
|
|
200
|
+
"elif": "sinon_si",
|
|
201
|
+
"else": "sinon",
|
|
202
|
+
"except": "excepté",
|
|
203
|
+
"finally": "finalement",
|
|
204
|
+
"for": "pour",
|
|
205
|
+
"from": "depuis",
|
|
206
|
+
"global": "global",
|
|
207
|
+
"if": "si",
|
|
208
|
+
"import": "importer",
|
|
209
|
+
"in": "dans",
|
|
210
|
+
"is": "est",
|
|
211
|
+
"lambda": "lambda",
|
|
212
|
+
"nonlocal": "non_local",
|
|
213
|
+
"not": "non",
|
|
214
|
+
"or": "ou",
|
|
215
|
+
"pass": "passer",
|
|
216
|
+
"raise": "lever",
|
|
217
|
+
"return": "retourner",
|
|
218
|
+
"try": "essayer",
|
|
219
|
+
"while": "tant_que",
|
|
220
|
+
"with": "avec",
|
|
221
|
+
"yield": "générer"
|
|
222
|
+
},
|
|
223
|
+
"messages": {
|
|
224
|
+
"syntax_error": "Erreur de syntaxe",
|
|
225
|
+
"runtime_error": "Erreur d'exécution",
|
|
226
|
+
"token_error": "Erreur de tokenisation"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"zh": {
|
|
230
|
+
"name": "中文",
|
|
231
|
+
"keywords": {
|
|
232
|
+
"False": "假",
|
|
233
|
+
"None": "空",
|
|
234
|
+
"True": "真",
|
|
235
|
+
"and": "并且",
|
|
236
|
+
"as": "作为",
|
|
237
|
+
"assert": "断言",
|
|
238
|
+
"async": "异步",
|
|
239
|
+
"await": "等待",
|
|
240
|
+
"break": "跳出",
|
|
241
|
+
"class": "类",
|
|
242
|
+
"continue": "继续",
|
|
243
|
+
"def": "函数",
|
|
244
|
+
"del": "删除",
|
|
245
|
+
"elif": "否则如果",
|
|
246
|
+
"else": "否则",
|
|
247
|
+
"except": "捕获",
|
|
248
|
+
"finally": "最终",
|
|
249
|
+
"for": "对于",
|
|
250
|
+
"from": "从",
|
|
251
|
+
"global": "全局",
|
|
252
|
+
"if": "如果",
|
|
253
|
+
"import": "导入",
|
|
254
|
+
"in": "在",
|
|
255
|
+
"is": "是",
|
|
256
|
+
"lambda": "匿名",
|
|
257
|
+
"nonlocal": "非局部",
|
|
258
|
+
"not": "不是",
|
|
259
|
+
"or": "或者",
|
|
260
|
+
"pass": "跳过",
|
|
261
|
+
"raise": "抛出",
|
|
262
|
+
"return": "返回",
|
|
263
|
+
"try": "尝试",
|
|
264
|
+
"while": "当",
|
|
265
|
+
"with": "使用",
|
|
266
|
+
"yield": "产出"
|
|
267
|
+
},
|
|
268
|
+
"messages": {
|
|
269
|
+
"syntax_error": "语法错误",
|
|
270
|
+
"runtime_error": "运行时错误",
|
|
271
|
+
"token_error": "分词错误"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"hi": {
|
|
275
|
+
"name": "हिन्दी",
|
|
276
|
+
"keywords": {
|
|
277
|
+
"False": "गलत",
|
|
278
|
+
"None": "कुछ_नहीं",
|
|
279
|
+
"True": "सही",
|
|
280
|
+
"and": "और",
|
|
281
|
+
"as": "के_रूप_में",
|
|
282
|
+
"assert": "दावा",
|
|
283
|
+
"async": "अतुल्यकालिक",
|
|
284
|
+
"await": "प्रतीक्षा",
|
|
285
|
+
"break": "तोड़ो",
|
|
286
|
+
"class": "वर्ग",
|
|
287
|
+
"continue": "जारी_रखो",
|
|
288
|
+
"def": "फलन",
|
|
289
|
+
"del": "हटाओ",
|
|
290
|
+
"elif": "अन्यथा_अगर",
|
|
291
|
+
"else": "अन्यथा",
|
|
292
|
+
"except": "पकड़ो",
|
|
293
|
+
"finally": "अंततः",
|
|
294
|
+
"for": "के_लिए",
|
|
295
|
+
"from": "से",
|
|
296
|
+
"global": "वैश्विक",
|
|
297
|
+
"if": "अगर",
|
|
298
|
+
"import": "आयात",
|
|
299
|
+
"in": "में",
|
|
300
|
+
"is": "है",
|
|
301
|
+
"lambda": "लैम्ब्डा",
|
|
302
|
+
"nonlocal": "स्थानीय_नहीं",
|
|
303
|
+
"not": "नहीं",
|
|
304
|
+
"or": "या",
|
|
305
|
+
"pass": "छोड़ो",
|
|
306
|
+
"raise": "उठाओ",
|
|
307
|
+
"return": "लौटाओ",
|
|
308
|
+
"try": "प्रयास",
|
|
309
|
+
"while": "जबकि",
|
|
310
|
+
"with": "के_साथ",
|
|
311
|
+
"yield": "उत्पन्न"
|
|
312
|
+
},
|
|
313
|
+
"messages": {
|
|
314
|
+
"syntax_error": "वाक्यविन्यास त्रुटि",
|
|
315
|
+
"runtime_error": "रनटाइम त्रुटि",
|
|
316
|
+
"token_error": "टोकन त्रुटि"
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
"ar": {
|
|
320
|
+
"name": "العربية",
|
|
321
|
+
"keywords": {
|
|
322
|
+
"False": "خطأ",
|
|
323
|
+
"None": "لا_شيء",
|
|
324
|
+
"True": "صحيح",
|
|
325
|
+
"and": "و",
|
|
326
|
+
"as": "بصفة",
|
|
327
|
+
"assert": "أكد",
|
|
328
|
+
"async": "غير_متزامن",
|
|
329
|
+
"await": "انتظر",
|
|
330
|
+
"break": "اكسر",
|
|
331
|
+
"class": "صنف",
|
|
332
|
+
"continue": "تابع",
|
|
333
|
+
"def": "دالة",
|
|
334
|
+
"del": "احذف",
|
|
335
|
+
"elif": "وإلا_إذا",
|
|
336
|
+
"else": "وإلا",
|
|
337
|
+
"except": "التقط",
|
|
338
|
+
"finally": "أخيرا",
|
|
339
|
+
"for": "لكل",
|
|
340
|
+
"from": "من",
|
|
341
|
+
"global": "عام",
|
|
342
|
+
"if": "إذا",
|
|
343
|
+
"import": "استورد",
|
|
344
|
+
"in": "في",
|
|
345
|
+
"is": "هو",
|
|
346
|
+
"lambda": "لامبدا",
|
|
347
|
+
"nonlocal": "غير_محلي",
|
|
348
|
+
"not": "ليس",
|
|
349
|
+
"or": "أو",
|
|
350
|
+
"pass": "مرر",
|
|
351
|
+
"raise": "ارفع",
|
|
352
|
+
"return": "إرجاع",
|
|
353
|
+
"try": "حاول",
|
|
354
|
+
"while": "بينما",
|
|
355
|
+
"with": "مع",
|
|
356
|
+
"yield": "أنتج"
|
|
357
|
+
},
|
|
358
|
+
"messages": {
|
|
359
|
+
"syntax_error": "خطأ في الصياغة",
|
|
360
|
+
"runtime_error": "خطأ في وقت التشغيل",
|
|
361
|
+
"token_error": "خطأ في الترميز"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"ru": {
|
|
365
|
+
"name": "Русский",
|
|
366
|
+
"keywords": {
|
|
367
|
+
"False": "Ложь",
|
|
368
|
+
"None": "Ничто",
|
|
369
|
+
"True": "Истина",
|
|
370
|
+
"and": "и",
|
|
371
|
+
"as": "как",
|
|
372
|
+
"assert": "утверждать",
|
|
373
|
+
"async": "асинхронный",
|
|
374
|
+
"await": "ожидать",
|
|
375
|
+
"break": "прервать",
|
|
376
|
+
"class": "класс",
|
|
377
|
+
"continue": "продолжить",
|
|
378
|
+
"def": "определить",
|
|
379
|
+
"del": "удалить",
|
|
380
|
+
"elif": "иначе_если",
|
|
381
|
+
"else": "иначе",
|
|
382
|
+
"except": "исключение",
|
|
383
|
+
"finally": "наконец",
|
|
384
|
+
"for": "для",
|
|
385
|
+
"from": "из",
|
|
386
|
+
"global": "глобальный",
|
|
387
|
+
"if": "если",
|
|
388
|
+
"import": "импорт",
|
|
389
|
+
"in": "в",
|
|
390
|
+
"is": "это",
|
|
391
|
+
"lambda": "лямбда",
|
|
392
|
+
"nonlocal": "нелокальный",
|
|
393
|
+
"not": "не",
|
|
394
|
+
"or": "или",
|
|
395
|
+
"pass": "пропустить",
|
|
396
|
+
"raise": "поднять",
|
|
397
|
+
"return": "вернуть",
|
|
398
|
+
"try": "попробовать",
|
|
399
|
+
"while": "пока",
|
|
400
|
+
"with": "с",
|
|
401
|
+
"yield": "генерировать"
|
|
402
|
+
},
|
|
403
|
+
"messages": {
|
|
404
|
+
"syntax_error": "Синтаксическая ошибка",
|
|
405
|
+
"runtime_error": "Ошибка выполнения",
|
|
406
|
+
"token_error": "Ошибка токенизации"
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Deterministic translation of localized Python keywords.
|
|
2
|
+
|
|
3
|
+
Only exact ``NAME`` tokens that match a locale keyword are rewritten; strings,
|
|
4
|
+
comments, f-strings, numbers and attribute names are never touched. Line
|
|
5
|
+
numbers are preserved exactly, and a per-line column map lets callers report
|
|
6
|
+
positions against the original localized source.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import keyword
|
|
14
|
+
import re
|
|
15
|
+
import tokenize
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from importlib import resources
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from types import MappingProxyType
|
|
21
|
+
from typing import Mapping
|
|
22
|
+
|
|
23
|
+
_SUFFIX_LOCALES = {
|
|
24
|
+
".tr": "tr",
|
|
25
|
+
".es": "es",
|
|
26
|
+
".pt": "pt",
|
|
27
|
+
".de": "de",
|
|
28
|
+
".fr": "fr",
|
|
29
|
+
".zh": "zh",
|
|
30
|
+
".hi": "hi",
|
|
31
|
+
".ar": "ar",
|
|
32
|
+
".ru": "ru",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_DIRECTIVE = re.compile(r"^#\s*polyglot-python:\s*([A-Za-z][A-Za-z0-9_-]*)\s*$")
|
|
36
|
+
|
|
37
|
+
_MESSAGE_KEYS = (
|
|
38
|
+
"syntax_error",
|
|
39
|
+
"runtime_error",
|
|
40
|
+
"token_error",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_SIGNIFICANT = (tokenize.NAME, tokenize.OP, tokenize.NUMBER, tokenize.STRING)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PolyglotError(Exception):
|
|
47
|
+
"""Base class for all polyglot-python errors."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LocaleError(PolyglotError):
|
|
51
|
+
"""Raised when locale data is malformed or a locale is unknown."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TokenizeFailure(PolyglotError):
|
|
55
|
+
"""Raised when the localized source cannot be tokenized."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, line: int, column: int, detail: str) -> None:
|
|
58
|
+
super().__init__(detail)
|
|
59
|
+
self.line = line
|
|
60
|
+
self.column = column
|
|
61
|
+
self.detail = detail
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Locale:
|
|
66
|
+
"""A validated locale: Python keyword -> localized keyword."""
|
|
67
|
+
|
|
68
|
+
code: str
|
|
69
|
+
name: str
|
|
70
|
+
keywords: Mapping[str, str]
|
|
71
|
+
messages: Mapping[str, str]
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def reverse_keywords(self) -> dict[str, str]:
|
|
75
|
+
return {local: python for python, local in self.keywords.items()}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Translation:
|
|
80
|
+
"""A translated program plus its original-source column maps."""
|
|
81
|
+
|
|
82
|
+
original: str
|
|
83
|
+
python: str
|
|
84
|
+
column_maps: tuple[tuple[int, ...], ...]
|
|
85
|
+
|
|
86
|
+
def original_column(self, lineno: int, offset: int) -> int:
|
|
87
|
+
"""Map a 1-based translated column back to a 1-based original column.
|
|
88
|
+
|
|
89
|
+
An empty map means the line was not touched, so the column is already
|
|
90
|
+
correct. Out-of-range positions are clamped to the line bounds.
|
|
91
|
+
"""
|
|
92
|
+
if not self.column_maps or lineno < 1:
|
|
93
|
+
return offset
|
|
94
|
+
index = min(lineno, len(self.column_maps)) - 1
|
|
95
|
+
column_map = self.column_maps[index]
|
|
96
|
+
if not column_map:
|
|
97
|
+
return offset
|
|
98
|
+
position = min(max(offset, 1), len(column_map)) - 1
|
|
99
|
+
return column_map[position] + 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _tokenizes_as_name(word: str) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(word).readline))
|
|
105
|
+
except (tokenize.TokenError, IndentationError):
|
|
106
|
+
return False
|
|
107
|
+
significant = [
|
|
108
|
+
token
|
|
109
|
+
for token in tokens
|
|
110
|
+
if token.type not in (tokenize.ENDMARKER, tokenize.NEWLINE, tokenize.NL)
|
|
111
|
+
]
|
|
112
|
+
return (
|
|
113
|
+
len(significant) == 1
|
|
114
|
+
and significant[0].type == tokenize.NAME
|
|
115
|
+
and significant[0].string == word
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _validate_keywords(code: str, raw: object) -> Mapping[str, str]:
|
|
120
|
+
if not isinstance(raw, dict):
|
|
121
|
+
raise LocaleError(f"{code}: keywords must be a JSON object")
|
|
122
|
+
expected = set(keyword.kwlist)
|
|
123
|
+
actual = set(raw)
|
|
124
|
+
if actual != expected:
|
|
125
|
+
missing = sorted(expected - actual)
|
|
126
|
+
extra = sorted(actual - expected)
|
|
127
|
+
raise LocaleError(
|
|
128
|
+
f"{code}: keywords must match Python keywords exactly "
|
|
129
|
+
f"(missing={missing}, extra={extra})"
|
|
130
|
+
)
|
|
131
|
+
seen: dict[str, str] = {}
|
|
132
|
+
for python_word in keyword.kwlist:
|
|
133
|
+
local = raw[python_word]
|
|
134
|
+
if not isinstance(local, str) or not local.isidentifier():
|
|
135
|
+
raise LocaleError(
|
|
136
|
+
f"{code}: {python_word!r} maps to an invalid identifier: {local!r}"
|
|
137
|
+
)
|
|
138
|
+
if keyword.iskeyword(local) and local != python_word:
|
|
139
|
+
raise LocaleError(
|
|
140
|
+
f"{code}: {python_word!r} maps to reserved Python keyword {local!r}"
|
|
141
|
+
)
|
|
142
|
+
if local in seen:
|
|
143
|
+
raise LocaleError(
|
|
144
|
+
f"{code}: duplicate localized keyword {local!r} "
|
|
145
|
+
f"for {seen[local]!r} and {python_word!r}"
|
|
146
|
+
)
|
|
147
|
+
if not _tokenizes_as_name(local):
|
|
148
|
+
raise LocaleError(
|
|
149
|
+
f"{code}: {python_word!r} maps to {local!r}, which this "
|
|
150
|
+
"interpreter does not tokenize as one identifier "
|
|
151
|
+
"(Python 3.12+ is required for Indic scripts)"
|
|
152
|
+
)
|
|
153
|
+
seen[local] = python_word
|
|
154
|
+
return MappingProxyType(dict(raw))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _validate_messages(code: str, raw: object) -> Mapping[str, str]:
|
|
158
|
+
if not isinstance(raw, dict):
|
|
159
|
+
raise LocaleError(f"{code}: messages must be a JSON object")
|
|
160
|
+
messages: dict[str, str] = {}
|
|
161
|
+
for key in _MESSAGE_KEYS:
|
|
162
|
+
value = raw.get(key)
|
|
163
|
+
if not isinstance(value, str) or not value:
|
|
164
|
+
raise LocaleError(f"{code}: missing message {key!r}")
|
|
165
|
+
messages[key] = value
|
|
166
|
+
return MappingProxyType(messages)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@lru_cache(maxsize=1)
|
|
170
|
+
def load_locales() -> Mapping[str, Locale]:
|
|
171
|
+
"""Load and validate every bundled locale."""
|
|
172
|
+
raw_text = resources.files("polyglot_python").joinpath("locales.json").read_text(
|
|
173
|
+
encoding="utf-8"
|
|
174
|
+
)
|
|
175
|
+
return _parse_locales(raw_text)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_locales(raw_text: str) -> Mapping[str, Locale]:
|
|
179
|
+
data = json.loads(raw_text)
|
|
180
|
+
if not isinstance(data, dict) or not isinstance(data.get("locales"), dict):
|
|
181
|
+
raise LocaleError("locales.json: expected an object with a 'locales' object")
|
|
182
|
+
locales: dict[str, Locale] = {}
|
|
183
|
+
for code, entry in data["locales"].items():
|
|
184
|
+
if not isinstance(entry, dict):
|
|
185
|
+
raise LocaleError(f"{code}: locale entry must be a JSON object")
|
|
186
|
+
name = entry.get("name")
|
|
187
|
+
if not isinstance(name, str) or not name:
|
|
188
|
+
raise LocaleError(f"{code}: missing locale name")
|
|
189
|
+
locales[code] = Locale(
|
|
190
|
+
code=code,
|
|
191
|
+
name=name,
|
|
192
|
+
keywords=_validate_keywords(code, entry.get("keywords")),
|
|
193
|
+
messages=_validate_messages(code, entry.get("messages")),
|
|
194
|
+
)
|
|
195
|
+
return MappingProxyType(locales)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def resolve_locale(
|
|
199
|
+
explicit: str | None, text: str, filename: str | None = None
|
|
200
|
+
) -> str | None:
|
|
201
|
+
"""Resolve a locale code: --lang, then ``# polyglot-python: xx``, then suffix."""
|
|
202
|
+
if explicit:
|
|
203
|
+
return explicit.strip().lower()
|
|
204
|
+
first_line = text.split("\n", 1)[0].strip()
|
|
205
|
+
match = _DIRECTIVE.match(first_line)
|
|
206
|
+
if match:
|
|
207
|
+
return match.group(1).lower()
|
|
208
|
+
if filename:
|
|
209
|
+
suffix = Path(filename).suffix.lower()
|
|
210
|
+
if suffix in _SUFFIX_LOCALES:
|
|
211
|
+
return _SUFFIX_LOCALES[suffix]
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def translate(text: str, locale: Locale | None) -> Translation:
|
|
216
|
+
"""Translate localized keywords to Python keywords, deterministically."""
|
|
217
|
+
original = text
|
|
218
|
+
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
219
|
+
if locale is None:
|
|
220
|
+
return Translation(
|
|
221
|
+
original=original,
|
|
222
|
+
python=normalized,
|
|
223
|
+
column_maps=tuple(() for _ in normalized.split("\n")),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
reverse = locale.reverse_keywords
|
|
227
|
+
try:
|
|
228
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(normalized).readline))
|
|
229
|
+
except tokenize.TokenError as exc:
|
|
230
|
+
detail = str(exc.args[0]) if exc.args else "tokenization failed"
|
|
231
|
+
position = exc.args[1] if len(exc.args) > 1 else (1, 0)
|
|
232
|
+
raise TokenizeFailure(
|
|
233
|
+
int(position[0]), int(position[1]) + 1, detail
|
|
234
|
+
) from exc
|
|
235
|
+
except IndentationError as exc:
|
|
236
|
+
raise TokenizeFailure(exc.lineno or 1, exc.offset or 1, str(exc)) from exc
|
|
237
|
+
|
|
238
|
+
replacements: dict[int, list[tuple[int, int, str]]] = {}
|
|
239
|
+
previous: tokenize.TokenInfo | None = None
|
|
240
|
+
for token in tokens:
|
|
241
|
+
after_dot = (
|
|
242
|
+
previous is not None
|
|
243
|
+
and previous.type == tokenize.OP
|
|
244
|
+
and previous.string == "."
|
|
245
|
+
)
|
|
246
|
+
if (
|
|
247
|
+
token.type == tokenize.NAME
|
|
248
|
+
and not after_dot
|
|
249
|
+
and token.string in reverse
|
|
250
|
+
):
|
|
251
|
+
row, column = token.start
|
|
252
|
+
replacements.setdefault(row, []).append(
|
|
253
|
+
(column, token.end[1], reverse[token.string])
|
|
254
|
+
)
|
|
255
|
+
if token.type in _SIGNIFICANT:
|
|
256
|
+
previous = token
|
|
257
|
+
|
|
258
|
+
out_lines: list[str] = []
|
|
259
|
+
column_maps: list[tuple[int, ...]] = []
|
|
260
|
+
for lineno, line in enumerate(normalized.split("\n"), 1):
|
|
261
|
+
edits = sorted(replacements.get(lineno, ()))
|
|
262
|
+
if not edits:
|
|
263
|
+
out_lines.append(line)
|
|
264
|
+
column_maps.append(())
|
|
265
|
+
continue
|
|
266
|
+
parts: list[str] = []
|
|
267
|
+
column_map: list[int] = []
|
|
268
|
+
position = 0
|
|
269
|
+
for start, end, replacement in edits:
|
|
270
|
+
parts.append(line[position:start])
|
|
271
|
+
column_map.extend(range(position, start))
|
|
272
|
+
parts.append(replacement)
|
|
273
|
+
column_map.extend([start] * len(replacement))
|
|
274
|
+
position = end
|
|
275
|
+
parts.append(line[position:])
|
|
276
|
+
column_map.extend(range(position, len(line)))
|
|
277
|
+
out_lines.append("".join(parts))
|
|
278
|
+
column_maps.append(tuple(column_map))
|
|
279
|
+
|
|
280
|
+
return Translation(
|
|
281
|
+
original=original,
|
|
282
|
+
python="\n".join(out_lines),
|
|
283
|
+
column_maps=tuple(column_maps),
|
|
284
|
+
)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyglot-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Deterministic multilingual Python keyword transpiler: write Python in your own language.
|
|
5
|
+
Author: Furox-Art
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Furox-Art/polyglot-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Furox-Art/polyglot-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Furox-Art/polyglot-python/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: python,transpiler,localization,multilingual,deterministic
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# polyglot-python
|
|
26
|
+
|
|
27
|
+
Write Python in your own language, deterministically.
|
|
28
|
+
|
|
29
|
+
polyglot-python is a keyword transpiler for Python. You keep Python's grammar and write
|
|
30
|
+
programs with localized keywords; polyglot-python maps those keywords to the real Python
|
|
31
|
+
keywords and hands the result to CPython. No AI, no network, no randomness:
|
|
32
|
+
the same source, locale and version always produce byte-identical Python.
|
|
33
|
+
|
|
34
|
+
It is not a prose-to-code translator and it does not guess intent. It is Python
|
|
35
|
+
syntax with your words.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install polyglot-python # after the first release
|
|
41
|
+
pip install -e . # from source
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first release is prepared but not yet on PyPI; install from source until it
|
|
45
|
+
is published. Requires Python 3.12+. Python 3.10/3.11 are not supported because
|
|
46
|
+
their tokenizer splits Indic combining marks, so several Hindi keywords would
|
|
47
|
+
not translate. No runtime dependencies.
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
`examples/classify.tr`:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
fonksiyon classify(n):
|
|
55
|
+
eğer n % 15 == 0:
|
|
56
|
+
döndür "FizzBuzz"
|
|
57
|
+
değilse_eğer n % 3 == 0:
|
|
58
|
+
döndür "Fizz"
|
|
59
|
+
değilse_eğer n % 5 == 0:
|
|
60
|
+
döndür "Buzz"
|
|
61
|
+
değilse:
|
|
62
|
+
döndür str(n)
|
|
63
|
+
|
|
64
|
+
için n içinde range(1, 16):
|
|
65
|
+
print(classify(n))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
polyglot-python run examples/classify.tr
|
|
70
|
+
polyglot-python check examples/classify.tr
|
|
71
|
+
polyglot-python translate examples/classify.tr
|
|
72
|
+
polyglot-python translate examples/classify.tr -o classify.py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same FizzBuzz program is included for every locale under `examples/`.
|
|
76
|
+
|
|
77
|
+
## Locales
|
|
78
|
+
|
|
79
|
+
| Code | Language | `if` | `def` | `return` |
|
|
80
|
+
|------|------------|----------|-------------|-----------|
|
|
81
|
+
| tr | Türkçe | `eğer` | `fonksiyon` | `döndür` |
|
|
82
|
+
| es | Español | `si` | `función` | `devolver`|
|
|
83
|
+
| pt | Português | `se` | `função` | `retornar`|
|
|
84
|
+
| de | Deutsch | `wenn` | `funktion` | `zurückgeben` |
|
|
85
|
+
| fr | Français | `si` | `fonction` | `retourner` |
|
|
86
|
+
| zh | 中文 | `如果` | `函数` | `返回` |
|
|
87
|
+
| hi | हिन्दी | `अगर` | `फलन` | `लौटाओ` |
|
|
88
|
+
| ar | العربية | `إذا` | `دالة` | `إرجاع` |
|
|
89
|
+
| ru | Русский | `если` | `определить`| `вернуть` |
|
|
90
|
+
|
|
91
|
+
`polyglot-python locales` prints the full list. Every locale defines exactly the 35
|
|
92
|
+
Python hard keywords (`keyword.kwlist`); soft keywords such as `match`, `case`
|
|
93
|
+
and `type` stay English.
|
|
94
|
+
|
|
95
|
+
Locale selection order:
|
|
96
|
+
|
|
97
|
+
1. `--lang CODE`
|
|
98
|
+
2. first line directive: `# polyglot-python: tr`
|
|
99
|
+
3. file suffix: `.tr`, `.es`, `.pt`, `.de`, `.fr`, `.zh`, `.hi`, `.ar`, `.ru`
|
|
100
|
+
|
|
101
|
+
## CLI
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
polyglot-python run FILE [--lang CODE] [args...]
|
|
105
|
+
polyglot-python check FILE [--lang CODE]
|
|
106
|
+
polyglot-python translate FILE [--lang CODE] [-o OUT]
|
|
107
|
+
polyglot-python locales
|
|
108
|
+
polyglot-python --version
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For `run`, place options before `FILE`; everything after `FILE` is passed to the
|
|
112
|
+
program as `sys.argv[1:]`.
|
|
113
|
+
|
|
114
|
+
## Guarantees
|
|
115
|
+
|
|
116
|
+
- Only exact `NAME` tokens that match a locale keyword are rewritten. Strings,
|
|
117
|
+
comments, f-strings, numbers, identifiers and attribute names are untouched.
|
|
118
|
+
- Line numbers are preserved one-to-one; a per-line column map translates
|
|
119
|
+
reported error positions back to the localized source.
|
|
120
|
+
- `translate -o` and stdout output are UTF-8 with LF line endings on every
|
|
121
|
+
platform, so exports are byte-identical and re-verifiable.
|
|
122
|
+
- `check` translates and compiles without executing the program, so localized
|
|
123
|
+
sources can be validated safely.
|
|
124
|
+
- A localized keyword after a dot is treated as an attribute name and never
|
|
125
|
+
rewritten.
|
|
126
|
+
- Localized keywords are reserved words in their locale.
|
|
127
|
+
- Missing files, invalid UTF-8, unknown locales and untokenizable input fail
|
|
128
|
+
with a clear message; nothing is guessed or silently skipped.
|
|
129
|
+
- Error headers are localized; CPython's detail text (for example
|
|
130
|
+
`ZeroDivisionError`) remains English for now.
|
|
131
|
+
- Exit codes from the executed program are propagated.
|
|
132
|
+
|
|
133
|
+
## Limits
|
|
134
|
+
|
|
135
|
+
- Phase 1 localizes keywords only: no localization of builtins (`print`,
|
|
136
|
+
`range`, ...), the standard library, or English exception text.
|
|
137
|
+
- This is not a natural-language compiler. It does not understand Spanish or
|
|
138
|
+
Turkish prose; it understands Python syntax written with localized keywords.
|
|
139
|
+
- The local prototype ships 9 locales. Other languages can be added by editing
|
|
140
|
+
`src/polyglot_python/locales.json`; malformed tables are rejected at load time.
|
|
141
|
+
- Tested on CPython 3.12 on Windows; CI covers 3.12-3.14 on Linux and 3.12
|
|
142
|
+
on Windows and macOS. The code targets 3.12+.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python -m venv .venv
|
|
148
|
+
.venv/Scripts/pip install -e ".[dev]"
|
|
149
|
+
.venv/Scripts/python -m pytest -q --cov=polyglot_python --cov-fail-under=100
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The suite contains 119 tests with 100% statement coverage, including every
|
|
153
|
+
keyword in both directions for all 9 locales, end-to-end CLI runs of every
|
|
154
|
+
example, version consistency with the changelog, and byte-level determinism
|
|
155
|
+
checks. CI runs Ubuntu 3.12-3.14 plus Windows and macOS at 3.12, enforces the
|
|
156
|
+
100% coverage gate on Linux 3.12, translates every example twice on all three
|
|
157
|
+
operating systems to compare bytes and line counts, and builds the wheel,
|
|
158
|
+
installs it into a clean environment, and smoke-tests it. Dependabot keeps
|
|
159
|
+
GitHub Actions and development dependencies current. A separate release
|
|
160
|
+
workflow publishes tagged GitHub Releases to PyPI through Trusted Publishing.
|
|
161
|
+
|
|
162
|
+
## Releasing
|
|
163
|
+
|
|
164
|
+
Publishing uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
|
|
165
|
+
and stores no API token.
|
|
166
|
+
|
|
167
|
+
1. On PyPI, add a pending publisher for project `polyglot-python`: owner
|
|
168
|
+
`Furox-Art`, repository `polyglot-python`, workflow `release.yml`,
|
|
169
|
+
environment `pypi`.
|
|
170
|
+
2. Change `version` in `pyproject.toml` and
|
|
171
|
+
`src/polyglot_python/__init__.py` together; the test suite rejects drift.
|
|
172
|
+
3. Publish a GitHub Release with tag `v<version>`. The release workflow verifies
|
|
173
|
+
the tag, builds the sdist and wheel, and uploads them to PyPI.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
polyglot_python/__init__.py,sha256=Ya4xPYS4dgX6dnB-vEdNpmPz26NaVnBVGZBRmyIF1f4,417
|
|
2
|
+
polyglot_python/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
polyglot_python/cli.py,sha256=J3KzlL5rDe3JDDXJe0LEgfczEEHXV95w0yxlrOQ10Ow,6906
|
|
4
|
+
polyglot_python/locales.json,sha256=jUMWGLZYG_BSEfKTevkJ1HS7LkZNDL2wjHfufLVyGkA,11356
|
|
5
|
+
polyglot_python/translator.py,sha256=608uk99H70f83qJ0Lhbp2F7covS6BsIB70UfSqJNGao,9455
|
|
6
|
+
polyglot_python-0.2.0.dist-info/licenses/LICENSE,sha256=GIA7odPA7v0zSqQfgBW_YoyTsNu058_DxugfvrwRoiM,1066
|
|
7
|
+
polyglot_python-0.2.0.dist-info/METADATA,sha256=9et5Xt-QeGkKmOtDkP1JWevZH6LAMaQD-27pEFbwk9s,7020
|
|
8
|
+
polyglot_python-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
polyglot_python-0.2.0.dist-info/entry_points.txt,sha256=Ovsh-hpTr6d0kZX8woBOJR9F5JStN12irNBomgvA_q4,61
|
|
10
|
+
polyglot_python-0.2.0.dist-info/top_level.txt,sha256=gEuO6xZPIIHdeSV92QIEWohn5QJCug2Jilw4x7ldF9E,16
|
|
11
|
+
polyglot_python-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furox-Art
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
polyglot_python
|