mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -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:
@@ -167,11 +167,11 @@ export default function LibraryScreen() {
|
|||||||
const ratingOptions = useMemo(
|
const ratingOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "", label: t`Any` },
|
{ value: "", label: t`Any` },
|
||||||
{ value: "1", label: "1\u2605+" },
|
{ value: "1", label: t`1★+` },
|
||||||
{ value: "2", label: "2\u2605+" },
|
{ value: "2", label: t`2★+` },
|
||||||
{ value: "3", label: "3\u2605+" },
|
{ value: "3", label: t`3★+` },
|
||||||
{ value: "4", label: "4\u2605+" },
|
{ value: "4", label: t`4★+` },
|
||||||
{ value: "5", label: "5\u2605" },
|
{ value: "5", label: t`5★` },
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
@@ -179,11 +179,11 @@ export default function LibraryScreen() {
|
|||||||
const yearOptions = useMemo(
|
const yearOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "", label: t`Any year` },
|
{ value: "", label: t`Any year` },
|
||||||
{ value: "2020", label: "2020s" },
|
{ value: "2020", label: t`2020s` },
|
||||||
{ value: "2010", label: "2010s" },
|
{ value: "2010", label: t`2010s` },
|
||||||
{ value: "2000", label: "2000s" },
|
{ value: "2000", label: t`2000s` },
|
||||||
{ value: "1990", label: "1990s" },
|
{ value: "1990", label: t`1990s` },
|
||||||
{ value: "1980", label: "1980s" },
|
{ value: "1980", label: t`1980s` },
|
||||||
{ value: "older", label: t`Pre-1980` },
|
{ value: "older", label: t`Pre-1980` },
|
||||||
],
|
],
|
||||||
[t],
|
[t],
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ interface FilterSheetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DECADES = [
|
const DECADES = [
|
||||||
{ label: "2020s", yearMin: 2020, yearMax: 2029 },
|
{ yearMin: 2020, yearMax: 2029 },
|
||||||
{ label: "2010s", yearMin: 2010, yearMax: 2019 },
|
{ yearMin: 2010, yearMax: 2019 },
|
||||||
{ label: "2000s", yearMin: 2000, yearMax: 2009 },
|
{ yearMin: 2000, yearMax: 2009 },
|
||||||
{ label: "90s", yearMin: 1990, yearMax: 1999 },
|
{ yearMin: 1990, yearMax: 1999 },
|
||||||
{ label: "80s", yearMin: 1980, yearMax: 1989 },
|
{ yearMin: 1980, yearMax: 1989 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const CONTENT_RATINGS = [
|
const CONTENT_RATINGS = [
|
||||||
@@ -197,6 +197,14 @@ export function FilterSheet({ open, onOpenChange, filters, onApply }: FilterShee
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decadeLabels: Record<number, string> = {
|
||||||
|
2020: t`2020s`,
|
||||||
|
2010: t`2010s`,
|
||||||
|
2000: t`2000s`,
|
||||||
|
1990: t`90s`,
|
||||||
|
1980: t`80s`,
|
||||||
|
};
|
||||||
|
|
||||||
const selectedGenreLabel =
|
const selectedGenreLabel =
|
||||||
genreData?.genres.find((g) => g.id === local.genreId)?.name ?? t`All genres`;
|
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">
|
<View className="flex-row flex-wrap gap-2">
|
||||||
{DECADES.map((decade) => (
|
{DECADES.map((decade) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={decade.label}
|
key={decade.yearMin}
|
||||||
label={decade.label}
|
label={decadeLabels[decade.yearMin]}
|
||||||
isSelected={activeDecade === decade}
|
isSelected={activeDecade === decade}
|
||||||
onPress={() => selectDecade(decade)}
|
onPress={() => selectDecade(decade)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={toggleExpanded}
|
onPress={toggleExpanded}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`${label}, ${connection ? config.connectedStatus(connection.lastEventAt) : "Not configured"}`}
|
accessibilityLabel={`${label}, ${connection ? config.connectedStatus(connection.lastEventAt) : t`Not configured`}`}
|
||||||
accessibilityState={{ expanded }}
|
accessibilityState={{ expanded }}
|
||||||
className="flex-row items-center justify-between p-3"
|
className="flex-row items-center justify-between p-3"
|
||||||
>
|
>
|
||||||
@@ -232,7 +232,7 @@ export function IntegrationCard({ config, connection }: IntegrationCardProps) {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleCopy}
|
onPress={handleCopy}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={copied ? "Copied" : "Copy URL"}
|
accessibilityLabel={copied ? t`Copied` : t`Copy URL`}
|
||||||
className="ml-2 active:opacity-60"
|
className="ml-2 active:opacity-60"
|
||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
import { IconTrash } from "@tabler/icons-react-native";
|
import { IconTrash } from "@tabler/icons-react-native";
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
@@ -33,9 +34,10 @@ interface SwipeableRowProps extends PropsWithChildren {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SwipeableRow({ onDelete, children }: SwipeableRowProps) {
|
export function SwipeableRow({ onDelete, children }: SwipeableRowProps) {
|
||||||
|
const { t } = useLingui();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
accessibilityActions={[{ name: "delete", label: "Delete" }]}
|
accessibilityActions={[{ name: "delete", label: t`Delete` }]}
|
||||||
onAccessibilityAction={(event) => {
|
onAccessibilityAction={(event) => {
|
||||||
if (event.nativeEvent.actionName === "delete") {
|
if (event.nativeEvent.actionName === "delete") {
|
||||||
onDelete();
|
onDelete();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { msg } from "@lingui/core/macro";
|
||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import {
|
import {
|
||||||
IconDeviceTv,
|
IconDeviceTv,
|
||||||
@@ -41,31 +42,41 @@ import {
|
|||||||
recentSearchesAtom,
|
recentSearchesAtom,
|
||||||
} from "@/lib/atoms/command-palette";
|
} from "@/lib/atoms/command-palette";
|
||||||
import { orpc } from "@/lib/orpc/client";
|
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.
|
// TanStack's HotkeyManager/SequenceManager handle all actual key listening.
|
||||||
const SHORTCUT_DESCRIPTIONS = [
|
const SHORTCUT_GROUPS = [
|
||||||
{ scope: "Global", description: "Search", keys: ["/"] },
|
{
|
||||||
{ scope: "Global", description: "Keyboard shortcuts", keys: ["?"] },
|
scope: msg`Global`,
|
||||||
{ scope: "Navigation", description: "Go to dashboard", keys: ["g", "h"] },
|
shortcuts: [
|
||||||
{ scope: "Navigation", description: "Go to explore", keys: ["g", "e"] },
|
{ description: msg`Search`, keys: ["/"] },
|
||||||
{ scope: "Navigation", description: "Go to upcoming", keys: ["g", "u"] },
|
{ description: msg`Keyboard shortcuts`, keys: ["?"] },
|
||||||
{ 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: msg`Navigation`,
|
||||||
{ scope: "Title", description: "Rate 1 star", keys: ["1"] },
|
shortcuts: [
|
||||||
{ scope: "Title", description: "Rate 2 stars", keys: ["2"] },
|
{ description: msg`Go to dashboard`, keys: ["g", "h"] },
|
||||||
{ scope: "Title", description: "Rate 3 stars", keys: ["3"] },
|
{ description: msg`Go to explore`, keys: ["g", "e"] },
|
||||||
{ scope: "Title", description: "Rate 4 stars", keys: ["4"] },
|
{ description: msg`Go to upcoming`, keys: ["g", "u"] },
|
||||||
{ scope: "Title", description: "Rate 5 stars", keys: ["5"] },
|
{ description: msg`Go to settings`, keys: ["g", "s"] },
|
||||||
] as const;
|
],
|
||||||
|
},
|
||||||
const groupedShortcuts: Record<string, { description: string; keys: readonly string[] }[]> = {};
|
{
|
||||||
for (const entry of SHORTCUT_DESCRIPTIONS) {
|
scope: msg`Title`,
|
||||||
if (!groupedShortcuts[entry.scope]) groupedShortcuts[entry.scope] = [];
|
shortcuts: [
|
||||||
groupedShortcuts[entry.scope].push(entry);
|
{ 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 {
|
interface SearchResult {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -192,7 +203,9 @@ export function CommandPalette() {
|
|||||||
<>
|
<>
|
||||||
<Dialog open={commandPaletteOpen} onOpenChange={handleOpenChange}>
|
<Dialog open={commandPaletteOpen} onOpenChange={handleOpenChange}>
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>Command Palette</DialogTitle>
|
<DialogTitle>
|
||||||
|
<Trans>Command Palette</Trans>
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans>Search for movies, TV shows, or run commands</Trans>
|
<Trans>Search for movies, TV shows, or run commands</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@@ -286,7 +299,9 @@ export function CommandPalette() {
|
|||||||
) : (
|
) : (
|
||||||
<IconDeviceTv aria-hidden={true} className="size-[11px]" />
|
<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 && (
|
{r.type !== "person" && r.releaseDate && (
|
||||||
<span>{r.releaseDate.slice(0, 4)}</span>
|
<span>{r.releaseDate.slice(0, 4)}</span>
|
||||||
)}
|
)}
|
||||||
@@ -395,23 +410,25 @@ export function CommandPalette() {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-5 py-2">
|
<div className="space-y-5 py-2">
|
||||||
{Object.entries(groupedShortcuts).map(([scope, items]) => (
|
{SHORTCUT_GROUPS.map((group) => (
|
||||||
<div key={scope} className="space-y-2">
|
<div key={i18n._(group.scope)} className="space-y-2">
|
||||||
<h3 className="text-muted-foreground text-[10px] font-semibold tracking-wider uppercase">
|
<h3 className="text-muted-foreground text-[10px] font-semibold tracking-wider uppercase">
|
||||||
{scope}
|
{i18n._(group.scope)}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{items.map((item) => (
|
{group.shortcuts.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.description}
|
key={i18n._(item.description)}
|
||||||
className="flex items-center justify-between rounded-md px-2 py-1.5"
|
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">
|
<div className="flex items-center gap-1">
|
||||||
{item.keys.map((key, i) => (
|
{item.keys.map((key, i) => (
|
||||||
<span key={key} className="flex items-center gap-1">
|
<span key={key} className="flex items-center gap-1">
|
||||||
{i > 0 && (
|
{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>
|
<Kbd>{formatKey(key)}</Kbd>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -602,9 +602,10 @@ function StorageCard({
|
|||||||
</div>
|
</div>
|
||||||
{backupCount > 0 ? (
|
{backupCount > 0 ? (
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{plural(backupCount, { one: "# backup", other: "# backups" })}
|
||||||
|
{" · "}
|
||||||
<Trans>
|
<Trans>
|
||||||
{plural(backupCount, { one: "# backup", other: "# backups" })} · last{" "}
|
last <LiveTimeAgo date={lastBackupAt} fallback={t`unknown`} />
|
||||||
<LiveTimeAgo date={lastBackupAt} fallback={t`unknown`} />
|
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { mergeProps } from "@base-ui/react/merge-props";
|
import { mergeProps } from "@base-ui/react/merge-props";
|
||||||
import { useRender } from "@base-ui/react/use-render";
|
import { useRender } from "@base-ui/react/use-render";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { IconChevronRight, IconDots } from "@tabler/icons-react";
|
import { IconChevronRight, IconDots } from "@tabler/icons-react";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
@@ -88,7 +89,9 @@ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span"
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">
|
||||||
|
<Trans>More</Trans>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { IconX } from "@tabler/icons-react";
|
import { IconX } from "@tabler/icons-react";
|
||||||
import type * as React from "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" />}
|
render={<Button variant="ghost" className="absolute end-2 top-2" size="icon-sm" />}
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Popup>
|
</DialogPrimitive.Popup>
|
||||||
@@ -90,7 +93,9 @@ function DialogFooter({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { IconChevronLeft, IconChevronRight, IconDots } from "@tabler/icons-react";
|
import { IconChevronLeft, IconChevronRight, IconDots } from "@tabler/icons-react";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
@@ -56,35 +57,37 @@ function PaginationLink({ className, isActive, size = "icon", ...props }: Pagina
|
|||||||
|
|
||||||
function PaginationPrevious({
|
function PaginationPrevious({
|
||||||
className,
|
className,
|
||||||
text = "Previous",
|
text,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
const { t } = useLingui();
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label={t`Go to previous page`}
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("ps-2!", className)}
|
className={cn("ps-2!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconChevronLeft data-icon="inline-start" />
|
<IconChevronLeft data-icon="inline-start" />
|
||||||
<span className="hidden sm:block">{text}</span>
|
<span className="hidden sm:block">{text ?? t`Previous`}</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationNext({
|
function PaginationNext({
|
||||||
className,
|
className,
|
||||||
text = "Next",
|
text,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
const { t } = useLingui();
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label={t`Go to next page`}
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("pe-2!", className)}
|
className={cn("pe-2!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block">{text}</span>
|
<span className="hidden sm:block">{text ?? t`Next`}</span>
|
||||||
<IconChevronRight data-icon="inline-end" />
|
<IconChevronRight data-icon="inline-end" />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
);
|
);
|
||||||
@@ -102,7 +105,9 @@ function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span"
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
<span className="sr-only">More pages</span>
|
<span className="sr-only">
|
||||||
|
<Trans>More pages</Trans>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { IconX } from "@tabler/icons-react";
|
import { IconX } from "@tabler/icons-react";
|
||||||
import type * as React from "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" />}
|
render={<Button variant="ghost" className="absolute end-4 top-4" size="icon-sm" />}
|
||||||
>
|
>
|
||||||
<IconX />
|
<IconX />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</SheetPrimitive.Popup>
|
</SheetPrimitive.Popup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { mergeProps } from "@base-ui/react/merge-props";
|
import { mergeProps } from "@base-ui/react/merge-props";
|
||||||
import { useRender } from "@base-ui/react/use-render";
|
import { useRender } from "@base-ui/react/use-render";
|
||||||
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { IconLayoutSidebar } from "@tabler/icons-react";
|
import { IconLayoutSidebar } from "@tabler/icons-react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -187,8 +188,12 @@ function Sidebar({
|
|||||||
side={side}
|
side={side}
|
||||||
>
|
>
|
||||||
<SheetHeader className="sr-only">
|
<SheetHeader className="sr-only">
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
<SheetTitle>
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
<Trans>Sidebar</Trans>
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
<Trans>Displays the mobile sidebar.</Trans>
|
||||||
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -259,22 +264,25 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconLayoutSidebar />
|
<IconLayoutSidebar />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">
|
||||||
|
<Trans>Toggle Sidebar</Trans>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||||
|
const { t } = useLingui();
|
||||||
const { toggleSidebar } = useSidebar();
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
data-sidebar="rail"
|
data-sidebar="rail"
|
||||||
data-slot="sidebar-rail"
|
data-slot="sidebar-rail"
|
||||||
aria-label="Toggle Sidebar"
|
aria-label={t`Toggle Sidebar`}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
title="Toggle Sidebar"
|
title={t`Toggle Sidebar`}
|
||||||
className={cn(
|
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",
|
"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",
|
"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 { IconExternalLink, IconKey } from "@tabler/icons-react";
|
||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
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 { TmdbLogo } from "@/components/tmdb-logo";
|
||||||
import { client } from "@/lib/orpc/client";
|
import { client } from "@/lib/orpc/client";
|
||||||
|
|
||||||
const steps = [
|
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";
|
||||||
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 envSnippets = [
|
const envSnippets = [
|
||||||
{
|
{
|
||||||
@@ -87,6 +36,63 @@ export const Route = createFileRoute("/setup")({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function SetupPage() {
|
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 (
|
return (
|
||||||
<div className="mx-auto my-10 max-w-2xl space-y-10">
|
<div className="mx-auto my-10 max-w-2xl space-y-10">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -99,21 +105,20 @@ function SetupPage() {
|
|||||||
<Trans>Connect to TMDB</Trans>
|
<Trans>Connect to TMDB</Trans>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground max-w-lg leading-relaxed">
|
<p className="text-muted-foreground max-w-lg leading-relaxed">
|
||||||
Sofa uses{" "}
|
<Trans>
|
||||||
<a
|
Sofa uses{" "}
|
||||||
href="https://www.themoviedb.org/"
|
<a
|
||||||
target="_blank"
|
href="https://www.themoviedb.org/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="text-primary decoration-primary/30 hover:decoration-primary inline-flex items-center gap-1 font-medium underline underline-offset-2 transition-colors"
|
rel="noopener noreferrer"
|
||||||
>
|
className={LINK_CLASS}
|
||||||
The Movie Database
|
>
|
||||||
<IconExternalLink
|
The Movie Database
|
||||||
aria-hidden={true}
|
{linkIcon}
|
||||||
className="text-muted-foreground size-3.5 translate-y-[-1px]"
|
</a>{" "}
|
||||||
/>
|
for movie & TV metadata, posters, and streaming availability. You'll need a free
|
||||||
</a>{" "}
|
API key to get started.
|
||||||
for movie & TV metadata, posters, and streaming availability. You'll need a free API
|
</Trans>
|
||||||
key to get started.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,9 +165,11 @@ function SetupPage() {
|
|||||||
<div className="border-border/50 bg-card/40 rounded-xl border p-5">
|
<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="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="space-y-0.5">
|
<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">
|
<p className="text-muted-foreground text-xs">
|
||||||
Click the button to verify your configuration
|
<Trans>Click the button to verify your configuration</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
|
|||||||
+257
-10
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user