$30 off During Our Annual Pro Sale. View Details »

Fail fast, Fail cheap, Fail automatically: Localization

Fail fast, Fail cheap, Fail automatically: Localization

Keishin Yokomaku

January 11, 2018
Tweet

More Decks by Keishin Yokomaku

Other Decks in Technology

Transcript

  1. Fail fast, Fail cheap,
    Fail automatically: Localization
    Keishin Yokomaku (@KeithYokoma) / Shibuya.apk #21

    View Slide

  2. Fail fast, Fail cheap, Fail automatically: Localization
    Keishin Yokomaku
    2
    Shibuya.apk #21
    Drivemode, Inc. / Principal Engineer
    @KeithYokoma: GitHub / Twitter / Qiita / Tumblr / Stack Overflow

    View Slide

  3. Fail fast, Fail cheap, Fail automatically: Localization
    String resource template
    3
    %1$s has %2$d items
    Shibuya.apk #21

    View Slide

  4. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    4
    ̦

    View Slide

  5. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    5
    ̦

    View Slide

  6. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    6
    ̦

    View Slide

  7. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    7
    ̦

    View Slide

  8. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    8
    ̦

    View Slide

  9. Fail fast, Fail cheap, Fail automatically: Localization
    Phrase: template engines for string resource
    9
    ̦

    View Slide

  10. Fail fast, Fail cheap, Fail automatically: Localization
    Localization flow for text translation
    10
    strings.xml
    Upload
    Translate
    Download
    Notify
    Shibuya.apk #21

    View Slide

  11. Fail fast, Fail cheap, Fail automatically: Localization
    Placeholders stay the same across languages
    11
    en: {username} has {number} items
    fr: {username} a {number} articles
    ja: {username} ͸ {number} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    Shibuya.apk #21

    View Slide

  12. Fail fast, Fail cheap, Fail automatically: Localization
    Placeholders stay the same across languages
    12
    en: {username} has {number} items
    fr: {username} a {number} articles
    ja: {username} ͸ {number} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    Shibuya.apk #21

    View Slide

  13. Fail fast, Fail cheap, Fail automatically: Localization
    But sometimes…
    13
    en: {username} has {number} items
    fr: {username} a {nombre} articles
    ja: {username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    Shibuya.apk #21

    View Slide

  14. Fail fast, Fail cheap, Fail automatically: Localization
    But sometimes…
    14
    en: {username} has {number} items
    fr: {username} a {nombre} articles
    ja: {username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    Shibuya.apk #21

    View Slide

  15. Fail fast, Fail cheap, Fail automatically: Localization
    It crashes for IllegalArgumentException
    15
    en: {username} has {number} items
    fr: {username} a {nombre} articles
    ja: {username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    val message = Phrase.from(context, R.string.message)

    .put(“username”, “Username”)

    .put(“number”, 3)

    .format()

    .toString()
    Shibuya.apk #21

    View Slide

  16. Fail fast, Fail cheap, Fail automatically: Localization
    It crashes for IllegalArgumentException
    16
    en: {username} has {number} items
    fr: {username} a {nombre} articles
    ja: {username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢
    val message = Phrase.from(context, R.string.message)

    .put(“username”, “Username”)

    .put(“number”, 3)

    .format()

    .toString()



    Shibuya.apk #21

    View Slide

  17. Fail fast, Fail cheap, Fail automatically: Localization
    Why not detect this kind of mistake at compile time?
    17
    Fail fast, Fail cheap, Fail automatically
    Shibuya.apk #21

    View Slide

  18. Fail fast, Fail cheap, Fail automatically: Localization
    Why not detecting this kind of mistake at compile time?
    18
    Fail fast, Fail cheap, Fail automatically

    by testing with Robolectric
    Shibuya.apk #21

    View Slide

  19. Fail fast, Fail cheap, Fail automatically: Localization
    Test to match placeholders in default strings with others
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    private val strings: MutableMap = HashMap() // default strings
    private val placeholderPattern: Pattern = Pattern.compile(“.*\\{.+}.*”)
    private val locales: List = arrayListOf(“en”, “fr”, “ja”)
    private val optionalPlaceholders: Map> = hashMapOf(
    Pair(R.string.text_having_optional_placeholder, arrayListOf(“{optional}”)),
    // ……
    )
    // ……
    }
    19
    Shibuya.apk #21

    View Slide

  20. Fail fast, Fail cheap, Fail automatically: Localization
    Read all strings defined in values/strings.xml
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    private val strings: MutableMap = HashMap() // default strings
    @Before
    fun setUp() {
    R.string::class.java.fields.forEach {
    val id = it.getInt(null)
    val value = RuntimeEnvironment.application.resources.getString(id)
    strings.put(id, value)
    }
    }
    }
    20
    Shibuya.apk #21

    View Slide

  21. Fail fast, Fail cheap, Fail automatically: Localization
    Exclude strings without placeholders
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    private val strings: MutableMap = HashMap() // default strings
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    if (!pattern.matcher(entry.value).find())
    return@forEach // no placeholder in the text!
    // ……
    }
    }
    }
    21
    Shibuya.apk #21

    View Slide

  22. Fail fast, Fail cheap, Fail automatically: Localization
    Finding placeholders in each strings
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    // ……
    val placeholders = ArrayList()
    var idx = 0
    do {
    // find placeholders……
    } while (entry.value.indexOf(“{“, idx) != -1)
    }
    }
    }
    22
    Shibuya.apk #21

    View Slide

  23. Fail fast, Fail cheap, Fail automatically: Localization
    Finding placeholders in each strings
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    // ……
    val placeholders = ArrayList()
    var idx = 0
    do {
    val open = entry.value.indexOf(“{“, idx)
    val close = entry.value.indexOf(“}”, idx)
    idx = close + 1
    placeholders.add(entry.value.substring(open, idx))
    } while (entry.value.indexOf(“{“, idx) != -1)
    }
    }
    }
    23
    Shibuya.apk #21

    View Slide

  24. Fail fast, Fail cheap, Fail automatically: Localization
    Load translated strings using RuntimeEnvironment
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    private val locales: List = arrayListOf(“en”, “fr”, “ja”)
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    val placeholders = ArrayList()
    // ……
    locales.forEach { locale ->
    RuntimeEnvironment.setQualifiers(locale) // change resource loading behavior
    val translated = RuntimeEnvironment.application.getString(entry.key) // translated
    // check translated string contains placeholders……
    }
    }
    }
    }
    24
    Shibuya.apk #21

    View Slide

  25. Fail fast, Fail cheap, Fail automatically: Localization
    Pass the test for an optional placeholder
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    val placeholders = ArrayList()
    // ……
    locales.forEach { locale ->
    // ……
    placeholders.forEach loop@ {
    if (optionalPlaceholders[entry.key]?.contains(it) == true)
    return@loop // It’s optional so don’t worry
    // ……
    }
    }
    }
    }
    }
    25
    Shibuya.apk #21

    View Slide

  26. Fail fast, Fail cheap, Fail automatically: Localization
    Pass the test when translated text has the same placeholder
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    val placeholders = ArrayList()
    // ……
    locales.forEach { locale ->
    // ……
    placeholders.forEach loop@ {
    if (translated.contains(it))
    return@loop // OK!
    // ……
    }
    }
    }
    }
    }
    26
    Shibuya.apk #21

    View Slide

  27. Fail fast, Fail cheap, Fail automatically: Localization
    Otherwise the test fails
    @RunWith(RobolectricTestRunner::Class)
    class PlaceholderTest {
    @Test
    fun validatePlaceholders() {
    strings.forEach { entry ->
    val placeholders = ArrayList()
    // ……
    locales.forEach { locale ->
    // ……
    placeholders.forEach loop@ {
    // ……
    fail(“no matching placeholder ($it) in sentence of “$locale” locale!”)
    }
    }
    }
    }
    }
    27
    Shibuya.apk #21

    View Slide

  28. Fail fast, Fail cheap, Fail automatically: Localization
    Another solution
    ▸ https://github.com/JakeWharton/paraphrase
    ▸ Plugin to generate type-safe methods for phrase template
    ▸ Experimental
    28

    View Slide

  29. Fail fast, Fail cheap,
    Fail automatically: Localization
    Keishin Yokomaku (@KeithYokoma) / Shibuya.apk #21

    View Slide

  30. Drivemode
    We are hiring!
    30
    Shibuya.apk #21

    View Slide

  31. Drivemode
    We are hiring!
    31
    Shibuya.apk #21

    View Slide

  32. DroidKaigi
    DroidKaigi 2018 is coming soon!
    ▸ Ticket:
    ▸ Early Bird: 9,800 JPY
    ▸ Regular: 12,000 JPY
    ▸ Students: 4,000 JPY
    32
    Shibuya.apk #21

    View Slide