|
|
|
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 ->
|
|
|
|
val rawString = element.textContent.orEmpty()
|
|
|
|
element.getAttribute("name") to StringItem.Value(handleSpecialCharacters(rawString))
|
|
|
|
}
|
|
|
|
val arrays = nodes.getElementsWithName("string-array").associate { arrayElement ->
|
|
|
|
val items = arrayElement.childNodes.getElementsWithName("item").map { element ->
|
|
|
|
val rawString = element.textContent.orEmpty()
|
|
|
|
handleSpecialCharacters(rawString)
|
|
|
|
}
|
|
|
|
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, args, { "" }) { 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 }
|
|
|
|
|
|
|
|
//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
|
|
|
|
}
|