Browse Source

Support plural string resource (#4519)

Ports a part of Unicode's ICU in pure Kotlin and implements
Android-style plural string resource support. Fixes
JetBrains/compose-multiplatform#425.

# Changes

- Added `org.jetbrains.compose.resources.intl.{PluralCategory,
PluralRule, PluralRuleList}`, which parses and evaluates scripts in
Unicode's Locale Data Markup Langauge.
- Copied `plurals.xml` from Unicode's
[CLDR](https://github.com/unicode-org/cldr/blob/release-44-1/common/supplemental/plurals.xml).
- Added `GeneratePluralRuleListsTask`, which parses `plurals.xml` and
generates required Kotlin source codes.
- Added `PluralStringResource`, `pluralStringResource`, or
`getPluralString`, corresponding to `StringResource`, `stringResource`,
or `getString`.
- Modified `ResourcesSpec.kt` so the generated `Res` class exposes
`Res.plurals`.

# Potential Further Improvements

- [ ] Allow configuring the default language in the `compose.resources
{}` block (#4482) to determine the default pluralization rule (or just
presume English as default)
- [ ] Move the parser logic to the Gradle plugin and generate
pluralization rules in `Res` only for languages used in
`composeResources`

---------

Co-authored-by: Konstantin Tskhovrebov <konstantin.tskhovrebov@jetbrains.com>
pull/4543/head
Chanjung Kim 8 months ago committed by GitHub
parent
commit
2b1bf65244
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 134
      components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt
  2. 4
      components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml
  3. 26
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt
  4. 257
      components/resources/library/CLDRPluralRules/plurals.xml
  5. 9
      components/resources/library/build.gradle.kts
  6. 139
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  7. 443
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt
  8. 26
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralCategory.kt
  9. 406
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRule.kt
  10. 73
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt
  11. 933
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt
  12. 108
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  13. 143
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/PluralRulesTest.kt
  14. 34
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt
  15. 12
      components/resources/library/src/commonTest/resources/strings.xml
  16. 20
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt
  17. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  18. 26
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt
  19. 2
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt
  20. 26
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt
  21. 2
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  22. 10
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml
  23. 2
      gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt
  24. 2
      gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

134
components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt

@ -0,0 +1,134 @@
/*
* 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.
*/
import groovy.util.Node
import groovy.xml.XmlParser
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.*
/**
* Reads a pluralization rules XML file from Unicode's CLDR and generates a Kotlin file that holds the XML content as
* arrays. This Task is required for quantity string resource support.
*/
@CacheableTask
abstract class GeneratePluralRuleListsTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val pluralsFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:OutputFile
abstract val samplesOutputFile: RegularFileProperty
@TaskAction
fun generatePluralRuleLists() {
val pluralRuleLists = parsePluralRuleLists()
val mainContent = generateMainContent(pluralRuleLists)
outputFile.get().asFile.writeText(mainContent)
val testContent = generateTestContent(pluralRuleLists)
samplesOutputFile.get().asFile.writeText(testContent)
}
private fun parsePluralRuleLists(): List<PluralRuleList> {
val parser = XmlParser(false, false).apply {
setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
}
val supplementalData = parser.parse(pluralsFile.get().asFile)
val pluralRuleLists = supplementalData.children().filterIsInstance<Node>().first { it.name() == "plurals" }
return pluralRuleLists.children().filterIsInstance<Node>().map { pluralRules ->
val locales = pluralRules.attribute("locales").toString().split(' ')
PluralRuleList(
locales,
pluralRules.children().filterIsInstance<Node>().map { pluralRule ->
val rule = pluralRule.text().split('@')
PluralRule(
pluralRule.attribute("count").toString(),
// trim samples as not needed
rule[0].trim(),
rule.firstOrNull { it.startsWith("integer") }?.substringAfter("integer")?.trim() ?: "",
rule.firstOrNull { it.startsWith("decimal") }?.substringAfter("decimal")?.trim() ?: "",
)
}
)
}
}
private fun generateMainContent(pluralRuleLists: List<PluralRuleList>): String {
val pluralRuleListIndexByLocale = pluralRuleLists.flatMapIndexed { idx, pluralRuleList ->
pluralRuleList.locales.map { locale ->
locale to idx
}
}
return """
package org.jetbrains.compose.resources.plural
/**
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists'
* DO NOT EDIT!!!
*/
internal val cldrPluralRuleListIndexByLocale = mapOf(
${pluralRuleListIndexByLocale.joinToString(separator = ",\n ") { (locale, idx) ->
"\"$locale\" to $idx"
}}
)
internal val cldrPluralRuleLists = arrayOf(${pluralRuleLists.joinToString(",") { pluralRuleList ->
"""
arrayOf(
${pluralRuleList.rules.joinToString(",\n ") { rule ->
"PluralCategory.${rule.count.uppercase()} to \"${rule.rule}\""
}}
)"""
}}
)
""".trimIndent()
}
private fun generateTestContent(pluralRuleLists: List<PluralRuleList>): String {
val pluralRuleIntegerSamplesByLocale = pluralRuleLists.flatMap { pluralRuleList ->
pluralRuleList.locales.map { locale ->
locale to pluralRuleList.rules.map { it.count to it.integerSample }
}
}
return """
package org.jetbrains.compose.resources.plural
/**
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists'
* DO NOT EDIT!!!
*/
internal val cldrPluralRuleIntegerSamples = arrayOf(
${pluralRuleIntegerSamplesByLocale.joinToString(",\n ") { (locale, samples) ->
""""$locale" to arrayOf(
${samples.joinToString(",\n ") { (count, sample) ->
"PluralCategory.${count.uppercase()} to \"$sample\""
}}
)"""
}}
)
""".trimIndent()
}
}
private data class PluralRuleList(
val locales: List<String>,
val rules: List<PluralRule>,
)
private data class PluralRule(
val count: String,
val rule: String,
val integerSample: String,
val decimalSample: String,
)

4
components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml

@ -9,4 +9,8 @@ Donec eget turpis ac sem ultricies consequat.</string>
<item>item \u2318</item>
<item>item \u00BD</item>
</string-array>
<plurals name="new_message">
<item quantity="one">%1$d new message</item>
<item quantity="other">%1$d new messages</item>
</plurals>
</resources>

26
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt

@ -4,14 +4,14 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.*
import org.jetbrains.compose.resources.stringArrayResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.*
@Composable
fun StringRes(paddingValues: PaddingValues) {
@ -99,5 +99,25 @@ fun StringRes(paddingValues: PaddingValues) {
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
)
)
var numMessages by remember { mutableStateOf(0) }
OutlinedTextField(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = pluralStringResource(Res.plurals.new_message, numMessages, numMessages),
onValueChange = {},
label = { Text("Text(pluralStringResource(Res.plurals.new_message, $numMessages, $numMessages))") },
leadingIcon = {
Row {
IconButton({ numMessages += 1 }) {
Icon(Icons.Default.Add, contentDescription = "Add Message")
}
}
},
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
disabledLabelColor = MaterialTheme.colorScheme.onSurface,
)
)
}
}

257
components/resources/library/CLDRPluralRules/plurals.xml

