Skip to content

ENH: Implement translations infrastructure #61380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
52 changes: 50 additions & 2 deletions web/pandas/_templates/layout.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="{{ lang }}">
<head>
<script defer data-domain="pandas.pydata.org" src="https://views.scientific-python.org/js/script.js"></script>
<title>pandas - Python Data Analysis Library</title>
Expand All @@ -15,6 +15,38 @@
href="{{ base_url }}{{ stylesheet }}">
{% endfor %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<script type="text/javascript">
function changeLanguage(lang) {
var absBaseUrl = document.baseURI;
var baseUrl = location.protocol + "//" + location.hostname
var currentLanguage = document.documentElement.lang;
var languages = [
{% for lang, name in translations["languages"].items() -%}
"{{ lang }}",
{% endfor -%}
]

if (location.port) {
baseUrl = baseUrl + ":" + location.port
}

// Handle preview URLs on github
// If preview URL changes, this regex will need to be updated
var re = /preview\/pandas-dev\/pandas\/(?<pr>[0-9]*)\//g;
var previewUrl = '';
for (const match of absBaseUrl.matchAll(re)) {
previewUrl = `/preview/pandas-dev/pandas/${match.groups.pr}`;
}
var pathName = location.pathname.replace(previewUrl, '')
var urlLanguage = '';
if (lang !== 'en') {
urlLanguage = '/' + lang;
}
pathName = pathName.replace('/' + currentLanguage + '/', '/')
var newUrl = baseUrl + previewUrl + urlLanguage + pathName
window.location.href = newUrl;
}
</script>
</head>
<body>
<header>
Expand All @@ -28,7 +60,7 @@

<div class="collapse navbar-collapse" id="nav-content">
<ul class="navbar-nav ms-auto">
{% for item in navbar %}
{% for item in navbar[lang] %}
{% if not item.has_subitems %}
<li class="nav-item">
<a class="nav-link" href="{% if not item.target.startswith("http") %}{{ base_url }}{% endif %}{{ item.target }}">{{ item.name }}</a>
Expand All @@ -50,6 +82,22 @@
</li>
{% endif %}
{% endfor %}
<!-- Language switcher -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
data-bs-toggle="dropdown"
href="#"
role="button"
aria-haspopup="true"
aria-expanded="false">{{ translations["languages"][lang] }}</a>
<div class="dropdown-menu">
{% for language, name in translations["languages"].items() -%}
<a class="dropdown-item"
href="#"
onclick="changeLanguage('{{ language }}')">{{ name }}</a>
{% endfor -%}
</div>
</li>
</ul>
</div>
</div>
Expand Down
44 changes: 11 additions & 33 deletions web/pandas/config.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
main:
templates_path: _templates
base_template: "layout.html"
navbar_fname: "navbar.yml"
production_url: "https://pandas.pydata.org/"
ignore:
- _templates/layout.html
- config.yml
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
Expand All @@ -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
Expand Down Expand Up @@ -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
34 changes: 34 additions & 0 deletions web/pandas/navbar.yml
Original file line number Diff line number Diff line change
@@ -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
117 changes: 104 additions & 13 deletions web/pandas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import collections
import datetime
import importlib
import io
import itertools
import json
import operator
Expand All @@ -36,6 +37,7 @@
import re
import shutil
import sys
import tarfile
import time
import typing

Expand Down Expand Up @@ -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.
Expand All @@ -74,23 +76,110 @@ 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()),
)
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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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__":
Expand Down