mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-14 23:18:43 +00:00
Move the pullTranslations script into the codebase as kotlin.
This commit is contained in:
committed by
jeffrey-signal
parent
fe1755f250
commit
3031d68863
@@ -17,7 +17,6 @@ plugins {
|
||||
id("kotlin-parcelize")
|
||||
id("com.squareup.wire")
|
||||
id("translations")
|
||||
id("translations-kotlin")
|
||||
id("licenses")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import org.signal.buildtools.SmartlingClient
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
|
||||
|
||||
/**
|
||||
* Kotlin-based translation tasks.
|
||||
*
|
||||
* Requires the following properties in local.properties:
|
||||
* smartling.userIdentifier - Smartling API user identifier
|
||||
* smartling.userSecret - Smartling API user secret
|
||||
* smartling.projectId - (optional) Smartling project ID, defaults to "3e5533321"
|
||||
*/
|
||||
|
||||
tasks.register("pushTranslations") {
|
||||
group = "Translations"
|
||||
description = "Pushes the main strings.xml file to Smartling for translation"
|
||||
|
||||
doLast {
|
||||
val localPropertiesFile = File(rootDir, "local.properties")
|
||||
if (!localPropertiesFile.exists()) {
|
||||
throw GradleException("local.properties not found at ${localPropertiesFile.absolutePath}")
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
localPropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
val userIdentifier = localProperties.requireProperty("smartling.userIdentifier")
|
||||
val userSecret = localProperties.requireProperty("smartling.userSecret")
|
||||
val projectId = localProperties.requireProperty("smartling.projectId")
|
||||
|
||||
val stringsFile = File(rootDir, "app/src/main/res/values/strings.xml")
|
||||
if (!stringsFile.exists()) {
|
||||
throw GradleException("strings.xml not found at ${stringsFile.absolutePath}")
|
||||
}
|
||||
|
||||
println("Using Signal-Android root directory of $rootDir")
|
||||
|
||||
val client = SmartlingClient(userIdentifier, userSecret, projectId)
|
||||
|
||||
println("Fetching auth...")
|
||||
val authToken = client.authenticate()
|
||||
println("> Done")
|
||||
println()
|
||||
|
||||
println("Uploading file...")
|
||||
val response = client.uploadFile(authToken, stringsFile, "strings.xml")
|
||||
println(response)
|
||||
println("> Done")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Properties.requireProperty(name: String): String {
|
||||
return getProperty(name) ?: throw GradleException("$name not found in local.properties")
|
||||
}
|
||||
@@ -1,12 +1,104 @@
|
||||
import groovy.util.Node
|
||||
import groovy.xml.XmlParser
|
||||
import org.signal.buildtools.SmartlingClient
|
||||
import org.signal.buildtools.StaticIpResolver
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* Tasks for managing translations and static files.
|
||||
*
|
||||
* Smartling tasks require the following properties in local.properties:
|
||||
* smartling.userIdentifier - Smartling API user identifier
|
||||
* smartling.userSecret - Smartling API user secret
|
||||
* smartling.projectId - Smartling project ID
|
||||
*/
|
||||
|
||||
// =====================
|
||||
// Smartling Tasks
|
||||
// =====================
|
||||
|
||||
tasks.register("pushTranslations") {
|
||||
group = "Translations"
|
||||
description = "Pushes the main strings.xml file to Smartling for translation"
|
||||
|
||||
doLast {
|
||||
val client = createSmartlingClient()
|
||||
|
||||
val stringsFile = File(rootDir, "app/src/main/res/values/strings.xml")
|
||||
if (!stringsFile.exists()) {
|
||||
throw GradleException("strings.xml not found at ${stringsFile.absolutePath}")
|
||||
}
|
||||
|
||||
println("Using Signal-Android root directory of $rootDir")
|
||||
|
||||
println("Fetching auth...")
|
||||
val authToken = client.authenticate()
|
||||
println("> Done")
|
||||
println()
|
||||
|
||||
println("Uploading file...")
|
||||
val response = client.uploadFile(authToken, stringsFile, "strings.xml")
|
||||
println(response)
|
||||
println("> Done")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("pullTranslations") {
|
||||
group = "Translations"
|
||||
description = "Pulls translated strings.xml files from Smartling for all locales"
|
||||
|
||||
doLast {
|
||||
val client = createSmartlingClient()
|
||||
val resDir = File(rootDir, "app/src/main/res")
|
||||
|
||||
println("Using Signal-Android root directory of $rootDir")
|
||||
|
||||
println("Fetching auth...")
|
||||
val authToken = client.authenticate()
|
||||
println("> Done")
|
||||
println()
|
||||
|
||||
println("Fetching locales...")
|
||||
val locales = client.getLocales(authToken, "strings.xml")
|
||||
println("Found ${locales.size} locales")
|
||||
println("> Done")
|
||||
println()
|
||||
|
||||
println("Fetching files...")
|
||||
val executor = Executors.newFixedThreadPool(35)
|
||||
val futures = mutableListOf<Future<Pair<String, String>>>()
|
||||
|
||||
for (locale in locales) {
|
||||
if (locale in localeBlocklist) {
|
||||
continue
|
||||
}
|
||||
|
||||
futures += executor.submit<Pair<String, String>> {
|
||||
val content = client.downloadFile(authToken, "strings.xml", locale)
|
||||
println("Successfully pulled file for locale $locale")
|
||||
locale to content
|
||||
}
|
||||
}
|
||||
|
||||
val results = futures.map { it.get() }
|
||||
executor.shutdown()
|
||||
println("> Done")
|
||||
println()
|
||||
|
||||
println("Writing files...")
|
||||
for ((locale, content) in results) {
|
||||
val androidLocale = localeMap[locale] ?: locale
|
||||
val localeDir = File(resDir, "values-$androidLocale")
|
||||
localeDir.mkdirs()
|
||||
File(localeDir, "strings.xml").writeText(content)
|
||||
}
|
||||
println("> Done")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("replaceEllipsis") {
|
||||
group = "Static Files"
|
||||
description = "Process strings for ellipsis characters."
|
||||
@@ -110,16 +202,17 @@ tasks.register("resolveStaticIps") {
|
||||
description = "Fetches static IPs for core hosts and writes them to static-ips.gradle"
|
||||
doLast {
|
||||
val staticIpResolver = StaticIpResolver()
|
||||
val tripleQuote = "\"\"\""
|
||||
val content = """
|
||||
rootProject.extra["service_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("chat.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["storage_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("storage.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["cdn_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("cdn.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["cdn2_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("cdn2.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["cdn3_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("cdn3.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["sfu_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("sfu.voip.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["content_proxy_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("contentproxy.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["svr2_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("svr2.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["cdsi_ips"] = ${"\"\"\""}${staticIpResolver.resolveToBuildConfig("cdsi.signal.org")}${"\"\"\""}
|
||||
rootProject.extra["service_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("chat.signal.org")}$tripleQuote
|
||||
rootProject.extra["storage_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("storage.signal.org")}$tripleQuote
|
||||
rootProject.extra["cdn_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("cdn.signal.org")}$tripleQuote
|
||||
rootProject.extra["cdn2_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("cdn2.signal.org")}$tripleQuote
|
||||
rootProject.extra["cdn3_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("cdn3.signal.org")}$tripleQuote
|
||||
rootProject.extra["sfu_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("sfu.voip.signal.org")}$tripleQuote
|
||||
rootProject.extra["content_proxy_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("contentproxy.signal.org")}$tripleQuote
|
||||
rootProject.extra["svr2_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("svr2.signal.org")}$tripleQuote
|
||||
rootProject.extra["cdsi_ips"] = $tripleQuote${staticIpResolver.resolveToBuildConfig("cdsi.signal.org")}$tripleQuote
|
||||
""".trimIndent() + "\n"
|
||||
File(projectDir, "static-ips.gradle.kts").writeText(content)
|
||||
}
|
||||
@@ -137,3 +230,79 @@ private fun allStringsResourceFiles(action: (File) -> Unit) {
|
||||
.filter { it.isFile && it.name == "strings.xml" }
|
||||
.forEach(action)
|
||||
}
|
||||
|
||||
private fun createSmartlingClient(): SmartlingClient {
|
||||
val localPropertiesFile = File(rootDir, "local.properties")
|
||||
if (!localPropertiesFile.exists()) {
|
||||
throw GradleException("local.properties not found at ${localPropertiesFile.absolutePath}")
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
localPropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
val userIdentifier = localProperties.requireProperty("smartling.userIdentifier")
|
||||
val userSecret = localProperties.requireProperty("smartling.userSecret")
|
||||
val projectId = localProperties.requireProperty("smartling.projectId")
|
||||
|
||||
return SmartlingClient(userIdentifier, userSecret, projectId)
|
||||
}
|
||||
|
||||
private fun Properties.requireProperty(name: String): String {
|
||||
return getProperty(name) ?: throw GradleException("$name not found in local.properties")
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping of smartling-locale => Android locale.
|
||||
* Only needed when they differ.
|
||||
*/
|
||||
private val localeMap = mapOf(
|
||||
"af-ZA" to "af",
|
||||
"az-AZ" to "az",
|
||||
"be-BY" to "be",
|
||||
"bg-BG" to "bg",
|
||||
"bn-BD" to "bn",
|
||||
"bs-BA" to "bs",
|
||||
"et-EE" to "et",
|
||||
"fa-IR" to "fa",
|
||||
"ga-IE" to "ga",
|
||||
"gl-ES" to "gl",
|
||||
"gu-IN" to "gu",
|
||||
"he" to "iw",
|
||||
"hi-IN" to "hi",
|
||||
"hr-HR" to "hr",
|
||||
"id" to "in",
|
||||
"ka-GE" to "ka",
|
||||
"kk-KZ" to "kk",
|
||||
"km-KH" to "km",
|
||||
"kn-IN" to "kn",
|
||||
"ky-KG" to "ky",
|
||||
"lt-LT" to "lt",
|
||||
"lv-LV" to "lv",
|
||||
"mk-MK" to "mk",
|
||||
"ml-IN" to "ml",
|
||||
"mr-IN" to "mr",
|
||||
"pa-IN" to "pa",
|
||||
"pt-BR" to "pt-rBR",
|
||||
"pt-PT" to "pt",
|
||||
"ro-RO" to "ro",
|
||||
"sk-SK" to "sk",
|
||||
"sl-SI" to "sl",
|
||||
"sq-AL" to "sq",
|
||||
"sr-RS" to "sr-rRS",
|
||||
"sr-YR" to "sr",
|
||||
"ta-IN" to "ta",
|
||||
"te-IN" to "te",
|
||||
"tl-PH" to "tl",
|
||||
"uk-UA" to "uk",
|
||||
"zh-CN" to "zh-rCN",
|
||||
"zh-HK" to "zh-rHK",
|
||||
"zh-TW" to "zh-rTW",
|
||||
"zh-YU" to "yue"
|
||||
)
|
||||
|
||||
/**
|
||||
* Locales that should not be saved, even if present remotely.
|
||||
* Typically for unfinished translations not ready to be public.
|
||||
*/
|
||||
val localeBlocklist = emptySet<String>()
|
||||
|
||||
@@ -9,6 +9,7 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Client for interacting with the Smartling translation API.
|
||||
@@ -19,7 +20,12 @@ class SmartlingClient(
|
||||
private val projectId: String
|
||||
) {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val jsonParser = JsonSlurper()
|
||||
|
||||
/**
|
||||
@@ -82,5 +88,52 @@ class SmartlingClient(
|
||||
return responseBody
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of locales that have translations for a given file.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getLocales(authToken: String, fileUri: String): List<String> {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.smartling.com/files-api/v2/projects/$projectId/file/status?fileUri=$fileUri")
|
||||
.header("Authorization", "Bearer $authToken")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw SmartlingException("Failed to get locales with code ${response.code}: $responseBody")
|
||||
}
|
||||
|
||||
val json = jsonParser.parseText(responseBody) as Map<String, Any>
|
||||
val responseObj = json["response"] as? Map<String, Any>
|
||||
val data = responseObj?.get("data") as? Map<String, Any>
|
||||
val items = data?.get("items") as? List<Map<String, Any>>
|
||||
|
||||
return items?.mapNotNull { it["localeId"] as? String }
|
||||
?: throw SmartlingException("Failed to extract locales from response: $responseBody")
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the translated file for a specific locale.
|
||||
*/
|
||||
fun downloadFile(authToken: String, fileUri: String, locale: String): String {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.smartling.com/files-api/v2/projects/$projectId/locales/$locale/file?fileUri=$fileUri")
|
||||
.header("Authorization", "Bearer $authToken")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw SmartlingException("Failed to download file for locale $locale with code ${response.code}: $responseBody")
|
||||
}
|
||||
|
||||
return responseBody
|
||||
}
|
||||
|
||||
class SmartlingException(message: String) : RuntimeException(message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user