debconf-translator 0.4.1__tar.gz

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.
Files changed (26) hide show
  1. debconf_translator-0.4.1/PKG-INFO +100 -0
  2. debconf_translator-0.4.1/README.md +89 -0
  3. debconf_translator-0.4.1/debconf_translator/__init__.py +20 -0
  4. debconf_translator-0.4.1/debconf_translator/__main__.py +6 -0
  5. debconf_translator-0.4.1/debconf_translator/app.py +178 -0
  6. debconf_translator-0.4.1/debconf_translator/cli/__init__.py +125 -0
  7. debconf_translator-0.4.1/debconf_translator/models.py +77 -0
  8. debconf_translator-0.4.1/debconf_translator/scraper.py +259 -0
  9. debconf_translator-0.4.1/debconf_translator/views/__init__.py +1 -0
  10. debconf_translator-0.4.1/debconf_translator/views/dashboard.py +210 -0
  11. debconf_translator-0.4.1/debconf_translator/views/packages.py +273 -0
  12. debconf_translator-0.4.1/debconf_translator/views/reviews.py +226 -0
  13. debconf_translator-0.4.1/debconf_translator/views/settings.py +194 -0
  14. debconf_translator-0.4.1/debconf_translator/views/submit.py +409 -0
  15. debconf_translator-0.4.1/debconf_translator/widgets/__init__.py +1 -0
  16. debconf_translator-0.4.1/debconf_translator/widgets/po_editor.py +407 -0
  17. debconf_translator-0.4.1/debconf_translator/window.py +262 -0
  18. debconf_translator-0.4.1/debconf_translator.egg-info/PKG-INFO +100 -0
  19. debconf_translator-0.4.1/debconf_translator.egg-info/SOURCES.txt +24 -0
  20. debconf_translator-0.4.1/debconf_translator.egg-info/dependency_links.txt +1 -0
  21. debconf_translator-0.4.1/debconf_translator.egg-info/entry_points.txt +2 -0
  22. debconf_translator-0.4.1/debconf_translator.egg-info/top_level.txt +1 -0
  23. debconf_translator-0.4.1/pyproject.toml +22 -0
  24. debconf_translator-0.4.1/setup.cfg +4 -0
  25. debconf_translator-0.4.1/setup.py +12 -0
  26. debconf_translator-0.4.1/tests/test_release_sanity.py +25 -0
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: debconf-translator
3
+ Version: 0.4.1
4
+ Summary: GTK4/Adwaita tool for managing Debconf template translations
5
+ Author-email: Daniel Nylander <daniel@danielnylander.se>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/yeager/debconf-translator
8
+ Project-URL: Bug Tracker, https://github.com/yeager/debconf-translator/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Debconf Translation Manager
13
+
14
+ A GTK4/Adwaita tool for managing Debconf template translations in Debian and Ubuntu.
15
+
16
+ <img width="1206" height="754" alt="image" src="https://github.com/user-attachments/assets/801eb597-ecab-428d-a843-f3fc85bb7e00" />
17
+
18
+
19
+ ## Features
20
+
21
+ - **Dashboard** — Overview of translation status per language with statistics
22
+ - **Package Browser** — Browse untranslated packages sorted by popularity (popcon)
23
+ - **Built-in PO Editor** — Edit translations inline with source/translation side-by-side
24
+ - **Review Board** — Track debconf templates under review on l10n.debian.org
25
+ - **BTS Submission** — Generate bug reports for submitting translations
26
+ - **CLI** — Command-line interface for status checks and batch operations
27
+
28
+ ## Installation
29
+
30
+ ### PyPI / pipx (Debian/Ubuntu)
31
+
32
+ Install GTK4, libadwaita and the system PyGObject bindings first; pip cannot
33
+ install these native system libraries:
34
+
35
+ ```bash
36
+ sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
37
+ pipx install --system-site-packages debconf-translator
38
+ ```
39
+
40
+
41
+ ### From .deb package (Debian/Ubuntu)
42
+ ```bash
43
+ # Add the repository
44
+ echo "deb https://yeager.github.io/debian-repo/ stable main" | sudo tee /etc/apt/sources.list.d/yeager.list
45
+ sudo apt update && sudo apt install debconf-translator
46
+ ```
47
+
48
+ ### From .rpm package (Fedora)
49
+ ```bash
50
+ sudo dnf install debconf-translator
51
+ ```
52
+
53
+ ### From source
54
+ ```bash
55
+ pip install .
56
+ debconf-translator
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### GUI
62
+ ```bash
63
+ debconf-translator
64
+ ```
65
+
66
+ ### CLI
67
+ ```bash
68
+ # Check translation status
69
+ debconf-translator status sv
70
+ debconf-translator status sv --json
71
+
72
+ # Download untranslated templates
73
+ debconf-translator fetch sv -o ./translations/
74
+
75
+ # Show packages under review
76
+ debconf-translator reviews
77
+ ```
78
+
79
+ ## Keyboard Shortcuts
80
+
81
+ | Shortcut | Action |
82
+ |----------|--------|
83
+ | Ctrl+1 | Dashboard |
84
+ | Ctrl+2 | Packages |
85
+ | Ctrl+3 | Review Board |
86
+ | Ctrl+4 | Submit |
87
+ | Ctrl+T | Toggle Dark Theme |
88
+ | Ctrl+Q | Quit |
89
+
90
+ ## Translation
91
+
92
+ Translations are managed on [Transifex](https://www.transifex.com/danielnylander/debconf-translator/).
93
+
94
+ ## License
95
+
96
+ GPL-3.0-or-later
97
+
98
+ ## Author
99
+
100
+ Daniel Nylander <daniel@danielnylander.se>
@@ -0,0 +1,89 @@
1
+ # Debconf Translation Manager
2
+
3
+ A GTK4/Adwaita tool for managing Debconf template translations in Debian and Ubuntu.
4
+
5
+ <img width="1206" height="754" alt="image" src="https://github.com/user-attachments/assets/801eb597-ecab-428d-a843-f3fc85bb7e00" />
6
+
7
+
8
+ ## Features
9
+
10
+ - **Dashboard** — Overview of translation status per language with statistics
11
+ - **Package Browser** — Browse untranslated packages sorted by popularity (popcon)
12
+ - **Built-in PO Editor** — Edit translations inline with source/translation side-by-side
13
+ - **Review Board** — Track debconf templates under review on l10n.debian.org
14
+ - **BTS Submission** — Generate bug reports for submitting translations
15
+ - **CLI** — Command-line interface for status checks and batch operations
16
+
17
+ ## Installation
18
+
19
+ ### PyPI / pipx (Debian/Ubuntu)
20
+
21
+ Install GTK4, libadwaita and the system PyGObject bindings first; pip cannot
22
+ install these native system libraries:
23
+
24
+ ```bash
25
+ sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
26
+ pipx install --system-site-packages debconf-translator
27
+ ```
28
+
29
+
30
+ ### From .deb package (Debian/Ubuntu)
31
+ ```bash
32
+ # Add the repository
33
+ echo "deb https://yeager.github.io/debian-repo/ stable main" | sudo tee /etc/apt/sources.list.d/yeager.list
34
+ sudo apt update && sudo apt install debconf-translator
35
+ ```
36
+
37
+ ### From .rpm package (Fedora)
38
+ ```bash
39
+ sudo dnf install debconf-translator
40
+ ```
41
+
42
+ ### From source
43
+ ```bash
44
+ pip install .
45
+ debconf-translator
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### GUI
51
+ ```bash
52
+ debconf-translator
53
+ ```
54
+
55
+ ### CLI
56
+ ```bash
57
+ # Check translation status
58
+ debconf-translator status sv
59
+ debconf-translator status sv --json
60
+
61
+ # Download untranslated templates
62
+ debconf-translator fetch sv -o ./translations/
63
+
64
+ # Show packages under review
65
+ debconf-translator reviews
66
+ ```
67
+
68
+ ## Keyboard Shortcuts
69
+
70
+ | Shortcut | Action |
71
+ |----------|--------|
72
+ | Ctrl+1 | Dashboard |
73
+ | Ctrl+2 | Packages |
74
+ | Ctrl+3 | Review Board |
75
+ | Ctrl+4 | Submit |
76
+ | Ctrl+T | Toggle Dark Theme |
77
+ | Ctrl+Q | Quit |
78
+
79
+ ## Translation
80
+
81
+ Translations are managed on [Transifex](https://www.transifex.com/danielnylander/debconf-translator/).
82
+
83
+ ## License
84
+
85
+ GPL-3.0-or-later
86
+
87
+ ## Author
88
+
89
+ Daniel Nylander <daniel@danielnylander.se>
@@ -0,0 +1,20 @@
1
+ """Debconf Translation Manager — GTK4/Adwaita tool for managing Debconf template translations."""
2
+
3
+ import gettext
4
+ import locale
5
+ import os
6
+
7
+ __version__ = "0.4.1"
8
+ __app_id__ = "se.danielnylander.debconf-translator"
9
+ __author__ = "Daniel Nylander"
10
+ __email__ = "daniel@danielnylander.se"
11
+
12
+ # Centralized locale/gettext setup — all modules import _ from here
13
+ _localedir = os.path.join(os.path.dirname(__file__), '..', 'locale')
14
+ if not os.path.isdir(_localedir):
15
+ _localedir = '/usr/share/locale'
16
+
17
+ gettext.bindtextdomain('debconf-translator', _localedir)
18
+ gettext.textdomain('debconf-translator')
19
+
20
+ _ = gettext.gettext
@@ -0,0 +1,6 @@
1
+ """Allow running as python3 -m debconf_translator."""
2
+ import sys
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,178 @@
1
+ """GTK4/Adwaita application entry point."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import gi
10
+ gi.require_version("Gtk", "4.0")
11
+ gi.require_version("Adw", "1")
12
+ from gi.repository import Adw, Gio, GLib, Gtk
13
+
14
+ from . import __app_id__, __version__, _
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ CONFIG_DIR = Path(GLib.get_user_config_dir()) / "debconf-translator"
19
+ CONFIG_FILE = CONFIG_DIR / "config.json"
20
+
21
+
22
+ def load_config() -> dict:
23
+ if CONFIG_FILE.exists():
24
+ try:
25
+ return json.loads(CONFIG_FILE.read_text())
26
+ except (json.JSONDecodeError, OSError):
27
+ pass
28
+ return {}
29
+
30
+
31
+ def save_config(config: dict) -> None:
32
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
33
+ CONFIG_FILE.write_text(json.dumps(config, indent=2))
34
+
35
+
36
+ class DebconfTranslatorApp(Adw.Application):
37
+ def __init__(self):
38
+ super().__init__(
39
+ application_id=__app_id__,
40
+ flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
41
+ )
42
+ self._setup_actions()
43
+
44
+ def _setup_actions(self):
45
+ actions = [
46
+ ("quit", self._on_quit, ["<primary>q"]),
47
+ ("about", self._on_about, None),
48
+ ("shortcuts", self._on_shortcuts, ["<primary>question"]),
49
+ ("theme", self._on_toggle_theme, ["<primary>t"]),
50
+ ]
51
+ for name, callback, accels in actions:
52
+ action = Gio.SimpleAction.new(name, None)
53
+ action.connect("activate", callback)
54
+ self.add_action(action)
55
+ if accels:
56
+ self.set_accels_for_action(f"app.{name}", accels)
57
+
58
+ self.set_accels_for_action("win.navigate-dashboard", ["<primary>1"])
59
+ self.set_accels_for_action("win.navigate-packages", ["<primary>2"])
60
+ self.set_accels_for_action("win.navigate-reviews", ["<primary>3"])
61
+ self.set_accels_for_action("win.navigate-submit", ["<primary>4"])
62
+
63
+ def do_activate(self):
64
+ from .window import DebconfTranslatorWindow
65
+
66
+ win = self.props.active_window
67
+ if not win:
68
+ win = DebconfTranslatorWindow(application=self)
69
+ win.present()
70
+
71
+ def _on_quit(self, *_args):
72
+ self.quit()
73
+
74
+ def _on_about(self, *_args):
75
+ win = self.props.active_window
76
+ about = Adw.AboutDialog.new()
77
+ about.set_application_name(_("Debconf Translation Manager"))
78
+ about.set_application_icon(__app_id__)
79
+ about.set_version(__version__)
80
+ about.set_developer_name("Daniel Nylander")
81
+ about.set_license_type(Gtk.License.GPL_3_0)
82
+ about.set_website("https://github.com/yeager/debconf-translator")
83
+ about.set_issue_url("https://github.com/yeager/debconf-translator/issues")
84
+ about.set_developers(["Daniel Nylander <daniel@danielnylander.se>"])
85
+ about.set_copyright("© 2026 Daniel Nylander")
86
+ about.set_comments(
87
+ _("GTK4/Adwaita tool for managing Debconf template translations")
88
+ )
89
+ debug_lines = [
90
+ f"debconf-translator {__version__}",
91
+ f"GTK {Gtk.get_major_version()}.{Gtk.get_minor_version()}.{Gtk.get_micro_version()}",
92
+ f"libadwaita {Adw.get_major_version()}.{Adw.get_minor_version()}.{Adw.get_micro_version()}",
93
+ f"Python {sys.version}",
94
+ f"OS: {os.uname().sysname} {os.uname().release}",
95
+ ]
96
+ about.set_debug_info("\n".join(debug_lines))
97
+ about.set_debug_info_filename("debconf-translator-debug.txt")
98
+ about.present(win)
99
+
100
+ def _on_shortcuts(self, *_args):
101
+ win = self.props.active_window
102
+ builder = Gtk.Builder.new_from_string(SHORTCUTS_UI, -1)
103
+ shortcuts_win = builder.get_object("shortcuts")
104
+ shortcuts_win.set_transient_for(win)
105
+ shortcuts_win.present()
106
+
107
+ def _on_toggle_theme(self, *_args):
108
+ sm = Adw.StyleManager.get_default()
109
+ if sm.get_dark():
110
+ sm.set_color_scheme(Adw.ColorScheme.FORCE_LIGHT)
111
+ else:
112
+ sm.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
113
+
114
+
115
+ SHORTCUTS_UI = """\
116
+ <?xml version="1.0" encoding="UTF-8"?>
117
+ <interface>
118
+ <object class="GtkShortcutsWindow" id="shortcuts">
119
+ <property name="modal">1</property>
120
+ <child>
121
+ <object class="GtkShortcutsSection">
122
+ <property name="section-name">shortcuts</property>
123
+ <child>
124
+ <object class="GtkShortcutsGroup">
125
+ <property name="title" translatable="yes">General</property>
126
+ <child>
127
+ <object class="GtkShortcutsShortcut">
128
+ <property name="title" translatable="yes">Toggle Dark Theme</property>
129
+ <property name="accelerator">&lt;primary&gt;t</property>
130
+ </object>
131
+ </child>
132
+ <child>
133
+ <object class="GtkShortcutsShortcut">
134
+ <property name="title" translatable="yes">Quit</property>
135
+ <property name="accelerator">&lt;primary&gt;q</property>
136
+ </object>
137
+ </child>
138
+ </object>
139
+ </child>
140
+ <child>
141
+ <object class="GtkShortcutsGroup">
142
+ <property name="title" translatable="yes">Navigation</property>
143
+ <child>
144
+ <object class="GtkShortcutsShortcut">
145
+ <property name="title" translatable="yes">Dashboard</property>
146
+ <property name="accelerator">&lt;primary&gt;1</property>
147
+ </object>
148
+ </child>
149
+ <child>
150
+ <object class="GtkShortcutsShortcut">
151
+ <property name="title" translatable="yes">Packages</property>
152
+ <property name="accelerator">&lt;primary&gt;2</property>
153
+ </object>
154
+ </child>
155
+ <child>
156
+ <object class="GtkShortcutsShortcut">
157
+ <property name="title" translatable="yes">Review Board</property>
158
+ <property name="accelerator">&lt;primary&gt;3</property>
159
+ </object>
160
+ </child>
161
+ <child>
162
+ <object class="GtkShortcutsShortcut">
163
+ <property name="title" translatable="yes">Submit</property>
164
+ <property name="accelerator">&lt;primary&gt;4</property>
165
+ </object>
166
+ </child>
167
+ </object>
168
+ </child>
169
+ </object>
170
+ </child>
171
+ </object>
172
+ </interface>
173
+ """
174
+
175
+
176
+ def main():
177
+ app = DebconfTranslatorApp()
178
+ return app.run(sys.argv)
@@ -0,0 +1,125 @@
1
+ """CLI interface for debconf-translator."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .. import __version__, _
7
+
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(
12
+ prog="debconf-translator",
13
+ description=_("Debconf Translation Manager — manage debconf template translations"),
14
+ )
15
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
16
+
17
+ sub = parser.add_subparsers(dest="command")
18
+
19
+ # GUI
20
+ sub.add_parser("gui", help=_("Launch the graphical interface"))
21
+
22
+ # Status
23
+ status_p = sub.add_parser("status", help=_("Show translation status for a language"))
24
+ status_p.add_argument("lang", default="sv", nargs="?", help=_("Language code (default: sv)"))
25
+ status_p.add_argument("--json", "-j", action="store_true", help=_("Output as JSON"))
26
+ status_p.add_argument("-q", "--quiet", action="store_true", help=_("Minimal output"))
27
+
28
+ # Fetch
29
+ fetch_p = sub.add_parser("fetch", help=_("Download POT files for untranslated packages"))
30
+ fetch_p.add_argument("lang", default="sv", nargs="?")
31
+ fetch_p.add_argument("--output", "-o", default=".", help=_("Output directory"))
32
+
33
+ # Reviews
34
+ reviews_p = sub.add_parser("reviews", help=_("Show packages under review"))
35
+ reviews_p.add_argument("--json", "-j", action="store_true", help=_("Output as JSON"))
36
+
37
+ args = parser.parse_args()
38
+
39
+ if args.command is None or args.command == "gui":
40
+ from ..app import main as gui_main
41
+ return gui_main()
42
+
43
+ if args.command == "status":
44
+ return _cmd_status(args)
45
+ elif args.command == "fetch":
46
+ return _cmd_fetch(args)
47
+ elif args.command == "reviews":
48
+ return _cmd_reviews(args)
49
+
50
+
51
+ def _cmd_status(args):
52
+ from ..scraper import fetch_language_status
53
+ stats, packages, translators = fetch_language_status(args.lang)
54
+
55
+ if args.json:
56
+ import json
57
+ print(json.dumps({
58
+ "language": stats.code,
59
+ "translated": stats.translated,
60
+ "total": stats.total,
61
+ "progress": round(stats.progress, 1),
62
+ "untranslated_packages": len(packages),
63
+ "packages": [{"name": p.name, "strings": p.strings_total} for p in packages],
64
+ }, indent=2))
65
+ elif args.quiet:
66
+ print(f"{stats.translated}/{stats.total} ({stats.progress:.1f}%)")
67
+ else:
68
+ print(f"Language: {args.lang}")
69
+ print(f"Translated: {stats.translated}/{stats.total} ({stats.progress:.1f}%)")
70
+ print(f"Untranslated packages: {len(packages)}")
71
+ if packages:
72
+ print()
73
+ packages.sort(key=lambda p: p.strings_total, reverse=True)
74
+ for p in packages[:20]:
75
+ print(f" {p.name:40s} {p.strings_total:4d} strings")
76
+ if len(packages) > 20:
77
+ print(f" ... and {len(packages) - 20} more")
78
+
79
+ if translators:
80
+ medals = ["🥇", "🥈", "🥉"]
81
+ print(f"\nTop translators ({args.lang}):")
82
+ for i, t in enumerate(translators[:10]):
83
+ medal = medals[i] if i < 3 else f" #{i+1}"
84
+ print(f" {medal} {t.name:35s} {t.strings:5d} strings ({t.packages} pkgs)")
85
+
86
+
87
+ def _cmd_fetch(args):
88
+ from ..scraper import fetch_language_status, fetch_pot_file
89
+ from pathlib import Path
90
+
91
+ outdir = Path(args.output)
92
+ outdir.mkdir(parents=True, exist_ok=True)
93
+
94
+ _, packages, _ = fetch_language_status(args.lang)
95
+ print(f"Found {len(packages)} untranslated packages")
96
+
97
+ for pkg in packages:
98
+ pot = fetch_pot_file(pkg.name)
99
+ if pot:
100
+ (outdir / f"{pkg.name}.pot").write_text(pot)
101
+ print(f" ✓ {pkg.name}")
102
+ else:
103
+ print(f" ✗ {pkg.name} (not found)")
104
+
105
+
106
+ def _cmd_reviews(args):
107
+ from ..scraper import fetch_reviews
108
+ reviews = fetch_reviews()
109
+
110
+ if not reviews:
111
+ print("No active reviews found")
112
+ return
113
+
114
+ if args.json:
115
+ import json
116
+ print(json.dumps([{
117
+ "package": r.package,
118
+ "status": r.status,
119
+ "bug": r.bug_number,
120
+ } for r in reviews], indent=2))
121
+ else:
122
+ print(f"{len(reviews)} packages under review:")
123
+ for r in reviews:
124
+ bug = f" (#{r.bug_number})" if r.bug_number else ""
125
+ print(f" {r.package:40s} {r.status:10s}{bug}")
@@ -0,0 +1,77 @@
1
+ from . import _
2
+ """Data models for debconf translation tracking."""
3
+
4
+ import json
5
+ from dataclasses import dataclass, field, asdict
6
+ from enum import IntEnum
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ class TranslationStatus(IntEnum):
12
+ UNTRANSLATED = 0
13
+ IN_PROGRESS = 1
14
+ TRANSLATED = 2
15
+ SUBMITTED = 3 # Sent to BTS
16
+
17
+
18
+ @dataclass
19
+ class DebconfPackage:
20
+ """A Debian package with debconf templates."""
21
+ name: str
22
+ section: str = "main"
23
+ strings_total: int = 0
24
+ strings_translated: int = 0
25
+ popcon_rank: int = 0
26
+ status: TranslationStatus = TranslationStatus.UNTRANSLATED
27
+ pot_url: str = ""
28
+ po_path: str = "" # Local path to .po file
29
+ bts_bug: str = "" # Bug number if submitted
30
+ notes: str = ""
31
+
32
+ @property
33
+ def progress(self) -> float:
34
+ if self.strings_total == 0:
35
+ return 0.0
36
+ return self.strings_translated / self.strings_total * 100
37
+
38
+ @property
39
+ def display_status(self) -> str:
40
+ return {
41
+ TranslationStatus.UNTRANSLATED: _("Untranslated"),
42
+ TranslationStatus.IN_PROGRESS: _("In Progress"),
43
+ TranslationStatus.TRANSLATED: _("Translated"),
44
+ TranslationStatus.SUBMITTED: _("Submitted"),
45
+ }.get(self.status, "?")
46
+
47
+
48
+ @dataclass
49
+ class ReviewItem:
50
+ """A debconf template under review on l10n.debian.org."""
51
+ package: str
52
+ status: str = "" # "bts", "review", etc.
53
+ bug_number: str = ""
54
+ reviewer: str = ""
55
+ date: str = ""
56
+ original_strings: list = field(default_factory=list)
57
+ new_strings: list = field(default_factory=list)
58
+ url: str = ""
59
+
60
+
61
+ @dataclass
62
+ class LanguageStats:
63
+ """Statistics for a language."""
64
+ code: str
65
+ name: str
66
+ translated: int = 0
67
+ total: int = 0
68
+ untranslated_packages: int = 0
69
+ in_progress_packages: int = 0
70
+
71
+ @property
72
+ def progress(self) -> float:
73
+ if self.total == 0:
74
+ return 0.0
75
+ return self.translated / self.total * 100
76
+
77
+