Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Drivemode We are hiring! 30 Shibuya.apk #21

Slide 31

Slide 31 text

Drivemode We are hiring! 31 Shibuya.apk #21

Slide 32

Slide 32 text

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