├── requirements.txt ├── .gitignore ├── .github ├── workflows │ ├── python-37.yml │ ├── python-38.yml │ ├── python-39.yml │ ├── python-310.yml │ ├── python-311.yml │ ├── python-312.yml │ ├── python-314.yml │ ├── python-313.yml │ ├── sync.yml │ └── check.yml └── dependabot.yml ├── scripts ├── potodo.sh ├── build.sh ├── lint.sh ├── prepmsg.sh ├── setup.sh ├── commit.sh ├── generate_templates.sh ├── pull_translations.sh ├── stats.py ├── tx_stats.py ├── generate_commit_msg.py └── generate_txconfig.py ├── LICENSE └── README.rst /requirements.txt: -------------------------------------------------------------------------------- 1 | polib==1.2.0 2 | pomerge==0.2.1 3 | potodo==0.35 4 | powrap==1.0.2 5 | sphinx-intl==2.3.2 6 | sphinx-lint==1.0.2 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Do not include content specific for versioned branches 2 | *.po 3 | *.po~ 4 | *.mo 5 | *.pot 6 | stats.json 7 | potodo.md 8 | .tx/config 9 | 10 | # Do not include virtual environments 11 | venv/ 12 | .venv/ 13 | 14 | # Do not include temporary directories 15 | logs/ 16 | .potodo/ 17 | cpython 18 | -------------------------------------------------------------------------------- /.github/workflows/python-37.yml: -------------------------------------------------------------------------------- 1 | name: python-37 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | sync: 8 | uses: ./.github/workflows/sync.yml 9 | with: 10 | tx_project: ${{ github.workflow }} 11 | version: 3.7 12 | secrets: inherit 13 | check: 14 | uses: ./.github/workflows/check.yml 15 | needs: sync 16 | with: 17 | tx_project: ${{ github.workflow }} 18 | version: 3.7 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/python-38.yml: -------------------------------------------------------------------------------- 1 | name: python-38 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | sync: 8 | uses: ./.github/workflows/sync.yml 9 | with: 10 | tx_project: ${{ github.workflow }} 11 | version: 3.8 12 | secrets: inherit 13 | check: 14 | uses: ./.github/workflows/check.yml 15 | needs: sync 16 | with: 17 | tx_project: ${{ github.workflow }} 18 | version: 3.8 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/python-39.yml: -------------------------------------------------------------------------------- 1 | name: python-39 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | sync: 8 | uses: ./.github/workflows/sync.yml 9 | with: 10 | tx_project: ${{ github.workflow }} 11 | version: 3.9 12 | secrets: inherit 13 | check: 14 | uses: ./.github/workflows/check.yml 15 | needs: sync 16 | with: 17 | tx_project: ${{ github.workflow }} 18 | version: 3.9 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/python-310.yml: -------------------------------------------------------------------------------- 1 | name: python-310 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | sync: 10 | uses: ./.github/workflows/sync.yml 11 | with: 12 | tx_project: ${{ github.workflow }} 13 | version: '3.10' 14 | secrets: inherit 15 | check: 16 | uses: ./.github/workflows/check.yml 17 | needs: sync 18 | with: 19 | tx_project: ${{ github.workflow }} 20 | version: '3.10' 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/python-311.yml: -------------------------------------------------------------------------------- 1 | name: python-311 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '45 23 * * *' 7 | 8 | jobs: 9 | sync: 10 | uses: ./.github/workflows/sync.yml 11 | with: 12 | tx_project: ${{ github.workflow }} 13 | version: '3.11' 14 | secrets: inherit 15 | check: 16 | uses: ./.github/workflows/check.yml 17 | needs: sync 18 | with: 19 | tx_project: ${{ github.workflow }} 20 | version: '3.11' 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/python-312.yml: -------------------------------------------------------------------------------- 1 | name: python-312 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '30 23 * * *' 7 | 8 | jobs: 9 | sync: 10 | uses: ./.github/workflows/sync.yml 11 | with: 12 | tx_project: ${{ github.workflow }} 13 | version: '3.12' 14 | secrets: inherit 15 | check: 16 | uses: ./.github/workflows/check.yml 17 | needs: sync 18 | with: 19 | tx_project: ${{ github.workflow }} 20 | version: '3.12' 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/python-314.yml: -------------------------------------------------------------------------------- 1 | name: python-314 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 23 * * *' 7 | pull_request: 8 | branches: 9 | - main 10 | - '3.14' 11 | push: 12 | branches: 13 | - main 14 | - '3.14' 15 | 16 | jobs: 17 | sync: 18 | uses: ./.github/workflows/sync.yml 19 | with: 20 | tx_project: python-newest 21 | version: 3.14 22 | secrets: inherit 23 | check: 24 | uses: ./.github/workflows/check.yml 25 | needs: sync 26 | with: 27 | tx_project: python-newest 28 | version: 3.14 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | assignees: 8 | - "rffontenelle" 9 | commit-message: 10 | prefix: "gha" 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | 16 | - package-ecosystem: "pip" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | assignees: 21 | - "rffontenelle" 22 | commit-message: 23 | prefix: "pip" 24 | groups: 25 | pip-packages: 26 | patterns: 27 | - "*" 28 | -------------------------------------------------------------------------------- /.github/workflows/python-313.yml: -------------------------------------------------------------------------------- 1 | name: python-313 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '15 23 * * *' 7 | pull_request: 8 | branches: 9 | - main 10 | - '3.13' 11 | push: 12 | branches: 13 | - main 14 | - '3.13' 15 | 16 | jobs: 17 | sync: 18 | uses: ./.github/workflows/sync.yml 19 | with: 20 | tx_project: ${{ github.workflow }} 21 | version: 3.13 22 | secrets: inherit 23 | check: 24 | uses: ./.github/workflows/check.yml 25 | needs: sync 26 | with: 27 | tx_project: ${{ github.workflow }} 28 | version: 3.13 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /scripts/potodo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Extract the list of incomplete translation files. 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | set -xeu 7 | 8 | # Fail earlier if required variables are not set 9 | test -n ${PYDOC_VERSION+x} 10 | test -n ${PYDOC_LANGUAGE+x} 11 | 12 | rootdir=$(realpath $(dirname $0)/..) 13 | language_dir="${PYDOC_LANG_DIR:-$rootdir/cpython/Doc/locales/${PYDOC_LANGUAGE}/LC_MESSAGES}" 14 | 15 | cd "$language_dir" 16 | 17 | potodo --no-cache > potodo.md 18 | 19 | # Show version number instead of the directory name, if present 20 | sed -i "s|LC_MESSAGES|${PYDOC_VERSION}|" potodo.md 21 | 22 | # Remove cache directory 23 | rm -rf .potodo/ 24 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Build translated docs 3 | # Expects input like 'html' and 'latex', defaults to 'html'. 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | set -xeu 8 | 9 | if [ -z "$1" ]; then 10 | format=html 11 | else 12 | format="$1" 13 | fi 14 | 15 | # Fail earlier if required variables are not set 16 | test -n ${PYDOC_LANGUAGE+x} 17 | 18 | cd "$(dirname $0)/.." 19 | mkdir -p logs 20 | 21 | # If version is 3.12 or older, set gettext_compact. 22 | # This confval is not needed since 3.12. 23 | # In 3.13, its presence messes 3.13's syntax checking (?) 24 | opts="-D language=${PYDOC_LANGUAGE} --keep-going -w ../../logs/sphinxwarnings-${format}.txt" 25 | minor_version=$(git -C cpython/Doc branch --show-current | sed 's|^3\.||') 26 | if [ $minor_version -lt 12 ]; then 27 | opts="$opts -D gettext_compact=False" 28 | fi 29 | 30 | make -C cpython/Doc "${format}" SPHINXOPTS="${opts}" 31 | 32 | # Remove empty file 33 | if [ ! -s "logs/sphinxwarnings-${format}.txt" ]; then 34 | rm "logs/sphinxwarnings-${format}.txt" 35 | fi 36 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Build translated docs to pop up errors 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | set -xeu 7 | 8 | # Fail earlier if required variables are not set 9 | test -n ${PYDOC_LANGUAGE+x} 10 | 11 | # Fail earlier if sphinx-lint is not installed 12 | sphinx-lint --help >/dev/null 13 | 14 | rootdir=$(realpath $(dirname $0)/..) 15 | 16 | cd "$rootdir" 17 | 18 | mkdir -p logs 19 | touch logs/sphinxlint.txt 20 | 21 | cd cpython/Doc 22 | 23 | # If version 3.11 or older, disable new 'unnecessary-parentheses' check, 24 | # not fixed before 3.12. 25 | minor_version=$(git branch --show-current | sed 's|^3\.||') 26 | if [ $minor_version -le 11 ]; then 27 | alias sphinx-lint='sphinx-lint --disable unnecessary-parentheses' 28 | fi 29 | 30 | cd locales/${PYDOC_LANGUAGE}/LC_MESSAGES 31 | set +e 32 | sphinx-lint 2> $(realpath "$rootdir/logs/sphinxlint.txt") 33 | set -e 34 | 35 | cd "$rootdir" 36 | 37 | # Check of logfile is empty 38 | if [ ! -s logs/sphinxlint.txt ]; then 39 | # OK, it is empty. Remove it. 40 | rm logs/sphinxlint.txt 41 | else 42 | # print contents and exit with error status (to trigger notification in CI) 43 | cat logs/sphinxlint.txt 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /scripts/prepmsg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Prepare message for Telegram notification 3 | set -ex 4 | 5 | die() { echo "$0: error: $*" >&2; exit 1; } 6 | 7 | [ $# -ne 2 ] && die "Expected 1 input and 1 output files, got $#" 8 | [ ! -f "$1" ] && die "Input file $1 not found, skipping." 9 | [ -z "${PYDOC_REPO}" ] && die "PYDOC_REPO is empty." 10 | [ -z "${PYDOC_VERSION}" ] && die "PYDOC_VERSION is empty." 11 | [ -z "${GITHUB_RUN_ID}" ] && die "GITHUB_RUN_ID is empty." 12 | [ -z "${GITHUB_JOB}" ] && die "GITHUB_JOB is empty." 13 | 14 | URL="${PYDOC_REPO}/actions/runs/${GITHUB_RUN_ID}" 15 | 16 | input="$1" 17 | output="$2" 18 | 19 | touch aux 20 | if [[ "${GITHUB_JOB}" == "build" ]]; then 21 | grep 'cpython/Doc/.*WARNING:' "$input" | \ 22 | sed 's|.*/cpython/Doc/||' | \ 23 | uniq | \ 24 | sed 's|^|```\n|;s|$|\n```\n|' \ 25 | > aux 26 | elif [[ "${GITHUB_JOB}" == "lint" ]]; then 27 | grep -P '^.*\.po:\d+:\s+.*\(.*\)$' "$input" | \ 28 | sed 's|.*/cpython/Doc/||' | \ 29 | sort -u | \ 30 | sed 's|^|```\n|;s|$|\n```\n|' \ 31 | > aux 32 | else 33 | die "Unexpected job name ${GITHUB_JOB}" 34 | fi 35 | 36 | [[ $(cat aux) == "" ]] && die "Unexpected empty output message." 37 | 38 | echo "❌ *${PYDOC_VERSION} ${GITHUB_JOB}* (ID [${GITHUB_RUN_ID}]($URL)):" > "$output"; 39 | { echo ""; cat aux; echo ""; } >> "$output" 40 | rm aux 41 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Set up environment for operations on Python docs translation 3 | # This is meant for running locally to make it easier to test etc. 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | set -xeu 8 | 9 | # Fail earlier if required variables are not set 10 | test -n ${PYDOC_VERSION+x} 11 | test -n ${PYDOC_REPO+x} 12 | test -n ${PYDOC_LANGUAGE+x} 13 | 14 | # Make sure to run all commands from repository root directory 15 | cd $(dirname $0)/.. 16 | 17 | # Clean up 18 | rm -rf cpython 19 | 20 | # Check out needed repositories 21 | git clone --depth 1 --single-branch --branch ${PYDOC_VERSION} https://github.com/python/cpython cpython 22 | git clone --depth 1 --single-branch --branch ${PYDOC_VERSION} ${PYDOC_REPO} cpython/Doc/locales/${PYDOC_LANGUAGE}/LC_MESSAGES 23 | 24 | # Install dependencies; Require being in a VENV or in GitHub Actions 25 | set +u 26 | if [ -z "${VIRTUAL_ENV+x}" ] && [ -z "${CI+x}" ]; then 27 | echo "Expected to be in a virtual environment. For instance:" 28 | echo " rm -rf .venv && python -m venv .venv && source ~/venv/bin/activate" 29 | exit 1 30 | fi 31 | set -u 32 | pip install -r requirements.txt 33 | make -C cpython/Doc venv 34 | 35 | if ! command -v tx > /dev/null; then 36 | echo "WARNING: Transifex CLI tool was not found." 37 | echo "If going to pull translations it is needed, can be ignored otherwise." 38 | echo "See https://github.com/transifex/cli for install info" 39 | fi 40 | -------------------------------------------------------------------------------- /scripts/commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Commit changed files filtering to the git repository 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | set -eu 7 | 8 | test -n ${PYDOC_LANGUAGE+x} 9 | 10 | rootdir=$(realpath $(dirname $0)/..) 11 | language_dir="${PYDOC_LANG_DIR:-$rootdir/cpython/Doc/locales/${PYDOC_LANGUAGE}/LC_MESSAGES}" 12 | 13 | cd "$language_dir" 14 | 15 | extra_files=".tx/config stats.json potodo.md" 16 | 17 | set +u 18 | if [ -n "${CI+x}" ]; then 19 | git config user.email "github-actions[bot]@users.noreply.github.com" 20 | git config user.name "github-actions[bot]" 21 | fi 22 | set -u 23 | 24 | # Set for removal the deleted obsolete PO files 25 | git status -s | grep '^ D ' | cut -d' ' -f3 | xargs -r git rm 26 | 27 | # Add only updates that do not consist only of the following header lines 28 | git diff -I'^# Copyright ' -I'^"Project-Id-Version: ' -I'^"POT-Creation-Date: ' -I'^"Language: ' --numstat *.po **/*.po | cut -f3 | xargs -r git add -v 29 | 30 | # Add currently untracked PO files, and update other helper files 31 | untracked_files=$(git ls-files -o --exclude-standard *.po **/*.po) 32 | if [ -n "${untracked_files+x}" ]; then 33 | git add -v $untracked_files 34 | fi 35 | 36 | # Debug in GitHub Actions 37 | set +u 38 | if [ -n "${CI+x}" ]; then 39 | echo "::group::status" 40 | git status 41 | echo "::endgroup::" 42 | echo "::group::diff" 43 | git diff 44 | echo "::endgroup::" 45 | fi 46 | set -u 47 | 48 | # Commit only if there is any cached file 49 | if ! git diff-index --cached --quiet HEAD; then 50 | git add -v $extra_files 51 | git commit -vm "$($rootdir/scripts/generate_commit_msg.py)" 52 | fi 53 | -------------------------------------------------------------------------------- /scripts/generate_templates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate .pot files and Transifex .tx/config file 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | # 6 | # The following need to be set: 7 | # PYDOC_TX_PROJECT (e.g. python-newest) 8 | # PYDOC_LANGUAGE (e.g. pt_BR) 9 | # TX_TOKEN (or have a ~/.transifexrc file) 10 | 11 | set -xeu 12 | 13 | # Fail earlier if required variables are not set (do not expose TX_TOKEN) 14 | test -n ${PYDOC_TX_PROJECT+x} 15 | test -n ${PYDOC_LANGUAGE+x} 16 | 17 | # Make sure to run all commands from CPython docs locales directory 18 | cd $(dirname $0)/../cpython/Doc/locales 19 | 20 | # Generate message catalog template (.pot) files 21 | # TODO: use `make -C .. gettext` when there are only Python >= 3.12 22 | opts='-E -b gettext -D gettext_compact=0 -d build/.doctrees . build/gettext' 23 | make -C .. build ALLSPHINXOPTS="$opts" 24 | 25 | # Generate updated Transifex project configuration file (.tx/config) 26 | rm -rf ./.tx/config 27 | sphinx-intl create-txconfig 28 | sphinx-intl update-txconfig-resources \ 29 | --transifex-organization-name=python-doc \ 30 | --transifex-project-name=$PYDOC_TX_PROJECT \ 31 | --locale-dir=. \ 32 | --pot-dir=../build/gettext 33 | 34 | # Patch .tx/config and store in the repository to enable running tx command 35 | # Explanation: 36 | # - Adds 'trans.$PYDOC_LANGUAGE' to not need to pass tx pull with '-l LANGUAGE' 37 | # - Don't remove 'file_filter' otherwise tx pull complains 38 | # - Replace PO file path to a local directory (easier manual use of tx pull) 39 | mkdir -p "${PYDOC_LANGUAGE}/LC_MESSAGES/.tx/" 40 | sed .tx/config \ 41 | -e 's|.//LC_MESSAGES/||' \ 42 | -e "/^file_filter/{p;s/file_filter/trans.${PYDOC_LANGUAGE}/g;}" \ 43 | > "${PYDOC_LANGUAGE}/LC_MESSAGES/.tx/config" 44 | -------------------------------------------------------------------------------- /scripts/pull_translations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Pull translations files from Transifex for the given language code 3 | # 4 | # SPDX-License-Identifier: CC0-1.0 5 | # 6 | # Usage examples: 7 | # 8 | # Pull all translations for the given language. 9 | # 10 | # ./scripts/pull_translations.sh 11 | # 12 | # Pull translations for "library/os.po" and "faq/library.po" 13 | # for the Transifex project set in PYDOC_TX_PROJECT. For instance, 14 | # for PYDOC_TX_PROJECT=python-newest, that means pulling the resources 15 | # "python-newest.library--os" and "python-newest.faq--library". 16 | # 17 | # ./scripts/pull_translations.sh library/os.po faq/library.po 18 | # 19 | 20 | set -xeu 21 | 22 | test -n ${PYDOC_TX_PROJECT+x} 23 | test -n ${PYDOC_LANGUAGE+x} 24 | 25 | rootdir=$(realpath $(dirname $0)/..) 26 | language_dir="${PYDOC_LANG_DIR:-$rootdir/cpython/Doc/locales/${PYDOC_LANGUAGE}/LC_MESSAGES}" 27 | 28 | cd "$language_dir" 29 | 30 | # If a PO file is provided as input, convert it into Transifex resource 31 | # and add it be pulled (instead of pulling all translations files). 32 | resources_to_pull="" 33 | if [ $# -gt 0 ]; then 34 | resources_to_pull="-r" 35 | for po_input in $@; do 36 | # trim possible filepath part not used for Transifex resources 37 | po=$(echo $po_input | sed 's|.*LC_MESSAGES/||') 38 | # fail if PO file doesn't exist 39 | [ ! -f "$po" ] && (echo "'$po_input' not found."; exit 1;) 40 | # convert po filename into transifex resource 41 | tx_res=$(echo $po | sed 's|\.po||;s|/|--|g;s|\.|_|g') 42 | # append to a list of resources to be pulled 43 | resources_to_pull="$resources_to_pull ${PYDOC_TX_PROJECT}.${tx_res}" 44 | done 45 | fi 46 | 47 | tx pull -f -l "${PYDOC_LANGUAGE}" ${resources_to_pull} 48 | 49 | # Drop translation of Python's changelog in python 3.12 or older. 50 | minor_version=$(git branch --show-current | sed 's|^3\.||') 51 | if [ $minor_version -le 12 ]; then 52 | git checkout whatsnew/changelog.po 53 | fi 54 | -------------------------------------------------------------------------------- /scripts/stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Obtain translation stats from the PO files directory and 4 | store it with JSON format into 'stats.json'. 5 | """ 6 | 7 | import json 8 | import os 9 | import logging 10 | from datetime import datetime, timezone 11 | from pathlib import Path 12 | 13 | from potodo.po_file import PoDirectory 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | 17 | 18 | def main() -> None: 19 | """Main function to generate translation stats.""" 20 | 21 | lang_dir = os.environ.get("PYDOC_LANG_DIR") 22 | if lang_dir: 23 | pofiles_path = Path(lang_dir) 24 | else: 25 | language = os.environ.get("PYDOC_LANGUAGE") 26 | if not language: 27 | raise ValueError("Environment variable PYDOC_LANGUAGE is not set.") 28 | pofiles_path = Path(f"cpython/Doc/locales/{language}/LC_MESSAGES") 29 | 30 | if not pofiles_path.exists(): 31 | raise FileNotFoundError(f"Path does not exist: {pofiles_path}") 32 | 33 | # Check for PO files inside the pofiles_path 34 | if not list(pofiles_path.rglob("*.po")): 35 | raise FileNotFoundError(f"No PO files found in {pofiles_path}") 36 | 37 | stats = PoDirectory(pofiles_path, use_cache=False) 38 | stats.scan() 39 | 40 | stats_data = { 41 | "completion": str(round(stats.completion, 2)) + "%", 42 | "translated": stats.translated, 43 | "entries": stats.entries, 44 | "updated_at": datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z", 45 | } 46 | 47 | stats_json = pofiles_path / "stats.json" 48 | try: 49 | with stats_json.open("w") as output_file: 50 | json.dump(stats_data, output_file) 51 | logging.info(f"Content written to {stats_json}") 52 | except IOError as e: 53 | logging.error(f"Failed to write to {stats_json}: {e}") 54 | raise 55 | 56 | 57 | if __name__ == "__main__": 58 | try: 59 | main() 60 | except Exception as e: 61 | logging.error(f"An error occurred: {e}") 62 | -------------------------------------------------------------------------------- /scripts/tx_stats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Print translation percentage to update statistics.""" 3 | 4 | import json 5 | import os 6 | import configparser 7 | import urllib.request 8 | from datetime import datetime, timezone 9 | 10 | # Get language and project from environment variables 11 | language = os.environ.get("PYDOC_LANGUAGE") 12 | project = os.environ.get("PYDOC_TX_PROJECT") 13 | if language is None: 14 | raise ValueError("The PYDOC_LANGUAGE environment variable must be set.") 15 | if project is None: 16 | raise ValueError("The PYDOC_TX_PROJECT environment variable must be set.") 17 | 18 | 19 | # Try to read API token from TX_TOKEN env and then from ~/.transifexrc 20 | def get_transifex_token(): 21 | key = os.environ.get("TX_TOKEN") 22 | if key is None: 23 | config = configparser.ConfigParser() 24 | config.read(os.path.expanduser("~/.transifexrc")) 25 | try: 26 | key = config["https://www.transifex.com"]["token"] 27 | except KeyError: 28 | raise ValueError("Unable to retrieve Transifex API token.") 29 | return key 30 | 31 | 32 | # API URL setup 33 | url_template = ( 34 | "https://rest.api.transifex.com/resource_language_stats" 35 | "?filter[project]=o%3Apython-doc%3Ap%3A{project}" 36 | "&filter[language]=l%3A{language}" 37 | ) 38 | 39 | # Get the authorization key 40 | key = get_transifex_token() 41 | 42 | url = url_template.format(project=project, language=language) 43 | 44 | headers = {"accept": "application/vnd.api+json", "authorization": f"Bearer {key}"} 45 | 46 | # Initialize counters 47 | total_strings = 0 48 | translated_strings = 0 49 | 50 | 51 | # Function to make an API request and handle potential errors 52 | def fetch_data(url): 53 | request = urllib.request.Request(url=url, headers=headers) 54 | try: 55 | with urllib.request.urlopen(request) as response: 56 | return json.loads(response.read().decode("utf-8")) 57 | except urllib.error.URLError as e: 58 | raise ConnectionError(f"Error fetching data: {e}") 59 | except json.JSONDecodeError as e: 60 | raise ValueError(f"Error decoding JSON response: {e}") 61 | 62 | 63 | # Fetch and process translation stats 64 | while url: 65 | data = fetch_data(url) 66 | url = data["links"].get("next") 67 | for resource in data["data"]: 68 | translated_strings += resource["attributes"]["translated_strings"] 69 | total_strings += resource["attributes"]["total_strings"] 70 | 71 | # Calculate translation percentage 72 | if total_strings == 0: 73 | raise ValueError("Total strings cannot be zero.") 74 | 75 | percentage = f"{(translated_strings / total_strings):.2%}" 76 | 77 | # Print the result as JSON 78 | print( 79 | json.dumps( 80 | { 81 | "translation": percentage, 82 | "total": total_strings, 83 | "updated_at": datetime.now(timezone.utc).isoformat(timespec="seconds") 84 | + "Z", 85 | } 86 | ) 87 | ) 88 | -------------------------------------------------------------------------------- /scripts/generate_commit_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generate a commit message 4 | Parses staged files and generates a commit message with Last-Translator's as 5 | co-authors. 6 | Based on Stan Ulbrych's implementation for Python Doc' Polish team 7 | """ 8 | 9 | import argparse 10 | import contextlib 11 | import os 12 | from subprocess import run, CalledProcessError 13 | from pathlib import Path 14 | 15 | from polib import pofile, POFile 16 | 17 | 18 | def generate_commit_msg(): 19 | translators: set[str] = set() 20 | 21 | result = run( 22 | ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"], 23 | capture_output=True, 24 | text=True, 25 | check=True, 26 | ) 27 | staged = [ 28 | filename for filename in result.stdout.splitlines() if filename.endswith(".po") 29 | ] 30 | 31 | for file in staged: 32 | staged_file = run( 33 | ["git", "show", f":{file}"], capture_output=True, text=True, check=True 34 | ).stdout 35 | try: 36 | old_file = run( 37 | ["git", "show", f"HEAD:{file}"], 38 | capture_output=True, 39 | text=True, 40 | check=True, 41 | ).stdout 42 | except CalledProcessError: 43 | old_file = "" 44 | 45 | new_po = pofile(staged_file) 46 | old_po = pofile(old_file) if old_file else POFile() 47 | old_entries = {entry.msgid: entry.msgstr for entry in old_po} 48 | 49 | for entry in new_po: 50 | if entry.msgstr and ( 51 | entry.msgid not in old_entries 52 | or old_entries[entry.msgid] != entry.msgstr 53 | ): 54 | # Prevent failure on missing Last-Translator field. 55 | # Transifex only adds Last-Translator if someone from 56 | # the team translated. If it was uploaded by an account 57 | # that is not in the team, this field will be missing. 58 | translator = ( 59 | (new_po.metadata.get("Last-Translator") or "").split(",")[0].strip() 60 | ) 61 | if translator: 62 | translators.add(f"Co-Authored-By: {translator}") 63 | break 64 | 65 | print("Update translation\n\n" + "\n".join(translators)) 66 | 67 | 68 | # contextlib implemented chdir since Python 3.11 69 | @contextlib.contextmanager 70 | def chdir(path: Path): 71 | """Temporarily change the working directory.""" 72 | original_dir = Path.cwd() 73 | os.chdir(path) 74 | try: 75 | yield 76 | finally: 77 | os.chdir(original_dir) 78 | 79 | 80 | if __name__ == "__main__": 81 | parser = argparse.ArgumentParser( 82 | description="Generate commit message with translators as co-authors." 83 | ) 84 | parser.add_argument( 85 | "path", 86 | type=Path, 87 | nargs="?", 88 | default=".", 89 | help="Path to the Git repository (default: current directory)", 90 | ) 91 | args = parser.parse_args() 92 | 93 | with chdir(args.path): 94 | generate_commit_msg() 95 | -------------------------------------------------------------------------------- /scripts/generate_txconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate the .tx/config file based on the existing projects 4 | in Python docs Transifex project. Takes an project slug as 5 | positional argument, e.g. python-newest or python-313 6 | """ 7 | 8 | import argparse 9 | import re 10 | import subprocess 11 | import sys 12 | from pathlib import Path 13 | 14 | # Replaces required to fix the default values set by 'tx add remote' command. 15 | # Add or remove 16 | TEXT_TO_REPLACE = { 17 | "2_": "2.", 18 | "3_": "3.", 19 | "glossary_": "glossary", 20 | "collections_": "collections.", 21 | "compression_": "compression.", 22 | "concurrent_": "concurrent.", 23 | "curses_": "curses.", 24 | "email_": "email.", 25 | "html_": "html.", 26 | "http_": "http.", 27 | "importlib_resources_": "importlib.resources.", 28 | "importlib_": "importlib.", 29 | "logging_": "logging.", 30 | "multiprocessing_": "multiprocessing.", 31 | "os_": "os.", 32 | "string_": "string.", 33 | "sys_monitoring": "sys.monitoring", 34 | "tkinter_": "tkinter.", 35 | "unittest_": "unittest.", 36 | "urllib_": "urllib.", 37 | "xml_dom_": "xml.dom.", 38 | "xml_etree_": "xml.etree.", 39 | "xmlrpc_": "xmlrpc.", 40 | "xml_sax_": "xml.sax.", 41 | "xml_": "xml.", 42 | } 43 | 44 | 45 | def parse_args(): 46 | parser = argparse.ArgumentParser(description=__doc__) 47 | parser.add_argument( 48 | "--root-path", 49 | "-p", 50 | default=Path("."), 51 | help="Path to the translation files, and also the .tx/config (defaults to current directory)", 52 | ) 53 | parser.add_argument( 54 | "tx_project", help="Slug of the Transifex project to query resources from" 55 | ) 56 | return parser.parse_args() 57 | 58 | 59 | def reset_tx_config(txconfig: Path): 60 | """Create or reset the .tx/config file with basic header.""" 61 | txconfig.parent.mkdir(exist_ok=True) 62 | txconfig.write_text("[main]\nhost = https://www.transifex.com\n", encoding="utf-8") 63 | print("Initialized .tx/config.") 64 | 65 | 66 | def populate_resources_from_remote(config_file: Path, tx_project: str): 67 | """Add the remote resources from the Transifex project to .tx/config.""" 68 | result = subprocess.run( 69 | [ 70 | "tx", 71 | "--config", 72 | str(config_file), 73 | "add", 74 | "remote", 75 | "--file-filter", 76 | "/.", 77 | f"https://app.transifex.com/python-doc/{tx_project}/", 78 | ], 79 | check=True, 80 | ) 81 | if result.returncode != 0: 82 | print("Failed to add the resources from remote:") 83 | print(result.stderr.strip()) 84 | sys.exit(result.returncode) 85 | print("Added remote resources to Transifex.") 86 | 87 | 88 | def patch_config(txconfig: Path): 89 | """Patch .tx/config to fixing PO filenames to match the expected.""" 90 | content = txconfig.read_text(encoding="utf-8").splitlines() 91 | new_lines = [] 92 | 93 | for line in content: 94 | if line.startswith(("source_file", "source_lang")): 95 | continue 96 | 97 | if line.startswith("file_filter"): 98 | line = line.replace("/", "") 99 | line = line.replace("--", "/") 100 | 101 | for pattern, replacement in TEXT_TO_REPLACE.items(): 102 | if pattern in line: 103 | line = line.replace(pattern, replacement) 104 | break 105 | 106 | new_lines.append(line) 107 | 108 | text = "\n".join(new_lines) 109 | 110 | txconfig.write_text(text + "\n", encoding="utf-8") 111 | print("Updated .tx/config with character substitutions") 112 | 113 | 114 | def main(): 115 | args = parse_args() 116 | config_path = Path(".tx/config") 117 | if args.root_path: 118 | config_path = args.root_path / config_path 119 | reset_tx_config(config_path) 120 | populate_resources_from_remote(config_path, args.tx_project) 121 | patch_config(config_path) 122 | 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | description: "Branch name corresponding to a Python version" 8 | required: true 9 | type: string 10 | tx_project: 11 | description: "Name of the Transifex translation project" 12 | required: true 13 | type: string 14 | secrets: 15 | TX_TOKEN: 16 | description: "Token required for interacting with Transifex API" 17 | required: false 18 | 19 | env: 20 | PYDOC_LANGUAGE: pt_BR 21 | PYDOC_TX_PROJECT: ${{ inputs.tx_project }} 22 | PYDOC_VERSION: ${{ inputs.version }} 23 | TX_CLI_VERSION: '1.6.17' 24 | 25 | jobs: 26 | sync: 27 | runs-on: ubuntu-latest 28 | steps: 29 | 30 | # 1- Set up environment 31 | 32 | - name: Check out this repository 33 | uses: actions/checkout@v6 34 | 35 | - name: Set language dir variable 36 | run: 37 | echo "PYDOC_LANG_DIR=${{ env.PYDOC_VERSION }}" >> $GITHUB_ENV 38 | 39 | - name: Checkout this repository ${{ env.PYDOC_VERSION }} 40 | uses: actions/checkout@v6 41 | with: 42 | ref: ${{ env.PYDOC_VERSION }} 43 | path: ${{ env.PYDOC_LANG_DIR }} 44 | 45 | - uses: actions/setup-python@v6 46 | with: 47 | python-version: ${{ inputs.version }} 48 | allow-prereleases: true 49 | cache: 'pip' 50 | pip-install: -r requirements.txt 51 | 52 | # 2- Install dependencies 53 | 54 | - name: Install Transifex CLI tool 55 | run: | 56 | cd /usr/local/bin 57 | curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- v$TX_CLI_VERSION 58 | 59 | - name: Install APT dependencies 60 | run: sudo apt update -y && sudo apt install gettext -y 61 | 62 | # 3- Pull translations 63 | 64 | - name: Generate updated .tx/config 65 | if: ${{ contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) }} 66 | run: python scripts/generate_txconfig.py -p ./${{ env.PYDOC_LANG_DIR }} ${{ env.PYDOC_TX_PROJECT }} 67 | env: 68 | TX_TOKEN: ${{ secrets.TX_TOKEN }} 69 | 70 | - name: Pull translations from Transifex 71 | id: pull 72 | if: ${{ contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) }} 73 | run: | 74 | # Clean up obsolete files 75 | find ./${{ env.PYDOC_LANG_DIR }} -name '*.po' -exec rm {} \; 76 | ./scripts/pull_translations.sh 77 | env: 78 | TX_TOKEN: ${{ secrets.TX_TOKEN }} 79 | 80 | - name: Merge translations from newer branch 81 | if: inputs.tx_project != 'python-newest' # python-newest doesn't have a newer branch 82 | run: | 83 | newer_branch=${PYDOC_VERSION%%.*}.$((${PYDOC_VERSION##*.}+1)) 84 | git clone --depth 1 --single-branch --branch $newer_branch https://github.com/python/python-docs-pt-br ${newer_branch}-dir 85 | pomerge --from ./${newer_branch}-dir/{**/,}*.po --to ./${{ env.PYDOC_LANG_DIR }}/{**/,}*.po 86 | rm -rf ./${newer_branch}-dir 87 | 88 | - name: powrap 89 | if: steps.pull.outcome == 'success' 90 | run: | 91 | cd ./${{ env.PYDOC_LANG_DIR }} 92 | powrap *.po **/*.po 93 | 94 | - name: Update statistics 95 | if: always() 96 | run: | 97 | ./scripts/stats.py 98 | git -C ./${{ env.PYDOC_LANG_DIR }} diff stats.json 99 | env: 100 | PYDOC_LANG_DIR: ${{ env.PYDOC_LANG_DIR }} 101 | 102 | - name: Update potodo.md 103 | if: always() 104 | run: | 105 | ./scripts/potodo.sh 106 | git -C ./${{ env.PYDOC_LANG_DIR }} diff potodo.md 107 | env: 108 | PYDOC_LANG_DIR: ${{ env.PYDOC_LANG_DIR }} 109 | 110 | # 4- Commit and push translations 111 | 112 | - name: Commit 113 | run: ./scripts/commit.sh 114 | env: 115 | PYDOC_LANG_DIR: ${{ env.PYDOC_LANG_DIR }} 116 | 117 | - name: Push 118 | if: ${{ contains(fromJSON('["schedule", "workflow_dispatch"]'), github.event_name) }} 119 | run: | 120 | cd ./${{ env.PYDOC_LANG_DIR }} 121 | git push 122 | 123 | 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # Run some tests in the Python Doc translations 2 | 3 | name: Check 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Branch name corresponding to a Python version" 10 | required: true 11 | type: string 12 | tx_project: 13 | description: "Name of the Transifex translation project" 14 | required: true 15 | type: string 16 | workflow_call: 17 | inputs: 18 | version: 19 | description: "Branch name corresponding to a Python version" 20 | required: true 21 | type: string 22 | tx_project: 23 | description: "Name of the Transifex translation project" 24 | required: true 25 | type: string 26 | secrets: 27 | TELEGRAM_TOKEN: 28 | description: "Token required for interacting with Telegram API" 29 | required: false 30 | TELEGRAM_TO: 31 | description: "Account ID that will receive the telegram notification" 32 | required: false 33 | 34 | permissions: 35 | contents: read 36 | 37 | env: 38 | PYDOC_LANGUAGE: pt_BR 39 | PYDOC_REPO: ${{ github.server_url }}/${{ github.repository }} 40 | PYDOC_VERSION: ${{ inputs.version }} 41 | 42 | jobs: 43 | 44 | # Build documentation handling warnings as errors in both HTML and PDF. 45 | # If success, upload built docs as artifact. 46 | # If failure in HTML, notify telegram and upload logs. 47 | build: 48 | name: Build docs 49 | runs-on: ubuntu-latest 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | format: [ html, latex, epub ] 54 | steps: 55 | - uses: actions/checkout@v6 56 | with: 57 | fetch-depth: 5 58 | 59 | - name: Set up Python 3 60 | uses: actions/setup-python@v6 61 | with: 62 | python-version: ${{ inputs.version }} 63 | cache: pip 64 | allow-prereleases: true 65 | 66 | - name: Make sure the repository is up to date 67 | if: github.event_name != 'pull_request' 68 | run: git pull --rebase 69 | 70 | - name: setup 71 | run: ./scripts/setup.sh 72 | 73 | - name: Add problem matcher 74 | uses: sphinx-doc/github-problem-matcher@v1.1 75 | 76 | - name: Build docs 77 | id: build 78 | run: ./scripts/build.sh ${{ matrix.format }} 79 | 80 | - name: Prepare notification (only on error) 81 | if: always() && steps.build.outcome == 'failure' && matrix.format == 'html' 82 | id: prepare 83 | run: | 84 | scripts/prepmsg.sh logs/sphinxwarnings-${{ matrix.format }}.txt logs/notify.txt 85 | cat logs/notify.txt 86 | env: 87 | GITHUB_JOB: ${{ github.job }} 88 | GITHUB_RUN_ID: ${{ github.run_id }} 89 | 90 | - name: Notify via Telegram 91 | if: always() && steps.prepare.outcome == 'success' && github.event_name == 'schedule' && inputs.tx_project == 'python-newest' 92 | uses: appleboy/telegram-action@v1.0.1 93 | with: 94 | to: ${{ secrets.TELEGRAM_TO }} 95 | token: ${{ secrets.TELEGRAM_TOKEN }} 96 | format: markdown 97 | disable_web_page_preview: true 98 | message_file: logs/notify.txt 99 | 100 | - name: Upload artifact - log files 101 | if: always() && steps.build.outcome == 'failure' 102 | uses: actions/upload-artifact@v6 103 | with: 104 | name: logs-${{ inputs.version }}-${{ matrix.format }} 105 | path: logs/* 106 | 107 | - name: Upload artifact - docs 108 | if: always() && steps.build.outcome == 'success' 109 | uses: actions/upload-artifact@v6 110 | with: 111 | name: python-docs-pt-br-${{ inputs.version }}-${{ matrix.format }} 112 | path: cpython/Doc/build/${{ matrix.format }} 113 | 114 | # Build Python docs in PDF format and make available for download. 115 | output-pdf: 116 | name: Build docs (pdf) 117 | runs-on: ubuntu-latest 118 | needs: [ 'build' ] 119 | steps: 120 | - uses: actions/download-artifact@v7 121 | with: 122 | name: python-docs-pt-br-${{ inputs.version }}-latex 123 | - run: sudo apt-get update 124 | - run: sudo apt-get install -y latexmk texlive-xetex fonts-freefont-otf xindy texlive-lang-portuguese 125 | - run: make 126 | - uses: actions/upload-artifact@v6 127 | if: always() 128 | with: 129 | name: python-docs-pt-br-${{ inputs.version }}-pdf 130 | path: ./*.pdf 131 | 132 | # Run sphinx-lint to find wrong reST syntax in PO files. Always store logs. 133 | # If issues are found, notify telegram and upload logs. 134 | lint: 135 | name: Lint translations 136 | runs-on: ubuntu-latest 137 | steps: 138 | - uses: actions/checkout@v6 139 | with: 140 | fetch-depth: 5 141 | 142 | - name: Set up Python 3 143 | uses: actions/setup-python@v6 144 | with: 145 | python-version: ${{ inputs.version }} 146 | cache: pip 147 | allow-prereleases: true 148 | 149 | - name: Make sure the repository is up to date 150 | if: github.event_name != 'pull_request' 151 | run: git pull --rebase 152 | 153 | - name: setup 154 | run: ./scripts/setup.sh 155 | 156 | - name: Add problem matcher 157 | uses: rffontenelle/sphinx-lint-problem-matcher@v1.0.0 158 | 159 | - name: lint translations files 160 | id: lint 161 | run: ./scripts/lint.sh 162 | 163 | - name: Prepare notification (only on error) 164 | if: always() && steps.lint.outcome == 'failure' 165 | id: prepare 166 | run: | 167 | scripts/prepmsg.sh logs/sphinxlint.txt logs/notify.txt 168 | cat logs/notify.txt 169 | env: 170 | GITHUB_JOB: ${{ github.job }} 171 | GITHUB_RUN_ID: ${{ github.run_id }} 172 | 173 | - name: Notify via Telegram 174 | if: always() && steps.prepare.outcome == 'success' && github.event_name == 'schedule' && inputs.tx_project == 'python-newest' 175 | uses: appleboy/telegram-action@v1.0.1 176 | with: 177 | to: ${{ secrets.TELEGRAM_TO }} 178 | token: ${{ secrets.TELEGRAM_TOKEN }} 179 | format: markdown 180 | disable_web_page_preview: true 181 | message_file: logs/notify.txt 182 | 183 | - name: Upload artifact - log files 184 | if: always() && steps.lint.outcome == 'failure' 185 | uses: actions/upload-artifact@v6 186 | with: 187 | name: ${{ inputs.version }}-lint-logs 188 | path: logs/* 189 | 190 | 191 | # Check for zero-width space charcters in translations. 192 | # These are known to come together with (some?) machine translation like Google translate, 193 | # and - as one of the consequences - it may avoid Transifex glossary matching (e.g. variáveis) 194 | zero-width-space: 195 | name: Check for zero-width space characters 196 | runs-on: ubuntu-latest 197 | steps: 198 | - uses: actions/checkout@v6 199 | with: 200 | ref: ${{ inputs.version }} 201 | 202 | - name: Make sure the repository is up to date 203 | if: github.event_name != 'pull_request' 204 | run: git pull --rebase 205 | 206 | - name: Remove zero-width space characters 207 | run: | 208 | sed -i 's/\xe2\x80\x8b//g' *.po **/*.po 209 | # Undo removal from where we should not 210 | sed -i 's|`````|``​`​``|' reference/lexical_analysis.po 211 | 212 | - name: Show difference (error if there is any) 213 | run: | 214 | git diff --exit-code --color-words 215 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Brazilian Portuguese Translation of the Python Documentation 2 | ============================================= 3 | 4 | All translations are done on Transifex. 5 | https://explore.transifex.com/python-doc/ 6 | 7 | Please use pull request only for improving scripts, README, etc.; not for translation 8 | 9 | For guides, contacts and more, please see the `Wiki `_. 10 | 11 | Join the translation team at the Telegram group `@pybr_i18n `_. 12 | 13 | Versions translation status 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | See below a table of the translation status for each version, separated by versions that are still maintained and version that reached the end-of-life (EOL). 17 | 18 | Maintained versions: 19 | -------------------- 20 | 21 | 22 | .. list-table:: 23 | :header-rows: 1 24 | 25 | * - Version 26 | - Sync status 27 | - Translation progress 28 | - Total strings 29 | * - `3.14 `_ 30 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-314/badge.svg 31 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-314 32 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.14%2Fstats.json&query=completion&label=pt_BR 33 | :alt: Brazilian Portuguese translation status for Python 3.14 34 | :target: https://app.transifex.com/python-doc/python-newest/ 35 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.14%2Fstats.json&query=entries&label=3.14 36 | :alt: Total strings for Python 3.14 37 | :target: https://app.transifex.com/python-doc/python-newest/ 38 | * - `3.13 `_ 39 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-313/badge.svg 40 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-313 41 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.13%2Fstats.json&query=completion&label=pt_BR 42 | :alt: Brazilian Portuguese translation status for Python 3.13 43 | :target: https://app.transifex.com/python-doc/python-newest/ 44 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.13%2Fstats.json&query=entries&label=3.13 45 | :alt: Total strings for Python 3.13 46 | :target: https://app.transifex.com/python-doc/python-newest/ 47 | * - `3.12 `_ 48 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-312/badge.svg 49 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-312 50 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.12%2Fstats.json&query=completion&label=pt_BR 51 | :alt: Brazilian Portuguese translation status for Python 3.12 52 | :target: https://app.transifex.com/python-doc/python-312/ 53 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.12%2Fstats.json&query=entries&label=3.12 54 | :alt: Total strings for Python 3.12 55 | :target: https://app.transifex.com/python-doc/python-312/ 56 | * - `3.11 `_ 57 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-311/badge.svg 58 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-311 59 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.11%2Fstats.json&query=completion&label=pt_BR 60 | :alt: Brazilian Portuguese translation status for Python 3.11 61 | :target: https://app.transifex.com/python-doc/python-311/ 62 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.11%2Fstats.json&query=entries&label=3.11 63 | :alt: Total strings for Python 3.11 64 | :target: https://app.transifex.com/python-doc/python-311/ 65 | * - `3.10 `_ 66 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-310/badge.svg 67 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-310 68 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.10%2Fstats.json&query=completion&label=pt_BR 69 | :alt: Brazilian Portuguese translation status for Python 3.10 70 | :target: https://app.transifex.com/python-doc/python-310/ 71 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.10%2Fstats.json&query=entries&label=3.10 72 | :alt: Total strings for Python 3.10 73 | :target: https://app.transifex.com/python-doc/python-310/ 74 | 75 | 76 | EOL versions: 77 | ------------- 78 | 79 | 80 | .. list-table:: 81 | :header-rows: 1 82 | 83 | * - Version 84 | - Sync status 85 | - Translation progress 86 | - Total strings 87 | * - `3.9 `_ 88 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-39/badge.svg 89 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-39 90 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.9%2Fstats.json&query=completion&label=pt_BR 91 | :alt: Brazilian Portuguese translation status for Python 3.9 92 | :target: https://app.transifex.com/python-doc/python-39/ 93 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.9%2Fstats.json&query=entries&label=3.9 94 | :alt: Total strings for Python 3.9 95 | :target: https://app.transifex.com/python-doc/python-39/ 96 | * - `3.8 `_ 97 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-38/badge.svg 98 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-38 99 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.8%2Fstats.json&query=completion&label=pt_BR 100 | :alt: Brazilian Portuguese translation status for Python 3.8 101 | :target: https://app.transifex.com/python-doc/python-38/ 102 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.8%2Fstats.json&query=entries&label=3.8 103 | :alt: Total strings for Python 3.8 104 | :target: https://app.transifex.com/python-doc/python-38/ 105 | * - `3.7 `_ 106 | - .. image:: https://github.com/python/python-docs-pt-br/workflows/python-37/badge.svg 107 | :target: https://github.com/python/python-docs-pt-br/actions?workflow=python-37 108 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.7%2Fstats.json&query=completion&label=pt_BR 109 | :alt: Brazilian Portuguese translation status for Python 3.7 110 | :target: https://app.transifex.com/python-doc/python-37/ 111 | - .. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Fpython%2Fpython-docs-pt-br%2Fraw%2F3.7%2Fstats.json&query=entries&label=3.7 112 | :alt: Total strings for Python 3.7 113 | :target: https://app.transifex.com/python-doc/python-37/ 114 | 115 | 116 | Documentation Contribution Agreement 117 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | 120 | NOTE REGARDING THE LICENSE FOR TRANSLATIONS: Python's documentation is 121 | maintained using a global network of volunteers. By posting this 122 | project on Transifex, GitHub, and other public places, and inviting 123 | you to participate, we are proposing an agreement that you will 124 | provide your improvements to Python's documentation or the translation 125 | of Python's documentation for the PSF's use under the CC0 license 126 | (available at 127 | https://creativecommons.org/publicdomain/zero/1.0/legalcode). In 128 | return, you may publicly claim credit for the portion of the 129 | translation you contributed and if your translation is accepted by the 130 | PSF, you may (but are not required to) submit a patch including an 131 | appropriate annotation in the Misc/ACKS or TRANSLATORS file. Although 132 | nothing in this Documentation Contribution Agreement obligates the PSF 133 | to incorporate your textual contribution, your participation in the 134 | Python community is welcomed and appreciated. 135 | 136 | You signify acceptance of this agreement by submitting your work to 137 | the PSF for inclusion in the documentation. 138 | --------------------------------------------------------------------------------