mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-05 18:30:59 +01:00
Add localized duration formatting and related string resources (#1571)
## Description Instead of using English-only duration format (e.g. `HHh MMm SSs`) in various places, I've added a format function that is based on several new localized resources keys. That includes: 1. Media runtime in Item view 2. Time remaining in Item view 3. End time in Player view 4. Time passed and time remained in Player view ### Related issues None ### Testing I installed the app on my Google TV Streamer, switched to my language, browsed all item views that I could think of to see that the duration labels are all localized in my language. I didn't commit those strings, I will do it through the Weblate after it will get merged. I also played a few items and saw that the labels there are localized as well. ## Screenshots It looks the same in English as before, currently no other language has custom resources for it yet. ## AI or LLM usage Used Claude Sonnet to detect places that I missed that are related to this localization change. Also it helped me to get access to my format function from various places in the code. --------- Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
parent
1d7ddd713b
commit
f6e00c7820
14 changed files with 86 additions and 25 deletions
|
|
@ -5,12 +5,15 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.ui.abbreviateNumber
|
||||
import com.github.damontecres.wholphin.ui.detail.CardGridItem
|
||||
import com.github.damontecres.wholphin.ui.detail.music.artistsString
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||
import com.github.damontecres.wholphin.ui.dot
|
||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.getDateFormatter
|
||||
import com.github.damontecres.wholphin.ui.joinNotBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -161,10 +164,18 @@ data class BaseItem(
|
|||
?.ticks
|
||||
?.roundMinutes
|
||||
?.takeIf { it > Duration.ZERO }
|
||||
?.let { add(it.toString()) }
|
||||
?.let { add(WholphinApplication.instance.resources.formatDuration(it)) }
|
||||
data.timeRemaining
|
||||
?.roundMinutes
|
||||
?.let { add("$it left") }
|
||||
?.let {
|
||||
val resources = WholphinApplication.instance.resources
|
||||
add(
|
||||
resources.getString(
|
||||
R.string.time_left,
|
||||
resources.formatDuration(it),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
details.forEachIndexed { index, string ->
|
||||
append(string)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.text.format.DateFormat
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
|
|
@ -21,6 +22,7 @@ import java.time.format.DateTimeParseException
|
|||
import java.time.format.FormatStyle
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Format the time-of-day of [time] honouring the device's 12/24-hour setting and the current
|
||||
|
|
@ -259,3 +261,18 @@ fun formatTypeName(type: BaseItemKind): Int =
|
|||
BaseItemKind.LIVE_TV_PROGRAM -> TODO()
|
||||
BaseItemKind.PLAYLISTS_FOLDER -> TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a [Duration] into a localized "Xh Ym Zs" string using string resources.
|
||||
*/
|
||||
fun Resources.formatDuration(duration: Duration): String =
|
||||
duration.toComponents { hours, minutes, seconds, _ ->
|
||||
when {
|
||||
hours > 0 && minutes > 0 && seconds > 0 -> getString(R.string.duration_hours_minutes_seconds, hours.toInt(), minutes, seconds)
|
||||
hours > 0 && minutes > 0 -> getString(R.string.duration_hours_minutes, hours.toInt(), minutes)
|
||||
hours > 0 -> getString(R.string.duration_hours, hours.toInt())
|
||||
minutes > 0 && seconds > 0 -> getString(R.string.duration_minutes_seconds, minutes, seconds)
|
||||
minutes > 0 -> getString(R.string.duration_minutes, minutes)
|
||||
else -> getString(R.string.duration_seconds, seconds)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Card
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.roundSeconds
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -62,8 +65,10 @@ fun ChapterCard(
|
|||
text = it,
|
||||
)
|
||||
}
|
||||
val resources = LocalResources.current
|
||||
val positionText = remember(position) { resources.formatDuration(position.roundSeconds) }
|
||||
Text(
|
||||
text = position.roundSeconds.toString(),
|
||||
text = positionText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import androidx.compose.ui.focus.focusRestorer
|
|||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.ListItemDefaults
|
||||
|
|
@ -58,6 +59,7 @@ import com.github.damontecres.wholphin.ui.cards.WatchedIcon
|
|||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.detail.AlphabetButtons
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
|
|
@ -357,7 +359,7 @@ fun ListItemGeneric(
|
|||
?.runTimeTicks
|
||||
?.ticks
|
||||
?.roundMinutes
|
||||
?.toString() ?: "",
|
||||
?.let { LocalResources.current.formatDuration(it) } ?: "",
|
||||
)
|
||||
},
|
||||
scale = scale,
|
||||
|
|
@ -419,7 +421,7 @@ fun ListItemMovie(
|
|||
?.runTimeTicks
|
||||
?.ticks
|
||||
?.roundMinutes
|
||||
?.toString() ?: "",
|
||||
?.let { LocalResources.current.formatDuration(it) } ?: "",
|
||||
)
|
||||
if (item?.played == true) {
|
||||
WatchedIcon(padding = 0.dp)
|
||||
|
|
@ -546,7 +548,7 @@ fun ListItemAlbum(
|
|||
.runTimeTicks
|
||||
?.ticks
|
||||
?.roundMinutes
|
||||
?.toString() ?: "",
|
||||
?.let { LocalResources.current.formatDuration(it) } ?: "",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
|||
import com.github.damontecres.wholphin.ui.formatBitrate
|
||||
import com.github.damontecres.wholphin.ui.formatBytes
|
||||
import com.github.damontecres.wholphin.ui.formatDate
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.util.StreamFormatting.formatAudioCodec
|
||||
|
|
@ -153,7 +154,7 @@ fun ItemDetailsDialog(
|
|||
)
|
||||
}
|
||||
source.runTimeTicks?.let {
|
||||
add(runtimeLabel to it.ticks.toString())
|
||||
add(runtimeLabel to resources.formatDuration(it.ticks))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import androidx.compose.ui.focus.focusRestorer
|
|||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -94,6 +95,7 @@ import com.github.damontecres.wholphin.ui.detail.music.MusicViewModel
|
|||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.formatDateTime
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.formatTime
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
|
|
@ -809,9 +811,11 @@ fun PlaylistItem(
|
|||
val endTime = now.toLocalTime().plusSeconds(duration.inWholeSeconds)
|
||||
formatTime(context, endTime)
|
||||
}
|
||||
val resources = LocalResources.current
|
||||
val durationText = remember(duration) { resources.formatDuration(duration) }
|
||||
Column {
|
||||
Text(
|
||||
text = duration.toString(),
|
||||
text = durationText,
|
||||
)
|
||||
if (item.type != BaseItemKind.AUDIO) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -25,6 +26,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.ui.components.GenreText
|
||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetailsText
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.listToDotString
|
||||
|
|
@ -64,16 +66,16 @@ fun DiscoverMovieDetailsHeader(
|
|||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
val padding = 4.dp
|
||||
val resources = LocalResources.current
|
||||
val details =
|
||||
remember(movie, rating) {
|
||||
remember(movie, rating, resources) {
|
||||
buildList {
|
||||
movie.releaseDate?.let(::add)
|
||||
movie.runtime
|
||||
?.toDouble()
|
||||
?.minutes
|
||||
?.roundMinutes
|
||||
?.toString()
|
||||
?.let(::add)
|
||||
?.let { add(resources.formatDuration(it)) }
|
||||
val release =
|
||||
movie.releases
|
||||
?.results
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.ui.components.OverviewText
|
|||
import com.github.damontecres.wholphin.ui.components.QuickDetailsText
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.listToDotString
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -446,8 +447,9 @@ fun DiscoverSeriesDetailsHeader(
|
|||
modifier = Modifier.fillMaxWidth(.60f),
|
||||
) {
|
||||
val padding = 4.dp
|
||||
val resources = LocalResources.current
|
||||
val details =
|
||||
remember(series, rating) {
|
||||
remember(series, rating, resources) {
|
||||
buildList {
|
||||
series.firstAirDate?.let(::add)
|
||||
series.episodeRunTime
|
||||
|
|
@ -455,8 +457,7 @@ fun DiscoverSeriesDetailsHeader(
|
|||
?.takeIf { !it.isNaN() && it > 0 }
|
||||
?.minutes
|
||||
?.roundMinutes
|
||||
?.toString()
|
||||
?.let(::add)
|
||||
?.let { add(resources.formatDuration(it)) }
|
||||
// TODO
|
||||
}.let {
|
||||
listToDotString(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.github.damontecres.wholphin.ui.AppColors
|
|||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.dot
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
|
|
@ -643,18 +644,16 @@ data class TvProgram(
|
|||
append(time)
|
||||
|
||||
if (!isFake) {
|
||||
val resources = WholphinApplication.instance.resources
|
||||
dot()
|
||||
duration
|
||||
.roundMinutes
|
||||
.toString()
|
||||
.let(::append)
|
||||
append(resources.formatDuration(duration.roundMinutes))
|
||||
if (now.isAfter(start) && now.isBefore(end)) {
|
||||
dot()
|
||||
java.time.Duration
|
||||
.between(now, end)
|
||||
.toKotlinDuration()
|
||||
.roundMinutes
|
||||
.let { append("$it left") }
|
||||
.let { append(resources.getString(R.string.time_left, resources.formatDuration(it))) }
|
||||
}
|
||||
seasonEpisode?.let { "S${it.season} E${it.episode}" }?.let {
|
||||
dot()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.DenseListItem
|
||||
|
|
@ -32,6 +33,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.Button
|
||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.roundSeconds
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
|
|
@ -115,7 +117,7 @@ fun SongListItem(
|
|||
}
|
||||
val trailingContent = @Composable {
|
||||
Text(
|
||||
text = runtime.toString(),
|
||||
text = runtime?.let { LocalResources.current.formatDuration(it) } ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ fun Controller(
|
|||
}
|
||||
}
|
||||
Text(
|
||||
text = "Ends $endTimeStr",
|
||||
text = stringResource(R.string.ends_at, endTimeStr),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -70,6 +71,7 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
|||
import com.github.damontecres.wholphin.ui.components.Button
|
||||
import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent
|
||||
import com.github.damontecres.wholphin.ui.components.TextButton
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
import com.github.damontecres.wholphin.ui.playback.PlaybackDialogType
|
||||
|
|
@ -274,9 +276,13 @@ fun SeekBar(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
val remaining = ((player.duration - position) / 1000).seconds
|
||||
val resources = LocalResources.current
|
||||
val positionSec = position / 1000
|
||||
val remainingSec = (player.duration - position) / 1000
|
||||
val positionText = remember(positionSec) { resources.formatDuration(positionSec.seconds) }
|
||||
val remainingText = remember(remainingSec) { "-${resources.formatDuration(remainingSec.seconds)}" }
|
||||
Text(
|
||||
text = (position / 1000).seconds.toString(),
|
||||
text = positionText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
|
|
@ -284,7 +290,7 @@ fun SeekBar(
|
|||
.padding(8.dp),
|
||||
)
|
||||
Text(
|
||||
text = "-$remaining",
|
||||
text = remainingText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -56,6 +57,7 @@ import com.github.damontecres.wholphin.ui.AppColors
|
|||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||
import com.github.damontecres.wholphin.ui.formatDuration
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.playback.AnalyticsState
|
||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||
|
|
@ -304,8 +306,10 @@ fun PlaybackOverlay(
|
|||
)
|
||||
}
|
||||
}
|
||||
val resources = LocalResources.current
|
||||
val seekText = remember(seekProgressMs / 1000L) { resources.formatDuration((seekProgressMs / 1000L).seconds) }
|
||||
Text(
|
||||
text = (seekProgressMs / 1000L).seconds.toString(),
|
||||
text = seekText,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
<string name="continue_watching">Continue watching</string>
|
||||
<string name="critic_rating">Critic Rating</string>
|
||||
<string name="decimal_seconds">%.2f seconds</string>
|
||||
<string name="duration_hours_minutes_seconds">%1$dh %2$dm %3$ds</string>
|
||||
<string name="duration_hours_minutes">%1$dh %2$dm</string>
|
||||
<string name="duration_minutes_seconds">%1$dm %2$ds</string>
|
||||
<string name="duration_hours">%1$dh</string>
|
||||
<string name="duration_minutes">%1$dm</string>
|
||||
<string name="duration_seconds">%1$ds</string>
|
||||
<string name="default_track">Default</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="died">Died</string>
|
||||
|
|
@ -144,6 +150,7 @@
|
|||
<string name="suggestions">Suggestions</string>
|
||||
<string name="switch_servers">Switch servers</string>
|
||||
<string name="switch_user">Switch</string>
|
||||
<string name="time_left">%1$s left</string>
|
||||
<string name="top_unwatched">Top Rated Unwatched</string>
|
||||
<string name="trailers_title">Trailers</string>
|
||||
<string name="tv_dvr_schedule">DVR Schedule</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue