如何在 Java/Android 中的格式化字符串中显示本地化的日期单位?

How to show localized date units in a formatted string in Java/Android?

假设我有几天:

Period p = Period.ofDays(3);

我想用字符串中的 "days" 标签格式化句点,以将此字符串作为输出:

// "3 days"

...但是,我想本地化 "days" 组件,所以我不能使用像下面这样的格式化字符串,否则它只会以英文正确显示:

String.format("%d days", numberOfDays); // Won't localize 'days'

Java/Kotlin/Android 中有哪些 API 表示一段时间,例如语言环境中的小时、天、周、年?如果能让 API 去做,我宁愿不自己本地化这些词。

如一条评论中所建议,您可以使用我的库 Time4J 并使用以下代码:

Period p = Period.ofDays(3);
Locale loc = Locale.ENGLISH; // or any other supported locale
String formatted = PrettyTime.of(loc).print(p); // 3 days

该教程还包含一个 list of currently supported languages。顺便说一句,如果您对规范化或扩展的 ISO 兼容性等其他功能感兴趣,java.time.Period 的 Time4J 等价物将是 net.time4j.Duration<CalendarUnit>

注意:如果您使用的是 Android,那么您应该使用姊妹库 Time4A 而不是 Time4J,但提供的代码是相同的.

当使用具有本地化格式的Kotlin Duration类型时,因为找不到好的解决方案,我自己写了一个。它基于从 Android 9(针对本地化单元)开始提供的 API,但对于较低的 Android 版本可以回退到英语单元,因此它可以与较低目标的应用程序一起使用。

这是它在使用方面的样子(请参阅 Kotlin Duration 类型以了解第一行):

val duration = 5.days.plus(3.hours).plus(2.minutes).plus(214.milliseconds)

DurationFormat().format(duration) // "5day 3hour 2min"
DurationFormat(Locale.GERMANY).format(duration) // "5T 3Std. 2Min."
DurationFormat(Locale.forLanguageTag("ar").format(duration) // "٥يوم ٣ساعة ٢د"
DurationFormat().format(duration, smallestUnit = DurationFormat.Unit.HOUR) // "5day 3hour"
DurationFormat().format(15.minutes) // "15min"
DurationFormat().format(0.hours) // "0sec"

如您所见,您可以将自定义 locale 指定为 DurationFormat 类型。默认情况下它使用 Locale.getDefault()。还支持数字符号不同于罗马数字的语言(通过 NumberFormat)。另外,您可以指定自定义 smallestUnit,默认设置为 SECOND,因此不会显示毫秒。请注意,任何值为 0 的单位都将被忽略,如果整个数字为 0,则将使用值为 0.

的最小单位

这是用于复制和粘贴的完整DurationFormat类型(也可作为GitHub gist包括单元测试):

import android.icu.text.MeasureFormat
import android.icu.text.NumberFormat
import android.icu.util.MeasureUnit
import android.os.Build
import java.util.Locale
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.days
import kotlin.time.hours
import kotlin.time.milliseconds
import kotlin.time.minutes
import kotlin.time.seconds

@ExperimentalTime
data class DurationFormat(val locale: Locale = Locale.getDefault()) {
    enum class Unit {
        DAY, HOUR, MINUTE, SECOND, MILLISECOND
    }

    fun format(duration: kotlin.time.Duration, smallestUnit: Unit = Unit.SECOND): String {
        var formattedStringComponents = mutableListOf<String>()
        var remainder = duration

        for (unit in Unit.values()) {
            val component = calculateComponent(unit, remainder)

            remainder = when (unit) {
                Unit.DAY -> remainder - component.days
                Unit.HOUR -> remainder - component.hours
                Unit.MINUTE -> remainder - component.minutes
                Unit.SECOND -> remainder - component.seconds
                Unit.MILLISECOND -> remainder - component.milliseconds
            }

            val unitDisplayName = unitDisplayName(unit)

            if (component > 0) {
                val formattedComponent = NumberFormat.getInstance(locale).format(component)
                formattedStringComponents.add("$formattedComponent$unitDisplayName")
            }

            if (unit == smallestUnit) {
                val formattedZero = NumberFormat.getInstance(locale).format(0)
                if (formattedStringComponents.isEmpty()) formattedStringComponents.add("$formattedZero$unitDisplayName")
                break
            }
        }

        return formattedStringComponents.joinToString(" ")
    }

    private fun calculateComponent(unit: Unit, remainder: Duration) = when (unit) {
        Unit.DAY -> remainder.inDays.toLong()
        Unit.HOUR -> remainder.inHours.toLong()
        Unit.MINUTE -> remainder.inMinutes.toLong()
        Unit.SECOND -> remainder.inSeconds.toLong()
        Unit.MILLISECOND -> remainder.inMilliseconds.toLong()
    }

    private fun unitDisplayName(unit: Unit) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        val measureFormat = MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.NARROW)
        when (unit) {
            DurationFormat.Unit.DAY -> measureFormat.getUnitDisplayName(MeasureUnit.DAY)
            DurationFormat.Unit.HOUR -> measureFormat.getUnitDisplayName(MeasureUnit.HOUR)
            DurationFormat.Unit.MINUTE -> measureFormat.getUnitDisplayName(MeasureUnit.MINUTE)
            DurationFormat.Unit.SECOND -> measureFormat.getUnitDisplayName(MeasureUnit.SECOND)
            DurationFormat.Unit.MILLISECOND -> measureFormat.getUnitDisplayName(MeasureUnit.MILLISECOND)
        }
    } else {
        when (unit) {
            Unit.DAY -> "day"
            Unit.HOUR -> "hour"
            Unit.MINUTE -> "min"
            Unit.SECOND -> "sec"
            Unit.MILLISECOND -> "msec"
        }
    }
}