From eeb133325ed2e1204f8d16bd8daf14afdb0806df Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 22 Jan 2026 14:01:11 +0100 Subject: [PATCH] ci: Properly implement Crowdin strings processing --- .github/workflows/push_strings.yml | 4 +- patches/build.gradle.kts | 115 +++++++++++++++--- .../app/revanced/util/CrowdinPreprocessor.kt | 40 ------ 3 files changed, 97 insertions(+), 62 deletions(-) delete mode 100644 patches/src/main/kotlin/app/revanced/util/CrowdinPreprocessor.kt diff --git a/.github/workflows/push_strings.yml b/.github/workflows/push_strings.yml index c51254f75..e31c13bc3 100644 --- a/.github/workflows/push_strings.yml +++ b/.github/workflows/push_strings.yml @@ -16,10 +16,10 @@ jobs: - name: Checkout uses: actions/checkout@v5 - - name: Preprocess strings + - name: Process strings env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew clean preprocessCrowdinStrings + run: ./gradlew processStringsForCrowdin - name: Push strings uses: crowdin/github-action@v2 diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index fa7bd65bd..9694cc07f 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -1,3 +1,10 @@ +import org.w3c.dom.* +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + group = "app.revanced" patches { @@ -22,25 +29,6 @@ dependencies { compileOnly(project(":patches:stub")) } -tasks { - register("preprocessCrowdinStrings") { - description = "Preprocess strings for Crowdin push" - - dependsOn(compileKotlin) - - classpath = sourceSets["main"].runtimeClasspath - mainClass.set("app.revanced.util.CrowdinPreprocessorKt") - - args = listOf( - "src/main/resources/addresources/values/strings.xml", - // Ideally this would use build/tmp/crowdin/strings.xml - // But using that does not work with Crowdin pull because - // it does not recognize the strings.xml file belongs to this project. - "src/main/resources/addresources/values/strings.xml" - ) - } -} - kotlin { compilerOptions { freeCompilerArgs = listOf("-Xcontext-receivers") @@ -55,4 +43,91 @@ publishing { credentials(PasswordCredentials::class) } } -} \ No newline at end of file +} + +tasks.register("processStringsForCrowdin") { + description = "Process strings file for Crowdin by commenting out non-standard tags." + + doLast { + // Comment out the non-standard tags. Otherwise, Crowdin interprets the file + // not as Android but instead a generic xml file where strings are + // identified by xml position and not key + val stringsXmlFile = project.projectDir.resolve("src/main/resources/addresources/values/strings.xml") + + val builder = DocumentBuilderFactory.newInstance().apply { + isIgnoringComments = false + isCoalescing = false + isNamespaceAware = false + }.newDocumentBuilder() + + val document = builder.newDocument() + val root = document.createElement("resources").also(document::appendChild) + + fun walk(node: Node, appId: String? = null, patchId: String? = null, insideResources: Boolean = false) { + fun walkChildren(el: Element, appId: String?, patchId: String?, insideResources: Boolean) { + val children = el.childNodes + for (i in 0 until children.length) { + walk(children.item(i), appId, patchId, insideResources) + } + } + when (node.nodeType) { + Node.COMMENT_NODE -> { + val comment = document.createComment(node.nodeValue) + if (insideResources) root.appendChild(comment) else document.insertBefore(comment, root) + } + + Node.ELEMENT_NODE -> { + val element = node as Element + + when (element.tagName) { + "resources" -> walkChildren(element, appId, patchId, insideResources = true) + + "app" -> { + val newAppId = element.getAttribute("id") + + root.appendChild(document.createComment(" ")) + walkChildren(element, newAppId, patchId, insideResources) + root.appendChild(document.createComment(" ")) + } + + "patch" -> { + val newPatchId = element.getAttribute("id") + + root.appendChild(document.createComment(" ")) + walkChildren(element, appId, newPatchId, insideResources) + root.appendChild(document.createComment(" ")) + } + + "string" -> { + val name = element.getAttribute("name") + val value = element.textContent + val fullName = "$appId.$patchId.$name" + + val stringElement = document.createElement("string") + stringElement.setAttribute("name", fullName) + stringElement.appendChild(document.createTextNode(value)) + root.appendChild(stringElement) + } + + else -> walkChildren(element, appId, patchId, insideResources) + } + } + } + } + + builder.parse(stringsXmlFile).let { + val topLevel = it.childNodes + for (i in 0 until topLevel.length) { + val node = topLevel.item(i) + if (node != it.documentElement) walk(node) + } + + walk(it.documentElement) + } + + TransformerFactory.newInstance().newTransformer().apply { + setOutputProperty(OutputKeys.INDENT, "yes") + setOutputProperty(OutputKeys.ENCODING, "utf-8") + }.transform(DOMSource(document), StreamResult(stringsXmlFile)) + } +} diff --git a/patches/src/main/kotlin/app/revanced/util/CrowdinPreprocessor.kt b/patches/src/main/kotlin/app/revanced/util/CrowdinPreprocessor.kt deleted file mode 100644 index c49c4c642..000000000 --- a/patches/src/main/kotlin/app/revanced/util/CrowdinPreprocessor.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.revanced.util - -import java.io.File - -/** - * Comments out the non-standard and tags. - * - * Previously this was done on Crowdin after pushing. - * But Crowdin preprocessing has randomly failed but still used the unmodified - * strings.xml file, which effectively deletes all patch strings from Crowdin. - */ -internal fun main(args: Array) { - if (args.size != 2) { - throw RuntimeException("Exactly two arguments are required: ") - } - - val inputFilePath = args[0] - val inputFile = File(inputFilePath) - if (!inputFile.exists()) { - throw RuntimeException( - "Input file not found: $inputFilePath currentDirectory: " + File(".").canonicalPath - ) - } - - // Comment out the non-standard tags. Otherwise Crowdin interprets the file - // not as Android but instead a generic xml file where strings are - // identified by xml position and not key. - val content = inputFile.readText() - val tagRegex = """(()|()|()|())""".toRegex() - val modifiedContent = content.replace(tagRegex, """""") - - // Write modified content to the output file (creates file if it doesn't exist). - val outputFilePath = args[1] - val outputFile = File(outputFilePath) - outputFile.parentFile?.mkdirs() - outputFile.writeText(modifiedContent) - - println("Preprocessed strings.xml to: $outputFilePath") -} -