Kurrency
0.4.0indexedType-safe currency formatting with comprehensive locale management, ISO/code and symbol styles, Compose-ready composables, result-based error handling, lightweight and instance-based formatter API.
Type-safe currency formatting with comprehensive locale management, ISO/code and symbol styles, Compose-ready composables, result-based error handling, lightweight and instance-based formatter API.
Type-safe currency formatting for Kotlin Multiplatform, with locale-aware output and optional Compose integration.
dependencies {
implementation("org.kimplify:kurrency-core:0.4.0")
}
dependencies {
implementation("org.kimplify:kurrency-core:0.4.0")
implementation("org.kimplify:kurrency-compose:0.4.0")
}
Create a CurrencyFormatter for a locale (the no-argument constructor uses the system locale), then format with Result-based methods.
import org.kimplify.kurrency.CurrencyFormatter
val formatter = CurrencyFormatter() // system locale
formatter.formatCurrencyStyleResult("1234.56", "USD") // "$1,234.56" (en-US)
formatter.formatIsoCurrencyStyleResult("1234.56", "USD") // "USD 1,234.56"
formatter.formatCompactStyleResult("1234567.89", "USD") // "$1.2M"
Fraction digits are a property of the currency and never vary by locale, so they are available statically:
CurrencyFormatter.getFractionDigits("USD") // Result.success(2)
CurrencyFormatter.getFractionDigitsOrDefault("JPY") // 0
A currency always uses the same number of fraction digits, regardless of where it is displayed:
The locale only controls presentation: decimal separator, grouping separator, symbol placement, and spacing.
val us = CurrencyFormatter(KurrencyLocale.US)
val de = CurrencyFormatter(KurrencyLocale.GERMANY)
us.formatCurrencyStyleResult("1234.56", "USD") // "$1,234.56"
de.formatCurrencyStyleResult("1234.56", "USD") // "1.234,56 $"
// JPY has no fraction digits in any locale
us.formatCurrencyStyleResult("1234", "JPY") // "¥1,234"
Currencies are represented by Kurrency. Build one from a code with fromCode, or use a predefined constant.
import org.kimplify.kurrency.Kurrency
val usd = Kurrency.fromCode("USD").getOrThrow()
val eur = Kurrency.EUR
val valid = Kurrency.isValid("USD") // true
usd.formatAmount("1234.56").getOrNull() // "$1,234.56" (system locale)
eur.formatAmount("1234.56", locale = KurrencyLocale.GERMANY).getOrNull() // "1.234,56 €"
import org.kimplify.kurrency.CurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale
val formatter = CurrencyFormatter(KurrencyLocale.JAPAN)
formatter.formatCurrencyStyleResult("1234.56", "JPY") // "¥1,235"
Available constants include:
KurrencyLocale.US // en-US
KurrencyLocale.UK // en-GB
KurrencyLocale.CANADA // en-CA
KurrencyLocale.CANADA_FRENCH // fr-CA
KurrencyLocale.GERMANY // de-DE
KurrencyLocale.FRANCE // fr-FR
KurrencyLocale.ITALY // it-IT
KurrencyLocale.SPAIN // es-ES
KurrencyLocale.JAPAN // ja-JP
KurrencyLocale.CHINA
KurrencyLocale.KOREA
KurrencyLocale.BRAZIL
KurrencyLocale.RUSSIA
KurrencyLocale.SAUDI_ARABIA
KurrencyLocale.INDIA
// BCP 47 language tag
val austrian = KurrencyLocale.fromLanguageTag("de-AT").getOrNull()
// Device locale
val system = KurrencyLocale.systemLocale()
Add kurrency-compose for Jetpack Compose Multiplatform support.
The formatter is recreated when the locale changes (key-based recomposition). Formatting is cheap, so call it directly during composition rather than caching it.
import org.kimplify.kurrency.compose.rememberCurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale
@Composable
fun PriceDisplay(amount: String, currencyCode: String) {
var locale by remember { mutableStateOf(KurrencyLocale.US) }
val formatter = rememberCurrencyFormatter(locale)
Column {
Text("Price: ${formatter.formatCurrencyStyle(amount, currencyCode)}")
Button(onClick = { locale = KurrencyLocale.GERMANY }) {
Text()
}
}
}
Provide a formatter to an entire subtree via CompositionLocal.
import org.kimplify.kurrency.compose.ProvideCurrencyFormatter
import org.kimplify.kurrency.compose.LocalCurrencyFormatter
import org.kimplify.kurrency.KurrencyLocale
@Composable
fun App() {
ProvideCurrencyFormatter(locale = KurrencyLocale.US) {
ProductScreen()
}
}
@Composable
fun ProductScreen {
formatter = LocalCurrencyFormatter.current
Text()
}
Convert a Compose Locale to a KurrencyLocale:
import androidx.compose.ui.text.intl.Locale
import org.kimplify.kurrency.KurrencyLocale
import org.kimplify.kurrency.compose.fromComposeLocale
@Composable
fun MyComposable() {
val kurrencyLocale = KurrencyLocale.fromComposeLocale(Locale.current)
val formatter = rememberCurrencyFormatter(kurrencyLocale)
}
A formatter bound to a locale. Fraction-digit lookups are available on the companion object.
// Construction
CurrencyFormatter(locale: KurrencyLocale = KurrencyLocale.systemLocale())
// Instance formatting (Result-based)
: Result<String>
: Result<String>
: Result<String>
CurrencyFormatter.getFractionDigits(currencyCode: String): Result<>
CurrencyFormatter.getFractionDigitsOrDefault(currencyCode: String):
Kurrency.fromCode(code: String): Result<Kurrency>
Kurrency.isValid(code: String):
): Result<String>
): Result<String>
): String
CurrencyStyle is one of Standard (symbol), Iso (ISO code), or Accounting (parentheses for negatives).
The shared surface implemented per platform. These methods return a plain String (falling back to the input on failure); use the *Result methods on CurrencyFormatter for explicit error handling.
interface CurrencyFormat {
:
: String
: String
: String
}
KurrencyLocale.fromLanguageTag(languageTag: String): Result<KurrencyLocale>
KurrencyLocale.systemLocale(): KurrencyLocale
val languageTag: String // e.g. "en-US"
val decimalSeparator: Char
val groupingSeparator: Char
kurrency-compose)Result-based methods return Result<String>, with failures modeled as KurrencyError:
CurrencyFormatter(KurrencyLocale.US)
.formatCurrencyStyleResult("1234.56", "USD")
.onSuccess { println(it) }
.onFailure { error ->
when (error) {
is KurrencyError.InvalidAmount -> println("Invalid amount")
is KurrencyError.InvalidCurrencyCode -> println("Invalid currency")
else -> println("Formatting error")
}
}
Apache License 2.0 — Copyright © 2025
Result-based API with a sealed KurrencyError hierarchy fun rememberCurrencyFormatter(locale: KurrencyLocale = KurrencyLocale.current()): CurrencyFormat
fun rememberSystemCurrencyFormatter(): CurrencyFormat
val LocalCurrencyFormatter: ProvidableCompositionLocal<CurrencyFormat>
fun ProvideCurrencyFormatter(locale: KurrencyLocale, content: @Composable () -> Unit)
fun ProvideSystemCurrencyFormatter(content: @Composable () -> Unit)
fun KurrencyLocale.Companion.fromComposeLocale(composeLocale: Locale): KurrencyLocale
fun KurrencyLocale.Companion.current(): KurrencyLocale
| Error | Meaning |
|---|
KurrencyError.InvalidCurrencyCode | Unknown or malformed currency code |
KurrencyError.InvalidAmount | Amount could not be parsed |
KurrencyError.FormattingFailure | Platform formatting error |
KurrencyError.FractionDigitsFailure | Could not resolve fraction digits |
KurrencyError.InvalidLocale | Unrecognized locale tag |
| Platform | Backend |
|---|
| Android (API 24+) | ICU (android.icu) |
| iOS (13+) | NSNumberFormatter |
| JVM (17+) | java.text.NumberFormat |
| JS (Browser / Node.js) | Intl.NumberFormat |
| WasmJs (Browser) | Intl.NumberFormat |
Surfaced from shared tags and platforms — no rankings paid for.