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:
parent
919f708df2
commit
71161e20f8
24 changed files with 320 additions and 395 deletions
|
@ -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"
|
||||
|
|
|
@ -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=",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue