Introduction#

Android's default localization uses strings.xml files, but many developers prefer JSON-based i18n for consistency across platforms. This guide covers both approaches and shows you how to implement robust localization in your Android app.

Whether you're building a new app or maintaining an existing one, proper internationalization (i18n) is crucial for reaching global markets. Let's explore how to do it right.

Prerequisites#

  • Android Studio installed
  • Basic knowledge of Kotlin or Java
  • Understanding of Android resource system

Understanding Android Localization Basics#

The Default Approach: strings.xml#

Android's built-in localization system uses XML files in the res/values directory:

xml
<!-- res/values/strings.xml (default/English) -->
<resources>
    <string name="app_name">My App</string>
    <string name="welcome_message">Welcome, %1$s!</string>
    <string name="items_count">%d items</string>
</resources>
xml
<!-- res/values-de/strings.xml (German) -->
<resources>
    <string name="app_name">Meine App</string>
    <string name="welcome_message">Willkommen, %1$s!</string>
    <string name="items_count">%d Artikel</string>
</resources>

Why Consider JSON for Android i18n?#

JSON-based localization offers several advantages:

  1. Cross-platform consistency - Same format as iOS, web, and backend
  2. Easier automation - JSON is simpler to parse and generate
  3. Better tooling - More translation management tools support JSON
  4. Nested structures - Organize translations hierarchically

Implementing JSON-Based i18n in Android#

Step 1: Create JSON Translation Files#

First, create your translation files in the assets directory:

json
// assets/i18n/en.json
{
  "app": {
    "name": "My App",
    "welcome": "Welcome, {name}!",
    "items": {
      "one": "{count} item",
      "other": "{count} items"
    }
  },
  "buttons": {
    "submit": "Submit",
    "cancel": "Cancel"
  }
}
json
// assets/i18n/de.json
{
  "app": {
    "name": "Meine App",
    "welcome": "Willkommen, {name}!",
    "items": {
      "one": "{count} Artikel",
      "other": "{count} Artikel"
    }
  },
  "buttons": {
    "submit": "Absenden",
    "cancel": "Abbrechen"
  }
}

Step 2: Create a Translation Manager#

kotlin
// TranslationManager.kt
import android.content.Context
import org.json.JSONObject
import java.util.Locale

object TranslationManager {
    private var translations: JSONObject? = null
    private var currentLocale: String = "en"

    fun init(context: Context, locale: String = Locale.getDefault().language) {
        currentLocale = locale
        loadTranslations(context)
    }

    private fun loadTranslations(context: Context) {
        try {
            val fileName = "i18n/$currentLocale.json"
            val jsonString = context.assets.open(fileName)
                .bufferedReader()
                .use { it.readText() }
            translations = JSONObject(jsonString)
        } catch (e: Exception) {
            // Fallback to English
            val jsonString = context.assets.open("i18n/en.json")
                .bufferedReader()
                .use { it.readText() }
            translations = JSONObject(jsonString)
        }
    }

    fun getString(key: String, params: Map<String, Any> = emptyMap()): String {
        val keys = key.split(".")
        var current: Any? = translations

        for (k in keys) {
            current = (current as? JSONObject)?.opt(k)
        }

        var result = current?.toString() ?: key

        // Replace placeholders
        params.forEach { (placeholder, value) ->
            result = result.replace("{$placeholder}", value.toString())
        }

        return result
    }
}

Step 3: Use Translations in Your App#

kotlin
// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize translations
        TranslationManager.init(this)

        // Use translations
        val welcomeText = TranslationManager.getString(
            "app.welcome",
            mapOf("name" to "John")
        )
        // Result: "Welcome, John!"

        val itemsText = TranslationManager.getString(
            "app.items.other",
            mapOf("count" to 5)
        )
        // Result: "5 items"
    }
}

Handling Plurals in Android JSON i18n#

Android's default plurals system is powerful but complex. Here's how to implement it with JSON:

