diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f532a..64d4cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ ## v1.9.3 (2024-12-17) * 💥 Updated to Jetpack Compose version 1.7.5 ([BOM](https://developer.android.com/jetpack/compose/bom) 2024.11.00). +* 🚀 Added localization support. + * See `res/values/strings.xml` for the full list of translatable strings, which you can override in your app's `strings.xml`. + * For more information, see [Localize your app on Android Developers](https://developer.android.com/guide/topics/resources/localization). * 🐛 Fix `SeekBar` not working for livestreams with a large `player.seekable.start(0)`, such as MPEG-DASH streams that use Unix timestamps for their MPD timeline. ([#52](https://github.com/THEOplayer/android-ui/pull/52)) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..e3a7129 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,42 @@ + + + THEOplayer Open Video UI voor Android Demo + Afspelen + Pauzeren + Opnieuw spelen + Starten met casten + Stop casting + Volledig scherm + Volledig scherm sluiten + Sluiten + Terug + Dempen + Dempen opheffen + + Spring %1$d seconde vooruit + Spring %1$d seconden vooruit + + + Spring %1$d seconde achteruit + Spring %1$d seconden achteruit + + LIVE + Speelt op %1$s + Speelt op + Speelt op Chromecast + Speelt op + Chromecast + Taal + Instellingen + Audio + Ondertitels + Afspeelsnelheid + Kwaliteit + Geen + Uit + Normaal + Automatisch + Automatisch (%1$dp) + Onbekend + Fout + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d1a3a8..611cf98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,53 @@ - + THEOplayer Open Video UI for Android Demo + + + Play + Pause + Replay + Start casting + Stop casting + Enter fullscreen + Exit fullscreen + Close + Back + Mute + Unmute + + Seek forward by %1$d second + Seek forward by %1$d seconds + + + Seek backward by %1$d second + Seek backward by %1$d seconds + + LIVE + %1$s + %1$s + %1$s / %2$s + %1$s / %2$s + Playing on %1$s + Playing on + %1$s + Playing on Chromecast + Playing on + Chromecast + Language + Settings + Audio + Subtitles + Playback speed + Quality + None + Off + Normal + #.##x + Automatic + %1$dp + Automatic (%1$dp) + #Mbps + #.#Mbps + #kbps + Unknown + An error occurred \ No newline at end of file diff --git a/docs/guides/localization.md b/docs/guides/localization.md new file mode 100644 index 0000000..b5c25d7 --- /dev/null +++ b/docs/guides/localization.md @@ -0,0 +1,81 @@ +--- +sidebar_position: 3 +--- + +# Localization + +The Open Video UI for Android can be localized to different languages, +enabling you to reach audiences from different regions of the world. + +Localization works +by [providing alternative resources](https://developer.android.com/guide/topics/resources/providing-resources#AlternativeResources) +in one or more languages. You can choose to [change the text of the default language](#change-default-language) +to target a single audience, or [add alternative languages](#add-alternative-languages) to target many audiences. + +## Changing the default language {#change-default-language} + +By default, the Open Video UI ships with English texts only. If your app targets a non-English speaking audience, +you can override these texts with translations for another language. + +1. Copy the English string resources + from [`res/values/strings.xml`](https://github.com/THEOplayer/android-ui/blob/main/ui/src/main/res/values/strings.xml) + to your app's resources for the default locale (i.e. `res/values/strings.xml`). +2. Change the resource values to the translated texts. + For example, to change the title of the "Language" menu, you would change the contents + of the `theoplayer_ui_menu_language` resource: + + ```xml title="res/values/strings.xml" + + Langue + + ``` + + :::tip + You can also use + the [Translations Editor in Android Studio](https://developer.android.com/studio/write/translations-editor) + to edit these values. + ::: + +3. Build and run your app. The translated texts should now appear in your player UI. + +## Add alternative languages {#add-alternative-languages} + +If your app targets many audiences speaking different languages, you can add multiple translations using +locale-specific resources. + +1. Copy the English string resources + from [`res/values/strings.xml`](https://github.com/THEOplayer/android-ui/blob/main/ui/src/main/res/values/strings.xml) + to your app's resources for the default locale (i.e. `res/values/strings.xml`). +2. Add a new string resources file for a new locale. + For example, for French, create the file: `res/values-fr/strings.xml`. +3. Add translated versions for each English string resource to the new resources file. + For example, to translate the title of the "Language" menu, you would add an entry for `theoplayer_ui_menu_language`: + ```xml title="res/values-fr/strings.xml" + + Langue + + ``` + +## Remarks + +### Update translations when upgrading Open Video UI + +Newer versions of the Open Video UI for Android may add new string resources that need to be translated. + +When using custom translations in your app, we recommend pinning the `com.theoplayer.android-ui:android-ui` dependency +in your app's `build.gradle` to a specific version. Avoid using `+` in the dependency's version range. + +```groovy title="build.gradle" +dependencies { + implementation "com.theoplayer.android-ui:android-ui:1.9.0" +} +``` + +When you decide to upgrade Open Video UI to the latest version, make sure to also update your translations. +Check the history for [`res/values/strings.xml`](https://github.com/THEOplayer/android-ui/commits/main/ui/src/main/res/values/strings.xml) +to see whether any string resources were added or changed since the previous version. + +## More information + +- [Localize your app on Android Developers](https://developer.android.com/guide/topics/resources/localization) +- [Support different languages and cultures on Android Developers](https://developer.android.com/training/basics/supporting-devices/languages) diff --git a/ui/src/main/java/com/theoplayer/android/ui/ChromecastButton.kt b/ui/src/main/java/com/theoplayer/android/ui/ChromecastButton.kt index 935999d..6e317b3 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/ChromecastButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/ChromecastButton.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.rounded.CastConnected import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.theoplayer.android.api.cast.chromecast.PlayerCastState @@ -31,19 +32,19 @@ fun ChromecastButton( availableIcon: @Composable () -> Unit = { Icon( Icons.Rounded.Cast, - contentDescription = "Start casting" + contentDescription = stringResource(R.string.theoplayer_ui_btn_chromecast_start) ) }, connectingIcon: @Composable () -> Unit = { Icon( Icons.Rounded.Cast, - contentDescription = "Stop casting" + contentDescription = stringResource(R.string.theoplayer_ui_btn_chromecast_stop) ) }, connectedIcon: @Composable () -> Unit = { Icon( Icons.Rounded.CastConnected, - contentDescription = "Stop casting" + contentDescription = stringResource(R.string.theoplayer_ui_btn_chromecast_stop) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/ChromecastDisplay.kt b/ui/src/main/java/com/theoplayer/android/ui/ChromecastDisplay.kt index 526db64..df7817f 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/ChromecastDisplay.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/ChromecastDisplay.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.theoplayer.android.api.cast.chromecast.PlayerCastState @@ -68,7 +69,13 @@ fun ChromecastDisplayCompact( ) Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Text( - text = "Playing on ${player.castReceiverName ?: "Chromecast"}" + text = player.castReceiverName?.let { + stringResource( + R.string.theoplayer_ui_chromecast_playing_on_receiver, + it + ) + } + ?: stringResource(R.string.theoplayer_ui_chromecast_playing_on_unknown_receiver) ) } } @@ -101,11 +108,23 @@ fun ChromecastDisplayExpanded( } Column { Text( - text = "Playing on", + text = player.castReceiverName?.let { + stringResource( + R.string.theoplayer_ui_chromecast_playing_on_receiver_expanded_first_line, + it + ) + } + ?: stringResource(R.string.theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_first_line), style = MaterialTheme.typography.bodyLarge ) Text( - text = player.castReceiverName ?: "Chromecast", + text = player.castReceiverName?.let { + stringResource( + R.string.theoplayer_ui_chromecast_playing_on_receiver_expanded_second_line, + it + ) + } + ?: stringResource(R.string.theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_second_line), style = MaterialTheme.typography.headlineSmall ) } diff --git a/ui/src/main/java/com/theoplayer/android/ui/CurrentTimeDisplay.kt b/ui/src/main/java/com/theoplayer/android/ui/CurrentTimeDisplay.kt index 0b83c76..c5187de 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/CurrentTimeDisplay.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/CurrentTimeDisplay.kt @@ -3,6 +3,7 @@ package com.theoplayer.android.ui import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource /** * A text display that shows the player's current time. @@ -29,12 +30,26 @@ fun CurrentTimeDisplay( currentTime } - val text = StringBuilder() - text.append(formatTime(time, duration)) - if (showDuration) { - text.append(" / ") - text.append(formatTime(duration)) + val text = if (showDuration) { + stringResource( + if (showRemaining) { + R.string.theoplayer_ui_current_time_remaining_with_duration + } else { + R.string.theoplayer_ui_current_time_with_duration + }, + formatTime(time, duration, preferNegative = showRemaining), + formatTime(duration) + ) + } else { + stringResource( + if (showRemaining) { + R.string.theoplayer_ui_current_time_remaining + } else { + R.string.theoplayer_ui_current_time + }, + formatTime(time, duration, preferNegative = showRemaining), + ) } - Text(modifier = modifier, text = text.toString()) + Text(modifier = modifier, text = text) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt b/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt index f433613..09b973f 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt @@ -90,6 +90,7 @@ fun DefaultUI( } Spacer(modifier = Modifier.weight(1f)) LanguageMenuButton() + SettingsMenuButton() ChromecastButton() } } diff --git a/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt b/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt index edcedd4..b44a93a 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -43,7 +44,7 @@ fun ErrorDisplay( contentAlignment = Alignment.CenterStart ) { Text( - text = "An error occurred", + text = stringResource(R.string.theoplayer_ui_error_title), style = MaterialTheme.typography.headlineMedium ) } diff --git a/ui/src/main/java/com/theoplayer/android/ui/FullscreenButton.kt b/ui/src/main/java/com/theoplayer/android/ui/FullscreenButton.kt index 5863fdd..16b63c8 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/FullscreenButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/FullscreenButton.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.rounded.FullscreenExit import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -25,13 +26,13 @@ fun FullscreenButton( enter: @Composable () -> Unit = { Icon( Icons.Rounded.Fullscreen, - contentDescription = "Enter fullscreen" + contentDescription = stringResource(R.string.theoplayer_ui_btn_fullscreen_enter) ) }, exit: @Composable () -> Unit = { Icon( Icons.Rounded.FullscreenExit, - contentDescription = "Exit fullscreen" + contentDescription = stringResource(R.string.theoplayer_ui_btn_fullscreen_exit) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 7605c07..cef88a0 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -1,5 +1,7 @@ package com.theoplayer.android.ui +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import com.theoplayer.android.api.player.track.Track import java.util.Locale import kotlin.math.absoluteValue @@ -16,7 +18,7 @@ import kotlin.math.absoluteValue * for example because it represents the time remaining in the video. */ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = false): String { - val isNegative = time < 0 || (preferNegative && time == 0.0); + val isNegative = time < 0 || (preferNegative && time == 0.0) val absoluteTime = time.absoluteValue.toInt() val guideMinutes = ((guide / 60) % 60).toInt() @@ -60,6 +62,7 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals * * @param track the media track or text track */ +@Composable fun formatTrackLabel(track: Track): String { val label = track.label if (!label.isNullOrEmpty()) { @@ -74,5 +77,5 @@ fun formatTrackLabel(track: Track): String { } return languageCode } - return "" + return stringResource(R.string.theoplayer_ui_track_unknown) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt index 911aa18..c3824d4 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -26,11 +27,11 @@ import androidx.compose.ui.unit.dp @Composable fun MenuScope.LanguageMenu() { Menu( - title = { Text(text = "Language") }, + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_language)) }, backIcon = { Icon( Icons.Rounded.Close, - contentDescription = "Close" + contentDescription = stringResource(R.string.theoplayer_ui_btn_menu_close) ) }, ) { @@ -75,7 +76,7 @@ fun MenuScope.LanguageMenuCompact() { modifier = Modifier .weight(1f) .alignByBaseline(), - text = "Audio" + text = stringResource(R.string.theoplayer_ui_menu_audio) ) TextButton( modifier = Modifier @@ -85,7 +86,10 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeAudioTrack?.let { formatTrackLabel(it) } ?: "None", + text = player?.activeAudioTrack?.let { formatTrackLabel(it) } + ?: stringResource( + R.string.theoplayer_ui_audio_none + ), textAlign = TextAlign.Center ) Icon( @@ -101,7 +105,7 @@ fun MenuScope.LanguageMenuCompact() { modifier = Modifier .weight(1f) .alignByBaseline(), - text = "Subtitles" + text = stringResource(R.string.theoplayer_ui_menu_subtitles) ) TextButton( modifier = Modifier @@ -111,7 +115,9 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeSubtitleTrack?.let { formatTrackLabel(it) } ?: "Off", + text = player?.activeSubtitleTrack?.let { formatTrackLabel(it) } ?: stringResource( + R.string.theoplayer_ui_subtitles_off + ), textAlign = TextAlign.Center ) Icon( @@ -142,7 +148,7 @@ fun MenuScope.LanguageMenuExpanded() { modifier = Modifier .fillMaxWidth(1f) .padding(0.dp, 8.dp), - text = "Audio", + text = stringResource(R.string.theoplayer_ui_menu_audio), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium ) @@ -155,7 +161,7 @@ fun MenuScope.LanguageMenuExpanded() { modifier = Modifier .fillMaxWidth(1f) .padding(0.dp, 8.dp), - text = "Subtitles", + text = stringResource(R.string.theoplayer_ui_menu_subtitles), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium ) @@ -173,7 +179,7 @@ fun MenuScope.LanguageMenuExpanded() { @Composable fun MenuScope.AudioTrackMenu() { Menu( - title = { Text(text = "Audio") } + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_audio)) } ) { AudioTrackList(onClick = { closeCurrentMenu() }) } @@ -187,7 +193,7 @@ fun MenuScope.AudioTrackMenu() { @Composable fun MenuScope.SubtitleMenu() { Menu( - title = { Text(text = "Subtitles") } + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_subtitles)) } ) { SubtitleTrackList(onClick = { closeCurrentMenu() }) } diff --git a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenuButton.kt b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenuButton.kt index 99f5836..d736449 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenuButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenuButton.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -21,7 +22,7 @@ fun MenuScope.LanguageMenuButton( content: @Composable () -> Unit = { Icon( painter = painterResource(id = R.drawable.language), - contentDescription = "Language" + contentDescription = stringResource(R.string.theoplayer_ui_menu_language) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt b/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt index ca1d1c3..ad0ad9f 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt @@ -2,7 +2,9 @@ package com.theoplayer.android.ui import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Circle import androidx.compose.material3.ButtonColors @@ -13,7 +15,9 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.theoplayer.android.ui.theme.THEOplayerTheme @@ -42,26 +46,45 @@ fun LiveButton( live: @Composable RowScope.() -> Unit = { Icon( Icons.Rounded.Circle, - modifier = Modifier.size(12.dp), + modifier = Modifier + .size(12.dp) + .align(Alignment.CenterVertically), tint = THEOplayerTheme.playerColors.liveButtonLive, contentDescription = null ) - Text(text = " LIVE") + Spacer( + modifier = Modifier + .width(4.dp) + ) + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(R.string.theoplayer_ui_btn_live) + ) }, dvr: @Composable RowScope. () -> Unit = { Icon( Icons.Rounded.Circle, - modifier = Modifier.size(12.dp), + modifier = Modifier + .size(12.dp) + .align(Alignment.CenterVertically), tint = THEOplayerTheme.playerColors.liveButtonDvr, contentDescription = null ) - Text(text = " LIVE") + Spacer( + modifier = Modifier + .width(4.dp) + ) + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(R.string.theoplayer_ui_btn_live) + ) } ) { val player = Player.current if (player?.streamType == StreamType.Live || player?.streamType == StreamType.Dvr) { val isLive = - !player.paused && ((player.seekable.lastEnd ?: 0.0) - player.currentTime) <= liveThreshold + !player.paused && ((player.seekable.lastEnd + ?: 0.0) - player.currentTime) <= liveThreshold TextButton( modifier = modifier, contentPadding = contentPadding, diff --git a/ui/src/main/java/com/theoplayer/android/ui/Menu.kt b/ui/src/main/java/com/theoplayer/android/ui/Menu.kt index efbe422..49b6f1d 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Menu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Menu.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource /** * The content for a new menu, typically this should draw a single [Menu]. @@ -38,7 +39,7 @@ fun MenuScope.Menu( backIcon: @Composable () -> Unit = { Icon( Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.theoplayer_ui_btn_back) ) }, content: @Composable () -> Unit, diff --git a/ui/src/main/java/com/theoplayer/android/ui/MuteButton.kt b/ui/src/main/java/com/theoplayer/android/ui/MuteButton.kt index aad606a..c892e07 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/MuteButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/MuteButton.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -25,13 +26,13 @@ fun MuteButton( mute: @Composable () -> Unit = { Icon( Icons.AutoMirrored.Rounded.VolumeUp, - contentDescription = "Mute" + contentDescription = stringResource(R.string.theoplayer_ui_btn_mute) ) }, unmute: @Composable () -> Unit = { Icon( Icons.AutoMirrored.Rounded.VolumeOff, - contentDescription = "Unmute" + contentDescription = stringResource(R.string.theoplayer_ui_btn_unmute) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/PlayButton.kt b/ui/src/main/java/com/theoplayer/android/ui/PlayButton.kt index a0d6479..9e448fd 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/PlayButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/PlayButton.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -29,21 +30,21 @@ fun PlayButton( Icon( painter = painterResource(id = R.drawable.play), modifier = iconModifier, - contentDescription = "Play" + contentDescription = stringResource(id = R.string.theoplayer_ui_btn_play) ) }, pause: @Composable () -> Unit = { Icon( painter = painterResource(id = R.drawable.pause), modifier = iconModifier, - contentDescription = "Pause" + contentDescription = stringResource(id = R.string.theoplayer_ui_btn_pause) ) }, replay: @Composable () -> Unit = { Icon( Icons.Rounded.Replay, modifier = iconModifier, - contentDescription = "Replay" + contentDescription = stringResource(id = R.string.theoplayer_ui_btn_replay) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/PlaybackRateList.kt b/ui/src/main/java/com/theoplayer/android/ui/PlaybackRateList.kt index ba98a15..81adba7 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/PlaybackRateList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/PlaybackRateList.kt @@ -6,7 +6,9 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import java.text.DecimalFormat /** @@ -47,12 +49,13 @@ fun PlaybackRateList( } } -private val playbackRateFormat = DecimalFormat("#.##") - +@Composable internal fun formatPlaybackRate(rate: Double): String { return if (rate == 1.0) { - "Normal" + stringResource(R.string.theoplayer_ui_playback_rate_normal, rate) } else { - "${playbackRateFormat.format(rate)}x" + val format = stringResource(R.string.theoplayer_ui_playback_rate_format) + val decimalFormat = remember(format) { DecimalFormat(format) } + decimalFormat.format(rate) } } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/QualityList.kt b/ui/src/main/java/com/theoplayer/android/ui/QualityList.kt index 01a937d..29cc90e 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/QualityList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/QualityList.kt @@ -6,7 +6,9 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import java.text.DecimalFormat /** @@ -26,7 +28,7 @@ fun QualityList( LazyColumn(modifier = modifier) { item(key = null) { ListItem( - headlineContent = { Text(text = "Automatic") }, + headlineContent = { Text(text = stringResource(R.string.theoplayer_ui_quality_automatic)) }, leadingContent = { RadioButton( selected = (targetVideoQuality == null), @@ -45,7 +47,14 @@ fun QualityList( ) { val quality = videoQualities[it] ListItem( - headlineContent = { Text(text = "${quality.height}p") }, + headlineContent = { + Text( + text = stringResource( + R.string.theoplayer_ui_quality_with_height, + quality.height + ) + ) + }, supportingContent = { Text(text = formatBandwidth(quality.bandwidth)) }, leadingContent = { RadioButton( @@ -62,15 +71,17 @@ fun QualityList( } } -private val zeroPrecisionFormat = DecimalFormat("#") -private val singlePrecisionFormat = DecimalFormat("#.#") - +@Composable internal fun formatBandwidth(bandwidth: Long): String { - return if (bandwidth > 1e7) { - "${zeroPrecisionFormat.format(bandwidth / 1e6)}Mbps" - } else if (bandwidth > 1e6) { - "${singlePrecisionFormat.format(bandwidth / 1e6)}Mbps" - } else { - "${zeroPrecisionFormat.format(bandwidth / 1e3)}kbps" - } + val format = stringResource( + if (bandwidth >= 1e7) { + R.string.theoplayer_ui_bandwidth_format_10mbps + } else if (bandwidth >= 1e6) { + R.string.theoplayer_ui_bandwidth_format_1mbps + } else { + R.string.theoplayer_ui_bandwidth_format_kbps + } + ) + val decimalFormat = remember(format) { DecimalFormat(format) } + return decimalFormat.format(bandwidth / (if (bandwidth >= 1e6) 1e6 else 1e3)) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt index 209d77f..2b3bf32 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -49,11 +50,15 @@ fun SeekButton( modifier = Modifier .size(iconSize) .scale(scaleX = if (seekOffset >= 0) -1f else 1f, scaleY = 1f), - contentDescription = if (seekOffset >= 0) { - "Seek forward by $seekOffset seconds" - } else { - "Seek backward by $seekOffset seconds" - } + contentDescription = pluralStringResource( + if (seekOffset >= 0) { + R.plurals.theoplayer_ui_btn_seek_forward + } else { + R.plurals.theoplayer_ui_btn_seek_backward + }, + seekOffset.absoluteValue, + seekOffset.absoluteValue + ) ) Text( modifier = Modifier diff --git a/ui/src/main/java/com/theoplayer/android/ui/SettingsMenu.kt b/ui/src/main/java/com/theoplayer/android/ui/SettingsMenu.kt index 0232429..1279685 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SettingsMenu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SettingsMenu.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.theoplayer.android.api.player.track.mediatrack.quality.VideoQuality @@ -24,11 +25,11 @@ import com.theoplayer.android.api.player.track.mediatrack.quality.VideoQuality @Composable fun MenuScope.SettingsMenu() { Menu( - title = { Text(text = "Settings") }, + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_settings)) }, backIcon = { Icon( Icons.Rounded.Close, - contentDescription = "Close" + contentDescription = stringResource(R.string.theoplayer_ui_btn_menu_close) ) }, ) { @@ -39,7 +40,7 @@ fun MenuScope.SettingsMenu() { modifier = Modifier .weight(1f) .alignByBaseline(), - text = "Quality" + text = stringResource(R.string.theoplayer_ui_menu_quality) ) TextButton( modifier = Modifier @@ -66,7 +67,7 @@ fun MenuScope.SettingsMenu() { modifier = Modifier .weight(1f) .alignByBaseline(), - text = "Playback speed" + text = stringResource(R.string.theoplayer_ui_menu_playback_rate) ) TextButton( modifier = Modifier @@ -97,7 +98,7 @@ fun MenuScope.SettingsMenu() { @Composable fun MenuScope.QualityMenu() { Menu( - title = { Text(text = "Quality") } + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_quality)) } ) { QualityList(onClick = { closeCurrentMenu() }) } @@ -111,17 +112,28 @@ fun MenuScope.QualityMenu() { @Composable fun MenuScope.PlaybackRateMenu() { Menu( - title = { Text(text = "Playback speed") } + title = { Text(text = stringResource(R.string.theoplayer_ui_menu_playback_rate)) } ) { PlaybackRateList(onClick = { closeCurrentMenu() }) } } +@Composable internal fun formatActiveQualityLabel( targetVideoQuality: VideoQuality?, activeVideoQuality: VideoQuality? ): String { - return targetVideoQuality?.let { "${it.height}p" } - ?: activeVideoQuality?.let { "Automatic (${it.height}p)" } - ?: "Automatic" + return if (targetVideoQuality != null) { + stringResource( + R.string.theoplayer_ui_quality_with_height, + targetVideoQuality.height + ) + } else if (activeVideoQuality != null) { + stringResource( + R.string.theoplayer_ui_quality_automatic_with_height, + activeVideoQuality.height + ) + } else { + stringResource(R.string.theoplayer_ui_quality_automatic) + } } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/SettingsMenuButton.kt b/ui/src/main/java/com/theoplayer/android/ui/SettingsMenuButton.kt index e9f92d2..7a10fd0 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SettingsMenuButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SettingsMenuButton.kt @@ -6,6 +6,7 @@ import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp /** @@ -22,7 +23,7 @@ fun MenuScope.SettingsMenuButton( content: @Composable () -> Unit = { Icon( Icons.Rounded.Settings, - contentDescription = "Settings" + contentDescription = stringResource(R.string.theoplayer_ui_menu_settings) ) } ) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt index 1a900c5..8e70f19 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource /** * A list of subtitle tracks, from which the user can choose an active subtitle track. @@ -25,7 +26,7 @@ fun SubtitleTrackList( LazyColumn(modifier = modifier) { item(key = null) { ListItem( - headlineContent = { Text(text = "Off") }, + headlineContent = { Text(text = stringResource(R.string.theoplayer_ui_subtitles_off)) }, leadingContent = { RadioButton( selected = (activeSubtitleTrack == null), diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 73862c4..98c3479 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1 +1,98 @@ - \ No newline at end of file + + Play + Pause + Replay + Start casting + Stop casting + Enter fullscreen + Exit fullscreen + Close + Back + Mute + Unmute + + Seek forward by %1$d second + Seek forward by %1$d seconds + + + Seek backward by %1$d second + Seek backward by %1$d seconds + + LIVE + + + %1$s + + %1$s + + %1$s / %2$s + + %1$s / %2$s + + + Playing on %1$s + + Playing on + %1$s + + Playing on Chromecast + + Playing on + Chromecast + + + Language + + Settings + + Audio + + Subtitles + + Playback speed + + Quality + + + None + + + Off + + + Normal + + #.##x + + + %1$dp + + Automatic + + Automatic (%1$dp) + + + #Mbps + + #.#Mbps + + #kbps + + + Unknown + + + An error occurred + \ No newline at end of file