Skip to content

Commit

Permalink
Check translation process for downloading strings (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefMa authored Jan 10, 2025
1 parent 14080b0 commit cff6da1
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 5 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ If you run the latter, it will only download the translated strings for spanish.

There is also an `downloadTranslationsForAll` task that aggregates all created tasks to run all of them together.

Optional, you can set to each download config the boolean `checkTranslationProcess` to `true`.
If enabled, it will check if everything is translated on Lokalise **before** downloading the strings.
If set, and it is not translated by 100%, the build will fail.
This is quite useful on CI pipelines to make sure that you don't ship half translated apps
**before starting a heavy build/lint task**.

You can set this property like this:
```kotlin
lokalise {
downloadStringsConfigs {
register("main") {
checkTranslationProcess = true
}
}
}
```

#### Polling configuration (optional, default `true`)

By default, the plugin will poll the Lokalise API until the upload is finished.
Expand Down
28 changes: 25 additions & 3 deletions src/main/kotlin/com/ioki/lokalise/gradle/plugin/LokaliseApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.ioki.lokalise.api.Lokalise
import com.ioki.lokalise.api.Result
import com.ioki.lokalise.api.models.FileDownload
import com.ioki.lokalise.api.models.FileUpload
import com.ioki.lokalise.api.models.Project
import com.ioki.lokalise.api.models.Projects
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
Expand All @@ -15,8 +17,14 @@ class LokaliseApiFactory(
private val apiTokenProvider: Provider<String>,
private val projectIdProvider: Provider<String>,
) {
fun createUploadApi(): LokaliseUploadApi = DefaultLokaliseApi(Lokalise(apiTokenProvider.get()), projectIdProvider.get())
fun createDownloadApi(): LokaliseDownloadApi = DefaultLokaliseApi(Lokalise(apiTokenProvider.get()), projectIdProvider.get())
fun createUploadApi(): LokaliseUploadApi =
DefaultLokaliseApi(Lokalise(apiTokenProvider.get()), projectIdProvider.get())

fun createDownloadApi(): LokaliseDownloadApi =
DefaultLokaliseApi(Lokalise(apiTokenProvider.get()), projectIdProvider.get())

fun createProjectApi(): LokaliseProjectApi =
DefaultLokaliseApi(Lokalise(apiTokenProvider.get()), projectIdProvider.get())
}

interface LokaliseUploadApi {
Expand All @@ -25,6 +33,7 @@ interface LokaliseUploadApi {
langIso: String,
params: Map<String, Any>
): List<FileUpload>

suspend fun checkProcess(fileUploads: List<FileUpload>)
}

Expand All @@ -35,10 +44,14 @@ interface LokaliseDownloadApi {
): FileDownload
}

interface LokaliseProjectApi {
suspend fun getProject(): Project
}

internal class DefaultLokaliseApi(
private val lokalise: Lokalise,
private val projectId: String,
) : LokaliseUploadApi, LokaliseDownloadApi {
) : LokaliseUploadApi, LokaliseDownloadApi, LokaliseProjectApi {

private val finishedProcessStatus = listOf("cancelled", "finished", "failed")

Expand Down Expand Up @@ -116,6 +129,15 @@ internal class DefaultLokaliseApi(
is Result.Success -> result.data
}
}

override suspend fun getProject(): Project {
return when (val result = lokalise.allProjects()) {
is Result.Failure -> throw GradleException("Can't get all project\n${result.error.message}")
is Result.Success<Projects> -> result.data.projects.find { it.projectId == projectId }
?: throw GradleException("Can't find project with id $projectId")
}
}

/**
* This is required because Lokalise API only allows 6 files to be uploaded at once.
* See also [https://lokalise.com/blog/announcing-api-rate-limits/](https://lokalise.com/blog/announcing-api-rate-limits/)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ abstract class LokaliseExtension(
abstract class DownloadStringsConfig(
private val name: String,
) : Named, Parameter {
/**
* Checks if all translations are done.
* If set to true and translations are not done, the task will fail.
*/
abstract val checkTranslationProcess: Property<Boolean>

override fun getName(): String = name
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ioki.lokalise.gradle.plugin

