Add translation and fix ts issues

This commit is contained in:
Owen 2025-06-10 18:34:04 -04:00
parent d66739f69e
commit 3c2ea1a75f
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
9 changed files with 167 additions and 112 deletions

View file

@ -3,7 +3,8 @@ export default [
{ {
rules: { rules: {
semi: "error", semi: "error",
"prefer-const": "error" "prefer-const": "error",
quotes: ["error", "double"]
} }
} }
]; ];

View file

@ -1089,5 +1089,40 @@
"sidebarSettings": "Settings", "sidebarSettings": "Settings",
"sidebarAllUsers": "All Users", "sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers", "sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License" "sidebarLicense": "License",
"enableDockerSocket": "Enable Docker Socket",
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information, useful in resource targets.",
"enableDockerSocketLink": "Enable Docker Socket discovery for populating container information, useful in resource targets.",
"viewDockerContainers": "View Docker Containers",
"containersIn": "Containers in {siteName}",
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
"containerName": "Name",
"containerImage": "Image",
"containerState": "State",
"containerNetworks": "Networks",
"containerHostnameIp": "Hostname/IP",
"containerLabels": "Labels",
"containerLabelsCount": "{count} label{s,plural,one{} other{s}}",
"containerLabelsTitle": "Container Labels",
"containerLabelEmpty": "<empty>",
"containerPorts": "Ports",
"containerPortsMore": "+{count} more",
"containerActions": "Actions",
"select": "Select",
"noContainersMatchingFilters": "No containers found matching the current filters.",
"showContainersWithoutPorts": "Show containers without ports",
"showStoppedContainers": "Show stopped containers",
"noContainersFound": "No containers found. Make sure Docker containers are running.",
"searchContainersPlaceholder": "Search across {count} containers...",
"searchResultsCount": "{count} result{s,plural,one{} other{s}}",
"filters": "Filters",
"filterOptions": "Filter Options",
"filterPorts": "Ports",
"filterStopped": "Stopped",
"clearAllFilters": "Clear all filters",
"columns": "Columns",
"toggleColumns": "Toggle Columns",
"refreshContainersList": "Refresh containers list",
"searching": "Searching...",
"noContainersFoundMatching": "No containers found matching \"{filter}\"."
} }

View file

