Created
June 24, 2026 16:57
-
-
Save flodolo/d224f95d0f6acbb09a66cdfa5f97d1a8 to your computer and use it in GitHub Desktop.
VPN mobile completion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Query Transvision for IP-protection strings and report completion per locale, | |
| then map a CSV of country/locale/DAUs to the best-matching Transvision locale.""" | |
| import csv | |
| import io | |
| import json | |
| import sys | |
| import urllib.request | |
| import urllib.parse | |
| API_BASE = "https://transvision.flod.org/api/v1/entity/android_l10n/" | |
| STRINGS = [ | |
| "android_l10n/mozilla-mobile/android-components/components/feature/ipprotection/src/main/res/values/strings.xml:mozac_feature_ipprotection_unavaliable_dialog_body", | |
| "android_l10n/mozilla-mobile/android-components/components/feature/ipprotection/src/main/res/values/strings.xml:mozac_feature_ipprotection_unavaliable_dialog_confirm_button", | |
| "android_l10n/mozilla-mobile/android-components/components/feature/ipprotection/src/main/res/values/strings.xml:mozac_feature_ipprotection_unavaliable_dialog_dismiss_button", | |
| "android_l10n/mozilla-mobile/android-components/components/feature/ipprotection/src/main/res/values/strings.xml:mozac_feature_ipprotection_unavaliable_dialog_title", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:preferences_ip_protection_title_2", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:preferences_ip_protection_beta_badge_label", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_title", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_toggle_label", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_navigate_back_button_content_description", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_promo_headline", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_promo_body_2", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_learn_more", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_limit_label", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_limit_value", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_reset_info", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_location_section", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_location_recommended_label", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_location_recommended_description", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:preferences_ip_protection_on", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:preferences_ip_protection_off", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_menu_connecting", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_menu_paused", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_menu_error", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_menu_auth_required", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_navigate_settings", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_menu_limit_reached", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_limit_reached_description", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_toolbar_pill_label", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_toolbar_pill_description", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_get_started", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_connecting", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_card_headline", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_body", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_body_link", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_body_promo", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_body_link_promo", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_onboarding_not_now_button", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_connection_error_snackbar", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_limit_reached_snackbar", | |
| "android_l10n/mozilla-mobile/fenix/app/src/main/res/values/strings.xml:ip_protection_data_limit_reached_snackbar_action", | |
| ] | |
| CSV_DATA = """country,locale,users | |
| AT,de,43676960 | |
| AU,en,49417461 | |
| BE,fr,15374859 | |
| BG,bg,6079542 | |
| CA,en-CA,63241461 | |
| CH,de,26950501 | |
| CL,es,14530061 | |
| CO,es,18592310 | |
| DE,de,620459769 | |
| DK,da,8973551 | |
| ES,es-ES,88085788 | |
| FI,fi,30151558 | |
| FR,fr,290477419 | |
| GB,en-GB,109113565 | |
| IE,en,6978074 | |
| IT,it,98989313 | |
| MX,es,36643047 | |
| MY,en-GB,6127980 | |
| NL,nl,45995449 | |
| NO,nb-NO,8304558 | |
| NZ,en,9955492 | |
| PL,pl,104574564 | |
| PT,pt,12682037 | |
| SE,sv-SE,20748719 | |
| SG,en-GB,5177808 | |
| TH,th,10729869 | |
| US,en-US,527085602 | |
| ZA,en,12288090 | |
| """ | |
| def fetch_entity(entity_id): | |
| """Return the API JSON for one entity: {locale: translation, ...}.""" | |
| # The API expects the id raw (slashes and colon not percent-encoded). | |
| url = API_BASE + "?id=" + entity_id | |
| req = urllib.request.Request(url, headers={"User-Agent": "transvision-completion/1.0"}) | |
| with urllib.request.urlopen(req, timeout=60) as resp: | |
| return json.loads(resp.read().decode("utf-8")) | |
| def main(): | |
| # locale -> count of strings that have a (non-empty) translation | |
| translated = {} | |
| all_locales = set() | |
| total = len(STRINGS) | |
| for i, entity_id in enumerate(STRINGS, 1): | |
| try: | |
| data = fetch_entity(entity_id) | |
| except Exception as e: | |
| print(f"[{i}/{total}] ERROR querying {entity_id}: {e}", file=sys.stderr) | |
| continue | |
| if not isinstance(data, dict): | |
| print(f"[{i}/{total}] Unexpected response for {entity_id}: {data!r}", file=sys.stderr) | |
| continue | |
| for locale, value in data.items(): | |
| all_locales.add(locale) | |
| if value and str(value).strip(): | |
| translated[locale] = translated.get(locale, 0) + 1 | |
| print(f"[{i}/{total}] {entity_id.split(':')[-1]}: {len(data)} locales", file=sys.stderr) | |
| # completion percentage per locale | |
| completion = {loc: round(100.0 * translated.get(loc, 0) / total, 1) for loc in all_locales} | |
| print("\n=== Completion percentage per locale ===") | |
| for loc in sorted(completion): | |
| print(f"{loc}: {completion[loc]}% ({translated.get(loc, 0)}/{total})") | |
| # ---- map CSV locales to best-matching Transvision locale ---- | |
| def best_match(country, csv_locale): | |
| # 1) exact match on the requested locale code | |
| if csv_locale in completion: | |
| return csv_locale | |
| lang = csv_locale.split("-")[0] | |
| candidates = sorted(loc for loc in completion if loc.split("-")[0] == lang) | |
| if not candidates: | |
| return None | |
| # 2) language + this country's region (es in MX -> es-MX, en in GB -> en-GB) | |
| regional = f"{lang}-{country}" | |
| if regional in candidates: | |
| return regional | |
| # 3) generic English: fall back to the en-US source locale | |
| if lang == "en" and "en-US" in candidates: | |
| return "en-US" | |
| # 4) region == language (es -> es-ES, it -> it-IT) | |
| same = f"{lang}-{lang.upper()}" | |
| if same in candidates: | |
| return same | |
| # 5) otherwise first available variant | |
| return candidates[0] | |
| print("\n=== COUNTRY, LOCALE CODE, COMPLETION LEVEL ===") | |
| reader = csv.DictReader(io.StringIO(CSV_DATA)) | |
| for row in reader: | |
| country = row["country"] | |
| csv_locale = row["locale"] | |
| match = best_match(country, csv_locale) | |
| if match is None: | |
| print(f"{country}, {csv_locale} (no match), N/A") | |
| else: | |
| note = "" if match == csv_locale else f" -> {match}" | |
| print(f"{country}, {csv_locale}{note}, {completion[match]}%") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment