Repository: JetBrains/compose-multiplatform
File: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt (Kotlin)

Code before:
package org.jetbrains.compose.resources

import androidx.compose.runtime.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList

private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")

/**
 * Represents a string resource in the application.
 *
 * @param id The unique identifier of the resource.
 * @param key The key used to retrieve the string resource.
 * @param items The set of resource items associated with the string resource.
 */
@OptIn(InternalResourceApi::class)
@ExperimentalResourceApi
@Immutable
class StringResource
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>): Resource(id, items)

private sealed interface StringItem {
    data class Value(val text: String) : StringItem
    data class Array(val items: List<String>) : StringItem
}

private val stringsCacheMutex = Mutex()
private val parsedStringsCache = mutableMapOf<String, Deferred<Map<String, StringItem>>>()

//@TestOnly
internal fun dropStringsCache() {
    parsedStringsCache.clear()
}

private suspend fun getParsedStrings(
    path: String,
    resourceReader: ResourceReader
): Map<String, StringItem> = coroutineScope {
    val deferred = stringsCacheMutex.withLock {
        parsedStringsCache.getOrPut(path) {
            //LAZY - to free the mutex lock as fast as possible
            async(start = CoroutineStart.LAZY) {
                parseStringXml(path, resourceReader)
            }
        }
    }
    deferred.await()
}

private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map<String, StringItem> {
    val nodes = resourceReader.read(path).toXmlElement().childNodes
    val strings = nodes.getElementsWithName("string").associate { element ->
        element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty())
    }
    val arrays = nodes.getElementsWithName("string-array").associate { arrayElement ->
        val items = arrayElement.childNodes.getElementsWithName("item").map { element ->
            element.textContent.orEmpty()
        }
        arrayElement.getAttribute("name") to StringItem.Array(items)
    }
    return strings + arrays
}

/**
 * Retrieves a string using the specified string resource.
 *
 * @param resource The string resource to be used.
 * @return The retrieved string resource.
 *
 * @throws IllegalArgumentException If the provided ID is not found in the resource file.
 */
@ExperimentalResourceApi
@Composable
fun stringResource(resource: StringResource): String {
    val resourceReader = LocalResourceReader.current
    val str by rememberResourceState(resource, { "" }) { env ->
        loadString(resource, resourceReader, env)
    }
    return str
}

/**
 * Loads a string using the specified string resource.
 *
 * @param resource The string resource to be used.
 * @return The loaded string resource.
 *
 * @throws IllegalArgumentException If the provided ID is not found in the resource file.
 */
@ExperimentalResourceApi
suspend fun getString(resource: StringResource): String =
    loadString(resource, DefaultResourceReader, getResourceEnvironment())

@OptIn(ExperimentalResourceApi::class)
private suspend fun loadString(
    resource: StringResource,
    resourceReader: ResourceReader,
    environment: ResourceEnvironment
): String {
    val path = resource.getPathByEnvironment(environment)
    val keyToValue = getParsedStrings(path, resourceReader)
    val item = keyToValue[resource.key] as? StringItem.Value
        ?: error("String ID=`${resource.key}` is not found!")
    return item.text
}

/**
 * Retrieves a formatted string using the specified string resource and arguments.
 *
 * @param resource The string resource to be used.
 * @param formatArgs The arguments to be inserted into the formatted string.
 * @return The formatted string resource.
 *
 * @throws IllegalArgumentException If the provided ID is not found in the resource file.
 */
@ExperimentalResourceApi
@Composable
fun stringResource(resource: StringResource, vararg formatArgs: Any): String {
    val resourceReader = LocalResourceReader.current
    val args = formatArgs.map { it.toString() }
    val str by rememberResourceState(resource, { "" }) { env ->
        loadString(resource, args, resourceReader, env)
    }
    return str
}

/**
 * Loads a formatted string using the specified string resource and arguments.
 *
 * @param resource The string resource to be used.
 * @param formatArgs The arguments to be inserted into the formatted string.
 * @return The formatted string resource.
 *
 * @throws IllegalArgumentException If the provided ID is not found in the resource file.
 */
@ExperimentalResourceApi
suspend fun getString(resource: StringResource, vararg formatArgs: Any): String = loadString(
    resource,
    formatArgs.map { it.toString() },
    DefaultResourceReader,
    getResourceEnvironment()
)

@OptIn(ExperimentalResourceApi::class)
private suspend fun loadString(
    resource: StringResource,
    args: List<String>,
    resourceReader: ResourceReader,
    environment: ResourceEnvironment
): String {
    val str = loadString(resource, resourceReader, environment)
    return SimpleStringFormatRegex.replace(str) { matchResult ->
        args[matchResult.groupValues[1].toInt() - 1]
    }
}

/**
 * Retrieves a list of strings using the specified string array resource.
 *
 * @param resource The string resource to be used.
 * @return A list of strings representing the items in the string array.
 *
 * @throws IllegalStateException if the string array with the given ID is not found.
 */
@ExperimentalResourceApi
@Composable
fun stringArrayResource(resource: StringResource): List<String> {
    val resourceReader = LocalResourceReader.current
    val array by rememberResourceState(resource, { emptyList() }) { env ->
        loadStringArray(resource, resourceReader, env)
    }
    return array
}

/**
 * Loads a list of strings using the specified string array resource.
 *
 * @param resource The string resource to be used.
 * @return A list of strings representing the items in the string array.
 *
 * @throws IllegalStateException if the string array with the given ID is not found.
 */
@ExperimentalResourceApi
suspend fun getStringArray(resource: StringResource): List<String> =
    loadStringArray(resource, DefaultResourceReader, getResourceEnvironment())

@OptIn(ExperimentalResourceApi::class)
private suspend fun loadStringArray(
    resource: StringResource,
    resourceReader: ResourceReader,
    environment: ResourceEnvironment
): List<String> {
    val path = resource.getPathByEnvironment(environment)
    val keyToValue = getParsedStrings(path, resourceReader)
    val item = keyToValue[resource.key] as? StringItem.Array
        ?: error("String array ID=`${resource.key}` is not found!")
    return item.items
}

private fun NodeList.getElementsWithName(name: String): List<Element> =
    List(length) { item(it) }
        .filterIsInstance<Element>()
        .filter { it.localName == name }

Review comment (bug):
Should not we handle an error here somehow, if we fail to parse int? 

Code after:

//https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes
/**
 * Replaces
 *
 * '\n' -> new line
 *
 * '\t' -> tab
 *
 * '\uXXXX' -> unicode symbol
 *
 * '\\' -> '\'
 *
 * @param string The input string to handle.
 * @return The string with special characters replaced according to the logic.
 */
internal fun handleSpecialCharacters(string: String): String {
    val unicodeNewLineTabRegex = Regex("""\\u[a-fA-F\d]{4}|\\n|\\t""")
    val doubleSlashRegex = Regex("""\\\\""")
    val doubleSlashIndexes = doubleSlashRegex.findAll(string).map { it.range.first }
    val handledString = unicodeNewLineTabRegex.replace(string) { matchResult ->
        if (doubleSlashIndexes.contains(matchResult.range.first - 1)) matchResult.value
        else when (matchResult.value) {
            "\\n" -> "\n"
            "\\t" -> "\t"
            else -> matchResult.value.substring(2).toInt(16).toChar().toString()
        }
    }.replace("""\\""", """\""")
    return handledString
}