Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 04f9259

Browse files
committedMay 12, 2025·
Update scripts to handle tranlsations
1 parent d7f0545 commit 04f9259

File tree

7 files changed

+230
-138
lines changed

7 files changed

+230
-138
lines changed
 

‎.github/workflows/docbuild-and-upload.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ jobs:
5050
# https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions-azure-pipelines-travis-ci-and-gitlab-ci-cd
5151
run: sudo apt-get update && sudo apt-get install -y libegl1 libopengl0
5252

53-
- name: Download and update translations
54-
run: python web/pandas_translations.py
55-
5653
- name: Test website
5754
run: python -m pytest web/
5855

‎.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ doc/source/savefig/
145145

146146
# Web & Translations #
147147
##############################
148-
web/pandas-translations.tar.gz
149148
web/translations/
150149
web/pandas/pt/
151150
web/pandas/es/

‎web/pandas/_templates/layout.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html>
2+
<html lang="{{ selected_language }}">
33
<head>
44
<script defer data-domain="pandas.pydata.org" src="https://views.scientific-python.org/js/script.js"></script>
55
<title>pandas - Python Data Analysis Library</title>
@@ -15,6 +15,8 @@
1515
href="{{ base_url }}{{ stylesheet }}">
1616
{% endfor %}
1717
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
18+
<meta id="languages" data-lang="{{ languages }}">
19+
<script src="{{ base_url }}static/js/language_switcher.js"></script>
1820
</head>
1921
<body>
2022
<header>
@@ -50,6 +52,8 @@
5052
</li>
5153
{% endif %}
5254
{% endfor %}
55+
<!-- Language switcher -->
56+
<div id="language-switcher-container"></div>
5357
</ul>
5458
</div>
5559
</div>

