From 2b1bf65244ad1178cd1f0b7ec9c3531cdf06619b Mon Sep 17 00:00:00 2001 From: Chanjung Kim Date: Tue, 26 Mar 2024 00:59:05 +0900 Subject: [PATCH] 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 --- .../kotlin/GeneratePluralRuleListsTask.kt | 134 +++ .../composeResources/values/strings.xml | 4 + .../resources/demo/shared/StringRes.kt | 26 +- .../library/CLDRPluralRules/plurals.xml | 257 +++++ components/resources/library/build.gradle.kts | 9 +- .../compose/resources/StringResources.kt | 141 ++- .../resources/plural/CLDRPluralRuleLists.kt | 443 +++++++++ .../resources/plural/PluralCategory.kt | 26 + .../compose/resources/plural/PluralRule.kt | 406 ++++++++ .../resources/plural/PluralRuleList.kt | 73 ++ .../resources/CLDRPluralRuleLists.test.kt | 933 ++++++++++++++++++ .../compose/resources/ComposeResourceTest.kt | 108 ++ .../compose/resources/PluralRulesTest.kt | 143 +++ .../jetbrains/compose/resources/TestUtils.kt | 36 +- .../src/commonTest/resources/strings.xml | 12 + .../compose/resources/GenerateResClassTask.kt | 20 +- .../compose/resources/ResourcesSpec.kt | 4 +- .../expected-open-res/Plurals0.kt | 26 + .../commonResources/expected-open-res/Res.kt | 2 + .../misc/commonResources/expected/Plurals0.kt | 26 + .../misc/commonResources/expected/Res.kt | 2 + .../composeResources/values/strings.xml | 10 + .../misc/emptyResources/expected/Res.kt | 2 + .../misc/jvmOnlyResources/expected/Res.kt | 2 + 24 files changed, 2827 insertions(+), 18 deletions(-) create mode 100644 components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt create mode 100644 components/resources/library/CLDRPluralRules/plurals.xml create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralCategory.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRule.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt create mode 100644 components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt create mode 100644 components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/PluralRulesTest.kt create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt create mode 100644 gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt diff --git a/components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt b/components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt new file mode 100644 index 0000000000..bbe472e471 --- /dev/null +++ b/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 { + 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().first { it.name() == "plurals" } + + return pluralRuleLists.children().filterIsInstance().map { pluralRules -> + val locales = pluralRules.attribute("locales").toString().split(' ') + PluralRuleList( + locales, + pluralRules.children().filterIsInstance().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): 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): 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, + val rules: List, +) + +private data class PluralRule( + val count: String, + val rule: String, + val integerSample: String, + val decimalSample: String, +) \ No newline at end of file diff --git a/components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml b/components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml index 2fa6bad2bc..1ebddb26d3 100644 --- a/components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml +++ b/components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml @@ -9,4 +9,8 @@ Donec eget turpis ac sem ultricies consequat. item \u2318 item \u00BD + + %1$d new message + %1$d new messages + diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt index b5b983b28b..5ae0091647 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt +++ b/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, + ) + ) } } \ No newline at end of file diff --git a/components/resources/library/CLDRPluralRules/plurals.xml b/components/resources/library/CLDRPluralRules/plurals.xml new file mode 100644 index 0000000000..bb2cef2584 --- /dev/null +++ b/components/resources/library/CLDRPluralRules/plurals.xml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + @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, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + @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, … + + + i = 0,1 @integer 0, 1 @decimal 0.0~1.5 + @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, … + + + i = 1 and v = 0 @integer 1 + @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, … + + + 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 + @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, … + + + n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000 + @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, … + + + 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 + @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, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @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, … + + + n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6 + @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, … + + + 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, … + @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, … + + + 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, … + @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, … + + + 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, … + @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, … + + + + + + 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, … + 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, … + @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6 + @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, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @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, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + @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, … + + + + + + i = 1 and v = 0 or i = 0 and v != 0 @integer 1 @decimal 0.0~0.9, 0.00~0.05 + i = 2 and v = 0 @integer 2 + @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, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + @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, … + + + + + + i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04 + 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 + @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, … + + + i = 1 and v = 0 @integer 1 + 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, … + @integer 20~35, 100, 1000, 10000, 100000, 1000000, … + + + 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, … + 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, … + @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, … + + + + + + i = 0,1 @integer 0, 1 @decimal 0.0~1.5 + 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, … + @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, … + + + i = 0..1 @integer 0, 1 @decimal 0.0~1.5 + 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, … + @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, … + + + i = 1 and v = 0 @integer 1 + 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, … + @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, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + 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, … + @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, … + + + + + + n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000 + n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000 + 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 + @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, … + + + v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … + v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … + 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, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + 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, … + 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, … + 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, … + @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, … + + + + + + i = 1 and v = 0 @integer 1 + i = 2..4 and v = 0 @integer 2~4 + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … + + + i = 1 and v = 0 @integer 1 + 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, … + 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, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + 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, … + 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, … + 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, … + @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + + + 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, … + 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, … + f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + @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, … + + + v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … + 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, … + 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, … + @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + + + + + + 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, … + 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, … + 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, … + n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, … + @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, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + 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, … + 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, … + @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, … + + + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + 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 + 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 + @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, … + + + v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, … + v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, … + v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, … + v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + @integer 3~10, 13~19, 23, 103, 1003, … + + + + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + 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, … + 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, … + 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, … + @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + 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, … + 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, … + @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, … + + + n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000 + n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000 + n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000 + n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000 + n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000 + @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, … + + + \ No newline at end of file diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 1e563934c5..1bfe97d25d 100644 --- a/components/resources/library/build.gradle.kts +++ b/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("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 { diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt index 77913ab495..9e63bf4fe1 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt +++ b/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) = 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) : 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) : Resource(id, items) + private sealed interface StringItem { data class Value(val text: String) : StringItem + data class Plurals(val items: Map) : StringItem data class Array(val items: List) : 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, + resourceReader: ResourceReader, + environment: ResourceEnvironment +): String { + val str = loadPluralString(resource, quantity, resourceReader, environment) + return str.replaceWithArgs(args) } /** @@ -235,4 +368,4 @@ internal fun handleSpecialCharacters(string: String): String { } }.replace("""\\""", """\""") return handledString -} \ No newline at end of file +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/CLDRPluralRuleLists.kt new file mode 100644 index 0000000000..3701ac30d9 --- /dev/null +++ b/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 "" + ) +) \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralCategory.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralCategory.kt new file mode 100644 index 0000000000..6640784432 --- /dev/null +++ b/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) + } + } + } +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRule.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRule.kt new file mode 100644 index 0000000000..a5723bab59 --- /dev/null +++ b/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, + ) : 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() + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/plural/PluralRuleList.kt new file mode 100644 index 0000000000..6dc343516a --- /dev/null +++ b/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) { + fun getCategory(quantity: Int): PluralCategory { + return rules.first { rule -> rule.appliesTo(quantity) }.category + } + + companion object { + private val cacheMutex = Mutex() + private val cache = Array?>(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()) + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/CLDRPluralRuleLists.test.kt new file mode 100644 index 0000000000..b5d915e376 --- /dev/null +++ b/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, …" + ) +) \ No newline at end of file diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt index 61949ee842..212d9d81e9 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt +++ b/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 { @@ -170,6 +266,18 @@ class ComposeResourceTest { item 2 item 3 + + one + other + + + another one + another other + + + %1${'$'}d message for %2${'$'}s + %1${'$'}d messages for %2${'$'}s + """.trimIndent(), diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/PluralRulesTest.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/PluralRulesTest.kt new file mode 100644 index 0000000000..2d92619158 --- /dev/null +++ b/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)) + } + } + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt b/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt index 7675790707..9ab4d9462c 100644 --- a/components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt +++ b/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")) -) \ No newline at end of file +) + +@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 { + 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): PluralRuleList { + val pluralRules = rules.map { PluralRule(it.first, it.second) } + PluralRule(PluralCategory.OTHER, "") + return PluralRuleList(pluralRules.toTypedArray()) +} \ No newline at end of file diff --git a/components/resources/library/src/commonTest/resources/strings.xml b/components/resources/library/src/commonTest/resources/strings.xml index 2f19269c97..a1fe3a6c29 100644 --- a/components/resources/library/src/commonTest/resources/strings.xml +++ b/components/resources/library/src/commonTest/resources/strings.xml @@ -8,4 +8,16 @@ item 2 item 3 + + one + other + + + another one + another other + + + %1$d message for %2$s + %1$d messages for %2$s + diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt index 79410c0971..5e7a234cd3 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt +++ b/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 { + //type -> id + private val stringTypeNames = listOf("string", "string-array", "plurals") + private fun getStringResources(stringsXml: File): List> { 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 = diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt index 6064e1f8c1..87e2d38dbd 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt +++ b/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 -> diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Plurals0.kt new file mode 100644 index 0000000000..31cac7ea1f --- /dev/null +++ b/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"), + ) + ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt index 76964658a9..4644d746fe 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt +++ b/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 } diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Plurals0.kt new file mode 100644 index 0000000000..81148dce1d --- /dev/null +++ b/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"), + ) + ) \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt index 3bfdad8113..416db64499 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt +++ b/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 } diff --git a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml index 6fa9a0966b..9e537278ba 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml +++ b/gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml @@ -13,4 +13,14 @@ PascalCase 1-kebab-case camelCase + + + %d zero + %d one + %d two + %d few + %d many + %d other + + diff --git a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt index 81038d418b..160bec3ba9 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt +++ b/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 } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt index be1a2fe52a..21484d23c2 100644 --- a/gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt +++ b/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 }