Working with localization in Kotlin Multiplatform projects is a challenging task. Most existing solutions rely on static resources, requiring a full app redeployment for any text update. Imagine you need to change a single word in your app's UI, but the entire app needs to be redeployed and reviewed for days. That's inefficient!. As mobile developers we are more interested on spending time on adding new features, rather than updating app just to change one title. On other side we have to write a lot of boilerplate-code for each resource type, working with resources is takes more time, than we want, and it would be better to spend this time on more important things.
Out-of-the-box solutionsThere aren't many solutions available at the moment for KMP technology, let’s see most popular solutions:
Compose Multiplatform ResourcesJetBrains recommends using Compose Multiplatform Resources, but it has same problems that i explained, also we need to add Compose-Multiplatform which is not stable yet, and our command developing ui using native sdk, so it not match our requirements.
MOKO resourcesMOKO resources is another popular solution. It is based on code generation and uses static resources. We need to declare an XML resource for code generation. Depending on the number of supported languages, the library generates fields that provide data from the XML file. In client code we need also provide current locale and that’s it. Generated code will look like this : getString(MR.strings.mock_value, locale // current locale).
\ That sounds good, right? One issue is that the library is still evolving and has some bugs. The author actively maintains it with community support, but it still has some bugs. Additionally, MOKO doesn’t support dynamic updating of localization.
SummaryConsidering all these problems, i decided to write our own custom solution that supports code generation, validation, and runtime updates.
Solution SchemeFirst, we need to decide on the localization scheme. Let’s not reinvent the wheel and choose a key-value structure in JSON format, which will look like this: { "feature": { "key": "value" } }
Code GenerationThe next step is generating Kotlin code based on this JSON file. We can generate code using Gradle. It is not only build-system but also useful tool for writing scripts.
Gradle Plugin ImplementationGradle plugins are typically used to add functionality such as tasks, extensions, and convention plugins.Let’s define a Gradle plugin for localization.
/** * A Gradle plugin for configuring localization in a Kotlin Multiplatform project. * This plugin sets up code generation for localization resources across different modules. */ class LocalizationPlugin : Plugin\ In the provided code, we declare our plugin and retrieve information from the extension. We need key to extract required localization for our module and package to bind generated code to our current namespace.
\ After evaluating the project, we retrieve our source sets using KotlinMultiplatformExtension, which is already part of the Kotlin Gradle Plugin API. Gradle generates all code in the /build directory, so we need to provide its location to the plugin.
Generating Kotlin CodeThat’s all we need in plugin, let’s declare out gradle task, that will generate code.
/** * A Gradle task responsible for generating localization resources. * * This task handles the reading of input localization files and the * generation of corresponding Kotlin classes for use within the project. */ abstract class GenerationTask : DefaultTask() { /** * The generator used for the code generation process. * This property is marked as internal to avoid being considered as a task input or output. */ @get:Internal abstract val generator: Property\ Annotations like @Input indicate that the task requires input parameters, while @OutputDirectory specifies that the task generates a new directory. @TaskAction marks the method that will run when the task is invoked. Now i’ll explain how code generation works.
\ Gradle does not directly provide an API for generating Kotlin code, so we have to manually write raw symbols to a file using the Streams API.
\ It’s not comfortable to write kotlin code in symbols by yourself, so we will use library called Kotlin Poet. KotlinPoet is a very popular solution for generating Kotlin code and is supported by Square, which is well-known among Android developers. Let’s break down the code of Generator :
class Generator( generatedDir: File, sourceSetName: String, buildDirAbsolutePath: String, private val sourceSet: SourceSet, private val resourcesPackage: String, private val localizationPrefix: String?, private val multiplatformExtension: KotlinMultiplatformExtension, ) { /** * The path to the localization JSON file, derived from the build directory. */ private val pathToLocalization = "file:" + buildDirAbsolutePath .split(SEPARATOR_KEY) .first() + PATH_TO_LOCALIZATION /** * The directory where the generated output files will be stored. */ internal val outputDir = File(generatedDir, sourceSetName) /** * The input file for localization data, specified as a JSON file. */ private val inputFile = File(URI(pathToLocalization)) /** * The JSON parser used to parse the localization data. */ private val jsonParser: JsonParser = JsonParserIml() /** * Lazily-loaded localization string that reads the content of the input file. */ private fun localizationString(): String { val localizationData = StringBuilder() Scanner(inputFile).use { localizationReader -> while (localizationReader.hasNextLine()) { val data: String = localizationReader.nextLine() localizationData.appendLine(data) } } return localizationData.toString() } /** * The directory where the generated source files will be placed. */ private val sourcesGenerationDir get() = File(outputDir, "src") init { sourcesGenerationDir.mkdirs() sourceSet.addSourceDir(sourcesGenerationDir) } /** * Generates the Kotlin code for localization based on the parsed JSON data. * It first deletes any previously generated files and then creates new Kotlin files. */ fun generate() { sourcesGenerationDir.deleteRecursively() //this code will not provided, because you can have, own requirements for generated code generateKotlinCode( localization = jsonParser.parse( json = localizationString(), prefix = localizationPrefix ) ).writeTo(sourcesGenerationDir) generateLocalizationMapClass() .writeTo(sourcesGenerationDir) }\ Here we are reading json file for localization and creating directory for code. Also we have additional check of deleting current localization files and binding generated code to main source-set. Register your plugin and provide data through extension : multiplatformLocalization { targetPrefix = "feature_prefix", resourcesPackage = "company.startup.app" }
How to useHow localization look before :
object PrefixLocalization { val prefixOne get() = str(MR.strings.prefix_one, locale) val prefixTwo get() = str(MR.strings.prefix_two, locale) val prefixThree get() = str(MR.strings.prefix_three, locale) }
\ Invoke task in pre-compile time and here how result will look like :
fun getLocalization(): PrefixLocalization { //data could be from any source-sets or from json file return PrefixLocalization(data = data) }It perfectly matches with MVI or other UDF architectures, where you can provide generated localization class to UI like this :
@Composable fun Render(state : PrefixState) { Text(state.prefixLocalization.title) }\
Localization UpdatingNow that we’ve completed the code generation and localization setup, let's see how to update localization values at runtime. Users should not experience delays, so we will use caching and check for updates over the network. We are using REST as network protocol, so we have options like : Cache-Control and Head requests with modification date.
\ To cache data, we can use DataStore (which is now multiplatform) or implement our own file system using expect/actual technology in KMP.
/** * GENERATED CODE */ public class Localization { /** * @param localizationMap - here we can provide updated data */ public class PrefixLocalization( localizationMap: LocalizationMap, ) } } ConclusionThis approach makes localization easier, removes boilerplate, and supports runtime updates, making it a powerful solution for KMP apps.
What’s Next?In the future, we could extend this by adding support for pluralization and formatted strings.
Closing NotesThank you for reading this article, hope you enjoyed it. It is my first article, i will be thankfull for your feedback.
All Rights Reserved. Copyright , Central Coast Communications, Inc.