Browse Source

Determine PluralCategory by current environment

pull/4519/head
Chanjung Kim 8 months ago
parent
commit
7413a2249e
  1. 35
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  2. 26
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCategory.kt
  3. 257
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt
  4. 8
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt
  5. 41
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt
  6. 14
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt
  7. 73
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt

35
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt

@ -4,6 +4,8 @@ import androidx.compose.runtime.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.intl.PluralCategory
import org.jetbrains.compose.resources.intl.PluralRuleList
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList
@ -39,15 +41,6 @@ class StringResource
class QuantityStringResource
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>) : Resource(id, items)
internal enum class PluralCategory {
ZERO,
ONE,
TWO,
FEW,
MANY,
OTHER
}
private sealed interface StringItem {
data class Value(val text: String) : StringItem
data class Plurals(val items: Map<PluralCategory, String>) : StringItem
@ -85,9 +78,9 @@ private suspend fun parseStringXml(path: String, resourceReader: ResourceReader)
}
val plurals = nodes.getElementsWithName("plurals").associate { pluralElement ->
val items = pluralElement.childNodes.getElementsWithName("item").mapNotNull { element ->
val pluralCategory = PluralCategory.entries.firstOrNull {
it.name.equals(element.getAttribute("quantity"), true)
} ?: return@mapNotNull null
val pluralCategory = PluralCategory.fromString(
element.getAttribute("quantity"),
) ?: return@mapNotNull null
pluralCategory to element.textContent.orEmpty()
}
pluralElement.getAttribute("name") to StringItem.Plurals(items.toMap())
@ -236,7 +229,11 @@ private suspend fun loadQuantityString(
val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Plurals
?: error("String ID=`${resource.key}` is not found!")
val pluralCategory = getPluralCategory(environment.language, quantity)
val pluralRuleList = PluralRuleList.getInstance(
environment.language,
environment.region,
)
val pluralCategory = pluralRuleList.getCategory(quantity)
val str = item.items[pluralCategory]
?: error("String ID=`${resource.key}` does not have the pluralization $pluralCategory for quantity $quantity!")
return str
@ -371,15 +368,3 @@ internal fun handleSpecialCharacters(string: String): String {
}.replace("""\\""", """\""")
return handledString
}
/**
* @param languageQualifier
* @param quantity
*/
@OptIn(InternalResourceApi::class)
internal fun getPluralCategory(languageQualifier: LanguageQualifier, quantity: Int): PluralCategory {
return when {
quantity == 1 -> PluralCategory.ONE
else -> PluralCategory.OTHER
}
}

26
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCategory.kt

@ -0,0 +1,26 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
/**
* Plural categories defined in the [CLDR Language Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules).
*/
internal enum class PluralCategory {
ZERO,
ONE,
TWO,
FEW,
MANY,
OTHER;
companion object {
fun fromString(name: String): PluralCategory? {
return entries.firstOrNull {
it.name.equals(name, true)
}
}
}
}

257
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt

@ -0,0 +1,257 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
import kotlin.math.absoluteValue
internal sealed class PluralCondition {
abstract fun isFulfilled(n: Int): Boolean
// add isFulfilled(n: Double) or isFulfilled(n: Decimal) as needed
class And(
private val left: PluralCondition,
private val right: PluralCondition,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) and right.isFulfilled(n)
override fun toString(): String = "$left and $right"
}
class Or(
private val left: PluralCondition,
private val right: PluralCondition,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) or right.isFulfilled(n)
override fun toString(): String = "$left or $right"
}
class Relation(
private val operand: PluralOperand,
private val operandDivisor: Int?,
private val comparisonIsNegated: Boolean,
private val ranges: Array<IntRange>,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean {
val expressionOperandValue = when (operand) {
PluralOperand.N, PluralOperand.I -> n.absoluteValue
else -> 0
}
val moduloAppliedValue = if (operandDivisor != null) {
expressionOperandValue % operandDivisor
} else {
expressionOperandValue
}
return ranges.any { moduloAppliedValue in it } != comparisonIsNegated
}
override fun toString(): String {
return StringBuilder().run {
append(operand.name.lowercase())
if (operandDivisor != null) {
append(" % ")
append(operand)
}
append(' ')
if (comparisonIsNegated) {
append('!')
}
append("= ")
var first = true
for (range in ranges) {
if (!first) {
append(',')
}
first = false
append(range.first)
if (range.first != range.last) {
append("..")
append(range.last)
}
}
toString()
}
}
private class Parser(private val description: String) {
private var currentIdx = 0
private fun consumeWhitespaces() {
while (currentIdx < description.length && description[currentIdx].isWhitespace()) {
currentIdx += 1
}
}
private fun peekNextOrNull(): Char? {
return description.getOrNull(currentIdx)
}
private fun peekNext(): Char {
return peekNextOrNull() ?: throw PluralConditionParseException(description)
}
private fun consumeNext(): Char {
val next = peekNext()
currentIdx += 1
return next
}
private fun consumeNextInt(): Int {
if (!peekNext().isDigit()) throw PluralConditionParseException(description)
var integerValue = 0
var integerLastIdx = currentIdx
while (integerLastIdx < description.length && description[integerLastIdx].isDigit()) {
integerValue *= 10
integerValue += description[integerLastIdx] - '0'
integerLastIdx += 1
}
currentIdx = integerLastIdx
return integerValue
}
fun nextOperand(): PluralOperand {
consumeWhitespaces()
return when (consumeNext()) {
'n' -> PluralOperand.N
'i' -> PluralOperand.I
'f' -> PluralOperand.F
't' -> PluralOperand.T
'v' -> PluralOperand.V
'w' -> PluralOperand.W
else -> throw PluralConditionParseException(description)
}
}
fun nextModulusDivisor(): Int? {
consumeWhitespaces()
if (peekNext() == '%') {
consumeNext()
consumeWhitespaces()
return consumeNextInt()
}
return null
}
/**
* Returns `true` for `!=`, `false` for `=`.
*/
fun nextComparisonIsNegated(): Boolean {
consumeWhitespaces()
when (peekNext()) {
'!' -> {
consumeNext()
if (consumeNext() != '=') throw PluralConditionParseException(description)
return true
}
'=' -> {
consumeNext()
return false
}
else -> throw PluralConditionParseException(description)
}
}
/**
* Returns `number..number` if the range is actually a value.
*/
fun nextRange(): IntRange {
consumeWhitespaces()
val start = consumeNextInt()
if (peekNextOrNull() != '.') {
return start..start
}
consumeNext()
if (peekNext() != '.') throw PluralConditionParseException(description)
val endInclusive = consumeNextInt()
return start..endInclusive
}
fun nextCommaOrNull(): Char? {
return when (peekNextOrNull()) {
',' -> ','
null -> null
else -> throw PluralConditionParseException(description)
}
}
}
companion object {
/**
* Syntax:
* ```
* relation = operand ('%' value)? ('=' | '!=') range_list
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* range_list = (range | value) (',' range_list)*
* range = value'..'value
* value = digit+
* digit = [0-9]
* ```
*/
fun parse(description: String): Relation {
val parser = Parser(description)
val operand = parser.nextOperand()
val divisor = parser.nextModulusDivisor()
val negated = parser.nextComparisonIsNegated()
val ranges = mutableListOf<IntRange>()
while (true) {
ranges.add(parser.nextRange())
if (parser.nextCommaOrNull() == null) break
}
// ranges is not empty here
return Relation(operand, divisor, negated, ranges.toTypedArray())
}
}
}
companion object {
/**
* Parses [description] as defined in the [Unicode Plural rules syntax](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).
* For compact implementation, samples and keywords for backward compatibility are also not handled. You can
* find such keywords in the [Relations Examples](https://unicode.org/reports/tr35/tr35-numbers.html#Relations_Examples) section.
* ```
* condition = and_condition ('or' and_condition)*
* and_condition = relation ('and' relation)*
* relation = operand ('%' value)? ('=' | '!=') range_list
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* range_list = (range | value) (',' range_list)*
* range = value'..'value
* value = digit+
* digit = [0-9]
* ```
* See [Relation.parse] for the remaining part of the syntax.
*/
fun parse(description: String): PluralCondition {
var condition: PluralCondition? = null
val orConditionDescriptions = OR_PATTERN.split(description.trim())
if (orConditionDescriptions.isNotEmpty() && orConditionDescriptions[0].isNotEmpty()) {
for (orConditionDescription in orConditionDescriptions) {
if (orConditionDescription.isEmpty()) throw PluralConditionParseException(description)
var andCondition: PluralCondition? = null
val andConditionDescriptions = AND_PATTERN.split(orConditionDescription.trim())
for (relationDescription in andConditionDescriptions) {
val relation = Relation.parse(relationDescription)
andCondition = if (andCondition == null)
relation
else
And(andCondition, relation)
}
if (andCondition == null) throw PluralConditionParseException(description)
condition = if (condition == null) andCondition else Or(condition, andCondition)
}
}
return condition ?: NoCondition
}
private val AND_PATTERN = Regex("""\s*and\s*""")
private val OR_PATTERN = Regex("""\s*or\s*""")
}
}
internal object NoCondition : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = true
override fun toString(): String = ""
}

8
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt

@ -0,0 +1,8 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
internal class PluralConditionParseException(description: String) : Exception("Invalid LDML: $description")

41
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt

@ -0,0 +1,41 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
/**
* Plural operands defined in the [Unicode Locale Data Markup Language](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_Operand_Meanings).
*/
internal enum class PluralOperand {
/**
* The absolute value of the source number.
*/
N,
/**
* The integer digits of the source number.
*/
I,
/**
* The number of visible fraction digits in the source number, *with* trailing zeros.
*/
V,
/**
* The number of visible fraction digits in the source number, *without* trailing zeros.
*/
W,
/**
* The visible fraction digits in the source number, *with* trailing zeros, expressed as an integer.
*/
F,
/**
* The visible fraction digits in the source number, *without* trailing zeros, expressed as an integer.
*/
T,
}

14
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt

@ -0,0 +1,14 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
internal class PluralRule(val category: PluralCategory, private val constraint: PluralCondition) {
fun appliesTo(n: Int): Boolean {
return constraint.isFulfilled(n)
}
// add appliesTo(n: Double) or appliesTo(n: Decimal) as needed
}

73
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt

@ -0,0 +1,73 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.LanguageQualifier
import org.jetbrains.compose.resources.RegionQualifier
internal class PluralRuleList private constructor(private val rules: Array<PluralRule>) {
fun getCategory(quantity: Int): PluralCategory {
return rules.first { rule -> rule.appliesTo(quantity) }.category
}
companion object {
private val cacheMutex = Mutex()
private val cache = mutableMapOf<String, Deferred<PluralRuleList>>()
private val emptyList = PluralRuleList(emptyArray())
@OptIn(InternalResourceApi::class)
suspend fun getInstance(
languageQualifier: LanguageQualifier,
regionQualifier: RegionQualifier,
): PluralRuleList {
val cldrLocaleName = buildCldrLocaleName(languageQualifier, regionQualifier) ?: return emptyList
return coroutineScope {
val deferred = cacheMutex.withLock {
cache.getOrPut(cldrLocaleName) {
async(start = CoroutineStart.LAZY) {
createInstance(cldrLocaleName)
}
}
}
deferred.await()
}
}
@OptIn(InternalResourceApi::class)
private fun buildCldrLocaleName(
languageQualifier: LanguageQualifier,
regionQualifier: RegionQualifier,
): String? {
val localeWithRegion = languageQualifier.language + "_" + regionQualifier.region
if (cldrPluralRuleListIndexByLocale.containsKey(localeWithRegion)) {
return localeWithRegion
}
if (cldrPluralRuleListIndexByLocale.containsKey(languageQualifier.language)) {
return languageQualifier.language
}
return null
}
private fun createInstance(cldrLocaleName: String): PluralRuleList {
val cldrPluralRuleListIndex = cldrPluralRuleListIndexByLocale[cldrLocaleName]!!
val cldrPluralRuleList = cldrPluralRuleLists[cldrPluralRuleListIndex]
val pluralRules = cldrPluralRuleList.map {
PluralRule(
it.first,
PluralCondition.parse(it.second),
)
}
return PluralRuleList(pluralRules.toTypedArray())
}
}
}
Loading…
Cancel
Save