‎web/pandas/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,9 @@ sponsors:
204204
kind: partner
205205
roadmap:
206206
pdeps_path: pdeps
207+
translations:
208+
url: https://github.com/Scientific-Python-Translations/pandas-translations/archive/refs/heads/main.tar.gz
209+
folder: translations
210+
default_language: 'en'
211+
ignore:
212+
- docs/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
window.addEventListener("DOMContentLoaded", function() {
2+
var BASE_URL = location.protocol + "//" + location.hostname + ":" + location.port
3+
var CURRENT_LANGUAGE = document.documentElement.lang;
4+
var PATHNAME = location.pathname.replace('/' + CURRENT_LANGUAGE + '/', '')
5+
var languages = JSON.parse(document.getElementById("languages").getAttribute('data-lang').replace(/'/g, '"'));
6+
const language_names = {
7+
'en': 'English',
8+
'es': 'Español',
9+
'fr': 'Français',
10+
'pt': 'Português'
11+
}
12+
13+
// Create dropdown menu
14+
function makeDropdown(options) {
15+
var dropdown = document.createElement("li");
16+
dropdown.classList.add("nav-item");
17+
dropdown.classList.add("dropdown");
18+
19+
var link = document.createElement("a");
20+
link.classList.add("nav-link");
21+
link.classList.add("dropdown-toggle");
22+
link.setAttribute("data-bs-toggle", "dropdown");
23+
link.setAttribute("href", "#");
24+
link.setAttribute("role", "button");
25+
link.setAttribute("aria-haspopup", "true");
26+
link.setAttribute("aria-expanded", "false");
27+
link.textContent = language_names[CURRENT_LANGUAGE];
28+
29+
var dropdownMenu = document.createElement("div");
30+
dropdownMenu.classList.add("dropdown-menu");
31+
32+
options.forEach(function(i) {
33+
var dropdownItem = document.createElement("a");
34+
dropdownItem.classList.add("dropdown-item");
35+
dropdownItem.textContent = language_names[i] || i.toUpperCase();
36+
dropdownItem.setAttribute("href", "#");
37+
dropdownItem.addEventListener("click", function() {
38+
if (i == 'en') {
39+
URL_LANGUAGE = '';
40+
} else {
41+
URL_LANGUAGE = '/' + i;
42+
}
43+
var PATHNAME = location.pathname.replace('/' + CURRENT_LANGUAGE + '/', '/')
44+
var newUrl = BASE_URL + URL_LANGUAGE + PATHNAME
45+
window.location.href = newUrl;
46+
});
47+
dropdownMenu.appendChild(dropdownItem);
48+
});
49+
50+
dropdown.appendChild(link);
51+
dropdown.appendChild(dropdownMenu);
52+
return dropdown;
53+
}
54+
55+
var container = document.getElementById("language-switcher-container");
56+
if (container) {
57+
var dropdown = makeDropdown(languages);
58+
container.appendChild(dropdown);
59+
}
60+
});

‎web/pandas_translations.py

Lines changed: 0 additions & 89 deletions
This file was deleted.

‎web/pandas_web.py

Lines changed: 159 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@
2828
import collections
2929
import datetime
3030
import importlib
31+
import io
3132
import itertools
3233
import json
3334
import operator
3435
import os
3536
import pathlib
3637
import re
3738
import shutil
39+
from subprocess import (
40+
PIPE,
41+
Popen,
42+
)
3843
import sys
44+
import tarfile
3945
import time
4046
import typing
4147

@@ -81,11 +87,15 @@ def navbar_add_info(context):
8187
``has_subitems`` that tells which one of them every element is. It
8288
also adds a ``slug`` field to be used as a CSS id.
8389
"""
90+
ignore = context["translations"]["ignore"]
8491
for i, item in enumerate(context["navbar"]):
92+
if item["target"] in ignore:
93+
item["target"] = "/" + item["target"]
94+
8595
context["navbar"][i] = dict(
8696
item,
8797
has_subitems=isinstance(item["target"], list),
88-
slug=(item["name"].replace(" ", "-").lower()),
98+
slug=item["name"].replace(" ", "-").lower(),
8999
)
90100
return context
91101

@@ -386,16 +396,22 @@ def get_callable(obj_as_str: str) -> object:
386396
return obj
387397

388398

399+
def get_config(config_fname: str) -> dict:
400+
with open(config_fname, encoding="utf-8") as f:
401+
context = yaml.safe_load(f)
402+
return context
403+
404+
389405
def get_context(config_fname: str, **kwargs):
390406
"""
391407
Load the config yaml as the base context, and enrich it with the
392408
information added by the context preprocessors defined in the file.
393409
"""
394-
with open(config_fname, encoding="utf-8") as f:
395-
context = yaml.safe_load(f)
396-
410+
context = get_config(config_fname)
397411
context["source_path"] = os.path.dirname(config_fname)
398412
context.update(kwargs)
413+
context["languages"] = context.get("languages", ["en"])
414+
context["selected_language"] = context.get("language", "en")
399415

400416
preprocessors = (
401417
get_callable(context_prep)
@@ -409,14 +425,27 @@ def get_context(config_fname: str, **kwargs):
409425
return context
410426

411427

412-
def get_source_files(source_path: str) -> typing.Generator[str, None, None]:
428+
def get_source_files(
429+
source_path: str, language, languages
430+
) -> typing.Generator[str, None, None]:
413431
"""
414432
Generate the list of files present in the source directory.
415433
"""
434+
paths = []
435+
all_languages = languages[:]
436+
all_languages.remove(language)
416437
for root, dirs, fnames in os.walk(source_path):
417438
root_rel_path = os.path.relpath(root, source_path)
418439
for fname in fnames:
419-
yield os.path.join(root_rel_path, fname)
440+
path = os.path.join(root_rel_path, fname)
441+
for language in all_languages:
442+
if path.startswith(language + "/"):
443+
break
444+
else:
445+
paths.append(path)
446+
447+
for path in paths:
448+
yield path
420449

421450

422451
def extend_base_template(content: str, base_template: str) -> str:
@@ -431,6 +460,51 @@ def extend_base_template(content: str, base_template: str) -> str:
431460
return result
432461

433462

463+
def download_and_extract_translations(url: str, dir_name: str):
464+
"""
465+
Download the translations from the GitHub repository.
466+
"""
467+
response = requests.get(url)
468+
if response.status_code == 200:
469+
doc = io.BytesIO(response.content)
470+
with tarfile.open(None, "r:gz", doc) as tar:
471+
tar.extractall(dir_name)
472+
else:
473+
raise Exception(f"Failed to download translations: {response.status_code}")
474+
475+
476+
def get_languages(source_path: str):
477+
"""
478+
Get the list of languages available in the translations directory.
479+
"""
480+
languages_path = f"{source_path}/pandas-translations-main/web/pandas/"
481+
en_path = f"{languages_path}/en/"
482+
if os.path.exists(en_path):
483+
shutil.rmtree(en_path)
484+
485+
paths = os.listdir(languages_path)
486+
return [path for path in paths if os.path.isdir(f"{languages_path}/{path}")]
487+
488+
489+
def copy_translations(source_path: str, target_path: str, languages: list[str]):
490+
"""
491+
Copy the translations to the appropriate directory.
492+
"""
493+
languages_path = f"{source_path}/pandas-translations-main/web/pandas/"
494+
for lang in languages:
495+
cmds = [
496+
"rsync",
497+
"-av",
498+
"--delete",
499+
f"{languages_path}/{lang}/",
500+
f"{target_path}/{lang}/",
501+
]
502+
p = Popen(cmds, stdout=PIPE, stderr=PIPE)
503+
stdout, stderr = p.communicate()
504+
sys.stderr.write(stdout.decode())
505+
sys.stderr.write(stderr.decode())
506+
507+
434508
def main(
435509
source_path: str,
436510
target_path: str,
@@ -441,58 +515,99 @@ def main(
441515
For ``.md`` and ``.html`` files, render them with the context
442516
before copying them. ``.md`` files are transformed to HTML.
443517
"""
444-
config_fname = os.path.join(source_path, "config.yml")
518+
base_folder = os.path.dirname(__file__)
519+
base_source_path = source_path
520+
base_target_path = target_path
521+
base_config_fname = os.path.join(source_path, "config.yml")
445522

446-
shutil.rmtree(target_path, ignore_errors=True)
447-
os.makedirs(target_path, exist_ok=True)
523+
config = get_config(base_config_fname)
524+
translations_path = os.path.join(base_folder, f"{config['translations']['folder']}")
448525

449-
sys.stderr.write("Generating context...\n")
450-
context = get_context(config_fname, target_path=target_path)
451-
sys.stderr.write("Context generated\n")
526+
sys.stderr.write("Downloading and extracting translations...\n")
527+
download_and_extract_translations(config["translations"]["url"], translations_path)
452528

453-
templates_path = os.path.join(source_path, context["main"]["templates_path"])
454-
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_path))
529+
translated_languages = get_languages(translations_path)
530+
default_language = config["translations"]["default_language"]
531+
languages = [default_language] + translated_languages
455532

