Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add localization support #54

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
42 changes: 42 additions & 0 deletions app/src/main/res/values-nl/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="nl">
<string name="app_name">THEOplayer Open Video UI voor Android Demo</string>
<string name="theoplayer_ui_btn_play">Afspelen</string>
<string name="theoplayer_ui_btn_pause">Pauzeren</string>
<string name="theoplayer_ui_btn_replay">Opnieuw spelen</string>
<string name="theoplayer_ui_btn_chromecast_start">Starten met casten</string>
<string name="theoplayer_ui_btn_chromecast_stop">Stop casting</string>
<string name="theoplayer_ui_btn_fullscreen_enter">Volledig scherm</string>
<string name="theoplayer_ui_btn_fullscreen_exit">Volledig scherm sluiten</string>
<string name="theoplayer_ui_btn_menu_close">Sluiten</string>
<string name="theoplayer_ui_btn_back">Terug</string>
<string name="theoplayer_ui_btn_mute">Dempen</string>
<string name="theoplayer_ui_btn_unmute">Dempen opheffen</string>
<plurals name="theoplayer_ui_btn_seek_forward">
<item quantity="one">Spring %1$d seconde vooruit</item>
<item quantity="other">Spring %1$d seconden vooruit</item>
</plurals>
<plurals name="theoplayer_ui_btn_seek_backward">
<item quantity="one">Spring %1$d seconde achteruit</item>
<item quantity="other">Spring %1$d seconden achteruit</item>
</plurals>
<string name="theoplayer_ui_btn_live">LIVE</string>
<string name="theoplayer_ui_chromecast_playing_on_receiver">Speelt op %1$s</string>
<string name="theoplayer_ui_chromecast_playing_on_receiver_expanded_first_line">Speelt op</string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver">Speelt op Chromecast</string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_first_line">Speelt op</string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_second_line">Chromecast</string>
<string name="theoplayer_ui_menu_language">Taal</string>
<string name="theoplayer_ui_menu_settings">Instellingen</string>
<string name="theoplayer_ui_menu_audio">Audio</string>
<string name="theoplayer_ui_menu_subtitles">Ondertitels</string>
<string name="theoplayer_ui_menu_playback_rate">Afspeelsnelheid</string>
<string name="theoplayer_ui_menu_quality">Kwaliteit</string>
<string name="theoplayer_ui_audio_none">Geen</string>
<string name="theoplayer_ui_subtitles_off">Uit</string>
<string name="theoplayer_ui_playback_rate_normal">Normaal</string>
<string name="theoplayer_ui_quality_automatic">Automatisch</string>
<string name="theoplayer_ui_quality_automatic_with_height">Automatisch (%1$dp)</string>
<string name="theoplayer_ui_track_unknown">Onbekend</string>
<string name="theoplayer_ui_error_title">Fout</string>
</resources>
52 changes: 51 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:locale="en">
<string name="app_name">THEOplayer Open Video UI for Android Demo</string>

