diff --git a/.eslintrc.yml b/.eslintrc.yml index 880315fd12..f501f33546 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -49,8 +49,7 @@ globals: getWindowLocationSearch: writable Globalize: writable Hls: writable - humaneDate: writable - humaneElapsed: writable + dfnshelper: writable LibraryMenu: writable LinkParser: writable LiveTvHelpers: writable diff --git a/package.json b/package.json index 6fcd0e8766..bac0d7e251 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "dependencies": { "alameda": "^1.4.0", "core-js": "^3.6.4", + "date-fns": "^2.11.1", "document-register-element": "^1.14.3", "flv.js": "^1.5.0", "hls.js": "^0.13.1", @@ -89,7 +90,8 @@ "src/components/input/keyboardnavigation.js", "src/components/sanatizefilename.js", "src/scripts/settings/webSettings.js", - "src/components/scrollManager.js" + "src/components/scrollManager.js", + "src/scripts/dfnshelper.js" ], "plugins": [ "@babel/plugin-transform-modules-amd" diff --git a/src/bundle.js b/src/bundle.js index ba5f74b163..316f42c94a 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -112,3 +112,14 @@ var polyfill = require("@babel/polyfill/dist/polyfill"); _define("polyfill", function () { return polyfill; }); + +// Date-FNS +var date_fns = require("date-fns"); +_define("date-fns", function () { + return date_fns; +}); + +var date_fns_locale = require("date-fns/locale"); +_define("date-fns/locale", function () { + return date_fns_locale; +}); diff --git a/src/components/activitylog.js b/src/components/activitylog.js index 05971f01b8..934a610ad0 100644 --- a/src/components/activitylog.js +++ b/src/components/activitylog.js @@ -1,4 +1,4 @@ -define(["events", "globalize", "dom", "datetime", "userSettings", "serverNotifications", "connectionManager", "emby-button", "listViewStyle"], function (events, globalize, dom, datetime, userSettings, serverNotifications, connectionManager) { +define(["events", "globalize", "dom", "date-fns", "dfnshelper", "userSettings", "serverNotifications", "connectionManager", "emby-button", "listViewStyle"], function (events, globalize, dom, datefns, dfnshelper, userSettings, serverNotifications, connectionManager) { "use strict"; function getEntryHtml(entry, apiClient) { @@ -26,8 +26,7 @@ define(["events", "globalize", "dom", "datetime", "userSettings", "serverNotific html += entry.Name; html += ""; html += '
'; - var date = datetime.parseISO8601Date(entry.Date, true); - html += datetime.toLocaleString(date).toLowerCase(); + html += datefns.formatRelative(Date.parse(entry.Date), Date.parse(new Date()), { locale: dfnshelper.getLocale() }); html += "
"; html += '
'; html += entry.ShortOverview || ""; diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 3dc328f129..a4cf6edadc 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1082,11 +1082,7 @@ import 'programStyles'; if (options.showPersonRoleOrType) { if (item.Role) { - lines.push('as ' + item.Role); - } else if (item.Type) { - lines.push(globalize.translate('' + item.Type)); - } else { - lines.push(''); + lines.push(globalize.translate('PersonRole', item.Role)); } } } diff --git a/src/components/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js index b71f7bbb05..1d5c23eb1f 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -89,7 +89,7 @@ define(['loading', 'dialogHelper', 'dom', 'listViewStyle', 'emby-input', 'paper- var instruction = options.instruction ? options.instruction + "

" : ""; html += '
'; html += instruction; - html += Globalize.translate("MessageDirectoryPickerInstruction").replace("{0}", "\\\\server").replace("{1}", "\\\\192.168.1.101"); + html += Globalize.translate("MessageDirectoryPickerInstruction", "\\\\server", "\\\\192.168.1.101"); if ("bsd" === systemInfo.OperatingSystem.toLowerCase()) { html += "
"; html += "
"; @@ -163,16 +163,15 @@ define(['loading', 'dialogHelper', 'dom', 'listViewStyle', 'emby-input', 'paper- } }).catch(function(response) { if (response) { - // TODO All alerts (across the project), should use Globalize.translate() if (response.status === 404) { - alertText("The path could not be found. Please ensure the path is valid and try again."); + alertText(Globalize.translate("PathNotFound")); return Promise.reject(); } if (response.status === 500) { if (validateWriteable) { - alertText("Jellyfin Server requires write access to this folder. Please ensure write access and try again."); + alertText(Globalize.translate("WriteAccessRequired")); } else { - alertText("The path could not be found. Please ensure the path is valid and try again.") + alertText(Globalize.translate("PathNotFound")) } return Promise.reject() } diff --git a/src/components/humanedate.js b/src/components/humanedate.js deleted file mode 100644 index 26ce26d942..0000000000 --- a/src/components/humanedate.js +++ /dev/null @@ -1,74 +0,0 @@ -define(["datetime"], function (datetime) { - "use strict"; - - function humaneDate(date_str) { - var format; - var time_formats = [ - [90, "a minute"], - [3600, "minutes", 60], - [5400, "an hour"], - [86400, "hours", 3600], - [129600, "a day"], - [604800, "days", 86400], - [907200, "a week"], - [2628e3, "weeks", 604800], - [3942e3, "a month"], - [31536e3, "months", 2628e3], - [47304e3, "a year"], - [31536e5, "years", 31536e3] - ]; - var dt = new Date(); - var date = datetime.parseISO8601Date(date_str, true); - var seconds = (dt - date) / 1000.0; - var i = 0; - - if (seconds < 0) { - seconds = Math.abs(seconds); - } - // eslint-disable-next-line no-cond-assign - for (; format = time_formats[i++];) { - if (seconds < format[0]) { - if (2 == format.length) { - return format[1] + " ago"; - } - - return Math.round(seconds / format[2]) + " " + format[1] + " ago"; - } - } - - if (seconds > 47304e5) { - return Math.round(seconds / 47304e5) + " centuries ago"; - } - - return date_str; - } - - function humaneElapsed(firstDateStr, secondDateStr) { - // TODO replace this whole script with a library or something - var dateOne = new Date(firstDateStr); - var dateTwo = new Date(secondDateStr); - var delta = (dateTwo.getTime() - dateOne.getTime()) / 1e3; - var days = Math.floor(delta % 31536e3 / 86400); - var hours = Math.floor(delta % 31536e3 % 86400 / 3600); - var minutes = Math.floor(delta % 31536e3 % 86400 % 3600 / 60); - var seconds = Math.round(delta % 31536e3 % 86400 % 3600 % 60); - var elapsed = ""; - elapsed += 1 == days ? days + " day " : ""; - elapsed += days > 1 ? days + " days " : ""; - elapsed += 1 == hours ? hours + " hour " : ""; - elapsed += hours > 1 ? hours + " hours " : ""; - elapsed += 1 == minutes ? minutes + " minute " : ""; - elapsed += minutes > 1 ? minutes + " minutes " : ""; - elapsed += elapsed.length > 0 ? "and " : ""; - elapsed += 1 == seconds ? seconds + " second" : ""; - elapsed += 0 == seconds || seconds > 1 ? seconds + " seconds" : ""; - return elapsed; - } - - window.humaneDate = humaneDate; - window.humaneElapsed = humaneElapsed; - return { - humaneDate: humaneDate, - humaneElapsed: humaneElapsed - }; -}); diff --git a/src/components/imagedownloader/imagedownloader.js b/src/components/imagedownloader/imagedownloader.js index f4fcd7091f..9df083aea2 100644 --- a/src/components/imagedownloader/imagedownloader.js +++ b/src/components/imagedownloader/imagedownloader.js @@ -109,7 +109,7 @@ define(['dom', 'loading', 'apphost', 'dialogHelper', 'connectionManager', 'image html += ''; var startAtDisplay = totalRecordCount ? startIndex + 1 : 0; - html += startAtDisplay + '-' + recordsEnd + ' of ' + totalRecordCount; + html += globalize.translate("ListPaging", startAtDisplay, recordsEnd, totalRecordCount); html += ''; diff --git a/src/components/itemidentifier/itemidentifier.js b/src/components/itemidentifier/itemidentifier.js index 2a779618f2..9f89aef947 100644 --- a/src/components/itemidentifier/itemidentifier.js +++ b/src/components/itemidentifier/itemidentifier.js @@ -309,7 +309,7 @@ define(["dialogHelper", "loading", "connectionManager", "require", "globalize", fullName = idInfo.Name + " " + globalize.translate(idInfo.Type); } - var idLabel = globalize.translate("LabelDynamicExternalId").replace("{0}", fullName); + var idLabel = globalize.translate("LabelDynamicExternalId", fullName); html += ''; diff --git a/src/components/metadataeditor/metadataeditor.js b/src/components/metadataeditor/metadataeditor.js index 84b60b32a3..8a64cac7ef 100644 --- a/src/components/metadataeditor/metadataeditor.js +++ b/src/components/metadataeditor/metadataeditor.js @@ -470,7 +470,7 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi fullName = idInfo.Name + " " + globalize.translate(idInfo.Type); } - var labelText = globalize.translate("LabelDynamicExternalId").replace("{0}", fullName); + var labelText = globalize.translate('LabelDynamicExternalId', fullName); html += '
'; html += '
'; diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 2c3e45b630..c8a79a3627 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -173,15 +173,15 @@ define(['serverNotifications', 'playbackManager', 'events', 'globalize', 'requir }; if (status === 'completed') { - notification.title = globalize.translate('PackageInstallCompleted').replace('{0}', installation.Name + ' ' + installation.Version); + notification.title = globalize.translate('PackageInstallCompleted', installation.Name, installation.Version); notification.vibrate = true; } else if (status === 'cancelled') { - notification.title = globalize.translate('PackageInstallCancelled').replace('{0}', installation.Name + ' ' + installation.Version); + notification.title = globalize.translate('PackageInstallCancelled', installation.Name, installation.Version); } else if (status === 'failed') { - notification.title = globalize.translate('PackageInstallFailed').replace('{0}', installation.Name + ' ' + installation.Version); + notification.title = globalize.translate('PackageInstallFailed', installation.Name, installation.Version); notification.vibrate = true; } else if (status === 'progress') { - notification.title = globalize.translate('InstallingPackage').replace('{0}', installation.Name + ' ' + installation.Version); + notification.title = globalize.translate('InstallingPackage', installation.Name, installation.Version); notification.actions = [ diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 2057deaf6f..78f5cdca01 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -1,4 +1,4 @@ -define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globalize", "loading", "connectionManager", "playMethodHelper", "cardBuilder", "imageLoader", "components/activitylog", "scripts/imagehelper", "indicators", "humanedate", "listViewStyle", "emby-button", "flexStyles", "emby-button", "emby-itemscontainer"], function (datetime, events, itemHelper, serverNotifications, dom, globalize, loading, connectionManager, playMethodHelper, cardBuilder, imageLoader, ActivityLog, imageHelper, indicators) { +define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globalize", "date-fns", "dfnshelper", "loading", "connectionManager", "playMethodHelper", "cardBuilder", "imageLoader", "components/activitylog", "scripts/imagehelper", "indicators", "listViewStyle", "emby-button", "flexStyles", "emby-button", "emby-itemscontainer"], function (datetime, events, itemHelper, serverNotifications, dom, globalize, datefns, dfnshelper, loading, connectionManager, playMethodHelper, cardBuilder, imageLoader, ActivityLog, imageHelper, indicators) { "use strict"; function showPlaybackInfo(btn, session) { @@ -467,10 +467,11 @@ define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globa getNowPlayingName: function (session) { var imgUrl = ""; var nowPlayingItem = session.NowPlayingItem; - + // FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix + // how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences if (!nowPlayingItem) { return { - html: "Last seen " + humaneDate(session.LastActivityDate), + html: globalize.translate("LastSeen", datefns.formatDistanceToNow(Date.parse(session.LastActivityDate), dfnshelper.localeWithSuffix)), image: imgUrl }; } diff --git a/src/controllers/dashboard/plugins/add.js b/src/controllers/dashboard/plugins/add.js index 72a7134fac..a05cac461b 100644 --- a/src/controllers/dashboard/plugins/add.js +++ b/src/controllers/dashboard/plugins/add.js @@ -84,7 +84,7 @@ define(["jQuery", "loading", "libraryMenu", "globalize", "connectionManager", "e } if (installedPlugin) { - var currentVersionText = globalize.translate("MessageYouHaveVersionInstalled").replace("{0}", "" + installedPlugin.Version + ""); + var currentVersionText = globalize.translate("MessageYouHaveVersionInstalled", "" + installedPlugin.Version + ""); $("#pCurrentVersion", page).show().html(currentVersionText); } else { $("#pCurrentVersion", page).hide().html(""); diff --git a/src/controllers/dashboard/plugins/available.js b/src/controllers/dashboard/plugins/available.js index 5526bd9ade..adccfa3935 100644 --- a/src/controllers/dashboard/plugins/available.js +++ b/src/controllers/dashboard/plugins/available.js @@ -116,7 +116,7 @@ define(["loading", "libraryMenu", "globalize", "cardStyle", "emby-button", "emby return ip.Id == plugin.guid; })[0]; html += "
"; - html += installedPlugin ? globalize.translate("LabelVersionInstalled").replace("{0}", installedPlugin.Version) : " "; + html += installedPlugin ? globalize.translate("LabelVersionInstalled", installedPlugin.Version) : " "; html += "
"; html += "
"; html += "
"; diff --git a/src/controllers/dashboard/plugins/installed.js b/src/controllers/dashboard/plugins/installed.js index 026b58ce67..c381b2409e 100644 --- a/src/controllers/dashboard/plugins/installed.js +++ b/src/controllers/dashboard/plugins/installed.js @@ -2,7 +2,7 @@ define(["loading", "libraryMenu", "dom", "globalize", "cardStyle", "emby-button" "use strict"; function deletePlugin(page, uniqueid, name) { - var msg = globalize.translate("UninstallPluginConfirmation").replace("{0}", name); + var msg = globalize.translate("UninstallPluginConfirmation", name); require(["confirm"], function (confirm) { confirm({ diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtask.js b/src/controllers/dashboard/scheduledtasks/scheduledtask.js index 03eeeeb870..8a3cdf5d69 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtask.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtask.js @@ -75,17 +75,19 @@ define(["jQuery", "loading", "datetime", "dom", "globalize", "emby-input", "emby html += "
"; context.querySelector(".taskTriggers").innerHTML = html; }, + // TODO: Replace this mess with date-fns and remove datetime completely getTriggerFriendlyName: function (trigger) { if ("DailyTrigger" == trigger.Type) { - return "Daily at " + ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks); + return globalize.translate("DailyAt", ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks)); } if ("WeeklyTrigger" == trigger.Type) { - return trigger.DayOfWeek + "s at " + ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks); + // TODO: The day of week isn't localised as well + return globalize.translate("WeeklyAt", trigger.DayOfWeek, ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks)); } if ("SystemEventTrigger" == trigger.Type && "WakeFromSleep" == trigger.SystemEvent) { - return "On wake from sleep"; + return globalize.translate("OnWakeFromSleep"); } if (trigger.Type == "IntervalTrigger") { @@ -93,23 +95,23 @@ define(["jQuery", "loading", "datetime", "dom", "globalize", "emby-input", "emby var hours = trigger.IntervalTicks / 36e9; if (hours == 0.25) { - return "Every 15 minutes"; + return globalize.translate("EveryXMinutes", "15"); } if (hours == 0.5) { - return "Every 30 minutes"; + return globalize.translate("EveryXMinutes", "30"); } if (hours == 0.75) { - return "Every 45 minutes"; + return globalize.translate("EveryXMinutes", "45"); } if (hours == 1) { - return "Every hour"; + return globalize.translate("EveryHour"); } - return "Every " + hours + " hours"; + return globalize.translate("EveryXHours", hours); } if (trigger.Type == "StartupTrigger") { - return "On application startup"; + return globalize.translate("OnApplicationStartup"); } return trigger.Type; diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index 390fd17353..b91158d8bf 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -1,4 +1,4 @@ -define(["jQuery", "loading", "events", "globalize", "serverNotifications", "humanedate", "listViewStyle", "emby-button"], function($, loading, events, globalize, serverNotifications) { +define(["jQuery", "loading", "events", "globalize", "serverNotifications", "date-fns", "dfnshelper", "listViewStyle", "emby-button"], function ($, loading, events, globalize, serverNotifications, datefns, dfnshelper) { "use strict"; function reloadList(page) { @@ -66,7 +66,10 @@ define(["jQuery", "loading", "events", "globalize", "serverNotifications", "huma var html = ""; if (task.State === "Idle") { if (task.LastExecutionResult) { - html += globalize.translate("LabelScheduledTaskLastRan").replace("{0}", humaneDate(task.LastExecutionResult.EndTimeUtc)).replace("{1}", humaneElapsed(task.LastExecutionResult.StartTimeUtc, task.LastExecutionResult.EndTimeUtc)); + var endtime = Date.parse(task.LastExecutionResult.EndTimeUtc); + var starttime = Date.parse(task.LastExecutionResult.StartTimeUtc); + html += globalize.translate("LabelScheduledTaskLastRan", datefns.formatDistanceToNow(endtime, dfnshelper.localeWithSuffix), + datefns.formatDistance(starttime, endtime, dfnshelper.localeWithSuffix)); if (task.LastExecutionResult.Status === "Failed") { html += " (" + globalize.translate("LabelFailed") + ")"; } else if (task.LastExecutionResult.Status === "Cancelled") { diff --git a/src/controllers/devices.js b/src/controllers/devices.js index 3fd2be983e..8dd665f7fa 100644 --- a/src/controllers/devices.js +++ b/src/controllers/devices.js @@ -1,4 +1,4 @@ -define(["loading", "dom", "libraryMenu", "globalize", "scripts/imagehelper", "humanedate", "emby-button", "emby-itemscontainer", "cardStyle"], function (loading, dom, libraryMenu, globalize, imageHelper) { +define(["loading", "dom", "libraryMenu", "globalize", "scripts/imagehelper", "date-fns", "dfnshelper", "emby-button", "emby-itemscontainer", "cardStyle"], function (loading, dom, libraryMenu, globalize, imageHelper, datefns, dfnshelper) { "use strict"; function canDelete(deviceId) { @@ -103,7 +103,7 @@ define(["loading", "dom", "libraryMenu", "globalize", "scripts/imagehelper", "hu if (device.LastUserName) { deviceHtml += device.LastUserName; - deviceHtml += ", " + humaneDate(device.DateLastActivity); + deviceHtml += ", " + datefns.formatDistanceToNow(Date.parse(device.DateLastActivity), dfnshelper.localeWithSuffix); } deviceHtml += " "; diff --git a/src/controllers/dlnaprofile.js b/src/controllers/dlnaprofile.js index fb4cdb425e..ca0d3afdb1 100644 --- a/src/controllers/dlnaprofile.js +++ b/src/controllers/dlnaprofile.js @@ -258,14 +258,14 @@ define(["jQuery", "loading", "fnchecked", "emby-select", "emby-button", "emby-in html += "
"; html += ''; - html += "

" + Globalize.translate("ValueContainer").replace("{0}", profile.Container || allText) + "

"; + html += "

" + Globalize.translate("ValueContainer", profile.Container || allText) + "

"; if ("Video" == profile.Type) { - html += "

" + Globalize.translate("ValueVideoCodec").replace("{0}", profile.VideoCodec || allText) + "

"; - html += "

" + Globalize.translate("ValueAudioCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueVideoCodec", profile.VideoCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueAudioCodec", profile.AudioCodec || allText) + "

"; } else { if ("Audio" == profile.Type) { - html += "

" + Globalize.translate("ValueCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueCodec", profile.AudioCodec || allText) + "

"; } } @@ -319,14 +319,14 @@ define(["jQuery", "loading", "fnchecked", "emby-select", "emby-button", "emby-in html += "
"; html += ''; html += "

Protocol: " + (profile.Protocol || "Http") + "

"; - html += "

" + Globalize.translate("ValueContainer").replace("{0}", profile.Container || allText) + "

"; + html += "

" + Globalize.translate("ValueContainer", profile.Container || allText) + "

"; if ("Video" == profile.Type) { - html += "

" + Globalize.translate("ValueVideoCodec").replace("{0}", profile.VideoCodec || allText) + "

"; - html += "

" + Globalize.translate("ValueAudioCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueVideoCodec", profile.VideoCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueAudioCodec", profile.AudioCodec || allText) + "

"; } else { if ("Audio" == profile.Type) { - html += "

" + Globalize.translate("ValueCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueCodec", profile.AudioCodec || allText) + "

"; } } @@ -404,11 +404,11 @@ define(["jQuery", "loading", "fnchecked", "emby-select", "emby-button", "emby-in html += "
"; html += ''; - html += "

" + Globalize.translate("ValueContainer").replace("{0}", profile.Container || allText) + "

"; + html += "

" + Globalize.translate("ValueContainer", profile.Container || allText) + "

"; if (profile.Conditions && profile.Conditions.length) { html += "

"; - html += Globalize.translate("ValueConditions").replace("{0}", profile.Conditions.map(function (c) { + html += Globalize.translate("ValueConditions", profile.Conditions.map(function (c) { return c.Property; }).join(", ")); html += "

"; @@ -476,11 +476,11 @@ define(["jQuery", "loading", "fnchecked", "emby-select", "emby-button", "emby-in html += "
"; html += ''; - html += "

" + Globalize.translate("ValueCodec").replace("{0}", profile.Codec || allText) + "

"; + html += "

" + Globalize.translate("ValueCodec", profile.Codec || allText) + "

"; if (profile.Conditions && profile.Conditions.length) { html += "

"; - html += Globalize.translate("ValueConditions").replace("{0}", profile.Conditions.map(function (c) { + html += Globalize.translate("ValueConditions", profile.Conditions.map(function (c) { return c.Property; }).join(", ")); html += "

"; @@ -547,20 +547,20 @@ define(["jQuery", "loading", "fnchecked", "emby-select", "emby-button", "emby-in html += "
"; html += ''; - html += "

" + Globalize.translate("ValueContainer").replace("{0}", profile.Container || allText) + "

"; + html += "

" + Globalize.translate("ValueContainer", profile.Container || allText) + "

"; if ("Video" == profile.Type) { - html += "

" + Globalize.translate("ValueVideoCodec").replace("{0}", profile.VideoCodec || allText) + "

"; - html += "

" + Globalize.translate("ValueAudioCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueVideoCodec", profile.VideoCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueAudioCodec", profile.AudioCodec || allText) + "

"; } else { if ("Audio" == profile.Type) { - html += "

" + Globalize.translate("ValueCodec").replace("{0}", profile.AudioCodec || allText) + "

"; + html += "

" + Globalize.translate("ValueCodec", profile.AudioCodec || allText) + "

"; } } if (profile.Conditions && profile.Conditions.length) { html += "

"; - html += Globalize.translate("ValueConditions").replace("{0}", profile.Conditions.map(function (c) { + html += Globalize.translate("ValueConditions", profile.Conditions.map(function (c) { return c.Property; }).join(", ")); html += "

"; diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemdetailpage.js index 23a672751c..178419e284 100644 --- a/src/controllers/itemdetailpage.js +++ b/src/controllers/itemdetailpage.js @@ -591,7 +591,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti try { var birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString(); itemBirthday.classList.remove("hide"); - itemBirthday.innerHTML = globalize.translate("BirthDateValue").replace("{0}", birthday); + itemBirthday.innerHTML = globalize.translate("BirthDateValue", birthday); } catch (err) { itemBirthday.classList.add("hide"); } @@ -605,7 +605,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti try { var deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString(); itemDeathDate.classList.remove("hide"); - itemDeathDate.innerHTML = globalize.translate("DeathDateValue").replace("{0}", deathday); + itemDeathDate.innerHTML = globalize.translate("DeathDateValue", deathday); } catch (err) { itemDeathDate.classList.add("hide"); } @@ -618,7 +618,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti if ("Person" == item.Type && item.ProductionLocations && item.ProductionLocations.length) { var gmap = '
' + item.ProductionLocations[0] + ""; itemBirthLocation.classList.remove("hide"); - itemBirthLocation.innerHTML = globalize.translate("BirthPlaceValue").replace("{0}", gmap); + itemBirthLocation.innerHTML = globalize.translate("BirthPlaceValue", gmap); } else { itemBirthLocation.classList.add("hide"); } diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js index 7e19af4b9f..98e0871479 100644 --- a/src/controllers/movies/moviesrecommended.js +++ b/src/controllers/movies/moviesrecommended.js @@ -91,21 +91,21 @@ define(["events", "layoutManager", "inputManager", "userSettings", "libraryMenu" switch (recommendation.RecommendationType) { case "SimilarToRecentlyPlayed": - title = Globalize.translate("RecommendationBecauseYouWatched").replace("{0}", recommendation.BaselineItemName); + title = Globalize.translate("RecommendationBecauseYouWatched", recommendation.BaselineItemName); break; case "SimilarToLikedItem": - title = Globalize.translate("RecommendationBecauseYouLike").replace("{0}", recommendation.BaselineItemName); + title = Globalize.translate("RecommendationBecauseYouLike", recommendation.BaselineItemName); break; case "HasDirectorFromRecentlyPlayed": case "HasLikedDirector": - title = Globalize.translate("RecommendationDirectedBy").replace("{0}", recommendation.BaselineItemName); + title = Globalize.translate("RecommendationDirectedBy", recommendation.BaselineItemName); break; case "HasActorFromRecentlyPlayed": case "HasLikedActor": - title = Globalize.translate("RecommendationStarring").replace("{0}", recommendation.BaselineItemName); + title = Globalize.translate("RecommendationStarring", recommendation.BaselineItemName); break; } diff --git a/src/controllers/userprofilespage.js b/src/controllers/userprofilespage.js index 2a2387ab60..180d0e62ae 100644 --- a/src/controllers/userprofilespage.js +++ b/src/controllers/userprofilespage.js @@ -1,4 +1,4 @@ -define(["loading", "dom", "globalize", "humanedate", "paper-icon-button-light", "cardStyle", "emby-button", "indicators", "flexStyles"], function (loading, dom, globalize) { +define(["loading", "dom", "globalize", "date-fns", "dfnshelper", "paper-icon-button-light", "cardStyle", "emby-button", "indicators", "flexStyles"], function (loading, dom, globalize, datefns, dfnshelper) { "use strict"; function deleteUser(page, id) { @@ -125,10 +125,11 @@ define(["loading", "dom", "globalize", "humanedate", "paper-icon-button-light", html += "
"; return html + "
"; } - + // FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix + // how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences function getLastSeenText(lastActivityDate) { if (lastActivityDate) { - return "Last seen " + humaneDate(lastActivityDate); + return globalize.translate("LastSeen", datefns.formatDistanceToNow(Date.parse(lastActivityDate), dfnshelper.localeWithSuffix)); } return ""; diff --git a/src/scripts/dfnshelper.js b/src/scripts/dfnshelper.js new file mode 100644 index 0000000000..a593c42c0d --- /dev/null +++ b/src/scripts/dfnshelper.js @@ -0,0 +1,104 @@ +import { ar, be, bg, ca, cs, da, de, el, enGB, enUS, es, faIR, fi, fr, frCA, he, hi, hr, hu, id, it, kk, ko, lt, ms, nb, nl, pl, ptBR, pt, ro, ru, sk, sl, sv, tr, uk, vi, zhCN, zhTW } from 'date-fns/locale'; +import globalize from 'globalize'; + +export function getLocale() { + switch (globalize.getCurrentLocale()) { + case 'ar': + return ar; + case 'be-by': + return be; + case 'bg-bg': + return bg; + case 'ca': + return ca; + case 'cs': + return cs; + case 'da': + return da; + case 'de': + return de; + case 'el': + return el; + case 'en-gb': + return enGB; + case 'en-us': + return enUS; + case 'es': + return es; + case 'es-ar': + return es; + case 'es-mx': + return es; + case 'fa': + return faIR; + case 'fi': + return fi; + case 'fr': + return fr; + case 'fr-ca': + return frCA; + case 'gsw': + return de; + case 'he': + return he; + case 'hi-in': + return hi; + case 'hr': + return hr; + case 'hu': + return hu; + case 'id': + return id; + case 'it': + return it; + case 'kk': + return kk; + case 'ko': + return ko; + case 'lt-lt': + return lt; + case 'ms': + return ms; + case 'nb': + return nb; + case 'nl': + return nl; + case 'pl': + return pl; + case 'pt-br': + return ptBR; + case 'pt-pt': + return pt; + case 'ro': + return ro; + case 'ru': + return ru; + case 'sk': + return sk; + case 'sl-si': + return sl; + case 'sv': + return sv; + case 'tr': + return tr; + case 'uk': + return uk; + case 'vi': + return vi; + case 'zh-cn': + return zhCN; + case 'zh-hk': + return zhCN; + case 'zh-tw': + return zhTW; + default: + return enUS; + } +} + +export const localeWithSuffix = { addSuffix: true, locale: getLocale() }; + +export default { + getLocale: getLocale, + localeWithSuffix: localeWithSuffix +} diff --git a/src/scripts/librarybrowser.js b/src/scripts/librarybrowser.js index bd8980aed2..bc8908fe6c 100644 --- a/src/scripts/librarybrowser.js +++ b/src/scripts/librarybrowser.js @@ -83,7 +83,7 @@ define(["userSettings"], function (userSettings) { if (html += '
', showControls) { html += ''; - html += (totalRecordCount ? startIndex + 1 : 0) + "-" + recordsEnd + " of " + totalRecordCount; + html += Globalize.translate("ListPaging", (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount); html += ""; } diff --git a/src/scripts/site.js b/src/scripts/site.js index 513d0c0754..5eb3c2a346 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -574,6 +574,7 @@ var AppInfo = {}; } require(["mediaSession", "serverNotifications"]); + require(["date-fns", "date-fns/locale"]); if (!browser.tv && !browser.xboxOne) { require(["components/playback/playbackorientation"]); @@ -647,12 +648,12 @@ var AppInfo = {}; inputManager: "scripts/inputManager", datetime: "scripts/datetime", globalize: "scripts/globalize", + dfnshelper: "scripts/dfnshelper", libraryMenu: "scripts/librarymenu", playlisteditor: componentsPath + "/playlisteditor/playlisteditor", medialibrarycreator: componentsPath + "/medialibrarycreator/medialibrarycreator", medialibraryeditor: componentsPath + "/medialibraryeditor/medialibraryeditor", imageoptionseditor: componentsPath + "/imageoptionseditor/imageoptionseditor", - humanedate: componentsPath + "/humanedate", apphost: componentsPath + "/apphost", visibleinviewport: componentsPath + "/visibleinviewport", qualityoptions: componentsPath + "/qualityoptions", @@ -693,6 +694,7 @@ var AppInfo = {}; "webcomponents", "material-icons", "jellyfin-noto", + "date-fns", "page", "polyfill" ] diff --git a/src/strings/en-us.json b/src/strings/en-us.json index a62bb18aaf..008c8565f9 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -521,7 +521,7 @@ "Images": "Images", "ImportFavoriteChannelsHelp": "If enabled, only channels that are marked as favorite on the tuner device will be imported.", "ImportMissingEpisodesHelp": "If enabled, information about missing episodes will be imported into your Jellyfin database and displayed within seasons and series. This may cause significantly longer library scans.", - "InstallingPackage": "Installing {0}", + "InstallingPackage": "Installing {0} (version {1})", "InstantMix": "Instant mix", "ItemCount": "{0} items", "Items": "Items", @@ -1210,9 +1210,9 @@ "OriginalAirDateValue": "Original air date: {0}", "OtherArtist": "Other Artist", "Overview": "Overview", - "PackageInstallCancelled": "{0} installation cancelled.", - "PackageInstallCompleted": "{0} installation completed.", - "PackageInstallFailed": "{0} installation failed.", + "PackageInstallCancelled": "{0} (version {1}) installation cancelled.", + "PackageInstallCompleted": "{0} (version {1}) installation completed.", + "PackageInstallFailed": "{0} (version {1}) installation failed.", "ParentalRating": "Parental rating", "PasswordMatchError": "Password and password confirmation must match.", "PasswordResetComplete": "The password has been reset.", @@ -1480,5 +1480,17 @@ "XmlTvPathHelp": "A path to a XMLTV file. Jellyfin will read this file and periodically check it for updates. You are responsible for creating and updating the file.", "XmlTvSportsCategoriesHelp": "Programs with these categories will be displayed as sports programs. Separate multiple with '|'.", "Yes": "Yes", - "Yesterday": "Yesterday" + "Yesterday": "Yesterday", + "PathNotFound": "The path could not be found. Please ensure the path is valid and try again.", + "WriteAccessRequired": "Jellyfin Server requires write access to this folder. Please ensure write access and try again.", + "ListPaging": "{0}-{1} of {2}", + "PersonRole": "as {0}", + "LastSeen": "Last seen {0}", + "DailyAt": "Daily at {0}", + "WeeklyAt": "{0}s at {1}", + "OnWakeFromSleep": "On wake from sleep", + "EveryXMinutes": "Every {0} minutes", + "EveryHour": "Every hour", + "EveryXHours": "Every {0} hours", + "OnApplicationStartup": "On application startup" } diff --git a/src/strings/es.json b/src/strings/es.json index 06318c22d3..727fb24775 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -425,7 +425,7 @@ "Images": "Imágenes", "ImportFavoriteChannelsHelp": "Si está activado, sólo los canales guardados como favoritos en el sintonizador se importarán.", "ImportMissingEpisodesHelp": "Si está activada, la información sobre los episodios que faltan se importará en su base de datos Jellyfin y se mostrará en temporadas y series. Esto puede causar exploraciones de bibliotecas significativamente más largas.", - "InstallingPackage": "Instalando {0}", + "InstallingPackage": "Instalando {0} (versión {1})", "InstantMix": "Mix instantáneo", "ItemCount": "Elementos {0}", "Items": "Elemento", @@ -998,9 +998,9 @@ "OptionWeekly": "Semanal", "OriginalAirDateValue": "Fecha de emisión original: {0}", "Overview": "Sinopsis", - "PackageInstallCancelled": "{0} instalación cancelada.", - "PackageInstallCompleted": "{0} instalación completada.", - "PackageInstallFailed": "{0} instalación fallida.", + "PackageInstallCancelled": "{0} (versión {1}) instalación cancelada.", + "PackageInstallCompleted": "{0} (versión {1}) instalación completada.", + "PackageInstallFailed": "{0} (versión {1}) instalación fallida.", "ParentalRating": "Calificación de los padres", "PasswordMatchError": "La contraseña y la confirmación de la contraseña deben de ser iguales.", "PasswordResetComplete": "La contraseña se ha restablecido.", @@ -1477,5 +1477,17 @@ "AllowFfmpegThrottling": "Acelerar transcodificación", "ClientSettings": "Ajustes de cliente", "PreferEmbeddedEpisodeInfosOverFileNames": "Priorizar la información embebida sobre los nombres de archivos", - "PreferEmbeddedEpisodeInfosOverFileNamesHelp": "Usar la información de episodio de los metadatos embebidos si está disponible." + "PreferEmbeddedEpisodeInfosOverFileNamesHelp": "Usar la información de episodio de los metadatos embebidos si está disponible.", + "PathNotFound": "No se encontró la ruta especificada. Asegúrate de que existe e inténtalo de nuevo.", + "WriteAccessRequired": "Jellyfin requiere de permisos de escritura en esta carpeta. Asegúrate de que existe este permiso e inténtalo de nuevo.", + "ListPaging": "{0}-{1} de {2}", + "PersonRole": "como {0}", + "LastSeen": "Última vez {0}", + "DailyAt": "Diariamente a las {0}", + "WeeklyAt": "Los {0}s a las {1}", + "OnWakeFromSleep": "Al reanudar el servidor", + "EveryXMinutes": "Cada {0} minutos", + "EveryHour": "Cada hora", + "EveryXHours": "Cada {0} horas", + "OnApplicationStartup": "Al iniciarse el servidor" } diff --git a/yarn.lock b/yarn.lock index 4eea6831e1..ac4a39b6ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3107,6 +3107,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.1.tgz#197b8be1bbf5c5e6fe8bea817f0fe111820e7a12" + integrity sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w== + dateformat@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062"