From 89291a0fd4a35756adf0fc6b386fb8b50989e170 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Mon, 22 Sep 2025 18:55:27 +0200 Subject: [PATCH] feat: conditional, relative/absolute date/time formatter Signed-off-by: Andy Scherzinger --- .tx/config | 10 ++ .tx/config.license | 2 + .../android/common/core/SampleLibraryClass.kt | 11 --- .../common/core/utils/DateFormatter.kt | 94 +++++++++++++++++++ core/src/main/res/values/strings.xml | 18 ++++ 5 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 .tx/config create mode 100644 .tx/config.license delete mode 100644 core/src/main/java/com/nextcloud/android/common/core/SampleLibraryClass.kt create mode 100644 core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt create mode 100644 core/src/main/res/values/strings.xml diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..3ae1f32a --- /dev/null +++ b/.tx/config @@ -0,0 +1,10 @@ +[main] +host = https://www.transifex.com + +[o:nextcloud:p:nextcloud:r:android-common] +file_filter = core/src/main/res/values-/strings.xml +source_file = core/src/main/res/values/strings.xml +source_lang = en +type = ANDROID +lang_map = en_NZ: en-rNZ, qut_GT: qut-rGT, fo_FO: fo-rFO, ky_KG: ky-rKG, pl_PL: pl-rPL, ar_SA: ar-rSA, sa_IN: sa-rIN, ar_OM: ar-rOM, sr_RS: sr-rRS, bn_BD: bn-rBD, en_BZ: en-rBZ, ga_IE: ga-rIE, gsw_FR: gsw-rFR, rm_CH: rm-rCH, es_AR: es-rAR, ja_JP: ja-rJP, kok_IN: kok-rIN, zh_CN.GB2312: zh-rBG, it_CH: it-rCH, bo_CN: bo-rCN, ta_LK: ta-rLK, de_LU: de-rLU, fr_CH: fr-rCH, ug_CN: ug-rCN, az_AZ: az-rAZ, hu_HU: hu-rHU, lb_LU: lb-rLU, quz_EC: quz-rEC, tr_TR: tr-rTR, es_GT: es-rGT, tg_TJ: tg-rTJ, tk_TM: tk-rTM, ar_EG: ar-rEG, en_ZA: en-rZA, mn_MN: mn-rMN, sms_FI: sms-rFI, ar_LY: ar-rLY, ba_RU: ba-rRU, kk_KZ: kk-rKZ, nl_BE: nl-rBE, tzm_DZ: tzm-rDZ, de_AT: de-rAT, es_NI: es-rNI, or_IN: or-rIN, da_DK: da-rDK, dsb_DE: dsb-rDE, en@pirate: en-rpirate, lt_LT: lt-rLT, es_PY: es-rPY, bs_BA: bs-rBA, es_CO: es-rCO, pt_BR: pt-rBR, smj_NO: smj-rNO, ar_YE: ar-rYE, es_DO: es-rDO, fr_FR: fr-rFR, si_LK: si-rLK, sv_FI: sv-rFI, en_JM: en-rJM, sma_NO: sma-rNO, sma_SE: sma-rSE, zu_ZA: zu-rZA, ii_CN: ii-rCN, arn_CL: arn-rCL, be_BY: be-rBY, en_IN: en-rIN, es_EC: es-rEC, sq_AL: sq-rAL, ar_TN: ar-rTN, fr_LU: fr-rLU, xh_ZA: xh-rZA, am_ET: am-rET, ca_ES: ca-rES, fy_NL: fy-rNL, sr_ME: sr-rME, ar_KW: ar-rKW, hy_AM: hy-rAM, prs_AF: prs-rAF, en_AU: en-rAU, syr_SY: syr-rSY, uz_UZ: uz-rUZ, yo_NG: yo-rNG, es_HN: es-rHN, de_DE: de-rDE, es_CR: es-rCR, eu_ES: eu-rES, quz_BO: quz-rBO, sr_CS: sr-rCS, bn_IN: bn-rIN, dv_MV: dv-rMV, en_IE: en-rIE, mr_IN: mr-rIN, ne_NP: ne-rNP, co_FR: co-rFR, en_GB: b+en+001, ar_LB: ar-rLB, sw_KE: sw-rKE, hr_HR: hr-rHR, en_TT: en-rTT, smj_SE: smj-rSE, as_IN: as-rIN, id_ID: id-rID, mk_MK: mk-rMK, de_CH: de-rCH, oc_FR: oc-rFR, zh_CN: zh-rCN, zh_HK: zh-rHK, zh_SG: zh-rSG, ar_MA: ar-rMA, es_VE: es-rVE, ko_KR: ko-rKR, sr@latin: sr-rSP, ar_IQ: ar-rIQ, ig_NG: ig-rNG, mt_MT: mt-rMT, se_NO: se-rNO, bg_BG: bg-rBG, en_SG: en-rSG, ku_IQ: ku-rIQ, lo_LA: lo-rLA, ml_IN: ml-rIN, smn_FI: smn-rFI, uk_UA: uk-rUA, sr_BA: sr-rBA, mi_NZ: mi-rNZ, ar_DZ: ar-rDZ, hi_IN: hi-rIN, se_FI: se-rFI, fi_FI: fi-rFI, en_PH: en-rPH, ms_BN: ms-rBN, nn_NO: nn-rNO, pt_PT: pt-rPT, af_ZA: af-rZA, km_KH: km-rKH, es_SV: es-rSV, sah_RU: sah-rRU, en_US: en-rUS, id: in, pa_IN: pa-rIN, tt_RU: tt-rRU, ar_SY: ar-rSY, gu_IN: gu-rIN, my_MM: my, es_ES: es-rES, hr_BA: hr-rBA, br_FR: br-rFR, fr_MC: fr-rMC, ms_MY: ms-rMY, vi_VN: vi-rVN, ar_JO: ar-rJO, es_CL: es-rCL, fa_IR: fa-rIR, gl_ES: gl-rES, ps_AF: ps-rAF, ar_QA: ar-rQA, rw_RW: rw-rRW, ro_RO: ro-rRO, de_LI: de-rLI, cs_CZ: cs-rCZ, kl_GL: kl-rGL, es_PE: es-rPE, zh_TW: zh-rTW, fr_BE: fr-rBE, kn_IN: kn-rIN, tn_ZA: tn-rZA, hsb_DE: hsb-rDE, ar_BH: ar-rBH, ar_AE: ar-rAE, en_CA: en-rCA, he_IL: he-rIL, sv_SE: sv-rSE, ta_IN: ta-rIN, es_419: b+es+419, is_IS: is-rIS, iu_CA: iu-rCA, fil_PH: fil-rPH, mn_CN: mn-rCN, nso_ZA: nso-rZA, es_PA: es-rPA, nl_NL: nl-rNL, sk_SK: sk-rSK, en_MY: en-rMY, moh_CA: moh-rCA, cy_GB: cy-rGB, en_ZW: en-rZW, es_BO: es-rBO, es_MX: es-rMX, fr_CA: fr-rCA, it_IT: it-rIT, zh_MO: zh-rMO, sl_SI: sl-rSI, et_EE: et-rEE, gd_GB: gd-rGB, ka_GE: ka-rGE, nb_NO: nb-rNO, se_SE: se-rSE, ur_PK: ur-rPK, wo_SN: wo-rSN, el_GR: el-rGR, es_UY: es-rUY, quz_PE: quz-rPE, he: iw, ru_RU: ru-rRU, es_PR: es-rPR, lv_LV: lv-rLV, te_IN: te-rIN, th_TH: th-rTH, ha_NG: ha-rNG + diff --git a/.tx/config.license b/.tx/config.license new file mode 100644 index 00000000..812b9859 --- /dev/null +++ b/.tx/config.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: MIT diff --git a/core/src/main/java/com/nextcloud/android/common/core/SampleLibraryClass.kt b/core/src/main/java/com/nextcloud/android/common/core/SampleLibraryClass.kt deleted file mode 100644 index 7116ab15..00000000 --- a/core/src/main/java/com/nextcloud/android/common/core/SampleLibraryClass.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-License-Identifier: MIT - */ -package com.nextcloud.android.common.core - -// TODO remove when the core lib actually contains useful stuff -class SampleLibraryClass diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt new file mode 100644 index 00000000..3031f785 --- /dev/null +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt @@ -0,0 +1,94 @@ +/** + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ +package com.nextcloud.android.common.core.utils + +import android.content.Context +import android.os.Build +import android.text.format.DateFormat +import com.nextcloud.android.common.core.R +import java.text.SimpleDateFormat +import java.util.Calendar +import kotlin.text.toInt + +/** + * Helper implementation for date formatting. + */ +class DateFormatter( + private val context: Context +) { + private val sdfDays: SimpleDateFormat + private val sdfMonths: SimpleDateFormat + private val sdfYears: SimpleDateFormat + + /** + * constructor. + */ + init { + val locale = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + this.context.resources.configuration + .getLocales() + .get(0) + } else { + this.context.resources.configuration.locale + } + this.sdfDays = SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, "EEE"), locale) + this.sdfMonths = + SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, "MMM d"), locale) + this.sdfYears = + SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, "MMM d, yyyy"), locale) + } + + /** + * Returns a conditionally relative date formated string. + * For anything less than 1h hours a relative time in minutes will be returned, + * for anything less than 24h hours a relative time in hours will be returned, + * for anything less than 6 days the day of the week in short form, + * for anything up to 364 days a day and moth string will be returned, + * for anything more than 364 days from now a complete date will be returned. + * + * @param calendar to be formatted calendar + * @return formatted date strings + */ + fun getConditionallyRelativeFormattedTimeSpan(calendar: Calendar): String { + val span = System.currentTimeMillis() - calendar.getTimeInMillis() + return when { + // less than 1m + span < ONE_MINUTE_IN_MILLIS -> context.getString(R.string.date_formatting_now) + // less than 1h + span < ONE_HOUR_IN_MILLIS -> + context.getString( + R.string.date_formatting_relative_minutes, + span / ONE_MINUTE_IN_MILLIS + ) + // less than 1d + span < ONE_DAY_IN_MILLIS -> { + val hours: Int = span.toInt() / ONE_HOUR_IN_MILLIS + context + .resources + .getQuantityString(R.plurals.date_formatting_relative_hours, hours, hours) + } + // less than 1w + span <= SIX_DAYS_IN_MILLIS -> sdfDays.format(calendar.getTime()) + // less than 1y -> up to 364 days + span <= YEAR_IN_MILLIS -> sdfMonths.format(calendar.getTime()) + // more than 1y -> more than 364 days + else -> sdfYears.format(calendar.getTime()) + } + } + + /** + * Time constants for conditional formatting. + */ + companion object { + private const val ONE_MINUTE_IN_MILLIS: Int = 60000 + private const val ONE_HOUR_IN_MILLIS: Int = ONE_MINUTE_IN_MILLIS * 60 + private const val ONE_DAY_IN_MILLIS: Int = ONE_HOUR_IN_MILLIS * 24 + private const val SIX_DAYS_IN_MILLIS: Int = ONE_DAY_IN_MILLIS * 6 + private const val YEAR_IN_MILLIS: Long = ONE_DAY_IN_MILLIS * 364L + } +} diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml new file mode 100644 index 00000000..7aff6e87 --- /dev/null +++ b/core/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + + Now + + %dm + + + %dh + %dh + +