Skip to content

Commit 929849d

Browse files
committed
Add Transifex glossary terms as hunspell personal dict
1 parent 2f1e110 commit 929849d

3 files changed

Lines changed: 100 additions & 2 deletions

File tree

‎.github/workflows/spellcheck.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ on:
1313
description: Translation branch updated by the calling workflow
1414
required: true
1515
type: string
16+
secrets:
17+
TX_TOKEN:
18+
description: Transifex API token with access to the Polish terminology glossary
19+
required: true
1620

1721
permissions:
1822
contents: read
1923

2024
jobs:
2125
pospell:
2226
runs-on: ubuntu-latest
27+
timeout-minutes: 15
2328
defaults:
2429
run:
2530
working-directory: cpython/Doc
@@ -48,7 +53,7 @@ jobs:
4853
make venv
4954
# Pospell needs newer Docutils than the Sphinx used by older branches.
5055
uv venv "$RUNNER_TEMP/spellcheck-venv"
51-
uv pip install --python "$RUNNER_TEMP/spellcheck-venv/bin/python" sphinx-intl pospell
56+
uv pip install --python "$RUNNER_TEMP/spellcheck-venv/bin/python" sphinx-intl pospell transifex-python six
5257
- name: Prepare UTF-8 Polish dictionary
5358
# The packaged dictionary uses ISO-8859-2, which cannot represent all prose.
5459
run: |
@@ -59,6 +64,14 @@ jobs:
5964
sed -i 's/^SET .*/SET UTF-8/' "$RUNNER_TEMP/hunspell-utf8/pl_PL.aff"
6065
iconv -f "$dictionary_encoding" -t UTF-8 /usr/share/hunspell/pl_PL.dic \
6166
> "$RUNNER_TEMP/hunspell-utf8/pl_PL.dic"
67+
- name: Download Polish terminology glossary
68+
working-directory: cpython/Doc/locales/pl/LC_MESSAGES
69+
env:
70+
TX_TOKEN: ${{ secrets.TX_TOKEN }}
71+
run: >
72+
"$RUNNER_TEMP/spellcheck-venv/bin/python"
73+
manage_translation.py fetch_glossary
74+
--output "$RUNNER_TEMP/hunspell-glossary.dic"
6275
- name: Generate POT files without literal blocks
6376
run: |
6477
if make -n gettext >/dev/null 2>&1; then
@@ -78,5 +91,6 @@ jobs:
7891
DICPATH: ${{ runner.temp }}/hunspell-utf8
7992
run: >
8093
"$RUNNER_TEMP/spellcheck-venv/bin/pospell" --language pl_PL
94+
--personal-dict "$RUNNER_TEMP/hunspell-glossary.dic"
8195
--glob 'locales/pl/LC_MESSAGES/**/*.po'
8296
# PO updates exist only in this disposable checkout; nothing is committed.

‎.github/workflows/update-and-build.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ jobs:
8282
with:
8383
version: ${{ github.ref_name }}
8484
translations-ref: ${{ github.ref_name }}
85+
secrets:
86+
TX_TOKEN: ${{ secrets.TX_TOKEN }}
8587

8688
build:
8789
runs-on: ubuntu-latest

‎manage_translation.py‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,33 @@
1212
# * recreate_tx_config: recreate configuration for all resources.
1313
# * warn_about_files_to_delete: lists files that are not available upstream
1414
# * generate_commit_msg: generates commit message with co-authors
15+
# * fetch_glossary: download Polish terminology as a Hunspell personal dictionary
1516

1617
from argparse import ArgumentParser
18+
import csv
19+
import io
1720
import os
1821
from contextlib import chdir
1922
from dataclasses import dataclass
2023
from pathlib import Path
24+
import re
2125
from subprocess import call, run, CalledProcessError
2226
import sys
2327
from tempfile import TemporaryDirectory
28+
import time
2429
from typing import Self, Iterable
30+
import unicodedata
2531
from warnings import warn
2632

2733
from polib import pofile, POFile
34+
import requests
2835
from transifex.api import transifex_api
36+
from transifex.api.jsonapi.exceptions import JsonApiException
2937

