266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""Module for updating XML strings from source to target files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from defusedxml import ElementTree as DefusedET
|
|
|
|
from config.settings import Settings
|
|
from utils.xml_processor import XMLProcessor
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger("xml_tools")
|
|
|
|
|
|
def _find_source_translation_files(source_base_path: Path, lang_code: str, app: str) -> list[Path]:
|
|
"""Find source translation files for the app and the shared-youtube directory.
|
|
|
|
Matches 'ar' to 'values-ar-rSA', 'in' to 'values-in', etc.
|
|
|
|
Args:
|
|
source_base_path: The base directory containing 'values-*' folders.
|
|
lang_code: The two-letter language code (e.g., 'ar').
|
|
app: The application identifier ('youtube' or 'music').
|
|
|
|
Returns:
|
|
A list of paths to the 'strings.xml' files if found.
|
|
|
|
"""
|
|
matching_dir = next(
|
|
(d for d in source_base_path.iterdir() if d.is_dir() and d.name.startswith(f"values-{lang_code}")),
|
|
None, # Default to None if no directory matches
|
|
)
|
|
|
|
found_paths: list[Path] = []
|
|
if matching_dir:
|
|
# Check for app-specific strings
|
|
app_file = matching_dir / f"{app}/strings.xml"
|
|
if app_file.exists():
|
|
found_paths.append(app_file)
|
|
|
|
# Check for shared-youtube strings
|
|
shared_file = matching_dir / "shared-youtube/strings.xml"
|
|
if shared_file.exists():
|
|
found_paths.append(shared_file)
|
|
|
|
return found_paths
|
|
|
|
|
|
def update_strings(
|
|
target_path: Path,
|
|
source_path: Path,
|
|
filter_keys: set[str] | None = None,
|
|
) -> None:
|
|
"""Update target XML file with strings from source file.
|
|
|
|
Args:
|
|
target_path: Path to target XML file
|
|
source_path: Path to source XML file
|
|
filter_keys: Optional set of keys to filter which strings are updated.
|
|
If None, all strings are updated (subject to blacklist).
|
|
|
|
"""
|
|
blacklist: set[str] = {
|
|
"revanced_enable_swipe_brightness_summary_off",
|
|
"revanced_enable_swipe_brightness_summary_on",
|
|
"revanced_enable_swipe_volume_summary_off",
|
|
"revanced_enable_swipe_volume_summary_on",
|
|
}
|
|
try:
|
|
# Parse source and target files
|
|
_, target_root, _ = XMLProcessor.parse_file(target_path)
|
|
_, source_root, source_strings = XMLProcessor.parse_file(source_path)
|
|
|
|
if target_root is None or source_root is None:
|
|
logger.error("Failed to parse files: %s or %s", target_path, source_path)
|
|
return
|
|
|
|
# Create a dictionary of existing elements
|
|
existing_elements: dict[str, Any] = {
|
|
elem.get("name"): elem # type: ignore[dict-item]
|
|
for elem in target_root.findall(".//string")
|
|
if elem.get("name") is not None
|
|
}
|
|
|
|
# Update existing strings or add new ones
|
|
for original_name, data in source_strings.items():
|
|
names_to_process = [original_name]
|
|
|
|
# Always add a copy of the string renamed to 'revanced' if applicable
|
|
if original_name.startswith("morphe_"):
|
|
names_to_process.append("revanced_" + original_name[7:])
|
|
|
|
# Skip if any of the derived names are blacklisted
|
|
if any(name in blacklist for name in names_to_process):
|
|
continue
|
|
|
|
# Apply filter if provided
|
|
if filter_keys is not None and not any(name in filter_keys for name in names_to_process):
|
|
continue
|
|
|
|
for name in names_to_process:
|
|
new_elem: Any = DefusedET.fromstring(data["text"])
|
|
new_elem.set("name", name)
|
|
|
|
if name in existing_elements:
|
|
existing_elem: Any = existing_elements[name]
|
|
# Replace attributes and children
|
|
existing_elem.attrib.clear()
|
|
existing_elem.attrib.update(new_elem.attrib)
|
|
existing_elem[:] = new_elem[:]
|
|
existing_elem.text = new_elem.text
|
|
existing_elem.tail = new_elem.tail
|
|
else:
|
|
target_root.append(new_elem)
|
|
|
|
# Write updated file
|
|
XMLProcessor.write_file(target_path, target_root)
|
|
logger.debug("Updated strings in %s", target_path)
|
|
|
|
except Exception:
|
|
logger.exception("Failed to update strings in %s: ", target_path)
|
|
|
|
|
|
def update_base_strings(base_path: Path, rvx_base_path: Path) -> None:
|
|
"""Update the base strings.xml file from RVX source."""
|
|
source_path = base_path / "host/values/strings.xml"
|
|
rvx_source_path = rvx_base_path / "settings/host/values/strings.xml"
|
|
if rvx_source_path.exists():
|
|
update_strings(source_path, rvx_source_path)
|
|
|
|
|
|
def sync_translations(translations_path: Path, rvx_base_path: Path) -> None:
|
|
"""Sync translation folders and strings from RVX source."""
|
|
rvx_translations = rvx_base_path / "translations"
|
|
if not rvx_translations.exists():
|
|
return
|
|
|
|
# Create new translation folders if they don't exist
|
|
for lang_dir in rvx_translations.iterdir():
|
|
if not lang_dir.is_dir():
|
|
continue
|
|
lang = lang_dir.name
|
|
dest_lang_dir = translations_path / lang
|
|
dest_strings_path = dest_lang_dir / "strings.xml"
|
|
source_strings_path = lang_dir / "strings.xml"
|
|
|
|
if not dest_lang_dir.exists() and source_strings_path.exists():
|
|
dest_lang_dir.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source_strings_path, dest_strings_path)
|
|
logger.info("Created new translation folder %s and copied strings.xml", lang)
|
|
|
|
# Update existing translation strings
|
|
if source_strings_path.exists() and dest_strings_path.exists():
|
|
update_strings(dest_strings_path, source_strings_path)
|
|
|
|
|
|
def update_translations_with_keys(
|
|
translations_path: Path,
|
|
base_dir: Path,
|
|
app: str,
|
|
additional_keys: set[str] | None = None,
|
|
) -> None:
|
|
"""Update translation strings with specific keys."""
|
|
source_base_path = base_dir / "src/main/resources/addresources"
|
|
|
|
for lang_dir in translations_path.iterdir():
|
|
if not lang_dir.is_dir():
|
|
continue
|
|
|
|
target_path = lang_dir / "strings.xml"
|
|
|
|
# Find the corresponding source files (app-specific + shared)
|
|
rvx_lang_paths = _find_source_translation_files(source_base_path, lang_dir.name, app)
|
|
|
|
if rvx_lang_paths:
|
|
for source_path in rvx_lang_paths:
|
|
logger.debug("Found source %s for target %s", source_path, target_path)
|
|
update_strings(target_path, source_path, filter_keys=additional_keys)
|
|
else:
|
|
logger.warning("No matching source translation found for language: %s", lang_dir.name)
|
|
|
|
|
|
def process(app: str, base_dir: Path) -> None:
|
|
"""Process files to update strings and copy new translation folders.
|
|
|
|
Args:
|
|
app: Application name (youtube/music).
|
|
base_dir: Base directory of RVX patches operations.
|
|
|
|
"""
|
|
settings = Settings()
|
|
base_path = settings.get_resource_path(app, "settings")
|
|
translations_path = settings.get_resource_path(app, "translations")
|
|
rvx_base_path = base_dir / "src/main/resources" / app
|
|
|
|
is_morphe = "morphe-patches" in str(base_dir)
|
|
|
|
# Update base strings file
|
|
update_base_strings(base_path, rvx_base_path)
|
|
|
|
# Handle translations
|
|
# ruff: noqa: ERA001
|
|
# additional_keys: set[str] = {
|
|
# "revanced_hide_ask_button_summary_off",
|
|
# "revanced_hide_ask_button_summary_on",
|
|
# "revanced_hide_ask_button_title",
|
|
# "revanced_hide_ask_section_summary_off",
|
|
# "revanced_hide_ask_section_summary_on",
|
|
# "revanced_hide_ask_section_title",
|
|
# "revanced_hide_hype_button_summary_off",
|
|
# "revanced_hide_hype_button_summary_on",
|
|
# "revanced_hide_hype_button_title",
|
|
# "revanced_hide_player_control_buttons_background_summary_off",
|
|
# "revanced_hide_player_control_buttons_background_summary_on",
|
|
# "revanced_hide_player_control_buttons_background_title",
|
|
# "revanced_hide_promote_button_summary_off",
|
|
# "revanced_hide_promote_button_summary_on",
|
|
# "revanced_hide_promote_button_title",
|
|
# "revanced_hide_shorts_hashtag_button_summary_off",
|
|
# "revanced_hide_shorts_hashtag_button_summary_on",
|
|
# "revanced_hide_shorts_hashtag_button_title",
|
|
# "revanced_hide_shorts_preview_comment_summary_off",
|
|
# "revanced_hide_shorts_preview_comment_summary_on",
|
|
# "revanced_hide_shorts_preview_comment_title",
|
|
# "revanced_swipe_lowest_value_enable_auto_brightness_overlay_text",
|
|
# "revanced_swipe_overlay_minimal_style_summary_off",
|
|
# "revanced_swipe_overlay_minimal_style_summary_on",
|
|
# "revanced_swipe_overlay_minimal_style_title",
|
|
# "revanced_swipe_overlay_progress_brightness_color_summary",
|
|
# "revanced_swipe_overlay_progress_brightness_color_title",
|
|
# "revanced_swipe_overlay_progress_color_invalid_toast",
|
|
# "revanced_swipe_overlay_progress_color_summary",
|
|
# "revanced_swipe_overlay_progress_color_title",
|
|
# "revanced_swipe_overlay_progress_seek_color_summary",
|
|
# "revanced_swipe_overlay_progress_seek_color_title",
|
|
# "revanced_swipe_overlay_progress_speed_color_summary",
|
|
# "revanced_swipe_overlay_progress_speed_color_title",
|
|
# "revanced_swipe_overlay_progress_volume_color_summary",
|
|
# "revanced_swipe_overlay_progress_volume_color_title",
|
|
# "revanced_swipe_overlay_style_entry_1",
|
|
# "revanced_swipe_overlay_style_entry_2",
|
|
# "revanced_swipe_overlay_style_entry_3",
|
|
# "revanced_swipe_overlay_style_entry_4",
|
|
# "revanced_swipe_overlay_style_entry_5",
|
|
# "revanced_swipe_overlay_style_entry_6",
|
|
# "revanced_swipe_overlay_style_entry_7",
|
|
# "revanced_swipe_overlay_style_title",
|
|
# "revanced_swipe_show_circular_overlay_summary_off",
|
|
# "revanced_swipe_show_circular_overlay_summary_on",
|
|
# "revanced_swipe_show_circular_overlay_title",
|
|
# "revanced_swipe_text_overlay_size_invalid_toast",
|
|
# "revanced_swipe_text_overlay_size_summary",
|
|
# "revanced_swipe_text_overlay_size_title",
|
|
# }
|
|
|
|
if is_morphe:
|
|
update_translations_with_keys(translations_path, base_dir, app)
|
|
# update_translations_with_keys(translations_path, base_dir, app, additional_keys)
|
|
else:
|
|
sync_translations(translations_path, rvx_base_path)
|