@ -148,7 +148,7 @@ export default function InvitationsTable({
dialog={ dialog={
<div className="space-y-4"> <div className="space-y-4">
<p> <p>
{t('inviteQuestionRemove', {email: selectedInvitation?.email})} {t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}
</p> </p>
<p> <p>
{t('inviteMessageRemove')} {t('inviteMessageRemove')}

View file

@ -177,7 +177,7 @@ export default function RegenerateInvitationForm({
{!inviteLink ? ( {!inviteLink ? (
<div> <div>
<p> <p>
{t('inviteQuestionRegenerate', {email: invitation?.email})} {t('inviteQuestionRegenerate', {email: invitation?.email || ""})}
</p> </p>
<div className="flex items-center space-x-2 mt-4"> <div className="flex items-center space-x-2 mt-4">
<Checkbox <Checkbox

View file

@ -222,7 +222,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
toast({ toast({
variant: "default", variant: "default",
title: t('userOrgRemoved'), title: t('userOrgRemoved'),
description: t('userOrgRemovedDescription', {email: selectedUser.email}) description: t('userOrgRemovedDescription', {email: selectedUser.email || ""})
}); });
setUsers((prev) => setUsers((prev) =>
@ -244,7 +244,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
dialog={ dialog={
<div className="space-y-4"> <div className="space-y-4">
<p> <p>
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username})} {t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}
</p> </p>
<p> <p>

View file

@ -71,7 +71,6 @@ const TransferFormSchema = z.object({
siteId: z.number() siteId: z.number()
}); });
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
type TransferFormValues = z.infer<typeof TransferFormSchema>; type TransferFormValues = z.infer<typeof TransferFormSchema>;
export default function GeneralForm() { export default function GeneralForm() {
@ -123,7 +122,7 @@ export default function GeneralForm() {
return true; return true;
}, },
{ {
message: t('proxyErrorInvalidPort'), message: t("proxyErrorInvalidPort"),
path: ["proxyPort"] path: ["proxyPort"]
} }
) )
@ -135,11 +134,13 @@ export default function GeneralForm() {
return true; return true;
}, },
{ {
message: t('subdomainErrorInvalid'), message: t("subdomainErrorInvalid"),
path: ["subdomain"] path: ["subdomain"]
} }
); );
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
defaultValues: { defaultValues: {
@ -176,8 +177,11 @@ export default function GeneralForm() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('domainErrorFetch'), title: t("domainErrorFetch"),
description: formatAxiosError(e, t('domainErrorFetchDescription')) description: formatAxiosError(
e,
t("domainErrorFetchDescription")
)
}); });
}); });
@ -215,15 +219,18 @@ export default function GeneralForm() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorUpdate'), title: t("resourceErrorUpdate"),
description: formatAxiosError(e, t('resourceErrorUpdateDescription')) description: formatAxiosError(
e,
t("resourceErrorUpdateDescription")
)
}); });
}); });
if (res && res.status === 200) { if (res && res.status === 200) {
toast({ toast({
title: t('resourceUpdated'), title: t("resourceUpdated"),
description: t('resourceUpdatedDescription') description: t("resourceUpdatedDescription")
}); });
const resource = res.data.data; const resource = res.data.data;
@ -251,16 +258,18 @@ export default function GeneralForm() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorTransfer'), title: t("resourceErrorTransfer"),
description: formatAxiosError(e, t('resourceErrorTransferDescription') description: formatAxiosError(
e,
t("resourceErrorTransferDescription")
) )
}); });
}); });
if (res && res.status === 200) { if (res && res.status === 200) {
toast({ toast({
title: t('resourceTransferred'), title: t("resourceTransferred"),
description: t('resourceTransferredDescription') description: t("resourceTransferredDescription")
}); });
router.refresh(); router.refresh();
@ -284,10 +293,10 @@ export default function GeneralForm() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorToggle'), title: t("resourceErrorToggle"),
description: formatAxiosError( description: formatAxiosError(
e, e,
t('resourceErrorToggleDescription') t("resourceErrorToggleDescription")
) )
}); });
}); });
@ -302,15 +311,17 @@ export default function GeneralForm() {
<SettingsContainer> <SettingsContainer>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle>{t('resourceVisibilityTitle')}</SettingsSectionTitle> <SettingsSectionTitle>
{t("resourceVisibilityTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('resourceVisibilityTitleDescription')} {t("resourceVisibilityTitleDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SwitchInput <SwitchInput
id="enable-resource" id="enable-resource"
label={t('resourceEnable')} label={t("resourceEnable")}
defaultChecked={resource.enabled} defaultChecked={resource.enabled}
onCheckedChange={async (val) => { onCheckedChange={async (val) => {
await toggleResourceEnabled(val); await toggleResourceEnabled(val);
@ -322,10 +333,10 @@ export default function GeneralForm() {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('resourceGeneral')} {t("resourceGeneral")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('resourceGeneralDescription')} {t("resourceGeneralDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
@ -342,7 +353,9 @@ export default function GeneralForm() {
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('name')}</FormLabel> <FormLabel>
{t("name")}
</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
@ -361,7 +374,9 @@ export default function GeneralForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('domainType')} {t(
"domainType"
)}
</FormLabel> </FormLabel>
<Select <Select
value={ value={
@ -392,10 +407,14 @@ export default function GeneralForm() {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="subdomain"> <SelectItem value="subdomain">
{t('subdomain')} {t(
"subdomain"
)}
</SelectItem> </SelectItem>
<SelectItem value="basedomain"> <SelectItem value="basedomain">
{t('baseDomain')} {t(
"baseDomain"
)}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -409,7 +428,7 @@ export default function GeneralForm() {
{domainType === "subdomain" ? ( {domainType === "subdomain" ? (
<div className="w-fill space-y-2"> <div className="w-fill space-y-2">
<FormLabel> <FormLabel>
{t('subdomain')} {t("subdomain")}
</FormLabel> </FormLabel>
<div className="flex"> <div className="flex">
<div className="w-1/2"> <div className="w-1/2">
@ -495,7 +514,9 @@ export default function GeneralForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('baseDomain')} {t(
"baseDomain"
)}
</FormLabel> </FormLabel>
<Select <Select
onValueChange={ onValueChange={
@ -549,7 +570,9 @@ export default function GeneralForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('resourcePortNumber')} {t(
"resourcePortNumber"
)}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@ -589,7 +612,7 @@ export default function GeneralForm() {
disabled={saveLoading} disabled={saveLoading}
form="general-settings-form" form="general-settings-form"
> >
{t('saveGeneralSettings')} {t("saveGeneralSettings")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
@ -597,10 +620,10 @@ export default function GeneralForm() {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('resourceTransfer')} {t("resourceTransfer")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('resourceTransferDescription')} {t("resourceTransferDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
@ -620,7 +643,7 @@ export default function GeneralForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t('siteDestination')} {t("siteDestination")}
</FormLabel> </FormLabel>
<Popover <Popover
open={open} open={open}
@ -645,16 +668,24 @@ export default function GeneralForm() {
site.siteId === site.siteId ===
field.value field.value
)?.name )?.name
: t('siteSelect')} : t(
"siteSelect"
)}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0"> <PopoverContent className="w-full p-0">
<Command> <Command>
<CommandInput placeholder={t('searchSites')} /> <CommandInput
placeholder={t(
"searchSites"
)}
/>
<CommandEmpty> <CommandEmpty>
{t('sitesNotFound')} {t(
"sitesNotFound"
)}
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{sites.map( {sites.map(
@ -709,7 +740,7 @@ export default function GeneralForm() {
disabled={transferLoading} disabled={transferLoading}
form="transfer-form" form="transfer-form"
> >
{t('resourceTransferSubmit')} {t("resourceTransferSubmit")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>

View file

@ -74,7 +74,6 @@ import {
CollapsibleTrigger CollapsibleTrigger
} from "@app/components/ui/collapsible"; } from "@app/components/ui/collapsible";
import { ContainersSelector } from "@app/components/ContainersSelector"; import { ContainersSelector } from "@app/components/ContainersSelector";
import { FaDocker } from "react-icons/fa";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const addTargetSchema = z.object({ const addTargetSchema = z.object({

View file

@ -34,7 +34,6 @@ import { useState } from "react";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight } from "lucide-react";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"), name: z.string().nonempty("Name is required"),
@ -53,13 +52,6 @@ export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"),
dockerSocketEnabled: z.boolean().optional()
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
defaultValues: { defaultValues: {
@ -80,10 +72,10 @@ export default function GeneralPage() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('siteErrorUpdate'), title: t("siteErrorUpdate"),
description: formatAxiosError( description: formatAxiosError(
e, e,
t('siteErrorUpdateDescription') t("siteErrorUpdateDescription")
) )
}); });
}); });
@ -94,8 +86,8 @@ export default function GeneralPage() {
}); });
toast({ toast({
title: t('siteUpdated'), title: t("siteUpdated"),
description: t('siteUpdatedDescription') description: t("siteUpdatedDescription")
}); });
setLoading(false); setLoading(false);
@ -108,10 +100,10 @@ export default function GeneralPage() {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t('generalSettings')} {t("generalSettings")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t('siteGeneralDescription')} {t("siteGeneralDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
@ -128,13 +120,13 @@ export default function GeneralPage() {
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('name')}</FormLabel> <FormLabel>{t("name")}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
{t('siteNameDescription')} {t("siteNameDescription")}
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
@ -148,7 +140,9 @@ export default function GeneralPage() {
<FormControl> <FormControl>
<SwitchInput <SwitchInput
id="docker-socket-enabled" id="docker-socket-enabled"
label="Enable Docker Socket" label={t(
"enableDockerSocket"
)}
defaultChecked={ defaultChecked={
field.value field.value
} }
@ -159,10 +153,9 @@ export default function GeneralPage() {
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
Enable Docker Socket {t(
discovery for populating "enableDockerSocketDescription"
container information, )}
useful in resource targets.
<Link <Link
href="https://docs.fossorial.io/Newt/overview#docker-socket-integration" href="https://docs.fossorial.io/Newt/overview#docker-socket-integration"
target="_blank" target="_blank"
@ -171,10 +164,9 @@ export default function GeneralPage() {
> >
<span> <span>
{" "} {" "}
Docker socket path {t(
must be provided to "enableDockerSocketLink"
Newt in order to use )}
this feature.
</span> </span>
</Link> </Link>
</FormDescription> </FormDescription>
@ -194,7 +186,7 @@ export default function GeneralPage() {
loading={loading} loading={loading}
disabled={loading} disabled={loading}
> >
{t('saveGeneralSettings')} {t("saveGeneralSettings")}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>

View file

@ -45,7 +45,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Search, RefreshCw, Filter, Columns } from "lucide-react"; import { Search, RefreshCw, Filter, Columns } from "lucide-react";
import { GetSiteResponse, Container } from "@server/routers/site"; import { GetSiteResponse, Container } from "@server/routers/site";
import { useDockerSocket } from "@app/hooks/useDockerSocket"; import { useDockerSocket } from "@app/hooks/useDockerSocket";
import { FaDocker } from "react-icons/fa"; import { useTranslations } from "next-intl";
// Type definitions based on the JSON structure // Type definitions based on the JSON structure
@ -60,6 +60,8 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const t = useTranslations();
const { isAvailable, containers, fetchContainers } = useDockerSocket(site); const { isAvailable, containers, fetchContainers } = useDockerSocket(site);
useEffect(() => { useEffect(() => {
@ -87,15 +89,16 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
className="text-sm text-primary hover:underline cursor-pointer" className="text-sm text-primary hover:underline cursor-pointer"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
View Docker Containers {t("viewDockerContainers")}
</a> </a>
<Credenza open={open} onOpenChange={setOpen}> <Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent className="max-w-[75vw] max-h-[75vh] flex flex-col"> <CredenzaContent className="max-w-[75vw] max-h-[75vh] flex flex-col">
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle>Containers in {site.name}</CredenzaTitle> <CredenzaTitle>
{t("containersIn", { siteName: site.name })}
</CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
Select any container to use as a hostname for this {t("selectContainerDescription")}
target. Click a port to use a port.
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
@ -109,7 +112,7 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline">Close</Button> <Button variant="outline">{t("close")}</Button>
</CredenzaClose> </CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
@ -132,6 +135,8 @@ const DockerContainersTable: FC<{
labels: false labels: false
}); });
const t = useTranslations();
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setGlobalFilter(searchInput); setGlobalFilter(searchInput);
@ -182,14 +187,14 @@ const DockerContainersTable: FC<{
const columns: ColumnDef<Container>[] = [ const columns: ColumnDef<Container>[] = [
{ {
accessorKey: "name", accessorKey: "name",
header: "Name", header: t("containerName"),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium">{row.original.name}</div> <div className="font-medium">{row.original.name}</div>
) )
}, },
{ {
accessorKey: "image", accessorKey: "image",
header: "Image", header: t("containerImage"),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{row.original.image} {row.original.image}
@ -198,7 +203,7 @@ const DockerContainersTable: FC<{
}, },
{ {
accessorKey: "state", accessorKey: "state",
header: "State", header: t("containerState"),
cell: ({ row }) => ( cell: ({ row }) => (
<Badge <Badge
variant={ variant={
@ -213,7 +218,7 @@ const DockerContainersTable: FC<{
}, },
{ {
accessorKey: "networks", accessorKey: "networks",
header: "Networks", header: t("containerNetworks"),
cell: ({ row }) => { cell: ({ row }) => {
const networks = Object.keys(row.original.networks); const networks = Object.keys(row.original.networks);
return ( return (
@ -231,7 +236,7 @@ const DockerContainersTable: FC<{
}, },
{ {
accessorKey: "hostname", accessorKey: "hostname",
header: "Hostname/IP", header: t("containerHostnameIp"),
enableHiding: false, enableHiding: false,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-sm font-mono"> <div className="text-sm font-mono">
@ -241,7 +246,7 @@ const DockerContainersTable: FC<{
}, },
{ {
accessorKey: "labels", accessorKey: "labels",
header: "Labels", header: t("containerLabels"),
cell: ({ row }) => { cell: ({ row }) => {
const labels = row.original.labels || {}; const labels = row.original.labels || {};
const labelEntries = Object.entries(labels); const labelEntries = Object.entries(labels);
@ -258,15 +263,14 @@ const DockerContainersTable: FC<{
size="sm" size="sm"
className="h-6 px-2 text-xs hover:bg-muted" className="h-6 px-2 text-xs hover:bg-muted"
> >
{labelEntries.length} label {t("containerLabelsCount", { count: labelEntries.length })}
{labelEntries.length !== 1 ? "s" : ""}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent side="top" align="start"> <PopoverContent side="top" align="start">
<ScrollArea className="w-64 h-64"> <ScrollArea className="w-64 h-64">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-medium text-sm"> <h4 className="font-medium text-sm">
Container Labels {t("containerLabelsTitle")}
</h4> </h4>
<div className="space-y-1"> <div className="space-y-1">
{labelEntries.map(([key, value]) => ( {labelEntries.map(([key, value]) => (
@ -275,7 +279,7 @@ const DockerContainersTable: FC<{
{key} {key}
</div> </div>
<div className="font-mono text-muted-foreground pl-2 break-all"> <div className="font-mono text-muted-foreground pl-2 break-all">
{value || "<empty>"} {value || t("containerLabelEmpty")}
</div> </div>
</div> </div>
))} ))}
@ -289,7 +293,7 @@ const DockerContainersTable: FC<{
}, },
{ {
accessorKey: "ports", accessorKey: "ports",
header: "Ports", header: t("containerPorts"),
enableHiding: false, enableHiding: false,
cell: ({ row }) => { cell: ({ row }) => {
const ports = getExposedPorts(row.original); const ports = getExposedPorts(row.original);
@ -312,7 +316,7 @@ const DockerContainersTable: FC<{
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="link" size="sm"> <Button variant="link" size="sm">
+{ports.length - 2} more {t("containerPortsMore", { count: ports.length - 2 })}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
@ -345,7 +349,7 @@ const DockerContainersTable: FC<{
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: t("containerActions"),
cell: ({ row }) => { cell: ({ row }) => {
const ports = getExposedPorts(row.original); const ports = getExposedPorts(row.original);
return ( return (
@ -355,7 +359,7 @@ const DockerContainersTable: FC<{
onClick={() => onContainerSelect(row.original, ports[0])} onClick={() => onContainerSelect(row.original, ports[0])}
disabled={row.original.state !== "running"} disabled={row.original.state !== "running"}
> >
Select {t("select")}
</Button> </Button>
); );
} }
@ -412,8 +416,7 @@ const DockerContainersTable: FC<{
containers.length > 0 ? ( containers.length > 0 ? (
<> <>
<p> <p>
No containers found matching the current {t("noContainersMatchingFilters")}
filters.
</p> </p>
<div className="space-x-2"> <div className="space-x-2">
{hideContainersWithoutPorts && ( {hideContainersWithoutPorts && (
@ -426,7 +429,7 @@ const DockerContainersTable: FC<{
) )
} }
> >
Show containers without ports {t("showContainersWithoutPorts")}
</Button> </Button>
)} )}
{hideStoppedContainers && ( {hideStoppedContainers && (
@ -437,15 +440,14 @@ const DockerContainersTable: FC<{
setHideStoppedContainers(false) setHideStoppedContainers(false)
} }
> >
Show stopped containers {t("showStoppedContainers")}
</Button> </Button>
)} )}
</div> </div>
</> </>
) : ( ) : (
<p> <p>
No containers found. Make sure Docker containers {t("noContainersFound")}
are running.
</p> </p>
)} )}
</div> </div>
@ -461,7 +463,7 @@ const DockerContainersTable: FC<{
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
placeholder={`Search across ${initialFilters.length} containers...`} placeholder={t("searchContainersPlaceholder", { count: initialFilters.length })}
value={searchInput} value={searchInput}
onChange={(event) => onChange={(event) =>
setSearchInput(event.target.value) setSearchInput(event.target.value)
@ -471,12 +473,7 @@ const DockerContainersTable: FC<{
{searchInput && {searchInput &&
table.getFilteredRowModel().rows.length > 0 && ( table.getFilteredRowModel().rows.length > 0 && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-sm text-muted-foreground"> <div className="absolute right-2 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length}{" "} {t("searchResultsCount", { count: table.getFilteredRowModel().rows.length })}
result
{table.getFilteredRowModel().rows.length !==
1
? "s"
: ""}
</div> </div>
)} )}
</div> </div>
@ -489,7 +486,7 @@ const DockerContainersTable: FC<{
className="gap-2" className="gap-2"
> >
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
Filters {t("filters")}
{(hideContainersWithoutPorts || {(hideContainersWithoutPorts ||
hideStoppedContainers) && ( hideStoppedContainers) && (
<span className="bg-primary text-primary-foreground rounded-full w-5 h-5 text-xs flex items-center justify-center"> <span className="bg-primary text-primary-foreground rounded-full w-5 h-5 text-xs flex items-center justify-center">
@ -502,7 +499,7 @@ const DockerContainersTable: FC<{
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64"> <DropdownMenuContent align="start" className="w-64">
<DropdownMenuLabel> <DropdownMenuLabel>
Filter Options {t("filterOptions")}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
@ -511,13 +508,13 @@ const DockerContainersTable: FC<{
setHideContainersWithoutPorts setHideContainersWithoutPorts
} }
> >
Ports {t("filterPorts")}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
checked={hideStoppedContainers} checked={hideStoppedContainers}
onCheckedChange={setHideStoppedContainers} onCheckedChange={setHideStoppedContainers}
> >
Stopped {t("filterStopped")}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
{(hideContainersWithoutPorts || {(hideContainersWithoutPorts ||
hideStoppedContainers) && ( hideStoppedContainers) && (
@ -537,7 +534,7 @@ const DockerContainersTable: FC<{
}} }}
className="w-full text-xs" className="w-full text-xs"
> >
Clear all filters {t("clearAllFilters")}
</Button> </Button>
</div> </div>
</> </>
@ -553,12 +550,12 @@ const DockerContainersTable: FC<{
className="gap-2" className="gap-2"
> >
<Columns className="h-4 w-4" /> <Columns className="h-4 w-4" />
Columns {t("columns")}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48"> <DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel> <DropdownMenuLabel>
Toggle Columns {t("toggleColumns")}
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{table {table
@ -577,7 +574,7 @@ const DockerContainersTable: FC<{
} }
> >
{column.id === "hostname" {column.id === "hostname"
? "Hostname/IP" ? t("containerHostnameIp")
: column.id} : column.id}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
); );
@ -589,7 +586,7 @@ const DockerContainersTable: FC<{
variant="outline" variant="outline"
size="icon" size="icon"
onClick={onRefresh} onClick={onRefresh}
title="Refresh containers list" title={t("refreshContainersList")}
> >
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
</Button> </Button>
@ -644,10 +641,10 @@ const DockerContainersTable: FC<{
{searchInput && !globalFilter ? ( {searchInput && !globalFilter ? (
<div className="flex items-center justify-center gap-2 text-muted-foreground"> <div className="flex items-center justify-center gap-2 text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Searching... {t("searching")}
</div> </div>
) : ( ) : (
`No containers found matching "${globalFilter}".` t("noContainersFoundMatching", { filter: globalFilter })
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>