Skip to content

Commit 568df6c

Browse files
authored
Add i18n support for exercise and solution labels (#75)
* Add i18n support for exercise and solution labels Introduces internationalization for 'Exercise' and 'Solution to' labels using Sphinx's message catalog system. Adds translation JSONs, conversion script, and locale files for multiple languages. Updates code to use translated labels and registers message catalog in Sphinx extension setup. * Update project name and metadata in pyproject.toml Changed the project name to 'teachbooks-sphinx-exercise' and updated the description to indicate this is a TeachBooks temporary version. Added a new author to the authors list. * Replace CI workflow and add Dutch translation tests Removed the old CI workflow and introduced a new Python publishing workflow for building, publishing, and signing releases. Added initial files for Dutch translation tests under the tests/books/test-dutch directory. * Configure wheel build package path for Hatch Added [tool.hatch.build.targets.wheel] section to specify the package directory for building wheels, ensuring correct packaging of 'src/sphinx_exercise'. * Rename package to teachbooks_sphinx_exercise Renamed all occurrences of the 'sphinx_exercise' package to 'teachbooks_sphinx_exercise', including source files, assets, translations, and configuration in pyproject.toml. Updated optional dependencies and version path in pyproject.toml to reflect the new package name. * Revert "Rename package to teachbooks_sphinx_exercise" This reverts commit b56c535. * Revert "Configure wheel build package path for Hatch" This reverts commit 71ffa4a. * Revert "Replace CI workflow and add Dutch translation tests" This reverts commit e72061c. * Revert "Update project name and metadata in pyproject.toml" This reverts commit 2dab815.
1 parent a098092 commit 568df6c

File tree

48 files changed

+440
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+440
-7
lines changed

MANIFEST.in

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
include LICENSE
2+
include MANIFEST.in
3+
include README.md
4+
5+
recursive-include sphinx_exercise *.js
6+
recursive-include sphinx_exercise *.css
7+
8+
recursive-include sphinx_exercise *.json
9+
recursive-include sphinx_exercise *.mo
10+
recursive-include sphinx_exercise *.po
11+
recursive-include sphinx_exercise *.py

sphinx_exercise/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
__version__ = "1.0.1"
1111

12-
12+
import os
1313
from pathlib import Path
1414
from typing import Any, Dict, Set, Union, cast
1515
from sphinx.config import Config
@@ -19,6 +19,7 @@
1919
from docutils.nodes import Node
2020
from sphinx.util import logging
2121
from sphinx.util.fileutil import copy_asset
22+
from sphinx.locale import get_translation
2223

2324
from ._compat import findall
2425
from .directive import (
@@ -65,10 +66,11 @@
6566

6667
logger = logging.getLogger(__name__)
6768

69+
MESSAGE_CATALOG_NAME = "exercise"
70+
translate = get_translation(MESSAGE_CATALOG_NAME)
6871

6972
# Callback Functions
7073

71-
7274
def purge_exercises(app: Sphinx, env: BuildEnvironment, docname: str) -> None:
7375
"""Purge sphinx_exercise registry"""
7476

@@ -106,7 +108,7 @@ def init_numfig(app: Sphinx, config: Config) -> None:
106108
"""Initialize numfig"""
107109

108110
config["numfig"] = True
109-
numfig_format = {"exercise": "Exercise %s"}
111+
numfig_format = {"exercise": f"{translate('Exercise')} %s"}
110112
# Merge with current sphinx settings
111113
numfig_format.update(config.numfig_format)
112114
config.numfig_format = numfig_format
@@ -211,6 +213,11 @@ def setup(app: Sphinx) -> Dict[str, Any]:
211213

212214
app.add_css_file("exercise.css")
213215

216+
# add translations
217+
package_dir = os.path.abspath(os.path.dirname(__file__))
218+
locale_dir = os.path.join(package_dir, "translations", "locales")
219+
app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir)
220+
214221
return {
215222
"version": "builtin",
216223
"parallel_read_safe": True,

sphinx_exercise/directive.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929

3030
logger = logging.getLogger(__name__)
3131

32+
from sphinx.locale import get_translation
33+
MESSAGE_CATALOG_NAME = "exercise"
34+
translate = get_translation(MESSAGE_CATALOG_NAME)
35+
3236

3337
class SphinxExerciseBaseDirective(SphinxDirective):
3438
def duplicate_labels(self, label):
@@ -88,7 +92,7 @@ class : str,
8892
}
8993

9094
def run(self) -> List[Node]:
91-
self.defaults = {"title_text": "Exercise"}
95+
self.defaults = {"title_text": f"{translate('Exercise')}"}
9296
self.serial_number = self.env.new_serialno()
9397

9498
# Initialise Registry (if needed)
@@ -216,7 +220,7 @@ class : str,
216220
solution_node = solution_node
217221

218222
def run(self) -> List[Node]:
219-
self.defaults = {"title_text": "Solution to"}
223+
self.defaults = {"title_text": f"{translate('Solution to')}"}
220224
target_label = self.arguments[0]
221225
self.serial_number = self.env.new_serialno()
222226

sphinx_exercise/nodes.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
LaTeX = LaTeXMarkup()
2020

2121

22+
from sphinx.locale import get_translation
23+
MESSAGE_CATALOG_NAME = "exercise"
24+
translate = get_translation(MESSAGE_CATALOG_NAME)
25+
2226
# Nodes
2327

2428

@@ -50,7 +54,7 @@ class solution_end_node(docutil_nodes.Admonition, docutil_nodes.Element):
5054
class exercise_title(docutil_nodes.title):
5155
def default_title(self):
5256
title_text = self.children[0].astext()
53-
if title_text == "Exercise" or title_text == "Exercise %s":
57+
if title_text == f"{translate('Exercise')}" or title_text == f"{translate('Exercise')} %s":
5458
return True
5559
else:
5660
return False
@@ -63,7 +67,7 @@ class exercise_subtitle(docutil_nodes.subtitle):
6367
class solution_title(docutil_nodes.title):
6468
def default_title(self):
6569
title_text = self.children[0].astext()
66-
if title_text == "Solution to":
70+
if title_text == f"{translate('Solution to')}":
6771
return True
6872
else:
6973
return False
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
JSONs created using GitHub Copilot Pro.
2+
3+
To convert to locale files run `_convert.py` in this folder.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
import subprocess
5+
6+
MESSAGE_CATALOG_NAME = "exercise"
7+
8+
def convert_json(folder=None):
9+
folder = folder or Path(__file__).parent
10+
11+
# remove exising
12+
for path in (folder / "locales").glob(f"**/{MESSAGE_CATALOG_NAME}.po"):
13+
path.unlink()
14+
15+
# compile po
16+
for path in (folder / "jsons").glob("*.json"):
17+
data = json.loads(path.read_text("utf8"))
18+
assert data[0]["symbol"] == "en"
19+
english = data[0]["text"]
20+
for item in data[1:]:
21+
language = item["symbol"]
22+
out_path = folder / "locales" / language / "LC_MESSAGES" / f"{MESSAGE_CATALOG_NAME}.po"
23+
if not out_path.parent.exists():
24+
out_path.parent.mkdir(parents=True)
25+
if not out_path.exists():
26+
header = f"""
27+
msgid ""
28+
msgstr ""
29+
"Project-Id-Version: Sphinx-Exercise\\n"
30+
"MIME-Version: 1.0\\n"
31+
"Content-Type: text/plain; charset=UTF-8\\n"
32+
"Content-Transfer-Encoding: 8bit\\n"
33+
"Language: {language}\\n"
34+
"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
35+
"""
36+
out_path.write_text(header)
37+
38+
with out_path.open("a", encoding="utf8") as f:
39+
f.write("\n")
40+
f.write(f'msgid "{english}"\n')
41+
text = item["text"].replace('"', '\\"')
42+
f.write(f'msgstr "{text}"\n')
43+
44+
# compile mo
45+
for path in (folder / "locales").glob(f"**/{MESSAGE_CATALOG_NAME}.po"):
46+
print(path)
47+
subprocess.check_call(
48+
[
49+
"msgfmt",
50+
os.path.abspath(path),
51+
"-o",
52+
os.path.abspath(path.parent / f"{MESSAGE_CATALOG_NAME}.mo"),
53+
]
54+
)
55+
56+
57+
if __name__ == "__main__":
58+
convert_json()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{"language":"English","symbol":"en","text":"Exercise"},
3+
{"language":"Spanish","symbol":"es","text":"Ejercicio"},
4+
{"language":"French","symbol":"fr","text":"Exercice"},
5+
{"language":"Bengali","symbol":"bn","text":"অনুশীলনী"},
6+
{"language":"Russian","symbol":"ru","text":"Упражнение"},
7+
{"language":"Portuguese","symbol":"pt","text":"Exercício"},
8+
{"language":"Indonesian","symbol":"id","text":"Latihan"},
9+
{"language":"German","symbol":"de","text":"Übung"},
10+
{"language":"Vietnamese","symbol":"vi","text":"Bài tập"},
11+
{"language":"Tamil","symbol":"ta","text":"பயிற்சி"},
12+
{"language":"Italian","symbol":"it","text":"Esercizio"},
13+
{"language":"Dutch","symbol":"nl","text":"Opgave"},
14+
{"language":"Greek","symbol":"el","text":"Άσκηση"},
15+
{"language":"Polish","symbol":"pl","text":"Ćwiczenie"},
16+
{"language":"Ukrainian","symbol":"uk","text":"Вправа"},
17+
{"language":"Malay","symbol":"ms","text":"Latihan"},
18+
{"language":"Romanian","symbol":"ro","text":"Exercițiu"},
19+
{"language":"Czech","symbol":"cs","text":"Cvičení"},
20+
{"language":"Hungarian","symbol":"hu","text":"Gyakorlat"},
21+
{"language":"Swedish","symbol":"sv","text":"Övning"},
22+
{"language":"Norwegian","symbol":"no","text":"Øvelse"}
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{"language":"English","symbol":"en","text":"Solution to"},
3+
{"language":"Spanish","symbol":"es","text":"Solución a"},
4+
{"language":"French","symbol":"fr","text":"Solution de"},
5+
{"language":"Bengali","symbol":"bn","text":"সমাধান"},
6+
{"language":"Russian","symbol":"ru","text":"Решение к"},
7+
{"language":"Portuguese","symbol":"pt","text":"Solução para"},
8+
{"language":"Indonesian","symbol":"id","text":"Solusi untuk"},
9+
{"language":"German","symbol":"de","text":"Lösung zu"},
10+
{"language":"Vietnamese","symbol":"vi","text":"Lời giải cho"},
11+
{"language":"Tamil","symbol":"ta","text":"தீர்வு"},
12+
{"language":"Italian","symbol":"it","text":"Soluzione a"},
13+
{"language":"Dutch","symbol":"nl","text":"Oplossing van"},
14+
{"language":"Greek","symbol":"el","text":"Λύση στο"},
15+
{"language":"Polish","symbol":"pl","text":"Rozwiązanie do"},
16+
{"language":"Ukrainian","symbol":"uk","text":"Розв'язок до"},
17+
{"language":"Malay","symbol":"ms","text":"Penyelesaian untuk"},
18+
{"language":"Romanian","symbol":"ro","text":"Soluția pentru"},
19+
{"language":"Czech","symbol":"cs","text":"Řešení k"},
20+
{"language":"Hungarian","symbol":"hu","text":"Megoldás a"},
21+
{"language":"Swedish","symbol":"sv","text":"Lösning till"},
22+
{"language":"Norwegian","symbol":"no","text":"Løsning til"}
23+
]
369 Bytes
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
msgid ""
3+
msgstr ""
4+
"Project-Id-Version: Sphinx-Exercise\n"
5+
"MIME-Version: 1.0\n"
6+
"Content-Type: text/plain; charset=UTF-8\n"
7+
"Content-Transfer-Encoding: 8bit\n"
8+
"Language: bn\n"
9+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
10+
11+
msgid "Exercise"
12+
msgstr "অনুশীলনী"
13+
14+
msgid "Solution to"
15+
msgstr "সমাধান"

0 commit comments

Comments
 (0)