Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
77e89da
Bump version to 2.0.0
jacobras Jun 22, 2026
c4bfca9
Custom i18n impl
jacobras Jun 20, 2026
8c5039b
Fix setting language tag with region
jacobras Jun 23, 2026
93665d4
Fix setting capitalised language tag
jacobras Jun 23, 2026
15629cd
Add tvOS + watchOS support
jacobras Jun 23, 2026
8f2f078
Update spelling
jacobras Jun 24, 2026
1c8f959
Fix a bunch of translation issues
jacobras Jun 23, 2026
613a904
Improve test stability
jacobras Jun 24, 2026
e439b2c
Update Kdoc
jacobras Jun 24, 2026
61c35d9
Package time things together
jacobras Jun 24, 2026
0d15879
Add `Rounding` parameter
jacobras Jun 24, 2026
d998e6a
Add `UpIfClose` rounding
jacobras Jun 24, 2026
22b5baa
Group time tests together
jacobras Jun 24, 2026
31e4d54
Add `units` parameter to limit time units used in formatting
jacobras Jun 24, 2026
86e4610
Fix duration formatting of numbers > 1000
jacobras Jun 24, 2026
644ba96
Tweak `UpIfClose` rounding to floor by default
jacobras Jun 26, 2026
91fa065
Extract `UpIfClose` rounding params into constants
jacobras Jun 26, 2026
8fd8bf6
Expand configuration options
jacobras Jun 26, 2026
6f3f892
Add tests for new configuration options
jacobras Jun 26, 2026
6e8e72c
Implement yesterday/today/tomorrow
jacobras Jun 26, 2026
5217a0d
Remove wildcard import
jacobras Jun 26, 2026
3152fe8
Tweak `FormatStyle`, splitting up Short/Narrow and adding Digital var…
jacobras Jun 27, 2026
66139ed
Add Unicode links for translators
jacobras Jun 27, 2026
e994041
Make yesterday/today/tomorrow work for `LocalDate`s
jacobras Jun 30, 2026
93b2700
Implement new configurability options
jacobras Jun 29, 2026
761b083
Fix `Rounding.HalfUp`
jacobras Jul 1, 2026
8098166
Implement digital time formatting
jacobras Jul 1, 2026
000a232
Shorten approximation word
jacobras Jul 1, 2026
24e32fd
Rework demo UI
jacobras Jul 1, 2026
e12b099
Fix timeAgo() with LocalDate returning "now" incorrectly
jacobras Jul 2, 2026
b8518ee
Rename parameters
jacobras Jul 2, 2026
11ec270
Update README
jacobras Jul 2, 2026
f2f3349
Only format consecutive parts by default
jacobras Jul 2, 2026
7073098
Support setting global time formatting options
jacobras Jul 2, 2026
984c65c
Show version number in web demo
jacobras Jul 2, 2026
94c968e
Remove outdated workaround
jacobras Jul 2, 2026
320a2b8
Remove outdated TODO
jacobras Jul 2, 2026
8466c14
Update Compose to 1.12.0-beta01
jacobras Jul 2, 2026
13822d6
Update Gradle to 9.6.1
jacobras Jul 2, 2026
d24afcc
Fix "about" prefix not correctly added when formatting multiple parts
jacobras Jul 6, 2026
df02984
Make `IfClose` rounding configurable
jacobras Jul 7, 2026
5030d8b
Reorder list of targets
jacobras Jul 7, 2026
4d0ba25
Add native targets
jacobras Jul 7, 2026
ff376b0
Update translations
jacobras Jul 7, 2026
114925a
Fix today/tomorrow swapped in Arabic translation
jacobras Jul 9, 2026
76c1c13
Filter out language part on default value
jacobras Jul 9, 2026
e9b705e
Fix docs inconsistencies
jacobras Jul 9, 2026
523f77e
Add tests for large durations
jacobras Jul 9, 2026
6cc09df
Update docs
jacobras Jul 9, 2026
20b6285
Bump version to 2.0.0-alpha01
jacobras Jul 9, 2026
9269268
Update examples in `Date` docs
jacobras Jul 9, 2026
4ebb6a3
Reword docs
jacobras Jul 9, 2026
7431603
Remove duplicate docs
jacobras Jul 9, 2026
01fe028
Set version to 2.0.0-alpha01
jacobras Jul 9, 2026
92ddf17
Move `Simple` marker interface down
jacobras Jul 9, 2026
043b311
Add missing kdoc
jacobras Jul 9, 2026
3778bde
Simplify language tag examples in kdoc
jacobras Jul 9, 2026
65ce72b
Update timeInFuture to match Vietnamese grammar
phucynwa Jul 12, 2026
de6b11d
Fix `vi_vietnamese()` test
jacobras Jul 22, 2026
eeb87be
Bump version to 2.0.0-alpha02
jacobras Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
91 changes: 59 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

![Android](http://img.shields.io/badge/-android-6EDB8D.svg?style=flat)
![iOS](http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat)
![tvOS](http://img.shields.io/badge/-tvos-808080.svg?style=flat)
![watchOS](http://img.shields.io/badge/-watchos-D32D41.svg?style=flat)
![JS](http://img.shields.io/badge/-js-F8DB5D.svg?style=flat)
![wasm](https://img.shields.io/badge/-wasm-624DE9.svg?style=flat)

A small set of data formatting utilities for Kotlin Multiplatform (KMP).

This library only supports [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime).
The library depends on [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime).

## Installation

Expand All @@ -17,7 +19,7 @@ The library is published to Maven Central.

```kotlin
dependencies {
implementation("nl.jacobras:Human-Readable:1.13.1")
implementation("nl.jacobras:Human-Readable:2.0.0-alpha02")
}
```

Expand All @@ -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), // 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.Days), // limits the output to these units, e.g. "391 days"
rounding = Rounding.HalfUp // or Floor to round down, or IfClose to round up or down when close enough (5s/5m/23h/6d, configurable) to the larger unit
)
```

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

Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
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 <https://st.unicode.org/cldr-apps/v#/en/Duration/>
or <https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-units-full/main/en/units.json>, if you prefer the
JSON format.
49 changes: 24 additions & 25 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.1"
version = project.property("nl.jacobras.humanreadable.version") as String

mavenPublishing {
publishToMavenCentral()
Expand Down Expand Up @@ -49,6 +48,7 @@ kotlin {
explicitApi()
applyDefaultHierarchyTemplate()

// Targets, alphabetically sorted
iosX64()
iosArm64()
iosSimulatorArm64()
Expand All @@ -61,15 +61,6 @@ kotlin {
}
}
}
wasmJs {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
jvm {
testRuns.named("test") {
executionTask.configure {
Expand All @@ -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)
}
}
}
Expand All @@ -111,10 +114,6 @@ tasks.withType<AbstractPublishToMaven>().configureEach {
mustRunAfter(signingTasks)
}

libres {
generatedClassName = "HumanReadableRes"
}

signing {
setRequired {
!gradle.taskGraph.allTasks.any { it is PublishToMavenLocal }
Expand Down
8 changes: 8 additions & 0 deletions demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.buildconfig)
}

kotlin {
Expand All @@ -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)
Expand All @@ -37,4 +41,8 @@ kotlin {

rootProject.plugins.withType<YarnPlugin> {
rootProject.the<YarnRootExtension>().yarnLockMismatchReport = YarnLockMismatchReport.NONE
}

buildConfig {
buildConfigField("VERSION", project.property("nl.jacobras.humanreadable.version") as String)
}
Loading
Loading