Skip to content

Conversation

@IgnisDa
Copy link
Owner

@IgnisDa IgnisDa commented Jan 18, 2026

Implement per-season and per-episode translation metadata storage and retrieval for shows and podcasts. Update TMDB and iTunes providers to return translations for season and episode titles and summaries. Enhance the media translation service to handle granular fetching of translations, improving user experience during translation operations. Adjust GraphQL schema and frontend hooks to support the new translation structure. This addresses issues with missing translations and improves synchronization for TV show episodes.

Fixes #1672

Summary by CodeRabbit

  • New Features

    • Episode- and season-level translations for TV shows and episode-level translations for podcasts.
    • On-demand translation fetching with background job deployment and live polling until translations are available.
  • Improvements

    • UI now prefers translated titles, images, and descriptions with safe fallbacks to original data.
    • Translation-aware flows across dashboards, media details, history, and edit modals for more consistent localized content.

✏️ Tip: You can customize this high-level summary in your review settings.

IgnisDa and others added 30 commits January 18, 2026 11:35
…ations

Add extra information structs and JSONB columns so translation rows can be keyed by season/episode while keeping base translation uniqueness intact. This also registers a guarded migration that updates indexes for per-season/per-episode translations without breaking fresh installs.
…tions

TMDB show translations now include season and episode names/overviews, while iTunes podcast translations surface episode titles and descriptions in the preferred language. The movie translation results are updated to populate the extended translation struct defaults.
…items

Update the media translation service to write and read season/episode translation rows with extra metadata and reconstruct nested translation payloads. Translation-aware search and sorting now ignore episode-specific rows so list results remain stable for top-level titles.
Rename show/podcast translation indexes to stay under the identifier length limit and remove legacy or truncated variants during migration so fresh installs and upgrades do not collide.
Keep the migration focused on the current short index names so fresh and existing instances apply the same schema without extra compatibility logic in this development branch.
Extend the MediaTranslations query to include season and episode translation payloads so codegen captures the new schema fields. Regenerate the backend GraphQL artifacts to align frontend types with the updated query.
…ties

This commit removes the `has_translations_for_languages` column from metadata,
metadata_group, and person tables as part of the transition to granular
translation fetching.

Changes:
- Add migration m20260118_changes_for_issue_1672 to drop the columns
- Update original table creation migrations to remove column definitions
- Remove column from database models (metadata, metadata_group, person)
- Clean up v10 migration to remove references to this column

This change enables per-variant translation tracking instead of tracking
all translations for an entity at once, allowing more efficient and
granular translation fetching.

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Introduce a new cache mechanism to track ongoing translation fetch
operations, preventing duplicate requests and enabling UI status updates.

Changes:
- Add MediaTranslationInProgressCacheInput struct with entity_id,
  entity_lot, variant, and language fields
- Add ApplicationCacheKey::MediaTranslationInProgress variant
- Add ApplicationCacheValue::MediaTranslationInProgress with EmptyCacheValue
- Set TTL to 15 minutes for translation in-progress cache keys
- Remove UserEntityTranslations cache key (replaced by per-query caching)

This enables the frontend to differentiate between "not yet fetched" and
"currently being fetched" states for individual translation variants,
improving user experience during translation operations.

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactor MediaTranslationResult union from 3 variants to 2 by consolidating
InProgress and NotFetched into a single Pending variant with an enum status field.

Before:
- MediaTranslationValue { value }
- MediaTranslationInProgress { in_progress: bool }
- MediaTranslationNotFetched { not_fetched: bool }

After:
- MediaTranslationValue { value }
- MediaTranslationPending { status: MediaTranslationPendingStatus }

Where MediaTranslationPendingStatus is an enum with InProgress and NotFetched
variants.

This change eliminates redundant boolean fields and provides a cleaner,
more maintainable type structure. The enum-based approach is more idiomatic
and makes the pending states explicit without needing marker boolean fields.

Changes:
- Add MediaTranslationPendingStatus enum (InProgress, NotFetched)
- Add MediaTranslationPending struct with status field
- Update MediaTranslationResult union to use Pending variant
- Add MediaTranslationInput and MediaTranslationValue types for the new API

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace bulk translation fetching with granular per-variant approach,
allowing independent fetching of title, description, and image translations.

Key changes:

Service layer (media-translation):
- Rename media_translations → media_translation (singular)
- Remove cache_service wrapper; query directly returns MediaTranslationResult
- Query entity_translation by (entity_id, entity_lot, language, variant)
- Return MediaTranslationValue when row exists (value may be null)
- Return MediaTranslationPending with InProgress status when cache key exists
- Return MediaTranslationPending with NotFetched status otherwise
- Add helper functions for entity source lookup and preferred language retrieval
- Add translation_value_for_variant to extract specific variant from details

Job handling (UpdateMediaTranslationJob):
- Change payload from full entity to specific variant:
  user_id, entity_id, entity_lot, variant
