Skip to content

Commit 395549a

Browse files
authored
Merge pull request #9 from Scientific-Python-Translations/enh/dashboard
Add status dashboard and contributors pages
2 parents 351b43c + 4c41f77 commit 395549a

File tree

7 files changed

+320
-2
lines changed

7 files changed

+320
-2
lines changed

.github/workflows/deploy_website.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
branches:
66
- main
7+
schedule:
8+
- cron: "0 12 * * SUN" # Every Sunday at noon
79
workflow_dispatch:
810

911
concurrency:
@@ -27,16 +29,21 @@ jobs:
2729
uses: actions/checkout@v4
2830

2931
- name: Install Hugo CLI
30-
env:
31-
HUGO_VERSION:
3232
run: |
3333
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v0.124.1/hugo_extended_0.124.1_linux-amd64.deb \
3434
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
3535
3636
- name: Install Dart Sass Embedded # Installs dart-sass
3737
run: sudo snap install dart-sass-embedded
3838

39+
- name: Install python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: '3.8'
43+
3944
- name: Build with Hugo
45+
env:
46+
CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
4047
run: |
4148
git submodule update --init --recursive # fetch theme
4249
make html

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.hugo_build.lock
2+
content/status.md
3+
content/contributors.md
24
public/
35
resources/_gen/
6+
.DS_Store
7+
.env

.pre-commit-config.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
repos:
2+
- repo: https://github.com/MarcoGorelli/absolufy-imports
3+
rev: v0.3.1
4+
hooks:
5+
- id: absolufy-imports
6+
- repo: https://github.com/hadialqattan/pycln
7+
rev: v2.5.0
8+
hooks:
9+
- id: pycln
10+
- repo: https://github.com/psf/black-pre-commit-mirror
11+
rev: 25.1.0
12+
hooks:
13+
- id: black
14+
pass_filenames: true
15+
- repo: https://github.com/astral-sh/ruff-pre-commit
16+
rev: v0.9.4
17+
hooks:
18+
- id: ruff
19+
- repo: https://github.com/seddonym/import-linter
20+
rev: v2.1
21+
hooks:
22+
- id: import-linter
23+
stages: [manual]
24+
- repo: https://github.com/python-jsonschema/check-jsonschema
25+
rev: 0.31.1
26+
hooks:
27+
- id: check-github-workflows
28+
- repo: https://github.com/psf/black
29+
rev: 3702ba224ecffbcec30af640c149f231d90aebdb # frozen: 24.4.2
30+
hooks:
31+
- id: black
32+
33+
- repo: https://github.com/asottile/pyupgrade
34+
rev: 32151ac97cbfd7f9dcd22e49516fb32266db45b4 # frozen: v3.16.0
35+
hooks:
36+
- id: pyupgrade
37+
args: [--py38-plus]
38+
39+
ci:
40+
autoupdate_schedule: monthly

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ themes: themes/scientific-python-hugo-theme
2323

2424
html: ## Build site in `./public`
2525
html: themes content/shortcodes.md
26+
python -m pip install --upgrade pip
27+
python -m pip install crowdin-api-client python-dotenv
28+
python scripts/update_dashboard.py
2629
hugo
2730

2831
serve: ## Serve site, typically on http://localhost:1313

assets/css/custom.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
.navbar-logo-text {
22
font-family: "Lato";
33
}
4+
5+
.dashboard {
6+
text-align: center;
7+
}

config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ params:
3939
url: /about/
4040
- title: Translate
4141
url: /translate/
42+
- title: Status
43+
url: /status/
44+
- title: Contributors
45+
url: /contributors/
4246
footer:
4347
logo: logo.svg
4448
socialmediatitle: ""

