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 11 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
59 changes: 42 additions & 17 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;
};

private getTranslatedFields = (item: PlaylistItem, language?: string) => {
CarinaDraganJW marked this conversation as resolved.
Show resolved Hide resolved
if (!language) {
return item;
}

const defaultLanguage = env.APP_DEFAULT_LANGUAGE;
const newItem = { ...item };

if (language !== defaultLanguage) {
for (const [key, _] of Object.entries(newItem)) {
if (item[`${key}-${language}`]) {
newItem[key] = item[`${key}-${language}`] as string;
}
}
}

return newItem;
};

/**
* Transform incoming content lists
*/
private transformContentList = (contentList: ContentList): Playlist => {
private 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
*/
private transformPlaylist = (playlist: Playlist, relatedMediaId?: string) => {
playlist.playlist = playlist.playlist.map((item) => this.transformMediaItem(item, playlist));
private 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,7 @@ export default class ApiService {
/**
* Get watchlist by playlistId
*/
getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise<PlaylistItem[] | undefined> => {
getMediaByWatchlist = async (playlistId: string, mediaIds: string[], language?: string, token?: string): Promise<PlaylistItem[] | undefined> => {
if (!mediaIds?.length) {
return [];
}
Expand All @@ -148,7 +169,7 @@ 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 }));
};

/**
Expand All @@ -157,7 +178,7 @@ export default class ApiService {
* @param {string} [token]
* @param {string} [drmPolicyId]
*/
getMediaById = async (id: string, token?: string, drmPolicyId?: string): Promise<PlaylistItem | undefined> => {
getMediaById = async (id: string, language: string, token?: string, drmPolicyId?: 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 +187,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 +226,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 +248,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 +259,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 +277,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 +294,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 +304,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 +317,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
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('menu');
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
9 changes: 7 additions & 2 deletions packages/hooks-react/src/series/useNextEpisode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('menu');
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 });
CarinaDraganJW marked this conversation as resolved.
Show resolved Hide resolved

return item?.episodes?.[0];
},
Expand Down
9 changes: 7 additions & 2 deletions packages/hooks-react/src/useMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData = PlaylistItem, TError = unknown> = UseBaseQueryResult<TData, TError>;

export default function useMedia(mediaId: string, enabled: boolean = true): UseMediaResult {
export default function useMedia({ mediaId, enabled = true }: { mediaId: string; enabled?: boolean }): UseMediaResult {
CarinaDraganJW marked this conversation as resolved.
Show resolved Hide resolved
const apiService = getModule(ApiService);

return useQuery(['media', mediaId], () => apiService.getMediaById(mediaId), {
// Determine currently selected language
const { i18n } = useTranslation('menu');
const language = i18n.language;

return useQuery(['media', mediaId, language], () => apiService.getMediaById(mediaId, language), {
enabled: !!mediaId && enabled,
refetchInterval: (data, _) => {
if (!data) return false;
Expand Down
35 changes: 24 additions & 11 deletions packages/hooks-react/src/usePlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ApiError } from '@jwp/ott-common/src/utils/api';
import type { PlaylistMenuType } from '@jwp/ott-common/types/config';
import { PLAYLIST_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);

Expand All @@ -20,6 +21,7 @@ export const getPlaylistQueryOptions = ({
usePlaceholderData,
params = {},
queryClient,
language,
}: {
type: PlaylistMenuType;
contentId: string | undefined;
Expand All @@ -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],
queryKey: ['playlist', type, contentId, language],
queryFn: async () => {
if (type === PLAYLIST_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) => {
Expand All @@ -45,7 +48,7 @@ export const getPlaylistQueryOptions = ({

return playlist;
} else if (type === PLAYLIST_TYPE.content_list) {
const contentList = await apiService.getContentList({ siteId, id: contentId });
const contentList = await apiService.getContentList({ siteId, id: contentId, language });

return contentList;
}
Expand All @@ -62,17 +65,27 @@ export const getPlaylistQueryOptions = ({
};
};

export default function usePlaylist(
contentId?: string,
params: GetPlaylistParams = {},
enabled: boolean = true,
usePlaceholderData: boolean = true,
type: PlaylistMenuType = PLAYLIST_TYPE.playlist,
) {
export default function usePlaylist({
contentId,
params = {},
enabled = true,
usePlaceholderData = true,
type = PLAYLIST_TYPE.playlist,
}: {
contentId?: string;
params?: GetPlaylistParams;
enabled?: boolean;
usePlaceholderData?: boolean;
type?: PlaylistMenuType;
}) {
CarinaDraganJW marked this conversation as resolved.
Show resolved Hide resolved
// Determine currently selected language
const { i18n } = useTranslation('menu');
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<Playlist | undefined, ApiError>(queryOptions);
}
6 changes: 6 additions & 0 deletions packages/hooks-react/src/usePlaylists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
import type { Content, PlaylistMenuType, PlaylistType } 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';

Expand All @@ -25,6 +26,10 @@ 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('menu');
AntonLantukh marked this conversation as resolved.
Show resolved Hide resolved
const language = i18n.language;

const playlistQueries = useQueries(
content.map(({ contentId, type }, index) => {
if (isPlaylistType(type)) {
Expand All @@ -36,6 +41,7 @@ const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undef
queryClient,
usePlaceholderData: true,
params: { page_limit },
language,
});
}

Expand Down
Loading
Loading