mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 23:55:43 -04:00
fix(i18n): wrap remaining hardcoded strings in web and native with LingUI macros
- 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
This commit is contained in:
13 files changed
+457
-151
No files matched your search
@@ -167,11 +167,11 @@ export default function LibraryScreen() {
|
||||
const ratingOptions = useMemo(
|
||||
() => [
|
||||
{ value: "", label: t`Any` },
|
||||
{ value: "1", label: "1\u2605+" },
|
||||
{ value: "2", label: "2\u2605+" },
|
||||
{ value: "3", label: "3\u2605+" },
|
||||
{ value: "4", label: "4\u2605+" },
|
||||
{ value: "5", label: "5\u2605" },
|
||||
{ value: "1", label: t`1★+` },
|
||||
{ value: "2", label: t`2★+` },
|
||||
{ value: "3", label: t`3★+` },
|
||||
{ value: "4", label: t`4★+` },
|
||||
{ value: "5", label: t`5★` },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
@@ -179,11 +179,11 @@ export default function LibraryScreen() {
|
||||
const yearOptions = useMemo(
|
||||
() => [
|
||||
{ value: "", label: t`Any year` },
|
||||
{ value: "2020", label: "2020s" },
|
||||
{ value: "2010", label: "2010s" },
|
||||
{ value: "2000", label: "2000s" },
|
||||
{ value: "1990", label: "1990s" },
|
||||
{ value: "1980", label: "1980s" },
|
||||
{ value: "2020", label: t`2020s` },
|
||||
{ value: "2010", label: t`2010s` },
|
||||
{ value: "2000", label: t`2000s` },
|
||||
{ value: "1990", label: t`1990s` },
|
||||
{ value: "1980", label: t`1980s` },
|
||||
{ value: "older", label: t`Pre-1980` },
|
||||
],
|
||||
[t],
|
||||
|
||||
@@ -34,11 +34,11 @@ interface FilterSheetProps {
|
||||
}
|
||||
|
||||
const DECADES = [
|
||||
{ label: "2020s", yearMin: 2020, yearMax: 2029 },
|
||||
{ label: "2010s", yearMin: 2010, yearMax: 2019 },
|
||||
{ label: "2000s", yearMin: 2000, yearMax: 2009 },
|
||||
{ label: "90s", yearMin: 1990, yearMax: 1999 },
|
||||
{ label: "80s", yearMin: 1980, yearMax: 1989 },
|
||||
{ yearMin: 2020, yearMax: 2029 },
|
||||
{ yearMin: 2010, yearMax: 2019 },
|
||||
{ yearMin: 2000, yearMax: 2009 },
|
||||
{ yearMin: 1990, yearMax: 1999 },
|
||||
{ yearMin: 1980, yearMax: 1989 },
|
||||
] as const;
|
||||
|
||||
const CONTENT_RATINGS = [
|
||||
@@ -197,6 +197,14 @@ export function FilterSheet({ open, onOpenChange, filters, onApply }: FilterShee
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
const decadeLabels: Record<number, string> = {
|
||||
2020: t`2020s`,
|
||||
2010: t`2010s`,
|
||||
2000: t`2000s`,
|
||||
1990: t`90s`,
|
||||
1980: t`80s`,
|
||||
};
|
||||
|
||||
const selectedGenreLabel =
|
||||
genreData?.genres.find((g) => g.id === local.genreId)?.name ?? t`All genres`;
|
||||
|
||||
@@ -308,8 +316,8 @@ export function FilterSheet({ open, onOpenChange, filters, onApply }: FilterShee
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{DECADES.map((decade) => (
|
||||
<Chip
|
||||
key={decade.label}
|
||||
label={decade.label}
|
||||
key={decade.yearMin}
|
||||
label={decadeLabels[decade.yearMin]}
|
||||
isSelected={activeDecade === decade}
|
||||
onPress={() => selectDecade(decade)}
|
||||
/>
|
||||
|
||||
@@ -171,7 +171,7 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
|
||||
<Pressable
|
||||
onPress={toggleExpanded}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${label}, ${connection ? config.connectedStatus(connection.lastEventAt) : "Not configured"}`}
|
||||
accessibilityLabel={`${label}, ${connection ? config.connectedStatus(connection.lastEventAt) : t`Not configured`}`}
|
||||
accessibilityState={{ expanded }}
|
||||
className="flex-row items-center justify-between p-3"
|
||||
>
|
||||
@@ -232,7 +232,7 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
|
||||
<Pressable
|
||||
onPress={handleCopy}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={copied ? "Copied" : "Copy URL"}
|
||||
accessibilityLabel={copied ? t`Copied` : t`Copy URL`}
|
||||
className="ml-2 active:opacity-60"
|
||||
hitSlop={8}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { IconTrash } from "@tabler/icons-react-native";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { View } from "react-native";
|
||||
@@ -33,9 +34,10 @@ interface SwipeableRowProps extends PropsWithChildren {
|
||||
}
|
||||
|
||||
export function SwipeableRow({ onDelete, children }: SwipeableRowProps) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<View
|
||||
accessibilityActions={[{ name: "delete", label: "Delete" }]}
|
||||
accessibilityActions={[{ name: "delete", label: t`Delete` }]}
|
||||
onAccessibilityAction={(event) => {
|
||||
if (event.nativeEvent.actionName === "delete") {
|
||||
onDelete();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import {
|
||||
IconDeviceTv,
|
||||
@@ -41,31 +42,41 @@ import {
|
||||
recentSearchesAtom,
|
||||
} from "@/lib/atoms/command-palette";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import { i18n } from "@sofa/i18n";
|
||||
|
||||
// Static shortcut descriptions for the help dialog.
|
||||
// Static shortcut descriptions for the help dialog, pre-grouped by scope.
|
||||
// TanStack's HotkeyManager/SequenceManager handle all actual key listening.
|
||||
const SHORTCUT_DESCRIPTIONS = [
|
||||
{ scope: "Global", description: "Search", keys: ["/"] },
|
||||
{ scope: "Global", description: "Keyboard shortcuts", keys: ["?"] },
|
||||
{ scope: "Navigation", description: "Go to dashboard", keys: ["g", "h"] },
|
||||
{ scope: "Navigation", description: "Go to explore", keys: ["g", "e"] },
|
||||
{ scope: "Navigation", description: "Go to upcoming", keys: ["g", "u"] },
|
||||
{ scope: "Navigation", description: "Go to settings", keys: ["g", "s"] },
|
||||
{ scope: "Title", description: "Cycle status", keys: ["w"] },
|
||||
{ scope: "Title", description: "Mark watched", keys: ["m"] },
|
||||
{ scope: "Title", description: "Go back", keys: ["Escape"] },
|
||||
{ scope: "Title", description: "Rate 1 star", keys: ["1"] },
|
||||
{ scope: "Title", description: "Rate 2 stars", keys: ["2"] },
|
||||
{ scope: "Title", description: "Rate 3 stars", keys: ["3"] },
|
||||
{ scope: "Title", description: "Rate 4 stars", keys: ["4"] },
|
||||
{ scope: "Title", description: "Rate 5 stars", keys: ["5"] },
|
||||
] as const;
|
||||
|
||||
const groupedShortcuts: Record<string, { description: string; keys: readonly string[] }[]> = {};
|
||||
for (const entry of SHORTCUT_DESCRIPTIONS) {
|
||||
if (!groupedShortcuts[entry.scope]) groupedShortcuts[entry.scope] = [];
|
||||
groupedShortcuts[entry.scope].push(entry);
|
||||
}
|
||||
const SHORTCUT_GROUPS = [
|
||||
{
|
||||
scope: msg`Global`,
|
||||
shortcuts: [
|
||||
{ description: msg`Search`, keys: ["/"] },
|
||||
{ description: msg`Keyboard shortcuts`, keys: ["?"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
scope: msg`Navigation`,
|
||||
shortcuts: [
|
||||
{ description: msg`Go to dashboard`, keys: ["g", "h"] },
|
||||
{ description: msg`Go to explore`, keys: ["g", "e"] },
|
||||
{ description: msg`Go to upcoming`, keys: ["g", "u"] },
|
||||
{ description: msg`Go to settings`, keys: ["g", "s"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
scope: msg`Title`,
|
||||
shortcuts: [
|
||||
{ description: msg`Cycle status`, keys: ["w"] },
|
||||
{ description: msg`Mark watched`, keys: ["m"] },
|
||||
{ description: msg`Go back`, keys: ["Escape"] },
|
||||
{ description: msg`Rate 1 star`, keys: ["1"] },
|
||||
{ description: msg`Rate 2 stars`, keys: ["2"] },
|
||||
{ description: msg`Rate 3 stars`, keys: ["3"] },
|
||||
{ description: msg`Rate 4 stars`, keys: ["4"] },
|
||||
{ description: msg`Rate 5 stars`, keys: ["5"] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface SearchResult {
|
||||
id?: string;
|
||||
@@ -192,7 +203,9 @@ export function CommandPalette() {
|
||||
<>
|
||||
<Dialog open={commandPaletteOpen} onOpenChange={handleOpenChange}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Command Palette</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>Command Palette</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>Search for movies, TV shows, or run commands</Trans>
|
||||
</DialogDescription>
|
||||
@@ -286,7 +299,9 @@ export function CommandPalette() {
|
||||
) : (
|
||||
<IconDeviceTv aria-hidden={true} className="size-[11px]" />
|
||||
)}
|
||||
<span className="uppercase">{r.type}</span>
|
||||
<span className="uppercase">
|
||||
{r.type === "movie" ? t`movie` : r.type === "tv" ? t`tv` : t`person`}
|
||||
</span>
|
||||
{r.type !== "person" && r.releaseDate && (
|
||||
<span>{r.releaseDate.slice(0, 4)}</span>
|
||||
)}
|
||||
@@ -395,23 +410,25 @@ export function CommandPalette() {
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-5 py-2">
|
||||
{Object.entries(groupedShortcuts).map(([scope, items]) => (
|
||||
<div key={scope} className="space-y-2">
|
||||
{SHORTCUT_GROUPS.map((group) => (
|
||||
<div key={i18n._(group.scope)} className="space-y-2">
|
||||
<h3 className="text-muted-foreground text-[10px] font-semibold tracking-wider uppercase">
|
||||
{scope}
|
||||
{i18n._(group.scope)}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{items.map((item) => (
|
||||
{group.shortcuts.map((item) => (
|
||||
<div
|
||||
key={item.description}
|
||||
key={i18n._(item.description)}
|
||||
className="flex items-center justify-between rounded-md px-2 py-1.5"
|
||||
>
|
||||
<span className="text-foreground text-xs">{item.description}</span>
|
||||
<span className="text-foreground text-xs">{i18n._(item.description)}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{item.keys.map((key, i) => (
|
||||
<span key={key} className="flex items-center gap-1">
|
||||
{i > 0 && (
|
||||
<span className="text-muted-foreground text-[10px]">then</span>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
<Trans>then</Trans>
|
||||
</span>
|
||||
)}
|
||||
<Kbd>{formatKey(key)}</Kbd>
|
||||
</span>
|
||||
|
||||
@@ -602,9 +602,10 @@ function StorageCard({
|
||||
</div>
|
||||
{backupCount > 0 ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{plural(backupCount, { one: "# backup", other: "# backups" })}
|
||||
{" · "}
|
||||
<Trans>
|
||||
{plural(backupCount, { one: "# backup", other: "# backups" })} · last{" "}
|
||||
<LiveTimeAgo date={lastBackupAt} fallback={t`unknown`} />
|
||||
last <LiveTimeAgo date={lastBackupAt} fallback={t`unknown`} />
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconChevronRight, IconDots } from "@tabler/icons-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -88,7 +89,9 @@ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span"
|
||||
{...props}
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
<span className="sr-only">
|
||||
<Trans>More</Trans>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -60,7 +61,9 @@ function DialogContent({
|
||||
render={<Button variant="ghost" className="absolute end-2 top-2" size="icon-sm" />}
|
||||
>
|
||||
<IconX />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">
|
||||
<Trans>Close</Trans>
|
||||
</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
@@ -90,7 +93,9 @@ function DialogFooter({
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
<Trans>Close</Trans>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconChevronLeft, IconChevronRight, IconDots } from "@tabler/icons-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -56,35 +57,37 @@ function PaginationLink({ className, isActive, size = "icon", ...props }: Pagina
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
text = "Previous",
|
||||
text,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
aria-label={t`Go to previous page`}
|
||||
size="default"
|
||||
className={cn("ps-2!", className)}
|
||||
{...props}
|
||||
>
|
||||
<IconChevronLeft data-icon="inline-start" />
|
||||
<span className="hidden sm:block">{text}</span>
|
||||
<span className="hidden sm:block">{text ?? t`Previous`}</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
text = "Next",
|
||||
text,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||
const { t } = useLingui();
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
aria-label={t`Go to next page`}
|
||||
size="default"
|
||||
className={cn("pe-2!", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">{text}</span>
|
||||
<span className="hidden sm:block">{text ?? t`Next`}</span>
|
||||
<IconChevronRight data-icon="inline-end" />
|
||||
</PaginationLink>
|
||||
);
|
||||
@@ -102,7 +105,9 @@ function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span"
|
||||
{...props}
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More pages</span>
|
||||
<span className="sr-only">
|
||||
<Trans>More pages</Trans>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import type * as React from "react";
|
||||
|
||||
@@ -63,7 +64,9 @@ function SheetContent({
|
||||
render={<Button variant="ghost" className="absolute end-4 top-4" size="icon-sm" />}
|
||||
>
|
||||
<IconX />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">
|
||||
<Trans>Close</Trans>
|
||||
</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Popup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconLayoutSidebar } from "@tabler/icons-react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
@@ -187,8 +188,12 @@ function Sidebar({
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
<SheetTitle>
|
||||
<Trans>Sidebar</Trans>
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
<Trans>Displays the mobile sidebar.</Trans>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
@@ -259,22 +264,25 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
|
||||
{...props}
|
||||
>
|
||||
<IconLayoutSidebar />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
<span className="sr-only">
|
||||
<Trans>Toggle Sidebar</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { t } = useLingui();
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
aria-label={t`Toggle Sidebar`}
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
title={t`Toggle Sidebar`}
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=left]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconExternalLink, IconKey } from "@tabler/icons-react";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
@@ -7,59 +7,8 @@ import { RefreshButton } from "@/components/setup/refresh-button";
|
||||
import { TmdbLogo } from "@/components/tmdb-logo";
|
||||
import { client } from "@/lib/orpc/client";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: "1",
|
||||
title: "Create a TMDB account",
|
||||
description: (
|
||||
<>
|
||||
Head to{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/signup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary decoration-primary/30 hover:decoration-primary inline-flex items-center gap-1 font-medium underline underline-offset-2 transition-colors"
|
||||
>
|
||||
themoviedb.org
|
||||
<IconExternalLink
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground size-3.5 translate-y-[-1px]"
|
||||
/>
|
||||
</a>{" "}
|
||||
and sign up for a free account.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
title: "Request an API key",
|
||||
description: (
|
||||
<>
|
||||
Go to{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/settings/api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary decoration-primary/30 hover:decoration-primary inline-flex items-center gap-1 font-medium underline underline-offset-2 transition-colors"
|
||||
>
|
||||
Settings → API
|
||||
<IconExternalLink
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground size-3.5 translate-y-[-1px]"
|
||||
/>
|
||||
</a>{" "}
|
||||
and request an API key. Choose “Developer” when asked. You need the{" "}
|
||||
<span className="text-primary font-mono text-xs">API Read Access Token</span> (the long
|
||||
one).
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
number: "3",
|
||||
title: "Add it to your environment",
|
||||
description: "Set the TMDB_API_READ_ACCESS_TOKEN environment variable and restart Sofa.",
|
||||
},
|
||||
];
|
||||
const LINK_CLASS =
|
||||
"text-primary decoration-primary/30 hover:decoration-primary inline-flex items-center gap-1 font-medium underline underline-offset-2 transition-colors";
|
||||
|
||||
const envSnippets = [
|
||||
{
|
||||
@@ -87,6 +36,63 @@ export const Route = createFileRoute("/setup")({
|
||||
});
|
||||
|
||||
function SetupPage() {
|
||||
const { t } = useLingui();
|
||||
|
||||
const linkIcon = (
|
||||
<IconExternalLink
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground size-3.5 translate-y-[-1px]"
|
||||
/>
|
||||
);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: "1",
|
||||
title: t`Create a TMDB account`,
|
||||
description: (
|
||||
<Trans>
|
||||
Head to{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/signup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
themoviedb.org
|
||||
{linkIcon}
|
||||
</a>{" "}
|
||||
and sign up for a free account.
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
title: t`Request an API key`,
|
||||
description: (
|
||||
<Trans>
|
||||
Go to{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/settings/api"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
Settings → API
|
||||
{linkIcon}
|
||||
</a>{" "}
|
||||
and request an API key. Choose “Developer” when asked. You need the{" "}
|
||||
<span className="text-primary font-mono text-xs">API Read Access Token</span> (the long
|
||||
one).
|
||||
</Trans>
|
||||
),
|
||||
},
|
||||
{
|
||||
number: "3",
|
||||
title: t`Add it to your environment`,
|
||||
description: t`Set the TMDB_API_READ_ACCESS_TOKEN environment variable and restart Sofa.`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto my-10 max-w-2xl space-y-10">
|
||||
{/* Header */}
|
||||
@@ -99,21 +105,20 @@ function SetupPage() {
|
||||
<Trans>Connect to TMDB</Trans>
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-lg leading-relaxed">
|
||||
Sofa uses{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary decoration-primary/30 hover:decoration-primary inline-flex items-center gap-1 font-medium underline underline-offset-2 transition-colors"
|
||||
>
|
||||
The Movie Database
|
||||
<IconExternalLink
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground size-3.5 translate-y-[-1px]"
|
||||
/>
|
||||
</a>{" "}
|
||||
for movie & TV metadata, posters, and streaming availability. You'll need a free API
|
||||
key to get started.
|
||||
<Trans>
|
||||
Sofa uses{" "}
|
||||
<a
|
||||
href="https://www.themoviedb.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={LINK_CLASS}
|
||||
>
|
||||
The Movie Database
|
||||
{linkIcon}
|
||||
</a>{" "}
|
||||
for movie & TV metadata, posters, and streaming availability. You'll need a free
|
||||
API key to get started.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -160,9 +165,11 @@ function SetupPage() {
|
||||
<div className="border-border/50 bg-card/40 rounded-xl border p-5">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">After setting the key and restarting:</p>
|
||||
<p className="text-sm font-medium">
|
||||
<Trans>After setting the key and restarting:</Trans>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Click the button to verify your configuration
|
||||
<Trans>Click the button to verify your configuration</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<RefreshButton />
|
||||
|
||||
+257
-10
@@ -42,10 +42,9 @@ msgstr "{0, plural, one {# status} other {# statuses}}"
|
||||
msgid "{0, plural, one {# title} other {# titles}}"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: import { msg, plural } from "@lingui/core/macro"; import { Trans, useLingui } from "@lingui/react/macro"; import { IconActivity, IconAlertTriangle, IconCalendarCheck, IconCheck, IconDatabase, IconPlayerPlay, IconRefresh, } from "@tabler/icons-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { StatusDot } from "@/components/status-dot"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useTimeAgo } from "@/hooks/use-time-ago"; import { orpc } from "@/lib/orpc/client"; import type { CronJobName, SystemHealthData } from "@sofa/api/schemas"; import { i18n } from "@sofa/i18n"; const DIGITS_ONLY_RE = /^\d+$/; /** Convert a cron pattern to a short human-readable string */ function cronToHuman(pattern: string): string { const parts = pattern.split(" "); if (parts.length !== 5) return pattern; const [min, hour, _dom, _mon, dow] = parts; // Every N hours: "0 */6 * * *" if (hour.startsWith("*/")) { const n = Number.parseInt(hour.slice(2), 10); return i18n._(msg`Every ${n}h`); } // Twice daily: "0 1,13 * * *" if (hour.includes(",") && !hour.includes("/") && !hour.includes("-")) { const hours = hour.split(","); if (hours.length === 2) { const times = hours.map((h) => `${h.padStart(2, "0")}:${min.padStart(2, "0")}`).join(", "); return i18n._(msg`Daily at ${times}`); } } // Daily at specific time: "0 3 * * *" if (DIGITS_ONLY_RE.test(hour) && DIGITS_ONLY_RE.test(min) && dow === "*") { const time = `${hour.padStart(2, "0")}:${min.padStart(2, "0")}`; return i18n._(msg`Daily at ${time}`); } // Weekly — use Intl for locale-aware day name if (DIGITS_ONLY_RE.test(dow)) { const dayDate = new Date(2024, 0, 7 + Number(dow)); // 2024-01-07 is a Sunday const dayName = new Intl.DateTimeFormat(i18n.locale, { weekday: "short" }).format(dayDate); return i18n._(msg`Weekly on ${dayName}`); } return pattern; } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.round(ms / 60000)}m`; } export function SkeletonCards() { return ( <div className="space-y-3"> {["status", "jobs", "storage"].map((s) => ( <Card key={s} className="border-s-primary/30 border-s-2"> <CardContent> <div className="flex items-start gap-3"> <Skeleton className="mt-0.5 h-8 w-8 rounded-lg" /> <div className="flex-1 space-y-2"> <Skeleton className="h-4 w-28" /> <Skeleton className="h-3 w-48" /> </div> </div> </CardContent> </Card> ))} </div> ); } /** Inline component that live-updates a relative timestamp */ function LiveTimeAgo({ date, fallback = "", }: { date: string | Date | null | undefined; fallback?: string; }) { const text = useTimeAgo(date, { fallback }); return <span suppressHydrationWarning>{text}</span>; } /** Hydrates system health state and renders the 3 cards */ export function SystemHealthCards() { const queryClient = useQueryClient(); const { data, isPending, isFetching } = useQuery(orpc.admin.systemHealth.queryOptions()); const isRefreshing = isFetching; const refresh = () => queryClient.invalidateQueries({ queryKey: orpc.admin.systemHealth.key() }); if (isPending || !data) return <SkeletonCards />; return ( <div className="space-y-3"> <SystemStatusCard checkedAt={data.checkedAt} database={data.database} tmdb={data.tmdb} environment={data.environment} isRefreshing={isRefreshing} onRefresh={refresh} /> <BackgroundJobsCard jobs={data.jobs} isRefreshing={isRefreshing} onRefresh={refresh} /> <StorageCard imageCache={data.imageCache} backups={data.backups} isRefreshing={isRefreshing} onRefresh={refresh} /> </div> ); } function RefreshButton({ isRefreshing, onRefresh, }: { isRefreshing: boolean; onRefresh: () => void; }) { const { t } = useLingui(); return ( <Tooltip> <TooltipTrigger render={ <Button variant="ghost" size="icon" aria-label={t`Refresh system health`} onClick={onRefresh} disabled={isRefreshing} className="text-muted-foreground" /> } > {isRefreshing ? <Spinner /> : <IconRefresh />} </TooltipTrigger> <TooltipContent> <Trans>Refresh</Trans> </TooltipContent> </Tooltip> ); } function SystemStatusCard({ checkedAt, database, tmdb, environment, isRefreshing, onRefresh, }: Pick<SystemHealthData, "checkedAt" | "database" | "tmdb" | "environment"> & { isRefreshing: boolean; onRefresh: () => void; }) { rLine truncated
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
msgid "{0} · last <0/>"
|
||||
msgstr "{0} · last <0/>"
|
||||
#~ msgid "{0} · last <0/>"
|
||||
#~ msgstr "{0} · last <0/>"
|
||||
|
||||
#: apps/web/src/components/settings/backup-section.tsx
|
||||
#~ msgid "{0} {1, plural, one {backup} other {backups}} stored"
|
||||
@@ -233,6 +232,57 @@ msgstr "{watchedEpisodes}/{totalEpisodes, plural, one {# episode} other {# episo
|
||||
msgid "{watchlistCount} items"
|
||||
msgstr "{watchlistCount} items"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "1★+"
|
||||
msgstr "1★+"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "1980s"
|
||||
msgstr "1980s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "1990s"
|
||||
msgstr "1990s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "2★+"
|
||||
msgstr "2★+"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "2000s"
|
||||
msgstr "2000s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "2010s"
|
||||
msgstr "2010s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "2020s"
|
||||
msgstr "2020s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "3★+"
|
||||
msgstr "3★+"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "4★+"
|
||||
msgstr "4★+"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "5★"
|
||||
msgstr "5★"
|
||||
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "80s"
|
||||
msgstr "80s"
|
||||
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "90s"
|
||||
msgstr "90s"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
msgid "Account"
|
||||
@@ -265,6 +315,10 @@ msgstr ""
|
||||
msgid "Add a new webhook and paste the URL above"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Add it to your environment"
|
||||
msgstr "Add it to your environment"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "Add profile photo"
|
||||
msgstr "Add profile photo"
|
||||
@@ -310,6 +364,11 @@ msgstr ""
|
||||
msgid "Admin only"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "After setting the key and restarting:"
|
||||
msgstr "After setting the key and restarting:"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
msgid "Age"
|
||||
msgstr "Age"
|
||||
@@ -320,7 +379,8 @@ msgid "age {age}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/explore/filterable-title-row.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
@@ -333,6 +393,7 @@ msgstr ""
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
@@ -370,6 +431,7 @@ msgstr ""
|
||||
msgid "Anonymous usage reporting"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
msgid "Any"
|
||||
msgstr "Any"
|
||||
@@ -382,6 +444,7 @@ msgstr "Any language"
|
||||
msgid "Any rating"
|
||||
msgstr "Any rating"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
msgid "Any year"
|
||||
@@ -527,6 +590,7 @@ msgstr ""
|
||||
msgid "Catch up"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||
@@ -600,6 +664,12 @@ msgstr "Choose how to import your {sourceLabel} data."
|
||||
msgid "Choose your subscriptions"
|
||||
msgstr "Choose your subscriptions"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
|
||||
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
|
||||
@@ -644,12 +714,24 @@ msgstr ""
|
||||
msgid "Click <0>Add Webhook</0> and paste the URL above"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Click the button to verify your configuration"
|
||||
msgstr "Click the button to verify your configuration"
|
||||
|
||||
#: apps/native/src/components/navigation/modal-layout.tsx
|
||||
#: apps/native/src/components/navigation/modal-layout.tsx
|
||||
#: apps/web/src/components/ui/dialog.tsx
|
||||
#: apps/web/src/components/ui/dialog.tsx
|
||||
#: apps/web/src/components/ui/sheet.tsx
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Command Palette"
|
||||
msgstr "Command Palette"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||
@@ -726,6 +808,7 @@ msgstr ""
|
||||
msgid "Content rating"
|
||||
msgstr "Content rating"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
msgid "Content Rating"
|
||||
@@ -741,6 +824,7 @@ msgstr ""
|
||||
msgid "Continue Watching"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/settings/integration-card.tsx
|
||||
#: apps/web/src/components/setup/copy-button.tsx
|
||||
msgid "Copied"
|
||||
msgstr ""
|
||||
@@ -749,6 +833,7 @@ msgstr ""
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/settings/integration-card.tsx
|
||||
#: apps/web/src/components/settings/integration-card.tsx
|
||||
msgid "Copy URL"
|
||||
msgstr ""
|
||||
@@ -774,6 +859,10 @@ msgstr "Could not load your library"
|
||||
msgid "Crash reporting"
|
||||
msgstr "Crash reporting"
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Create a TMDB account"
|
||||
msgstr "Create a TMDB account"
|
||||
|
||||
#: apps/native/src/app/(auth)/register.tsx
|
||||
#: apps/native/src/app/(auth)/register.tsx
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
@@ -808,6 +897,10 @@ msgstr ""
|
||||
msgid "Current password is required"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Cycle status"
|
||||
msgstr "Cycle status"
|
||||
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
msgid "Daily at {time}"
|
||||
msgstr "Daily at {time}"
|
||||
@@ -858,6 +951,7 @@ msgstr "Decade"
|
||||
msgid "Default"
|
||||
msgstr "Default"
|
||||
|
||||
#: apps/native/src/components/ui/swipeable-row.tsx
|
||||
#: apps/web/src/components/settings/backup-section.tsx
|
||||
#: apps/web/src/components/settings/backup-section.tsx
|
||||
msgid "Delete"
|
||||
@@ -915,6 +1009,10 @@ msgstr "Discover"
|
||||
msgid "Display name"
|
||||
msgstr "Display name"
|
||||
|
||||
#: apps/web/src/components/ui/sidebar.tsx
|
||||
msgid "Displays the mobile sidebar."
|
||||
msgstr "Displays the mobile sidebar."
|
||||
|
||||
#: apps/native/src/app/(auth)/server-url.tsx
|
||||
msgid "Don't have a server?"
|
||||
msgstr ""
|
||||
@@ -1315,7 +1413,6 @@ msgstr ""
|
||||
msgid "Filmography"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Filters"
|
||||
@@ -1353,6 +1450,8 @@ msgstr ""
|
||||
#~ msgid "From"
|
||||
#~ msgstr "From"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
@@ -1372,10 +1471,15 @@ msgstr "German"
|
||||
msgid "Get Started"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Global"
|
||||
msgstr "Global"
|
||||
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/app/person/[id].tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go back"
|
||||
msgstr ""
|
||||
|
||||
@@ -1387,6 +1491,14 @@ msgstr ""
|
||||
msgid "Go to <0>Dashboard > Plugins > Webhook</0>"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Go to <0>Settings → API{linkIcon}</0> and request an API key. Choose “Developer” when asked. You need the <1>API Read Access Token</1> (the long one)."
|
||||
msgstr "Go to <0>Settings → API{linkIcon}</0> and request an API key. Choose “Developer” when asked. You need the <1>API Read Access Token</1> (the long one)."
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to dashboard"
|
||||
msgstr "Go to dashboard"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to Dashboard"
|
||||
msgstr ""
|
||||
@@ -1395,10 +1507,34 @@ msgstr ""
|
||||
msgid "Go to Dashboard > Plugins > Webhook"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to explore"
|
||||
msgstr "Go to explore"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to Explore"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/pagination.tsx
|
||||
msgid "Go to next page"
|
||||
msgstr "Go to next page"
|
||||
|
||||
#: apps/web/src/components/ui/pagination.tsx
|
||||
msgid "Go to previous page"
|
||||
msgstr "Go to previous page"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to settings"
|
||||
msgstr "Go to settings"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Go to upcoming"
|
||||
msgstr "Go to upcoming"
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Head to <0>themoviedb.org{linkIcon}</0> and sign up for a free account."
|
||||
msgstr "Head to <0>themoviedb.org{linkIcon}</0> and sign up for a free account."
|
||||
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
msgid "Health status"
|
||||
msgstr ""
|
||||
@@ -1565,6 +1701,10 @@ msgstr "Keeping"
|
||||
#~ msgid "Keeping <0><1><2>{0}</2></1><3>{1}</3></0> backups."
|
||||
#~ msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Keyboard shortcuts"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Keyboard Shortcuts"
|
||||
@@ -1591,6 +1731,10 @@ msgstr ""
|
||||
msgid "last {value}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
msgid "last <0/>"
|
||||
msgstr "last <0/>"
|
||||
|
||||
#: apps/web/src/components/settings/integration-card.tsx
|
||||
#~ msgid "Last event {0}"
|
||||
#~ msgstr ""
|
||||
@@ -1627,6 +1771,7 @@ msgstr ""
|
||||
msgid "Last run succeeded"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/_layout.tsx
|
||||
#: apps/native/src/components/navigation/native-tab-bar.tsx
|
||||
#: apps/web/src/components/nav-bar.tsx
|
||||
@@ -1702,6 +1847,10 @@ msgstr "Mark episode {epNum} watched"
|
||||
msgid "Mark Episode Watched"
|
||||
msgstr "Mark Episode Watched"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Mark watched"
|
||||
msgstr "Mark watched"
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/ui/poster-card.tsx
|
||||
@@ -1775,10 +1924,18 @@ msgstr "Min 8 characters…"
|
||||
msgid "Monday"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/breadcrumb.tsx
|
||||
msgid "More"
|
||||
msgstr "More"
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/pagination.tsx
|
||||
msgid "More pages"
|
||||
msgstr "More pages"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||
msgid "More Settings"
|
||||
msgstr ""
|
||||
@@ -1787,6 +1944,10 @@ msgstr ""
|
||||
msgid "Most popular"
|
||||
msgstr "Most popular"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "movie"
|
||||
msgstr "movie"
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/search/recently-viewed-row-content.tsx
|
||||
@@ -1800,6 +1961,7 @@ msgid "Movie"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/web/src/components/people/filmography-grid.tsx
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
@@ -1846,6 +2008,10 @@ msgstr "Name must be at least 2 characters"
|
||||
msgid "Name updated"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Navigation"
|
||||
msgstr "Navigation"
|
||||
|
||||
#: apps/web/src/components/settings/integration-card.tsx
|
||||
msgid "Need more help?"
|
||||
msgstr ""
|
||||
@@ -1895,6 +2061,10 @@ msgstr "New Season"
|
||||
msgid "Newest"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/pagination.tsx
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Next backup {distance}"
|
||||
msgstr ""
|
||||
@@ -1931,8 +2101,8 @@ msgid "No results for \"{debouncedQuery}\""
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "No results for \"{debouncedSearch}\""
|
||||
msgstr "No results for \"{debouncedSearch}\""
|
||||
#~ msgid "No results for \"{debouncedSearch}\""
|
||||
#~ msgstr "No results for \"{debouncedSearch}\""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "No results found."
|
||||
@@ -1956,6 +2126,7 @@ msgstr "No titles match your filters"
|
||||
msgid "No upcoming episodes or releases in the next 90 days."
|
||||
msgstr "No upcoming episodes or releases in the next 90 days."
|
||||
|
||||
#: apps/native/src/components/settings/integration-card.tsx
|
||||
#: apps/native/src/components/settings/integration-card.tsx
|
||||
#: apps/web/src/components/settings/integration-card.tsx
|
||||
#: apps/web/src/components/settings/system-health-section.tsx
|
||||
@@ -2116,6 +2287,10 @@ msgstr ""
|
||||
msgid "Periodically check GitHub for new Sofa releases"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "person"
|
||||
msgstr "person"
|
||||
|
||||
#: apps/native/src/components/search/recently-viewed-row-content.tsx
|
||||
#: apps/native/src/components/search/search-result-row.tsx
|
||||
#: apps/native/src/components/search/search-result-row.tsx
|
||||
@@ -2168,6 +2343,8 @@ msgstr "Portuguese"
|
||||
msgid "Pre-1970"
|
||||
msgstr "Pre-1970"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
msgid "Pre-1980"
|
||||
@@ -2177,6 +2354,10 @@ msgstr "Pre-1980"
|
||||
msgid "Pre-restore backup"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/pagination.tsx
|
||||
msgid "Previous"
|
||||
msgstr "Previous"
|
||||
|
||||
#: apps/web/src/components/nav-bar.tsx
|
||||
#: apps/web/src/components/nav-bar.tsx
|
||||
msgid "Primary"
|
||||
@@ -2262,6 +2443,26 @@ msgstr ""
|
||||
msgid "Rate {star, plural, one {# star} other {# stars}}"
|
||||
msgstr "Rate {star, plural, one {# star} other {# stars}}"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Rate 1 star"
|
||||
msgstr "Rate 1 star"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Rate 2 stars"
|
||||
msgstr "Rate 2 stars"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Rate 3 stars"
|
||||
msgstr "Rate 3 stars"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Rate 4 stars"
|
||||
msgstr "Rate 4 stars"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Rate 5 stars"
|
||||
msgstr "Rate 5 stars"
|
||||
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#~ msgid "Rated {0} {1, plural, one {star} other {stars}}"
|
||||
#~ msgstr ""
|
||||
@@ -2283,6 +2484,8 @@ msgstr "Rated {ratingStars, plural, one {# star} other {# stars}}"
|
||||
msgid "Rated {stars, plural, one {# star} other {# stars}}"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
@@ -2453,6 +2656,10 @@ msgstr ""
|
||||
msgid "Rent"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Request an API key"
|
||||
msgstr "Request an API key"
|
||||
|
||||
#: apps/web/src/components/settings/integration-configs.tsx
|
||||
msgid "Requires <0>Emby Server 4.7.9+</0> and an active <1>Emby Premiere<2/></1> license."
|
||||
msgstr ""
|
||||
@@ -2583,6 +2790,7 @@ msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(search)/_layout.tsx
|
||||
#: apps/native/src/components/navigation/native-tab-bar.tsx
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Search"
|
||||
msgstr "Search"
|
||||
|
||||
@@ -2616,8 +2824,8 @@ msgid "Search movies, shows, people..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
msgid "Search your library..."
|
||||
msgstr "Search your library..."
|
||||
#~ msgid "Search your library..."
|
||||
#~ msgstr "Search your library..."
|
||||
|
||||
#: apps/web/src/components/nav-bar.tsx
|
||||
#: apps/web/src/components/nav-bar.tsx
|
||||
@@ -2690,6 +2898,10 @@ msgstr "Session expired"
|
||||
msgid "Set password"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Set the TMDB_API_READ_ACCESS_TOKEN environment variable and restart Sofa."
|
||||
msgstr "Set the TMDB_API_READ_ACCESS_TOKEN environment variable and restart Sofa."
|
||||
|
||||
#: apps/native/src/components/settings/integration-configs.ts
|
||||
#: apps/native/src/components/settings/integration-configs.ts
|
||||
#: apps/web/src/components/settings/integration-configs.tsx
|
||||
@@ -2727,6 +2939,10 @@ msgstr ""
|
||||
msgid "Show more"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/sidebar.tsx
|
||||
msgid "Sidebar"
|
||||
msgstr "Sidebar"
|
||||
|
||||
#: apps/native/src/app/(auth)/login.tsx
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
@@ -2798,6 +3014,10 @@ msgstr ""
|
||||
msgid "Sofa needs to restart to apply the new layout direction."
|
||||
msgstr "Sofa needs to restart to apply the new layout direction."
|
||||
|
||||
#: apps/web/src/routes/setup.tsx
|
||||
msgid "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
|
||||
msgstr "Sofa uses <0>The Movie Database{linkIcon}</0> for movie & TV metadata, posters, and streaming availability. You'll need a free API key to get started."
|
||||
|
||||
#: apps/web/src/components/update-toast.tsx
|
||||
#~ msgid "Sofa v{0} is available"
|
||||
#~ msgstr ""
|
||||
@@ -2877,6 +3097,9 @@ msgstr ""
|
||||
msgid "Starting import..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
@@ -2931,6 +3154,10 @@ msgstr ""
|
||||
msgid "The title you're looking for doesn't exist or may have been removed from the database."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "then"
|
||||
msgstr "then"
|
||||
|
||||
#: apps/web/src/components/settings/account-section.tsx
|
||||
#: apps/web/src/components/settings/imports-section.tsx
|
||||
msgid "This may take a few minutes for large libraries. Please don't close this tab."
|
||||
@@ -3023,6 +3250,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Title A-Z"
|
||||
@@ -3082,6 +3313,12 @@ msgstr ""
|
||||
msgid "Toggle scheduled backups"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/ui/sidebar.tsx
|
||||
#: apps/web/src/components/ui/sidebar.tsx
|
||||
#: apps/web/src/components/ui/sidebar.tsx
|
||||
msgid "Toggle Sidebar"
|
||||
msgstr "Toggle Sidebar"
|
||||
|
||||
#: apps/web/src/components/landing-page.tsx
|
||||
msgid "Track what you watch. Know what's next.<0/>Your library, your data, your rules."
|
||||
msgstr ""
|
||||
@@ -3119,6 +3356,12 @@ msgstr ""
|
||||
msgid "Tuesday"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "tv"
|
||||
msgstr "tv"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/search/recently-viewed-row-content.tsx
|
||||
@@ -3139,7 +3382,6 @@ msgstr ""
|
||||
msgid "TV show"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/web/src/routes/_app/upcoming.tsx
|
||||
msgid "TV Shows"
|
||||
msgstr "TV Shows"
|
||||
@@ -3349,6 +3591,7 @@ msgid "Watched S{sNum} E{eNum}"
|
||||
msgstr "Watched S{sNum} E{eNum}"
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||
@@ -3360,6 +3603,7 @@ msgid "Watching"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
#: apps/web/src/components/library/library-filters.tsx
|
||||
@@ -3416,6 +3660,9 @@ msgstr ""
|
||||
msgid "Writer"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/app/(tabs)/(library)/index.tsx
|
||||
#: apps/native/src/components/library/filter-sheet.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
#: apps/web/src/components/explore/discover-section.tsx
|
||||
|
||||
Reference in new issue
Block a user