fix: Process strings from Crowdin to strip the app/patch prefixes again

This commit is contained in:
oSumAtrIX 2026-02-01 21:59:18 +01:00
parent 2c0e81ee17
commit e566efc51f
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
3 changed files with 177 additions and 99 deletions

View file

@ -1,8 +1,6 @@
name: Pull strings name: Pull strings
on: on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -17,7 +15,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
with: with:
ref: dev ref: dev
clean: true persist-credentials: true
- name: Pull strings - name: Pull strings
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2
@ -25,16 +23,28 @@ jobs:
config: crowdin.yml config: crowdin.yml
upload_sources: false upload_sources: false
download_translations: true download_translations: true
push_translations: false
skip_ref_checkout: true skip_ref_checkout: true
localization_branch_name: feat/translations
create_pull_request: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 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 - name: Open pull request
if: github.event_name == 'workflow_dispatch'
uses: repo-sync/pull-request@v2 uses: repo-sync/pull-request@v2
with: with:
source_branch: feat/translations source_branch: feat/translations

View file

@ -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" group = "app.revanced"
patches { patches {
@ -45,89 +38,4 @@ publishing {
} }
} }
tasks.register("processStringsForCrowdin") { apply(from = "strings-processing.gradle.kts")
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(" <app id=\"$newAppId\"> "))
walkChildren(element, newAppId, patchId, insideResources)
root.appendChild(document.createComment(" </app> "))
}
"patch" -> {
val newPatchId = element.getAttribute("id")
root.appendChild(document.createComment(" <patch id=\"$newPatchId\"> "))
walkChildren(element, appId, newPatchId, insideResources)
root.appendChild(document.createComment(" </patch> "))
}
"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))
}
}

View file

@ -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<DocumentBuilder, Document> {
val builder = DocumentBuilderFactory.newInstance().apply {
isIgnoringComments = false
isCoalescing = false
isNamespaceAware = false
}.newDocumentBuilder()
return builder to builder.parse(file)
}
fun createXmlDocument(): Pair<DocumentBuilder, Document> {
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(" </${element.tagName}> "))
} 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)
}
}
}