diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0ff197d01..ba6540839 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -70,7 +70,7 @@ jobs: - name: Build docs id: build - run: ./scripts/build.sh + run: python ./scripts/manage_translations.py build - name: Upload artifact - docs if: steps.build.outcome == 'success' diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 7ff462ff2..be0f37346 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -86,7 +86,7 @@ jobs: # 3- Pull translations - name: Generate template files and Transifex config file - run: ./scripts/generate_templates.sh + run: python ./scripts/manage_translations.py generate_templates - name: Pull translations from Transifex id: pull diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index ee338368e..000000000 --- a/scripts/build.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# Build translated docs to pop up errors -# -# SPDX-License-Identifier: CC0-1.0 - -set -xeu - -# Fail earlier if required variables are not set -test -n ${PYDOC_LANGUAGE+x} - -cd "$(dirname $0)/.." -mkdir -p logs - -# If version is 3.12 or older, set gettext_compact. -# This confval is not needed since 3.12. -# In 3.13, its presence messes 3.13's syntax checking (?) -opts="-D language=${PYDOC_LANGUAGE} --keep-going -w ../../logs/sphinxwarnings.txt" -minor_version=$(git -C cpython/Doc branch --show-current | sed 's|^3\.||') -if [ $minor_version -lt 12 ]; then - opts="$opts -D gettext_compact=False" -fi - -make -C cpython/Doc html SPHINXOPTS="${opts}" - -# Remove empty file -if [ ! -s logs/sphinxwarnings.txt ]; then - rm logs/sphinxwarnings.txt -fi diff --git a/scripts/generate_templates.sh b/scripts/generate_templates.sh deleted file mode 100755 index d1878f411..000000000 --- a/scripts/generate_templates.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# Generate .pot files and Transifex .tx/config file -# -# SPDX-License-Identifier: CC0-1.0 -# -# The following need to be set: -# PYDOC_TX_PROJECT (e.g. python-newest) -# PYDOC_LANGUAGE (e.g. pt_BR) -# TX_TOKEN (or have a ~/.transifexrc file) - -set -xeu - -# Fail earlier if required variables are not set (do not expose TX_TOKEN) -test -n ${PYDOC_TX_PROJECT+x} -test -n ${PYDOC_LANGUAGE+x} - -# Make sure to run all commands from CPython docs locales directory -cd $(dirname $0)/../cpython/Doc/locales - -# Generate message catalog template (.pot) files -# TODO: use `make -C .. gettext` when there are only Python >= 3.12 -opts='-E -b gettext -D gettext_compact=0 -d build/.doctrees . build/gettext' -make -C .. build ALLSPHINXOPTS="$opts" - -# Generate updated Transifex project configuration file (.tx/config) -rm -rf ./.tx/config -sphinx-intl create-txconfig -sphinx-intl update-txconfig-resources \ - --transifex-organization-name=python-doc \ - --transifex-project-name=$PYDOC_TX_PROJECT \ - --locale-dir=. \ - --pot-dir=../build/gettext - -# Patch .tx/config and store in the repository to enable running tx command -# Explanation: -# - Adds 'trans.$PYDOC_LANGUAGE' to not need to pass tx pull with '-l LANGUAGE' -# - Don't remove 'file_filter' otherwise tx pull complains -# - Replace PO file path to a local directory (easier manual use of tx pull) -mkdir -p "${PYDOC_LANGUAGE}/LC_MESSAGES/.tx/" -sed .tx/config \ - -e 's|.//LC_MESSAGES/||' \ - -e "/^file_filter/{p;s/file_filter/trans.${PYDOC_LANGUAGE}/g;}" \ - > "${PYDOC_LANGUAGE}/LC_MESSAGES/.tx/config" diff --git a/scripts/manage_translations.py b/scripts/manage_translations.py new file mode 100755 index 000000000..01dbff42c --- /dev/null +++ b/scripts/manage_translations.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python + +# SPDX-License-Identifier: CC0-1.0 + +import argparse +import contextlib +import logging +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Optional + +from sphinx_intl.transifex import create_txconfig, update_txconfig_resources + +ROOTDIR = Path(__file__).resolve().parent.parent +COMMANDS = ["build", 'generate_templates'] + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + + +def configure_parser() -> argparse.ArgumentParser: + """Configure and return the argument parser.""" + parser = argparse.ArgumentParser(description="Manage translation for Python documentation") + parser.add_argument("command", choices=COMMANDS, help="The command to execute") + parser.add_argument("-l", "--language", help="Language for the translated documentation") + parser.add_argument("-v", "--python-version", help="Python version to be used") + parser.add_argument("-L", "--logs-dir", default=ROOTDIR / "logs", type=Path, help="Directory for logs (default: 'logs' in root directory") + parser.add_argument("-c", "--cpython-path", default=ROOTDIR / "cpython", type=Path, help="Path to the CPython repository (default: 'cpython' in root directory") + parser.add_argument("-p", "--po-dir", type=Path, help="Path to the language team repository containing PO files (default: CPYTHON_PATH/Doc/locales/LANGUAGE/LC_MESSAGES") + parser.add_argument('-t', '--tx-project', help="Name of the Transifex project under python-doc Transifex organization") + return parser + + +def get_value(arg_value: Optional[str], arg_name: str, env_var_name: str) -> str: + """Return a CLI argument or environment variable value.""" + value = arg_value or os.getenv(env_var_name) + if not value: + logger.error(f"'{arg_name}' not provided and the environment variable {env_var_name} is not set.") + sys.exit(1) + return value + + +def validate_cpython_path(cpython_path: Path) -> None: + if not (cpython_path / "Doc" / "conf.py").exists(): + logger.error(f"Missing conf.py in {cpython_path}. Invalid CPython directory.") + sys.exit(1) + + +def validate_po_dir(po_dir: Path) -> None: + if not po_dir.exists() or not list(po_dir.glob("*.po")): + logger.error(f"Invalid locale directory '{po_dir}'. No PO files found.") + sys.exit(1) + + +def validate_tx_config(tx_config: str) -> None: + if not re.match(r"python-(newest|\d+)", tx_config): + logger.error(f"Invalid Transifex project name: {tx_config}") + sys.exit(1) + + +# contextlib implemented chdir since Python 3.11 +@contextlib.contextmanager +def chdir(path: Path): + """Temporarily change the working directory.""" + original_dir = Path.cwd() + logger.info(path) + os.chdir(path) + try: + yield + finally: + os.chdir(original_dir) + + +def build_docs(language: str, version: str, po_dir: Path, logs_dir: Path, cpython_path: Path) -> None: + """Build the documentation using Sphinx.""" + warning_log = logs_dir / "sphinx_warnings_build_docs.txt" + sphinx_opts = ["-E", "-Dgettext_compact=0", f"-Dlanguage={language}", "--keep-going", "-w", f"{warning_log}"] + locale_dirs = cpython_path / "Doc/locales" + target_locale_dir = cpython_path / "Doc/locales" / language / "LC_MESSAGES" + + # TODO Fix symlinking when po_dir is not equal to target_locale_dir + #if not po_dir.relative_to(locale_dirs) and + # not target_locale_dir.readlink() == po_dir: + # if target_locale_dir.is_symlink(): + # target_locale_dir.unlink() # remove only if it is a symlink + # if not target_locale_dir.exists() and not target_locale_dir.is_symlink(): + # (locale_dirs / language).mkdir(parents=True, exist_ok=True) + # os.symlink(po_dir, target_locale_dir) + + try: + logger.info(f"Building documentation for {language}, Python {version}.") + subprocess.run([ + "make", "-C", str(cpython_path / "Doc"), "html", f"SPHINXOPTS={' '.join(sphinx_opts)}" + ], check=True) + + if warning_log.exists() and not warning_log.stat().st_size: + warning_log.unlink() + logger.info("Removed empty warning log file.") + + except subprocess.CalledProcessError as e: + logger.error(f"Make command failed: {e}") + sys.exit(1) + + +def generate_templates(logs_dir: Path, cpython_path: Path, tx_project: str) -> None: + """Generate translation template files (a.k.a. POT files) with Sphinx""" + warning_log = logs_dir / "sphinx_warnings_generate_templates.txt" + all_sphinx_opts = [ + "-E", "-b", "gettext", "-Dgettext_compact=0", "--keep-going", + "-w", f"{warning_log}", "-d", "build/.doctrees-gettext", ".", "build/gettext" + ] + + try: + logger.info("Generating template files for Python docs.") + subprocess.run([ + "make", "-C", str(cpython_path / "Doc"), "build", f"ALLSPHINXOPTS={' '.join(all_sphinx_opts)}" + ], check=True) + + if warning_log.exists() and not warning_log.stat().st_size: + warning_log.unlink() + logger.info("Removed empty warning log file.") + + except subprocess.CalledProcessError as e: + logger.error(f"Make command failed: {e}") + sys.exit(1) + + with chdir(cpython_path / "Doc/locales"): + logger.info("Updating Transifex's resources configuration file") + Path(".tx/config").unlink(missing_ok=True) + create_txconfig() + update_txconfig_resources( + transifex_organization_name='python-doc', + transifex_project_name=tx_project, + locale_dir=Path("."), + pot_dir=Path("../build/gettext") + ) + + +def main() -> None: + parser = configure_parser() + args = parser.parse_args() + + # Set and require variable depending on the command issued by the user + cpython_path = args.cpython_path + logs_dir = Path(get_value(str(args.logs_dir), "--logs-dir", "PYDOC_LOGS")) + + if args.command == "generate_templates": + tx_project = get_value(args.tx_project, "--tx-project", "PYDOC_TX_PROJECT") + + if args.command == "build": + language = get_value(args.language, "--language", "PYDOC_LANGUAGE") + version = get_value(args.python_version, "--python-version", "PYDOC_VERSION") + po_dir = args.po_dir.absolute() or cpython_path / f"Doc/locales/{language}/LC_MESSAGES" + + if args.command in ["build", "generate_templates"]: + if not shutil.which("make"): + logger.error("'make' not found. Please install it.") + sys.exit(1) + + logs_dir.mkdir(exist_ok=True) + logger.info(f"Logs will be stored in: {logs_dir}") + + if args.command == "build": + validate_cpython_path(cpython_path) + validate_po_dir(po_dir) + build_docs(language, version, po_dir, logs_dir, cpython_path) + logger.info("Documentation build completed successfully.") + elif args.command == "generate_templates": + validate_cpython_path(cpython_path) + validate_tx_config(tx_project) + generate_templates(logs_dir, cpython_path, tx_project) + + +if __name__ == "__main__": + main() +