feat(i18n): localize shell and common UI
This commit is contained in:
@@ -3,6 +3,7 @@ import { Toaster } from "sonner"
|
||||
import Navbar from "@/components/layout/navbar"
|
||||
import AppSidebar from "@/components/layout/sidebar"
|
||||
import { SidebarProvider } from "@/components/ui/sidebar"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function LayoutDashboard({
|
||||
@@ -11,10 +12,14 @@ export default async function LayoutDashboard({
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await auth()
|
||||
const { dictionary } = await getI18n()
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar userRole={session?.user.role} />
|
||||
<AppSidebar
|
||||
copy={dictionary.layout.sidebar}
|
||||
userRole={session?.user.role}
|
||||
/>
|
||||
<main className="w-full">
|
||||
<Navbar />
|
||||
<div className="flex-1 p-6">{children}</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import Link from "next/link"
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
import { getI18n } from "@/i18n/server"
|
||||
|
||||
export default async function ForbiddenPage() {
|
||||
const { dictionary } = await getI18n()
|
||||
const copy = dictionary.common.forbidden
|
||||
return (
|
||||
<main>
|
||||
<h1>Acceso denegado</h1>
|
||||
<p>No tienes permisos para acceder a esta sección.</p>
|
||||
<Link href="/">Volver al inicio</Link>
|
||||
<h1>{copy.title}</h1>
|
||||
<p>{copy.description}</p>
|
||||
<Link href="/">{copy.homeLink}</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
import { signOut } from "@/lib/auth"
|
||||
import { SIGN_IN_URL } from "@/lib/constants"
|
||||
|
||||
export function SignOut() {
|
||||
type SignOutProps = {
|
||||
copy: Dictionary["layout"]["logout"]
|
||||
}
|
||||
|
||||
export function SignOut({ copy }: SignOutProps) {
|
||||
return (
|
||||
<form
|
||||
action={async () => {
|
||||
@@ -11,7 +16,7 @@ export function SignOut() {
|
||||
}}
|
||||
>
|
||||
<Button type="submit" variant="destructive">
|
||||
Sign Out
|
||||
{copy.label}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link"
|
||||
|
||||
import Search from "@/components/common/search"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
|
||||
interface PageHeaderProps {
|
||||
title?: string
|
||||
@@ -11,17 +12,21 @@ interface PageHeaderProps {
|
||||
data: unknown[]
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
export default async function PageHeader({
|
||||
title,
|
||||
link,
|
||||
search,
|
||||
data,
|
||||
}: PageHeaderProps) {
|
||||
const { dictionary } = await getI18n()
|
||||
return (
|
||||
<header className="mb-4 flex w-full flex-col gap-4 md:flex-row">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<Search hidden={data.length === 0 && !search} />
|
||||
<Search
|
||||
copy={dictionary.common.search}
|
||||
hidden={data.length === 0 && !search}
|
||||
/>
|
||||
</div>
|
||||
{link && (
|
||||
<div className="justify-end md:ml-auto md:flex">
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
type PaginationCopy = Dictionary["common"]["pagination"]
|
||||
|
||||
type PaginationClientProps = {
|
||||
copy: PaginationCopy
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export function PaginationClient({ copy, totalPages }: PaginationClientProps) {
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const currentPage = Number(searchParams.get("page")) || 1
|
||||
|
||||
const createPageURL = (pageNumber: number | string) => {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set("page", pageNumber.toString())
|
||||
router.push(`${pathname}?${params.toString()}`)
|
||||
return `${pathname}?${params.toString()}`
|
||||
}
|
||||
|
||||
const getPageNumbers = () => {
|
||||
let start = Math.max(1, currentPage - 1)
|
||||
let end = Math.min(totalPages, currentPage + 1)
|
||||
|
||||
// Always try to show 3 pages if possible
|
||||
if (end - start < 2) {
|
||||
if (start === 1) {
|
||||
end = Math.min(totalPages, start + 2)
|
||||
} else if (end === totalPages) {
|
||||
start = Math.max(1, end - 2)
|
||||
}
|
||||
}
|
||||
|
||||
const pages = []
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
const pageNumbers = getPageNumbers()
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
{copy.summaryPrefix} {currentPage} {copy.summarySeparator} {totalPages}
|
||||
</div>
|
||||
<div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{currentPage > 1 && (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => createPageURL(currentPage - 1)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.previous}
|
||||
</PaginationPrevious>
|
||||
</PaginationItem>
|
||||
)}
|
||||
{pageNumbers.map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
onClick={() => createPageURL(page)}
|
||||
isActive={page === currentPage}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
{currentPage < totalPages && (
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => createPageURL(currentPage + 1)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{copy.next}
|
||||
</PaginationNext>
|
||||
</PaginationItem>
|
||||
)}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +1,18 @@
|
||||
"use client"
|
||||
import { getI18n } from "@/i18n/server"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { PaginationClient } from "./pagination.client"
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
|
||||
export default function PaginationButtons({
|
||||
export default async function PaginationButtons({
|
||||
totalPages,
|
||||
}: {
|
||||
totalPages: number
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const currentPage = Number(searchParams.get("page")) || 1
|
||||
|
||||
const createPageURL = (pageNumber: number | string) => {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set("page", pageNumber.toString())
|
||||
router.push(`${pathname}?${params.toString()}`)
|
||||
return `${pathname}?${params.toString()}`
|
||||
}
|
||||
|
||||
const getPageNumbers = () => {
|
||||
let start = Math.max(1, currentPage - 1)
|
||||
let end = Math.min(totalPages, currentPage + 1)
|
||||
|
||||
// Always try to show 3 pages if possible
|
||||
if (end - start < 2) {
|
||||
if (start === 1) {
|
||||
end = Math.min(totalPages, start + 2)
|
||||
} else if (end === totalPages) {
|
||||
start = Math.max(1, end - 2)
|
||||
}
|
||||
}
|
||||
|
||||
const pages = []
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
const pageNumbers = getPageNumbers()
|
||||
const { dictionary } = await getI18n()
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
Showing page {currentPage} of {totalPages}
|
||||
</div>
|
||||
<div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{currentPage > 1 && (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => createPageURL(currentPage - 1)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Previous
|
||||
</PaginationPrevious>
|
||||
</PaginationItem>
|
||||
)}
|
||||
{pageNumbers.map((page) => (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
onClick={() => createPageURL(page)}
|
||||
isActive={page === currentPage}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
{currentPage < totalPages && (
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => createPageURL(currentPage + 1)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Next
|
||||
</PaginationNext>
|
||||
</PaginationItem>
|
||||
)}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
<PaginationClient
|
||||
copy={dictionary.common.pagination}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,23 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useDebouncedCallback } from "use-debounce"
|
||||
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
import { Input } from "../ui/input"
|
||||
|
||||
type SearchCopy = Dictionary["common"]["search"]
|
||||
|
||||
interface SearchProps {
|
||||
copy: SearchCopy
|
||||
paramKey?: string
|
||||
placeholder?: string
|
||||
[x: string]: unknown
|
||||
}
|
||||
|
||||
export default function Search({
|
||||
copy,
|
||||
paramKey = "search",
|
||||
placeholder = "Search...",
|
||||
placeholder = copy.placeholder,
|
||||
...props
|
||||
}: SearchProps) {
|
||||
const pathname = usePathname()
|
||||
@@ -73,7 +79,7 @@ export default function Search({
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
role="searchbox"
|
||||
aria-label="Buscar"
|
||||
aria-label={copy.label}
|
||||
placeholder={placeholder}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
@@ -86,7 +92,7 @@ export default function Search({
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Limpiar búsqueda"
|
||||
aria-label={copy.clearLabel}
|
||||
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-3 -translate-y-1/2"
|
||||
onClick={clearSearch}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Plus } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
import { ENVIRONMENT } from "@/lib/constants"
|
||||
|
||||
import { Button } from "../ui/button"
|
||||
@@ -13,35 +16,43 @@ import {
|
||||
} from "../ui/dropdown-menu"
|
||||
import ResetButton from "./resetButton"
|
||||
|
||||
const items = [
|
||||
type AddMenuCopy = Dictionary["layout"]["addMenu"]
|
||||
|
||||
type AddMenuProps = {
|
||||
copy: AddMenuCopy
|
||||
resetCopy: Dictionary["layout"]["resetDatabase"]
|
||||
}
|
||||
|
||||
const items: { key: keyof AddMenuCopy; href: string }[] = [
|
||||
{
|
||||
name: "Category",
|
||||
key: "category",
|
||||
href: "/inventory/categories/new",
|
||||
},
|
||||
{
|
||||
name: "Item",
|
||||
key: "item",
|
||||
href: "/inventory/items/new",
|
||||
},
|
||||
{
|
||||
name: "Asset",
|
||||
key: "asset",
|
||||
href: "/inventory/assets/new",
|
||||
},
|
||||
{
|
||||
name: "Recipient",
|
||||
key: "recipient",
|
||||
href: "/recipients/new",
|
||||
},
|
||||
{
|
||||
name: "Assignment",
|
||||
key: "assignment",
|
||||
href: "/assignments/new",
|
||||
},
|
||||
]
|
||||
|
||||
export default function AddMenu() {
|
||||
export default function AddMenu({ copy, resetCopy }: AddMenuProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" className="btn btn-primary">
|
||||
<Button type="button" aria-label={copy.add} className="btn btn-primary">
|
||||
<Plus />
|
||||
<span className="sr-only">{copy.add}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
@@ -51,18 +62,18 @@ export default function AddMenu() {
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
passHref
|
||||
>
|
||||
<Plus /> Import
|
||||
<Plus /> {copy.import}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem key={item.name} asChild>
|
||||
<DropdownMenuItem key={item.key} asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
passHref
|
||||
>
|
||||
<Plus /> {item.name}
|
||||
<Plus /> {copy[item.key]}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -70,7 +81,7 @@ export default function AddMenu() {
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<ResetButton />
|
||||
<ResetButton copy={resetCopy} />
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,10 @@ export default async function Navbar() {
|
||||
<nav className="flex items-center justify-between border-b p-4">
|
||||
<SidebarTrigger />
|
||||
<div className="flex items-center gap-4">
|
||||
<AddMenu />
|
||||
<AddMenu
|
||||
copy={dictionary.layout.addMenu}
|
||||
resetCopy={dictionary.layout.resetDatabase}
|
||||
/>
|
||||
<LanguageSwitcher
|
||||
activeLocale={locale}
|
||||
copy={dictionary.common.languageSwitcher}
|
||||
@@ -57,10 +60,12 @@ export default async function Navbar() {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>
|
||||
{dictionary.layout.navbar.accountLabel}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<SignOut />
|
||||
<SignOut copy={dictionary.layout.logout} />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -5,8 +5,13 @@ import { useRouter } from "next/navigation"
|
||||
import { signOut } from "next-auth/react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
export default function ResetButton() {
|
||||
type ResetButtonProps = {
|
||||
copy: Dictionary["layout"]["resetDatabase"]
|
||||
}
|
||||
|
||||
export default function ResetButton({ copy }: ResetButtonProps) {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
@@ -20,11 +25,11 @@ export default function ResetButton() {
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Database reseted successfully")
|
||||
toast.success(copy.successToast)
|
||||
signOut()
|
||||
router.push("/login")
|
||||
} else {
|
||||
toast.error("Error resetting database")
|
||||
toast.error(copy.errorToast)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -37,7 +42,7 @@ export default function ResetButton() {
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <Loader2 className="animate-spin" /> : <Trash />}
|
||||
{loading ? "Resetting..." : "Reset Database"}
|
||||
{loading ? copy.loading : copy.idle}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,60 +22,84 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
import type { UserRole } from "@/generated/prisma/client"
|
||||
import type { Dictionary } from "@/i18n/dictionaries"
|
||||
|
||||
import { SidebarSection } from "./sidebar/sidebarSection"
|
||||
|
||||
const items = [
|
||||
type SidebarCopy = Dictionary["layout"]["sidebar"]
|
||||
|
||||
type SidebarLabelKey = keyof SidebarCopy
|
||||
|
||||
type SidebarItem =
|
||||
| {
|
||||
type: "item"
|
||||
labelKey: SidebarLabelKey
|
||||
url: string
|
||||
icon: React.ElementType
|
||||
}
|
||||
| {
|
||||
type: "section"
|
||||
labelKey: SidebarLabelKey
|
||||
url: string
|
||||
icon: React.ElementType
|
||||
items: { labelKey: SidebarLabelKey; url: string }[]
|
||||
}
|
||||
|
||||
const items: SidebarItem[] = [
|
||||
{
|
||||
type: "item",
|
||||
title: "Home",
|
||||
labelKey: "home",
|
||||
url: "/",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
title: "Inventory",
|
||||
labelKey: "inventory",
|
||||
url: "#",
|
||||
icon: Package,
|
||||
items: [
|
||||
{
|
||||
title: "Items",
|
||||
labelKey: "items",
|
||||
url: "/inventory/items",
|
||||
},
|
||||
{
|
||||
title: "Categories",
|
||||
labelKey: "categories",
|
||||
url: "/inventory/categories",
|
||||
},
|
||||
{
|
||||
title: "Assets",
|
||||
labelKey: "assets",
|
||||
url: "/inventory/assets",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
title: "Recipients",
|
||||
labelKey: "recipients",
|
||||
url: "/recipients",
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
title: "Movements",
|
||||
labelKey: "movements",
|
||||
url: "/movements",
|
||||
icon: BarChart,
|
||||
},
|
||||
{
|
||||
type: "item",
|
||||
title: "Assignments",
|
||||
labelKey: "assignments",
|
||||
url: "/assignments",
|
||||
icon: Clipboard,
|
||||
},
|
||||
]
|
||||
|
||||
export default function AppSidebar({
|
||||
copy,
|
||||
userRole,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar> & { userRole?: UserRole }) {
|
||||
}: React.ComponentProps<typeof Sidebar> & {
|
||||
copy: SidebarCopy
|
||||
userRole?: UserRole
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const visibleItems =
|
||||
userRole === "ADMIN"
|
||||
@@ -83,10 +107,10 @@ export default function AppSidebar({
|
||||
...items,
|
||||
{
|
||||
type: "item",
|
||||
title: "Users",
|
||||
labelKey: "users",
|
||||
url: "/admin/users",
|
||||
icon: Shield,
|
||||
},
|
||||
} satisfies SidebarItem,
|
||||
]
|
||||
: items
|
||||
|
||||
@@ -110,11 +134,11 @@ export default function AppSidebar({
|
||||
: pathname.startsWith(item.url)
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={`item-${item.title}`}>
|
||||
<SidebarMenuItem key={`item-${item.labelKey}`}>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<Link href={item.url}>
|
||||
<item.icon className="mr-2 h-4 w-4" />
|
||||
<span>{item.title}</span>
|
||||
<span>{copy[item.labelKey]}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -123,10 +147,13 @@ export default function AppSidebar({
|
||||
if (item.type === "section") {
|
||||
return (
|
||||
<SidebarSection
|
||||
key={`section-${item.title}`}
|
||||
title={item.title}
|
||||
key={`section-${item.labelKey}`}
|
||||
title={copy[item.labelKey]}
|
||||
icon={item.icon}
|
||||
items={item.items}
|
||||
items={item.items.map((subItem) => ({
|
||||
title: copy[subItem.labelKey],
|
||||
url: subItem.url,
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,56 @@ export const en = {
|
||||
es: "Spanish",
|
||||
},
|
||||
},
|
||||
search: {
|
||||
placeholder: "Search...",
|
||||
label: "Search",
|
||||
clearLabel: "Clear search",
|
||||
},
|
||||
pagination: {
|
||||
summaryPrefix: "Showing page",
|
||||
summarySeparator: "of",
|
||||
previous: "Previous",
|
||||
next: "Next",
|
||||
},
|
||||
forbidden: {
|
||||
title: "Access denied",
|
||||
description: "You do not have permission to access this section.",
|
||||
homeLink: "Back to home",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
sidebar: {
|
||||
home: "Home",
|
||||
inventory: "Inventory",
|
||||
items: "Items",
|
||||
categories: "Categories",
|
||||
assets: "Assets",
|
||||
recipients: "Recipients",
|
||||
movements: "Movements",
|
||||
assignments: "Assignments",
|
||||
users: "Users",
|
||||
},
|
||||
navbar: {
|
||||
accountLabel: "My Account",
|
||||
},
|
||||
addMenu: {
|
||||
add: "Add",
|
||||
import: "Import",
|
||||
category: "Category",
|
||||
item: "Item",
|
||||
asset: "Asset",
|
||||
recipient: "Recipient",
|
||||
assignment: "Assignment",
|
||||
},
|
||||
resetDatabase: {
|
||||
idle: "Reset Database",
|
||||
loading: "Resetting...",
|
||||
successToast: "Database reset successfully",
|
||||
errorToast: "Error resetting database",
|
||||
},
|
||||
logout: {
|
||||
label: "Sign Out",
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Sign In",
|
||||
|
||||
@@ -9,6 +9,56 @@ export const es = {
|
||||
es: "Español",
|
||||
},
|
||||
},
|
||||
search: {
|
||||
placeholder: "Buscar...",
|
||||
label: "Buscar",
|
||||
clearLabel: "Limpiar búsqueda",
|
||||
},
|
||||
pagination: {
|
||||
summaryPrefix: "Mostrando página",
|
||||
summarySeparator: "de",
|
||||
previous: "Anterior",
|
||||
next: "Siguiente",
|
||||
},
|
||||
forbidden: {
|
||||
title: "Acceso denegado",
|
||||
description: "No tienes permisos para acceder a esta sección.",
|
||||
homeLink: "Volver al inicio",
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
sidebar: {
|
||||
home: "Inicio",
|
||||
inventory: "Inventario",
|
||||
items: "Artículos",
|
||||
categories: "Categorías",
|
||||
assets: "Activos",
|
||||
recipients: "Destinatarios",
|
||||
movements: "Movimientos",
|
||||
assignments: "Asignaciones",
|
||||
users: "Usuarios",
|
||||
},
|
||||
navbar: {
|
||||
accountLabel: "Mi cuenta",
|
||||
},
|
||||
addMenu: {
|
||||
add: "Añadir",
|
||||
import: "Importar",
|
||||
category: "Categoría",
|
||||
item: "Artículo",
|
||||
asset: "Activo",
|
||||
recipient: "Destinatario",
|
||||
assignment: "Asignación",
|
||||
},
|
||||
resetDatabase: {
|
||||
idle: "Reiniciar base de datos",
|
||||
loading: "Reiniciando...",
|
||||
successToast: "Base de datos reiniciada correctamente",
|
||||
errorToast: "Error al reiniciar la base de datos",
|
||||
},
|
||||
logout: {
|
||||
label: "Cerrar sesión",
|
||||
},
|
||||
},
|
||||
login: {
|
||||
title: "Iniciar sesión",
|
||||
|
||||
Reference in New Issue
Block a user