diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a86d2e3..5d005676 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ plugins { } crowdin { - projectName = "android-password-store" + crowdinIdentifier = "android-password-store" skipCleanup = false } 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 index 3d45aebc..2f5cab46 100644 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt @@ -5,16 +5,18 @@ package app.passwordstore.gradle.crowdin +import org.gradle.api.provider.Property + /** Extension for configuring [CrowdinDownloadPlugin] */ interface CrowdinExtension { /** Configure the project name on Crowdin */ - var projectName: String + val crowdinIdentifier: Property /** * Don't delete downloaded and extracted translation archives from build directory. * * Useful for debugging. */ - var skipCleanup: Boolean + 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 index 98882af5..5442e085 100644 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt @@ -6,18 +6,12 @@ package app.passwordstore.gradle.crowdin import de.undercouch.gradle.tasks.download.Download -import java.io.File -import java.util.concurrent.TimeUnit -import javax.xml.parsers.DocumentBuilderFactory -import okhttp3.OkHttpClient -import okhttp3.Request -import org.gradle.api.GradleException 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 -import org.w3c.dom.Document private const val EXCEPTION_MESSAGE = """Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension.""" @@ -29,112 +23,56 @@ class CrowdinDownloadPlugin : Plugin { override fun apply(project: Project) { with(project) { - val buildDirectory = layout.buildDirectory.asFile.get() val extension = extensions.create("crowdin") - afterEvaluate { - val projectName = extension.projectName - if (projectName.isEmpty()) { - throw GradleException(EXCEPTION_MESSAGE) + 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 buildOnApi = - tasks.register("buildOnApi") { - doLast { - val login = providers.environmentVariable("CROWDIN_LOGIN") - val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") - if (!login.isPresent) { - throw GradleException("CROWDIN_LOGIN environment variable must be set") - } - if (!key.isPresent) { - throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set") - } - val client = - OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.MINUTES) - .writeTimeout(5, TimeUnit.MINUTES) - .readTimeout(5, TimeUnit.MINUTES) - .callTimeout(10, TimeUnit.MINUTES) - .build() - val url = CROWDIN_BUILD_API_URL.format(projectName, login.get(), key.get()) - val request = Request.Builder().url(url).get().build() - client.newCall(request).execute().close() - } - } - val downloadCrowdin = - tasks.register("downloadCrowdin") { - dependsOn(buildOnApi) - src("https://crowdin.com/backend/download/project/$projectName.zip") - dest("$buildDirectory/translations.zip") - overwrite(true) - } - val extractCrowdin = - tasks.register("extractCrowdin") { - dependsOn(downloadCrowdin) - doFirst { File(buildDir, "translations").deleteRecursively() } - from(zipTree("$buildDirectory/translations.zip")) - into("$buildDirectory/translations") - } - val extractStrings = - tasks.register("extractStrings") { - dependsOn(extractCrowdin) - from("$buildDirectory/translations/") - into("${projectDir}/src/") - setFinalizedBy(setOf("removeIncompleteStrings")) - } - tasks.register("removeIncompleteStrings") { - doLast { - val sourceSets = arrayOf("main", "nonFree") - for (sourceSet in sourceSets) { - val fileTreeWalk = projectDir.resolve("src/$sourceSet").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() - } - } - } - } + 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) } - tasks.register("crowdin") { - dependsOn(extractStrings) - if (!extension.skipCleanup) { - doLast { - File("$buildDirectory/translations").deleteRecursively() - File("$buildDirectory/nonFree-translations").deleteRecursively() - File("$buildDirectory/translations.zip").delete() - } - } + 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 }, + ) + } } } } - - 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 - } }