From e018c21f99e7627d48090957e06cac09eae6e925 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 23 May 2025 13:53:13 +0200 Subject: [PATCH 1/9] docs: adding translation stats to docs --- TRANSLATING.md | 5 +++ _ext/translation_graph.py | 73 +++++++++++++++++++++++++++++++++++++++ conf.py | 1 + pyproject.toml | 2 ++ 4 files changed, 81 insertions(+) create mode 100644 _ext/translation_graph.py diff --git a/TRANSLATING.md b/TRANSLATING.md index 25e5170d3..2c105f579 100644 --- a/TRANSLATING.md +++ b/TRANSLATING.md @@ -8,6 +8,11 @@ This guide will help you get started contributing to the translation of the Pyth The process of contributing to the translation of the guide is similar to the process of contributing to the guide itself, except that instead of working on the guide source files directly, you will be working on the translation files. +# Translation Status + +```{translation-graph} +``` + ## Overview of the Translation Process The process of adapting software to different languages is called internationalization, or i18n for short. Internationalization makes sure that translation can happen without having to modify the source code, or in our case, the original English source files of the guide. diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py new file mode 100644 index 000000000..684f65ac6 --- /dev/null +++ b/_ext/translation_graph.py @@ -0,0 +1,73 @@ +from pathlib import Path +import json + +from docutils import nodes +from docutils.parsers.rst import Directive +import plotly.graph_objects as go +from plotly.offline import plot + +class TranslationGraph(Directive): + # Tells Sphinx that this directive can be used in the document body + # and has no content + has_content = False + + def run(self): + # Read the JSON file containing translation statistics + json_path = Path(__file__).parent.parent / '_static' / 'translation_stats.json' + with json_path.open('r') as f: + data = json.load(f) + + # Collect all module names -- iterates over the JSON data in 2 levels + all_modules = {module for stats in data.values() for module in stats} + all_modules = sorted(all_modules) + + # Build one trace per locale with full hover info + traces = [] + + for locale, modules in data.items(): + y_vals = [] + hover_texts = [] + + for module in all_modules: + stats = modules.get(module) + y_vals.append(stats["percentage"]) + + hover_text = ( + f"{module}
" + f"Translated: {stats['translated']}
" + f"Fuzzy: {stats['fuzzy']}
" + f"Untranslated: {stats['untranslated']}
" + f"Total: {stats['total']}
" + f"Completed: {stats['percentage']}%" + ) + hover_texts.append(hover_text) + + traces.append(go.Bar( + name=locale, + x=all_modules, + y=y_vals, + hovertext=hover_texts, + hoverinfo="text" + )) + + # Create figure + fig = go.Figure(data=traces) + fig.update_layout( + barmode='group', + title="Translation Coverage by Module and Locale", + xaxis_title="Module", + yaxis_title="Percentage Translated", + height=600, + margin=dict(l=40, r=40, t=40, b=40) + ) + + div = plot(fig, output_type='div', include_plotlyjs=True) + return [nodes.raw('', div, format='html')] + +def setup(app): + app.add_directive("translation-graph", TranslationGraph) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/conf.py b/conf.py index de9920210..63f4f2d98 100644 --- a/conf.py +++ b/conf.py @@ -83,6 +83,7 @@ "sphinxext.opengraph", "sphinx_favicon", "sphinxcontrib.bibtex", + '_ext.translation_graph', ] # colon fence for card support in md diff --git a/pyproject.toml b/pyproject.toml index 5a524d3ed..566466843 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,8 @@ dependencies = [ "sphinx-inline-tabs", # for project cards "matplotlib", + # for translation graphs + "plotly", # for license page bibliography "sphinxcontrib-bibtex", ] From f2ce21d9a7764a2905f84b390200b4f71494577a Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 23 May 2025 14:09:16 +0200 Subject: [PATCH 2/9] Update _ext/translation_graph.py --- _ext/translation_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index 684f65ac6..c689c85db 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -8,7 +8,7 @@ class TranslationGraph(Directive): # Tells Sphinx that this directive can be used in the document body - # and has no content + # and has no content has_content = False def run(self): From a3567ca7e88887d1c65683b01e2e96ed12f5588b Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 23 May 2025 14:17:05 +0200 Subject: [PATCH 3/9] chore: change single quotes for double quotes --- _ext/translation_graph.py | 10 +++++----- conf.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index c689c85db..7ed22b734 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -13,8 +13,8 @@ class TranslationGraph(Directive): def run(self): # Read the JSON file containing translation statistics - json_path = Path(__file__).parent.parent / '_static' / 'translation_stats.json' - with json_path.open('r') as f: + json_path = Path(__file__).parent.parent / "_static" / "translation_stats.json" + with json_path.open("r") as f: data = json.load(f) # Collect all module names -- iterates over the JSON data in 2 levels @@ -53,7 +53,7 @@ def run(self): # Create figure fig = go.Figure(data=traces) fig.update_layout( - barmode='group', + barmode="group", title="Translation Coverage by Module and Locale", xaxis_title="Module", yaxis_title="Percentage Translated", @@ -61,8 +61,8 @@ def run(self): margin=dict(l=40, r=40, t=40, b=40) ) - div = plot(fig, output_type='div', include_plotlyjs=True) - return [nodes.raw('', div, format='html')] + div = plot(fig, output_type="div", include_plotlyjs=True) + return [nodes.raw("", div, format="html")] def setup(app): app.add_directive("translation-graph", TranslationGraph) diff --git a/conf.py b/conf.py index 63f4f2d98..63d1eb72d 100644 --- a/conf.py +++ b/conf.py @@ -83,7 +83,7 @@ "sphinxext.opengraph", "sphinx_favicon", "sphinxcontrib.bibtex", - '_ext.translation_graph', + "_ext.translation_graph", ] # colon fence for card support in md From b3e26d8097aec38d36179eab2deb16fd79f85eee Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Fri, 30 May 2025 08:01:11 +0200 Subject: [PATCH 4/9] Update _ext/translation_graph.py Co-authored-by: Jonny Saunders --- _ext/translation_graph.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index 7ed22b734..b1013f55e 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -1,11 +1,23 @@ from pathlib import Path import json +from typing import TypeAlias, TypedDict, Annotated as A from docutils import nodes from docutils.parsers.rst import Directive import plotly.graph_objects as go from plotly.offline import plot + +class ModuleStats(TypedDict): + total: int + translated: int + fuzzy: int + untranslated: int + percentage: float + +TranslationStats: TypeAlias = dict[A[str, "locale"], dict[A[str, "module"], ModuleStats]] + + class TranslationGraph(Directive): # Tells Sphinx that this directive can be used in the document body # and has no content @@ -15,7 +27,7 @@ def run(self): # Read the JSON file containing translation statistics json_path = Path(__file__).parent.parent / "_static" / "translation_stats.json" with json_path.open("r") as f: - data = json.load(f) + data: TranslationStats = json.load(f) # Collect all module names -- iterates over the JSON data in 2 levels all_modules = {module for stats in data.values() for module in stats} From 11bd218bd1a9e34b83562deaf5cbb7651fbe38de Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:40:44 +0200 Subject: [PATCH 5/9] Apply suggestions from code review Co-authored-by: Jonny Saunders --- _ext/translation_graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index b1013f55e..f92d0df36 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -6,6 +6,7 @@ from docutils.parsers.rst import Directive import plotly.graph_objects as go from plotly.offline import plot +import numpy as np class ModuleStats(TypedDict): From 1c5f63a5331b7649d3dade2de70976dce40c8e5d Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 5 Jun 2025 09:02:35 +0200 Subject: [PATCH 6/9] feat: adding @sneakers-the-rat suggestions --- _ext/translation_graph.py | 96 ++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index f92d0df36..e99732a84 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -24,56 +24,70 @@ class TranslationGraph(Directive): # and has no content has_content = False + # oddly, this is evaluated in the js not python, + # so we treat customdata like a json object + HOVER_TEMPLATE = """ + %{customdata.module}
+ Translated: %{customdata.translated}
+ Fuzzy: %{customdata.fuzzy}
+ Untranslated: %{customdata.untranslated}
+ Total: %{customdata.total}
+ Completed: %{customdata.percentage}% + """ def run(self): # Read the JSON file containing translation statistics json_path = Path(__file__).parent.parent / "_static" / "translation_stats.json" with json_path.open("r") as f: data: TranslationStats = json.load(f) - # Collect all module names -- iterates over the JSON data in 2 levels - all_modules = {module for stats in data.values() for module in stats} - all_modules = sorted(all_modules) - - # Build one trace per locale with full hover info - traces = [] - - for locale, modules in data.items(): - y_vals = [] - hover_texts = [] - - for module in all_modules: - stats = modules.get(module) - y_vals.append(stats["percentage"]) - - hover_text = ( - f"{module}
" - f"Translated: {stats['translated']}
" - f"Fuzzy: {stats['fuzzy']}
" - f"Untranslated: {stats['untranslated']}
" - f"Total: {stats['total']}
" - f"Completed: {stats['percentage']}%" - ) - hover_texts.append(hover_text) - - traces.append(go.Bar( - name=locale, - x=all_modules, - y=y_vals, - hovertext=hover_texts, - hoverinfo="text" - )) - + # Sort data by locale and module + data = {locale: dict(sorted(loc_stats.items())) for locale, loc_stats in sorted(data.items())} + + # prepend english, everything set to 100% + en = {module: ModuleStats(total=stats['total'], translated=stats['total'], fuzzy=stats['total'], untranslated=0, percentage=100) for module, stats in next(iter(data.values())).items()} + data = {'en': en} | data + + # extract data to plot + locales = list(data.keys()) + modules = list(data[locales[-1]].keys()) + values = [[stats['percentage'] for stats in loc_stats.values()] for loc_stats in data.values()] + hoverdata = [[{'module': module} | stats for module, stats in loc_stats.items()] for loc_stats in data.values()] + heatmap = go.Heatmap( + x =modules, + y=locales, + z=values, + xgap=5, + ygap=5, + customdata=np.array(hoverdata), + hovertemplate=self.HOVER_TEMPLATE, + colorbar={ + 'orientation': 'h', + 'y': 0, + "yanchor": "bottom", + "yref": "container", + "title": "Completion %", + "thickness": 10, + }, + colorscale="Plotly3", + ) # Create figure - fig = go.Figure(data=traces) + fig = go.Figure(data=heatmap) fig.update_layout( - barmode="group", - title="Translation Coverage by Module and Locale", - xaxis_title="Module", - yaxis_title="Percentage Translated", - height=600, - margin=dict(l=40, r=40, t=40, b=40) + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + font_color="var(--bs-body-color)", + margin=dict(l=40, r=40, t=40, b=40), + xaxis_showgrid=False, + xaxis_side="top", + xaxis_tickangle=-45, + xaxis_tickfont = { + "family": "var(--bs-font-monospace)", + "color": "#fff" + }, + yaxis_showgrid=False, + yaxis_title="Locale", + yaxis_autorange="reversed", ) - div = plot(fig, output_type="div", include_plotlyjs=True) return [nodes.raw("", div, format="html")] From 5681b0b2b6f22ef0c6a396f3958a02d27e2a8c2a Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:58:25 +0200 Subject: [PATCH 7/9] feat: adding heatmap style colors, percentage and sorting --- _ext/translation_graph.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index e99732a84..d118e8a88 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -47,15 +47,31 @@ def run(self): en = {module: ModuleStats(total=stats['total'], translated=stats['total'], fuzzy=stats['total'], untranslated=0, percentage=100) for module, stats in next(iter(data.values())).items()} data = {'en': en} | data - # extract data to plot + # Calculate average completion percentage for each locale and sort locales + locale_completion = {locale: np.mean([stats['percentage'] for stats in loc_stats.values()]) for locale, loc_stats in data.items()} + sorted_locales = sorted(locale_completion.keys(), key=lambda locale: locale_completion[locale], reverse=True) + + # Reorder data based on sorted locales + data = {locale: data[locale] for locale in sorted_locales} + + # Update locales list after sorting locales = list(data.keys()) - modules = list(data[locales[-1]].keys()) + modules = list(next(iter(data.values())).keys()) + + # Extract data to plot values = [[stats['percentage'] for stats in loc_stats.values()] for loc_stats in data.values()] hoverdata = [[{'module': module} | stats for module, stats in loc_stats.items()] for loc_stats in data.values()] + + # Add text to display percentages directly in the heatmap boxes + text = [[f"{int(stats['percentage'])}%" for stats in loc_stats.values()] for loc_stats in data.values()] + heatmap = go.Heatmap( - x =modules, + x=modules, y=locales, z=values, + text=text, # Add text to the heatmap + texttemplate="%{text}", # Format the text to display directly + textfont={"size": 10}, # Adjust font size for better readability xgap=5, ygap=5, customdata=np.array(hoverdata), @@ -67,8 +83,18 @@ def run(self): "yref": "container", "title": "Completion %", "thickness": 10, + "tickvals": [12.5, 50, 87.5, 100], # Midpoints for each category + "ticktext": ["0-25%", "25-75%", "75-<100%", "100%"], # Labels for categories }, - colorscale="Plotly3", + colorscale=[ + [0.0, "rgb(254, 255, 231)"], # 0-25% + [0.25, "rgb(254, 255, 231)"], + [0.25, "rgb(187, 130, 176)"], # 25-75% + [0.75, "rgb(187, 130, 176)"], + [0.75, "rgb(129, 192, 170)"], # 75-<100% + [0.99, "rgb(129, 192, 170)"], + [1.0, "rgb(0, 128, 0)"], # 100% + ], ) # Create figure fig = go.Figure(data=heatmap) @@ -82,7 +108,6 @@ def run(self): xaxis_tickangle=-45, xaxis_tickfont = { "family": "var(--bs-font-monospace)", - "color": "#fff" }, yaxis_showgrid=False, yaxis_title="Locale", From 0b8720fb2bbc71b422a619e81da232490eef1388 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:11:54 +0200 Subject: [PATCH 8/9] Update _ext/translation_graph.py Co-authored-by: Leah Wasser --- _ext/translation_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index d118e8a88..4756b1324 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -93,7 +93,7 @@ def run(self): [0.75, "rgb(187, 130, 176)"], [0.75, "rgb(129, 192, 170)"], # 75-<100% [0.99, "rgb(129, 192, 170)"], - [1.0, "rgb(0, 128, 0)"], # 100% + [1.0, "rgb(78, 112, 100)"], # 100% ], ) # Create figure From 81f7cb0f3f226f336eab07e264f6be21c9adb168 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:15:06 +0200 Subject: [PATCH 9/9] feat: increasing number size --- _ext/translation_graph.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_ext/translation_graph.py b/_ext/translation_graph.py index 4756b1324..05e7bae98 100644 --- a/_ext/translation_graph.py +++ b/_ext/translation_graph.py @@ -71,7 +71,7 @@ def run(self): z=values, text=text, # Add text to the heatmap texttemplate="%{text}", # Format the text to display directly - textfont={"size": 10}, # Adjust font size for better readability + textfont={"size": 15}, # Adjust font size for better readability xgap=5, ygap=5, customdata=np.array(hoverdata), @@ -87,13 +87,13 @@ def run(self): "ticktext": ["0-25%", "25-75%", "75-<100%", "100%"], # Labels for categories }, colorscale=[ - [0.0, "rgb(254, 255, 231)"], # 0-25% + [0.0, "rgb(254, 255, 231)"], # 0-25% [0.25, "rgb(254, 255, 231)"], [0.25, "rgb(187, 130, 176)"], # 25-75% [0.75, "rgb(187, 130, 176)"], [0.75, "rgb(129, 192, 170)"], # 75-<100% [0.99, "rgb(129, 192, 170)"], - [1.0, "rgb(78, 112, 100)"], # 100% + [1.0, "rgb(78, 112, 100)"], # 100% ], ) # Create figure