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): Resource(id, items) private sealed interface StringItem { data class Value(val text: String) : StringItem data class Array(val items: List) : StringItem } private val stringsCacheMutex = Mutex() private val parsedStringsCache = mutableMapOf>>() //@TestOnly internal fun dropStringsCache() { parsedStringsCache.clear() } private suspend fun getParsedStrings( path: String, resourceReader: ResourceReader ): Map = 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 { 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, 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 { 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 = loadStringArray(resource, DefaultResourceReader, getResourceEnvironment()) @OptIn(ExperimentalResourceApi::class) private suspend fun loadStringArray( resource: StringResource, resourceReader: ResourceReader, environment: ResourceEnvironment ): List { 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 = List(length) { item(it) } .filterIsInstance() .filter { it.localName == name }