ddtp-translate 0.14.4__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.
- ddtp_translate-0.14.4/LICENSE +22 -0
- ddtp_translate-0.14.4/PKG-INFO +84 -0
- ddtp_translate-0.14.4/README.md +67 -0
- ddtp_translate-0.14.4/pyproject.toml +31 -0
- ddtp_translate-0.14.4/setup.cfg +4 -0
- ddtp_translate-0.14.4/src/ddtp_translate/__init__.py +3 -0
- ddtp_translate-0.14.4/src/ddtp_translate/__main__.py +3 -0
- ddtp_translate-0.14.4/src/ddtp_translate/accessibility.py +66 -0
- ddtp_translate-0.14.4/src/ddtp_translate/ddtp_api.py +356 -0
- ddtp_translate-0.14.4/src/ddtp_translate/ddtss_client.py +646 -0
- ddtp_translate-0.14.4/src/ddtp_translate/main.py +3550 -0
- ddtp_translate-0.14.4/src/ddtp_translate/settings.py +95 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/PKG-INFO +84 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/SOURCES.txt +17 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/dependency_links.txt +1 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/entry_points.txt +2 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/requires.txt +3 -0
- ddtp_translate-0.14.4/src/ddtp_translate.egg-info/top_level.txt +1 -0
- ddtp_translate-0.14.4/tests/test_release_sanity.py +25 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
Preamble
|
|
9
|
+
|
|
10
|
+
The GNU General Public License is a free, copyleft license for
|
|
11
|
+
software and other kinds of works.
|
|
12
|
+
|
|
13
|
+
The licenses for most software and other practical works are designed
|
|
14
|
+
to take away your freedom to share and change the works. By contrast,
|
|
15
|
+
the GNU General Public License is intended to guarantee your freedom to
|
|
16
|
+
share and change all versions of a program--to make sure it remains free
|
|
17
|
+
software for all its users. We, the Free Software Foundation, use the
|
|
18
|
+
GNU General Public License for most of our software; it applies also to
|
|
19
|
+
any other work released this way by its authors. You can apply it to
|
|
20
|
+
your programs, too.
|
|
21
|
+
|
|
22
|
+
For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ddtp-translate
|
|
3
|
+
Version: 0.14.4
|
|
4
|
+
Summary: Translate Debian package descriptions via DDTP
|
|
5
|
+
Author-email: Daniel Nylander <daniel@danielnylander.se>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/yeager/ddtp-translate
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Topic :: Software Development :: Localization
|
|
10
|
+
Classifier: Environment :: X11 Applications :: GTK
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Provides-Extra: translate
|
|
15
|
+
Requires-Dist: po-translate; extra == "translate"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# DDTP Translate [](https://github.com/yeager/ddtp-translate)
|
|
19
|
+
|
|
20
|
+
## Screenshot
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
## Description
|
|
25
|
+
|
|
26
|
+
DDTP Translate is a GTK4/Adwaita application for translating Debian package descriptions via the DDTP (Debian Description Translation Project). The application provides an intuitive interface for translators to contribute to Debian package description translations, helping make Debian more accessible to users worldwide.
|
|
27
|
+
|
|
28
|
+
The application streamlines the translation workflow by providing easy access to untranslated descriptions, translation memory features, and quality assurance tools for reviewing existing translations.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- Translate Debian package descriptions
|
|
33
|
+
- Browse untranslated and pending descriptions
|
|
34
|
+
- Review and edit existing translations
|
|
35
|
+
- Translation memory and suggestions
|
|
36
|
+
- Quality assurance tools
|
|
37
|
+
- Modern GTK4/Adwaita interface
|
|
38
|
+
- Offline translation capabilities
|
|
39
|
+
- Batch translation support
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### PyPI / pipx (Debian/Ubuntu)
|
|
44
|
+
|
|
45
|
+
Install GTK4, libadwaita and the system PyGObject bindings first; pip cannot
|
|
46
|
+
install these native system libraries:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
|
|
50
|
+
pipx install --system-site-packages ddtp-translate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### APT Repository (Debian/Ubuntu)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
echo "deb https://yeager.github.io/debian-repo stable main" | sudo tee /etc/apt/sources.list.d/yeager-l10n.list
|
|
58
|
+
sudo apt update
|
|
59
|
+
sudo apt install ddtp-translate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Building from Source
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/yeager/ddtp-translate.git
|
|
66
|
+
cd ddtp-translate
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Translation
|
|
71
|
+
|
|
72
|
+
Translations are maintained through GitHub pull requests. Copy the template
|
|
73
|
+
catalogue in [`po/ddtp-translate.pot`](po/ddtp-translate.pot) to a new locale
|
|
74
|
+
file such as `po/de.po`, translate it with a gettext editor, then open a PR.
|
|
75
|
+
Run `msgfmt --check po/<locale>.po` before submitting it. See
|
|
76
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for the full review workflow.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
GPL-3.0-or-later
|
|
81
|
+
|
|
82
|
+
## Author
|
|
83
|
+
|
|
84
|
+
Daniel Nylander (daniel@danielnylander.se)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# DDTP Translate [](https://github.com/yeager/ddtp-translate)
|
|
2
|
+
|
|
3
|
+
## Screenshot
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
DDTP Translate is a GTK4/Adwaita application for translating Debian package descriptions via the DDTP (Debian Description Translation Project). The application provides an intuitive interface for translators to contribute to Debian package description translations, helping make Debian more accessible to users worldwide.
|
|
10
|
+
|
|
11
|
+
The application streamlines the translation workflow by providing easy access to untranslated descriptions, translation memory features, and quality assurance tools for reviewing existing translations.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Translate Debian package descriptions
|
|
16
|
+
- Browse untranslated and pending descriptions
|
|
17
|
+
- Review and edit existing translations
|
|
18
|
+
- Translation memory and suggestions
|
|
19
|
+
- Quality assurance tools
|
|
20
|
+
- Modern GTK4/Adwaita interface
|
|
21
|
+
- Offline translation capabilities
|
|
22
|
+
- Batch translation support
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### PyPI / pipx (Debian/Ubuntu)
|
|
27
|
+
|
|
28
|
+
Install GTK4, libadwaita and the system PyGObject bindings first; pip cannot
|
|
29
|
+
install these native system libraries:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 pipx
|
|
33
|
+
pipx install --system-site-packages ddtp-translate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### APT Repository (Debian/Ubuntu)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
echo "deb https://yeager.github.io/debian-repo stable main" | sudo tee /etc/apt/sources.list.d/yeager-l10n.list
|
|
41
|
+
sudo apt update
|
|
42
|
+
sudo apt install ddtp-translate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Building from Source
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/yeager/ddtp-translate.git
|
|
49
|
+
cd ddtp-translate
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Translation
|
|
54
|
+
|
|
55
|
+
Translations are maintained through GitHub pull requests. Copy the template
|
|
56
|
+
catalogue in [`po/ddtp-translate.pot`](po/ddtp-translate.pot) to a new locale
|
|
57
|
+
file such as `po/de.po`, translate it with a gettext editor, then open a PR.
|
|
58
|
+
Run `msgfmt --check po/<locale>.po` before submitting it. See
|
|
59
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for the full review workflow.
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
GPL-3.0-or-later
|
|
64
|
+
|
|
65
|
+
## Author
|
|
66
|
+
|
|
67
|
+
Daniel Nylander (daniel@danielnylander.se)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ddtp-translate"
|
|
7
|
+
version = "0.14.4"
|
|
8
|
+
description = "Translate Debian package descriptions via DDTP"
|
|
9
|
+
license = "GPL-3.0-or-later"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Daniel Nylander", email = "daniel@danielnylander.se"},
|
|
12
|
+
]
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development :: Localization",
|
|
18
|
+
"Environment :: X11 Applications :: GTK",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
translate = ["po-translate"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ddtp-translate = "ddtp_translate.main:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/yeager/ddtp-translate"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Accessibility features: zoom, high contrast, ATK."""
|
|
2
|
+
import gi
|
|
3
|
+
gi.require_version('Gtk', '4.0')
|
|
4
|
+
from gi.repository import Gtk, Gdk, Gio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AccessibilityManager:
|
|
8
|
+
"""Manages accessibility features for a GTK4 window."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, window, app=None):
|
|
11
|
+
self._window = window
|
|
12
|
+
self._app = app or window.get_application()
|
|
13
|
+
self._font_scale = 1.0
|
|
14
|
+
self._high_contrast = False
|
|
15
|
+
self._css = Gtk.CssProvider()
|
|
16
|
+
Gtk.StyleContext.add_provider_for_display(
|
|
17
|
+
Gdk.Display.get_default(), self._css,
|
|
18
|
+
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1
|
|
19
|
+
)
|
|
20
|
+
self._setup_actions()
|
|
21
|
+
|
|
22
|
+
def _setup_actions(self):
|
|
23
|
+
if self._app is None:
|
|
24
|
+
return
|
|
25
|
+
actions = [
|
|
26
|
+
('zoom-in', self._zoom_in, ['<Control>plus', '<Control>equal']),
|
|
27
|
+
('zoom-out', self._zoom_out, ['<Control>minus']),
|
|
28
|
+
('zoom-reset', self._zoom_reset, ['<Control>0']),
|
|
29
|
+
('toggle-high-contrast', self._toggle_hc, ['<Control><Shift>h']),
|
|
30
|
+
]
|
|
31
|
+
for name, cb, accels in actions:
|
|
32
|
+
if not self._app.lookup_action(name):
|
|
33
|
+
action = Gio.SimpleAction.new(name, None)
|
|
34
|
+
action.connect('activate', lambda a, p, c=cb: c())
|
|
35
|
+
self._app.add_action(action)
|
|
36
|
+
self._app.set_accels_for_action(f'app.{name}', accels)
|
|
37
|
+
|
|
38
|
+
def _apply_css(self):
|
|
39
|
+
css = f'window {{ font-size: {self._font_scale}em; }}'
|
|
40
|
+
if self._high_contrast:
|
|
41
|
+
css += """
|
|
42
|
+
window.high-contrast {
|
|
43
|
+
border: 2px solid @accent_color;
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
}"""
|
|
46
|
+
self._css.load_from_string(css.encode())
|
|
47
|
+
|
|
48
|
+
def _zoom_in(self):
|
|
49
|
+
self._font_scale = min(self._font_scale + 0.1, 3.0)
|
|
50
|
+
self._apply_css()
|
|
51
|
+
|
|
52
|
+
def _zoom_out(self):
|
|
53
|
+
self._font_scale = max(self._font_scale - 0.1, 0.5)
|
|
54
|
+
self._apply_css()
|
|
55
|
+
|
|
56
|
+
def _zoom_reset(self):
|
|
57
|
+
self._font_scale = 1.0
|
|
58
|
+
self._apply_css()
|
|
59
|
+
|
|
60
|
+
def _toggle_hc(self):
|
|
61
|
+
self._high_contrast = not self._high_contrast
|
|
62
|
+
if self._high_contrast:
|
|
63
|
+
self._window.add_css_class('high-contrast')
|
|
64
|
+
else:
|
|
65
|
+
self._window.remove_css_class('high-contrast')
|
|
66
|
+
self._apply_css()
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""DDTP HTTP API client — fetch untranslated Debian package descriptions."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
DDTP_BASE = "https://ddtp.debian.org/ddt.cgi"
|
|
12
|
+
# Fallback: fetch from Debian mirror i18n Translation files
|
|
13
|
+
MIRROR_BASE = "https://deb.debian.org/debian"
|
|
14
|
+
DISTS = ["sid", "trixie", "bookworm"]
|
|
15
|
+
CACHE_TTL = 86400 # 24 hours
|
|
16
|
+
|
|
17
|
+
# All DDTP-supported language codes
|
|
18
|
+
DDTP_LANGUAGES = [
|
|
19
|
+
("ar", "Arabic"),
|
|
20
|
+
("bg", "Bulgarian"),
|
|
21
|
+
("ca", "Catalan"),
|
|
22
|
+
("cs", "Czech"),
|
|
23
|
+
("da", "Danish"),
|
|
24
|
+
("de", "German"),
|
|
25
|
+
("el", "Greek"),
|
|
26
|
+
("eo", "Esperanto"),
|
|
27
|
+
("es", "Spanish"),
|
|
28
|
+
("eu", "Basque"),
|
|
29
|
+
("fi", "Finnish"),
|
|
30
|
+
("fr", "French"),
|
|
31
|
+
("gl", "Galician"),
|
|
32
|
+
("hu", "Hungarian"),
|
|
33
|
+
("id", "Indonesian"),
|
|
34
|
+
("it", "Italian"),
|
|
35
|
+
("ja", "Japanese"),
|
|
36
|
+
("km", "Khmer"),
|
|
37
|
+
("ko", "Korean"),
|
|
38
|
+
("lt", "Lithuanian"),
|
|
39
|
+
("ml", "Malayalam"),
|
|
40
|
+
("nb", "Norwegian Bokmål"),
|
|
41
|
+
("nl", "Dutch"),
|
|
42
|
+
("pl", "Polish"),
|
|
43
|
+
("pt", "Portuguese"),
|
|
44
|
+
("pt_BR", "Brazilian Portuguese"),
|
|
45
|
+
("ro", "Romanian"),
|
|
46
|
+
("ru", "Russian"),
|
|
47
|
+
("sk", "Slovak"),
|
|
48
|
+
("sl", "Slovenian"),
|
|
49
|
+
("sr", "Serbian"),
|
|
50
|
+
("sv", "Swedish"),
|
|
51
|
+
("th", "Thai"),
|
|
52
|
+
("tr", "Turkish"),
|
|
53
|
+
("uk", "Ukrainian"),
|
|
54
|
+
("vi", "Vietnamese"),
|
|
55
|
+
("zh_CN", "Chinese (Simplified)"),
|
|
56
|
+
("zh_TW", "Chinese (Traditional)"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _cache_dir():
|
|
61
|
+
xdg = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
|
|
62
|
+
d = Path(xdg) / "ddtp-translate"
|
|
63
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
return d
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cache_path(lang):
|
|
68
|
+
return _cache_dir() / f"untranslated_{lang}.json"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_cache_valid(path):
|
|
72
|
+
if not path.exists():
|
|
73
|
+
return False
|
|
74
|
+
return (time.time() - path.stat().st_mtime) < CACHE_TTL
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_description_hash(description):
|
|
78
|
+
"""Compute MD5 hash of the description (used in DDTP email subject)."""
|
|
79
|
+
return hashlib.md5(description.encode("utf-8")).hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_ddtp_response(text):
|
|
83
|
+
"""Parse DDTP ddt.cgi response into list of package dicts.
|
|
84
|
+
|
|
85
|
+
Response format (one per package, separated by blank lines):
|
|
86
|
+
Package: <name>
|
|
87
|
+
Description-md5: <hash>
|
|
88
|
+
Description-en: <short description>
|
|
89
|
+
<long description line 1>
|
|
90
|
+
<long description line 2>
|
|
91
|
+
.
|
|
92
|
+
"""
|
|
93
|
+
packages = []
|
|
94
|
+
current = None
|
|
95
|
+
|
|
96
|
+
for line in text.splitlines():
|
|
97
|
+
if line.startswith("Package: "):
|
|
98
|
+
if current:
|
|
99
|
+
packages.append(current)
|
|
100
|
+
current = {
|
|
101
|
+
"package": line[9:].strip(),
|
|
102
|
+
"md5": "",
|
|
103
|
+
"short": "",
|
|
104
|
+
"long": "",
|
|
105
|
+
}
|
|
106
|
+
elif line.startswith("Description-md5: ") and current:
|
|
107
|
+
current["md5"] = line[17:].strip()
|
|
108
|
+
elif line.startswith("Description-en: ") and current:
|
|
109
|
+
current["short"] = line[16:].strip()
|
|
110
|
+
elif current and (line.startswith(" ") or line.startswith("\t")):
|
|
111
|
+
stripped = line.strip()
|
|
112
|
+
if stripped == ".":
|
|
113
|
+
current["long"] += "\n"
|
|
114
|
+
else:
|
|
115
|
+
if current["long"]:
|
|
116
|
+
current["long"] += "\n"
|
|
117
|
+
current["long"] += stripped
|
|
118
|
+
|
|
119
|
+
if current:
|
|
120
|
+
packages.append(current)
|
|
121
|
+
|
|
122
|
+
return packages
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _fetch_translation_file(dist, lang):
|
|
126
|
+
"""Download and decompress Translation-XX.bz2 from Debian mirror."""
|
|
127
|
+
import bz2
|
|
128
|
+
url = f"{MIRROR_BASE}/dists/{dist}/main/i18n/Translation-{lang}.bz2"
|
|
129
|
+
try:
|
|
130
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ddtp-translate/0.1"})
|
|
131
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
132
|
+
compressed = resp.read()
|
|
133
|
+
return bz2.decompress(compressed).decode("utf-8", errors="replace")
|
|
134
|
+
except Exception:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_translation_file(text):
|
|
139
|
+
"""Parse Debian Translation file into dict of md5 -> {package, short, long}."""
|
|
140
|
+
entries = {}
|
|
141
|
+
current = {}
|
|
142
|
+
in_desc = False
|
|
143
|
+
|
|
144
|
+
for line in text.splitlines():
|
|
145
|
+
if line.startswith("Package: "):
|
|
146
|
+
if current.get("md5"):
|
|
147
|
+
entries[current["md5"]] = current
|
|
148
|
+
current = {"package": line[9:].strip(), "md5": "", "short": "", "long": ""}
|
|
149
|
+
in_desc = False
|
|
150
|
+
elif line.startswith("Description-md5: "):
|
|
151
|
+
current["md5"] = line[17:].strip()
|
|
152
|
+
elif line.startswith("Description-"):
|
|
153
|
+
current["short"] = line.split(": ", 1)[1] if ": " in line else ""
|
|
154
|
+
in_desc = True
|
|
155
|
+
elif in_desc and (line.startswith(" ") or line.startswith("\t")):
|
|
156
|
+
stripped = line.strip()
|
|
157
|
+
if stripped == ".":
|
|
158
|
+
current["long"] += "\n"
|
|
159
|
+
else:
|
|
160
|
+
if current["long"]:
|
|
161
|
+
current["long"] += "\n"
|
|
162
|
+
current["long"] += stripped
|
|
163
|
+
else:
|
|
164
|
+
in_desc = False
|
|
165
|
+
|
|
166
|
+
if current.get("md5"):
|
|
167
|
+
entries[current["md5"]] = current
|
|
168
|
+
return entries
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def fetch_untranslated_from_mirror(lang, dist="sid"):
|
|
172
|
+
"""Fetch untranslated descriptions by comparing en vs lang Translation files."""
|
|
173
|
+
cache = _cache_dir() / f"mirror_untranslated_{lang}_{dist}.json"
|
|
174
|
+
|
|
175
|
+
if _is_cache_valid(cache):
|
|
176
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
177
|
+
return json.load(f)
|
|
178
|
+
|
|
179
|
+
en_text = _fetch_translation_file(dist, "en")
|
|
180
|
+
if not en_text:
|
|
181
|
+
raise RuntimeError(f"Failed to download Translation-en for {dist}")
|
|
182
|
+
|
|
183
|
+
lang_text = _fetch_translation_file(dist, lang)
|
|
184
|
+
|
|
185
|
+
en_entries = _parse_translation_file(en_text)
|
|
186
|
+
lang_entries = _parse_translation_file(lang_text) if lang_text else {}
|
|
187
|
+
|
|
188
|
+
# Untranslated = in en but not in lang
|
|
189
|
+
untranslated = []
|
|
190
|
+
for md5, entry in en_entries.items():
|
|
191
|
+
if md5 not in lang_entries:
|
|
192
|
+
untranslated.append(entry)
|
|
193
|
+
|
|
194
|
+
# Sort by package name
|
|
195
|
+
untranslated.sort(key=lambda x: x.get("package", ""))
|
|
196
|
+
|
|
197
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
198
|
+
json.dump(untranslated, f, ensure_ascii=False, indent=2)
|
|
199
|
+
|
|
200
|
+
return untranslated
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def fetch_untranslated(lang, force_refresh=False):
|
|
204
|
+
"""Fetch untranslated descriptions for a language. Returns list of dicts.
|
|
205
|
+
|
|
206
|
+
Tries ddtp.debian.org first, falls back to Debian mirror comparison.
|
|
207
|
+
"""
|
|
208
|
+
cache = _cache_path(lang)
|
|
209
|
+
|
|
210
|
+
if not force_refresh and _is_cache_valid(cache):
|
|
211
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
212
|
+
return json.load(f)
|
|
213
|
+
|
|
214
|
+
# Try ddtp.debian.org first
|
|
215
|
+
url = f"{DDTP_BASE}?lcode={lang}&getuntranslated=1"
|
|
216
|
+
try:
|
|
217
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ddtp-translate/0.1"})
|
|
218
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
219
|
+
text = resp.read().decode("utf-8", errors="replace")
|
|
220
|
+
packages = parse_ddtp_response(text)
|
|
221
|
+
if packages:
|
|
222
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
223
|
+
json.dump(packages, f, ensure_ascii=False, indent=2)
|
|
224
|
+
return packages
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# Fallback: Debian mirror comparison
|
|
229
|
+
try:
|
|
230
|
+
packages = fetch_untranslated_from_mirror(lang, "sid")
|
|
231
|
+
if packages:
|
|
232
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
233
|
+
json.dump(packages, f, ensure_ascii=False, indent=2)
|
|
234
|
+
return packages
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
# Last resort: stale cache
|
|
239
|
+
if cache.exists():
|
|
240
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
241
|
+
return json.load(f)
|
|
242
|
+
|
|
243
|
+
raise RuntimeError(
|
|
244
|
+
"Could not fetch DDTP data. Both ddtp.debian.org and "
|
|
245
|
+
"the Debian mirror (deb.debian.org) are unreachable. "
|
|
246
|
+
"Please check your internet connection."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def fetch_popcon_data(force_refresh=False):
|
|
251
|
+
"""Fetch Debian popcon (popularity contest) install counts.
|
|
252
|
+
|
|
253
|
+
Returns dict mapping package_name -> install_count.
|
|
254
|
+
Data from https://popcon.debian.org/by_inst.gz
|
|
255
|
+
"""
|
|
256
|
+
import gzip
|
|
257
|
+
cache = _cache_dir() / "popcon_by_inst.json"
|
|
258
|
+
|
|
259
|
+
if not force_refresh and _is_cache_valid(cache):
|
|
260
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
261
|
+
return json.load(f)
|
|
262
|
+
|
|
263
|
+
url = "https://popcon.debian.org/by_inst.gz"
|
|
264
|
+
try:
|
|
265
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ddtp-translate/0.8"})
|
|
266
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
267
|
+
compressed = resp.read()
|
|
268
|
+
text = gzip.decompress(compressed).decode("utf-8", errors="replace")
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
# Try stale cache
|
|
271
|
+
if cache.exists():
|
|
272
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
273
|
+
return json.load(f)
|
|
274
|
+
return {}
|
|
275
|
+
|
|
276
|
+
popcon = {}
|
|
277
|
+
for line in text.splitlines():
|
|
278
|
+
if line.startswith("#") or not line.strip():
|
|
279
|
+
continue
|
|
280
|
+
parts = line.split()
|
|
281
|
+
if len(parts) >= 3:
|
|
282
|
+
try:
|
|
283
|
+
# Format: rank name inst vote old recent no-files
|
|
284
|
+
# or: name inst vote old recent no-files (without rank column)
|
|
285
|
+
# The by_inst format: rank name inst vote old recent no-files
|
|
286
|
+
rank = parts[0]
|
|
287
|
+
if rank.isdigit():
|
|
288
|
+
pkg_name = parts[1]
|
|
289
|
+
inst_count = int(parts[2])
|
|
290
|
+
else:
|
|
291
|
+
pkg_name = parts[0]
|
|
292
|
+
inst_count = int(parts[1])
|
|
293
|
+
popcon[pkg_name] = inst_count
|
|
294
|
+
except (ValueError, IndexError):
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
298
|
+
json.dump(popcon, f, ensure_ascii=False)
|
|
299
|
+
|
|
300
|
+
return popcon
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def fetch_ddtp_stats():
|
|
304
|
+
"""Fetch translation statistics from ddtp.debian.org main page.
|
|
305
|
+
|
|
306
|
+
Returns dict mapping lang_code -> {
|
|
307
|
+
'active_translations': int,
|
|
308
|
+
'previously_translated': int,
|
|
309
|
+
'total_translations': int,
|
|
310
|
+
}
|
|
311
|
+
Also returns total_packages and active_packages as top-level keys.
|
|
312
|
+
"""
|
|
313
|
+
cache = _cache_dir() / "ddtp_stats.json"
|
|
314
|
+
if _is_cache_valid(cache):
|
|
315
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
316
|
+
return json.load(f)
|
|
317
|
+
|
|
318
|
+
url = "https://ddtp.debian.org/"
|
|
319
|
+
try:
|
|
320
|
+
req = urllib.request.Request(url, headers={"User-Agent": "ddtp-translate/0.1"})
|
|
321
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
322
|
+
text = resp.read().decode("utf-8", errors="replace")
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
# Try stale cache
|
|
325
|
+
if cache.exists():
|
|
326
|
+
with open(cache, "r", encoding="utf-8") as f:
|
|
327
|
+
return json.load(f)
|
|
328
|
+
raise RuntimeError(f"Could not fetch DDTP statistics: {exc}")
|
|
329
|
+
|
|
330
|
+
stats = {"languages": {}, "total_packages": 0, "active_packages": 0}
|
|
331
|
+
|
|
332
|
+
# Parse total/active packages
|
|
333
|
+
m = re.search(r"(\d+) package descriptions;", text)
|
|
334
|
+
if m:
|
|
335
|
+
stats["total_packages"] = int(m.group(1))
|
|
336
|
+
m = re.search(r"(\d+) package descriptions are active;", text)
|
|
337
|
+
if m:
|
|
338
|
+
stats["active_packages"] = int(m.group(1))
|
|
339
|
+
|
|
340
|
+
# Parse per-language stats
|
|
341
|
+
# Pattern: lang XX has N (<a ...>M</a>) active translations from T translations;
|
|
342
|
+
for m in re.finditer(
|
|
343
|
+
r'lang (\S+) has (\d+) \(<a[^>]*>(\d+)</a>\) active translations from (\d+) translations;',
|
|
344
|
+
text,
|
|
345
|
+
):
|
|
346
|
+
lang_code = m.group(1)
|
|
347
|
+
stats["languages"][lang_code] = {
|
|
348
|
+
"active_translations": int(m.group(2)),
|
|
349
|
+
"previously_translated": int(m.group(3)),
|
|
350
|
+
"total_translations": int(m.group(4)),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
with open(cache, "w", encoding="utf-8") as f:
|
|
354
|
+
json.dump(stats, f, ensure_ascii=False, indent=2)
|
|
355
|
+
|
|
356
|
+
return stats
|