From e116b1274f63d893b920957d26172248c097cd91 Mon Sep 17 00:00:00 2001 From: Carina Dragan <92930790+CarinaDraganJW@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:39:46 +0300 Subject: [PATCH] [OWA-79] feat(i18n): add translations (#602) * feat(i18n): add custom params translations for media title and description * feat(i18n): add custom params translations for all translatable fields * feat(i18n): update getTranslatableFields function * feat(i18n): clean up getTranslatedFields function * feat(i18n): make language optional, clean up code * feat(i18n): remove hardcoded default language en and replace with env variable * feat(i18n): pass language to usePlaylist * feat(i18n): add language for useSearch * feat(i18n): add language to query key * feat(i18n): remove custom built hook, use i18n.language instead * feat(i18n): get language directly inside the hooks * feat(i18n): revert usePlaylist function signature * feat(i18n): revert usePlaylist to initial code usage * feat(i18n): revert function signature for useMedia * feat(i18n): fix order for code * feat(i18n): remove language import for useProtectedMedia * feat(i18n): use local storage for favorites, watchlist in combination with page reload * feat(i18n): use i18next instead of local storage * feat(i18n): remove reload code from language switcher * feat(i18n): fix language switch for favorites and continue watching * feat(i18n): pass language for app init as well * feat(i18n): clean up code * feat(i18n): fix test fail * feat(i18n): rename variable to better reflect function utility * feat(i18n): use i18n directly * feat(i18n): clean up functions * feat(i18n): fix getMediaById * feat(i18n): code cleanup * feat(i18n): clean up functions signatures * feat(i18n): get language directly in watchlist initialize function * feat(i18n): revert change for initI18n * feat(i18n): revert small formatting changes, irrelevant for this task * feat(i18n): revert to using useTranslation hook * feat(i18n): remove unnecessary type cast * feat(i18n): remove menu from useTranslation --- .../common/src/controllers/AppController.ts | 6 +- .../src/controllers/FavoritesController.ts | 8 +- .../src/controllers/WatchHistoryController.ts | 8 +- packages/common/src/services/ApiService.ts | 82 ++++++++++++++----- .../common/src/services/FavoriteService.ts | 4 +- .../src/services/WatchHistoryService.ts | 19 +++-- .../hooks-react/src/series/useEpisodes.ts | 11 ++- .../hooks-react/src/series/useNextEpisode.ts | 9 +- packages/hooks-react/src/useBootstrapApp.ts | 5 +- packages/hooks-react/src/useMedia.ts | 7 +- packages/hooks-react/src/usePlaylist.ts | 15 +++- packages/hooks-react/src/usePlaylists.ts | 5 ++ packages/hooks-react/src/useProtectedMedia.ts | 4 +- .../HeaderLanguageSwitcher.tsx | 8 ++ .../src/containers/Layout/Layout.test.tsx | 4 + 15 files changed, 142 insertions(+), 53 deletions(-) diff --git a/packages/common/src/controllers/AppController.ts b/packages/common/src/controllers/AppController.ts index 6d86d6d36..474d04c86 100644 --- a/packages/common/src/controllers/AppController.ts +++ b/packages/common/src/controllers/AppController.ts @@ -59,7 +59,7 @@ export default class AppController { return config; }; - initializeApp = async (url: string, refreshEntitlements?: () => Promise) => { + initializeApp = async (url: string, language: string, refreshEntitlements?: () => Promise) => { logDebug('AppController', 'Initializing app', { url }); const settings = await this.settingsService.initialize(); @@ -83,11 +83,11 @@ export default class AppController { } if (config.features?.continueWatchingList && config.content.some((el) => el.type === PersonalShelf.ContinueWatching)) { - await getModule(WatchHistoryController).initialize(); + await getModule(WatchHistoryController).initialize(language); } if (config.features?.favoritesList && config.content.some((el) => el.type === PersonalShelf.Favorites)) { - await getModule(FavoritesController).initialize(); + await getModule(FavoritesController).initialize(language); } return { config, settings, configSource }; diff --git a/packages/common/src/controllers/FavoritesController.ts b/packages/common/src/controllers/FavoritesController.ts index e871bcf63..9dc9bdf76 100644 --- a/packages/common/src/controllers/FavoritesController.ts +++ b/packages/common/src/controllers/FavoritesController.ts @@ -15,11 +15,11 @@ export default class FavoritesController { this.favoritesService = favoritesService; } - initialize = async () => { - await this.restoreFavorites(); + initialize = async (language: string) => { + await this.restoreFavorites(language); }; - restoreFavorites = async () => { + restoreFavorites = async (language?: string) => { const { user } = useAccountStore.getState(); const favoritesList = useConfigStore.getState().config.features?.favoritesList; @@ -27,7 +27,7 @@ export default class FavoritesController { return; } - const favorites = await this.favoritesService.getFavorites(user, favoritesList); + const favorites = await this.favoritesService.getFavorites(user, favoritesList, language); useFavoritesStore.setState({ favorites, favoritesPlaylistId: favoritesList }); }; diff --git a/packages/common/src/controllers/WatchHistoryController.ts b/packages/common/src/controllers/WatchHistoryController.ts index 708914ec1..e433bfa7f 100644 --- a/packages/common/src/controllers/WatchHistoryController.ts +++ b/packages/common/src/controllers/WatchHistoryController.ts @@ -15,11 +15,11 @@ export default class WatchHistoryController { this.watchHistoryService = watchHistoryService; } - initialize = async () => { - await this.restoreWatchHistory(); + initialize = async (language: string) => { + await this.restoreWatchHistory(language); }; - restoreWatchHistory = async () => { + restoreWatchHistory = async (language?: string) => { const { user } = useAccountStore.getState(); const continueWatchingList = useConfigStore.getState().config.features?.continueWatchingList; @@ -27,7 +27,7 @@ export default class WatchHistoryController { return; } - const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList); + const watchHistory = await this.watchHistoryService.getWatchHistory(user, continueWatchingList, language); useWatchHistoryStore.setState({ watchHistory: watchHistory.filter((item): item is WatchHistoryItem => !!item?.mediaid), diff --git a/packages/common/src/services/ApiService.ts b/packages/common/src/services/ApiService.ts index 6cf199d1d..0089768e2 100644 --- a/packages/common/src/services/ApiService.ts +++ b/packages/common/src/services/ApiService.ts @@ -46,10 +46,29 @@ export default class ApiService { return date ? parseISO(date) : undefined; }; + protected getTranslatedFields = (item: PlaylistItem, language?: string) => { + if (!language) { + return item; + } + + const defaultLanguage = env.APP_DEFAULT_LANGUAGE; + const transformedItem = { ...item }; + + if (language !== defaultLanguage) { + for (const [key, _] of Object.entries(transformedItem)) { + if (item[`${key}-${language}`]) { + transformedItem[key] = item[`${key}-${language}`]; + } + } + } + + return transformedItem; + }; + /** * Transform incoming content lists */ - protected transformContentList = (contentList: ContentList): Playlist => { + protected transformContentList = (contentList: ContentList, language: string): Playlist => { const { list, ...rest } = contentList; const playlist: Playlist = { ...rest, playlist: [] }; @@ -71,7 +90,7 @@ export default class ApiService { ...custom_params, }; - return this.transformMediaItem(playlistItem, playlist); + return this.transformMediaItem({ item: playlistItem, playlist, language }); }); return playlist; @@ -80,8 +99,8 @@ export default class ApiService { /** * Transform incoming playlists */ - protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string) => { - playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem(item, playlist)); + protected transformPlaylist = (playlist: Playlist, relatedMediaId?: string, language?: string) => { + playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem({ item, playlist, language })); // remove the related media item (when this is a recommendations playlist) if (relatedMediaId) { @@ -95,14 +114,16 @@ export default class ApiService { * Transform incoming media items * - Parses productId into MediaOffer[] for all cleeng offers */ - transformMediaItem = (item: PlaylistItem, playlist?: Playlist) => { + transformMediaItem = ({ item, playlist, language }: { item: PlaylistItem; playlist?: Playlist; language?: string }) => { const config = ConfigStore.getState().config; const offerKeys = Object.keys(config?.integrations)[0]; const playlistLabel = playlist?.imageLabel; const mediaId = item.mediaid; + const translatedFields = this.getTranslatedFields(item, language); const transformedMediaItem = { ...item, + ...translatedFields, cardImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CARD, playlistLabel }), channelLogoImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.CHANNEL_LOGO, playlistLabel }), backgroundImage: this.generateAlternateImageURL({ mediaId, label: ImageProperty.BACKGROUND }), @@ -117,7 +138,7 @@ export default class ApiService { return transformedMediaItem; }; - private transformEpisodes = (episodesRes: EpisodesRes, seasonNumber?: number) => { + private transformEpisodes = (episodesRes: EpisodesRes, language: string, seasonNumber?: number) => { const { episodes, page, page_limit, total } = episodesRes; // Adding images and keys for media items @@ -125,7 +146,7 @@ export default class ApiService { episodes: episodes .filter((el) => el.media_item) .map((el) => ({ - ...this.transformMediaItem(el.media_item as PlaylistItem), + ...this.transformMediaItem({ item: el.media_item as PlaylistItem, language }), seasonNumber: seasonNumber?.toString() || el.season_number?.toString() || '', episodeNumber: String(el.episode_number), })), @@ -136,7 +157,17 @@ export default class ApiService { /** * Get watchlist by playlistId */ - getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise => { + getMediaByWatchlist = async ({ + playlistId, + mediaIds, + token, + language, + }: { + playlistId: string; + mediaIds: string[]; + token?: string; + language?: string; + }): Promise => { if (!mediaIds?.length) { return []; } @@ -148,16 +179,23 @@ export default class ApiService { if (!data) throw new Error(`The data was not found using the watchlist ${playlistId}`); - return (data.playlist || []).map((item) => this.transformMediaItem(item)); + return (data.playlist || []).map((item) => this.transformMediaItem({ item, language })); }; /** * Get media by id - * @param {string} id - * @param {string} [token] - * @param {string} [drmPolicyId] */ - getMediaById = async (id: string, token?: string, drmPolicyId?: string): Promise => { + getMediaById = async ({ + id, + token, + drmPolicyId, + language, + }: { + id: string; + token?: string; + drmPolicyId?: string; + language?: string; + }): Promise => { const pathname = drmPolicyId ? `/v2/media/${id}/drm/${drmPolicyId}` : `/v2/media/${id}`; const url = createURL(`${env.APP_API_BASE_URL}${pathname}`, { token }); const response = await fetch(url); @@ -166,7 +204,7 @@ export default class ApiService { if (!mediaItem) throw new Error('MediaItem not found'); - return this.transformMediaItem(mediaItem); + return this.transformMediaItem({ item: mediaItem, language }); }; /** @@ -205,11 +243,13 @@ export default class ApiService { pageOffset, pageLimit = PAGE_LIMIT, afterId, + language, }: { seriesId: string | undefined; pageOffset?: number; pageLimit?: number; afterId?: string; + language: string; }): Promise => { if (!seriesId) { throw new Error('Series ID is required'); @@ -225,7 +265,7 @@ export default class ApiService { const response = await fetch(url); const episodesResponse = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesResponse); + return this.transformEpisodes(episodesResponse, language); }; /** @@ -236,8 +276,10 @@ export default class ApiService { seasonNumber, pageOffset, pageLimit = PAGE_LIMIT, + language, }: { seriesId: string | undefined; + language: string; seasonNumber: number; pageOffset?: number; pageLimit?: number; @@ -252,7 +294,7 @@ export default class ApiService { const response = await fetch(url); const episodesRes = (await getDataOrThrow(response)) as EpisodesRes; - return this.transformEpisodes(episodesRes, seasonNumber); + return this.transformEpisodes(episodesRes, language, seasonNumber); }; getAdSchedule = async (id: string | undefined | null): Promise => { @@ -269,7 +311,7 @@ export default class ApiService { /** * Get playlist by id */ - getPlaylistById = async (id?: string, params: GetPlaylistParams = {}): Promise => { + getPlaylistById = async (id?: string, params: GetPlaylistParams = {}, language: string = env.APP_DEFAULT_LANGUAGE): Promise => { if (!id) { return undefined; } @@ -279,10 +321,10 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as Playlist; - return this.transformPlaylist(data, params.related_media_id); + return this.transformPlaylist(data, params.related_media_id, language); }; - getContentList = async ({ id, siteId }: { id: string | undefined; siteId: string }): Promise => { + getContentList = async ({ id, siteId, language }: { id: string | undefined; siteId: string; language: string }): Promise => { if (!id || !siteId) { throw new Error('List ID and Site ID are required'); } @@ -292,7 +334,7 @@ export default class ApiService { const response = await fetch(url); const data = (await getDataOrThrow(response)) as ContentList; - return this.transformContentList(data); + return this.transformContentList(data, language); }; getContentSearch = async ({ siteId, params }: { siteId: string; params: GetContentSearchParams }) => { diff --git a/packages/common/src/services/FavoriteService.ts b/packages/common/src/services/FavoriteService.ts index b35be09b5..b6eb968f6 100644 --- a/packages/common/src/services/FavoriteService.ts +++ b/packages/common/src/services/FavoriteService.ts @@ -57,7 +57,7 @@ export default class FavoriteService { return this.validateFavorites(favorites); } - getFavorites = async (user: Customer | null, favoritesList: string) => { + getFavorites = async (user: Customer | null, favoritesList: string, language?: string) => { const savedItems = user ? await this.getFavoritesFromAccount(user) : await this.getFavoritesFromStorage(); const mediaIds = savedItems.map(({ mediaid }) => mediaid); @@ -66,7 +66,7 @@ export default class FavoriteService { } try { - const playlistItems = await this.apiService.getMediaByWatchlist(favoritesList, mediaIds); + const playlistItems = await this.apiService.getMediaByWatchlist({ playlistId: favoritesList, mediaIds, language }); return (playlistItems || []).map((item) => this.createFavorite(item)); } catch (error: unknown) { diff --git a/packages/common/src/services/WatchHistoryService.ts b/packages/common/src/services/WatchHistoryService.ts index 80ec9d56a..c8beaa396 100644 --- a/packages/common/src/services/WatchHistoryService.ts +++ b/packages/common/src/services/WatchHistoryService.ts @@ -39,22 +39,25 @@ export default class WatchHistoryService { } // Retrieve watch history media items info using a provided watch list - protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[]): Promise> => { - const watchHistoryItems = await this.apiService.getMediaByWatchlist(continueWatchingList, ids); + protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[], language?: string): Promise> => { + const watchHistoryItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: ids, language }); const watchHistoryItemsDict = Object.fromEntries((watchHistoryItems || []).map((item) => [item.mediaid, item])); return watchHistoryItemsDict; }; // We store separate episodes in the watch history and to show series card in the Continue Watching shelf we need to get their parent media items - protected getWatchHistorySeriesItems = async (continueWatchingList: string, ids: string[]): Promise> => { + protected getWatchHistorySeriesItems = async ( + continueWatchingList: string, + ids: string[], + language?: string, + ): Promise> => { const mediaWithSeries = await this.apiService.getSeriesByMediaIds(ids); const seriesIds = Object.keys(mediaWithSeries || {}) .map((key) => mediaWithSeries?.[key]?.[0]?.series_id) .filter(Boolean) as string[]; const uniqueSerieIds = [...new Set(seriesIds)]; - - const seriesItems = await this.apiService.getMediaByWatchlist(continueWatchingList, uniqueSerieIds); + const seriesItems = await this.apiService.getMediaByWatchlist({ playlistId: continueWatchingList, mediaIds: uniqueSerieIds, language }); const seriesItemsDict = Object.keys(mediaWithSeries || {}).reduce((acc, key) => { const seriesItemId = mediaWithSeries?.[key]?.[0]?.series_id; if (seriesItemId) { @@ -86,7 +89,7 @@ export default class WatchHistoryService { return this.validateWatchHistory(history); } - getWatchHistory = async (user: Customer | null, continueWatchingList: string) => { + getWatchHistory = async (user: Customer | null, continueWatchingList: string, language?: string) => { const savedItems = user ? await this.getWatchHistoryFromAccount(user) : await this.getWatchHistoryFromStorage(); // When item is an episode of the new flow -> show the card as a series one, but keep episode to redirect in a right way @@ -97,8 +100,8 @@ export default class WatchHistoryService { } try { - const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids); - const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids); + const watchHistoryItems = await this.getWatchHistoryItems(continueWatchingList, ids, language); + const seriesItems = await this.getWatchHistorySeriesItems(continueWatchingList, ids, language); return savedItems .map((item) => { diff --git a/packages/hooks-react/src/series/useEpisodes.ts b/packages/hooks-react/src/series/useEpisodes.ts index 7d91fa9e2..76a29aa93 100644 --- a/packages/hooks-react/src/series/useEpisodes.ts +++ b/packages/hooks-react/src/series/useEpisodes.ts @@ -4,6 +4,7 @@ import type { Pagination } from '@jwp/ott-common/types/pagination'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; const getNextPageParam = (pagination: Pagination) => { const { page, page_limit, total } = pagination; @@ -28,22 +29,26 @@ export const useEpisodes = ( } => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { data, fetchNextPage, isLoading, hasNextPage = false, } = useInfiniteQuery( - [seriesId, seasonNumber], + [seriesId, seasonNumber, language], async ({ pageParam = 0 }) => { if (Number(seasonNumber)) { // Get episodes from a selected season using pagination - const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam }); + const season = await apiService.getSeasonWithEpisodes({ seriesId, seasonNumber: Number(seasonNumber), pageOffset: pageParam, language }); return { pagination: season.pagination, episodes: season.episodes }; } else { // Get episodes from a selected series using pagination - const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam }); + const data = await apiService.getEpisodes({ seriesId, pageOffset: pageParam, language }); return data; } }, diff --git a/packages/hooks-react/src/series/useNextEpisode.ts b/packages/hooks-react/src/series/useNextEpisode.ts index 2a33d573c..ea97a0c3f 100644 --- a/packages/hooks-react/src/series/useNextEpisode.ts +++ b/packages/hooks-react/src/series/useNextEpisode.ts @@ -3,14 +3,19 @@ import type { Series } from '@jwp/ott-common/types/series'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; +import { useTranslation } from 'react-i18next'; export const useNextEpisode = ({ series, episodeId }: { series: Series | undefined; episodeId: string | undefined }) => { const apiService = getModule(ApiService); + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const { isLoading, data } = useQuery( - ['next-episode', series?.series_id, episodeId], + ['next-episode', series?.series_id, episodeId, language], async () => { - const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId }); + const item = await apiService.getEpisodes({ seriesId: series?.series_id, pageLimit: 1, afterId: episodeId, language }); return item?.episodes?.[0]; }, diff --git a/packages/hooks-react/src/useBootstrapApp.ts b/packages/hooks-react/src/useBootstrapApp.ts index 6c366dc21..ad85e2564 100644 --- a/packages/hooks-react/src/useBootstrapApp.ts +++ b/packages/hooks-react/src/useBootstrapApp.ts @@ -6,6 +6,7 @@ import AppController from '@jwp/ott-common/src/controllers/AppController'; import type { AppError } from '@jwp/ott-common/src/utils/error'; import { CACHE_TIME, STALE_TIME } from '@jwp/ott-common/src/constants'; import { logDebug } from '@jwp/ott-common/src/logger'; +import { useTranslation } from 'react-i18next'; const applicationController = getModule(AppController); @@ -19,11 +20,13 @@ export type OnReadyCallback = (config: Config | undefined) => void; export const useBootstrapApp = (url: string, onReady: OnReadyCallback) => { const queryClient = useQueryClient(); + const { i18n } = useTranslation(); + const refreshEntitlements = () => queryClient.invalidateQueries({ queryKey: ['entitlements'] }); const { data, isLoading, error, isSuccess, refetch } = useQuery( 'config-init', - () => applicationController.initializeApp(url, refreshEntitlements), + () => applicationController.initializeApp(url, i18n.language, refreshEntitlements), { refetchInterval: false, retry: 1, diff --git a/packages/hooks-react/src/useMedia.ts b/packages/hooks-react/src/useMedia.ts index a3cd68aa3..78de95e3d 100644 --- a/packages/hooks-react/src/useMedia.ts +++ b/packages/hooks-react/src/useMedia.ts @@ -3,13 +3,18 @@ import type { PlaylistItem } from '@jwp/ott-common/types/playlist'; import ApiService from '@jwp/ott-common/src/services/ApiService'; import { getModule } from '@jwp/ott-common/src/modules/container'; import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; +import { useTranslation } from 'react-i18next'; export type UseMediaResult = UseBaseQueryResult; export default function useMedia(mediaId: string, enabled: boolean = true): UseMediaResult { const apiService = getModule(ApiService); - return useQuery(['media', mediaId], () => apiService.getMediaById(mediaId), { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + + return useQuery(['media', mediaId, language], () => apiService.getMediaById({ id: mediaId, language }), { enabled: !!mediaId && enabled, refetchInterval: (data, _) => { if (!data) return false; diff --git a/packages/hooks-react/src/usePlaylist.ts b/packages/hooks-react/src/usePlaylist.ts index 5e8ed77b5..6768653f7 100644 --- a/packages/hooks-react/src/usePlaylist.ts +++ b/packages/hooks-react/src/usePlaylist.ts @@ -9,6 +9,7 @@ import type { ApiError } from '@jwp/ott-common/src/utils/api'; import type { AppMenuType } from '@jwp/ott-common/types/config'; import { APP_CONFIG_ITEM_TYPE } from '@jwp/ott-common/src/constants'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useTranslation } from 'react-i18next'; const placeholderData = generatePlaylistPlaceholder(30); @@ -20,6 +21,7 @@ export const getPlaylistQueryOptions = ({ usePlaceholderData, params = {}, queryClient, + language, }: { type: AppMenuType; contentId: string | undefined; @@ -28,15 +30,16 @@ export const getPlaylistQueryOptions = ({ queryClient: QueryClient; usePlaceholderData?: boolean; params?: GetPlaylistParams; + language: string; }) => { const apiService = getModule(ApiService); return { enabled: !!contentId && enabled, - queryKey: ['playlist', type, contentId, params], + queryKey: ['playlist', type, contentId, params, language], queryFn: async () => { if (type === APP_CONFIG_ITEM_TYPE.playlist) { - const playlist = await apiService.getPlaylistById(contentId, params); + const playlist = await apiService.getPlaylistById(contentId, params, language); // This pre-caches all playlist items and makes navigating a lot faster. playlist?.playlist?.forEach((playlistItem) => { @@ -45,7 +48,7 @@ export const getPlaylistQueryOptions = ({ return playlist; } else if (type === APP_CONFIG_ITEM_TYPE.content_list) { - const contentList = await apiService.getContentList({ siteId, id: contentId }); + const contentList = await apiService.getContentList({ siteId, id: contentId, language }); return contentList; } @@ -69,10 +72,14 @@ export default function usePlaylist( usePlaceholderData: boolean = true, type: AppMenuType = APP_CONFIG_ITEM_TYPE.playlist, ) { + // Determine currently selected language + const { i18n } = useTranslation(); + const language = i18n.language; + const queryClient = useQueryClient(); const siteId = useConfigStore((state) => state.config.siteId); - const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData }); + const queryOptions = getPlaylistQueryOptions({ type, contentId, siteId, params, queryClient, enabled, usePlaceholderData, language }); return useQuery(queryOptions); } diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts index 14e658246..b3824ac9b 100644 --- a/packages/hooks-react/src/usePlaylists.ts +++ b/packages/hooks-react/src/usePlaylists.ts @@ -5,6 +5,7 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import type { Content, AppContentType, AppMenuType } from '@jwp/ott-common/types/config'; import type { Playlist } from '@jwp/ott-common/types/playlist'; import { useQueries, useQueryClient } from 'react-query'; +import { useTranslation } from 'react-i18next'; import { getPlaylistQueryOptions } from './usePlaylist'; @@ -25,6 +26,9 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef const favorites = useFavoritesStore((state) => state.getPlaylist()); const watchHistory = useWatchHistoryStore((state) => state.getPlaylist()); + // Determine currently selected language + const { i18n } = useTranslation(); + const playlistQueries = useQueries( content.map(({ contentId, type }, index) => { if (isPlaylistType(type)) { @@ -36,6 +40,7 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef queryClient, usePlaceholderData: true, params: { page_limit }, + language: i18n.language, }); } diff --git a/packages/hooks-react/src/useProtectedMedia.ts b/packages/hooks-react/src/useProtectedMedia.ts index c75649b95..f034e3378 100644 --- a/packages/hooks-react/src/useProtectedMedia.ts +++ b/packages/hooks-react/src/useProtectedMedia.ts @@ -7,7 +7,9 @@ import useContentProtection from './useContentProtection'; export default function useProtectedMedia(item: PlaylistItem) { const apiService = getModule(ApiService); - const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => apiService.getMediaById(item.mediaid, token, drmPolicyId)); + const contentProtectionQuery = useContentProtection('media', item.mediaid, (token, drmPolicyId) => + apiService.getMediaById({ id: item.mediaid, token, drmPolicyId }), + ); const { isLoading, data: isGeoBlocked } = useQuery( ['media', 'geo', item.mediaid], diff --git a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx index 1513d9089..15ce22af5 100644 --- a/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx +++ b/packages/ui-react/src/containers/HeaderLanguageSwitcher/HeaderLanguageSwitcher.tsx @@ -2,6 +2,9 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; import { useUIStore } from '@jwp/ott-common/src/stores/UIStore'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import LanguageMenu from '../../components/LanguageMenu/LanguageMenu'; @@ -10,11 +13,16 @@ const HeaderLanguageSwitcher = () => { const supportedLanguages = useConfigStore((state) => state.supportedLanguages); const languageMenuOpen = useUIStore((state) => state.languageMenuOpen); + const favoritesController = getModule(FavoritesController); + const watchlistController = getModule(WatchHistoryController); + const openLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: true }), []); const closeLanguageMenu = useCallback(() => useUIStore.setState({ languageMenuOpen: false }), []); const languageClickHandler = (code: string) => { i18n.changeLanguage(code); + favoritesController.restoreFavorites(code); + watchlistController.restoreWatchHistory(code); }; const currentLanguage = useMemo(() => supportedLanguages.find(({ code }) => code === i18n.language), [i18n.language, supportedLanguages]); diff --git a/packages/ui-react/src/containers/Layout/Layout.test.tsx b/packages/ui-react/src/containers/Layout/Layout.test.tsx index 6ba246c09..bccd5de3f 100644 --- a/packages/ui-react/src/containers/Layout/Layout.test.tsx +++ b/packages/ui-react/src/containers/Layout/Layout.test.tsx @@ -3,6 +3,8 @@ import { axe } from 'vitest-axe'; import AccountController from '@jwp/ott-common/src/controllers/AccountController'; import { mockService } from '@jwp/ott-common/test/mockService'; import { DEFAULT_FEATURES } from '@jwp/ott-common/src/constants'; +import FavoritesController from '@jwp/ott-common/src/controllers/FavoritesController'; +import WatchHistoryController from '@jwp/ott-common/src/controllers/WatchHistoryController'; import { renderWithRouter } from '../../../test/utils'; @@ -11,6 +13,8 @@ import Layout from './Layout'; describe('', () => { beforeEach(() => { mockService(AccountController, { getFeatures: () => DEFAULT_FEATURES }); + mockService(FavoritesController, {}); + mockService(WatchHistoryController, {}); }); test('renders layout', () => {