From 155e3518cf3dbf7a56eefc9428171498f0e755d5 Mon Sep 17 00:00:00 2001 From: Sepehr Rasouli Date: Fri, 18 Sep 2026 17:44:48 +0330 Subject: [PATCH 1/2] Bring glossary-searcher to the python-docs-farsi repository --- .../ISSUE_TEMPLATE/glossary_searcher_bug.md | 56 +++ .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/build-and-deploy.yml | 42 +- CONTRIBUTING.md | 14 +- glossary-searcher/assets/logo.svg | 1 + glossary-searcher/scripts/build_corpus.py | 150 +++++++ glossary-searcher/scripts/build_site.py | 98 +++++ glossary-searcher/scripts/requirements.txt | 1 + glossary-searcher/site/app.js | 247 ++++++++++++ glossary-searcher/site/assets/logo.svg | 1 + glossary-searcher/site/index.html | 54 +++ glossary-searcher/site/style.css | 379 ++++++++++++++++++ library/__main__.po | 2 +- 13 files changed, 1028 insertions(+), 19 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/glossary_searcher_bug.md create mode 100644 glossary-searcher/assets/logo.svg create mode 100644 glossary-searcher/scripts/build_corpus.py create mode 100644 glossary-searcher/scripts/build_site.py create mode 100644 glossary-searcher/scripts/requirements.txt create mode 100644 glossary-searcher/site/app.js create mode 100644 glossary-searcher/site/assets/logo.svg create mode 100644 glossary-searcher/site/index.html create mode 100644 glossary-searcher/site/style.css diff --git a/.github/ISSUE_TEMPLATE/glossary_searcher_bug.md b/.github/ISSUE_TEMPLATE/glossary_searcher_bug.md new file mode 100644 index 00000000..830fae73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/glossary_searcher_bug.md @@ -0,0 +1,56 @@ +--- +name: اشکال در واژه‌یاب +about: گزارش مشکل یا رفتار نادرست در ابزار واژه‌یاب +title: "[واژه‌یاب] " +labels: bug +assignees: '' +--- + +## توضیح مشکل + + + +## مراحل بازتولید + + + +## نتیجه‌ی مورد انتظار + + + +## نتیجه‌ی مشاهده‌شده + + + +## مرورگر و محیط مورد استفاده + + + +## اسکرین‌شات یا اطلاعات تکمیلی + + + +## آیین‌نامه‌ی رفتاری + +- [ ] می‌پذیرم که از [آیین‌نامه‌ی رفتاری PSF](https://www.python.org/psf/conduct/) پیروی کنم. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4c6ee607..07a15da4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,7 +19,7 @@ - [ ] `msgfmt --check` روی پرونده‌های تغییر یافته با موفقیت اجرا شده است - [ ] `python3 scripts/check_markup.py` روی پرونده‌های تغییر یافته اجرا شده است - [ ] نشانه‌گذاری‌های Sphinx (`:class:`، `:func:`، کد درون‌خطی) و جای‌گذارها (`%s`، `{name}`) دست‌نخورده مانده‌اند -- [ ] ترجمه‌ها با [واژه‌نامه (GLOSSARY.md)](https://github.com/python/python-docs-fa/blob/3.14/GLOSSARY.md) هماهنگ است +- [ ] ترجمه‌ها با [واژه‌نامه](http://python.github.io/python-docs-fa/glossary-searcher) هماهنگ است - [ ] رشته‌های `fuzzy` بررسی و در صورت لزوم بازنویسی شده‌اند - [ ] پرونده [CONTRIBUTING.md](https://github.com/python/python-docs-fa/blob/3.14/CONTRIBUTING.md) با دقت خوانده شده است. diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 439c5956..dfb7fc11 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -2,10 +2,11 @@ name: Build and Deploy to GitHub Pages on: schedule: - - cron: '0 2 * * *' + - cron: '0 2 * * *' push: branches: - 3.14 + paths-ignore: [] workflow_dispatch: permissions: @@ -26,21 +27,17 @@ jobs: with: repository: python/cpython ref: v3.14.7 - + - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.12' - + - name: Setup virtual environment run: make venv working-directory: ./Doc - name: Pin Sphinx to 9.1.0 - # make venv may resolve an older Sphinx via requirements.txt; pin - # explicitly so we get the reordered-ref / translated-display-text - # i18n fixes (sphinx-doc/sphinx#14144). Bump this once #14162 lands - # upstream and re-check whether the suppression below is still needed. run: ./venv/bin/pip install "sphinx==9.1.0" working-directory: ./Doc @@ -56,14 +53,39 @@ jobs: - name: Setup problem matcher uses: sphinx-doc/github-problem-matcher@v1.1 - + - name: Build documentation run: make -e SPHINXERRORHANDLING="" SPHINXOPTS="--color -D language='fa' -D gettext_allow_fuzzy_translations=1 -D html_theme_options.is_rtl=true -D suppress_warnings=i18n.inconsistent_references --keep-going" html working-directory: ./Doc - + + # ---- Glossary searcher build starts here ---- + - name: Install glossary build dependencies + run: pip install -r glossary-searcher/scripts/requirements.txt + working-directory: Doc/locales/fa/LC_MESSAGES + + - name: Build glossary corpus & glossary data + run: | + python glossary-searcher/scripts/build_corpus.py \ + --repo-dir . \ + --glossary-tsv glossary.tsv \ + --out-dir glossary-searcher/data + working-directory: Doc/locales/fa/LC_MESSAGES + + - name: Build glossary site + run: | + python glossary-searcher/scripts/build_site.py \ + --site-dir glossary-searcher/site \ + --data-dir glossary-searcher/data \ + --out-dir glossary-searcher/dist + working-directory: Doc/locales/fa/LC_MESSAGES + + - name: Merge glossary site into docs build + run: cp -r Doc/locales/fa/LC_MESSAGES/glossary-searcher/dist Doc/build/html/glossary-searcher + # ---- Glossary searcher build ends here ---- + - name: Setup Pages uses: actions/configure-pages@v4 - + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff6fe156..329c4564 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,8 @@ # راهنمای مشارکت در ترجمه‌ی مستندات پایتون -این راهنما مکمل [README.md](README.md) است و جزئیات فنی و فرایندهای پروژه را توضیح می‌دهد. پیش از شروع، حتماً [README.md](README.md) را بخوانید. +این راهنما مکمل [README.md](README.md) است و جزئیات فنی و فرایندهای مربوط به مشارکت در پروژه را توضیح می‌دهد. پیش از شروع مشارکت، حتماً [README.md](README.md) را مطالعه کنید. + +همچنین واژه‌نامه‌ی پروژه به‌صورت یک وبگاه از طریق [این پیوند](http://python.github.io/python-docs-fa/glossary-searcher) در دسترس است. با جست‌وجوی واژه‌ی انگلیسی مورد نظر، می‌توانید در صورت وجود آن در واژه‌نامه، معادل فارسی ثبت‌شده‌ی آن را به‌همراه نمونه‌های کاربرد آن در پیکره‌ی مستندات مشاهده کنید. تمام مشارکت‌کنندگان موظف‌اند از [آیین‌نامه‌ی رفتاری PSF](https://www.python.org/psf/conduct/) پیروی کنند. این تعهد در همه‌ی ایشیوها و پول‌ریکوئست‌ها به‌صورت یک چک‌باکس ثبت می‌شود. @@ -38,10 +40,11 @@ ## انواع ایشیو -پیش از باز کردن ایشیوی جدید، قالب مناسب را از [صفحه‌ی ایشیوهای پروژه](https://github.com/python/python-docs-fa/issues/new/choose) انتخاب کنید. دو قالب موجود است: +پیش از باز کردن ایشیوی جدید، قالب مناسب را از [صفحه‌ی ایشیوهای پروژه](https://github.com/python/python-docs-fa/issues/new/choose) انتخاب کنید. سه قالب موجود است: - **اشکال در ترجمه:** برای گزارش ترجمه‌ی نادرست یا مشکل‌دار در یک صفحه‌ی منتشرشده. `msgid`، `msgstr` فعلی، و ترجمه‌ی پیشنهادی خود را در قالب وارد کنید. - **پیشنهاد تغییرات در ترجمه:** برای پیشنهاد تغییر در یک واژه یا شیوه‌ی نگارشِ ثابت‌شده (نه یک اشکال ساده). فرایند بررسی این نوع پیشنهاد در بخش [«پیشنهاد تغییر در واژه یا شیوه‌ی نگارش»](#پیشنهاد-تغییر-در-واژه-یا-شیوهی-نگارش) توضیح داده شده است. +- **گزارش اشکال در واژه‌یاب:** برای گزارش اشکالات در واژه‌یاب، از این قالب استفاده کنید. ## فرایند ترجمه @@ -52,7 +55,7 @@ 3. متن `msgid` را ترجمه کنید و در `msgstr` وارد کنید. -4. برای پیدا کردن ترجمه‌ی مناسب برای کلمات و عبارات تخصصی، از [این](https://sepehr-rs.github.io/python-docs-fa-glossary/) پیوند به واژه‌یاب پایتون رفته و واژه/عبارت موردنظر خود را جست‌جو کنید. +4. برای پیدا کردن ترجمه‌ی مناسب برای کلمات و عبارات تخصصی، از [این پیوند](http://python.github.io/python-docs-fa/glossary-searcher) به واژه‌یاب پایتون رفته و واژه/عبارت موردنظر خود را جست‌جو کنید. 5. **نشانه‌گذاری‌های Sphinx** مثل `` :class:`int` ``، `` :func:`repr` ``، `` :ref:`...` ``، `` ``code`` `` و **جای‌گذارها** مثل `%s` یا `{name}` را دقیقاً بدون تغییر نگه دارید؛ فقط متن اطراف آن‌ها ترجمه می‌شود. در `` :term:`target` ``، اگر عبارت داخل بک‌تیک با شناسه‌ی واژه‌نامه یکی است، می‌توانید آن را به شکل `` :term:`ترجمه ` `` بنویسید تا هم متن ترجمه‌شده نمایش داده شود و هم لینک درست کار کند؛ در این حالت فقط بخش نمایشی (پیش از `<`) ترجمه می‌شود و `target` داخل `<>` باید دقیقاً همان شناسه‌ی انگلیسی اصلی (بدون تغییر) باقی بماند، چون تغییر آن لینک را خراب می‌کند. @@ -188,7 +191,4 @@ python3 scripts/update_python_version.py v3.15.0 --keep-src اگر واژه یا اصطلاحی در واژه‌نامه‌ی پروژه وجود ندارد و می‌خواهید معادل پیشنهادی برای آن اضافه شود، یک ایشیو با قالب **«پیشنهاد تغییرات در ترجمه»** باز کنید و واژه‌ی انگلیسی، معادل پیشنهادی، و دلیل انتخاب آن را توضیح دهید. -پیشنهادهای افزودن واژه‌ی جدید نیز پس از بررسی اولیه‌ی نگهدارندگان، در صورت نیاز از همان فرایند نظرسنجی توضیح داده شده در بالا استفاده خواهند کرد تا درباره‌ی پذیرش یا رد آن تصمیم‌گیری شود. - -## گزارش اشکال در واژه‌یاب -لطفا برای گزارش اشکال در واژه‌یاب به مخزن [python-docs-fa-glossary](https://github.com/sepehr-rs/python-docs-fa-glossary) مراجعه کنید. \ No newline at end of file +پیشنهادهای افزودن واژه‌ی جدید نیز پس از بررسی اولیه‌ی نگهدارندگان، در صورت نیاز از همان فرایند نظرسنجی توضیح داده شده در بالا استفاده خواهند کرد تا درباره‌ی پذیرش یا رد آن تصمیم‌گیری شود. \ No newline at end of file diff --git a/glossary-searcher/assets/logo.svg b/glossary-searcher/assets/logo.svg new file mode 100644 index 00000000..05155f37 --- /dev/null +++ b/glossary-searcher/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/glossary-searcher/scripts/build_corpus.py b/glossary-searcher/scripts/build_corpus.py new file mode 100644 index 00000000..f5114585 --- /dev/null +++ b/glossary-searcher/scripts/build_corpus.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Builds corpus.json and glossary.json from the python/python-docs-fa repository. + +corpus.json shape (consumed by the glossary searcher site): + [ { "msgid": "...", "msgstr": "...", "file": "library/functions.po", "line": 123 }, ... ] + +glossary.json shape: + [ { "en": "decorator", "fa": "دکوراتور، آراینده" }, ... ] + +Usage: + python build_corpus.py --repo-dir ./python-docs-fa --glossary-tsv ./glossary.tsv --out-dir ./data +""" +import argparse +import csv +import json +import os +import sys + +try: + import polib +except ImportError: + print("ERROR: polib is required. Install with: pip install polib", file=sys.stderr) + sys.exit(1) + + +def find_po_files(repo_dir): + po_files = [] + for root, _dirs, files in os.walk(repo_dir): + # skip VCS/meta directories + if "/.git" in root or root.endswith("/.git"): + continue + for fname in files: + if fname.endswith(".po"): + full_path = os.path.join(root, fname) + rel_path = os.path.relpath(full_path, repo_dir) + po_files.append((full_path, rel_path)) + return sorted(po_files, key=lambda x: x[1]) + + +def parse_po_files(repo_dir): + """Parse every .po file into flattened msgid/msgstr corpus entries.""" + entries = [] + skipped = 0 + po_files = find_po_files(repo_dir) + + if not po_files: + print(f"WARNING: no .po files found under {repo_dir}", file=sys.stderr) + + for full_path, rel_path in po_files: + try: + po = polib.pofile(full_path) + except Exception as e: + print(f"WARNING: failed to parse {rel_path}: {e}", file=sys.stderr) + skipped += 1 + continue + + for entry in po: + # Skip obsolete, fuzzy, or empty-translation entries -- they + # aren't useful corpus results and fuzzy ones are unreviewed. + if entry.obsolete: + continue + if "fuzzy" in entry.flags: + continue + if not entry.msgid or not entry.msgstr: + continue + + entries.append( + { + "msgid": entry.msgid, + "msgstr": entry.msgstr, + "file": rel_path, + "line": entry.linenum if hasattr(entry, "linenum") else 0, + } + ) + + print( + f"Parsed {len(po_files)} .po files ({skipped} skipped), " + f"{len(entries)} translated entries", + file=sys.stderr, + ) + return entries + + +def parse_glossary_tsv(tsv_path): + """Parse the glossary TSV (EnglishPersian) into glossary.json entries.""" + entries = [] + with open(tsv_path, "r", encoding="utf-8") as f: + reader = csv.reader(f, delimiter="\t") + rows = list(reader) + + if not rows: + return entries + + # Skip header row if it looks like one + start_idx = 1 if rows[0][:2] == ["English", "Persian"] else 0 + + for row in rows[start_idx:]: + if len(row) < 2: + continue + en, fa = row[0].strip(), row[1].strip() + if en and fa: + entries.append({"en": en, "fa": fa}) + + print(f"Parsed {len(entries)} glossary entries", file=sys.stderr) + return entries + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repo-dir", required=True, help="Path to the cloned python-docs-fa checkout" + ) + parser.add_argument( + "--glossary-tsv", + required=True, + help="Path to the glossary TSV file (EnglishPersian)", + ) + parser.add_argument( + "--out-dir", + required=True, + help="Directory to write corpus.json and glossary.json into", + ) + args = parser.parse_args() + + os.makedirs(args.out_dir, exist_ok=True) + + corpus = parse_po_files(args.repo_dir) + glossary = parse_glossary_tsv(args.glossary_tsv) + + corpus_path = os.path.join(args.out_dir, "corpus.json") + glossary_path = os.path.join(args.out_dir, "glossary.json") + + with open(corpus_path, "w", encoding="utf-8") as f: + json.dump(corpus, f, ensure_ascii=False, separators=(",", ":")) + + with open(glossary_path, "w", encoding="utf-8") as f: + json.dump(glossary, f, ensure_ascii=False, separators=(",", ":")) + + print( + f"Wrote {corpus_path} ({os.path.getsize(corpus_path):,} bytes)", file=sys.stderr + ) + print( + f"Wrote {glossary_path} ({os.path.getsize(glossary_path):,} bytes)", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/glossary-searcher/scripts/build_site.py b/glossary-searcher/scripts/build_site.py new file mode 100644 index 00000000..26fd9269 --- /dev/null +++ b/glossary-searcher/scripts/build_site.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" +Assembles the static site into dist/ from site/{index.html,style.css,app.js,assets/} +plus data/{glossary.json,corpus.json}. + +Copies index.html, style.css, app.js, and the assets/ directory as-is, and +generates dist/data.js which sets window.GLOSSARY and window.CORPUS -- the +only generated file. Keeping app.js static (not templated) means it's +cacheable across deploys and easy to lint/test on its own. + +Note: data/glossary.json and data/corpus.json are expected to already exist +(generated by build_corpus.py from glossary.tsv and the .po files). This +script does not regenerate them. + +Usage: + python build_site.py --site-dir site --data-dir data --out-dir dist +""" +import argparse +import json +import os +import shutil +import sys + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--site-dir", + required=True, + help="Directory containing index.html, style.css, app.js, assets/", + ) + parser.add_argument( + "--data-dir", + required=True, + help="Directory containing glossary.json and corpus.json", + ) + parser.add_argument( + "--out-dir", + required=True, + help="Output directory (e.g. dist/) to write the assembled site into", + ) + args = parser.parse_args() + + static_files = ["index.html", "style.css", "app.js"] + for name in static_files: + src = os.path.join(args.site_dir, name) + if not os.path.isfile(src): + print(f"ERROR: missing {src}", file=sys.stderr) + sys.exit(1) + + glossary_path = os.path.join(args.data_dir, "glossary.json") + corpus_path = os.path.join(args.data_dir, "corpus.json") + + with open(glossary_path, "r", encoding="utf-8") as f: + glossary_json = f.read() + json.loads(glossary_json) # validate + + with open(corpus_path, "r", encoding="utf-8") as f: + corpus_json = f.read() + json.loads(corpus_json) # validate + + os.makedirs(args.out_dir, exist_ok=True) + + for name in static_files: + shutil.copyfile( + os.path.join(args.site_dir, name), os.path.join(args.out_dir, name) + ) + print(f"Copied {name}", file=sys.stderr) + + # Copy any static assets (logo, images, etc.) referenced by relative + # paths in index.html/style.css, e.g. assets/logo.svg -> dist/assets/logo.svg + assets_src = os.path.join(args.site_dir, "assets") + if os.path.isdir(assets_src): + assets_dst = os.path.join(args.out_dir, "assets") + if os.path.exists(assets_dst): + shutil.rmtree(assets_dst) + shutil.copytree(assets_src, assets_dst) + copied = sum(len(files) for _, _, files in os.walk(assets_dst)) + print(f"Copied assets/ ({copied} file(s))", file=sys.stderr) + else: + print("No site/assets/ directory found, skipping", file=sys.stderr) + + data_js_path = os.path.join(args.out_dir, "data.js") + with open(data_js_path, "w", encoding="utf-8") as f: + f.write("window.GLOSSARY = ") + f.write(glossary_json) + f.write(";\nwindow.CORPUS = ") + f.write(corpus_json) + f.write(";\n") + + print( + f"Wrote {data_js_path} ({os.path.getsize(data_js_path):,} bytes)", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/glossary-searcher/scripts/requirements.txt b/glossary-searcher/scripts/requirements.txt new file mode 100644 index 00000000..17990d73 --- /dev/null +++ b/glossary-searcher/scripts/requirements.txt @@ -0,0 +1 @@ +polib>=1.2.0 diff --git a/glossary-searcher/site/app.js b/glossary-searcher/site/app.js new file mode 100644 index 00000000..a5b22084 --- /dev/null +++ b/glossary-searcher/site/app.js @@ -0,0 +1,247 @@ +(function(){ + const REPO_URL = "https://github.com/python/python-docs-fa"; + document.getElementById('github-link').href = REPO_URL; + + // window.GLOSSARY / window.CORPUS are defined by data.js, generated at + // build time from data/glossary.json and data/corpus.json. + const GLOSSARY = window.GLOSSARY || []; + // CORPUS entries shape: { msgid, msgstr, file, line } -- produced by + // scripts/build_corpus.py from every .po file in python-docs-fa. + const CORPUS = window.CORPUS || []; + + const MAX_RESULTS = 100; + + // Searches the real corpus for entries whose msgid contains the term + // (case-insensitive). Returns up to MAX_RESULTS matches. + function searchCorpus(term){ + const needle = term.toLowerCase(); + const results = []; + for (let i = 0; i < CORPUS.length && results.length < MAX_RESULTS; i++){ + const entry = CORPUS[i]; + if (entry.msgid.toLowerCase().includes(needle)){ + results.push(entry); + } + } + return results; + } + + // ---------- Simple fuzzy matching for glossary lookup ---------- + function normalize(s){ return s.toLowerCase().trim(); } + + function levenshtein(a, b){ + a = normalize(a); b = normalize(b); + const m = a.length, n = b.length; + if (m === 0) return n; + if (n === 0) return m; + const dp = new Array(n+1); + for (let j=0; j<=n; j++) dp[j] = j; + for (let i=1; i<=m; i++){ + let prev = dp[0]; + dp[0] = i; + for (let j=1; j<=n; j++){ + const tmp = dp[j]; + dp[j] = Math.min( + dp[j] + 1, + dp[j-1] + 1, + prev + (a[i-1] === b[j-1] ? 0 : 1) + ); + prev = tmp; + } + } + return dp[n]; + } + + function similarity(a, b){ + const dist = levenshtein(a, b); + const maxLen = Math.max(a.length, b.length) || 1; + return 1 - dist / maxLen; + } + + function findGlossaryMatch(term){ + const norm = normalize(term); + // exact match first (handles multi-variant "en" keys and comma lists) + for (const entry of GLOSSARY){ + if (normalize(entry.en) === norm) return { entry, score: 1 }; + } + // substring / contains match + let best = null; + for (const entry of GLOSSARY){ + const enNorm = normalize(entry.en); + if (enNorm.includes(norm) || norm.includes(enNorm)){ + const score = Math.min(norm.length, enNorm.length) / Math.max(norm.length, enNorm.length); + if (!best || score > best.score) best = { entry, score }; + } + } + if (best && best.score > 0.5) return best; + // fuzzy fallback (semantic-similarity stand-in) + best = null; + for (const entry of GLOSSARY){ + const score = similarity(term, entry.en); + if (score > 0.72 && (!best || score > best.score)) best = { entry, score }; + } + return best; + } + + // ---------- Rendering ---------- + const hero = document.getElementById('hero'); + const resultsArea = document.getElementById('results-area'); + const input = document.getElementById('search-input'); + + const PAGE_SIZE = 10; + let currentPage = 1; + let currentResults = []; + let currentTerm = ''; + + function escapeHtml(s){ + return s.replace(/&/g,'&').replace(//g,'>'); + } + + function highlight(text, term){ + const escaped = escapeHtml(text); + if (!term) return escaped; + const idx = escaped.toLowerCase().indexOf(term.toLowerCase()); + if (idx === -1) return escaped; + return escaped.slice(0, idx) + '' + escaped.slice(idx, idx+term.length) + '' + escaped.slice(idx+term.length); + } + + function githubContributingDocumentLink(kind, term){ + if (kind === 'report'){ // translation bugs + return "https://github.com/python/python-docs-fa/blob/3.14/CONTRIBUTING.md#%DA%AF%D8%B2%D8%A7%D8%B1%D8%B4-%D8%A7%D8%B4%DA%A9%D8%A7%D9%84-%D8%AF%D8%B1-%D8%AA%D8%B1%D8%AC%D9%85%D9%87"; + } else { + return "https://github.com/python/python-docs-fa/blob/3.14/CONTRIBUTING.md#%D8%A7%D9%81%D8%B2%D9%88%D8%AF%D9%86-%D9%88%D8%A7%DA%98%D9%87-%DB%8C%D8%A7-%D8%A7%D8%B5%D8%B7%D9%84%D8%A7%D8%AD-%D8%AC%D8%AF%DB%8C%D8%AF-%D8%A8%D9%87-%D9%88%D8%A7%DA%98%D9%87%E2%80%8C%D9%86%D8%A7%D9%85%D9%87"; + } + } + + function renderPagination(total){ + const pageCount = Math.ceil(total / PAGE_SIZE); + if (pageCount <= 1) return ''; + let html = ''; + return html; + } + + function renderResultsPage(){ + const start = (currentPage-1)*PAGE_SIZE; + const pageItems = currentResults.slice(start, start+PAGE_SIZE); + const container = document.getElementById('result-list-container'); + if (!container) return; + + if (currentResults.length === 0){ + container.innerHTML = `
+