- Set in-progress cache key before enqueueing job
- Skip enqueueing if cache key already exists (prevents duplicate fetches)
- Call provider translation method for specific variant only
- Insert/update single entity_translation row for the variant
- Expire in-progress cache key after completion
- Handle provider translation extraction per variant

Resolver changes:
- Update mutation input to accept MediaTranslationInput (entity + variant)
- Remove CachedResponse wrapper from query return type
- Compute preferred language from user preferences before job dispatch

Background job model:
- Simplify UpdateMediaTranslationJobInput to variant-specific fields
- Remove EntityWithLot usage in favor of explicit entity_id/entity_lot/variant

Dependencies:
- Add chrono dependency to background-models for Utc timestamp
- Update Cargo.lock with dependency changes

This refactoring enables more efficient translation fetching by only
requesting the specific translation variant needed (e.g., just the title),
rather than fetching all variants at once. It also provides better UI
feedback through the in-progress cache tracking.

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update GraphQL schema to support per-variant translation queries using
the new MediaTranslationResult union with enum-based pending status.

Query changes:
- Rename MediaTranslations → MediaTranslation (singular)
- Change input from EntityWithLotInput to MediaTranslationInput
- Add variant field to input (EntityTranslationVariant)
- Update response to use MediaTranslationResult union:
  * MediaTranslationValue with optional value field
  * MediaTranslationPending with status enum (InProgress | NotFetched)
- Remove CachedResponse wrapper

Mutation changes:
- Update deployUpdateMediaTranslationsJob to accept MediaTranslationInput
- Include variant field in mutation input

Generated types:
- Regenerate TypeScript types from updated schema
- Update MediaTranslationQuery to handle new union structure
- Add MediaTranslationPendingStatus enum type
- Update fragment types for MediaTranslationValue and MediaTranslationPending

This enables the frontend to query translations for specific variants
(title, description, image) independently and receive clear status
information about whether a translation is being fetched or not yet
requested.

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ant hooks

Replace bulk translation fetching with granular per-variant approach in
the frontend, complementing the backend refactoring.

Hooks changes (apps/frontend/app/lib/shared/hooks.ts):
- Replace useTranslationMonitor with useTranslationValue hook
- useTranslationValue accepts variant parameter (Title, Description, Image)
- Supports MediaTranslationResult union with enum-based pending status
- Handles MediaTranslationPending with NotFetched and InProgress states
- Deploys translation job on NotFetched status
- Continues polling while InProgress
- Returns translation value when MediaTranslationValue received
- Update useMetadataDetails to fetch 3 separate translation variants
- Update usePersonDetails to fetch 3 separate translation variants
- Update useMetadataGroupDetails to fetch 3 separate translation variants
- Return translations as { title, description, image } object

Query factory changes (apps/frontend/app/lib/shared/react-query.ts):
- Rename entityTranslations → entityTranslation query key
- Add variant parameter to query key (EntityTranslationVariant)
- Update refreshEntityDetails to invalidate all translation variants
- Import EntityTranslationVariant type

Component compatibility:
- Existing components already use translations.title, translations.description,
  translations.image syntax
- No component changes needed due to backwards-compatible return structure
- Components automatically benefit from granular fetching

This enables more efficient translation loading by fetching only the
specific variants needed (e.g., just title) rather than all at once,
with proper UI feedback through polling and status tracking.

Related to issue #1672.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Move `useTranslationValue` calls from detail hooks (useMetadataDetails,
usePersonDetails, useMetadataGroupDetails) to the components where translations
are actually needed. This change improves performance by only fetching
translations when and where they are required, rather than pre-fetching all
translation variants in the detail hooks.

Changes:
- Exported `useTranslationValue` hook from hooks.ts for use in components
- Removed translation calls from useMetadataDetails, usePersonDetails, and
  useMetadataGroupDetails - these hooks now return only 2 values instead of 3
- Updated all consuming components to call useTranslationValue directly with
  the specific variant they need (Title, Description, or Image)

Benefits:
- Better performance: translations are only fetched when needed
- More granular control: components can specify which translation variants
  they require
- Clearer component dependencies: translation requirements are explicit at
  the point of use
- Follows React best practices: hooks are called at the point where data is
  consumed

Files updated:
- lib/shared/hooks.ts: Exported useTranslationValue, removed translations
  from detail hooks
- components/media/base-display.tsx: Added useTranslationValue calls
- components/media/display-items.tsx: Added useTranslationValue calls for
  metadata, groups, and people
- routes/_dashboard.media.item.$id._index.tsx: Added useTranslationValue
  calls for all three variants
- routes/_dashboard.media.groups.item.$id._index.tsx: Added useTranslationValue
  calls for groups
- routes/_dashboard.media.people.item.$id._index.tsx: Added useTranslationValue
  calls for people and nested metadata groups
- routes/_dashboard.fitness.$entity.$id._index.tsx: Added useTranslationValue
  calls in ConsumedMetadataDisplay
- components/routes/dashboard/forms/metadata-progress-update/progress-update.tsx:
  Added useTranslationValue for metadata title
