From 58e141f735dbba2b3f12396fb54451cd47103add Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 1 Feb 2026 21:59:18 +0100 Subject: [PATCH] fix: Process strings from Crowdin to strip the app/patch prefixes again --- .github/workflows/pull_strings.yml | 22 +++- patches/build.gradle.kts | 94 +-------------- patches/strings-processing.gradle.kts | 160 ++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 99 deletions(-) create mode 100644 patches/strings-processing.gradle.kts diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml index 2ef9104579..241745e151 100644 --- a/.github/workflows/pull_strings.yml +++ b/.github/workflows/pull_strings.yml @@ -1,8 +1,6 @@ name: Pull strings on: - schedule: - - cron: "0 0 * * 0" workflow_dispatch: jobs: @@ -17,7 +15,7 @@ jobs: uses: actions/checkout@v5 with: ref: dev - clean: true + persist-credentials: true - name: Pull strings uses: crowdin/github-action@v2 @@ -25,16 +23,28 @@ jobs: config: crowdin.yml upload_sources: false download_translations: true + push_translations: false skip_ref_checkout: true - localization_branch_name: feat/translations - create_pull_request: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + - name: Process strings + run: | + gradlew processStringsFromCrowdin + env: + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore: Sync translations from Crowdin" + push_options: '--force' + branch: feat/translations + - name: Open pull request - if: github.event_name == 'workflow_dispatch' uses: repo-sync/pull-request@v2 with: source_branch: feat/translations diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index 49a9a8198e..4bddb28ac6 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -1,10 +1,3 @@ -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 { @@ -54,89 +47,4 @@ publishing { } } -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)) - } -} +apply(from = "strings-processing.gradle.kts") diff --git a/patches/strings-processing.gradle.kts b/patches/strings-processing.gradle.kts new file mode 100644 index 0000000000..47fd1dce91 --- /dev/null +++ b/patches/strings-processing.gradle.kts @@ -0,0 +1,160 @@ +import org.w3c.dom.* +import javax.xml.parsers.DocumentBuilder +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 + +fun parseXmlDocument(file: File): Pair { + val builder = DocumentBuilderFactory.newInstance().apply { + isIgnoringComments = false + isCoalescing = false + isNamespaceAware = false + }.newDocumentBuilder() + + return builder to builder.parse(file) +} + +fun createXmlDocument(): Pair { + val builder = DocumentBuilderFactory.newInstance().apply { + isIgnoringComments = false + isCoalescing = false + isNamespaceAware = false + }.newDocumentBuilder() + + return builder to builder.newDocument() +} + +fun writeXmlDocument(document: Document, file: File) { + TransformerFactory.newInstance().newTransformer().apply { + setOutputProperty(OutputKeys.INDENT, "yes") + setOutputProperty(OutputKeys.ENCODING, "utf-8") + setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4") + }.transform(DOMSource(document), StreamResult(file)) +} + +fun NodeList.forEach(action: (Node) -> Unit) { + for (i in 0 until length) { + action(item(i)) + } +} + +fun Element.getAppOrPatchId(): String? = if (tagName in setOf("app", "patch")) getAttribute("id") else null + +fun buildFullName(appId: String?, patchId: String?, name: String): String = listOfNotNull(appId, patchId, name).joinToString(".") + +fun buildPrefix(appId: String?, patchId: String?): String = listOfNotNull(appId, patchId).joinToString(".") + +fun processStringElements( + inputDoc: Document, + outputDoc: Document, + shouldCommentOut: Boolean = true, + transform: (Element, String?, String?) -> Node?, +) { + val root = outputDoc.createElement("resources").also(outputDoc::appendChild) + + fun walk(node: Node, appId: String? = null, patchId: String? = null, insideResources: Boolean = false, parentElement: Element = root) { + when (node.nodeType) { + Node.COMMENT_NODE -> { + val comment = outputDoc.createComment(node.nodeValue) + if (insideResources) parentElement.appendChild(comment) else outputDoc.insertBefore(comment, root) + } + + Node.ELEMENT_NODE -> { + val element = node as Element + + when (element.tagName) { + "resources" -> element.childNodes.forEach { walk(it, appId, patchId, true, parentElement) } + + "app", "patch" -> { + val id = element.getAttribute("id") + val (newAppId, newPatchId) = if (element.tagName == "app") { + id to patchId + } else { + appId to id + } + + if (shouldCommentOut) { + parentElement.appendChild(outputDoc.createComment(" <${element.tagName} id=\"$id\"> ")) + element.childNodes.forEach { walk(it, newAppId, newPatchId, true, parentElement) } + parentElement.appendChild(outputDoc.createComment(" ")) + } else { + val newElement = outputDoc.createElement(element.tagName).apply { + setAttribute("id", id) + } + parentElement.appendChild(newElement) + element.childNodes.forEach { walk(it, newAppId, newPatchId, true, newElement) } + } + } + + "string" -> transform(element, appId, patchId)?.let { parentElement.appendChild(it) } + + else -> element.childNodes.forEach { walk(it, appId, patchId, true, parentElement) } + } + } + } + } + + inputDoc.childNodes.forEach { + if (it != inputDoc.documentElement) walk(it) + } + walk(inputDoc.documentElement) +} + +tasks.register("processStringsForCrowdin") { + description = "Process strings file for Crowdin by flattening app/patch structure into string names." + + doLast { + val stringsXmlFile = project.projectDir.resolve("src/main/resources/addresources/values/strings.xml") + val (_, inputDoc) = parseXmlDocument(stringsXmlFile) + val (_, outputDoc) = createXmlDocument() + + processStringElements(inputDoc, outputDoc, shouldCommentOut = true) { element, appId, patchId -> + val name = element.getAttribute("name") + val fullName = buildFullName(appId, patchId, name) + + outputDoc.createElement("string").apply { + setAttribute("name", fullName) + appendChild(outputDoc.createTextNode(element.textContent)) + } + } + + writeXmlDocument(outputDoc, stringsXmlFile) + } +} + +tasks.register("processStringsFromCrowdin") { + description = "Strip app/patch prefix from string names in localized files." + + doLast { + val resDir = project.projectDir.resolve("src/main/resources/addresources") + + resDir.listFiles()?.filter { + it.isDirectory && it.name.startsWith("values-") + }?.forEach { valuesDir -> + val stringsXmlFile = valuesDir.resolve("strings.xml") + if (!stringsXmlFile.exists()) return@forEach + + val (_, inputDoc) = parseXmlDocument(stringsXmlFile) + val (_, outputDoc) = createXmlDocument() + + processStringElements(inputDoc, outputDoc, shouldCommentOut = false) { element, appId, patchId -> + val name = element.getAttribute("name") + val prefix = buildPrefix(appId, patchId) + val strippedName = if (prefix.isNotEmpty() && name.startsWith("$prefix.")) { + name.removePrefix("$prefix.") + } else { + name + } + + outputDoc.createElement("string").apply { + setAttribute("name", strippedName) + appendChild(outputDoc.createTextNode(element.textContent)) + } + } + + writeXmlDocument(outputDoc, stringsXmlFile) + } + } +}