"""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)