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