Software localization

Kotlin Multiplatform Mobile I18n for Android and iOS Apps

Learn how to share resources across your #Android and #iOS apps with Kotlin Multiplatform Mobile and implement #i18n support with ease.
Software localization blog category featured image | Phrase

The Android and iOS versions of an application can often have a lot in common but also differ significantly—especially in terms of their user interface (UI): from subtle variations in scrolling behavior to completely divergent navigation logic.

At the same time, the application’s business logic, including such features as data management, analytics, and authentication, is often identical. That’s why it’s natural to share some parts of an application across platforms while keeping other parts completely separate.

If want to learn how to easily share resources across your multilingual Android and iOS apps to reduce duplication and increase the quality of your code, follow this step-by-step tutorial and find out how internationalization works with Kotlin Multiplatform Mobile (KMM).

What is Kotlin MultiPlatform Mobile (KMM)?

Kotlin Multiplatform Mobile (KMM) is an SDK for cross-platform mobile development by JetBrains. One of Kotlin's key benefits is its support for multiplatform programming: It reduces the time needed for writing and maintaining the same code for different mobile platforms while retaining the flexibility and benefits of native programming. With Kotlin, you can use a single codebase for the business logic of iOS and Android apps and write platform-specific code only where necessary in order to implement a native UI or when working with platform-specific APIs.

The project structure in Kotlin Multiplatform Mobile

The default project structure created by the KMM plugin in Android Studio is as follows:

The project structure in Kotlin Multiplatform Mobile | Phrase

As you can see above, the project is divided into three modules:

  • androidApp—contains all the native code for the Android application
  • iosApp—contains all the native code for the iOS application
  • shared—contains all the shared code (eg business logic) to be used in both the Android and iOS platform

Internationalization architecture

Generally speaking, these are two ways to internationalize Kotlin Multiplatform Mobile apps...

Independent resource files

With this approach, you add internationalization to standalone Android and iOS projects. You'll need to add translation files to both the androidApp and the iosApp module and then maintain them independently. Have a look at our Deep Dive on Internationalizing Jetpack Compose Android Apps and SwiftUI Tutorial on Localization for step-by-step instructions on how it gets implemented.

Shared resource files

Instead of adding resource files to both Android and iOS modules, we can take advantage of KMM and add resource files to the shared module and then, essentially, use the same files in both the iOS and Android app modules. Since we only need to maintain resource files in a shared module, we can reduce resource files by 50%. We'll be using exactly this approach for the purposes of this tutorial.

Prerequisites

Here's what we need to get our project started:

  •  Android Studio—version 4.2 or higher; we'll use Android Studio for creating your multiplatform applications and running them on simulated or hardware Android devices.
  • Kotlin Multiplatform Mobile plugin—for setting up KMM projects for iOS and Android.
  • Xcode—version 11.3 or higher; most of the time, Xcode will work in the background; we'll use it to add Swift code to our iOS application and run it on an iOS emulator or device.

Getting started

For this tutorial, we'll build a checkout page with KMM for our app in both iOS and Android. The page consists of an interpolated text line at the top, where "John" is a placeholder, a hardcoded image, slider, and text at the bottom, which depends on the value of the slider.

Hardcoded demo app | Phrase
Create a new project in Android Studio using the Kotlin Multiplatform plugin.

🗒 Note » KMM is still in alpha. While creating a project, make sure you select Regular framework for iOS framework distribution since the alternative Cocoapods still has some dependency issues.

Creating a new KMM application | Phrase

Installing plugins and dependencies

We'll be using Moko Resources, a Kotlin MultiPlatform open-source library that provides access to the resources on both iOS and Android with the support of the default system localization.

🗒 Note » You not only need to share language resources between iOS and Android but also need to make sure that the correct resources are picked up by the platforms. For example, when an iOS/Android system language is set to German, you need to make sure the German language files are picked up by both the Android and iOS apps. This library helps us to do that automatically.

To the root build.gradle.kts file, add the moko-resource dependency.

dependencies

 {

 classpath("dev.icerock.moko:resourcesgenerator:0.16.2")

  }

Now go to shared > build.gradle.kts, and add the following:

