mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Move the pushTranslations script into the codebase as kotlin.
This commit is contained in:
committed by
jeffrey-signal
parent
e162eb27c7
commit
fe1755f250
@@ -17,6 +17,7 @@ plugins {
|
||||
id("kotlin-parcelize")
|
||||
id("com.squareup.wire")
|
||||
id("translations")
|
||||
id("translations-kotlin")
|
||||
id("licenses")
|
||||
}
|
||||
|
||||
|
||||
@@ -4189,7 +4189,7 @@
|
||||
<string name="preferences_normal">Normal</string>
|
||||
<!-- Preference summary for compact navigation bar size -->
|
||||
<string name="preferences_compact">Compact</string>
|
||||
<!-- Dialog message body explaining that we have to restart the app in order to apply the user's new language setting. -->
|
||||
<!-- Dialog message body explaining that we have to restart the app in order to apply the user\'s new language setting. -->
|
||||
<string name="preferences_language_change_confirmation_message">The app will restart to apply the new language setting.</string>
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
rootProject.extra["service_ips"] = """new String[]{"13.248.212.111","76.223.92.165"}"""
|
||||
rootProject.extra["storage_ips"] = """new String[]{"142.250.69.147"}"""
|
||||
rootProject.extra["cdn_ips"] = """new String[]{"13.225.196.60","13.225.196.76","13.225.196.77","13.225.196.9"}"""
|
||||
rootProject.extra["storage_ips"] = """new String[]{"142.250.176.211"}"""
|
||||
rootProject.extra["cdn_ips"] = """new String[]{"18.161.21.122","18.161.21.4","18.161.21.66","18.161.21.70"}"""
|
||||
rootProject.extra["cdn2_ips"] = """new String[]{"104.18.10.47","104.18.11.47"}"""
|
||||
rootProject.extra["cdn3_ips"] = """new String[]{"104.18.10.47","104.18.11.47"}"""
|
||||
rootProject.extra["sfu_ips"] = """new String[]{"34.117.136.13"}"""
|
||||
rootProject.extra["content_proxy_ips"] = """new String[]{"107.178.250.75"}"""
|
||||
rootProject.extra["svr2_ips"] = """new String[]{"20.104.52.125"}"""
|
||||
rootProject.extra["svr2_ips"] = """new String[]{"20.119.62.85"}"""
|
||||
rootProject.extra["cdsi_ips"] = """new String[]{"40.122.45.194"}"""
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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,128 +0,0 @@
|
||||
import groovy.io.FileType
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.SimpleType
|
||||
import org.signal.buildtools.StaticIpResolver
|
||||
|
||||
def allStringsResourceFiles(@ClosureParams(value = SimpleType.class, options = ['java.io.File']) Closure c) {
|
||||
file('src/main/res').eachFileRecurse(FileType.FILES) { f ->
|
||||
if (f.name == 'strings.xml') {
|
||||
c(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task replaceEllipsis {
|
||||
group 'Static Files'
|
||||
description 'Process strings for ellipsis characters.'
|
||||
doLast {
|
||||
allStringsResourceFiles { f ->
|
||||
def before = f.text
|
||||
def after = f.text.replace('...', '…')
|
||||
if (before != after) {
|
||||
f.text = after
|
||||
logger.info("$f.parentFile.name/$f.name...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task cleanApostropheErrors {
|
||||
group 'Static Files'
|
||||
description 'Fix transifex apostrophe string errors.'
|
||||
doLast {
|
||||
allStringsResourceFiles { f ->
|
||||
def before = f.text
|
||||
def after = before.replaceAll(/([^\\=08])(')/, '$1\\\\\'')
|
||||
if (before != after) {
|
||||
f.text = after
|
||||
logger.info("$f.parentFile.name/$f.name...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task excludeNonTranslatables {
|
||||
group 'Static Files'
|
||||
description 'Remove strings that are marked "translatable"="false" or are ExtraTranslations.'
|
||||
doLast {
|
||||
def englishFile = file('src/main/res/values/strings.xml')
|
||||
|
||||
def english = new XmlParser().parse(englishFile)
|
||||
def nonTranslatable = english
|
||||
.findAll { it['@translatable'] == 'false' }
|
||||
.collect { it['@name'] }
|
||||
.toSet()
|
||||
def all = english.collect { it['@name'] }.toSet()
|
||||
def translatable = all - nonTranslatable
|
||||
def inMultiline = false
|
||||
def endBlockName = ""
|
||||
|
||||
allStringsResourceFiles { f ->
|
||||
if (f != englishFile) {
|
||||
def newLines = f.readLines()
|
||||
.collect { line ->
|
||||
if (!inMultiline) {
|
||||
def singleLineMatcher = line =~ /name="([^"]*)".*(<\/|\/>)/
|
||||
if (singleLineMatcher.find()) {
|
||||
def name = singleLineMatcher.group(1)
|
||||
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
|
||||
return " <!-- Removed by excludeNonTranslatables ${line.trim()} -->"
|
||||
}
|
||||
} else {
|
||||
def multilineStartMatcher = line =~ /<(.*) .?name="([^"]*)".*/
|
||||
if (multilineStartMatcher.find()) {
|
||||
endBlockName = multilineStartMatcher.group(1)
|
||||
def name = multilineStartMatcher.group(2)
|
||||
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
|
||||
inMultiline = true;
|
||||
return " <!-- Removed by excludeNonTranslatables ${line.trim()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def multilineEndMatcher = line =~ /<\/${endBlockName}/
|
||||
if (multilineEndMatcher.find()) {
|
||||
inMultiline = false
|
||||
return "${line} -->"
|
||||
}
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
f.write(newLines.join("\n") + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task postTranslateQa {
|
||||
group 'Static Files'
|
||||
description 'Runs QA to check validity of updated strings, and ensure presence of any new languages in internal lists.'
|
||||
dependsOn ':qa'
|
||||
}
|
||||
|
||||
task resolveStaticIps {
|
||||
group 'Static Files'
|
||||
description 'Fetches static IPs for core hosts and writes them to static-ips.gradle'
|
||||
doLast {
|
||||
def staticIpResolver = new StaticIpResolver()
|
||||
new File(projectDir, "static-ips.gradle.kts").text = """
|
||||
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")}\"\"\"
|
||||
""".stripIndent().trim() + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
task updateStaticFilesAndQa {
|
||||
group 'Static Files'
|
||||
description 'Runs tasks to update static files. This includes translations, static IPs, and licenses. Runs QA afterwards to verify all went well. Intended to be run before cutting a release.'
|
||||
dependsOn replaceEllipsis, cleanApostropheErrors, excludeNonTranslatables, resolveStaticIps, postTranslateQa
|
||||
}
|
||||
139
build-logic/plugins/src/main/java/translations.gradle.kts
Normal file
139
build-logic/plugins/src/main/java/translations.gradle.kts
Normal file
@@ -0,0 +1,139 @@
|
||||
import groovy.util.Node
|
||||
import groovy.xml.XmlParser
|
||||
import org.signal.buildtools.StaticIpResolver
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Tasks for managing translations and static files.
|
||||
*/
|
||||
|
||||
tasks.register("replaceEllipsis") {
|
||||
group = "Static Files"
|
||||
description = "Process strings for ellipsis characters."
|
||||
doLast {
|
||||
allStringsResourceFiles { f ->
|
||||
val before = f.readText()
|
||||
val after = before.replace("...", "…")
|
||||
if (before != after) {
|
||||
f.writeText(after)
|
||||
logger.info("${f.parentFile.name}/${f.name}...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("cleanApostropheErrors") {
|
||||
group = "Static Files"
|
||||
description = "Fix smartling apostrophe string errors."
|
||||
doLast {
|
||||
val pattern = Regex("""([^\\=08])(')""")
|
||||
allStringsResourceFiles { f ->
|
||||
val before = f.readText()
|
||||
val after = pattern.replace(before) { match ->
|
||||
"${match.groupValues[1]}\\'"
|
||||
}
|
||||
if (before != after) {
|
||||
f.writeText(after)
|
||||
logger.info("${f.parentFile.name}/${f.name}...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("excludeNonTranslatables") {
|
||||
group = "Static Files"
|
||||
description = "Remove strings that are marked \"translatable\"=\"false\" or are ExtraTranslations."
|
||||
doLast {
|
||||
val englishFile = file("src/main/res/values/strings.xml")
|
||||
|
||||
val english = XmlParser().parse(englishFile)
|
||||
val nonTranslatable = english.children()
|
||||
.filterIsInstance<Node>()
|
||||
.filter { it.attribute("translatable") == "false" }
|
||||
.mapNotNull { it.attribute("name") as? String }
|
||||
.toSet()
|
||||
val all = english.children()
|
||||
.filterIsInstance<Node>()
|
||||
.mapNotNull { it.attribute("name") as? String }
|
||||
.toSet()
|
||||
val translatable = all - nonTranslatable
|
||||
|
||||
allStringsResourceFiles { f ->
|
||||
if (f != englishFile) {
|
||||
var inMultiline = false
|
||||
var endBlockName = ""
|
||||
|
||||
val newLines = f.readLines().map { line ->
|
||||
if (!inMultiline) {
|
||||
val singleLineMatcher = Regex("""name="([^"]*)".*(<\/|\/>)""").find(line)
|
||||
if (singleLineMatcher != null) {
|
||||
val name = singleLineMatcher.groupValues[1]
|
||||
if (!line.contains("excludeNonTranslatables") && name !in translatable) {
|
||||
return@map " <!-- Removed by excludeNonTranslatables ${line.trim()} -->"
|
||||
}
|
||||
} else {
|
||||
val multilineStartMatcher = Regex("""<(.*) .?name="([^"]*)".*""").find(line)
|
||||
if (multilineStartMatcher != null) {
|
||||
endBlockName = multilineStartMatcher.groupValues[1]
|
||||
val name = multilineStartMatcher.groupValues[2]
|
||||
if (!line.contains("excludeNonTranslatables") && name !in translatable) {
|
||||
inMultiline = true
|
||||
return@map " <!-- Removed by excludeNonTranslatables ${line.trim()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val multilineEndMatcher = Regex("""</$endBlockName""").find(line)
|
||||
if (multilineEndMatcher != null) {
|
||||
inMultiline = false
|
||||
return@map "$line -->"
|
||||
}
|
||||
}
|
||||
|
||||
line
|
||||
}
|
||||
|
||||
f.writeText(newLines.joinToString("\n") + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("postTranslateQa") {
|
||||
group = "Static Files"
|
||||
description = "Runs QA to check validity of updated strings, and ensure presence of any new languages in internal lists."
|
||||
dependsOn(":qa")
|
||||
}
|
||||
|
||||
tasks.register("resolveStaticIps") {
|
||||
group = "Static Files"
|
||||
description = "Fetches static IPs for core hosts and writes them to static-ips.gradle"
|
||||
doLast {
|
||||
val staticIpResolver = StaticIpResolver()
|
||||
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")}${"\"\"\""}
|
||||
""".trimIndent() + "\n"
|
||||
File(projectDir, "static-ips.gradle.kts").writeText(content)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("updateStaticFilesAndQa") {
|
||||
group = "Static Files"
|
||||
description = "Runs tasks to update static files. This includes translations, static IPs, and licenses. Runs QA afterwards to verify all went well. Intended to be run before cutting a release."
|
||||
dependsOn("replaceEllipsis", "cleanApostropheErrors", "excludeNonTranslatables", "resolveStaticIps", "postTranslateQa")
|
||||
}
|
||||
|
||||
private fun allStringsResourceFiles(action: (File) -> Unit) {
|
||||
val resDir = file("src/main/res")
|
||||
resDir.walkTopDown()
|
||||
.filter { it.isFile && it.name == "strings.xml" }
|
||||
.forEach(action)
|
||||
}
|
||||
@@ -25,6 +25,8 @@ dependencies {
|
||||
implementation(gradleApi())
|
||||
|
||||
implementation(libs.dnsjava)
|
||||
implementation(libs.square.okhttp3)
|
||||
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.mockk)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.signal.buildtools
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Client for interacting with the Smartling translation API.
|
||||
*/
|
||||
class SmartlingClient(
|
||||
private val userIdentifier: String,
|
||||
private val userSecret: String,
|
||||
private val projectId: String
|
||||
) {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
private val jsonParser = JsonSlurper()
|
||||
|
||||
/**
|
||||
* Authenticates with Smartling and returns an access token.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun authenticate(): String {
|
||||
val jsonBody = JsonBuilder(
|
||||
mapOf(
|
||||
"userIdentifier" to userIdentifier,
|
||||
"userSecret" to userSecret
|
||||
)
|
||||
).toString()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.smartling.com/auth-api/v2/authenticate")
|
||||
.post(jsonBody.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw SmartlingException("Authentication failed 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 accessToken = data?.get("accessToken") as? String
|
||||
|
||||
return accessToken
|
||||
?: throw SmartlingException("Failed to extract access token from response: $responseBody")
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to Smartling for translation.
|
||||
*/
|
||||
fun uploadFile(authToken: String, file: File, fileUri: String): String {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("file", file.name, file.asRequestBody("application/xml".toMediaType()))
|
||||
.addFormDataPart("fileUri", fileUri)
|
||||
.addFormDataPart("fileType", "android")
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://api.smartling.com/files-api/v2/projects/$projectId/file")
|
||||
.header("Authorization", "Bearer $authToken")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body.string()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw SmartlingException("Upload failed with code ${response.code}: $responseBody")
|
||||
}
|
||||
|
||||
return responseBody
|
||||
}
|
||||
|
||||
class SmartlingException(message: String) : RuntimeException(message)
|
||||
}
|
||||
Reference in New Issue
Block a user