-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support plural string resource (#4519)
Ports a part of Unicode's ICU in pure Kotlin and implements Android-style plural string resource support. Fixes #425. # Changes - Added `org.jetbrains.compose.resources.intl.{PluralCategory, PluralRule, PluralRuleList}`, which parses and evaluates scripts in Unicode's Locale Data Markup Langauge. - Copied `plurals.xml` from Unicode's [CLDR](https://github.com/unicode-org/cldr/blob/release-44-1/common/supplemental/plurals.xml). - Added `GeneratePluralRuleListsTask`, which parses `plurals.xml` and generates required Kotlin source codes. - Added `PluralStringResource`, `pluralStringResource`, or `getPluralString`, corresponding to `StringResource`, `stringResource`, or `getString`. - Modified `ResourcesSpec.kt` so the generated `Res` class exposes `Res.plurals`. # Potential Further Improvements - [ ] Allow configuring the default language in the `compose.resources {}` block (#4482) to determine the default pluralization rule (or just presume English as default) - [ ] Move the parser logic to the Gradle plugin and generate pluralization rules in `Res` only for languages used in `composeResources` --------- Co-authored-by: Konstantin Tskhovrebov <konstantin.tskhovrebov@jetbrains.com>
- Loading branch information
Showing
24 changed files
with
2,827 additions
and
18 deletions.
There are no files selected for viewing
134 changes: 134 additions & 0 deletions
134
components/buildSrc/src/main/kotlin/GeneratePluralRuleListsTask.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* | ||
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers. | ||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. | ||
*/ | ||
|
||
import groovy.util.Node | ||
import groovy.xml.XmlParser | ||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.file.RegularFileProperty | ||
import org.gradle.api.tasks.* | ||
|
||
/** | ||
* Reads a pluralization rules XML file from Unicode's CLDR and generates a Kotlin file that holds the XML content as | ||
* arrays. This Task is required for quantity string resource support. | ||
*/ | ||
@CacheableTask | ||
abstract class GeneratePluralRuleListsTask : DefaultTask() { | ||
@get:InputFile | ||
@get:PathSensitive(PathSensitivity.RELATIVE) | ||
abstract val pluralsFile: RegularFileProperty | ||
|
||
@get:OutputFile | ||
abstract val outputFile: RegularFileProperty | ||
|
||
@get:OutputFile | ||
abstract val samplesOutputFile: RegularFileProperty | ||
|
||
@TaskAction | ||
fun generatePluralRuleLists() { | ||
val pluralRuleLists = parsePluralRuleLists() | ||
|
||
val mainContent = generateMainContent(pluralRuleLists) | ||
outputFile.get().asFile.writeText(mainContent) | ||
|
||
val testContent = generateTestContent(pluralRuleLists) | ||
samplesOutputFile.get().asFile.writeText(testContent) | ||
} | ||
|
||
private fun parsePluralRuleLists(): List<PluralRuleList> { | ||
val parser = XmlParser(false, false).apply { | ||
setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) | ||
setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) | ||
} | ||
val supplementalData = parser.parse(pluralsFile.get().asFile) | ||
val pluralRuleLists = supplementalData.children().filterIsInstance<Node>().first { it.name() == "plurals" } | ||
|
||
return pluralRuleLists.children().filterIsInstance<Node>().map { pluralRules -> | ||
val locales = pluralRules.attribute("locales").toString().split(' ') | ||
PluralRuleList( | ||
locales, | ||
pluralRules.children().filterIsInstance<Node>().map { pluralRule -> | ||
val rule = pluralRule.text().split('@') | ||
PluralRule( | ||
pluralRule.attribute("count").toString(), | ||
// trim samples as not needed | ||
rule[0].trim(), | ||
rule.firstOrNull { it.startsWith("integer") }?.substringAfter("integer")?.trim() ?: "", | ||
rule.firstOrNull { it.startsWith("decimal") }?.substringAfter("decimal")?.trim() ?: "", | ||
) | ||
} | ||
) | ||
} | ||
} | ||
|
||
private fun generateMainContent(pluralRuleLists: List<PluralRuleList>): String { | ||
val pluralRuleListIndexByLocale = pluralRuleLists.flatMapIndexed { idx, pluralRuleList -> | ||
pluralRuleList.locales.map { locale -> | ||
locale to idx | ||
} | ||
} | ||
|
||
return """ | ||
package org.jetbrains.compose.resources.plural | ||
/** | ||
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists' | ||
* DO NOT EDIT!!! | ||
*/ | ||
internal val cldrPluralRuleListIndexByLocale = mapOf( | ||
${pluralRuleListIndexByLocale.joinToString(separator = ",\n ") { (locale, idx) -> | ||
"\"$locale\" to $idx" | ||
}} | ||
) | ||
internal val cldrPluralRuleLists = arrayOf(${pluralRuleLists.joinToString(",") { pluralRuleList -> | ||
""" | ||
arrayOf( | ||
${pluralRuleList.rules.joinToString(",\n ") { rule -> | ||
"PluralCategory.${rule.count.uppercase()} to \"${rule.rule}\"" | ||
}} | ||
)""" | ||
}} | ||
) | ||
""".trimIndent() | ||
} | ||
|
||
private fun generateTestContent(pluralRuleLists: List<PluralRuleList>): String { | ||
val pluralRuleIntegerSamplesByLocale = pluralRuleLists.flatMap { pluralRuleList -> | ||
pluralRuleList.locales.map { locale -> | ||
locale to pluralRuleList.rules.map { it.count to it.integerSample } | ||
} | ||
} | ||
|
||
return """ | ||
package org.jetbrains.compose.resources.plural | ||
/** | ||
* THIS CODE IS AUTOGENERATED BY './gradlew :resources:library:generatePluralRuleLists' | ||
* DO NOT EDIT!!! | ||
*/ | ||
internal val cldrPluralRuleIntegerSamples = arrayOf( | ||
${pluralRuleIntegerSamplesByLocale.joinToString(",\n ") { (locale, samples) -> | ||
""""$locale" to arrayOf( | ||
${samples.joinToString(",\n ") { (count, sample) -> | ||
"PluralCategory.${count.uppercase()} to \"$sample\"" | ||
}} | ||
)""" | ||
}} | ||
) | ||
""".trimIndent() | ||
} | ||
} | ||
|
||
private data class PluralRuleList( | ||
val locales: List<String>, | ||
val rules: List<PluralRule>, | ||
) | ||
|
||
private data class PluralRule( | ||
val count: String, | ||
val rule: String, | ||
val integerSample: String, | ||
val decimalSample: String, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.