From 71161e20f8610b989e7a0d803249e17362e10ddc Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Aug 2024 13:40:12 +0530 Subject: 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 --- .../main/kotlin/app/passwordstore/gradle/OkHttp.kt | 6 -- .../passwordstore/gradle/crowdin/BuildOnApiTask.kt | 64 -------------------- .../gradle/crowdin/CrowdinExtension.kt | 22 ------- .../passwordstore/gradle/crowdin/CrowdinPlugin.kt | 70 ---------------------- .../gradle/crowdin/StringCleanupTask.kt | 66 -------------------- .../gradle/crowdin/api/ListProjects.kt | 14 ----- 6 files changed, 242 deletions(-) delete mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/BuildOnApiTask.kt delete mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt delete mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt delete mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/StringCleanupTask.kt delete mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/api/ListProjects.kt (limited to 'build-logic/src/main/kotlin') diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/OkHttp.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/OkHttp.kt index e3d33432..7f26d427 100644 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/OkHttp.kt +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/OkHttp.kt @@ -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=", diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/BuildOnApiTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/BuildOnApiTask.kt deleted file mode 100644 index 0fc143d1..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/BuildOnApiTask.kt +++ /dev/null @@ -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 - @get:Internal abstract val crowdinLogin: Property - @get:Internal abstract val crowdinKey: Property - - @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" - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt deleted file mode 100644 index b78d66d3..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt +++ /dev/null @@ -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 - - /** - * Don't delete downloaded and extracted translation archives from build directory. - * - * Useful for debugging. - */ - val skipCleanup: Property -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt deleted file mode 100644 index f127507a..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt +++ /dev/null @@ -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 { - - override fun apply(project: Project) { - with(project) { - val extension = extensions.create("crowdin") - val login = providers.environmentVariable("CROWDIN_LOGIN") - val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") - val buildOnApi = - if (login.isPresent && key.isPresent) { - tasks.register("buildOnApi") { - crowdinIdentifier.set(extension.crowdinIdentifier) - crowdinLogin.set(login) - crowdinKey.set(key) - } - } else { - null - } - val downloadCrowdin = - tasks.register("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("extractCrowdin") { - from(zipTree(downloadCrowdin.map { it.outputFiles.first() })) - into(layout.buildDirectory.dir("translations")) - } - val extractStrings = - tasks.register("extractStrings") { - from(extractCrowdin.map { it.destinationDir }) - into(layout.projectDirectory.dir("src")) - } - val removeIncompleteStrings = - tasks.register("removeIncompleteStrings") { - sourceDirectory.set( - objects.directoryProperty().fileProvider(extractStrings.map { it.destinationDir }) - ) - } - tasks.register("crowdin") { - dependsOn(removeIncompleteStrings) - delete = - if (extension.skipCleanup.getOrElse(false)) { - emptySet() - } else { - setOf(extractStrings.map { it.source }, downloadCrowdin.map { it.outputFiles }) - } - } - } - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/StringCleanupTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/StringCleanupTask.kt deleted file mode 100644 index 7f91368c..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/StringCleanupTask.kt +++ /dev/null @@ -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 - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/api/ListProjects.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/api/ListProjects.kt deleted file mode 100644 index 933d7e02..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/api/ListProjects.kt +++ /dev/null @@ -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) - -data class ProjectData(@Json(name = "data") val project: Project) - -data class Project(val id: Long, val identifier: String) -- cgit v1.2.3