scripts/update_dashboard.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import os
2+
import traceback
3+
from datetime import datetime
4+
from pathlib import Path
5+
6+
from crowdin_api import CrowdinClient # type: ignore
7+
from dotenv import load_dotenv
8+
9+
10+
load_dotenv() # take environment variables
11+
12+
13+
class ScientificCrowdinClient:
14+
15+
def __init__(self, token: str, organization: str):
16+
self._token = token
17+
self._organization = organization
18+
self._client = CrowdinClient(token=token, organization=organization)
19+
20+
def get_projects(self) -> dict:
21+
"""Get projects from Crowdin."""
22+
result = {}
23+
projects = self._client.projects.with_fetch_all().list_projects()
24+
for project in projects["data"]:
25+
result[project["data"]["name"]] = project["data"]["id"]
26+
return result
27+
28+
def get_project_id(self, project_name: str) -> int:
29+
"""Get project ID from Crowdin."""
30+
projects = self._client.projects.with_fetch_all().list_projects()
31+
for project in projects["data"]:
32+
if project["data"]["name"] == project_name:
33+
return project["data"]["id"]
34+
else:
35+
raise ValueError(f"Project '{project_name}' not found.")
36+
37+
def get_project_status(self, project_name: str) -> dict:
38+
"""Get project status from Crowdin."""
39+
results = {}
40+
for p_name, project_id in self.get_projects().items():
41+
if project_name != p_name:
42+
continue
43+
44+
languages = self._client.translation_status.get_project_progress(
45+
project_id
46+
)["data"]
47+
for language in languages:
48+
language_id = language["data"]["language"]["id"]
49+
results[language_id] = {
50+
"language_name": language["data"]["language"]["name"],
51+
"progress": language["data"]["translationProgress"],
52+
"approval": language["data"]["approvalProgress"],
53+
}
54+
return results
55+
56+
def get_project_languages(self, project_name: str) -> list:
57+
"""Get project languages from Crowdin."""
58+
projects = self._client.projects.with_fetch_all().list_projects()
59+
for project in projects["data"]:
60+
if project["data"]["name"] == project_name:
61+
return project["data"]["targetLanguageIds"]
62+
else:
63+
raise ValueError(f"Project '{project_name}' not found.")
64+
65+
def get_valid_languages(
66+
self, project_name: str, translation_percentage: int, approval_percentage: int
67+
) -> dict:
68+
"""Get valid languages based on translation and approval percentage.
69+
70+
Parameters
71+
----------
72+
project_name : str
73+
Name of the project.
74+
translation_percentage : int
75+
Minimum translation percentage.
76+
approval_percentage : int
77+
Minimum approval percentage.
78+
79+
Returns
80+
-------
81+
valid_languages : dict
82+
Dictionary of valid languages.
83+
"""
84+
valid_languages = {}
85+
project_languages = self.get_project_status(project_name)
86+
# print(json.dumps(project_languages, sort_keys=True, indent=4))
87+
for language_id, data in project_languages.items():
88+
approval = data["approval"]
89+
progress = data["progress"]
90+
language_name = data["language_name"]
91+
if progress >= translation_percentage and approval >= approval_percentage:
92+
# print(f"\n{language_id} {language_name}: {progress}% / {approval}%")
93+
valid_languages[language_id] = {
94+
"language_name": language_name,
95+
"progress": progress,
96+
"approval": approval,
97+
}
98+
return valid_languages
99+
100+
def get_project_translators(self, project_name: str) -> dict:
101+
"""Get project translators from Crowdin."""
102+
results: dict = {}
103+
project_id = self.get_project_id(project_name)
104+
languages = self.get_project_languages(project_name)
105+
for lang in sorted(languages):
106+
results[lang] = []
107+
offset = 0
108+
limit = 500
109+
while True:
110+
items = self._client.string_translations.list_language_translations(
111+
lang, project_id, limit=limit, offset=offset
112+
)
113+
if data := items["data"]:
114+
for item in data:
115+
user_data = {
116+
"username": item["data"]["user"]["username"],
117+
"name": item["data"]["user"]["fullName"],
118+
"img_link": item["data"]["user"]["avatarUrl"].replace(
119+
"/medium/", "/large/"
120+
),
121+
}
122+
if user_data not in results[lang]:
123+
results[lang].append(user_data)
124+
offset += limit
125+
else:
126+
break
127+
128+
return results
129+
130+
131+
def generate_card(
132+
name: str,
133+
img_link: str,
134+
) -> str:
135+
"""
136+
Generate a card in TOML format.
137+
"""
138+
toml_card_template = """[[item]]
139+
type = 'card'
140+
classcard = 'text-center'
141+
body = '''{{{{< image >}}}}
142+
src = '{img_link}'
143+
alt = 'Avatar of {name}'
144+
{{{{< /image >}}}}
145+
{name}'''"""
146+
return toml_card_template.format(
147+
img_link=img_link,
148+
name=name,
149+
)
150+
151+
152+
def generate_contributors_md_file(data: dict) -> None:
153+
script_path = Path(__file__).resolve()
154+
parent_dir = script_path.parent.parent / "content"
155+
content = """---
156+
title: Translation Contributors
157+
draft: false
158+
---
159+
160+
"""
161+
162+
for crowdin_project in sorted(data, key=lambda x: x.lower()):
163+
content += f"\n## {crowdin_project}\n"
164+
content += '\n{{< grid columns="2 3 4 5" >}}\n\n'
165+
translators = data[crowdin_project]["translators"]
166+
for _, contributors in translators.items():
167+
if contributors:
168+
for contributor in contributors:
169+
content += "\n\n"
170+
content += generate_card(
171+
name=contributor["name"], img_link=contributor["img_link"]
172+
)
173+
content += "\n\n"
174+
175+
content += "\n{{< /grid >}}"
176+
177+
new_file_path = parent_dir / "contributors.md"
178+
content += f"\n\n---\n\nLast updated: {datetime.now().strftime('%Y-%m-%d')}\n"
179+
180+
with open(new_file_path, "w") as f:
181+
f.write(content)
182+
183+
184+
def generate_dashboard_md_file(data: dict) -> None:
185+
"""Generate a markdown file for the dashboard."""
186+
script_path = Path(__file__).resolve()
187+
parent_dir = script_path.parent.parent / "content"
188+
content = """---
189+
title: Translations Status
190+
draft: false
191+
---
192+
"""
193+
new_file_path = parent_dir / "status.md"
194+
for crowdin_project in sorted(data, key=lambda x: x.lower()):
195+
project_id = data[crowdin_project]["project_id"]
196+
content += f"\n## {crowdin_project}\n"
197+
content += """\n<table class="dashboard">
198+
<tr>
199+
<th align="center">Language</th>
200+
<th align="center" >Translators</th>
201+
<th align="center" >Completion %</th>
202+
<th align="center" >Approval %</th>
203+
</tr>
204+
"""
205+
status = data[crowdin_project]["status"]
206+
for language_id, _ in sorted(
207+
status.items(),
208+
key=lambda item: (item[1]["progress"], item[1]["approval"]),
209+
reverse=True,
210+
):
211+
print(language_id)
212+
url = f"https://scientific-python.crowdin.com/u/projects/{project_id}/l/{language_id}"
213+
content += f"""<tr>
214+
<td><a href='{url}'>{status[language_id]['language_name']} ({language_id})</a></td>
215+
<td>{len(data[crowdin_project]['translators'][language_id])}</td>
216+
<td>{status[language_id]['progress']}</td>
217+
<td>{status[language_id]['approval']}</td>
218+
</tr>"""
219+
220+
content += "\n</table>\n\n"
221+
222+
content += f"\n\n---\n\nLast updated: {datetime.now().strftime('%Y-%m-%d')}\n"
223+
224+
with open(new_file_path, "w") as f:
225+
f.write(content)
226+
227+
228+
def main() -> None:
229+
"""Main function to run the script."""
230+
try:
231+
client = ScientificCrowdinClient(
232+
token=os.environ["CROWDIN_TOKEN"], organization="Scientific-python"
233+
)
234+
projects = client.get_projects()
235+
data = {}
236+
for crowdin_project, project_id in sorted(projects.items()):
237+
print(f"Project: {crowdin_project} ({project_id})")
238+
project_status = client.get_project_status(crowdin_project)
239+
translators = client.get_project_translators(
240+
crowdin_project,
241+
)
242+
data[crowdin_project] = {
243+
"status": project_status,
244+
"translators": translators,
245+
"project_id": project_id,
246+
}
247+
generate_dashboard_md_file(data)
248+
generate_contributors_md_file(data)
249+
except Exception as e:
250+
print(f"Error: {e}")
251+
traceback.print_exc()
252+
return
253+
254+
255+
if __name__ == "__main__":
256+
main()

0 commit comments

Comments
 (0)