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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,12 @@ doc/source/savefig/
# Pyodide/WASM related files #
##############################
/.pyodide-xbuildenv-*


# Web & Translations #
##############################
web/preview/
web/pandas-translations-main/
web/pandas/es/
web/pandas/pt/
web/pandas/fr/
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
42 changes: 9 additions & 33 deletions web/pandas/config.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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.process_translations
- pandas_web.Preprocessors.current_year
- pandas_web.Preprocessors.navbar_add_info
- pandas_web.Preprocessors.blog_add_posts
Expand All @@ -25,39 +27,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 +173,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
89 changes: 72 additions & 17 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,30 @@ class Preprocessors:
"""

@staticmethod
def current_year(context):
def process_translations(context: dict) -> dict:
"""
Download the translations from the GitHub repository and extract them.
"""
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 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 +99,51 @@ def current_year(context):
return context

@staticmethod
def navbar_add_info(context):
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(
item,
has_subitems=isinstance(item["target"], list),
slug=(item["name"].replace(" ", "-").lower()),
)

def update_target(item: dict, url_prefix: str) -> None:
if item.get("translated", True):
item["target"] = f"{url_prefix}{item['target']}"
else:
item["target"] = f"../{item['target']}"

context["navbar"] = {}
for lang in context["translations"]["languages"]:
prefix = "" if lang == "en" else lang
url_prefix = "" if lang == "en" else lang + "/"
with open(
os.path.join(
context["source_path"], prefix, context["main"]["navbar_fname"]
),
encoding="utf-8",
) as f:
navbar_lang = yaml.safe_load(f)

context["navbar"][lang] = navbar_lang["navbar"]
for i, item in enumerate(navbar_lang["navbar"]):
has_subitems = isinstance(item["target"], list)
if lang != "en":
if has_subitems:
for sub_item in item["target"]:
update_target(sub_item, url_prefix)
else:
update_target(item, url_prefix)

context["navbar"][lang][i] = dict(
item,
has_subitems=has_subitems,
slug=(item["name"].replace(" ", "-").lower()),
)
return context

@staticmethod
def blog_add_posts(context):
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 +215,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 +265,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 +325,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 +439,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 +494,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 +538,7 @@ def main(
shutil.copy(
os.path.join(source_path, fname), os.path.join(target_path, dirname)
)
return 0


if __name__ == "__main__":
Expand Down
Loading