From 98c275c218017a428aa41951a3420eed706f25cb Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 13 Dec 2024 10:09:16 -0700 Subject: [PATCH] feat: convert iOS translations to Android format (#585) * feat: convert iOS translations to Android format * fix Gradle implicit dependencies --- androidApp/build.gradle.kts | 15 +- .../android/component/UpcomingTripView.kt | 2 +- androidApp/src/main/res/resources.properties | 1 + .../{values-es => values-b+es}/strings.xml | 1 - .../res/values-b+es/strings_ios_converted.xml | 36 +++++ .../src/main/res/values-b+ht/strings.xml | 3 + .../res/values-b+ht/strings_ios_converted.xml | 36 +++++ .../src/main/res/values-b+pt+BR/strings.xml | 3 + .../values-b+pt+BR/strings_ios_converted.xml | 36 +++++ .../src/main/res/values-b+vi/strings.xml | 3 + .../res/values-b+vi/strings_ios_converted.xml | 36 +++++ .../main/res/values-b+zh+Hans+CN/strings.xml | 3 + .../strings_ios_converted.xml | 36 +++++ .../main/res/values-b+zh+Hant+TW/strings.xml | 3 + .../strings_ios_converted.xml | 36 +++++ androidApp/src/main/res/values/strings.xml | 12 +- .../gradle/ConvertIosLocalizationTask.kt | 142 ++++++++++++++++++ 17 files changed, 394 insertions(+), 10 deletions(-) create mode 100644 androidApp/src/main/res/resources.properties rename androidApp/src/main/res/{values-es => values-b+es}/strings.xml (52%) create mode 100644 androidApp/src/main/res/values-b+es/strings_ios_converted.xml create mode 100644 androidApp/src/main/res/values-b+ht/strings.xml create mode 100644 androidApp/src/main/res/values-b+ht/strings_ios_converted.xml create mode 100644 androidApp/src/main/res/values-b+pt+BR/strings.xml create mode 100644 androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml create mode 100644 androidApp/src/main/res/values-b+vi/strings.xml create mode 100644 androidApp/src/main/res/values-b+vi/strings_ios_converted.xml create mode 100644 androidApp/src/main/res/values-b+zh+Hans+CN/strings.xml create mode 100644 androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml create mode 100644 androidApp/src/main/res/values-b+zh+Hant+TW/strings.xml create mode 100644 androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml create mode 100644 buildSrc/src/main/kotlin/com/mbta/tid/mbta_app/gradle/ConvertIosLocalizationTask.kt diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index dcb066cbd..56ae0e69a 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -1,3 +1,4 @@ +import com.mbta.tid.mbta_app.gradle.ConvertIosLocalizationTask import com.mbta.tid.mbta_app.gradle.ConvertIosMapIconsTask import java.io.BufferedReader import java.io.StringReader @@ -56,6 +57,10 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } + androidResources { + @Suppress("UnstableApiUsage") + generateLocaleConfig = true + } } dependencies { @@ -100,6 +105,11 @@ task("convertIosIconsToAssets") { assetsToReturnByName = listOf("alert-borderless-*") } +task("convertIosLocalization") { + xcstrings = layout.projectDirectory.file("../iosApp/iosApp/Localizable.xcstrings") + resources = layout.projectDirectory.dir("src/main/res") +} + // https://github.com/mapbox/mapbox-gl-native-android/blob/7f03a710afbd714368084e4b514d3880bad11c27/gradle/gradle-config.gradle task("mapboxTempToken") { val tokenFile = File("${projectDir}/src/main/res/values/secrets.xml") @@ -147,6 +157,9 @@ task("envVars") { } gradle.projectsEvaluated { - tasks.getByPath("preBuild").dependsOn("mapboxTempToken", "convertIosIconsToAssets") + tasks + .getByPath("preBuild") + .dependsOn("mapboxTempToken", "convertIosIconsToAssets", "convertIosLocalization") + tasks.getByPath("spotlessKotlin").mustRunAfter("convertIosLocalization") tasks.getByPath("check").dependsOn("checkMapboxBridge") } diff --git a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/component/UpcomingTripView.kt b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/component/UpcomingTripView.kt index 7e16e0046..6acdcb9f9 100644 --- a/androidApp/src/main/java/com/mbta/tid/mbta_app/android/component/UpcomingTripView.kt +++ b/androidApp/src/main/java/com/mbta/tid/mbta_app/android/component/UpcomingTripView.kt @@ -93,7 +93,7 @@ fun UpcomingTripView(state: UpcomingTripViewState) { ) is TripInstantDisplay.Approaching -> BoldedTripStatus( - text = stringResource(R.string.approaching_abbr), + text = stringResource(R.string.minutes_abbr, 1), modifier = modifier ) is TripInstantDisplay.Time -> diff --git a/androidApp/src/main/res/resources.properties b/androidApp/src/main/res/resources.properties new file mode 100644 index 000000000..92481bb0b --- /dev/null +++ b/androidApp/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en diff --git a/androidApp/src/main/res/values-es/strings.xml b/androidApp/src/main/res/values-b+es/strings.xml similarity index 52% rename from androidApp/src/main/res/values-es/strings.xml rename to androidApp/src/main/res/values-b+es/strings.xml index 1cbc5c65e..045e125f3 100644 --- a/androidApp/src/main/res/values-es/strings.xml +++ b/androidApp/src/main/res/values-b+es/strings.xml @@ -1,4 +1,3 @@ - ¡Hola, %1$s! diff --git a/androidApp/src/main/res/values-b+es/strings_ios_converted.xml b/androidApp/src/main/res/values-b+es/strings_ios_converted.xml new file mode 100644 index 000000000..14f785ca5 --- /dev/null +++ b/androidApp/src/main/res/values-b+es/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + versión %1$s + LLEGAN. + ABORD. + Cancelado + Desvío + %1$s a + Enviar comentarios sobre la aplicación + Todos + Más + Hecho con ♥ por T + Banderas de características + Recursos + Configuración + Información general y soporte de MBTA + De lunes a viernes de 6:30 a. m. a 8:00 p. m. + MBTA Go + Cerca + Sin servicio + Estás fuera del área de servicio de MBTA. + No hay paradas cercanas + Ahora + Política de privacidad + Ver código fuente en GitHub + Términos de uso + Información de tarifas + Boletos de tren de cercanías y ferri + mTicket App + Planificador de viaje + Ocultar Mapas + Configuración + Autobús de enlace + Parada cerrada + Suspensión + diff --git a/androidApp/src/main/res/values-b+ht/strings.xml b/androidApp/src/main/res/values-b+ht/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/androidApp/src/main/res/values-b+ht/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml b/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml new file mode 100644 index 000000000..b1a3d6050 --- /dev/null +++ b/androidApp/src/main/res/values-b+ht/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + vèsyon %1$s + ARR + BRD + Anile + Detou + %1$s pou + Voye kòmantè sou aplikasyon + Tout + Plis + Ki fèt ak ♥ pa T a + Drapo Opsyon + Resous + Paramèt + Enfòmasyon ak Sipò Jeneral MBTA + Lendi rive vandredi: 6:30 AM - 8 PM + MBTA Go + Toupre + Pa gen Sèvis + Ou andeyò zòn sèvis MBTA a. + Pa gen arè ki tou pre + Kounye a + Règleman sou Vi Prive + Gade sous sou GitHub + Kondisyon itilizasyon + Enfòmasyon Tarif + Tikè Tren Vil la ak Bato + Aplikasyon mTicket + Zouti pou planifye vwayaj + Kache Kat yo + Paramèt + Navèt + Arè Fèmen + Sispansyon + diff --git a/androidApp/src/main/res/values-b+pt+BR/strings.xml b/androidApp/src/main/res/values-b+pt+BR/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/androidApp/src/main/res/values-b+pt+BR/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml b/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml new file mode 100644 index 000000000..2d81bd7c3 --- /dev/null +++ b/androidApp/src/main/res/values-b+pt+BR/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + versão %1$s + CHE + TRA + Cancelado + Desvio + %1$s para + Enviar comentário no aplicativo + Tudo + Mais + Feito com ♥ pelo T + Exibir sinalizações + Recursos + Configurações + Informações Gerais e Suporte da MBTA + De segunda a sexta: 6:30 AM - 8 PM + MBTA Go + Próximo + Sem serviço + Você está fora da área de serviço da MBTA. + Nenhuma parada próxima + Agora + Política de Privacidade + Exibir origem no GitHub + Termos de Uso + Informações sobre tarifas + Bilhetes de transporte diário por trem e balsa + Aplicativo mTicket + Planejador de trajetos + Ocultar mapas + Configurações + Ônibus vai-e-vem + Parada fechada + Suspensão + diff --git a/androidApp/src/main/res/values-b+vi/strings.xml b/androidApp/src/main/res/values-b+vi/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/androidApp/src/main/res/values-b+vi/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml b/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml new file mode 100644 index 000000000..9d4a91298 --- /dev/null +++ b/androidApp/src/main/res/values-b+vi/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + phiên bản %1$s + ARR + BRD + Đã hủy + Đường vòng + %1$s đến + Gửi ý phản hồi về ứng dụng + Tất cả + Thêm + Được tạo ra với ♥ bởi T + Cờ tính năng + Nguồn + Cài đặt + Thông tin & Hỗ trợ chung của MBTA + Từ thứ Hai đến thứ Sáu: 6:30 sáng - 8:00 tối + MBTA Go + Ở gần + Không có dịch vụ + Bạn đang ở ngoài khu vực dịch vụ của MBTA. + Không có điểm dừng gần đó + Hiện nay + Chính sách bảo mật + Xem nguồn trên GitHub + Điều khoản sử dụng + Thông tin giá vé + Vé phà vé đường sắt ngoại ô (Commuter Rail) + Ứng dụng mTicket + Lập kế hoạch chuyến đi + Ẩn bản đồ + Cài đặt + Xe đưa đón + Điểm dừng bị đóng + Đình chỉ + diff --git a/androidApp/src/main/res/values-b+zh+Hans+CN/strings.xml b/androidApp/src/main/res/values-b+zh+Hans+CN/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/androidApp/src/main/res/values-b+zh+Hans+CN/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml b/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml new file mode 100644 index 000000000..00f5e4649 --- /dev/null +++ b/androidApp/src/main/res/values-b+zh+Hans+CN/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + 版本%1$s + 到站 + 上客 + 已取消 + 交通改道 + %1$s至 + 发送应用程序反馈 + 全部 + 更多 + Made with ♥ by the T + 功能标记 + 资源 + 设置 + MBTA通用信息与支持 + 周一至周五:上午6:30至晚上8:00 + MBTA Go + 附近 + 无服务 + 您位于 MBTA 服务区之外。 + 附近无站点 + 现在 + 隐私政策 + 在GitHub上查看源代码 + 使用条款 + 票价信息 + 通勤列车和轮渡票 + mTicket应用程序 + Trip Planner + 隐藏地图 + 设置 + 摆渡巴士 + 站点已关闭 + 暂停 + diff --git a/androidApp/src/main/res/values-b+zh+Hant+TW/strings.xml b/androidApp/src/main/res/values-b+zh+Hant+TW/strings.xml new file mode 100644 index 000000000..045e125f3 --- /dev/null +++ b/androidApp/src/main/res/values-b+zh+Hant+TW/strings.xml @@ -0,0 +1,3 @@ + + + diff --git a/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml b/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml new file mode 100644 index 000000000..4fe24d2b6 --- /dev/null +++ b/androidApp/src/main/res/values-b+zh+Hant+TW/strings_ios_converted.xml @@ -0,0 +1,36 @@ + + + 版本 %1$s + 到站 + 上客 + 已取消 + 交通改道 + %1$s至 + 傳送應用程式回饋 + 全部 + 更多 + Made with ♥ by the T + 功能標記 + 資源 + 設定 + MBTA通用資訊與支援 + 週一至週五:上午6:30至晚上8:00 + MBTA Go + 附近 + 無服務 + 您位於 MBTA 服務區域之外。 + 附近沒有停靠站 + 現在 + 隱私政策 + 在GitHub上查看原始程式碼 + 使用條款 + 票價資訊 + 通勤列車和輪渡票 + mTicket應用程式 + Trip Planner + 隱藏地圖 + 設定 + 擺渡巴士 + 網站已關閉 + 暫停 + diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 2c12d2c64..e04335c01 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ - + version %1$s - 1 min ARR BRD Cancelled @@ -9,9 +8,8 @@ %1$s to Send app feedback All - Hello, %1$s! - External Link - %1$s min + External Link + %1$s min More Made with ♥ by the T Feature Flags @@ -22,8 +20,8 @@ MBTA Go Nearby No Service - Your current location is outside of our search area. - No nearby MBTA stops + You’re outside the MBTA service area. + No nearby stops Now Privacy Policy View source on GitHub diff --git a/buildSrc/src/main/kotlin/com/mbta/tid/mbta_app/gradle/ConvertIosLocalizationTask.kt b/buildSrc/src/main/kotlin/com/mbta/tid/mbta_app/gradle/ConvertIosLocalizationTask.kt new file mode 100644 index 000000000..2506b3314 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/mbta/tid/mbta_app/gradle/ConvertIosLocalizationTask.kt @@ -0,0 +1,142 @@ +package com.mbta.tid.mbta_app.gradle + +import javax.xml.parsers.DocumentBuilderFactory +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import org.gradle.api.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class ConvertIosLocalizationTask : DefaultTask() { + @get:InputFile abstract var xcstrings: RegularFile + @get:OutputDirectory abstract var resources: Directory + + @TaskAction + fun run() { + val iosStrings = readIosStrings() + val languageTags = iosStrings.values.map { it.keys }.reduce(Set::plus) - "en" + val androidEnglishStringsById = parseAndroidStrings(resources.file("values/strings.xml")) + val androidIdsByEnglishString = + androidEnglishStringsById.entries.groupBy({ it.value }, { it.key }) + + for (languageTag in languageTags) { + val iosStringsByEnglishString = iosStrings.mapValues { it.value[languageTag] } + val translatedStringsById = + iosStringsByEnglishString + .flatMap { (englishString, translations) -> + val androidIds = androidIdsByEnglishString[englishString] + if (androidIds == null || translations == null) return@flatMap emptyList() + androidIds.map { Pair(it, translations) } + } + .toMap() + writeAndroidStrings(languageTag, translatedStringsById) + } + } + + /** Returns (English text => (BCP 47 tag => translated text)) for non-plural strings. */ + private fun readIosStrings(): Map> { + val inputData = xcstrings.asFile.readText() + val strings = Json.decodeFromString(inputData) + return strings.staticStringsByEnglishText() + } + + @Serializable + private data class XcStrings( + val sourceLanguage: String, + val strings: Map, + val version: String, + ) { + fun staticStringsByEnglishText(): Map> { + return strings + .mapNotNull { + val translations = it.value.invariantLocalizations() ?: return@mapNotNull null + val englishText = translations["en"] ?: convertIosTemplate(it.key) + Pair(englishText, translations) + } + .toMap() + } + } + + @Serializable + private data class XcStringInfo( + val comment: String? = null, + val extractionState: String? = null, + val localizations: Map? = null, + ) { + + fun invariantLocalizations() = + localizations + ?.mapNotNull { + Pair( + convertIosTemplate(it.key), + convertIosTemplate(it.value.stringUnit?.value ?: return@mapNotNull null) + ) + } + ?.toMap() + } + + @Serializable + private data class Localization( + val stringUnit: StringUnit? = null, + val variations: Variations? = null, + ) + + @Serializable private data class StringUnit(val state: String, val value: String) + + @Serializable private data class Variations(val plural: Map) + + /** @return A map from IDs to text content. */ + private fun parseAndroidStrings(stringsFile: RegularFile): Map { + val parser = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val xmlDocument = parser.parse(stringsFile.asFile) + val stringElements = xmlDocument.getElementsByTagName("string") + val result = mutableMapOf() + for (i in 0 until stringElements.length) { + val stringElement = stringElements.item(i) + val id = stringElement.attributes.getNamedItem("name").textContent + val value = stringElement.textContent + result[id] = value + } + return result + } + + private fun writeAndroidStrings(languageTag: String, stringsById: Map) { + val outputDir = resources.dir("values-b+${languageTag.replace("-", "+")}") + outputDir.asFile.mkdirs() + val overrideFile = outputDir.file("strings.xml") + val overrideStrings = parseAndroidStrings(overrideFile) + val entriesToWrite = + stringsById.filterNot { it.key in overrideStrings.keys }.entries.sortedBy { it.key } + val outputFile = outputDir.file("strings_ios_converted.xml") + val result = buildString { + appendLine("") + appendLine("") + for ((id, value) in entriesToWrite) { + val escapedValue = value.replace("&", "&").replace("<", "<") + appendLine(" $escapedValue") + } + appendLine("") + } + outputFile.asFile.writeText(result) + } + + companion object { + private val template = Regex("""%(?:(?\d+)\$)?l?(?[\w@])""") + + private fun replaceTemplate(match: MatchResult, getDefaultIndex: () -> Int): String { + val index = match.groups["index"]?.value ?: getDefaultIndex().toString() + val format = match.groups["format"]?.value.takeUnless { it == "@" } ?: "s" + return "%$index$$format" + } + + private fun convertIosTemplate(iosTemplate: String): String { + var unspecifiedIndex = 1 + return iosTemplate.replace(template) { + replaceTemplate(it) { unspecifiedIndex.also { unspecifiedIndex += 1 } } + } + } + } +}