|
2 | 2 | #
|
3 | 3 | # This python file contains utility scripts to manage Python docs Ukrainian translation.
|
4 | 4 | # It has to be run inside the python-docs-uk git root directory.
|
5 |
| -# |
6 |
| -# Inspired by django-docs-translations script by claudep. |
7 |
| -# |
8 |
| -# The following commands are available: |
9 |
| -# |
10 |
| -# * fetch: fetch translations from transifex.com and strip source lines from the |
11 |
| -# files. |
12 |
| -# * recreate_readme: recreate readme to update translation progress. |
13 |
| -# * regenerate_tx_config: recreate configuration for all resources. |
14 | 5 |
|
15 |
| -from argparse import ArgumentParser |
16 |
| -from collections import Counter |
17 | 6 | import os
|
18 |
| -from re import match |
19 |
| -from subprocess import call, run |
20 |
| -import sys |
| 7 | +import re |
| 8 | +from argparse import ArgumentParser |
| 9 | +from pathlib import Path |
21 | 10 |
|
22 |
| -LANGUAGE = 'uk' |
23 |
| -RESOURCE_NAME_MAP = {'glossary_': 'glossary'} |
24 |
| -TX_ORGANISATION = 'python-doc' |
25 |
| -TX_PROJECT = 'python-newest' |
26 |
| -GH_ORGANISATION = 'python' |
27 |
| -GH_PROJECT = 'python-docs-uk' |
28 |
| - |
29 |
| -def fetch(): |
30 |
| - """ |
31 |
| - Fetch translations from Transifex, remove source lines. |
32 |
| - """ |
33 |
| - if call("tx --version", shell=True) != 0: |
34 |
| - sys.stderr.write("The Transifex client app is required (https://developers.transifex.com/docs/cli).\n") |
35 |
| - exit(1) |
36 |
| - lang = LANGUAGE |
37 |
| - pull_returncode = call(f'tx pull -l {lang} --minimum-perc=1 --force --skip', shell=True) |
38 |
| - if pull_returncode != 0: |
39 |
| - exit(pull_returncode) |
40 |
| - for root, _, po_files in os.walk('../..'): |
41 |
| - for po_file in po_files: |
42 |
| - if not po_file.endswith(".po"): |
43 |
| - continue |
44 |
| - po_path = os.path.join(root, po_file) |
45 |
| - call(f'msgcat --no-location -o {po_path} {po_path}', shell=True) |
46 |
| - |
47 |
| - |
48 |
| -def recreate_tx_config(): |
49 |
| - """ |
50 |
| - Regenerate Transifex client config for all resources. |
51 |
| - """ |
52 |
| - resources = _get_resources() |
53 |
| - with open('.tx/config', 'w') as config: |
54 |
| - config.writelines(('[main]\n', 'host = https://www.transifex.com\n',)) |
55 |
| - for resource in resources: |
56 |
| - slug = resource['slug'] |
57 |
| - name = RESOURCE_NAME_MAP.get(slug, slug) |
58 |
| - if slug == '0': |
59 |
| - continue |
60 |
| - elif '--' in slug: |
61 |
| - directory, file_name = name.split('--') |
62 |
| - if match(r'\d+_\d+', file_name): |
63 |
| - file_name = file_name.replace('_', '.') |
64 |
| - config.writelines( |
65 |
| - ( |
66 |
| - '\n', |
67 |
| - f'[{TX_PROJECT}.{slug}]\n', |
68 |
| - f'trans.{LANGUAGE} = {directory}/{file_name}.po\n', |
69 |
| - 'type = PO\n', |
70 |
| - 'source_lang = en\n', |
71 |
| - ) |
72 |
| - ) |
73 |
| - else: |
74 |
| - config.writelines( |
75 |
| - ( |
76 |
| - '\n', |
77 |
| - f'[{TX_PROJECT}.{slug}]\n', |
78 |
| - f'trans.{LANGUAGE} = {name}.po\n', |
79 |
| - 'type = PO\n', |
80 |
| - 'source_lang = en\n', |
81 |
| - ) |
82 |
| - ) |
83 |
| - |
84 |
| - |
85 |
| -def _get_resources(): |
86 |
| - from requests import get |
87 |
| - |
88 |
| - resources = [] |
89 |
| - offset = 0 |
90 |
| - if os.path.exists('.tx/api-key'): |
91 |
| - with open('.tx/api-key') as f: |
92 |
| - transifex_api_key = f.read() |
93 |
| - else: |
94 |
| - transifex_api_key = os.getenv('TX_TOKEN') |
95 |
| - while True: |
96 |
| - response = get( |
97 |
| - f'https://api.transifex.com/organizations/{TX_ORGANISATION}/projects/{TX_PROJECT}/resources/', |
98 |
| - params={'language_code': LANGUAGE, 'offset': offset}, |
99 |
| - auth=('api', transifex_api_key), |
100 |
| - ) |
101 |
| - response.raise_for_status() |
102 |
| - response_list = response.json() |
103 |
| - resources.extend(response_list) |
104 |
| - if len(response_list) < 100: |
105 |
| - break |
106 |
| - offset += len(response_list) |
107 |
| - return resources |
108 |
| - |
109 |
| - |
110 |
| -def _get_unique_translators(): |
111 |
| - process = run( |
112 |
| - ['grep', '-ohP', r'(?<=^# )(.+)(?=, \d+$)', '-r', '.'], |
113 |
| - capture_output=True, |
114 |
| - text=True, |
115 |
| - ) |
116 |
| - translators = [match('(.*)( <.*>)?', t).group(1) for t in process.stdout.splitlines()] |
117 |
| - unique_translators = Counter(translators) |
118 |
| - return unique_translators |
119 |
| - |
120 |
| - |
121 |
| -def recreate_readme(): |
122 |
| - def language_switcher(entry): |
123 |
| - return ( |
124 |
| - entry['name'].startswith('bugs') |
125 |
| - or entry['name'].startswith('tutorial') |
126 |
| - or entry['name'].startswith('library--functions') |
127 |
| - ) |
128 |
| - |
129 |
| - def average(averages, weights): |
130 |
| - return sum([a * w for a, w in zip(averages, weights)]) / sum(weights) |
131 |
| - |
132 |
| - resources = _get_resources() |
133 |
| - filtered = list(filter(language_switcher, resources)) |
134 |
| - average_list = [e['stats']['translated']['percentage'] for e in filtered] |
135 |
| - weights_list = [e['wordcount'] for e in filtered] |
136 |
| - |
137 |
| - language_switcher_status = average(average_list, weights=weights_list) * 100 |
138 |
| - unique_translators = _get_unique_translators() |
139 |
| - number_of_translators = len(unique_translators) |
140 |
| - |
141 |
| - with open('README.md', 'w') as file: |
142 |
| - file.write( |
143 |
| - f'''\ |
144 |
| -Український переклад документації Python |
145 |
| -======================================== |
146 |
| - |
147 |
| - |
148 |
| - |
149 |
| - |
150 |
| -
|
151 |
| -Якщо ви знайшли помилку або маєте пропозицію, |
152 |
| -[додати issue](https://github.com/{GH_ORGANISATION}/{GH_PROJECT}/issues) у цьому проекті або запропонуйте зміни: |
153 |
| -
|
154 |
| -* Зареєструйтесь на платформі [Transifex](https://www.transifex.com/) |
155 |
| -* Перейдіть на сторінку [документації Python](https://www.transifex.com/{TX_ORGANISATION}/{TX_PROJECT}/). |
156 |
| -* Натисніть кнопку „Join Team”, оберіть українську (uk) мову та натисніть „Join” щоб приєднатися до команди. |
157 |
| -* Приєднавшись до команди, виберіть ресурс, що хочете виправити/оновити. |
158 |
| -
|
159 |
| -Додаткову інформацію про використання Transifex дивіться [в документації](https://docs.transifex.com/getting-started-1/translators). |
160 |
| -
|
161 |
| -**Прогрес перекладу** |
162 |
| -
|
163 |
| - |
164 |
| -
|
165 |
| -Українська мова з’явиться в меню вибору мови docs.python.org, [коли будуть повністю перекладені](https://www.python.org/dev/peps/pep-0545/#add-translation-to-the-language-switcher): |
166 |
| -* `bugs`, |
167 |
| -* всі ресурси в каталозі `tutorial`, |
168 |
| -* `library/functions`. |
169 |
| -
|
170 |
| -**Як переглянути останню збірку документації?** |
171 |
| -
|
172 |
| -Завантажте останню створену документацію зі списку артефактів в останній дії GitHub (вкладка Actions). |
173 |
| -Переклади завантажуються з Transifex до цього репозиторію приблизно кожні півгодини. |
174 |
| -Документація на python.org оновлюється приблизно раз на день. |
175 |
| -
|
176 |
| -**Канали зв'язку** |
177 |
| -
|
178 |
| -* [Telegram-чат перекладачів](https://t.me/+dXwqHZ0KPKYyNDc6) |
179 |
| -* [Python translations working group](https://mail.python.org/mailman3/lists/translation.python.org/) |
180 |
| -* [Python Documentation Special Interest Group](https://www.python.org/community/sigs/current/doc-sig/) |
181 |
| -
|
182 |
| -**Ліцензія** |
183 |
| -
|
184 |
| -Запрошуючи вас до спільного створення проекту на платформі Transifex, ми пропонуємо договір на передачу ваших перекладів |
185 |
| -Python Software Foundation [по ліцензії CC0](https://creativecommons.org/publicdomain/zero/1.0/deed.uk). |
186 |
| -Натомість ви побачите, що ви є перекладачем тієї частини, яку ви переклали. |
187 |
| -Ви висловлюєте свою згоду з цією угодою, надаючи свою роботу для включення в документацію. |
188 |
| -
|
189 |
| -**Оновлення локального перекладу** |
190 |
| -
|
191 |
| -* `.github/scripts/manage_translation.py recreate_tx_config` |
192 |
| -* `.github/scripts/manage_translation.py fetch` |
193 |
| -* `.github/scripts/manage_translation.py recreate_readme` |
194 |
| -
|
195 |
| -**Подяка** |
196 |
| -* Maciej Olko - Polish team |
197 |
| -* Julien Palard - French team |
198 |
| -* Tomo Cocoa - Japanese team |
| 11 | +from transifex.api import transifex_api |
199 | 12 |
|
200 |
| -**Внесок спільноти** |
201 | 13 |
|
202 |
| -| Перекладач | Кількість документів | |
203 |
| -|:----------------|:--------------------:| |
204 |
| -''' |
205 |
| - ) |
| 14 | +transifex_api.setup(auth=os.getenv('TX_TOKEN')) |
206 | 15 |
|
207 |
| - file.writelines( |
208 |
| - (f'|{t}|{c}|\n' for (t, c) in unique_translators.most_common()) |
209 |
| - ) |
| 16 | +RESOURCE_NAME_MAP = {'glossary_': 'glossary'} |
| 17 | + |
| 18 | +ORGANISATION_ID = 'o:python-doc' |
| 19 | +PROJECT_ID = 'o:python-doc:p:python-newest' |
| 20 | +LANGUAGE_ID = 'l:uk' |
| 21 | +ORGANISATION = transifex_api.Organization.get(id=ORGANISATION_ID) |
| 22 | +PROJECT = transifex_api.Project.get(id=PROJECT_ID) |
| 23 | +LANGUAGE = transifex_api.Language.get(id=LANGUAGE_ID) |
| 24 | + |
| 25 | + |
| 26 | +def _slug_to_file_path(slug: str) -> Path: |
| 27 | + """Set of rules how to transform slug to translation file path""" |
| 28 | + file_path = RESOURCE_NAME_MAP.get(slug, slug) # Legacy slug to file mapping |
| 29 | + file_path = file_path.replace('--', '/') |
| 30 | + if re.match(r'\d+_\d+', file_path): |
| 31 | + file_path = file_path.replace('_', '.') |
| 32 | + file_path = file_path + '.po' |
| 33 | + return Path(file_path) |
| 34 | + |
| 35 | + |
| 36 | +def recreate_config() -> None: |
| 37 | + """Regenerate Transifex client config for all resources.""" |
| 38 | + resources = transifex_api.Resource.filter(project=PROJECT).all() |
| 39 | + with open('.tx/config', 'w') as fo: |
| 40 | + fo.writelines(('[main]\n', 'host = https://api.transifex.com\n',)) |
| 41 | + for resource in resources: |
| 42 | + path = _slug_to_file_path(resource.slug) |
| 43 | + fo.writelines(( |
| 44 | + '\n', |
| 45 | + f'[{resource.id}]\n', |
| 46 | + f'file_filter = {path}\n', |
| 47 | + 'type = PO\n', |
| 48 | + 'source_lang = en\n', |
| 49 | + )) |
| 50 | + |
| 51 | + |
| 52 | +def recreate_resource_stats() -> None: |
| 53 | + """Create resource stats.""" |
| 54 | + stats = transifex_api.ResourceLanguageStats.filter(project=PROJECT, language=LANGUAGE).all() |
| 55 | + with open('RESOURCE.md', 'w') as fo: |
| 56 | + fo.writelines(('| Файл | Перекладено | Переглянуто | Вичитано |\n', '|:-----|:-----|:-----|:-----|\n')) |
| 57 | + for stat in stats: |
| 58 | + file_name = _slug_to_file_path(stat.id.split(':')[5]) |
| 59 | + translated_pct = round(100 * stat.attributes['translated_words'] / stat.attributes['total_words'], 1) |
| 60 | + reviewed_pct = round(100 * stat.attributes['reviewed_words'] / stat.attributes['total_words'], 1) |
| 61 | + proofread_pct = round(100 * stat.attributes['proofread_words'] / stat.attributes['total_words'], 1) |
| 62 | + fo.writelines(f'| {file_name} | {translated_pct} % | {reviewed_pct} % | {proofread_pct} % |\n') |
| 63 | + |
| 64 | + |
| 65 | +def recreate_team_stats() -> None: |
| 66 | + """Create contributor stats""" |
| 67 | + members = transifex_api.TeamMembership.filter(organization=ORGANISATION, language=LANGUAGE).all() |
| 68 | + |
| 69 | + users = {member.user.id: member.attributes['role'] for member in members} |
| 70 | + translators = dict.fromkeys(users.keys(), 0) |
| 71 | + reviewers = dict.fromkeys(users.keys(), 0) |
| 72 | + proofreaders = dict.fromkeys(users.keys(), 0) |
| 73 | + |
| 74 | + resources = transifex_api.Resource.filter(project=PROJECT).all() |
| 75 | + for resource in resources: |
| 76 | + translations = transifex_api.ResourceTranslation.filter(resource=resource, language=LANGUAGE).all() |
| 77 | + for translation in translations: |
| 78 | + if translation.relationships['translator']: |
| 79 | + translators[translation.relationships['translator']['data']['id']] += 1 |
| 80 | + if translation.relationships['reviewer']: |
| 81 | + reviewers[translation.relationships['reviewer']['data']['id']] += 1 |
| 82 | + if translation.relationships['proofreader']: |
| 83 | + proofreaders[translation.relationships['proofreader']['data']['id']] += 1 |
| 84 | + |
| 85 | + with open('TEAM.md', 'w') as fo: |
| 86 | + fo.writelines(('| | Роль | Переклав | Переглянув | Вичитав |\n', '|:---|:---|:---|:---|:---|\n',)) |
| 87 | + for user, role in users.items(): |
| 88 | + fo.writelines(f"| {user} | {role} | {translators[user]} | {reviewers[user]} | {proofreaders[user]} |\n") |
| 89 | + |
| 90 | + |
| 91 | +def fetch_translations(): |
| 92 | + """Fetch translations from Transifex, remove source lines.""" |
| 93 | + pull_return_code = os.system(f'tx pull -l uk --force --skip') |
| 94 | + if pull_return_code != 0: |
| 95 | + exit(pull_return_code) |
210 | 96 |
|
211 | 97 |
|
212 | 98 | if __name__ == "__main__":
|
213 |
| - RUNNABLE_SCRIPTS = ('fetch', 'recreate_tx_config', 'recreate_readme') |
| 99 | + RUNNABLE_SCRIPTS = ('recreate_config', 'recreate_resource_stats', 'recreate_team_stats', 'fetch_translations') |
214 | 100 |
|
215 | 101 | parser = ArgumentParser()
|
216 | 102 | parser.add_argument('cmd', nargs=1, choices=RUNNABLE_SCRIPTS)
|
|
0 commit comments