|
| 1 | +#!/usr/bin/env kotlin |
| 2 | +/** |
| 3 | + * This script downloads all the Android string translations |
| 4 | + * from the PoEditor API (https://poeditor.com/docs/api#projects_export) |
| 5 | + */ |
| 6 | + |
| 7 | +@file:DependsOn("org.json:json:20230227") |
| 8 | +@file:DependsOn("com.squareup.okhttp3:okhttp:4.11.0") |
| 9 | + |
| 10 | +import org.json.JSONObject |
| 11 | +import java.io.File |
| 12 | +import java.net.HttpURLConnection |
| 13 | +import java.net.URL |
| 14 | +import java.net.URLEncoder |
| 15 | +import kotlin.script.experimental.dependencies.DependsOn |
| 16 | +import kotlin.system.exitProcess |
| 17 | + |
| 18 | +val POEDITOR_API_TOKEN = "set api token here" |
| 19 | +val POEDITOR_PROJECT_ID = "set project id here" |
| 20 | + |
| 21 | +val resourceFolder = "./core/ui/src/main/res" |
| 22 | + |
| 23 | +// Map containing pairs of <language, translation-file-path> |
| 24 | +val LANGUAGES = mapOf( |
| 25 | + "en-gb" to listOf( |
| 26 | + "$resourceFolder/values/strings.xml", |
| 27 | + "$resourceFolder/values-en/strings.xml" |
| 28 | + ), |
| 29 | + "nl" to listOf("$resourceFolder/values-nl/strings.xml"), |
| 30 | +) |
| 31 | + |
| 32 | +val POEDITOR_API_ENDPOINT = "https://api.poeditor.com/v2/projects/export" |
| 33 | + |
| 34 | +// This defines the platform's format of the exported translation file |
| 35 | +// Android = android_strings / iOS = apple_strings |
| 36 | +val EXPORT_TYPE = "android_strings" |
| 37 | + |
| 38 | +// Running this flag while translations are missing stops the execution |
| 39 | +val NO_MISSING_TRANSLATIONS_FLAG = "-noMissingTranslations" |
| 40 | + |
| 41 | +val noMissingTranslations = args.getOrNull(0) == NO_MISSING_TRANSLATIONS_FLAG |
| 42 | + |
| 43 | +if (noMissingTranslations) { |
| 44 | + println("Missing translations are NOT allowed.") |
| 45 | +} else { |
| 46 | + println("Missing translations are allowed.") |
| 47 | +} |
| 48 | + |
| 49 | +// Note that we duplicate the default to /values-nl/ because without it nl-NL would default to /values-nl-rBE/ instead of /values/. |
| 50 | +// More details: https://stackoverflow.com/questions/69379425/android-os-language-lookup-let-os-know-what-the-apps-default-language-is |
| 51 | +for ((language, outputFiles) in LANGUAGES) { |
| 52 | + |
| 53 | + outputFiles.forEach { outputFile -> |
| 54 | + File(outputFile).parentFile.mkdirs() |
| 55 | + } |
| 56 | + |
| 57 | + val requestParams = mapOf( |
| 58 | + "api_token" to POEDITOR_API_TOKEN, |
| 59 | + "id" to POEDITOR_PROJECT_ID, |
| 60 | + "language" to language, |
| 61 | + "type" to EXPORT_TYPE, |
| 62 | + "order" to "terms" |
| 63 | + ).entries.joinToString("&") { (key, value) -> |
| 64 | + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" |
| 65 | + } |
| 66 | + |
| 67 | + // Send post request to get download URL |
| 68 | + val connection = URL(POEDITOR_API_ENDPOINT).openConnection() as HttpURLConnection |
| 69 | + connection.apply { |
| 70 | + requestMethod = "POST" |
| 71 | + doOutput = true |
| 72 | + setRequestProperty("Content-Type", "application/x-www-form-urlencoded") |
| 73 | + outputStream.write(requestParams.toByteArray()) |
| 74 | + } |
| 75 | + |
| 76 | + val response = connection.inputStream.bufferedReader().readText() |
| 77 | + val responseJson = JSONObject(response) |
| 78 | + println(responseJson) |
| 79 | + val downloadUrl = responseJson.getJSONObject("result").getString("url") |
| 80 | + |
| 81 | + // Download translation file |
| 82 | + var content = URL(downloadUrl).readText() |
| 83 | + |
| 84 | + // Check for empty strings and stop if the missing translations flag is enabled |
| 85 | + // and there are missing translations |
| 86 | + val emptyStringsRegex = Regex("""".*><""") |
| 87 | + val emptyStrings = emptyStringsRegex.findAll(content).map { it.value }.toList() |
| 88 | + |
| 89 | + if (emptyStrings.isNotEmpty() && noMissingTranslations) { |
| 90 | + println("ERROR: Translations for language $language contains the following empty (untranslated) strings:") |
| 91 | + emptyStrings.forEach { println(it.removeSuffix("><")) } |
| 92 | + exitProcess(1) |
| 93 | + } |
| 94 | + |
| 95 | + // Remove empty strings. Android will fall back to the base language for these. |
| 96 | + // If the current language is the base language, the string will be removed from the project, |
| 97 | + // and the project won't compile. This is fine since the base language should not have empty strings. |
| 98 | + content = content.replace(Regex("""<string name=".*"></string>"""), "") |
| 99 | + |
| 100 | + // Replace all dots in names/keys with underscores, android does not support dots: |
| 101 | + content = content.replace(Regex("""name="(.+?)"""")) { matchResult -> |
| 102 | + val newName = matchResult.groupValues[1].replace(".", "_") |
| 103 | + """name="$newName"""" |
| 104 | + } |
| 105 | + |
| 106 | + // Replace all %@'s (iOS replacement param) with the android %s string replacement param |
| 107 | + content = content.replace("%@", "%s") |
| 108 | + |
| 109 | + // Remove server specific strings |
| 110 | + content = content.replace(Regex("""<string name="server_.*">[\s\S]*?</string>"""), "") |
| 111 | + content = content.replace(Regex("""<plurals name="server_.*">[\s\S]*?</plurals>"""), "") |
| 112 | + |
| 113 | + // Remove iOS specific strings |
| 114 | + content = content.replace(Regex("""<string name=".*_ios">[\s\S]*?</string>"""), "") |
| 115 | + content = content.replace(Regex("""<plurals name=".*_ios">[\s\S]*?</plurals>"""), "") |
| 116 | + |
| 117 | + // Remove comments |
| 118 | + content = content.replace(Regex("""<!--(.*?)-->\n""", RegexOption.DOT_MATCHES_ALL), "") |
| 119 | + |
| 120 | + // Remove double empty lines |
| 121 | + content = content.replace(Regex("""\n\s*\n"""), "\n\n") |
| 122 | + |
| 123 | + // Remove double and triple tabs |
| 124 | + content = content.replace(" <string", " <string") |
| 125 | + content = content.replace(" <string", " <string") |
| 126 | + |
| 127 | + // Write/overwrite downloaded file to disk |
| 128 | + for (outputFile in outputFiles) { |
| 129 | + println(" Writing to file $outputFile") |
| 130 | + File(outputFile).writeText(content) |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +println("Finished downloading translations") |
| 135 | + |
0 commit comments