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) => {
const { message } = error;
toast({ description: message, title: "Error" });
toast({ description: message, title: t("common.error") });
console.log(error);
},
});

View File

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

View File

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

View File

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

View File

@ -254,7 +254,7 @@ export const CatalogPickerDataTable = ({
if (data?.total_items === 0 && !isFiltered) {
return (
<SimpleEmptyState
subtitle='Empieza cargando los artículos del catálogo'
subtitle={t("catalog.list.empty_state.subtitle")}
buttonText=''
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 { FileTextIcon, Loader2Icon, XCircleIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { t } from "i18next";
interface PDFGenerationLoadingProps {
className?: string;
@ -38,7 +39,7 @@ export const PDFGenerationLoading = ({
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<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' />}
{error?.errorMessage && <XCircleIcon className='w-4 h-4 text-destructive' />}
</CardTitle>
@ -60,14 +61,14 @@ export const PDFGenerationLoading = ({
</div>
<Progress value={percentage} className='w-full' />
<div className='text-sm text-center text-muted-foreground'>
<p>Progreso: {percentage}%</p>
<p>{t("quotes.pdf_generation.progress", { percentage })}</p>
<div className='hidden'>
<p>Tiempo transcurrido: {elapsed} segundos</p>
{estimatedTime > 0 && <p>Tiempo estimado restante: {estimatedTime} segundos</p>}
<p>{t("quotes.pdf_generation.elapsed", { elapsed })}</p>
{estimatedTime > 0 && <p>{t("quotes.pdf_generation.remaining", { estimatedTime })}</p>}
</div>
</div>
<Button variant='destructive' onClick={cancel} className='hidden'>
Cancelar generación
{t("quotes.pdf_generation.cancel")}
</Button>
</div>
)}

View File

@ -3,6 +3,7 @@ import { cn } from "@/lib/utils";
import { Card, CardContent } from "@/ui";
import { IListQuotes_Response_DTO } from "@shared/contexts";
import { useCallback, useEffect, useState } from "react";
import { t } from "i18next";
import { useQuotes } from "../hooks";
import { PDFGenerationLoading } from "./PDFGenerationLoading";
@ -60,7 +61,7 @@ const QuotePDFPreview = ({
return (
<Card className={cn("overflow-hidden", className)}>
<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>
</Card>
);

View File

@ -134,7 +134,7 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
return (
<Card className={cn("overflow-hidden", className)}>
<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>
</Card>
);
@ -204,11 +204,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}}
>
<EditIcon className='w-4 h-4' />
<span className='sr-only'>Editar</span>
<span className='sr-only'>{t("common.edit")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Editar</p>
<p>{t("common.edit")}</p>
</TooltipContent>
</Tooltip>
@ -223,11 +223,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}}
>
<CopyIcon className='w-4 h-4' />
<span className='sr-only'>Duplicar</span>
<span className='sr-only'>{t("common.duplicate")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Duplicar</p>
<p>{t("common.duplicate")}</p>
</TooltipContent>
</Tooltip>
@ -235,11 +235,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
<TooltipTrigger asChild>
<Button variant='outline' size='icon' onClick={handleDownload}>
<DownloadIcon className='w-4 h-4' />
<span className='sr-only'>Descargar</span>
<span className='sr-only'>{t("common.download")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Descargar</p>
<p>{t("common.download")}</p>
</TooltipContent>
</Tooltip>
@ -255,11 +255,11 @@ export const QuoteResume = ({ quoteId, className }: QuoteResumeProps) => {
}}
>
<ArchiveIcon className='w-4 h-4' />
<span className='sr-only'>Archivar</span>
<span className='sr-only'>{t("common.archive")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Archivar</p>
<p>{t("common.archive")}</p>
</TooltipContent>
</Tooltip>
</ButtonGroup>

View File

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

View File

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

View File

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

View File

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

View File

@ -314,13 +314,13 @@ export const QuoteEdit = () => {
mutate(data, {
onError: (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);
},
//onSettled: () => {},
onSuccess: () => {
reset(getValues());
toast({ title: "Cotización guardada", variant: "success" });
toast({ title: t("quotes.notifications.saved"), variant: "success" });
if (shouldRedirect) {
navigate("/quotes");
}
@ -354,7 +354,7 @@ export const QuoteEdit = () => {
return (
<>
{(isSubmitting || isPending) && <LoadingOverlay title='Guardando cotización' />}
{(isSubmitting || isPending) && <LoadingOverlay title={t("quotes.notifications.saving")} />}
<Form {...form}>
<form onSubmit={handleSubmit((data) => onSubmit(data, false))}>
<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 { useMemo } from "react";
import { t } from "i18next";
import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -53,7 +54,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label='Seleccionar todo'
aria-label={t("common.select_all")}
className='translate-y-[0px]'
/>
),
@ -64,7 +65,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onCheckedChange={row.getToggleSelectedHandler()}
aria-label='Seleccionar fila'
aria-label={t("common.select_row")}
className='mt-2'
/>
),

View File

@ -1,5 +1,5 @@
import { DataTableProvider } from "@/lib/hooks";
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { QuotesDataTable } from "./components";
import { HelpButton } from "@/components";
@ -19,12 +19,12 @@ import {
Toggle,
} from "@/ui";
import { useToggle } from "@wojtekmaj/react-hooks";
import { t } from "i18next";
import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export const QuotesList = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [status, setStatus] = useState("all");
const [enabledPreview, toggleEnabledPreview] = useToggle(true);
@ -72,7 +72,7 @@ export const QuotesList = () => {
<Label>{t("quotes.list.tabs_title")}</Label>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger>
<SelectValue placeholder='Seleccionar estado' />
<SelectValue placeholder={t("quotes.list.select_status")} />
</SelectTrigger>
<SelectContent>
{quoteStatuses.map((s) => (
@ -86,14 +86,14 @@ export const QuotesList = () => {
</div>
<div className='flex items-baseline justify-center mr-4 font-medium'>
<HelpButton
buttonText='Ayuda'
title='Ayuda sobre el Estado de Cotizaciones'
content={HelpContent}
buttonText={t("common.help")}
title={t("quotes.list.status_help.title")}
content={<HelpContent />}
/>
</div>
<div className='flex items-center gap-2 ml-auto'>
<Toggle
aria-label='Show quote preview'
aria-label={t("quotes.list.preview_aria")}
variant={"outline"}
defaultPressed={false}
pressed={enabledPreview}
@ -123,35 +123,32 @@ export const QuotesList = () => {
);
};
const HelpContent = (
<ScrollArea className='grid gap-4 py-4'>
<p>El estado de una cotización indica su posición actual en el proceso de negocio.</p>
<p>Los estados posibles son:</p>
const HelpContent = () => {
const { t } = useTranslation();
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'>
<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>
<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>
<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>
<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>
<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>
<strong>Arcivado:</strong> La cotización ha sido guardada para referencia futura y ya no
está activa.
<strong>{t("quotes.status.archived")}:</strong> {t("quotes.quote_status_editor.status.archived.description")}
</li>
</ul>
<p>
Utiliza estos estados para hacer un seguimiento eficiente de tus cotizaciones y optimizar tu
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>
);
<p>{t("quotes.list.status_help.outro")}</p>
</ScrollArea>;
};

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import { ButtonProps } from "@/ui";
import { t } from "i18next";
import { CustomButton } from "./CustomButton";
export interface SubmitButtonProps extends ButtonProps {
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} />
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Button, Sheet, SheetContent, SheetTrigger } from "@/ui";
import SupportModal from "@/app/support/components/SupportModal";
import { cn } from "@/lib/utils";
import { MenuIcon, Package2Icon } from "lucide-react";
import { t } from "i18next";
import { useCallback } from "react";
import { Trans } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
@ -58,7 +59,7 @@ export const LayoutHeader = () => {
<SheetTrigger asChild>
<Button variant='outline' size='icon' className='shrink-0 md:hidden'>
<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>
</SheetTrigger>
<SheetContent side='left'>

View File

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

View File

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

View File

@ -1,14 +1,12 @@
{
"translation": {
"common": {
"loading": "Loading...",
"required": "required",
"cancel": "Cancel",
"no": "No",
"yes": "Yes",
"save": "Save",
"save_close": "Save and Close",
"accept": "OK",
"discard": "Discard",
"hide": "Hide",
"back": "Back",
@ -16,7 +14,19 @@
"continue": "Continue",
"close": "Close",
"more": "More",
"pagination": "Pagination",
"breadcrumb": "Breadcrumb",
"previous": "Previous",
"next": "Next",
"more_pages": "More pages",
"previous_slide": "Previous slide",
"next_slide": "Next slide",
"add": "Add",
"submit": "Submit",
"download": "Download",
"help": "Help",
"select_all": "Select all",
"select_row": "Select row",
"sort_asc": "Asc",
"sort_asc_description": "In ascending order. Click to sort descending order.",
"sort_desc": "Desc",
@ -52,21 +62,18 @@
"insert_row_below": "Insert row below",
"pick_date": "Select a date",
"required_field": "This field is required",
"unsaved_changes_prompt": "There are unsaved changes. If you leave, you'll lose your changes.",
"edit": "Edit",
"remove": "Remove",
"archive": "Archive",
"duplicate": "Duplicate",
"print": "Print",
"disable_preview": "Disable preview",
"enable_preview": "Enable preview",
"disable_preview": "Hide preview",
"enable_preview": "Show preview",
"filter": {
"reset_filter": "Reset filter",
"badget_remove_tooltip": "Remove this filter term",
"input_placeholder": "Type here to filter...",
"button_add_term": "Add filter term",
"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."
}
},
@ -77,6 +84,21 @@
"loading_overlay": {
"title": "Loading...",
"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": {
@ -88,13 +110,11 @@
}
},
"main_menu": {
"home": "Home",
"toggle_navigation": "Toggle navigation menu",
"settings": "Settings",
"dealers": "Dealers",
"catalog": "Catalog",
"quotes": "Quotes",
"orders": "Orders",
"search_placeholder": "Type here for search quotes and articles",
"search_placeholder": "Search products, quotes, etc.",
"user": {
"user_menu": "User menu",
"my_account": "My account",
@ -104,14 +124,14 @@
"logout": "Logout"
},
"logout_dialog": {
"title": "Confirm",
"title": "Log out",
"description": "Are you sure you want to log out?",
"confirm_label": "Log out",
"cancel_label": "Cancel"
}
},
"login_page": {
"title": "Partner intranet",
"title": "Quoting tool for distributors",
"description": "Enter your email address and password to login",
"email_label": "Email",
"email_placeholder": "myemail@sample.com",
@ -127,11 +147,14 @@
"catalog": {
"list": {
"title": "Catalog of articles",
"empty_state": {
"subtitle": "Start by loading catalog items"
},
"columns": {
"description": "Description",
"points": "Points",
"retail_price": "Retail price",
"quantity": "quantity"
"quantity": "Quantity"
}
}
},
@ -139,7 +162,18 @@
"list": {
"title": "Quotes",
"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": {
"all": "All",
"draft": "Draft",
@ -156,58 +190,49 @@
"status": "Status",
"customer_reference": "Customer Ref.",
"customer_information": "Customer",
"total_price": "Imp. total",
"actions": {
"edit": "Edit quote",
"sent_to": "Send to Uecko"
}
"total_price": "Total amount"
},
"resume": {
"title": "Quote",
"download_quote": "Download quote",
"tabs": {
"resume": "Resume",
"resume": "Summary",
"preview": "Preview"
},
"quote_information": "Quote 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": {
"title": "New quote",
"tabs": {
"general": "General data",
"items": "Quote items",
"preview": "Quote preview",
"documents": "Documents",
"history": "History"
},
"form_groups": {
"general": {
"title": "General Data",
"desc": "General data and quote customer"
"desc": "General information and the quote's customer"
},
"status": {
"title": "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": {
"title": "Attached Documents",
"desc": "Attach documents such as photos, drawings, sketches, etc. to your quotation."
},
"history": {
"title": "History",
"desc": "Quote history"
"desc": "Attach documents such as photos, drawings, sketches, etc. to your quotation.",
"images": "Images"
}
},
"edit": {
"title": "Quote"
}
},
"edit": {
@ -225,8 +250,7 @@
"progress": {
"label": "Download is in {{status}}",
"status_in_progress": "in progress",
"status_stopped": "stopped",
"value_label": "Elapsed time in seconds {{elapsed}}"
"status_stopped": "stopped"
},
"cancel_button": "Cancel the download",
"toast_success": "Quote downloaded"
@ -238,9 +262,15 @@
},
"blocks_picker_dialog": {
"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:"
},
"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": {
"trigger_button": "Change quote status",
"title": "Change quote status",
@ -267,7 +297,7 @@
},
"archived": {
"title": "Archived",
"description": "The quote is archived."
"description": "The quote has been saved for future reference."
}
},
"submit_button": "Change status",
@ -308,11 +338,6 @@
"desc": "Quote reference",
"placeholder": ""
},
"status": {
"label": "Status",
"desc": "Quote status",
"placeholder": ""
},
"lang_code": {
"label": "Language",
"desc": "Quote language",
@ -329,13 +354,13 @@
"placeholder": ""
},
"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.",
"placeholder": "Name and surname\nStreet and number\nzip code and city or state..."
},
"payment_method": {
"label": "Payment method",
"placeholder": "",
"placeholder": "placeholder",
"desc": "Method of payment for this quote"
},
"notes": {
@ -364,14 +389,14 @@
"desc": "Percentage discount"
},
"discount_price": {
"label": "Discount price",
"label": "Discount amount",
"placeholder": "",
"desc": "Percentage discount price"
"desc": "Discount amount"
},
"before_tax_price": {
"label": "Before tax price",
"label": "Taxable amount",
"placeholder": "",
"desc": "Before tax price"
"desc": "Taxable amount"
},
"tax": {
"label": "Tax (%)",
@ -386,7 +411,7 @@
"tax_price": {
"label": "Tax amount",
"placeholder": "",
"desc": "Percentage tax price"
"desc": "Tax amount"
},
"total_price": {
"label": "Total price",
@ -415,7 +440,7 @@
"desc": ""
},
"discount": {
"label": "Dto (%)",
"label": "Discount (%)",
"placeholder": "",
"desc": "Percentage discount"
},
@ -430,7 +455,6 @@
"settings": {
"edit": {
"title": "Settings",
"subtitle": "",
"tabs": {
"profile": "Profile settings",
"quotes": "Quote settings",
@ -441,12 +465,19 @@
"language": {
"label": "Language",
"placeholder": "Select a language",
"desc": "Preferred language for the interface"
"desc": "Preferred language for the interface",
"options": {
"es": "Spanish",
"en": "English"
}
},
"image": {
"label": "Logotype",
"placeholder": "",
"desc": ""
"logo": {
"label": "Logo",
"desc": "This logo will appear in PDF-exported proposals",
"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": {
"label": "Your contact information",
@ -459,7 +490,6 @@
"desc": "Default tax rate for your quotes"
},
"default_tax_label": {
"label": "Tax label",
"placeholder": "% VAT",
"desc": "Name or label shown alongside the tax"
},
@ -470,7 +500,7 @@
"default_legal_terms": {
"label": "Legal terms",
"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": {
"label": "Payment method",
@ -487,7 +517,39 @@
"placeholder": "",
"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": {
"common": {
"loading": "Cargando...",
"required": "obligatorio",
"cancel": "Cancelar",
"no": "No",
"yes": "Sí",
"save": "Guardar",
"save_close": "Guardar y Cerrar",
"accept": "Aceptar",
"discard": "Descartar",
"hide": "Ocultar",
"back": "Volver",
@ -16,7 +14,19 @@
"continue": "Continuar",
"close": "Cerrar",
"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",
"submit": "Enviar",
"download": "Descargar",
"help": "Ayuda",
"select_all": "Seleccionar todo",
"select_row": "Seleccionar fila",
"sort_asc": "Asc",
"sort_asc_description": "En order ascendente. Click para ordenar descendentemente.",
"sort_desc": "Desc",
@ -52,12 +62,10 @@
"insert_row_below": "Insertar fila debajo",
"pick_date": "Elige una fecha",
"required_field": "Este campo es obligatorio",
"unsaved_changes_prompt": "Los últimos cambios no se han guardado. Si continúas, se perderán.",
"edit": "Editar",
"remove": "Eliminar",
"archive": "Archivar",
"duplicate": "Duplicar",
"print": "Imprimir",
"disable_preview": "Ocultar vista previa",
"enable_preview": "Mostrar vista previa",
"filter": {
@ -66,17 +74,31 @@
"input_placeholder": "Escribe aquí para filtrar...",
"button_add_term": "Añadir término al filtro",
"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."
}
},
"components": {
"LoadingIndicator": {
"loading_indicator": {
"title": "Cargando..."
},
"loading_overlay": {
"title": "Cargando...",
"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": {
@ -88,12 +110,10 @@
}
},
"main_menu": {
"home": "Inicio",
"toggle_navigation": "Alternar el menú de navegación",
"settings": "Ajustes",
"dealers": "Distribuidores",
"catalog": "Catálogo",
"quotes": "Cotizaciones",
"orders": "Pedidos",
"search_placeholder": "Buscar productos, cotizaciones, etc...",
"user": {
"user_menu": "Menú del usuario",
@ -127,6 +147,9 @@
"catalog": {
"list": {
"title": "Catálogo de artículos",
"empty_state": {
"subtitle": "Empieza cargando los artículos del catálogo"
},
"columns": {
"description": "Descripción",
"points": "Puntos",
@ -140,6 +163,17 @@
"title": "Cotizaciones",
"subtitle": "",
"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": {
"all": "Todas",
"draft": "Borradores",
@ -156,34 +190,35 @@
"status": "Estado",
"customer_reference": "Ref. cliente",
"customer_information": "Cliente",
"total_price": "Imp. total",
"actions": {
"edit": "Editar",
"sent_to": "Enviar a Uecko"
}
"total_price": "Imp. total"
},
"resume": {
"title": "Cotización",
"download_quote": "Descargar",
"tabs": {
"resume": "Resumen",
"preview": "Previsualización"
},
"quote_information": "Información",
"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": {
"title": "Nueva cotización",
"tabs": {
"general": "Datos generales",
"items": "Contenido",
"preview": "Vista previa",
"documents": "Documentos",
"history": "Historial"
},
"form_groups": {
"general": {
"title": "Datos generales",
@ -193,21 +228,11 @@
"title": "Estado",
"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": {
"title": "Documentos",
"desc": "Añada adjuntar con su cotización documentos como fotos, planos, croquis, etc."
},
"history": {
"title": "Historial",
"desc": "Historial de la cotización"
"desc": "Añada adjuntar con su cotización documentos como fotos, planos, croquis, etc.",
"images": "Imágenes"
}
},
"edit": {
"title": "Cotización"
}
},
"edit": {
@ -225,8 +250,7 @@
"progress": {
"label": "La descarga está {{status}}",
"status_in_progress": "en marcha",
"status_stopped": "parada",
"value_label": "Elapsed time in seconds {{elapsed}}"
"status_stopped": "parada"
},
"cancel_button": "Cancelar la descarga",
"toast_success": "Cotización descargada"
@ -245,7 +269,6 @@
"trigger_button": "Enviar a Uecko",
"title": "Enviar la cotización a Uecko",
"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"
},
"quote_status_editor": {
@ -432,7 +455,6 @@
"settings": {
"edit": {
"title": "Ajustes",
"subtitle": "",
"tabs": {
"profile": "Ajustes de perfil",
"quotes": "Ajustes para cotizaciones",
@ -443,11 +465,14 @@
"language": {
"label": "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": {
"label": "Logotipo",
"placeholder": "",
"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.",
"upload": "Subir logotipo",
@ -465,7 +490,6 @@
"desc": "Porcentaje de impuesto por defecto en las cotizaciones"
},
"default_tax_label": {
"label": "Etiqueta del impuesto",
"placeholder": "% IVA",
"desc": "Nombre o etiqueta que se mostrará junto al impuesto"
},
@ -493,12 +517,37 @@
"placeholder": "",
"desc": "Texto para indicar el tiempo de validez de la cotización"
}
},
"notifications": {
"saved": "Ajustes guardados",
"saving": "Guardando ajustes"
}
},
"support": {
"modal": {
"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": {}
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { t } from "i18next"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
@ -44,7 +45,7 @@ const DialogContent = React.forwardRef<
{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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
<span className="sr-only">{t("common.close")}</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>

View File

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

View File

@ -1,5 +1,6 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { t } from "i18next"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
@ -65,7 +66,7 @@ const SheetContent = React.forwardRef<
{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">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
<span className="sr-only">{t("common.close")}</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>