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:
<!-- 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>
<!-- 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:
- Cross-platform consistency - Same format as iOS, web, and backend
- Easier automation - JSON is simpler to parse and generate
- Better tooling - More translation management tools support JSON
- 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:
// assets/i18n/en.json
{
"app": {
"name": "My App",
"welcome": "Welcome, {name}!",
"items": {
"one": "{count} item",
"other": "{count} items"
}
},
"buttons": {
"submit": "Submit",
"cancel": "Cancel"
}
}
// 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#
// 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#
// 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:
// 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.
Best Practices for Android i18n#
1. Placeholder Protection#
Always use consistent placeholder formats. The most common formats are:
| Format | Example | Use Case |
|---|---|---|
{name} | Hello, {name}! | Named placeholders |
%1$s | Hello, %1$s! | Positional strings |
%d | %d items | Numbers |
{{var}} | {{count}} items | ICU format |
2. Organize by Feature#
{
"auth": {
"login": { ... },
"register": { ... }
},
"settings": {
"profile": { ... },
"notifications": { ... }
}
}
3. Include Context Comments#
{
"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#
- Export your base
en.jsonfile - Upload to shipglobal.dev
- Select target languages (29 available)
- 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:
// 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:
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:
<application
android:supportsRtl="true"
...>
Issue 3: Dynamic Language Switching#
Problem: User wants to change language without restarting app.
Solution: Implement a language switcher:
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.
Related Resources#
- JSON to Android strings.xml Converter — Free online tool to convert between JSON and strings.xml
- Android Localization — Translate strings.xml — Translate your Android app to 29 languages
- Android Official Localization Guide
- React i18next Placeholder Guide
- Vue i18n in JavaScript Guide