fix: Process strings from Crowdin to strip the app/patch prefixes again
This commit is contained in:
parent
2c0e81ee17
commit
e566efc51f
3 changed files with 177 additions and 99 deletions
22
.github/workflows/pull_strings.yml
vendored
22
.github/workflows/pull_strings.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
160
patches/strings-processing.gradle.kts
Normal file
160
patches/strings-processing.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue