Repository: JesusFilm/core
Pull request #6636: feat: watch video text translations
Author: login: csiyang
Created: 2025-05-14T04:40:13Z | Merged: 2025-05-22T21:38:28Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a language switcher dialog accessible from the app bar for changing the app's language.
  - Introduced a full-screen modal for language selection with localized language names.
  - Enhanced localization by making user-facing text in dialogs, menus, and video components translatable.
  - Improved video content fetching to include language-specific data based on user locale.

- **Improvements**
  - Updated UI to display the most recent titles, images, and descriptions for videos and containers.
  - Explicitly set English language for certain navigation and label elements in header and footer.

- **Bug Fixes**
  - Increased robustness in displaying titles and descriptions, preventing errors from missing or undefined data.

- **Tests**
  - Added tests for the language switcher dialog and locale-to-language ID mapping function.
  - Updated video content and container page tests to align with new data handling.

- **Documentation**
  - Added Storybook stories for the language switcher dialog to support interactive documentation.

- **Chores**
  - Expanded English localization with new keys for language selection, downloading, sharing, and contact UI text.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/watch/__generated__/GetVideoContainerPart2.ts b/apps/watch/__generated__/GetVideoContainerPart2.ts
index 1067269053b..ec51370d092 100644
--- a/apps/watch/__generated__/GetVideoContainerPart2.ts
+++ b/apps/watch/__generated__/GetVideoContainerPart2.ts
@@ -32,6 +32,7 @@ export interface GetVideoContainerPart2_container_description {
 export interface GetVideoContainerPart2_container_studyQuestions {
   __typename: "VideoStudyQuestion";
   value: string;
+  primary: boolean;
 }
 
 export interface GetVideoContainerPart2_container_title {
diff --git a/apps/watch/__generated__/GetVideoContent.ts b/apps/watch/__generated__/GetVideoContent.ts
index 45d39e0f761..5805c7362bc 100644
--- a/apps/watch/__generated__/GetVideoContent.ts
+++ b/apps/watch/__generated__/GetVideoContent.ts
@@ -32,6 +32,7 @@ export interface GetVideoContent_content_description {
 export interface GetVideoContent_content_studyQuestions {
   __typename: "VideoStudyQuestion";
   value: string;
+  primary: boolean;
 }
 
 export interface GetVideoContent_content_title {
diff --git a/apps/watch/__generated__/GetVideoContentPart3.ts b/apps/watch/__generated__/GetVideoContentPart3.ts
index fef938aa2ef..ae09f12ecfc 100644
--- a/apps/watch/__generated__/GetVideoContentPart3.ts
+++ b/apps/watch/__generated__/GetVideoContentPart3.ts
@@ -32,6 +32,7 @@ export interface GetVideoContentPart3_content_description {
 export interface GetVideoContentPart3_content_studyQuestions {
   __typename: "VideoStudyQuestion";
   value: string;
+  primary: boolean;
 }
 
 export interface GetVideoContentPart3_content_title {
diff --git a/apps/watch/__generated__/GetVideosForTestData.ts b/apps/watch/__generated__/GetVideosForTestData.ts
index 20b57a63115..8c34595e2cc 100644
--- a/apps/watch/__generated__/GetVideosForTestData.ts
+++ b/apps/watch/__generated__/GetVideosForTestData.ts
@@ -32,6 +32,7 @@ export interface GetVideosForTestData_videos_description {
 export interface GetVideosForTestData_videos_studyQuestions {
   __typename: "VideoStudyQuestion";
   value: string;
+  primary: boolean;
 }
 
 export interface GetVideosForTestData_videos_title {
diff --git a/apps/watch/__generated__/VideoContentFields.ts b/apps/watch/__generated__/VideoContentFields.ts
index 1209730ab4d..22b2677f769 100644
--- a/apps/watch/__generated__/VideoContentFields.ts
+++ b/apps/watch/__generated__/VideoContentFields.ts
@@ -32,6 +32,7 @@ export interface VideoContentFields_description {
 export interface VideoContentFields_studyQuestions {
   __typename: "VideoStudyQuestion";
   value: string;
+  primary: boolean;
 }
 
 export interface VideoContentFields_title {
diff --git a/apps/watch/pages/watch/[part1]/[part2].tsx b/apps/watch/pages/watch/[part1]/[part2].tsx
index 9690fdf0f36..dbf16647811 100644
--- a/apps/watch/pages/watch/[part1]/[part2].tsx
+++ b/apps/watch/pages/watch/[part1]/[part2].tsx
@@ -13,6 +13,7 @@ import type { VideoContentFields } from '../../../__generated__/VideoContentFiel
 import i18nConfig from '../../../next-i18next.config'
 import { createApolloClient } from '../../../src/libs/apolloClient'
 import { getFlags } from '../../../src/libs/getFlags'
+import { getLanguageIdFromLocale } from '../../../src/libs/getLanguageIdFromLocale'
 import { LanguageProvider } from '../../../src/libs/languageContext/LanguageContext'
 import { slugMap } from '../../../src/libs/slugMap'
 import { VIDEO_CONTENT_FIELDS } from '../../../src/libs/videoContentFields'
@@ -101,7 +102,8 @@ export const getStaticProps: GetStaticProps<Part2PageProps> = async (
     >({
       query: GET_VIDEO_CONTENT,
       variables: {
-        id: `${contentId}/${languageId}`
+        id: `${contentId}/${languageId}`,
+        languageId: getLanguageIdFromLocale(context.locale)
       }
     })
     if (data.content == null) {
diff --git a/apps/watch/pages/watch/[part1]/[part2]/[part3].tsx b/apps/watch/pages/watch/[part1]/[part2]/[part3].tsx
index 79dde16d59f..715b6c4e689 100644
--- a/apps/watch/pages/watch/[part1]/[part2]/[part3].tsx
+++ b/apps/watch/pages/watch/[part1]/[part2]/[part3].tsx
@@ -20,6 +20,7 @@ import i18nConfig from '../../../../next-i18next.config'
 import { VideoContentPage } from '../../../../src/components/VideoContentPage'
 import { createApolloClient } from '../../../../src/libs/apolloClient'
 import { getFlags } from '../../../../src/libs/getFlags'
+import { getLanguageIdFromLocale } from '../../../../src/libs/getLanguageIdFromLocale'
 import { LanguageProvider } from '../../../../src/libs/languageContext/LanguageContext'
 import { slugMap } from '../../../../src/libs/slugMap'
 import { VIDEO_CONTENT_FIELDS } from '../../../../src/libs/videoContentFields'
@@ -111,13 +112,15 @@ export const getStaticProps: GetStaticProps<Part3PageProps> = async (
       client.query<GetVideoContainerPart2, GetVideoContainerPart2Variables>({
         query: GET_VIDEO_CONTAINER_PART_2,
         variables: {
-          containerId: `${containerId}/${languageId}`
+          containerId: `${containerId}/${languageId}`,
+          languageId: getLanguageIdFromLocale(context.locale)
         }
       }),
       client.query<GetVideoContentPart3, GetVideoContentPart3Variables>({
         query: GET_VIDEO_CONTENT_PART_3,
         variables: {
-          contentId: `${contentId}/${languageId}`
+          contentId: `${contentId}/${languageId}`,
+          languageId: getLanguageIdFromLocale(context.locale)
         }
       })
     ])
diff --git a/apps/watch/src/components/AudioLanguageDialog/AudioLanguageDialog.tsx b/apps/watch/src/components/AudioLanguageDialog/AudioLanguageDialog.tsx
index bae15ea2fca..12b3ffc40fb 100644
--- a/apps/watch/src/components/AudioLanguageDialog/AudioLanguageDialog.tsx
+++ b/apps/watch/src/components/AudioLanguageDialog/AudioLanguageDialog.tsx
@@ -5,6 +5,7 @@ import TextField from '@mui/material/TextField'
 import { Form, Formik, FormikValues } from 'formik'
 import compact from 'lodash/compact'
 import { useRouter } from 'next/router'
+import { useTranslation } from 'next-i18next'
 import { ComponentProps, ReactElement } from 'react'
 
 import { Dialog } from '@core/shared/ui/Dialog'
@@ -38,6 +39,7 @@ export function AudioLanguageDialog({
   open,
   onClose
 }: AudioLanguageDialogProps): ReactElement {
+  const { t } = useTranslation('apps-watch')
   const { id, variant, variantLanguagesCount, container } = useVideo()
   const router = useRouter()
 
@@ -119,7 +121,7 @@ export function AudioLanguageDialog({
               onClose={handleClose(resetForm)}
               dialogTitle={{
                 icon: <LanguageIcon sx={{ mr: 3 }} />,
-                title: 'Language',
+                title: t('Language'),
                 closeButton: true
               }}
               divider
@@ -139,10 +141,10 @@ export function AudioLanguageDialog({
                       {...params}
                       hiddenLabel
                       placeholder="Search Language"
-                      label="Language"
+                      label={t('Language')}
                       helperText={`${String(
                         variantLanguagesCount
-                      )} Languages Available`}
+                      )} ${t('Languages Available')}`}
                       sx={{
                         '> .MuiOutlinedInput-root': {
                           borderRadius: 2
diff --git a/apps/watch/src/components/DownloadDialog/DownloadDialog.tsx b/apps/watch/src/components/DownloadDialog/DownloadDialog.tsx
index 2817a95e244..bf22a7ebaf5 100644
--- a/apps/watch/src/components/DownloadDialog/DownloadDialog.tsx
+++ b/apps/watch/src/components/DownloadDialog/DownloadDialog.tsx
@@ -14,6 +14,7 @@ import { useTheme } from '@mui/material/styles'
 import TextField from '@mui/material/TextField'
 import Typography from '@mui/material/Typography'
 import { Form, Formik } from 'formik'
+import last from 'lodash/last'
 import Image from 'next/image'
 import { useTranslation } from 'next-i18next'
 import { ComponentProps, ReactElement, useEffect, useState } from 'react'
@@ -135,7 +136,7 @@ export function DownloadDialog({
           )}
           <Stack>
             <Typography variant="h6" sx={{ mb: 1 }}>
-              {title[0].value}
+              {last(title)?.value}
             </Typography>
             <Stack direction="row" alignItems="center">
               <LanguageIcon fontSize="small" sx={{ mr: 1 }} />
diff --git a/apps/watch/src/components/ShareDialog/ShareDialog.tsx b/apps/watch/src/components/ShareDialog/ShareDialog.tsx
index c9573e02dd4..4526e143e1a 100644
--- a/apps/watch/src/components/ShareDialog/ShareDialog.tsx
+++ b/apps/watch/src/components/ShareDialog/ShareDialog.tsx
@@ -8,6 +8,7 @@ import Tab from '@mui/material/Tab'
 import Tabs from '@mui/material/Tabs'
 import TextField from '@mui/material/TextField'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import Image from 'next/image'
 import { useRouter } from 'next/router'
 import { useTranslation } from 'next-i18next'
@@ -41,9 +42,9 @@ export function ShareDialog({
 
   const shareDescription =
     description != null && description.length > 0
-      ? description[0].value
+      ? last(description)?.value
       : snippet != null && snippet.length > 0
-        ? snippet[0].value
+        ? last(snippet)?.value
         : ''
 
   const shareLink =
@@ -134,10 +135,10 @@ export function ShareDialog({
           )}
           <Stack sx={{ maxWidth: { sm: '272px' } }}>
             <Typography variant="subtitle2" sx={{ mb: 1 }}>
-              {title[0].value}
+              {last(title)?.value}
             </Typography>
             <Typography>
-              {`${shareDescription.split(' ').slice(0, 18).join(' ')}...`}
+              {`${shareDescription?.split(' ').slice(0, 18).join(' ')}...`}
             </Typography>
           </Stack>
         </Stack>
diff --git a/apps/watch/src/components/VideoCard/VideoCard.tsx b/apps/watch/src/components/VideoCard/VideoCard.tsx
index a959181c680..ebce42fd1ec 100644
--- a/apps/watch/src/components/VideoCard/VideoCard.tsx
+++ b/apps/watch/src/components/VideoCard/VideoCard.tsx
@@ -6,6 +6,7 @@ import Skeleton from '@mui/material/Skeleton'
 import Stack from '@mui/material/Stack'
 import { type SxProps, styled } from '@mui/material/styles'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import Image from 'next/image'
 import NextLink from 'next/link'
 import { useTranslation } from 'next-i18next'
@@ -115,10 +116,10 @@ export function VideoCard({
                 transition: (theme) => theme.transitions.create('transform')
               }}
             >
-              {video?.images[0]?.mobileCinematicHigh != null ? (
+              {last(video?.images)?.mobileCinematicHigh != null ? (
                 <Image
-                  src={video.images[0].mobileCinematicHigh}
-                  alt={video.imageAlt[0].value}
+                  src={last(video?.images)?.mobileCinematicHigh ?? ''}
+                  alt={last(video?.imageAlt)?.value ?? ''}
                   fill
                   sizes="100vw"
                   style={{
@@ -187,7 +188,7 @@ export function VideoCard({
                   }}
                 >
                   {video != null ? (
-                    video?.title[0].value
+                    last(video?.title)?.value
                   ) : (
                     <Skeleton width="60%" data-testid="VideoTitleSkeleton" />
                   )}
@@ -300,7 +301,7 @@ export function VideoCard({
                 lineHeight={27 / 21}
               >
                 {video?.title != null ? (
-                  video?.title[0].value
+                  last(video?.title)?.value
                 ) : (
                   <Skeleton width="60%" data-testid="VideoTitleSkeleton" />
                 )}
diff --git a/apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx b/apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx
index a065df37994..3fd2020fac3 100644
--- a/apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx
+++ b/apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx
@@ -4,6 +4,7 @@ import Container from '@mui/material/Container'
 import IconButton from '@mui/material/IconButton'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import Image from 'next/image'
 import { ReactElement } from 'react'
 
@@ -33,9 +34,9 @@ export function ContainerHero({
       }}
       data-testid="ContainerHero"
     >
-      {images[0]?.mobileCinematicHigh != null && (
+      {last(images)?.mobileCinematicHigh != null && (
         <Image
-          src={images[0].mobileCinematicHigh}
+          src={last(images)?.mobileCinematicHigh ?? ''}
           alt="Home Hero"
           fill
           sizes="100vw"
@@ -74,7 +75,7 @@ export function ContainerHero({
                 fontSize: { xs: 28, sm: 36, md: 48, xl: 64 }
               }}
             >
-              {title[0].value}
+              {last(title)?.value}
             </Typography>
           </Stack>
 
diff --git a/apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx b/apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx
index 68731a1c1c8..123114bd65d 100644
--- a/apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx
+++ b/apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx
@@ -86,7 +86,7 @@ describe('VideoContainerPage', () => {
     const { getByRole } = render(
       <MockedProvider mocks={[getVideoChildrenMock]}>
         <SnackbarProvider>
-          <VideoProvider value={{ content: videos[0] }}>
+          <VideoProvider value={{ content: videos[5] }}>
             <VideoContainerPage />
           </VideoProvider>
         </SnackbarProvider>
diff --git a/apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx b/apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx
index c43cc083edb..cef109dfd96 100644
--- a/apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx
+++ b/apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx
@@ -1,6 +1,8 @@
 import Box from '@mui/material/Box'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
+import last from 'lodash/last'
+import { useRouter } from 'next/router'
 import { ReactElement, useState } from 'react'
 
 import { ThemeMode } from '@core/shared/ui/themes'
@@ -16,8 +18,9 @@ import { ContainerHero } from './ContainerHero'
 
 // Usually Series or Collection Videos
 export function VideoContainerPage(): ReactElement {
+  const router = useRouter()
   const { snippet, slug, variant } = useVideo()
-  const { loading, children } = useVideoChildren(variant?.slug)
+  const { loading, children } = useVideoChildren(variant?.slug, router.locale)
   const [shareDialog, setShareDialog] = useState<boolean>(false)
   const realChildren = children.filter((video) => video.variant !== null)
   function handleOpenDialog(): void {
@@ -41,7 +44,7 @@ export function VideoContainerPage(): ReactElement {
           direction="column"
         >
           <ContainerDescription
-            value={snippet[0].value}
+            value={last(snippet)?.value ?? ''}
             openDialog={handleOpenDialog}
           />
           <ShareDialog open={shareDialog} onClose={handleCloseDialog} />
diff --git a/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx b/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx
index 98927661460..0dd9c3bbbfa 100644
--- a/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx
@@ -21,7 +21,7 @@ describe('VideoContent', () => {
     expect(getByText(video.studyQuestions[0].value)).toBeInTheDocument()
   })
 
-  it('should hide the dicussion questions tab if no questions', () => {
+  it('should hide the discussion questions tab if no questions', () => {
     const { queryByRole } = render(
       <VideoProvider value={{ content: { ...video, studyQuestions: [] } }}>
         <VideoContent />
@@ -33,4 +33,47 @@ describe('VideoContent', () => {
       })
     ).not.toBeInTheDocument()
   })
+
+  it('should only show non-primary questions when they exist', () => {
+    const studyQuestions = [
+      {
+        __typename: 'VideoStudyQuestion' as const,
+        value: 'Primary Question 1',
+        primary: true
+      },
+      {
+        __typename: 'VideoStudyQuestion' as const,
+        value: 'Non-Primary Question 1',
+        primary: false
+      },
+      {
+        __typename: 'VideoStudyQuestion' as const,
+        value: 'Primary Question 2',
+        primary: true
+      },
+      {
+        __typename: 'VideoStudyQuestion' as const,
+        value: 'Non-Primary Question 2',
+        primary: false
+      }
+    ]
+
+    const { getByRole, getByText, queryByText } = render(
+      <VideoProvider value={{ content: { ...video, studyQuestions } }}>
+        <VideoContent />
+      </VideoProvider>
+    )
+
+    fireEvent.click(
+      getByRole('tab', { name: 'Discussion Discussion Questions' })
+    )
+
+    // Should show non-primary questions
+    expect(getByText('Non-Primary Question 1')).toBeInTheDocument()
+    expect(getByText('Non-Primary Question 2')).toBeInTheDocument()
+
+    // Should not show primary questions
+    expect(queryByText('Primary Question 1')).not.toBeInTheDocument()
+    expect(queryByText('Primary Question 2')).not.toBeInTheDocument()
+  })
 })
diff --git a/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx b/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx
index e237452f2d7..2aef5b445e7 100644
--- a/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx
@@ -5,8 +5,15 @@ import Stack from '@mui/material/Stack'
 import Tab from '@mui/material/Tab'
 import Tabs from '@mui/material/Tabs'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import { useTranslation } from 'next-i18next'
-import { ReactElement, SyntheticEvent, useEffect, useState } from 'react'
+import {
+  ReactElement,
+  SyntheticEvent,
+  useEffect,
+  useMemo,
+  useState
+} from 'react'
 
 import { TabPanel, tabA11yProps } from '@core/shared/ui/TabPanel'
 
@@ -17,6 +24,25 @@ export function VideoContent(): ReactElement {
   const { description, studyQuestions } = useVideo()
   const [tabValue, setTabValue] = useState(0)
   const { t } = useTranslation('apps-watch')
+
+  const filteredStudyQuestions = useMemo(() => {
+    if (!studyQuestions?.length) return []
+
+    const nonPrimaryQuestions = studyQuestions.filter(
+      (q) => q.primary === false
+    )
+    if (nonPrimaryQuestions.length > 0) {
+      return nonPrimaryQuestions
+    }
+
+    const primaryQuestions = studyQuestions.filter((q) => q.primary === true)
+    if (primaryQuestions.length > 0) {
+      return primaryQuestions
+    }
+
+    return []
+  }, [studyQuestions])
+
   const handleTabChange = (
     _event: SyntheticEvent<Element, Event>,
     newValue: number
@@ -126,7 +152,7 @@ export function VideoContent(): ReactElement {
           }
           {...tabA11yProps('description', 0)}
         />
-        {studyQuestions?.length !== 0 && (
+        {filteredStudyQuestions.length !== 0 && (
           <Tab
             data-testid="discussion"
             label={
@@ -159,10 +185,10 @@ export function VideoContent(): ReactElement {
             }
           }}
         >
-          {description[0]?.value}
+          {last(description)?.value}
         </TextFormatter>
       </TabPanel>
-      {studyQuestions?.length !== 0 && (
+      {filteredStudyQuestions.length !== 0 && (
         <TabPanel name="discussion-questions" value={tabValue} index={1}>
           <Stack
             direction="column"
@@ -171,7 +197,7 @@ export function VideoContent(): ReactElement {
               py: 2
             }}
           >
-            {studyQuestions?.map((question, index: number) => (
+            {filteredStudyQuestions.map((question, index: number) => (
               <Stack
                 direction="row"
                 spacing={4}
diff --git a/apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx b/apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx
index 55852c8f57d..ebb4697ccbb 100644
--- a/apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx
@@ -89,7 +89,7 @@ describe('VideoContentPage', () => {
     const { getByRole } = render(
       <MockedProvider mocks={[getVideoChildrenMock]}>
         <SnackbarProvider>
-          <VideoProvider value={{ content: videos[0] }}>
+          <VideoProvider value={{ content: videos[1] }}>
             <VideoContentPage />
           </VideoProvider>
         </SnackbarProvider>
diff --git a/apps/watch/src/components/VideoContentPage/VideoContentPage.tsx b/apps/watch/src/components/VideoContentPage/VideoContentPage.tsx
index 06f86244c26..7147f869435 100644
--- a/apps/watch/src/components/VideoContentPage/VideoContentPage.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoContentPage.tsx
@@ -1,6 +1,8 @@
 import Box from '@mui/material/Box'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
+import last from 'lodash/last'
+import { useRouter } from 'next/router'
 import { NextSeo } from 'next-seo'
 import { ReactElement, useState } from 'react'
 
@@ -22,6 +24,7 @@ import 'video.js/dist/video-js.css'
 
 // Usually FeatureFilm, ShortFilm, Episode or Segment Videos
 export function VideoContentPage(): ReactElement {
+  const router = useRouter()
   const {
     title,
     snippet,
@@ -35,8 +38,10 @@ export function VideoContentPage(): ReactElement {
     childrenCount
   } = useVideo()
   const { loading, children } = useVideoChildren(
-    container?.variant?.slug ?? variant?.slug
+    container?.variant?.slug ?? variant?.slug,
+    router.locale
   )
+
   const [hasPlayed, setHasPlayed] = useState(false)
   const [openShare, setOpenShare] = useState(false)
   const [openDownload, setOpenDownload] = useState(false)
@@ -47,24 +52,24 @@ export function VideoContentPage(): ReactElement {
   return (
     <>
       <NextSeo
-        title={title[0].value}
-        description={snippet[0].value ?? undefined}
+        title={last(title)?.value}
+        description={last(snippet)?.value ?? undefined}
         openGraph={{
           type: 'website',
-          title: title[0].value,
+          title: last(title)?.value,
           url: `${
             process.env.NEXT_PUBLIC_WATCH_URL ??
             'https://watch-jesusfilm.vercel.app'
           }${ogSlug}`,
-          description: snippet[0].value ?? undefined,
+          description: last(snippet)?.value ?? undefined,
           images:
-            images[0]?.mobileCinematicHigh != null
+            last(images)?.mobileCinematicHigh != null
               ? [
                   {
-                    url: images[0].mobileCinematicHigh,
+                    url: last(images)?.mobileCinematicHigh ?? '',
                     width: 1080,
                     height: 600,
-                    alt: imageAlt[0].value,
+                    alt: last(imageAlt)?.value ?? '',
                     type: 'image/jpeg'
                   }
                 ]
diff --git a/apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx b/apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx
index 8ab8af50296..54edbaa38c0 100644
--- a/apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx
@@ -6,6 +6,7 @@ import Link from '@mui/material/Link'
 import Skeleton from '@mui/material/Skeleton'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import NextLink from 'next/link'
 import { useTranslation } from 'next-i18next'
 import { ReactElement, useMemo } from 'react'
@@ -50,7 +51,7 @@ export function VideoHeading({
               alignItems="center"
             >
               <Typography variant="h5" color="text.primary">
-                {title[0].value}
+                {last(title)?.value}
               </Typography>
               <Stack
                 direction="row"
@@ -83,7 +84,7 @@ export function VideoHeading({
                     color="primary"
                     sx={{ textDecoration: 'none' }}
                   >
-                    {container.title[0].value}
+                    {last(container.title)?.value}
                   </Link>
                 </NextLink>
                 <Typography
@@ -122,8 +123,8 @@ export function VideoHeading({
                   sx={{ display: { xs: 'none', xl: 'block' } }}
                 >
                   {container.label === VideoLabel.featureFilm
-                    ? 'Watch Full Film'
-                    : 'See All'}
+                    ? t('Watch Full Film')
+                    : t('See All')}
                 </Button>
               </NextLink>
               <Typography
diff --git a/apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx b/apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
index a55820f1fc8..7950dac2534 100644
--- a/apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
@@ -5,6 +5,7 @@ import Button from '@mui/material/Button'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
 import Image from 'next/image'
 import { useTranslation } from 'next-i18next'
 import { ReactElement, useState } from 'react'
@@ -42,10 +43,10 @@ export function VideoHeroOverlay({
       }}
       data-testid="VideoHeroOverlay"
     >
-      {images[0]?.mobileCinematicHigh != null && (
+      {last(images)?.mobileCinematicHigh != null && (
         <Image
-          src={images[0].mobileCinematicHigh}
-          alt={imageAlt[0].value}
+          src={last(images)?.mobileCinematicHigh ?? ''}
+          alt={last(imageAlt)?.value ?? ''}
           fill
           sizes="100vw"
           style={{
@@ -73,7 +74,7 @@ export function VideoHeroOverlay({
             color="text.primary"
             sx={{ width: { xs: '100%', lg: '70%' } }}
           >
-            {title[0]?.value}
+            {last(title)?.value}
           </Typography>
           <Stack
             direction="row"
diff --git a/apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx b/apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx
index b0ebf33ea37..baed998db55 100644
--- a/apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx
+++ b/apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx
@@ -1,3 +1,4 @@
+import last from 'lodash/last'
 import { ReactElement, useEffect, useRef, useState } from 'react'
 import videojs from 'video.js'
 import Player from 'video.js/dist/types/player'
@@ -28,7 +29,7 @@ export function VideoPlayer({
       const muxMetadata: MuxMetadata = {
         env_key: process.env.NEXT_PUBLIC_MUX_DEFAULT_REPORTING_KEY || '',
         player_name: 'watch',
-        video_title: title?.[0]?.value ?? '',
+        video_title: last(title)?.value ?? '',
         video_id: variant?.id ?? ''
       }
 
diff --git a/apps/watch/src/components/Videos/__generated__/testData.ts b/apps/watch/src/components/Videos/__generated__/testData.ts
index 67e3bc03d53..fff328ba157 100644
--- a/apps/watch/src/components/Videos/__generated__/testData.ts
+++ b/apps/watch/src/components/Videos/__generated__/testData.ts
@@ -37,21 +37,25 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: "How is the sacrifice of Jesus part of God's plan?"
+        value: "How is the sacrifice of Jesus part of God's plan?",
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'How do the different groups of people respond to Jesus and His teachings?'
+          'How do the different groups of people respond to Jesus and His teachings?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'What are some of the miracles Jesus performed? How do they affect those people?'
+          'What are some of the miracles Jesus performed? How do they affect those people?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How do you respond to the life of Jesus?'
+        value: 'How do you respond to the life of Jesus?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'JESUS' }],
@@ -182,15 +186,18 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How do the disciples react to the storm?'
+        value: 'How do the disciples react to the storm?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What does Jesus do when the disciples ask Him for help?'
+        value: 'What does Jesus do when the disciples ask Him for help?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How would you respond to the storm and to Jesus?'
+        value: 'How would you respond to the storm and to Jesus?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Jesus Calms the Storm' }],
@@ -262,17 +269,20 @@ export const videos: VideoContentFields[] = [
       {
         __typename: 'VideoStudyQuestion',
         value:
-          "Why do you think the woman so desperately wanted to touch Jesus' garment?"
+          "Why do you think the woman so desperately wanted to touch Jesus' garment?",
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'In such a large crowd of people, why do you think Jesus wanted to know who touched Him?'
+          'In such a large crowd of people, why do you think Jesus wanted to know who touched Him?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Of all the things Jesus said to the woman, what meant the most to you?'
+          'Of all the things Jesus said to the woman, what meant the most to you?',
+        primary: true
       }
     ],
     title: [
@@ -451,11 +461,13 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How do I feel about Jesus being crucified?'
+        value: 'How do I feel about Jesus being crucified?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: "How do Jesus' words to the thief on the cross give me hope?"
+        value: "How do Jesus' words to the thief on the cross give me hope?",
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Day 6: Jesus Died for Me' }],
@@ -521,9 +533,10 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What did you like best or what caught your attention?'
+        value: 'What did you like best or what caught your attention?',
+        primary: true
       },
-      { __typename: 'VideoStudyQuestion', value: 'Why?' }
+      { __typename: 'VideoStudyQuestion', value: 'Why?', primary: true }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Book of Acts' }],
     variant: {
@@ -588,11 +601,13 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'Who was affected by this miracle?'
+        value: 'Who was affected by this miracle?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What does this first miracle tell you about Jesus?'
+        value: 'What does this first miracle tell you about Jesus?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Wedding in Cana' }],
@@ -699,21 +714,25 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What did you like best or what caught your attention?'
+        value: 'What did you like best or what caught your attention?',
+        primary: true
       },
-      { __typename: 'VideoStudyQuestion', value: 'Why?' },
+      { __typename: 'VideoStudyQuestion', value: 'Why?', primary: true },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Herod also imprisoned John the Baptist. What happened to John? [See Matthew 14:1-12.]'
+          'Herod also imprisoned John the Baptist. What happened to John? [See Matthew 14:1-12.]',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How secure was Peter in prison? Any chance of escape?'
+        value: 'How secure was Peter in prison? Any chance of escape?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How do you think the non-believers explained Peter’s escape?'
+        value: 'How do you think the non-believers explained Peter’s escape?',
+        primary: true
       }
     ],
     title: [
@@ -837,21 +856,25 @@ export const videos: VideoContentFields[] = [
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'In what ways do you identify with the main character, Mary Magdalene?'
+          'In what ways do you identify with the main character, Mary Magdalene?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: "Why do you think the elders didn't approve of Jesus?"
+        value: "Why do you think the elders didn't approve of Jesus?",
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'After his resurrection, why do you think Jesus chose to speak first with Mary?'
+          'After his resurrection, why do you think Jesus chose to speak first with Mary?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'How do you respond to the life of Jesus? What emotions come to mind, and why?'
+          'How do you respond to the life of Jesus? What emotions come to mind, and why?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Chosen Witness' }],
@@ -969,31 +992,37 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What part of the story did you really like?'
+        value: 'What part of the story did you really like?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'If Jesus asked to come to your house, how would you respond?  How would that make you feel?  What would you do with him while He was there?'
+          'If Jesus asked to come to your house, how would you respond?  How would that make you feel?  What would you do with him while He was there?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Zacchaeus wanted to get as close as he could to see Jesus.  Why would he do that?  Why would you want to be close to Jesus?'
+          'Zacchaeus wanted to get as close as he could to see Jesus.  Why would he do that?  Why would you want to be close to Jesus?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'How can we get close to Jesus today?  (This would be a good opportunity to give children an opportunity to become Christ followers.)'
+          'How can we get close to Jesus today?  (This would be a good opportunity to give children an opportunity to become Christ followers.)',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'Something I learned about God is ________.'
+        value: 'Something I learned about God is ________.',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'From what you learned today, what do you feel God is asking you to do?'
+          'From what you learned today, what do you feel God is asking you to do?',
+        primary: true
       }
     ],
     title: [
@@ -1061,15 +1090,18 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How does Mary respond to the angel?'
+        value: 'How does Mary respond to the angel?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How does Jesus happen to be born in Bethlehem?'
+        value: 'How does Jesus happen to be born in Bethlehem?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'Who are the first to know and tell of the birth of Jesus? Why?'
+        value: 'Who are the first to know and tell of the birth of Jesus? Why?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Birth of Jesus' }],
@@ -1136,21 +1168,25 @@ export const videos: VideoContentFields[] = [
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Life is portrayed as falling plates. What do you think about that?'
+          'Life is portrayed as falling plates. What do you think about that?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Everyone is on a spiritual journey. Where do you think you are on that journey?'
+          'Everyone is on a spiritual journey. Where do you think you are on that journey?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Do you think you are moving toward God, away from God, or staying about the same?'
+          'Do you think you are moving toward God, away from God, or staying about the same?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'Would you like to hear how you can know God personally?'
+        value: 'Would you like to hear how you can know God personally?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: '#FallingPlates' }],
@@ -1203,18 +1239,21 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What did you like best or what caught your attention?'
+        value: 'What did you like best or what caught your attention?',
+        primary: true
       },
-      { __typename: 'VideoStudyQuestion', value: 'Why?' },
+      { __typename: 'VideoStudyQuestion', value: 'Why?', primary: true },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'Paul cast out a demon. Why did he go to jail? Was this charge correct?'
+          'Paul cast out a demon. Why did he go to jail? Was this charge correct?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'How did God resolve the problem? Do you think this is what Paul prayed for in verse 25?'
+          'How did God resolve the problem? Do you think this is what Paul prayed for in verse 25?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'Paul and Silas in Prison' }],
@@ -1280,16 +1319,19 @@ export const videos: VideoContentFields[] = [
     studyQuestions: [
       {
         __typename: 'VideoStudyQuestion',
-        value: 'What message do you get from this story?'
+        value: 'What message do you get from this story?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
-        value: 'How do you deal with things you feel guilty about?'
+        value: 'How do you deal with things you feel guilty about?',
+        primary: true
       },
       {
         __typename: 'VideoStudyQuestion',
         value:
-          'What do you think about the idea of being with Jesus in paradise?'
+          'What do you think about the idea of being with Jesus in paradise?',
+        primary: true
       }
     ],
     title: [{ __typename: 'VideoTitle', value: 'My Last Day' }],
diff --git a/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts b/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts
new file mode 100644
index 00000000000..13085c1a107
--- /dev/null
+++ b/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts
@@ -0,0 +1,29 @@
+import { getLanguageIdFromLocale } from './getLanguageIdFromLocale'
+
+describe('getLanguageIdFromLocale', () => {
+  it('should return correct language ID for valid locale', () => {
+    expect(getLanguageIdFromLocale('en')).toBe('529')
+    expect(getLanguageIdFromLocale('es')).toBe('21028')
+    expect(getLanguageIdFromLocale('fr')).toBe('496')
+    expect(getLanguageIdFromLocale('id')).toBe('16639')
+    expect(getLanguageIdFromLocale('th')).toBe('13169')
+    expect(getLanguageIdFromLocale('ja')).toBe('7083')
+    expect(getLanguageIdFromLocale('ko')).toBe('3804')
+    expect(getLanguageIdFromLocale('ru')).toBe('3934')
+    expect(getLanguageIdFromLocale('tr')).toBe('1942')
+    expect(getLanguageIdFromLocale('zh')).toBe('20615')
+    expect(getLanguageIdFromLocale('zh-Hans-CN')).toBe('21754')
+  })
+
+  it('should return English language ID (529) for undefined locale', () => {
+    expect(getLanguageIdFromLocale(undefined)).toBe('529')
+  })
+
+  it('should return English language ID (529) for null locale', () => {
+    expect(getLanguageIdFromLocale(null)).toBe('529')
+  })
+
+  it('should return English language ID (529) for unsupported locale', () => {
+    expect(getLanguageIdFromLocale('unsupported')).toBe('529')
+  })
+})
diff --git a/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts b/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts
new file mode 100644
index 00000000000..4c0a6bdd9a1
--- /dev/null
+++ b/apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts
@@ -0,0 +1,52 @@
+/**
+ * Type for supported UI language codes based on the i18n config
+ */
+const UI_LANGUAGE_CODES = [
+  'en',
+  'es',
+  'fr',
+  'id',
+  'th',
+  'ja',
+  'ko',
+  'ru',
+  'tr',
+  'zh',
+  'zh-Hans-CN'
+]
+
+/**
+ * Mapping of UI language codes to their corresponding language IDs
+ */
+export const UI_LANGUAGE_IDS: Readonly<Record<string, string>> = {
+  en: '529', // English
+  es: '21028', // Spanish (Latin American)
+  fr: '496', // French
+  id: '16639', // Indonesian (Standard)
+  th: '13169', // Thai
+  ja: '7083', // Japanese
+  ko: '3804', // Korean
+  ru: '3934', // Russian
+  tr: '1942', // Turkish
+  zh: '20615', // Mandarin China
+  'zh-Hans-CN': '21754' // Chinese Simplified
+} as const
+
+/**
+ * Gets the language ID for a given UI language code.
+ * @param locale - The UI language code
+ * @returns The corresponding language ID or undefined if not found
+ * @example
+ * ```ts
+ * const languageId = getLanguageIdFromLocale('en') // returns '529'
+ * ```
+ */
+export const getLanguageIdFromLocale = (
+  locale: string | undefined | null
+): string => {
+  if (locale != null && UI_LANGUAGE_CODES.includes(locale)) {
+    return UI_LANGUAGE_IDS[locale]
+  } else {
+    return UI_LANGUAGE_IDS['en']
+  }
+}
diff --git a/apps/watch/src/libs/getLanguageIdFromLocale/index.ts b/apps/watch/src/libs/getLanguageIdFromLocale/index.ts
new file mode 100644
index 00000000000..437e7640ac4
--- /dev/null
+++ b/apps/watch/src/libs/getLanguageIdFromLocale/index.ts
@@ -0,0 +1 @@
+export { getLanguageIdFromLocale } from './getLanguageIdFromLocale'
diff --git a/apps/watch/src/libs/useVideoChildren/useVideoChildren.ts b/apps/watch/src/libs/useVideoChildren/useVideoChildren.ts
index 51ebbdc44bb..971d392725f 100644
--- a/apps/watch/src/libs/useVideoChildren/useVideoChildren.ts
+++ b/apps/watch/src/libs/useVideoChildren/useVideoChildren.ts
@@ -5,6 +5,7 @@ import {
   GetVideoChildren,
   GetVideoChildren_video_children
 } from '../../../__generated__/GetVideoChildren'
+import { getLanguageIdFromLocale } from '../getLanguageIdFromLocale'
 import { VIDEO_CHILD_FIELDS } from '../videoChildFields'
 
 export const GET_VIDEO_CHILDREN = gql`
@@ -19,7 +20,10 @@ export const GET_VIDEO_CHILDREN = gql`
   }
 `
 
-export function useVideoChildren(slug?: string): {
+export function useVideoChildren(
+  slug?: string,
+  locale?: string
+): {
   loading: boolean
   children: GetVideoChildren_video_children[]
 } {
@@ -28,11 +32,13 @@ export function useVideoChildren(slug?: string): {
 
   useEffect(() => {
     if (slug != null) {
+      const languageId = getLanguageIdFromLocale(locale)
+
       void getVideoChildren({
-        variables: { id: slug }
+        variables: { id: slug, languageId }
       })
     }
-  }, [getVideoChildren, slug])
+  }, [getVideoChildren, slug, locale])
 
   return {
     loading,
diff --git a/apps/watch/src/libs/videoContentFields.ts b/apps/watch/src/libs/videoContentFields.ts
index 92559505937..c5d1fd8eab0 100644
--- a/apps/watch/src/libs/videoContentFields.ts
+++ b/apps/watch/src/libs/videoContentFields.ts
@@ -18,11 +18,12 @@ export const VIDEO_CONTENT_FIELDS = gql`
     }
     studyQuestions(languageId: $languageId, primary: true) {
       value
+      primary
     }
     title(languageId: $languageId, primary: true) {
       value
     }
-    variant(languageId: $languageId) {
+    variant {
       id
       duration
       hls
diff --git a/libs/locales/en/apps-watch.json b/libs/locales/en/apps-watch.json
index 8132fcb45e0..a7054a978c4 100644
--- a/libs/locales/en/apps-watch.json
+++ b/libs/locales/en/apps-watch.json
@@ -1,5 +1,7 @@
 {
   "724": "724",
+  "Language": "Language",
+  "Languages Available": "Languages Available",
   "Download Video": "Download Video",
   "Select a file size": "Select a file size",
   "I agree to the": "I agree to the",
@@ -41,6 +43,8 @@
   "Discussion Questions": "Discussion Questions",
   "Clip ": "Clip ",
   " of ": " of ",
+  "Watch Full Film": "Watch Full Film",
+  "See All": "See All",
   "Play": "Play",
   "Play Video": "Play Video",
   "{{ duration }} min": "{{ duration }} min",
@@ -54,7 +58,6 @@
   "Streaming": "Streaming",
   "Library.": "Library.",
   "Watch, learn and share the gospel in over 2000 languages": "Watch, learn and share the gospel in over 2000 languages",
-  "See All": "See All",
   "About Our Project": "About Our Project",
   "With 70% of the world not being able to speak English, there is a huge opportunity for the gospel to spread to unreached places. We have a vision to make it easier to watch, download and share Christian videos with people in their native heart language.": "With 70% of the world not being able to speak English, there is a huge opportunity for the gospel to spread to unreached places. We have a vision to make it easier to watch, download and share Christian videos with people in their native heart language.",
   "Jesus Film Project is a Christian ministry with a vision to reach the world with the gospel of Jesus Christ through evangelistic videos. Watch from over 2000 languages on any device and share it with others.": "Jesus Film Project is a Christian ministry with a vision to reach the world with the gospel of Jesus Christ through evangelistic videos. Watch from over 2000 languages on any device and share it with others."


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (19)</summary><blockquote>

<details>
<summary>apps/watch/src/components/Footer/Footer.tsx (1)</summary><blockquote>

`154-154`: **Maintain consistent approach for forcing English translations.**

Similar to the navigation links, all these UI elements are now forced to display in English. This appears to be part of a broader pattern in this PR where navigation/UI elements remain in English while content can be translated.



Consider adding a brief comment at the top of this component explaining the intentional decision to keep these UI elements in English, to help future developers understand the reasoning.


Also applies to: 177-180, 186-190, 196-202, 222-223

</blockquote></details>
<details>
<summary>apps/watch/src/components/Header/HeaderMenuPanel/HeaderMenuPanel.tsx (1)</summary><blockquote>

`93-93`: **Consistent approach with Footer component for forcing English translations.**

Similar to the Footer component, this component has been updated to force English language for all UI elements. This creates a consistent experience where navigation elements remain in English regardless of the user's language selection.



Consider extracting the language option into a constant at the component level:

```diff
export function HeaderMenuPanel({
  onClose
}: HeaderMenuPanelProps): ReactElement {
  const { t } = useTranslation('apps-watch')
+ const englishOption = { lng: 'en' }
  const [expanded, setExpanded] = useState<string | false>(false)

  // Then use it throughout the component
  // Example: t('Give Now', englishOption)
```

This would slightly improve readability and make it easier to update if the language strategy changes.


Also applies to: 111-112, 117-118

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx (1)</summary><blockquote>

`87-88`: **Consider updating container title access pattern**

While the main title display has been updated to use `last()`, the container title still uses direct array indexing with `container.title[0].value`. Consider updating this for consistency with other title accesses.

```diff
-                    {container.title[0].value}
+                    {last(container.title)?.value}
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx (1)</summary><blockquote>

`31-31`: **Remove debugging console.log statement**

The console.log statement should be removed before merging to production to prevent console clutter and potential exposure of sensitive information.

```diff
-  console.log(snippet)
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/ShareDialog/ShareDialog.tsx (1)</summary><blockquote>

`120-135`: **Missed opportunity: Inconsistent array access pattern.**

While most of the code has been updated to use `last()`, there are still direct accesses to array elements in this section. For consistency, consider updating these as well.

```diff
 {images[0]?.mobileCinematicHigh != null && (
   <Box sx={{ display: { xs: 'none', sm: 'flex' } }}>
     <Image
-      src={images[0].mobileCinematicHigh}
-      alt={title[0].value}
+      src={last(images)?.mobileCinematicHigh ?? ''}
+      alt={last(title)?.value ?? ''}
       width={240}
       height={115}
       style={{
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/DownloadDialog/DownloadDialog.tsx (1)</summary><blockquote>

`96-117`: **Missed opportunity: Inconsistent array access pattern.**

While some of the code has been updated to use `last()`, there are still direct accesses to array elements in the Image component and download function call. For consistency, consider updating these as well.

```diff
 <Image
-  src={images[0].mobileCinematicHigh}
-  alt={imageAlt[0].value}
+  src={last(images)?.mobileCinematicHigh ?? ''}
+  alt={last(imageAlt)?.value ?? ''}
   width={240}
   height={115}
```

And in the download function:

```diff
- void download(values.file, `${title[0].value}.mp4`)
+ void download(values.file, `${last(title)?.value ?? ''}.mp4`)
```


Also applies to: 150-150

</blockquote></details>
<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (1)</summary><blockquote>

`16-24`: **JSDoc example might be misleading.**

The example in the JSDoc comment suggests `getVideoLanguageId('english')` returns `'en'`, but based on the `languageSlugs` data, it would actually return `'529'` (the ID for 'english'). Consider updating the example to accurately reflect the actual mapping.


```diff
 * @example
 * ```ts
-* const languageId = getVideoLanguageId('english') // returns 'en'
+* const languageId = getVideoLanguageId('english') // returns '529'
 * ```
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx (1)</summary><blockquote>

`46-56`: **Consistent use of last localized value instead of first.**

Changed from using the first element in the arrays to the last one, which is a good practice if the most specific or relevant localization is expected to be last in the array.


Consider adding a comment explaining why the last element is preferred over the first element for better code maintainability.

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContentPage.tsx (1)</summary><blockquote>

`52-54`: **SEO metadata now uses last localized values.**

Changed from using the first elements to last elements for title and description in the SEO metadata, which is consistent with the internationalization approach seen in other components.


Consider adding a comment explaining why the last element is used for SEO metadata, as this could impact site discoverability in search engines.

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.spec.tsx (1)</summary><blockquote>

`17-80`: **Consider adding edge case tests for robustness.**

While the current test coverage is good, consider adding tests for edge cases:
- Testing with an empty locales array
- Testing with a current language that isn't in the locales list
- Testing behavior when the i18n options are undefined

These additional tests would enhance the robustness of your test suite.

</blockquote></details>
<details>
<summary>apps/watch/src/libs/useVideoChildren/useVideoChildren.ts (1)</summary><blockquote>

`32-36`: **Consider adding defensive coding for language slug extraction.**

While the current implementation works for properly formatted slugs, it may be vulnerable to edge cases. If the slug doesn't contain forward slashes or has an invalid language segment, the code might not behave as expected.

Consider adding defensive coding:

```diff
-      const languageSlug = slug.split('/').pop()
-      const languageId = getVideoLanguageId(languageSlug)
+      const slugParts = slug.split('/')
+      const languageSlug = slugParts.length > 0 ? slugParts.pop() : undefined
+      const languageId = languageSlug ? getVideoLanguageId(languageSlug) : undefined
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx (5)</summary><blockquote>

`50-51`: **Consider using a more descriptive cookie fingerprint variable name**

The variable `cookieFingerprint` with value '00005' is used without explanation. Consider adding a comment explaining its purpose or using a more descriptive constant name to improve code maintainability.


```diff
-    const cookieFingerprint = '00005'
+    // This fingerprint helps identify cookies set by the language switch feature
+    const LANGUAGE_COOKIE_FINGERPRINT = '00005'
-    document.cookie = `NEXT_LOCALE=${cookieFingerprint}---${languageCode}; path=/`
+    document.cookie = `NEXT_LOCALE=${LANGUAGE_COOKIE_FINGERPRINT}---${languageCode}; path=/`
```

---

`64-64`: **Add a comment explaining the 'zh' filter**

The code filters out the 'zh' language code without explanation. Add a comment to clarify why this specific language is being excluded.


```diff
-      .filter((languageCode) => languageCode !== 'zh')
+      // Filter out generic Chinese code as we use specific variants (zh-Hans, zh-Hant) instead
+      .filter((languageCode) => languageCode !== 'zh')
```

---

`128-141`: **Remove redundant style spreading**

There's redundant style spreading with `commonButtonStyles`. It appears both as a direct prop and inside the `sx` prop. This can cause conflicts and is unnecessary.


```diff
              <Button
                key={language.languageCode}
                onClick={() => handleChange(language.languageCode)}
-                {...commonButtonStyles}
                sx={{
                  ...commonButtonStyles,
                  textTransform: 'uppercase',
                  bgcolor: 'rgba(41, 37, 36, 0.5)'
                }}
                data-testid={`language-button-${language.languageCode}`}
              >
                {language.localName}
              </Button>
```

---

`143-155`: **Remove redundant style spreading in Contact button**

Similar to the language buttons, there's redundant style spreading in the Contact button with `commonButtonStyles`.


```diff
            <Button
              href="/contact"
              startIcon={<EmailIcon />}
-              {...commonButtonStyles}
              sx={{
                ...commonButtonStyles,
                mt: 3,
                bgcolor: 'rgba(41, 37, 36, 0.1)'
              }}
              data-testid="language-button-support"
            >
              {t('Contact Us')}
            </Button>
```

---

`93-104`: **Improve dialog accessibility**

While the dialog has an `aria-label`, consider adding more accessibility attributes to make it fully ARIA compliant.


```diff
    <Dialog
      fullScreen
      open={open}
      onClose={handleClose}
      TransitionComponent={Transition}
      aria-label="language-modal-title"
+      aria-modal="true"
+      aria-describedby="language-selector-description"
      sx={{
        '& .MuiDialog-paper': {
          backgroundColor: 'transparent'
        }
      }}
    >
```

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.stories.tsx (3)</summary><blockquote>

`32-39`: **LGTM, but consider adding i18n context provider**

The story setup looks good, but since the component relies on i18n context and Next.js router, it may not function correctly without proper mocking. Consider wrapping the component with the necessary providers in a decorator.


```diff
export const Default: Story = {
  args: {
    open: true,
    handleClose: () => {
      console.log('Dialog closed')
    }
  },
+  decorators: [
+    (Story) => (
+      <MockI18nProvider>
+        <MockRouterProvider>
+          <Story />
+        </MockRouterProvider>
+      </MockI18nProvider>
+    )
+  ]
}
```

You would need to create the mock providers or use existing ones in your project.

---

`41-48`: **Consider removing console.log in production storybook**

Using `console.log` in the stories is fine for development, but consider using Storybook's action system instead for a cleaner production storybook.


```diff
export const Closed: Story = {
  args: {
    open: false,
-    handleClose: () => {
-      console.log('Dialog closed')
-    }
+    handleClose: action('Dialog closed')
  }
}
```

You would need to import the action function:
```javascript
import { action } from '@storybook/addon-actions';
```

---

`1-31`: **Consider adding more diverse story variations**

The story setup is good, but consider adding more variations to showcase different aspects of the component, such as different language settings or error states.


You could add more story variations like:
- A story with a specific set of languages
- A story demonstrating error handling
- A story with RTL language support

This would provide better documentation and testing scenarios for the component.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c111d25eb46284d7a73784bd3ad759b798c53da2 and cc5929d0743d903f0f04b117ea3610d047526fba.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `apis/api-languages/src/__generated__/languageSlugs.ts` is excluded by `!**/__generated__/**`

</details>

<details>
<summary>📒 Files selected for processing (26)</summary>

* `apis/api-media/src/lib/exportExistingLanguageSlugs.ts` (1 hunks)
* `apps/watch/pages/watch/[part1]/[part2].tsx` (2 hunks)
* `apps/watch/pages/watch/[part1]/[part2]/[part3].tsx` (2 hunks)
* `apps/watch/src/components/DownloadDialog/DownloadDialog.tsx` (6 hunks)
* `apps/watch/src/components/DownloadDialog/TermsOfUseDialog/TermsOfUseDialog.tsx` (1 hunks)
* `apps/watch/src/components/Footer/Footer.tsx` (4 hunks)
* `apps/watch/src/components/Header/HeaderMenuPanel/HeaderMenuPanel.tsx` (2 hunks)
* `apps/watch/src/components/Header/HeaderTabButtons/HeaderTabButtons.tsx` (1 hunks)
* `apps/watch/src/components/Header/LocalAppBar/LocalAppBar.tsx` (3 hunks)
* `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.spec.tsx` (1 hunks)
* `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.stories.tsx` (1 hunks)
* `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx` (1 hunks)
* `apps/watch/src/components/LanguageSwitchDialog/index.ts` (1 hunks)
* `apps/watch/src/components/ShareDialog/ShareDialog.tsx` (5 hunks)
* `apps/watch/src/components/VideoCard/VideoCard.tsx` (4 hunks)
* `apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx` (3 hunks)
* `apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx` (3 hunks)
* `apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx` (2 hunks)
* `apps/watch/src/components/VideoContentPage/VideoContentPage.tsx` (3 hunks)
* `apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx` (2 hunks)
* `apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx` (3 hunks)
* `apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx` (2 hunks)
* `apps/watch/src/libs/getVideoLanguageId.ts` (1 hunks)
* `apps/watch/src/libs/languageSlugs.ts` (1 hunks)
* `apps/watch/src/libs/useVideoChildren/useVideoChildren.ts` (2 hunks)
* `libs/locales/en/apps-watch.json` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (10)</summary>

<details>
<summary>apis/api-media/src/lib/exportExistingLanguageSlugs.ts (2)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/languageSlugs.ts (1)</summary>

* `languageSlugs` (11-2246)

</details>
<details>
<summary>apis/api-languages/src/__generated__/languageSlugs.ts (1)</summary>

* `languageSlugs` (11-2246)

</details>

</blockquote></details>
<details>
<summary>apps/watch/pages/watch/[part1]/[part2]/[part3].tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (1)</summary>

* `getVideoLanguageId` (25-30)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (1)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/languageSlugs.ts (1)</summary>

* `languageSlugs` (11-2246)

</details>

</blockquote></details>
<details>
<summary>apps/watch/pages/watch/[part1]/[part2].tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (1)</summary>

* `getVideoLanguageId` (25-30)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/libs/useVideoChildren/useVideoChildren.ts (1)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (1)</summary>

* `getVideoLanguageId` (25-30)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx (2)</summary><blockquote>

<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/index.ts (1)</summary>

* `LanguageSwitchDialog` (1-1)

</details>
<details>
<summary>apps/arclight/src/app/v2/[...route]/_metadata-language-tags/languages.ts (1)</summary>

* `languages` (7-103)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.stories.tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx (1)</summary>

* `LanguageSwitchDialog` (35-178)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/libs/languageSlugs.ts (1)</summary><blockquote>

<details>
<summary>apis/api-languages/src/__generated__/languageSlugs.ts (1)</summary>

* `languageSlugs` (11-2246)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoCard/VideoCard.tsx (1)</summary><blockquote>

<details>
<summary>apis/api-journeys/src/app/modules/block/video/video.resolver.ts (1)</summary>

* `video` (264-284)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.spec.tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx (1)</summary>

* `LanguageSwitchDialog` (35-178)

</details>

</blockquote></details>

</details><details>
<summary>🪛 ESLint</summary>

<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx</summary>

[error] 80-80: Definition for rule 'react-hooks/exhaustive-deps' was not found.

(react-hooks/exhaustive-deps)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Deploy Preview and Test (watch, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (journeys-admin, 6636/merge, pull_request, 22)

</details>

<details>
<summary>🔇 Additional comments (56)</summary><blockquote>

<details>
<summary>apis/api-media/src/lib/exportExistingLanguageSlugs.ts (2)</summary>

`24-33`: **Well-documented file header!**

The detailed file header provides clear documentation about the auto-generated nature of the output file, including a warning not to edit directly, a timestamp, and a good description of the purpose.

---

`41-45`: **Good enhancement to write to multiple locations**

Adding the second output location ensures both the API languages module and the watch app have direct access to the same language slugs mapping, improving maintainability and consistency across the codebase.

</details>
<details>
<summary>apps/watch/src/components/DownloadDialog/TermsOfUseDialog/TermsOfUseDialog.tsx (2)</summary>

`23-23`: **Internationalization improvement**

Replacing the hardcoded "Terms of Use" string with the translation function improves the component's internationalization support.

---

`28-29`: **Internationalization for action buttons**

Good job adding translation support for "Accept" and "Cancel" buttons. This ensures consistent localization throughout the dialog.

</details>
<details>
<summary>apps/watch/src/components/Header/HeaderTabButtons/HeaderTabButtons.tsx (3)</summary>

`29-32`: **Enforcing English for navigation labels**

The code explicitly sets the language to English for the "Resources" navigation label using `{ lng: 'en' }`. This ensures the navigation label remains in English regardless of the user's selected language.


Is this intentional for all navigation elements to remain in English? This appears to be part of a coordinated change across multiple components, but it's worth confirming that main navigation elements shouldn't be localized.

---

`36-39`: **Enforcing English for Journeys navigation**

The "Journeys" navigation label is explicitly set to English using `{ lng: 'en' }`. This matches the pattern used for other navigation elements.

---

`41-41`: **Consistent English enforcement for Videos tab**

The "Videos" label is also explicitly set to English, maintaining consistency with other navigation elements. This coordinated change ensures all main navigation tabs appear in English regardless of the user's selected language.

</details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/index.ts (1)</summary>

`1-2`: **Clean export for new LanguageSwitchDialog component**

Good practice to use this index file for exporting the new LanguageSwitchDialog component. This follows the barrel pattern for cleaner imports throughout the application.

</details>
<details>
<summary>apps/watch/src/components/Footer/Footer.tsx (1)</summary>

`15-25`: **Consider explaining the rationale for forcing English language in navigation links.**

All translation calls now explicitly specify English language with `{ lng: 'en' }`, forcing these navigation links to always appear in English regardless of the user's selected language. While this provides consistency for navigation elements, it could impact the user experience for non-English speakers.


Is this intentional design? If so, consider documenting this decision in a comment for future maintainers. Alternatively, if the navigation elements should be translatable, these parameters should be removed.

</details>
<details>
<summary>apps/watch/pages/watch/[part1]/[part2]/[part3].tsx (1)</summary>

`23-23`: <details>
<summary>✅ Verification successful</summary>

**Well-implemented language ID transformation for GraphQL queries.**

The utility function `getVideoLanguageId` is now used to transform the language slug from the URL into the appropriate language ID for GraphQL queries. This ensures consistency between URL parameters and API queries.



Let's verify that all language slug to ID mappings work as expected:




Also applies to: 115-117

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the implementation of the language mapping function and test with a few examples

echo "Checking slugToIdMap implementation..."
cat apps/watch/src/libs/getVideoLanguageId.ts

echo "Testing with a few sample language slugs..."
# Create a small test script
cat > /tmp/test-lang-mapping.js << 'EOL'
const { getVideoLanguageId } = require('./apps/watch/src/libs/getVideoLanguageId');
console.log('english -> ', getVideoLanguageId('english'));
console.log('spanish -> ', getVideoLanguageId('spanish'));
console.log('french -> ', getVideoLanguageId('french'));
console.log('undefined -> ', getVideoLanguageId(undefined));
console.log('null -> ', getVideoLanguageId(null));
EOL

# Run the test script
node /tmp/test-lang-mapping.js
```

Length of output: 2078

---

🏁 Script executed:

```shell
#!/bin/bash
echo "Displaying languageSlugs definitions..."
cat apps/watch/src/libs/languageSlugs.ts
```

Length of output: 54451

---



</details>

**Validated getVideoLanguageId mapping**

- `getVideoLanguageId('english')` → `"529"`  
- `getVideoLanguageId('french')`  → `"496"`  
- `getVideoLanguageId(undefined | null)` → `undefined`  
- `getVideoLanguageId('spanish')` → `undefined` (there are only region-specific Spanish slugs: `spanish-latin-american`, `spanish-castilian`)

The reverse mapping is built correctly from `languageSlugs`. No changes required.

</details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx (2)</summary>

`8-8`: **Added import for array element access pattern change**

The addition of the `last` function from lodash is part of the broader pattern in this PR to use the last element of content arrays rather than the first one.

---

`163-163`: **Improved content handling using last localized value**

This change from `description[0]?.value` to `last(description)?.value` ensures the component displays the most relevant localized description. This approach is more robust for internationalized content.

</details>
<details>
<summary>apps/watch/pages/watch/[part1]/[part2].tsx (2)</summary>

`16-16`: **Added language ID mapping utility import**

The import of `getVideoLanguageId` supports proper language handling for the GraphQL queries, aligning with the PR's objective to improve internationalization support.

---

`105-107`: **Enhanced language handling in GraphQL query**

The addition of the `languageId` parameter using `getVideoLanguageId()` improves handling of internationalized content by explicitly passing the language ID to the GraphQL query. This enables proper language-specific content retrieval.

</details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoHeading/VideoHeading.tsx (2)</summary>

`9-9`: **Added lodash utility for consistent content handling**

Import of the `last` function from lodash follows the pattern established in this PR for accessing localized content.

---

`54-54`: **Updated to use last localized title**

Changing from `title[0].value` to `last(title)?.value` ensures the component displays the most appropriate localized title, consistent with other components in this PR.

</details>
<details>
<summary>apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx (2)</summary>

`4-4`: **Added lodash utility for content access pattern**

The import of `last` from lodash supports the consistent pattern of accessing the last element in content arrays across the application.

---

`46-46`: **Improved snippet handling with fallback**

The change from `snippet[0].value` to `last(snippet)?.value ?? ''` provides a more robust way to access the localized content with a fallback to an empty string when no content is available.

</details>
<details>
<summary>apps/watch/src/components/VideoCard/VideoCard.tsx (4)</summary>

`9-9`: **LGTM: Added lodash dependency for array handling.**

Import of `last` from lodash is a good choice for safely accessing the last element of potentially empty arrays.

---

`119-123`: **LGTM: Improved robustness with last element access.**

The change from accessing the first element (`[0]`) to using `last()` is a good improvement. This provides better consistency across the codebase for how video metadata is accessed, and handles potentially empty arrays more safely with optional chaining.

---

`191-191`: **LGTM: Consistent use of last element for title display.**

Consistently using the last element for title display improves the component's behavior with multilingual content.

---

`304-304`: **LGTM: Consistent pattern application.**

The same pattern is correctly applied to the expanded variant of the component.

</details>
<details>
<summary>apps/watch/src/components/ShareDialog/ShareDialog.tsx (6)</summary>

`11-11`: **LGTM: Added lodash dependency for array handling.**

Import of `last` from lodash is a good choice for safely accessing the last element of potentially empty arrays.

---

`45-48`: **LGTM: Improved robustness with last element access.**

Using `last()` instead of index access provides better consistency across the codebase for how descriptions and snippets are accessed.

---

`107-107`: **Good: Added internationalization support for dialog title.**

Wrapping the dialog title with the translation function improves internationalization support.

---

`138-138`: **LGTM: Consistent use of last element for title display.**

Using the last element of the title array is consistent with the pattern used throughout the app.

---

`141-142`: **Good: Added null safety with optional chaining.**

Adding optional chaining to the `split()` method call helps prevent potential runtime errors if `shareDescription` is undefined.

---

`175-176`: **Good: Added internationalization support for tab labels.**

Wrapping tab labels with the translation function improves internationalization support.

</details>
<details>
<summary>apps/watch/src/components/VideoContainerPage/ContainerHero/ContainerHero.tsx (3)</summary>

`7-7`: **LGTM: Added lodash dependency for array handling.**

Import of `last` from lodash is a good choice for safely accessing the last element of potentially empty arrays.

---

`37-40`: **LGTM: Improved robustness with last element access.**

Using `last()` instead of index access provides better consistency across the codebase and safely handles potentially empty arrays with optional chaining and nullish coalescing.

---

`78-78`: **LGTM: Consistent use of last element for title display.**

Using the last element of the title array is consistent with the pattern used throughout the app for multilingual content.

</details>
<details>
<summary>apps/watch/src/components/DownloadDialog/DownloadDialog.tsx (5)</summary>

`17-17`: **LGTM: Added lodash dependency for array handling.**

Import of `last` from lodash is a good choice for safely accessing the last element of potentially empty arrays.

---

`84-86`: **Good: Added internationalization support for dialog title.**

Wrapping the dialog title with the translation function improves internationalization support.

---

`139-140`: **LGTM: Consistent use of last element for title display.**

Using the last element of the title array is consistent with the pattern used throughout the app for multilingual content.

---

`157-158`: **Good: Added internationalization support for form labels.**

Wrapping form labels with the translation function improves internationalization support.

---

`201-230`: **Good: Added internationalization support for UI text.**

Wrapping checkbox labels, link text, and button text with the translation function improves internationalization support.

</details>
<details>
<summary>apps/watch/src/libs/getVideoLanguageId.ts (2)</summary>

`1-15`: **Well-implemented reverse mapping for language slugs to IDs.**

The implementation of the reverse mapping from language slugs to their corresponding IDs is clean and efficient, using a functional approach with `reduce`.

---

`25-30`: **Good implementation with null safety.**

The function properly handles undefined or null input by immediately returning undefined, which is a good defensive programming practice.

</details>
<details>
<summary>apps/watch/src/libs/languageSlugs.ts (2)</summary>

`1-10`: **Well-documented auto-generated file.**

The header comment clearly indicates this is an auto-generated file with important information about its source, generation time, and purpose. This prevents accidental manual edits and provides context for developers.

---

`11-2246`: **Comprehensive language mapping with appropriate structure.**

The exported `languageSlugs` object provides a thorough mapping of language IDs to language slugs, which serves as a good foundation for the language ID lookup functionality. The structure (Record<string, string>) is appropriate for the lookup purposes needed in the application.

</details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx (2)</summary>

`8-8`: **Added lodash utility for safer array access.**

Using `last` from lodash provides safer array access than direct indexing, particularly for potentially undefined arrays.

---

`77-77`: **Consistent use of last element for title.**

This change aligns with the pattern of using the last available value from the title array, maintaining consistency with the image handling changes.

</details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContentPage.tsx (2)</summary>

`4-4`: **Added lodash utility for consistent array access.**

Using `last` from lodash aligns with the pattern used in other components and provides more robust handling of potentially undefined arrays.

---

`55-74`: **OpenGraph properties consistently use last localized values.**

Updated the OpenGraph metadata to use last elements of arrays for title, description, and image properties, maintaining consistency with the SEO changes above.

</details>
<details>
<summary>apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.spec.tsx (6)</summary>

`1-36`: **Good test setup and mocking approach.**

The test file is well-structured with proper mocking of Next.js router and i18n functionality. The mock data is appropriately set up to provide the necessary context for testing the component.

---

`38-45`: **LGTM. Good test coverage for language button rendering.**

This test effectively verifies that all supported language buttons are correctly rendered in the document.

---

`47-57`: **LGTM. Language change functionality is well tested.**

The test correctly verifies the user interaction flow: clicking a language button updates the locale, navigates to the correct path, and closes the dialog.

---

`59-64`: **LGTM. Contact button rendering is properly tested.**

The test correctly verifies that the contact button is rendered with the appropriate href attribute.

---

`66-73`: **LGTM. Close button functionality is properly tested.**

The test correctly verifies that clicking the close button calls the handleClose function.

---

`75-79`: **LGTM. Conditional rendering is properly tested.**

This test correctly verifies that the dialog doesn't render when the open prop is false.

</details>
<details>
<summary>apps/watch/src/components/Header/LocalAppBar/LocalAppBar.tsx (4)</summary>

`1-1`: **LGTM. Appropriate imports added for the language selector.**

The imports for LanguageRoundedIcon, dynamic, and useState are correctly added to support the new language selector feature.



Also applies to: 7-7, 10-10

---

`14-21`: **LGTM. Dynamic import for language dialog improves performance.**

Good use of dynamic import with the `dynamic` function from Next.js. This approach optimizes bundle size by code-splitting and loading the LanguageSwitchDialog component only when needed. The SSR false option is appropriate for this UI component.

---

`35-35`: **LGTM. State hook for dialog visibility.**

Proper use of useState for managing the visibility of the language selector dialog.

---

`86-103`: **LGTM. Well-integrated language selector UI.**

The language selector button and dialog are well-integrated into the existing UI. The implementation follows the same styling patterns as other elements in the app bar, and includes appropriate accessibility attributes.

</details>
<details>
<summary>apps/watch/src/libs/useVideoChildren/useVideoChildren.ts (1)</summary>

`8-8`: **LGTM. Appropriate import for the language ID utility.**

The import of getVideoLanguageId is correctly added to support language-specific video queries.

</details>
<details>
<summary>libs/locales/en/apps-watch.json (1)</summary>

`3-5`: **LGTM. Appropriate localization strings added.**

The new localization strings for video download, confirmation actions, contact information, and sharing functionality are well-structured and follow the existing format. These additions will properly support the new UI elements in the application.



Also applies to: 8-9, 29-29, 31-33

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T04:53:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx (1)</summary><blockquote>

`89-89`: **LGTM! This change aligns with the internationalization improvements.**

This update to use `videos[5]` instead of `videos[0]` supports the new locale-aware video fetching functionality. The test remains valid as it's still checking for the "Reflections of Hope" heading from the mock response.


Consider adding a brief comment explaining why `videos[5]` is used specifically, which would help future developers understand this test setup choice:

```diff
-          <VideoProvider value={{ content: videos[5] }}>
+          {/* Using videos[5] for language-specific testing */}
+          <VideoProvider value={{ content: videos[5] }}>
```

</blockquote></details>
<details>
<summary>apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts (1)</summary><blockquote>

`4-16`: **LGTM! Comprehensive test coverage for valid locales.**

The test thoroughly verifies all valid locale mappings. To improve maintainability, consider importing UI_LANGUAGE_IDS and using it directly in the expectations to avoid hardcoding IDs that might change in the future:

```diff
-    expect(getLanguageIdFromLocale('en')).toBe('529')
+    expect(getLanguageIdFromLocale('en')).toBe(UI_LANGUAGE_IDS.en)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cc5929d0743d903f0f04b117ea3610d047526fba and 3efb4bed663396c1dc12284a05ddc25c0c5c2b1d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `apps/watch/pages/watch/[part1]/[part2].tsx` (2 hunks)
* `apps/watch/pages/watch/[part1]/[part2]/[part3].tsx` (2 hunks)
* `apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx` (1 hunks)
* `apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx` (3 hunks)
* `apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx` (1 hunks)
* `apps/watch/src/components/VideoContentPage/VideoContentPage.tsx` (4 hunks)
* `apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts` (1 hunks)
* `apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts` (1 hunks)
* `apps/watch/src/libs/getLanguageIdFromLocale/index.ts` (1 hunks)
* `apps/watch/src/libs/useVideoChildren/useVideoChildren.ts` (3 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/watch/src/libs/getLanguageIdFromLocale/index.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx
* apps/watch/pages/watch/[part1]/[part2]/[part3].tsx
* apps/watch/pages/watch/[part1]/[part2].tsx
* apps/watch/src/components/VideoContentPage/VideoContentPage.tsx
* apps/watch/src/libs/useVideoChildren/useVideoChildren.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/components/Videos/__generated__/testData.ts (1)</summary>

* `videos` (10-1390)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx (1)</summary><blockquote>

<details>
<summary>apps/watch/src/components/Videos/__generated__/testData.ts (1)</summary>

* `videos` (10-1390)

</details>

</blockquote></details>
<details>
<summary>apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts (1)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts (1)</summary>

* `getLanguageIdFromLocale` (45-53)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Deploy Preview and Test (watch, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (videos-admin, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (journeys-admin, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (short-links, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (arclight, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (journeys, 6636/merge, pull_request, 22)
* GitHub Check: test (22, 3/3)
* GitHub Check: build (22)
* GitHub Check: test (22, 2/3)
* GitHub Check: test (22, 1/3)
* GitHub Check: Analyze (javascript)
* GitHub Check: lint (22)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts (3)</summary>

`4-17`: **Missing mapping for 'please-build' in UI_LANGUAGE_IDS.**

The 'please-build' language code is included in UI_LANGUAGE_CODES but doesn't have a corresponding entry in the UI_LANGUAGE_IDS mapping. This could potentially cause issues if this code is ever used for lookup.

Is this intentional? If 'please-build' is only meant for development purposes and should never be used in production, consider adding a comment explaining this or removing it from the array.

---

`22-34`: **LGTM! Good use of TypeScript type safety.**

The UI_LANGUAGE_IDS mapping is well-structured with proper type annotations (Readonly<Record<string, string>>) and helpful comments for each language. The "as const" assertion ensures type safety when accessing these values.

---

`45-53`: **LGTM! Well-implemented function with proper fallback.**

The function correctly handles all edge cases (null, undefined, unsupported locales) by defaulting to English ('529'). The implementation is simple, readable, and well-documented with JSDoc comments.

</details>
<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContentPage.spec.tsx (1)</summary>

`92-92`: **LGTM! Test correctly updated to match component changes.**

The change from videos[0] to videos[1] appropriately aligns this test with the component's updated locale-aware behavior. This ensures the test accurately reflects how the component now handles video content in relation to language settings.

</details>
<details>
<summary>apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts (1)</summary>

`18-28`: **LGTM! Good edge case coverage.**

The tests properly verify the function's behavior with undefined, null, and unsupported locales, ensuring it always defaults to the English language ID in these cases.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T02:41:25Z
user: login: levi-zustiak
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T22:37:34Z
user: login: csiyang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T23:06:19Z
user: login: csiyang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T23:17:34Z
user: login: levi-zustiak
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T23:41:45Z
user: login: levi-zustiak
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T23:42:16Z
user: login: levi-zustiak
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T16:14:10Z

Review comments:
diff_hunk: @@ -1,3 +1,4 @@
+import last from 'lodash/last'
path: apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Change to use last localized value instead of first.**

This change updates the `video_title` in Mux metadata to use the last element from the `title` array instead of the first element. This appears to be part of a broader change across the codebase to use the last available localized values.






Also applies to: 32-32

---

🏁 Script executed:

```shell
#!/bin/bash
# Find other instances of similar changes to verify consistency

echo "Looking for other changes from first array element to last..."
rg -A 1 "title\?\.\[0\]\?" --type=ts --type=tsx
rg -A 1 "last\(title\)" --type=ts --type=tsx

echo "Checking for patterns with other localized arrays like description..."
rg -A 1 "description\?\.\[0\]\?" --type=ts --type=tsx
rg -A 1 "last\(description\)" --type=ts --type=tsx
```

Length of output: 599

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for literal optional-chained array-access patterns and their replacements
echo "🔍 Searching for 'title?.[0]' in .ts/.tsx files"
rg -F "title?.[0]" -A1 -g '*.ts' -g '*.tsx'

echo "🔍 Searching for 'last(title)' in .ts/.tsx files"
rg -F "last(title)" -A1 -g '*.ts' -g '*.tsx'

echo "🔍 Searching for 'description?.[0]' in .ts/.tsx files"
rg -F "description?.[0]" -A1 -g '*.ts' -g '*.tsx'

echo "🔍 Searching for 'last(description)' in .ts/.tsx files"
rg -F "last(description)" -A1 -g '*.ts' -g '*.tsx'
```

Length of output: 5291

---



</details>

**Ensure consistent use of `last()` across all apps**  

While you’ve updated `VideoPlayer.tsx` in `apps/watch`, there are still multiple instances where the first localized element (`?.[0]?.value`) is used elsewhere. To maintain consistency, please replace these with `last(...)? .value`.  

Files requiring updates (non‐exhaustive list):  
- apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/nodes/StepBlockNode/libs/getCardMetadata/getCardMetadata.ts  
  - `priorityBlock.mediaVideo?.title?.[0]?.value`  
- apps/journeys-admin/src/components/Editor/Slider/JourneyFlow/nodes/StepBlockNode/ActionButton/ActionButton.tsx  
  - `block.video?.title?.[0]?.value`  
- apps/journeys-admin/src/components/Editor/Slider/Settings/CanvasDetails/Properties/blocks/Video/Video.tsx  
  - `block.mediaVideo?.title?.[0]?.value`  
- apps/videos-admin/src/app/(dashboard)/videos/[videoId]/layout.tsx  
  - `video?.title?.[0]?.value`  
- apps/arclight/src/app/videoPlayerUrl/page.tsx  
  - `videoTitleData?.video?.title?.[0]?.value`  

Also review any `description?.[0]?.value` usages and convert them to `last(description)?.value`.  

By aligning all localized‐array accesses to use `last()`, we ensure the app uniformly selects the most appropriate (typically latest) localization.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
apps/watch/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx
at line 1, ensure that all localized array accesses for titles and descriptions
use the last element instead of the first. Replace any occurrences of accessing
the first element with optional chaining like title?.[0]?.value or
description?.[0]?.value with last(title)?.value or last(description)?.value
respectively, importing lodash's last function if not already done. Apply this
pattern consistently across all similar files listed in the comment to maintain
uniform localization handling.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T04:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,8 +172,12 @@ export function DownloadDialog({
                   )
                   .map((download) => (
                     <MenuItem key={download.quality} value={download.url}>
-                      {download.quality.charAt(0).toUpperCase()}
-                      {download.quality.slice(1)} ({formatBytes(download.size)})
+                      {t(
+                        download.quality === VideoVariantDownloadQuality.high
+                          ? t('High')
+                          : t('Low')
+                      )}{' '}
+                      ({formatBytes(download.size)})
                     </MenuItem>
path: apps/watch/src/components/DownloadDialog/DownloadDialog.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix nested translation function calls.**

There's an unnecessary nesting of translation function calls that should be simplified.

```diff
-                     {t(
-                       download.quality === VideoVariantDownloadQuality.high
-                         ? t('High')
-                         : t('Low')
-                     )}{' '}
+                     {download.quality === VideoVariantDownloadQuality.high
+                       ? t('High')
+                       : t('Low')
+                     }{' '}
                      ({formatBytes(download.size)})
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
                      {download.quality === VideoVariantDownloadQuality.high
                        ? t('High')
                        : t('Low')
                      }{' '}
                      ({formatBytes(download.size)})
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/watch/src/components/DownloadDialog/DownloadDialog.tsx around lines 175
to 181, the translation function t is being called twice unnecessarily in a
nested manner. Remove the inner t calls so that the conditional expression
directly returns the translation keys 'High' or 'Low', and then call t once on
the result. This will simplify the code and avoid redundant translation function
calls.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T04:53:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,178 @@
+import CloseIcon from '@mui/icons-material/Close'
+import EmailIcon from '@mui/icons-material/Email'
+import Box from '@mui/material/Box'
+import Button from '@mui/material/Button'
+import Container from '@mui/material/Container'
+import Dialog from '@mui/material/Dialog'
+import IconButton from '@mui/material/IconButton'
+import Slide from '@mui/material/Slide'
+import Stack from '@mui/material/Stack'
+import { TransitionProps } from '@mui/material/transitions'
+import { useRouter } from 'next/router'
+import { useTranslation } from 'next-i18next'
+import { ReactElement, Ref, forwardRef, useEffect, useState } from 'react'
+
+const Transition = forwardRef(function Transition(
+  props: TransitionProps & {
+    children: ReactElement
+  },
+  ref: Ref<unknown>
+) {
+  return <Slide direction="left" ref={ref} {...props} />
+})
+
+interface Language {
+  languageCode: string
+  localName: string
+  nativeName?: string
+}
+
+interface LanguageSwitchDialogProps {
+  open: boolean
+  handleClose: () => void
+}
+
+export function LanguageSwitchDialog({
+  open,
+  handleClose
+}: LanguageSwitchDialogProps): ReactElement {
+  const { t, i18n } = useTranslation('apps-watch')
+  const router = useRouter()
+
+  const [languages, setLanguages] = useState<Language[]>([])
+  const [currentLanguageCode, setCurrentLanguageCode] = useState(
+    i18n?.language ?? 'en'
+  )
+
+  function handleChange(languageCode: string): void {
+    setCurrentLanguageCode(languageCode)
+
+    const cookieFingerprint = '00005'
+    document.cookie = `NEXT_LOCALE=${cookieFingerprint}---${languageCode}; path=/`
+    const path = router.asPath
+    void router.push(path, path, { locale: languageCode })
+    handleClose()
+  }
+
+  useEffect(() => {
+    const supportedLanguageCodes = (
+      i18n.options as unknown as { locales: string[] }
+    )?.locales
+
+    if (supportedLanguageCodes == null) return
+    const formattedLanguages = supportedLanguageCodes
+      .filter((languageCode) => languageCode !== 'zh')
+      .map((languageCode): Language => {
+        const nativeName = new Intl.DisplayNames([currentLanguageCode], {
+          type: 'language'
+        }).of(languageCode)
+        const localName = new Intl.DisplayNames([languageCode], {
+          type: 'language'
+        }).of(languageCode)
path: apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for Intl.DisplayNames usage**

The `Intl.DisplayNames` API might not be supported in all browsers or environments. Consider adding error handling to gracefully degrade if the API is unavailable.


```diff
-        const nativeName = new Intl.DisplayNames([currentLanguageCode], {
-          type: 'language'
-        }).of(languageCode)
-        const localName = new Intl.DisplayNames([languageCode], {
-          type: 'language'
-        }).of(languageCode)
+        let nativeName, localName;
+        try {
+          nativeName = new Intl.DisplayNames([currentLanguageCode], {
+            type: 'language'
+          }).of(languageCode)
+          localName = new Intl.DisplayNames([languageCode], {
+            type: 'language'
+          }).of(languageCode)
+        } catch (error) {
+          // Fallback for browsers without Intl.DisplayNames support
+          console.error('Error using Intl.DisplayNames:', error);
+          nativeName = languageCode;
+          localName = languageCode;
+        }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        let nativeName, localName;
        try {
          nativeName = new Intl.DisplayNames([currentLanguageCode], {
            type: 'language'
          }).of(languageCode)
          localName = new Intl.DisplayNames([languageCode], {
            type: 'language'
          }).of(languageCode)
        } catch (error) {
          // Fallback for browsers without Intl.DisplayNames support
          console.error('Error using Intl.DisplayNames:', error);
          nativeName = languageCode;
          localName = languageCode;
        }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx
around lines 66 to 71, the usage of Intl.DisplayNames lacks error handling for
environments where this API might be unsupported. Wrap the creation and usage of
Intl.DisplayNames in a try-catch block to catch potential errors. In the catch
block, provide a fallback value for nativeName and localName, such as defaulting
to the languageCode string itself, to ensure graceful degradation without
breaking the component.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T04:53:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,178 @@
+import CloseIcon from '@mui/icons-material/Close'
+import EmailIcon from '@mui/icons-material/Email'
+import Box from '@mui/material/Box'
+import Button from '@mui/material/Button'
+import Container from '@mui/material/Container'
+import Dialog from '@mui/material/Dialog'
+import IconButton from '@mui/material/IconButton'
+import Slide from '@mui/material/Slide'
+import Stack from '@mui/material/Stack'
+import { TransitionProps } from '@mui/material/transitions'
+import { useRouter } from 'next/router'
+import { useTranslation } from 'next-i18next'
+import { ReactElement, Ref, forwardRef, useEffect, useState } from 'react'
+
+const Transition = forwardRef(function Transition(
+  props: TransitionProps & {
+    children: ReactElement
+  },
+  ref: Ref<unknown>
+) {
+  return <Slide direction="left" ref={ref} {...props} />
+})
+
+interface Language {
+  languageCode: string
+  localName: string
+  nativeName?: string
+}
+
+interface LanguageSwitchDialogProps {
+  open: boolean
+  handleClose: () => void
+}
+
+export function LanguageSwitchDialog({
+  open,
+  handleClose
+}: LanguageSwitchDialogProps): ReactElement {
+  const { t, i18n } = useTranslation('apps-watch')
+  const router = useRouter()
+
+  const [languages, setLanguages] = useState<Language[]>([])
+  const [currentLanguageCode, setCurrentLanguageCode] = useState(
+    i18n?.language ?? 'en'
+  )
+
+  function handleChange(languageCode: string): void {
+    setCurrentLanguageCode(languageCode)
+
+    const cookieFingerprint = '00005'
+    document.cookie = `NEXT_LOCALE=${cookieFingerprint}---${languageCode}; path=/`
+    const path = router.asPath
+    void router.push(path, path, { locale: languageCode })
+    handleClose()
+  }
+
+  useEffect(() => {
+    const supportedLanguageCodes = (
+      i18n.options as unknown as { locales: string[] }
+    )?.locales
+
+    if (supportedLanguageCodes == null) return
+    const formattedLanguages = supportedLanguageCodes
+      .filter((languageCode) => languageCode !== 'zh')
+      .map((languageCode): Language => {
+        const nativeName = new Intl.DisplayNames([currentLanguageCode], {
+          type: 'language'
+        }).of(languageCode)
+        const localName = new Intl.DisplayNames([languageCode], {
+          type: 'language'
+        }).of(languageCode)
+
+        return {
+          languageCode,
+          nativeName: nativeName === localName ? undefined : nativeName,
+          localName: localName ?? ''
+        }
+      })
+    setLanguages(formattedLanguages)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [currentLanguageCode])
path: apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix the dependency array in useEffect**

The useEffect dependency array only includes `currentLanguageCode`, but the effect also depends on `i18n.options`. This could cause stale data if i18n.options changes.


```diff
  useEffect(() => {
    const supportedLanguageCodes = (
      i18n.options as unknown as { locales: string[] }
    )?.locales

    if (supportedLanguageCodes == null) return
    const formattedLanguages = supportedLanguageCodes
      .filter((languageCode) => languageCode !== 'zh')
      .map((languageCode): Language => {
        const nativeName = new Intl.DisplayNames([currentLanguageCode], {
          type: 'language'
        }).of(languageCode)
        const localName = new Intl.DisplayNames([languageCode], {
          type: 'language'
        }).of(languageCode)

        return {
          languageCode,
          nativeName: nativeName === localName ? undefined : nativeName,
          localName: localName ?? ''
        }
      })
    setLanguages(formattedLanguages)
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [currentLanguageCode])
+  }, [currentLanguageCode, i18n.options])
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  useEffect(() => {
    const supportedLanguageCodes = (
      i18n.options as unknown as { locales: string[] }
    )?.locales

    if (supportedLanguageCodes == null) return
    const formattedLanguages = supportedLanguageCodes
      .filter((languageCode) => languageCode !== 'zh')
      .map((languageCode): Language => {
        const nativeName = new Intl.DisplayNames([currentLanguageCode], {
          type: 'language'
        }).of(languageCode)
        const localName = new Intl.DisplayNames([languageCode], {
          type: 'language'
        }).of(languageCode)

        return {
          languageCode,
          nativeName: nativeName === localName ? undefined : nativeName,
          localName: localName ?? ''
        }
      })
    setLanguages(formattedLanguages)
  }, [currentLanguageCode, i18n.options])
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 ESLint</summary>

[error] 80-80: Definition for rule 'react-hooks/exhaustive-deps' was not found.

(react-hooks/exhaustive-deps)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx
around lines 57 to 81, the useEffect hook's dependency array currently only
includes currentLanguageCode, but it also depends on i18n.options. To fix this,
add i18n.options to the dependency array to ensure the effect runs whenever
i18n.options changes, preventing stale data issues.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T04:53:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,6 +5,7 @@ import Button from '@mui/material/Button'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
path: apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
user: login: levi-zustiak
body: Was something new added to require the last item?
created_at: 2025-05-15T22:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,7 +22,7 @@ export const VIDEO_CONTENT_FIELDS = gql`
     title(languageId: $languageId, primary: true) {
       value
     }
-    variant(languageId: $languageId) {
+    variant {
path: apps/watch/src/libs/videoContentFields.ts
user: login: levi-zustiak
body: Could you explain why this was removed?
created_at: 2025-05-15T22:36:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,6 +5,7 @@ import Button from '@mui/material/Button'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
path: apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
user: login: csiyang
body: Yes its expected that the first element of the text arrays (eg title), the first item is expected to be english, and the second one is expected to be the one from the requested translation.

But sometimes there is no translation available, so it should still default to showing english.
created_at: 2025-05-18T23:06:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,7 +22,7 @@ export const VIDEO_CONTENT_FIELDS = gql`
     title(languageId: $languageId, primary: true) {
       value
     }
-    variant(languageId: $languageId) {
+    variant {
path: apps/watch/src/libs/videoContentFields.ts
user: login: csiyang
body: I think there's a backend bug here. When a languageId is passed in for the text translations, it conflicts with the translation being used for the audio, making it return no variants.
created_at: 2025-05-18T23:17:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,6 +5,7 @@ import Button from '@mui/material/Button'
 import Container from '@mui/material/Container'
 import Stack from '@mui/material/Stack'
 import Typography from '@mui/material/Typography'
+import last from 'lodash/last'
path: apps/watch/src/components/VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx
user: login: levi-zustiak
body: Okay, sounds good 👍 
created_at: 2025-05-18T23:41:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-05-14T04:40:16Z
body: <p><a href="https://linear.app/jesus-film-project/issue/PRJ-2718/update-watch-front-page-to-use-text-translation-from-locale">PRJ-2718 Update watch front page to use text translation from locale</a></p>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-14T04:40:20Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces a language-switching feature and improves internationalization in the Watch app. It adds a `LanguageSwitchDialog` component, integrates it into the app bar, and updates several components to use locale-aware strings and language IDs. Array element access for titles, images, and descriptions is standardized to use the last element. New localization keys and tests are also included.

## Changes

| File(s)                                                                                      | Change Summary |
|----------------------------------------------------------------------------------------------|---------------|
| `apps/watch/pages/watch/[part1]/[part2].tsx`, `apps/watch/pages/watch/[part1]/[part2]/[part3].tsx` | Added import of `getLanguageIdFromLocale` and updated GraphQL query variables to include `languageId` derived from locale. |
| `apps/watch/src/libs/useVideoChildren/useVideoChildren.ts`                                   | Updated `useVideoChildren` hook to accept optional `locale` parameter and use `getLanguageIdFromLocale` to pass `languageId` to GraphQL queries. |
| `apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.ts`, `apps/watch/src/libs/getLanguageIdFromLocale/index.ts` | Introduced utility and export for mapping locales to language IDs; exported `getLanguageIdFromLocale`. |
| `apps/watch/src/libs/getLanguageIdFromLocale/getLanguageIdFromLocale.spec.ts`                | Added tests for `getLanguageIdFromLocale` function. |
| `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.tsx`, `index.ts`        | Added new `LanguageSwitchDialog` component and export. |
| `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.spec.tsx`               | Added tests for the `LanguageSwitchDialog` component. |
| `apps/watch/src/components/LanguageSwitchDialog/LanguageSwitchDialog.stories.tsx`            | Added Storybook stories for `LanguageSwitchDialog`. |
| `apps/watch/src/components/Header/LocalAppBar/LocalAppBar.tsx`                              | Integrated `LanguageSwitchDialog` into the app bar with new state and icon button for language selection. |
| `apps/watch/src/components/DownloadDialog/DownloadDialog.tsx`, `TermsOfUseDialog/TermsOfUseDialog.tsx`, `ShareDialog/ShareDialog.tsx` | Wrapped static UI strings with translation function `t()` for internationalization; improved array access with `last`. |
| `apps/watch/src/components/Footer/Footer.tsx`, `Header/HeaderMenuPanel/HeaderMenuPanel.tsx`, `Header/HeaderTabButtons/HeaderTabButtons.tsx` | Updated all `t()` translation calls to explicitly use English locale (`{ lng: 'en' }`). |
| `apps/watch/src/components/VideoCard/VideoCard.tsx`, `VideoContainerPage/ContainerHero/ContainerHero.tsx`, `VideoContentPage/VideoContent/VideoContent.tsx`, `VideoContentPage/VideoContentPage.tsx`, `VideoContentPage/VideoHeading/VideoHeading.tsx`, `VideoContentPage/VideoHero/VideoHeroOverlay/VideoHeroOverlay.tsx`, `VideoContentPage/VideoHero/VideoPlayer/VideoPlayer.tsx` | Standardized array element access to use `last` from lodash for titles, images, descriptions, and alt texts. |
| `apps/watch/src/components/VideoContainerPage/VideoContainerPage.tsx`, `VideoContentPage/VideoContentPage.tsx` | Passed current locale from router to `useVideoChildren` hook; improved array access with `last`. |
| `apps/watch/src/components/VideoContainerPage/VideoContainerPage.spec.tsx`, `VideoContentPage/VideoContentPage.spec.tsx` | Updated tests to use different video objects for context, aligning with new array access logic. |
| `libs/locales/en/apps-watch.json`                                                            | Added new English localization keys for download, sharing, and dialog UI. |
| `apps/watch/src/components/AudioLanguageDialog/AudioLanguageDialog.tsx`                      | Added internationalization support by wrapping static text with translation function `t()`. |
| `apps/watch/src/libs/videoContentFields.ts`                                                 | Removed `languageId` argument from `variant` field in GraphQL fragment; added `primary` field to `studyQuestions`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant LocalAppBar
    participant LanguageSwitchDialog
    participant NextRouter
    participant Cookie

    User->>LocalAppBar: Clicks language icon
    LocalAppBar->>LanguageSwitchDialog: Opens dialog
    User->>LanguageSwitchDialog: Selects language
    LanguageSwitchDialog->>Cookie: Set language cookie
    LanguageSwitchDialog->>NextRouter: Push route with new locale
    LanguageSwitchDialog->>LocalAppBar: Calls handleClose
```

```mermaid
sequenceDiagram
    participant PageComponent
    participant useVideoChildren
    participant getLanguageIdFromLocale

    PageComponent->>useVideoChildren: Call with slug, locale
    useVideoChildren->>getLanguageIdFromLocale: Get languageId from locale
    getLanguageIdFromLocale-->>useVideoChildren: Return languageId
    useVideoChildren->>PageComponent: Return video data (with languageId)
```

## Possibly related PRs

- [JesusFilm/core#6589](https://github.com/JesusFilm/core/pull/6589): The main PR and this related PR both introduce the `LanguageSwitchDialog` component and integrate it into the `LocalAppBar` component to provide language switching functionality, making their changes directly related at the code level.

## Suggested reviewers

- levi-zustiak

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> npm warn config production Use `--omit=dev` instead.
> npm warn EBADENGINE Unsupported engine {
> npm warn EBADENGINE   package: 'i18next-parser@9.3.0',
> npm warn EBADENGINE   required: { node: '^18.0.0 || ^20.0.0 || ^22.0.0', npm: '>=6', yarn: '>=1' },
> npm warn EBADENGINE   current: { node: 'v24.0.2', npm: '11.3.0' }
> npm warn EBADENGINE }
> npm warn EBADENGINE Unsupported engine {
> npm warn EBADENGINE   package: 'next-firebase-auth-edge@1.9.1',
> npm warn EBADENGINE   required: {
> npm warn EBADENGINE     node: '>=16.0.0 <24.0.0',
> npm warn EBADENGINE     npm: '>=8.0.0 <12.0.0',
> npm warn EBADENGINE     yarn: '>=1.22.0 <2.0.0'
> npm warn EBADENGINE   },
> npm warn EBADENGINE   current: { node: 'v24.0.2', npm: '11.3.0' }
> npm warn EBADENGINE }
> npm error Exit handler never called!
> npm error This is an error with npm itself. Please report this error at:
> npm error   <https://github.com/npm/cli/issues>
> npm error A complete log of this run can be found in: /.npm/_logs/2025-05-22T03_21_35_017Z-debug-0.log
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 37597de4646e8c1cae8786dd3a9d530292040717 and 694e06f64dd5481bed3dac37f038fd4450aecb95.

</details>

<details>
<summary>⛔ Files ignored due to path filters (7)</summary>

* `apps/watch/__generated__/GetVideoContainerPart2.ts` is excluded by `!**/__generated__/**`
* `apps/watch/__generated__/GetVideoContent.ts` is excluded by `!**/__generated__/**`
* `apps/watch/__generated__/GetVideoContentPart3.ts` is excluded by `!**/__generated__/**`
* `apps/watch/__generated__/GetVideosForTestData.ts` is excluded by `!**/__generated__/**`
* `apps/watch/__generated__/VideoContentFields.ts` is excluded by `!**/__generated__/**`
* `apps/watch/src/components/Videos/__generated__/testData.ts` is excluded by `!**/__generated__/**`
* `package-lock.json` is excluded by `!**/package-lock.json`

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx` (2 hunks)
* `apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx` (5 hunks)
* `apps/watch/src/libs/useVideoChildren/useVideoChildren.ts` (3 hunks)
* `apps/watch/src/libs/videoContentFields.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* apps/watch/src/libs/videoContentFields.ts
* apps/watch/src/libs/useVideoChildren/useVideoChildren.ts
* apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (10)</summary>

* GitHub Check: Deploy Preview and Test (videos-admin, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (watch, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (journeys-admin, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (arclight, 6636/merge, pull_request, 22)
* GitHub Check: Deploy Preview and Test (journeys, 6636/merge, pull_request, 22)
* GitHub Check: test (22, 2/3)
* GitHub Check: test (22, 1/3)
* GitHub Check: build (22)
* GitHub Check: lint (22)
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>apps/watch/src/components/VideoContentPage/VideoContent/VideoContent.spec.tsx (2)</summary>

`24-36`: **Great typo fix in test description.**

The fix from "dicussion" to "discussion" improves readability and maintains consistency in test naming.

---

`37-78`: **Well-structured test for question filtering logic.**

This test properly verifies that only non-primary questions are displayed when both primary and non-primary questions exist, which aligns with the updated filtering logic in the component. The test includes:

1. Clear test data setup with both primary and non-primary questions
2. Appropriate assertions for both presence and absence of elements
3. Helpful comments explaining the test's purpose

This improves test coverage for an important filtering behavior that could be prone to regression.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=JesusFilm/core&utm_content=6636):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:43:11Z
body: <!-- add-pr-comment:docs -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **docs** | ✅ Ready | [docs preview](https://docs-6636-jesusfilm.vercel.app) | Thu May 22 15:21:53 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:43:30Z
body: <!-- add-pr-comment:short-links -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **short-links** | ✅ Ready | [short-links preview](https://short-links-6636-jesusfilm.vercel.app) | Thu May 22 15:22:21 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:43:51Z
body: <!-- add-pr-comment:watch -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **watch** | ✅ Ready | [watch preview](https://watch-6636-jesusfilm.vercel.app) | Thu May 22 15:28:45 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:43:59Z
body: <!-- add-pr-comment:arclight -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **arclight** | ✅ Ready | [arclight preview](https://arclight-6636-jesusfilm.vercel.app) | Thu May 22 15:21:57 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:44:16Z
body: <!-- add-pr-comment:journeys -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **journeys** | ✅ Ready | [journeys preview](https://journeys-6636-jesusfilm.vercel.app) | Thu May 22 15:27:42 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:44:28Z
body: <!-- add-pr-comment:videos-admin -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **videos-admin** | ✅ Ready | [videos-admin preview](https://videos-admin-6636-jesusfilm.vercel.app) | Thu May 22 15:29:00 NZST 2025 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T04:44:47Z
body: <!-- add-pr-comment:journeys-admin -->

**The latest updates on your projects**.

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------------ |
| **journeys-admin** | ✅ Ready | [journeys-admin preview](https://journeys-admin-6636-jesusfilm.vercel.app) | Thu May 22 15:29:25 NZST 2025 |
author_association: CONTRIBUTOR