456-
for fname in get_source_files(source_path):
457-
if os.path.normpath(fname) in context["main"]["ignore"]:
458-
continue
533+
sys.stderr.write("Copying translations...\n")
534+
copy_translations(translations_path, source_path, translated_languages)
535+
536+
for language in languages:
537+
sys.stderr.write(f"\nProcessing language: {language}...\n\n")
538+
if language != default_language:
539+
target_path = os.path.join(base_target_path, language)
540+
source_path = os.path.join(base_source_path, language)
541+
542+
shutil.rmtree(target_path, ignore_errors=True)
543+
os.makedirs(target_path, exist_ok=True)
544+
545+
config_fname = os.path.join(source_path, "config.yml")
546+
sys.stderr.write("Generating context...\n")
547+
548+
context = get_context(
549+
config_fname,
550+
target_path=target_path,
551+
language=language,
552+
languages=languages,
553+
)
554+
sys.stderr.write("Context generated\n")
459555

460-
sys.stderr.write(f"Processing {fname}\n")
461-
dirname = os.path.dirname(fname)
462-
os.makedirs(os.path.join(target_path, dirname), exist_ok=True)
556+
templates_path = os.path.join(source_path, context["main"]["templates_path"])
557+
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_path))
463558

464-
extension = os.path.splitext(fname)[-1]
465-
if extension in (".html", ".md"):
466-
with open(os.path.join(source_path, fname), encoding="utf-8") as f:
467-
content = f.read()
468-
if extension == ".md":
469-
body = markdown.markdown(
470-
content, extensions=context["main"]["markdown_extensions"]
559+
for fname in get_source_files(source_path, language, languages):
560+
if os.path.normpath(fname) in context["main"]["ignore"]:
561+
continue
562+
563+
sys.stderr.write(f"Processing {fname}\n")
564+
dirname = os.path.dirname(fname)
565+
os.makedirs(os.path.join(target_path, dirname), exist_ok=True)
566+
567+
extension = os.path.splitext(fname)[-1]
568+
if extension in (".html", ".md"):
569+
with open(os.path.join(source_path, fname), encoding="utf-8") as f:
570+
content = f.read()
571+
if extension == ".md":
572+
body = markdown.markdown(
573+
content, extensions=context["main"]["markdown_extensions"]
574+
)
575+
# Apply Bootstrap's table formatting manually
576+
# Python-Markdown doesn't let us config table attributes by hand
577+
body = body.replace(
578+
"<table>", '<table class="table table-bordered">'
579+
)
580+
content = extend_base_template(
581+
body, context["main"]["base_template"]
582+
)
583+
context["base_url"] = "".join(
584+
["../"] * os.path.normpath(fname).count("/")
471585
)
472-
# Apply Bootstrap's table formatting manually
473-
# Python-Markdown doesn't let us config table attributes by hand
474-
body = body.replace("<table>", '<table class="table table-bordered">')
475-
content = extend_base_template(body, context["main"]["base_template"])
476-
context["base_url"] = "".join(["../"] * os.path.normpath(fname).count("/"))
477-
content = jinja_env.from_string(content).render(**context)
478-
fname_html = os.path.splitext(fname)[0] + ".html"
479-
with open(
480-
os.path.join(target_path, fname_html), "w", encoding="utf-8"
481-
) as f:
482-
f.write(content)
483-
else:
484-
shutil.copy(
485-
os.path.join(source_path, fname), os.path.join(target_path, dirname)
486-
)
586+
content = jinja_env.from_string(content).render(**context)
587+
fname_html = os.path.splitext(fname)[0] + ".html"
588+
with open(
589+
os.path.join(target_path, fname_html), "w", encoding="utf-8"
590+
) as f:
591+
f.write(content)
592+
else:
593+
shutil.copy(
594+
os.path.join(source_path, fname), os.path.join(target_path, dirname)
595+
)
596+
return 0
487597

488598

489599
if __name__ == "__main__":
490600
parser = argparse.ArgumentParser(description="Documentation builder.")
601+
602+
# For each language, the output will be written in a subdirectory named
603+
# after the language (default: build/, build/es, etc...)
491604
parser.add_argument(
492-
"source_path", help="path to the source directory (must contain config.yml)"
605+
"--target-path", default="build", help="directory where to write the output."
493606
)
607+
# e.g. python pandas_web.py --source_path pandas/
494608
parser.add_argument(
495-
"--target-path", default="build", help="directory where to write the output"
609+
"source_path",
610+
help="path to the source directory (must contain each language folder)",
496611
)
497612
args = parser.parse_args()
498613
sys.exit(main(args.source_path, args.target_path))

0 commit comments

Comments
 (0)
Please sign in to comment.