FTL-Extract 0.9.0__py3-none-win_amd64.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.
- ftl_extract/__init__.py +4 -0
- ftl_extract/__version__.py +3 -0
- ftl_extract/cli.py +198 -0
- ftl_extract/code_extractor.py +209 -0
- ftl_extract/const.py +29 -0
- ftl_extract/exceptions.py +70 -0
- ftl_extract/ftl_extractor.py +219 -0
- ftl_extract/ftl_importer.py +89 -0
- ftl_extract/matcher.py +233 -0
- ftl_extract/process/__init__.py +0 -0
- ftl_extract/process/commentator.py +13 -0
- ftl_extract/process/kwargs_extractor.py +178 -0
- ftl_extract/process/serializer.py +34 -0
- ftl_extract/py.typed +0 -0
- ftl_extract/stub/__init__.py +0 -0
- ftl_extract/stub/generator.py +354 -0
- ftl_extract/stub/tree.py +46 -0
- ftl_extract/stub/utils.py +16 -0
- ftl_extract/stub/visitor.py +169 -0
- ftl_extract/utils.py +31 -0
- ftl_extract-0.9.0.data/scripts/fast-ftl-extract.exe +0 -0
- ftl_extract-0.9.0.dist-info/METADATA +190 -0
- ftl_extract-0.9.0.dist-info/RECORD +27 -0
- ftl_extract-0.9.0.dist-info/WHEEL +5 -0
- ftl_extract-0.9.0.dist-info/entry_points.txt +2 -0
- ftl_extract-0.9.0.dist-info/licenses/LICENSE +21 -0
- ftl_extract-0.9.0.dist-info/top_level.txt +1 -0
ftl_extract/__init__.py
ADDED
ftl_extract/cli.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from time import perf_counter_ns
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ftl_extract.const import (
|
|
10
|
+
COMMENT_KEYS_MODE,
|
|
11
|
+
DEFAULT_EXCLUDE_DIRS,
|
|
12
|
+
DEFAULT_FTL_FILE,
|
|
13
|
+
DEFAULT_I18N_KEYS,
|
|
14
|
+
DEFAULT_IGNORE_ATTRIBUTES,
|
|
15
|
+
DEFAULT_IGNORE_KWARGS,
|
|
16
|
+
)
|
|
17
|
+
from ftl_extract.ftl_extractor import extract
|
|
18
|
+
from ftl_extract.stub.generator import generate_stubs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group("ftl")
|
|
22
|
+
@click.version_option()
|
|
23
|
+
def ftl() -> None: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@ftl.command("extract")
|
|
27
|
+
@click.argument("code_path", type=click.Path(exists=True, path_type=Path))
|
|
28
|
+
@click.argument("output_path", type=click.Path(path_type=Path))
|
|
29
|
+
@click.option(
|
|
30
|
+
"--language",
|
|
31
|
+
"-l",
|
|
32
|
+
multiple=True,
|
|
33
|
+
default=("en",),
|
|
34
|
+
show_default=True,
|
|
35
|
+
help="Language of translation.",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--i18n-keys",
|
|
39
|
+
"-k",
|
|
40
|
+
default=DEFAULT_I18N_KEYS,
|
|
41
|
+
multiple=True,
|
|
42
|
+
show_default=True,
|
|
43
|
+
help="Names of function that is used to get translation.",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--i18n-keys-append",
|
|
47
|
+
"-K",
|
|
48
|
+
default=(),
|
|
49
|
+
multiple=True,
|
|
50
|
+
help="Append names of function that is used to get translation.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--i18n-keys-prefix",
|
|
54
|
+
"-p",
|
|
55
|
+
default=(),
|
|
56
|
+
multiple=True,
|
|
57
|
+
help="Prefix names of function that is used to get translation. `self.i18n.*()`",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--exclude-dirs",
|
|
61
|
+
"-e",
|
|
62
|
+
multiple=True,
|
|
63
|
+
default=DEFAULT_EXCLUDE_DIRS,
|
|
64
|
+
show_default=True,
|
|
65
|
+
help="Exclude directories.",
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--exclude-dirs-append",
|
|
69
|
+
"-E",
|
|
70
|
+
default=(),
|
|
71
|
+
multiple=True,
|
|
72
|
+
help="Append directories to exclude.",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--ignore-attributes",
|
|
76
|
+
"-i",
|
|
77
|
+
default=DEFAULT_IGNORE_ATTRIBUTES,
|
|
78
|
+
multiple=True,
|
|
79
|
+
show_default=True,
|
|
80
|
+
help="Ignore attributes, like `i18n.set_locale`.",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--append-ignore-attributes",
|
|
84
|
+
"-I",
|
|
85
|
+
multiple=True,
|
|
86
|
+
help="Append attributes to ignore.",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--ignore-kwargs",
|
|
90
|
+
default=DEFAULT_IGNORE_KWARGS,
|
|
91
|
+
multiple=True,
|
|
92
|
+
show_default=True,
|
|
93
|
+
help="Ignore kwargs, like `when` from `aiogram_dialog.I18nFormat(..., when=...)`.",
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--comment-junks",
|
|
97
|
+
is_flag=True,
|
|
98
|
+
default=False,
|
|
99
|
+
show_default=True,
|
|
100
|
+
help="Comments Junk elements.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--default-ftl-file",
|
|
104
|
+
default=DEFAULT_FTL_FILE,
|
|
105
|
+
show_default=True,
|
|
106
|
+
type=click.Path(path_type=Path),
|
|
107
|
+
)
|
|
108
|
+
@click.option(
|
|
109
|
+
"--comment-keys-mode",
|
|
110
|
+
default=COMMENT_KEYS_MODE[0],
|
|
111
|
+
show_default=True,
|
|
112
|
+
help="Comment keys mode.",
|
|
113
|
+
type=click.Choice(COMMENT_KEYS_MODE, case_sensitive=False),
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--dry-run",
|
|
117
|
+
is_flag=True,
|
|
118
|
+
default=False,
|
|
119
|
+
show_default=True,
|
|
120
|
+
help="Do not write to output files.",
|
|
121
|
+
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--verbose",
|
|
124
|
+
"-v",
|
|
125
|
+
is_flag=True,
|
|
126
|
+
default=False,
|
|
127
|
+
show_default=True,
|
|
128
|
+
help="Verbose output.",
|
|
129
|
+
)
|
|
130
|
+
def cli_extract(
|
|
131
|
+
code_path: Path,
|
|
132
|
+
output_path: Path,
|
|
133
|
+
language: tuple[str, ...],
|
|
134
|
+
i18n_keys: tuple[str, ...],
|
|
135
|
+
i18n_keys_append: tuple[str, ...],
|
|
136
|
+
i18n_keys_prefix: tuple[str, ...],
|
|
137
|
+
exclude_dirs: tuple[str, ...],
|
|
138
|
+
exclude_dirs_append: tuple[str, ...],
|
|
139
|
+
ignore_attributes: tuple[str, ...],
|
|
140
|
+
append_ignore_attributes: tuple[str, ...],
|
|
141
|
+
ignore_kwargs: tuple[str, ...],
|
|
142
|
+
comment_junks: bool,
|
|
143
|
+
default_ftl_file: Path,
|
|
144
|
+
comment_keys_mode: Literal["comment", "warn"],
|
|
145
|
+
dry_run: bool,
|
|
146
|
+
verbose: bool,
|
|
147
|
+
) -> None:
|
|
148
|
+
click.echo(f"Extracting from {code_path}")
|
|
149
|
+
start_time = perf_counter_ns()
|
|
150
|
+
|
|
151
|
+
statistics = extract(
|
|
152
|
+
code_path=code_path,
|
|
153
|
+
output_path=output_path,
|
|
154
|
+
language=language,
|
|
155
|
+
i18n_keys=i18n_keys,
|
|
156
|
+
i18n_keys_append=i18n_keys_append,
|
|
157
|
+
i18n_keys_prefix=i18n_keys_prefix,
|
|
158
|
+
exclude_dirs=exclude_dirs,
|
|
159
|
+
exclude_dirs_append=exclude_dirs_append,
|
|
160
|
+
ignore_attributes=ignore_attributes,
|
|
161
|
+
append_ignore_attributes=append_ignore_attributes,
|
|
162
|
+
ignore_kwargs=ignore_kwargs,
|
|
163
|
+
comment_junks=comment_junks,
|
|
164
|
+
default_ftl_file=default_ftl_file,
|
|
165
|
+
comment_keys_mode=comment_keys_mode,
|
|
166
|
+
dry_run=dry_run,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if verbose:
|
|
170
|
+
click.echo("Extraction statistics:")
|
|
171
|
+
click.echo(f" - Py files count: {statistics.py_files_count}")
|
|
172
|
+
click.echo(f" - FTL files count: {statistics.ftl_files_count}")
|
|
173
|
+
click.echo(f" - FTL keys in code: {statistics.ftl_in_code_keys_count}")
|
|
174
|
+
click.echo(f" - FTL keys stored: {statistics.ftl_stored_keys_count}")
|
|
175
|
+
click.echo(f" - FTL keys updated: {statistics.ftl_keys_updated}")
|
|
176
|
+
click.echo(f" - FTL keys added: {statistics.ftl_keys_added}")
|
|
177
|
+
click.echo(f" - FTL keys commented: {statistics.ftl_keys_commented}")
|
|
178
|
+
|
|
179
|
+
click.echo(f"[Python] Done in {(perf_counter_ns() - start_time) * 1e-9:.3f}s.")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@ftl.command("stub")
|
|
183
|
+
@click.argument("locale_path", type=click.Path(exists=True, path_type=Path))
|
|
184
|
+
@click.argument("output_path", type=click.Path(path_type=Path))
|
|
185
|
+
@click.option(
|
|
186
|
+
"--export-tree",
|
|
187
|
+
is_flag=True,
|
|
188
|
+
default=False,
|
|
189
|
+
show_default=True,
|
|
190
|
+
help="Export tree structure of FTL messages.",
|
|
191
|
+
)
|
|
192
|
+
def cli_stub(locale_path: Path, output_path: Path, export_tree: bool) -> None:
|
|
193
|
+
click.echo(f"Generating stubs from {locale_path}")
|
|
194
|
+
start_time = perf_counter_ns()
|
|
195
|
+
|
|
196
|
+
generate_stubs(locale_path, output_path, export_tree)
|
|
197
|
+
|
|
198
|
+
click.echo(f"[Python] Done in {(perf_counter_ns() - start_time) * 1e-9:.3f}s.")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
from fluent.syntax import ast as fluent_ast
|
|
8
|
+
|
|
9
|
+
from ftl_extract.exceptions import (
|
|
10
|
+
FTLExtractorDifferentPathsError,
|
|
11
|
+
FTLExtractorDifferentTranslationError,
|
|
12
|
+
)
|
|
13
|
+
from ftl_extract.matcher import FluentKey, I18nMatcher
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Iterable, Iterator
|
|
17
|
+
|
|
18
|
+
from ftl_extract.matcher import FluentKey
|
|
19
|
+
from ftl_extract.utils import ExtractionStatistics
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_py_files(*, search_path: Path, exclude_dirs: frozenset[Path]) -> Iterator[Path]:
|
|
23
|
+
"""
|
|
24
|
+
First step: find all .py files in given path.
|
|
25
|
+
|
|
26
|
+
:param search_path: Path to directory with .py files.
|
|
27
|
+
:type search_path: Path
|
|
28
|
+
:param exclude_dirs: Exclude directories from search.
|
|
29
|
+
:type exclude_dirs: frozenset[Path]
|
|
30
|
+
:return: Iterator with Path to .py files.
|
|
31
|
+
:rtype: Iterator[Path]
|
|
32
|
+
"""
|
|
33
|
+
if search_path.is_dir():
|
|
34
|
+
for path in search_path.rglob("[!{.}]*.py"):
|
|
35
|
+
if path.is_file() and not any(
|
|
36
|
+
path.is_relative_to(exclude_dir) for exclude_dir in exclude_dirs
|
|
37
|
+
):
|
|
38
|
+
yield path
|
|
39
|
+
else:
|
|
40
|
+
# If search_path is not a directory, check if it is a file
|
|
41
|
+
if search_path.is_file() and search_path.suffix == ".py":
|
|
42
|
+
yield search_path
|
|
43
|
+
else:
|
|
44
|
+
# If search_path is not a directory or file, return an empty iterator
|
|
45
|
+
yield from iter([])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_file(
|
|
49
|
+
*,
|
|
50
|
+
path: Path,
|
|
51
|
+
i18n_keys: Iterable[str],
|
|
52
|
+
i18n_keys_prefix: Iterable[str],
|
|
53
|
+
ignore_attributes: Iterable[str],
|
|
54
|
+
ignore_kwargs: Iterable[str],
|
|
55
|
+
default_ftl_file: Path,
|
|
56
|
+
) -> dict[str, FluentKey]:
|
|
57
|
+
"""
|
|
58
|
+
Second step: parse given .py file and find all i18n calls.
|
|
59
|
+
|
|
60
|
+
:param path: Path to .py file.
|
|
61
|
+
:type path: Path
|
|
62
|
+
:param i18n_keys: Names of function that is used to get translation.
|
|
63
|
+
:type i18n_keys: Iterable[str]
|
|
64
|
+
:param i18n_keys_prefix: Prefix names of function that is used to get translation.
|
|
65
|
+
:type i18n_keys_prefix: Iterable[str]
|
|
66
|
+
:param ignore_attributes: Ignore attributes, like `i18n.set_locale`.
|
|
67
|
+
:type ignore_attributes: Iterable[str]
|
|
68
|
+
:param ignore_kwargs: Ignore kwargs, like `when` from
|
|
69
|
+
`aiogram_dialog.I18nFormat(..., when=...)`.
|
|
70
|
+
:type ignore_kwargs: Iterable[str]
|
|
71
|
+
:param default_ftl_file: Default name of FTL file.
|
|
72
|
+
:type default_ftl_file: Path
|
|
73
|
+
:return: Dict with `key` and `FluentKey`.
|
|
74
|
+
:rtype: dict[str, FluentKey]
|
|
75
|
+
"""
|
|
76
|
+
node = ast.parse(path.read_bytes())
|
|
77
|
+
matcher = I18nMatcher(
|
|
78
|
+
code_path=path,
|
|
79
|
+
default_ftl_file=default_ftl_file,
|
|
80
|
+
i18n_keys=i18n_keys,
|
|
81
|
+
i18n_keys_prefix=i18n_keys_prefix,
|
|
82
|
+
ignore_attributes=ignore_attributes,
|
|
83
|
+
ignore_kwargs=ignore_kwargs,
|
|
84
|
+
)
|
|
85
|
+
matcher.visit(node)
|
|
86
|
+
return matcher.fluent_keys
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def post_process_fluent_keys(*, fluent_keys: dict[str, FluentKey], default_ftl_file: Path) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Third step: post-process parsed `FluentKey`.
|
|
92
|
+
|
|
93
|
+
:param fluent_keys: Dict with `key` and `FluentKey` that will be post-processed.
|
|
94
|
+
:type fluent_keys: dict[str, FluentKey]
|
|
95
|
+
:param default_ftl_file: Default name of FTL file.
|
|
96
|
+
:type default_ftl_file: Path
|
|
97
|
+
"""
|
|
98
|
+
for fluent_key in fluent_keys.values():
|
|
99
|
+
if not isinstance(fluent_key.path, Path):
|
|
100
|
+
fluent_key.path = Path(fluent_key.path)
|
|
101
|
+
|
|
102
|
+
if not fluent_key.path.suffix: # if path looks like directory (no suffix)
|
|
103
|
+
fluent_key.path /= default_ftl_file
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_conflicts(
|
|
107
|
+
*,
|
|
108
|
+
current_fluent_keys: dict[str, FluentKey],
|
|
109
|
+
new_fluent_keys: dict[str, FluentKey],
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Fourth step: find conflicts between current and new `FluentKey`s.
|
|
113
|
+
|
|
114
|
+
If conflict is found, raise `ValueError`.
|
|
115
|
+
|
|
116
|
+
Conflict is when `key` is the same, but `path` or `kwargs` are different.
|
|
117
|
+
"""
|
|
118
|
+
# Find common keys
|
|
119
|
+
conflict_keys = set(current_fluent_keys.keys()) & set(new_fluent_keys.keys())
|
|
120
|
+
|
|
121
|
+
if not conflict_keys:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
for key in conflict_keys:
|
|
125
|
+
if current_fluent_keys[key].path != new_fluent_keys[key].path:
|
|
126
|
+
raise FTLExtractorDifferentPathsError(
|
|
127
|
+
key,
|
|
128
|
+
current_fluent_keys[key].path,
|
|
129
|
+
new_fluent_keys[key].path,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not current_fluent_keys[key].translation.equals(new_fluent_keys[key].translation):
|
|
133
|
+
raise FTLExtractorDifferentTranslationError(
|
|
134
|
+
key,
|
|
135
|
+
cast(fluent_ast.Message, current_fluent_keys[key].translation),
|
|
136
|
+
cast(fluent_ast.Message, new_fluent_keys[key].translation),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def extract_fluent_keys(
|
|
141
|
+
*,
|
|
142
|
+
path: Path,
|
|
143
|
+
i18n_keys: Iterable[str],
|
|
144
|
+
i18n_keys_prefix: Iterable[str],
|
|
145
|
+
exclude_dirs: frozenset[Path],
|
|
146
|
+
ignore_attributes: Iterable[str],
|
|
147
|
+
ignore_kwargs: Iterable[str],
|
|
148
|
+
default_ftl_file: Path,
|
|
149
|
+
statistics: ExtractionStatistics | None = None,
|
|
150
|
+
) -> dict[str, FluentKey]:
|
|
151
|
+
"""
|
|
152
|
+
Extract all `FluentKey`s from given path.
|
|
153
|
+
|
|
154
|
+
:param path: Path to [.py file] / [directory with .py files].
|
|
155
|
+
:type path: Path
|
|
156
|
+
:param i18n_keys: Names of function that is used to get translation.
|
|
157
|
+
:type i18n_keys: Iterable[str]
|
|
158
|
+
:param i18n_keys_prefix: Prefix names of function that is used to get translation.
|
|
159
|
+
:type i18n_keys_prefix: Iterable[str]
|
|
160
|
+
:param exclude_dirs: Exclude directories from search.
|
|
161
|
+
:type exclude_dirs: frozenset[Path]
|
|
162
|
+
:param ignore_attributes: Ignore attributes, like `i18n.set_locale`.
|
|
163
|
+
:type ignore_attributes: Iterable[str]
|
|
164
|
+
:param ignore_kwargs: Ignore kwargs, like `when` from
|
|
165
|
+
`aiogram_dialog.I18nFormat(..., when=...)`.
|
|
166
|
+
:type ignore_kwargs: Iterable[str]
|
|
167
|
+
:param default_ftl_file: Default name of FTL file.
|
|
168
|
+
:type default_ftl_file: Path
|
|
169
|
+
:param statistics: Statistics of extraction.
|
|
170
|
+
:type statistics: ExtractionStatistics
|
|
171
|
+
:return: Dict with `key` and `FluentKey`.
|
|
172
|
+
:rtype: dict[str, FluentKey]
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
fluent_keys: dict[str, FluentKey] = {}
|
|
176
|
+
|
|
177
|
+
for file in find_py_files(search_path=path, exclude_dirs=exclude_dirs):
|
|
178
|
+
keys = parse_file(
|
|
179
|
+
path=file,
|
|
180
|
+
i18n_keys=i18n_keys,
|
|
181
|
+
i18n_keys_prefix=i18n_keys_prefix,
|
|
182
|
+
ignore_attributes=ignore_attributes,
|
|
183
|
+
ignore_kwargs=ignore_kwargs,
|
|
184
|
+
default_ftl_file=default_ftl_file,
|
|
185
|
+
)
|
|
186
|
+
post_process_fluent_keys(fluent_keys=keys, default_ftl_file=default_ftl_file)
|
|
187
|
+
find_conflicts(current_fluent_keys=fluent_keys, new_fluent_keys=keys)
|
|
188
|
+
fluent_keys.update(keys)
|
|
189
|
+
|
|
190
|
+
if statistics and keys:
|
|
191
|
+
statistics.py_files_count += 1
|
|
192
|
+
|
|
193
|
+
return fluent_keys
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sort_fluent_keys_by_path(*, fluent_keys: dict[str, FluentKey]) -> dict[Path, list[FluentKey]]:
|
|
197
|
+
"""
|
|
198
|
+
Sort `FluentKey`s by their paths.
|
|
199
|
+
|
|
200
|
+
:param fluent_keys: Dict with `key` and `FluentKey`.
|
|
201
|
+
:type fluent_keys: dict[str, FluentKey]
|
|
202
|
+
:return: Dict with `Path` and list of `FluentKey`.
|
|
203
|
+
:rtype: dict[Path, list[FluentKey]]
|
|
204
|
+
"""
|
|
205
|
+
sorted_fluent_keys: dict[Path, list[FluentKey]] = {}
|
|
206
|
+
for fluent_key in fluent_keys.values():
|
|
207
|
+
sorted_fluent_keys.setdefault(fluent_key.path, []).append(fluent_key)
|
|
208
|
+
|
|
209
|
+
return sorted_fluent_keys
|
ftl_extract/const.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
I18N_LITERAL: Final[str] = "i18n"
|
|
10
|
+
GET_LITERAL: Final[str] = "get"
|
|
11
|
+
PATH_LITERAL: Final[str] = "_path"
|
|
12
|
+
DEFAULT_I18N_KEYS: Final[tuple[str, ...]] = (I18N_LITERAL, "L", "LazyProxy", "LazyFilter")
|
|
13
|
+
DEFAULT_IGNORE_ATTRIBUTES: Final[tuple[str, ...]] = (
|
|
14
|
+
"set_locale",
|
|
15
|
+
"use_locale",
|
|
16
|
+
"use_context",
|
|
17
|
+
"set_context",
|
|
18
|
+
)
|
|
19
|
+
DEFAULT_IGNORE_KWARGS: Final[tuple[str, ...]] = ()
|
|
20
|
+
DEFAULT_FTL_FILE: Final[Path] = Path("_default.ftl")
|
|
21
|
+
FTL_DEBUG_VAR_NAME: Final[str] = "FTL_DEBUG"
|
|
22
|
+
COMMENT_KEYS_MODE: Final[tuple[str, ...]] = ("comment", "warn")
|
|
23
|
+
DEFAULT_EXCLUDE_DIRS: Final[tuple[str, ...]] = (
|
|
24
|
+
".venv",
|
|
25
|
+
"venv",
|
|
26
|
+
".git",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
".pytest_cache",
|
|
29
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from os import environ
|
|
4
|
+
from pprint import pformat
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ftl_extract.const import FTL_DEBUG_VAR_NAME
|
|
8
|
+
from ftl_extract.utils import to_json_no_span
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from fluent.syntax import ast
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FTLExtractorError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FTLExtractorDifferentPathsError(FTLExtractorError):
|
|
21
|
+
def __init__(self, key: str, current_path: Path, new_path: Path) -> None:
|
|
22
|
+
self.current_path = current_path
|
|
23
|
+
self.new_path = new_path
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"Key {key!r} already exists with different path: "
|
|
26
|
+
f"{self.current_path} != {self.new_path}",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FTLExtractorDifferentTranslationError(FTLExtractorError):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
key: str,
|
|
34
|
+
current_translation: ast.Message,
|
|
35
|
+
new_translation: ast.Message,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.current_translation = current_translation
|
|
38
|
+
self.new_translation = new_translation
|
|
39
|
+
|
|
40
|
+
if bool(environ.get(FTL_DEBUG_VAR_NAME, "")) is True:
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"Translation {key!r} already exists with different elements:\n"
|
|
43
|
+
f"current_translation: "
|
|
44
|
+
f"{pformat(self.current_translation.to_json(fn=to_json_no_span))}\n!= "
|
|
45
|
+
f"new_translation: {pformat(self.new_translation.to_json(fn=to_json_no_span))}",
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
super().__init__(
|
|
49
|
+
f"Translation {key!r} already exists with different elements: "
|
|
50
|
+
f"{self.current_translation} != {self.new_translation}",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FTLExtractorCantFindReferenceError(FTLExtractorError):
|
|
55
|
+
def __init__(self, key: str, key_path: Path, reference_key: str) -> None:
|
|
56
|
+
self.key = key
|
|
57
|
+
self.key_path = key_path
|
|
58
|
+
self.reference_key = reference_key
|
|
59
|
+
super().__init__(f"Can't find reference {reference_key!r} for key {key!r} at {key_path}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FTLExtractorCantFindTermError(FTLExtractorError):
|
|
63
|
+
def __init__(self, key: str, locale: str, key_path: Path, term_key: str) -> None:
|
|
64
|
+
self.key = key
|
|
65
|
+
self.locale = locale
|
|
66
|
+
self.key_path = key_path
|
|
67
|
+
self.term_key = term_key
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"Can't find term {term_key!r} for key {key!r} with locale {locale!r} at: {key_path}",
|
|
70
|
+
)
|