import com.ioki.lokalise.gradle.plugin.tasks.registerCheckEverythingTranslatedTask
import com.ioki.lokalise.gradle.plugin.tasks.registerDownloadTranslationTask
import com.ioki.lokalise.gradle.plugin.tasks.registerUploadTranslationTask
import org.gradle.api.Plugin
Expand All @@ -20,12 +21,18 @@ class LokaliseGradlePlugin : Plugin<Project> {
)

val downloadTranslationsForAll = project.tasks.register("downloadTranslationsForAll")
lokaliseExtensions.downloadStringsConfigs.all {
lokaliseExtensions.downloadStringsConfigs.all { downloadConfig ->
val customDownloadTask = project.tasks.registerDownloadTranslationTask(
config = it,
config = downloadConfig,
lokaliseApiFactory = apiFactory,
)
downloadTranslationsForAll.configure { allTask -> allTask.dependsOn(customDownloadTask) }

val checkTranslationTask = project.tasks.registerCheckEverythingTranslatedTask(
lokaliseApiFactory = apiFactory,
config = downloadConfig,
)
customDownloadTask.configure { downloadTask -> downloadTask.dependsOn(checkTranslationTask) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.ioki.lokalise.gradle.plugin.tasks

import com.ioki.lokalise.gradle.plugin.DownloadStringsConfig
import com.ioki.lokalise.gradle.plugin.LokaliseApiFactory
import com.ioki.lokalise.gradle.plugin.LokaliseProjectApi
import kotlinx.coroutines.runBlocking
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider

abstract class CheckEverythingTranslatedTask : DefaultTask() {

@get:Input
abstract val lokaliseApiFactory: Property<() -> LokaliseProjectApi>

@TaskAction
fun f() {
val project = runBlocking { lokaliseApiFactory.get().invoke().getProject() }
if (project.statistics.progressTotal != 100) {
throw GradleException("Not all keys are translated")
}
}
}

internal fun TaskContainer.registerCheckEverythingTranslatedTask(
lokaliseApiFactory: LokaliseApiFactory,
config: DownloadStringsConfig,
): TaskProvider<CheckEverythingTranslatedTask> = register(
"checkEverythingIsTranslatedFor${config.name.replaceFirstChar { it.titlecase() }}",
CheckEverythingTranslatedTask::class.java
) {
it.lokaliseApiFactory.set(lokaliseApiFactory::createProjectApi)
it.onlyIf { config.checkTranslationProcess.getOrElse(false) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package com.ioki.lokalise.gradle.plugin.unit

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import strikt.api.expectThat
import strikt.assertions.contains
import strikt.assertions.isEqualTo
import strikt.assertions.isNotNull
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.createFile
import kotlin.io.path.writeText

class CheckEverythingTranslatedTaskTest {

@TempDir
lateinit var tempDir: Path

@BeforeEach
fun `setup lokalise test project dir`() {
Paths.get(tempDir.toString(), "settings.gradle").createFile()
val buildGradle = Paths.get(tempDir.toString(), "build.gradle.kts")

buildGradle.writeText(
"""
plugins {
id("com.ioki.lokalise")
}
lokalise {
apiToken.set("AWESOM3-AP1-T0KEN")
projectId.set("AW3S0ME-PR0J3C7-1D")
downloadStringsConfigs {
register("library") {
checkTranslationProcess = true
}
register("flavor")
}
}
""".trimIndent()
)
}

@Test
fun `running downloadTranslationsForLibrary will first run checkEverythingIsTranslatedForLibrary`() {
val result = GradleRunner.create()
.withProjectDir(tempDir.toFile())
.withPluginClasspath()
.withArguments("downloadTranslationsForLibrary")
.buildAndFail()

expectThat(result.output).contains(":checkEverythingIsTranslatedForLibrary")
expectThat(result.task(":checkEverythingIsTranslatedForLibrary"))
.isNotNull()
.get { outcome }
.isEqualTo(TaskOutcome.FAILED) // Failed because of API call, but not skipped
}

@Test
fun `running downloadTranslationsForFlavor will skip checkEverythingIsTranslatedForFlavor`() {
val result = GradleRunner.create()
.withProjectDir(tempDir.toFile())
.withPluginClasspath()
.withArguments("downloadTranslationsForFlavor")
.buildAndFail()

expectThat(result.task(":checkEverythingIsTranslatedForFlavor"))
.isNotNull()
.get { outcome }
.isEqualTo(TaskOutcome.SKIPPED)
}

@Test
fun `running checkEverythingIsTranslatedForLibrary if project is not translated will throw`() {
val buildGradle = Paths.get(tempDir.toString(), "build.gradle.kts")
buildGradle.writeText(
"""
import com.ioki.lokalise.gradle.plugin.tasks.CheckEverythingTranslatedTask
import com.ioki.lokalise.gradle.plugin.*
import com.ioki.lokalise.api.models.Project as LokaliseProject
plugins {
id("com.ioki.lokalise")
}
val fakeLokaliseApi: LokaliseProjectApi = object : LokaliseProjectApi {
override suspend fun getProject(): LokaliseProject {
return ${lokaliseProject(10)}
}
}
tasks.register<CheckEverythingTranslatedTask>("testCheckEverythingTranslatedTask") {
lokaliseApiFactory.set({ fakeLokaliseApi })
}
""".trimIndent()
)
val result = GradleRunner.create()
.withProjectDir(tempDir.toFile())
.withPluginClasspath()
.withArguments("testCheckEverythingTranslatedTask", "--info")
.buildAndFail()

expectThat(result.output.contains("Not all keys are translated"))
}

@Test
fun `running checkEverythingIsTranslatedForLibrary if project is translated will succeed`() {
val buildGradle = Paths.get(tempDir.toString(), "build.gradle.kts")
buildGradle.writeText(
"""
import com.ioki.lokalise.gradle.plugin.tasks.CheckEverythingTranslatedTask
import com.ioki.lokalise.gradle.plugin.*
import com.ioki.lokalise.api.models.Project as LokaliseProject
plugins {
id("com.ioki.lokalise")
}
val fakeLokaliseApi: LokaliseProjectApi = object : LokaliseProjectApi {
override suspend fun getProject(): LokaliseProject {
return ${lokaliseProject(100)}
}
tasks.register<CheckEverythingTranslatedTask>("testCheckEverythingTranslatedTask") {
lokaliseApiFactory.set({ fakeLokaliseApi })
}
""".trimIndent()
)
val result = GradleRunner.create()
.withProjectDir(tempDir.toFile())
.withPluginClasspath()
.withArguments("testCheckEverythingTranslatedTask", "--info")
.buildAndFail()

expectThat(result.output.contains("Not all keys are translated"))
}
}

private fun lokaliseProject(totalProgress: Int): String = """
LokaliseProject(
projectId = "",
projectType = "",
name = "",
description = "",
createdAt = "",
createdAtTimestamp = 0,
createdBy = 0,
createdByEmail = "",
teamId = 0,
baseLanguageId = 0,
baseLanguageIso = "",
settings = LokaliseProject.Settings(
perPlatformKeyNames = false,
reviewing = false,
autoToggleUnverified = false,
offlineTranslation = false,
keyEditing = false,
inlineMachineTranslations = false,
branching = false,
segmentation = false,
customTranslationStatuses = false,
customTranslationStatusesAllowMultiple = false,
contributorPreviewDownloadEnabled = false
),
statistics = LokaliseProject.Statistics(
progressTotal = $totalProgress,
keysTotal = 0,
team = 0,
baseWords = 0,
qaIssuesTotal = 0,
qaIssues = LokaliseProject.Statistics.QaIssues(
notReviewed = 0,
unverified = 0,
spellingGrammar = 0,
inconsistentPlaceholders = 0,
inconsistentHtml = 0,
differentNumberOfUrls = 0,
differentUrls = 0,
leadingWhitespace = 0,
trailingWhitespace = 0,
differentNumberOfEmailAddress = 0,
differentEmailAddress = 0,
differentBrackets = 0,
differentNumbers = 0,
doubleSpace = 0,
specialPlaceholder = 0,
unbalancedBrackets = 0
),
languages = emptyList()
)
)
""".trimIndent()

0 comments on commit cff6da1

Please sign in to comment.