- Replace the static "Mark Watched" button with a toggle that shows "Mark Unwatched" (destructive style) when a movie is already completed
- Add `handleUnwatchMovie` to web `useTitleActions` with optimistic update (reverts status to `in_watchlist` on error)
- Add `unwatchMovie` mutation to native `useTitleActions` hook and `titleActions` imperative helper
- Update poster-card and upcoming-row context menus to show the correct action based on current watch status
- Wire the `M` keyboard shortcut on the title detail page to unwatch when the movie is already marked watched
- Add i18n strings for "Mark Unwatched", "Marked as unwatched", and "Failed to mark as unwatched"
- Replace `tmdbProviderId` (single int) with a many-to-many `platformTmdbProviders` join table so one platform entry can map to multiple TMDB provider IDs (e.g. a single "Max" platform covers several regional TMDB IDs)
- Add `isSubscription` flag to platforms to distinguish subscription services from transactional ones, replacing the old `displayOrder` field in API responses
- Add `getPlatformTmdbIds` / `getPlatformTmdbIdMap` helpers in `@sofa/core/platforms` and thread them through discover, explore, and platform list procedures
- Replace `providerId: number` with `platformId: string` (UUID) in discover input schema so the client never needs to resolve TMDB IDs
- Add `scripts/sync-tmdb-providers.ts` script to pull live provider data from TMDB and update `platforms.json`
- Split native title detail availability into separate "Stream" and "Buy or Rent" sections matching the web pattern
- Add two new DB migrations for the join table and platform schema changes
- Wrap star rating and decade labels in library screen/filter-sheet with `t` macro so they're translatable (and move decade labels out of static const into a reactive map)
- Wrap accessibility labels in `integration-card`, `swipeable-row` with `t` macro
- Refactor `SHORTCUT_DESCRIPTIONS` in command-palette into `SHORTCUT_GROUPS` with `msg` descriptors; render scope/description/connector via `i18n._()` and `<Trans>` so all shortcut help text is extractable
- Wrap "Command Palette" dialog title, media type badges, and "then" connector in `<Trans>`/`t`
- Fix `system-health-section` backup count string: move `plural()` outside `<Trans>` to avoid untranslatable interpolation
- Wrap sr-only strings ("More", "Close") in breadcrumb, dialog, pagination, sheet, and sidebar with `<Trans>`
- Wrap setup route strings with `<Trans>`/`t` macros
- Update `en.po` with newly extracted message IDs
Adds @sentry/react-native for native crash reporting (segfaults, OOM,
ANRs, watchdog kills) that PostHog's JS-level tracking can't capture.
Privacy measures:
- sendDefaultPii: false, no tracing/replay/profiling/screenshots
- beforeSend strips user/request data from JS errors
- beforeBreadcrumb drops navigation/http breadcrumbs
- Screenshot, ViewHierarchy, UserInteraction integrations filtered out
Crash reporting has its own toggle in Settings, independent from the
existing analytics toggle. Enabled by default, takes effect on restart
since native crash handlers must be installed at init time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add filtering and sorting across library, explore, and upcoming
Add comprehensive filtering and sorting to all list views on both web and mobile.
Library (new dedicated page):
- New /library route (web) with URL search param persistence
- New Library tab (5th tab, mobile) with FlashList grid
- Filters: status (multi-select), type, genre (library-only), user rating range,
release year (decade presets + custom), content rating, available to stream
- Sort by: title, date added, release date, popularity, user rating, TMDB rating
- Text search within collection
- Filter popover (web) / bottom sheet with Apply (mobile)
- Remove old dashboard.library endpoint in favor of library.list
Explore/Discover:
- New Discover section on Explore page with inline filter controls
- TMDB filters: genre (now optional), year range, min rating, sort order,
original language, streaming provider
- Watch provider list endpoint (WATCH_REGION env var, default "US")
- Default to popular content when no filters selected
Upcoming:
- Type filter (All/Movies/TV Shows) and status filter (All/Watching/Watchlist)
- Toggle chips on both web (URL params) and mobile
- Backend: mediaType and statusFilter params on upcoming endpoint
Navigation:
- Library added to web nav bar (desktop + mobile tab bar)
- Library added as 5th tab on mobile (between Home and Explore)
- Dashboard library section simplified to compact preview with "See all" link
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add user streaming platform subscriptions and redesign filter UI
Replace the denormalized availabilityOffers table with a normalized
platforms + titleAvailability + userPlatforms schema. Users can now
declare which streaming services they subscribe to, enabling
personalized "On your services" availability display and filtered
library/explore results.
Backend: new platforms table seeded from TMDB provider data on startup,
new API endpoints (platforms.list, account.platforms, account.updatePlatforms),
title detail annotates availability with isUserSubscribed flag, library
"on my services" filter checks user's subscribed platforms.
Web UI: post-registration onboarding page for platform selection,
streaming services settings section, title detail split into "On your
services" / "Also available on", explore dropdown uses platforms table
with active filter highlighting. Library filters redesigned from
cluttered popover to clean collapsible inline strip with consolidated
dropdowns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address race conditions, stale state, and edge cases in platforms feature
- Fix debounced autosave race in streaming services settings: use ref
for latest selection + counter guard so only the most recent save
updates UI. Clean up timers on unmount.
- Fix explore provider dropdown desyncing from query filter when
switching Movie/TV type: reset selectedPlatformId alongside providerId.
- Fix platformIdsExist rejecting duplicate valid IDs by deduplicating
input before comparing against DB results.
- Fix stale platform metadata: always upsert name/logo on TMDB refresh
instead of skipping when platform already exists.
- Fix invisible ratingMax filter: clear ratingMax when user changes
ratingMin since the UI only exposes a single rating dropdown now.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clear stale saved-state timer and preserve falsy filter values
- Clear previous savedTimerRef timeout before scheduling a new one in
streaming-services-section, preventing an older timer from hiding the
"Saved" indicator too early on rapid successive saves
- Replace `value || undefined` with explicit empty-value check in
library filter handler so numeric 0 (e.g. ratingMin=0) is not
incorrectly dropped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add TanStack Form and migrate auth + change password forms
Set up TanStack Form v1 composition layer with createFormHook, pre-bound
field components (TextField, TextareaField, CheckboxField, SwitchField,
SubmitButton), and migrate the two traditional submit-based forms:
- AuthForm: replace 4 useState calls with useAppForm, use form.Field
for each input while preserving motion animation layout
- ChangePasswordDialog: replace 6 useState calls with useAppForm + Zod
schema validation (cross-field password match, min length), per-field
error display, form.reset() on dialog close
Also adds @tanstack/react-form to the monorepo catalog and updates both
web and native apps to reference it via catalog:.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace hand-rolled pluralization (ternary suffix hacks) with `plural` macro
- Wrap 40+ untranslated user-facing strings with LingUI macros: aria-labels,
accessibilityLabels/Hints, placeholders, error fallbacks, toast overrides
- Unify episode progress format to `${watched}/${plural(total, ...)}` across
all 4 call sites (title-card, continue-watching, season-accordion)
- Unify capitalization: "Sign in", "Sign out", "Create account",
"Change password", "Update password", "Image cache", "Remove from library",
"Mark Watched", "Trending Today", "On Watchlist", "or"
- Translate cron schedule strings in system health via `i18n._(msg)` with
locale-aware day names via `Intl.DateTimeFormat`
- Move Zod validation schemas inside components with `useMemo(() => ..., [t])`
so validation messages are translatable at runtime
- Unify "Watched all of" variable naming to `seasonLabel` across all call
sites to produce a single msgid
- Remove trailing period from lone "Failed to..." error message
- Add EAS build i18n template extraction step
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `RouteError` component for route-level error boundaries (web) with "Try again" and "Dashboard" links; wire it into dashboard, explore, upcoming, people, and settings routes
- Add `await queryClient.cancelQueries()` before every optimistic update in `useTitleActions` to prevent in-flight refetches from overwriting local state
- Cap all `useInfiniteQuery` calls at `maxPages: 10` (web and native) to bound memory growth on scroll-heavy pages
- Replace `ActivityIndicator` with `Spinner` in search, person detail, and list footer components (native)
- Tune native query client defaults: add `staleTime: 60_000` and lower `gcTime` to `300_000` (was 24h)
- Darken `--color-muted-foreground` for better contrast in the native theme
- Improve error message copy in `error-messages.ts` for clarity (TMDB, import, registration errors)
- Update i18n `.po` files across all 12 locales with new strings (route error, improved error messages)
- Add `@sofa/tsconfig` package with a shared `base.json` (target, lib, strict, moduleResolution, etc.) and migrate all packages/apps to `"extends": "@sofa/tsconfig/base.json"` to eliminate duplicated compiler options
- Move `display-status.ts` (and its `DisplayStatus` type + `getDisplayStatus` fn) from `@sofa/api` to `@sofa/core`; remove the `./display-status` export from `@sofa/api` and update all import sites in `discovery.ts` and `tracking.ts`
- Add `packages/core/test/display-status.test.ts`: 15 tests covering `ONGOING_TMDB_STATUSES` and all `getDisplayStatus` branches (movie, TV watchlist/in_progress/caught_up/completed, edge cases)
- Add `packages/i18n/vitest.config.ts` and two new test files: `date-buckets.test.ts` and `format.test.ts` covering date bucketing and locale formatting utilities
- Bump `@tanstack/react-query` and related packages to 5.95.0
- Add `@orpc/json-schema`, `@orpc/openapi`, `@orpc/server`, `@orpc/zod` to catalog and use catalog refs in `apps/server`
- Move `ORPCError` throws for backup not-found/invalid into `@sofa/core/backup` so the server procedure can simply `await deleteBackup()` without re-wrapping errors
- Add `@orpc/server` dep to `packages/core` to support the above
- Add `expo-updates` to native deps; switch `requireNativeModule` import to `expo` in `WidgetImagesModule`
- Pin native `react`/`react-dom` to catalog to avoid duplicate copies
- Add `@lingui/core` to web and `react` to i18n package deps
- Replace root `eslint-plugin-lingui` jsPlugin with `@e18e/eslint-plugin`; move lingui rules into per-app `.oxlintrc.json` overrides for native and web
- Add `jsx-a11y`, `react-hooks-js` (native), and expanded lingui rule sets to per-app configs
- Add `oxc/no-barrel-file` warning at threshold 0 to root config
- Fix `e18e/prefer-url-canparse`: replace `try { new URL() } catch` with `URL.canParse()` in server-url screen and server lib
- Fix `e18e/prefer-timer-args`: pass callback args directly to `setTimeout` instead of wrapping in arrow functions (use-debounce, integration-card)
- Fix `e18e/prefer-static-regex`: hoist `/\/+$/` to module-level constant in server-url screen
- Fix `react-hooks-js/refs` and `react-hooks-js/set-state-in-effect`: convert `useRef` tracking patterns to `useState` + render-time derived updates in `use-server-connection`, `expandable-text`, and settings screen
- Fix `react-hooks-js/immutability`: extract stable palette sub-values before `useMemo` deps in title detail screen
- Apply `e18e` and other rule fixes across core, tmdb, web components, and i18n packages
- Add Vercel rewrites to proxy `/ingest/*` and `/ingest/static/*` to PostHog's US endpoints
- Update native `.env.example` to point `EXPO_PUBLIC_POSTHOG_HOST` at the self-hosted proxy URL
- Export `isLocaleRTL` from `@sofa/i18n` to detect right-to-left locales
- Native: call `I18nManager.allowRTL` / `forceRTL` on locale init and on locale change; reload the app immediately if the current layout direction doesn't match the persisted locale, and prompt the user to restart when switching between LTR and RTL locales
- Native: flip the `SettingsRow` chevron to `IconChevronLeft` when `I18nManager.isRTL` is true
- Native: add Arabic Intl polyfill locale data (PluralRules, NumberFormat, DateTimeFormat, RelativeTimeFormat)
- Web: enable `"rtl": true` in `components.json` and regenerate all shadcn UI components to use CSS logical properties (`ms-*`, `me-*`, `ps-*`, `pe-*`, `text-start`, `text-end`) instead of physical `ml-*`/`mr-*`/`text-left` equivalents
- Web: apply the same logical-property conversions to non-generated components (nav-bar, settings sections, title cards, hero banner, continue-watching card)
- Web: set `document.dir` based on the active locale's RTL flag in the i18n initialiser and root route
- Add `../../packages/i18n/src/po.d.ts` to native `tsconfig.json` includes so TypeScript accepts `.po` imports
- Remove `lingui compile` / `i18n:compile` script — Vite and Metro LingUI plugins now compile `.po` files on the fly at dev/build time; no manual compile step needed
- Import `.po` files directly in `@sofa/i18n` (`./po/en.po` etc.) and add `po.d.ts` ambient module declaration so TypeScript accepts them
- Add `fallbackLocales: { default: "en" }` and `lineNumbers: false` to `lingui.config.ts`; prefix catalog paths with `<rootDir>` for correctness
- Derive `SupportedLocale` and `SUPPORTED_LOCALES` from `LOCALE_INFO` in `locales.ts` to eliminate the circular import between `index.ts` and `locales.ts`
- Update Crowdin config to use `project_id_env` instead of a hardcoded ID, drop `languages_mapping` (no longer needed), and add `preserve_hierarchy: true`
- Add a daily cron trigger (`0 6 * * *`) to the Crowdin GitHub Actions workflow and drop the now-redundant compile step from it
- Update docs and `AGENTS.md` to reflect that no compile step is required after `i18n:extract`
- Rename `watchlist` → `in_watchlist` and `in_progress` → `watching` across the full stack (DB migration, Drizzle schema, core services, API contract, web, native)
- Add a new `caught_up` status for TV shows where all aired episodes are watched but the show is still airing
- Add `titles.watchAll` procedure and `markAllWatched` action to mark every episode of a TV show as watched in one step; replace the old "Mark as Watching" / "Mark as Completed" context-menu actions with "Mark All Watched" (TV) and "Mark as Watched" (movie)
- Extract `display-status.ts` in `@sofa/api` to share status → display label/color logic between web and native
- Add `getDisplayStatusesByTitleIds` to `@sofa/core/tracking` and wire it into the dashboard library feed so clients receive resolved display statuses
- Show a destructive Alert confirmation before removing a title from the library (native)
- Remove "Mark as Completed" from the continue-watching card context menu
- Update i18n catalogs for all 6 locales (de, en, es, fr, it, pt)
- Wrap tab bar trigger labels and stack screen titles with `useLingui()` / `t` so they translate correctly across all 6 supported locales
- Swap `IconDeviceTv` → `IconDeviceTvOld` in the recently-viewed badge to match the icon used elsewhere in the app
- Show the type icon inside the pill badge and display a department label (Actor, Director, Writer, etc.) for person results instead of the generic "Person" / "TV" label
- Suppress the subtitle row for person results (department is shown in the badge instead)
- Merge the separate iOS/Android `headerTitleStyle` variables in `TabStack` into a single shared style
- Mark `packages/i18n/src/po/*` as readonly/excluded in VS Code settings alongside `routeTree.gen.ts`
- Extract and compile updated PO/TS catalogs for all locales (de, en, es, fr, it, pt)
- Add `packages/i18n/scripts/claude.ts` — a Bun script that translates empty PO entries via the Claude Code CLI, supporting batch mode, dry-run, optional AI context from `crowdin-context.jsonl`, and structured JSON output validation
- Add `i18n:claude` root script (`bun packages/i18n/scripts/claude.ts`) as a convenience alias
- Fill all previously empty msgstr entries across de, es, fr, it, pt PO catalogs and recompile `.ts` catalogs
- Polish README with badges (license, CI status, release, language, Crowdin localization) and minor formatting fixes
Migrate the entire monorepo from Biome 2.4.7 to oxlint 1.56.0 (linter)
and oxfmt 0.41.0 (formatter) for faster lint/format and broader rule
coverage.
- Add `.oxlintrc.json` with React, TypeScript, unicorn, import plugins
and correctness/suspicious categories
- Add `.oxfmtrc.json` with 2-space indent, import sorting, and Tailwind
class sorting (all 30+ custom className attributes migrated)
- Add `docs/.oxlintrc.json` and `docs/.oxfmtrc.json` with Next.js plugin
- Update all 12 workspace package.json scripts: `oxlint`, `oxfmt`,
`oxfmt --check`
- Add `format:check` turbo task and CI step
- Update VS Code settings/extensions to use `oxc.oxc-vscode`
- Update CI path triggers from `biome.json` to new config files
- Remove all `biome-ignore` comments and fix shadowed variables
- Delete `biome.json` and `docs/biome.json`
- Reformat entire codebase with oxfmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add i18n with LingUI, Crowdin integration, and typed error codes
- Add `@sofa/i18n` shared package with LingUI v5 (v6-ready config using
`defineConfig` + `@lingui/format-po`), eager English + lazy-loaded
fr/de/es/it/pt catalogs, `Intl`-based date/number format utilities,
and test helpers
- Wire `@lingui/vite-plugin` + babel macro plugin for web, and
`@lingui/metro-transformer` + babel config for native
- Add `I18nProvider` to both app roots with locale auto-detection
(navigator.language / expo-localization) and persistence
(localStorage / MMKV)
- Wrap all ~512 user-facing strings across web and native with LingUI
macros (`<Trans>`, `useLingui`/`t`, `i18n._(msg`...`)`, `plural`)
- Add language switcher to Settings in both apps
- Add `@sofa/api/errors` with 13 typed `AppErrorCode` values and
`appErrorData()` helper for contract `.errors()` schemas
- Update all oRPC contract error definitions with typed `data` fields;
convert import procedure `throw new Error()` to `ORPCError` with codes
- Add per-app `error-messages.ts` utilities that map error codes to
localized strings; update global `QueryCache.onError` handlers to stop
leaking raw `error.message` to users
- Add `crowdin.yml` config and `.github/workflows/crowdin.yml` for
automated source upload on merge and translation pull via dispatch
- Add `lingui.config.ts` at repo root with `bun run i18n:extract` and
`bun run i18n:compile` convenience scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add Crowdin language mapping and ignore context file
* fix(i18n): address 20 localization quality issues
- Stop leaking raw error.message to users in change-password and
account-section; use localized fallbacks instead
- Add typed error data to discover contract's .errors() schema
- Replace all manual English plural suffixes with LingUI plural() macro
in episode counts, backup counts, star ratings, title/image counts
- Replace date-fns formatDistanceToNow and hardcoded date patterns with
locale-aware formatDate/formatRelativeTime from @sofa/i18n/format
- Convert integration-configs from module-scope translations (frozen at
import time) to lazy getIntegrationConfigs(i18n) function
- Combine split Trans fragments into single translatable units in
backup-schedule retention sentence and stats-display period labels
- Localize TV media type badge and full episode accessibilityLabel
- Replace Android-incompatible Alert.alert language picker (max 3
buttons) with zeego DropdownMenu
- Await async locale initialization before hiding splash screen so
non-English users don't see English flash on cold start
- Add @lingui/core as direct native app dependency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(i18n): add Intl polyfills for Hermes, replace DropdownMenu language picker, and remove date-fns
- Add `@formatjs/intl-*` polyfills loaded at app startup in `intl-polyfills.ts` so Hermes on Android has full `Intl` support for locale-aware formatting
- Alias `@formatjs/icu-messageformat-parser` to its `no-parser` variant in metro config to reduce bundle size
- Replace the zeego `DropdownMenu` language picker in Settings with a new `SelectModal` component that works reliably on all platforms without the Android 3-button limit
- Replace `date-fns` `format`/`parseISO` in `person/[id].tsx` with `formatDate` from `@sofa/i18n/format`; remove `date-fns` from native and web `package.json`
- Refactor `StatusActionButton` to use a `StatusLabel` component with `<Trans>` rather than a `getStatusConfig(t)` factory so labels are always reactive to locale changes
- Fix crowdin workflow to use root `bun run i18n:compile` script instead of `cd packages/i18n && bun run compile`
- Wrap root layout `GestureHandlerRootView` in `SafeAreaProvider` and add `.catch` to locale-ready promise to prevent unhandled rejections blocking the splash screen
* fix(i18n): localize integration status helpers, fix stats-display Trans wrapping, and clean up SelectModal
- Localize `webhookStatus` and `listStatus` in `integration-card.tsx` using `i18n._(msg`...`)` so status strings are translated instead of always rendering in English
- Wrap "Movies {select}" and "Episodes {select}" in a single `<Trans>` in `stats-display.tsx` so the period selector element is embedded inside the translatable unit rather than concatenated outside it
- Fix locale activation order in Settings: close the modal first, then `activateLocale`, and only call `setPersistedLocale` on success to avoid persisting a locale that failed to load
- Remove the redundant `SafeAreaProvider`/`SafeAreaView` wrapper from `SelectModal` — the root layout already provides `SafeAreaProvider`
- Recompile all six `.po`/`.ts` catalogs to pick up new and updated message strings
* chore(i18n): crowdin sync
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>