Skip to content

Instantly share code, notes, and snippets.

@flodolo
Created June 24, 2026 16:57
Show Gist options
  • Select an option

  • Save flodolo/d224f95d0f6acbb09a66cdfa5f97d1a8 to your computer and use it in GitHub Desktop.

Select an option

Save flodolo/d224f95d0f6acbb09a66cdfa5f97d1a8 to your computer and use it in GitHub Desktop.
VPN mobile completion
#!/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