Skip to content

Commit f4357f7

Browse files
committed
fix(plyr): add l10n to speed selections
Signed-off-by: Simon L. <szaimen@e.mail.de>
1 parent 0518262 commit f4357f7

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/components/Audios.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
3737
3838
import logger from '../services/logger.js'
3939
import { preloadMedia } from '../services/mediaPreloader'
40-
import { plyrTranslations } from '../utils/plyrTranslations'
40+
import { localizeSpeedLabels, plyrTranslations } from '../utils/plyrTranslations'
4141
4242
const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')
4343
@@ -53,6 +53,7 @@ export default {
5353
data() {
5454
return {
5555
fallback: false,
56+
speedListenerBound: false,
5657
}
5758
},
5859
@@ -111,6 +112,15 @@ export default {
111112
control.addEventListener('mouseenter', this.disableSwipe)
112113
control.addEventListener('mouseleave', this.enableSwipe)
113114
})
115+
116+
// The Plyr menu is only in the DOM once the controls are mounted (see
117+
// above), so wire up the speed-label localization here rather than in
118+
// mounted(). Register the rate-change listener once.
119+
if (!this.speedListenerBound && this.$refs.plyr?.player) {
120+
this.$refs.plyr.player.on('ratechange', this.localizeSpeed)
121+
this.speedListenerBound = true
122+
}
123+
this.localizeSpeed()
114124
},
115125
116126
beforeDestroy() {
@@ -122,6 +132,11 @@ export default {
122132
},
123133
124134
methods: {
135+
localizeSpeed() {
136+
// Defer so we run after Plyr's own label/badge update for this event.
137+
this.$nextTick(() => localizeSpeedLabels(this.$el))
138+
},
139+
125140
donePlaying() {
126141
this.$refs.audio.autoplay = false
127142
this.$refs.audio.load()

src/components/Videos.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import logger from '../services/logger.js'
4848
import { findLivePhotoPeerFromName } from '../utils/livePhotoUtils'
4949
import { getPreviewIfAny } from '../utils/previewUtils'
5050
import { preloadMedia } from '../services/mediaPreloader.js'
51-
import { plyrTranslations } from '../utils/plyrTranslations'
51+
import { localizeSpeedLabels, plyrTranslations } from '../utils/plyrTranslations'
5252
5353
const VuePlyr = () => import(/* webpackChunkName: 'plyr' */'@skjnldsv/vue-plyr')
5454
@@ -66,6 +66,7 @@ export default {
6666
return {
6767
isFullscreenButtonVisible: false,
6868
fallback: false,
69+
speedListenerBound: false,
6970
}
7071
},
7172
@@ -138,6 +139,15 @@ export default {
138139
control.addEventListener('mouseenter', this.disableSwipe)
139140
control.addEventListener('mouseleave', this.enableSwipe)
140141
})
142+
143+
// The Plyr menu is only in the DOM once the controls are mounted (see
144+
// above), so wire up the speed-label localization here rather than in
145+
// mounted(). Register the rate-change listener once.
146+
if (!this.speedListenerBound && this.$refs.plyr?.player) {
147+
this.$refs.plyr.player.on('ratechange', this.localizeSpeed)
148+
this.speedListenerBound = true
149+
}
150+
this.localizeSpeed()
141151
},
142152
143153
beforeDestroy() {
@@ -149,6 +159,11 @@ export default {
149159
},
150160
151161
methods: {
162+
localizeSpeed() {
163+
// Defer so we run after Plyr's own label/badge update for this event.
164+
this.$nextTick(() => localizeSpeedLabels(this.$el))
165+
},
166+
152167
hideHeaderAndFooter() {
153168
// work arround to get the state of the fullscreen button, aria-selected attribute is not reliable
154169
this.isFullscreenButtonVisible = !this.isFullscreenButtonVisible

src/utils/plyrTranslations.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { translate as t } from '@nextcloud/l10n'
7+
import { getCanonicalLocale } from '@nextcloud/l10n'
78

89
/**
910
* Plyr `i18n` option so the player controls and menus
@@ -49,3 +50,48 @@ export const plyrTranslations: Record<string, string> = {
4950
enabled: t('viewer', 'Enabled'),
5051
advertisement: t('viewer', 'Ad'),
5152
}
53+
54+
/**
55+
* Plyr renders speed values via a plain template literal (`` `${speed}×` ``),
56+
* which always uses a `.` decimal separator regardless of locale — so German
57+
* shows `1.5×` instead of `1,5×`. Plyr exposes no hook for this, so we
58+
* re-format the rendered labels using the user's locale.
59+
*
60+
* The speed panel's id always ends in `-speed`. Each entry is a
61+
* `button[role="menuitemradio"]` carrying the numeric speed in its `value`
62+
* attribute (the source of truth). The home-pane `.plyr__menu__value` badge
63+
* shows the current speed as a bare `<number>×` string, which we match by
64+
* pattern so we never touch the quality/captions badges.
65+
*
66+
* @param root the Plyr root element to localize labels within
67+
*/
68+
export function localizeSpeedLabels(root: ParentNode): void {
69+
const formatter = new Intl.NumberFormat(getCanonicalLocale())
70+
const speedLabel = (value: number): string =>
71+
value === 1 ? t('viewer', 'Normal') : `${formatter.format(value)}×`
72+
73+
// Speed submenu radio items, scoped to the speed panel so we don't touch
74+
// quality (e.g. "1080") or captions entries.
75+
const items = root.querySelectorAll<HTMLButtonElement>(
76+
'.plyr__menu__container [id$="-speed"] [role="menuitemradio"]',
77+
)
78+
items.forEach((item) => {
79+
const value = Number.parseFloat(item.value)
80+
if (Number.isNaN(value)) {
81+
return
82+
}
83+
const label = item.querySelector('span')
84+
if (label) {
85+
label.textContent = speedLabel(value)
86+
}
87+
})
88+
89+
// Home-pane badge showing the current speed (e.g. "Speed: 1,5×"). Match the
90+
// bare "<number>×" form so quality/captions badges are left untouched.
91+
root.querySelectorAll<HTMLElement>('.plyr__menu__value').forEach((badge) => {
92+
const match = /^(\d+(?:\.\d+)?)×$/.exec((badge.textContent ?? '').trim())
93+
if (match) {
94+
badge.textContent = `${formatter.format(Number.parseFloat(match[1]))}×`
95+
}
96+
})
97+
}

0 commit comments

Comments
 (0)