diff --git a/web/pandas/_templates/layout.html b/web/pandas/_templates/layout.html index c26b093b0c4ba..eeefc715b1853 100644 --- a/web/pandas/_templates/layout.html +++ b/web/pandas/_templates/layout.html @@ -1,5 +1,5 @@ - + pandas - Python Data Analysis Library @@ -15,6 +15,38 @@ href="{{ base_url }}{{ stylesheet }}"> {% endfor %} +
@@ -28,7 +60,7 @@ diff --git a/web/pandas/config.yml b/web/pandas/config.yml index cb5447591dab6..b0a4bc8f5cf7e 100644 --- a/web/pandas/config.yml +++ b/web/pandas/config.yml @@ -1,6 +1,7 @@ main: templates_path: _templates base_template: "layout.html" + navbar_fname: "navbar.yml" production_url: "https://pandas.pydata.org/" ignore: - _templates/layout.html @@ -8,7 +9,10 @@ main: github_repo_url: pandas-dev/pandas context_preprocessors: - pandas_web.Preprocessors.current_year + - pandas_web.Preprocessors.download_translated_content + - pandas_web.Preprocessors.add_navbar_content - pandas_web.Preprocessors.navbar_add_info + - pandas_web.Preprocessors.navbar_add_translated_info - pandas_web.Preprocessors.blog_add_posts - pandas_web.Preprocessors.maintainers_add_info - pandas_web.Preprocessors.home_add_releases @@ -25,39 +29,6 @@ static: css: - static/css/pandas.css - static/css/codehilite.css -navbar: - - name: "About us" - target: - - name: "About pandas" - target: about/ - - name: "Project roadmap" - target: about/roadmap.html - - name: "Governance" - target: about/governance.html - - name: "Team" - target: about/team.html - - name: "Sponsors" - target: about/sponsors.html - - name: "Citing and logo" - target: about/citing.html - - name: "Getting started" - target: getting_started.html - - name: "Documentation" - target: docs/ - - name: "Community" - target: - - name: "Blog" - target: community/blog/ - - name: "Ask a question (StackOverflow)" - target: https://stackoverflow.com/questions/tagged/pandas - - name: "Code of conduct" - target: community/coc.html - - name: "Ecosystem" - target: community/ecosystem.html - - name: "Benchmarks" - target: community/benchmarks.html - - name: "Contribute" - target: contribute.html blog: num_posts: 50 posts_path: community/blog @@ -204,3 +175,10 @@ sponsors: kind: partner roadmap: pdeps_path: pdeps +translations: + url: https://github.com/Scientific-Python-Translations/pandas-translations/archive/refs/heads/main.tar.gz + source_path: pandas-translations-main/web/pandas/ + languages: + en: English + es: Español + pt: Português diff --git a/web/pandas/navbar.yml b/web/pandas/navbar.yml new file mode 100644 index 0000000000000..07ff0a64314a5 --- /dev/null +++ b/web/pandas/navbar.yml @@ -0,0 +1,34 @@ +navbar: + - name: "About us" + target: + - name: "About pandas" + target: about/ + - name: "Project roadmap" + target: about/roadmap.html + - name: "Governance" + target: about/governance.html + - name: "Team" + target: about/team.html + - name: "Sponsors" + target: about/sponsors.html + - name: "Citing and logo" + target: about/citing.html + - name: "Getting started" + target: getting_started.html + - name: "Documentation" + target: docs/ + translated: false + - name: "Community" + target: + - name: "Blog" + target: community/blog/ + - name: "Ask a question (StackOverflow)" + target: https://stackoverflow.com/questions/tagged/pandas + - name: "Code of conduct" + target: community/coc.html + - name: "Ecosystem" + target: community/ecosystem.html + - name: "Benchmarks" + target: community/benchmarks.html + - name: "Contribute" + target: contribute.html diff --git a/web/pandas_web.py b/web/pandas_web.py index b3872b829c73a..77da821cc98c5 100755 --- a/web/pandas_web.py +++ b/web/pandas_web.py @@ -28,6 +28,7 @@ import collections import datetime import importlib +import io import itertools import json import operator @@ -36,6 +37,7 @@ import re import shutil import sys +import tarfile import time import typing @@ -65,7 +67,7 @@ class Preprocessors: """ @staticmethod - def current_year(context): + def current_year(context: dict) -> dict: """ Add the current year to the context, so it can be used for the copyright note, or other places where it is needed. @@ -74,15 +76,69 @@ def current_year(context): return context @staticmethod - def navbar_add_info(context): + def download_translated_content(context: dict) -> dict: + """ + Download the translations from the mirror translations repository. + https://github.com/Scientific-Python-Translations/pandas-translations + + All translated languages are downloaded, extracted and place inside the + ``pandas`` folder in a separate folder for each language (e.g. ``pandas/es``). + + The extracted folder and the translations folders are deleted before + downloading the information, so the translations are always up to date. + """ + base_folder = os.path.dirname(__file__) + extract_path = os.path.join(base_folder, context["translations"]["source_path"]) + shutil.rmtree(extract_path, ignore_errors=True) + response = requests.get(context["translations"]["url"]) + if response.status_code == 200: + with tarfile.open(None, "r:gz", io.BytesIO(response.content)) as tar: + tar.extractall(base_folder) + else: + raise Exception(f"Failed to download translations: {response.status_code}") + + for lang in list(context["translations"]["languages"].keys())[1:]: + shutil.rmtree(os.path.join(base_folder, "pandas", lang), ignore_errors=True) + shutil.move( + os.path.join(extract_path, lang), + os.path.join(base_folder, "pandas", lang), + ) + return context + + @staticmethod + def add_navbar_content(context: dict) -> dict: + """ + Add the navbar content to the context. + + The navbar content is loaded for all available languages. + """ + context["navbar"] = {} + for lang in context["translations"]["languages"]: + path = os.path.join( + context["source_path"], + "" if lang == "en" else f"{lang}", + context["main"]["navbar_fname"], + ) + if os.path.exists(path): + with open( + path, + encoding="utf-8", + ) as f: + navbar_lang = yaml.safe_load(f) + context["navbar"][lang] = navbar_lang["navbar"] + + return context + + @staticmethod + def navbar_add_info(context: dict) -> dict: """ Items in the main navigation bar can be direct links, or dropdowns with subitems. This context preprocessor adds a boolean field ``has_subitems`` that tells which one of them every element is. It also adds a ``slug`` field to be used as a CSS id. """ - for i, item in enumerate(context["navbar"]): - context["navbar"][i] = dict( + for i, item in enumerate(context["navbar"]["en"]): + context["navbar"]["en"][i] = dict( item, has_subitems=isinstance(item["target"], list), slug=(item["name"].replace(" ", "-").lower()), @@ -90,7 +146,40 @@ def navbar_add_info(context): return context @staticmethod - def blog_add_posts(context): + def navbar_add_translated_info(context: dict) -> dict: + """ + Prepare the translated navbar information for the template. + + Items in the main navigation bar can be direct links, or dropdowns with + subitems. This context preprocessor adds a boolean field + ``has_subitems`` that tells which one of them every element is. It + also adds a ``slug`` field to be used as a CSS id. + """ + + def update_target(item: dict, prefix: str) -> None: + if item.get("translated", True): + item["target"] = f"{prefix}/{item['target']}" + else: + item["target"] = f"../{item['target']}" + + for lang in list(context["translations"]["languages"].keys())[1:]: + for i, item in enumerate(context["navbar"][lang]): + has_subitems = isinstance(item["target"], list) + if has_subitems: + for sub_item in item["target"]: + update_target(sub_item, lang) + else: + update_target(item, lang) + + context["navbar"][lang][i] = dict( + item, + has_subitems=has_subitems, + slug=(item["name"].replace(" ", "-").lower()), + ) + return context + + @staticmethod + def blog_add_posts(context: dict) -> dict: """ Given the blog feed defined in the configuration yaml, this context preprocessor fetches the posts in the feeds, and returns the relevant @@ -162,7 +251,7 @@ def blog_add_posts(context): return context @staticmethod - def maintainers_add_info(context): + def maintainers_add_info(context: dict) -> dict: """ Given the active maintainers defined in the yaml file, it fetches the GitHub user information for them. @@ -212,7 +301,7 @@ def maintainers_add_info(context): return context @staticmethod - def home_add_releases(context): + def home_add_releases(context: dict) -> dict: context["releases"] = [] github_repo_url = context["main"]["github_repo_url"] @@ -272,7 +361,7 @@ def home_add_releases(context): return context @staticmethod - def roadmap_pdeps(context): + def roadmap_pdeps(context: dict) -> dict: """ PDEP's (pandas enhancement proposals) are not part of the bar navigation. They are included as lists in the "Roadmap" page @@ -386,7 +475,7 @@ def get_callable(obj_as_str: str) -> object: return obj -def get_context(config_fname: str, **kwargs): +def get_context(config_fname: str, **kwargs: dict) -> dict: """ Load the config yaml as the base context, and enrich it with the information added by the context preprocessors defined in the file. @@ -441,19 +530,20 @@ def main( For ``.md`` and ``.html`` files, render them with the context before copying them. ``.md`` files are transformed to HTML. """ - config_fname = os.path.join(source_path, "config.yml") - shutil.rmtree(target_path, ignore_errors=True) os.makedirs(target_path, exist_ok=True) sys.stderr.write("Generating context...\n") - context = get_context(config_fname, target_path=target_path) + context = get_context( + os.path.join(source_path, "config.yml"), + target_path=target_path, + ) sys.stderr.write("Context generated\n") templates_path = os.path.join(source_path, context["main"]["templates_path"]) jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_path)) - for fname in get_source_files(source_path): + context["lang"] = fname[0:2] if fname[2] == "/" else "en" if os.path.normpath(fname) in context["main"]["ignore"]: continue @@ -484,6 +574,7 @@ def main( shutil.copy( os.path.join(source_path, fname), os.path.join(target_path, dirname) ) + return 0 if __name__ == "__main__":