هیچ نمونه‌ای در پیکره‌ی مستندات برای این واژه پیدا نشد.

+
`; + return; + } + + let html = `
+ ${currentResults.length.toLocaleString()} نمونه یافت شد + صفحه‌ی ${currentPage} از ${Math.ceil(currentResults.length/PAGE_SIZE)} +
`; + html += '
'; + for (const r of pageItems){ + html += `
+
+
${highlight(r.msgid, currentTerm)}
+
${escapeHtml(r.file)}:${r.line}
+
+
${escapeHtml(r.msgstr)}
+
`; + } + html += '
'; + html += renderPagination(currentResults.length); + container.innerHTML = html; + + container.querySelectorAll('.page-btn[data-page]').forEach(btn => { + btn.addEventListener('click', () => { + const p = btn.getAttribute('data-page'); + const pageCount = Math.ceil(currentResults.length/PAGE_SIZE); + if (p === 'prev') currentPage = Math.max(1, currentPage-1); + else if (p === 'next') currentPage = Math.min(pageCount, currentPage+1); + else currentPage = parseInt(p, 10); + renderResultsPage(); + container.scrollIntoView({behavior:'smooth', block:'start'}); + }); + }); + } + + function doSearch(term){ + term = term.trim(); + if (!term) return; + currentTerm = term; + currentPage = 1; + + hero.classList.add('compact'); + resultsArea.classList.add('visible'); + + const match = findGlossaryMatch(term); + currentResults = searchCorpus(term); + + let html = ''; + + if (match){ + html += `
+
واژه‌های مشابه درخواست شما که در واژه‌نامه یافت شدند:
+
+ ${escapeHtml(match.entry.en)} + + ${escapeHtml(match.entry.fa)} +
+ ${match.score < 1 ? `
مطابقت تقریبی (${Math.round(match.score*100)}٪)
` : ''} +
`; + } else { + html += `
+ + این واژه در واژه‌نامه رسمی یافت نشد. نتایج زیر از پیکره‌ی ترجمه‌ی مستندات هستند. + اگر فکر می‌کنید این واژه باید به واژه‌نامه اضافه شود، + یک واژه‌ی جدید پیشنهاد دهید. +
`; + } + + html += `
`; + + html += ``; + + resultsArea.innerHTML = html; + renderResultsPage(); + } + + let debounceTimer; + input.addEventListener('input', () => { + clearTimeout(debounceTimer); + const val = input.value; + if (!val.trim()){ + hero.classList.remove('compact'); + resultsArea.classList.remove('visible'); + resultsArea.innerHTML = ''; + return; + } + debounceTimer = setTimeout(() => doSearch(val), 380); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter'){ + clearTimeout(debounceTimer); + doSearch(input.value); + } + }); +})(); diff --git a/glossary-searcher/site/assets/logo.svg b/glossary-searcher/site/assets/logo.svg new file mode 100644 index 00000000..05155f37 --- /dev/null +++ b/glossary-searcher/site/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/glossary-searcher/site/index.html b/glossary-searcher/site/index.html new file mode 100644 index 00000000..97bbe808 --- /dev/null +++ b/glossary-searcher/site/index.html @@ -0,0 +1,54 @@ + + + + + +واژه‌یاب پایتون + + + + + + + + + + +
+ +

واژه‌یاب پایتون

+

ترجمه‌ی واژگان انگلیسی را در مستندات فارسی پایتون پیدا کنید

+
+ +

جست‌وجو در واژه‌نامه رسمی و پیکره‌ی ترجمه‌ی مستندات پایتون

+
+
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/glossary-searcher/site/style.css b/glossary-searcher/site/style.css new file mode 100644 index 00000000..d5961ec5 --- /dev/null +++ b/glossary-searcher/site/style.css @@ -0,0 +1,379 @@ +:root{ + /* ---- Blues (clean cornflower) ---- */ + --palette-blue-1:#bfdbfe; + --palette-blue-2:#93c5fd; + --palette-blue-3:#3b82f6; + --palette-blue-4:#2563eb; + --palette-blue-5:#1d4ed8; + + /* ---- Greens (clean sage) ---- */ + --palette-green-1:#bbf7d0; + --palette-green-2:#86efac; + --palette-green-3:#4ade80; + --palette-green-4:#22c55e; + --palette-green-5:#16a34a; + + /* ---- Yellows (clean amber) ---- */ + --palette-yellow-1:#fef08a; + --palette-yellow-2:#fde047; + --palette-yellow-3:#facc15; + --palette-yellow-4:#eab308; + --palette-yellow-5:#ca8a04; + + /* ---- Oranges ---- */ + --palette-orange-1:#fed7aa; + --palette-orange-2:#fdba74; + --palette-orange-3:#fb923c; + --palette-orange-4:#f97316; + --palette-orange-5:#ea580c; + + /* ---- Reds ---- */ + --palette-red-1:#fecaca; + --palette-red-2:#fca5a5; + --palette-red-3:#f87171; + --palette-red-4:#ef4444; + --palette-red-5:#dc2626; + + /* ---- Purples ---- */ + --palette-purple-1:#e9d5ff; + --palette-purple-2:#d8b4fe; + --palette-purple-3:#a855f7; + --palette-purple-4:#9333ea; + --palette-purple-5:#7e22ce; + + /* ---- Browns (warm neutral) ---- */ + --palette-brown-1:#e7d5c0; + --palette-brown-2:#d4b896; + --palette-brown-3:#b89470; + --palette-brown-4:#967550; + --palette-brown-5:#735838; + + /* ---- Lights (pure cool gray) ---- */ + --palette-light-1:#ffffff; + --palette-light-2:#f9fafb; + --palette-light-3:#e5e7eb; + --palette-light-4:#d1d5db; + --palette-light-5:#9ca3af; + + /* ---- Darks (clean charcoal) ---- */ + --palette-dark-1:#6b7280; + --palette-dark-2:#4b5563; + --palette-dark-3:#374151; + --palette-dark-4:#1f2937; + --palette-dark-5:#111827; + + /* ---- semantic tokens (light) ---- */ + --bg:var(--palette-light-1); + --bg-soft:var(--palette-light-2); + --ink:var(--palette-dark-5); + --muted:var(--palette-dark-1); + --line:var(--palette-light-3); + --blue:var(--palette-blue-3); + --blue-deep:var(--palette-blue-5); + --yellow:var(--palette-yellow-3); + --card-glossary:#fffbeb; + --card-glossary-line:var(--palette-yellow-4); + + --radius:10px; + --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --fa: 'Vazirmatn', Tahoma, sans-serif; + + /* ---- fluid layout tokens ---- */ + --page-gutter: clamp(20px, 4vw, 56px); + --content-max: 1400px; +} +@media (prefers-color-scheme: dark){ + :root:not([data-theme="light"]){ + --bg:var(--palette-dark-5); + --bg-soft:var(--palette-dark-4); + --ink:var(--palette-light-2); + --muted:var(--palette-light-5); + --line:var(--palette-dark-3); + --blue:var(--palette-blue-2); + --blue-deep:var(--palette-blue-1); + --card-glossary:#1c1a0a; + --card-glossary-line:#44380a; + } +} +:root[data-theme="dark"]{ + --bg:var(--palette-dark-5); + --bg-soft:var(--palette-dark-4); + --ink:var(--palette-light-2); + --muted:var(--palette-light-5); + --line:var(--palette-dark-3); + --blue:var(--palette-blue-2); + --blue-deep:var(--palette-blue-1); + --card-glossary:#1c1a0a; + --card-glossary-line:#44380a; +} + +* { box-sizing: border-box; } +html,body{ margin:0; padding:0; width:100%; } +body[dir="rtl"]{ + direction: rtl; + text-align: right; +} +body{ + background:var(--bg); + color:var(--ink); + font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + min-height:100vh; + width:100%; + display:flex; + flex-direction:column; +} + +.hero-sub, +.search-hint, +.no-glossary-note, +.glossary-fa, +.result-fa, +.footer{ + direction: rtl; + unicode-bidi: plaintext; +} + +a { color: var(--blue-deep); text-decoration:none; } +a:hover { text-decoration:underline; } +a:focus-visible, button:focus-visible, input:focus-visible { + outline: 2px solid var(--blue); outline-offset:2px; +} + +/* ---- Topbar ---- */ +.topbar{ + display:flex; align-items:center; justify-content:space-between; + padding: 18px var(--page-gutter); + width:100%; +} +.topbar-links{ + direction: ltr; + display:flex; align-items:center; gap:18px; +} +.icon-link{ + display:flex; align-items:center; gap:7px; + color:var(--muted); font-size:14px; font-weight:500; +} +.icon-link:hover{ color:var(--ink); text-decoration:none; } +.icon-link svg{ width:18px; height:18px; flex-shrink:0; } + +/* ---- Hero / Search ---- */ +.hero{ + flex:1; + width:100%; + display:flex; flex-direction:column; align-items:center; justify-content:center; + padding: 40px var(--page-gutter) 60px; + transition: padding 0.3s ease; +} +.hero.compact{ + flex:none; + padding: 28px var(--page-gutter) 20px; +} +.py-logo{ + width:56px; height:56px; margin-bottom:18px; + transition: width 0.3s ease, height 0.3s ease, margin 0.3s ease; +} +.hero.compact .py-logo{ width:34px; height:34px; margin-bottom:10px; } + +.hero-title{ + font-size:26px; font-weight:600; letter-spacing:-0.01em; + margin: 0 0 4px; + text-align:center; +} +.hero-sub{ + font-size:15px; color:var(--muted); margin:0 0 28px; + text-align:center; +} +.hero.compact .hero-title, .hero.compact .hero-sub{ display:none; } + +.search-wrap{ + width:100%; max-width:min(640px, 90vw); position:relative; +} +.search-box{ + direction: rtl; + width:100%; + display:flex; align-items:center; gap:10px; + background: var(--bg-soft); + border: 1.5px solid var(--line); + border-radius: 999px; + padding: 13px 20px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} +.search-box:focus-within{ + border-color: var(--blue); + box-shadow: 0 0 0 3px rgba(59,130,246,0.15); +} +.search-box svg{ width:19px; height:19px; color:var(--muted); flex-shrink:0; } +.search-box input{ + flex:1; border:0; background:transparent; outline:none; + font-size:16px; color:var(--ink); font-family: var(--mono); +} +.search-box input::placeholder{ + color: var(--muted); + font-family: var(--fa); + direction: rtl; + text-align: right; + unicode-bidi: plaintext; +} + +.search-hint{ + text-align:center; font-size:12.5px; color:var(--muted); + margin-top:10px; +} +.hero.compact .search-hint{ display:none; } + +/* ---- Results area ---- */ +.results-area{ + width:100%; + max-width: var(--content-max); + margin: 0 auto; + padding: 0 var(--page-gutter) 80px; + display:none; +} +.results-area.visible{ display:block; } + +.empty-state{ + text-align:center; padding: 60px 20px; color:var(--muted); +} +.empty-state svg{ width:40px; height:40px; margin-bottom:14px; opacity:0.5; } +.empty-state p{ margin:0; font-size:14.5px; } + +.no-glossary-note{ + display:flex; align-items:flex-start; gap:10px; + background: var(--bg-soft); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 14px 16px; + margin-bottom: 22px; + font-size: 13.5px; + color: var(--muted); +} +.no-glossary-note svg{ width:16px; height:16px; flex-shrink:0; margin-top:2px; color:var(--blue); } +.no-glossary-note a{ font-weight:600; } + +/* ---- Glossary match card ---- */ +.glossary-card{ + direction: rtl; + background: var(--card-glossary); + border: 1.5px solid var(--card-glossary-line); + border-radius: var(--radius); + padding: 20px 22px; + margin-bottom: 24px; +} +.glossary-card-label{ + direction: rtl; + font-size:11px; font-weight:700; letter-spacing:0.03em; + color:var(--palette-yellow-5); margin-bottom:10px; +} +:root[data-theme="dark"] .glossary-card-label{ color:var(--palette-yellow-3); } +@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]) .glossary-card-label{ color:var(--palette-yellow-3); } } + +.glossary-card-term{ + direction: rtl; + display:flex; + flex-direction: row-reverse; + align-items:baseline; + gap:14px; + flex-wrap:wrap; + margin-bottom: 6px; +} +.glossary-en{ + font-family: var(--mono); font-size:19px; font-weight:600; +} +.glossary-arrow{ color:var(--muted); font-size:15px; } +.glossary-fa{ + font-size:19px; font-weight:600; +} +.glossary-match-score{ + text-align: left; + font-size:13.5px; color:var(--muted); margin-top:8px; +} + +/* ---- Result rows ---- */ +.results-count{ + direction:rtl; + font-size:13px; color:var(--muted); margin: 4px 0 14px; + display:flex; justify-content:space-between; align-items:center; +} +.result-list{ border-top:1px solid var(--line); } +.result-row{ + display:flex; gap:18px; padding: 14px 4px; + border-bottom:1px solid var(--line); + align-items:flex-start; +} +.result-row:hover{ background: var(--bg-soft); } +.result-en{ + flex: 1 1 45%; + font-family: var(--mono); font-size:14px; line-height:1.6; + text-align:left; + word-break: break-word; +} +.result-en mark{ + background: rgba(250,204,21,0.40); + color:inherit; border-radius:3px; padding:0 2px; +} +:root[data-theme="dark"] .result-en mark{ background: rgba(250,204,21,0.22); } +@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]) .result-en mark{ background: rgba(250,204,21,0.22); } } + +.result-fa{ + flex: 1 1 55%; + font-size:14.5px; line-height:1.8; + text-align:right; + word-break: break-word; +} +.result-source{ + font-size:11px; color:var(--muted); margin-top:5px; font-family: var(--mono); + text-align:left; +} + +/* ---- Pagination ---- */ +.pagination{ + display:flex; align-items:center; justify-content:center; gap:6px; + margin-top:24px; flex-wrap:wrap; +} +.page-btn{ + min-width:34px; height:34px; padding:0 8px; + border:1px solid var(--line); background:var(--bg); + border-radius:7px; font-size:13.5px; color:var(--ink); + cursor:pointer; font-family: inherit; +} +.page-btn:hover:not(:disabled){ border-color:var(--blue); color:var(--blue); } +.page-btn:disabled{ opacity:0.35; cursor:default; } +.page-btn.active{ background:var(--blue); border-color:var(--blue); color:#fff; font-weight:600; } +.page-ellipsis{ color:var(--muted); padding:0 4px; font-size:13px; } + +/* ---- Feedback prompts ---- */ +.prompt-row{ + justify-content: flex-end; + display:flex; flex-wrap:wrap; gap:10px; + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid var(--line); +} +.prompt-btn{ + direction: rtl; + display:inline-flex; align-items:center; gap:7px; + font-size:13px; font-weight:500; + padding: 9px 14px; + border:1px solid var(--line); border-radius:8px; + background:var(--bg-soft); color:var(--ink); + cursor:pointer; +} +.prompt-btn:hover{ border-color:var(--blue); color:var(--blue); text-decoration:none; } +.prompt-btn svg{ width:14px; height:14px; } +.prompt-btn.suggest-new{ + border-color: var(--card-glossary-line); + background: var(--card-glossary); +} + +/* ---- Footer ---- */ +.footer{ + direction: rtl; + text-align:center; padding: 20px; font-size:12px; color:var(--muted); +} +.footer span{ display:inline-block; } + +@media (max-width: 560px){ + .result-row{ flex-direction:column; gap:6px; } + .result-fa, .result-en{ flex-basis:auto; text-align:inherit; } + .glossary-card-term{ flex-direction:column; gap:4px; } +} \ No newline at end of file diff --git a/library/__main__.po b/library/__main__.po index a4c4e202..3be712a1 100644 --- a/library/__main__.po +++ b/library/__main__.po @@ -302,7 +302,7 @@ msgstr "" "'asyncio.__main__'" msgid "This won't work for ``__main__.py`` files in the root directory of a ``.zip`` file though. Hence, for consistency, a minimal ``__main__.py`` without a ``__name__`` check is preferred." -msgstr "با این حال، این برای فایل‌های ``__main__.py`` در پوشهٔ ریشهٔ یک فایل ``.zip`` کار نخواهد کرد. بنابراین، برای یکدستی، یک ``__main__.py`` حداقلی بدون بررسی ``__name__`` ترجیح داده می‌شود." +msgstr "با این حال، این برای فایل‌های ``__main__.py`` در پوشه‌ی ریشه‌ی یک فایل ``.zip`` کار نخواهد کرد. بنابراین، برای یکدستی، یک ``__main__.py`` حداقلی بدون بررسی ``__name__`` ترجیح داده می‌شود." msgid "See :mod:`venv` for an example of a package with a minimal ``__main__.py`` in the standard library. It doesn't contain a ``if __name__ == '__main__'`` block. You can invoke it with ``python -m venv [directory]``." msgstr "برای مثال یک بسته با یک ``__main__.py`` حداقلی در کتابخانه‌ی استاندارد، به :mod:`venv` مراجعه کنید. این بسته شامل یک بلوک ``if __name__ == '__main__'`` نمی‌شود. می‌توانید آن را با ``python -m venv [directory]`` فراخوانی کنید." From d0de8908c1afb0bb1aa4db83d02f69eb1f16d940 Mon Sep 17 00:00:00 2001 From: Sepehr Rasouli Date: Fri, 18 Sep 2026 18:45:54 +0330 Subject: [PATCH 2/2] Update README and CONTRIBUTING --- CONTRIBUTING.md | 4 +++- README.md | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 329c4564..c4045fb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -191,4 +191,6 @@ python3 scripts/update_python_version.py v3.15.0 --keep-src اگر واژه یا اصطلاحی در واژه‌نامه‌ی پروژه وجود ندارد و می‌خواهید معادل پیشنهادی برای آن اضافه شود، یک ایشیو با قالب **«پیشنهاد تغییرات در ترجمه»** باز کنید و واژه‌ی انگلیسی، معادل پیشنهادی، و دلیل انتخاب آن را توضیح دهید. -پیشنهادهای افزودن واژه‌ی جدید نیز پس از بررسی اولیه‌ی نگهدارندگان، در صورت نیاز از همان فرایند نظرسنجی توضیح داده شده در بالا استفاده خواهند کرد تا درباره‌ی پذیرش یا رد آن تصمیم‌گیری شود. \ No newline at end of file +پیشنهادهای افزودن واژه‌ی جدید نیز پس از بررسی اولیه‌ی نگهدارندگان، در صورت نیاز از همان فرایند نظرسنجی توضیح داده شده در بالا استفاده خواهند کرد تا درباره‌ی پذیرش یا رد آن تصمیم‌گیری شود. + +مشارکت‌ها به واژه‌نامه باید به پرونده‌ی `glossary.tsv` انجام شوند. \ No newline at end of file diff --git a/README.md b/README.md index 1039071c..683d0320 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,11 @@ پیش از شروع، حتماً نگاهی به این پرونده‌ها بیندازید: - [CONTRIBUTING.md](CONTRIBUTING.md) — راهنمای کامل مشارکت: انواع ایشیو، نکات نگارشی و تایپوگرافی فارسی، سطح رسمیت و لحن نوشتار، اندازه‌ی پول‌ریکوئست، فرایند بازبینی و تأیید پول‌ریکوئست، همگام‌سازی با نسخه‌های جدید پایتون (`scripts/update_python_version.py`)، پاک‌سازی رشته‌های `fuzzy` و نگهداری اعتبار مترجمان در سرآیند پرونده‌های `.po`. -- [GLOSSARY.md](GLOSSARY.md) — واژه‌نامه‌ی معادل‌های فارسی اصطلاحات تخصصی؛ هنگام ترجمه باید به آن پایبند باشید. + +- [واژه‌یاب](http://python.github.io/python-docs-fa/glossary-searcher) — ابزار جست‌وجوی واژگان تخصصی مستندات پایتون؛ هنگام ترجمه می‌توانید از آن برای یافتن معادل‌های فارسی ثبت‌شده و مشاهده‌ی نمونه‌های کاربرد واژگان در پیکره‌ی مستندات استفاده کنید. + - [TEAM.md](TEAM.md) — فهرست هماهنگ‌کننده‌ها، بازبین‌ها و مترجمان به‌همراه آمار مشارکت. + - [STATUS.md](STATUS.md) — جدول وضعیت ترجمه‌ی پرونده‌ها که به‌صورت خودکار به‌روزرسانی می‌شود. ### شاخه‌های نسخه