<!-- Copied from Open Video UI for Android -->
<string name="theoplayer_ui_btn_play">Play</string>
<string name="theoplayer_ui_btn_pause">Pause</string>
<string name="theoplayer_ui_btn_replay">Replay</string>
<string name="theoplayer_ui_btn_chromecast_start">Start casting</string>
<string name="theoplayer_ui_btn_chromecast_stop">Stop casting</string>
<string name="theoplayer_ui_btn_fullscreen_enter">Enter fullscreen</string>
<string name="theoplayer_ui_btn_fullscreen_exit">Exit fullscreen</string>
<string name="theoplayer_ui_btn_menu_close">Close</string>
<string name="theoplayer_ui_btn_back">Back</string>
<string name="theoplayer_ui_btn_mute">Mute</string>
<string name="theoplayer_ui_btn_unmute">Unmute</string>
<plurals name="theoplayer_ui_btn_seek_forward">
<item quantity="one">Seek forward by <xliff:g example="5" id="seek_offset">%1$d</xliff:g> second</item>
<item quantity="other">Seek forward by <xliff:g example="5" id="seek_offset">%1$d</xliff:g> seconds</item>
</plurals>
<plurals name="theoplayer_ui_btn_seek_backward">
<item quantity="one">Seek backward by <xliff:g example="5" id="seek_offset">%1$d</xliff:g> second</item>
<item quantity="other">Seek backward by <xliff:g example="5" id="seek_offset">%1$d</xliff:g> seconds</item>
</plurals>
<string name="theoplayer_ui_btn_live">LIVE</string>
<string name="theoplayer_ui_current_time" translatable="false"><xliff:g example="01:23" id="current_time">%1$s</xliff:g></string>
<string name="theoplayer_ui_current_time_remaining" translatable="false"><xliff:g example="-01:23" id="remaining_time">%1$s</xliff:g></string>
<string name="theoplayer_ui_current_time_with_duration" translatable="false"><xliff:g example="01:23" id="current_time">%1$s</xliff:g> / <xliff:g example="60:00" id="duration">%2$s</xliff:g></string>
<string name="theoplayer_ui_current_time_remaining_with_duration" translatable="false"><xliff:g example="-01:23" id="remaining_time">%1$s</xliff:g> / <xliff:g example="60:00" id="duration">%2$s</xliff:g></string>
<string name="theoplayer_ui_chromecast_playing_on_receiver">Playing on <xliff:g example="Living Room" id="receiver_name">%1$s</xliff:g></string>
<string name="theoplayer_ui_chromecast_playing_on_receiver_expanded_first_line">Playing on</string>
<string name="theoplayer_ui_chromecast_playing_on_receiver_expanded_second_line" translatable="false"><xliff:g example="Living Room" id="receiver_name">%1$s</xliff:g></string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver">Playing on Chromecast</string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_first_line">Playing on</string>
<string name="theoplayer_ui_chromecast_playing_on_unknown_receiver_expanded_second_line">Chromecast</string>
<string name="theoplayer_ui_menu_language">Language</string>
<string name="theoplayer_ui_menu_settings">Settings</string>
<string name="theoplayer_ui_menu_audio">Audio</string>
<string name="theoplayer_ui_menu_subtitles">Subtitles</string>
<string name="theoplayer_ui_menu_playback_rate">Playback speed</string>
<string name="theoplayer_ui_menu_quality">Quality</string>
<string name="theoplayer_ui_audio_none">None</string>
<string name="theoplayer_ui_subtitles_off">Off</string>
<string name="theoplayer_ui_playback_rate_normal">Normal</string>
<string name="theoplayer_ui_playback_rate_format" translatable="false">#.##x</string>
<string name="theoplayer_ui_quality_automatic">Automatic</string>
<string name="theoplayer_ui_quality_with_height" translatable="false"><xliff:g example="720" id="quality_height">%1$d</xliff:g>p</string>
<string name="theoplayer_ui_quality_automatic_with_height">Automatic (<xliff:g example="720" id="quality_height">%1$d</xliff:g>p)</string>
<string name="theoplayer_ui_bandwidth_format_10mbps" translatable="false">#Mbps</string>
<string name="theoplayer_ui_bandwidth_format_1mbps" translatable="false">#.#Mbps</string>
<string name="theoplayer_ui_bandwidth_format_kbps" translatable="false">#kbps</string>
<string name="theoplayer_ui_track_unknown">Unknown</string>
<string name="theoplayer_ui_error_title">An error occurred</string>
</resources>
81 changes: 81 additions & 0 deletions docs/guides/localization.md
Original file line number Diff line number Diff line change
@@ -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"
<resources>
<string name="theoplayer_ui_menu_language">Langue</string> <!-- translated to French -->
</resources>
```

:::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"
<resources>
<string name="theoplayer_ui_menu_language">Langue</string>
</resources>
```

## 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)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
)
}
) {
Expand Down
25 changes: 22 additions & 3 deletions ui/src/main/java/com/theoplayer/android/ui/ChromecastDisplay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
)
}
}
Expand Down Expand Up @@ -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
)
}
Expand Down
27 changes: 21 additions & 6 deletions ui/src/main/java/com/theoplayer/android/ui/CurrentTimeDisplay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ fun DefaultUI(
}
Spacer(modifier = Modifier.weight(1f))
LanguageMenuButton()
SettingsMenuButton()
ChromecastButton()
}
}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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)
)
}
) {
Expand Down
7 changes: 5 additions & 2 deletions ui/src/main/java/com/theoplayer/android/ui/Helper.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()) {
Expand All @@ -74,5 +77,5 @@ fun formatTrackLabel(track: Track): String {
}
return languageCode
}
return ""
return stringResource(R.string.theoplayer_ui_track_unknown)
}
Loading
Loading