diff --git a/frontend/app/admin/availability/page.tsx b/frontend/app/admin/availability/page.tsx index 4a1f59c..aab1123 100644 --- a/frontend/app/admin/availability/page.tsx +++ b/frontend/app/admin/availability/page.tsx @@ -5,6 +5,7 @@ import { Permission } from "../../auth-context"; import { adminApi } from "../../api"; import { Header } from "../../components/Header"; import { useRequireAuth } from "../../hooks/useRequireAuth"; +import { useTranslation } from "../../hooks/useTranslation"; import { components } from "../../generated/api"; import constants from "../../../../shared/constants.json"; import { @@ -49,6 +50,8 @@ interface EditSlot { } export default function AdminAvailabilityPage() { + const t = useTranslation("admin"); + const tCommon = useTranslation("common"); const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.MANAGE_AVAILABILITY, fallbackRedirect: "/", @@ -145,7 +148,7 @@ export default function AdminAvailabilityPage() { await fetchAvailability(); closeModal(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to save"); + setError(err instanceof Error ? err.message : t("availability.errors.saveFailed")); } finally { setIsSaving(false); } @@ -166,7 +169,7 @@ export default function AdminAvailabilityPage() { await fetchAvailability(); closeModal(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to clear"); + setError(err instanceof Error ? err.message : t("availability.errors.clearFailed")); } finally { setIsSaving(false); } @@ -208,7 +211,7 @@ export default function AdminAvailabilityPage() { await fetchAvailability(); cancelCopyMode(); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to copy"); + setError(err instanceof Error ? err.message : t("availability.errors.copyFailed")); } finally { setIsCopying(false); } @@ -221,7 +224,7 @@ export default function AdminAvailabilityPage() { if (isLoading) { return (
-
Loading...
+
{tCommon("loading")}
); } @@ -238,23 +241,25 @@ export default function AdminAvailabilityPage() {
-

Availability

+

{t("availability.title")}

- Configure your available time slots for the next {maxAdvanceDays} days + {t("availability.subtitle", { days: maxAdvanceDays })}

{copySource && (
- Select days to copy to, then click Copy + {t("availability.copyMode.hint")}
)} @@ -299,7 +304,7 @@ export default function AdminAvailabilityPage() { startCopyMode(dateStr); }} style={styles.copyFromButton} - title="Copy to other days" + title={t("availability.copyMode.copyTo", { count: 0 })} > 📋 @@ -307,7 +312,7 @@ export default function AdminAvailabilityPage() {
{slots.length === 0 ? ( - No availability + {t("availability.modal.noAvailability")} ) : ( slots.map((slot, i) => ( @@ -328,7 +333,7 @@ export default function AdminAvailabilityPage() {
e.stopPropagation()}>

- Edit Availability - {formatDisplayDate(selectedDate)} + {t("availability.modal.title")} - {formatDisplayDate(selectedDate)}

{error &&
{error}
} @@ -362,31 +367,31 @@ export default function AdminAvailabilityPage() {
))}
diff --git a/frontend/app/admin/invites/page.tsx b/frontend/app/admin/invites/page.tsx index 943fcdc..166f4f9 100644 --- a/frontend/app/admin/invites/page.tsx +++ b/frontend/app/admin/invites/page.tsx @@ -7,6 +7,7 @@ import { Header } from "../../components/Header"; import { StatusBadge } from "../../components/StatusBadge"; import { useRequireAuth } from "../../hooks/useRequireAuth"; import { useMutation } from "../../hooks/useMutation"; +import { useTranslation } from "../../hooks/useTranslation"; import { components } from "../../generated/api"; import constants from "../../../../shared/constants.json"; import { @@ -27,6 +28,8 @@ type PaginatedInvites = components["schemas"]["PaginatedResponse_InviteResponse_ type UserOption = components["schemas"]["AdminUserResponse"]; export default function AdminInvitesPage() { + const t = useTranslation("admin"); + const tCommon = useTranslation("common"); const [data, setData] = useState(null); const [error, setError] = useState(null); const [page, setPage] = useState(1); @@ -47,16 +50,19 @@ export default function AdminInvitesPage() { } }, []); - const fetchInvites = useCallback(async (page: number, status: string) => { - setError(null); - try { - const data = await adminApi.getInvites(page, 10, status || undefined); - setData(data); - } catch (err) { - setData(null); - setError(err instanceof Error ? err.message : "Failed to load invites"); - } - }, []); + const fetchInvites = useCallback( + async (page: number, status: string) => { + setError(null); + try { + const data = await adminApi.getInvites(page, 10, status || undefined); + setData(data); + } catch (err) { + setData(null); + setError(err instanceof Error ? err.message : t("invites.errors.loadFailed")); + } + }, + [t] + ); useEffect(() => { if (user && isAuthorized) { @@ -91,7 +97,7 @@ export default function AdminInvitesPage() { fetchInvites(page, statusFilter); }, onError: (err) => { - setError(err instanceof Error ? err.message : "Failed to revoke invite"); + setError(err instanceof Error ? err.message : t("invites.errors.revokeFailed")); }, } ); @@ -135,7 +141,7 @@ export default function AdminInvitesPage() { if (isLoading) { return (
-
Loading...
+
{tCommon("loading")}
); } @@ -152,16 +158,16 @@ export default function AdminInvitesPage() {
{/* Create Invite Section */}
-

Create Invite

+

{t("invites.createInvite")}

- + {users.length === 0 && ( - - No users loaded yet. Create at least one invite to populate the list. - + {t("invites.noUsersHint")} )}
{createError &&
{createError}
} @@ -184,7 +188,7 @@ export default function AdminInvitesPage() { ...(!newGodfatherId ? buttonStyles.buttonDisabled : {}), }} > - {isCreating ? "Creating..." : "Create Invite"} + {isCreating ? t("invites.creating") : t("invites.createInvite")}
@@ -192,7 +196,7 @@ export default function AdminInvitesPage() { {/* Invites Table */}
-

All Invites

+

{t("invites.allInvites")}

- {data?.total ?? 0} invites + + {t("invites.invitesCount", { count: data?.total ?? 0 })} +
@@ -215,12 +221,12 @@ export default function AdminInvitesPage() { - - - - - - + + + + + + @@ -249,7 +255,7 @@ export default function AdminInvitesPage() { onClick={() => handleRevoke(record.id)} style={buttonStyles.dangerButton} > - Revoke + {t("invites.revoke")} )} @@ -258,7 +264,7 @@ export default function AdminInvitesPage() { {!error && (!data || data.records.length === 0) && ( )} diff --git a/frontend/app/admin/price-history/page.tsx b/frontend/app/admin/price-history/page.tsx index 2ecf1e8..5521591 100644 --- a/frontend/app/admin/price-history/page.tsx +++ b/frontend/app/admin/price-history/page.tsx @@ -6,9 +6,11 @@ import { adminApi } from "../../api"; import { sharedStyles } from "../../styles/shared"; import { PageLayout } from "../../components/PageLayout"; import { useRequireAuth } from "../../hooks/useRequireAuth"; +import { useTranslation } from "../../hooks/useTranslation"; import { useAsyncData } from "../../hooks/useAsyncData"; export default function AdminPriceHistoryPage() { + const t = useTranslation("admin"); const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.VIEW_AUDIT, fallbackRedirect: "/", @@ -61,14 +63,16 @@ export default function AdminPriceHistoryPage() { >
-

Bitcoin Price History

+

{t("priceHistory.title")}

- {records?.length ?? 0} records + + {t("priceHistory.recordsCount", { count: records?.length ?? 0 })} +
@@ -77,10 +81,10 @@ export default function AdminPriceHistoryPage() {
CodeGodfatherStatusUsed ByCreatedActions{t("invites.tableHeaders.code")}{t("invites.tableHeaders.godfather")}{t("invites.tableHeaders.status")}{t("invites.tableHeaders.usedBy")}{t("invites.tableHeaders.created")}{t("invites.tableHeaders.actions")}
- No invites yet + {t("invites.noInvites")}
- - - - + + + + @@ -94,14 +98,14 @@ export default function AdminPriceHistoryPage() { {!error && isLoadingData && ( )} {!error && !isLoadingData && (records?.length ?? 0) === 0 && ( )} diff --git a/frontend/app/admin/trades/page.tsx b/frontend/app/admin/trades/page.tsx index ff59eeb..f6cf22f 100644 --- a/frontend/app/admin/trades/page.tsx +++ b/frontend/app/admin/trades/page.tsx @@ -9,6 +9,7 @@ import { StatusBadge } from "../../components/StatusBadge"; import { EmptyState } from "../../components/EmptyState"; import { ConfirmationButton } from "../../components/ConfirmationButton"; import { useRequireAuth } from "../../hooks/useRequireAuth"; +import { useTranslation } from "../../hooks/useTranslation"; import { components } from "../../generated/api"; import { formatDateTime } from "../../utils/date"; import { formatEur } from "../../utils/exchange"; @@ -19,6 +20,8 @@ type AdminExchangeResponse = components["schemas"]["AdminExchangeResponse"]; type Tab = "upcoming" | "past"; export default function AdminTradesPage() { + const t = useTranslation("admin"); + const tCommon = useTranslation("common"); const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.VIEW_ALL_EXCHANGES, fallbackRedirect: "/", @@ -48,9 +51,9 @@ export default function AdminTradesPage() { return null; } catch (err) { console.error("Failed to fetch upcoming trades:", err); - return "Failed to load upcoming trades"; + return t("trades.errors.loadUpcomingFailed"); } - }, []); + }, [t]); const fetchPastTrades = useCallback(async (): Promise => { try { @@ -69,9 +72,9 @@ export default function AdminTradesPage() { return null; } catch (err) { console.error("Failed to fetch past trades:", err); - return "Failed to load past trades"; + return t("trades.errors.loadPastFailed"); } - }, [statusFilter, userSearch]); + }, [statusFilter, userSearch, t]); useEffect(() => { if (user && isAuthorized) { @@ -112,7 +115,7 @@ export default function AdminTradesPage() { } setConfirmAction(null); } catch (err) { - setError(err instanceof Error ? err.message : `Failed to ${action} trade`); + setError(err instanceof Error ? err.message : t("trades.errors.actionFailed", { action })); } finally { // Remove this trade from the set of actioning trades setActioningIds((prev) => { @@ -126,7 +129,7 @@ export default function AdminTradesPage() { if (isLoading) { return (
-
Loading...
+
{tCommon("loading")}
); } @@ -141,8 +144,8 @@ export default function AdminTradesPage() {
-

Trades

-

Manage Bitcoin exchange trades

+

{t("trades.title")}

+

{t("trades.subtitle")}

{error &&
{error}
} @@ -155,7 +158,7 @@ export default function AdminTradesPage() { ...(activeTab === "upcoming" ? styles.tabButtonActive : {}), }} > - Upcoming ({upcomingTrades.length}) + {t("trades.tabs.upcoming", { count: upcomingTrades.length })}
@@ -176,15 +179,15 @@ export default function AdminTradesPage() { onChange={(e) => setStatusFilter(e.target.value)} style={styles.filterSelect} > - - - - - + + + + + setUserSearch(e.target.value)} style={styles.searchInput} @@ -193,10 +196,14 @@ export default function AdminTradesPage() { )} {isLoadingTrades ? ( - + ) : trades.length === 0 ? ( ) : (
@@ -221,7 +228,7 @@ export default function AdminTradesPage() { )} {trade.user_contact.signal && ( - Signal: {trade.user_contact.signal} + {t("trades.tradeDetails.signal", { value: trade.user_contact.signal })} )}
@@ -238,7 +245,9 @@ export default function AdminTradesPage() { color: isBuy ? "#f87171" : "#4ade80", }} > - {isBuy ? "SELL BTC" : "BUY BTC"} + {isBuy + ? t("trades.tradeDetails.sellBtc") + : t("trades.tradeDetails.buyBtc")} {isBuy - ? `Send via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}` - : `Receive via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}`} + ? t("trades.tradeDetails.sendVia", { + method: + trade.bitcoin_transfer_method === "onchain" + ? t("trades.tradeDetails.onchain") + : t("trades.tradeDetails.lightning"), + }) + : t("trades.tradeDetails.receiveVia", { + method: + trade.bitcoin_transfer_method === "onchain" + ? t("trades.tradeDetails.onchain") + : t("trades.tradeDetails.lightning"), + })} {formatEur(trade.eur_amount)} ↔ @@ -259,7 +278,9 @@ export default function AdminTradesPage() {
- Rate: + + {t("trades.tradeDetails.rate")} + € {trade.agreed_price_eur.toLocaleString("es-ES", { @@ -267,7 +288,9 @@ export default function AdminTradesPage() { })} /BTC - Market: + + {t("trades.tradeDetails.market")} + € {trade.market_price_eur.toLocaleString("es-ES", { @@ -298,7 +321,7 @@ export default function AdminTradesPage() { type: "complete", }) } - actionLabel="Complete" + actionLabel={t("trades.actions.complete")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="success" confirmButtonStyle={styles.successButton} @@ -317,7 +340,7 @@ export default function AdminTradesPage() { type: "no_show", }) } - actionLabel="No Show" + actionLabel={t("trades.actions.noShow")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="primary" actionButtonStyle={styles.warningButton} @@ -338,7 +361,7 @@ export default function AdminTradesPage() { type: "cancel", }) } - actionLabel="Cancel" + actionLabel={t("trades.actions.cancel")} isLoading={actioningIds.has(trade.public_id)} confirmVariant="danger" confirmButtonStyle={styles.dangerButton} diff --git a/frontend/app/components/IntlProvider.tsx b/frontend/app/components/IntlProvider.tsx index 72273f6..9b2a526 100644 --- a/frontend/app/components/IntlProvider.tsx +++ b/frontend/app/components/IntlProvider.tsx @@ -26,6 +26,9 @@ import caInvites from "../../locales/ca/invites.json"; import esProfile from "../../locales/es/profile.json"; import enProfile from "../../locales/en/profile.json"; import caProfile from "../../locales/ca/profile.json"; +import esAdmin from "../../locales/es/admin.json"; +import enAdmin from "../../locales/en/admin.json"; +import caAdmin from "../../locales/ca/admin.json"; const messages = { es: { @@ -36,6 +39,7 @@ const messages = { trades: esTrades, invites: esInvites, profile: esProfile, + admin: esAdmin, }, en: { common: enCommon, @@ -45,6 +49,7 @@ const messages = { trades: enTrades, invites: enInvites, profile: enProfile, + admin: enAdmin, }, ca: { common: caCommon, @@ -54,6 +59,7 @@ const messages = { trades: caTrades, invites: caInvites, profile: caProfile, + admin: caAdmin, }, }; diff --git a/frontend/locales/ca/admin.json b/frontend/locales/ca/admin.json new file mode 100644 index 0000000..d41f5b3 --- /dev/null +++ b/frontend/locales/ca/admin.json @@ -0,0 +1,113 @@ +{ + "invites": { + "title": "Invitacions", + "createInvite": "Crear Invitació", + "godfatherLabel": "Padrí (usuari que pot compartir aquesta invitació)", + "selectUser": "Seleccionar un usuari...", + "noUsersHint": "Encara no hi ha usuaris carregats. Crea almenys una invitació per omplir la llista.", + "creating": "Creant...", + "allInvites": "Totes les Invitacions", + "allStatuses": "Tots els estats", + "statusReady": "Llista", + "statusSpent": "Usada", + "statusRevoked": "Revocada", + "invitesCount": "{count} invitacions", + "tableHeaders": { + "code": "Codi", + "godfather": "Padrí", + "status": "Estat", + "usedBy": "Usada Per", + "created": "Creada", + "actions": "Accions" + }, + "noInvites": "Encara no hi ha invitacions", + "revoke": "Revocar", + "errors": { + "loadFailed": "Error en carregar invitacions", + "revokeFailed": "Error en revocar invitació" + } + }, + "trades": { + "title": "Intercanvis", + "subtitle": "Gestionar intercanvis de Bitcoin", + "tabs": { + "upcoming": "Pròxims ({count})", + "history": "Historial ({count})" + }, + "filters": { + "allStatuses": "Tots els Estats", + "completed": "Completat", + "noShow": "No Presentat", + "userCancelled": "Cancel·lat per Usuari", + "adminCancelled": "Cancel·lat per Admin" + }, + "searchPlaceholder": "Buscar per correu electrònic...", + "loading": "Carregant intercanvis...", + "emptyStates": { + "upcoming": "No hi ha intercanvis propers.", + "past": "No s'han trobat intercanvis." + }, + "tradeDetails": { + "rate": "Taxa:", + "market": "Mercat:", + "sellBtc": "VENDE BTC", + "buyBtc": "COMPRA BTC", + "sendVia": "Enviar via {method}", + "receiveVia": "Rebre via {method}", + "onchain": "Onchain", + "lightning": "Lightning", + "signal": "Signal: {value}" + }, + "actions": { + "complete": "Completar", + "noShow": "No Presentat", + "cancel": "Cancel·lar" + }, + "errors": { + "loadUpcomingFailed": "Error en carregar intercanvis propers", + "loadPastFailed": "Error en carregar historial d'intercanvis", + "actionFailed": "Error en {action} intercanvi" + } + }, + "priceHistory": { + "title": "Historial de Preus de Bitcoin", + "recordsCount": "{count} registres", + "refresh": "Actualitzar", + "fetching": "Obtenint...", + "fetchNow": "Obtindre Ara", + "tableHeaders": { + "source": "Font", + "pair": "Parella", + "price": "Preu", + "timestamp": "Data i Hora" + }, + "loading": "Carregant...", + "emptyState": "Encara no hi ha registres de preus. Fes clic a \"Obtindre Ara\" per obtenir el preu actual." + }, + "availability": { + "title": "Disponibilitat", + "subtitle": "Configurar les teves franges horàries disponibles per als propers {days} dies", + "copyMode": { + "hint": "Selecciona els dies als quals copiar, després fes clic a Copiar", + "copyTo": "Copiar a {count} dia(s)", + "copying": "Copiant...", + "cancel": "Cancel·lar" + }, + "modal": { + "title": "Editar Franges Horàries", + "startTime": "Hora d'inici", + "endTime": "Hora de fi", + "addSlot": "Afegir franja", + "removeSlot": "Eliminar", + "save": "Guardar", + "clear": "Netejar", + "close": "Tancar", + "noAvailability": "Sense disponibilitat" + }, + "errors": { + "saveFailed": "Error en guardar", + "clearFailed": "Error en netejar", + "copyFailed": "Error en copiar" + } + } +} diff --git a/frontend/locales/ca/common.json b/frontend/locales/ca/common.json index 416f6a1..6a6c4a1 100644 --- a/frontend/locales/ca/common.json +++ b/frontend/locales/ca/common.json @@ -2,6 +2,7 @@ "loading": "Carregant...", "error": "Error", "save": "Desar", + "saving": "Desant...", "cancel": "Cancel·lar", "back": "Enrere", "continue": "Continuar", diff --git a/frontend/locales/en/admin.json b/frontend/locales/en/admin.json new file mode 100644 index 0000000..d2ce2f4 --- /dev/null +++ b/frontend/locales/en/admin.json @@ -0,0 +1,113 @@ +{ + "invites": { + "title": "Invites", + "createInvite": "Create Invite", + "godfatherLabel": "Godfather (user who can share this invite)", + "selectUser": "Select a user...", + "noUsersHint": "No users loaded yet. Create at least one invite to populate the list.", + "creating": "Creating...", + "allInvites": "All Invites", + "allStatuses": "All statuses", + "statusReady": "Ready", + "statusSpent": "Spent", + "statusRevoked": "Revoked", + "invitesCount": "{count} invites", + "tableHeaders": { + "code": "Code", + "godfather": "Godfather", + "status": "Status", + "usedBy": "Used By", + "created": "Created", + "actions": "Actions" + }, + "noInvites": "No invites yet", + "revoke": "Revoke", + "errors": { + "loadFailed": "Failed to load invites", + "revokeFailed": "Failed to revoke invite" + } + }, + "trades": { + "title": "Trades", + "subtitle": "Manage Bitcoin exchange trades", + "tabs": { + "upcoming": "Upcoming ({count})", + "history": "History ({count})" + }, + "filters": { + "allStatuses": "All Statuses", + "completed": "Completed", + "noShow": "No Show", + "userCancelled": "User Cancelled", + "adminCancelled": "Admin Cancelled" + }, + "searchPlaceholder": "Search by email...", + "loading": "Loading trades...", + "emptyStates": { + "upcoming": "No upcoming trades.", + "past": "No trades found." + }, + "tradeDetails": { + "rate": "Rate:", + "market": "Market:", + "sellBtc": "SELL BTC", + "buyBtc": "BUY BTC", + "sendVia": "Send via {method}", + "receiveVia": "Receive via {method}", + "onchain": "Onchain", + "lightning": "Lightning", + "signal": "Signal: {value}" + }, + "actions": { + "complete": "Complete", + "noShow": "No Show", + "cancel": "Cancel" + }, + "errors": { + "loadUpcomingFailed": "Failed to load upcoming trades", + "loadPastFailed": "Failed to load past trades", + "actionFailed": "Failed to {action} trade" + } + }, + "priceHistory": { + "title": "Bitcoin Price History", + "recordsCount": "{count} records", + "refresh": "Refresh", + "fetching": "Fetching...", + "fetchNow": "Fetch Now", + "tableHeaders": { + "source": "Source", + "pair": "Pair", + "price": "Price", + "timestamp": "Timestamp" + }, + "loading": "Loading...", + "emptyState": "No price records yet. Click \"Fetch Now\" to get the current price." + }, + "availability": { + "title": "Availability", + "subtitle": "Configure your available time slots for the next {days} days", + "copyMode": { + "hint": "Select days to copy to, then click Copy", + "copyTo": "Copy to {count} day(s)", + "copying": "Copying...", + "cancel": "Cancel" + }, + "modal": { + "title": "Edit Time Slots", + "startTime": "Start Time", + "endTime": "End Time", + "addSlot": "Add Slot", + "removeSlot": "Remove", + "save": "Save", + "clear": "Clear", + "close": "Close", + "noAvailability": "No availability" + }, + "errors": { + "saveFailed": "Failed to save", + "clearFailed": "Failed to clear", + "copyFailed": "Failed to copy" + } + } +} diff --git a/frontend/locales/en/common.json b/frontend/locales/en/common.json index e2aa236..1a03950 100644 --- a/frontend/locales/en/common.json +++ b/frontend/locales/en/common.json @@ -2,6 +2,7 @@ "loading": "Loading...", "error": "Error", "save": "Save", + "saving": "Saving...", "cancel": "Cancel", "back": "Back", "continue": "Continue", diff --git a/frontend/locales/es/admin.json b/frontend/locales/es/admin.json new file mode 100644 index 0000000..594d0a1 --- /dev/null +++ b/frontend/locales/es/admin.json @@ -0,0 +1,113 @@ +{ + "invites": { + "title": "Invitaciones", + "createInvite": "Crear Invitación", + "godfatherLabel": "Padrino (usuario que puede compartir esta invitación)", + "selectUser": "Seleccionar un usuario...", + "noUsersHint": "Aún no hay usuarios cargados. Crea al menos una invitación para poblar la lista.", + "creating": "Creando...", + "allInvites": "Todas las Invitaciones", + "allStatuses": "Todos los estados", + "statusReady": "Lista", + "statusSpent": "Usada", + "statusRevoked": "Revocada", + "invitesCount": "{count} invitaciones", + "tableHeaders": { + "code": "Código", + "godfather": "Padrino", + "status": "Estado", + "usedBy": "Usada Por", + "created": "Creada", + "actions": "Acciones" + }, + "noInvites": "Aún no hay invitaciones", + "revoke": "Revocar", + "errors": { + "loadFailed": "Error al cargar invitaciones", + "revokeFailed": "Error al revocar invitación" + } + }, + "trades": { + "title": "Intercambios", + "subtitle": "Gestionar intercambios de Bitcoin", + "tabs": { + "upcoming": "Próximos ({count})", + "history": "Historial ({count})" + }, + "filters": { + "allStatuses": "Todos los Estados", + "completed": "Completado", + "noShow": "No Presentado", + "userCancelled": "Cancelado por Usuario", + "adminCancelled": "Cancelado por Admin" + }, + "searchPlaceholder": "Buscar por email...", + "loading": "Cargando intercambios...", + "emptyStates": { + "upcoming": "No hay intercambios próximos.", + "past": "No se encontraron intercambios." + }, + "tradeDetails": { + "rate": "Tasa:", + "market": "Mercado:", + "sellBtc": "VENDER BTC", + "buyBtc": "COMPRAR BTC", + "sendVia": "Enviar vía {method}", + "receiveVia": "Recibir vía {method}", + "onchain": "Onchain", + "lightning": "Lightning", + "signal": "Signal: {value}" + }, + "actions": { + "complete": "Completar", + "noShow": "No Presentado", + "cancel": "Cancelar" + }, + "errors": { + "loadUpcomingFailed": "Error al cargar intercambios próximos", + "loadPastFailed": "Error al cargar historial de intercambios", + "actionFailed": "Error al {action} intercambio" + } + }, + "priceHistory": { + "title": "Historial de Precios de Bitcoin", + "recordsCount": "{count} registros", + "refresh": "Actualizar", + "fetching": "Obteniendo...", + "fetchNow": "Obtener Ahora", + "tableHeaders": { + "source": "Fuente", + "pair": "Par", + "price": "Precio", + "timestamp": "Fecha y Hora" + }, + "loading": "Cargando...", + "emptyState": "Aún no hay registros de precios. Haz clic en \"Obtener Ahora\" para obtener el precio actual." + }, + "availability": { + "title": "Disponibilidad", + "subtitle": "Configurar tus franjas horarias disponibles para los próximos {days} días", + "copyMode": { + "hint": "Selecciona los días a los que copiar, luego haz clic en Copiar", + "copyTo": "Copiar a {count} día(s)", + "copying": "Copiando...", + "cancel": "Cancelar" + }, + "modal": { + "title": "Editar Franjas Horarias", + "startTime": "Hora de inicio", + "endTime": "Hora de fin", + "addSlot": "Añadir franja", + "removeSlot": "Eliminar", + "save": "Guardar", + "clear": "Limpiar", + "close": "Cerrar", + "noAvailability": "Sin disponibilidad" + }, + "errors": { + "saveFailed": "Error al guardar", + "clearFailed": "Error al limpiar", + "copyFailed": "Error al copiar" + } + } +} diff --git a/frontend/locales/es/common.json b/frontend/locales/es/common.json index 206784c..cc00e9f 100644 --- a/frontend/locales/es/common.json +++ b/frontend/locales/es/common.json @@ -2,6 +2,7 @@ "loading": "Cargando...", "error": "Error", "save": "Guardar", + "saving": "Guardando...", "cancel": "Cancelar", "back": "Atrás", "continue": "Continuar",
SourcePairPriceTimestamp{t("priceHistory.tableHeaders.source")}{t("priceHistory.tableHeaders.pair")}{t("priceHistory.tableHeaders.price")}{t("priceHistory.tableHeaders.timestamp")}
- Loading... + {t("priceHistory.loading")}
- No price records yet. Click "Fetch Now" to get the current price. + {t("priceHistory.emptyState")}