diff --git a/src/assets/css/librarybrowser.scss b/src/assets/css/librarybrowser.scss
index af3eacadaf..d251bde1c1 100644
--- a/src/assets/css/librarybrowser.scss
+++ b/src/assets/css/librarybrowser.scss
@@ -141,6 +141,10 @@
height: 1.7em;
align-items: center;
flex-shrink: 1;
+
+ [dir='rtl'] & {
+ margin: 0 0.5em 0 0;
+ }
}
.pageTitleWithDefaultLogo {
@@ -162,6 +166,10 @@
background-size: contain;
background-repeat: no-repeat;
width: 13.2em;
+
+ [dir='rtl'] & {
+ background-position: right center;
+ }
}
.skinHeader {
@@ -213,11 +221,20 @@
font-weight: 400 !important;
margin: 0 !important;
border-radius: 0 !important;
+
+ [dir='rtl'] & {
+ padding: 0.9em 2.4em 0.9em 0 !important;
+ }
}
.navMenuOptionIcon {
margin-right: 1.2em;
flex-shrink: 0;
+
+ [dir='rtl'] & {
+ margin-right: unset;
+ margin-left: 1.2em;
+ }
}
.navMenuOptionText {
@@ -226,8 +243,15 @@
}
.sidebarHeader {
- padding-left: 1.2em;
margin: 1em 0 0.5em;
+
+ [dir='ltr'] & {
+ padding-left: 1.2em;
+ }
+
+ [dir='rtl'] & {
+ padding-right: 1.2em;
+ }
}
.dashboardDocument .skinBody {
@@ -484,6 +508,19 @@
margin-left: 0;
}
}
+
+ [dir="rtl"] & {
+ padding-right: 32.45vw;
+ padding-left: 2%;
+
+ .layout-desktop &,
+ .layout-tv & {
+ .emby-scroller {
+ margin-left: unset;
+ margin-right: 0;
+ }
+ }
+ }
}
.detailSectionContent a {
@@ -655,6 +692,18 @@
display: block;
padding-left: 32.45vw;
}
+
+ [dir="rtl"] & {
+ .layout-mobile & {
+ padding: 0.5rem 0 0 5%;
+ }
+
+ .layout-desktop &,
+ .layout-tv & {
+ padding-right: 32.45vw;
+ padding-left: unset;
+ }
+ }
}
.layout-desktop .detailRibbon {
@@ -685,6 +734,10 @@
text-align: left;
min-width: 0;
max-width: 100%;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
}
.detailPageSecondaryContainer {
@@ -746,6 +799,19 @@
width: 25vw;
transform: translateY(-50%);
}
+
+ [dir="rtl"] & {
+ left: unset;
+
+ .layout-mobile &,
+ .layout-tv & {
+ right: 5%;
+ }
+
+ .layout-desktop & {
+ right: 3.3%;
+ }
+ }
}
.detailPagePrimaryContent {
@@ -1146,10 +1212,20 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards {
.padded-left {
padding-left: 3.3%;
+
+ [dir="rtl"] & {
+ padding-right: 3.3%;
+ padding-left: unset;
+ }
}
.padded-right {
padding-right: 3.3%;
+
+ [dir="rtl"] & {
+ padding-right: unset;
+ padding-left: 3.3%;
+ }
}
.padded-top {
diff --git a/src/components/cardbuilder/card.scss b/src/components/cardbuilder/card.scss
index ffd395a733..4163b9d8ed 100644
--- a/src/components/cardbuilder/card.scss
+++ b/src/components/cardbuilder/card.scss
@@ -303,6 +303,10 @@ button::-moz-focus-inner {
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
+
+ [dir="rtl"] & {
+ text-align: right;
+ }
}
.dialog .cardText {
diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index be0df0e512..4d42366590 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -11,7 +11,7 @@ import imageLoader from '../images/imageLoader';
import itemHelper from '../itemHelper';
import focusManager from '../focusManager';
import indicators from '../indicators/indicators';
-import globalize from '../../scripts/globalize';
+import globalize, { getCurrentDateTimeLocale } from '../../scripts/globalize';
import layoutManager from '../layoutManager';
import dom from '../../scripts/dom';
import browser from '../../scripts/browser';
@@ -909,19 +909,20 @@ import { appRouter } from '../appRouter';
}
if (options.showYear || options.showSeriesYear) {
+ const productionYear = item.ProductionYear?.toLocaleString(getCurrentDateTimeLocale(), {useGrouping: false});
if (item.Type === 'Series') {
if (item.Status === 'Continuing') {
- lines.push(globalize.translate('SeriesYearToPresent', item.ProductionYear || ''));
+ lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
} else {
if (item.EndDate && item.ProductionYear) {
- const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear();
- lines.push(item.ProductionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
+ const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear().toLocaleString(getCurrentDateTimeLocale(), {useGrouping: false});
+ lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
} else {
- lines.push(item.ProductionYear || '');
+ lines.push(productionYear || '');
}
}
} else {
- lines.push(item.ProductionYear || '');
+ lines.push(productionYear || '');
}
}
diff --git a/src/components/itemHelper.js b/src/components/itemHelper.js
index 030b6d7fcf..eba77649ce 100644
--- a/src/components/itemHelper.js
+++ b/src/components/itemHelper.js
@@ -27,7 +27,7 @@ export function getDisplayName(item, options = {}) {
let nameSeparator = ' - ';
if (options.includeParentInfo !== false) {
- number = 'S' + item.ParentIndexNumber + ':E' + number;
+ number = 'S' + item.ParentIndexNumber.toLocaleString() + ':E' + number.toLocaleString();
} else {
nameSeparator = '. ';
}
diff --git a/src/components/listview/listview.scss b/src/components/listview/listview.scss
index a6d1fc7efa..4bf98767fb 100644
--- a/src/components/listview/listview.scss
+++ b/src/components/listview/listview.scss
@@ -13,6 +13,10 @@
padding: 0.25em 0.25em 0.25em 0.5em;
cursor: pointer;
overflow: hidden;
+
+ [dir='rtl'] & {
+ text-align: right;
+ }
}
.listItem-withContentWrapper {
@@ -211,7 +215,7 @@
width: 1em !important;
height: 1em !important;
font-size: 143%;
- padding: 0 0.25em 0 0;
+ margin: 0 0.25em 0 0;
}
.listItemIcon:not(.listItemIcon-transparent) {
diff --git a/src/components/loading/loading.js b/src/components/loading/loading.js
index 66b4be421a..0a3815c01c 100644
--- a/src/components/loading/loading.js
+++ b/src/components/loading/loading.js
@@ -18,7 +18,7 @@ export function show() {
elem.classList.add('docspinner');
elem.classList.add('mdl-spinner');
- elem.innerHTML = '
";
+ html += "
";
html += virtualFolder.Locations[0];
html += '
';
} else {
diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js
index d5da82b740..d6598dca3e 100644
--- a/src/controllers/playback/video/index.js
+++ b/src/controllers/playback/video/index.js
@@ -10,7 +10,7 @@ import itemHelper from '../../../components/itemHelper';
import mediaInfo from '../../../components/mediainfo/mediainfo';
import focusManager from '../../../components/focusManager';
import { Events } from 'jellyfin-apiclient';
-import globalize from '../../../scripts/globalize';
+import globalize, { getCurrentDateTimeLocale } from '../../../scripts/globalize';
import { appHost } from '../../../components/apphost';
import layoutManager from '../../../components/layoutManager';
import * as userSettings from '../../../scripts/settings/userSettings';
@@ -216,7 +216,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
let title = itemName;
if (item.PremiereDate) {
try {
- const year = datetime.parseISO8601Date(item.PremiereDate).getFullYear();
+ const year = datetime.parseISO8601Date(item.PremiereDate).getFullYear().toLocaleString(getCurrentDateTimeLocale(), {useGrouping: false});
title += ` (${year})`;
} catch (e) {
console.error(e);
diff --git a/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss b/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss
index 5af739bac1..a50424a644 100644
--- a/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss
+++ b/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss
@@ -10,6 +10,11 @@
z-index: 1;
color: #fff;
display: flex;
+
+ [dir='rtl'] & {
+ left: 0;
+ right: unset;
+ }
}
.emby-scrollbuttons-button > .material-icons {
diff --git a/src/elements/emby-select/emby-select.scss b/src/elements/emby-select/emby-select.scss
index 32aec69c46..dfac666023 100644
--- a/src/elements/emby-select/emby-select.scss
+++ b/src/elements/emby-select/emby-select.scss
@@ -18,6 +18,10 @@
outline: none !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
width: 100%;
+
+ [dir="rtl"] & {
+ padding: 0.5em 0.5em 0.5em 1.9em;
+ }
}
.emby-select[disabled] {
@@ -36,6 +40,10 @@
.selectContainer-inline > .emby-select {
padding: 0.3em 1.9em 0.3em 0.5em;
font-size: inherit;
+
+ [dir="rtl"] & {
+ padding: 0.3em 0.5em 0.3em 1.9em;
+ }
}
.selectContainer-inline > .emby-select[disabled] {
@@ -96,6 +104,11 @@
top: 0.2em;
color: inherit;
pointer-events: none;
+
+ [dir="rtl"] & {
+ right: unset;
+ left: 0.3em;
+ }
}
.selectContainer-inline > .selectArrowContainer {
diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js
index 0bb20270e6..7cab8d5dc2 100644
--- a/src/elements/emby-slider/emby-slider.js
+++ b/src/elements/emby-slider/emby-slider.js
@@ -5,6 +5,7 @@ import keyboardnavigation from '../../scripts/keyboardNavigation';
import './emby-slider.scss';
import 'webcomponents.js/webcomponents-lite';
import '../emby-input/emby-input';
+import { getIsRTL } from '../../scripts/globalize';
/* eslint-disable indent */
@@ -32,6 +33,10 @@ import '../emby-input/emby-input';
let fraction = (clientX - rect.left) / rect.width;
+ if (getIsRTL()) {
+ fraction = (rect.width - (clientX - rect.left)) / rect.width;
+ }
+
// Snap to step
const valueRange = range.max - range.min;
if (range.step !== 'any' && valueRange !== 0) {
@@ -111,6 +116,9 @@ import '../emby-input/emby-input';
const bubbleRect = bubble.getBoundingClientRect();
let bubblePos = bubbleTrackRect.width * value / 100;
+ if (getIsRTL()) {
+ bubblePos = bubbleTrackRect.width - bubblePos;
+ }
bubblePos = Math.min(Math.max(bubblePos, bubbleRect.width / 2), bubbleTrackRect.width - bubbleRect.width / 2);
bubble.style.left = bubblePos + 'px';
@@ -411,7 +419,11 @@ import '../emby-input/emby-input';
function setRange(elem, startPercent, endPercent) {
const style = elem.style;
- style.left = Math.max(startPercent, 0) + '%';
+ if (getIsRTL()) {
+ style.right = Math.max(startPercent, 0) + '%';
+ } else {
+ style.left = Math.max(startPercent, 0) + '%';
+ }
const widthPercent = endPercent - startPercent;
style.width = Math.max(Math.min(widthPercent, 100), 0) + '%';
diff --git a/src/elements/emby-slider/emby-slider.scss b/src/elements/emby-slider/emby-slider.scss
index f7503d4fd5..0aa6204ca5 100644
--- a/src/elements/emby-slider/emby-slider.scss
+++ b/src/elements/emby-slider/emby-slider.scss
@@ -154,6 +154,12 @@
left: 0;
position: absolute;
padding: 0 0.54em; /* half of slider thumb size */
+
+ [dir="rtl"] & {
+ left: unset;
+ right: 0;
+ padding: 0 0 0 0.54em;
+ }
}
.mdl-slider-background-flex {
@@ -167,6 +173,11 @@
overflow: hidden;
border: 0;
padding: 0;
+
+ [dir="rtl"] & {
+ left: unset;
+ right: 0;
+ }
}
.mdl-slider-background-flex-inner {
@@ -182,6 +193,11 @@
top: 0;
bottom: 0;
background-color: #00a4dc;
+
+ [dir="rtl"] & {
+ left: unset;
+ right: 0;
+ }
}
.mdl-slider-background-lower-clear {
@@ -212,6 +228,10 @@
left: 0;
right: 0;
margin: 0 0.54em; /* half of slider thumb size */
+
+ [dir="rtl"] & {
+ margin: 0 0 0 0.54em;
+ }
}
.sliderBubble {
diff --git a/src/scripts/datetime.js b/src/scripts/datetime.js
index 0a014b1de4..691d2802f5 100644
--- a/src/scripts/datetime.js
+++ b/src/scripts/datetime.js
@@ -84,7 +84,7 @@ import globalize from './globalize';
hours = Math.floor(hours);
if (hours) {
- parts.push(hours);
+ parts.push(hours.toLocaleString());
}
ticks -= (hours * ticksPerHour);
@@ -95,7 +95,9 @@ import globalize from './globalize';
ticks -= (minutes * ticksPerMinute);
if (minutes < 10 && hours) {
- minutes = '0' + minutes;
+ minutes = (0).toLocaleString() + minutes.toLocaleString();
+ } else {
+ minutes = minutes.toLocaleString();
}
parts.push(minutes);
@@ -103,7 +105,9 @@ import globalize from './globalize';
seconds = Math.floor(seconds);
if (seconds < 10) {
- seconds = '0' + seconds;
+ seconds = (0).toLocaleString() + seconds.toLocaleString();
+ } else {
+ seconds = seconds.toLocaleString();
}
parts.push(seconds);
diff --git a/src/scripts/globalize.js b/src/scripts/globalize.js
index 3953ac9d50..201db1153b 100644
--- a/src/scripts/globalize.js
+++ b/src/scripts/globalize.js
@@ -10,6 +10,7 @@ import { currentSettings as userSettings } from './settings/userSettings';
const allTranslations = {};
let currentCulture;
let currentDateTimeCulture;
+ let isRTL = false;
export function getCurrentLocale() {
return currentCulture;
@@ -38,6 +39,10 @@ import { currentSettings as userSettings } from './settings/userSettings';
return fallbackCulture;
}
+ export function getIsRTL() {
+ return isRTL;
+ }
+
export function updateCurrentCulture() {
let culture;
try {
@@ -46,6 +51,13 @@ import { currentSettings as userSettings } from './settings/userSettings';
console.error('no language set in user settings');
}
culture = culture || getDefaultLanguage();
+ isRTL = culture === 'ar' || culture === 'fa' || culture === 'ur_PK' || culture === 'he';
+
+ if (isRTL) {
+ document.getElementsByTagName('body')[0].setAttribute('dir', 'rtl');
+ } else {
+ document.getElementsByTagName('body')[0].setAttribute('dir', 'ltr');
+ }
currentCulture = normalizeLocaleName(culture);
@@ -257,7 +269,8 @@ export default {
getCurrentLocale,
getCurrentDateTimeLocale,
register,
- updateCurrentCulture
+ updateCurrentCulture,
+ getIsRTL
};
/* eslint-enable indent */
diff --git a/src/scripts/site.js b/src/scripts/site.js
index edf2dab2cd..0a69aca00e 100644
--- a/src/scripts/site.js
+++ b/src/scripts/site.js
@@ -100,6 +100,7 @@ function onGlobalizeInit() {
import('../assets/css/fonts.scss');
}
+ import('../styles/rtl.scss');
import('../assets/css/librarybrowser.scss');
loadPlugins().then(function () {
diff --git a/src/styles/rtl.scss b/src/styles/rtl.scss
new file mode 100644
index 0000000000..436effb3a1
--- /dev/null
+++ b/src/styles/rtl.scss
@@ -0,0 +1,23 @@
+.chevron_right,
+.chevron_left,
+.arrow_back,
+.play_arrow,
+.playlist_add,
+.video_library,
+.shuffle,
+.input,
+.live_tv,
+.dvr,
+.play_circle_filled,
+.shopping_cart,
+.vpn_key,
+.skip_next,
+.skip_previous,
+.fast_forward,
+.fast_rewind,
+.undo,
+.redo {
+ [dir='rtl'] & {
+ transform: scale(-1, 1);
+ }
+}