- components/routes/media-item/displays/metadata-creator.tsx: Added
  useTranslationValue calls for person translations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…uple structs

Move translation persistence into provider translate calls so the job handler only triggers translation and timestamps entities, while utilities handle database upserts centrally.

Store SupportingService in base providers where possible to avoid duplicating fields, and keep wrapper services as tuple structs when they only carry a single base service for consistency and ergonomics.
IgnisDa and others added 6 commits January 18, 2026 23:13
…ndex

The unique index on entity_translation table was only covering
(language, metadata_id, variant), which caused season/episode
translations to conflict with base translations since they share
the same metadata_id.

By adding show_extra_information and podcast_extra_information to the
unique index, translations for different seasons/episodes are now
properly distinguished:
- Base translation: (fr, id, Title, NULL, NULL)
- Season translation: (fr, id, Title, {season:1}, NULL)
- Episode translation: (fr, id, Title, {season:1,episode:1}, NULL)

PostgreSQL treats these as distinct index keys since JSONB values
are directly comparable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Walkthrough

Refactors media translation end-to-end: adds per-season/episode/podcast extra-info, replaces user-scoped cached translations with in-progress media translation caching, updates provider/trait signatures and DB/models, and adds frontend polling hooks with per-variant translation-value hooks.

Changes

Cohort / File(s) Summary
Migrations
crates/migrations/sql/src/lib.rs, crates/migrations/sql/src/m20260118_changes_for_issue_1672.rs, crates/migrations/sql/src/m20251128_create_entity_translation.rs, crates/migrations/sql/src/m20251218_is_v10_migration.rs
Add migration to drop legacy has_translations_for_languages columns; add show_extra_information and podcast_extra_information JSONB columns to entity_translation; update unique index to include extra-info.
DB models & ORM attrs
crates/models/database/src/entity_translation.rs, crates/models/database/src/metadata.rs, crates/models/database/src/metadata_group.rs, crates/models/database/src/person.rs, crates/models/database/src/*
Remove has_translations_for_languages fields; change many #[sea_orm(column_type = "Json")]JsonBinary; add show/podcast extra-information fields to EntityTranslation; adjust GraphQL exposure.
Media translation types & background jobs
crates/models/media/src/translation.rs, crates/models/background/src/lib.rs, crates/models/background/*, apps/backend/src/job.rs
New types: ShowTranslationExtraInformation, PodcastTranslationExtraInformation, MediaTranslationInput/Result; add UpdateMediaTranslationJobInput; change MpApplicationJob::UpdateMediaTranslations payload shape.
Translation service & resolver
crates/services/miscellaneous/media-translation/src/lib.rs, crates/resolvers/miscellaneous/media-translation/src/lib.rs, crates/resolvers/miscellaneous/media-translation/Cargo.toml
Major rewrite: expose media_translation query and deploy_update_media_translations_job mutation, introduce in-progress cache key, deploy/polling/upsert flow, and change update_media_translation signature to accept UpdateMediaTranslationJobInput (no user_id).
Cache & dependent models
crates/services/cache/src/lib.rs, crates/models/dependent/src/caches.rs, crates/models/dependent/src/generic_types.rs
Replace UserEntityTranslations cache entries with MediaTranslationInProgress keyed by translation input; add 15-minute expiry mapping; remove previous cached translation-details types.
Providers & trait API
crates/traits/src/lib.rs, crates/providers/* (tmdb, itunes, tvdb, anilist, spotify, youtube-music, etc.)
Extend MediaProvider::translate_metadata signature to accept optional ShowTranslationExtraInformation and PodcastTranslationExtraInformation; implement extra-info handling in several providers (shows/podcast episode paths).
Provider/service integration & utils
crates/providers/tmdb/src/base.rs, crates/providers/spotify/src/lib.rs, crates/providers/spotify/Cargo.toml, crates/services/miscellaneous/lookup/src/lib.rs, crates/utils/dependent/provider/src/lib.rs
Store SupportingService on TmdbService, simplify SpotifyService::new to take only ss, remove ss param from smart_search, update Spotify constructor call sites.
GraphQL schema
libs/graphql/src/backend/mutations/combined.gql, libs/graphql/src/backend/queries/combined.gql
Mutation/query input changed from EntityWithLotInputMediaTranslationInput; query response now union MediaTranslationResult with MediaTranslationValue/MediaTranslationPending.
Frontend hooks & react-query
apps/frontend/app/lib/shared/hooks/index.ts, apps/frontend/app/lib/shared/hooks/polling.ts, apps/frontend/app/lib/shared/react-query.ts
Add useTranslationValue, useEntityUpdateMonitor, createDeployMediaEntityJob; change metadata/person/group hooks to drop embedded translations and expose per-variant translation-value hooks; update query keys to include variant and extra-info.
Frontend display & integration
many apps/frontend/app/components/... and apps/frontend/app/routes/... (e.g., base-display.tsx, display-items.tsx, displays/*, modals/*, routes/*)
Replace tuple-based translations with per-variant hooks (useMetadataTranslationValue, usePersonTranslationValue, useMetadataGroupTranslationValue), pass metadataId down, add extra-info (season/episode/podcast) to lookups, preserve fallbacks to original values.
Forms, contexts & props changes
apps/frontend/app/components/routes/dashboard/forms/metadata-progress-update/*, apps/frontend/app/components/routes/media-item/displays/history-item.tsx, .../modals/*
Many components moved from receiving full metadataDetails/userMetadataDetails props to accepting metadataId and fetching details via hooks; BulkUpdateContext now holds metadata: MetadataDetails.
Miscellaneous
assorted files (crates/migrations/*, crates/models/background/Cargo.toml, provider crates, Cargo.tomls, utils)
Dependency edits, constructor/signature updates, ORM attribute changes, removed legacy deploy helper, and assorted API tweaks to accommodate new types and flows.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Frontend Component
    participant Hook as useTranslationValue
    participant Monitor as useEntityUpdateMonitor
    participant API as GraphQL MediaTranslation
    participant Service as Media Translation Service
    participant Scheduler as Background Job Scheduler
    participant Provider as Media Provider
    participant DB as Database

    User->>UI: opens page needing translation
    UI->>Hook: request translation(entityId, variant, extraInfo)
    Hook->>API: query mediaTranslation(input)
    API-->>Hook: returns Value / Pending / NotFetched
    alt Value
        Hook-->>UI: return translation value
    else Pending or NotFetched
        Hook->>Monitor: start monitor
        Monitor->>Service: deploy_update_media_translations_job(input)
        Service->>Scheduler: schedule UpdateMediaTranslations job
        Scheduler->>Provider: translate_metadata(identifier, lang, extraInfo)
        Provider-->>Service: return translation details
        Service->>DB: upsert entity_translation (with extra_info)
        Monitor->>API: poll mediaTranslation periodically
        API-->>Monitor: returns Value when ready
        Monitor-->>Hook: notify updated value
        Hook-->>UI: deliver translation value
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the main change: adding season and episode translation support for TMDB and iTunes providers.
Linked Issues check ✅ Passed PR implements per-season/episode translation storage, retrieves translations from TMDB/iTunes, adds GraphQL schema support, and updates frontend hooks. Addresses #1672 requirements for season/episode title and summary localization.
Out of Scope Changes check ✅ Passed Changes include backend refactoring of translation service, database schema updates, provider API signature changes, and frontend hook refactoring. While comprehensive, all changes are necessary to support granular translation storage/retrieval and appear directly related to the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In
`@apps/frontend/app/components/routes/dashboard/forms/metadata-progress-update/media-types/podcast-form.tsx`:
- Around line 28-31: The Select's data prop can be undefined when
metadataDetails.podcastSpecifics is null; update the expression used for the
Select data (where you currently call
metadataDetails.podcastSpecifics?.episodes.map(...)) to provide a fallback array
(e.g., `|| []`) so the Select always receives an array — locate the Select in
podcast-form.tsx that builds its data from
metadataDetails.podcastSpecifics?.episodes and append the empty-array fallback
to the mapped expression.

In
`@apps/frontend/app/components/routes/dashboard/forms/metadata-progress-update/media-types/show-form.tsx`:
- Around line 27-30: The Season Select's data can be undefined when
metadataDetails.showSpecifics is null—add a defensive fallback so the data prop
always receives an array (mirror the Episode Select approach) by using
metadataDetails.showSpecifics and its seasons list when present, otherwise
supply an empty array; update the data expression for the Season Select (the
mapping over metadataDetails.showSpecifics?.seasons and the Season Select
component) to return [] if showSpecifics or seasons are missing.

In
`@apps/frontend/app/components/routes/dashboard/forms/metadata-progress-update/progress-update.tsx`:
- Line 39: Replace the logical OR fallback that uses || with nullish coalescing
?? so intentionally empty translations are preserved; specifically update the
JSX expression that currently reads metadataTitleTranslation ||
metadataDetails.title to use metadataTitleTranslation ?? metadataDetails.title
in the ProgressUpdate component (referencing metadataTitleTranslation and
metadataDetails.title) so only null/undefined trigger the fallback.

In `@apps/frontend/app/lib/shared/hooks.ts`:
- Around line 383-403: The inner function useMetadataTranslationValue defined
inside useMetadataDetails violates React Rules of Hooks because it calls
useTranslationValue from within another hook; extract it into a standalone
exported hook (useMetadataTranslationValue) that accepts metadataId, enabled,
variant, showExtraInformation, and podcastExtraInformation, call
useMetadataDetails(metadataId, enabled) inside the new hook to obtain
metadataDetailsQuery, then call useTranslationValue with entityId = metadataId,
entityLot = EntityLot.Metadata, mediaSource = metadataDetailsQuery.data?.source,
and the other props; remove the inner definition from useMetadataDetails and
update all callers to import and call the new top-level
useMetadataTranslationValue with the metadataId instead of using the returned
function.

In `@crates/migrations/sql/src/m20260118_changes_for_issue_1672.rs`:
- Around line 11-23: The unique index
"entity_translation__language_metadata_id_variant_idx" should use PostgreSQL
15's NULLS NOT DISTINCT so NULL values are treated as equal; update the CREATE
UNIQUE INDEX statement to list the indexed columns with NULLS NOT DISTINCT (e.g.
("language" NULLS NOT DISTINCT, "metadata_id" NULLS NOT DISTINCT, "variant"
NULLS NOT DISTINCT, "show_extra_information" NULLS NOT DISTINCT,
"podcast_extra_information" NULLS NOT DISTINCT)) and keep the existing WHERE
"metadata_id" IS NOT NULL clause so the index enforces the intended uniqueness
across NULLs.

In `@crates/services/miscellaneous/media-translation/src/lib.rs`:
- Around line 273-312: deploy_update_media_translations_job currently sets an
in-progress cache key via cache_service::set_key
(ApplicationCacheValue::MediaTranslationInProgress) but does not remove that
marker if ss.perform_application_job (MpApplicationJob::UpdateMediaTranslations)
fails; add cleanup so the cache_key is deleted on error to avoid a 15-minute
stale block: after calling ss.perform_application_job(...).await, catch any Err,
call cache_service::delete_key(ss, cache_key).await (or an equivalent removal
API) before propagating the error, ensuring the cache removal references the
same cache_key and value type used when setting the marker.

IgnisDa and others added 11 commits January 19, 2026 00:22
PostgreSQL treats NULLs as distinct by default in unique indexes,
which would allow multiple base translations (where show_extra_information
and podcast_extra_information are NULL) for the same (language, metadata_id,
variant) combination.

Adding NULLS NOT DISTINCT ensures the database enforces uniqueness even
when the extra information columns are NULL, providing a safety net
alongside the application's upsert logic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This reverts commit 6dbbe4d.
…terval

The previous implementation of useEntityUpdateMonitor created independent
polling loops for each hook instance, leading to duplicate listeners when
multiple components used the same entity hooks (e.g., useMetadataDetails).
This caused multiple setTimeout chains, redundant job deployments, and
unnecessary refetch calls for the same entity.

Key issues addressed:
- Multiple components calling useMetadataDetails("abc") would create
  separate polling loops, each deploying jobs and refetching independently
- useTranslationValue inside detail hooks created additional polling
  instances for the same entity
- Manual setTimeout-based polling didn't leverage React Query's built-in
  query observer coordination

Changes:
1. Replaced useEntityUpdateMonitor with useEntityDetailsPolling that returns
   a refetchInterval function instead of managing polling directly
2. Moved polling logic to React Query's refetchInterval option, which
   automatically deduplicates across multiple observers of the same query
3. Introduced useDeployJobOnce with a global Set tracker to ensure jobs
   are deployed only once per entity, even across multiple hook instances
4. Updated useTranslationValue to use refetchInterval directly on its query
5. Simplified useMetadataDetails, usePersonDetails, and useMetadataGroupDetails
   to use the new polling approach

Benefits:
- React Query naturally deduplicates refetch intervals across observers
- Single polling loop per query regardless of component count
- Global job tracker prevents duplicate deployments
- Cleaner code with less manual state management
- Eliminated setTimeout chains in favor of React Query's scheduler

Trade-offs:
- Global deployedJobs Set grows indefinitely (acceptable for now, can add
  TTL if needed in long-running sessions)
- Job deployment uses raw clientGqlService.request instead of useMutation
  (could be improved in future)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/frontend/app/lib/shared/hooks/polling.ts`:
- Around line 20-25: The deploy helper createDeployMediaEntityJob currently
fires clientGqlService.request without returning the Promise, causing unhandled
rejections and leaving polling unaware of failures; update
createDeployMediaEntityJob to return the Promise from clientGqlService.request
(i.e., return clientGqlService.request(...)) so callers receive the Promise, and
then update the place where deployJob is invoked to await or .then/.catch the
returned Promise (or wrap in try/catch) to stop or signal the polling on
rejection. Apply the same change to the other similar helpers referenced around
lines 111-114 and 191-201 so all deploy-job creators return their requests and
callers handle errors.
♻️ Duplicate comments (1)
apps/frontend/app/lib/shared/hooks/index.ts (1)

195-215: Rules of Hooks violation: nested translation hooks.
These use*TranslationValue functions call hooks but are defined inside another hook and returned. Consumers can’t guarantee consistent hook call order, so React may throw “Rendered more hooks than during the previous render.” Extract them as top-level hooks and update call sites.

✅ Example extraction (repeat for person & metadata group)
export const useMetadataTranslationValue = (props: {
	metadataId?: string;
	enabled?: boolean;
	variant: EntityTranslationVariant;
	showExtraInformation?: ShowTranslationExtraInformationInput;
	podcastExtraInformation?: PodcastTranslationExtraInformationInput;
}) => {
	const [metadataDetailsQuery] = useMetadataDetails(
		props.metadataId,
		props.enabled,
	);

	return useTranslationValue({
		entityId: props.metadataId,
		variant: props.variant,
		entityLot: EntityLot.Metadata,
		enabled: props.enabled,
		mediaSource: metadataDetailsQuery.data?.source,
		showExtraInformation: props.showExtraInformation,
		podcastExtraInformation: props.podcastExtraInformation,
	});
};

Also applies to: 232-248, 274-295

…on value hooks

The previous implementation violated React's Rules of Hooks by defining
hooks inside other hooks (useMetadataDetails, usePersonDetails,
useMetadataGroupDetails) and returning them to be called by consumers.
This pattern breaks React's guarantee of consistent hook call order
across renders, which can cause runtime errors like "Rendered more hooks
than during the previous render".

This commit refactors the code to follow React best practices by:

1. Extracting useMetadataTranslationValue, usePersonTranslationValue, and
   useMetadataGroupTranslationValue as standalone top-level hooks
2. Changing the return signatures of details hooks from 3-element tuples
   to 2-element tuples (removing the hook function)
3. Having the standalone translation hooks accept entity IDs as parameters
   and internally call the details hooks to get media source information

This pattern ensures hooks are always called in a consistent order and
at the top level of components, as required by React. The translation
hooks now properly encapsulate the logic for fetching translation values
while maintaining the same functionality.

This addresses the critical issue raised in the code review comment at
#1679 (comment)
…ne translation hooks

Update all media display components to use the newly extracted standalone
translation hooks instead of destructuring them from details hooks.

Changes made to each component:
- Import the standalone useMetadataTranslationValue,
  usePersonTranslationValue, or useMetadataGroupTranslationValue hooks
- Remove the third element from details hook destructuring
- Pass the appropriate entity ID (metadataId, personId, metadataGroupId)
  to translation value hooks

Affected components:
- MetadataDisplayItem: Now passes metadataId to translation hooks
- MetadataGroupDisplayItem: Now passes metadataGroupId to translation hooks
- PersonDisplayItem: Now passes personId to translation hooks
- PartialMetadataDisplay: Updated for metadata translations
- DisplayPodcastEpisode: Updated with metadataId parameter
- DisplayShowEpisode: Updated with metadataId parameter
- DisplayShowSeason: Updated with metadataId parameter

This completes the migration to the Rules of Hooks compliant pattern,
ensuring all translation hooks are called as proper top-level hooks with
explicit entity IDs rather than being returned from other hooks.
…tion hooks

Update the main media detail route pages to use the standalone translation
hooks extracted in the previous commit. These pages are the primary
consumers of translation functionality and needed significant updates.

Media Item Details Page (_dashboard.media.item.$id._index.tsx):
- Import useMetadataTranslationValue and useMetadataGroupTranslationValue
- Update metadata details destructuring from 3 to 2 elements
- Pass metadataId to all useMetadataTranslationValue calls for title,
  description, and image translations
- Update metadata group translation call with metadataGroupId parameter

Person Details Page (_dashboard.media.people.item.$id._index.tsx):
- Import usePersonTranslationValue and useMetadataGroupTranslationValue
- Update person details destructuring from 3 to 2 elements
- Pass personId to all usePersonTranslationValue calls
- Update MetadataGroupDisplay component to pass metadataGroupId to
  translation hooks

Metadata Group Details Page (_dashboard.media.groups.item.$id._index.tsx):
- Import useMetadataGroupTranslationValue
- Update metadata group details destructuring from 3 to 2 elements
- Pass metadataGroupId to all useMetadataGroupTranslationValue calls for
  title, description, and image translations

These changes ensure that the main detail pages follow the Rules of Hooks
by calling translation hooks as top-level functions with explicit entity
IDs, maintaining proper React hook ordering and preventing runtime errors.
…ents

Remove translation hook usage and imports from components that don't
actually need translation functionality. These components were using the
old pattern of destructuring translation hooks from details hooks but
weren't calling the translation hooks, making the destructuring
unnecessary.

Components updated:
- MetadataProgressUpdateForm: Removed unused useMetadataTranslationValue
  destructuring and EntityTranslationVariant import, now uses direct
  metadata title access
- MetadataCreatorDisplay: Removed unused usePersonTranslationValue calls
  and EntityTranslationVariant import, now uses direct person details
- DisplayShowSeasonEpisodesModal: Removed unused
  useMetadataTranslationValue destructuring, EntityTranslationVariant
  import, and translation logic
- ConsumedMetadataDisplay (fitness page): Removed unused translation
  hooks and EntityTranslationVariant import, now uses direct metadata
  details

These components either:
1. Don't display translated content at all
2. Display content in contexts where translations aren't needed (e.g.,
   modal titles, internal forms)
3. Were carrying over the old destructuring pattern without actually
   using the translation functionality

This cleanup reduces unnecessary hook calls and imports while maintaining
the same functionality. The components now have cleaner code that more
accurately reflects their actual dependencies.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/frontend/app/routes/_dashboard.media.groups.item.$id._index.tsx (1)

234-240: Inconsistent title usage in ReviewItemDisplay.

The title prop uses metadataGroupDetailsData.data.details.title directly, while other places in this file prefer metadataGroupTitleTranslation with a fallback. Consider using the translated title for consistency.

Suggested fix
 								<ReviewItemDisplay
 									review={r}
 									key={r.id}
 									entityLot={EntityLot.MetadataGroup}
 									entityId={loaderData.metadataGroupId}
-									title={metadataGroupDetailsData.data.details.title}
+									title={title}
 								/>
apps/frontend/app/routes/_dashboard.media.people.item.$id._index.tsx (1)

365-373: Inconsistent title usage in reviews tab.

ReviewItemDisplay receives title={personDetails.data.details.name} (untranslated) while setEntityToReview at line 310 uses personTitleTranslation || personDetails.data.details.name (translated). Consider using the translated title for consistency:

♻️ Suggested fix
 <ReviewItemDisplay
   review={r}
   key={r.id}
   entityLot={EntityLot.Person}
   entityId={loaderData.personId}
-  title={personDetails.data.details.name}
+  title={personTitleTranslation || personDetails.data.details.name}
 />
apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx (1)

1048-1056: Inconsistent title usage in reviews panel.

ReviewItemDisplay receives title={metadataDetails.data.title} (untranslated) while setEntityToReview at line 874 uses metadataTitleTranslation || metadataDetails.data.title (translated). This creates inconsistency in how the title is displayed across the application.

♻️ Suggested fix
 <ReviewItemDisplay
   key={r.id}
   review={r}
   lot={metadataDetails.data.lot}
   entityLot={EntityLot.Metadata}
   entityId={loaderData.metadataId}
-  title={metadataDetails.data.title}
+  title={title}
 />

This uses the already-computed title variable from line 300 which includes the translation fallback.

🤖 Fix all issues with AI agents
In `@apps/frontend/app/components/media/base-display.tsx`:
- Around line 100-102: Replace boolean-OR fallbacks with nullish coalescing for
translation values: in base-display.tsx update the image expression to use
metadataImageTranslation ?? images.at(0) (instead of metadataImageTranslation ||
images.at(0)) and update the title expression to metadataTitleTranslation ??
metadataDetails?.title ?? undefined (instead of metadataTitleTranslation ||
metadataDetails?.title || undefined); keep link/props.metadataId unchanged. This
ensures empty strings from translations are preserved while still falling back
when the translation is null or undefined.

In `@apps/frontend/app/components/media/display-items.tsx`:
- Around line 118-119: Replace the boolean-OR fallbacks with nullish coalescing
so empty-string translations are preserved: in the JSX where image and title
props are set (the expressions using metadataImageTranslation || images.at(0)
and metadataTitleTranslation || metadataDetails?.title), change the operators to
?? so they only fall back when the translation is null or undefined (i.e., use
metadataImageTranslation ?? images.at(0) and metadataTitleTranslation ??
metadataDetails?.title).
- Around line 287-288: The two fallback expressions in display-items.tsx use ||
inconsistently; change the fallbacks for image and title to use nullish
coalescing (??) so personImageTranslation ?? images.at(0) and
personTitleTranslation ?? personDetails?.details.name are used (referencing the
personImageTranslation, images.at(0), personTitleTranslation, and
personDetails?.details.name symbols) to avoid incorrectly treating falsy but
valid values as null.
- Around line 209-212: Replace the loose-OR fallbacks with nullish coalescing to
avoid treating empty strings or falsey but valid values as missing: change the
expressions using || — specifically the image fallback
(metadataGroupImageTranslation || images.at(0)) and the title fallback
(metadataGroupTitleTranslation || metadataGroupDetails?.details.title) — to use
?? instead so they become metadataGroupImageTranslation ?? images.at(0) and
metadataGroupTitleTranslation ?? metadataGroupDetails?.details.title in the
display-items.tsx component.

In
`@apps/frontend/app/components/routes/media-item/displays/metadata-creator.tsx`:
- Line 26: The prop assignment uses a redundant fallback: replace the expression
"personDetails?.details.assets.remoteImages.at(0) || undefined" with just
"personDetails?.details.assets.remoteImages.at(0)" (in the metadata-creator
component where the image prop is set) to remove the unnecessary "|| undefined"
since Array.prototype.at(0) already yields undefined for empty arrays.

In `@apps/frontend/app/components/routes/media-item/displays/show-season.tsx`:
- Around line 13-14: The helper getShowSeasonDisplayName is duplicated; extract
it into a shared utility (e.g., create a utils.ts exporting
getShowSeasonDisplayName(season: Season, title: string) =>
`${season.seasonNumber}. ${title}`) and replace the local definitions in both
components (the one defining getShowSeasonDisplayName and the one in the
episodes modal) with imports from the new utility so both files reuse the single
implementation.

In
`@apps/frontend/app/components/routes/media-item/modals/show-season-episodes-modal.tsx`:
- Around line 9-10: The helper getShowSeasonDisplayName is duplicated; extract
it into a shared utility (e.g., a new function in a common utils file) and
replace both occurrences (this file's getShowSeasonDisplayName and the one in
show-season.tsx) with an import from that utility; update the usages in the
modal component and show-season.tsx to import the shared function (preserving
the signature getShowSeasonDisplayName(season: Season, title: string)) and
remove the duplicated definition from both files.
- Around line 24-26: The modal currently computes title using season.name
directly; replicate the translation logic from DisplayShowSeason by calling
useMetadataTranslationValue with metadataId (props.metadataId),
showExtraInformation memoized as ({ season: season?.seasonNumber }) and variant
EntityTranslationVariant.Title to produce seasonTitleTranslation, then pass
seasonTitleTranslation ?? season.name into getShowSeasonDisplayName inside the
existing useMemo (the const title useMemo) so the translated title is used when
available.

In `@apps/frontend/app/routes/_dashboard.media.groups.item`.$id._index.tsx:
- Around line 177-179: Replace the logical-or fallback for entityTitle with a
nullish-coalescing fallback to mirror the fixes for title and description: use
metadataGroupTitleTranslation ?? metadataGroupDetailsData.data.details.title
instead of metadataGroupTitleTranslation ||
metadataGroupDetailsData.data.details.title so empty strings don't trigger the
fallback; update the expression that sets entityTitle accordingly.
- Around line 96-102: The fallback logic for title and description uses logical
OR which treats empty strings as falsy; update the assignments for title and
description to use nullish coalescing (replace || with ??) so
metadataGroupTitleTranslation and metadataGroupDescriptionTranslation only fall
back to metadataGroupDetailsData.data?.details.title/description when the
translations are null or undefined; adjust the expressions involving
metadataGroupTitleTranslation, metadataGroupDetailsData.data?.details.title,
metadataGroupDescriptionTranslation, and
metadataGroupDetailsData.data?.details.description accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/frontend/app/components/routes/media-item/displays/show-season.tsx`:
- Around line 52-59: Replace nullish coalescing with logical OR for translation
fallbacks: change the two occurrences where seasonDescriptionTranslation ??
props.season.overview and seasonTitleTranslation ?? props.season.name are used
so they use || instead (i.e., seasonDescriptionTranslation ||
props.season.overview and seasonTitleTranslation || props.season.name) to ensure
empty-string translations fall back to the original values passed into overview
and getShowSeasonDisplayName.

Comment on lines +52 to +59
overview={seasonDescriptionTranslation ?? props.season.overview}
endDate={props.season.episodes.at(-1)?.publishDate}
startDate={props.season.episodes.at(0)?.publishDate}
runtime={sum(props.season.episodes.map((e) => e.runtime || 0))}
name={getShowSeasonDisplayName(
props.season,
seasonTitleTranslation ?? props.season.name,
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use || instead of ?? for translation fallbacks.

Based on learnings from this codebase, translation fallbacks should use logical OR (||) to ensure empty string translations fall back to the original value. Nullish coalescing (??) would preserve empty strings, potentially displaying blank content.

🔧 Suggested fix
-				overview={seasonDescriptionTranslation ?? props.season.overview}
+				overview={seasonDescriptionTranslation || props.season.overview}
 				endDate={props.season.episodes.at(-1)?.publishDate}
 				startDate={props.season.episodes.at(0)?.publishDate}
 				runtime={sum(props.season.episodes.map((e) => e.runtime || 0))}
 				name={getShowSeasonDisplayName(
 					props.season,
-					seasonTitleTranslation ?? props.season.name,
+					seasonTitleTranslation || props.season.name,
 				)}
📝 Committable suggestion

‼️ 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.

Suggested change
overview={seasonDescriptionTranslation ?? props.season.overview}
endDate={props.season.episodes.at(-1)?.publishDate}
startDate={props.season.episodes.at(0)?.publishDate}
runtime={sum(props.season.episodes.map((e) => e.runtime || 0))}
name={getShowSeasonDisplayName(
props.season,
seasonTitleTranslation ?? props.season.name,
)}
overview={seasonDescriptionTranslation || props.season.overview}
endDate={props.season.episodes.at(-1)?.publishDate}
startDate={props.season.episodes.at(0)?.publishDate}
runtime={sum(props.season.episodes.map((e) => e.runtime || 0))}
name={getShowSeasonDisplayName(
props.season,
seasonTitleTranslation || props.season.name,
)}
🤖 Prompt for AI Agents
In `@apps/frontend/app/components/routes/media-item/displays/show-season.tsx`
around lines 52 - 59, Replace nullish coalescing with logical OR for translation
fallbacks: change the two occurrences where seasonDescriptionTranslation ??
props.season.overview and seasonTitleTranslation ?? props.season.name are used
so they use || instead (i.e., seasonDescriptionTranslation ||
props.season.overview and seasonTitleTranslation || props.season.name) to ensure
empty-string translations fall back to the original values passed into overview
and getShowSeasonDisplayName.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shows and podcasts episodes don't show translations

2 participants