Skip to content

Commit

Permalink
[OWA-79] feat(i18n): add translations (#602)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
CarinaDraganJW authored Sep 19, 2024
1 parent 80acd7f commit e116b12
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 53 deletions.
6 changes: 3 additions & 3 deletions packages/common/src/controllers/AppController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default class AppController {
return config;
};

initializeApp = async (url: string, refreshEntitlements?: () => Promise<void>) => {
initializeApp = async (url: string, language: string, refreshEntitlements?: () => Promise<void>) => {
logDebug('AppController', 'Initializing app', { url });

const settings = await this.settingsService.initialize();
Expand All @@ -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 };
Expand Down
8 changes: 4 additions & 4 deletions packages/common/src/controllers/FavoritesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ 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;

if (!favoritesList) {
return;
}

const favorites = await this.favoritesService.getFavorites(user, favoritesList);
const favorites = await this.favoritesService.getFavorites(user, favoritesList, language);

useFavoritesStore.setState({ favorites, favoritesPlaylistId: favoritesList });
};
Expand Down
8 changes: 4 additions & 4 deletions packages/common/src/controllers/WatchHistoryController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ 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;

if (!continueWatchingList) {
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),
Expand Down
82 changes: 62 additions & 20 deletions packages/common/src/services/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] };
Expand All @@ -71,7 +90,7 @@ export default class ApiService {
...custom_params,
};

return this.transformMediaItem(playlistItem, playlist);
return this.transformMediaItem({ item: playlistItem, playlist, language });
});

return playlist;
Expand All @@ -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) {
Expand All @@ -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 }),
Expand All @@ -117,15 +138,15 @@ 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
return {
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),
})),
Expand All @@ -136,7 +157,17 @@ export default class ApiService {
/**
* Get watchlist by playlistId
*/
getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise<PlaylistItem[] | undefined> => {
getMediaByWatchlist = async ({
playlistId,
mediaIds,
token,
language,
}: {
playlistId: string;
mediaIds: string[];
token?: string;
language?: string;
}): Promise<PlaylistItem[] | undefined> => {
if (!mediaIds?.length) {
return [];
}
Expand All @@ -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<PlaylistItem | undefined> => {
getMediaById = async ({
id,
token,
drmPolicyId,
language,
}: {
id: string;
token?: string;
drmPolicyId?: string;
language?: string;
}): Promise<PlaylistItem | undefined> => {
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);
Expand All @@ -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 });
};

/**
Expand Down Expand Up @@ -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<EpisodesWithPagination> => {
if (!seriesId) {
throw new Error('Series ID is required');
Expand All @@ -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);
};

/**
Expand All @@ -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;
Expand All @@ -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<AdSchedule | undefined> => {
Expand All @@ -269,7 +311,7 @@ export default class ApiService {
/**
* Get playlist by id
*/
getPlaylistById = async (id?: string, params: GetPlaylistParams = {}): Promise<Playlist | undefined> => {
getPlaylistById = async (id?: string, params: GetPlaylistParams = {}, language: string = env.APP_DEFAULT_LANGUAGE): Promise<Playlist | undefined> => {
if (!id) {
return undefined;
}
Expand All @@ -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<Playlist | undefined> => {
getContentList = async ({ id, siteId, language }: { id: string | undefined; siteId: string; language: string }): Promise<Playlist | undefined> => {
if (!id || !siteId) {
throw new Error('List ID and Site ID are required');
}
Expand All @@ -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 }) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/services/FavoriteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand Down
19 changes: 11 additions & 8 deletions packages/common/src/services/WatchHistoryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, PlaylistItem>> => {
const watchHistoryItems = await this.apiService.getMediaByWatchlist(continueWatchingList, ids);
protected getWatchHistoryItems = async (continueWatchingList: string, ids: string[], language?: string): Promise<Record<string, PlaylistItem>> => {
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<Record<string, PlaylistItem | undefined>> => {
protected getWatchHistorySeriesItems = async (
continueWatchingList: string,
ids: string[],
language?: string,
): Promise<Record<string, PlaylistItem | undefined>> => {
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) {
Expand Down Expand Up @@ -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
Expand All @@ -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) => {
Expand Down
11 changes: 8 additions & 3 deletions packages/hooks-react/src/series/useEpisodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
},
Expand Down
Loading

0 comments on commit e116b12

Please sign in to comment.