feat: internationalize button labels and UI components

- Updated CancelButton and SubmitButton to use i18next for localization.
- Modified DataTableColumnHeader and DataTableFacetedFilter to include translations for sorting and filtering messages.
- Enhanced DataTableSelectionColumn with localized aria-labels for accessibility.
- Localized titles and messages in SimpleEmptyState and ErrorOverlay components.
- Updated LayoutHeader to use i18next for navigation toggle label.
- Integrated i18next into useLogout for error messages.
- Localized alert dialog actions in useRemoveAlertDialog.
- Added new translations in en.json and es.json for various UI components.
- Updated breadcrumb and carousel components to use localized strings.
- Enhanced pagination component with i18next for navigation labels.
- Localized sheet component close button text.
This commit is contained in:
David Arranz 2026-10-05 13:58:03 +02:00
parent 988790f904
commit c77a3a3af5
35 changed files with 368 additions and 243 deletions

View File

@ -73,7 +73,7 @@ export const LoginPageWithLanguageSelector = () => {
}, },
onError: (error) => { onError: (error) => {
const { message } = error; const { message } = error;
toast({ description: message, title: "Error" }); toast({ description: message, title: t("common.error") });
console.log(error); console.log(error);
}, },
}); });

View File

@ -100,7 +100,7 @@ export const CatalogDataTable = () => {
if (data?.total_items === 0 && !isFiltered) { if (data?.total_items === 0 && !isFiltered) {
return ( return (
<SimpleEmptyState <SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo' subtitle={t("catalog.list.empty_state.subtitle")}
buttonText='' buttonText=''
onButtonClick={() => navigate("/catalog/add")} onButtonClick={() => navigate("/catalog/add")}
/> />

View File

@ -80,10 +80,10 @@ export function CatalogDataTableFilter<TData>({
<XIcon className='w-4 h-4' /> <XIcon className='w-4 h-4' />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{t("catalog.filter.badget_remove_tooltip")}</p> <p>{t("common.filter.badget_remove_tooltip")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<span className='sr-only'>{t("catalog.filter.badget_remove_tooltip")}</span> <span className='sr-only'>{t("common.filter.badget_remove_tooltip")}</span>
</Button> </Button>
</Badge> </Badge>
))} ))}

View File

@ -7,6 +7,7 @@ import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { IListArticles_Response_DTO } from "@shared/contexts"; import { IListArticles_Response_DTO } from "@shared/contexts";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { t } from "i18next";
import { useMemo } from "react"; import { useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useDealerList } from "../hooks"; import { useDealerList } from "../hooks";
@ -70,7 +71,7 @@ export const DealerDataTable = () => {
if (data?.total_items === 0 && !isFiltered) { if (data?.total_items === 0 && !isFiltered) {
return ( return (
<SimpleEmptyState <SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo' subtitle={t("catalog.list.empty_state.subtitle")}
buttonText='' buttonText=''
onButtonClick={() => navigate("/catalog/add")} onButtonClick={() => navigate("/catalog/add")}
/> />

View File

@ -254,7 +254,7 @@ export const CatalogPickerDataTable = ({
if (data?.total_items === 0 && !isFiltered) { if (data?.total_items === 0 && !isFiltered) {
return ( return (
<SimpleEmptyState <SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo' subtitle={t("catalog.list.empty_state.subtitle")}
buttonText='' buttonText=''
onButtonClick={() => navigate("/catalog/add")} onButtonClick={() => navigate("/catalog/add")}
/> />

View File

@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
import { Button, Card, CardContent, CardHeader, CardTitle, Progress } from "@/ui"; import { Button, Card, CardContent, CardHeader, CardTitle, Progress } from "@/ui";
import { FileTextIcon, Loader2Icon, XCircleIcon } from "lucide-react"; import { FileTextIcon, Loader2Icon, XCircleIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { t } from "i18next";
interface PDFGenerationLoadingProps { interface PDFGenerationLoadingProps {
className?: string; className?: string;
@ -38,7 +39,7 @@ export const PDFGenerationLoading = ({
<Card className={cn("overflow-hidden", className)}> <Card className={cn("overflow-hidden", className)}>
<CardHeader> <CardHeader>
<CardTitle className='flex items-center justify-between'> <CardTitle className='flex items-center justify-between'>
{error?.errorMessage ? "Error en la generación del PDF" : "Generando PDF"} {error?.errorMessage ? t("quotes.pdf_generation.error") : t("quotes.pdf_generation.title")}
{isInProgress && <Loader2Icon className='w-4 h-4 animate-spin' />} {isInProgress && <Loader2Icon className='w-4 h-4 animate-spin' />}
{error?.errorMessage && <XCircleIcon className='w-4 h-4 text-destructive' />} {error?.errorMessage && <XCircleIcon className='w-4 h-4 text-destructive' />}
</CardTitle> </CardTitle>
@ -60,14 +61,14 @@ export const PDFGenerationLoading = ({
</div> </div>
<Progress value={percentage} className='w-full' /> <Progress value={percentage} className='w-full' />
<div className='text-sm text-center text-muted-foreground'> <div className='text-sm text-center text-muted-foreground'>
<p>Progreso: {percentage}%</p> <p>{t("quotes.pdf_generation.progress", { percentage })}</p>
<div className='hidden'> <div className='hidden'>
<p>Tiempo transcurrido: {elapsed} segundos</p> <p>{t("quotes.pdf_generation.elapsed", { elapsed })}</p>
{estimatedTime > 0 && <p>Tiempo estimado restante: {estimatedTime} segundos</p>} {estimatedTime > 0 && <p>{t("quotes.pdf_generation.remaining", { estimatedTime })}</p>}
</div> </div>
</div> </div>
<Button variant='destructive' onClick={cancel} className='hidden'> <Button variant='destructive' onClick={cancel} className='hidden'>
Cancelar generación {t("quotes.pdf_generation.cancel")}
</Button> </Button>
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import { cn } from "@/lib/utils";
import { Card, CardContent } from "@/ui"; import { Card, CardContent } from "@/ui";
import { IListQuotes_Response_DTO } from "@shared/contexts"; import { IListQuotes_Response_DTO } from "@shared/contexts";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { t } from "i18next";
import { useQuotes } from "../hooks"; import { useQuotes } from "../hooks";
import { PDFGenerationLoading } from "./PDFGenerationLoading"; import { PDFGenerationLoading } from "./PDFGenerationLoading";
@ -60,7 +61,7 @@ const QuotePDFPreview = ({
return ( return (
<Card className={cn("overflow-hidden", className)}> <Card className={cn("overflow-hidden", className)}>
<CardContent className='px-4 py-6 text-center'> <CardContent className='px-4 py-6 text-center'>
<p className='mx-auto'>Select a quote</p> <p className='mx-auto'>{t("quotes.list.resume.select_quote")}</p>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -134,7 +134,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
return ( return (
<Card className={cn("overflow-hidden", className)}> <Card className={cn("overflow-hidden", className)}>
<CardContent className='px-4 py-6 text-center'> <CardContent className='px-4 py-6 text-center'>
<p className='mx-auto'>Select a quote</p> <p className='mx-auto'>{t("quotes.list.resume.select_quote")}</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@ -204,11 +204,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}} }}
> >
<EditIcon className='w-4 h-4' /> <EditIcon className='w-4 h-4' />
<span className='sr-only'>Editar</span> <span className='sr-only'>{t("common.edit")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Editar</p> <p>{t("common.edit")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -223,11 +223,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}} }}
> >
<CopyIcon className='w-4 h-4' /> <CopyIcon className='w-4 h-4' />
<span className='sr-only'>Duplicar</span> <span className='sr-only'>{t("common.duplicate")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Duplicar</p> <p>{t("common.duplicate")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -235,11 +235,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant='outline' size='icon' onClick={handleDownload}> <Button variant='outline' size='icon' onClick={handleDownload}>
<DownloadIcon className='w-4 h-4' /> <DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span> <span className='sr-only'>{t("common.download")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Descargar</p> <p>{t("common.download")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -255,11 +255,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}} }}
> >
<ArchiveIcon className='w-4 h-4' /> <ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>Archivar</span> <span className='sr-only'>{t("common.archive")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Archivar</p> <p>{t("common.archive")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>

View File

@ -277,11 +277,11 @@ export const QuotesDataTable = ({
}} }}
> >
<EditIcon className='w-4 h-4' /> <EditIcon className='w-4 h-4' />
<span className='sr-only'>Editar</span> <span className='sr-only'>{t("common.edit")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Editar</p> <p>{t("common.edit")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -296,11 +296,11 @@ export const QuotesDataTable = ({
}} }}
> >
<CopyIcon className='w-4 h-4' /> <CopyIcon className='w-4 h-4' />
<span className='sr-only'>Duplicar</span> <span className='sr-only'>{t("common.duplicate")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Duplicar</p> <p>{t("common.duplicate")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -314,11 +314,11 @@ export const QuotesDataTable = ({
}} }}
> >
<DownloadIcon className='w-4 h-4' /> <DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span> <span className='sr-only'>{t("common.download")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Descargar</p> <p>{t("common.download")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -334,11 +334,11 @@ export const QuotesDataTable = ({
}} }}
> >
<ArchiveIcon className='w-4 h-4' /> <ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>Archivar</span> <span className='sr-only'>{t("common.archive")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Archivar</p> <p>{t("common.archive")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
@ -398,7 +398,7 @@ export const QuotesDataTable = ({
<Card> <Card>
<CardContent> <CardContent>
<SimpleEmptyState <SimpleEmptyState
subtitle='Empieza creando alguna cotización' subtitle={t("quotes.list.empty_state.subtitle")}
buttonText={t("quotes.create.title")} buttonText={t("quotes.create.title")}
onButtonClick={() => navigate("add", { relative: "path" })} onButtonClick={() => navigate("add", { relative: "path" })}
/> />

View File

@ -24,7 +24,7 @@ export const QuoteDocumentsCardEditor = () => {
render={() => ( render={() => (
<div className='space-y-6'> <div className='space-y-6'>
<FormItem className='w-full'> <FormItem className='w-full'>
<FormLabel>Images</FormLabel> <FormLabel>{t("quotes.create.form_groups.documents.images")}</FormLabel>
<FormControl></FormControl> <FormControl></FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -32,7 +32,7 @@ export const QuoteDocumentsCardEditor = () => {
)} )}
/> />
<Button className='w-fit' disabled={formState.disabled}> <Button className='w-fit' disabled={formState.disabled}>
Save {t("common.save")}
</Button> </Button>
</FormGroup> </FormGroup>
</div> </div>

View File

@ -77,15 +77,15 @@ export const QuoteGeneralCardEditor = () => {
> >
<div className='grid gap-6'> <div className='grid gap-6'>
<div className='grid gap-3'> <div className='grid gap-3'>
<Label htmlFor='status'>Status</Label> <Label htmlFor='status'>{t("quotes.create.form_groups.status.title")}</Label>
<Select> <Select>
<SelectTrigger id='status' aria-label='Select status'> <SelectTrigger id='status' aria-label={t("quotes.list.select_status")}>
<SelectValue placeholder='Select status' /> <SelectValue placeholder={t("quotes.list.select_status")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='draft'>Draft</SelectItem> <SelectItem value='draft'>{t("quotes.status.draft")}</SelectItem>
<SelectItem value='published'>Active</SelectItem> <SelectItem value='published'>{t("quotes.status.ready")}</SelectItem>
<SelectItem value='archived'>Archived</SelectItem> <SelectItem value='archived'>{t("quotes.status.archived")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -69,7 +69,7 @@ export const QuoteCreate = () => {
onError: (error) => { onError: (error) => {
console.debug(error); console.debug(error);
toast({ toast({
title: "Error", title: t("common.error"),
description: error.message, description: error.message,
variant: "destructive", variant: "destructive",
}); });
@ -77,7 +77,7 @@ export const QuoteCreate = () => {
onSuccess: (data) => { onSuccess: (data) => {
reset(getValues()); reset(getValues());
toast({ toast({
title: "Cotización creada", title: t("quotes.notifications.created"),
variant: "default", variant: "default",
}); });
navigate(`/quotes/edit/${data.id}`, { relative: "path" }); navigate(`/quotes/edit/${data.id}`, { relative: "path" });
@ -89,7 +89,7 @@ export const QuoteCreate = () => {
}; };
if (isSubmitting) { if (isSubmitting) {
return <LoadingOverlay title='Guardando ajustes' />; return <LoadingOverlay title={t("quotes.notifications.saving")} />;
} }
/*if (isError) { /*if (isError) {

View File

@ -314,13 +314,13 @@ export const QuoteEdit = () => {
mutate(data, { mutate(data, {
onError: (error) => { onError: (error) => {
console.error(error); console.error(error);
toast({ title: "Error", description: error.message, variant: "destructive" }); toast({ title: t("common.error"), description: error.message, variant: "destructive" });
//alert(error.message); //alert(error.message);
}, },
//onSettled: () => {}, //onSettled: () => {},
onSuccess: () => { onSuccess: () => {
reset(getValues()); reset(getValues());
toast({ title: "Cotización guardada", variant: "success" }); toast({ title: t("quotes.notifications.saved"), variant: "success" });
if (shouldRedirect) { if (shouldRedirect) {
navigate("/quotes"); navigate("/quotes");
} }
@ -354,7 +354,7 @@ export const QuoteEdit = () => {
return ( return (
<> <>
{(isSubmitting || isPending) && <LoadingOverlay title='Guardando cotización' />} {(isSubmitting || isPending) && <LoadingOverlay title={t("quotes.notifications.saving")} />}
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit((data) => onSubmit(data, false))}> <form onSubmit={handleSubmit((data) => onSubmit(data, false))}>
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'> <div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>

View File

@ -6,6 +6,7 @@ import { Checkbox } from "@/ui";
import { ColumnDef, Row } from "@tanstack/react-table"; import { ColumnDef, Row } from "@tanstack/react-table";
import { useMemo } from "react"; import { useMemo } from "react";
import { t } from "i18next";
import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell"; import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -53,7 +54,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label='Seleccionar todo' aria-label={t("common.select_all")}
className='translate-y-[0px]' className='translate-y-[0px]'
/> />
), ),
@ -64,7 +65,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
checked={row.getIsSelected()} checked={row.getIsSelected()}
disabled={!row.getCanSelect()} disabled={!row.getCanSelect()}
onCheckedChange={row.getToggleSelectedHandler()} onCheckedChange={row.getToggleSelectedHandler()}
aria-label='Seleccionar fila' aria-label={t("common.select_row")}
className='mt-2' className='mt-2'
/> />
), ),

View File

@ -1,5 +1,5 @@
import { DataTableProvider } from "@/lib/hooks"; import { DataTableProvider } from "@/lib/hooks";
import { Trans } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { QuotesDataTable } from "./components"; import { QuotesDataTable } from "./components";
import { HelpButton } from "@/components"; import { HelpButton } from "@/components";
@ -19,12 +19,12 @@ import {
Toggle, Toggle,
} from "@/ui"; } from "@/ui";
import { useToggle } from "@wojtekmaj/react-hooks"; import { useToggle } from "@wojtekmaj/react-hooks";
import { t } from "i18next";
import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react"; import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export const QuotesList = () => { export const QuotesList = () => {
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [status, setStatus] = useState("all"); const [status, setStatus] = useState("all");
const [enabledPreview, toggleEnabledPreview] = useToggle(true); const [enabledPreview, toggleEnabledPreview] = useToggle(true);
@ -72,7 +72,7 @@ export const QuotesList = () => {
<Label>{t("quotes.list.tabs_title")}</Label> <Label>{t("quotes.list.tabs_title")}</Label>
<Select value={status} onValueChange={setStatus}> <Select value={status} onValueChange={setStatus}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder='Seleccionar estado' /> <SelectValue placeholder={t("quotes.list.select_status")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{quoteStatuses.map((s) => ( {quoteStatuses.map((s) => (
@ -86,14 +86,14 @@ export const QuotesList = () => {
</div> </div>
<div className='flex items-baseline justify-center mr-4 font-medium'> <div className='flex items-baseline justify-center mr-4 font-medium'>
<HelpButton <HelpButton
buttonText='Ayuda' buttonText={t("common.help")}
title='Ayuda sobre el Estado de Cotizaciones' title={t("quotes.list.status_help.title")}
content={HelpContent} content={<HelpContent />}
/> />
</div> </div>
<div className='flex items-center gap-2 ml-auto'> <div className='flex items-center gap-2 ml-auto'>
<Toggle <Toggle
aria-label='Show quote preview' aria-label={t("quotes.list.preview_aria")}
variant={"outline"} variant={"outline"}
defaultPressed={false} defaultPressed={false}
pressed={enabledPreview} pressed={enabledPreview}
@ -123,35 +123,32 @@ export const QuotesList = () => {
); );
}; };
const HelpContent = ( const HelpContent = () => {
<ScrollArea className='grid gap-4 py-4'> const { t } = useTranslation();
<p>El estado de una cotización indica su posición actual en el proceso de negocio.</p>
<p>Los estados posibles son:</p> return <ScrollArea className='grid gap-4 py-4'>
<p>{t("quotes.list.status_help.intro")}</p>
<p>{t("quotes.list.status_help.possible")}</p>
<ul className='pl-6 space-y-2 list-disc'> <ul className='pl-6 space-y-2 list-disc'>
<li> <li>
<strong>Borrador:</strong> La cotización está en proceso de creación o edición. <strong>{t("quotes.status.draft")}:</strong> {t("quotes.quote_status_editor.status.draft.description")}
</li> </li>
<li> <li>
<strong>Preparado:</strong> La cotización está lista para ser enviada al cliente. <strong>{t("quotes.status.ready")}:</strong> {t("quotes.quote_status_editor.status.ready.description")}
</li> </li>
<li> <li>
<strong>Entregado:</strong> La cotización ha sido enviada al cliente. <strong>{t("quotes.status.delivered")}:</strong> {t("quotes.quote_status_editor.status.delivered.description")}
</li> </li>
<li> <li>
<strong>Aceptado:</strong> El cliente ha aprobado la cotización. <strong>{t("quotes.status.accepted")}:</strong> {t("quotes.quote_status_editor.status.accepted.description")}
</li> </li>
<li> <li>
<strong>Rechazado:</strong> El cliente no ha aceptado la cotización. <strong>{t("quotes.status.rejected")}:</strong> {t("quotes.quote_status_editor.status.rejected.description")}
</li> </li>
<li> <li>
<strong>Arcivado:</strong> La cotización ha sido guardada para referencia futura y ya no <strong>{t("quotes.status.archived")}:</strong> {t("quotes.quote_status_editor.status.archived.description")}
está activa.
</li> </li>
</ul> </ul>
<p> <p>{t("quotes.list.status_help.outro")}</p>
Utiliza estos estados para hacer un seguimiento eficiente de tus cotizaciones y optimizar tu </ScrollArea>;
proceso de ventas. El estado <strong>Archivado</strong> es útil para mantener un historial de };
cotizaciones pasadas sin que interfieran con las cotizaciones activas.
</p>
</ScrollArea>
);

View File

@ -133,7 +133,7 @@ export const SettingsEditor = () => {
mutate(data, { mutate(data, {
onError: (error) => { onError: (error) => {
console.debug(error); console.debug(error);
toast({ title: "Error", description: error.message, variant: "destructive" }); toast({ title: t("common.error"), description: error.message, variant: "destructive" });
//alert(error.message); //alert(error.message);
}, },
//onSettled: () => {}, //onSettled: () => {},
@ -142,13 +142,13 @@ export const SettingsEditor = () => {
void i18n.changeLanguage(data.lang_code ?? "es"); void i18n.changeLanguage(data.lang_code ?? "es");
} }
reset(getValues()); reset(getValues());
toast({ description: "Ajustes guardados", variant: "success" }); toast({ description: t("settings.notifications.saved"), variant: "success" });
}, },
}); });
}; };
if (isSubmitting) { if (isSubmitting) {
return <LoadingOverlay title='Guardando ajustes' />; return <LoadingOverlay title={t("settings.notifications.saving")} />;
} }
if (status === "error") { if (status === "error") {
@ -219,8 +219,8 @@ export const SettingsEditor = () => {
<SelectValue placeholder={t("settings.form_fields.language.placeholder")} /> <SelectValue placeholder={t("settings.form_fields.language.placeholder")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='es'>Español</SelectItem> <SelectItem value='es'>{t("settings.form_fields.language.options.es")}</SelectItem>
<SelectItem value='en'>English</SelectItem> <SelectItem value='en'>{t("settings.form_fields.language.options.en")}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
)} )}

View File

@ -30,12 +30,12 @@ import Joi from "joi";
import { HelpCircleIcon } from "lucide-react"; import { HelpCircleIcon } from "lucide-react";
import { useSupport } from "../hooks"; import { useSupport } from "../hooks";
const formSchema = Joi.object({ const createFormSchema = () => Joi.object({
incidence: Joi.string().min(10).required().messages({ incidence: Joi.string().min(10).required().messages({
"string.empty": "Debe escribir algo antes de enviar", "string.empty": t("support.modal.validation.empty"),
"string.min": "El texto es demasiado corto. Debe tener al menos 10 caracteres", "string.min": t("support.modal.validation.min"),
"string.max": "El texto es demasiado largo.", "string.max": t("support.modal.validation.max"),
"any.required": "La descripción es requerida", "any.required": t("support.modal.validation.required"),
}), }),
}); });
@ -49,7 +49,7 @@ export default function SupportModal() {
const form = useForm<SupportDataForm>({ const form = useForm<SupportDataForm>({
mode: "onBlur", mode: "onBlur",
resolver: joiResolver(formSchema), resolver: joiResolver(createFormSchema()),
defaultValues: { defaultValues: {
incidence: "", incidence: "",
}, },
@ -63,8 +63,8 @@ export default function SupportModal() {
mutateOptions: { mutateOptions: {
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: "Incidencia enviada", title: t("support.modal.notifications.sent_title"),
description: "La incidencia se ha enviado correctamente", description: t("support.modal.notifications.sent_description"),
variant: "success", variant: "success",
}); });
setIsOpen(false); setIsOpen(false);
@ -72,9 +72,8 @@ export default function SupportModal() {
}, },
onError: () => { onError: () => {
toast({ toast({
title: "Error en el envío", title: t("support.modal.notifications.error_title"),
description: description: t("support.modal.notifications.error_description"),
"No se ha podido enviar la incidencia correctamente. Por favor, inténtalo de nuevo.",
variant: "destructive", variant: "destructive",
}); });
}, },
@ -111,7 +110,7 @@ export default function SupportModal() {
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<HelpCircleIcon className='w-5 h-5' /> <HelpCircleIcon className='w-5 h-5' />
<span className='sr-only'>Abrir ventana de soporte</span> <span className='sr-only'>{t("support.modal.open")}</span>
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className='sm:max-w-xl'> <DialogContent className='sm:max-w-xl'>
@ -123,14 +122,14 @@ export default function SupportModal() {
<form onSubmit={handleSubmit(onSubmit)} className='space-y-4'> <form onSubmit={handleSubmit(onSubmit)} className='space-y-4'>
<FormTextAreaField <FormTextAreaField
name='incidence' name='incidence'
placeholder='Describe la incidencia aquí...' placeholder={t("support.modal.placeholder")}
className='min-h-96' className='min-h-96'
/> />
<DialogFooter> <DialogFooter>
<Button type='button' variant='outline' onClick={handleClose}> <Button type='button' variant='outline' onClick={handleClose}>
Cancelar {t("common.cancel")}
</Button> </Button>
<Button type='submit'>Enviar incidencia</Button> <Button type='submit'>{t("support.modal.submit")}</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>
@ -140,17 +139,16 @@ export default function SupportModal() {
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Estás seguro de que quieres cancelar?</AlertDialogTitle> <AlertDialogTitle>{t("support.modal.cancel_confirmation.title")}</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Has escrito texto en el campo de descripción. Si cierras la ventana, perderás los {t("support.modal.cancel_confirmation.description")}
cambios no guardados.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={() => setShowConfirmDialog(false)}> <AlertDialogCancel onClick={() => setShowConfirmDialog(false)}>
Volver al formulario {t("support.modal.cancel_confirmation.back")}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction onClick={confirmClose}>Sí, cerrar</AlertDialogAction> <AlertDialogAction onClick={confirmClose}>{t("support.modal.cancel_confirmation.confirm")}</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View File

@ -1,10 +1,11 @@
import { Button, ButtonProps } from "@/ui"; import { Button, ButtonProps } from "@/ui";
import { t } from "i18next";
export interface CancelButtonProps extends ButtonProps { export interface CancelButtonProps extends ButtonProps {
label?: string; label?: string;
} }
export const CancelButton = ({ label = "Cancelar", ...props }: CancelButtonProps): JSX.Element => { export const CancelButton = ({ label = t("common.cancel"), ...props }: CancelButtonProps): JSX.Element => {
return ( return (
<Button type='button' variant='secondary' {...props}> <Button type='button' variant='secondary' {...props}>
{label} {label}

View File

@ -1,11 +1,12 @@
import { ButtonProps } from "@/ui"; import { ButtonProps } from "@/ui";
import { t } from "i18next";
import { CustomButton } from "./CustomButton"; import { CustomButton } from "./CustomButton";
export interface SubmitButtonProps extends ButtonProps { export interface SubmitButtonProps extends ButtonProps {
label?: string; label?: string;
} }
export const SubmitButton = ({ label = "Enviar", ...props }: SubmitButtonProps) => ( export const SubmitButton = ({ label = t("common.submit"), ...props }: SubmitButtonProps) => (
<CustomButton type='submit' label={label} variant='default' {...props} /> <CustomButton type='submit' label={label} variant='default' {...props} />
); );

View File

@ -70,7 +70,7 @@ export function DataTableColumnHeader<TData, TValue>({
? t("common.sort_desc_description") ? t("common.sort_desc_description")
: header.column.getIsSorted() === "asc" : header.column.getIsSorted() === "asc"
? t("common.sort_asc_description") ? t("common.sort_asc_description")
: t("sort_none_description") : t("common.sort_none_description")
} }
size='sm' size='sm'
variant='ghost' variant='ghost'
@ -121,13 +121,13 @@ export function DataTableColumnHeader<TData, TValue>({
{header.column.getCanHide() && ( {header.column.getCanHide() && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => header.column.toggleVisibility(false)} onClick={() => header.column.toggleVisibility(false)}
aria-label={t("Hide")} aria-label={t("common.hide")}
> >
<EyeOffIcon <EyeOffIcon
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
aria-hidden='true' aria-hidden='true'
/> />
{t("Hide")} {t("common.hide")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -22,6 +22,7 @@ import {
import { Separator } from "@/ui" import { Separator } from "@/ui"
import { CheckIcon, PlusCircleIcon } from 'lucide-react' import { CheckIcon, PlusCircleIcon } from 'lucide-react'
import { t } from "i18next"
interface DataTableFacetedFilterProps<TData, TValue> { interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue> column?: Column<TData, TValue>
@ -62,7 +63,7 @@ export function DataTableFacetedFilter<TData, TValue>({
variant="secondary" variant="secondary"
className="px-1 font-normal rounded-sm" className="px-1 font-normal rounded-sm"
> >
{selectedValues.size} selected {t("components.data_table.selected_count", { count: selectedValues.size })}
</Badge> </Badge>
) : ( ) : (
options options
@ -86,7 +87,7 @@ export function DataTableFacetedFilter<TData, TValue>({
<Command> <Command>
<CommandInput placeholder={title} /> <CommandInput placeholder={title} />
<CommandList> <CommandList>
<CommandEmpty>No results found.</CommandEmpty> <CommandEmpty>{t("components.data_table.no_results")}</CommandEmpty>
<CommandGroup> <CommandGroup>
{options.map((option) => { {options.map((option) => {
const isSelected = selectedValues.has(option.value) const isSelected = selectedValues.has(option.value)
@ -136,7 +137,7 @@ export function DataTableFacetedFilter<TData, TValue>({
onSelect={() => column?.setFilterValue(undefined)} onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center" className="justify-center text-center"
> >
Clear filters {t("components.data_table.clear_filters")}
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</> </>
@ -146,4 +147,4 @@ export function DataTableFacetedFilter<TData, TValue>({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) )
} }

View File

@ -1,4 +1,5 @@
import { Checkbox } from "@/ui"; import { Checkbox } from "@/ui";
import { t } from "i18next";
import { DataTableColumnProps } from "./DataTable"; import { DataTableColumnProps } from "./DataTable";
export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnProps<TData, TError> { export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnProps<TData, TError> {
@ -11,7 +12,7 @@ export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnPro
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate") table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label='Seleccionar todo' aria-label={t("common.select_all")}
className='translate-y-[2px]' className='translate-y-[2px]'
/> />
), ),
@ -23,7 +24,7 @@ export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnPro
onCheckedChange={(value) => { onCheckedChange={(value) => {
row.toggleSelected(!!value); row.toggleSelected(!!value);
}} }}
aria-label='Seleccionar file' aria-label={t("common.select_row")}
className='translate-y-[2px]' className='translate-y-[2px]'
/> />
), ),

View File

@ -10,6 +10,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/ui"; } from "@/ui";
import { Settings2Icon } from "lucide-react"; import { Settings2Icon } from "lucide-react";
import { t } from "i18next";
interface DataTableColumnOptionsProps<TData> { interface DataTableColumnOptionsProps<TData> {
table: Table<TData>; table: Table<TData>;
@ -27,11 +28,11 @@ export function DataTableColumnOptions<TData>({
className="hidden h-8 ml-auto lg:flex" className="hidden h-8 ml-auto lg:flex"
> >
<Settings2Icon className="w-4 h-4 mr-2" /> <Settings2Icon className="w-4 h-4 mr-2" />
Columnas {t("components.data_table.columns")}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]"> <DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Columnas</DropdownMenuLabel> <DropdownMenuLabel>{t("components.data_table.columns")}</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{table {table
.getAllColumns() .getAllColumns()

View File

@ -1,10 +1,11 @@
import { Button } from "@/ui"; import { Button } from "@/ui";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { t } from "i18next";
export const SimpleEmptyState = ({ export const SimpleEmptyState = ({
title = "Esto está muy vacío", title = t("components.empty_state.title"),
subtitle = "Empieza solicitando la carga del catálogo", subtitle = t("components.empty_state.subtitle"),
buttonText = "Solicitar carga del catálogo", buttonText = t("components.empty_state.button"),
onButtonClick = () => {}, onButtonClick = () => {},
actions = () => <></>, actions = () => <></>,
}) => { }) => {

View File

@ -1,5 +1,6 @@
import { Button } from "@/ui"; import { Button } from "@/ui";
import { HomeIcon } from "lucide-react"; import { HomeIcon } from "lucide-react";
import { t } from "i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { BackHistoryButton } from "../CustomButtons"; import { BackHistoryButton } from "../CustomButtons";
@ -18,7 +19,7 @@ interface ErrorOverlayProps {
};*/ };*/
export const ErrorOverlay = ({ export const ErrorOverlay = ({
title = "Se ha producido un error", title = t("components.error_overlay.title"),
subtitle = undefined, subtitle = undefined,
description = undefined, description = undefined,
errorMessage = undefined, errorMessage = undefined,
@ -42,7 +43,7 @@ ErrorOverlayProps): JSX.Element => {
<div className='mt-6 space-x-8 md:ml-4'> <div className='mt-6 space-x-8 md:ml-4'>
<BackHistoryButton /> <BackHistoryButton />
<Button onClick={() => navigate("/")}> <Button onClick={() => navigate("/")}>
<HomeIcon className='w-4 h-4 mr-2' /> Ir al inicio <HomeIcon className='w-4 h-4 mr-2' /> {t("components.error_overlay.go_home")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import { Button, Sheet, SheetContent, SheetTrigger } from "@/ui";
import SupportModal from "@/app/support/components/SupportModal"; import SupportModal from "@/app/support/components/SupportModal";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { MenuIcon, Package2Icon } from "lucide-react"; import { MenuIcon, Package2Icon } from "lucide-react";
import { t } from "i18next";
import { useCallback } from "react"; import { useCallback } from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
@ -58,7 +59,7 @@ export const LayoutHeader = () => {
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant='outline' size='icon' className='shrink-0 md:hidden'> <Button variant='outline' size='icon' className='shrink-0 md:hidden'>
<MenuIcon className='w-5 h-5' /> <MenuIcon className='w-5 h-5' />
<span className='sr-only'>Toggle navigation menu</span> <span className='sr-only'>{t("main_menu.toggle_navigation")}</span>
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side='left'> <SheetContent side='left'>

View File

@ -4,6 +4,7 @@ import { useNavigate } from "react-router-dom";
import { useToast } from "@/ui/use-toast"; import { useToast } from "@/ui/use-toast";
import { useQueryKey } from "../useQueryKey"; import { useQueryKey } from "../useQueryKey";
import { t } from "i18next";
export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>) => { export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>) => {
const { onSuccess, onError, ...restParams } = params || {}; const { onSuccess, onError, ...restParams } = params || {};
@ -31,7 +32,7 @@ export const useLogout = (params?: UseMutationOptions<AuthActionResponse, Error>
onError: (error, variables, context) => { onError: (error, variables, context) => {
const { message } = error; const { message } = error;
toast({ toast({
title: "Error", title: t("common.error"),
description: message, description: message,
variant: "destructive", variant: "destructive",
}); });

View File

@ -12,6 +12,7 @@ import {
} from "@/ui/alert-dialog"; } from "@/ui/alert-dialog";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { t } from "i18next";
interface UseRemoveAlertDialogProps { interface UseRemoveAlertDialogProps {
isOpen?: boolean; isOpen?: boolean;
@ -75,7 +76,7 @@ export const useRemoveAlertDialog = ({
<AlertDialogDescription>{description}</AlertDialogDescription> <AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancelar</AlertDialogCancel> <AlertDialogCancel onClick={onCancel}>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={cn( className={cn(
buttonVariants({ variant: "destructive" }), buttonVariants({ variant: "destructive" }),
@ -83,7 +84,7 @@ export const useRemoveAlertDialog = ({
)} )}
onClick={(event) => (onRemove ? onRemove(payload, event) : null)} onClick={(event) => (onRemove ? onRemove(payload, event) : null)}
> >
Eliminar {t("common.remove")}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View File

@ -1,14 +1,12 @@
{ {
"translation": { "translation": {
"common": { "common": {
"loading": "Loading...",
"required": "required", "required": "required",
"cancel": "Cancel", "cancel": "Cancel",
"no": "No", "no": "No",
"yes": "Yes", "yes": "Yes",
"save": "Save", "save": "Save",
"save_close": "Save and Close", "save_close": "Save and Close",
"accept": "OK",
"discard": "Discard", "discard": "Discard",
"hide": "Hide", "hide": "Hide",
"back": "Back", "back": "Back",
@ -16,7 +14,19 @@
"continue": "Continue", "continue": "Continue",
"close": "Close", "close": "Close",
"more": "More", "more": "More",
"pagination": "Pagination",
"breadcrumb": "Breadcrumb",
"previous": "Previous",
"next": "Next",
"more_pages": "More pages",
"previous_slide": "Previous slide",
"next_slide": "Next slide",
"add": "Add", "add": "Add",
"submit": "Submit",
"download": "Download",
"help": "Help",
"select_all": "Select all",
"select_row": "Select row",
"sort_asc": "Asc", "sort_asc": "Asc",
"sort_asc_description": "In ascending order. Click to sort descending order.", "sort_asc_description": "In ascending order. Click to sort descending order.",
"sort_desc": "Desc", "sort_desc": "Desc",
@ -52,21 +62,18 @@
"insert_row_below": "Insert row below", "insert_row_below": "Insert row below",
"pick_date": "Select a date", "pick_date": "Select a date",
"required_field": "This field is required", "required_field": "This field is required",
"unsaved_changes_prompt": "There are unsaved changes. If you leave, you'll lose your changes.",
"edit": "Edit", "edit": "Edit",
"remove": "Remove", "remove": "Remove",
"archive": "Archive", "archive": "Archive",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"print": "Print", "disable_preview": "Hide preview",
"disable_preview": "Disable preview", "enable_preview": "Show preview",
"enable_preview": "Enable preview",
"filter": { "filter": {
"reset_filter": "Reset filter", "reset_filter": "Reset filter",
"badget_remove_tooltip": "Remove this filter term", "badget_remove_tooltip": "Remove this filter term",
"input_placeholder": "Type here to filter...", "input_placeholder": "Type here to filter...",
"button_add_term": "Add filter term", "button_add_term": "Add filter term",
"button_add_term_tooltip": "Add filter term (or press Enter)", "button_add_term_tooltip": "Add filter term (or press Enter)",
"button_remove_all": "Remove all terms from filter",
"help_text": "Press Enter or click the + button to add a term to the filter. Use multiple terms for a broader search." "help_text": "Press Enter or click the + button to add a term to the filter. Use multiple terms for a broader search."
} }
}, },
@ -77,6 +84,21 @@
"loading_overlay": { "loading_overlay": {
"title": "Loading...", "title": "Loading...",
"subtitle": "This may take a few seconds. Please do not close this page." "subtitle": "This may take a few seconds. Please do not close this page."
},
"error_overlay": {
"title": "An error occurred",
"go_home": "Go home"
},
"empty_state": {
"title": "There is nothing here yet",
"subtitle": "Start by requesting the catalog upload",
"button": "Request catalog upload"
},
"data_table": {
"columns": "Columns",
"no_results": "No results found.",
"clear_filters": "Clear filters",
"selected_count": "{{count}} selected"
} }
}, },
"hooks": { "hooks": {
@ -88,13 +110,11 @@
} }
}, },
"main_menu": { "main_menu": {
"home": "Home", "toggle_navigation": "Toggle navigation menu",
"settings": "Settings", "settings": "Settings",
"dealers": "Dealers",
"catalog": "Catalog", "catalog": "Catalog",
"quotes": "Quotes", "quotes": "Quotes",
"orders": "Orders", "search_placeholder": "Search products, quotes, etc.",
"search_placeholder": "Type here for search quotes and articles",
"user": { "user": {
"user_menu": "User menu", "user_menu": "User menu",
"my_account": "My account", "my_account": "My account",
@ -104,14 +124,14 @@
"logout": "Logout" "logout": "Logout"
}, },
"logout_dialog": { "logout_dialog": {
"title": "Confirm", "title": "Log out",
"description": "Are you sure you want to log out?", "description": "Are you sure you want to log out?",
"confirm_label": "Log out", "confirm_label": "Log out",
"cancel_label": "Cancel" "cancel_label": "Cancel"
} }
}, },
"login_page": { "login_page": {
"title": "Partner intranet", "title": "Quoting tool for distributors",
"description": "Enter your email address and password to login", "description": "Enter your email address and password to login",
"email_label": "Email", "email_label": "Email",
"email_placeholder": "myemail@sample.com", "email_placeholder": "myemail@sample.com",
@ -127,11 +147,14 @@
"catalog": { "catalog": {
"list": { "list": {
"title": "Catalog of articles", "title": "Catalog of articles",
"empty_state": {
"subtitle": "Start by loading catalog items"
},
"columns": { "columns": {
"description": "Description", "description": "Description",
"points": "Points", "points": "Points",
"retail_price": "Retail price", "retail_price": "Retail price",
"quantity": "quantity" "quantity": "Quantity"
} }
} }
}, },
@ -139,7 +162,18 @@
"list": { "list": {
"title": "Quotes", "title": "Quotes",
"subtitle": "", "subtitle": "",
"tabs_title": "Status", "tabs_title": "Statuses",
"select_status": "Select status",
"preview_aria": "Show quote preview",
"empty_state": {
"subtitle": "Start by creating a quote"
},
"status_help": {
"title": "Quote status help",
"intro": "A quote status indicates its current position in the business process.",
"possible": "The possible statuses are:",
"outro": "Use these statuses to track quotes efficiently and improve your sales process. The Archived status helps keep history without interfering with active quotes."
},
"tabs": { "tabs": {
"all": "All", "all": "All",
"draft": "Draft", "draft": "Draft",
@ -156,58 +190,49 @@
"status": "Status", "status": "Status",
"customer_reference": "Customer Ref.", "customer_reference": "Customer Ref.",
"customer_information": "Customer", "customer_information": "Customer",
"total_price": "Imp. total", "total_price": "Total amount"
"actions": {
"edit": "Edit quote",
"sent_to": "Send to Uecko"
}
}, },
"resume": { "resume": {
"title": "Quote", "title": "Quote",
"download_quote": "Download quote",
"tabs": { "tabs": {
"resume": "Resume", "resume": "Summary",
"preview": "Preview" "preview": "Preview"
}, },
"quote_information": "Quote Information", "quote_information": "Quote Information",
"customer_information": "Customer Information", "customer_information": "Customer Information",
"payment_information": "Payment Information", "price_information": "Quote totals",
"price_information": "Quote totals" "select_quote": "Select a quote"
} }
}, },
"notifications": {
"created": "Quote created",
"saved": "Quote saved",
"saving": "Saving quote"
},
"pdf_generation": {
"title": "Generating PDF",
"error": "Error generating PDF",
"progress": "Progress: {{percentage}}%",
"elapsed": "Elapsed time: {{elapsed}} seconds",
"remaining": "Estimated time remaining: {{estimatedTime}} seconds",
"cancel": "Cancel generation"
},
"create": { "create": {
"title": "New quote", "title": "New quote",
"tabs": {
"general": "General data",
"items": "Quote items",
"preview": "Quote preview",
"documents": "Documents",
"history": "History"
},
"form_groups": { "form_groups": {
"general": { "general": {
"title": "General Data", "title": "General Data",
"desc": "General data and quote customer" "desc": "General information and the quote's customer"
}, },
"status": { "status": {
"title": "Status", "title": "Status",
"desc": "Quote status" "desc": "Quote status"
}, },
"items": {
"title": "Quote Items",
"desc": "Quote detail lines. Use the catalog to make it easier to fill in the content."
},
"documents": { "documents": {
"title": "Attached Documents", "title": "Attached Documents",
"desc": "Attach documents such as photos, drawings, sketches, etc. to your quotation." "desc": "Attach documents such as photos, drawings, sketches, etc. to your quotation.",
}, "images": "Images"
"history": {
"title": "History",
"desc": "Quote history"
} }
},
"edit": {
"title": "Quote"
} }
}, },
"edit": { "edit": {
@ -225,8 +250,7 @@
"progress": { "progress": {
"label": "Download is in {{status}}", "label": "Download is in {{status}}",
"status_in_progress": "in progress", "status_in_progress": "in progress",
"status_stopped": "stopped", "status_stopped": "stopped"
"value_label": "Elapsed time in seconds {{elapsed}}"
}, },
"cancel_button": "Cancel the download", "cancel_button": "Cancel the download",
"toast_success": "Quote downloaded" "toast_success": "Quote downloaded"
@ -238,9 +262,15 @@
}, },
"blocks_picker_dialog": { "blocks_picker_dialog": {
"title": "Select a text block", "title": "Select a text block",
"description": "To complete your quote, you can default text blocks.", "description": "To help complete your quote, you can add predefined text blocks from this list.",
"toast_article_added": "Text block added:" "toast_article_added": "Text block added:"
}, },
"quote_sent_to_editor": {
"trigger_button": "Send to Uecko",
"title": "Send quote to Uecko",
"description": "Do you want to send this quote to Uecko? This action cannot be undone.",
"toast_status_changed": "Quote sent to Uecko"
},
"quote_status_editor": { "quote_status_editor": {
"trigger_button": "Change quote status", "trigger_button": "Change quote status",
"title": "Change quote status", "title": "Change quote status",
@ -267,7 +297,7 @@
}, },
"archived": { "archived": {
"title": "Archived", "title": "Archived",
"description": "The quote is archived." "description": "The quote has been saved for future reference."
} }
}, },
"submit_button": "Change status", "submit_button": "Change status",
@ -308,11 +338,6 @@
"desc": "Quote reference", "desc": "Quote reference",
"placeholder": "" "placeholder": ""
}, },
"status": {
"label": "Status",
"desc": "Quote status",
"placeholder": ""
},
"lang_code": { "lang_code": {
"label": "Language", "label": "Language",
"desc": "Quote language", "desc": "Quote language",
@ -329,13 +354,13 @@
"placeholder": "" "placeholder": ""
}, },
"customer_information": { "customer_information": {
"label": "Customer's contact data", "label": "Customer details",
"desc": "Recommendation: enter the customer's name on the first line, the address on the second line, and the zip code and city/state on the third line.", "desc": "Recommendation: enter the customer's name on the first line, the address on the second line, and the zip code and city/state on the third line.",
"placeholder": "Name and surname\nStreet and number\nzip code and city or state..." "placeholder": "Name and surname\nStreet and number\nzip code and city or state..."
}, },
"payment_method": { "payment_method": {
"label": "Payment method", "label": "Payment method",
"placeholder": "", "placeholder": "placeholder",
"desc": "Method of payment for this quote" "desc": "Method of payment for this quote"
}, },
"notes": { "notes": {
@ -364,14 +389,14 @@
"desc": "Percentage discount" "desc": "Percentage discount"
}, },
"discount_price": { "discount_price": {
"label": "Discount price", "label": "Discount amount",
"placeholder": "", "placeholder": "",
"desc": "Percentage discount price" "desc": "Discount amount"
}, },
"before_tax_price": { "before_tax_price": {
"label": "Before tax price", "label": "Taxable amount",
"placeholder": "", "placeholder": "",
"desc": "Before tax price" "desc": "Taxable amount"
}, },
"tax": { "tax": {
"label": "Tax (%)", "label": "Tax (%)",
@ -386,7 +411,7 @@
"tax_price": { "tax_price": {
"label": "Tax amount", "label": "Tax amount",
"placeholder": "", "placeholder": "",
"desc": "Percentage tax price" "desc": "Tax amount"
}, },
"total_price": { "total_price": {
"label": "Total price", "label": "Total price",
@ -415,7 +440,7 @@
"desc": "" "desc": ""
}, },
"discount": { "discount": {
"label": "Dto (%)", "label": "Discount (%)",
"placeholder": "", "placeholder": "",
"desc": "Percentage discount" "desc": "Percentage discount"
}, },
@ -430,7 +455,6 @@
"settings": { "settings": {
"edit": { "edit": {
"title": "Settings", "title": "Settings",
"subtitle": "",
"tabs": { "tabs": {
"profile": "Profile settings", "profile": "Profile settings",
"quotes": "Quote settings", "quotes": "Quote settings",
@ -441,12 +465,19 @@
"language": { "language": {
"label": "Language", "label": "Language",
"placeholder": "Select a language", "placeholder": "Select a language",
"desc": "Preferred language for the interface" "desc": "Preferred language for the interface",
"options": {
"es": "Spanish",
"en": "English"
}
}, },
"image": { "logo": {
"label": "Logotype", "label": "Logo",
"placeholder": "", "desc": "This logo will appear in PDF-exported proposals",
"desc": "" "requirements": "JPG or PNG files are allowed. The logo dimensions must be 200x100 pixels or proportional.",
"upload": "Upload logo",
"invalid_format": "Only JPG or PNG files are allowed",
"invalid_dimensions": "The logo dimensions must be 200x100 pixels or proportional."
}, },
"contact_information": { "contact_information": {
"label": "Your contact information", "label": "Your contact information",
@ -459,7 +490,6 @@
"desc": "Default tax rate for your quotes" "desc": "Default tax rate for your quotes"
}, },
"default_tax_label": { "default_tax_label": {
"label": "Tax label",
"placeholder": "% VAT", "placeholder": "% VAT",
"desc": "Name or label shown alongside the tax" "desc": "Name or label shown alongside the tax"
}, },
@ -470,7 +500,7 @@
"default_legal_terms": { "default_legal_terms": {
"label": "Legal terms", "label": "Legal terms",
"placeholder": "", "placeholder": "",
"desc": "Legal information to be included at the end of your quotes" "desc": "Legal text to include in all new quotes"
}, },
"default_payment_method": { "default_payment_method": {
"label": "Payment method", "label": "Payment method",
@ -487,7 +517,39 @@
"placeholder": "", "placeholder": "",
"desc": "Default validity time to be used for new quotes" "desc": "Default validity time to be used for new quotes"
} }
},
"notifications": {
"saved": "Settings saved",
"saving": "Saving settings"
} }
},
"support": {
"modal": {
"title": "Send an issue",
"subtitle": "Use this form to report any problem you encountered while using the application. Our development team will review your issue and try to resolve it.",
"open": "Open support window",
"placeholder": "Describe the issue here...",
"submit": "Send issue",
"validation": {
"empty": "Write something before sending",
"min": "The text is too short. It must contain at least 10 characters",
"max": "The text is too long.",
"required": "A description is required"
},
"notifications": {
"sent_title": "Issue sent",
"sent_description": "The issue was sent successfully",
"error_title": "Sending error",
"error_description": "The issue could not be sent. Please try again."
},
"cancel_confirmation": {
"title": "Are you sure you want to cancel?",
"description": "You have entered text in the description field. Closing the window will discard your unsaved changes.",
"back": "Return to form",
"confirm": "Yes, close"
}
},
"form_fields": {}
} }
} }
} }

View File

@ -1,14 +1,12 @@
{ {
"translation": { "translation": {
"common": { "common": {
"loading": "Cargando...",
"required": "obligatorio", "required": "obligatorio",
"cancel": "Cancelar", "cancel": "Cancelar",
"no": "No", "no": "No",
"yes": "Sí", "yes": "Sí",
"save": "Guardar", "save": "Guardar",
"save_close": "Guardar y Cerrar", "save_close": "Guardar y Cerrar",
"accept": "Aceptar",
"discard": "Descartar", "discard": "Descartar",
"hide": "Ocultar", "hide": "Ocultar",
"back": "Volver", "back": "Volver",
@ -16,7 +14,19 @@
"continue": "Continuar", "continue": "Continuar",
"close": "Cerrar", "close": "Cerrar",
"more": "More", "more": "More",
"pagination": "Paginación",
"breadcrumb": "Migas de pan",
"previous": "Anterior",
"next": "Siguiente",
"more_pages": "Más páginas",
"previous_slide": "Diapositiva anterior",
"next_slide": "Diapositiva siguiente",
"add": "Añadir", "add": "Añadir",
"submit": "Enviar",
"download": "Descargar",
"help": "Ayuda",
"select_all": "Seleccionar todo",
"select_row": "Seleccionar fila",
"sort_asc": "Asc", "sort_asc": "Asc",
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.", "sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
"sort_desc": "Desc", "sort_desc": "Desc",
@ -52,12 +62,10 @@
"insert_row_below": "Insertar fila debajo", "insert_row_below": "Insertar fila debajo",
"pick_date": "Elige una fecha", "pick_date": "Elige una fecha",
"required_field": "Este campo es obligatorio", "required_field": "Este campo es obligatorio",
"unsaved_changes_prompt": "Los últimos cambios no se han guardado. Si continúas, se perderán.",
"edit": "Editar", "edit": "Editar",
"remove": "Eliminar", "remove": "Eliminar",
"archive": "Archivar", "archive": "Archivar",
"duplicate": "Duplicar", "duplicate": "Duplicar",
"print": "Imprimir",
"disable_preview": "Ocultar vista previa", "disable_preview": "Ocultar vista previa",
"enable_preview": "Mostrar vista previa", "enable_preview": "Mostrar vista previa",
"filter": { "filter": {
@ -66,17 +74,31 @@
"input_placeholder": "Escribe aquí para filtrar...", "input_placeholder": "Escribe aquí para filtrar...",
"button_add_term": "Añadir término al filtro", "button_add_term": "Añadir término al filtro",
"button_add_term_tooltip": "Añadir término al filtro (o pulsa Enter)", "button_add_term_tooltip": "Añadir término al filtro (o pulsa Enter)",
"button_remove_all": "Quitar todos los términos del filtro",
"help_text": "Presiona Enter o haz clic en el botón + para añadir un término al filtro. Usa múltiples términos para una búsqueda más amplia." "help_text": "Presiona Enter o haz clic en el botón + para añadir un término al filtro. Usa múltiples términos para una búsqueda más amplia."
} }
}, },
"components": { "components": {
"LoadingIndicator": { "loading_indicator": {
"title": "Cargando..." "title": "Cargando..."
}, },
"loading_overlay": { "loading_overlay": {
"title": "Cargando...", "title": "Cargando...",
"subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página." "subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página."
},
"error_overlay": {
"title": "Se ha producido un error",
"go_home": "Ir al inicio"
},
"empty_state": {
"title": "Esto está muy vacío",
"subtitle": "Empieza solicitando la carga del catálogo",
"button": "Solicitar carga del catálogo"
},
"data_table": {
"columns": "Columnas",
"no_results": "No se han encontrado resultados.",
"clear_filters": "Limpiar filtros",
"selected_count": "{{count}} seleccionados"
} }
}, },
"hooks": { "hooks": {
@ -88,12 +110,10 @@
} }
}, },
"main_menu": { "main_menu": {
"home": "Inicio", "toggle_navigation": "Alternar el menú de navegación",
"settings": "Ajustes", "settings": "Ajustes",
"dealers": "Distribuidores",
"catalog": "Catálogo", "catalog": "Catálogo",
"quotes": "Cotizaciones", "quotes": "Cotizaciones",
"orders": "Pedidos",
"search_placeholder": "Buscar productos, cotizaciones, etc...", "search_placeholder": "Buscar productos, cotizaciones, etc...",
"user": { "user": {
"user_menu": "Menú del usuario", "user_menu": "Menú del usuario",
@ -127,6 +147,9 @@
"catalog": { "catalog": {
"list": { "list": {
"title": "Catálogo de artículos", "title": "Catálogo de artículos",
"empty_state": {
"subtitle": "Empieza cargando los artículos del catálogo"
},
"columns": { "columns": {
"description": "Descripción", "description": "Descripción",
"points": "Puntos", "points": "Puntos",
@ -140,6 +163,17 @@
"title": "Cotizaciones", "title": "Cotizaciones",
"subtitle": "", "subtitle": "",
"tabs_title": "Estados", "tabs_title": "Estados",
"select_status": "Seleccionar estado",
"preview_aria": "Mostrar vista previa de la cotización",
"empty_state": {
"subtitle": "Empieza creando alguna cotización"
},
"status_help": {
"title": "Ayuda sobre el estado de las cotizaciones",
"intro": "El estado de una cotización indica su posición actual en el proceso de negocio.",
"possible": "Los estados posibles son:",
"outro": "Utiliza estos estados para hacer un seguimiento eficiente de tus cotizaciones y optimizar tu proceso de ventas. El estado Archivado ayuda a conservar el historial sin interferir con las cotizaciones activas."
},
"tabs": { "tabs": {
"all": "Todas", "all": "Todas",
"draft": "Borradores", "draft": "Borradores",
@ -156,34 +190,35 @@
"status": "Estado", "status": "Estado",
"customer_reference": "Ref. cliente", "customer_reference": "Ref. cliente",
"customer_information": "Cliente", "customer_information": "Cliente",
"total_price": "Imp. total", "total_price": "Imp. total"
"actions": {
"edit": "Editar",
"sent_to": "Enviar a Uecko"
}
}, },
"resume": { "resume": {
"title": "Cotización", "title": "Cotización",
"download_quote": "Descargar",
"tabs": { "tabs": {
"resume": "Resumen", "resume": "Resumen",
"preview": "Previsualización" "preview": "Previsualización"
}, },
"quote_information": "Información", "quote_information": "Información",
"customer_information": "Cliente", "customer_information": "Cliente",
"payment_information": "Pago", "price_information": "Importes",
"price_information": "Importes" "select_quote": "Selecciona una cotización"
} }
}, },
"notifications": {
"created": "Cotización creada",
"saved": "Cotización guardada",
"saving": "Guardando cotización"
},
"pdf_generation": {
"title": "Generando PDF",
"error": "Error al generar el PDF",
"progress": "Progreso: {{percentage}}%",
"elapsed": "Tiempo transcurrido: {{elapsed}} segundos",
"remaining": "Tiempo estimado restante: {{estimatedTime}} segundos",
"cancel": "Cancelar generación"
},
"create": { "create": {
"title": "Nueva cotización", "title": "Nueva cotización",
"tabs": {
"general": "Datos generales",
"items": "Contenido",
"preview": "Vista previa",
"documents": "Documentos",
"history": "Historial"
},
"form_groups": { "form_groups": {
"general": { "general": {
"title": "Datos generales", "title": "Datos generales",
@ -193,21 +228,11 @@
"title": "Estado", "title": "Estado",
"desc": "Estado de la cotización" "desc": "Estado de la cotización"
}, },
"items": {
"title": "Contenido de la cotización",
"desc": "Líneas de detalle de la cotización. Ayúdese del catálogo para rellenar más fácilmente el contenido."
},
"documents": { "documents": {
"title": "Documentos", "title": "Documentos",
"desc": "Añada adjuntar con su cotización documentos como fotos, planos, croquis, etc." "desc": "Añada adjuntar con su cotización documentos como fotos, planos, croquis, etc.",
}, "images": "Imágenes"
"history": {
"title": "Historial",
"desc": "Historial de la cotización"
} }
},
"edit": {
"title": "Cotización"
} }
}, },
"edit": { "edit": {
@ -225,8 +250,7 @@
"progress": { "progress": {
"label": "La descarga está {{status}}", "label": "La descarga está {{status}}",
"status_in_progress": "en marcha", "status_in_progress": "en marcha",
"status_stopped": "parada", "status_stopped": "parada"
"value_label": "Elapsed time in seconds {{elapsed}}"
}, },
"cancel_button": "Cancelar la descarga", "cancel_button": "Cancelar la descarga",
"toast_success": "Cotización descargada" "toast_success": "Cotización descargada"
@ -245,7 +269,6 @@
"trigger_button": "Enviar a Uecko", "trigger_button": "Enviar a Uecko",
"title": "Enviar la cotización a Uecko", "title": "Enviar la cotización a Uecko",
"description": "¿Desea enviar esta cotización a Uecko? Esta acción no se puede deshacer.", "description": "¿Desea enviar esta cotización a Uecko? Esta acción no se puede deshacer.",
"submit_button": "Enviar",
"toast_status_changed": "Cotización enviada a Uecko" "toast_status_changed": "Cotización enviada a Uecko"
}, },
"quote_status_editor": { "quote_status_editor": {
@ -432,7 +455,6 @@
"settings": { "settings": {
"edit": { "edit": {
"title": "Ajustes", "title": "Ajustes",
"subtitle": "",
"tabs": { "tabs": {
"profile": "Ajustes de perfil", "profile": "Ajustes de perfil",
"quotes": "Ajustes para cotizaciones", "quotes": "Ajustes para cotizaciones",
@ -443,11 +465,14 @@
"language": { "language": {
"label": "Idioma", "label": "Idioma",
"placeholder": "Selecciona un idioma", "placeholder": "Selecciona un idioma",
"desc": "Idioma preferido para la interfaz" "desc": "Idioma preferido para la interfaz",
"options": {
"es": "Español",
"en": "Inglés"
}
}, },
"logo": { "logo": {
"label": "Logotipo", "label": "Logotipo",
"placeholder": "",
"desc": "Este logotipo aparecerá en las propuestas exportadas en PDF", "desc": "Este logotipo aparecerá en las propuestas exportadas en PDF",
"requirements": "Se permiten ficheros JPG o PNG. Las dimensiones del logotipo deben ser 200x100 píxeles o proporcionales.", "requirements": "Se permiten ficheros JPG o PNG. Las dimensiones del logotipo deben ser 200x100 píxeles o proporcionales.",
"upload": "Subir logotipo", "upload": "Subir logotipo",
@ -465,7 +490,6 @@
"desc": "Porcentaje de impuesto por defecto en las cotizaciones" "desc": "Porcentaje de impuesto por defecto en las cotizaciones"
}, },
"default_tax_label": { "default_tax_label": {
"label": "Etiqueta del impuesto",
"placeholder": "% IVA", "placeholder": "% IVA",
"desc": "Nombre o etiqueta que se mostrará junto al impuesto" "desc": "Nombre o etiqueta que se mostrará junto al impuesto"
}, },
@ -493,12 +517,37 @@
"placeholder": "", "placeholder": "",
"desc": "Texto para indicar el tiempo de validez de la cotización" "desc": "Texto para indicar el tiempo de validez de la cotización"
} }
},
"notifications": {
"saved": "Ajustes guardados",
"saving": "Guardando ajustes"
} }
}, },
"support": { "support": {
"modal": { "modal": {
"title": "Enviar una incidencia", "title": "Enviar una incidencia",
"subtitle": "Utiliza este formulario para informar sobre cualquier problema que hayas encontrado mientras usabas la aplicación. Nuestro equipo de desarrollo revisará tu incidencia y tratará de resolverla." "subtitle": "Utiliza este formulario para informar sobre cualquier problema que hayas encontrado mientras usabas la aplicación. Nuestro equipo de desarrollo revisará tu incidencia y tratará de resolverla.",
"open": "Abrir ventana de soporte",
"placeholder": "Describe la incidencia aquí...",
"submit": "Enviar incidencia",
"validation": {
"empty": "Debe escribir algo antes de enviar",
"min": "El texto es demasiado corto. Debe tener al menos 10 caracteres",
"max": "El texto es demasiado largo.",
"required": "La descripción es requerida"
},
"notifications": {
"sent_title": "Incidencia enviada",
"sent_description": "La incidencia se ha enviado correctamente",
"error_title": "Error en el envío",
"error_description": "No se ha podido enviar la incidencia correctamente. Por favor, inténtalo de nuevo."
},
"cancel_confirmation": {
"title": "¿Estás seguro de que quieres cancelar?",
"description": "Has escrito texto en el campo de descripción. Si cierras la ventana, perderás los cambios no guardados.",
"back": "Volver al formulario",
"confirm": "Sí, cerrar"
}
}, },
"form_fields": {} "form_fields": {}
} }

View File

@ -1,6 +1,7 @@
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react"
import { t } from "i18next"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -9,7 +10,7 @@ const Breadcrumb = React.forwardRef<
React.ComponentPropsWithoutRef<"nav"> & { React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: React.ReactNode
} }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) >(({ ...props }, ref) => <nav ref={ref} aria-label={t("common.breadcrumb")} {...props} />)
Breadcrumb.displayName = "Breadcrumb" Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef< const BreadcrumbList = React.forwardRef<
@ -99,7 +100,7 @@ const BreadcrumbEllipsis = ({
{...props} {...props}
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span> <span className="sr-only">{t("common.more")}</span>
</span> </span>
) )
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"

View File

@ -3,6 +3,7 @@ import useEmblaCarousel, {
} from "embla-carousel-react"; } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react"; import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { t } from "i18next";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "./button"; import { Button } from "./button";
@ -215,7 +216,7 @@ const CarouselPrevious = React.forwardRef<
{...props} {...props}
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
<span className="sr-only">Previous slide</span> <span className="sr-only">{t("common.previous_slide")}</span>
</Button> </Button>
); );
}); });
@ -244,7 +245,7 @@ const CarouselNext = React.forwardRef<
{...props} {...props}
> >
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
<span className="sr-only">Next slide</span> <span className="sr-only">{t("common.next_slide")}</span>
</Button> </Button>
); );
}); });

View File

@ -1,5 +1,6 @@
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
import { t } from "i18next"
import { X } from "lucide-react" import { X } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -44,7 +45,7 @@ const DialogContent = React.forwardRef<
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">{t("common.close")}</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>

View File

@ -1,4 +1,5 @@
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { t } from "i18next";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -7,7 +8,7 @@ import { ButtonProps, buttonVariants } from "./button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label={t("common.pagination")}
className={cn("mx-auto flex w-full justify-center", className)} className={cn("mx-auto flex w-full justify-center", className)}
{...props} {...props}
/> />
@ -64,13 +65,13 @@ const PaginationPrevious = ({
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label={t("common.go_to_prev_page")}
size="default" size="default"
className={cn("gap-1 pl-2.5", className)} className={cn("gap-1 pl-2.5", className)}
{...props} {...props}
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
<span>Previous</span> <span>{t("common.previous")}</span>
</PaginationLink> </PaginationLink>
); );
PaginationPrevious.displayName = "PaginationPrevious"; PaginationPrevious.displayName = "PaginationPrevious";
@ -80,12 +81,12 @@ const PaginationNext = ({
...props ...props
}: React.ComponentProps<typeof PaginationLink>) => ( }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label={t("common.go_to_next_page")}
size="default" size="default"
className={cn("gap-1 pr-2.5", className)} className={cn("gap-1 pr-2.5", className)}
{...props} {...props}
> >
<span>Next</span> <span>{t("common.next")}</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</PaginationLink> </PaginationLink>
); );
@ -101,7 +102,7 @@ const PaginationEllipsis = ({
{...props} {...props}
> >
<MoreHorizontal className="w-4 h-4" /> <MoreHorizontal className="w-4 h-4" />
<span className="sr-only">More pages</span> <span className="sr-only">{t("common.more_pages")}</span>
</span> </span>
); );
PaginationEllipsis.displayName = "PaginationEllipsis"; PaginationEllipsis.displayName = "PaginationEllipsis";

View File

@ -1,5 +1,6 @@
import * as React from "react" import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
import { t } from "i18next"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react" import { X } from "lucide-react"
@ -65,7 +66,7 @@ const SheetContent = React.forwardRef<
{children} {children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">{t("common.close")}</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>