diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index ea4d6a0195..fc7c6c276f 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ declare module 'jellyfin-apiclient' { + import type { Api } from '@jellyfin/sdk'; import type { AllThemeMediaResult, AuthenticationResult, @@ -70,6 +71,8 @@ declare module 'jellyfin-apiclient' { import type { ConnectionState } from 'lib/jellyfin-apiclient'; class ApiClient { + _sdk?: Api; + constructor(serverAddress: string, appName: string, appVersion: string, deviceName: string, deviceId: string); accessToken(): string; diff --git a/src/constants/eventType.ts b/src/constants/eventType.ts index b63bd78158..910cff71f4 100644 --- a/src/constants/eventType.ts +++ b/src/constants/eventType.ts @@ -3,6 +3,8 @@ */ export enum EventType { HEADER_RENDERED = 'HEADER_RENDERED', + /** The user's language has changed. */ + LANGUAGE_CHANGE = 'LANGUAGE_CHANGE', /** Items have been updated/deleted and the UI should refresh. */ REFRESH_NEEDED = 'REFRESH_NEEDED', SET_TABS = 'SET_TABS', diff --git a/src/index.jsx b/src/index.jsx index 5fce39ae3b..186190871d 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -13,6 +13,7 @@ import loading from 'components/loading/loading'; import { pluginManager } from './components/pluginManager'; import { appRouter } from './components/router/appRouter'; import { AppFeature } from 'constants/appFeature'; +import { EventType } from 'constants/eventType'; import globalize from './lib/globalize'; import { loadCoreDictionary } from 'lib/globalize/loader'; import getServerAddress from 'lib/jellyfin-apiclient/utils/getServerAddress'; @@ -23,6 +24,7 @@ import { getPlugins } from './scripts/settings/webSettings'; import taskButton from './scripts/taskbutton'; import { pageClassOn, serverAddress } from './utils/dashboard'; import Events from './utils/events'; +import { updateApiClientSdk } from 'utils/jellyfin-apiclient/compat'; import { initializeServerConnections } from './scripts/serverNotifications'; import RootApp from './RootApp'; @@ -89,6 +91,14 @@ build: ${__JF_BUILD_VERSION__}`); // Update localization on user changes Events.on(ServerConnections, 'localusersignedin', globalize.updateCurrentCulture); Events.on(ServerConnections, 'localusersignedout', globalize.updateCurrentCulture); + // Update sdk language on language changes + Events.on(document, EventType.LANGUAGE_CHANGE, () => { + ServerConnections.getCurrentApiClientAsync() + .then(updateApiClientSdk) + .catch(err => { + console.error('Failed to update ApiClient SDK on language change', err); + }); + }); // Load the font styles loadFonts(); diff --git a/src/lib/globalize/index.js b/src/lib/globalize/index.js index 1029a2280d..e4efbafe33 100644 --- a/src/lib/globalize/index.js +++ b/src/lib/globalize/index.js @@ -1,5 +1,6 @@ import isEmpty from 'lodash-es/isEmpty'; +import { EventType } from 'constants/eventType'; import { currentSettings as userSettings } from 'scripts/settings/userSettings'; import Events from 'utils/events'; import { updateLocale } from 'utils/dateFnsLocale'; @@ -13,6 +14,10 @@ export const FALLBACK_CULTURE = 'en-us'; const RTL_LANGS = ['ar', 'fa', 'ur', 'he']; const allTranslations = {}; +/** + * The current application locale/language. + * @type {string|undefined} + */ let currentCulture; let currentDateTimeCulture; let isRTL = false; @@ -21,6 +26,16 @@ export function getCurrentLocale() { return currentCulture; } +export function getLanguages() { + const culture = currentCulture || FALLBACK_CULTURE; + + const [ baseLang ] = culture.split('-'); + if (baseLang && baseLang !== culture) { + return [ culture, baseLang ]; + } + return [ culture ]; +} + export function getCurrentDateTimeLocale() { return currentDateTimeCulture; } @@ -76,6 +91,7 @@ export function getIsElementRTL(element) { } export function updateCurrentCulture() { + const lastCulture = currentCulture; let culture; try { culture = userSettings.language(); @@ -87,7 +103,10 @@ export function updateCurrentCulture() { currentCulture = normalizeLocaleName(culture); - document.documentElement.setAttribute('lang', currentCulture); + if (lastCulture !== currentCulture) { + document.documentElement.setAttribute('lang', currentCulture); + Events.trigger(document, EventType.LANGUAGE_CHANGE, [ currentCulture ]); + } let dateTimeCulture; try { @@ -299,4 +318,3 @@ export default { getIsRTL, getIsElementRTL }; - diff --git a/src/lib/jellyfin-apiclient/connectionManager.js b/src/lib/jellyfin-apiclient/connectionManager.js index 2b0f6692ea..728b19d9e7 100644 --- a/src/lib/jellyfin-apiclient/connectionManager.js +++ b/src/lib/jellyfin-apiclient/connectionManager.js @@ -6,10 +6,9 @@ import { compareVersions } from '@jellyfin/sdk/lib/utils/versioning'; import events from 'utils/events'; import { ajax } from 'utils/fetch'; -import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { toApi, updateApiClientSdk } from 'utils/jellyfin-apiclient/compat'; import { createApiClient } from 'utils/jellyfin-apiclient/createApiClient'; import { equalsIgnoreCase } from 'utils/string'; -import { safeDecodeURIComponent } from 'utils/url'; import { ConnectionMode } from './connectionMode'; import { ConnectionState } from './connectionState'; @@ -187,18 +186,7 @@ export default class ConnectionManager { apiClient.setAuthenticationInfo(result.AccessToken, result.User.Id); // Update SDK Api instance - apiClient._sdk?.update({ - basePath: apiClient.serverAddress(), - accessToken: apiClient.accessToken(), - clientInfo: { - name: safeDecodeURIComponent(apiClient.appName()), - version: safeDecodeURIComponent(apiClient.appVersion()) - }, - deviceInfo: { - name: safeDecodeURIComponent(apiClient.deviceName()), - id: safeDecodeURIComponent(apiClient.deviceId()) - } - }); + updateApiClientSdk(apiClient); afterConnected(apiClient, options); @@ -621,18 +609,7 @@ export default class ConnectionManager { result.ApiClient.setAuthenticationInfo(server.AccessToken, server.UserId); // Update SDK Api instance - result.ApiClient._sdk?.update({ - basePath: result.ApiClient.serverAddress(), - accessToken: result.ApiClient.accessToken(), - clientInfo: { - name: safeDecodeURIComponent(result.ApiClient.appName()), - version: safeDecodeURIComponent(result.ApiClient.appVersion()) - }, - deviceInfo: { - name: safeDecodeURIComponent(result.ApiClient.deviceName()), - id: safeDecodeURIComponent(result.ApiClient.deviceId()) - } - }); + updateApiClientSdk(result.ApiClient); const resolveActions = function () { resolve(result); diff --git a/src/utils/jellyfin-apiclient/compat.ts b/src/utils/jellyfin-apiclient/compat.ts index d4a8a1053f..92fa919dcd 100644 --- a/src/utils/jellyfin-apiclient/compat.ts +++ b/src/utils/jellyfin-apiclient/compat.ts @@ -1,27 +1,46 @@ -import { Api, Jellyfin } from '@jellyfin/sdk'; -import { ApiClient } from 'jellyfin-apiclient'; +import { type Api, Jellyfin, type JellyfinParameters } from '@jellyfin/sdk'; +import type { ApiClient } from 'jellyfin-apiclient'; +import { getLanguages } from 'lib/globalize'; import { safeDecodeURIComponent } from 'utils/url'; +const getParameters = (apiClient: ApiClient): JellyfinParameters => ({ + // The SDK encodes these values when creating the authorization header, + // so we need to decode them here to avoid double encoding. + clientInfo: { + name: safeDecodeURIComponent(apiClient.appName()), + version: safeDecodeURIComponent(apiClient.appVersion()) + }, + deviceInfo: { + name: safeDecodeURIComponent(apiClient.deviceName()), + id: safeDecodeURIComponent(apiClient.deviceId()), + languages: getLanguages() + } +}); + /** * Returns an SDK Api instance using the same parameters as the provided ApiClient. * @param {ApiClient} apiClient The (legacy) ApiClient. * @returns {Api} An equivalent SDK Api instance. */ export const toApi = (apiClient: ApiClient): Api => { - return (new Jellyfin({ - // The SDK encodes these values when creating the authorization header, - // so we need to decode them here to avoid double encoding. - clientInfo: { - name: safeDecodeURIComponent(apiClient.appName()), - version: safeDecodeURIComponent(apiClient.appVersion()) - }, - deviceInfo: { - name: safeDecodeURIComponent(apiClient.deviceName()), - id: safeDecodeURIComponent(apiClient.deviceId()) - } - })).createApi( - apiClient.serverAddress(), - apiClient.accessToken() - ); + return (new Jellyfin(getParameters(apiClient))) + .createApi( + apiClient.serverAddress(), + apiClient.accessToken() + ); +}; + +/** + * Updates the SDK Api instance of the provided ApiClient with the same parameters as the ApiClient. + * @param {ApiClient} apiClient The (legacy) ApiClient. + */ +export const updateApiClientSdk = (apiClient: ApiClient) => { + const { clientInfo, deviceInfo } = getParameters(apiClient); + apiClient._sdk?.update({ + basePath: apiClient.serverAddress(), + accessToken: apiClient.accessToken(), + clientInfo, + deviceInfo + }); };