Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fail fast, Fail cheap, Fail automatically: Loca...

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 2

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

    3 <string name=“message”>%1$s has %2$d items</string> Shibuya.apk #21
  3. Fail fast, Fail cheap, Fail automatically: Localization Localization flow for

    text translation 10 strings.xml Upload Translate Download Notify Shibuya.apk #21
  4. Fail fast, Fail cheap, Fail automatically: Localization Placeholders stay the

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

    same across languages 12 en: <string name=“message”>{username} has {number} items</string> fr: <string name=“message”>{username} a {number} articles</string> ja: <string name=“message”>{username} ͸ {number} ͭͷ঎඼Λ͍࣋ͬͯ·͢</string> Shibuya.apk #21
  6. Fail fast, Fail cheap, Fail automatically: Localization But sometimes… 13

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

    en: <string name=“message”>{username} has {number} items</string> fr: <string name=“message”>{username} a {nombre} articles</string> ja: <string name=“message”>{username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢</string> Shibuya.apk #21
  8. Fail fast, Fail cheap, Fail automatically: Localization It crashes for

    IllegalArgumentException 15 en: <string name=“message”>{username} has {number} items</string> fr: <string name=“message”>{username} a {nombre} articles</string> ja: <string name=“message”>{username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢</string> val message = Phrase.from(context, R.string.message)
 .put(“username”, “Username”)
 .put(“number”, 3)
 .format()
 .toString() Shibuya.apk #21
  9. Fail fast, Fail cheap, Fail automatically: Localization It crashes for

    IllegalArgumentException 16 en: <string name=“message”>{username} has {number} items</string> fr: <string name=“message”>{username} a {nombre} articles</string> ja: <string name=“message”>{username} ͸ {਺ࣈ} ͭͷ঎඼Λ͍࣋ͬͯ·͢</string> val message = Phrase.from(context, R.string.message)
 .put(“username”, “Username”)
 .put(“number”, 3)
 .format()
 .toString() ✔ ✘ ✘ Shibuya.apk #21
  10. 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
  11. 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
  12. 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<Int, String> = HashMap() // default strings private val placeholderPattern: Pattern = Pattern.compile(“.*\\{.+}.*”) private val locales: List<String> = arrayListOf(“en”, “fr”, “ja”) private val optionalPlaceholders: Map<Int, List<String>> = hashMapOf( Pair(R.string.text_having_optional_placeholder, arrayListOf(“{optional}”)), // …… ) // …… } 19 Shibuya.apk #21
  13. Fail fast, Fail cheap, Fail automatically: Localization Read all strings

    defined in values/strings.xml @RunWith(RobolectricTestRunner::Class) class PlaceholderTest { private val strings: MutableMap<Int, String> = 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
  14. Fail fast, Fail cheap, Fail automatically: Localization Exclude strings without

    placeholders @RunWith(RobolectricTestRunner::Class) class PlaceholderTest { private val strings: MutableMap<Int, String> = 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
  15. 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<String>() var idx = 0 do { // find placeholders…… } while (entry.value.indexOf(“{“, idx) != -1) } } } 22 Shibuya.apk #21
  16. 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<String>() 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
  17. Fail fast, Fail cheap, Fail automatically: Localization Load translated strings

    using RuntimeEnvironment @RunWith(RobolectricTestRunner::Class) class PlaceholderTest { private val locales: List<String> = arrayListOf(“en”, “fr”, “ja”) @Test fun validatePlaceholders() { strings.forEach { entry -> val placeholders = ArrayList<String>() // …… 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
  18. 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<String>() // …… 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
  19. 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<String>() // …… locales.forEach { locale -> // …… placeholders.forEach loop@ { if (translated.contains(it)) return@loop // OK! // …… } } } } } 26 Shibuya.apk #21
  20. 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<String>() // …… locales.forEach { locale -> // …… placeholders.forEach loop@ { // …… fail(“no matching placeholder ($it) in sentence of “$locale” locale!”) } } } } } 27 Shibuya.apk #21
  21. 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
  22. DroidKaigi DroidKaigi 2018 is coming soon! ▸ Ticket: ▸ Early

    Bird: 9,800 JPY ▸ Regular: 12,000 JPY ▸ Students: 4,000 JPY 32 Shibuya.apk #21