plugins {

..

    id("dev.icerock.mobile.multiplatform-resources")

}

dependencies {

    commonMainApi("dev.icerock.moko:resources:0.16.2")

}

multiplatformResources {

    multiplatformResourcesPackage = "org.example.library" // required

    iosBaseLocalizationRegion = "en" // optional, default "en"

}

Adding languages to iOS

Now you need to add all the compatible languages to the iOS plist file. This step is only required for the iOS platform; Android picks up all the added languages automatically.

Go to the iosApp > iosApp > Info.plist file and add all the supported languages. In this project, we'll support English, Russian, and German.

<key>CFBundleLocalizations</key>

<array>

    <string>en</string>

    <string>ru</string>

    <string>de</string>

</array>

Creating language resource packages

Now we need to insert string resources for all supported languages in the shared module.

Go to shared > src > commonMain, create a new directory "resources", and another directory called "MR" under it; MR stands for Moko Resources. It's important to name the package this way because the Mobile Kotlin resources library we had added previously will generate a class called MR, containing all the strings which will be accessible in commonMain.

MR will contain folders of all the supported languages and each will have its own string.xml file. The final file structure will look as follows:

MR file structure | Phrase
🗒 Note » The base package contains the default strings file. If our app doesn't support the phone OS's active languages, all the values from the base package will be used. In our project, the base package contains English strings.

Adding string values

Now we'll add string values for all the supported languages.

//for base>strings.xml

<resources>

    <string name="greeting">Hello</string>

</resources>

//for de>strings.xml

<resources>

    <string name="greeting">Hallo</string>

</resources>

//for ru>strings.xml

<resources>

    <string name="greeting">Привет</string>

</resources>

Adding common code to extract strings

Go to shared > src > commonMain > kotlin, create a class Text.kt  , and add the following code:

class Text {

    fun getGreeting(): StringDesc {

        //greeting is the id associated with string resource

        return StringDesc.Resource(MR.strings.greeting)

    }

}

Configuring the View Layer

Android

Go to androidApp > src> main > java > (package name) > MainActivity,  get the text from the common shared module, and set it to a TextView.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {

        ..

        val tv: TextView = findViewById(R.id.text_view)

        tv.text = Text().getGreeting().toString(this)

    }

}

Alternatively, if you're using Jetpack Compose, you can also get the text by calling this function below:

text = stringResource(id = MR.strings.greeting)

Go to Android Studio and click the Run button to start the app.

Android studio run button | Phrase

iOS

Fire up Xcode, go to iosApp > ContentView.swift, and add the following code.

struct ContentView: View {

    let greet = Text().getGreeting().localized()

    var body: some View {

        VStack{

            Text(greet)

  }

}

You can run the app via Xcode or directly on Android Studio. We'll use the latter.

Running an iOS App | Phrase
Once the app is running on both platforms, we need to check if it picks up the correct language resources for both platforms.

This is how it looks like when the system language is set to English (iOS, Android).

Demo app with system language set to English | Phrase
And here it is with Russian as the system language (iOS, Android).

Demo app with system language set to Russian | Phrase
As you can see above, the app on both platforms is perfectly picking up the system's default language.

Interpolation

Now we want the app to show something like "Hello, $name" instead of just "Hello" while the "name" field is dynamic.

Go to shared > src > commonMain > resources > MR and add these strings to all language string.xml files:

//for base>strings.xml

<string name="greeting_with_name">Hello, %s</string>

//for de>strings.xml

<string name="greeting_with_name">Hallo, %s</string>

//for ru>strings.xml

<string name="greeting_with_name">Привет, %s</string>

Next we need to add a method to commonMain, which extracts the string added above.

Go to shared > src > commonMain > kotlin > (Project ID) > Text and add this method:

fun getGreetingWithName(name:String): StringDesc {

       return StringDesc.ResourceFormatted(

           MR.strings.greeting_with_name, name)

   }

The method takes in a parameter called name and returns a formatted string.

Now we need to fetch these strings from both the Android and iOS modules.

Android

Go to androidApp > src> main > java > (package name) > MainActivity and replace the previous code with the following one:

class MainActivity : AppCompatActivity() {