3038
LANGUAGE = 'pl'
3139
PROJECT_SLUG = 'python-newest'
3240
VERSION = '3.14'
41+
GLOSSARY_ID = 'o:python-doc:g:python_global'
3342

3443

3544
def fetch():
@@ -45,6 +54,59 @@ def fetch():
4554
_call(f'msgcat --no-location -o {file} {file}')
4655

4756

57+
def _download_glossary(token: str, *, max_wait: float = 300) -> str:
58+
transifex_api.setup(auth=token)
59+
# The SDK supports new API resource types before adding dedicated classes.
60+
download = transifex_api.new(
61+
type='glossaries_async_downloads',
62+
glossary=transifex_api.new(type='glossaries', id=GLOSSARY_ID),
63+
language=transifex_api.Language(id=f'l:{LANGUAGE}'),
64+
)
65+
download.save()
66+
deadline = time.monotonic() + max_wait
67+
while not download.redirect:
68+
status = download.attributes['status']
69+
if status == 'failed':
70+
raise ValueError('Transifex glossary export failed.')
71+
if status not in {'pending', 'processing'}:
72+
raise ValueError(f'Unexpected glossary export status: {status!r}')
73+
if time.monotonic() >= deadline:
74+
raise ValueError('Transifex glossary export timed out.')
75+
time.sleep(3)
76+
download.reload()
77+
# Download the CSV without sending the API token to the signed storage URL.
78+
export = requests.get(download.redirect, timeout=30)
79+
export.raise_for_status()
80+
return export.content.decode('utf-8-sig')
81+
82+
83+
def _glossary_words(content: str) -> list[str]:
84+
reader = csv.DictReader(io.StringIO(content))
85+
translation_column = f'translation_{LANGUAGE}'
86+
if not reader.fieldnames or translation_column not in reader.fieldnames:
87+
raise ValueError('Transifex glossary CSV has no Polish translation column.')
88+
words = {
89+
word
90+
for row in reader
91+
for word in re.findall(
92+
r'[^\W\d_]+', unicodedata.normalize('NFC', row[translation_column] or '')
93+
)
94+
}
95+
if not words:
96+
raise ValueError('Transifex glossary has no Polish terms.')
97+
return sorted(words)
98+
99+
100+
def fetch_glossary(output: Path):
101+
"""Fetch Polish terminology and write a UTF-8 Hunspell personal dictionary."""
102+
token = _get_tx_token().strip()
103+
if not token:
104+
raise ValueError('TX_TOKEN is required to download the Transifex glossary.')
105+
words = _glossary_words(_download_glossary(token))
106+
output.write_text(f'{len(words)}\n' + '\n'.join(words) + '\n', encoding='utf-8')
107+
print(f'Added {len(words)} Polish glossary words to {output}.')
108+
109+
48110
def _call(command: str):
49111
if (return_code := call(command, shell=True)) != 0:
50112
exit(return_code)
@@ -223,10 +285,30 @@ def generate_commit_msg():
223285
'recreate_tx_config',
224286
'warn_about_files_to_delete',
225287
'generate_commit_msg',
288+
'fetch_glossary',
226289
)
227290

228291
parser = ArgumentParser()
229292
parser.add_argument('cmd', choices=RUNNABLE_SCRIPTS)
293+
parser.add_argument(
294+
'--output', type=Path, help='Hunspell dictionary path for fetch_glossary'
295+
)
230296
options = parser.parse_args()
231297

232-
eval(options.cmd)()
298+
if options.cmd == 'fetch_glossary':
299+
if options.output is None:
300+
parser.error('--output is required for fetch_glossary')
301+
try:
302+
fetch_glossary(options.output)
303+
except (requests.RequestException, JsonApiException) as error:
304+
# Exceptions may contain signed URLs; report the status without those URLs.
305+
status = (
306+
error.response.status_code
307+
if error.response is not None
308+
else 'network error'
309+
)
310+
parser.exit(1, f'Transifex glossary request failed: {status}.\n')
311+
except (ValueError, KeyError) as error:
312+
parser.exit(1, f'Invalid Transifex glossary export: {error}\n')
313+
else:
314+
eval(options.cmd)()

0 commit comments

Comments
 (0)