@ -0,0 +1,257 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
<!-- copied from https://github.com/unicode-org/cldr/blob/release-44-1/common/supplemental/plurals.xml -->
<!--
Copyright © 1991-2022 Unicode, Inc.
For terms of use, see http://www.unicode.org/copyright.html
SPDX-License-Identifier: Unicode-DFS-2016
CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
-->
<supplementalData>
<version number="$Revision$"/>
<plurals type="cardinal">
<!-- For a canonicalized list, use GeneratedPluralSamples -->
<!-- 1: other -->
<pluralRules locales="bm bo dz hnj id ig ii in ja jbo jv jw kde kea km ko lkt lo ms my nqo osa root sah ses sg su th to tpi vi wo yo yue zh">
<pluralRule count="other"> @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 2: one,other -->
<pluralRules locales="am as bn doi fa gu hi kn pcm zu">
<pluralRule count="one">i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ff hy kab">
<pluralRule count="one">i = 0,1 @integer 0, 1 @decimal 0.0~1.5</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ast de en et fi fy gl ia io ji lij nl sc scn sv sw ur yi">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="si">
<pluralRule count="one">n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ak bho guw ln mg nso pa ti wa">
<pluralRule count="one">n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="tzm">
<pluralRule count="one">n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0</pluralRule>
<pluralRule count="other"> @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="af an asa az bal bem bez bg brx ce cgg chr ckb dv ee el eo eu fo fur gsw ha haw hu jgo jmc ka kaj kcg kk kkj kl ks ksb ku ky lb lg mas mgo ml mn mr nah nb nd ne nn nnh no nr ny nyn om or os pap ps rm rof rwk saq sd sdh seh sn so sq ss ssy st syr ta te teo tig tk tn tr ts ug uz ve vo vun wae xh xog">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="da">
<pluralRule count="one">n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="is">
<pluralRule count="one">t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~0.9, 1.2~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mk">
<pluralRule count="one">v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ceb fil tl">
<pluralRule count="one">v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …</pluralRule>
</pluralRules>
<!-- 3: zero,one,other -->
<pluralRules locales="lv prg">
<pluralRule count="zero">n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="one">n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …</pluralRule>
</pluralRules>
<pluralRules locales="lag">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ksh">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="blo">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="other"> @integer 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~2.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 3: one,two,other -->
<pluralRules locales="he iw">
<pluralRule count="one">i = 1 and v = 0 or i = 0 and v != 0 @integer 1 @decimal 0.0~0.9, 0.00~0.05</pluralRule>
<pluralRule count="two">i = 2 and v = 0 @integer 2</pluralRule>
<pluralRule count="other"> @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.0~2.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="iu naq sat se sma smi smj smn sms">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="other"> @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 3: one,few,other -->
<pluralRules locales="shi">
<pluralRule count="one">i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04</pluralRule>
<pluralRule count="few">n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00</pluralRule>
<pluralRule count="other"> @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mo ro">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 20~35, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="bs hr sh sr">
<pluralRule count="one">v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 3: one,many,other -->
<pluralRules locales="fr">
<pluralRule count="one">i = 0,1 @integer 0, 1 @decimal 0.0~1.5</pluralRule>
<pluralRule count="many">e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …</pluralRule>
</pluralRules>
<pluralRules locales="pt">
<pluralRule count="one">i = 0..1 @integer 0, 1 @decimal 0.0~1.5</pluralRule>
<pluralRule count="many">e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …</pluralRule>
<pluralRule count="other"> @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …</pluralRule>
</pluralRules>
<pluralRules locales="ca it pt_PT vec">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="many">e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …</pluralRule>
</pluralRules>
<pluralRules locales="es">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="many">e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …</pluralRule>
<pluralRule count="other"> @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …</pluralRule>
</pluralRules>
<!-- 4: one,two,few,other -->
<pluralRules locales="gd">
<pluralRule count="one">n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00</pluralRule>
<pluralRule count="other"> @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="sl">
<pluralRule count="one">v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …</pluralRule>
<pluralRule count="two">v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="dsb hsb">
<pluralRule count="one">v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="two">v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 4: one,few,many,other -->
<pluralRules locales="cs sk">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">i = 2..4 and v = 0 @integer 2~4</pluralRule>
<pluralRule count="many">v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
</pluralRules>
<pluralRules locales="pl">
<pluralRule count="one">i = 1 and v = 0 @integer 1</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …</pluralRule>
<pluralRule count="many">v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="other"> @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="be">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="few">n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="many">n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …</pluralRule>
</pluralRules>
<pluralRules locales="lt">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="few">n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="many">f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …</pluralRule>
<pluralRule count="other"> @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ru uk">
<pluralRule count="one">v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …</pluralRule>
<pluralRule count="few">v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …</pluralRule>
<pluralRule count="many">v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="other"> @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<!-- 5: one,two,few,many,other -->
<pluralRules locales="br">
<pluralRule count="one">n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …</pluralRule>
<pluralRule count="two">n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …</pluralRule>
<pluralRule count="few">n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …</pluralRule>
<pluralRule count="many">n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, …</pluralRule>
<pluralRule count="other"> @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="mt">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n = 0 or n % 100 = 3..10 @integer 0, 3~10, 103~109, 1003, … @decimal 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …</pluralRule>
<pluralRule count="many">n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …</pluralRule>
<pluralRule count="other"> @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ga">
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000</pluralRule>
<pluralRule count="many">n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000</pluralRule>
<pluralRule count="other"> @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="gv">
<pluralRule count="one">v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …</pluralRule>
<pluralRule count="two">v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …</pluralRule>
<pluralRule count="few">v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …</pluralRule>
<pluralRule count="many">v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
<pluralRule count="other"> @integer 3~10, 13~19, 23, 103, 1003, …</pluralRule>
</pluralRules>
<!-- 6: zero,one,two,few,many,other -->
<pluralRules locales="kw">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, …</pluralRule>
<pluralRule count="few">n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, …</pluralRule>
<pluralRule count="many">n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, …</pluralRule>
<pluralRule count="other"> @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="ar ars">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …</pluralRule>
<pluralRule count="many">n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …</pluralRule>
<pluralRule count="other"> @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
<pluralRules locales="cy">
<pluralRule count="zero">n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000</pluralRule>
<pluralRule count="one">n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000</pluralRule>
<pluralRule count="two">n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000</pluralRule>
<pluralRule count="few">n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000</pluralRule>
<pluralRule count="many">n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000</pluralRule>
<pluralRule count="other"> @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …</pluralRule>
</pluralRules>
</plurals>
</supplementalData>

9
components/resources/library/build.gradle.kts

