Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First Lokalise integration #62

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kinta-lib/src/main/kotlin/com/dailymotion/kinta/KintaEnv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ object KintaEnv {
TRANSIFEX_TOKEN,
TRANSIFEX_PROJECT,
TRANSIFEX_ORG,
LOKALISE_TOKEN,
LOKALISE_PROJECT,
GOOGLE_PLAY_JSON,
GOOGLE_CLOUD_STORAGE_JSON,
GOOGLE_CLOUD_DATASTORE_JSON,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dailymotion.kinta.helper

import java.io.BufferedOutputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.nio.file.Path
import java.util.zip.ZipFile
import kotlin.io.path.absolutePathString
import kotlin.io.path.createDirectories
import kotlin.io.path.exists

object UnzipUtils {

fun unzip(zipFilePath: Path, destDirectory: Path) {
if (!destDirectory.exists()) {
destDirectory.createDirectories()
}

ZipFile(zipFilePath.absolutePathString()).use { zip ->
zip.entries().asSequence().forEach { entry ->
zip.getInputStream(entry).use { input ->
val filePath = destDirectory.resolve(entry.name)
if (entry.isDirectory) {
filePath.createDirectories()
} else {
extractFile(input, filePath)
}
}
}
}
}

private fun extractFile(inputStream: InputStream, destFilePath: Path) {
BufferedOutputStream(FileOutputStream(destFilePath.absolutePathString())).use { bos ->
var read: Int
val bytesIn = ByteArray(4096)
while (inputStream.read(bytesIn).also { read = it } != -1) {
bos.write(bytesIn, 0, read)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package com.dailymotion.kinta.integration.lokalise

import com.dailymotion.kinta.KintaEnv
import com.dailymotion.kinta.Logger
import com.dailymotion.kinta.globalJson
import com.dailymotion.kinta.helper.UnzipUtils
import com.dailymotion.kinta.integration.lokalise.internal.model.LkDownloadPayload
import com.dailymotion.kinta.integration.lokalise.internal.model.LkLangResponse
import com.dailymotion.kinta.integration.lokalise.internal.model.LkUploadPayload
import com.dailymotion.kinta.integration.lokalise.internal.model.LokaliseService
import com.google.gson.Gson
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import java.io.Closeable
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name


object Lokalise {

private const val BASE_URL = "https://api.lokalise.com/api2/"
private const val HEADER_TOKEN = "X-Api-Token"

fun uploadResource(
token: String? = null,
project: String? = null,
resource: String,
lang: String,
content: String
) {
val project_ = project ?: KintaEnv.getOrFail(KintaEnv.Var.LOKALISE_PROJECT)

val payload = LkUploadPayload(
data = content,
filename = resource,
lang_iso = lang,
)

requestUpload(
token = token,
projectId = project_,
payload = payload
)
}

fun downloadResource(
token: String? = null,
project: String? = null,
resource: String,
format: String,
langList: List<String>,
): LokaliseDownloadResponse {
val project_ = project ?: KintaEnv.getOrFail(KintaEnv.Var.LOKALISE_PROJECT)

val payload = LkDownloadPayload(
filter_langs = langList,
filter_filenames = listOf(resource),
format = format,
)

return requestDownload(
project = project_,
token = token,
payload = payload
)
}

fun getLanguages(
token: String? = null,
project: String? = null
): List<String> {
val project_ = project ?: KintaEnv.getOrFail(KintaEnv.Var.LOKALISE_PROJECT)

val response = service(token).getLanguages(project_).execute()

check (response.isSuccessful) {
"Lokalise: cannot get languages: ${response.code()}: ${response.errorBody()?.string()}"
}

return response.body()!!.languages.map { it.lang_iso }
}

private fun requestUpload(
token: String? = null,
projectId: String,
payload: LkUploadPayload,
) {
val requestBody = RequestBody.create(MediaType.get("application/json"), Gson().toJson(payload))
val response = service(token).requestUpload(
projectId = projectId,
requestBody = requestBody,
).execute()

check (response.isSuccessful) {
"Lokalise: cannot push Resource: ${response.code()}: ${response.errorBody()?.string()}"
}
}

private fun requestDownload(
token: String? = null,
project: String,
payload: LkDownloadPayload,
): LokaliseDownloadResponse {

val requestBody = RequestBody.create(MediaType.get("application/json"), Gson().toJson(payload))
val response = service(token).requestDownload(
projectId = project,
requestBody = requestBody
).execute()

check (response.isSuccessful) {
"Lokalise: cannot request download for ${payload.filter_langs}: ${response.code()}: ${response.errorBody()?.string()}"
}

val bundleUrl = globalJson.parseToJsonElement(response.body()?.string().orEmpty())
.jsonObject.getValue("bundle_url").jsonPrimitive.content

return extractDownloadResponse(bundleUrl)
}

private fun extractDownloadResponse(bundleUrl: String): LokaliseDownloadResponse {
val url = URL(bundleUrl)
val zipPath = Paths.get("lokalise.zip")
val folderPath = Paths.get("lokalise")

Logger.d("downloading archive...")

url.openStream().use { Files.copy(it, zipPath) }

Logger.d("unzipping archive...")

UnzipUtils.unzip(
zipFilePath = zipPath,
destDirectory = folderPath
)

return LokaliseDownloadResponse(
pathsToClean = listOf(zipPath,folderPath),
response = folderPath.listDirectoryEntries().map {
LkLangResponse(
lang_iso = it.name,
file = it.listDirectoryEntries().first().toFile()
)
}
)
}

@OptIn(ExperimentalSerializationApi::class)
private fun service(
token: String?
): LokaliseService {

val token_ = token ?: KintaEnv.getOrFail(KintaEnv.Var.LOKALISE_TOKEN)
val logging = HttpLoggingInterceptor { Logger.d(it) }.apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.addInterceptor { chain ->
val newRequest = chain.request().newBuilder()
.addHeader(HEADER_TOKEN, token_)
.build()
chain.proceed(newRequest)
}
.addInterceptor(logging)
.build()

val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(globalJson.asConverterFactory(MediaType.get("application/json")))
.build()

return retrofit.create(LokaliseService::class.java)
}
}

class LokaliseDownloadResponse(
private val pathsToClean: List<Path>,
val response: List<LkLangResponse>,
): Closeable {

override fun close() {
pathsToClean.forEach { it.toFile().deleteRecursively() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.dailymotion.kinta.integration.lokalise.internal.model

import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

interface LokaliseService {

@POST("projects/{project_id}/files/download")
fun requestDownload(
@Path("project_id") projectId: String,
@Body requestBody: RequestBody,
): Call<ResponseBody>

@POST("projects/{project_id}/files/upload")
fun requestUpload(
@Path("project_id") projectId: String,
@Body requestBody: RequestBody,
): Call<ResponseBody>

@GET("projects/{project_id}/languages")
fun getLanguages(@Path("project_id") projectId: String,): Call<LkSupportedLanguagesResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dailymotion.kinta.integration.lokalise.internal.model

import kotlinx.serialization.Serializable
import java.io.File


@Serializable
data class LkSupportedLanguagesResponse(val languages: List<LkLanguage>){
@Serializable
data class LkLanguage(
val lang_iso: String
)
}

data class LkLangResponse(
val file: File,
val lang_iso: String,
)

@Serializable
data class LkDownloadPayload(
val filter_langs: List<String>,
val filter_filenames: List<String>,
val format: String,
val directory_prefix: String = "%LANG_ISO%",
)

@Serializable
data class LkUploadPayload(
val data: String,
val filename: String,
val lang_iso: String,
val replace_modified: Boolean = true,
)