FTL-Extract 0.0.1__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.
@@ -0,0 +1,4 @@
1
+ from .__version__ import __version__
2
+ from .code_extractor import extract_fluent_keys
3
+
4
+ __all__ = ("extract_fluent_keys", "__version__")
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version(__package__)
ftl_extract/cli.py ADDED
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from ftl_extract.ftl_extractor import extract
8
+
9
+
10
+ @click.command()
11
+ @click.argument("code_path", type=click.Path(exists=True, path_type=Path))
12
+ @click.argument("output_path", type=click.Path(path_type=Path))
13
+ @click.option(
14
+ "--language",
15
+ "-l",
16
+ multiple=True,
17
+ default=("en",),
18
+ show_default=True,
19
+ help="Language of translation.",
20
+ )
21
+ @click.option(
22
+ "--i18n_keys",
23
+ "-k",
24
+ default=("i18n", "L", "LazyProxy", "LazyFilter"),
25
+ multiple=True,
26
+ show_default=True,
27
+ help="Names of function that is used to get translation.",
28
+ )
29
+ @click.option(
30
+ "--beauty",
31
+ is_flag=True,
32
+ default=False,
33
+ show_default=True,
34
+ help="Beautify output FTL files.",
35
+ )
36
+ def cli_extract(
37
+ code_path: Path,
38
+ output_path: Path,
39
+ language: tuple[str, ...],
40
+ i18n_keys: tuple[str, ...],
41
+ beauty: bool = False,
42
+ ) -> None:
43
+ click.echo(f"Extracting from {code_path}...")
44
+
45
+ extract(
46
+ code_path=code_path,
47
+ output_path=output_path,
48
+ language=language,
49
+ i18n_keys=i18n_keys,
50
+ beauty=beauty,
51
+ )
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ import libcst as cst
7
+
8
+ from ftl_extract.exceptions import (
9
+ FTLExtractorDifferentPathsError,
10
+ FTLExtractorDifferentTranslationError,
11
+ )
12
+ from ftl_extract.matcher import I18nMatcher
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator, Sequence
16
+
17
+ from ftl_extract.matcher import FluentKey
18
+
19
+
20
+ def find_py_files(path: Path) -> Iterator[Path]:
21
+ """
22
+ First step: find all .py files in given path.
23
+
24
+ :param path: Path to directory with .py files.
25
+ :type path: Path
26
+ :return: Iterator with Path to .py files.
27
+ :rtype: Iterator[Path]
28
+ """
29
+ yield from path.rglob("[!{.}]*.py") if path.is_dir() else [path]
30
+
31
+
32
+ def parse_file(path: Path, i18n_keys: str | Sequence[str]) -> dict[str, FluentKey]:
33
+ """
34
+ Second step: parse given .py file and find all i18n calls.
35
+
36
+ :param path: Path to .py file.
37
+ :type path: Path
38
+ :param i18n_keys: Names of function that is used to get translation.
39
+ :type i18n_keys: str | Sequence[str]
40
+ :return: Dict with `key` and `FluentKey`.
41
+ :rtype: dict[str, FluentKey]
42
+ """
43
+ module = cst.parse_module(path.read_bytes())
44
+ matcher = I18nMatcher(code_path=path, func_names=i18n_keys)
45
+ matcher.extract_matches(module)
46
+ return matcher.fluent_keys
47
+
48
+
49
+ def post_process_fluent_keys(fluent_keys: dict[str, FluentKey]) -> None:
50
+ """
51
+ Third step: post-process parsed `FluentKey`.
52
+
53
+ :param fluent_keys: Dict with `key` and `FluentKey` that will be post-processed.
54
+ :type fluent_keys: dict[str, FluentKey]
55
+ """
56
+ for fluent_key in fluent_keys.values():
57
+ if not isinstance(fluent_key.path, Path):
58
+ fluent_key.path = Path(fluent_key.path)
59
+
60
+ if not fluent_key.path.suffix: # if path looks like directory (no suffix)
61
+ fluent_key.path /= "_default.ftl"
62
+
63
+
64
+ def find_conflicts(
65
+ current_fluent_keys: dict[str, FluentKey],
66
+ new_fluent_keys: dict[str, FluentKey],
67
+ ) -> None:
68
+ """
69
+ Fourth step: find conflicts between current and new `FluentKey`s.
70
+
71
+ If conflict is found, raise `ValueError`.
72
+
73
+ Conflict is when `key` is the same, but `path` or `kwargs` are different.
74
+ """
75
+ # Find common keys
76
+ conflict_keys = set(current_fluent_keys.keys()) & set(new_fluent_keys.keys())
77
+
78
+ if not conflict_keys:
79
+ return
80
+
81
+ for key in conflict_keys:
82
+ if current_fluent_keys[key].path != new_fluent_keys[key].path:
83
+ raise FTLExtractorDifferentPathsError(
84
+ key,
85
+ current_fluent_keys[key].path,
86
+ new_fluent_keys[key].path,
87
+ )
88
+
89
+ if not current_fluent_keys[key].translation.equals(new_fluent_keys[key].translation):
90
+ raise FTLExtractorDifferentTranslationError(
91
+ key,
92
+ current_fluent_keys[key].translation,
93
+ new_fluent_keys[key].translation,
94
+ )
95
+
96
+
97
+ def extract_fluent_keys(path: Path, i18n_keys: str | Sequence[str]) -> dict[str, FluentKey]:
98
+ """
99
+ Extract all `FluentKey`s from given path.
100
+
101
+ :param path: Path to [.py file] / [directory with .py files].
102
+ :type path: Path
103
+ :param i18n_keys: Names of function that is used to get translation.
104
+ :type i18n_keys: str | Sequence[str]
105
+ :return: Dict with `key` and `FluentKey`.
106
+ :rtype: dict[str, FluentKey]
107
+
108
+ """
109
+ fluent_keys: dict[str, FluentKey] = {}
110
+
111
+ for file in find_py_files(path):
112
+ keys = parse_file(file, i18n_keys)
113
+ post_process_fluent_keys(keys)
114
+ find_conflicts(fluent_keys, keys)
115
+ fluent_keys.update(keys)
116
+
117
+ return fluent_keys
118
+
119
+
120
+ def sort_fluent_keys_by_path(fluent_keys: dict[str, FluentKey]) -> dict[Path, list[FluentKey]]:
121
+ """
122
+ Sort `FluentKey`s by their paths.
123
+
124
+ :param fluent_keys: Dict with `key` and `FluentKey`.
125
+ :type fluent_keys: dict[str, FluentKey]
126
+ :return: Dict with `Path` and list of `FluentKey`.
127
+ :rtype: dict[Path, list[FluentKey]]
128
+ """
129
+ sorted_fluent_keys: dict[Path, list[FluentKey]] = {}
130
+ for fluent_key in fluent_keys.values():
131
+ sorted_fluent_keys.setdefault(fluent_key.path, []).append(fluent_key)
132
+
133
+ return sorted_fluent_keys
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from pathlib import Path
7
+
8
+ from fluent.syntax import ast
9
+
10
+
11
+ class FTLExtractorError(Exception):
12
+ pass
13
+
14
+
15
+ class FTLExtractorDifferentPathsError(FTLExtractorError):
16
+ def __init__(self, key: str, current_path: Path, new_path: Path) -> None:
17
+ self.current_path = current_path
18
+ self.new_path = new_path
19
+ super().__init__(
20
+ f"Key {key!r} already exists with different path: "
21
+ f"{self.current_path} != {self.new_path}"
22
+ )
23
+
24
+
25
+ class FTLExtractorDifferentTranslationError(FTLExtractorError):
26
+ def __init__(
27
+ self,
28
+ key: str,
29
+ current_translation: ast.Message | ast.Comment,
30
+ new_translation: ast.Message | ast.Comment,
31
+ ) -> None:
32
+ self.current_translation = current_translation
33
+ self.new_translation = new_translation
34
+ super().__init__(
35
+ f"Translation {key!r} already exists with different elements: "
36
+ f"{self.current_translation} != {self.new_translation}"
37
+ )
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from click import echo
6
+ from fluent.syntax import FluentSerializer
7
+
8
+ from ftl_extract import extract_fluent_keys
9
+ from ftl_extract.code_extractor import sort_fluent_keys_by_path
10
+ from ftl_extract.ftl_importer import import_ftl_from_dir
11
+ from ftl_extract.process.commentator import comment_ftl_key
12
+ from ftl_extract.process.serializer import BeautyFluentSerializer, generate_ftl
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ from ftl_extract.matcher import FluentKey
18
+
19
+
20
+ def extract(
21
+ code_path: Path,
22
+ output_path: Path,
23
+ language: tuple[str, ...],
24
+ i18n_keys: tuple[str, ...],
25
+ beauty: bool = False,
26
+ ) -> None:
27
+ serializer: FluentSerializer | BeautyFluentSerializer
28
+
29
+ if beauty is True:
30
+ serializer = BeautyFluentSerializer(with_junk=True)
31
+ else:
32
+ serializer = FluentSerializer(with_junk=True)
33
+
34
+ # Extract fluent keys from code
35
+ in_code_fluent_keys = extract_fluent_keys(code_path, i18n_keys)
36
+
37
+ for lang in language:
38
+ # Import fluent keys from existing FTL files
39
+ stored_fluent_keys = import_ftl_from_dir(output_path, lang)
40
+ for fluent_key in stored_fluent_keys.values():
41
+ fluent_key.path = fluent_key.path.relative_to(output_path / lang)
42
+
43
+ keys_to_comment: dict[str, FluentKey] = {}
44
+ keys_to_add: dict[str, FluentKey] = {}
45
+
46
+ # Find keys should be commented
47
+ # Keys, that are not in code or not in their `path_` file
48
+ # First step: find keys that have different paths
49
+ for key, fluent_key in in_code_fluent_keys.items():
50
+ if key in stored_fluent_keys and fluent_key.path != stored_fluent_keys[key].path:
51
+ keys_to_comment[key] = stored_fluent_keys.pop(key)
52
+ keys_to_add[key] = fluent_key
53
+
54
+ elif key not in stored_fluent_keys:
55
+ keys_to_add[key] = fluent_key
56
+
57
+ else:
58
+ stored_fluent_keys[key].code_path = fluent_key.code_path
59
+
60
+ # Second step: find keys that are not in code
61
+ for key in stored_fluent_keys.keys() - in_code_fluent_keys.keys():
62
+ keys_to_comment[key] = stored_fluent_keys.pop(key)
63
+
64
+ for fluent_key in keys_to_comment.values():
65
+ comment_ftl_key(fluent_key, serializer)
66
+
67
+ sorted_fluent_keys = sort_fluent_keys_by_path(stored_fluent_keys)
68
+
69
+ for path, keys in sort_fluent_keys_by_path(keys_to_add).items():
70
+ sorted_fluent_keys.setdefault(path, []).extend(keys)
71
+
72
+ for path, keys in sort_fluent_keys_by_path(keys_to_comment).items():
73
+ sorted_fluent_keys.setdefault(path, []).extend(keys)
74
+
75
+ for path, keys in sorted_fluent_keys.items():
76
+ ftl, _ = generate_ftl(keys, serializer=serializer)
77
+ (output_path / lang / path).parent.mkdir(parents=True, exist_ok=True)
78
+ (output_path / lang / path).write_text(ftl, encoding="utf-8")
79
+ echo(f"File {output_path / lang / path} has been saved. {len(keys)} keys updated.")
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from fluent.syntax import ast, parse
6
+
7
+ from ftl_extract.matcher import FluentKey
8
+
9
+
10
+ def import_from_ftl(path: Path, locale: str) -> tuple[dict[str, FluentKey], ast.Resource]:
11
+ """Import `FluentKey`s from FTL."""
12
+ ftl_keys = {}
13
+
14
+ resource = parse(path.read_text(encoding="utf-8"), with_spans=True)
15
+
16
+ for entry in resource.body:
17
+ if isinstance(entry, ast.Message):
18
+ ftl_keys[entry.id.name] = FluentKey(
19
+ code_path=Path(),
20
+ key=entry.id.name,
21
+ translation=entry,
22
+ # Cut off the locale from the path
23
+ path=path,
24
+ locale=locale,
25
+ )
26
+
27
+ return ftl_keys, resource
28
+
29
+
30
+ def import_ftl_from_dir(path: Path, locale: str) -> dict[str, FluentKey]:
31
+ """Import `FluentKey`s from directory of FTL files."""
32
+ ftl_files = (path / locale).rglob("*.ftl") if path.is_dir() else [path]
33
+ ftl_keys = {}
34
+
35
+ for ftl_file in ftl_files:
36
+ keys, _ = import_from_ftl(ftl_file, locale)
37
+ ftl_keys.update(keys)
38
+
39
+ return ftl_keys
ftl_extract/matcher.py ADDED
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Callable, cast
7
+
8
+ import libcst as cst
9
+ from fluent.syntax import ast
10
+ from libcst import matchers as m
11
+
12
+ from ftl_extract.exceptions import (
13
+ FTLExtractorDifferentPathsError,
14
+ FTLExtractorDifferentTranslationError,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from typing import Literal
19
+
20
+ I18N_LITERAL: Literal["i18n"] = "i18n"
21
+ GET_LITERAL: Literal["get"] = "get"
22
+ PATH_LITERAL: Literal["_path"] = "_path"
23
+
24
+
25
+ @dataclass
26
+ class FluentKey:
27
+ """
28
+ Dataclass for storing information about key and its translation.
29
+
30
+ :param code_path: Path to .py file where key was found.
31
+ :type code_path: Path
32
+ :param key: Key that will be used to get translation.
33
+ :type key: str | None
34
+ :param translation: Translation of key.
35
+ :type translation: str | None
36
+ :param path: Path to .ftl file where key will be stored.
37
+ :type path: Path
38
+ :param locale: Locale of translation. When extracting from .py file, it will not be needed.
39
+ :type locale: str | None
40
+ """
41
+
42
+ code_path: Path
43
+ key: str
44
+ translation: ast.Message | ast.Comment
45
+ path: Path = field(default=Path("_default.ftl"))
46
+ locale: str | None = field(default=None)
47
+
48
+
49
+ class I18nMatcher:
50
+ def __init__(self, code_path: Path, func_names: str | Sequence[str] = I18N_LITERAL) -> None:
51
+ """
52
+
53
+ :param code_path: Path to .py file where visitor will be used.
54
+ :type code_path: Path
55
+ :param func_names: Name of function that is used to get translation. Default is "i18n".
56
+ :type func_names: str | Sequence[str]
57
+ """
58
+ self.code_path = code_path
59
+ self._func_names = {func_names} if isinstance(func_names, str) else set(func_names)
60
+ self.fluent_keys: dict[str, FluentKey] = {}
61
+
62
+ self._matcher = m.OneOf(
63
+ m.Call(
64
+ func=m.Attribute(
65
+ value=m.OneOf(*map(cast(Callable, m.Name), self._func_names)),
66
+ attr=m.SaveMatchedNode(matcher=~m.Name(GET_LITERAL) & m.Name(), name="key"),
67
+ ),
68
+ args=[
69
+ m.SaveMatchedNode(
70
+ matcher=m.ZeroOrMore(
71
+ m.Arg(
72
+ value=m.DoNotCare(),
73
+ keyword=m.Name(),
74
+ )
75
+ ),
76
+ name="kwargs",
77
+ )
78
+ ],
79
+ ),
80
+ m.Call(
81
+ func=m.Attribute(
82
+ value=m.OneOf(*map(cast(Callable, m.Name), self._func_names)),
83
+ attr=m.Name(value=GET_LITERAL),
84
+ ),
85
+ args=[
86
+ m.Arg(
87
+ value=m.SaveMatchedNode(matcher=m.SimpleString(), name="key"), keyword=None
88
+ ),
89
+ m.SaveMatchedNode(
90
+ matcher=m.ZeroOrMore(
91
+ m.Arg(
92
+ value=m.DoNotCare(),
93
+ keyword=m.Name(),
94
+ )
95
+ ),
96
+ name="kwargs",
97
+ ),
98
+ ],
99
+ ),
100
+ m.Call(
101
+ func=m.OneOf(*map(cast(Callable, m.Name), self._func_names)),
102
+ args=[
103
+ m.Arg(
104
+ value=m.SaveMatchedNode(matcher=m.SimpleString(), name="key"), keyword=None
105
+ ),
106
+ m.SaveMatchedNode(
107
+ matcher=m.ZeroOrMore(
108
+ m.Arg(
109
+ value=m.DoNotCare(),
110
+ keyword=m.Name(),
111
+ )
112
+ ),
113
+ name="kwargs",
114
+ ),
115
+ ],
116
+ ),
117
+ )
118
+
119
+ def extract_matches(self, module: cst.Module) -> None:
120
+ for match in m.extractall(module, self._matcher):
121
+ # Key
122
+ if isinstance(match["key"], cst.Name):
123
+ key = cast(cst.Name, match["key"]).value
124
+ translation = ast.Message(
125
+ id=ast.Identifier(name=key),
126
+ value=ast.Pattern(
127
+ elements=[ast.TextElement(value=cast(cst.Name, match["key"]).value)]
128
+ ),
129
+ )
130
+ fluent_key = FluentKey(code_path=self.code_path, key=key, translation=translation)
131
+ elif isinstance(match["key"], cst.SimpleString):
132
+ key = cast(cst.SimpleString, match["key"]).raw_value
133
+ translation = ast.Message(
134
+ id=ast.Identifier(name=key),
135
+ value=ast.Pattern(elements=[ast.TextElement(value=key)]),
136
+ )
137
+ fluent_key = FluentKey(code_path=self.code_path, key=key, translation=translation)
138
+ else:
139
+ msg = f"Unknown type of key: {type(match['key'])} | {match['key']}"
140
+ raise TypeError(msg)
141
+
142
+ # Kwargs
143
+ for kwarg in cast(Sequence[m.Arg], match["kwargs"]):
144
+ keyword = cast(cst.Name, kwarg.keyword)
145
+ if keyword.value == PATH_LITERAL:
146
+ fluent_key.path = Path(cast(cst.SimpleString, kwarg.value).raw_value)
147
+
148
+ else:
149
+ if (
150
+ isinstance(fluent_key.translation, ast.Message)
151
+ and fluent_key.translation.value is not None
152
+ ):
153
+ fluent_key.translation.value.elements.append(
154
+ ast.Placeable(
155
+ expression=ast.VariableReference(
156
+ id=ast.Identifier(name=keyword.value)
157
+ )
158
+ )
159
+ )
160
+
161
+ if fluent_key.key in self.fluent_keys:
162
+ if self.fluent_keys[fluent_key.key].path != fluent_key.path:
163
+ raise FTLExtractorDifferentPathsError(
164
+ fluent_key.key,
165
+ fluent_key.path,
166
+ self.fluent_keys[fluent_key.key].path,
167
+ )
168
+
169
+ if not self.fluent_keys[fluent_key.key].translation.equals(fluent_key.translation):
170
+ raise FTLExtractorDifferentTranslationError(
171
+ fluent_key.key,
172
+ fluent_key.translation,
173
+ self.fluent_keys[fluent_key.key].translation,
174
+ )
175
+
176
+ else:
177
+ self.fluent_keys[fluent_key.key] = fluent_key
File without changes
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from fluent.syntax import FluentSerializer, ast
6
+
7
+ if TYPE_CHECKING:
8
+ from ftl_extract.matcher import FluentKey
9
+
10
+
11
+ def comment_ftl_key(key: FluentKey, serializer: FluentSerializer) -> None:
12
+ raw_entry = serializer.serialize_entry(key.translation)
13
+ key.translation = ast.Comment(content=raw_entry.strip())
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from fluent.syntax import FluentSerializer, ast
6
+ from fluent.syntax.serializer import serialize_junk, serialize_message, serialize_term
7
+
8
+ if TYPE_CHECKING:
9
+ from fluent.syntax.ast import Resource
10
+
11
+ from ftl_extract.matcher import FluentKey
12
+
13
+
14
+ class BeautyFluentSerializer(FluentSerializer):
15
+ """A serializer that formats the output FTL for better readability."""
16
+
17
+ def serialize_entry(self, entry: ast.EntryType, state: int = 0) -> str: # pragma: no cover
18
+ """Serialize an :class:`.ast.Entry` to a string."""
19
+ if isinstance(entry, ast.Message):
20
+ return serialize_message(entry)
21
+ if isinstance(entry, ast.Term):
22
+ return serialize_term(entry)
23
+ if isinstance(entry, ast.Comment):
24
+ if state & self.HAS_ENTRIES:
25
+ return "\n{}\n".format(serialize_comment(entry, "#"))
26
+ return "{}\n".format(serialize_comment(entry, "#"))
27
+ if isinstance(entry, ast.GroupComment):
28
+ if state & self.HAS_ENTRIES:
29
+ return "\n{}\n".format(serialize_comment(entry, "##"))
30
+ return "{}\n".format(serialize_comment(entry, "##"))
31
+ if isinstance(entry, ast.ResourceComment):
32
+ if state & self.HAS_ENTRIES:
33
+ return "\n{}\n".format(serialize_comment(entry, "###"))
34
+ return "{}\n".format(serialize_comment(entry, "###"))
35
+ if isinstance(entry, ast.Junk):
36
+ return serialize_junk(entry)
37
+ raise Exception(f"Unknown entry type: {type(entry)}") # noqa: TRY002, TRY003, EM102
38
+
39
+
40
+ def serialize_comment(
41
+ comment: ast.Comment | ast.GroupComment | ast.ResourceComment,
42
+ prefix: str = "#",
43
+ ) -> str: # pragma: no cover
44
+ if not comment.content:
45
+ return f"{prefix}"
46
+
47
+ return "\n".join(
48
+ [prefix if len(line) == 0 else f"{prefix} {line}" for line in comment.content.split("\n")]
49
+ )
50
+
51
+
52
+ def generate_ftl(
53
+ fluent_keys: dict[str, FluentKey] | list[FluentKey],
54
+ serializer: FluentSerializer,
55
+ ) -> tuple[str, Resource]:
56
+ """Generate FTL translations from `fluent_keys`."""
57
+ resource = ast.Resource(body=None)
58
+
59
+ if isinstance(fluent_keys, list):
60
+ for fluent_key in fluent_keys:
61
+ resource.body.append(fluent_key.translation)
62
+ else:
63
+ for fluent_key in fluent_keys.values():
64
+ resource.body.append(fluent_key.translation)
65
+
66
+ return serializer.serialize(resource), resource
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
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,47 @@
1
+ Metadata-Version: 2.1
2
+ Name: FTL-Extract
3
+ Version: 0.0.1
4
+ Summary: Extracts FTL files from a directory and outputs them to a directory
5
+ License: MIT
6
+ Keywords: ftl,ftl-extract,ftl-extractor
7
+ Author: andrew000
8
+ Requires-Python: >=3.9
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Dist: fluent-syntax (>=0.19,<0.20)
16
+ Requires-Dist: libcst (>=1.4,<2.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # FTL-Extract
20
+
21
+ ## Description
22
+
23
+ FTL-Extract is a Python package that extracts Fluent keys from .py files
24
+ and generates a .ftl file with extracted keys.
25
+
26
+ ## Installation
27
+
28
+ Use the package manager [pip](https://pip.pypa.io/en/stable) to install
29
+ FTL-Extract.
30
+
31
+ ``` bash
32
+ pip install FTL-Extract
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ``` bash
38
+ ftl_extract code_path output_path
39
+ ```
40
+
41
+ ## Contributing
42
+
43
+ Pull requests are welcome. For major changes, please open an issue first
44
+ to discuss what you would like to change.
45
+
46
+ Please make sure to update tests as appropriate.
47
+
@@ -0,0 +1,16 @@
1
+ ftl_extract/__init__.py,sha256=xEB6MaWHCeQxHk3DihmluYSHjBY-UwB8MVCS19dkmPU,135
2
+ ftl_extract/__version__.py,sha256=52eq0hdOZ19xMJqe7e4sVjLkwbVTMZhaGwPK5PqLPeM,81
3
+ ftl_extract/cli.py,sha256=e24c87_yq0Ubgl2DkmoUNli-vymwir9tbHpkXTKpYF8,1134
4
+ ftl_extract/code_extractor.py,sha256=6ZrT47RenuzI8NCQaEPqA5ewtBtCEWQickwRnFwp3Zg,4181
5
+ ftl_extract/exceptions.py,sha256=IyiFbOaXrsreLdSm7L7WzK3fuUTRbLmstS9a5bb5sug,1086
6
+ ftl_extract/ftl_extractor.py,sha256=GlPNy3Z7LOfvCEVIIylk-zZ6QIr8azYv6TAWXs8-0dQ,3031
7
+ ftl_extract/ftl_importer.py,sha256=VRaT-UV2XuUCLGUgKstavEcZC8jGqu1jUtPijoaEST4,1113
8
+ ftl_extract/matcher.py,sha256=_x290_fpUtBA18ajseEa8N6rPAI2zuq9iCXXZ4MJs_M,6753
9
+ ftl_extract/process/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ ftl_extract/process/commentator.py,sha256=U5Rv94p2X2FHJ83rWUXdAbPo8tQTt7MAe4cnVwygS6M,381
11
+ ftl_extract/process/serializer.py,sha256=PMQy0SYIEhkidMcjtbFmQU7K8WtMgtbn0ckUc0KXh0A,2475
12
+ ftl_extract-0.0.1.dist-info/LICENSE,sha256=ACwmltkrXIz5VsEQcrqljq-fat6ZXAMepjXGoe40KtE,1069
13
+ ftl_extract-0.0.1.dist-info/METADATA,sha256=qBq-B18lrGPh-hb7Yv9o50IspvhP7EqSbi0V8Glo0x4,1182
14
+ ftl_extract-0.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ ftl_extract-0.0.1.dist-info/entry_points.txt,sha256=f9qVcPQk9dav2NwUW_XLkyz-LHfrZT7Srmp9ha2r9uw,95
16
+ ftl_extract-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ ftl_export=ftl_export.__main__:main
3
+ ftl_extract=ftl_extract.cli:cli_extract
4
+