summaryrefslogtreecommitdiff
path: root/scripts/pullTranslations.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/pullTranslations.py')
-rw-r--r--[-rwxr-xr-x]scripts/pullTranslations.py388
1 files changed, 248 insertions, 140 deletions
diff --git a/scripts/pullTranslations.py b/scripts/pullTranslations.py
index e688862a..5462df2a 100755..100644
--- a/scripts/pullTranslations.py
+++ b/scripts/pullTranslations.py
@@ -1,164 +1,272 @@
#!/usr/bin/env python3
-""
+"""
+Pull store metadata translations from the l10n git repo and split into
+fastlane-compatible files. Creates a MR against bitmask_android.
+"""
__author__ = "kwadronaut, cyberta"
-__copyright__ = "Copyright 2023, LEAP"
-__license__ = "GPL3 or later3 or later3 or later"
-__version__ = "2"
+__copyright__ = "Copyright 2026, LEAP"
+__license__ = "GPL3 or later"
+__version__ = "3"
import os
import re
-import argparse
import json
+import shutil
import subprocess
import sys
+import argparse
+import tempfile
+import urllib.request
+import urllib.error
def get_script_path():
return os.path.dirname(os.path.realpath(sys.argv[0]))
-# Set the path to the res directory containing different language folders
-main_res_dir = get_script_path() + "/../app/src/main/res"
-custom_res_dir = get_script_path() + "/../app/src/custom/res"
-
-# List all valid locale folders in the res directory
-# We don't want to create a translated store listing without localized app
-def list_locales(app_type):
- locales = []
- if app_type == "main":
- res_dir = main_res_dir
- elif app_type == "custom":
- res_dir = custom_res_dir
- else:
- raise ValueError("Invalid app type. Use 'main' or 'custom'.")
-
- valid_locale_pattern = re.compile(r'^values-(?P<language>[a-z]{2})(-(?P<script>[a-zA-Z]{4}))?(-r(?P<region>[a-zA-Z]{2}))?$')
- for folder in os.listdir(res_dir):
- if valid_locale_pattern.match(folder):
- locale_code = valid_locale_pattern.match(folder).group(0)
- locales.append(locale_code)
-
- # add default locale
- locales.append("en-US")
-
- return locales
-
-# Create empty JSON file for each locale metadata directory
-# If there's no file, tx will skip the translations
-def create_metadata_files(locales, app_type):
- if app_type == "main":
- metadata_dir = get_script_path() + "/../src/normalProductionFat/fastlane/metadata/android"
- elif app_type == "custom":
- metadata_dir = get_script_path() + "/../src/customProductionFat/fastlane/metadata/android"
- else:
- raise ValueError("Invalid app type. Use 'main' or 'custom'.")
- for locale_code in locales:
- locale_dir_name = map_android_locale_dir_to_metadata_dir(locale_code)
- file_path = os.path.join(metadata_dir, locale_dir_name, f"store-meta-{locale_dir_name}.json")
- if not os.path.exists(file_path): # Check if the file already exists
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with open(file_path, "w", encoding="utf-8") as file:
- file.write("{}") # Write an empty JSON object to the file
-
-def map_android_locale_dir_to_metadata_dir(locale_code):
- locale_region_pattern = re.compile(r'^values-(?P<language>[a-z]{2})(-r(?P<region>[a-zA-Z]{2}))$')
- pattern_match = locale_region_pattern.match(locale_code)
- if pattern_match:
- # locale directory contains a region, replace values-<language>-r<region> with <language>-<region>
- locale_dir_name = locale_code.replace("values-" + pattern_match.group("language") + "-r", pattern_match.group("language") + "-")
- # print(f"locale with region suffix: '{locale_code}' - new metadata locale dir name: {locale_dir_name}")
+# ---------------------------------------------------------------------------
+# Config
+# ---------------------------------------------------------------------------
+
+APP_CONFIG = {
+ "main": {
+ "l10n_branch": "bitmask-playstore",
+ "metadata_dir": "src/normalProductionFat/fastlane/metadata/android",
+ },
+ "custom": {
+ "l10n_branch": "bitmask.riseupvpn-playstore-metadata",
+ "metadata_dir": "src/customProductionFat/fastlane/metadata/android",
+ },
+}
+
+L10N_REPO = "https://0xacab.org/leap/l10n.git"
+GITLAB_API = "https://0xacab.org/api/v4"
+BITMASK_ANDROID_PROJECT_ID = "leap%2Fbitmask_android"
+
+# Play Store doesn't support Cuban Spanish, but maybe other store do
+SKIP_LOCALES = {"es_CU"}
+
+# Explicit locale mappings: l10n filename stem -> Play Store / fastlane dir name
+# Only entries that differ from the default (underscore->dash) need to be here
+LOCALE_MAP = {
+ "en": "en-US",
+ "zh_Hans": "zh-CN",
+ "zh_Hant": "zh-TW",
+ "pt_BR": "pt-BR",
+}
+
+
+# ---------------------------------------------------------------------------
+# Locale helpers
+# ---------------------------------------------------------------------------
+
+def stem(filename):
+ """Return filename without .json extension."""
+ return filename.replace(".json", "")
+
+
+def to_fastlane_locale(locale_stem):
+ """Map a l10n locale stem to a Play Store / fastlane directory."""
+ if locale_stem in SKIP_LOCALES:
+ return None
+ if locale_stem in LOCALE_MAP:
+ return LOCALE_MAP[locale_stem]
+ # default: replace underscore with dash
+ return locale_stem.replace("_", "-")
+
+
+# ---------------------------------------------------------------------------
+# Cloning the l10n repo
+# ---------------------------------------------------------------------------
+
+def clone_l10n_repo(branch, token=None):
+ """
+ Shallow-clone the l10n repo at branch into a temp directory.
+ Returns the path to the cloned directory.
+ """
+ tmpdir = tempfile.mkdtemp(prefix="l10n_")
+ repo_url = L10N_REPO
+ if token:
+ # token into URL
+ repo_url = repo_url.replace("https://", f"https://l10n-lab-weblate:{token}@")
+
+ cmd = [
+ "git", "clone",
+ "--depth", "1",
+ "--branch", branch,
+ repo_url,
+ tmpdir,
+ ]
+ print(f"Cloning l10n repo (branch: {branch}) ...")
+ subprocess.run(cmd, check=True)
+ return tmpdir
+
+
+# ---------------------------------------------------------------------------
+# Processing JSON files
+# ---------------------------------------------------------------------------
+
+def process_locale_files(l10n_dir, metadata_dir):
+ """
+ Read each <locale>.json from l10n_dir, map to fastlane locale name,
+ write title.txt / full_description.txt / short_description.txt.
+ """
+ processed = []
+ skipped = []
+
+ for filename in sorted(os.listdir(l10n_dir)):
+ if not filename.endswith(".json"):
+ continue
+
+ locale_stem = stem(filename)
+ fastlane_locale = to_fastlane_locale(locale_stem)
+
+ if fastlane_locale is None:
+ skipped.append(locale_stem)
+ continue
+
+ json_path = os.path.join(l10n_dir, filename)
+ with open(json_path, "r", encoding="utf-8") as f:
+ try:
+ data = json.load(f)
+ except json.JSONDecodeError as e:
+ print(f" WARNING: skipping {filename}: {e}")
+ continue
+
+ if not data:
+ skipped.append(locale_stem)
+ continue
+
+ locale_dir = os.path.join(metadata_dir, fastlane_locale)
+ os.makedirs(locale_dir, exist_ok=True)
+
+ wrote_any = False
+ for key, out_filename in [
+ ("title", "title.txt"),
+ ("full_description", "full_description.txt"),
+ ("short_description", "short_description.txt"),
+ ]:
+ value = data.get(key)
+ if value:
+ out_path = os.path.join(locale_dir, out_filename)
+ with open(out_path, "w", encoding="utf-8") as f:
+ f.write(value)
+ wrote_any = True
+
+ if wrote_any:
+ processed.append(f"{locale_stem} -> {fastlane_locale}")
else:
- # Remove "values-" prefix from the locale directory name
- locale_dir_name = locale_code.replace("values-", "")
- # print(f"new metadata locale dir name: '{locale_dir_name}'")
- return locale_dir_name
-
-
-# Split JSON data and save to separate files for each locale
-def split_json_and_save(locales, metadata_dir):
- for locale_code in locales:
- locale_dir_name = map_android_locale_dir_to_metadata_dir(locale_code)
- json_file_path = os.path.join(metadata_dir, locale_dir_name, f"store-meta-{locale_dir_name}.json")
-
- if os.path.exists(json_file_path):
- with open(json_file_path, "r", encoding="utf-8") as json_file:
- json_data = json.load(json_file)
-
- title = json_data.get("title")
- full_description = json_data.get("full_description")
- short_description = json_data.get("short_description")
-
- if title:
- title_file_path = os.path.join(metadata_dir, locale_dir_name, "title.txt")
- with open(title_file_path, "w", encoding="utf-8") as title_file:
- title_file.write(title)
-
- if full_description:
- full_description_file_path = os.path.join(metadata_dir, locale_dir_name, "full_description.txt")
- with open(full_description_file_path, "w", encoding="utf-8") as full_description_file:
- full_description_file.write(full_description)
-
- if short_description:
- short_description_file_path = os.path.join(metadata_dir, locale_dir_name, "short_description.txt")
- with open(short_description_file_path, "w", encoding="utf-8") as short_description_file:
- short_description_file.write(short_description)
-
-# delete directories that doesn't contain any meta data
-def clean_empty_dirs(locales, metadata_dir_name):
- print("cleanup")
- for locale in locales:
- metadata_locale_dir = map_android_locale_dir_to_metadata_dir(locale)
- json_file_path = os.path.join(metadata_dir_name, metadata_locale_dir, f"store-meta-{metadata_locale_dir}.json")
- if os.path.exists(json_file_path):
- remove = False
- with open(json_file_path, "r", encoding="utf-8") as json_file:
- json_data = json.load(json_file)
- # print(f"'{json_file_path}': '{json.dumps(json_data, sort_keys=True)}' ")
-
- if json.dumps(json_data, sort_keys=True) == "{}":
- remove = True
- # print(f"cleaning up empty json")
-
- if remove:
- # remove the empty json
- os.remove(json_file_path)
- print(f"removing empty json at '{json_file_path}'")
-
- # remove the directory if it's empty
- try:
- os.rmdir(os.path.join(metadata_dir_name, metadata_locale_dir))
- print(f"Removed dir '{os.path.join(metadata_dir_name, metadata_locale_dir)}'")
- except OSError:
- print(f"Skipped removal of dir '{os.path.join(metadata_dir_name, metadata_locale_dir)}'. Not empty.")
+ skipped.append(locale_stem)
+
+ print(f"\nProcessed: {', '.join(processed)}")
+ if skipped:
+ print(f"Skipped: {', '.join(skipped)}")
+
+# ---------------------------------------------------------------------------
+# GitLab MR
+# ---------------------------------------------------------------------------
+def create_mr(token, source_branch, target_branch="master", app_type="main"):
+ """Open a merge request on bitmask_android via the GitLab API."""
+ url = f"{GITLAB_API}/projects/{BITMASK_ANDROID_PROJECT_ID}/merge_requests" payload = json.dumps({
+ "source_branch": source_branch,
+ "target_branch": target_branch,
+ "title": f"chore: update {app_type} Play Store translations",
+ "description": "Automated update from the l10n repo (Weblate).",
+ "remove_source_branch": True,
+ }).encode("utf-8")
+
+ req = urllib.request.Request(
+ url,
+ data=payload,
+ headers={
+ "Content-Type": "application/json",
+ "PRIVATE-TOKEN": token,
+ },
+ method="POST",
+ )
+ try:
+ with urllib.request.urlopen(req) as resp:
+ mr = json.loads(resp.read())
+ print(f"\nMR created: {mr['web_url']}")
+ except urllib.error.HTTPError as e:
+ body = e.read().decode()
+ print(f"\nFailed to create MR: {e.code} {body}", file=sys.stderr)
+
+
+def commit_and_push(repo_root, source_branch, app_type):
+ """Stage changes, commit, push to a new branch."""
+ def git(*args):
+ subprocess.run(["git", "-C", repo_root] + list(args), check=True)
+
+ git("checkout", "-b", source_branch)
+ git("add", APP_CONFIG[app_type]["metadata_dir"])
+
+ result = subprocess.run(
+ ["git", "-C", repo_root, "diff", "--cached", "--quiet"],
+ capture_output=True,
+ )
+ if result.returncode == 0:
+ print("No changes to commit.")
+ return False
+
+ git("commit", "-m", f"chore: update {app_type} Play Store translations from Weblate")
+ git("push", "origin", source_branch)
+ return True
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
if __name__ == "__main__":
- parser = argparse.ArgumentParser(description='Pull translations from transifex for app and app store localization.')
- parser.add_argument('app_type', choices=['main', 'custom'], help='Type of the app (main or custom)')
+ parser = argparse.ArgumentParser(
+ description="Pull Play Store translations from l10n repo and create MR."
+ )
+ parser.add_argument(
+ "app_type",
+ choices=["main", "custom"],
+ help="'main' for Bitmask, 'custom' for RiseupVPN",
+ )
+ parser.add_argument(
+ "--token",
+ default=os.environ.get("L10N_GITLAB_TOKEN"),
+ help="GitLab token for l10n-lab-weblate (or set L10N_GITLAB_TOKEN env var)",
+ )
+ parser.add_argument(
+ "--target-branch",
+ default="master",
+ help="Target branch for the MR (default: master)",
+ )
+ parser.add_argument(
+ "--mr-branch",
+ default=None,
+ help="Source branch name for the MR (default: auto-generated)",
+ )
args = parser.parse_args()
- if args.app_type == "main":
- metadata_dir = get_script_path() + "/../src/normalProductionFat/fastlane/metadata/android"
- elif args.app_type == "custom":
- metadata_dir = get_script_path() + "/../src/customProductionFat/fastlane/metadata/android"
- else:
- raise ValueError("Invalid app type. Use 'main' or 'custom'.")
-
- locales_list = list_locales(args.app_type)
- if not locales_list:
- raise ValueError(f"No valid locales found in the '{args.app_type}' app's 'res' directory.")
+ config = APP_CONFIG[args.app_type]
+ repo_root = os.path.abspath(os.path.join(get_script_path(), ".."))
+ metadata_dir = os.path.join(repo_root, config["metadata_dir"])
+ mr_branch = args.mr_branch or f"translations/{args.app_type}-playstore-update"
- # create empty meta data files in case they don't exist yet
- create_metadata_files(locales_list, args.app_type)
- print(f"Empty JSON files created for each locale in the '{args.app_type}' app.")
+ # 1. Clone l10n repo
+ l10n_dir = clone_l10n_repo(config["l10n_branch"], token=args.token)
- # pull from transifex
- command = "tx pull -af"
- subprocess.run(command, shell = True, executable="/bin/bash")
+ try:
+ # 2. Process JSON files -> fastlane txt files
+ print(f"\nWriting fastlane metadata to: {metadata_dir}")
+ process_locale_files(l10n_dir, metadata_dir)
- # parse the meta data jsons and create separate files, compatible with the fastlane file scheme
- split_json_and_save(locales_list, metadata_dir)
- print(f"JSON data split and saved to separate files for each locale in the '{args.app_type}' app.")
+ # 3. Commit & push
+ pushed = commit_and_push(repo_root, mr_branch, args.app_type)
- # remove directories that doesn't contain any localizations or other fastlane meta data files
- clean_empty_dirs(locales_list, metadata_dir)
+ # 4. Open MR
+ if pushed:
+ if args.token:
+ create_mr(args.token, mr_branch, args.target_branch, args.app_type)
+ else:
+ print("\nNo token provided — skipping MR creation.")
+ print(f"Push branch '{mr_branch}' and open MR manually.")
+ finally:
+ shutil.rmtree(l10n_dir, ignore_errors=True)