From 459cf30ec75abada5fe4ddb8c8873b5583c1f586 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 5 Jul 2023 15:04:41 +0300 Subject: [PATCH] Add localize CI import/export support --- ...export-en-localization-on-sprint-push.yaml | 17 +++ ...anslation-and-test-on-manual-dispatch.yaml | 48 +++++++ .../kotlin/io/template/LocalizationTest.kt | 126 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 .github/workflows/export-en-localization-on-sprint-push.yaml create mode 100644 .github/workflows/import-translation-and-test-on-manual-dispatch.yaml create mode 100644 app/src/testUnit/kotlin/io/template/LocalizationTest.kt diff --git a/.github/workflows/export-en-localization-on-sprint-push.yaml b/.github/workflows/export-en-localization-on-sprint-push.yaml new file mode 100644 index 0000000..230e531 --- /dev/null +++ b/.github/workflows/export-en-localization-on-sprint-push.yaml @@ -0,0 +1,17 @@ +name: export 'en' localization on sprint push + +on: + push: + branches: + - main + +jobs: + build: + if: false # remove when ready + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: upload en localisation + run: curl --fail -H "Authorization:Loco ${{ secrets.LOCALISE_KEY }}" --data @app/src/main/res/values/strings.xml https://localise.biz/api/import/xml\?locale\=en\&format\=android\&ignore-existing\=true\&flag-new\=Incomplete diff --git a/.github/workflows/import-translation-and-test-on-manual-dispatch.yaml b/.github/workflows/import-translation-and-test-on-manual-dispatch.yaml new file mode 100644 index 0000000..179dba0 --- /dev/null +++ b/.github/workflows/import-translation-and-test-on-manual-dispatch.yaml @@ -0,0 +1,48 @@ +name: import translation and test + +on: + workflow_dispatch: + inputs: + import_translation: + description: 'Import translation and test' + +jobs: + build: + if: false # remove when ready + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11.0.16+1 + cache: 'gradle' + - name: get all translations + run: curl --header "Authorization:Loco ${{ secrets.LOCALISE_KEY }}" https://localise.biz/api/export/archive/xml.zip\?format\=android\&status\=translated -o ./app/src/main/res/xml.zip + - name: apply translations + run: | + unzip -o ./app/src/main/res/xml.zip + rm ./app/src/main/res/xml.zip + cp -rf ####ARCHIVE_NAME####/res app/src/main/ + rm -rf ####ARCHIVE_NAME#### + - name: init build tools + run: ln -s $ANDROID_HOME/build-tools/33.0.2/d8 $ANDROID_HOME/build-tools/33.0.2/dx; ln -s $ANDROID_HOME/build-tools/33.0.2/lib/d8.jar $ANDROID_HOME/build-tools/33.0.2/lib/dx.jar; + - name: run localization tests + run: ./gradlew :app:testRelease --tests io.template.LocalizationTest + - name: Android Test Report + uses: asadmansr/android-test-report-action@v1.2.0 + if: ${{ always() }} + - name: commit files + run: | + git status + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + git commit -m "Update translations" + - name: push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/app/src/testUnit/kotlin/io/template/LocalizationTest.kt b/app/src/testUnit/kotlin/io/template/LocalizationTest.kt new file mode 100644 index 0000000..4b4c475 --- /dev/null +++ b/app/src/testUnit/kotlin/io/template/LocalizationTest.kt @@ -0,0 +1,126 @@ +package io.template + +import android.content.res.Resources +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import io.template.app.R + +@RunWith(RobolectricTestRunner::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class LocalizationTest { + + companion object { + private val PLURAL_QUANTITIES = listOf(0, 1, 2, 3, 5, 11, 21, 42, 102) + private val PLATFORM_PATTERN = Regex("%s|%d|%\\d[$]s|%\\d[$]d") + private val PHRASE_PATTERN = Regex("[{].*?[}]") + private val SKIP_KEYS = listOf("abc_shareactionprovider_share_with_application") + + private var platformPatternMap: Map> = mapOf() + private var phrasePatternMap: Map> = mapOf() + } + + private val resources: Resources + get() = RuntimeEnvironment.getApplication().resources + + @Test + @Config(sdk = [28]) + fun initPatterns() { + platformPatternMap = getAllStringsAndPlurals() + .associate { (key, string) -> key to PLATFORM_PATTERN.findAll(string).map { it.value }.toList() } + .filter { (key) -> !SKIP_KEYS.contains(key) } + .filter { (_, list) -> list.isNotEmpty() } + println("platformPatternMap.size - ${platformPatternMap.size}") + + phrasePatternMap = getAllStringsAndPlurals() + .associate { (key, string) -> key to PHRASE_PATTERN.findAll(string).map { it.value }.toList() } + .filter { (key) -> !SKIP_KEYS.contains(key) } + .filter { (_, list) -> list.isNotEmpty() } + println("phrasePatternMap.size - ${phrasePatternMap.size}") + } + + private fun getAllStringsAndPlurals(): List> { + val strings = R.string::class.java.fields.map { it.name to resources.getString(it.getInt(null)) } + val quantities = R.plurals::class.java.fields.map { it.name to resources.getQuantityString(it.getInt(null), 1) } + return strings + quantities + } + + @Test + @Config(sdk = [28], qualifiers = "en") + fun testEnglish() = test() + + @Test + @Config(sdk = [28], qualifiers = "de") + fun testGerman() = test() + + + private fun test() { + testStrings() + testPlurals() + } + + private fun testStrings() { + R.string::class.java.fields.forEach { field -> + val key = field.name + val resourceValue = field.getInt(null) + val value = resources.getString(resourceValue) + + platformPatternMap[key].let { expected -> + if (expected.isNullOrEmpty()) return@let + + val actual = PLATFORM_PATTERN.findAll(value).map { it.value }.toMutableList() + + assertEquals(expected.sorted(), actual.sorted(), "Format arguments are not equal for string with key - $key.") + assertDoesNotThrow("Creating formatted string should be possible with key - $key.") { + resources.getString( + /* id = */ resourceValue, + /* ...formatArgs = */ *expected.map { if (it.contains('s')) "test" else 1 }.toTypedArray() + ) + } + } + + phrasePatternMap[key]?.onEach { expectedKey -> + assert(value.contains(expectedKey)) { "Key - $key, value - $value, should contain $expectedKey" } + } + } + } + + private fun testPlurals() { + R.plurals::class.java.fields.forEach { field -> + PLURAL_QUANTITIES.onEach { quantity -> + val key = field.name + val resourceValue = field.getInt(null) + val value = assertDoesNotThrow("Failed to crate plural with key - $key and quantity - $quantity") { + resources.getQuantityString(resourceValue, quantity) + } + assert(value.isNotBlank()) { "Value for key - $key and quantity - $quantity should no be blank" } + + platformPatternMap[key].let { expected -> + if (expected.isNullOrEmpty()) return@let + + val actual = PLATFORM_PATTERN.findAll(value).map { it.value }.toMutableList() + + assertEquals(expected.sorted(), actual.sorted(), "Format arguments are not equal for plural with key - $key " + + "and quantity - $quantity.") + assertDoesNotThrow("Creating formatted plural should be possible with key - $key and quantity - $quantity.") { + resources.getQuantityString( + /* id = */ resourceValue, + /* quantity = */ quantity, + /* ...formatArgs = */ *expected.map { if (it.contains('s')) "test" else 1 }.toTypedArray() + ) + } + } + + phrasePatternMap[key]?.onEach { phraseKey -> + assert(value.contains(phraseKey)) { "Key - $key, value - $value, should contain $phraseKey" } + } + } + } + } +}