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 = '
'; + elem.innerHTML = '
'; document.body.appendChild(elem); diff --git a/src/components/mediainfo/mediainfo.js b/src/components/mediainfo/mediainfo.js index f21bc8f666..2172329d89 100644 --- a/src/components/mediainfo/mediainfo.js +++ b/src/components/mediainfo/mediainfo.js @@ -1,6 +1,6 @@ import escapeHtml from 'escape-html'; import datetime from '../../scripts/datetime'; -import globalize from '../../scripts/globalize'; +import globalize, { getCurrentDateTimeLocale } from '../../scripts/globalize'; import { appRouter } from '../appRouter'; import itemHelper from '../itemHelper'; import indicators from '../indicators/indicators'; @@ -111,6 +111,8 @@ import '../../elements/emby-button/emby-button'; const showFolderRuntime = item.Type === 'MusicAlbum' || item.MediaType === 'MusicArtist' || item.Type === 'Playlist' || item.MediaType === 'Playlist' || item.MediaType === 'MusicGenre'; + const dateTimeLocale = getCurrentDateTimeLocale(); + if (showFolderRuntime) { count = item.SongCount || item.ChildCount; @@ -175,16 +177,16 @@ import '../../elements/emby-button/emby-button'; if (options.year !== false && item.ProductionYear && item.Type === 'Series') { if (item.Status === 'Continuing') { - miscInfo.push(globalize.translate('SeriesYearToPresent', item.ProductionYear)); + miscInfo.push(globalize.translate('SeriesYearToPresent', item.ProductionYear.toLocaleString(dateTimeLocale, {useGrouping: false}))); } else if (item.ProductionYear) { - text = item.ProductionYear; + text = item.ProductionYear.toLocaleString(dateTimeLocale, {useGrouping: false}); if (item.EndDate) { try { - const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); + const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear().toLocaleString(dateTimeLocale, {useGrouping: false}); if (endYear !== item.ProductionYear) { - text += `-${datetime.parseISO8601Date(item.EndDate).getFullYear()}`; + text += `-${endYear}`; } } catch (e) { console.error('error parsing date:', item.EndDate); @@ -245,7 +247,7 @@ import '../../elements/emby-button/emby-button'; miscInfo.push(item.ProductionYear); } else if (item.PremiereDate) { try { - text = datetime.parseISO8601Date(item.PremiereDate).getFullYear(); + text = datetime.parseISO8601Date(item.PremiereDate).getFullYear().toLocaleString(dateTimeLocale, {useGrouping: false}); miscInfo.push(text); } catch (e) { console.error('error parsing date:', item.PremiereDate); diff --git a/src/components/mediainfo/mediainfo.scss b/src/components/mediainfo/mediainfo.scss index 508c9d96ad..4c43f41fea 100644 --- a/src/components/mediainfo/mediainfo.scss +++ b/src/components/mediainfo/mediainfo.scss @@ -1,6 +1,10 @@ .mediaInfoItem { margin: 0 1em 0 0; padding: 0; + + [dir="rtl"] & { + margin: 0 0 0 1em; + } } .mediaInfoText { @@ -26,6 +30,11 @@ .mediaInfoItem:last-child { margin-right: 0; + + [dir='rtl'] & { + margin-right: unset; + margin-left: 0; + } } .starRatingContainer { diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index a6fe46b295..fcf5255a94 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -280,7 +280,7 @@ import confirm from '../../components/confirm/confirm'; html += clientImage; } - html += '
'; + html += '
'; html += '
' + escapeHtml(session.DeviceName) + '
'; html += '
' + escapeHtml(DashboardPage.getAppSecondaryText(session)) + '
'; html += '
'; diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 8a43d7b690..df8be24712 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -343,7 +343,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder'; html += ' '; html += '
'; } else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) { - html += "
"; + 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); + } +}