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 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 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 val groupingArtist: String
get() = if (artist.contains("soundtrack", ignoreCase = true)) "Soundtracks" else artist 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.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.keylightpiano.pianotimeline.R
import com.keylightpiano.pianotimeline.domain.Recording import com.keylightpiano.pianotimeline.domain.Recording
import com.keylightpiano.pianotimeline.player.PlaybackState import com.keylightpiano.pianotimeline.player.PlaybackState
@ -154,7 +156,7 @@ fun MiniPlayer(
IconButton(onClick = onPrevious, modifier = Modifier.size(40.dp)) { IconButton(onClick = onPrevious, modifier = Modifier.size(40.dp)) {
Icon( Icon(
painter = painterResource(android.R.drawable.ic_media_previous), painter = painterResource(android.R.drawable.ic_media_previous),
contentDescription = "Previous", contentDescription = stringResource(R.string.cd_previous),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -165,7 +167,7 @@ fun MiniPlayer(
if (playback.isPlaying) android.R.drawable.ic_media_pause if (playback.isPlaying) android.R.drawable.ic_media_pause
else android.R.drawable.ic_media_play 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) modifier = Modifier.size(24.dp)
) )
} }
@ -173,7 +175,7 @@ fun MiniPlayer(
IconButton(onClick = onNext, modifier = Modifier.size(40.dp)) { IconButton(onClick = onNext, modifier = Modifier.size(40.dp)) {
Icon( Icon(
painter = painterResource(android.R.drawable.ic_media_next), painter = painterResource(android.R.drawable.ic_media_next),
contentDescription = "Next", contentDescription = stringResource(R.string.cd_next),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -207,7 +209,7 @@ fun MiniPlayer(
IconButton(onClick = onDismiss, modifier = Modifier.size(40.dp)) { IconButton(onClick = onDismiss, modifier = Modifier.size(40.dp)) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Stop", contentDescription = stringResource(R.string.cd_stop),
modifier = Modifier.size(20.dp) 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.keylightpiano.pianotimeline.R
import com.keylightpiano.pianotimeline.domain.Recording import com.keylightpiano.pianotimeline.domain.Recording
private val NotesGreen = Color(0xFF25B300) private val NotesGreen = Color(0xFF25B300)
@ -88,7 +90,7 @@ fun RecordingItem(
if (isPlaying) android.R.drawable.ic_media_pause if (isPlaying) android.R.drawable.ic_media_pause
else android.R.drawable.ic_media_play 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) modifier = Modifier.size(22.dp)
) )
} }
@ -148,7 +150,7 @@ fun RecordingItem(
Icon( Icon(
imageVector = if (recording.isFavorite) Icons.Filled.Star imageVector = if (recording.isFavorite) Icons.Filled.Star
else Icons.Outlined.StarBorder, 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) tint = if (recording.isFavorite) Color(0xFFFFC107)
else MaterialTheme.colorScheme.onSurfaceVariant, else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
@ -164,7 +166,7 @@ fun RecordingItem(
border = BorderStroke(1.5.dp, NotesGreen) border = BorderStroke(1.5.dp, NotesGreen)
) { ) {
Text( Text(
"Notes", stringResource(R.string.notes),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = NotesGreen color = NotesGreen
) )
@ -191,7 +193,7 @@ fun RecordingItem(
onDismissRequest = { menuExpanded = false } onDismissRequest = { menuExpanded = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Share audio") }, text = { Text(stringResource(R.string.menu_share_audio)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
onShareAudio() onShareAudio()
@ -199,7 +201,7 @@ fun RecordingItem(
) )
if (onShareVisualization != null) { if (onShareVisualization != null) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Share visualization") }, text = { Text(stringResource(R.string.menu_share_visualization)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
onShareVisualization() onShareVisualization()
@ -207,7 +209,7 @@ fun RecordingItem(
) )
} }
DropdownMenuItem( DropdownMenuItem(
text = { Text("Copy link") }, text = { Text(stringResource(R.string.menu_copy_link)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
onCopyLink() onCopyLink()

View file

@ -57,6 +57,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -172,7 +173,7 @@ fun RecordingListScreen(
.size(28.dp) .size(28.dp)
.padding(end = 8.dp) .padding(end = 8.dp)
) )
Text("Piano Timeline", fontWeight = FontWeight.Bold) Text(stringResource(R.string.title_app), fontWeight = FontWeight.Bold)
} }
}, },
actions = { actions = {
@ -180,7 +181,7 @@ fun RecordingListScreen(
IconButton(onClick = { vm.toggleShuffle() }) { IconButton(onClick = { vm.toggleShuffle() }) {
Icon( Icon(
imageVector = Icons.Default.Shuffle, imageVector = Icons.Default.Shuffle,
contentDescription = "Shuffle", contentDescription = stringResource(R.string.cd_shuffle),
tint = if (shuffle) MaterialTheme.colorScheme.primary tint = if (shuffle) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -190,7 +191,7 @@ fun RecordingListScreen(
Icon( Icon(
imageVector = if (repeatMode == RepeatMode.ONE) imageVector = if (repeatMode == RepeatMode.ONE)
Icons.Default.RepeatOne else Icons.Default.Repeat, Icons.Default.RepeatOne else Icons.Default.Repeat,
contentDescription = "Repeat", contentDescription = stringResource(R.string.cd_repeat),
tint = if (repeatMode == RepeatMode.OFF) tint = if (repeatMode == RepeatMode.OFF)
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
@ -200,7 +201,7 @@ fun RecordingListScreen(
IconButton(onClick = { showControls = !showControls }) { IconButton(onClick = { showControls = !showControls }) {
Icon( Icon(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
contentDescription = "Search & Filter", contentDescription = stringResource(R.string.cd_search_filter),
tint = if (hasActiveFilter) MaterialTheme.colorScheme.primary tint = if (hasActiveFilter) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -229,7 +230,7 @@ fun RecordingListScreen(
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator() CircularProgressIndicator()
Spacer(Modifier.height(12.dp)) 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), verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(24.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) 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, value = searchQuery,
onValueChange = { vm.setSearchQuery(it) }, onValueChange = { vm.setSearchQuery(it) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp), 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) }, leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = { trailingIcon = {
if (searchQuery.isNotEmpty()) { if (searchQuery.isNotEmpty()) {
IconButton(onClick = { vm.setSearchQuery("") }) { 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 { Box {
TextButton(onClick = { showSortMenu = true }) { 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 }) { DropdownMenu(expanded = showSortMenu, onDismissRequest = { showSortMenu = false }) {
SortMode.entries.forEach { mode -> SortMode.entries.forEach { mode ->
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text( Text(
mode.label, stringResource(mode.labelRes),
fontWeight = if (mode == sortMode) FontWeight.Bold else FontWeight.Normal fontWeight = if (mode == sortMode) FontWeight.Bold else FontWeight.Normal
) )
}, },
@ -302,16 +303,16 @@ fun RecordingListScreen(
TextButton(onClick = { showArtistFilter = !showArtistFilter }) { TextButton(onClick = { showArtistFilter = !showArtistFilter }) {
Text( Text(
if (hiddenArtists.isEmpty()) "Artists" if (hiddenArtists.isEmpty()) stringResource(R.string.filter_artists)
else "Artists (${hiddenArtists.size} hidden)", else stringResource(R.string.filter_artists_hidden, hiddenArtists.size),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium
) )
} }
TextButton(onClick = { showYearFilter = !showYearFilter }) { TextButton(onClick = { showYearFilter = !showYearFilter }) {
Text( Text(
if (hiddenYears.isEmpty()) "Years" if (hiddenYears.isEmpty()) stringResource(R.string.filter_years)
else "Years (${hiddenYears.size} hidden)", else stringResource(R.string.filter_years_hidden, hiddenYears.size),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium
) )
} }
@ -325,10 +326,10 @@ fun RecordingListScreen(
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { vm.showAllArtists() }) { TextButton(onClick = { vm.showAllArtists() }) {
Text("All", style = MaterialTheme.typography.labelSmall) Text(stringResource(R.string.filter_all), style = MaterialTheme.typography.labelSmall)
} }
TextButton(onClick = { vm.hideAllArtists() }) { TextButton(onClick = { vm.hideAllArtists() }) {
Text("None", style = MaterialTheme.typography.labelSmall) Text(stringResource(R.string.filter_none), style = MaterialTheme.typography.labelSmall)
} }
} }
FlowRow( FlowRow(
@ -354,10 +355,10 @@ fun RecordingListScreen(
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { vm.showAllYears() }) { TextButton(onClick = { vm.showAllYears() }) {
Text("All", style = MaterialTheme.typography.labelSmall) Text(stringResource(R.string.filter_all), style = MaterialTheme.typography.labelSmall)
} }
TextButton(onClick = { vm.hideAllYears() }) { TextButton(onClick = { vm.hideAllYears() }) {
Text("None", style = MaterialTheme.typography.labelSmall) Text(stringResource(R.string.filter_none), style = MaterialTheme.typography.labelSmall)
} }
} }
FlowRow( FlowRow(
@ -380,7 +381,7 @@ fun RecordingListScreen(
} }
Text( Text(
text = "${recordings.size} recordings", text = stringResource(R.string.recordings_count, recordings.size),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) 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 // On Android 13+ the system shows its own "copied" confirmation; on older
// versions a Toast is the norm. // versions a Toast is the norm.
if (android.os.Build.VERSION.SDK_INT < 33) { 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 package com.keylightpiano.pianotimeline.ui
import androidx.annotation.StringRes
import com.keylightpiano.pianotimeline.R
/** /**
* Available sort modes for the recording list. * 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) { enum class SortMode(@StringRes val labelRes: Int) {
DATE_DESC("Newest first"), DATE_DESC(R.string.sort_newest),
DATE_ASC("Oldest first"), DATE_ASC(R.string.sort_oldest),
ARTIST_AZ("Artist A → Z"), ARTIST_AZ(R.string.sort_artist_az),
ARTIST_ZA("Artist Z → A"), ARTIST_ZA(R.string.sort_artist_za),
TITLE_AZ("Title A → Z"), TITLE_AZ(R.string.sort_title_az),
DURATION_DESC("Longest first"), DURATION_DESC(R.string.sort_longest),
DURATION_ASC("Shortest first"); 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> <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> </resources>