Added localization

This commit is contained in:
OmniaX-Dev 2026-06-24 01:02:56 +02:00
parent 40ca9b7090
commit 9eb2bc7fda
8 changed files with 217 additions and 44 deletions

View file

@ -26,9 +26,11 @@ data class Recording(
) {
val hasMidi: Boolean get() = midiFileName != null
/** "22 Jun 2026" style for display. */
/** "22 Jun 2026" style for display, in the device's current locale. */
val formattedDate: String
get() = date.format(DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH))
get() = date.format(
DateTimeFormatter.ofPattern("d MMM yyyy", Locale.getDefault())
)
val groupingArtist: String
get() = if (artist.contains("soundtrack", ignoreCase = true)) "Soundtracks" else artist

View file

@ -36,9 +36,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.keylightpiano.pianotimeline.R
import com.keylightpiano.pianotimeline.domain.Recording
import com.keylightpiano.pianotimeline.player.PlaybackState
@ -154,7 +156,7 @@ fun MiniPlayer(
IconButton(onClick = onPrevious, modifier = Modifier.size(40.dp)) {
Icon(
painter = painterResource(android.R.drawable.ic_media_previous),
contentDescription = "Previous",
contentDescription = stringResource(R.string.cd_previous),
modifier = Modifier.size(22.dp)
)
}
@ -165,7 +167,7 @@ fun MiniPlayer(
if (playback.isPlaying) android.R.drawable.ic_media_pause
else android.R.drawable.ic_media_play
),
contentDescription = if (playback.isPlaying) "Pause" else "Play",
contentDescription = stringResource(if (playback.isPlaying) R.string.cd_pause else R.string.cd_play),
modifier = Modifier.size(24.dp)
)
}
@ -173,7 +175,7 @@ fun MiniPlayer(
IconButton(onClick = onNext, modifier = Modifier.size(40.dp)) {
Icon(
painter = painterResource(android.R.drawable.ic_media_next),
contentDescription = "Next",
contentDescription = stringResource(R.string.cd_next),
modifier = Modifier.size(22.dp)
)
}
@ -207,7 +209,7 @@ fun MiniPlayer(
IconButton(onClick = onDismiss, modifier = Modifier.size(40.dp)) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Stop",
contentDescription = stringResource(R.string.cd_stop),
modifier = Modifier.size(20.dp)
)
}

View file

@ -36,9 +36,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.keylightpiano.pianotimeline.R
import com.keylightpiano.pianotimeline.domain.Recording
private val NotesGreen = Color(0xFF25B300)
@ -88,7 +90,7 @@ fun RecordingItem(
if (isPlaying) android.R.drawable.ic_media_pause
else android.R.drawable.ic_media_play
),
contentDescription = if (isPlaying) "Pause" else "Play",
contentDescription = stringResource(if (isPlaying) R.string.cd_pause else R.string.cd_play),
modifier = Modifier.size(22.dp)
)
}
@ -148,7 +150,7 @@ fun RecordingItem(
Icon(
imageVector = if (recording.isFavorite) Icons.Filled.Star
else Icons.Outlined.StarBorder,
contentDescription = if (recording.isFavorite) "Unfavorite" else "Favorite",
contentDescription = stringResource(if (recording.isFavorite) R.string.cd_unfavorite else R.string.cd_favorite),
tint = if (recording.isFavorite) Color(0xFFFFC107)
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
@ -164,7 +166,7 @@ fun RecordingItem(
border = BorderStroke(1.5.dp, NotesGreen)
) {
Text(
"Notes",
stringResource(R.string.notes),
style = MaterialTheme.typography.labelSmall,
color = NotesGreen
)
@ -191,7 +193,7 @@ fun RecordingItem(
onDismissRequest = { menuExpanded = false }
) {
DropdownMenuItem(
text = { Text("Share audio") },
text = { Text(stringResource(R.string.menu_share_audio)) },
onClick = {
menuExpanded = false
onShareAudio()
@ -199,7 +201,7 @@ fun RecordingItem(
)
if (onShareVisualization != null) {
DropdownMenuItem(
text = { Text("Share visualization") },
text = { Text(stringResource(R.string.menu_share_visualization)) },
onClick = {
menuExpanded = false
onShareVisualization()
@ -207,7 +209,7 @@ fun RecordingItem(
)
}
DropdownMenuItem(
text = { Text("Copy link") },
text = { Text(stringResource(R.string.menu_copy_link)) },
onClick = {
menuExpanded = false
onCopyLink()

View file

@ -57,6 +57,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -172,7 +173,7 @@ fun RecordingListScreen(
.size(28.dp)
.padding(end = 8.dp)
)
Text("Piano Timeline", fontWeight = FontWeight.Bold)
Text(stringResource(R.string.title_app), fontWeight = FontWeight.Bold)
}
},
actions = {
@ -180,7 +181,7 @@ fun RecordingListScreen(
IconButton(onClick = { vm.toggleShuffle() }) {
Icon(
imageVector = Icons.Default.Shuffle,
contentDescription = "Shuffle",
contentDescription = stringResource(R.string.cd_shuffle),
tint = if (shuffle) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant
)
@ -190,7 +191,7 @@ fun RecordingListScreen(
Icon(
imageVector = if (repeatMode == RepeatMode.ONE)
Icons.Default.RepeatOne else Icons.Default.Repeat,
contentDescription = "Repeat",
contentDescription = stringResource(R.string.cd_repeat),
tint = if (repeatMode == RepeatMode.OFF)
MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.primary
@ -200,7 +201,7 @@ fun RecordingListScreen(
IconButton(onClick = { showControls = !showControls }) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search & Filter",
contentDescription = stringResource(R.string.cd_search_filter),
tint = if (hasActiveFilter) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant
)
@ -229,7 +230,7 @@ fun RecordingListScreen(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(Modifier.height(12.dp))
Text("Syncing...", style = MaterialTheme.typography.bodyMedium)
Text(stringResource(R.string.syncing), style = MaterialTheme.typography.bodyMedium)
}
}
}
@ -241,9 +242,9 @@ fun RecordingListScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(24.dp)
) {
Text("Couldn't load recordings", style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.error_load_title), style = MaterialTheme.typography.titleMedium)
Text(s.message, style = MaterialTheme.typography.bodySmall)
Button(onClick = { vm.retry() }) { Text("Retry") }
Button(onClick = { vm.retry() }) { Text(stringResource(R.string.retry)) }
}
}
}
@ -260,12 +261,12 @@ fun RecordingListScreen(
value = searchQuery,
onValueChange = { vm.setSearchQuery(it) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
placeholder = { Text("Search artist, title...") },
placeholder = { Text(stringResource(R.string.search_placeholder)) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { vm.setSearchQuery("") }) {
Icon(Icons.Default.Clear, contentDescription = "Clear")
Icon(Icons.Default.Clear, contentDescription = stringResource(R.string.cd_clear))
}
}
},
@ -280,14 +281,14 @@ fun RecordingListScreen(
) {
Box {
TextButton(onClick = { showSortMenu = true }) {
Text(sortMode.label, style = MaterialTheme.typography.labelMedium)
Text(stringResource(sortMode.labelRes), style = MaterialTheme.typography.labelMedium)
}
DropdownMenu(expanded = showSortMenu, onDismissRequest = { showSortMenu = false }) {
SortMode.entries.forEach { mode ->
DropdownMenuItem(
text = {
Text(
mode.label,
stringResource(mode.labelRes),
fontWeight = if (mode == sortMode) FontWeight.Bold else FontWeight.Normal
)
},
@ -302,16 +303,16 @@ fun RecordingListScreen(
TextButton(onClick = { showArtistFilter = !showArtistFilter }) {
Text(
if (hiddenArtists.isEmpty()) "Artists"
else "Artists (${hiddenArtists.size} hidden)",
if (hiddenArtists.isEmpty()) stringResource(R.string.filter_artists)
else stringResource(R.string.filter_artists_hidden, hiddenArtists.size),
style = MaterialTheme.typography.labelMedium
)
}
TextButton(onClick = { showYearFilter = !showYearFilter }) {
Text(
if (hiddenYears.isEmpty()) "Years"
else "Years (${hiddenYears.size} hidden)",
if (hiddenYears.isEmpty()) stringResource(R.string.filter_years)
else stringResource(R.string.filter_years_hidden, hiddenYears.size),
style = MaterialTheme.typography.labelMedium
)
}
@ -325,10 +326,10 @@ fun RecordingListScreen(
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { vm.showAllArtists() }) {
Text("All", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.filter_all), style = MaterialTheme.typography.labelSmall)
}
TextButton(onClick = { vm.hideAllArtists() }) {
Text("None", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.filter_none), style = MaterialTheme.typography.labelSmall)
}
}
FlowRow(
@ -354,10 +355,10 @@ fun RecordingListScreen(
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { vm.showAllYears() }) {
Text("All", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.filter_all), style = MaterialTheme.typography.labelSmall)
}
TextButton(onClick = { vm.hideAllYears() }) {
Text("None", style = MaterialTheme.typography.labelSmall)
Text(stringResource(R.string.filter_none), style = MaterialTheme.typography.labelSmall)
}
}
FlowRow(
@ -380,7 +381,7 @@ fun RecordingListScreen(
}
Text(
text = "${recordings.size} recordings",
text = stringResource(R.string.recordings_count, recordings.size),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
@ -450,6 +451,6 @@ private fun copyLink(context: android.content.Context, url: String) {
// On Android 13+ the system shows its own "copied" confirmation; on older
// versions a Toast is the norm.
if (android.os.Build.VERSION.SDK_INT < 33) {
android.widget.Toast.makeText(context, "Link copied", android.widget.Toast.LENGTH_SHORT).show()
android.widget.Toast.makeText(context, context.getString(R.string.toast_link_copied), android.widget.Toast.LENGTH_SHORT).show()
}
}

View file

@ -1,15 +1,19 @@
package com.keylightpiano.pianotimeline.ui
import androidx.annotation.StringRes
import com.keylightpiano.pianotimeline.R
/**
* Available sort modes for the recording list.
* Each knows how to produce a Comparator for Recording.
* Each carries a string-resource id for its label (resolved at display time via
* stringResource), so the labels localize automatically.
*/
enum class SortMode(val label: String) {
DATE_DESC("Newest first"),
DATE_ASC("Oldest first"),
ARTIST_AZ("Artist A → Z"),
ARTIST_ZA("Artist Z → A"),
TITLE_AZ("Title A → Z"),
DURATION_DESC("Longest first"),
DURATION_ASC("Shortest first");
enum class SortMode(@StringRes val labelRes: Int) {
DATE_DESC(R.string.sort_newest),
DATE_ASC(R.string.sort_oldest),
ARTIST_AZ(R.string.sort_artist_az),
ARTIST_ZA(R.string.sort_artist_za),
TITLE_AZ(R.string.sort_title_az),
DURATION_DESC(R.string.sort_longest),
DURATION_ASC(R.string.sort_shortest);
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- app_name intentionally NOT translated: keep the brand name "Piano Timeline" -->
<!-- Top bar -->
<string name="title_app">Piano Timeline</string>
<string name="cd_shuffle">Aleatorio</string>
<string name="cd_repeat">Repetir</string>
<string name="cd_search_filter">Buscar y filtrar</string>
<!-- Sync states -->
<string name="syncing">Sincronizando\u2026</string>
<string name="error_load_title">No se pudieron cargar las grabaciones</string>
<string name="retry">Reintentar</string>
<!-- Search & filters -->
<string name="search_placeholder">Buscar artista, t\u00EDtulo\u2026</string>
<string name="cd_clear">Borrar</string>
<string name="filter_artists">Artistas</string>
<string name="filter_artists_hidden">Artistas (%1$d ocultos)</string>
<string name="filter_years">A\u00F1os</string>
<string name="filter_years_hidden">A\u00F1os (%1$d ocultos)</string>
<string name="filter_all">Todos</string>
<string name="filter_none">Ninguno</string>
<!-- Recording count -->
<string name="recordings_count">%1$d grabaciones</string>
<!-- Card -->
<string name="cd_play">Reproducir</string>
<string name="cd_pause">Pausar</string>
<string name="cd_favorite">A\u00F1adir a favoritos</string>
<string name="cd_unfavorite">Quitar de favoritos</string>
<string name="notes">Notas</string>
<!-- Long-press menu -->
<string name="menu_share_audio">Compartir audio</string>
<string name="menu_share_visualization">Compartir visualizaci\u00F3n</string>
<string name="menu_copy_link">Copiar enlace</string>
<string name="toast_link_copied">Enlace copiado</string>
<!-- Mini-player -->
<string name="cd_previous">Anterior</string>
<string name="cd_next">Siguiente</string>
<string name="cd_stop">Detener</string>
<!-- Sort modes -->
<string name="sort_newest">M\u00E1s recientes</string>
<string name="sort_oldest">M\u00E1s antiguas</string>
<string name="sort_artist_az">Artista A \u2192 Z</string>
<string name="sort_artist_za">Artista Z \u2192 A</string>
<string name="sort_title_az">T\u00EDtulo A \u2192 Z</string>
<string name="sort_longest">M\u00E1s largas</string>
<string name="sort_shortest">M\u00E1s cortas</string>
</resources>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- app_name intentionally NOT translated: keep the brand name "Piano Timeline" -->
<!-- Top bar -->
<string name="title_app">Piano Timeline</string>
<string name="cd_shuffle">Casuale</string>
<string name="cd_repeat">Ripeti</string>
<string name="cd_search_filter">Cerca e filtra</string>
<!-- Sync states -->
<string name="syncing">Sincronizzazione\u2026</string>
<string name="error_load_title">Impossibile caricare le registrazioni</string>
<string name="retry">Riprova</string>
<!-- Search & filters -->
<string name="search_placeholder">Cerca artista, titolo\u2026</string>
<string name="cd_clear">Cancella</string>
<string name="filter_artists">Artisti</string>
<string name="filter_artists_hidden">Artisti (%1$d nascosti)</string>
<string name="filter_years">Anni</string>
<string name="filter_years_hidden">Anni (%1$d nascosti)</string>
<string name="filter_all">Tutti</string>
<string name="filter_none">Nessuno</string>
<!-- Recording count -->
<string name="recordings_count">%1$d registrazioni</string>
<!-- Card -->
<string name="cd_play">Riproduci</string>
<string name="cd_pause">Pausa</string>
<string name="cd_favorite">Aggiungi ai preferiti</string>
<string name="cd_unfavorite">Rimuovi dai preferiti</string>
<string name="notes">Note</string>
<!-- Long-press menu -->
<string name="menu_share_audio">Condividi audio</string>
<string name="menu_share_visualization">Condividi visualizzazione</string>
<string name="menu_copy_link">Copia link</string>
<string name="toast_link_copied">Link copiato</string>
<!-- Mini-player -->
<string name="cd_previous">Precedente</string>
<string name="cd_next">Successivo</string>
<string name="cd_stop">Ferma</string>
<!-- Sort modes -->
<string name="sort_newest">Pi\u00F9 recenti</string>
<string name="sort_oldest">Meno recenti</string>
<string name="sort_artist_az">Artista A \u2192 Z</string>
<string name="sort_artist_za">Artista Z \u2192 A</string>
<string name="sort_title_az">Titolo A \u2192 Z</string>
<string name="sort_longest">Pi\u00F9 lunghi</string>
<string name="sort_shortest">Pi\u00F9 brevi</string>
</resources>

View file

@ -1,3 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">PianoTimeline</string>
<string name="app_name">Piano Timeline</string>
<!-- Top bar -->
<string name="title_app">Piano Timeline</string>
<string name="cd_shuffle">Shuffle</string>
<string name="cd_repeat">Repeat</string>
<string name="cd_search_filter">Search &amp; Filter</string>
<!-- Sync states -->
<string name="syncing">Syncing\u2026</string>
<string name="error_load_title">Couldn\'t load recordings</string>
<string name="retry">Retry</string>
<!-- Search & filters -->
<string name="search_placeholder">Search artist, title\u2026</string>
<string name="cd_clear">Clear</string>
<string name="filter_artists">Artists</string>
<string name="filter_artists_hidden">Artists (%1$d hidden)</string>
<string name="filter_years">Years</string>
<string name="filter_years_hidden">Years (%1$d hidden)</string>
<string name="filter_all">All</string>
<string name="filter_none">None</string>
<!-- Recording count -->
<string name="recordings_count">%1$d recordings</string>
<!-- Card -->
<string name="cd_play">Play</string>
<string name="cd_pause">Pause</string>
<string name="cd_favorite">Favorite</string>
<string name="cd_unfavorite">Unfavorite</string>
<string name="notes">Notes</string>
<!-- Long-press menu -->
<string name="menu_share_audio">Share audio</string>
<string name="menu_share_visualization">Share visualization</string>
<string name="menu_copy_link">Copy link</string>
<string name="toast_link_copied">Link copied</string>
<!-- Mini-player -->
<string name="cd_previous">Previous</string>
<string name="cd_next">Next</string>
<string name="cd_stop">Stop</string>
<!-- Sort modes -->
<string name="sort_newest">Newest first</string>
<string name="sort_oldest">Oldest first</string>
<string name="sort_artist_az">Artist A \u2192 Z</string>
<string name="sort_artist_za">Artist Z \u2192 A</string>
<string name="sort_title_az">Title A \u2192 Z</string>
<string name="sort_longest">Longest first</string>
<string name="sort_shortest">Shortest first</string>
</resources>