Files

165 lines
5.5 KiB
Python

# Copyright 2026 The Helium Authors
# You can use, redistribute, and/or modify this source code under
# the terms of the GPL-3.0 license that can be found in the LICENSE file.
"""
String extraction from Helium patches for translation.
"""
import subprocess
import json
import sys
import re
from pathlib import Path
from third_party import unidiff
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'utils'))
from downloads import DownloadInfo # pylint: disable=wrong-import-order
import utils.name_substitution_utils as namesub # pylint: disable=wrong-import-order
PLATFORMS = ("windows", "macos", "linux")
REPO_URL = "https://github.com/imputnet/helium-{repo}.git"
ROOT_DIR = Path(__file__).resolve().parent.parent
ONBOARDING_VERSION = DownloadInfo([ROOT_DIR / "deps.ini"])['onboarding'].version
def get_xml_attr(text, attr):
"""Extract an XML attribute from an opening tag."""
match = re.search(rf'{attr}="([^"]*)"', text)
return match.group(1) if match else None
def prep_platform_repos(platforms_dir):
"""Clone and update platform repos into the given directory."""
platforms_dir.mkdir(parents=True, exist_ok=True)
for repo in list(PLATFORMS) + ["onboarding"]:
dest = platforms_dir / repo
if not dest.is_dir():
subprocess.run(
["git", "clone", REPO_URL.format(repo=repo),
str(dest)],
check=True,
)
subprocess.check_call(["git", "-C", str(dest), "fetch"])
subprocess.check_call([
"git", "-C",
str(dest), "checkout", ONBOARDING_VERSION if repo == "onboarding" else "origin/main"
])
def get_patch_paths(repo_root, platforms_dir):
"""
Generate patch file paths from all series files (main repo + platforms).
"""
series_files = [repo_root / "patches" / "series"] + \
[platforms_dir / platform / "patches" / "series" for platform in PLATFORMS]
for series_file in series_files:
patches_dir = series_file.parent
for line in series_file.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
yield str(patches_dir / line.split()[0])
def get_relevant_patches(repo_root, platforms_dir):
"""Generate patched hunks from patches that touch .grd or .grdp files."""
for path in get_patch_paths(repo_root, platforms_dir):
patch_set = unidiff.PatchSet.from_filename(path)
for file in patch_set:
if Path(file.path).suffix in ['.grd', '.grdp']:
yield file
def extract_strings_from_hunk(hunk, clean=False):
"""
Parse a diff hunk and generate (name, desc, meaning, message)
for added GRD message elements.
What we want:
- any completely new unit
- any unit where the string or metadata was changed
What we don't want:
- untouched (ergo already translated) units
- broken chunks of untouched, pre-existing units
- units that were added in a different patch (avoiding duplication)
"""
name, message, desc, meaning = None, '', None, None
meta_acc = ''
had_any_additive = False
for line in str(hunk).split('\n'):
is_additive = line.startswith('+') or clean
is_subtractive = line.startswith('-')
line = line.lstrip('+')
if is_subtractive:
continue
if not name:
line = line.strip()
if line.startswith('<message') or meta_acc:
meta_acc += line
elif '</message>' in line:
if name and message and had_any_additive:
yield name, desc, meaning, message.strip()
name, message, desc, meaning = None, '', None, None
had_any_additive = False
elif name:
message += line + '\n'
if meta_acc and line.endswith('>'):
name = get_xml_attr(meta_acc, 'name')
desc = get_xml_attr(meta_acc, 'desc')
meaning = get_xml_attr(meta_acc, 'meaning')
meta_acc = ''
had_any_additive |= bool(name) and is_additive
def to_source_format(path, name, desc, meaning, message):
"""Takes an extracted XML tuple and converts it to the JSON format."""
context = namesub.replace_text(desc)[0]
message = namesub.replace_text(message)[0]
entry = {
'name': name,
'source': path,
'context': context,
}
if meaning:
entry['meaning'] = namesub.replace_text(meaning)[0]
entry['message'] = message
yield entry
def extract_strings(repo_root, platforms_dir):
"""Generate strings to be translated for all grit strings in patches."""
for patch in get_relevant_patches(repo_root, platforms_dir):
for hunk in patch:
for source in extract_strings_from_hunk(hunk):
yield from to_source_format(patch.path, *source)
onboarding_path = platforms_dir / "onboarding" / "helium_onboarding_strings.grdp"
if onboarding_path.exists():
onboarding_text = onboarding_path.read_text()
for source in extract_strings_from_hunk(onboarding_text, True):
yield from to_source_format(
'components/helium_onboarding/helium_onboarding_strings.grdp', *source)
def run(args, repo_root):
"""Generate the base source strings JSON from patches."""
prep_platform_repos(args.platforms_dir)
with open(args.output, 'w', encoding='utf-8') as out:
data = json.dumps(
list(extract_strings(repo_root, args.platforms_dir)),
indent=2,
ensure_ascii=False,
)
out.write(data + '\n')