From 85404c184b410a5cd8af73d89ba964e7c81addab Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:08:55 -1000 Subject: [PATCH] fix: badges (#36566) * re-add * need to validate * ok looks good * oops * lint --- .github/workflows/badges.yaml | 2 +- selfdrive/ui/translations/create_badges.py | 108 +++++++++++++++++++++ selfdrive/ui/update_translations.py | 1 + 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100755 selfdrive/ui/translations/create_badges.py diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 63ee736dca..cd30e4f370 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -23,7 +23,7 @@ jobs: - uses: ./.github/workflows/setup-with-retry - name: Push badges run: | - ${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/ui/translations/create_badges.py" + ${{ env.RUN }} "python3 selfdrive/ui/translations/create_badges.py" rm .gitattributes diff --git a/selfdrive/ui/translations/create_badges.py b/selfdrive/ui/translations/create_badges.py new file mode 100755 index 0000000000..2948c4857d --- /dev/null +++ b/selfdrive/ui/translations/create_badges.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +import json +import os +import requests +import xml.etree.ElementTree as ET + +from openpilot.common.basedir import BASEDIR +from openpilot.selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR + +BADGE_HEIGHT = 20 + 8 +SHIELDS_URL = "https://img.shields.io/badge" + +def parse_po_file(file_path): + """ + Parse a .po file and count total and unfinished translations. + Returns: (total_translations, unfinished_translations) + """ + with open(file_path) as f: + content = f.read() + + total_translations = 0 + unfinished_translations = 0 + + # Split into entries (separated by blank lines) + entries = content.split('\n\n') + + for entry in entries: + # Skip header entry (contains Project-Id-Version) + if 'Project-Id-Version' in entry: + continue + + # Check if this entry has a msgid (translation entry) + # After skipping header, any entry with msgid " is a translation + # (both msgid "content" and msgid "" for multiline contain msgid ") + if 'msgid "' not in entry: + continue + + total_translations += 1 + + # Check if msgstr is empty (unfinished translation) + if 'msgstr ""' in entry: + # Check if there are continuation lines with content after msgstr "" + lines = entry.split('\n') + msgstr_idx = None + for i, line in enumerate(lines): + if line.strip().startswith('msgstr ""'): + msgstr_idx = i + break + + if msgstr_idx is not None: + # Check if any continuation lines have content + has_content = False + for line in lines[msgstr_idx + 1:]: + stripped = line.strip() + # Continuation line with content + if stripped.startswith('"') and len(stripped) > 2: + has_content = True + break + # End of entry + if stripped.startswith(('msgid', '#')) or not stripped: + break + + if not has_content: + unfinished_translations += 1 + + return (total_translations, unfinished_translations) + +if __name__ == "__main__": + with open(LANGUAGES_FILE) as f: + translation_files = json.load(f) + + badge_svg = [] + max_badge_width = 0 # keep track of max width to set parent element + for idx, (name, file) in enumerate(translation_files.items()): + po_file_path = os.path.join(str(TRANSLATIONS_DIR), f"app_{file}.po") + + total_translations, unfinished_translations = parse_po_file(po_file_path) + + percent_finished = int(100 - (unfinished_translations / total_translations * 100.)) if total_translations > 0 else 0 + color = f"rgb{(94, 188, 0) if percent_finished == 100 else (248, 255, 50) if percent_finished > 90 else (204, 55, 27)}" + + # Download badge + badge_label = f"LANGUAGE {name}" + badge_message = f"{percent_finished}% complete" + if unfinished_translations != 0: + badge_message += f" ({unfinished_translations} unfinished)" + + r = requests.get(f"{SHIELDS_URL}/{badge_label}-{badge_message}-{color}", timeout=10) + assert r.status_code == 200, "Error downloading badge" + content_svg = r.content.decode("utf-8") + + xml = ET.fromstring(content_svg) + assert "width" in xml.attrib + max_badge_width = max(max_badge_width, int(xml.attrib["width"])) + + # Make tag ids in each badge unique to combine them into one svg + for tag in ("r", "s"): + content_svg = content_svg.replace(f'id="{tag}"', f'id="{tag}{idx}"') + content_svg = content_svg.replace(f'"url(#{tag})"', f'"url(#{tag}{idx})"') + + badge_svg.extend([f'', content_svg, ""]) + + badge_svg.insert(0, '') + badge_svg.append("") + + with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f: + badge_f.write("\n".join(badge_svg)) diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index b692552fb8..bded80b2e5 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -4,6 +4,7 @@ import os from openpilot.common.basedir import BASEDIR from openpilot.system.ui.lib.multilang import SYSTEM_UI_DIR, UI_DIR, TRANSLATIONS_DIR, multilang +LANGUAGES_FILE = os.path.join(str(TRANSLATIONS_DIR), "languages.json") POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot")