ci: Properly implement Crowdin strings processing
This commit is contained in:
parent
e8d58ca9af
commit
eeb133325e
3 changed files with 97 additions and 62 deletions
4
.github/workflows/push_strings.yml
vendored
4
.github/workflows/push_strings.yml
vendored
|
|
@ -16,10 +16,10 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Preprocess strings
|
- name: Process strings
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew clean preprocessCrowdinStrings
|
run: ./gradlew processStringsForCrowdin
|
||||||
|
|
||||||
- name: Push strings
|
- name: Push strings
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|
|
||||||
|
|
@ -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"
|
group = "app.revanced"
|
||||||
|
|
||||||
patches {
|
patches {
|
||||||
|
|
@ -22,25 +29,6 @@ dependencies {
|
||||||
compileOnly(project(":patches:stub"))
|
compileOnly(project(":patches:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
|
||||||
register<JavaExec>("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 {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
freeCompilerArgs = listOf("-Xcontext-receivers")
|
freeCompilerArgs = listOf("-Xcontext-receivers")
|
||||||
|
|
@ -55,4 +43,91 @@ publishing {
|
||||||
credentials(PasswordCredentials::class)
|
credentials(PasswordCredentials::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(" <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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package app.revanced.util
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comments out the non-standard <app> and <patch> 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<String>) {
|
|
||||||
if (args.size != 2) {
|
|
||||||
throw RuntimeException("Exactly two arguments are required: <input_file> <output_file>")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = """((<app\s+.*>)|(</app>)|(<patch\s+.*>)|(</patch>))""".toRegex()
|
|
||||||
val modifiedContent = content.replace(tagRegex, """<!-- $1 -->""")
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue