feat: Move strings to resources for localization (#2440)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
Co-authored-by: Aunali321 <aunvakil.aa@gmail.com>

BREAKING CHANGE: Various APIs have been changed.
This commit is contained in:
LisoUseInAIKyrios
2024-01-27 02:34:01 +01:00
committed by GitHub
co-authored by oSumAtrIX semantic-release-bot Aunali321
parent 15a83a6d28
commit 55fb1ebd9d
201 changed files with 3411 additions and 3784 deletions
@@ -7,7 +7,7 @@ import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.shared.mapping.misc.ResourceMappingPatch
import app.revanced.patches.shared.misc.mapping.ResourceMappingPatch
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
@@ -2,15 +2,33 @@ package app.revanced.util
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.util.DomFileEditor
import app.revanced.patches.shared.settings.preference.impl.StringResource
import app.revanced.patches.youtube.misc.settings.SettingsPatch
import app.revanced.patches.all.misc.resources.AddResourcesPatch
import app.revanced.util.resource.BaseResource
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
private val classLoader = object {}.javaClass.classLoader
/**
* Returns a sequence for all child nodes.
*/
fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(it) }
/**
* Returns a sequence for all child nodes.
*/
fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE }
/**
* Performs the given [action] on each child element.
*/
fun Node.forEachChildElement(action: (Node) -> Unit) = childElementsSequence().forEach {
action(it)
}
/**
* Recursively traverse the DOM tree starting from the given root node.
*
@@ -21,26 +39,6 @@ fun Node.doRecursively(action: (Node) -> Unit) {
for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action)
}
/**
* Merge strings. This manages [StringResource]s automatically.
*
* @param host The hosting xml resource. Needs to be a valid strings.xml resource.
*/
fun ResourceContext.mergeStrings(host: String) {
this.iterateXmlNodeChildren(host, "resources") {
// TODO: figure out why this is needed
if (!it.hasAttributes()) return@iterateXmlNodeChildren
val attributes = it.attributes
val key = attributes.getNamedItem("name")!!.nodeValue!!
val value = it.textContent!!
val formatted = attributes.getNamedItem("formatted") == null
SettingsPatch.addString(key, value, formatted)
}
}
/**
* Copy resources from the current class loader to the resource directory.
*
@@ -112,4 +110,16 @@ fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoClosea
source.close()
target.close()
}
}
}
/**
* Add a resource node child.
*
* @param resource The resource to add.
* @param resourceCallback Called when a resource has been processed.
*/
internal fun Node.addResource(resource: BaseResource, resourceCallback: (BaseResource) -> Unit = { }) {
appendChild(resource.serialize(ownerDocument, resourceCallback))
}
internal fun DomFileEditor?.getNode(tagName: String) = this!!.file.getElementsByTagName(tagName).item(0)
@@ -0,0 +1,37 @@
package app.revanced.util.resource
import app.revanced.util.childElementsSequence
import org.w3c.dom.Document
import org.w3c.dom.Node
/**
* An array resource.
*
* @param name The name of the array resource.
* @param items The items of the array resource.
*/
@Suppress("MemberVisibilityCanBePrivate")
class ArrayResource(
name: String,
val items: List<String>,
) : BaseResource(name, "string-array") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
setAttribute("name", name)
items.forEach { item ->
appendChild(ownerDocument.createElement("item").also { itemNode ->
itemNode.textContent = item
})
}
}
companion object {
fun fromNode(node: Node): ArrayResource {
val key = node.attributes.getNamedItem("name").textContent
val items = node.childElementsSequence().map { it.textContent }.toList()
return ArrayResource(key, items)
}
}
}
@@ -0,0 +1,43 @@
package app.revanced.util.resource
import org.w3c.dom.Document
import org.w3c.dom.Element
/**
* Base resource class for all resources.
*
* @param name The name of the resource.
* @param tag The tag of the resource.
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseResource(
val name: String,
val tag: String
) {
/**
* Serialize resource element to XML.
* Overriding methods should invoke super and operate on its return value.
* @param ownerDocument Target document to create elements from.
* @param resourceCallback Called when a resource has been processed.
*/
open fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit = { }): Element {
return ownerDocument.createElement(tag).apply {
setAttribute("name", name)
}
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + tag.hashCode()
return result
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BaseResource
return name == other.name
}
}
@@ -0,0 +1,41 @@
package app.revanced.util.resource
import app.revanced.patcher.patch.PatchException
import org.w3c.dom.Document
import org.w3c.dom.Node
/**
* A string value.
* Represents a string in the strings.xml file.
*
* @param name The name of the string.
* @param value The value of the string.
* @param formatted If the string is formatted. Defaults to `true`.
*/
class StringResource(
name: String,
val value: String,
val formatted: Boolean = true,
) : BaseResource(name, "string") {
override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) =
super.serialize(ownerDocument, resourceCallback).apply {
// if the string is un-formatted, explicitly add the formatted attribute
if (!formatted) setAttribute("formatted", "false")
if (value.contains(Regex("(?<!\\\\)['\"]")))
throw PatchException("String $name cannot contain unescaped quotes in value \"$value\".")
textContent = value
}
companion object {
fun fromNode(node: Node): StringResource {
val name = node.attributes.getNamedItem("name").textContent
val value = node.textContent
// TODO: Should this be true by default? It is too in the constructor of StringResource.
val formatted = node.attributes.getNamedItem("formatted")?.textContent?.toBoolean() ?: true
return StringResource(name, value, formatted)
}
}
}