kotlin
// PluralHelper.kt
object PluralHelper {
    fun getPlural(
        key: String,
        count: Int,
        params: Map<String, Any> = emptyMap()
    ): String {
        val pluralKey = when {
            count == 0 -> "$key.zero"
            count == 1 -> "$key.one"
            count == 2 -> "$key.two"
            count in 3..10 -> "$key.few"
            count in 11..99 -> "$key.many"
            else -> "$key.other"
        }

        // Try specific plural form, fallback to "other"
        val translation = TranslationManager.getString(pluralKey, params)
        return if (translation == pluralKey) {
            TranslationManager.getString("$key.other", params + ("count" to count))
        } else {
            translation.replace("{count}", count.toString())
        }
    }
}

Translate your strings.xml

Printf placeholders (%s, %1$s, %d) stay put. Percentage text (79% off) is translated as text.

Create accountfrom €19 per month

Best Practices for Android i18n#

1. Placeholder Protection#

Always use consistent placeholder formats. The most common formats are:

FormatExampleUse Case
{name}Hello, {name}!Named placeholders
%1$sHello, %1$s!Positional strings
%d%d itemsNumbers
{{var}}{{count}} itemsICU format

2. Organize by Feature#

json
{
  "auth": {
    "login": { ... },
    "register": { ... }
  },
  "settings": {
    "profile": { ... },
    "notifications": { ... }
  }
}

3. Include Context Comments#

json
{
  "button_save": "Save",
  "_button_save_context": "Button label for saving user profile"
}

Automating Android Translations#

Manually translating JSON files is time-consuming and error-prone. Here's how to automate the process:

Using shipglobal.dev#

  1. Export your base en.json file
  2. Upload to shipglobal.dev
  3. Select target languages (29 available)
  4. Download translated files

The service automatically:

  • Preserves placeholders like {name} and %1$s
  • Maintains JSON structure
  • Handles plurals correctly
  • Uses Translation Memory for consistent updates

Migrating from strings.xml to JSON#

If you have an existing app using strings.xml, here's a migration script:

kotlin
// StringsXmlToJson.kt
import org.w3c.dom.Element
import javax.xml.parsers.DocumentBuilderFactory
import org.json.JSONObject

fun convertStringsXmlToJson(xmlContent: String): String {
    val factory = DocumentBuilderFactory.newInstance()
    val builder = factory.newDocumentBuilder()
    val doc = builder.parse(xmlContent.byteInputStream())

    val json = JSONObject()
    val strings = doc.getElementsByTagName("string")

    for (i in 0 until strings.length) {
        val element = strings.item(i) as Element
        val name = element.getAttribute("name")
        val value = element.textContent
        json.put(name, value)
    }

    return json.toString(2)
}

Common Issues and Solutions#

Issue 1: Missing Translations at Runtime#

Problem: App crashes when translation key doesn't exist.

Solution: Always implement fallback logic:

kotlin
fun getString(key: String, default: String = key): String {
    return try {
        // ... lookup logic
        result ?: default
    } catch (e: Exception) {
        default
    }
}

Issue 2: RTL Language Support#

Problem: Right-to-left languages display incorrectly.

Solution: Enable RTL support in your manifest:

xml
<application
    android:supportsRtl="true"
    ...>

Issue 3: Dynamic Language Switching#

Problem: User wants to change language without restarting app.

Solution: Implement a language switcher:

kotlin
fun switchLanguage(context: Context, languageCode: String) {
    TranslationManager.init(context, languageCode)
    // Recreate activities to apply changes
    (context as? Activity)?.recreate()
}

Conclusion#

JSON-based i18n in Android offers flexibility and cross-platform consistency. While strings.xml remains the default, JSON is excellent for:

  • Multi-platform apps (Android + iOS + Web)
  • Apps requiring frequent translation updates
  • Teams using translation management systems

Ready to translate your Android app? Try shipglobal.dev to automatically translate your JSON files to 29+ languages while preserving all placeholders and formatting.