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 @@ ![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) @@ -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