diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2aaf05c..8323a67 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -38,8 +38,6 @@ jobs:
require_tests: true
report_paths: "**/build/test-results/**/TEST-*.xml"
- # compileProductionExecutableKotlinJs
-
- name: Build production JS file for size comparison
run: ./gradlew compileProductionExecutableKotlinJs
diff --git a/README.md b/README.md
index 112d7cc..b825e17 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@


+
+


@@ -17,7 +19,7 @@ The library is published to Maven Central.
```kotlin
dependencies {
- implementation("nl.jacobras:Human-Readable:1.13.0")
+ implementation("nl.jacobras:Human-Readable:2.0.0") // Not yet published!
}
```
@@ -40,7 +42,39 @@ HumanReadable.duration(7.days) // "1 week"
HumanReadable.duration(544.hours) // "3 weeks"
```
-**Note**: The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. See [Precision](#datetime-precision).
+### ✍️ Formatting options
+
+The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can.
+
+There are a number of configuration options available for both `timeAgo()` and `duration()`.
+
+```kotlin
+HumanReadable.timeAgo(
+ instant = now - 134.minutes,
+ formatting = FormatStyle(
+ date = FormatStyle.Date.Long, // or Short: "1 hr, 50 min" or Narrow: "1h 50m"
+ time = FormatStyle.Time.Regular, // or Digital: "01:50:00"
+ indicateApproximation = true // will prefix "about" if the formatted time is not exact (i.e. a part was dropped or rounded)
+ ),
+ parts = PartsConfig(
+ max = 2, // "1 hour, 50 minutes"
+ smallestDuration = 10.minutes, // anything smaller will return "less than 10 minutes"
+ subpartCutOffs = mapOf(TimeUnit.Hours to 12.hours), // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours"
+ onlyConsecutiveParts = true // whether "1 hour, 5 seconds" can be returned (as seconds are not the next unit after hours)
+ ),
+ units = setOf(TimeUnit.Hours), // limits the output to these units, e.g. "391 days"
+ rounding = Rounding.HalfUp // or Floor to round down, or UpIfClose to round up on 55 seconds/55 minutes/23 hours/13 days
+)
+```
+
+The above parameters can be set globally via `HumanReadable.config.time`.
+
+```kotlin
+HumanReadable.config.time.units = setOf(TimeUnit.Hours)
+HumanReadable.timeAgo(now - 2.days) // "48 hours ago"
+```
+
+Visit the [interactive demo](#features) to see more examples in action.
### 📂 File size
@@ -54,7 +88,7 @@ HumanReadable.fileSize(21_947_282_882, decimals = 2) // "20.44 GB" in English /
### 🔢 Number abbreviation
-Available since version 1.8, localized since 1.10.
+Available since version 1.8, localised since 1.10.
```kotlin
HumanReadable.abbreviation(3_000) // "3K"
@@ -67,7 +101,7 @@ HumanReadable.abbreviation(2_500_000, decimals = 1) // "2.5M"
Available since version 1.10.
```kotlin
-// English/default
+// English
HumanReadable.number(1_000_000.34) // "1,000,000.34"
// French
@@ -93,48 +127,34 @@ HumanReadable.distance(value = 5350, unit = DistanceUnit.Foot) // "1.0 mi"
HumanReadable.distance(value = 28512, unit = DistanceUnit.Foot, decimals = 2) // "5.40 mi"
```
-**Note:** numbers in meters and feet are always formatted with 0 decimals. The passed in
+**Note:** numbers in meters and feet are always formatted with zero decimals. The passed in
number of decimals is only used for the larger units kilometers and miles.
-## Date/time precision
-The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can.
-For example:
-
-* `59.seconds` is "59 seconds" but `60.seconds` becomes "1 minute"
-* `6.days` is "6 days" but `7.days` becomes "1 week"
-* `29.days` is "29 days" but `30.days` becomes "1 month"
-
-There's also some rounding involved:
-
-* `8.days` and `10.days` are "1 week", but `11.days` already becomes "2 weeks"
-
-This behaviour may become configurable in future releases.
-
## Localisation
-The library uses the small [Libres](https://github.com/Skeptick/libres) library for its string resources. It detects the
-current locale by default, but it's changeable on runtime.
-See [Libres: Changing Localization](https://github.com/Skeptick/libres/blob/master/docs/LOCALIZATION.md#changing-localization).
-
-You don't need to manually import Libres, as Gradle already pulls it in along with HumanReadable.
+The library uses an internal i18n mechanism. It detects the current locale by default, but it's changeable at
+runtime via `HumanReadable.config.languageTag`:
```kotlin
HumanReadable.timeAgo(instant) // "3 days ago"
-LibresSettings.languageCode = "nl"
+HumanReadable.config.languageTag = "nl"
HumanReadable.timeAgo(instant) // "3 dagen geleden"
-LibresSettings.languageCode = "fr"
+HumanReadable.config.languageTag = "fr"
HumanReadable.timeAgo(instant) // "il y a 3 jours"
```
+If the requested locale is not supported, the library will fall back to `HumanReadable.fallbackLanguageTag`, which by
+default is set to English.
+
### Supported languages
* Arabic (since 1.13.0)
* Czech
* Chinese (since 1.3.0)
* Dutch
-* English (**default**)
+* **English**
* Finnish (since 1.7.0)
* French
* German
@@ -157,7 +177,14 @@ HumanReadable.timeAgo(instant) // "il y a 3 jours"
Missing a language? Feel free to open an issue about it. Or, add it yourself:
-1. Fork the code and navigate to [src/commonMain/libres/strings/](https://github.com/jacobras/Human-Readable/tree/main/src/commonMain/libres/strings)
-2. Add a file named `time_units_[LANGUAGE CODE].xml` (see [Unicode: CLDR chart](https://www.unicode.org/cldr/charts/42/supplemental/language_plural_rules.html) for the code & plural categories).
-3. If the language deviates from English data units (like French does), also add `data_units_[LANGUAGE CODE].xml`.
-4. Open a PR.
\ No newline at end of file
+1. Fork the code and navigate to `src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations`
+2. Add a file named `XxStrings.kt` (where `Xx` is
+ the [language code](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html)). Follow the
+ example of other translations.
+3. Add a new entry to the `translations` map in
+ `src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt`.
+4. Open a PR.
+
+Follow the Unicode spec at
+or , if you prefer the
+JSON format.
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 72bb3c3..6c119cd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,12 +5,11 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.compose.compiler) apply false
id("com.vanniktech.maven.publish") version "0.37.0"
- id("io.github.skeptick.libres") version "1.2.4"
signing
}
group = "nl.jacobras"
-version = "1.13.0"
+version = project.property("nl.jacobras.humanreadable.version") as String
mavenPublishing {
publishToMavenCentral()
@@ -49,6 +48,7 @@ kotlin {
explicitApi()
applyDefaultHierarchyTemplate()
+ // Targets, alphabetically sorted
iosX64()
iosArm64()
iosSimulatorArm64()
@@ -61,15 +61,6 @@ kotlin {
}
}
}
- wasmJs {
- browser {
- testTask {
- useKarma {
- useChromeHeadless()
- }
- }
- }
- }
jvm {
testRuns.named("test") {
executionTask.configure {
@@ -84,23 +75,35 @@ kotlin {
}
}
}
+ linuxArm64()
+ linuxX64()
+ macosArm64()
+ mingwX64()
+ tvosArm64()
+ tvosSimulatorArm64()
+ wasmJs {
+ browser {
+ testTask {
+ useKarma {
+ useChromeHeadless()
+ }
+ }
+ }
+ }
+ watchosArm32()
+ watchosArm64()
+ watchosSimulatorArm64()
sourceSets {
- val commonMain by getting {
- dependencies {
- implementation(libs.kotlinX.datetime)
- }
+ commonMain.dependencies {
+ implementation(libs.kotlinX.datetime)
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.assertK)
}
- val wasmJsMain by getting
- val appleAndJsMain by creating {
- dependsOn(commonMain)
- appleMain.get().dependsOn(this)
- jsMain.get().dependsOn(this)
- wasmJsMain.dependsOn(this)
+ webMain.dependencies {
+ implementation(libs.kotlinX.browser)
}
}
}
@@ -111,10 +114,6 @@ tasks.withType().configureEach {
mustRunAfter(signingTasks)
}
-libres {
- generatedClassName = "HumanReadableRes"
-}
-
signing {
setRequired {
!gradle.taskGraph.allTasks.any { it is PublishToMavenLocal }
diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts
index 875d07f..d0d70e5 100644
--- a/demo/build.gradle.kts
+++ b/demo/build.gradle.kts
@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.buildconfig)
}
kotlin {
@@ -28,6 +29,9 @@ kotlin {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+ implementation(libs.compose.adaptive)
+ implementation(libs.compose.adaptive.layout)
+ implementation(libs.compose.adaptive.navigation)
implementation(libs.kotlinX.datetime)
implementation(rootProject)
@@ -37,4 +41,8 @@ kotlin {
rootProject.plugins.withType {
rootProject.the().yarnLockMismatchReport = YarnLockMismatchReport.NONE
+}
+
+buildConfig {
+ buildConfigField("VERSION", project.property("nl.jacobras.humanreadable.version") as String)
}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/App.kt b/demo/src/webMain/kotlin/App.kt
index 6ea7c2b..4138d9f 100644
--- a/demo/src/webMain/kotlin/App.kt
+++ b/demo/src/webMain/kotlin/App.kt
@@ -1,25 +1,29 @@
+import Human_Readable.demo.BuildConfig
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -31,19 +35,20 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import io.github.skeptick.libres.LibresSettings
-import ui.AbbreviationDemo
-import ui.DistanceDemo
-import ui.FileSizeDemo
-import ui.FlexibleLayout
+import feature.DistanceDemo
+import feature.FileSizeDemo
+import feature.NumberFormatDemo
+import feature.TimeDemo
+import kotlinx.coroutines.launch
+import nl.jacobras.humanreadable.HumanReadable
+import ui.FeaturesList
import ui.LanguageChip
-import ui.NumberFormatDemo
-import ui.TimeDemo
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalMaterial3AdaptiveApi::class)
@Composable
internal fun App() {
- var selectedLanguageCode by remember { mutableStateOf(LibresSettings.languageCode ?: "en") }
+ var selectedLanguageCode by remember { mutableStateOf(HumanReadable.config.languageTag) }
+ var selectedFeature by remember { mutableStateOf(Feature.Time) }
val layoutDirection = if (selectedLanguageCode == "ar") {
LayoutDirection.Rtl
} else {
@@ -52,17 +57,22 @@ internal fun App() {
fun onSelectLanguage(code: String) {
selectedLanguageCode = code
- LibresSettings.languageCode = code
+ HumanReadable.config.languageTag = code
}
+ val navigator = rememberListDetailPaneScaffoldNavigator()
+ val scope = rememberCoroutineScope()
+
MaterialTheme {
CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+
Scaffold(
topBar = {
- TopAppBar(title = { Text("Human-Readable web demo") })
+ TopAppBar(title = { Text("Human-Readable ${BuildConfig.VERSION} web demo") })
}
) { paddingValues ->
+
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
@@ -74,7 +84,7 @@ internal fun App() {
) {
Text(
text = buildAnnotatedString {
- append("LibresSettings.languageCode = \"")
+ append("HumanReadable.config.languageTag = \"")
withStyle(monoBodyStringBold) {
append(selectedLanguageCode)
}
@@ -97,16 +107,29 @@ internal fun App() {
}
Spacer(modifier = Modifier.height(32.dp))
- FlexibleLayout(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(bottom = 32.dp),
- timeDemo = { TimeDemo(selectedLanguageCode, it) },
- fileSizeDemo = { FileSizeDemo(selectedLanguageCode, it) },
- abbreviationDemo = { AbbreviationDemo(selectedLanguageCode, it) },
- numberFormatDemo = { NumberFormatDemo(selectedLanguageCode, it) },
- distanceDemo = { DistanceDemo(selectedLanguageCode, it) }
+ ListDetailPaneScaffold(
+ directive = navigator.scaffoldDirective,
+ value = navigator.scaffoldValue,
+ listPane = {
+ FeaturesList(
+ selected = selectedFeature,
+ onSelect = {
+ selectedFeature = it
+ scope.launch {
+ navigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
+ }
+ }
+ )
+ },
+ detailPane = {
+ when (selectedFeature) {
+ Feature.Time -> TimeDemo(selectedLanguageCode)
+ Feature.FileSize -> FileSizeDemo(selectedLanguageCode)
+ Feature.Number -> NumberFormatDemo(selectedLanguageCode)
+ Feature.Distance -> DistanceDemo(selectedLanguageCode)
+ }
+ },
+ paneExpansionDragHandle = { _ -> VerticalDivider() }
)
}
}
diff --git a/demo/src/webMain/kotlin/Feature.kt b/demo/src/webMain/kotlin/Feature.kt
new file mode 100644
index 0000000..8fc0ecd
--- /dev/null
+++ b/demo/src/webMain/kotlin/Feature.kt
@@ -0,0 +1,6 @@
+enum class Feature(val title: String) {
+ Time("Time"),
+ Number("Number"),
+ FileSize("File size"),
+ Distance("Distance")
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/feature/DistanceDemo.kt b/demo/src/webMain/kotlin/feature/DistanceDemo.kt
new file mode 100644
index 0000000..f2fe6cd
--- /dev/null
+++ b/demo/src/webMain/kotlin/feature/DistanceDemo.kt
@@ -0,0 +1,100 @@
+package feature
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import monoBodyOrange
+import nl.jacobras.humanreadable.DistanceUnit
+import nl.jacobras.humanreadable.HumanReadable
+import ui.CodeExample
+import ui.DecimalsInput
+
+@Composable
+internal fun DistanceDemo(
+ selectedLanguageCode: String,
+ modifier: Modifier = Modifier
+) {
+ val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+
+ Column(modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+ Text(
+ text = "Distance formatting",
+ style = MaterialTheme.typography.headlineLarge
+ )
+ Spacer(Modifier.height(16.dp))
+ var myNumber by remember { mutableStateOf("3000") }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(monoBodyOrange) { append("val ") }
+ append("myDistance = ")
+ },
+ style = monoBody
+ )
+ TextField(
+ modifier = Modifier.sizeIn(minWidth = 20.dp),
+ value = myNumber,
+ onValueChange = {
+ if (it.length < 16) {
+ myNumber = it
+ }
+ },
+ isError = myNumber.toLongOrNull() == null,
+ supportingText = if (myNumber.toLongOrNull() == null) {
+ { Text("Invalid number") }
+ } else {
+ null
+ }
+ )
+ }
+ Spacer(Modifier.height(8.dp))
+ var decimals by remember { mutableIntStateOf(2) }
+ DecimalsInput(
+ decimals = decimals,
+ onChange = { decimals = it }
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "HumanReadable.distance(myDistance, DistanceUnit.Meter, decimals)",
+ res = remember(selectedLanguageCode, myNumber, decimals) {
+ HumanReadable.distance(
+ value = myNumber.toLongOrNull() ?: 0L,
+ unit = DistanceUnit.Meter,
+ decimals = decimals
+ )
+ }
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "HumanReadable.distance(myDistance, DistanceUnit.Foot, decimals)",
+ res = remember(selectedLanguageCode, myNumber, decimals) {
+ HumanReadable.distance(
+ value = myNumber.toLongOrNull() ?: 0L,
+ unit = DistanceUnit.Foot,
+ decimals = decimals
+ )
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/FileSizeDemo.kt b/demo/src/webMain/kotlin/feature/FileSizeDemo.kt
similarity index 53%
rename from demo/src/webMain/kotlin/ui/FileSizeDemo.kt
rename to demo/src/webMain/kotlin/feature/FileSizeDemo.kt
index 1173fca..75cbd88 100644
--- a/demo/src/webMain/kotlin/ui/FileSizeDemo.kt
+++ b/demo/src/webMain/kotlin/feature/FileSizeDemo.kt
@@ -1,10 +1,20 @@
-package ui
+package feature
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.buildAnnotatedString
@@ -12,9 +22,9 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import monoBodyOrange
-import monoBodyString
-import monoBodyStringBold
import nl.jacobras.humanreadable.HumanReadable
+import ui.CodeExample
+import ui.DecimalsInput
@Composable
internal fun FileSizeDemo(
@@ -25,7 +35,7 @@ internal fun FileSizeDemo(
Column(modifier.fillMaxWidth()) {
Text(
- text = "File size",
+ text = "File size formatting",
style = MaterialTheme.typography.headlineLarge
)
Text("File size formatting uses base 1024.")
@@ -60,44 +70,21 @@ internal fun FileSizeDemo(
)
}
Spacer(Modifier.height(8.dp))
- var decimals by remember { mutableStateOf("2") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("decimals = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = decimals,
- onValueChange = { decimals = it },
- isError = decimals.toIntOrNull() == null,
- supportingText = if (decimals.toIntOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
- Spacer(Modifier.height(8.dp))
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.fileSize(myFile, decimals)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(
- remember(selectedLanguageCode, myFile, decimals) {
- HumanReadable.fileSize(
- bytes = myFile.toLongOrNull() ?: 0L,
- decimals = decimals.toIntOrNull() ?: 0
- )
- })
- append("\"")
- }
- },
- style = monoBody
+ var decimals by remember { mutableIntStateOf(2) }
+ DecimalsInput(
+ decimals = decimals,
+ onChange = { decimals = it }
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "HumanReadable.fileSize(myFile, decimals)",
+ res = remember(selectedLanguageCode, myFile, decimals) {
+ HumanReadable.fileSize(
+ bytes = myFile.toLongOrNull() ?: 0L,
+ decimals = decimals
+ )
+ }
)
}
}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/NumberFormatDemo.kt b/demo/src/webMain/kotlin/feature/NumberFormatDemo.kt
similarity index 52%
rename from demo/src/webMain/kotlin/ui/NumberFormatDemo.kt
rename to demo/src/webMain/kotlin/feature/NumberFormatDemo.kt
index 2b6bac3..3329354 100644
--- a/demo/src/webMain/kotlin/ui/NumberFormatDemo.kt
+++ b/demo/src/webMain/kotlin/feature/NumberFormatDemo.kt
@@ -1,10 +1,20 @@
-package ui
+package feature
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.buildAnnotatedString
@@ -12,9 +22,9 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import monoBodyOrange
-import monoBodyString
-import monoBodyStringBold
import nl.jacobras.humanreadable.HumanReadable
+import ui.CodeExample
+import ui.DecimalsInput
@Composable
internal fun NumberFormatDemo(
@@ -29,7 +39,7 @@ internal fun NumberFormatDemo(
style = MaterialTheme.typography.headlineLarge
)
Spacer(Modifier.height(16.dp))
- var myNumber by remember { mutableStateOf("1000000.34") }
+ var myNumber by remember { mutableStateOf("105122.34") }
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = buildAnnotatedString {
@@ -55,44 +65,31 @@ internal fun NumberFormatDemo(
)
}
Spacer(Modifier.height(8.dp))
- var decimals by remember { mutableStateOf("2") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("decimals = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = decimals,
- onValueChange = { decimals = it },
- isError = decimals.toIntOrNull() == null,
- supportingText = if (decimals.toIntOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
+ var decimals by remember { mutableIntStateOf(1) }
+ DecimalsInput(
+ decimals = decimals,
+ onChange = { decimals = it }
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "HumanReadable.number(myNumber, decimals)",
+ res = remember(selectedLanguageCode, myNumber, decimals) {
+ HumanReadable.number(
+ number = myNumber.toDoubleOrNull() ?: 0L,
+ decimals = decimals
+ )
+ }
+ )
Spacer(Modifier.height(8.dp))
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.number(myNumber, decimals)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(
- remember(selectedLanguageCode, myNumber, decimals) {
- HumanReadable.number(
- number = myNumber.toDoubleOrNull() ?: 0L,
- decimals = decimals.toIntOrNull() ?: 0
- )
- })
- append("\"")
- }
- },
- style = monoBody
+ CodeExample(
+ code = "HumanReadable.abbreviation(myNumber, decimals)",
+ res = remember(selectedLanguageCode, myNumber, decimals) {
+ HumanReadable.abbreviation(
+ number = myNumber.toDoubleOrNull() ?: 0L,
+ decimals = decimals
+ )
+ }
)
}
}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt
new file mode 100644
index 0000000..b77065f
--- /dev/null
+++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt
@@ -0,0 +1,288 @@
+package feature
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.minus
+import kotlinx.datetime.toLocalDateTime
+import monoBodyOrange
+import nl.jacobras.humanreadable.HumanReadable
+import nl.jacobras.humanreadable.time.FormatStyle
+import nl.jacobras.humanreadable.time.PartsConfig
+import nl.jacobras.humanreadable.time.Rounding
+import nl.jacobras.humanreadable.time.TimeUnit
+import ui.CodeExample
+import ui.appendKotlinCode
+import kotlin.time.Clock
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalTime::class)
+@Composable
+internal fun TimeDemo(
+ selectedLanguageCode: String,
+ modifier: Modifier = Modifier
+) {
+ val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+ val now = remember { Clock.System.now() }
+ var myInstant by remember { mutableStateOf(now.minus(90, DateTimeUnit.SECOND)) }
+
+ Column(modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+ Text(
+ text = "Date/Time formatting",
+ style = MaterialTheme.typography.headlineLarge
+ )
+ Spacer(Modifier.height(16.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(monoBodyOrange) { append("val ") }
+ append("now =")
+ },
+ style = monoBody
+ )
+ DateTimeField(now) {}
+ }
+ Spacer(Modifier.height(8.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(monoBodyOrange) { append("val ") }
+ append("myInstant =")
+ },
+ style = monoBody
+ )
+ DateTimeField(myInstant) { myInstant = it }
+ }
+ Spacer(Modifier.height(4.dp))
+
+ FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
+ Button(onClick = { myInstant = now }) { Text("[NOW]") }
+ Button(onClick = { myInstant = myInstant.minus(10.seconds) }) { Text("-10 sec") }
+ Button(onClick = { myInstant = myInstant.minus(1.minutes) }) { Text("-1 min") }
+ Button(onClick = { myInstant = myInstant.minus(1.hours) }) { Text("-1 hr") }
+ Button(onClick = { myInstant = myInstant.minus(1.days) }) { Text("-1 day") }
+ Button(onClick = { myInstant = myInstant.minus(7.days) }) { Text("-7 days") }
+ Button(onClick = { myInstant = myInstant.minus(30.days) }) { Text("-30 days") }
+ Button(onClick = { myInstant = myInstant.minus(365.days) }) { Text("-365 days") }
+ }
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "HumanReadable.duration(now - myInstant)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.duration(now - myInstant)
+ }
+ )
+ Spacer(Modifier.height(12.dp))
+
+ CodeExample(
+ code = "HumanReadable.timeAgo(myInstant) // compared to now",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(myInstant, now)
+ }
+ )
+ Spacer(Modifier.height(12.dp))
+
+ CodeExample(
+ code = "HumanReadable.timeAgo(myLocalDate) // overloaded method that takes a LocalDate",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ date = myInstant.toLocalDateTime(TimeZone.currentSystemDefault()).date,
+ baseDate = now.toLocalDateTime(TimeZone.currentSystemDefault()).date
+ )
+ }
+ )
+ Spacer(Modifier.height(32.dp))
+
+ Text(
+ text = "Configuration parameters",
+ style = MaterialTheme.typography.titleLarge
+ )
+ Text(buildAnnotatedString {
+ append("The examples below call ")
+ appendKotlinCode("HumanReadable.timeAgo(myInstant, option1, option2, ...)")
+ append(". All configuration parameters are optional and also work with ")
+ appendKotlinCode("HumanReadable.duration()")
+ append(".")
+ })
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "formatting = FormatStyle(date = Date.Short)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ formatting = FormatStyle(date = FormatStyle.Date.Short)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "formatting = FormatStyle(date = Date.Narrow)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ formatting = FormatStyle(date = FormatStyle.Date.Narrow)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "formatting = FormatStyle(time = FormatStyle.Time.Digital)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ formatting = FormatStyle(time = FormatStyle.Time.Digital)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "formatting = FormatStyle(indicateApproximation = true)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ formatting = FormatStyle(indicateApproximation = true)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "rounding = Rounding.Floor",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ rounding = Rounding.Floor
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "rounding = Rounding.UpIfClose",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ rounding = Rounding.IfClose()
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "parts = PartsConfig(max = 3)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ parts = PartsConfig(max = 3)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "parts = PartsConfig(max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12))",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ parts = PartsConfig(
+ max = 3,
+ subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12)
+ )
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ CodeExample(
+ code = "parts = PartsConfig(smallestDuration = 10.minutes)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ parts = PartsConfig(smallestDuration = 10.minutes)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(16.dp))
+
+ CodeExample(
+ code = "units = setOf(TimeUnit.Hours, TimeUnit.Days)",
+ res = remember(selectedLanguageCode, now, myInstant) {
+ HumanReadable.timeAgo(
+ myInstant, now,
+ units = setOf(TimeUnit.Hours, TimeUnit.Days)
+ )
+ },
+ inline = true
+ )
+ Spacer(Modifier.height(8.dp))
+ }
+}
+
+@OptIn(ExperimentalTime::class)
+@Composable
+private fun DateTimeField(instant: Instant, onUpdate: (Instant) -> Unit) {
+ var error by remember { mutableStateOf(false) }
+ var value by remember(instant) { mutableStateOf(instant.toString()) }
+
+ TextField(
+ modifier = Modifier.widthIn(min = 0.dp),
+ value = value,
+ onValueChange = {
+ value = it
+ try {
+ onUpdate(Instant.parse(it))
+ error = false
+ } catch (e: IllegalArgumentException) {
+ error = true
+ }
+ },
+ isError = error,
+ supportingText = if (error) {
+ { Text("Invalid date format. Please use ISO-8601 ;-)") }
+ } else null
+ )
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt b/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt
deleted file mode 100644
index a583211..0000000
--- a/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package ui
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import monoBodyOrange
-import monoBodyStringBold
-import nl.jacobras.humanreadable.HumanReadable
-
-@Composable
-internal fun AbbreviationDemo(
- selectedLanguageCode: String,
- modifier: Modifier = Modifier
-) {
- val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
-
- Column(modifier.fillMaxWidth()) {
- Text(
- text = "Abbreviation",
- style = MaterialTheme.typography.headlineLarge
- )
- Spacer(Modifier.height(16.dp))
- var myNumber by remember { mutableStateOf("3000") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("myNumber = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = myNumber,
- onValueChange = {
- if (it.length < 16) {
- myNumber = it
- }
- },
- isError = myNumber.toLongOrNull() == null,
- supportingText = if (myNumber.toLongOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
- Spacer(Modifier.height(8.dp))
- var decimals by remember { mutableStateOf("2") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("decimals = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = decimals,
- onValueChange = { decimals = it },
- isError = decimals.toIntOrNull() == null,
- supportingText = if (decimals.toIntOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
- Spacer(Modifier.height(8.dp))
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.abbreviation(myNumber, decimals)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(
- remember(selectedLanguageCode, myNumber, decimals) {
- HumanReadable.abbreviation(
- number = myNumber.toLongOrNull() ?: 0L,
- decimals = decimals.toIntOrNull() ?: 0
- )
- })
- append("\"")
- }
- },
- style = monoBody
- )
- }
-}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/CodeExample.kt b/demo/src/webMain/kotlin/ui/CodeExample.kt
new file mode 100644
index 0000000..6b0480e
--- /dev/null
+++ b/demo/src/webMain/kotlin/ui/CodeExample.kt
@@ -0,0 +1,19 @@
+package ui
+
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+
+@Composable
+internal fun CodeExample(code: String, res: String, inline: Boolean = false) {
+ SelectionContainer {
+ val separator = if (inline) " " else "\n"
+ Text(
+ text = buildAnnotatedString { appendKotlinCode("$code$separator// \"$res\"") },
+ style = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+ )
+ }
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/DecimalsInput.kt b/demo/src/webMain/kotlin/ui/DecimalsInput.kt
new file mode 100644
index 0000000..4dda3a9
--- /dev/null
+++ b/demo/src/webMain/kotlin/ui/DecimalsInput.kt
@@ -0,0 +1,69 @@
+package ui
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import monoBodyOrange
+
+@Composable
+internal fun DecimalsInput(
+ decimals: Int,
+ onChange: (Int) -> Unit,
+) {
+ var input by remember { mutableStateOf(decimals.toString()) }
+ val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = buildAnnotatedString {
+ withStyle(monoBodyOrange) { append("val ") }
+ append("decimals = ")
+ },
+ style = monoBody
+ )
+ TextField(
+ modifier = Modifier.sizeIn(minWidth = 20.dp),
+ value = input,
+ onValueChange = {
+ input = it
+ val new = it.toIntOrNull()
+ if (new != null) {
+ onChange(new)
+ }
+ },
+ isError = input.toIntOrNull() == null,
+ supportingText = if (input.toIntOrNull() == null) {
+ { Text("Invalid number") }
+ } else {
+ null
+ }
+ )
+ Spacer(Modifier.width(8.dp))
+ Button(onClick = {
+ val new = (decimals - 1).coerceAtLeast(0)
+ input = new.toString()
+ onChange(new)
+ }) { Text("-") }
+ Spacer(Modifier.width(4.dp))
+ Button(onClick = {
+ val new = decimals + 1
+ input = new.toString()
+ onChange(new)
+ }) { Text("+") }
+ }
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/DistanceDemo.kt b/demo/src/webMain/kotlin/ui/DistanceDemo.kt
deleted file mode 100644
index 5461584..0000000
--- a/demo/src/webMain/kotlin/ui/DistanceDemo.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package ui
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import monoBodyOrange
-import monoBodyStringBold
-import nl.jacobras.humanreadable.DistanceUnit
-import nl.jacobras.humanreadable.HumanReadable
-
-@Composable
-internal fun DistanceDemo(
- selectedLanguageCode: String,
- modifier: Modifier = Modifier
-) {
- val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
-
- Column(modifier.fillMaxWidth()) {
- Text(
- text = "Distance",
- style = MaterialTheme.typography.headlineLarge
- )
- Spacer(Modifier.height(16.dp))
- var myNumber by remember { mutableStateOf("3000") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("myDistance = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = myNumber,
- onValueChange = {
- if (it.length < 16) {
- myNumber = it
- }
- },
- isError = myNumber.toLongOrNull() == null,
- supportingText = if (myNumber.toLongOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
- Spacer(Modifier.height(8.dp))
- var decimals by remember { mutableStateOf("2") }
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("decimals = ")
- },
- style = monoBody
- )
- TextField(
- modifier = Modifier.sizeIn(minWidth = 20.dp),
- value = decimals,
- onValueChange = { decimals = it },
- isError = decimals.toIntOrNull() == null,
- supportingText = if (decimals.toIntOrNull() == null) {
- { Text("Invalid number") }
- } else {
- null
- }
- )
- }
- Spacer(Modifier.height(8.dp))
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.distance(myDistance, DistanceUnit.Meter, decimals)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(
- remember(selectedLanguageCode, myNumber, decimals) {
- HumanReadable.distance(
- value = myNumber.toLongOrNull() ?: 0L,
- unit = DistanceUnit.Meter,
- decimals = decimals.toIntOrNull() ?: 0
- )
- })
- append("\"")
- }
- appendLine()
-
- appendLine("HumanReadable.distance(myDistance, DistanceUnit.Foot, decimals)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(
- remember(selectedLanguageCode, myNumber, decimals) {
- HumanReadable.distance(
- value = myNumber.toLongOrNull() ?: 0L,
- unit = DistanceUnit.Foot,
- decimals = decimals.toIntOrNull() ?: 0
- )
- })
- append("\"")
- }
- },
- style = monoBody
- )
- }
-}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/FeaturesList.kt b/demo/src/webMain/kotlin/ui/FeaturesList.kt
new file mode 100644
index 0000000..9265769
--- /dev/null
+++ b/demo/src/webMain/kotlin/ui/FeaturesList.kt
@@ -0,0 +1,35 @@
+package ui
+
+import Feature
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FeaturesList(
+ selected: Feature,
+ onSelect: (Feature) -> Unit
+) {
+ Column {
+ for (feature in Feature.entries) {
+ Text(
+ text = feature.title,
+ fontWeight = if (selected == feature) {
+ FontWeight.Bold
+ } else {
+ FontWeight.Normal
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onSelect(feature) }
+ .padding(8.dp, 4.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/FlexibleLayout.kt b/demo/src/webMain/kotlin/ui/FlexibleLayout.kt
deleted file mode 100644
index 1b3f54f..0000000
--- a/demo/src/webMain/kotlin/ui/FlexibleLayout.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package ui
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-internal fun FlexibleLayout(
- modifier: Modifier = Modifier,
- timeDemo: @Composable (Modifier) -> Unit,
- fileSizeDemo: @Composable (Modifier) -> Unit,
- abbreviationDemo: @Composable (Modifier) -> Unit,
- numberFormatDemo: @Composable (Modifier) -> Unit,
- distanceDemo: @Composable (Modifier) -> Unit,
-) {
- val colModifier = Modifier
- .widthIn(max = 500.dp)
- .padding(horizontal = 16.dp)
-
- FlowRow(
- modifier = modifier,
- horizontalArrangement = Arrangement.SpaceEvenly,
- verticalArrangement = Arrangement.spacedBy(48.dp)
- ) {
- timeDemo(colModifier)
- fileSizeDemo(colModifier)
- abbreviationDemo(colModifier)
- numberFormatDemo(colModifier)
- distanceDemo(colModifier)
- }
-}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/TimeDemo.kt b/demo/src/webMain/kotlin/ui/TimeDemo.kt
deleted file mode 100644
index 96aa315..0000000
--- a/demo/src/webMain/kotlin/ui/TimeDemo.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package ui
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import kotlinx.datetime.*
-import monoBodyOrange
-import monoBodyStringBold
-import nl.jacobras.humanreadable.HumanReadable
-import kotlin.time.Clock
-import kotlin.time.Duration.Companion.days
-import kotlin.time.Duration.Companion.hours
-import kotlin.time.ExperimentalTime
-import kotlin.time.Instant
-
-@OptIn(ExperimentalLayoutApi::class, ExperimentalTime::class)
-@Composable
-internal fun TimeDemo(
- selectedLanguageCode: String,
- modifier: Modifier = Modifier
-) {
- val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
- val now = remember { Clock.System.now() }
- var instant1 by remember { mutableStateOf(now.minus(1337, DateTimeUnit.HOUR)) }
- var instant2 by remember { mutableStateOf(now.plus(2, DateTimeUnit.HOUR)) }
-
- Column(modifier.fillMaxWidth()) {
- Text(
- text = "Date/Time",
- style = MaterialTheme.typography.headlineLarge
- )
- Spacer(Modifier.height(16.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("instant1 =")
- },
- style = monoBody
- )
- DateTimeField(instant1) { instant1 = it }
- }
- Spacer(Modifier.height(8.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text(
- text = buildAnnotatedString {
- withStyle(monoBodyOrange) { append("val ") }
- append("instant2 =")
- },
- style = monoBody
- )
- DateTimeField(instant2) { instant2 = it }
- }
- Spacer(Modifier.height(4.dp))
-
- FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
- Button(onClick = { instant2 = instant2.plus(1.hours) }) { Text("+ hour") }
- Button(onClick = { instant2 = instant2.plus(1.days) }) { Text("+ day") }
- Button(onClick = { instant2 = instant2.plus(30.days) }) { Text("+ ~month") }
- Button(onClick = { instant2 = instant2.plus(365.days) }) { Text("+ ~year") }
- }
- Spacer(Modifier.height(16.dp))
-
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.timeAgo(instant1)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(remember(selectedLanguageCode, instant1) { HumanReadable.timeAgo(instant1) })
- appendLine("\"")
- }
- },
- style = monoBody
- )
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.timeAgo(instant2)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(remember(selectedLanguageCode, instant2) { HumanReadable.timeAgo(instant2) })
- append("\"")
- }
- },
- style = monoBody
- )
- Spacer(Modifier.height(16.dp))
-
- Text(
- text = buildAnnotatedString {
- appendLine("HumanReadable.duration(instant2 - instant1)")
- withStyle(monoBodyStringBold) {
- append("// \"")
- append(remember(selectedLanguageCode, instant1, instant2) {
- HumanReadable.duration(instant2 - instant1)
- })
- append("\"")
- }
- },
- style = monoBody
- )
- }
-}
-
-@OptIn(ExperimentalTime::class)
-@Composable
-private fun DateTimeField(instant: Instant, onUpdate: (Instant) -> Unit) {
- var error by remember { mutableStateOf(false) }
- var value by remember(instant) { mutableStateOf(instant.toString()) }
-
- TextField(
- modifier = Modifier.widthIn(min = 0.dp),
- value = value,
- onValueChange = {
- value = it
- try {
- onUpdate(Instant.parse(it))
- error = false
- } catch (e: IllegalArgumentException) {
- error = true
- }
- },
- isError = error,
- supportingText = if (error) {
- { Text("Invalid date format. Please use ISO-8601 ;-)") }
- } else null
- )
-}
\ No newline at end of file
diff --git a/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt b/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt
new file mode 100644
index 0000000..5cea35a
--- /dev/null
+++ b/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt
@@ -0,0 +1,44 @@
+package ui
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.withStyle
+
+fun AnnotatedString.Builder.appendKotlinCode(code: String) {
+ var lastIndex = 0
+
+ for (match in TOKEN_REGEX.findAll(code)) {
+ if (match.range.first > lastIndex) {
+ append(code.substring(lastIndex, match.range.first))
+ }
+
+ val groups = match.groupValues
+ when {
+ groups[1].isNotEmpty() -> withStyle(
+ SpanStyle(color = Color(0xFF6aab73), fontWeight = FontWeight.Bold)
+ ) { append(match.value) }
+ groups[2].isNotEmpty() || groups[3].isNotEmpty() -> withStyle(
+ SpanStyle(color = Color(0xFF00627A))
+ ) { append(match.value) }
+ groups[4].isNotEmpty() -> withStyle(
+ SpanStyle(color = Color(0xFF999999))
+ ) { append(match.value) }
+ else -> append(match.value)
+ }
+
+ lastIndex = match.range.last + 1
+ }
+
+ if (lastIndex < code.length) {
+ append(code.substring(lastIndex))
+ }
+}
+
+private val TOKEN_REGEX = Regex(
+ """(//[^\n]*)""" + // 1: comments
+ """|(\b[A-Z][A-Za-z0-9_]*\b)""" + // 2: class names
+ """|(\b[a-z_][A-Za-z0-9_]*)(?=\s*\()""" + // 3: method names
+ """|([(),.+*-])""" // 4: punctuation/operators
+)
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 61bedfd..40789ea 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,5 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerM
kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=320m -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -Xms4g
kotlin.code.style=official
-org.jetbrains.compose.experimental.jscanvas.enabled=true
\ No newline at end of file
+org.jetbrains.compose.experimental.jscanvas.enabled=true
+nl.jacobras.humanreadable.version=2.0.0
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7345a8f..9bc7d2f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,12 +1,18 @@
[versions]
kotlin = "2.4.0"
-compose-plugin = "1.12.0-alpha02"
+compose = "1.12.0-beta01"
+compose-adaptive = "1.3.0-beta02"
[libraries]
assertK = { module = "com.willowtreeapps.assertk:assertk", version = "0.28.1" }
+compose-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "compose-adaptive" }
+compose-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "compose-adaptive" }
+compose-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "compose-adaptive" }
+kotlinX-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = "0.5.0" }
kotlinX-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.8.0" }
[plugins]
+buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
+jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7e7d24f..a9db115 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
+retries=0
+retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/karma.config.d/index.js b/karma.config.d/index.js
deleted file mode 100644
index bfc5ab9..0000000
--- a/karma.config.d/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// TODO: remove the file it after 2.0.20
-// https://youtrack.jetbrains.com/issue/KT-67468
-
-function KarmaWebpackOutputFramework(config) {
- // This controller is instantiated and set during the preprocessor phase.
- const controller = config.__karmaWebpackController;
-
- // only if webpack has instantiated its controller
- if (!controller) {
- console.warn(
- "Webpack has not instantiated controller yet.\n" +
- "Check if you have enabled webpack preprocessor and framework before this framework"
- )
- return
- }
-
- config.files.push({
- pattern: `${controller.outputPath}/**/*`,
- included: false,
- served: true,
- watched: false
- })
-}
-
-const KarmaWebpackOutputPlugin = {
- 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework],
-};
-
-config.plugins.push(KarmaWebpackOutputPlugin);
-config.frameworks.push("webpack-output");
\ No newline at end of file
diff --git a/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
deleted file mode 100644
index 732d740..0000000
--- a/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package nl.jacobras.humanreadable
-
-import io.github.skeptick.libres.strings.PluralForm
-import io.github.skeptick.libres.strings.PluralRule
-import io.github.skeptick.libres.strings.PluralRules
-
-/**
- * Only used by Apple & JS.
- */
-internal actual fun extendLibresPlurals() {
- // Make all Libres plurals default to the English implementation
- // Workaround until https://github.com/Skeptick/libres/issues/53 is fixed
- PluralRules["nl"] = PluralRules["en"]
- PluralRules["de"] = PluralRules["en"]
- PluralRules["es"] = PluralRules["en"]
- PluralRules["it"] = PluralRules["en"]
- PluralRules["tr"] = PluralRules["en"]
- PluralRules["el"] = PluralRules["en"]
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["cs"] = PluralRule { number ->
- when (number) {
- 1 -> PluralForm.One
- in (2..4) -> PluralForm.Few
- else -> PluralForm.Other
- }
- }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["id"] = PluralRule { PluralForm.Other }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["pl"] = PluralRule { number ->
- when {
- number == 1 -> PluralForm.One
- (number % 10) in 2..4 && (number % 100) !in 12..14 -> PluralForm.Few
- else -> PluralForm.Many
- }
- }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["zh"] = PluralRule { PluralForm.Other }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["uz"] = PluralRule { number ->
- when (number) {
- 1 -> PluralForm.One
- else -> PluralForm.Other
- }
- }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["ja"] = PluralRule { PluralForm.Other }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["ko"] = PluralRule { PluralForm.Other }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["vi"] = PluralRule { PluralForm.Other }
-
- // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged
- PluralRules["fi"] = PluralRule { number ->
- when (number) {
- 1 -> PluralForm.One
- else -> PluralForm.Other
- }
- }
-
- PluralRules["pt"] = PluralRule { number ->
- when {
- number == 0 || number == 1 -> PluralForm.One
- number % 1_000_000 == 0 -> PluralForm.Many
- else -> PluralForm.Other
- }
- }
-
- PluralRules["ar"] = PluralRule { number ->
- when {
- number == 0 -> PluralForm.Zero
- number == 1 -> PluralForm.One
- number == 2 -> PluralForm.Two
- number % 100 in 3..10 -> PluralForm.Few
- number % 100 in 11..99 -> PluralForm.Many
- else -> PluralForm.Other
- }
- }
-}
\ No newline at end of file
diff --git a/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.apple.kt b/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.apple.kt
new file mode 100644
index 0000000..0834531
--- /dev/null
+++ b/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.apple.kt
@@ -0,0 +1,7 @@
+package nl.jacobras.humanreadable.i18n
+
+import platform.Foundation.NSLocale
+import platform.Foundation.currentLocale
+import platform.Foundation.languageCode
+
+internal actual fun systemLanguageTag(): String = NSLocale.currentLocale.languageCode
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt
new file mode 100644
index 0000000..bf08b3c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt
@@ -0,0 +1,47 @@
+package nl.jacobras.humanreadable
+
+import nl.jacobras.humanreadable.HumanReadable.localisation
+import nl.jacobras.humanreadable.time.FormatStyle
+import nl.jacobras.humanreadable.time.PartsConfig
+import nl.jacobras.humanreadable.time.Rounding
+import nl.jacobras.humanreadable.time.TimeUnit
+
+public class Config {
+ /**
+ * The language tag (e.g. `"en"`, `"fr"`) to use when formatting. Defaults to the detected system
+ * language when it is supported, otherwise [fallbackLanguageTag].
+ */
+ public var languageTag: String
+ get() = localisation.languageTag
+ set(value) {
+ localisation.requestedLanguageTag = value
+ }
+
+ /**
+ * The fallback language tag (e.g. `"en"`, `"fr"`) to use when [languageTag] is not supported.
+ * If an unsupported language is passed in, nothing changes.
+ *
+ * Defaults to English.
+ */
+ public var fallbackLanguageTag: String
+ get() = localisation.fallbackLanguageTag
+ set(value) {
+ localisation.fallbackLanguageTag = value
+ }
+
+ /**
+ * Holds default parameters for time formatting of `duration()` and `timeAgo()`.
+ */
+ public var time: Time = Time()
+ internal set
+}
+
+/**
+ * Default parameters for time formatting of `duration()` and `timeAgo()`.
+ */
+public class Time {
+ public var formatting: FormatStyle = FormatStyle()
+ public var parts: PartsConfig = PartsConfig()
+ public var units: Set = TimeUnit.all
+ public var rounding: Rounding = Rounding.HalfUp
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt
index 89a8777..f8ba41b 100644
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt
@@ -2,8 +2,20 @@
package nl.jacobras.humanreadable
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.todayIn
import nl.jacobras.humanreadable.HumanReadable.duration
import nl.jacobras.humanreadable.HumanReadable.number
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.Localisation
+import nl.jacobras.humanreadable.time.FormatStyle
+import nl.jacobras.humanreadable.time.PartsConfig
+import nl.jacobras.humanreadable.time.RelativeTime
+import nl.jacobras.humanreadable.time.Rounding
+import nl.jacobras.humanreadable.time.TimeUnit
+import nl.jacobras.humanreadable.time.formatDuration
+import nl.jacobras.humanreadable.time.formatTimeAgo
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
@@ -14,48 +26,112 @@ import kotlin.time.Instant
*/
public object HumanReadable {
- init {
- extendLibresPlurals()
- }
+ internal val localisation = Localisation()
+ internal val strings: HumanReadableStrings
+ get() = localisation.currentStrings
+ public val config: Config = Config()
/**
- * Returns the difference between now and [instant], in human-readable format. Also supports
- * instants in the future. For example: an instant that's 5 hours ago will return "5 hours ago".
+ * Returns the difference between [baseInstant] and [instant], in human-readable format. Also supports
+ * instants in the future. For example, an instant that's 5 hours ago will return "5 hours ago".
*
* @param instant The [Instant] to format.
+ * @param baseInstant The base/starting [Instant], defaulting to "now".
+ * @param formatting The [FormatStyle] to use, defaulting to "long".
+ * @param parts Configures the formatting of multiple parts, defaulting to 1 part.
+ * @param units The [TimeUnit]s to limit to during formatting, not limited by default.
+ * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp].
* @return a formatted string
*/
@OptIn(ExperimentalTime::class)
public fun timeAgo(
instant: Instant,
- baseInstant: Instant = Clock.System.now()
+ baseInstant: Instant = Clock.System.now(),
+ formatting: FormatStyle = config.time.formatting,
+ parts: PartsConfig = config.time.parts,
+ units: Set = config.time.units,
+ rounding: Rounding = config.time.rounding
+ ): String {
+ return formatTimeAgo(
+ instant = instant,
+ baseInstant = baseInstant,
+ formatting = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
+ }
+
+ /**
+ * Returns the difference between [baseDate] and [date], in human-readable format. Also supports
+ * dates in the future or past. For example, a date that's 2 days ago will return "2 days ago".
+ *
+ * @param date The [LocalDate] to format.
+ * @param baseDate The base/starting [LocalDate], defaulting to "today".
+ * @param formatting The [FormatStyle] to use, defaulting to "long".
+ * @param parts Configures the formatting of multiple parts, defaulting to 1 part.
+ * @param units The [TimeUnit]s to limit to during formatting, not limited by default.
+ * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp].
+ * @return a formatted string
+ */
+ @OptIn(ExperimentalTime::class)
+ public fun timeAgo(
+ date: LocalDate,
+ baseDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()),
+ formatting: FormatStyle = config.time.formatting,
+ parts: PartsConfig = config.time.parts,
+ units: Set = config.time.units,
+ rounding: Rounding = config.time.rounding
): String {
- return safelyTranslate { formatTimeAgo(instant, baseInstant) }
+ return formatTimeAgo(
+ date = date,
+ baseDate = baseDate,
+ formatting = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
}
/**
* Returns the given [duration] in human-readable format.
- * For example: a duration of 3 seconds returns "3 seconds".
+ * For example, a duration of 3 seconds returns "3 seconds".
*
* @param duration The [Duration] to format.
- * @return a formatted string
+ * @param formatting The [FormatStyle] to use, defaulting to "long".
+ * @param parts Configures the formatting of multiple parts, defaulting to 1 part.
+ * @param units The [TimeUnit]s to limit to during formatting, not limited by default.
+ * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp].
*/
- public fun duration(duration: Duration): String {
- return safelyTranslate { formatDuration(duration, RelativeTime.Present) }
+ public fun duration(
+ duration: Duration,
+ formatting: FormatStyle = config.time.formatting,
+ parts: PartsConfig = config.time.parts,
+ units: Set = config.time.units,
+ rounding: Rounding = config.time.rounding
+ ): String {
+ return formatDuration(
+ duration = duration,
+ relativeTime = RelativeTime.Present,
+ format = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
}
/**
- * Returns the given [bytes] size in human-readable format. For example:
+ * Returns the given [bytes] size in human-readable format. For example,
* a size of 3_500_000 bytes returns "3.5 MB". Assumes base 1024.
*
- * For example, 3_5000_000 bytes returns: "3.5 MB" for EN or "3.5 Mo" for FR.
+ * For example, `3_5000_000` returns: "3.5 MB" for EN or "3.5 Mo" for FR.
*
* @param bytes The size in bytes to format.
* @param decimals The number of decimals to use in formatting.
* @return a formatted string
*/
public fun fileSize(bytes: Long, decimals: Int = 1): String {
- return safelyTranslate { formatFileSize(bytes, decimals) }
+ return formatFileSize(bytes, decimals)
}
/**
@@ -63,20 +139,20 @@ public object HumanReadable {
*
* Supported abbreviations: K (1,000), M (1,000,000), B (1,000,000,000) and T (1,000,000,000,000).
*
- * For example: 10394 returns "10K" and "4234321" returns "4M".
+ * For example, `10394` returns "10K" and `4234321` returns "4M".
*
* @param number The number to abbreviate.
* @param decimals The number of decimals to use in formatting.
* @return a formatted string
*/
public fun abbreviation(number: Number, decimals: Int = 0): String {
- return safelyTranslate { formatAbbreviation(number.toDouble(), decimals) }
+ return formatAbbreviation(number.toDouble(), decimals)
}
/**
* Formats the given [number].
*
- * For example: 1_000_000.34 returns:
+ * For example, `1_000_000.34` returns:
* - "1,000,000.34" for EN
* - "1 000 000.34" for FR
* - "1.000.000,34" for NL
@@ -86,19 +162,19 @@ public object HumanReadable {
* @return a formatted string
*/
public fun number(number: Number, decimals: Int = 0): String {
- return safelyTranslate { number.toDouble().formatNumber(decimals) }
+ return number.toDouble().formatNumber(decimals)
}
/**
* Formats the given [value] of the given [unit] to a readable distance.
*
* Metric examples:
- * - 956 with DistanceUnit METERS returns "956 m" for EN.
- * - 1534 with DistanceUnit METERS returns "1,5 km" for EN.
+ * - `956` with DistanceUnit METERS returns "956 m" for EN.
+ * - `1534` with DistanceUnit METERS returns "1,5 km" for EN.
*
* Imperial examples:
- * - 5200 with DistanceUnit FEET returns "5,200 ft" for EN.
- * - 5350 returns with DistanceUnit FEET returns "1.0 mi" for EN.
+ * - `5200` with DistanceUnit FEET returns "5,200 ft" for EN.
+ * - `5350` returns with DistanceUnit FEET returns "1.0 mi" for EN.
*
* In other languages, the numbers are formatted accordingly, see [number].
*
@@ -111,6 +187,6 @@ public object HumanReadable {
* @return a formatted string
*/
public fun distance(value: Number, unit: DistanceUnit, decimals: Int = 1): String {
- return safelyTranslate { formatDistance(value, unit, decimalsForLargeUnits = decimals) }
+ return formatDistance(value, unit, decimalsForLargeUnits = decimals)
}
}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt
index 375bdc4..8cc3ebc 100644
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt
@@ -1,6 +1,6 @@
package nl.jacobras.humanreadable
-import HumanReadableRes as Res
+import nl.jacobras.humanreadable.HumanReadable.strings
/**
* Returns the given distance [value] in human-readable format.
@@ -24,20 +24,20 @@ internal fun formatDistance(
private fun toMetric(meters: Double, decimalsForLargeUnits: Int): String {
return if (meters < 1000) {
// Use meters
- meters.formatNumber(decimals = 0) + " " + Res.string.meter_abbreviation
+ meters.formatNumber(decimals = 0) + " " + strings.distance.meterAbbreviation
} else {
// Convert to kilometers
- (meters / 1000).formatNumber(decimals = decimalsForLargeUnits) + " " + Res.string.kilometer_abbreviation
+ (meters / 1000).formatNumber(decimals = decimalsForLargeUnits) + " " + strings.distance.kilometerAbbreviation
}
}
private fun toImperial(feet: Double, decimalsForLargeUnits: Int): String {
return if (feet < MILE_IN_FEET) {
// Less than a mile, use feet
- feet.formatNumber(decimals = 0) + " " + Res.string.feet_abbreviation
+ feet.formatNumber(decimals = 0) + " " + strings.distance.feetAbbreviation
} else {
// Convert to miles
- (feet / MILE_IN_FEET).formatNumber(decimals = decimalsForLargeUnits) + " " + Res.string.mile_abbreviation
+ (feet / MILE_IN_FEET).formatNumber(decimals = decimalsForLargeUnits) + " " + strings.distance.mileAbbreviation
}
}
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt
deleted file mode 100644
index 9f38aab..0000000
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package nl.jacobras.humanreadable
-
-import io.github.skeptick.libres.LibresSettings
-import kotlin.math.roundToInt
-import kotlin.time.Duration
-
-/**
- * Returns the given [duration] in human-readable format.
- */
-internal fun formatDuration(
- duration: Duration,
- relativeTime: RelativeTime
-): String {
- val secondsAgo = duration.inWholeSeconds.toInt()
- val hoursAgo = duration.inWholeHours.toInt()
- val daysAgo = duration.inWholeDays.toInt()
- val weeksAgo = (duration.inWholeDays / 7f).roundToInt()
- val monthsAgo = (duration.inWholeDays / 30.5f).roundToInt()
- val yearsAgo = (duration.inWholeDays / 365).toInt()
-
- return when {
- secondsAgo < 60 -> {
- formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime)
- }
- secondsAgo < 3600 -> {
- val minutes = duration.inWholeMinutes.toInt()
- formatUnit(minutes, TimeUnit.Minutes, relativeTime)
- }
- daysAgo < 1 -> {
- formatUnit(hoursAgo, TimeUnit.Hours, relativeTime)
- }
- daysAgo < 7 -> {
- formatUnit(daysAgo, TimeUnit.Days, relativeTime)
- }
- daysAgo < 30 -> {
- formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime)
- }
- monthsAgo < 12 || yearsAgo == 0 -> {
- formatUnit(monthsAgo, TimeUnit.Months, relativeTime)
- }
- else -> {
- formatUnit(yearsAgo, TimeUnit.Years, relativeTime)
- }
- }
-}
-
-/**
- * Formats a [count] with its [unit]. Normally produces "$count $unit", but for Arabic the
- * singular (1) and dual (2) forms are encoded in the unit word itself, so the numeral is omitted.
- */
-private fun formatUnit(
- count: Int,
- unit: TimeUnit,
- relativeTime: RelativeTime
-): String {
- val unitText = unit.format(count, relativeTime)
- return if (LibresSettings.languageCode == "ar" && (count == 1 || count == 2)) {
- unitText
- } else {
- "$count $unitText"
- }
-}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt
index 24ebe15..b72b0d8 100644
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt
@@ -1,6 +1,6 @@
package nl.jacobras.humanreadable
-import HumanReadableRes as Res
+import nl.jacobras.humanreadable.HumanReadable.strings
/**
* Returns the given [bytes] size in human-readable format.
@@ -8,19 +8,19 @@ import HumanReadableRes as Res
internal fun formatFileSize(bytes: Long, decimals: Int): String {
return when {
bytes < 1024 -> {
- "$bytes ${Res.string.byte_symbol}"
+ "$bytes ${strings.fileSize.byteSymbol}"
}
bytes < 1_048_576 -> {
- "${(bytes / 1_024.0).formatNumber(decimals)} ${Res.string.kilobyte_symbol}"
+ "${(bytes / 1_024.0).formatNumber(decimals)} ${strings.fileSize.kilobyteSymbol}"
}
bytes < 1.07374182E9 -> {
- "${(bytes / 1_048_576.0).formatNumber(decimals)} ${Res.string.megabyte_symbol}"
+ "${(bytes / 1_048_576.0).formatNumber(decimals)} ${strings.fileSize.megabyteSymbol}"
}
bytes < 1.09951163E12 -> {
- "${(bytes / 1.07374182E9).formatNumber(decimals)} ${Res.string.gigabyte_symbol}"
+ "${(bytes / 1.07374182E9).formatNumber(decimals)} ${strings.fileSize.gigabyteSymbol}"
}
else -> {
- "${(bytes / 1.09951163E12).formatNumber(decimals)} ${Res.string.terabyte_symbol}"
+ "${(bytes / 1.09951163E12).formatNumber(decimals)} ${strings.fileSize.terabyteSymbol}"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt
index ea759e8..09d7db1 100644
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt
@@ -1,14 +1,14 @@
package nl.jacobras.humanreadable
-import HumanReadableRes as Res
+import nl.jacobras.humanreadable.HumanReadable.strings
import kotlin.math.pow
import kotlin.math.roundToLong
internal fun Double.formatNumber(
decimals: Int
): String {
- val groupSeparator = Res.string.groupSeparator.formatWithSpaceIfNeeded()
- val decimalSymbol = Res.string.decimalSymbol
+ val groupSeparator = strings.number.groupSeparator
+ val decimalSymbol = strings.number.decimalSymbol
val rounded = formatWithDecimals(decimals)
val parts = rounded.split('.')
@@ -28,7 +28,6 @@ internal fun Double.formatNumber(
val formattedIntegerPart = if (isNegative) "-$formattedNumberPart" else formattedNumberPart
-
// Format the decimal part
val decimalPart = if (parts.size > 1) parts[1] else ""
val formattedDecimalPart = if (decimals > 0) {
@@ -52,11 +51,4 @@ private fun Double.formatWithDecimals(decimals: Int): String {
} else {
"$mainRes.$fractionRes"
}
-}
-
-/**
- * Workaround for Libres returning an empty string if it contains only a space.
- */
-private fun String.formatWithSpaceIfNeeded(): String {
- return ifEmpty { " " }
}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt
deleted file mode 100644
index 819e95b..0000000
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package nl.jacobras.humanreadable
-
-import kotlin.time.ExperimentalTime
-import kotlin.time.Instant
-import HumanReadableRes as Res
-
-/**
- * Returns the difference between [baseInstant] and [instant], in human-readable format.
- * Also supports instants in the future.
- *
- * @param instant The [Instant] to compare with [baseInstant].
- * @param baseInstant The base/starting [Instant], usually "now".
- */
-@OptIn(ExperimentalTime::class)
-internal fun formatTimeAgo(
- instant: Instant,
- baseInstant: Instant
-): String {
- val diff = baseInstant - instant
- val secondsAgo = diff.inWholeSeconds
-
- return when {
- secondsAgo < 0 -> Res.string.time_in_future.format(
- formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Future)
- )
- secondsAgo <= 1 -> Res.string.now
- else -> Res.string.time_ago.format(
- formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Past)
- )
- }
-}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt
deleted file mode 100644
index 945b79c..0000000
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package nl.jacobras.humanreadable
-
-import io.github.skeptick.libres.strings.VoidPluralString
-
-/**
- * Prevents crashing consumer app on a translation issue.
- */
-internal fun safelyTranslate(block: () -> String): String {
- return try {
- block()
- } catch (e: IllegalStateException) {
- "Translation error: ${e.message ?: "IllegalStateException"}"
- }
-}
-
-/**
- * @return `null` if the given resource doesn't exist in the current locale.
- */
-internal fun VoidPluralString.optionallyFormat(number: Int): String? {
- return try {
- format(number).takeIf { it.isNotEmpty() }
- } catch (e: IllegalStateException) {
- return null
- }
-}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt
deleted file mode 100644
index 60f29fa..0000000
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package nl.jacobras.humanreadable
-
-import io.github.skeptick.libres.strings.VoidPluralString
-import HumanReadableRes as Res
-
-internal enum class TimeUnit(
- val past: () -> VoidPluralString,
- val present: () -> VoidPluralString,
- val future: () -> VoidPluralString
-) {
- Seconds(
- past = { Res.string.seconds_past },
- present = { Res.string.seconds },
- future = { Res.string.seconds_future }
- ),
- Minutes(
- past = { Res.string.minutes_past },
- present = { Res.string.minutes },
- future = { Res.string.minutes_future }
- ),
- Hours(
- past = { Res.string.hours_past },
- present = { Res.string.hours },
- future = { Res.string.hours_future }
- ),
- Days(
- past = { Res.string.days_past },
- present = { Res.string.days },
- future = { Res.string.days_future }
- ),
- Weeks(
- past = { Res.string.weeks_past },
- present = { Res.string.weeks },
- future = { Res.string.weeks_future }
- ),
- Months(
- past = { Res.string.months_past },
- present = { Res.string.months },
- future = { Res.string.months_future }
- ),
- Years(
- past = { Res.string.years_past },
- present = { Res.string.years },
- future = { Res.string.years_future }
- );
-
- fun format(value: Int, relativeTime: RelativeTime): String {
- return when (relativeTime) {
- RelativeTime.Past -> past().optionallyFormat(value) ?: present().format(value)
- RelativeTime.Present -> present().format(value)
- RelativeTime.Future -> future().optionallyFormat(value) ?: present().format(value)
- }
- }
-}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
deleted file mode 100644
index d006f43..0000000
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package nl.jacobras.humanreadable
-
-/**
- * Workaround until https://github.com/Skeptick/libres/issues/53 is fixed.
- */
-internal expect fun extendLibresPlurals()
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt
new file mode 100644
index 0000000..5500c1c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt
@@ -0,0 +1,103 @@
+package nl.jacobras.humanreadable.i18n
+
+/**
+ * All localised strings for a single language, grouped per type.
+ */
+internal data class HumanReadableStrings(
+ val dateTime: DateTimeStrings,
+ val number: NumberStrings = NumberStrings(),
+ val fileSize: FileSizeStrings = FileSizeStrings(),
+ val distance: DistanceStrings = DistanceStrings()
+)
+
+/**
+ * Relative-time and duration strings.
+ */
+internal data class DateTimeStrings(
+ /**
+ * Resolves a count to its plural category for this language.
+ *
+ * See [Unicode Plural Rules](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html).
+ */
+ val plural: (Int) -> Plural,
+
+ val secondsLong: TenseForms,
+ val minutesLong: TenseForms,
+ val hoursLong: TenseForms,
+ val daysLong: TenseForms,
+ val weeksLong: TenseForms,
+ val monthsLong: TenseForms,
+ val yearsLong: TenseForms,
+
+ val secondsShort: TenseForms,
+ val minutesShort: TenseForms,
+ val hoursShort: TenseForms,
+ val daysShort: TenseForms,
+ val weeksShort: TenseForms,
+ val monthsShort: TenseForms,
+ val yearsShort: TenseForms,
+
+ val secondsNarrow: TenseForms,
+ val minutesNarrow: TenseForms,
+ val hoursNarrow: TenseForms,
+ val daysNarrow: TenseForms,
+ val weeksNarrow: TenseForms,
+ val monthsNarrow: TenseForms,
+ val yearsNarrow: TenseForms,
+
+ /**
+ * Wraps a formatted duration as past relative time, e.g. "3 days ago".
+ */
+ val timeAgo: (String) -> String,
+
+ /**
+ * Wraps a formatted duration as future relative time, e.g. "in 3 days".
+ */
+ val timeInFuture: (String) -> String,
+
+ val now: String,
+ val today: String,
+ val yesterday: String,
+ val tomorrow: String,
+ val lessThan: String,
+ val about: String
+)
+
+/**
+ * Number-formatting symbols.
+ *
+ * Only override what differs from the default values.
+ */
+internal data class NumberStrings(
+ /**
+ * Separator used for grouping thousands.
+ */
+ val groupSeparator: String = ",",
+
+ val decimalSymbol: String = "."
+)
+
+/**
+ * File-size unit symbols.
+ *
+ * Only override what differs from the default values.
+ */
+internal data class FileSizeStrings(
+ val byteSymbol: String = "B",
+ val kilobyteSymbol: String = "kB",
+ val megabyteSymbol: String = "MB",
+ val gigabyteSymbol: String = "GB",
+ val terabyteSymbol: String = "TB"
+)
+
+/**
+ * Distance unit abbreviations.
+ *
+ * Only override what differs from the default values.
+ */
+internal data class DistanceStrings(
+ val meterAbbreviation: String = "m",
+ val kilometerAbbreviation: String = "km",
+ val feetAbbreviation: String = "ft",
+ val mileAbbreviation: String = "mi"
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt
new file mode 100644
index 0000000..6f6fc4c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt
@@ -0,0 +1,60 @@
+package nl.jacobras.humanreadable.i18n
+
+/**
+ * Holds language settings and a reference to the current translation strings.
+ */
+internal class Localisation {
+
+ private var currentTagAndStrings: Pair? = null
+
+ /**
+ * The requested language tag (e.g. `"en"`, `"fr"`). A language tag with a region, e.g. `"en-US"`, may also
+ * be passed in. The region code is ignored.
+ *
+ * @see languageTag for the actual language tag used, which may differ if the requested one is not supported.
+ */
+ var requestedLanguageTag = systemLanguageTag()
+ set(value) {
+ field = value.lowercase().substringBefore("-")
+ currentTagAndStrings = null
+ }
+
+ /**
+ * The fallback language tag to use if [requestedLanguageTag] is not supported.
+ *
+ * If this field is changed to a language tag that is not supported, nothing changes.
+ */
+ var fallbackLanguageTag = "en"
+ set(value) {
+ if (translations.containsKey(value)) {
+ field = value
+ currentTagAndStrings = null
+ }
+ }
+
+ /**
+ * The currently used language tag (e.g. `"en"`, `"fr"`).
+ */
+ val languageTag: String
+ get() {
+ val (tag, _) = resolve()
+ return tag
+ }
+
+ /**
+ * The current set of translation strings.
+ */
+ val currentStrings: HumanReadableStrings
+ get() {
+ val (_, strings) = resolve()
+ return strings
+ }
+
+ private fun resolve(): Pair {
+ return currentTagAndStrings ?: run {
+ val tag = requestedLanguageTag.takeIf { it in translations } ?: fallbackLanguageTag
+ val strings = translations[tag] ?: error("Internal error: unsupported language tag $tag")
+ (tag to strings).also { currentTagAndStrings = it }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt
new file mode 100644
index 0000000..d6b69e6
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt
@@ -0,0 +1,15 @@
+package nl.jacobras.humanreadable.i18n
+
+/**
+ * CLDR plural categories.
+ *
+ * See [Unicode Plural Rules](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html).
+ */
+internal enum class Plural {
+ Zero,
+ One,
+ Two,
+ Few,
+ Many,
+ Other
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt
new file mode 100644
index 0000000..11e0418
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt
@@ -0,0 +1,113 @@
+@file:Suppress("LongParameterList")
+
+package nl.jacobras.humanreadable.i18n
+
+/**
+ * The word forms for a single unit (e.g. "minute") in a single language.
+ *
+ * @property present The word forms for the present tense. E.g. "minute" for [Plural.One]
+ * and "minutes" for [Plural.Other].
+ * @property past The word forms for the past tense, if they differ from [present].
+ * @property future The word forms for the future tense, if they differ from [present].
+ */
+internal data class TenseForms(
+ val present: Map,
+ val past: Map = emptyMap(),
+ val future: Map = emptyMap()
+)
+
+/**
+ * Builds a map of plural category -> word entries, ignoring empty ones.
+ */
+private fun buildMap(
+ zero: String = "",
+ one: String = "",
+ two: String = "",
+ few: String = "",
+ many: String = "",
+ other: String = ""
+): Map = buildMap {
+ if (zero.isNotEmpty()) put(Plural.Zero, zero)
+ if (one.isNotEmpty()) put(Plural.One, one)
+ if (two.isNotEmpty()) put(Plural.Two, two)
+ if (few.isNotEmpty()) put(Plural.Few, few)
+ if (many.isNotEmpty()) put(Plural.Many, many)
+ if (other.isNotEmpty()) put(Plural.Other, other)
+}
+
+/**
+ * Shortcut for languages that don't need past/future grammatical-case overrides.
+ *
+ * - If there's no difference between the different categories, just fill [one].
+ * - Use [multipleTenses] when past/future grammatical-case overrides are needed.
+ */
+internal fun presentTense(
+ zero: String = "",
+ one: String = "",
+ two: String = "",
+ few: String = "",
+ many: String = "",
+ other: String = ""
+): TenseForms = TenseForms(present = buildMap(zero, one, two, few, many, other))
+
+/**
+ * Builds [TenseForms] for a unit that needs past/future grammatical-case overrides. For
+ * present-only units prefer [presentTense].
+ */
+internal fun multipleTenses(block: TenseFormsBuilder.() -> Unit): TenseForms {
+ return TenseFormsBuilder().apply(block).build()
+}
+
+/** DSL for building [TenseForms] with present and optional past/future overrides. */
+@DslMarker
+private annotation class TenseFormsDsl
+
+/**
+ * Builder for [TenseForms].
+ *
+ * - Use [present] to set the base forms.
+ * - Use [past]/[future] to override the past/future forms.
+ *
+ * For languages where past/future are identical (but different from present), use [pastOrFuture].
+ */
+@TenseFormsDsl
+internal class TenseFormsBuilder {
+ private var present: Map = emptyMap()
+ private var past: Map = emptyMap()
+ private var future: Map = emptyMap()
+
+ fun present(
+ zero: String = "", one: String = "", two: String = "",
+ few: String = "", many: String = "", other: String = ""
+ ) {
+ present = buildMap(zero, one, two, few, many, other)
+ }
+
+ fun past(
+ zero: String = "", one: String = "", two: String = "",
+ few: String = "", many: String = "", other: String = ""
+ ) {
+ past = buildMap(zero, one, two, few, many, other)
+ }
+
+ fun future(
+ zero: String = "", one: String = "", two: String = "",
+ few: String = "", many: String = "", other: String = ""
+ ) {
+ future = buildMap(zero, one, two, few, many, other)
+ }
+
+ /**
+ * Sets both [past] and [future] to the same forms, for languages where they are identical.
+ */
+ fun pastOrFuture(
+ zero: String = "", one: String = "", two: String = "",
+ few: String = "", many: String = "", other: String = ""
+ ) {
+ val forms = buildMap(zero, one, two, few, many, other)
+ past = forms
+ future = forms
+ }
+
+ fun build(): TenseForms = TenseForms(present = present, past = past, future = future)
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.kt
new file mode 100644
index 0000000..fad8ee9
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.kt
@@ -0,0 +1,6 @@
+package nl.jacobras.humanreadable.i18n
+
+/**
+ * Returns the current platform language tag (e.g. "en", "fr").
+ */
+internal expect fun systemLanguageTag(): String
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt
new file mode 100644
index 0000000..38c50cc
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt
@@ -0,0 +1,52 @@
+package nl.jacobras.humanreadable.i18n
+
+import nl.jacobras.humanreadable.i18n.translations.ArStrings
+import nl.jacobras.humanreadable.i18n.translations.CsStrings
+import nl.jacobras.humanreadable.i18n.translations.DeStrings
+import nl.jacobras.humanreadable.i18n.translations.ElStrings
+import nl.jacobras.humanreadable.i18n.translations.EnStrings
+import nl.jacobras.humanreadable.i18n.translations.EsStrings
+import nl.jacobras.humanreadable.i18n.translations.FiStrings
+import nl.jacobras.humanreadable.i18n.translations.FrStrings
+import nl.jacobras.humanreadable.i18n.translations.IdStrings
+import nl.jacobras.humanreadable.i18n.translations.ItStrings
+import nl.jacobras.humanreadable.i18n.translations.JaStrings
+import nl.jacobras.humanreadable.i18n.translations.KkStrings
+import nl.jacobras.humanreadable.i18n.translations.KoStrings
+import nl.jacobras.humanreadable.i18n.translations.NlStrings
+import nl.jacobras.humanreadable.i18n.translations.PlStrings
+import nl.jacobras.humanreadable.i18n.translations.PtStrings
+import nl.jacobras.humanreadable.i18n.translations.RuStrings
+import nl.jacobras.humanreadable.i18n.translations.TrStrings
+import nl.jacobras.humanreadable.i18n.translations.UkStrings
+import nl.jacobras.humanreadable.i18n.translations.UzStrings
+import nl.jacobras.humanreadable.i18n.translations.ViStrings
+import nl.jacobras.humanreadable.i18n.translations.ZhStrings
+
+/**
+ * All supported translations. Make sure to add a new entry here if you add a new language.
+ */
+internal val translations: Map = mapOf(
+ "ar" to ArStrings,
+ "cs" to CsStrings,
+ "de" to DeStrings,
+ "el" to ElStrings,
+ "en" to EnStrings,
+ "es" to EsStrings,
+ "fi" to FiStrings,
+ "fr" to FrStrings,
+ "id" to IdStrings,
+ "it" to ItStrings,
+ "ja" to JaStrings,
+ "kk" to KkStrings,
+ "ko" to KoStrings,
+ "nl" to NlStrings,
+ "pl" to PlStrings,
+ "pt" to PtStrings,
+ "ru" to RuStrings,
+ "tr" to TrStrings,
+ "uk" to UkStrings,
+ "uz" to UzStrings,
+ "vi" to ViStrings,
+ "zh" to ZhStrings
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt
new file mode 100644
index 0000000..10a94b1
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt
@@ -0,0 +1,87 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.DistanceStrings
+import nl.jacobras.humanreadable.i18n.FileSizeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+
+internal val ArStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when {
+ count == 0 -> Plural.Zero
+ count == 1 -> Plural.One
+ count == 2 -> Plural.Two
+ count % 100 in 3..10 -> Plural.Few
+ count % 100 in 11..99 -> Plural.Many
+ else -> Plural.Other
+ }
+ },
+ secondsLong = multipleTenses {
+ present(zero = "ثانية", one = "ثانية", two = "ثانيتان", few = "ثوان", many = "ثانية", other = "ثانية")
+ pastOrFuture(two = "ثانيتين")
+ },
+ minutesLong = multipleTenses {
+ present(zero = "دقيقة", one = "دقيقة", two = "دقيقتان", few = "دقائق", many = "دقيقة", other = "دقيقة")
+ pastOrFuture(two = "دقيقتين")
+ },
+ hoursLong = multipleTenses {
+ present(zero = "ساعة", one = "ساعة", two = "ساعتان", few = "ساعات", many = "ساعة", other = "ساعة")
+ pastOrFuture(two = "ساعتين")
+ },
+ daysLong = multipleTenses {
+ present(zero = "يوم", one = "يوم", two = "يومان", few = "أيام", many = "يوم", other = "يوم")
+ pastOrFuture(two = "يومين")
+ },
+ weeksLong = multipleTenses {
+ present(zero = "أسبوع", one = "أسبوع", two = "أسبوعان", few = "أسابيع", many = "أسبوع", other = "أسبوع")
+ pastOrFuture(two = "أسبوعين")
+ },
+ monthsLong = multipleTenses {
+ present(zero = "شهر", one = "شهر", two = "شهران", few = "أشهر", many = "شهر", other = "شهر")
+ pastOrFuture(two = "شهرين")
+ },
+ yearsLong = multipleTenses {
+ present(zero = "سنة", one = "سنة", two = "سنتان", few = "سنوات", many = "سنة", other = "سنة")
+ pastOrFuture(two = "سنتين")
+ },
+ secondsShort = presentTense(one = "ثانية"),
+ minutesShort = presentTense(one = "دقيقة"),
+ hoursShort = presentTense(one = "ساعة"),
+ daysShort = presentTense(one = "يوم"),
+ weeksShort = presentTense(one = "أسبوع"),
+ monthsShort = presentTense(one = "شهر"),
+ yearsShort = presentTense(one = "سنة"),
+ secondsNarrow = presentTense(one = "ث"),
+ minutesNarrow = presentTense(one = "د"),
+ hoursNarrow = presentTense(one = "س"),
+ daysNarrow = presentTense(one = "ي"),
+ weeksNarrow = presentTense(one = "أ"),
+ monthsNarrow = presentTense(one = "شهر"),
+ yearsNarrow = presentTense(one = "سنة"),
+ timeAgo = { "قبل $it" },
+ timeInFuture = { "بعد $it" },
+ now = "الآن",
+ today = "اليوم",
+ tomorrow = "أمس",
+ yesterday = "غداً",
+ lessThan = "أقل من",
+ about = "حوالي"
+ ),
+ fileSize = FileSizeStrings(
+ byteSymbol = "ب",
+ kilobyteSymbol = "ك.ب",
+ megabyteSymbol = "م.ب",
+ gigabyteSymbol = "ج.ب",
+ terabyteSymbol = "ت.ب"
+ ),
+ distance = DistanceStrings(
+ meterAbbreviation = "م",
+ kilometerAbbreviation = "كم",
+ feetAbbreviation = "قدم",
+ mileAbbreviation = "ميل"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt
new file mode 100644
index 0000000..a049530
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt
@@ -0,0 +1,74 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val CsStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when (count) {
+ 1 -> Plural.One
+ in 2..4 -> Plural.Few
+ else -> Plural.Other
+ }
+ },
+ secondsLong = multipleTenses {
+ present(one = "sekunda", few = "sekundy", other = "sekund")
+ past(one = "sekundou", few = "sekundami", other = "sekundami")
+ future(one = "sekundu", few = "sekundy", other = "sekund")
+ },
+ minutesLong = multipleTenses {
+ present(one = "minuta", few = "minuty", other = "minut")
+ past(one = "minutou", few = "minutami", other = "minutami")
+ future(one = "minutu", few = "minuty", other = "minut")
+ },
+ hoursLong = multipleTenses {
+ present(one = "hodina", few = "hodiny", other = "hodin")
+ past(one = "hodinou", few = "hodinami", other = "hodinami")
+ future(one = "hodinu", few = "hodiny", other = "hodin")
+ },
+ daysLong = multipleTenses {
+ present(one = "den", few = "dny", other = "dní")
+ past(one = "dnem", few = "dny", other = "dny")
+ },
+ weeksLong = multipleTenses {
+ present(one = "týden", few = "týdny", other = "týdnů")
+ past(one = "týdnem", few = "týdny", other = "týdny")
+ },
+ monthsLong = multipleTenses {
+ present(one = "měsíc", few = "měsíce", other = "měsíců")
+ past(one = "měsícem", few = "měsíci", other = "měsíci")
+ },
+ yearsLong = multipleTenses {
+ present(one = "rok", few = "roky", other = "let")
+ past(one = "rokem", few = "roky", other = "lety")
+ },
+ secondsShort = presentTense(one = "s"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "h"),
+ daysShort = presentTense(one = "d"),
+ weeksShort = presentTense(one = "týd."),
+ monthsShort = presentTense(one = "měs."),
+ yearsShort = presentTense(one = "r."),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "h"),
+ daysNarrow = presentTense(one = "d"),
+ weeksNarrow = presentTense(one = "t"),
+ monthsNarrow = presentTense(one = "m"),
+ yearsNarrow = presentTense(one = "r"),
+ timeAgo = { "před $it" },
+ timeInFuture = { "za $it" },
+ now = "nyní",
+ today = "dnes",
+ yesterday = "včera",
+ tomorrow = "zítra",
+ lessThan = "méně než",
+ about = "přibližně"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt
new file mode 100644
index 0000000..e99b9d3
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt
@@ -0,0 +1,53 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val DeStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "Sekunde", other = "Sekunden"),
+ minutesLong = presentTense(one = "Minute", other = "Minuten"),
+ hoursLong = presentTense(one = "Stunde", other = "Stunden"),
+ daysLong = multipleTenses {
+ present(one = "Tag", other = "Tage")
+ pastOrFuture(other = "Tagen")
+ },
+ weeksLong = presentTense(one = "Woche", other = "Wochen"),
+ monthsLong = multipleTenses {
+ present(one = "Monat", other = "Monate")
+ pastOrFuture(other = "Monaten")
+ },
+ yearsLong = multipleTenses {
+ present(one = "Jahr", other = "Jahre")
+ pastOrFuture(other = "Jahren")
+ },
+ secondsShort = presentTense(one = "Sek."),
+ minutesShort = presentTense(one = "Min."),
+ hoursShort = presentTense(one = "Std."),
+ daysShort = presentTense(one = "Tg."),
+ weeksShort = presentTense(one = "Wo."),
+ monthsShort = presentTense(one = "Mon."),
+ yearsShort = presentTense(one = "J."),
+ secondsNarrow = presentTense(one = "Sek."),
+ minutesNarrow = presentTense(one = "Min."),
+ hoursNarrow = presentTense(one = "Std."),
+ daysNarrow = presentTense(one = "T"),
+ weeksNarrow = presentTense(one = "Wo."),
+ monthsNarrow = presentTense(one = "M"),
+ yearsNarrow = presentTense(one = "J"),
+ timeAgo = { "vor $it" },
+ timeInFuture = { "in $it" },
+ now = "jetzt",
+ today = "heute",
+ yesterday = "gestern",
+ tomorrow = "morgen",
+ lessThan = "weniger als",
+ about = "etwa"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt
new file mode 100644
index 0000000..db92acf
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt
@@ -0,0 +1,47 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val ElStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "δευτερόλεπτο", other = "δευτερόλεπτα"),
+ minutesLong = presentTense(one = "λεπτό", other = "λεπτά"),
+ hoursLong = presentTense(one = "ώρα", other = "ώρες"),
+ daysLong = presentTense(one = "μέρα", other = "μέρες"),
+ weeksLong = presentTense(one = "εβδομάδα", other = "εβδομάδες"),
+ monthsLong = multipleTenses {
+ present(one = "μήνας", other = "μήνες")
+ pastOrFuture(one = "μήνα")
+ },
+ yearsLong = presentTense(one = "έτος", other = "έτη"),
+ secondsShort = presentTense(one = "δευτ."),
+ minutesShort = presentTense(one = "λεπ."),
+ hoursShort = presentTense(one = "ώ."),
+ daysShort = presentTense(one = "ημ."),
+ weeksShort = presentTense(one = "εβδ."),
+ monthsShort = presentTense(one = "μήν."),
+ yearsShort = presentTense(one = "έτ."),
+ secondsNarrow = presentTense(one = "δ"),
+ minutesNarrow = presentTense(one = "λ"),
+ hoursNarrow = presentTense(one = "ώ"),
+ daysNarrow = presentTense(one = "η"),
+ weeksNarrow = presentTense(one = "ε"),
+ monthsNarrow = presentTense(one = "μ"),
+ yearsNarrow = presentTense(one = "έ"),
+ timeAgo = { "$it πριν" },
+ timeInFuture = { "σε $it" },
+ now = "τώρα",
+ today = "σήμερα",
+ yesterday = "χθες",
+ tomorrow = "αύριο",
+ lessThan = "λιγότερο από",
+ about = "περίπου"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt
new file mode 100644
index 0000000..ff9af90
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt
@@ -0,0 +1,52 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+
+/**
+ * English is the default language that will be used if no better translation is found.
+ */
+internal val EnStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+
+ secondsLong = presentTense(one = "second", other = "seconds"),
+ secondsShort = presentTense(one = "sec", other = "sec"),
+ secondsNarrow = presentTense(one = "s"),
+
+ minutesLong = presentTense(one = "minute", other = "minutes"),
+ minutesShort = presentTense(one = "min"),
+ minutesNarrow = presentTense(one = "m"),
+
+ hoursLong = presentTense(one = "hour", other = "hours"),
+ hoursShort = presentTense(one = "hr"),
+ hoursNarrow = presentTense(one = "h"),
+
+ daysLong = presentTense(one = "day", other = "days"),
+ daysShort = presentTense(one = "day", other = "days"),
+ daysNarrow = presentTense(one = "d"),
+
+ weeksLong = presentTense(one = "week", other = "weeks"),
+ weeksShort = presentTense(one = "wk", other = "wks"),
+ weeksNarrow = presentTense(one = "w"),
+
+ monthsLong = presentTense(one = "month", other = "months"),
+ monthsShort = presentTense(one = "mth", other = "mths"),
+ monthsNarrow = presentTense(one = "m"),
+
+ yearsLong = presentTense(one = "year", other = "years"),
+ yearsShort = presentTense(one = "yr", other = "yrs"),
+ yearsNarrow = presentTense(one = "y"),
+
+ timeAgo = { "$it ago" },
+ timeInFuture = { "in $it" },
+ now = "now",
+ today = "today",
+ yesterday = "yesterday",
+ tomorrow = "tomorrow",
+ lessThan = "less than",
+ about = "about"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt
new file mode 100644
index 0000000..269d46f
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val EsStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "segundo", other = "segundos"),
+ minutesLong = presentTense(one = "minuto", other = "minutos"),
+ hoursLong = presentTense(one = "hora", other = "horas"),
+ daysLong = presentTense(one = "día", other = "días"),
+ weeksLong = presentTense(one = "semana", other = "semanas"),
+ monthsLong = presentTense(one = "mes", other = "meses"),
+ yearsLong = presentTense(one = "año", other = "años"),
+ secondsShort = presentTense(one = "s"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "h"),
+ daysShort = presentTense(one = "d"),
+ weeksShort = presentTense(one = "sem."),
+ monthsShort = presentTense(one = "m."),
+ yearsShort = presentTense(one = "a"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "h"),
+ daysNarrow = presentTense(one = "d"),
+ weeksNarrow = presentTense(one = "sem."),
+ monthsNarrow = presentTense(one = "m."),
+ yearsNarrow = presentTense(one = "a"),
+ timeAgo = { "hace $it" },
+ timeInFuture = { "en $it" },
+ now = "ahora",
+ today = "hoy",
+ yesterday = "ayer",
+ tomorrow = "mañana",
+ lessThan = "menos de",
+ about = "aprox."
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt
new file mode 100644
index 0000000..0db9625
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt
@@ -0,0 +1,73 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.FileSizeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val FiStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = multipleTenses {
+ present(one = "sekunti", other = "sekuntia")
+ future(one = "sekunnin", other = "sekunnin")
+ },
+ minutesLong = multipleTenses {
+ present(one = "minuutti", other = "minuuttia")
+ future(one = "minuutin", other = "minuutin")
+ },
+ hoursLong = multipleTenses {
+ present(one = "tunti", other = "tuntia")
+ future(one = "tunnin", other = "tunnin")
+ },
+ daysLong = multipleTenses {
+ present(one = "päivä", other = "päivää")
+ future(one = "päivän", other = "päivän")
+ },
+ weeksLong = multipleTenses {
+ present(one = "viikko", other = "viikkoa")
+ future(one = "viikon", other = "viikon")
+ },
+ monthsLong = multipleTenses {
+ present(one = "kuukausi", other = "kuukautta")
+ future(one = "kuukauden", other = "kuukauden")
+ },
+ yearsLong = multipleTenses {
+ present(one = "vuosi", other = "vuotta")
+ future(one = "vuoden", other = "vuoden")
+ },
+ secondsShort = presentTense(one = "s"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "t"),
+ daysShort = presentTense(one = "pv"),
+ weeksShort = presentTense(one = "vk"),
+ monthsShort = presentTense(one = "kk"),
+ yearsShort = presentTense(one = "v"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "t"),
+ daysNarrow = presentTense(one = "pv"),
+ weeksNarrow = presentTense(one = "vk"),
+ monthsNarrow = presentTense(one = "kk"),
+ yearsNarrow = presentTense(one = "v"),
+ timeAgo = { "$it sitten" },
+ timeInFuture = { "$it kuluttua" },
+ now = "nyt",
+ today = "tänään",
+ yesterday = "eilen",
+ tomorrow = "huomenna",
+ lessThan = "alle",
+ about = "noin"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ","),
+ fileSize = FileSizeStrings(
+ byteSymbol = "t",
+ kilobyteSymbol = "kt",
+ megabyteSymbol = "Mt",
+ gigabyteSymbol = "Gt",
+ terabyteSymbol = "Tt"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt
new file mode 100644
index 0000000..bc2a6cc
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt
@@ -0,0 +1,51 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.FileSizeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val FrStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 0 || count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "seconde", other = "secondes"),
+ minutesLong = presentTense(one = "minute", other = "minutes"),
+ hoursLong = presentTense(one = "heure", other = "heures"),
+ daysLong = presentTense(one = "jour", other = "jours"),
+ weeksLong = presentTense(one = "semaine", other = "semaines"),
+ monthsLong = presentTense(one = "mois", other = "mois"),
+ yearsLong = presentTense(one = "an", other = "ans"),
+ secondsShort = presentTense(one = "s"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "h"),
+ daysShort = presentTense(one = "j"),
+ weeksShort = presentTense(one = "sem."),
+ monthsShort = presentTense(one = "m."),
+ yearsShort = presentTense(one = "an"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "h"),
+ daysNarrow = presentTense(one = "j"),
+ weeksNarrow = presentTense(one = "sem."),
+ monthsNarrow = presentTense(one = "m."),
+ yearsNarrow = presentTense(one = "an"),
+ timeAgo = { "il y a $it" },
+ timeInFuture = { "dans $it" },
+ now = "maintenant",
+ today = "aujourd'hui",
+ yesterday = "hier",
+ tomorrow = "demain",
+ lessThan = "moins de",
+ about = "environ"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ","),
+ fileSize = FileSizeStrings(
+ byteSymbol = "o",
+ kilobyteSymbol = "ko",
+ megabyteSymbol = "Mo",
+ gigabyteSymbol = "Go",
+ terabyteSymbol = "To"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt
new file mode 100644
index 0000000..2d8e8b7
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val IdStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { Plural.Other },
+ secondsLong = presentTense(other = "detik"),
+ minutesLong = presentTense(other = "menit"),
+ hoursLong = presentTense(other = "jam"),
+ daysLong = presentTense(other = "hari"),
+ weeksLong = presentTense(other = "minggu"),
+ monthsLong = presentTense(other = "bulan"),
+ yearsLong = presentTense(other = "tahun"),
+ secondsShort = presentTense(other = "dtk"),
+ minutesShort = presentTense(other = "mnt"),
+ hoursShort = presentTense(other = "jam"),
+ daysShort = presentTense(other = "h"),
+ weeksShort = presentTense(other = "mgg"),
+ monthsShort = presentTense(other = "bln"),
+ yearsShort = presentTense(other = "thn"),
+ secondsNarrow = presentTense(other = "dtk"),
+ minutesNarrow = presentTense(other = "mnt"),
+ hoursNarrow = presentTense(other = "jam"),
+ daysNarrow = presentTense(other = "h"),
+ weeksNarrow = presentTense(other = "mg"),
+ monthsNarrow = presentTense(other = "bln"),
+ yearsNarrow = presentTense(other = "thn"),
+ timeAgo = { "$it yang lalu" },
+ timeInFuture = { "dalam $it" },
+ now = "sekarang",
+ today = "hari ini",
+ yesterday = "kemarin",
+ tomorrow = "besok",
+ lessThan = "kurang dari",
+ about = "sekitar"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt
new file mode 100644
index 0000000..4028713
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val ItStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "secondo", other = "secondi"),
+ minutesLong = presentTense(one = "minuto", other = "minuti"),
+ hoursLong = presentTense(one = "ora", other = "ore"),
+ daysLong = presentTense(one = "giorno", other = "giorni"),
+ weeksLong = presentTense(one = "settimana", other = "settimane"),
+ monthsLong = presentTense(one = "mese", other = "mesi"),
+ yearsLong = presentTense(one = "anno", other = "anni"),
+ secondsShort = presentTense(one = "sec"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "h"),
+ daysShort = presentTense(one = "g"),
+ weeksShort = presentTense(one = "sett."),
+ monthsShort = presentTense(one = "mese"),
+ yearsShort = presentTense(one = "anno"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "h"),
+ daysNarrow = presentTense(one = "g"),
+ weeksNarrow = presentTense(one = "sett."),
+ monthsNarrow = presentTense(one = "m."),
+ yearsNarrow = presentTense(one = "a"),
+ timeAgo = { "$it fa" },
+ timeInFuture = { "tra $it" },
+ now = "adesso",
+ today = "oggi",
+ yesterday = "ieri",
+ tomorrow = "domani",
+ lessThan = "meno di",
+ about = "circa"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt
new file mode 100644
index 0000000..75b83c5
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt
@@ -0,0 +1,41 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+
+internal val JaStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { Plural.Other },
+ secondsLong = presentTense(other = "秒"),
+ minutesLong = presentTense(other = "分"),
+ hoursLong = presentTense(other = "時間"),
+ daysLong = presentTense(other = "日"),
+ weeksLong = presentTense(other = "週"),
+ monthsLong = presentTense(other = "ヶ月"),
+ yearsLong = presentTense(other = "年"),
+ secondsShort = presentTense(other = "秒"),
+ minutesShort = presentTense(other = "分"),
+ hoursShort = presentTense(other = "時間"),
+ daysShort = presentTense(other = "日"),
+ weeksShort = presentTense(other = "週間"),
+ monthsShort = presentTense(other = "か月"),
+ yearsShort = presentTense(other = "年"),
+ secondsNarrow = presentTense(other = "秒"),
+ minutesNarrow = presentTense(other = "分"),
+ hoursNarrow = presentTense(other = "時"),
+ daysNarrow = presentTense(other = "日"),
+ weeksNarrow = presentTense(other = "週"),
+ monthsNarrow = presentTense(other = "月"),
+ yearsNarrow = presentTense(other = "年"),
+ timeAgo = { "${it}前" },
+ timeInFuture = { "${it}後" },
+ now = "今",
+ today = "今日",
+ yesterday = "昨日",
+ tomorrow = "明日",
+ lessThan = "未満",
+ about = "約"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt
new file mode 100644
index 0000000..c59f69c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt
@@ -0,0 +1,51 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+
+internal val KkStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = multipleTenses {
+ present(one = "секунд", other = "секунд")
+ future(one = "секундтан", other = "секундтан")
+ },
+ minutesLong = multipleTenses {
+ present(one = "минут", other = "минут")
+ future(one = "минуттан", other = "минуттан")
+ },
+ hoursLong = multipleTenses {
+ present(one = "сағат", other = "сағат")
+ future(one = "сағаттан", other = "сағаттан")
+ },
+ daysLong = multipleTenses { present(one = "күн", other = "күн"); future(one = "күннен", other = "күннен") },
+ weeksLong = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") },
+ monthsLong = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") },
+ yearsLong = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") },
+ secondsShort = presentTense(one = "сек"),
+ minutesShort = presentTense(one = "мин"),
+ hoursShort = presentTense(one = "сағ"),
+ daysShort = presentTense(one = "күн"),
+ weeksShort = presentTense(one = "ап."),
+ monthsShort = presentTense(one = "ай"),
+ yearsShort = presentTense(one = "ж."),
+ secondsNarrow = presentTense(one = "с"),
+ minutesNarrow = presentTense(one = "мин"),
+ hoursNarrow = presentTense(one = "сағ"),
+ daysNarrow = presentTense(one = "к"),
+ weeksNarrow = presentTense(one = "ап"),
+ monthsNarrow = presentTense(one = "ай"),
+ yearsNarrow = presentTense(one = "ж"),
+ timeAgo = { "$it бұрын" },
+ timeInFuture = { "$it кейін" },
+ now = "қазір",
+ today = "бүгін",
+ yesterday = "кеше",
+ tomorrow = "ертең",
+ lessThan = "кем",
+ about = "шамамен"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt
new file mode 100644
index 0000000..5c0e644
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt
@@ -0,0 +1,41 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+
+internal val KoStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { Plural.Other },
+ secondsLong = presentTense(other = "초"),
+ minutesLong = presentTense(other = "분"),
+ hoursLong = presentTense(other = "시간"),
+ daysLong = presentTense(other = "일"),
+ weeksLong = presentTense(other = "주"),
+ monthsLong = presentTense(other = "개월"),
+ yearsLong = presentTense(other = "년"),
+ secondsShort = presentTense(other = "초"),
+ minutesShort = presentTense(other = "분"),
+ hoursShort = presentTense(other = "시간"),
+ daysShort = presentTense(other = "일"),
+ weeksShort = presentTense(other = "주"),
+ monthsShort = presentTense(other = "개월"),
+ yearsShort = presentTense(other = "년"),
+ secondsNarrow = presentTense(other = "초"),
+ minutesNarrow = presentTense(other = "분"),
+ hoursNarrow = presentTense(other = "시"),
+ daysNarrow = presentTense(other = "일"),
+ weeksNarrow = presentTense(other = "주"),
+ monthsNarrow = presentTense(other = "개월"),
+ yearsNarrow = presentTense(other = "년"),
+ timeAgo = { "$it 전" },
+ timeInFuture = { "$it 후" },
+ now = "지금",
+ today = "오늘",
+ yesterday = "어제",
+ tomorrow = "내일",
+ lessThan = "미만",
+ about = "약"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt
new file mode 100644
index 0000000..237e6a1
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val NlStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(one = "seconde", other = "seconden"),
+ minutesLong = presentTense(one = "minuut", other = "minuten"),
+ hoursLong = presentTense(one = "uur", other = "uur"),
+ daysLong = presentTense(one = "dag", other = "dagen"),
+ weeksLong = presentTense(one = "week", other = "weken"),
+ monthsLong = presentTense(one = "maand", other = "maanden"),
+ yearsLong = presentTense(one = "jaar", other = "jaar"),
+ secondsShort = presentTense(one = "sec"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "uur"),
+ daysShort = presentTense(one = "d"),
+ weeksShort = presentTense(one = "wk"),
+ monthsShort = presentTense(one = "mnd"),
+ yearsShort = presentTense(one = "jr"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "m"),
+ hoursNarrow = presentTense(one = "u"),
+ daysNarrow = presentTense(one = "d"),
+ weeksNarrow = presentTense(one = "w"),
+ monthsNarrow = presentTense(one = "mnd"),
+ yearsNarrow = presentTense(one = "j"),
+ timeAgo = { "$it geleden" },
+ timeInFuture = { "over $it" },
+ now = "nu",
+ today = "vandaag",
+ yesterday = "gisteren",
+ tomorrow = "morgen",
+ lessThan = "minder dan",
+ about = "ongeveer"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt
new file mode 100644
index 0000000..34e173a
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt
@@ -0,0 +1,59 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val PlStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when {
+ count == 1 -> Plural.One
+ count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few
+ else -> Plural.Many
+ }
+ },
+ secondsLong = multipleTenses {
+ present(one = "sekunda", few = "sekundy", many = "sekund")
+ pastOrFuture(one = "sekundę")
+ },
+ minutesLong = multipleTenses {
+ present(one = "minuta", few = "minuty", many = "minut")
+ pastOrFuture(one = "minutę")
+ },
+ hoursLong = multipleTenses {
+ present(one = "godzina", few = "godziny", many = "godzin")
+ pastOrFuture(one = "godzinę")
+ },
+ daysLong = presentTense(one = "dzień", few = "dni", many = "dni"),
+ weeksLong = presentTense(one = "tydzień", few = "tygodnie", many = "tygodni"),
+ monthsLong = presentTense(one = "miesiąc", few = "miesiące", many = "miesięcy"),
+ yearsLong = presentTense(one = "rok", few = "lata", many = "lat"),
+ secondsShort = presentTense(one = "sek."),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "godz."),
+ daysShort = presentTense(one = "dz."),
+ weeksShort = presentTense(one = "tydz."),
+ monthsShort = presentTense(one = "mies."),
+ yearsShort = presentTense(one = "r."),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "g"),
+ daysNarrow = presentTense(one = "d"),
+ weeksNarrow = presentTense(one = "t"),
+ monthsNarrow = presentTense(one = "m"),
+ yearsNarrow = presentTense(one = "r"),
+ timeAgo = { "$it temu" },
+ timeInFuture = { "za $it" },
+ now = "teraz",
+ today = "dzisiaj",
+ yesterday = "wczoraj",
+ tomorrow = "jutro",
+ lessThan = "mniej niż",
+ about = "około"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt
new file mode 100644
index 0000000..bf50290
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt
@@ -0,0 +1,49 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val PtStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when {
+ count == 0 || count == 1 -> Plural.One
+ count % 1_000_000 == 0 -> Plural.Many
+ else -> Plural.Other
+ }
+ },
+ secondsLong = presentTense(one = "segundo", many = "de segundos", other = "segundos"),
+ minutesLong = presentTense(one = "minuto", many = "de minutos", other = "minutos"),
+ hoursLong = presentTense(one = "hora", many = "de horas", other = "horas"),
+ daysLong = presentTense(one = "dia", many = "de dias", other = "dias"),
+ weeksLong = presentTense(one = "semana", many = "de semanas", other = "semanas"),
+ monthsLong = presentTense(one = "mês", many = "de meses", other = "meses"),
+ yearsLong = presentTense(one = "ano", many = "de anos", other = "anos"),
+ secondsShort = presentTense(one = "s"),
+ minutesShort = presentTense(one = "min"),
+ hoursShort = presentTense(one = "h"),
+ daysShort = presentTense(one = "dia"),
+ weeksShort = presentTense(one = "sem."),
+ monthsShort = presentTense(one = "mês"),
+ yearsShort = presentTense(one = "ano"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "min"),
+ hoursNarrow = presentTense(one = "h"),
+ daysNarrow = presentTense(one = "d"),
+ weeksNarrow = presentTense(one = "sem."),
+ monthsNarrow = presentTense(one = "m."),
+ yearsNarrow = presentTense(one = "a"),
+ timeAgo = { "há $it" },
+ timeInFuture = { "em $it" },
+ now = "agora",
+ today = "hoje",
+ yesterday = "ontem",
+ tomorrow = "amanhã",
+ lessThan = "menos de",
+ about = "cerca de"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt
new file mode 100644
index 0000000..4deb522
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt
@@ -0,0 +1,59 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val RuStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when {
+ count % 10 == 1 && count % 100 != 11 -> Plural.One
+ count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few
+ else -> Plural.Many
+ }
+ },
+ secondsLong = multipleTenses {
+ present(one = "секунда", few = "секунды", many = "секунд")
+ pastOrFuture(one = "секунду")
+ },
+ minutesLong = multipleTenses {
+ present(one = "минута", few = "минуты", many = "минут")
+ pastOrFuture(one = "минуту")
+ },
+ hoursLong = presentTense(one = "час", few = "часа", many = "часов"),
+ daysLong = presentTense(one = "день", few = "дня", many = "дней"),
+ weeksLong = multipleTenses {
+ present(one = "неделя", few = "недели", many = "недель")
+ pastOrFuture(one = "неделю")
+ },
+ monthsLong = presentTense(one = "месяц", few = "месяца", many = "месяцев"),
+ yearsLong = presentTense(one = "год", few = "года", many = "лет"),
+ secondsShort = presentTense(one = "с"),
+ minutesShort = presentTense(one = "мин"),
+ hoursShort = presentTense(one = "ч"),
+ daysShort = presentTense(one = "дн"),
+ weeksShort = presentTense(one = "нед"),
+ monthsShort = presentTense(one = "мес"),
+ yearsShort = presentTense(one = "г"),
+ secondsNarrow = presentTense(one = "с"),
+ minutesNarrow = presentTense(one = "м"),
+ hoursNarrow = presentTense(one = "ч"),
+ daysNarrow = presentTense(one = "д"),
+ weeksNarrow = presentTense(one = "н"),
+ monthsNarrow = presentTense(one = "мес"),
+ yearsNarrow = presentTense(one = "г"),
+ timeAgo = { "$it назад" },
+ timeInFuture = { "через $it" },
+ now = "сейчас",
+ today = "сегодня",
+ yesterday = "вчера",
+ tomorrow = "завтра",
+ lessThan = "менее",
+ about = "около"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt
new file mode 100644
index 0000000..e6b6d22
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val TrStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = presentTense(other = "saniye"),
+ minutesLong = presentTense(other = "dakika"),
+ hoursLong = presentTense(other = "saat"),
+ daysLong = presentTense(other = "gün"),
+ weeksLong = presentTense(other = "hafta"),
+ monthsLong = presentTense(other = "ay"),
+ yearsLong = presentTense(other = "yıl"),
+ secondsShort = presentTense(other = "sn."),
+ minutesShort = presentTense(other = "dk."),
+ hoursShort = presentTense(other = "sa."),
+ daysShort = presentTense(other = "g"),
+ weeksShort = presentTense(other = "hf."),
+ monthsShort = presentTense(other = "ay"),
+ yearsShort = presentTense(other = "yıl"),
+ secondsNarrow = presentTense(other = "sn"),
+ minutesNarrow = presentTense(other = "dk"),
+ hoursNarrow = presentTense(other = "sa"),
+ daysNarrow = presentTense(other = "g"),
+ weeksNarrow = presentTense(other = "h"),
+ monthsNarrow = presentTense(other = "a"),
+ yearsNarrow = presentTense(other = "y"),
+ timeAgo = { "$it önce" },
+ timeInFuture = { "$it sonra" },
+ now = "şimdi",
+ today = "bugün",
+ yesterday = "dün",
+ tomorrow = "yarın",
+ lessThan = "daha az",
+ about = "yaklaşık"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt
new file mode 100644
index 0000000..64c4761
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt
@@ -0,0 +1,59 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val UkStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count ->
+ when {
+ count % 10 == 1 && count % 100 != 11 -> Plural.One
+ count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few
+ else -> Plural.Many
+ }
+ },
+ secondsLong = multipleTenses {
+ present(one = "секунда", few = "секунди", many = "секунд")
+ pastOrFuture(one = "секунду")
+ },
+ minutesLong = multipleTenses {
+ present(one = "хвилина", few = "хвилини", many = "хвилин")
+ pastOrFuture(one = "хвилину")
+ },
+ hoursLong = multipleTenses {
+ present(one = "година", few = "години", many = "годин")
+ pastOrFuture(one = "годину")
+ },
+ daysLong = presentTense(one = "день", few = "дні", many = "днів"),
+ weeksLong = presentTense(one = "тиждень", few = "тижні", many = "тижнів"),
+ monthsLong = presentTense(one = "місяць", few = "місяці", many = "місяців"),
+ yearsLong = presentTense(one = "рік", few = "роки", many = "років"),
+ secondsShort = presentTense(one = "с"),
+ minutesShort = presentTense(one = "хв"),
+ hoursShort = presentTense(one = "год"),
+ daysShort = presentTense(one = "дн"),
+ weeksShort = presentTense(one = "тиж"),
+ monthsShort = presentTense(one = "міс"),
+ yearsShort = presentTense(one = "р"),
+ secondsNarrow = presentTense(one = "с"),
+ minutesNarrow = presentTense(one = "хв"),
+ hoursNarrow = presentTense(one = "год"),
+ daysNarrow = presentTense(one = "д"),
+ weeksNarrow = presentTense(one = "т"),
+ monthsNarrow = presentTense(one = "м"),
+ yearsNarrow = presentTense(one = "р"),
+ timeAgo = { "$it тому" },
+ timeInFuture = { "через $it" },
+ now = "зараз",
+ today = "сьогодні",
+ yesterday = "вчора",
+ tomorrow = "завтра",
+ lessThan = "менше",
+ about = "близько"
+ ),
+ number = NumberStrings(groupSeparator = " ", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt
new file mode 100644
index 0000000..7634c0c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt
@@ -0,0 +1,65 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.multipleTenses
+
+internal val UzStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { count -> if (count == 1) Plural.One else Plural.Other },
+ secondsLong = multipleTenses {
+ present(one = "soniya", other = "soniya")
+ future(one = "soniyadan", other = "soniyadan")
+ },
+ minutesLong = multipleTenses {
+ present(one = "daqiqa", other = "daqiqa")
+ future(one = "daqiqadan", other = "daqiqadan")
+ },
+ hoursLong = multipleTenses {
+ present(one = "soat", other = "soat")
+ future(one = "soatdan", other = "soatdan")
+ },
+ daysLong = multipleTenses {
+ present(one = "kun", other = "kun")
+ future(one = "kundan", other = "kundan")
+ },
+ weeksLong = multipleTenses {
+ present(one = "hafta", other = "hafta")
+ future(one = "haftadan", other = "haftadan")
+ },
+ monthsLong = multipleTenses {
+ present(one = "oy", other = "oy")
+ future(one = "oydan", other = "oydan")
+ },
+ yearsLong = multipleTenses {
+ present(one = "yil", other = "yil")
+ future(one = "yildan", other = "yildan")
+ },
+ secondsShort = presentTense(one = "son."),
+ minutesShort = presentTense(one = "daq."),
+ hoursShort = presentTense(one = "soat"),
+ daysShort = presentTense(one = "kun"),
+ weeksShort = presentTense(one = "hafta"),
+ monthsShort = presentTense(one = "oy"),
+ yearsShort = presentTense(one = "yil"),
+ secondsNarrow = presentTense(one = "s"),
+ minutesNarrow = presentTense(one = "d"),
+ hoursNarrow = presentTense(one = "soat"),
+ daysNarrow = presentTense(one = "k"),
+ weeksNarrow = presentTense(one = "h"),
+ monthsNarrow = presentTense(one = "oy"),
+ yearsNarrow = presentTense(one = "y"),
+ timeAgo = { "$it oldin" },
+ timeInFuture = { "$it keyin" },
+ now = "hozir",
+ today = "bugun",
+ yesterday = "kecha",
+ tomorrow = "ertaga",
+ lessThan = "kamroq",
+ about = "taxminan"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt
new file mode 100644
index 0000000..edcc8ce
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt
@@ -0,0 +1,43 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+import nl.jacobras.humanreadable.i18n.NumberStrings
+
+internal val ViStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { Plural.Other },
+ secondsLong = presentTense(other = "giây"),
+ minutesLong = presentTense(other = "phút"),
+ hoursLong = presentTense(other = "giờ"),
+ daysLong = presentTense(other = "ngày"),
+ weeksLong = presentTense(other = "tuần"),
+ monthsLong = presentTense(other = "tháng"),
+ yearsLong = presentTense(other = "năm"),
+ secondsShort = presentTense(other = "giây"),
+ minutesShort = presentTense(other = "phút"),
+ hoursShort = presentTense(other = "giờ"),
+ daysShort = presentTense(other = "ngày"),
+ weeksShort = presentTense(other = "tuần"),
+ monthsShort = presentTense(other = "tháng"),
+ yearsShort = presentTense(other = "năm"),
+ secondsNarrow = presentTense(other = "giây"),
+ minutesNarrow = presentTense(other = "phút"),
+ hoursNarrow = presentTense(other = "giờ"),
+ daysNarrow = presentTense(other = "ngày"),
+ weeksNarrow = presentTense(other = "tuần"),
+ monthsNarrow = presentTense(other = "tháng"),
+ yearsNarrow = presentTense(other = "năm"),
+ timeAgo = { "$it trước" },
+ timeInFuture = { "sau $it" },
+ now = "bây giờ",
+ today = "hôm nay",
+ yesterday = "hôm qua",
+ tomorrow = "ngày mai",
+ lessThan = "ít hơn",
+ about = "khoảng"
+ ),
+ number = NumberStrings(groupSeparator = ".", decimalSymbol = ",")
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt
new file mode 100644
index 0000000..82c9b1a
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt
@@ -0,0 +1,41 @@
+package nl.jacobras.humanreadable.i18n.translations
+
+import nl.jacobras.humanreadable.i18n.presentTense
+import nl.jacobras.humanreadable.i18n.Plural
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.HumanReadableStrings
+
+internal val ZhStrings = HumanReadableStrings(
+ dateTime = DateTimeStrings(
+ plural = { Plural.Other },
+ secondsLong = presentTense(other = "秒"),
+ minutesLong = presentTense(other = "分钟"),
+ hoursLong = presentTense(other = "小时"),
+ daysLong = presentTense(other = "天"),
+ weeksLong = presentTense(other = "周"),
+ monthsLong = presentTense(other = "个月"),
+ yearsLong = presentTense(other = "年"),
+ secondsShort = presentTense(other = "秒"),
+ minutesShort = presentTense(other = "分钟"),
+ hoursShort = presentTense(other = "小时"),
+ daysShort = presentTense(other = "天"),
+ weeksShort = presentTense(other = "周"),
+ monthsShort = presentTense(other = "个月"),
+ yearsShort = presentTense(other = "年"),
+ secondsNarrow = presentTense(other = "秒"),
+ minutesNarrow = presentTense(other = "分"),
+ hoursNarrow = presentTense(other = "时"),
+ daysNarrow = presentTense(other = "天"),
+ weeksNarrow = presentTense(other = "周"),
+ monthsNarrow = presentTense(other = "月"),
+ yearsNarrow = presentTense(other = "年"),
+ timeAgo = { "${it}之前" },
+ timeInFuture = { "${it}之后" },
+ now = "现在",
+ today = "今天",
+ yesterday = "昨天",
+ tomorrow = "明天",
+ lessThan = "少于",
+ about = "大约"
+ )
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt
new file mode 100644
index 0000000..51b583a
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt
@@ -0,0 +1,46 @@
+package nl.jacobras.humanreadable.time
+
+/**
+ * Determines the format style of the output.
+ *
+ * @property date The date notation to use.
+ * @property time The time notation to use.
+ * @property indicateApproximation Whether to indicate that the output is approximate (e.g. "about 5 minutes ago").
+ */
+public data class FormatStyle(
+ val date: Date = Date.Long,
+ val time: Time = Time.Regular,
+ val indicateApproximation: Boolean = false
+) {
+
+ public enum class Date {
+
+ /**
+ * Long format style, e.g. "1 hour, 50 minutes".
+ */
+ Long,
+
+ /**
+ * Shorter format style, e.g. "1 hr, 50 min".
+ */
+ Short,
+
+ /**
+ * Shortest format style, e.g. "1h 50m".
+ */
+ Narrow
+ }
+
+ public enum class Time {
+
+ /**
+ * Regular format style, e.g. "1 hour, 50 minutes".
+ */
+ Regular,
+
+ /**
+ * Digital format style, e.g. "1:50:00".
+ */
+ Digital
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt
new file mode 100644
index 0000000..7b92ac4
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt
@@ -0,0 +1,209 @@
+package nl.jacobras.humanreadable.time
+
+import nl.jacobras.humanreadable.HumanReadable
+import nl.jacobras.humanreadable.HumanReadable.strings
+import nl.jacobras.humanreadable.formatNumber
+import kotlin.time.Duration
+
+/**
+ * Returns the given [duration] in human-readable format.
+ *
+ * @param duration The duration to format.
+ * @param relativeTime Whether this is in the past, current or future (for grammatical correctness).
+ * @param format The [FormatStyle] to use.
+ * @param parts Configures the formatting of multiple parts (defaults to 1 part).
+ * @param units The [TimeUnit]s to limit to during formatting.
+ * @param rounding The [Rounding] strategy to use.
+ */
+internal fun formatDuration(
+ duration: Duration,
+ relativeTime: RelativeTime,
+ format: FormatStyle,
+ parts: PartsConfig,
+ units: Set,
+ rounding: Rounding
+): String {
+ val tooSmall = parts.smallestDuration > Duration.ZERO && duration < parts.smallestDuration
+
+ val parts = getNeededParts(
+ units = if (format.time == FormatStyle.Time.Digital) {
+ units.minus(setOf(TimeUnit.Hours, TimeUnit.Minutes, TimeUnit.Seconds))
+ } else {
+ units
+ },
+ duration = if (tooSmall) parts.smallestDuration else duration,
+ rounding = rounding,
+ parts = parts
+ )
+
+ return buildString {
+ if (tooSmall) {
+ // Too small? Add "less than ..."
+ when (format.date) {
+ FormatStyle.Date.Long -> {
+ append(strings.dateTime.lessThan)
+ append(' ')
+ }
+ FormatStyle.Date.Short -> append('<')
+ FormatStyle.Date.Narrow -> append('<')
+ }
+ } else if (format.indicateApproximation
+ && duration != parts.totalDuration
+ && format.time != FormatStyle.Time.Digital
+ ) {
+ // Large enough, somewhere rounding occurred? Add "about ..."
+ when (format.date) {
+ FormatStyle.Date.Long -> {
+ append(strings.dateTime.about)
+ append(' ')
+ }
+ FormatStyle.Date.Short, FormatStyle.Date.Narrow -> append('~')
+ }
+ }
+
+ // Format all parts
+ for ((unit, value) in parts) {
+ if (unit != parts.keys.first()) {
+ when (format.date) {
+ FormatStyle.Date.Long -> append(", ")
+ FormatStyle.Date.Short -> append(", ")
+ FormatStyle.Date.Narrow -> append(" ")
+ }
+ }
+ append(formatUnit(value, unit, relativeTime, format.date))
+ }
+
+ // Digital time?
+ if (format.time == FormatStyle.Time.Digital) {
+ if (!isEmpty()) append(", ")
+
+ val hours = (duration.inWholeHours % 24).toString().padStart(2, '0')
+ val minutes = (duration.inWholeMinutes % 60).toString().padStart(2, '0')
+ val seconds = (duration.inWholeSeconds % 60).toString().padStart(2, '0')
+ append("$hours:$minutes:$seconds")
+ }
+
+ // Nothing to format? Add "0 ..."
+ if (isEmpty() && units.isNotEmpty()) {
+ append(formatUnit(0, units.first(), relativeTime, format.date))
+ }
+ }
+}
+
+private fun getNeededParts(
+ units: Set,
+ duration: Duration,
+ rounding: Rounding,
+ parts: PartsConfig
+): Map {
+ val unitsDescending = units.sorted().reversed()
+ val res = mutableMapOf()
+ var remainingDuration = duration
+ var lastUnit: TimeUnit? = null
+ var remainingBeforeLast = remainingDuration
+
+ // Main division into parts
+ for (unit in unitsDescending) {
+ if (res.size == parts.max) {
+ break
+ }
+
+ // Initial division using Floor rounding, because actual rounding happens only in the last part.
+ val value = unit.calculateValue(remainingDuration, Rounding.Floor)
+ if (value > 0) {
+ lastUnit = unit
+ remainingBeforeLast = remainingDuration
+ res[unit] = value
+ remainingDuration -= unit.valueToDuration(value)
+ } else if (res.isNotEmpty() && parts.onlyConsecutiveParts) {
+ // Started formatting, but the next part is zero, so we're done because
+ // anything after this would be non-consecutive.
+ break
+ }
+ }
+
+ // Round the last part. Only needed for HalfUp, because Floor is already done above
+ // and UpIfClose is done below in the roll-overs section.
+ val roundMethod = if (rounding is Rounding.IfClose) rounding.default else rounding
+ if (lastUnit != null && roundMethod == Rounding.HalfUp) {
+ res[lastUnit] = lastUnit.calculateValue(remainingBeforeLast, Rounding.HalfUp)
+ }
+
+ // Roll-overs
+ if (rounding is Rounding.IfClose) {
+ val thresholdsAscending = rounding.thresholds.entries.sortedBy { it.key }
+
+ for ((unit, threshold) in thresholdsAscending) {
+ val value = res[unit] ?: continue
+ val largerUnit = unit.largerUnit
+
+ // Round up if the next unit is nearer than the threshold
+ if (largerUnit != null) {
+ val largerUnitSmallestDuration = largerUnit.valueToDuration(1)
+ val currentUnitSmallestDuration = unit.valueToDuration(1)
+ val maxValueForThisUnit = (largerUnitSmallestDuration / currentUnitSmallestDuration).toInt()
+ if (value >= (maxValueForThisUnit - threshold)) {
+ res[largerUnit] = (res[largerUnit] ?: 0) + 1
+ res.remove(unit)
+ }
+ }
+
+ // Round down if the unit is closer than the threshold to 0
+ if (res.size > 1 && unit == res.keys.min() && value < threshold) {
+ res.remove(unit)
+ }
+ }
+ }
+
+ // Sub-part cut-offs
+ if (parts.subpartCutOffs.isNotEmpty()) {
+ for ((cutoffUnit, cutoffValue) in parts.subpartCutOffs) {
+ val value = res[cutoffUnit]
+ if (value != null && value >= cutoffValue) {
+ res.keys.removeAll { it < cutoffUnit }
+ }
+ }
+ }
+ return res
+}
+
+private val Map.totalDuration: Duration
+ get() = entries.fold(Duration.ZERO) { acc, (unit, value) ->
+ acc + unit.valueToDuration(value)
+ }
+
+/**
+ * Formats a [count] with its [unit].
+ *
+ * Note about Arabic: normally this produces "$count $unit", but for Arabic the
+ * singular (1) and dual (2) forms are encoded in the unit word itself, so the
+ * numeral is omitted.
+ *
+ * Note about Korean: the number and unit are written without a separating space, e.g. "2초".
+ */
+private fun formatUnit(
+ count: Int,
+ unit: TimeUnit,
+ relativeTime: RelativeTime,
+ formatStyle: FormatStyle.Date
+): String {
+ val formattedCount = if (count > 1_000) {
+ count.toDouble().formatNumber(decimals = 0)
+ } else {
+ count.toString()
+ }
+
+ val unitText = unit.format(count, relativeTime, formatStyle)
+ val languageTag = HumanReadable.localisation.languageTag
+ return when (languageTag) {
+ "ar" if (count == 1 || count == 2) -> unitText
+ "ko" -> "$formattedCount$unitText"
+ else -> {
+ if (formatStyle == FormatStyle.Date.Narrow) {
+ "$formattedCount$unitText"
+ } else {
+ "$formattedCount $unitText"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt
new file mode 100644
index 0000000..20edaca
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt
@@ -0,0 +1,112 @@
+package nl.jacobras.humanreadable.time
+
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.daysUntil
+import kotlinx.datetime.minus
+import kotlinx.datetime.plus
+import nl.jacobras.humanreadable.HumanReadable.strings
+import kotlin.math.absoluteValue
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+
+/**
+ * Returns the difference between [baseInstant] and [instant], in human-readable format.
+ * Also supports instants in the future or past.
+ *
+ * @param instant The [Instant] to compare with [baseInstant].
+ * @param baseInstant The base/starting [Instant], usually "now".
+ * @param formatting The [FormatStyle] to use.
+ * @param parts Configures the formatting of multiple parts (defaults to 1 part).
+ * @param units The [TimeUnit]s to limit to during formatting.
+ * @param rounding The [Rounding] strategy to use.
+ */
+@OptIn(ExperimentalTime::class)
+internal fun formatTimeAgo(
+ instant: Instant,
+ baseInstant: Instant,
+ formatting: FormatStyle,
+ parts: PartsConfig,
+ units: Set,
+ rounding: Rounding
+): String {
+ val diff = baseInstant - instant
+ val secondsAgo = diff.inWholeSeconds
+
+ return when {
+ secondsAgo < 0 -> strings.dateTime.timeInFuture(
+ formatDuration(
+ duration = diff.absoluteValue,
+ relativeTime = RelativeTime.Future,
+ format = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
+ )
+ secondsAgo <= 1 -> strings.dateTime.now
+ else -> strings.dateTime.timeAgo(
+ formatDuration(
+ duration = diff.absoluteValue,
+ relativeTime = RelativeTime.Past,
+ format = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
+ )
+ }
+}
+
+/**
+ * Returns the difference between [date] and [baseDate], in human-readable format.
+ * Also supports dates in the future or past.
+ *
+ * Will return "today", "tomorrow" or "yesterday" if the date is today, tomorrow, or yesterday.
+ *
+ * @param date The [LocalDate] to compare with [baseDate].
+ * @param baseDate The base/starting [LocalDate], usually "today".
+ * @param formatting The [FormatStyle] to use.
+ * @param parts Configures the formatting of multiple parts (defaults to 1 part).
+ * @param units The [TimeUnit]s to limit to during formatting.
+ * @param rounding The [Rounding] strategy to use.
+ */
+internal fun formatTimeAgo(
+ date: LocalDate,
+ baseDate: LocalDate,
+ formatting: FormatStyle,
+ parts: PartsConfig,
+ units: Set,
+ rounding: Rounding
+): String {
+ when (date) {
+ baseDate.minus(1, DateTimeUnit.DAY) -> return strings.dateTime.yesterday
+ baseDate -> return strings.dateTime.today
+ baseDate.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow
+ }
+ val secondsAgo = date.daysUntil(baseDate) * 86_400
+
+ return when {
+ secondsAgo < 0 -> strings.dateTime.timeInFuture(
+ formatDuration(
+ duration = secondsAgo.absoluteValue.seconds,
+ relativeTime = RelativeTime.Future,
+ format = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
+ )
+ else -> strings.dateTime.timeAgo(
+ formatDuration(
+ duration = secondsAgo.absoluteValue.seconds,
+ relativeTime = RelativeTime.Past,
+ format = formatting,
+ parts = parts,
+ units = units,
+ rounding = rounding
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt
new file mode 100644
index 0000000..f42cd04
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt
@@ -0,0 +1,58 @@
+package nl.jacobras.humanreadable.time
+
+import kotlin.time.Duration
+
+/**
+ * Configures the formatting of multiple parts when formatting a duration.
+ *
+ * Note that only adjacent subparts are used:
+ * - "1 day, 20 hours" could be output;
+ * - "1 day, 20 minutes" will never be output (because minutes are not next to days).
+ */
+public data class PartsConfig(
+
+ /**
+ * Maximum number of parts to show.
+ *
+ * For example, `14.days + 5.hours + 6.minutes + 30.seconds` will be:
+ * - "14 days, 5 hours, 6 minutes" if this is set to 3;
+ * - "14 days, 5 hours" if this is set to 2;
+ * - "14 days" if this is set to 1.
+ */
+ val max: Int = 1,
+
+ /**
+ * Smallest duration to show. Anything smaller than this will be shown as "less than x".
+ *
+ * For example, if this is set to `45.seconds`, then:
+ * - `44.seconds` will be "less than 45 seconds"
+ * - `45.seconds` will be "45 seconds"
+ */
+ val smallestDuration: Duration = Duration.ZERO,
+
+ /**
+ * Controls how large a [TimeUnit] may be at most to still get subparts after it.
+ *
+ * For example, if this is set to `[TimeUnit.Minutes = 2]`:
+ * - `1.minutes + 4.seconds` will be "1 minute, 4 seconds"
+ * - `2.minutes + 4.seconds` will be "2 minutes"
+ *
+ * If this is set to `[TimeUnit.Hours = 20]`:
+ * - `19.hours + 4.minutes` will be "19 hours, 4 minutes"
+ * - `20.hours + 4.minutes` will be "20 hours"
+ */
+ val subpartCutOffs: Map = mapOf(
+ TimeUnit.Minutes to 2,
+ TimeUnit.Hours to 2,
+ TimeUnit.Days to 2
+ ),
+
+ /**
+ * If `true`, only consecutive [TimeUnit]s will be formatted.
+ *
+ * For example:
+ * - "1 hour, 5 minutes" will be possible (because minutes are the next unit after hours)
+ * - "1 hour, 5 seconds" will not be returned (because seconds are not the next unit after hours)
+ */
+ val onlyConsecutiveParts: Boolean = true
+)
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt
similarity index 84%
rename from src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt
rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt
index a36cbcb..9868bc6 100644
--- a/src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt
@@ -1,4 +1,4 @@
-package nl.jacobras.humanreadable
+package nl.jacobras.humanreadable.time
/**
* Indicates in what time frame the requested time unit needs
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt
new file mode 100644
index 0000000..b27f018
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt
@@ -0,0 +1,55 @@
+package nl.jacobras.humanreadable.time
+
+/**
+ * Rounding method.
+ *
+ * Note about multi-parts (when [PartsConfig.max] > 1): rounding is done on the smallest included unit.
+ */
+public sealed interface Rounding {
+
+ /**
+ * Either [Floor] or [HalfUp]. Marker interface to use with [IfClose].
+ */
+ public sealed interface Simple : Rounding
+
+ /**
+ * Rounds down.
+ *
+ * Some examples:
+ * - `12.days` results in "1 week".
+ * - `46.days` results in "1 month".
+ */
+ public data object Floor : Simple
+
+ /**
+ * Rounds up.
+ *
+ * Some examples:
+ * - `12.days` results in "2 weeks".
+ * - `46.days` results in "2 months".
+ *
+ * Note that when [PartsConfig.max] is larger than 1, the rounding is done on the smallest unit.
+ */
+ public data object HalfUp : Simple
+
+ /**
+ * Eagerly rounds up to the next unit, or down to the previous, if within [thresholds].
+ *
+ * For example, with [PartsConfig.max] set to 2 and [thresholds] set to [Seconds = 5]:
+ * - `1.minutes + 5.seconds` remains "1 minute, 5 seconds";
+ * - `1.minutes + 4.seconds` gets rounded to "1 minute";
+ * - `1.minutes + 54 seconds` remains "1 minute, 54 seconds";
+ * - `1.minutes + 55 seconds` gets rounded to "2 minutes".
+ *
+ * Anything else is rounded according to [default], which defaults to [Floor].
+ */
+ public data class IfClose(
+ val thresholds: Map = mapOf(
+ TimeUnit.Seconds to 5,
+ TimeUnit.Minutes to 5,
+ TimeUnit.Hours to 1,
+ TimeUnit.Days to 1
+ ),
+ val default: Simple = Floor
+ ) : Rounding
+}
\ No newline at end of file
diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt
new file mode 100644
index 0000000..bf9188c
--- /dev/null
+++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt
@@ -0,0 +1,105 @@
+package nl.jacobras.humanreadable.time
+
+import nl.jacobras.humanreadable.HumanReadable.localisation
+import nl.jacobras.humanreadable.HumanReadable.strings
+import nl.jacobras.humanreadable.i18n.DateTimeStrings
+import nl.jacobras.humanreadable.i18n.TenseForms
+import nl.jacobras.humanreadable.time.Rounding.IfClose
+import kotlin.math.roundToInt
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+public enum class TimeUnit(
+ internal val calculateValue: (Duration, Rounding) -> Int,
+ internal val valueToDuration: (Int) -> Duration,
+ internal val longForms: (DateTimeStrings) -> TenseForms,
+ internal val shortForms: (DateTimeStrings) -> TenseForms,
+ internal val narrowForms: (DateTimeStrings) -> TenseForms,
+) {
+ Seconds(
+ calculateValue = { duration, rounding -> duration.inWholeSeconds.toFloat().round(rounding) },
+ valueToDuration = { it.seconds },
+ longForms = { it.secondsLong },
+ shortForms = { it.secondsShort },
+ narrowForms = { it.secondsNarrow }
+ ),
+ Minutes(
+ calculateValue = { duration, rounding -> (duration.inWholeSeconds / 60f).round(rounding) },
+ valueToDuration = { it.minutes },
+ longForms = { it.minutesLong },
+ shortForms = { it.minutesShort },
+ narrowForms = { it.minutesNarrow }
+ ),
+ Hours(
+ calculateValue = { duration, rounding -> (duration.inWholeMinutes / 60f).round(rounding) },
+ valueToDuration = { it.hours },
+ longForms = { it.hoursLong },
+ shortForms = { it.hoursShort },
+ narrowForms = { it.hoursNarrow }
+ ),
+ Days(
+ calculateValue = { duration, rounding -> duration.inWholeDays.toFloat().round(rounding) },
+ valueToDuration = { it.days },
+ longForms = { it.daysLong },
+ shortForms = { it.daysShort },
+ narrowForms = { it.daysNarrow }
+ ),
+ Weeks(
+ calculateValue = { duration, rounding -> (duration.inWholeDays / 7f).round(rounding) },
+ valueToDuration = { (it * 7).days },
+ longForms = { it.weeksLong },
+ shortForms = { it.weeksShort },
+ narrowForms = { it.weeksNarrow }
+ ),
+ Months(
+ calculateValue = { duration, rounding -> (duration.inWholeDays / 30.5f).round(rounding) },
+ valueToDuration = { (it * 30.5).days },
+ longForms = { it.monthsLong },
+ shortForms = { it.monthsShort },
+ narrowForms = { it.monthsNarrow }
+ ),
+ Years(
+ calculateValue = { duration, rounding -> (duration.inWholeDays / 365f).round(rounding) },
+ valueToDuration = { (it * 365).days },
+ longForms = { it.yearsLong },
+ shortForms = { it.yearsShort },
+ narrowForms = { it.yearsNarrow }
+ );
+
+ internal val largerUnit: TimeUnit?
+ get() = entries.getOrNull(ordinal + 1)
+
+ internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.Date): String {
+ val dateTimeStrings = strings.dateTime
+ val tenseForms = when (formatStyle) {
+ FormatStyle.Date.Long -> longForms(dateTimeStrings)
+ FormatStyle.Date.Short -> shortForms(dateTimeStrings)
+ FormatStyle.Date.Narrow -> narrowForms(dateTimeStrings)
+ }
+ val pluralCategory = dateTimeStrings.plural(value)
+ val correctTense = when (relativeTime) {
+ RelativeTime.Past -> tenseForms.past
+ RelativeTime.Future -> tenseForms.future
+ RelativeTime.Present -> tenseForms.present
+ }
+ return correctTense[pluralCategory]
+ ?: tenseForms.present[pluralCategory]
+ ?: tenseForms.present.values.firstOrNull()
+ ?: error("No translation for $value $this in '${localisation.languageTag}'")
+ }
+
+ internal companion object {
+ internal val all = entries.toSet()
+ }
+}
+
+private fun Float.round(rounding: Rounding): Int {
+ return when (rounding) {
+ is IfClose -> toInt()
+ Rounding.Floor -> toInt()
+ Rounding.HalfUp -> roundToInt()
+ }
+}
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/data_units_ar.xml b/src/commonMain/libres/strings/data_units_ar.xml
deleted file mode 100644
index 5a567c8..0000000
--- a/src/commonMain/libres/strings/data_units_ar.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- ب
- ك.ب
- م.ب
- ج.ب
- ت.ب
-
diff --git a/src/commonMain/libres/strings/data_units_en.xml b/src/commonMain/libres/strings/data_units_en.xml
deleted file mode 100644
index 52054d5..0000000
--- a/src/commonMain/libres/strings/data_units_en.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- B
- kB
- MB
- GB
- TB
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/data_units_fi.xml b/src/commonMain/libres/strings/data_units_fi.xml
deleted file mode 100644
index b7f8224..0000000
--- a/src/commonMain/libres/strings/data_units_fi.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- t
- kt
- Mt
- Gt
- Tt
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/data_units_fr.xml b/src/commonMain/libres/strings/data_units_fr.xml
deleted file mode 100644
index d1c3382..0000000
--- a/src/commonMain/libres/strings/data_units_fr.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- o
- ko
- Mo
- Go
- To
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/distance_units_ar.xml b/src/commonMain/libres/strings/distance_units_ar.xml
deleted file mode 100644
index 54999bb..0000000
--- a/src/commonMain/libres/strings/distance_units_ar.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- م
- كم
- قدم
- ميل
-
diff --git a/src/commonMain/libres/strings/distance_units_en.xml b/src/commonMain/libres/strings/distance_units_en.xml
deleted file mode 100644
index 87b95cb..0000000
--- a/src/commonMain/libres/strings/distance_units_en.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- m
- km
- ft
- mi
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_cs.xml b/src/commonMain/libres/strings/number_units_cs.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_cs.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_de.xml b/src/commonMain/libres/strings/number_units_de.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_de.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_el.xml b/src/commonMain/libres/strings/number_units_el.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_el.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_en.xml b/src/commonMain/libres/strings/number_units_en.xml
deleted file mode 100644
index 60a9cff..0000000
--- a/src/commonMain/libres/strings/number_units_en.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- ,
- .
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_es.xml b/src/commonMain/libres/strings/number_units_es.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_es.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_fi.xml b/src/commonMain/libres/strings/number_units_fi.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_fi.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_fr.xml b/src/commonMain/libres/strings/number_units_fr.xml
deleted file mode 100644
index a2acbea..0000000
--- a/src/commonMain/libres/strings/number_units_fr.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
diff --git a/src/commonMain/libres/strings/number_units_id.xml b/src/commonMain/libres/strings/number_units_id.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_id.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_it.xml b/src/commonMain/libres/strings/number_units_it.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_it.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_nl.xml b/src/commonMain/libres/strings/number_units_nl.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_nl.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_pl.xml b/src/commonMain/libres/strings/number_units_pl.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_pl.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_pt.xml b/src/commonMain/libres/strings/number_units_pt.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_pt.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_ru.xml b/src/commonMain/libres/strings/number_units_ru.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_ru.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_tr.xml b/src/commonMain/libres/strings/number_units_tr.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_tr.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_uk.xml b/src/commonMain/libres/strings/number_units_uk.xml
deleted file mode 100644
index 4a32de0..0000000
--- a/src/commonMain/libres/strings/number_units_uk.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_uz.xml b/src/commonMain/libres/strings/number_units_uz.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_uz.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/number_units_vi.xml b/src/commonMain/libres/strings/number_units_vi.xml
deleted file mode 100644
index defdcc7..0000000
--- a/src/commonMain/libres/strings/number_units_vi.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- .
- ,
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_ar.xml b/src/commonMain/libres/strings/time_units_ar.xml
deleted file mode 100644
index b03787e..0000000
--- a/src/commonMain/libres/strings/time_units_ar.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
- - ثانية
- - ثانية
- - ثانيتان
- - ثوان
- - ثانية
- - ثانية
-
-
- - ثانيتين
-
-
- - ثانيتين
-
-
-
- - دقيقة
- - دقيقة
- - دقيقتان
- - دقائق
- - دقيقة
- - دقيقة
-
-
- - دقيقتين
-
-
- - دقيقتين
-
-
-
- - ساعة
- - ساعة
- - ساعتان
- - ساعات
- - ساعة
- - ساعة
-
-
- - ساعتين
-
-
- - ساعتين
-
-
-
- - يوم
- - يوم
- - يومان
- - أيام
- - يوم
- - يوم
-
-
- - يومين
-
-
- - يومين
-
-
-
- - أسبوع
- - أسبوع
- - أسبوعان
- - أسابيع
- - أسبوع
- - أسبوع
-
-
- - أسبوعين
-
-
- - أسبوعين
-
-
-
- - شهر
- - شهر
- - شهران
- - أشهر
- - شهر
- - شهر
-
-
- - شهرين
-
-
- - شهرين
-
-
-
- - سنة
- - سنة
- - سنتان
- - سنوات
- - سنة
- - سنة
-
-
- - سنتين
-
-
- - سنتين
-
-
- قبل ${time}
- بعد ${time}
- الآن
-
diff --git a/src/commonMain/libres/strings/time_units_cs.xml b/src/commonMain/libres/strings/time_units_cs.xml
deleted file mode 100644
index 8a90a57..0000000
--- a/src/commonMain/libres/strings/time_units_cs.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
- - sekunda
- - sekundy
- - sekund
-
-
- - sekundou
- - sekundami
- - sekundami
-
-
- - sekundu
- - sekundy
- - sekund
-
-
- - minuta
- - minuty
- - minut
-
-
- - minutou
- - minutami
- - minutami
-
-
- - minutu
- - minuty
- - minut
-
-
- - hodina
- - hodiny
- - hodin
-
-
- - hodinou
- - hodinami
- - hodinami
-
-
- - hodinu
- - hodiny
- - hodin
-
-
- - den
- - dny
- - dní
-
-
- - dnem
- - dny
- - dny
-
-
- - týden
- - týdny
- - týdnů
-
-
- - týdnem
- - týdny
- - týdny
-
-
- - měsíc
- - měsíce
- - měsíců
-
-
- - měsícem
- - měsíci
- - měsíci
-
-
- - rok
- - roky
- - let
-
-
- - rokem
- - roky
- - lety
-
-
- před ${time}
- za ${time}
- nyní
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_de.xml b/src/commonMain/libres/strings/time_units_de.xml
deleted file mode 100644
index c9bb630..0000000
--- a/src/commonMain/libres/strings/time_units_de.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- - Sekunde
- - Sekunden
-
-
-
- - Minute
- - Minuten
-
-
-
- - Stunde
- - Stunden
-
-
-
- - Tag
- - Tage
-
-
- - Tagen
-
-
- - Tagen
-
-
-
- - Woche
- - Wochen
-
-
-
- - Monat
- - Monate
-
-
- - Monaten
-
-
- - Monaten
-
-
-
- - Jahr
- - Jahre
-
-
- - Jahren
-
-
- - Jahren
-
-
- vor ${time}
- in ${time}
- jetzt
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_el.xml b/src/commonMain/libres/strings/time_units_el.xml
deleted file mode 100644
index 5a71bd2..0000000
--- a/src/commonMain/libres/strings/time_units_el.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- - δευτερόλεπτο
- - δευτερόλεπτα
-
-
- - λεπτό
- - λεπτά
-
-
- - ώρα
- - ώρες
-
-
- - μέρα
- - μέρες
-
-
- - εβδομάδα
- - εβδομάδες
-
-
- - μήνα
- - μήνες
-
-
- - έτος
- - έτη
-
- ${time} πριν
- σε ${time}
- τώρα
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_en.xml b/src/commonMain/libres/strings/time_units_en.xml
deleted file mode 100644
index 756f7e2..0000000
--- a/src/commonMain/libres/strings/time_units_en.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
- - second
- - seconds
-
-
-
-
-
-
-
-
-
- - minute
- - minutes
-
-
-
-
-
-
-
-
-
- - hour
- - hours
-
-
-
-
-
-
-
-
-
- - day
- - days
-
-
-
-
-
-
-
-
-
- - week
- - weeks
-
-
-
-
-
-
-
-
-
- - month
- - months
-
-
-
-
-
-
-
-
-
- - year
- - years
-
-
-
-
-
-
-
-
- ${time} ago
- in ${time}
- now
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_es.xml b/src/commonMain/libres/strings/time_units_es.xml
deleted file mode 100644
index eb909a0..0000000
--- a/src/commonMain/libres/strings/time_units_es.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - segundo
- - segundos
-
-
- - minuto
- - minutos
-
-
- - hora
- - horas
-
-
- - día
- - días
-
-
- - semana
- - semanas
-
-
- - mes
- - meses
-
-
- - año
- - años
-
-
- hace ${time}
- en ${time}
- ahora
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_fi.xml b/src/commonMain/libres/strings/time_units_fi.xml
deleted file mode 100644
index f6d36b8..0000000
--- a/src/commonMain/libres/strings/time_units_fi.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - sekunti
- - sekuntia
-
-
- - minuutti
- - minuuttia
-
-
- - tunti
- - tuntia
-
-
- - päivä
- - päivää
-
-
- - viikko
- - viikkoa
-
-
- - kuukausi
- - kuukautta
-
-
- - vuosi
- - vuotta
-
-
- ${time} sitten
- ${time} tulevaisuudessa
- nyt
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_fr.xml b/src/commonMain/libres/strings/time_units_fr.xml
deleted file mode 100644
index 83786e1..0000000
--- a/src/commonMain/libres/strings/time_units_fr.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - seconde
- - secondes
-
-
- - minute
- - minutes
-
-
- - heure
- - heures
-
-
- - jour
- - jours
-
-
- - semaine
- - semaines
-
-
- - mois
- - mois
-
-
- - an
- - ans
-
-
- il y a ${time}
- dans ${time}
- maintenant
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_id.xml b/src/commonMain/libres/strings/time_units_id.xml
deleted file mode 100644
index 57b0d70..0000000
--- a/src/commonMain/libres/strings/time_units_id.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- - detik
-
-
- - menit
-
-
- - jam
-
-
- - hari
-
-
- - minggu
-
-
- - bulan
-
-
- - tahun
-
-
- ${time} yang lalu
- dalam ${time}
- sekarang
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_it.xml b/src/commonMain/libres/strings/time_units_it.xml
deleted file mode 100644
index 778d7ad..0000000
--- a/src/commonMain/libres/strings/time_units_it.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - secondo
- - secondi
-
-
- - minuto
- - minuti
-
-
- - ora
- - ore
-
-
- - giorno
- - giorni
-
-
- - settimana
- - settimane
-
-
- - mese
- - mesi
-
-
- - anno
- - anni
-
-
- ${time} fa
- tra ${time}
- adesso
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_ja.xml b/src/commonMain/libres/strings/time_units_ja.xml
deleted file mode 100644
index 922a845..0000000
--- a/src/commonMain/libres/strings/time_units_ja.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- - 秒
-
-
- - 分
-
-
- - 時間
-
-
- - 日
-
-
- - 週
-
-
- - ヶ月
-
-
- - 年
-
-
- ${time}前
- ${time}後
- 今
-
diff --git a/src/commonMain/libres/strings/time_units_kk.xml b/src/commonMain/libres/strings/time_units_kk.xml
deleted file mode 100644
index da15910..0000000
--- a/src/commonMain/libres/strings/time_units_kk.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
- - секунд
- - секунд
-
-
- - секундтан
- - секундтан
-
-
-
- - минут
- - минут
-
-
- - минуттан
- - минуттан
-
-
-
- - сағат
- - сағат
-
-
- - сағаттан
- - сағаттан
-
-
-
- - күн
- - күн
-
-
- - күннен
- - күннен
-
-
-
- - апта
- - апта
-
-
- - аптадан
- - аптадан
-
-
-
- - ай
- - ай
-
-
- - айдан
- - айдан
-
-
-
- - жыл
- - жыл
-
-
- - жылдан
- - жылдан
-
-
- ${time} бұрын
- ${time} кейін
- қазір
-
diff --git a/src/commonMain/libres/strings/time_units_ko.xml b/src/commonMain/libres/strings/time_units_ko.xml
deleted file mode 100644
index c413341..0000000
--- a/src/commonMain/libres/strings/time_units_ko.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- - 초
-
-
- - 분
-
-
- - 시
-
-
- - 일
-
-
- - 주
-
-
- - 월
-
-
- - 년
-
-
- ${time} 전
- ${time} 후
- 지금
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_nl.xml b/src/commonMain/libres/strings/time_units_nl.xml
deleted file mode 100644
index d3d0091..0000000
--- a/src/commonMain/libres/strings/time_units_nl.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - seconde
- - seconden
-
-
- - minuut
- - minuten
-
-
- - uur
- - uur
-
-
- - dag
- - dagen
-
-
- - week
- - weken
-
-
- - maand
- - maanden
-
-
- - jaar
- - jaar
-
-
- ${time} geleden
- over ${time}
- nu
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_pl.xml b/src/commonMain/libres/strings/time_units_pl.xml
deleted file mode 100644
index 7b4cef8..0000000
--- a/src/commonMain/libres/strings/time_units_pl.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
- - sekunda
- - sekundy
- - sekund
-
-
- - sekundę
-
-
- - sekundę
-
-
-
- - minuta
- - minuty
- - minut
-
-
- - minutę
-
-
- - minutę
-
-
-
- - godzina
- - godziny
- - godzin
-
-
- - godzinę
-
-
- - godzinę
-
-
-
- - dzień
- - dni
- - dni
-
-
-
- - tydzień
- - tygodnie
- - tygodni
-
-
-
- - miesiąc
- - miesiące
- - miesięcy
-
-
-
- - rok
- - lata
- - lat
-
-
- ${time} temu
- za ${time}
- teraz
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_pt.xml b/src/commonMain/libres/strings/time_units_pt.xml
deleted file mode 100644
index cb075cd..0000000
--- a/src/commonMain/libres/strings/time_units_pt.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- - segundo
- - de segundos
- - segundos
-
-
- - minuto
- - de minutos
- - minutos
-
-
- - hora
- - de horas
- - horas
-
-
- - dia
- - de dias
- - dias
-
-
- - semana
- - de semanas
- - semanas
-
-
- - mês
- - de meses
- - meses
-
-
- - ano
- - de anos
- - anos
-
-
- há ${time}
- em ${time}
- agora
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_ru.xml b/src/commonMain/libres/strings/time_units_ru.xml
deleted file mode 100644
index 8e82b2b..0000000
--- a/src/commonMain/libres/strings/time_units_ru.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
- - секунда
- - секунды
- - секунд
-
-
- - секунду
-
-
- - секунду
-
-
-
- - минута
- - минуты
- - минут
-
-
- - минуту
-
-
- - минуту
-
-
-
- - час
- - часа
- - часов
-
-
-
- - день
- - дня
- - дней
-
-
-
- - неделя
- - недели
- - недель
-
-
- - неделю
-
-
- - неделю
-
-
-
- - месяц
- - месяца
- - месяцев
-
-
-
- - год
- - года
- - лет
-
-
- ${time} назад
- через ${time}
- сейчас
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_tr.xml b/src/commonMain/libres/strings/time_units_tr.xml
deleted file mode 100644
index 9906153..0000000
--- a/src/commonMain/libres/strings/time_units_tr.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - saniye
- - saniye
-
-
- - dakika
- - dakika
-
-
- - saat
- - saat
-
-
- - gün
- - gün
-
-
- - hafta
- - hafta
-
-
- - ay
- - ay
-
-
- - yıl
- - yıl
-
-
- ${time} önce
- ${time} sonra
- şimdi
-
\ No newline at end of file
diff --git a/src/commonMain/libres/strings/time_units_uk.xml b/src/commonMain/libres/strings/time_units_uk.xml
deleted file mode 100644
index 67cd739..0000000
--- a/src/commonMain/libres/strings/time_units_uk.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- - секунду
- - секунди
- - секунд
-
-
- - хвилину
- - хвилини
- - хвилин
-
-
- - годину
- - години
- - годин
-
-
- - день
- - дні
- - днів
-
-
- - тиждень
- - тижні
- - тижнів
-
-
- - місяць
- - місяці
- - місяців
-
-
- - рік
- - роки
- - років
-
-
- ${time} тому
- через ${time}
- зараз
-
diff --git a/src/commonMain/libres/strings/time_units_uz.xml b/src/commonMain/libres/strings/time_units_uz.xml
deleted file mode 100644
index 36d680a..0000000
--- a/src/commonMain/libres/strings/time_units_uz.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
- - soniya
- - soniya
-
-
- - daqiqa
- - daqiqa
-
-
- - soat
- - soat
-
-
- - kun
- - kun
-
-
- - hafta
- - hafta
-
-
- - oy
- - oy
-
-
- - yil
- - yil
-
-
- ${time} oldin
- ${time} keyin
- hozir
-
diff --git a/src/commonMain/libres/strings/time_units_vi.xml b/src/commonMain/libres/strings/time_units_vi.xml
deleted file mode 100644
index 66020e0..0000000
--- a/src/commonMain/libres/strings/time_units_vi.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- - giây
-
-
- - phút
-
-
- - giờ
-
-
- - ngày
-
-
- - tuần
-
-
- - tháng
-
-
- - năm
-
-
- ${time} trước
- vào ${time}
- bây giờ
-
diff --git a/src/commonMain/libres/strings/time_units_zh.xml b/src/commonMain/libres/strings/time_units_zh.xml
deleted file mode 100644
index 8031f32..0000000
--- a/src/commonMain/libres/strings/time_units_zh.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- - 秒
-
-
- - 分钟
-
-
- - 小时
-
-
- - 天
-
-
- - 周
-
-
- - 个月
-
-
- - 年
-
-
- ${time}之前
- ${time}之后
- 现在
-
\ No newline at end of file
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt
index ee0a2c2..99f7b61 100644
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt
@@ -2,13 +2,12 @@ package nl.jacobras.humanreadable
import assertk.assertThat
import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
import kotlin.test.Test
class HumanReadableAbbreviationTests {
init {
- LibresSettings.languageCode = "en"
+ HumanReadable.config.languageTag = "en"
}
@Test
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt
deleted file mode 100644
index bec2beb..0000000
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package nl.jacobras.humanreadable
-
-import assertk.assertThat
-import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
-import kotlin.test.Test
-import kotlin.time.Duration.Companion.days
-import kotlin.time.Duration.Companion.hours
-import kotlin.time.Duration.Companion.minutes
-import kotlin.time.Duration.Companion.seconds
-
-class HumanReadableDurationTests {
-
- init {
- LibresSettings.languageCode = "en"
- }
-
- @Test
- fun seconds() {
- assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 second")
- assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 seconds")
- }
-
- @Test
- fun minutes() {
- assertThat(HumanReadable.duration(1.minutes)).isEqualTo("1 minute")
- assertThat(HumanReadable.duration(3.minutes)).isEqualTo("3 minutes")
- }
-
- @Test
- fun hours() {
- assertThat(HumanReadable.duration(1.hours)).isEqualTo("1 hour")
- assertThat(HumanReadable.duration(3.hours)).isEqualTo("3 hours")
- assertThat(HumanReadable.duration(23.hours)).isEqualTo("23 hours")
- }
-
- @Test
- fun days() {
- assertThat(HumanReadable.duration(1.days)).isEqualTo("1 day")
- assertThat(HumanReadable.duration(3.days)).isEqualTo("3 days")
- }
-
- @Test
- fun weeks() {
- assertThat(HumanReadable.duration(7.days)).isEqualTo("1 week")
- assertThat(HumanReadable.duration(10.days)).isEqualTo("1 week")
- assertThat(HumanReadable.duration(11.days)).isEqualTo("2 weeks")
- assertThat(HumanReadable.duration(21.days)).isEqualTo("3 weeks")
- }
-
- @Test
- fun months() {
- assertThat(HumanReadable.duration(30.days)).isEqualTo("1 month")
- assertThat(HumanReadable.duration(90.days)).isEqualTo("3 months")
- assertThat(HumanReadable.duration(364.days)).isEqualTo("12 months")
- }
-
- @Test
- fun years() {
- assertThat(HumanReadable.duration(365.days)).isEqualTo("1 year")
- assertThat(HumanReadable.duration(1095.days)).isEqualTo("3 years")
- }
-}
\ No newline at end of file
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt
index ae5e781..32a4130 100644
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt
@@ -2,13 +2,12 @@ package nl.jacobras.humanreadable
import assertk.assertThat
import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
import kotlin.test.Test
class HumanReadableFileSizeTests {
init {
- LibresSettings.languageCode = "en"
+ HumanReadable.config.languageTag = "en"
}
@Test
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt
index 06b9f18..dd05573 100644
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt
@@ -2,12 +2,11 @@ package nl.jacobras.humanreadable
import assertk.assertThat
import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
import kotlin.test.Test
class HumanReadableNumberEdgeCaseTests {
init {
- LibresSettings.languageCode = "en"
+ HumanReadable.config.languageTag = "en"
}
@Test
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt
deleted file mode 100644
index 946431c..0000000
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package nl.jacobras.humanreadable
-
-import assertk.assertThat
-import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
-import kotlin.test.Test
-import kotlin.time.Clock
-import kotlin.time.Duration.Companion.days
-import kotlin.time.Duration.Companion.seconds
-import kotlin.time.ExperimentalTime
-import kotlin.time.Instant
-
-@OptIn(ExperimentalTime::class)
-class HumanReadableRelativeTimeTests {
-
- init {
- LibresSettings.languageCode = "en"
- }
-
- private val now: Instant
- get() = Clock.System.now()
-
- @Test
- fun inFuture() {
- assertThat(HumanReadable.timeAgo(now + 200.seconds)).isEqualTo("in 3 minutes")
- }
-
- @Test
- fun now() {
- assertThat(HumanReadable.timeAgo(now)).isEqualTo("now")
- }
-
- @Test
- fun inPast() {
- assertThat(HumanReadable.timeAgo(now - 3.days)).isEqualTo("3 days ago")
- }
-}
\ No newline at end of file
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt
similarity index 65%
rename from src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt
rename to src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt
index 70f61b7..050c082 100644
--- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt
@@ -2,9 +2,9 @@ package nl.jacobras.humanreadable.localized
import assertk.assertThat
import assertk.assertions.isEqualTo
-import io.github.skeptick.libres.LibresSettings
import nl.jacobras.humanreadable.DistanceUnit
import nl.jacobras.humanreadable.HumanReadable
+import nl.jacobras.humanreadable.time.Rounding
import kotlin.test.Test
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
@@ -17,29 +17,42 @@ import kotlin.time.Instant
* Smoke test that verifies all supported languages have working plural formatting.
*/
@OptIn(ExperimentalTime::class)
-class LocalizedTests {
+class LocalisedTests {
private val now: Instant = Clock.System.now()
- private val oneSecondFromNow = now + 1.seconds
+ private val zeroSeconds = 0.seconds
+ private val oneSecond = 1.seconds
+ private val oneSecondFromNow = now + oneSecond
private val twoSeconds = 2.seconds
private val twoSecondsAgo = now - twoSeconds
private val twoSecondsFromNow = now + twoSeconds
+ private val threeSeconds = 3.seconds
+ private val elevenSeconds = 11.seconds
private val oneMinute = 1.minutes
private val oneMinuteAgo = now - oneMinute
private val oneMinuteFromNow = now + oneMinute
private val twoMinutes = 2.minutes
private val twoMinutesAgo = now - twoMinutes
+ private val twentyMinutes = 20.minutes
private val oneHour = 60.minutes
private val oneHourAgo = now - oneHour
private val oneHourFromNow = now + oneHour
+ private val twoHours = 120.minutes
+ private val twoHoursAgo = now - twoHours
+ private val twoHoursFromNow = now + twoHours
private val oneDay = 1.days
private val oneDayAgo = now - oneDay
private val oneDayFromNow = now + oneDay
private val twoDays = 2.days
private val twoDaysAgo = now - twoDays
+ private val threeDays = 3.days
+ private val fiveDays = 5.days
private val oneWeek = 7.days
private val oneWeekAgo = now - oneWeek
private val oneWeekFromNow = now + oneWeek
+ private val oneMonth = 31.days
+ private val oneMonthAgo = now - oneMonth
+ private val oneMonthFromNow = now + oneMonth
private val twoMonths = 60.days
private val twoMonthsAgo = now - twoMonths
private val twoMonthsFromNow = now + twoMonths
@@ -49,19 +62,19 @@ class LocalizedTests {
@Test
fun ar_arabic() {
- LibresSettings.languageCode = "ar"
- assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 ثانية")
- assertThat(HumanReadable.duration(1.seconds)).isEqualTo("ثانية")
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("ثانيتان")
- assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 ثوان")
- assertThat(HumanReadable.duration(11.seconds)).isEqualTo("11 ثانية")
+ HumanReadable.config.languageTag = "ar"
+ assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 ثانية")
+ assertThat(HumanReadable.duration(oneSecond)).isEqualTo("ثانية")
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("ثانيتان")
+ assertThat(HumanReadable.duration(threeSeconds)).isEqualTo("3 ثوان")
+ assertThat(HumanReadable.duration(elevenSeconds)).isEqualTo("11 ثانية")
+ assertThat(HumanReadable.duration(threeDays)).isEqualTo("3 أيام")
+ assertThat(HumanReadable.duration(twoMonths)).isEqualTo("شهران")
+ assertThat(HumanReadable.duration(oneYear)).isEqualTo("سنة")
assertThat(HumanReadable.timeAgo(now, baseInstant = now)).isEqualTo("الآن")
assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("قبل ثانيتين")
assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("بعد دقيقة")
- assertThat(HumanReadable.duration(3.days)).isEqualTo("3 أيام")
- assertThat(HumanReadable.duration(twoMonths)).isEqualTo("شهران")
- assertThat(HumanReadable.duration(oneYear)).isEqualTo("سنة")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34")
@@ -75,8 +88,11 @@ class LocalizedTests {
@Test
fun cs_czech() {
- LibresSettings.languageCode = "cs"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy")
+ HumanReadable.config.languageTag = "cs"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy")
+ assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den")
+ assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny")
+ assertThat(HumanReadable.duration(fiveDays, rounding = Rounding.Floor)).isEqualTo("5 dní")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -86,15 +102,14 @@ class LocalizedTests {
assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("před 1 minutou")
assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("za 1 minutu")
assertThat(HumanReadable.timeAgo(twoMinutesAgo, baseInstant = now)).isEqualTo("před 2 minutami")
- assertThat(HumanReadable.timeAgo(now - 20.minutes, baseInstant = now)).isEqualTo("před 20 minutami")
+ assertThat(HumanReadable.timeAgo(now - twentyMinutes, baseInstant = now)).isEqualTo("před 20 minutami")
assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("před 1 hodinou")
assertThat(HumanReadable.timeAgo(oneHourFromNow, baseInstant = now)).isEqualTo("za 1 hodinu")
- assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den")
- assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny")
assertThat(HumanReadable.timeAgo(oneDayAgo, baseInstant = now)).isEqualTo("před 1 dnem")
assertThat(HumanReadable.timeAgo(twoDaysAgo, baseInstant = now)).isEqualTo("před 2 dny")
- assertThat(HumanReadable.duration(5.days)).isEqualTo("5 dní")
- assertThat(HumanReadable.timeAgo(now - 5.days, baseInstant = now)).isEqualTo("před 5 dny")
+ assertThat(
+ HumanReadable.timeAgo(now - fiveDays, baseInstant = now, rounding = Rounding.Floor)
+ ).isEqualTo("před 5 dny")
assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("před 1 týdnem")
assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("před 2 měsíci")
assertThat(HumanReadable.timeAgo(oneYearAgo, baseInstant = now)).isEqualTo("před 1 rokem")
@@ -102,10 +117,10 @@ class LocalizedTests {
@Test
fun de_german() {
- LibresSettings.languageCode = "de"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 Sekunden")
-
+ HumanReadable.config.languageTag = "de"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 Sekunden")
assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 Monate")
+
assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("vor 2 Monaten")
assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("in 2 Monaten")
@@ -115,10 +130,15 @@ class LocalizedTests {
@Test
fun el_greek() {
- LibresSettings.languageCode = "el"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 δευτερόλεπτα")
- assertThat(HumanReadable.timeAgo(oneWeekAgo)).isEqualTo("1 εβδομάδα πριν")
- assertThat(HumanReadable.timeAgo(oneYearFromNow)).isEqualTo("σε 1 έτος")
+ HumanReadable.config.languageTag = "el"
+ assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("1 εβδομάδα πριν")
+ assertThat(HumanReadable.timeAgo(oneYearFromNow, baseInstant = now)).isEqualTo("σε 1 έτος")
+ assertThat(HumanReadable.timeAgo(oneMonthAgo, baseInstant = now)).isEqualTo("1 μήνα πριν")
+ assertThat(HumanReadable.timeAgo(oneMonthFromNow, baseInstant = now)).isEqualTo("σε 1 μήνα")
+
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 δευτερόλεπτα")
+ assertThat(HumanReadable.duration(oneMonth)).isEqualTo("1 μήνας")
+ assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 μήνες")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -132,8 +152,8 @@ class LocalizedTests {
@Test
fun en_english() {
- LibresSettings.languageCode = "en"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconds")
+ HumanReadable.config.languageTag = "en"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconds")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34")
@@ -147,8 +167,8 @@ class LocalizedTests {
@Test
fun es_spanish() {
- LibresSettings.languageCode = "es"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos")
+ HumanReadable.config.languageTag = "es"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -156,8 +176,12 @@ class LocalizedTests {
@Test
fun fi_finnish() {
- LibresSettings.languageCode = "fi"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekuntia")
+ HumanReadable.config.languageTag = "fi"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekuntia")
+
+ assertThat(HumanReadable.timeAgo(twoMinutesAgo, baseInstant = now)).isEqualTo("2 minuuttia sitten")
+ assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 minuutin kuluttua")
+ assertThat(HumanReadable.timeAgo(twoHoursFromNow, baseInstant = now)).isEqualTo("2 tunnin kuluttua")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -165,8 +189,8 @@ class LocalizedTests {
@Test
fun fr_french() {
- LibresSettings.languageCode = "fr"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondes")
+ HumanReadable.config.languageTag = "fr"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondes")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -176,8 +200,8 @@ class LocalizedTests {
@Test
fun id_indonesian() {
- LibresSettings.languageCode = "id"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 detik")
+ HumanReadable.config.languageTag = "id"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 detik")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -185,8 +209,8 @@ class LocalizedTests {
@Test
fun it_italian() {
- LibresSettings.languageCode = "it"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondi")
+ HumanReadable.config.languageTag = "it"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondi")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -194,8 +218,8 @@ class LocalizedTests {
@Test
fun ja_japanese() {
- LibresSettings.languageCode = "ja"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒")
+ HumanReadable.config.languageTag = "ja"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34")
@@ -203,35 +227,41 @@ class LocalizedTests {
@Test
fun kk_kazakh() {
- LibresSettings.languageCode = "kk"
+ HumanReadable.config.languageTag = "kk"
assertThat(HumanReadable.timeAgo(now)).isEqualTo("қазір")
- assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунд")
assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 секунд бұрын")
assertThat(HumanReadable.timeAgo(twoSecondsFromNow, baseInstant = now)).isEqualTo("2 секундтан кейін")
- assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минут")
assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 минут бұрын")
assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 минуттан кейін")
- assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 сағат")
assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1 сағат бұрын")
assertThat(HumanReadable.timeAgo(oneHourFromNow, baseInstant = now)).isEqualTo("1 сағаттан кейін")
- assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 күн")
assertThat(HumanReadable.timeAgo(oneDayAgo, baseInstant = now)).isEqualTo("1 күн бұрын")
assertThat(HumanReadable.timeAgo(oneDayFromNow, baseInstant = now)).isEqualTo("1 күннен кейін")
- assertThat(HumanReadable.duration(oneWeek)).isEqualTo("1 апта")
assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("1 апта бұрын")
assertThat(HumanReadable.timeAgo(oneWeekFromNow, baseInstant = now)).isEqualTo("1 аптадан кейін")
- assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 ай")
assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("2 ай бұрын")
assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("2 айдан кейін")
- assertThat(HumanReadable.duration(oneYear)).isEqualTo("1 жыл")
assertThat(HumanReadable.timeAgo(oneYearAgo, baseInstant = now)).isEqualTo("1 жыл бұрын")
assertThat(HumanReadable.timeAgo(oneYearFromNow, baseInstant = now)).isEqualTo("1 жылдан кейін")
+
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунд")
+ assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минут")
+ assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 сағат")
+ assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 күн")
+ assertThat(HumanReadable.duration(oneWeek)).isEqualTo("1 апта")
+ assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 ай")
+ assertThat(HumanReadable.duration(oneYear)).isEqualTo("1 жыл")
}
@Test
fun ko_korean() {
- LibresSettings.languageCode = "ko"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 초")
+ HumanReadable.config.languageTag = "ko"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2초")
+ assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2개월")
+
+ assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1시간 전")
+ assertThat(HumanReadable.timeAgo(twoHoursAgo, baseInstant = now)).isEqualTo("2시간 전")
+ assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("2개월 후")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34")
@@ -239,8 +269,8 @@ class LocalizedTests {
@Test
fun nl_dutch() {
- LibresSettings.languageCode = "nl"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconden")
+ HumanReadable.config.languageTag = "nl"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconden")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -254,10 +284,10 @@ class LocalizedTests {
@Test
fun pl_polish() {
- LibresSettings.languageCode = "pl"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy")
-
+ HumanReadable.config.languageTag = "pl"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy")
assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 minuta")
+
assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 minutę temu")
assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("za 1 minutę")
@@ -267,10 +297,10 @@ class LocalizedTests {
@Test
fun pt_portuguese() {
- LibresSettings.languageCode = "pt"
- assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 segundo")
- assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 segundo")
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos")
+ HumanReadable.config.languageTag = "pt"
+ assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 segundo")
+ assertThat(HumanReadable.duration(oneSecond)).isEqualTo("1 segundo")
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -278,10 +308,10 @@ class LocalizedTests {
@Test
fun ru_russian() {
- LibresSettings.languageCode = "ru"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунды")
-
+ HumanReadable.config.languageTag = "ru"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунды")
assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минута")
+
assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 минуту назад")
assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("через 1 минуту")
@@ -291,8 +321,8 @@ class LocalizedTests {
@Test
fun tr_turkish() {
- LibresSettings.languageCode = "tr"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 saniye")
+ HumanReadable.config.languageTag = "tr"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 saniye")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -300,8 +330,14 @@ class LocalizedTests {
@Test
fun uk_ukrainian() {
- LibresSettings.languageCode = "uk"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунди")
+ HumanReadable.config.languageTag = "uk"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунди")
+ assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 хвилина")
+ assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 година")
+
+ assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 хвилину тому")
+ assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("через 1 хвилину")
+ assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1 годину тому")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -309,8 +345,13 @@ class LocalizedTests {
@Test
fun uz_uzbek() {
- LibresSettings.languageCode = "uz"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 soniya")
+ HumanReadable.config.languageTag = "uz"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 soniya")
+ assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 daqiqa")
+
+ assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 daqiqa oldin")
+ assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 daqiqadan keyin")
+ assertThat(HumanReadable.timeAgo(twoHoursFromNow, baseInstant = now)).isEqualTo("2 soatdan keyin")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -318,8 +359,11 @@ class LocalizedTests {
@Test
fun vi_vietnamese() {
- LibresSettings.languageCode = "vi"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 giây")
+ HumanReadable.config.languageTag = "vi"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 giây")
+
+ assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 giây trước")
+ assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("sau 1 phút")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34")
@@ -327,10 +371,10 @@ class LocalizedTests {
@Test
fun zh_chinese() {
- LibresSettings.languageCode = "zh"
- assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒")
+ HumanReadable.config.languageTag = "zh"
+ assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒")
assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34")
assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34")
}
-}
+}
\ No newline at end of file
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt
new file mode 100644
index 0000000..01c576b
--- /dev/null
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt
@@ -0,0 +1,401 @@
+package nl.jacobras.humanreadable.time
+
+import assertk.assertThat
+import assertk.assertions.isEmpty
+import assertk.assertions.isEqualTo
+import nl.jacobras.humanreadable.HumanReadable
+import nl.jacobras.humanreadable.HumanReadable.duration
+import nl.jacobras.humanreadable.Time
+import nl.jacobras.humanreadable.localized.LocalisedTests
+import nl.jacobras.humanreadable.time.Rounding.Floor
+import nl.jacobras.humanreadable.time.Rounding.HalfUp
+import nl.jacobras.humanreadable.time.Rounding.IfClose
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Main test for the specific configuration options related to duration formatting.
+ *
+ * All in English, see [LocalisedTests] for specific tests for the supported languages.
+ */
+class HumanReadableDurationTests {
+
+ private val longStyle = FormatStyle(date = FormatStyle.Date.Long)
+ private val shortStyle = FormatStyle(date = FormatStyle.Date.Short)
+ private val narrowStyle = FormatStyle(date = FormatStyle.Date.Narrow)
+
+ init {
+ HumanReadable.config.languageTag = "en"
+ }
+
+ @BeforeTest
+ fun resetConfig() {
+ HumanReadable.config.time = Time()
+ }
+
+ @Test
+ fun halfUpRounding() {
+ assertThat(duration(1.seconds, rounding = HalfUp)).isEqualTo("1 second")
+ assertThat(duration(3.seconds, rounding = HalfUp)).isEqualTo("3 seconds")
+ assertThat(duration(55.seconds, rounding = HalfUp)).isEqualTo("55 seconds")
+
+ assertThat(duration(1.minutes, rounding = HalfUp)).isEqualTo("1 minute")
+ assertThat(duration(1.minutes + 55.seconds, rounding = HalfUp)).isEqualTo("2 minutes")
+ assertThat(duration(3.minutes, rounding = HalfUp)).isEqualTo("3 minutes")
+ assertThat(duration(55.minutes, rounding = HalfUp)).isEqualTo("55 minutes")
+
+ assertThat(duration(1.hours, rounding = HalfUp)).isEqualTo("1 hour")
+ assertThat(duration(3.hours, rounding = HalfUp)).isEqualTo("3 hours")
+ assertThat(duration(23.hours, rounding = HalfUp)).isEqualTo("23 hours")
+
+ assertThat(duration(1.days, rounding = HalfUp)).isEqualTo("1 day")
+ assertThat(duration(3.days, rounding = HalfUp)).isEqualTo("3 days")
+
+ assertThat(duration(7.days, rounding = HalfUp)).isEqualTo("1 week")
+ assertThat(duration(10.days, rounding = HalfUp)).isEqualTo("1 week")
+ assertThat(duration(11.days, rounding = HalfUp)).isEqualTo("2 weeks")
+ assertThat(duration(12.days, rounding = HalfUp)).isEqualTo("2 weeks")
+
+ assertThat(duration(21.days, rounding = HalfUp)).isEqualTo("3 weeks")
+ assertThat(duration(29.days, rounding = HalfUp)).isEqualTo("4 weeks")
+ assertThat(duration(30.days, rounding = HalfUp)).isEqualTo("4 weeks")
+ assertThat(duration(31.days, rounding = HalfUp)).isEqualTo("1 month")
+ assertThat(duration(46.days, rounding = HalfUp)).isEqualTo("2 months")
+ assertThat(duration(90.days, rounding = HalfUp)).isEqualTo("3 months")
+
+ assertThat(duration(360.days, rounding = HalfUp)).isEqualTo("12 months")
+ assertThat(duration(365.days, rounding = HalfUp)).isEqualTo("1 year")
+ assertThat(duration(555.days, rounding = HalfUp)).isEqualTo("2 years")
+ assertThat(duration(1095.days, rounding = HalfUp)).isEqualTo("3 years")
+ }
+
+ @Test
+ fun floorRounding() {
+ assertThat(duration(10.days, rounding = Floor)).isEqualTo("1 week")
+ assertThat(duration(11.days, rounding = Floor)).isEqualTo("1 week")
+ assertThat(duration(12.days, rounding = Floor)).isEqualTo("1 week")
+
+ assertThat(duration(21.days, rounding = Floor)).isEqualTo("3 weeks")
+ assertThat(duration(29.days, rounding = Floor)).isEqualTo("4 weeks")
+ assertThat(duration(30.days, rounding = Floor)).isEqualTo("4 weeks")
+ assertThat(duration(31.days, rounding = Floor)).isEqualTo("1 month")
+ assertThat(duration(46.days, rounding = Floor)).isEqualTo("1 month")
+
+ assertThat(duration(360.days, rounding = Floor)).isEqualTo("11 months")
+ assertThat(duration(365.days, rounding = Floor)).isEqualTo("1 year")
+ assertThat(duration(555.days, rounding = Floor)).isEqualTo("1 year")
+ assertThat(duration(1095.days, rounding = Floor)).isEqualTo("3 years")
+ }
+
+ @Test
+ fun ifCloseRounding() {
+ assertThat(duration(1.seconds, rounding = IfClose())).isEqualTo("1 second")
+ assertThat(duration(3.seconds, rounding = IfClose())).isEqualTo("3 seconds")
+
+ assertThat(duration(55.seconds, rounding = IfClose())).isEqualTo("1 minute")
+ assertThat(duration(1.minutes, rounding = IfClose())).isEqualTo("1 minute")
+ assertThat(duration(3.minutes, rounding = IfClose())).isEqualTo("3 minutes")
+
+ assertThat(duration(55.minutes, rounding = IfClose())).isEqualTo("1 hour")
+ assertThat(duration(1.hours, rounding = IfClose())).isEqualTo("1 hour")
+ assertThat(duration(3.hours, rounding = IfClose())).isEqualTo("3 hours")
+
+ assertThat(duration(23.hours, rounding = IfClose())).isEqualTo("1 day")
+ assertThat(duration(1.days, rounding = IfClose())).isEqualTo("1 day")
+ assertThat(duration(3.days, rounding = IfClose())).isEqualTo("3 days")
+
+ assertThat(duration(7.days, rounding = IfClose())).isEqualTo("1 week")
+ assertThat(duration(10.days, rounding = IfClose())).isEqualTo("1 week")
+ assertThat(duration(11.days, rounding = IfClose())).isEqualTo("1 week")
+ assertThat(duration(12.days, rounding = IfClose())).isEqualTo("1 week")
+
+ assertThat(duration(21.days, rounding = IfClose())).isEqualTo("3 weeks")
+ assertThat(duration(29.days, rounding = IfClose())).isEqualTo("4 weeks")
+ assertThat(duration(30.days, rounding = IfClose())).isEqualTo("4 weeks")
+ assertThat(duration(31.days, rounding = IfClose())).isEqualTo("1 month")
+ assertThat(duration(90.days, rounding = IfClose())).isEqualTo("2 months")
+
+ assertThat(duration(360.days, rounding = IfClose())).isEqualTo("11 months")
+ assertThat(duration(365.days, rounding = IfClose())).isEqualTo("1 year")
+ assertThat(duration(555.days, rounding = IfClose())).isEqualTo("1 year")
+ assertThat(duration(1095.days, rounding = IfClose())).isEqualTo("3 years")
+ }
+
+ @Test
+ fun ifCloseDefault() {
+ assertThat(duration(1.minutes + 30.seconds, rounding = IfClose(default = Floor))).isEqualTo("1 minute")
+ assertThat(duration(1.minutes + 30.seconds, rounding = IfClose(default = HalfUp))).isEqualTo("2 minutes")
+ }
+
+ @Test
+ fun limitedUnits() {
+ assertThat(duration(400.days, units = setOf(TimeUnit.Years))).isEqualTo("1 year")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Months))).isEqualTo("13 months")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Weeks))).isEqualTo("57 weeks")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Days))).isEqualTo("400 days")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Hours))).isEqualTo("9,600 hours")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes")
+ assertThat(duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds")
+ assertThat(duration(400.days, units = emptySet())).isEmpty()
+ assertThat(duration(1.days, units = setOf(TimeUnit.Months))).isEqualTo("0 months")
+ }
+
+ @Test
+ fun multipleParts() {
+ assertThat(
+ duration(70.seconds, parts = PartsConfig(max = 2))
+ ).isEqualTo("1 minute, 10 seconds")
+ assertThat(
+ duration(90.minutes + 10.seconds, parts = PartsConfig(max = 3, subpartCutOffs = emptyMap()))
+ ).isEqualTo("1 hour, 30 minutes, 10 seconds")
+ assertThat(
+ duration(3.seconds, parts = PartsConfig(max = 2))
+ ).isEqualTo("3 seconds")
+ assertThat(
+ duration(1.minutes + 55.seconds, rounding = HalfUp, parts = PartsConfig(max = 2))
+ ).isEqualTo("1 minute, 55 seconds")
+ assertThat(
+ duration(1.minutes + 55.seconds, rounding = IfClose(), parts = PartsConfig(max = 2))
+ ).isEqualTo("2 minutes")
+
+ // Edge cases
+ assertThat(
+ duration(59.minutes + 55.seconds, rounding = IfClose(), parts = PartsConfig(max = 2))
+ ).isEqualTo("1 hour")
+ assertThat(
+ duration(23.hours + 55.minutes, rounding = IfClose(), parts = PartsConfig(max = 2))
+ ).isEqualTo("1 day")
+ assertThat(
+ duration(6.days + 23.hours, rounding = IfClose(), parts = PartsConfig(max = 2))
+ ).isEqualTo("1 week")
+ }
+
+ @Test
+ fun onlyConnectedSubparts() {
+ assertThat(
+ duration(1.hours + 10.seconds, parts = PartsConfig(max = 2, onlyConsecutiveParts = false))
+ ).isEqualTo("1 hour, 10 seconds")
+ assertThat(
+ duration(1.hours + 10.seconds, parts = PartsConfig(max = 2, onlyConsecutiveParts = true))
+ ).isEqualTo("1 hour")
+ assertThat(
+ duration(23.hours + 5.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf()))
+ ).isEqualTo("23 hours, 5 minutes")
+ assertThat(
+ duration(24.hours + 5.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf()))
+ ).isEqualTo("1 day")
+ }
+
+ @Test
+ fun smallestDuration() {
+ assertThat(
+ duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = longStyle)
+ ).isEqualTo("less than 45 seconds")
+ assertThat(
+ duration(10.minutes, parts = PartsConfig(smallestDuration = 15.minutes), formatting = longStyle)
+ ).isEqualTo("less than 15 minutes")
+
+ assertThat(
+ duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = shortStyle)
+ ).isEqualTo("<45 sec")
+ assertThat(
+ duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = narrowStyle)
+ ).isEqualTo("<45s")
+
+ assertThat(duration(45.seconds, parts = PartsConfig(smallestDuration = 45.seconds))).isEqualTo("45 seconds")
+ }
+
+ @Test
+ fun subpartsCutOffs() {
+ assertThat(
+ duration(1.minutes + 4.seconds, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2)))
+ ).isEqualTo("1 minute, 4 seconds")
+ assertThat(
+ duration(2.minutes + 4.seconds, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2)))
+ ).isEqualTo("2 minutes")
+
+ assertThat(
+ duration(19.hours + 4.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2)))
+ ).isEqualTo("19 hours")
+ assertThat(
+ duration(20.hours + 4.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2)))
+ ).isEqualTo("20 hours")
+
+ assertThat(
+ duration(1.days + 5.hours, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2)))
+ ).isEqualTo("1 day, 5 hours")
+ assertThat(
+ duration(2.days + 5.hours, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2)))
+ ).isEqualTo("2 days")
+ }
+
+ @Test
+ fun formatStyle() {
+ assertThat(
+ duration(1.hours + 50.minutes, formatting = longStyle, parts = PartsConfig(max = 2))
+ ).isEqualTo("1 hour, 50 minutes")
+ assertThat(
+ duration(1.hours + 50.minutes, formatting = shortStyle, parts = PartsConfig(max = 2))
+ ).isEqualTo("1 hr, 50 min")
+ assertThat(
+ duration(1.hours + 50.minutes, formatting = narrowStyle, parts = PartsConfig(max = 2))
+ ).isEqualTo("1h 50m")
+
+ assertThat(duration(1.hours, formatting = longStyle)).isEqualTo("1 hour")
+ assertThat(duration(1.hours, formatting = shortStyle)).isEqualTo("1 hr")
+ assertThat(duration(1.hours, formatting = narrowStyle)).isEqualTo("1h")
+ assertThat(duration(14.days, formatting = longStyle)).isEqualTo("2 weeks")
+ assertThat(duration(14.days, formatting = shortStyle)).isEqualTo("2 wks")
+ assertThat(duration(14.days, formatting = narrowStyle)).isEqualTo("2w")
+ assertThat(duration(180.days, formatting = longStyle)).isEqualTo("6 months")
+ assertThat(duration(180.days, formatting = shortStyle)).isEqualTo("6 mths")
+ assertThat(duration(180.days, formatting = narrowStyle)).isEqualTo("6m")
+ }
+
+ @Test
+ fun formatStyleApproximation() {
+ assertThat(
+ duration(
+ duration = 1.hours,
+ formatting = longStyle.copy(indicateApproximation = true)
+ )
+ ).isEqualTo("1 hour")
+ assertThat(
+ duration(
+ duration = 1.hours + 1.minutes,
+ formatting = longStyle.copy(indicateApproximation = true)
+ )
+ ).isEqualTo("about 1 hour")
+ assertThat(
+ duration(
+ duration = 1.hours + 1.minutes,
+ formatting = shortStyle.copy(indicateApproximation = true)
+ )
+ ).isEqualTo("~1 hr")
+ assertThat(
+ duration(
+ duration = 1.hours + 1.minutes,
+ formatting = narrowStyle.copy(indicateApproximation = true)
+ )
+ ).isEqualTo("~1h")
+ assertThat(
+ duration(
+ duration = 1.hours + 1.minutes,
+ formatting = longStyle.copy(indicateApproximation = true),
+ parts = PartsConfig(smallestDuration = 1.days)
+ )
+ ).isEqualTo("less than 1 day")
+ assertThat(
+ duration(
+ duration = 1.days + 1.hours + 4.minutes,
+ formatting = longStyle.copy(indicateApproximation = true),
+ parts = PartsConfig(max = 5)
+ )
+ ).isEqualTo("1 day, 1 hour, 4 minutes")
+ }
+
+ @Test
+ fun formatStyleDigitalTime() {
+ assertThat(
+ duration(
+ duration = 5.minutes,
+ formatting = FormatStyle(
+ time = FormatStyle.Time.Digital,
+ indicateApproximation = true
+ )
+ )
+ ).isEqualTo("00:05:00")
+
+ assertThat(
+ duration(
+ duration = 1.hours,
+ formatting = FormatStyle(time = FormatStyle.Time.Digital),
+ parts = PartsConfig(max = 5)
+ )
+ ).isEqualTo("01:00:00")
+
+ assertThat(
+ duration(
+ duration = 1.hours + 10.minutes + 5.seconds,
+ formatting = FormatStyle(time = FormatStyle.Time.Digital),
+ parts = PartsConfig(max = 5)
+ )
+ ).isEqualTo("01:10:05")
+
+ assertThat(
+ duration(
+ duration = 5.days + 1.hours,
+ formatting = FormatStyle(time = FormatStyle.Time.Digital),
+ parts = PartsConfig(max = 5)
+ )
+ ).isEqualTo("5 days, 01:00:00")
+
+ assertThat(
+ duration(
+ duration = 21.days + 2.days + 1.hours,
+ formatting = FormatStyle(time = FormatStyle.Time.Digital),
+ parts = PartsConfig(max = 5)
+ )
+ ).isEqualTo("3 weeks, 2 days, 01:00:00")
+ }
+
+ @Test
+ fun globalConfig() {
+ HumanReadable.config.time.formatting = FormatStyle(
+ time = FormatStyle.Time.Digital,
+ indicateApproximation = true
+ )
+ assertThat(duration(5.minutes)).isEqualTo("00:05:00")
+ }
+
+ @Test
+ fun advancedConfig() {
+ HumanReadable.config.time.formatting = FormatStyle(indicateApproximation = true)
+ HumanReadable.config.time.parts = PartsConfig(
+ max = 2,
+ smallestDuration = 30.seconds,
+ subpartCutOffs = mapOf(
+ TimeUnit.Minutes to 1,
+ TimeUnit.Hours to 3
+ )
+ )
+ HumanReadable.config.time.rounding = IfClose(
+ thresholds = mapOf(TimeUnit.Minutes to 5),
+ default = Floor
+ )
+
+ assertThat(duration(15.seconds)).isEqualTo("less than 30 seconds")
+ assertThat(duration(45.seconds)).isEqualTo("45 seconds")
+ assertThat(duration(1.minutes + 45.seconds)).isEqualTo("about 1 minute")
+ assertThat(duration(2.minutes + 15.seconds)).isEqualTo("about 2 minutes")
+ assertThat(duration(1.hours + 4.minutes)).isEqualTo("about 1 hour")
+ assertThat(duration(1.hours + 5.minutes)).isEqualTo("1 hour, 5 minutes")
+ assertThat(duration(1.hours + 55.minutes)).isEqualTo("about 2 hours")
+ assertThat(duration(2.hours + 4.minutes)).isEqualTo("about 2 hours")
+ assertThat(duration(2.hours + 5.minutes)).isEqualTo("2 hours, 5 minutes")
+ assertThat(duration(2.hours + 54.minutes)).isEqualTo("2 hours, 54 minutes")
+ assertThat(duration(2.hours + 55.minutes)).isEqualTo("about 3 hours")
+ assertThat(duration(24.hours + 4.minutes)).isEqualTo("about 1 day")
+ assertThat(duration(24.hours + 5.minutes)).isEqualTo("about 1 day")
+ assertThat(duration(32.hours + 55.minutes)).isEqualTo("about 1 day, 8 hours")
+ assertThat(duration(2.days)).isEqualTo("2 days")
+ assertThat(duration(2.days + 4.minutes)).isEqualTo("about 2 days")
+ assertThat(duration(2.days + 5.minutes)).isEqualTo("about 2 days")
+ assertThat(duration(2.days + 1.hours + 4.minutes)).isEqualTo("about 2 days, 1 hour")
+ assertThat(duration(2.days + 2.hours + 4.minutes)).isEqualTo("about 2 days, 2 hours")
+ assertThat(duration(3.days)).isEqualTo("3 days")
+ assertThat(duration(3.days + 4.minutes)).isEqualTo("about 3 days")
+ assertThat(duration(3.days + 5.minutes)).isEqualTo("about 3 days")
+ assertThat(duration(3.days + 1.hours + 4.minutes)).isEqualTo("about 3 days, 1 hour")
+ assertThat(duration(3.days + 2.hours + 4.minutes)).isEqualTo("about 3 days, 2 hours")
+ assertThat(duration(4.days)).isEqualTo("4 days")
+ assertThat(duration(4.days + 4.minutes)).isEqualTo("about 4 days")
+ assertThat(duration(4.days + 5.minutes)).isEqualTo("about 4 days")
+ assertThat(duration(4.days + 1.hours + 4.minutes)).isEqualTo("about 4 days, 1 hour")
+ assertThat(duration(4.days + 2.hours + 4.minutes)).isEqualTo("about 4 days, 2 hours")
+ }
+}
\ No newline at end of file
diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt
new file mode 100644
index 0000000..9193ea9
--- /dev/null
+++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt
@@ -0,0 +1,84 @@
+package nl.jacobras.humanreadable.time
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.minus
+import kotlinx.datetime.plus
+import nl.jacobras.humanreadable.HumanReadable
+import nl.jacobras.humanreadable.localized.LocalisedTests
+import kotlin.test.Test
+import kotlin.time.Clock
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+
+/**
+ * Main test for the specific configuration options related to relative time formatting.
+ *
+ * All in English, see [LocalisedTests] for specific tests for the supported languages.
+ */
+@OptIn(ExperimentalTime::class)
+class HumanReadableRelativeTimeTests {
+
+ init {
+ HumanReadable.config.languageTag = "en"
+ }
+
+ private val now: Instant
+ get() = Clock.System.now()
+
+ @Test
+ fun inFuture() {
+ assertThat(HumanReadable.timeAgo(now + 200.seconds, baseInstant = now)).isEqualTo("in 3 minutes")
+ }
+
+ @Test
+ fun now() {
+ assertThat(HumanReadable.timeAgo(now, baseInstant = now)).isEqualTo("now")
+ }
+
+ @Test
+ fun inPast() {
+ assertThat(HumanReadable.timeAgo(now - 3.days, baseInstant = now)).isEqualTo("3 days ago")
+ }
+
+ @Test
+ fun todayTomorrowYesterday() {
+ val today = LocalDate.parse("2026-07-01")
+
+ assertThat(
+ HumanReadable.timeAgo(
+ date = today.minus(1, DateTimeUnit.DAY),
+ baseDate = today
+ )
+ ).isEqualTo("yesterday")
+ assertThat(
+ HumanReadable.timeAgo(
+ date = today,
+ baseDate = today
+ )
+ ).isEqualTo("today")
+ assertThat(
+ HumanReadable.timeAgo(
+ date = today.plus(1, DateTimeUnit.DAY),
+ baseDate = today
+ )
+ ).isEqualTo("tomorrow")
+
+ assertThat(
+ HumanReadable.timeAgo(
+ date = today.minus(2, DateTimeUnit.DAY),
+ baseDate = today
+ )
+ ).isEqualTo("2 days ago")
+ assertThat(
+ HumanReadable.timeAgo(
+ date = today.plus(2, DateTimeUnit.DAY),
+ baseDate = today
+ )
+ ).isEqualTo("in 2 days")
+ }
+}
\ No newline at end of file
diff --git a/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
deleted file mode 100644
index 699cbd0..0000000
--- a/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package nl.jacobras.humanreadable
-
-internal actual fun extendLibresPlurals() {
- // NOOP, only needed for Apple & JS.
-}
\ No newline at end of file
diff --git a/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.jvm.kt b/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.jvm.kt
new file mode 100644
index 0000000..d7d65c1
--- /dev/null
+++ b/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.jvm.kt
@@ -0,0 +1,5 @@
+package nl.jacobras.humanreadable.i18n
+
+import java.util.Locale
+
+internal actual fun systemLanguageTag(): String = Locale.getDefault().language
\ No newline at end of file
diff --git a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt
new file mode 100644
index 0000000..08b4d13
--- /dev/null
+++ b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt
@@ -0,0 +1,83 @@
+package nl.jacobras.humanreadable.i18n
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import org.junit.jupiter.api.BeforeEach
+import java.util.*
+import kotlin.test.Test
+
+class LocalisationTest {
+
+ @BeforeEach
+ fun setup() {
+ Locale.setDefault(Locale.ENGLISH)
+ }
+
+ @Test
+ fun `languageTag defaults to system language`() {
+ // Given a system language of French
+ Locale.setDefault(Locale.FRANCE)
+
+ val localisation = Localisation()
+
+ // Then the language tag should be French
+ assertThat(localisation.languageTag).isEqualTo("fr")
+ }
+
+ @Test
+ fun `languageTag falls back to English (by default) if system language is not supported`() {
+ // Given a system language of a language that is not supported
+ Locale.setDefault(Locale.forLanguageTag("xx-XX"))
+
+ val localisation = Localisation()
+
+ // Then the language tag should be English (the default fallback)
+ assertThat(localisation.languageTag).isEqualTo("en")
+ }
+
+ @Test
+ fun `setting requested language tag`() {
+ val localisation = Localisation()
+ localisation.requestedLanguageTag = "de"
+
+ assertThat(localisation.languageTag).isEqualTo("de")
+ }
+
+ @Test
+ fun `setting requested language tag with region`() {
+ val localisation = Localisation()
+ localisation.requestedLanguageTag = "fr-FR"
+
+ assertThat(localisation.languageTag).isEqualTo("fr")
+ }
+
+ @Test
+ fun `setting capitalised requested language tag`() {
+ val localisation = Localisation()
+ localisation.requestedLanguageTag = "DE"
+
+ assertThat(localisation.languageTag).isEqualTo("de")
+ }
+
+ @Test
+ fun `languageTag falls back to a custom fallback if system language is not supported`() {
+ // Given a system language of a language that is not supported
+ Locale.setDefault(Locale.forLanguageTag("xx-XX"))
+
+ // and the fallback language tag is set to French
+ val localisation = Localisation()
+ localisation.fallbackLanguageTag = "fr"
+
+ // Then the used language tag should be French
+ assertThat(localisation.languageTag).isEqualTo("fr")
+ }
+
+ @Test
+ fun `setting a non-supported fallback language tag changes nothing`() {
+ val localisation = Localisation()
+ localisation.fallbackLanguageTag = "xx"
+
+ // Then the used language tag stays English (the default fallback)
+ assertThat(localisation.languageTag).isEqualTo("en")
+ }
+}
\ No newline at end of file
diff --git a/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt b/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt
new file mode 100644
index 0000000..550b430
--- /dev/null
+++ b/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt
@@ -0,0 +1,20 @@
+package nl.jacobras.humanreadable.i18n
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.toKString
+import platform.posix.getenv
+
+@OptIn(ExperimentalForeignApi::class)
+internal actual fun systemLanguageTag(): String {
+ val envValue = getenv("LANGUAGE")?.toKString()?.substringBefore(':')
+ ?: getenv("LC_ALL")?.toKString()
+ ?: getenv("LANG")?.toKString()
+ return envValue
+ ?.substringBefore('.') // Remove charset: "en_US.UTF-8" » "en_US"
+ ?.substringBefore('@') // Remove modifier: "sr_RS@latin" » "sr_RS"
+ ?.takeIf {
+ // Apparently some distros may return "C" or "POSIX" if there is no locale data
+ it.isNotBlank() && !it.equals("C", true) && !it.equals("POSIX", true)
+ }
+ ?.replace('_', '-') ?: "en"
+}
\ No newline at end of file
diff --git a/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt b/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt
new file mode 100644
index 0000000..b693a26
--- /dev/null
+++ b/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt
@@ -0,0 +1,20 @@
+package nl.jacobras.humanreadable.i18n
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.allocArray
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.toKStringFromUtf16
+import platform.windows.GetUserDefaultLocaleName
+import platform.windows.LOCALE_NAME_MAX_LENGTH
+import platform.windows.WCHARVar
+
+@OptIn(ExperimentalForeignApi::class)
+internal actual fun systemLanguageTag(): String = memScoped {
+ val buffer = allocArray(LOCALE_NAME_MAX_LENGTH)
+ val length = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH)
+ return if (length > 0) {
+ buffer.toKStringFromUtf16()
+ } else {
+ "en"
+ }
+}
\ No newline at end of file
diff --git a/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.web.kt b/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.web.kt
new file mode 100644
index 0000000..a3994f8
--- /dev/null
+++ b/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.web.kt
@@ -0,0 +1,7 @@
+package nl.jacobras.humanreadable.i18n
+
+import kotlinx.browser.window
+
+private fun navigatorLanguage(): String = window.navigator.language
+
+internal actual fun systemLanguageTag(): String = navigatorLanguage().substringBefore('-')
\ No newline at end of file