  private val name = "John"

  override fun onCreate(savedInstanceState: Bundle?) {

      ..

      tv.text = Text().getGreetingWithName(name).toString(this)

  }}

iOS

Fire up Xcode, go to iosApp > ContentView.swift, and make the following changes.

let displayName = "John"

struct ContentView: View {

let greet = Text().getGreetingWithName(name:displayName).localized()

...

}

Now let us run the app on both platforms. As you can see below, both iOS (on the left) and Android (on the right) platforms are able to display the interpolated string.

Demo app with interpolated strings | Phrase

Plurals

Now we need to add plurals for the text communicating quantity.

Go to shared > src > commonMain > resources > MR > base and create a new file called plurals.xml.

🗒 Note »  Some languages, like Arabic, have 6 plural forms. You can use the attributes “zero”, "one“, "two”, “few,” “many”, or "other" as required by the language.

<?xml version="1.0" encoding="UTF-8" ?>

<resources>

    <plural name="dress">

        <item quantity="zero">%d dresses</item>

        <item quantity="one">%d dress</item>

        <item quantity="two">%d dresses</item>

        <item quantity="few">%d dresses</item>

        <item quantity="many">%d dresses</item>

        <item quantity="other">%d dresses</item>

    </plural>

</resources>

You can also add the plurals file for other languages as well. For example, to add the plurals for the German language, go to shared > src > commonMain > resources > MR > de > plurals.xml and add the translated text:

<?xml version="1.0" encoding="UTF-8" ?>

<resources>

    <plural name="dress">

        <item quantity="zero">%d Kleider</item>

        <item quantity="one">%d Kleid</item>

        <item quantity="two">%d Kleider</item>

        <item quantity="few">%d Kleider</item>

        <item quantity="many">%d Kleider</item>

        <item quantity="other">%d Kleider</item>

    </plural>

</resources>

The final resources structure looks like this:

commonMain resources root final structure | Phrase
To extract these newly added resources, we need to create a method that both Android and iOS modules can access. Go to shared > src > commonMain > kotlin > (package name) > Text and add this method.

fun getMyPluralFormattedDesc(quantity: Int): StringDesc {

      //we pass quantity once as a selector to get associated

      //plural and once again to get the interpolated string.

      return StringDesc.PluralFormatted(MR.plurals.dress, quantity, quantity)

   }

Android

Whenever the SeekBar's position is changed, we pass the new quantity Integer to the getMyPluralFormattedDesc() method we defined in shared module above. Go to androidApp > MainActivity and add the following code:

seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {

  override fun onProgressChanged(seekBar: SeekBar, seekbarProgress: Int, isFromUser: Boolean) {

   summary.text = text.getMyPluralFormattedDesc(seekbarProgress).toString(this)

       }

iOS

In a similar way, in iOS, go to iosApp > ContentView.swift and add the following code:

struct ContentView: View {

   @State var sliderValue: Double = 0

   var body: some View {

   Slider(value: $sliderValue, in: 0...5, step: 1)

   Text(Text().getMyPluralFormattedDesc(quantity:Int32(Int(sliderValue))).localized())

    }

}

We're at the finish line!

When the phone's language is set to English, the app picks up the string resources from the base(default) package, and when it's set to German, it picks up the German strings.xml and plurals.xml files. This architecture follows the single-source-of-truth (SSOT) principle as both the Android and iOS modules share common resource files, thereby reducing duplicity in code and making our code easier to test.

🔗 Resource » You can download the final version of the app on GitHub.

Our finalized Android app in English | Phrase

Our finalized Android app in English

The final Android app screen in German | Phrase

The final Android app screen in German

Our final iOS app in English | Phrase

Our final iOS app in English

The iOS app screen in German | Phrase

The iOS app screen in German

As soon as you've got your app ready for localization, make sure you give Phrase a try. The fastest, leanest, and most reliable software localization platform on the market will help you streamline the app localization process with everything you need to reach a global user base. Sign up for a free 14-day trial and let the team know any questions you might have.

If you want to drill down on mobile app localization even more, we suggest having a look at the following guides: