fix: rework Crowdin integration (#3175)

* chore: set up Crowdin configuration

* fix(app): sync strings from Crowdin

Closes #3174

* fix(ci): use crowdin/github-action instead of homebrew setup

* fix(build): remove obsolete Crowdin plugin
This commit is contained in:
Harsh Shandilya 2024-08-18 13:40:12 +05:30 committed by GitHub
parent 919f708df2
commit 71161e20f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 320 additions and 395 deletions

View file

@ -17,10 +17,6 @@ gradlePlugin {
id = "com.github.android-password-store.android-library"
implementationClass = "app.passwordstore.gradle.LibraryPlugin"
}
register("crowdin") {
id = "com.github.android-password-store.crowdin-plugin"
implementationClass = "app.passwordstore.gradle.crowdin.CrowdinDownloadPlugin"
}
register("git-hooks") {
id = "com.github.android-password-store.git-hooks"
implementationClass = "app.passwordstore.gradle.GitHooksPlugin"

View file

@ -12,12 +12,6 @@ import okhttp3.OkHttpClient
object OkHttp {
private val certificatePinner =
CertificatePinner.Builder()
.add(
"api.crowdin.com",
"sha256/qKpGqFXXIteblI82BcMyRX0eC2o7lpL9XVInWKIG7rc=",
"sha256/DxH4tt40L+eduF6szpY6TONlxhZhBd+pJ9wbHlQ2fuw=",
"sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=",
)
.add(
"publicsuffix.org",
"sha256/Ov/MkC2OkVtTp9MdY+uXOKAuV2Birfdeazval8seMZM=",

View file

@ -1,64 +0,0 @@
/*
* Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.gradle.crowdin
import app.passwordstore.gradle.OkHttp
import app.passwordstore.gradle.crowdin.api.ListProjects
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault
@DisableCachingByDefault(because = "This calls into a remote API and has nothing to cache")
abstract class BuildOnApiTask : DefaultTask() {
@get:Input abstract val crowdinIdentifier: Property<String>
@get:Internal abstract val crowdinLogin: Property<String>
@get:Internal abstract val crowdinKey: Property<String>
@TaskAction
fun doWork() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val projectAdapter = moshi.adapter(ListProjects::class.java)
val projectRequest =
Request.Builder()
.url("$CROWDIN_BASE_URL/projects")
.header("Authorization", "Bearer ${crowdinKey.get()}")
.get()
.build()
OkHttp.CLIENT.newCall(projectRequest).execute().use { response ->
val projects = projectAdapter.fromJson(response.body.source())
if (projects != null) {
val identifier =
projects.projects
.first { data -> data.project.identifier == crowdinIdentifier.get() }
.project
.id
.toString()
val buildRequest =
Request.Builder()
.url(CROWDIN_BUILD_API_URL.format(identifier))
.header("Authorization", "Bearer ${crowdinKey.get()}")
.post("{}".toRequestBody("application/json".toMediaType()))
.build()
OkHttp.CLIENT.newCall(buildRequest).execute().close()
}
}
}
private companion object {
private const val CROWDIN_BASE_URL = "https://api.crowdin.com/api/v2"
private const val CROWDIN_BUILD_API_URL = "$CROWDIN_BASE_URL/projects/%s/translations/builds"
}
}

View file

@ -1,22 +0,0 @@
/*
* Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.gradle.crowdin
import org.gradle.api.provider.Property
/** Extension for configuring [CrowdinDownloadPlugin] */
interface CrowdinExtension {
/** Configure the project name on Crowdin */
val crowdinIdentifier: Property<String>
/**
* Don't delete downloaded and extracted translation archives from build directory.
*
* Useful for debugging.
*/
val skipCleanup: Property<Boolean>
}

View file

@ -1,70 +0,0 @@
/*
* Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.gradle.crowdin
import de.undercouch.gradle.tasks.download.Download
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register
@Suppress("Unused")
class CrowdinDownloadPlugin : Plugin<Project> {
override fun apply(project: Project) {
with(project) {
val extension = extensions.create<CrowdinExtension>("crowdin")
val login = providers.environmentVariable("CROWDIN_LOGIN")
val key = providers.environmentVariable("CROWDIN_PROJECT_KEY")
val buildOnApi =
if (login.isPresent && key.isPresent) {
tasks.register<BuildOnApiTask>("buildOnApi") {
crowdinIdentifier.set(extension.crowdinIdentifier)
crowdinLogin.set(login)
crowdinKey.set(key)
}
} else {
null
}
val downloadCrowdin =
tasks.register<Download>("downloadCrowdin") {
if (buildOnApi != null) dependsOn(buildOnApi)
src(
"https://crowdin.com/backend/download/project/${extension.crowdinIdentifier.get()}.zip"
)
dest(layout.buildDirectory.file("translations.zip"))
overwrite(true)
}
val extractCrowdin =
tasks.register<Copy>("extractCrowdin") {
from(zipTree(downloadCrowdin.map { it.outputFiles.first() }))
into(layout.buildDirectory.dir("translations"))
}
val extractStrings =
tasks.register<Copy>("extractStrings") {
from(extractCrowdin.map { it.destinationDir })
into(layout.projectDirectory.dir("src"))
}
val removeIncompleteStrings =
tasks.register<StringCleanupTask>("removeIncompleteStrings") {
sourceDirectory.set(
objects.directoryProperty().fileProvider(extractStrings.map { it.destinationDir })
)
}
tasks.register<Delete>("crowdin") {
dependsOn(removeIncompleteStrings)
delete =
if (extension.skipCleanup.getOrElse(false)) {
emptySet()
} else {
setOf(extractStrings.map { it.source }, downloadCrowdin.map { it.outputFiles })
}
}
}
}
}

View file

@ -1,66 +0,0 @@
/*
* Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.gradle.crowdin
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.work.DisableCachingByDefault
import org.w3c.dom.Document
@DisableCachingByDefault(because = "The task runs quickly and has complicated semantics")
abstract class StringCleanupTask : DefaultTask() {
@get:InputDirectory abstract val sourceDirectory: DirectoryProperty
@TaskAction
fun clean() {
val sourceSets = arrayOf("main", "nonFree")
for (sourceSet in sourceSets) {
val fileTreeWalk = sourceDirectory.dir("$sourceSet/res").get().asFile.walkTopDown()
val valuesDirectories =
fileTreeWalk.filter { it.isDirectory }.filter { it.name.startsWith("values") }
val stringFiles = fileTreeWalk.filter { it.name == "strings.xml" }
val sourceFile =
stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") }
?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet")
val sourceDoc = parseDocument(sourceFile)
val baselineStringCount = countStrings(sourceDoc)
val threshold = 0.80 * baselineStringCount
stringFiles.forEach { file ->
if (file != sourceFile) {
val doc = parseDocument(file)
val stringCount = countStrings(doc)
if (stringCount < threshold) {
file.delete()
}
}
}
valuesDirectories.forEach { dir ->
if (dir.listFiles().isNullOrEmpty()) {
dir.delete()
}
}
}
}
private fun parseDocument(file: File): Document {
val dbFactory = DocumentBuilderFactory.newInstance()
val documentBuilder = dbFactory.newDocumentBuilder()
return documentBuilder.parse(file)
}
private fun countStrings(document: Document): Int {
// Normalization is beneficial for us
// https://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
document.documentElement.normalize()
return document.getElementsByTagName("string").length
}
}

View file

@ -1,14 +0,0 @@
/*
* Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package app.passwordstore.gradle.crowdin.api
import com.squareup.moshi.Json
data class ListProjects(@Json(name = "data") val projects: List<ProjectData>)
data class ProjectData(@Json(name = "data") val project: Project)
data class Project(val id: Long, val identifier: String)