[Android] Correctly display fractions in right-to-left mode

Robert Stone git at videolan.org
Thu Nov 20 06:53:55 UTC 2025


vlc-android | branch: master | Robert Stone <rhstone at gmail.com> | Fri Nov  7 10:42:17 2025 -0800| [68387cfe41e0e1c5fc715fe3a1b5ad059dee4705] | committer: Nicolas Pomepuy

Correctly display fractions in right-to-left mode

> https://code.videolan.org/videolan/vlc-android/commit/68387cfe41e0e1c5fc715fe3a1b5ad059dee4705
---

 .../src/main/java/org/videolan/tools/Strings.kt    | 22 +++++++++++++-------
 .../src/org/videolan/vlc/gui/audio/AudioPlayer.kt  | 24 +++++++++++++++++-----
 2 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/application/tools/src/main/java/org/videolan/tools/Strings.kt b/application/tools/src/main/java/org/videolan/tools/Strings.kt
index a8644d85a3..9c4747aa95 100644
--- a/application/tools/src/main/java/org/videolan/tools/Strings.kt
+++ b/application/tools/src/main/java/org/videolan/tools/Strings.kt
@@ -120,21 +120,29 @@ fun Long.readableNumber(): String {
 
 fun Int.forbiddenChars() = FORBIDDEN_CHARS.substrlng(this)
 
-fun String.markBidi(): String {
+fun String.markBidi(markLtr: Boolean = false): String {
+    //left-to-right isolate
+    val lri = "\u2066"
     //right-to-left isolate
     val rli = "\u2067"
     //pop directional isolate
     val pdi = "\u2069"
-    for (ch in this) {
+    return when {
+        markLtr -> lri + this + pdi
+        this.hasRtl() -> rli + this + pdi
+        else -> this
+    }
+}
+
+fun String.hasRtl(): Boolean {
+    return this.toCharArray().any { ch ->
         when (Character.getDirectionality(ch)) {
             Character.DIRECTIONALITY_RIGHT_TO_LEFT,
             Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC,
             Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING,
-            Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE -> return rli + this + pdi
-            Character.DIRECTIONALITY_LEFT_TO_RIGHT,
-            Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING,
-            Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE -> return this
+            Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE,
+            Character.DIRECTIONALITY_RIGHT_TO_LEFT_ISOLATE -> return true
+            else -> false
         }
     }
-    return this
 }
\ No newline at end of file
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt b/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt
index 6a0a05679a..edd7322e53 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt
@@ -87,7 +87,9 @@ import org.videolan.tools.Settings
 import org.videolan.tools.copy
 import org.videolan.tools.dp
 import org.videolan.tools.formatRateString
+import org.videolan.tools.hasRtl
 import org.videolan.tools.isStarted
+import org.videolan.tools.markBidi
 import org.videolan.tools.putSingle
 import org.videolan.tools.setGone
 import org.videolan.tools.setVisible
@@ -647,8 +649,11 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
                 val progressTimeDescription =  TalkbackUtil.millisToString(requireActivity(), if (showRemainingTime && totalTime > 0) totalTime - progressTime else progressTime)
                 val currentProgressText = if (progressTimeText.isNullOrEmpty()) "0:00" else progressTimeText
 
+                val isRtlLocale = LocaleUtil.isRtl()
                 val size = if (playlistModel.service?.playlistManager?.stopAfter != -1 ) (playlistModel.service?.playlistManager?.stopAfter ?: 0) + 1 else medias.size
-                val textTrack = getString(R.string.track_index, "${playlistModel.currentMediaPosition + 1} / $size")
+                val textTrack = getString(R.string.track_index, "${playlistModel.currentMediaPosition + 1} / $size".let {
+                    if (isRtlLocale) it.markBidi(true) else it
+                })
                 val textTrackDescription = getString(R.string.talkback_track_index, "${playlistModel.currentMediaPosition + 1}", "$size")
 
                 val textProgress = if (audioPlayProgressMode) {
@@ -656,7 +661,9 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
                     if ((lastEndsAt - endsAt).absoluteValue > 1) lastEndsAt = endsAt
                     getString(
                             R.string.audio_queue_progress_finished,
-                            getTimeInstance(java.text.DateFormat.MEDIUM).format(lastEndsAt)
+                            getTimeInstance(java.text.DateFormat.MEDIUM).format(lastEndsAt).let {
+                                if (isRtlLocale) it.markBidi(true) else it
+                            }
                     )
                 } else
                     if (showRemainingTime && totalTime > 0) getString(
@@ -665,14 +672,18 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
                     )
                     else getString(
                             R.string.audio_queue_progress,
-                            if (totalTimeText.isNullOrEmpty()) currentProgressText else "$currentProgressText / $totalTimeText"
+                            if (totalTimeText.isNullOrEmpty()) currentProgressText else "$currentProgressText / $totalTimeText".let {
+                                if (isRtlLocale) it.markBidi(true) else it
+                            }
                     )
                 val textDescription = if (audioPlayProgressMode) {
                     val endsAt = System.currentTimeMillis() + totalTime - progressTime
                     if ((lastEndsAt - endsAt).absoluteValue > 1) lastEndsAt = endsAt
                     getString(
                             R.string.audio_queue_progress_finished,
-                            getTimeInstance(java.text.DateFormat.MEDIUM).format(lastEndsAt)
+                            getTimeInstance(java.text.DateFormat.MEDIUM).format(lastEndsAt).let {
+                                if (isRtlLocale) it.markBidi(true) else it
+                            }
                     )
                 } else
                     if (showRemainingTime && totalTime > 0) getString(
@@ -683,7 +694,10 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
                             R.string.audio_queue_progress,
                             if (totalTimeText.isNullOrEmpty()) progressTimeDescription else getString(R.string.talkback_out_of, progressTimeDescription, totalTimeDescription)
                     )
-                Pair("$textTrack  ${TextUtils.SEPARATOR}  $textProgress", "$textTrackDescription. $textDescription")
+
+                val finalTextTrack = if (isRtlLocale && !textTrack.hasRtl()) textTrack.markBidi(true) else textTrack
+                val finalTextProgress = if (isRtlLocale && !textProgress.hasRtl()) textProgress.markBidi(true) else textProgress
+                Pair("$finalTextTrack  ${TextUtils.SEPARATOR}  $finalTextProgress", "$textTrackDescription. $textDescription")
             }
             binding.audioPlayProgress.text = text.first
             binding.audioPlayProgress.contentDescription = text.second



More information about the Android mailing list