@ -1,5 +1,4 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
@ -198,6 +197,14 @@ compose.experimental {
web.application {}
}
//utility task to generate CLDRPluralRuleLists.kt file by 'CLDRPluralRules/plurals.xml'
tasks.register<GeneratePluralRuleListsTask>("generatePluralRuleLists") {
val projectDir = project.layout.projectDirectory
pluralsFile = projectDir.file("CLDRPluralRules/plurals.xml")
outputFile = projectDir.file("src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt")
samplesOutputFile = projectDir.file("src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt")
}
afterEvaluate {
// TODO(o.k.): remove this after we refactor jsAndWasmMain source set in skiko to get rid of broken "common" js-interop
tasks.configureEach {

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

@ -4,11 +4,17 @@ import androidx.compose.runtime.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.plural.PluralRuleList
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList
private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")
private fun String.replaceWithArgs(args: List<String>) = SimpleStringFormatRegex.replace(this) { matchResult ->
args[matchResult.groupValues[1].toInt() - 1]
}
/**
* Represents a string resource in the application.
*
@ -22,8 +28,22 @@ private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")
class StringResource
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>) : Resource(id, items)
/**
* Represents a quantity 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 PluralStringResource
@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 Plurals(val items: Map<PluralCategory, String>) : StringItem
data class Array(val items: List<String>) : StringItem
}
@ -56,6 +76,15 @@ private suspend fun parseStringXml(path: String, resourceReader: ResourceReader)
val rawString = element.textContent.orEmpty()
element.getAttribute("name") to StringItem.Value(handleSpecialCharacters(rawString))
}
val plurals = nodes.getElementsWithName("plurals").associate { pluralElement ->
val items = pluralElement.childNodes.getElementsWithName("item").mapNotNull { element ->
val pluralCategory = PluralCategory.fromString(
element.getAttribute("quantity"),
) ?: return@mapNotNull null
pluralCategory to element.textContent.orEmpty()
}
pluralElement.getAttribute("name") to StringItem.Plurals(items.toMap())
}
val arrays = nodes.getElementsWithName("string-array").associate { arrayElement ->
val items = arrayElement.childNodes.getElementsWithName("item").map { element ->
val rawString = element.textContent.orEmpty()
@ -63,7 +92,7 @@ private suspend fun parseStringXml(path: String, resourceReader: ResourceReader)
}
arrayElement.getAttribute("name") to StringItem.Array(items)
}
return strings + arrays
return strings + plurals + arrays
}
/**
@ -154,9 +183,113 @@ private suspend fun loadString(
environment: ResourceEnvironment
): String {
val str = loadString(resource, resourceReader, environment)
return SimpleStringFormatRegex.replace(str) { matchResult ->
args[matchResult.groupValues[1].toInt() - 1]
return str.replaceWithArgs(args)
}
/**
* Retrieves the string for the pluralization for the given quantity using the specified quantity string resource.
*
* @param resource The quantity string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @return The retrieved string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
@Composable
fun pluralStringResource(resource: PluralStringResource, quantity: Int): String {
val resourceReader = LocalResourceReader.current
val pluralStr by rememberResourceState(resource, quantity, { "" }) { env ->
loadPluralString(resource, quantity, resourceReader, env)
}
return pluralStr
}
/**
* Loads a string using the specified string resource.
*
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(resource: PluralStringResource, quantity: Int): String =
loadPluralString(resource, quantity, DefaultResourceReader, getResourceEnvironment())
@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
private suspend fun loadPluralString(
resource: PluralStringResource,
quantity: Int,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Plurals
?: error("Quantity string ID=`${resource.key}` is not found!")
val pluralRuleList = PluralRuleList.getInstance(
environment.language,
environment.region,
)
val pluralCategory = pluralRuleList.getCategory(quantity)
val str = item.items[pluralCategory]
?: item.items[PluralCategory.OTHER]
?: error("Quantity string ID=`${resource.key}` does not have the pluralization $pluralCategory for quantity $quantity!")
return str
}
/**
* Retrieves the string for the pluralization for the given quantity using the specified quantity string resource.
*
* @param resource The quantity string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @param formatArgs The arguments to be inserted into the formatted string.
* @return The retrieved string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
@Composable
fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String {
val resourceReader = LocalResourceReader.current
val args = formatArgs.map { it.toString() }
val pluralStr by rememberResourceState(resource, quantity, args, { "" }) { env ->
loadPluralString(resource, quantity, args, resourceReader, env)
}
return pluralStr
}
/**
* Loads a string using the specified string resource.
*
* @param resource The string resource to be used.
* @param quantity The quantity of the pluralization to use.
* @param formatArgs The arguments to be inserted into the formatted string.
* @return The loaded string resource.
*
* @throws IllegalArgumentException If the provided ID or the pluralization is not found in the resource file.
*/
@ExperimentalResourceApi
suspend fun getPluralString(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String =
loadPluralString(
resource, quantity,
formatArgs.map { it.toString() },
DefaultResourceReader,
getResourceEnvironment(),
)
@OptIn(ExperimentalResourceApi::class)
private suspend fun loadPluralString(
resource: PluralStringResource,
quantity: Int,
args: List<String>,
resourceReader: ResourceReader,
environment: ResourceEnvironment
): String {
val str = loadPluralString(resource, quantity, resourceReader, environment)
return str.replaceWithArgs(args)
}
/**

443
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt

@ -0,0 +1,443 @@
package org.jetbrains.compose.resources.plural
/**
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists'
* DO NOT EDIT!!!
*/
internal val cldrPluralRuleListIndexByLocale = mapOf(
"bm" to 0,
"bo" to 0,
"dz" to 0,
"hnj" to 0,
"id" to 0,
"ig" to 0,
"ii" to 0,
"in" to 0,
"ja" to 0,
"jbo" to 0,
"jv" to 0,
"jw" to 0,
"kde" to 0,
"kea" to 0,
"km" to 0,
"ko" to 0,
"lkt" to 0,
"lo" to 0,
"ms" to 0,
"my" to 0,
"nqo" to 0,
"osa" to 0,
"root" to 0,
"sah" to 0,
"ses" to 0,
"sg" to 0,
"su" to 0,
"th" to 0,
"to" to 0,
"tpi" to 0,
"vi" to 0,
"wo" to 0,
"yo" to 0,
"yue" to 0,
"zh" to 0,
"am" to 1,
"as" to 1,
"bn" to 1,
"doi" to 1,
"fa" to 1,
"gu" to 1,
"hi" to 1,
"kn" to 1,
"pcm" to 1,
"zu" to 1,
"ff" to 2,
"hy" to 2,
"kab" to 2,
"ast" to 3,
"de" to 3,
"en" to 3,
"et" to 3,
"fi" to 3,
"fy" to 3,
"gl" to 3,
"ia" to 3,
"io" to 3,
"ji" to 3,
"lij" to 3,
"nl" to 3,
"sc" to 3,
"scn" to 3,
"sv" to 3,
"sw" to 3,
"ur" to 3,
"yi" to 3,
"si" to 4,
"ak" to 5,
"bho" to 5,
"guw" to 5,
"ln" to 5,
"mg" to 5,
"nso" to 5,
"pa" to 5,
"ti" to 5,
"wa" to 5,
"tzm" to 6,
"af" to 7,
"an" to 7,
"asa" to 7,
"az" to 7,
"bal" to 7,
"bem" to 7,
"bez" to 7,
"bg" to 7,
"brx" to 7,
"ce" to 7,
"cgg" to 7,
"chr" to 7,
"ckb" to 7,
"dv" to 7,
"ee" to 7,
"el" to 7,
"eo" to 7,
"eu" to 7,
"fo" to 7,
"fur" to 7,
"gsw" to 7,
"ha" to 7,
"haw" to 7,
"hu" to 7,
"jgo" to 7,
"jmc" to 7,
"ka" to 7,
"kaj" to 7,
"kcg" to 7,
"kk" to 7,
"kkj" to 7,
"kl" to 7,
"ks" to 7,
"ksb" to 7,
"ku" to 7,
"ky" to 7,
"lb" to 7,
"lg" to 7,
"mas" to 7,
"mgo" to 7,
"ml" to 7,
"mn" to 7,
"mr" to 7,
"nah" to 7,
"nb" to 7,
"nd" to 7,
"ne" to 7,
"nn" to 7,
"nnh" to 7,
"no" to 7,
"nr" to 7,
"ny" to 7,
"nyn" to 7,
"om" to 7,
"or" to 7,
"os" to 7,
"pap" to 7,
"ps" to 7,
"rm" to 7,
"rof" to 7,
"rwk" to 7,
"saq" to 7,
"sd" to 7,
"sdh" to 7,
"seh" to 7,
"sn" to 7,
"so" to 7,
"sq" to 7,
"ss" to 7,
"ssy" to 7,
"st" to 7,
"syr" to 7,
"ta" to 7,
"te" to 7,
"teo" to 7,
"tig" to 7,
"tk" to 7,
"tn" to 7,
"tr" to 7,
"ts" to 7,
"ug" to 7,
"uz" to 7,
"ve" to 7,
"vo" to 7,
"vun" to 7,
"wae" to 7,
"xh" to 7,
"xog" to 7,
"da" to 8,
"is" to 9,
"mk" to 10,
"ceb" to 11,
"fil" to 11,
"tl" to 11,
"lv" to 12,
"prg" to 12,
"lag" to 13,
"ksh" to 14,
"blo" to 15,
"he" to 16,
"iw" to 16,
"iu" to 17,
"naq" to 17,
"sat" to 17,
"se" to 17,
"sma" to 17,
"smi" to 17,
"smj" to 17,
"smn" to 17,
"sms" to 17,
"shi" to 18,
"mo" to 19,
"ro" to 19,
"bs" to 20,
"hr" to 20,
"sh" to 20,
"sr" to 20,
"fr" to 21,
"pt" to 22,
"ca" to 23,
"it" to 23,
"pt_PT" to 23,
"vec" to 23,
"es" to 24,
"gd" to 25,
"sl" to 26,
"dsb" to 27,
"hsb" to 27,
"cs" to 28,
"sk" to 28,
"pl" to 29,
"be" to 30,
"lt" to 31,
"ru" to 32,
"uk" to 32,
"br" to 33,
"mt" to 34,
"ga" to 35,
"gv" to 36,
"kw" to 37,
"ar" to 38,
"ars" to 38,
"cy" to 39
)
internal val cldrPluralRuleLists = arrayOf(
arrayOf(
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 0 or n = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 0,1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 0,1 or i = 0 and f = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 0..1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 0..1 or n = 11..99",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1 or t != 0 and i = 0,1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19",
PluralCategory.ONE to "n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "i = 0,1 and n != 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0 or i = 0 and v != 0",
PluralCategory.TWO to "i = 2 and v = 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 2",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 0 or n = 1",
PluralCategory.FEW to "n = 2..10",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0",
PluralCategory.FEW to "v != 0 or n = 0 or n != 1 and n % 100 = 1..19",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11",
PluralCategory.FEW to "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 0,1",
PluralCategory.MANY to "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 0..1",
PluralCategory.MANY to "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0",
PluralCategory.MANY to "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1",
PluralCategory.MANY to "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1,11",
PluralCategory.TWO to "n = 2,12",
PluralCategory.FEW to "n = 3..10,13..19",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 100 = 1",
PluralCategory.TWO to "v = 0 and i % 100 = 2",
PluralCategory.FEW to "v = 0 and i % 100 = 3..4 or v != 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 100 = 1 or f % 100 = 1",
PluralCategory.TWO to "v = 0 and i % 100 = 2 or f % 100 = 2",
PluralCategory.FEW to "v = 0 and i % 100 = 3..4 or f % 100 = 3..4",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0",
PluralCategory.FEW to "i = 2..4 and v = 0",
PluralCategory.MANY to "v != 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "i = 1 and v = 0",
PluralCategory.FEW to "v = 0 and i % 10 = 2..4 and i % 100 != 12..14",
PluralCategory.MANY to "v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n % 10 = 1 and n % 100 != 11",
PluralCategory.FEW to "n % 10 = 2..4 and n % 100 != 12..14",
PluralCategory.MANY to "n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n % 10 = 1 and n % 100 != 11..19",
PluralCategory.FEW to "n % 10 = 2..9 and n % 100 != 11..19",
PluralCategory.MANY to "f != 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 10 = 1 and i % 100 != 11",
PluralCategory.FEW to "v = 0 and i % 10 = 2..4 and i % 100 != 12..14",
PluralCategory.MANY to "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n % 10 = 1 and n % 100 != 11,71,91",
PluralCategory.TWO to "n % 10 = 2 and n % 100 != 12,72,92",
PluralCategory.FEW to "n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99",
PluralCategory.MANY to "n != 0 and n % 1000000 = 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 2",
PluralCategory.FEW to "n = 0 or n % 100 = 3..10",
PluralCategory.MANY to "n % 100 = 11..19",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 2",
PluralCategory.FEW to "n = 3..6",
PluralCategory.MANY to "n = 7..10",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ONE to "v = 0 and i % 10 = 1",
PluralCategory.TWO to "v = 0 and i % 10 = 2",
PluralCategory.FEW to "v = 0 and i % 100 = 0,20,40,60,80",
PluralCategory.MANY to "v != 0",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000",
PluralCategory.FEW to "n % 100 = 3,23,43,63,83",
PluralCategory.MANY to "n != 1 and n % 100 = 1,21,41,61,81",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 2",
PluralCategory.FEW to "n % 100 = 3..10",
PluralCategory.MANY to "n % 100 = 11..99",
PluralCategory.OTHER to ""
),
arrayOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 2",
PluralCategory.FEW to "n = 3",
PluralCategory.MANY to "n = 6",
PluralCategory.OTHER to ""
)
)

26
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/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.plural
/**
* 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)
}
}
}
}

406
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRule.kt

@ -0,0 +1,406 @@
/*
* 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.plural
import kotlin.math.absoluteValue
internal class PluralRuleParseException(description: String, position: Int) :
Exception("Invalid syntax at position $position: $description")
internal class PluralRule private constructor(val category: PluralCategory, private val condition: Condition) {
constructor(category: PluralCategory, condition: String) : this(category, Condition.parse(condition))
fun appliesTo(n: Int): Boolean {
return condition.isFulfilled(n)
}
private sealed class Condition {
abstract fun isFulfilled(n: Int): Boolean
abstract fun simplifyForInteger(): Condition
abstract fun equivalentForInteger(other: Condition): Boolean
/**
* Plural operands defined in the [Unicode Locale Data Markup Language](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_Operand_Meanings).
*/
enum class Operand {
/**
* 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,
/**
* Compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
*/
C,
}
class And(
private val left: Condition,
private val right: Condition,
) : Condition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) && right.isFulfilled(n)
override fun simplifyForInteger(): Condition {
val leftSimplified = left.simplifyForInteger()
if (leftSimplified == False) return False
val rightSimplified = right.simplifyForInteger()
when {
leftSimplified == True -> return rightSimplified
rightSimplified == False -> return False
rightSimplified == True -> return leftSimplified
}
if (leftSimplified.equivalentForInteger(rightSimplified)) return leftSimplified
return And(leftSimplified, rightSimplified)
}
override fun equivalentForInteger(other: Condition): Boolean {
if (this === other) return true
if (other !is And) return false
return left.equivalentForInteger(other.left) && right.equivalentForInteger(other.right)
}
override fun toString(): String = "$left and $right"
}
class Or(
private val left: Condition,
private val right: Condition,
) : Condition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) || right.isFulfilled(n)
override fun simplifyForInteger(): Condition {
val leftSimplified = left.simplifyForInteger()
if (leftSimplified == True) return True
val rightSimplified = right.simplifyForInteger()
when {
leftSimplified == False -> return rightSimplified
rightSimplified == True -> return True
rightSimplified == False -> return leftSimplified
}
if (leftSimplified.equivalentForInteger(rightSimplified)) return leftSimplified
return Or(leftSimplified, rightSimplified)
}
override fun equivalentForInteger(other: Condition): Boolean {
if (this === other) return true
if (other !is Or) return false
return left.equivalentForInteger(other.left) && right.equivalentForInteger(other.right)
}
override fun toString(): String = "$left or $right"
}
class Relation(
private val operand: Operand,
private val operandDivisor: Int?,
private val comparisonIsNegated: Boolean,
private val ranges: Array<IntRange>,
) : Condition() {
override fun isFulfilled(n: Int): Boolean {
val expressionOperandValue = when (operand) {
Operand.N, Operand.I -> n.absoluteValue
else -> 0
}
val moduloAppliedValue = if (operandDivisor != null) {
expressionOperandValue % operandDivisor
} else {
expressionOperandValue
}
return ranges.any { moduloAppliedValue in it } != comparisonIsNegated
}
override fun simplifyForInteger(): Condition {
return when (operand) {
Operand.N, Operand.I -> Relation(
Operand.N,
operandDivisor,
comparisonIsNegated,
ranges,
)
else -> if (ranges.any { 0 in it } != comparisonIsNegated) True else False
}
}
override fun equivalentForInteger(other: Condition): Boolean {
if (this === other) return true
if (other !is Relation) return false
if ((operand == Operand.N || operand == Operand.I) != (other.operand == Operand.N || other.operand == Operand.I)) return false
if (operandDivisor != other.operandDivisor) return false
if (comparisonIsNegated != other.comparisonIsNegated) return false
if (!ranges.contentEquals(other.ranges)) return false
return true
}
override fun toString(): String {
return StringBuilder().run {
append(operand.name.lowercase())
if (operandDivisor != null) {
append(" % ")
append(operandDivisor)
}
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 object True : Condition() {
override fun isFulfilled(n: Int) = true
override fun simplifyForInteger() = this
override fun equivalentForInteger(other: Condition) = this == other
override fun toString(): String = ""
}
private object False : Condition() {
override fun isFulfilled(n: Int) = false
override fun simplifyForInteger() = this
override fun equivalentForInteger(other: Condition) = this == other
override fun toString(): String = "(false)"
}
private class Parser(private val description: String) {
private var currentIdx = 0
private fun eof() = currentIdx >= description.length
private fun nextUnchecked() = description[currentIdx]
private fun consumeWhitespaces() {
while (!eof() && nextUnchecked().isWhitespace()) {
currentIdx += 1
}
}
private fun raise(): Nothing = throw PluralRuleParseException(description, currentIdx + 1)
private fun assert(condition: Boolean) {
if (!condition) raise()
}
private fun peekNextOrNull() = description.getOrNull(currentIdx)
private fun peekNext() = peekNextOrNull() ?: raise()
private fun consumeNext(): Char {
val next = peekNext()
currentIdx += 1
return next
}
private fun consumeNextInt(): Int {
assert(peekNext().isDigit())
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 parse(): Condition {
consumeWhitespaces()
if (eof()) return True
val condition = nextCondition()
consumeWhitespaces()
assert(eof())
return condition
}
/**
* Syntax:
* ```
* condition = and_condition ('or' and_condition)*
* ```
*/
private fun nextCondition(): Condition {
var condition: Condition = nextAndCondition()
while (true) {
consumeWhitespaces()
if (peekNextOrNull() != 'o') break
consumeNext()
assert(consumeNext() == 'r')
condition = Or(condition, nextAndCondition())
}
return condition
}
/**
* Syntax:
* ```
* and_condition = relation ('and' relation)*
* ```
*/
private fun nextAndCondition(): Condition {
var condition: Condition = nextRelation()
while (true) {
consumeWhitespaces()
if (peekNextOrNull() != 'a') break
consumeNext()
assert(consumeNext() == 'n')
assert(consumeNext() == 'd')
condition = And(condition, nextRelation())
}
return condition
}
/**
* Syntax:
* ```
* relation = operand ('%' value)? ('=' | '!=') range_list
* ```
*/
fun nextRelation(): Relation {
val operand = nextOperand()
val divisor = nextModulusDivisor()
val negated = nextComparisonIsNegated()
val ranges = mutableListOf(nextRange())
while (peekNextOrNull() == ',') {
consumeNext()
ranges.add(nextRange())
}
// ranges is not empty here
return Relation(operand, divisor, negated, ranges.toTypedArray())
}
/**
* Syntax:
* ```
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* ```
*/
fun nextOperand(): Operand {
consumeWhitespaces()
return when (consumeNext()) {
'n' -> Operand.N
'i' -> Operand.I
'f' -> Operand.F
't' -> Operand.T
'v' -> Operand.V
'w' -> Operand.W
'c', 'e' -> Operand.C
else -> raise()
}
}
fun nextModulusDivisor(): Int? {
consumeWhitespaces()
if (peekNext() == '%') {
consumeNext()
consumeWhitespaces()
return consumeNextInt()
}
return null
}
/**
* Returns `true` for `!=`, `false` for `=`.
*/
fun nextComparisonIsNegated(): Boolean {
consumeWhitespaces()
when (peekNext()) {
'!' -> {
consumeNext()
assert(consumeNext() == '=')
return true
}
'=' -> {
consumeNext()
return false
}
else -> raise()
}
}
/**
* Returns `number..number` if the range is actually a value.
*/
fun nextRange(): IntRange {
consumeWhitespaces()
val start = consumeNextInt()
if (peekNextOrNull() != '.') {
return start..start
}
consumeNext()
assert(consumeNext() == '.')
val endInclusive = consumeNextInt()
return start..endInclusive
}
}
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]
* ```
*/
fun parse(description: String): Condition = Parser(description).parse().simplifyForInteger()
}
}
}

73
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/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.plural
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 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 = Array<Deferred<PluralRuleList>?>(cldrPluralRuleLists.size) { null }
private val emptyList = PluralRuleList(emptyArray())
@OptIn(InternalResourceApi::class)
suspend fun getInstance(
languageQualifier: LanguageQualifier,
regionQualifier: RegionQualifier,
): PluralRuleList {
val cldrLocaleName = buildCldrLocaleName(languageQualifier, regionQualifier) ?: return emptyList
return getInstance(cldrLocaleName)
}
suspend fun getInstance(cldrLocaleName: String): PluralRuleList {
val listIndex = cldrPluralRuleListIndexByLocale[cldrLocaleName]!!
return coroutineScope {
val deferred = cacheMutex.withLock {
if (cache[listIndex] == null) {
cache[listIndex] = async(start = CoroutineStart.LAZY) {
createInstance(listIndex)
}
}
cache[listIndex]!!
}
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(cldrPluralRuleListIndex: Int): PluralRuleList {
val cldrPluralRuleList = cldrPluralRuleLists[cldrPluralRuleListIndex]
val pluralRules = cldrPluralRuleList.map { PluralRule(it.first, it.second) }
return PluralRuleList(pluralRules.toTypedArray())
}
}
}

933
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt

@ -0,0 +1,933 @@
package org.jetbrains.compose.resources.plural
/**
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists'
* DO NOT EDIT!!!
*/
internal val cldrPluralRuleIntegerSamples = arrayOf(
"bm" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"bo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"dz" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"hnj" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"id" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ig" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ii" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"in" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ja" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"jbo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"jv" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"jw" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"kde" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"kea" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"km" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ko" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"lkt" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"lo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ms" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"my" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"nqo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"osa" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"root" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"sah" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"ses" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"sg" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"su" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"th" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"to" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"tpi" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"vi" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"wo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"yo" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"yue" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"zh" to arrayOf(
PluralCategory.OTHER to "0~15, 100, 1000, 10000, 100000, 1000000, …"
),
"am" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"as" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"bn" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"doi" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"fa" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"gu" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"hi" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"kn" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"pcm" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"zu" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ff" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"hy" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"kab" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ast" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"de" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"en" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"et" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"fi" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"fy" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"gl" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ia" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"io" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ji" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"lij" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nl" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sc" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"scn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sv" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sw" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ur" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"yi" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"si" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ak" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"bho" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"guw" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ln" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"mg" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"nso" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"pa" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ti" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"wa" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"tzm" to arrayOf(
PluralCategory.ONE to "0, 1, 11~24",
PluralCategory.OTHER to "2~10, 100~106, 1000, 10000, 100000, 1000000, …"
),
"af" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"an" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"asa" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"az" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"bal" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"bem" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"bez" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"bg" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"brx" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ce" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"cgg" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"chr" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ckb" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"dv" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ee" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"el" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"eo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"eu" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"fo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"fur" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"gsw" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ha" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"haw" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"hu" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"jgo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"jmc" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ka" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"kaj" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"kcg" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"kk" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"kkj" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"kl" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ks" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ksb" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ku" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ky" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"lb" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"lg" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"mas" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"mgo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ml" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"mn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"mr" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nah" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nb" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nd" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ne" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nnh" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"no" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nr" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ny" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"nyn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"om" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"or" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"os" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"pap" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ps" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"rm" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"rof" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"rwk" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"saq" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sd" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sdh" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"seh" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"so" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"sq" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ss" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ssy" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"st" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"syr" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ta" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"te" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"teo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"tig" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"tk" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"tn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"tr" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ts" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ug" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"uz" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ve" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"vo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"vun" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"wae" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"xh" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"xog" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"da" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"is" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"mk" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"ceb" to arrayOf(
PluralCategory.ONE to "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to "4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, …"
),
"fil" to arrayOf(
PluralCategory.ONE to "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to "4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, …"
),
"tl" to arrayOf(
PluralCategory.ONE to "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to "4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, …"
),
"lv" to arrayOf(
PluralCategory.ZERO to "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.OTHER to "2~9, 22~29, 102, 1002, …"
),
"prg" to arrayOf(
PluralCategory.ZERO to "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.OTHER to "2~9, 22~29, 102, 1002, …"
),
"lag" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"ksh" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1000000, …"
),
"blo" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.OTHER to "2~16, 100, 1000, 10000, 100000, 1000000, …"
),
"he" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"iw" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"iu" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"naq" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"sat" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"se" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"sma" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"smi" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"smj" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"smn" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"sms" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.OTHER to "0, 3~17, 100, 1000, 10000, 100000, 1000000, …"
),
"shi" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.FEW to "2~10",
PluralCategory.OTHER to "11~26, 100, 1000, 10000, 100000, 1000000, …"
),
"mo" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.FEW to "0, 2~16, 101, 1001, …",
PluralCategory.OTHER to "20~35, 100, 1000, 10000, 100000, 1000000, …"
),
"ro" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.FEW to "0, 2~16, 101, 1001, …",
PluralCategory.OTHER to "20~35, 100, 1000, 10000, 100000, 1000000, …"
),
"bs" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"hr" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"sh" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"sr" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"fr" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"pt" to arrayOf(
PluralCategory.ONE to "0, 1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"ca" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"it" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"pt_PT" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"vec" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"es" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.MANY to "1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …",
PluralCategory.OTHER to "0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, …"
),
"gd" to arrayOf(
PluralCategory.ONE to "1, 11",
PluralCategory.TWO to "2, 12",
PluralCategory.FEW to "3~10, 13~19",
PluralCategory.OTHER to "0, 20~34, 100, 1000, 10000, 100000, 1000000, …"
),
"sl" to arrayOf(
PluralCategory.ONE to "1, 101, 201, 301, 401, 501, 601, 701, 1001, …",
PluralCategory.TWO to "2, 102, 202, 302, 402, 502, 602, 702, 1002, …",
PluralCategory.FEW to "3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"dsb" to arrayOf(
PluralCategory.ONE to "1, 101, 201, 301, 401, 501, 601, 701, 1001, …",
PluralCategory.TWO to "2, 102, 202, 302, 402, 502, 602, 702, 1002, …",
PluralCategory.FEW to "3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"hsb" to arrayOf(
PluralCategory.ONE to "1, 101, 201, 301, 401, 501, 601, 701, 1001, …",
PluralCategory.TWO to "2, 102, 202, 302, 402, 502, 602, 702, 1002, …",
PluralCategory.FEW to "3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, …",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"cs" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.FEW to "2~4",
PluralCategory.MANY to "",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"sk" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.FEW to "2~4",
PluralCategory.MANY to "",
PluralCategory.OTHER to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
),
"pl" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.MANY to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to ""
),
"be" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.MANY to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to ""
),
"lt" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~9, 22~29, 102, 1002, …",
PluralCategory.MANY to "",
PluralCategory.OTHER to "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, …"
),
"ru" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.MANY to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to ""
),
"uk" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
PluralCategory.FEW to "2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
PluralCategory.MANY to "0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
PluralCategory.OTHER to ""
),
"br" to arrayOf(
PluralCategory.ONE to "1, 21, 31, 41, 51, 61, 81, 101, 1001, …",
PluralCategory.TWO to "2, 22, 32, 42, 52, 62, 82, 102, 1002, …",
PluralCategory.FEW to "3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, …",
PluralCategory.MANY to "1000000, …",
PluralCategory.OTHER to "0, 5~8, 10~20, 100, 1000, 10000, 100000, …"
),
"mt" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.FEW to "0, 3~10, 103~109, 1003, …",
PluralCategory.MANY to "11~19, 111~117, 1011, …",
PluralCategory.OTHER to "20~35, 100, 1000, 10000, 100000, 1000000, …"
),
"ga" to arrayOf(
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.FEW to "3~6",
PluralCategory.MANY to "7~10",
PluralCategory.OTHER to "0, 11~25, 100, 1000, 10000, 100000, 1000000, …"
),
"gv" to arrayOf(
PluralCategory.ONE to "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …",
PluralCategory.TWO to "2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …",
PluralCategory.FEW to "0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …",
PluralCategory.MANY to "",
PluralCategory.OTHER to "3~10, 13~19, 23, 103, 1003, …"
),
"kw" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.TWO to "2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, …",
PluralCategory.FEW to "3, 23, 43, 63, 83, 103, 123, 143, 1003, …",
PluralCategory.MANY to "21, 41, 61, 81, 101, 121, 141, 161, 1001, …",
PluralCategory.OTHER to "4~19, 100, 1004, 1000000, …"
),
"ar" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.FEW to "3~10, 103~110, 1003, …",
PluralCategory.MANY to "11~26, 111, 1011, …",
PluralCategory.OTHER to "100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, …"
),
"ars" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.FEW to "3~10, 103~110, 1003, …",
PluralCategory.MANY to "11~26, 111, 1011, …",
PluralCategory.OTHER to "100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, …"
),
"cy" to arrayOf(
PluralCategory.ZERO to "0",
PluralCategory.ONE to "1",
PluralCategory.TWO to "2",
PluralCategory.FEW to "3",
PluralCategory.MANY to "6",
PluralCategory.OTHER to "4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, …"
)
)

108
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt

@ -144,6 +144,102 @@ class ComposeResourceTest {
assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
}
@Test
fun testPluralStringResourceCache() = runComposeUiTest {
val testResourceReader = TestResourceReader()
var res by mutableStateOf(TestPluralStringResource("plurals"))
var quantity by mutableStateOf(0)
var str = ""
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
str = pluralStringResource(res, quantity)
}
}
waitForIdle()
assertEquals("other", str)
quantity = 1
waitForIdle()
assertEquals("one", str)
assertEquals(1, quantity)
quantity = 2
waitForIdle()
assertEquals("other", str)
assertEquals(2, quantity)
quantity = 3
waitForIdle()
assertEquals("other", str)
assertEquals(3, quantity)
res = TestPluralStringResource("another_plurals")
quantity = 0
waitForIdle()
assertEquals("another other", str)
quantity = 1
waitForIdle()
assertEquals("another one", str)
}
@Test
fun testReadPluralStringResource() = runComposeUiTest {
var plurals = ""
var another_plurals = ""
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
plurals = pluralStringResource(TestPluralStringResource("plurals"), 1)
another_plurals = pluralStringResource(TestPluralStringResource("another_plurals"), 1)
}
}
waitForIdle()
assertEquals("one", plurals)
assertEquals("another one", another_plurals)
}
@Test
fun testReadQualityStringFromDifferentArgs() = runComposeUiTest {
// we're putting different integers to arguments and the quantity
var quantity by mutableStateOf(0)
var arg by mutableStateOf("me")
var str1 = ""
var str2 = ""
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
str1 = pluralStringResource(TestPluralStringResource("messages"), quantity, 3, arg)
str2 = pluralStringResource(TestPluralStringResource("messages"), quantity, 5, arg)
}
}
waitForIdle()
assertEquals("3 messages for me", str1)
assertEquals("5 messages for me", str2)
arg = "you"
waitForIdle()
assertEquals("3 messages for you", str1)
assertEquals("5 messages for you", str2)
quantity = 1
waitForIdle()
assertEquals("3 message for you", str1)
assertEquals("5 message for you", str2)
}
@Test
fun testLoadPluralStringResource() = runTest {
assertEquals("one", getPluralString(TestPluralStringResource("plurals"), 1))
assertEquals("other", getPluralString(TestPluralStringResource("plurals"), 5))
assertEquals("another one", getPluralString(TestPluralStringResource("another_plurals"), 1))
assertEquals("another other", getPluralString(TestPluralStringResource("another_plurals"), 5))
}
@Test
fun testMissingResource() = runTest {
assertFailsWith<MissingResourceException> {
@ -170,6 +266,18 @@ class ComposeResourceTest {
<item>item 2</item>
<item>item 3</item>
</string-array>
<plurals name="plurals">
<item quantity="one">one</item>
<item quantity="other">other</item>
</plurals>
<plurals name="another_plurals">
<item quantity="one">another one</item>
<item quantity="other">another other</item>
</plurals>
<plurals name="messages">
<item quantity="one">%1${'$'}d message for %2${'$'}s</item>
<item quantity="other">%1${'$'}d messages for %2${'$'}s</item>
</plurals>
</resources>
""".trimIndent(),

143
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/PluralRulesTest.kt

@ -0,0 +1,143 @@
/*
* 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
import kotlinx.coroutines.test.runTest
import org.jetbrains.compose.resources.plural.*
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.plural.PluralRuleList
import kotlin.test.*
/**
* Tests Unicode CLDR pluralization rules.
*/
class PluralRulesTest {
/**
* Tests the actual language pluralization rules with the integer samples given by Unicode.
*/
@Test
fun testIntegerSamples() = runTest {
for ((locale, samplesByCategory) in cldrPluralRuleIntegerSamples) {
val pluralRuleList = PluralRuleList.getInstance(locale)
for ((category, samples) in samplesByCategory) {
for (sample in parsePluralSamples(samples)) {
assertEquals(category, pluralRuleList.getCategory(sample))
}
}
}
}
@Test
fun testOrCondition() {
val pluralRuleList = pluralRuleListOf(
PluralCategory.ONE to "n = 15 or n = 24"
)
repeat(30) {
if (it == 15 || it == 24) {
assertEquals(PluralCategory.ONE, pluralRuleList.getCategory(it))
} else {
assertEquals(PluralCategory.OTHER, pluralRuleList.getCategory(it))
}
}
}
@Test
fun testAndCondition() {
val pluralRuleList = pluralRuleListOf(
PluralCategory.ONE to "n = 15 and n = 24"
)
repeat(30) {
assertEquals(PluralCategory.OTHER, pluralRuleList.getCategory(it))
}
}
@Test
fun testModulus() {
val pluralRuleList = pluralRuleListOf(
PluralCategory.ONE to "n % 3 = 2"
)
repeat(30) {
if (it % 3 == 2) {
assertEquals(PluralCategory.ONE, pluralRuleList.getCategory(it))
} else {
assertEquals(PluralCategory.OTHER, pluralRuleList.getCategory(it))
}
}
}
@Test
fun testRange() {
val pluralRuleList = pluralRuleListOf(
PluralCategory.ONE to "n = 2..3,5,10..24"
)
repeat(30) {
if (it in 2..3 || it == 5 || it in 10..24) {
assertEquals(PluralCategory.ONE, pluralRuleList.getCategory(it))
} else {
assertEquals(PluralCategory.OTHER, pluralRuleList.getCategory(it))
}
}
}
@Test
fun testMultipleRules() {
val pluralRuleList = pluralRuleListOf(
PluralCategory.ZERO to "n = 0",
PluralCategory.ONE to "n = 1",
PluralCategory.TWO to "n = 20",
PluralCategory.FEW to "n = 300",
PluralCategory.MANY to "n = 400",
)
repeat(500) {
val expected = when (it) {
0 -> PluralCategory.ZERO
1 -> PluralCategory.ONE
20 -> PluralCategory.TWO
300 -> PluralCategory.FEW
400 -> PluralCategory.MANY
else -> PluralCategory.OTHER
}
assertEquals(expected, pluralRuleList.getCategory(it))
}
}
@Test
fun testOperandValues() {
pluralRuleListOf(
PluralCategory.ONE to "n = 1"
).run {
assertEquals(PluralCategory.OTHER, getCategory(-3))
assertEquals(PluralCategory.OTHER, getCategory(-2))
assertEquals(PluralCategory.ONE, getCategory(-1))
assertEquals(PluralCategory.OTHER, getCategory(0))
assertEquals(PluralCategory.ONE, getCategory(1))
assertEquals(PluralCategory.OTHER, getCategory(2))
assertEquals(PluralCategory.OTHER, getCategory(3))
}
pluralRuleListOf(
PluralCategory.ONE to "i = 1"
).run {
assertEquals(PluralCategory.OTHER, getCategory(-3))
assertEquals(PluralCategory.OTHER, getCategory(-2))
assertEquals(PluralCategory.ONE, getCategory(-1))
assertEquals(PluralCategory.OTHER, getCategory(0))
assertEquals(PluralCategory.ONE, getCategory(1))
assertEquals(PluralCategory.OTHER, getCategory(2))
assertEquals(PluralCategory.OTHER, getCategory(3))
}
for (condition in arrayOf("v = 0", "w = 0", "f = 0", "t = 0", "e = 0")) {
pluralRuleListOf(
PluralCategory.ONE to condition
).run {
for (idx in -100..100) {
assertEquals(PluralCategory.ONE, getCategory(idx))
}
}
}
}
}

34
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt

@ -1,8 +1,42 @@
package org.jetbrains.compose.resources
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.plural.PluralRule
import org.jetbrains.compose.resources.plural.PluralRuleList
@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
internal fun TestStringResource(key: String) = StringResource(
"STRING:$key",
key,
setOf(ResourceItem(emptySet(), "strings.xml"))
)
@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
internal fun TestPluralStringResource(key: String) = PluralStringResource(
"PLURALS:$key",
key,
setOf(ResourceItem(emptySet(), "strings.xml"))
)
internal fun parsePluralSamples(samples: String): List<Int> {
return samples.split(',').flatMap {
val range = it.trim()
when {
range.isEmpty() -> emptyList()
range in arrayOf("", "...") -> emptyList()
// ignore numbers in compact exponent format
range.contains('c') || range.contains('e') -> emptyList()
range.contains('~') -> {
val (start, endInclusive) = range.split('~')
return@flatMap (start.toInt()..endInclusive.toInt()).toList()
}
else -> listOf(range.toInt())
}
}
}
internal fun pluralRuleListOf(vararg rules: Pair<PluralCategory, String>): PluralRuleList {
val pluralRules = rules.map { PluralRule(it.first, it.second) } + PluralRule(PluralCategory.OTHER, "")
return PluralRuleList(pluralRules.toTypedArray())
}

12
components/resources/library/src/commonTest/resources/strings.xml

@ -8,4 +8,16 @@
<item>item 2</item>
<item>item 3</item>
</string-array>
<plurals name="plurals">
<item quantity="one">one</item>
<item quantity="other">other</item>
</plurals>
<plurals name="another_plurals">
<item quantity="one">another one</item>
<item quantity="other">another other</item>
</plurals>
<plurals name="messages">
<item quantity="one">%1$d message for %2$s</item>
<item quantity="other">%1$d messages for %2$s</item>
</plurals>
</resources>

20
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt

@ -101,9 +101,13 @@ internal abstract class GenerateResClassTask : DefaultTask() {
}
if (typeString == "values" && file.name.equals("strings.xml", true)) {
val stringIds = getStringIds(file)
return stringIds.map { strId ->
ResourceItem(ResourceType.STRING, qualifiers, strId.asUnderscoredIdentifier(), path)
return getStringResources(file).mapNotNull { (typeName, strId) ->
val type = when(typeName) {
"string", "string-array" -> ResourceType.STRING
"plurals" -> ResourceType.PLURAL_STRING
else -> return@mapNotNull null
}
ResourceItem(type, qualifiers, strId.asUnderscoredIdentifier(), path)
}
}
@ -111,14 +115,14 @@ internal abstract class GenerateResClassTask : DefaultTask() {
return listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path))
}
private val stringTypeNames = listOf("string", "string-array")
private fun getStringIds(stringsXml: File): Set<String> {
//type -> id
private val stringTypeNames = listOf("string", "string-array", "plurals")
private fun getStringResources(stringsXml: File): List<Pair<String, String>> {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsXml)
val items = doc.getElementsByTagName("resources").item(0).childNodes
val ids = List(items.length) { items.item(it) }
return List(items.length) { items.item(it) }
.filter { it.nodeName in stringTypeNames }
.map { it.attributes.getNamedItem("name").nodeValue }
return ids.toSet()
.map { it.nodeName to it.attributes.getNamedItem("name").nodeValue }
}
private fun File.listNotHiddenFiles(): List<File> =

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

@ -9,6 +9,7 @@ import kotlin.io.path.invariantSeparatorsPathString
internal enum class ResourceType(val typeName: String) {
DRAWABLE("drawable"),
STRING("string"),
PLURAL_STRING("plurals"),
FONT("font");
override fun toString(): String = typeName
@ -31,6 +32,7 @@ internal data class ResourceItem(
private fun ResourceType.getClassName(): ClassName = when (this) {
ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource")
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
ResourceType.PLURAL_STRING -> ClassName("org.jetbrains.compose.resources", "PluralStringResource")
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource")
}
@ -225,7 +227,7 @@ private fun getChunkFileSpec(
CodeBlock.builder()
.add("return %T(\n", type.getClassName()).withIndent {
add("\"${type}:${resName}\",")
if (type == ResourceType.STRING) add(" \"$resName\",")
if (type == ResourceType.STRING || type == ResourceType.PLURAL_STRING) add(" \"$resName\",")
withIndent {
add("\nsetOf(\n").withIndent {
items.forEach { item ->

26
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt

@ -0,0 +1,26 @@
@file:OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
package my.lib.res
import kotlin.OptIn
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.PluralStringResource
@ExperimentalResourceApi
private object Plurals0 {
public val numberOfSongsAvailable: PluralStringResource by
lazy { init_numberOfSongsAvailable() }
}
@ExperimentalResourceApi
public val Res.plurals.numberOfSongsAvailable: PluralStringResource
get() = Plurals0.numberOfSongsAvailable
@ExperimentalResourceApi
private fun init_numberOfSongsAvailable(): PluralStringResource =
org.jetbrains.compose.resources.PluralStringResource(
"plurals:numberOfSongsAvailable", "numberOfSongsAvailable",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)

2
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt

@ -27,5 +27,7 @@ public object Res {
public object string
public object plurals
public object font
}

26
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt

@ -0,0 +1,26 @@
@file:OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
package app.group.resources_test.generated.resources
import kotlin.OptIn
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.PluralStringResource
@ExperimentalResourceApi
private object Plurals0 {
public val numberOfSongsAvailable: PluralStringResource by
lazy { init_numberOfSongsAvailable() }
}
@ExperimentalResourceApi
internal val Res.plurals.numberOfSongsAvailable: PluralStringResource
get() = Plurals0.numberOfSongsAvailable
@ExperimentalResourceApi
private fun init_numberOfSongsAvailable(): PluralStringResource =
org.jetbrains.compose.resources.PluralStringResource(
"plurals:numberOfSongsAvailable", "numberOfSongsAvailable",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)

2
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt

@ -27,5 +27,7 @@ internal object Res {
public object string
public object plurals
public object font
}

10
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml

@ -13,4 +13,14 @@
<string name="PascalCase">PascalCase</string>
<string name="1-kebab-case">1-kebab-case</string>
<string name="camelCase">camelCase</string>
<plurals name="numberOfSongsAvailable">
<item quantity="zero">%d zero</item>
<item quantity="one">%d one</item>
<item quantity="two">%d two</item>
<item quantity="few">%d few</item>
<item quantity="many">%d many</item>
<item quantity="other">%d other</item>
</plurals>
</resources>

2
gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt

@ -27,5 +27,7 @@ internal object Res {
public object string
public object plurals
public object font
}

2
gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

@ -27,5 +27,7 @@ internal object Res {
public object string
public object plurals
public object font
}

Loading…
Cancel
Save