Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OWA-79] feat(i18n): add translations #602

Merged
merged 40 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9118a3d
feat(i18n): add custom params translations for media title and descri…
CarinaDraganJW Aug 28, 2024
2145f9c
feat(i18n): add custom params translations for all translatable fields
CarinaDraganJW Aug 28, 2024
0d36a79
feat(i18n): update getTranslatableFields function
CarinaDraganJW Aug 29, 2024
be584ec
feat(i18n): clean up getTranslatedFields function
CarinaDraganJW Aug 29, 2024
c2f215c
feat(i18n): make language optional, clean up code
CarinaDraganJW Aug 29, 2024
bf79832
feat(i18n): remove hardcoded default language en and replace with env…
CarinaDraganJW Aug 30, 2024
c33dd23
feat(i18n): pass language to usePlaylist
CarinaDraganJW Aug 30, 2024
3775189
feat(i18n): add language for useSearch
CarinaDraganJW Aug 30, 2024
32e8440
feat(i18n): add language to query key
CarinaDraganJW Sep 2, 2024
8884221
feat(i18n): remove custom built hook, use i18n.language instead
CarinaDraganJW Sep 3, 2024
9e9f5b1
feat(i18n): get language directly inside the hooks
CarinaDraganJW Sep 3, 2024
7ad6f6c
feat(i18n): revert usePlaylist function signature
CarinaDraganJW Sep 3, 2024
849c05e
feat(i18n): revert usePlaylist to initial code usage
CarinaDraganJW Sep 3, 2024
d4b46fb
feat(i18n): revert function signature for useMedia
CarinaDraganJW Sep 3, 2024
8004218
feat(i18n): fix order for code
CarinaDraganJW Sep 3, 2024
a64aae1
feat(i18n): remove language import for useProtectedMedia
CarinaDraganJW Sep 3, 2024
baa0a25
feat(i18n): use local storage for favorites, watchlist in combination…
CarinaDraganJW Sep 4, 2024
dffc34e
feat(i18n): use i18next instead of local storage
CarinaDraganJW Sep 4, 2024
65b17b6
feat(i18n): remove reload code from language switcher
CarinaDraganJW Sep 5, 2024
4b90462
feat(i18n): fix language switch for favorites and continue watching
CarinaDraganJW Sep 5, 2024
2d456b1
feat(i18n): fix merge conflicts
CarinaDraganJW Sep 5, 2024
971aa52
feat(i18n): pass language for app init as well
CarinaDraganJW Sep 5, 2024
dd622a1
feat(i18n): clean up code
CarinaDraganJW Sep 5, 2024
18cd28a
feat(i18n): update branch with develop
CarinaDraganJW Sep 5, 2024
eb99c6f
feat(i18n): update with develop
CarinaDraganJW Sep 6, 2024
6e381b8
feat(i18n): fix test fail
CarinaDraganJW Sep 6, 2024
e8eb48e
feat(i18n): rename variable to better reflect function utility
CarinaDraganJW Sep 9, 2024
7118958
feat(i18n): use i18n directly
CarinaDraganJW Sep 9, 2024
36a8aca
feat(i18n): clean up functions
CarinaDraganJW Sep 9, 2024
39efb7d
feat(i18n): fix getMediaById
CarinaDraganJW Sep 9, 2024
8095038
feat(i18n): code cleanup
CarinaDraganJW Sep 9, 2024
47bc62d
feat(i18n): clean up functions signatures
CarinaDraganJW Sep 9, 2024
6ab4278
feat(i18n): get language directly in watchlist initialize function
CarinaDraganJW Sep 9, 2024
d6ef98d
feat(i18n): revert change for initI18n
CarinaDraganJW Sep 9, 2024
7fe961a
feat(i18n): revert small formatting changes, irrelevant for this task
CarinaDraganJW Sep 9, 2024
3233a65
feat(i18n): revert to using useTranslation hook
CarinaDraganJW Sep 12, 2024
8abf0f6
Merge branch 'develop' into feature/OWA-79.add-translations
CarinaDraganJW Sep 16, 2024
709f1e6
feat(i18n): remove unnecessary type cast
CarinaDraganJW Sep 17, 2024
27dbbc9
feat(i18n): fix merge conflicts
CarinaDraganJW Sep 19, 2024
bba1a37
feat(i18n): remove menu from useTranslation
CarinaDraganJW Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/common/src/controllers/FavoritesController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import i18next from 'i18next';
import { injectable } from 'inversify';
import { getI18n } from 'react-i18next';

import FavoriteService from '../services/FavoriteService';
import type { PlaylistItem } from '../../types/playlist';
Expand All @@ -16,18 +17,19 @@ export default class FavoritesController {
}

initialize = async () => {
await this.restoreFavorites();
const i18n = getI18n();
await this.restoreFavorites(i18n.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: 5 additions & 3 deletions packages/common/src/controllers/WatchHistoryController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { injectable } from 'inversify';
import { getI18n } from 'react-i18next';

import WatchHistoryService from '../services/WatchHistoryService';
import type { PlaylistItem } from '../../types/playlist';
Expand All @@ -16,18 +17,19 @@ export default class WatchHistoryController {
}

initialize = async () => {
await this.restoreWatchHistory();
const i18n = getI18n();
await this.restoreWatchHistory(i18n.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}`] as string;
}
}
}

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 { getI18n } 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 = getI18n();
const language = i18n.language;
CarinaDraganJW marked this conversation as resolved.
Show resolved Hide resolved

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
Loading