diff --git a/lib/core/app_theme.dart b/lib/core/app_theme.dart index 7cd18857..3c723334 100644 --- a/lib/core/app_theme.dart +++ b/lib/core/app_theme.dart @@ -148,6 +148,18 @@ abstract final class AppSpacing { static const double xxl = 32; } +// ── Responsive breakpoints ──────────────────────────────────────────────────── + +/// Logical-pixel breakpoints for responsive layouts. +/// +/// - < [tablet] → mobile (single-column, overlay drawer, bottom nav) +/// - [tablet] – [desktop] → tablet (2-column grid, side panel) +/// - ≥ [desktop] → desktop (3-column grid, persistent sidebar, no bottom nav) +abstract final class AppBreakpoints { + static const double tablet = 600; + static const double desktop = 1200; +} + // ── Border-radius tokens ─────────────────────────────────────────────────────── abstract final class AppRadius { diff --git a/lib/features/chat/screens/chat_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index 6266e239..35d04278 100644 --- a/lib/features/chat/screens/chat_room_screen.dart +++ b/lib/features/chat/screens/chat_room_screen.dart @@ -132,6 +132,102 @@ class _ChatRoomScreenState extends ConsumerState { final colors = Theme.of(context).extension(); if (colors == null) throw StateError('AppColors theme extension must be registered'); + final screenWidth = MediaQuery.sizeOf(context).width; + final showSidePanel = screenWidth >= AppBreakpoints.tablet; + + // ── Side panel (tablet+) ───────────────────────────────────────────────── + // On tablet/desktop the info panels are always visible as a persistent + // sidebar rather than toggling over the message list. + Widget? sidePanel; + if (showSidePanel) { + sidePanel = SizedBox( + width: 300, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: _showTradeInfo + ? TradeInformationTab( + key: const ValueKey('trade'), + orderId: widget.orderId, + ) + : _showUserInfo + ? UserInformationTab( + key: const ValueKey('user'), + peerHandle: room.peerHandle, + peerPubkey: room.peerPubkey, + peerIconIndex: room.peerIconIndex, + peerColorHue: room.peerColorHue, + ) + : Container( + key: const ValueKey('none'), + color: colors.backgroundCard, + child: Center( + child: Text( + 'Select ℹ or 👤\nfor details', + textAlign: TextAlign.center, + style: TextStyle(color: colors.textSubtle), + ), + ), + ), + ), + ); + } + + // ── Chat column ────────────────────────────────────────────────────────── + final chatColumn = Column( + children: [ + // Animated info panels (mobile only — on tablet+ shown as sidebar) + if (!showSidePanel) + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: _showTradeInfo + ? TradeInformationTab( + key: const ValueKey('trade'), + orderId: widget.orderId, + ) + : _showUserInfo + ? UserInformationTab( + key: const ValueKey('user'), + peerHandle: room.peerHandle, + peerPubkey: room.peerPubkey, + peerIconIndex: room.peerIconIndex, + peerColorHue: room.peerColorHue, + ) + : const SizedBox.shrink(key: ValueKey('none')), + ), + + // Message list + Expanded( + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + itemCount: _messages.length, + itemBuilder: (context, index) { + return MessageBubble( + message: _messages[index], + peerColorHue: room.peerColorHue, + ); + }, + ), + ), + + // Composition bar + Padding( + padding: EdgeInsets.only( + left: AppSpacing.sm, + right: AppSpacing.sm, + bottom: + MediaQuery.of(context).viewInsets.bottom + AppSpacing.sm, + top: AppSpacing.xs, + ), + child: MessageInput( + onSendText: _onSend, + onAttachFile: _onAttach, + isAttaching: _isAttaching, + ), + ), + ], + ); + return Scaffold( // Keyboard avoidance is handled manually via viewInsets.bottom padding // on the composition bar so the BottomNavBar does not push content twice. @@ -158,59 +254,15 @@ class _ChatRoomScreenState extends ConsumerState { ), ], ), - body: Column( - children: [ - // Animated info panels - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: _showTradeInfo - ? TradeInformationTab( - key: const ValueKey('trade'), - orderId: widget.orderId, - ) - : _showUserInfo - ? UserInformationTab( - key: const ValueKey('user'), - peerHandle: room.peerHandle, - peerPubkey: room.peerPubkey, - peerIconIndex: room.peerIconIndex, - peerColorHue: room.peerColorHue, - ) - : const SizedBox.shrink(key: ValueKey('none')), - ), - - // Message list - Expanded( - child: ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), - itemCount: _messages.length, - itemBuilder: (context, index) { - return MessageBubble( - message: _messages[index], - peerColorHue: room.peerColorHue, - ); - }, - ), - ), - - // Composition bar - Padding( - padding: EdgeInsets.only( - left: AppSpacing.sm, - right: AppSpacing.sm, - bottom: MediaQuery.of(context).viewInsets.bottom + - AppSpacing.sm, - top: AppSpacing.xs, - ), - child: MessageInput( - onSendText: _onSend, - onAttachFile: _onAttach, - isAttaching: _isAttaching, - ), - ), - ], - ), + body: showSidePanel && sidePanel != null + ? Row( + children: [ + Expanded(child: chatColumn), + const VerticalDivider(width: 1), + sidePanel, + ], + ) + : chatColumn, bottomNavigationBar: const BottomNavBar(), ); } diff --git a/lib/features/drawer/screens/drawer_menu.dart b/lib/features/drawer/screens/drawer_menu.dart index 58920904..79d30e11 100644 --- a/lib/features/drawer/screens/drawer_menu.dart +++ b/lib/features/drawer/screens/drawer_menu.dart @@ -4,14 +4,27 @@ import 'package:go_router/go_router.dart'; import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; -/// Drawer menu overlay — slides from left, ~70% screen width. +/// Drawer menu — overlay on mobile/tablet, persistent sidebar on desktop. +/// +/// **Overlay mode** (`persistent: false`, default): renders as a full-screen +/// Stack with a 30 % black overlay and a 70 %-wide panel from the left edge. +/// **Persistent mode** (`persistent: true`): renders as a fixed-width +/// [240 px] sidebar column, suitable for embedding in a [Row] on desktop. /// /// Header: Mostro mascot icon + "Beta" label + "MOSTRO" title. /// 3 menu items: Account, Settings, About. class DrawerMenu extends StatelessWidget { - const DrawerMenu({super.key, required this.onClose}); + const DrawerMenu({ + super.key, + this.onClose, + this.persistent = false, + }); + + /// Called when the user taps the overlay background (overlay mode only). + final VoidCallback? onClose; - final VoidCallback onClose; + /// When `true` the widget renders as a sidebar column rather than an overlay. + final bool persistent; @override Widget build(BuildContext context) { @@ -19,8 +32,19 @@ class DrawerMenu extends StatelessWidget { final colors = theme.extension(); final green = colors?.mostroGreen ?? const Color(0xFF8CC63F); final cardBg = colors?.backgroundCard ?? const Color(0xFF1E2230); - final screenWidth = MediaQuery.sizeOf(context).width; + final panel = _SidebarContent( + green: green, + cardBg: cardBg, + theme: theme, + onNavigate: persistent ? null : onClose, + ); + + if (persistent) { + return SizedBox(width: 240, child: panel); + } + + final screenWidth = MediaQuery.sizeOf(context).width; return Stack( children: [ // Black overlay — 30% opacity @@ -32,108 +56,121 @@ class DrawerMenu extends StatelessWidget { // Drawer panel — 70% screen width Align( alignment: Alignment.centerLeft, - child: Container( - width: screenWidth * 0.7, - color: cardBg, - child: SafeArea( + child: SizedBox(width: screenWidth * 0.7, child: panel), + ), + ], + ); + } +} + +// ── Sidebar content (shared between overlay and persistent modes) ───────────── + +class _SidebarContent extends StatelessWidget { + const _SidebarContent({ + required this.green, + required this.cardBg, + required this.theme, + required this.onNavigate, + }); + + final Color green; + final Color cardBg; + final ThemeData theme; + + /// Called before each navigation push (closes overlay drawer if not null). + final VoidCallback? onNavigate; + + @override + Widget build(BuildContext context) { + return Container( + color: cardBg, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.xl, + AppSpacing.xxl, + AppSpacing.xl, + AppSpacing.lg, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header - Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.xl, - AppSpacing.xxl, - AppSpacing.xl, - AppSpacing.lg, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Mascot icon placeholder (skull) - Icon( - Icons.psychology_outlined, - size: 48, - color: green, + Icon(Icons.psychology_outlined, size: 48, color: green), + const SizedBox(height: AppSpacing.md), + Row( + children: [ + Text( + 'MOSTRO', + style: (theme.textTheme.headlineLarge ?? + theme.textTheme.headlineMedium ?? + const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + )) + .copyWith(color: green, letterSpacing: 2), + ), + const SizedBox(width: AppSpacing.sm), + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: 2, ), - const SizedBox(height: AppSpacing.md), - Row( - children: [ - Text( - 'MOSTRO', - style: (theme.textTheme.headlineLarge ?? - theme.textTheme.headlineMedium ?? - const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - )) - .copyWith( - color: green, - letterSpacing: 2, - ), - ), - const SizedBox(width: AppSpacing.sm), - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.sm, - vertical: 2, - ), - decoration: BoxDecoration( - border: Border.all(color: green), - borderRadius: - BorderRadius.circular(AppRadius.chip), - ), - child: Text( - 'Beta', - style: TextStyle( - color: green, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ), - ], + decoration: BoxDecoration( + border: Border.all(color: green), + borderRadius: BorderRadius.circular(AppRadius.chip), ), - ], - ), - ), - - const Divider(height: 1), - const SizedBox(height: AppSpacing.lg), - - // Menu items - _MenuItem( - icon: Icons.key_outlined, - label: 'Account', - onTap: () { - onClose(); - context.push(AppRoute.keyManagement); - }, - ), - const SizedBox(height: AppSpacing.md), - _MenuItem( - icon: Icons.settings_outlined, - label: 'Settings', - onTap: () { - onClose(); - context.push(AppRoute.settings); - }, - ), - const SizedBox(height: AppSpacing.md), - _MenuItem( - icon: Icons.info_outline, - label: 'About', - onTap: () { - onClose(); - context.push(AppRoute.about); - }, + child: Text( + 'Beta', + style: TextStyle( + color: green, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), ], ), ), - ), + + const Divider(height: 1), + const SizedBox(height: AppSpacing.lg), + + // Menu items + _MenuItem( + icon: Icons.key_outlined, + label: 'Account', + onTap: () { + onNavigate?.call(); + context.push(AppRoute.keyManagement); + }, + ), + const SizedBox(height: AppSpacing.md), + _MenuItem( + icon: Icons.settings_outlined, + label: 'Settings', + onTap: () { + onNavigate?.call(); + context.push(AppRoute.settings); + }, + ), + const SizedBox(height: AppSpacing.md), + _MenuItem( + icon: Icons.info_outline, + label: 'About', + onTap: () { + onNavigate?.call(); + context.push(AppRoute.about); + }, + ), + ], ), - ], + ), ); } } diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 18caa2f5..0b754569 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -11,6 +11,7 @@ import 'package:mostro/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro/shared/utils/fiat_currencies.dart'; import 'package:mostro/shared/widgets/notification_bell.dart'; import 'package:mostro/shared/widgets/add_order_button.dart'; +import 'package:mostro/l10n/app_localizations.dart'; import 'package:mostro/shared/widgets/order_filter.dart'; /// Home screen — public order book with BUY/SELL tabs, filter, and drawer. @@ -63,138 +64,183 @@ class _HomeScreenState extends ConsumerState final green = colors?.mostroGreen ?? const Color(0xFF8CC63F); final filteredOrders = ref.watch(filteredOrdersProvider); final flags = ref.watch(currencyFlagsProvider); + final screenWidth = MediaQuery.sizeOf(context).width; + final isDesktop = screenWidth >= AppBreakpoints.desktop; - return Scaffold( - body: Stack( - children: [ - // Main content - Column( - children: [ - // AppBar - SafeArea( - bottom: false, - child: _MostroAppBar( - green: green, - showHappyFace: _showHappyFace, - onMenuTap: _toggleDrawer, - onLogoTap: _triggerHappyFace, - ), + // ── Order list: responsive column count ────────────────────────────────── + final columns = screenWidth >= AppBreakpoints.desktop + ? 3 + : screenWidth >= AppBreakpoints.tablet + ? 2 + : 1; + + Widget orderContent(void Function(String orderId, OrderType type) onTap) { + if (filteredOrders.isEmpty) return const OrderListEmpty(); + if (columns == 1) { + return ListView.separated( + padding: const EdgeInsets.only( + left: AppSpacing.lg, + right: AppSpacing.lg, + top: AppSpacing.xs, + bottom: 100, + ), + itemCount: filteredOrders.length, + separatorBuilder: (_, __) => const SizedBox(height: AppSpacing.sm), + itemBuilder: (context, index) { + final order = filteredOrders[index]; + return OrderListItem( + order: order, + currencyFlags: flags, + onTap: () => onTap(order.id, ref.read(homeOrderTypeProvider)), + ); + }, + ); + } + return GridView.builder( + padding: const EdgeInsets.only( + left: AppSpacing.lg, + right: AppSpacing.lg, + top: AppSpacing.xs, + bottom: 100, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + crossAxisSpacing: AppSpacing.sm, + mainAxisSpacing: AppSpacing.sm, + childAspectRatio: 1.1, + ), + itemCount: filteredOrders.length, + itemBuilder: (context, index) { + final order = filteredOrders[index]; + return OrderListItem( + order: order, + currencyFlags: flags, + onTap: () => onTap(order.id, ref.read(homeOrderTypeProvider)), + ); + }, + ); + } + + void onOrderTap(String id, OrderType type) { + if (type == OrderType.buy) { + context.push(AppRoute.takeSellPath(id)); + } else { + context.push(AppRoute.takeBuyPath(id)); + } + } + + // ── Main content column ─────────────────────────────────────────────────── + final mainContent = Column( + children: [ + // AppBar (hidden hamburger on desktop — sidebar is always visible) + SafeArea( + bottom: false, + child: _MostroAppBar( + green: green, + showHappyFace: _showHappyFace, + onMenuTap: isDesktop ? null : _toggleDrawer, + onLogoTap: _triggerHappyFace, + ), + ), + + // Tabs + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.white.withValues(alpha: 0.1), ), + ), + ), + child: TabBar( + controller: _tabController, + indicatorColor: green, + labelColor: colors?.textPrimary, + unselectedLabelColor: colors?.textSecondary, + tabs: [ + Tab(text: AppLocalizations.of(context).tabBuyBtc), + Tab(text: AppLocalizations.of(context).tabSellBtc), + ], + ), + ), - // Tabs - Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.white.withValues(alpha: 0.1), - ), - ), - ), - child: TabBar( - controller: _tabController, - indicatorColor: green, - labelColor: colors?.textPrimary, - unselectedLabelColor: colors?.textSecondary, - tabs: const [ - Tab(text: 'BUY BTC'), - Tab(text: 'SELL BTC'), - ], + // Filter pill + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + child: GestureDetector( + onTap: () => showOrderFilterDialog(context), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, ), + decoration: BoxDecoration( + color: colors?.backgroundInput, + borderRadius: BorderRadius.circular(AppRadius.button), ), - - // Filter pill - Padding( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm, - ), - child: GestureDetector( - onTap: () => showOrderFilterDialog(context), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - decoration: BoxDecoration( - color: colors?.backgroundInput, - borderRadius: BorderRadius.circular(AppRadius.button), + child: Row( + children: [ + Icon( + Icons.filter_alt_outlined, + size: 16, + color: colors?.textSecondary, + ), + const SizedBox(width: AppSpacing.xs), + Text( + AppLocalizations.of(context).filterButtonLabel, + style: TextStyle( + color: colors?.textSecondary, + fontSize: 12, + fontWeight: FontWeight.w600, ), - child: Row( - children: [ - Icon( - Icons.filter_alt_outlined, - size: 16, - color: colors?.textSecondary, - ), - const SizedBox(width: AppSpacing.xs), - Text( - 'FILTER', - style: TextStyle( - color: colors?.textSecondary, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - Text( - '${filteredOrders.length} offers', - style: TextStyle( - color: colors?.textSecondary, - fontSize: 12, - ), - ), - ], + ), + const Spacer(), + Text( + AppLocalizations.of(context).offersCount(filteredOrders.length), + style: TextStyle( + color: colors?.textSecondary, + fontSize: 12, ), ), - ), + ], ), + ), + ), + ), - // Order list - // TODO: Add RefreshIndicator when orderBookProvider is backed - // by the Rust bridge (Phase 7). Currently mock data — refresh is a no-op. - Expanded( - child: filteredOrders.isEmpty - ? const OrderListEmpty() - : ListView.separated( - padding: const EdgeInsets.only( - left: AppSpacing.lg, - right: AppSpacing.lg, - top: AppSpacing.xs, - bottom: 100, - ), - itemCount: filteredOrders.length, - separatorBuilder: (_, __) => - const SizedBox(height: AppSpacing.sm), - itemBuilder: (context, index) { - final order = filteredOrders[index]; - return OrderListItem( - order: order, - currencyFlags: flags, - onTap: () { - final orderType = - ref.read(homeOrderTypeProvider); - if (orderType == OrderType.buy) { - context.push( - AppRoute.takeSellPath(order.id), - ); - } else { - context.push( - AppRoute.takeBuyPath(order.id), - ); - } - }, - ); - }, - ), - ), + // Order list (responsive) + // TODO: Add RefreshIndicator when orderBookProvider is backed + // by the Rust bridge (Phase 7). Currently mock data — refresh is a no-op. + Expanded(child: orderContent(onOrderTap)), + ], + ); + + // ── Scaffold layout ─────────────────────────────────────────────────────── + // Desktop: persistent sidebar + main content in a Row (no overlay drawer). + // Mobile/tablet: Stack with optional overlay drawer. + final body = isDesktop + ? Row( + children: [ + const DrawerMenu(persistent: true), + const VerticalDivider(width: 1), + Expanded(child: mainContent), ], - ), + ) + : Stack( + children: [ + mainContent, + if (_drawerOpen) + DrawerMenu( + onClose: () => setState(() => _drawerOpen = false), + ), + ], + ); - // Drawer overlay - if (_drawerOpen) - DrawerMenu(onClose: () => setState(() => _drawerOpen = false)), - ], - ), + return Scaffold( + body: body, floatingActionButton: const AddOrderButton(), bottomNavigationBar: const BottomNavBar(), ); @@ -212,7 +258,8 @@ class _MostroAppBar extends StatelessWidget { final Color green; final bool showHappyFace; - final VoidCallback onMenuTap; + /// Null on desktop where the persistent sidebar replaces the overlay drawer. + final VoidCallback? onMenuTap; final VoidCallback onLogoTap; @override @@ -224,11 +271,12 @@ class _MostroAppBar extends StatelessWidget { ), child: Row( children: [ - IconButton( - onPressed: onMenuTap, - icon: const Icon(Icons.menu, size: 24), - tooltip: 'Menu', - ), + if (onMenuTap != null) + IconButton( + onPressed: onMenuTap, + icon: const Icon(Icons.menu, size: 24), + tooltip: 'Menu', + ), const Spacer(), GestureDetector( onTap: onLogoTap, diff --git a/lib/features/settings/screens/connect_wallet_screen.dart b/lib/features/settings/screens/connect_wallet_screen.dart index 8c5848a0..8e044116 100644 --- a/lib/features/settings/screens/connect_wallet_screen.dart +++ b/lib/features/settings/screens/connect_wallet_screen.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; - import 'package:mostro/core/app_routes.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:mostro/shared/widgets/platform_aware_qr_scanner.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/features/settings/providers/nwc_provider.dart'; @@ -92,10 +92,10 @@ class _ConnectWalletScreenState extends ConsumerState { } } - void _onQrDetected(BarcodeCapture capture) { - final raw = capture.barcodes.firstOrNull?.rawValue; - if (raw != null && raw.startsWith('nostr+walletconnect://')) { - _uriController.text = raw; + void _onQrDetected(String raw) { + final normalized = raw.trim(); + if (normalized.toLowerCase().startsWith('nostr+walletconnect://')) { + _uriController.text = normalized; setState(() => _showScanner = false); } } @@ -114,7 +114,10 @@ class _ConnectWalletScreenState extends ConsumerState { title: const Text('Scan QR Code'), leading: BackButton(onPressed: () => setState(() => _showScanner = false)), ), - body: MobileScanner(onDetect: _onQrDetected), + body: PlatformAwareQrScanner( + hint: AppLocalizations.of(context).pasteNwcUri, + onDetected: _onQrDetected, + ), ); } diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart index 0f482b0f..9df7bca4 100644 --- a/lib/features/settings/screens/settings_screen.dart +++ b/lib/features/settings/screens/settings_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/l10n/app_localizations.dart'; import 'package:mostro/features/settings/providers/nwc_provider.dart'; import 'package:mostro/features/settings/providers/settings_provider.dart'; import 'package:mostro/features/settings/widgets/currency_selector_dialog.dart'; @@ -31,7 +32,7 @@ class _SettingsScreenState extends ConsumerState { return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text(AppLocalizations.of(context).settingsScreenTitle), ), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), @@ -41,40 +42,52 @@ class _SettingsScreenState extends ConsumerState { context: context, colors: colors, icon: Icons.language, - title: 'Language', + title: AppLocalizations.of(context).languageSettingTitle, subtitle: languageNameForCode(settings.language), onTap: () => showLanguageSelector(context), ), - // 2 — Default Fiat Currency + // 2 — Appearance (theme) + _settingsCard( + context: context, + colors: colors, + icon: Icons.brightness_6_outlined, + title: AppLocalizations.of(context).appearanceSettingTitle, + subtitle: _themeLabel(context, settings.themeMode), + onTap: () => _showThemeDialog(context), + ), + + // 3 — Default Fiat Currency _settingsCard( context: context, colors: colors, icon: Icons.monetization_on_outlined, - title: 'Default Fiat Currency', - subtitle: settings.defaultFiatCode ?? 'All currencies', + title: AppLocalizations.of(context).defaultFiatCurrencyTitle, + subtitle: settings.defaultFiatCode ?? AppLocalizations.of(context).allCurrencies, onTap: () => showCurrencySelector(context), ), - // 3 — Lightning Address + // 4 — Lightning Address _settingsCard( context: context, colors: colors, icon: Icons.bolt, - title: 'Lightning Address', - subtitle: settings.defaultLightningAddress ?? 'Tap to set', + title: AppLocalizations.of(context).lightningAddressSettingTitle, + subtitle: settings.defaultLightningAddress ?? AppLocalizations.of(context).tapToSetSubtitle, onTap: () => _showLightningAddressDialog(context), ), - // 4 — NWC Wallet + // 5 — NWC Wallet _settingsCard( context: context, colors: colors, icon: Icons.account_balance_wallet_outlined, - title: 'NWC Wallet', + title: AppLocalizations.of(context).nwcWalletSettingTitle, subtitle: isWalletConnected - ? 'NWC — Connected. Balance: ${wallet.balanceSats != null ? '${wallet.balanceSats} sats' : 'N/A'}' - : 'Connect your Lightning wallet via NWC', + ? AppLocalizations.of(context).nwcConnectedBalance( + wallet.balanceSats != null ? '${wallet.balanceSats} sats' : 'N/A', + ) + : AppLocalizations.of(context).nwcConnectPrompt, onTap: () => context.push( isWalletConnected ? AppRoute.walletSettings @@ -82,7 +95,7 @@ class _SettingsScreenState extends ConsumerState { ), ), - // 5 — Relays + // 6 — Relays Container( margin: const EdgeInsets.only(bottom: AppSpacing.md), decoration: BoxDecoration( @@ -110,12 +123,12 @@ class _SettingsScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Relays', + AppLocalizations.of(context).relaysSettingTitle, style: Theme.of(context).textTheme.bodyLarge ?.copyWith(fontWeight: FontWeight.w600), ), Text( - 'Manage relay connections', + AppLocalizations.of(context).manageRelayConnections, style: Theme.of(context).textTheme.bodySmall, ), ], @@ -150,8 +163,8 @@ class _SettingsScreenState extends ConsumerState { context: context, colors: colors, icon: Icons.notifications_outlined, - title: 'Push Notifications', - subtitle: 'Manage notification preferences', + title: AppLocalizations.of(context).pushNotificationsSettingTitle, + subtitle: AppLocalizations.of(context).manageNotificationPreferences, onTap: () => context.push(AppRoute.notificationSettings), ), @@ -160,8 +173,8 @@ class _SettingsScreenState extends ConsumerState { context: context, colors: colors, icon: Icons.description_outlined, - title: 'Log Report', - subtitle: 'View diagnostic logs', + title: AppLocalizations.of(context).logReportSettingTitle, + subtitle: AppLocalizations.of(context).viewDiagnosticLogs, onTap: () => context.push(AppRoute.logs), ), @@ -170,7 +183,7 @@ class _SettingsScreenState extends ConsumerState { context: context, colors: colors, icon: Icons.hub_outlined, - title: 'Mostro Node', + title: AppLocalizations.of(context).mostroNodeSettingTitle, subtitle: truncatePubkey(mostroPubkey), onTap: () => showMostroNodeSelector(context), ), @@ -232,6 +245,37 @@ class _SettingsScreenState extends ConsumerState { ); } + // ── Theme helpers ───────────────────────────────────────────────────────────── + + String _themeLabel(BuildContext context, ThemeMode mode) { + final l10n = AppLocalizations.of(context); + return switch (mode) { + ThemeMode.dark => l10n.themeDark, + ThemeMode.light => l10n.themeLight, + ThemeMode.system => l10n.themeSystemDefault, + }; + } + + Future _showThemeDialog(BuildContext context) async { + final current = ref.read(settingsProvider).themeMode; + await showDialog( + context: context, + builder: (ctx) => SimpleDialog( + title: Text(AppLocalizations.of(ctx).appearanceDialogTitle), + children: ThemeMode.values.map((mode) { + return ListTile( + title: Text(_themeLabel(ctx, mode)), + trailing: mode == current ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setThemeMode(mode); + Navigator.of(ctx).pop(); + }, + ); + }).toList(), + ), + ); + } + // ── Lightning address dialog ────────────────────────────────────────────────── Future _showLightningAddressDialog(BuildContext context) async { @@ -246,13 +290,14 @@ class _SettingsScreenState extends ConsumerState { builder: (ctx) { return StatefulBuilder( builder: (ctx, setDialogState) { + final l10n = AppLocalizations.of(ctx); return AlertDialog( - title: const Text('Lightning Address'), + title: Text(l10n.lightningAddressDialogTitle), content: TextField( controller: controller, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( - hintText: 'user@domain.com', + hintText: l10n.lightningAddressHintText, errorText: errorText, ), onChanged: (_) { @@ -269,11 +314,11 @@ class _SettingsScreenState extends ConsumerState { .setDefaultLightningAddress(null); Navigator.of(ctx).pop(); }, - child: const Text('Clear'), + child: Text(l10n.clearButtonLabel), ), TextButton( onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), TextButton( onPressed: () { @@ -290,7 +335,7 @@ class _SettingsScreenState extends ConsumerState { parts[0].isEmpty || parts[1].isEmpty) { setDialogState( - () => errorText = 'Must be in user@domain format', + () => errorText = l10n.invalidLightningAddressFormat, ); return; } @@ -299,7 +344,7 @@ class _SettingsScreenState extends ConsumerState { .setDefaultLightningAddress(input); Navigator.of(ctx).pop(); }, - child: const Text('Save'), + child: Text(l10n.saveButtonLabel), ), ], ); diff --git a/lib/features/settings/widgets/currency_selector_dialog.dart b/lib/features/settings/widgets/currency_selector_dialog.dart index 76190e92..07dfe1a4 100644 --- a/lib/features/settings/widgets/currency_selector_dialog.dart +++ b/lib/features/settings/widgets/currency_selector_dialog.dart @@ -53,6 +53,7 @@ class _CurrencySelectorDialogState @override void dispose() { + _searchController.removeListener(_onSearch); _searchController.dispose(); super.dispose(); } diff --git a/lib/features/settings/widgets/language_selector.dart b/lib/features/settings/widgets/language_selector.dart index bee17a47..09b68dc1 100644 --- a/lib/features/settings/widgets/language_selector.dart +++ b/lib/features/settings/widgets/language_selector.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/features/settings/providers/settings_provider.dart'; +import 'package:mostro/l10n/app_localizations.dart'; // ── Language data ───────────────────────────────────────────────────────────── @@ -43,7 +44,7 @@ class LanguageSelector extends ConsumerWidget { child: Row( children: [ Text( - 'Select Language', + AppLocalizations.of(context).selectLanguageTitle, style: Theme.of(context).textTheme.headlineSmall, ), const Spacer(), diff --git a/lib/features/settings/widgets/relay_management_card.dart b/lib/features/settings/widgets/relay_management_card.dart index 6e38a979..e61c9bb1 100644 --- a/lib/features/settings/widgets/relay_management_card.dart +++ b/lib/features/settings/widgets/relay_management_card.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/core/mostro_defaults.dart'; +import 'package:mostro/l10n/app_localizations.dart'; // ── Model ───────────────────────────────────────────────────────────────────── @@ -70,12 +71,13 @@ class _RelayManagementCardState extends ConsumerState { builder: (ctx) { return StatefulBuilder( builder: (ctx, setDialogState) { + final l10n = AppLocalizations.of(ctx); return AlertDialog( - title: const Text('Add Relay'), + title: Text(l10n.addRelayDialogTitle), content: TextField( controller: controller, decoration: InputDecoration( - hintText: 'wss://relay.example.com', + hintText: l10n.relayHintText, errorText: errorText, ), onChanged: (_) { @@ -87,28 +89,31 @@ class _RelayManagementCardState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), TextButton( onPressed: () { final url = controller.text.trim(); if (!url.startsWith('wss://')) { setDialogState( - () => errorText = 'Must start with wss://', + () => errorText = l10n.relayErrorMustStartWithWss, ); return; } if (url.length < 10) { - setDialogState(() => errorText = 'URL is too short'); + setDialogState(() => errorText = l10n.relayErrorUrlTooShort); return; } if (_relays.any((r) => r.url == url)) { setDialogState( - () => errorText = 'Relay already in list', + () => errorText = l10n.relayErrorDuplicate, ); return; } - if (!mounted) return; + if (!mounted) { + if (ctx.mounted) Navigator.of(ctx).pop(); + return; + } setState(() { _relays.add( _RelayEntry( @@ -121,7 +126,7 @@ class _RelayManagementCardState extends ConsumerState { Navigator.of(ctx).pop(); // TODO(bridge): call add_relay(url) }, - child: const Text('Add'), + child: Text(l10n.addButtonLabel), ), ], ); @@ -197,7 +202,7 @@ class _RelayManagementCardState extends ConsumerState { onPressed: _showAddRelayDialog, icon: Icon(Icons.add, color: c.mostroGreen), label: Text( - 'Add Relay', + AppLocalizations.of(context).addRelayDialogTitle, style: TextStyle(color: c.mostroGreen), ), ), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 47a3d6da..493c15bb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,6 +1,7 @@ { "@@locale": "de", - "@@last_modified": "2026-03-29", + "@@last_modified": "2026-03-31", + "appName": "Mostro", "loading": "Laden…", "error": "Fehler", @@ -8,16 +9,18 @@ "confirm": "Bestätigen", "done": "Fertig", "skip": "Überspringen", + "chatTimestampYesterday": "Gestern", + "disputesEmptyState": "Deine Streitfälle werden hier angezeigt", "disputeAttachFile": "Datei anhängen", - "disputeWriteMessageHint": "Nachricht schreiben\u2026", + "disputeWriteMessageHint": "Nachricht schreiben…", "disputeSend": "Senden", "orderDispute": "Bestellstreit", - "disputeAdminAssigned": "Ein Administrator wurde Ihrem Streitfall zugewiesen. Er wird sich hier in Kürze bei Ihnen melden.", + "disputeAdminAssigned": "Ein Administrator wurde deinem Streitfall zugewiesen. Er wird sich hier in Kürze bei dir melden.", "disputeChatClosed": "Dieser Streitfall wurde gelöst. Der Chat ist geschlossen.", "messageCopied": "Kopiert", - "disputeLoadError": "Streitfälle konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "disputeLoadError": "Streitfälle konnten nicht geladen werden. Bitte versuche es erneut.", "disputeMessagingComingSoon": "Streitfall-Nachrichten demnächst verfügbar", "disputeAttachmentsComingSoon": "Dateianhänge demnächst verfügbar", "disputeNotFound": "Streitfall nicht gefunden.", @@ -32,5 +35,159 @@ "disputeInProgress": "In Bearbeitung", "disputeStatusClosed": "Geschlossen", "disputeLostFundsToBuyer": "Der Administrator hat den Streitfall zugunsten des Käufers entschieden. Die Sats wurden an den Käufer freigegeben.", - "disputeLostFundsToSeller": "Der Administrator hat die Bestellung storniert und die Sats an den Verkäufer zurückgegeben. Sie haben keine Sats erhalten." + "disputeLostFundsToSeller": "Der Administrator hat die Bestellung storniert und die Sats an den Verkäufer zurückgegeben. Du hast keine Sats erhalten.", + + "walkthroughSlideOneTitle": "Bitcoin frei handeln — kein KYC", + "walkthroughSlideOneBody": "Mostro ist eine Peer-to-Peer-Börse, mit der du Bitcoin gegen jede Währung und Zahlungsmethode tauschen kannst — ohne KYC und ohne deine Daten an irgendjemanden weiterzugeben. Es basiert auf Nostr, was es zensurresistent macht. Niemand kann dich am Handeln hindern.", + "walkthroughSlideTwoTitle": "Privatsphäre als Standard", + "walkthroughSlideTwoBody": "Mostro generiert für jeden Austausch eine neue Identität, sodass deine Trades nicht verknüpft werden können. Du kannst auch selbst entscheiden, wie viel Privatsphäre du möchtest:\n• Reputationsmodus – Andere können deine erfolgreichen Trades und dein Vertrauenslevel sehen.\n• Vollständiger Privatsphäre-Modus – Es wird keine Reputation aufgebaut, aber deine Aktivität ist vollständig anonym.\nWechsle jederzeit den Modus im Konto-Bildschirm, wo du auch deine geheimen Wörter sichern solltest — sie sind die einzige Möglichkeit, dein Konto wiederherzustellen.", + "walkthroughSlideThreeTitle": "Sicherheit bei jedem Schritt", + "walkthroughSlideThreeBody": "Mostro verwendet Hold Invoices (zurückgehaltene Rechnungen): Die Sats verbleiben bis zum Ende des Handels in der Wallet des Verkäufers. Das schützt beide Seiten. Die App ist außerdem so gestaltet, dass sie intuitiv und einfach für alle Arten von Nutzern ist.", + "walkthroughSlideFourTitle": "Vollständig verschlüsselter Chat", + "walkthroughSlideFourBody": "Jeder Trade hat seinen eigenen privaten Chat, der Ende-zu-Ende verschlüsselt ist. Nur die beiden beteiligten Nutzer können ihn lesen. Im Streitfall kannst du den gemeinsamen Schlüssel einem Administrator geben, um bei der Lösung zu helfen.", + "walkthroughSlideFiveTitle": "Ein Angebot annehmen", + "walkthroughSlideFiveBody": "Durchsuche das Orderbuch, wähle ein Angebot, das für dich passt, und folge dem Trade-Ablauf Schritt für Schritt. Du kannst das Profil des anderen Nutzers prüfen, sicher chatten und den Trade problemlos abschließen.", + "walkthroughSlideSixTitle": "Findest du nicht, was du brauchst?", + "walkthroughSlideSixBody": "Du kannst auch dein eigenes Angebot erstellen und warten, bis jemand es annimmt. Lege den Betrag und die bevorzugte Zahlungsmethode fest — Mostro erledigt den Rest.", + + "tabBuyBtc": "BTC KAUFEN", + "tabSellBtc": "BTC VERKAUFEN", + "filterButtonLabel": "FILTERN", + "offersCount": "{count, plural, =1{1 Angebot} other{{count} Angebote}}", + "noOrdersAvailable": "Keine Bestellungen verfügbar", + "justNow": "Gerade eben", + "minutesAgo": "Vor {m}m", + "hoursAgo": "Vor {h}h", + "daysAgo": "Vor {d}T", + + "creatingNewOrderTitle": "NEUE BESTELLUNG ERSTELLEN", + "youWantToBuyBitcoin": "Du möchtest Bitcoin kaufen", + "youWantToSellBitcoin": "Du möchtest Bitcoin verkaufen", + "rangeOrderLabel": "Bereichsbestellung", + "payLightningInvoiceTitle": "Lightning-Rechnung bezahlen", + "invoiceCopied": "Rechnung kopiert", + "addInvoiceTitle": "Rechnung hinzufügen", + "submitButtonLabel": "Absenden", + "orderAlreadyTaken": "Die Bestellung wurde bereits angenommen", + "orderIdCopied": "Bestell-ID kopiert", + + "orderDetailsTitle": "BESTELLDETAILS", + "timeRemainingLabel": "Verbleibende Zeit:", + "fiatSentButtonLabel": "FIAT GESENDET", + "disputeButtonLabel": "STREITFALL", + "contactButtonLabel": "KONTAKT", + "rateButtonLabel": "BEWERTEN", + "viewDisputeButtonLabel": "STREITFALL ANZEIGEN", + "comingSoonMessage": "Demnächst verfügbar", + "tradeStatusActive": "Aktiv", + "tradeStatusFiatSent": "Fiat gesendet", + "tradeStatusCompleted": "Abgeschlossen", + "tradeStatusCancelled": "Storniert", + "tradeStatusDisputed": "Strittiger Trade", + "releaseButtonLabel": "FREIGEBEN", + + "accountScreenTitle": "Konto", + "secretWordsTitle": "Geheime Wörter", + "toRestoreYourAccount": "Um dein Konto wiederherzustellen", + "privacyCardTitle": "Datenschutz", + "controlPrivacySettings": "Verwalte deine Datenschutzeinstellungen", + "reputationMode": "Reputationsmodus", + "reputationModeSubtitle": "Standard-Datenschutz mit Reputation", + "fullPrivacyMode": "Vollständiger Privatsphäre-Modus", + "fullPrivacyModeSubtitle": "Maximale Anonymität", + "generateNewUserButton": "Neuen Benutzer generieren", + "importMostroUserButton": "Mostro-Benutzer importieren", + "generateNewUserDialogTitle": "Neuen Benutzer generieren?", + "generateNewUserDialogContent": "Dadurch wird eine brandneue Identität erstellt. Deine aktuellen geheimen Wörter werden nicht mehr funktionieren — stelle sicher, dass du sie gesichert hast, bevor du fortfährst.", + "continueButtonLabel": "Weiter", + "importMnemonicDialogTitle": "Mnemonik importieren", + "importMnemonicHintText": "Gib deine 12- oder 24-Wort-Phrase ein…", + "importButtonLabel": "Importieren", + "refreshUserDialogTitle": "Benutzer aktualisieren?", + "refreshUserDialogContent": "Dadurch werden deine Trades und Bestellungen von der Mostro-Instanz erneut abgerufen. Verwende dies, wenn du glaubst, dass deine Daten nicht synchron sind oder Bestellungen fehlen.", + "hideButtonLabel": "Verbergen", + "showButtonLabel": "Anzeigen", + + "settingsScreenTitle": "Einstellungen", + "languageSettingTitle": "Sprache", + "appearanceSettingTitle": "Erscheinungsbild", + "appearanceDialogTitle": "Erscheinungsbild", + "defaultFiatCurrencyTitle": "Standard-Fiat-Währung", + "allCurrencies": "Alle Währungen", + "lightningAddressSettingTitle": "Lightning-Adresse", + "tapToSetSubtitle": "Tippen zum Einrichten", + "nwcWalletSettingTitle": "NWC-Wallet", + "nwcConnectPrompt": "Verbinde deine Lightning-Wallet über NWC", + "relaysSettingTitle": "Relays", + "manageRelayConnections": "Relay-Verbindungen verwalten", + "pushNotificationsSettingTitle": "Push-Benachrichtigungen", + "manageNotificationPreferences": "Benachrichtigungseinstellungen verwalten", + "logReportSettingTitle": "Protokollbericht", + "viewDiagnosticLogs": "Diagnoseprotokolle anzeigen", + "mostroNodeSettingTitle": "Mostro-Knoten", + "themeDark": "Dunkel", + "themeLight": "Hell", + "themeSystemDefault": "Systemstandard", + "lightningAddressDialogTitle": "Lightning-Adresse", + "lightningAddressHintText": "benutzer@domain.com", + "invalidLightningAddressFormat": "Muss im Format benutzer@domain vorliegen", + "clearButtonLabel": "Löschen", + "saveButtonLabel": "Speichern", + "connectWalletTitle": "Wallet verbinden", + "scanQrCodeTitle": "QR-Code scannen", + "pasteNwcUri": "NWC-URI einfügen", + "selectLanguageTitle": "Sprache auswählen", + "selectCurrencyDialogTitle": "Währung auswählen", + "addRelayDialogTitle": "Relay hinzufügen", + "addButtonLabel": "Hinzufügen", + "relayHintText": "wss://relay.example.com", + "relayErrorMustStartWithWss": "Muss mit wss:// beginnen", + "relayErrorUrlTooShort": "URL ist zu kurz", + "relayErrorDuplicate": "Relay bereits in der Liste", + "nwcConnectedBalance": "NWC — Verbunden. Guthaben: {balance}", + "pasteQrCodeHeading": "QR-Code-Inhalt einfügen", + "pasteButtonLabel": "Einfügen", + "clipboardEmptyError": "Zwischenablage ist leer", + "enterValueError": "Bitte einen Wert eingeben", + "pasteOrScanQrCode": "QR-Code einfügen oder scannen", + "mostroNodeTitle": "Mostro-Knoten", + "currentNodeLabel": "Aktueller Knoten", + "trustedBadgeLabel": "Vertrauenswürdig", + "useDefaultButtonLabel": "Standard verwenden", + "confirmButtonLabel": "Bestätigen", + "invalidHexPubkey": "Muss eine hexadezimale Zeichenfolge mit 64 Zeichen sein", + + "notificationsScreenTitle": "Benachrichtigungen", + "markAllAsReadMenuItem": "Alle als gelesen markieren", + "clearAllMenuItem": "Alle löschen", + "youMustBackUpYourAccount": "Du musst dein Konto sichern", + "tapToViewAndSaveSecretWords": "Tippe, um deine geheimen Wörter anzuzeigen und zu speichern.", + "noNotifications": "Keine Benachrichtigungen", + "markAsRead": "Als gelesen markieren", + "deleteNotificationLabel": "Löschen", + + "rateScreenHeader": "BEWERTEN", + "successfulOrder": "Erfolgreiche Bestellung", + "submitRatingButton": "ABSENDEN", + "closeRatingButton": "SCHLIESSEN", + + "aboutScreenTitle": "Über", + "mostroTagline": "Peer-to-Peer Bitcoin-Handel über Nostr", + "viewDocumentationButton": "Dokumentation anzeigen", + "linkCopiedToClipboard": "Link in die Zwischenablage kopiert", + "defaultNodeSection": "Standardknoten", + "pubkeyLabel": "Öffentlicher Schlüssel", + "relaysLabel": "Relays", + "pubkeyCopiedToClipboard": "Öffentlicher Schlüssel in die Zwischenablage kopiert", + "footerTagline": "Open-Source. Nicht-verwahrt. Privat.", + + "drawerTitle": "MOSTRO", + "betaBadgeLabel": "Beta", + "drawerAccountMenuItem": "Konto", + "drawerSettingsMenuItem": "Einstellungen", + "drawerAboutMenuItem": "Über", + + "navOrderBook": "Orderbuch", + "navMyTrades": "Meine Trades", + "navChat": "Chat" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c9f10757..609322e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,7 @@ { "@@locale": "en", - "@@last_modified": "2026-03-29", + "@@last_modified": "2026-03-31", + "appName": "Mostro", "@appName": {"description": "Application name"}, "loading": "Loading…", @@ -15,13 +16,15 @@ "@done": {"description": "Done action"}, "skip": "Skip", "@skip": {"description": "Skip action"}, + "chatTimestampYesterday": "Yesterday", "@chatTimestampYesterday": {"description": "Timestamp label for messages from yesterday"}, + "disputesEmptyState": "Your disputes will appear here", "@disputesEmptyState": {"description": "Empty state message on the disputes list screen"}, "disputeAttachFile": "Attach file", "@disputeAttachFile": {"description": "Tooltip for the attach file button in dispute chat"}, - "disputeWriteMessageHint": "Write a message\u2026", + "disputeWriteMessageHint": "Write a message…", "@disputeWriteMessageHint": {"description": "Hint text for the dispute chat message input field"}, "disputeSend": "Send", "@disputeSend": {"description": "Tooltip for the send button in dispute chat"}, @@ -73,5 +76,317 @@ "disputeLostFundsToBuyer": "The administrator settled the dispute in the buyer's favour. The sats were released to the buyer.", "@disputeLostFundsToBuyer": {"description": "Resolution text shown to the seller when admin released funds to the buyer"}, "disputeLostFundsToSeller": "The administrator canceled the order and returned the sats to the seller. You did not receive the sats.", - "@disputeLostFundsToSeller": {"description": "Resolution text shown to the buyer when admin returned funds to the seller"} + "@disputeLostFundsToSeller": {"description": "Resolution text shown to the buyer when admin returned funds to the seller"}, + + "walkthroughSlideOneTitle": "Trade Bitcoin freely — no KYC", + "@walkthroughSlideOneTitle": {"description": "Title for walkthrough slide 1"}, + "walkthroughSlideOneBody": "Mostro is a peer-to-peer exchange that lets you trade Bitcoin for any currency and payment method — no KYC, and no need to give your data to anyone. It's built on Nostr, which makes it censorship-resistant. No one can stop you from trading.", + "@walkthroughSlideOneBody": {"description": "Body text for walkthrough slide 1"}, + "walkthroughSlideTwoTitle": "Privacy by default", + "@walkthroughSlideTwoTitle": {"description": "Title for walkthrough slide 2"}, + "walkthroughSlideTwoBody": "Mostro generates a new identity for every exchange, so your trades can't be linked. You can also decide how private you want to be:\n• Reputation mode – Lets others see your successful trades and trust level.\n• Full privacy mode – No reputation is built, but your activity is completely anonymous.\nSwitch modes anytime from the Account screen, where you should also save your secret words — they're the only way to recover your account.", + "@walkthroughSlideTwoBody": {"description": "Body text for walkthrough slide 2"}, + "walkthroughSlideThreeTitle": "Security at every step", + "@walkthroughSlideThreeTitle": {"description": "Title for walkthrough slide 3"}, + "walkthroughSlideThreeBody": "Mostro uses Hold Invoices: sats stay in the seller's wallet until the end of the trade. This protects both sides. The app is also designed to be intuitive and easy for all kinds of users.", + "@walkthroughSlideThreeBody": {"description": "Body text for walkthrough slide 3"}, + "walkthroughSlideFourTitle": "Fully encrypted chat", + "@walkthroughSlideFourTitle": {"description": "Title for walkthrough slide 4"}, + "walkthroughSlideFourBody": "Each trade has its own private chat, end-to-end encrypted. Only the two users involved can read it. In case of a dispute, you can give the shared key to an admin to help resolve the issue.", + "@walkthroughSlideFourBody": {"description": "Body text for walkthrough slide 4"}, + "walkthroughSlideFiveTitle": "Take an offer", + "@walkthroughSlideFiveTitle": {"description": "Title for walkthrough slide 5"}, + "walkthroughSlideFiveBody": "Browse the order book, choose an offer that works for you, and follow the trade flow step by step. You'll be able to check the other user's profile, chat securely, and complete the trade with ease.", + "@walkthroughSlideFiveBody": {"description": "Body text for walkthrough slide 5"}, + "walkthroughSlideSixTitle": "Can't find what you need?", + "@walkthroughSlideSixTitle": {"description": "Title for walkthrough slide 6"}, + "walkthroughSlideSixBody": "You can also create your own offer and wait for someone to take it. Set the amount and preferred payment method — Mostro handles the rest.", + "@walkthroughSlideSixBody": {"description": "Body text for walkthrough slide 6"}, + + "tabBuyBtc": "BUY BTC", + "@tabBuyBtc": {"description": "Tab label for the buy Bitcoin order book"}, + "tabSellBtc": "SELL BTC", + "@tabSellBtc": {"description": "Tab label for the sell Bitcoin order book"}, + "filterButtonLabel": "FILTER", + "@filterButtonLabel": {"description": "Button label to open order book filter options"}, + "offersCount": "{count, plural, =1{1 offer} other{{count} offers}}", + "@offersCount": { + "description": "Number of offers shown in the order book", + "placeholders": {"count": {"type": "int"}} + }, + "noOrdersAvailable": "No orders available", + "@noOrdersAvailable": {"description": "Empty state message when the order book has no orders"}, + "justNow": "Just now", + "@justNow": {"description": "Timestamp label for a very recent event"}, + "minutesAgo": "{m}m ago", + "@minutesAgo": { + "description": "Relative timestamp in minutes", + "placeholders": {"m": {"type": "int"}} + }, + "hoursAgo": "{h}h ago", + "@hoursAgo": { + "description": "Relative timestamp in hours", + "placeholders": {"h": {"type": "int"}} + }, + "daysAgo": "{d}d ago", + "@daysAgo": { + "description": "Relative timestamp in days", + "placeholders": {"d": {"type": "int"}} + }, + + "creatingNewOrderTitle": "CREATING NEW ORDER", + "@creatingNewOrderTitle": {"description": "Screen title when the user is creating a new order"}, + "youWantToBuyBitcoin": "You want to buy Bitcoin", + "@youWantToBuyBitcoin": {"description": "Label shown when the order type is buy"}, + "youWantToSellBitcoin": "You want to sell Bitcoin", + "@youWantToSellBitcoin": {"description": "Label shown when the order type is sell"}, + "rangeOrderLabel": "Range order", + "@rangeOrderLabel": {"description": "Label for a range-amount order toggle"}, + "payLightningInvoiceTitle": "Pay Lightning Invoice", + "@payLightningInvoiceTitle": {"description": "Screen title for the pay Lightning invoice step"}, + "invoiceCopied": "Invoice copied", + "@invoiceCopied": {"description": "Snackbar shown after copying a Lightning invoice to clipboard"}, + "addInvoiceTitle": "Add Invoice", + "@addInvoiceTitle": {"description": "Screen title for adding a Lightning invoice"}, + "submitButtonLabel": "Submit", + "@submitButtonLabel": {"description": "Generic submit button label"}, + "orderAlreadyTaken": "Order has already been taken", + "@orderAlreadyTaken": {"description": "Error message when attempting to take an already-taken order"}, + "orderIdCopied": "Order ID copied", + "@orderIdCopied": {"description": "Snackbar shown after copying an order ID to clipboard"}, + + "orderDetailsTitle": "ORDER DETAILS", + "@orderDetailsTitle": {"description": "Screen title for the order/trade details screen"}, + "timeRemainingLabel": "Time remaining:", + "@timeRemainingLabel": {"description": "Label preceding the countdown timer in a trade"}, + "fiatSentButtonLabel": "FIAT SENT", + "@fiatSentButtonLabel": {"description": "Button label for the buyer to confirm fiat was sent"}, + "disputeButtonLabel": "DISPUTE", + "@disputeButtonLabel": {"description": "Button label to open a dispute for a trade"}, + "contactButtonLabel": "CONTACT", + "@contactButtonLabel": {"description": "Button label to open the trade chat"}, + "rateButtonLabel": "RATE", + "@rateButtonLabel": {"description": "Button label to rate the trading counterpart"}, + "viewDisputeButtonLabel": "VIEW DISPUTE", + "@viewDisputeButtonLabel": {"description": "Button label to view an active dispute"}, + "comingSoonMessage": "Coming soon", + "@comingSoonMessage": {"description": "Generic coming-soon placeholder message"}, + "tradeStatusActive": "Active", + "@tradeStatusActive": {"description": "Trade status chip label: active"}, + "tradeStatusFiatSent": "Fiat Sent", + "@tradeStatusFiatSent": {"description": "Trade status chip label: fiat sent"}, + "tradeStatusCompleted": "Completed", + "@tradeStatusCompleted": {"description": "Trade status chip label: completed"}, + "tradeStatusCancelled": "Cancelled", + "@tradeStatusCancelled": {"description": "Trade status chip label: cancelled"}, + "tradeStatusDisputed": "Disputed", + "@tradeStatusDisputed": {"description": "Trade status chip label: disputed"}, + "releaseButtonLabel": "RELEASE", + "@releaseButtonLabel": {"description": "Button label for the seller to release sats"}, + + "accountScreenTitle": "Account", + "@accountScreenTitle": {"description": "Screen title for the Account screen"}, + "secretWordsTitle": "Secret Words", + "@secretWordsTitle": {"description": "Section title for the mnemonic backup card"}, + "toRestoreYourAccount": "To restore your account", + "@toRestoreYourAccount": {"description": "Subtitle under the secret words section heading"}, + "privacyCardTitle": "Privacy", + "@privacyCardTitle": {"description": "Section title for the privacy settings card"}, + "controlPrivacySettings": "Control your privacy settings", + "@controlPrivacySettings": {"description": "Subtitle under the privacy section heading"}, + "reputationMode": "Reputation Mode", + "@reputationMode": {"description": "Label for reputation privacy mode option"}, + "reputationModeSubtitle": "Standard privacy with reputation", + "@reputationModeSubtitle": {"description": "Subtitle for reputation mode option"}, + "fullPrivacyMode": "Full Privacy Mode", + "@fullPrivacyMode": {"description": "Label for full privacy mode option"}, + "fullPrivacyModeSubtitle": "Maximum anonymity", + "@fullPrivacyModeSubtitle": {"description": "Subtitle for full privacy mode option"}, + "generateNewUserButton": "Generate New User", + "@generateNewUserButton": {"description": "Button label to generate a new Mostro identity"}, + "importMostroUserButton": "Import Mostro User", + "@importMostroUserButton": {"description": "Button label to import an existing Mostro identity via mnemonic"}, + "generateNewUserDialogTitle": "Generate New User?", + "@generateNewUserDialogTitle": {"description": "Confirmation dialog title for generating a new user"}, + "generateNewUserDialogContent": "This will create a brand-new identity. Your current secret words will no longer work — make sure they are backed up before continuing.", + "@generateNewUserDialogContent": {"description": "Confirmation dialog body for generating a new user"}, + "continueButtonLabel": "Continue", + "@continueButtonLabel": {"description": "Continue button label"}, + "importMnemonicDialogTitle": "Import Mnemonic", + "@importMnemonicDialogTitle": {"description": "Dialog title for importing a mnemonic phrase"}, + "importMnemonicHintText": "Enter your 12 or 24 word phrase…", + "@importMnemonicHintText": {"description": "Hint text in the mnemonic import text field"}, + "importButtonLabel": "Import", + "@importButtonLabel": {"description": "Button label to confirm mnemonic import"}, + "refreshUserDialogTitle": "Refresh User?", + "@refreshUserDialogTitle": {"description": "Dialog title for refreshing user data"}, + "refreshUserDialogContent": "This will re-fetch your trades and orders from the Mostro instance. Use this if you think your data is out of sync or orders are missing.", + "@refreshUserDialogContent": {"description": "Dialog body for refreshing user data"}, + "hideButtonLabel": "Hide", + "@hideButtonLabel": {"description": "Button label to hide sensitive information"}, + "showButtonLabel": "Show", + "@showButtonLabel": {"description": "Button label to reveal sensitive information"}, + + "settingsScreenTitle": "Settings", + "@settingsScreenTitle": {"description": "Screen title for the Settings screen"}, + "languageSettingTitle": "Language", + "@languageSettingTitle": {"description": "Settings list item title for language selection"}, + "appearanceSettingTitle": "Appearance", + "@appearanceSettingTitle": {"description": "Settings list item title for appearance/theme"}, + "appearanceDialogTitle": "Appearance", + "@appearanceDialogTitle": {"description": "Dialog title for the appearance/theme picker"}, + "defaultFiatCurrencyTitle": "Default Fiat Currency", + "@defaultFiatCurrencyTitle": {"description": "Settings list item title for default fiat currency"}, + "allCurrencies": "All currencies", + "@allCurrencies": {"description": "Option label meaning no currency filter is applied"}, + "lightningAddressSettingTitle": "Lightning Address", + "@lightningAddressSettingTitle": {"description": "Settings list item title for the user's Lightning address"}, + "tapToSetSubtitle": "Tap to set", + "@tapToSetSubtitle": {"description": "Subtitle shown when a settings value is not yet configured"}, + "nwcWalletSettingTitle": "NWC Wallet", + "@nwcWalletSettingTitle": {"description": "Settings list item title for NWC wallet connection"}, + "nwcConnectPrompt": "Connect your Lightning wallet via NWC", + "@nwcConnectPrompt": {"description": "Subtitle prompting the user to connect a wallet via Nostr Wallet Connect"}, + "relaysSettingTitle": "Relays", + "@relaysSettingTitle": {"description": "Settings list item title for Nostr relay management"}, + "manageRelayConnections": "Manage relay connections", + "@manageRelayConnections": {"description": "Subtitle for the relays settings entry"}, + "pushNotificationsSettingTitle": "Push Notifications", + "@pushNotificationsSettingTitle": {"description": "Settings list item title for push notification preferences"}, + "manageNotificationPreferences": "Manage notification preferences", + "@manageNotificationPreferences": {"description": "Subtitle for the push notifications settings entry"}, + "logReportSettingTitle": "Log Report", + "@logReportSettingTitle": {"description": "Settings list item title for viewing diagnostic logs"}, + "viewDiagnosticLogs": "View diagnostic logs", + "@viewDiagnosticLogs": {"description": "Subtitle for the log report settings entry"}, + "mostroNodeSettingTitle": "Mostro Node", + "@mostroNodeSettingTitle": {"description": "Settings list item title for the Mostro node configuration"}, + "themeDark": "Dark", + "@themeDark": {"description": "Theme option: dark mode"}, + "themeLight": "Light", + "@themeLight": {"description": "Theme option: light mode"}, + "themeSystemDefault": "System default", + "@themeSystemDefault": {"description": "Theme option: follow system setting"}, + "lightningAddressDialogTitle": "Lightning Address", + "@lightningAddressDialogTitle": {"description": "Dialog title for editing the Lightning address"}, + "lightningAddressHintText": "user@domain.com", + "@lightningAddressHintText": {"description": "Placeholder text in the Lightning address input field"}, + "invalidLightningAddressFormat": "Must be in user@domain format", + "@invalidLightningAddressFormat": {"description": "Validation error for an invalid Lightning address format"}, + "clearButtonLabel": "Clear", + "@clearButtonLabel": {"description": "Button label to clear a field or value"}, + "saveButtonLabel": "Save", + "@saveButtonLabel": {"description": "Button label to save a settings value"}, + "connectWalletTitle": "Connect Wallet", + "@connectWalletTitle": {"description": "Screen or dialog title for the NWC wallet connection flow"}, + "scanQrCodeTitle": "Scan QR Code", + "@scanQrCodeTitle": {"description": "Screen title for the QR code scanner"}, + "pasteNwcUri": "Paste NWC URI", + "@pasteNwcUri": {"description": "Hint text for the NWC URI input field / QR scanner fallback"}, + "selectLanguageTitle": "Select Language", + "@selectLanguageTitle": {"description": "Dialog or screen title for the language picker"}, + "selectCurrencyDialogTitle": "Select Currency", + "@selectCurrencyDialogTitle": {"description": "Dialog title for the currency picker"}, + "addRelayDialogTitle": "Add Relay", + "@addRelayDialogTitle": {"description": "Dialog title for adding a new Nostr relay"}, + "addButtonLabel": "Add", + "@addButtonLabel": {"description": "Generic add action button label"}, + "relayHintText": "wss://relay.example.com", + "@relayHintText": {"description": "Placeholder hint in the add-relay URL field"}, + "relayErrorMustStartWithWss": "Must start with wss://", + "@relayErrorMustStartWithWss": {"description": "Validation error when relay URL does not start with wss://"}, + "relayErrorUrlTooShort": "URL is too short", + "@relayErrorUrlTooShort": {"description": "Validation error when relay URL is too short"}, + "relayErrorDuplicate": "Relay already in list", + "@relayErrorDuplicate": {"description": "Validation error when relay URL is already added"}, + "nwcConnectedBalance": "NWC — Connected. Balance: {balance}", + "@nwcConnectedBalance": { + "description": "NWC wallet connected status with balance", + "placeholders": {"balance": {"type": "String"}} + }, + "pasteQrCodeHeading": "Paste QR Code Content", + "@pasteQrCodeHeading": {"description": "Heading text on the web QR code paste fallback screen"}, + "pasteButtonLabel": "Paste", + "@pasteButtonLabel": {"description": "Button label for paste-from-clipboard action"}, + "clipboardEmptyError": "Clipboard is empty", + "@clipboardEmptyError": {"description": "Error shown when clipboard has no text to paste"}, + "enterValueError": "Please enter a value", + "@enterValueError": {"description": "Validation error when QR input field is empty"}, + "pasteOrScanQrCode": "Paste or scan a QR code", + "@pasteOrScanQrCode": {"description": "Default hint text for the QR scanner widget"}, + "mostroNodeTitle": "Mostro Node", + "@mostroNodeTitle": {"description": "Section title on the Mostro node settings screen"}, + "currentNodeLabel": "Current Node", + "@currentNodeLabel": {"description": "Label for the currently active Mostro node"}, + "trustedBadgeLabel": "Trusted", + "@trustedBadgeLabel": {"description": "Badge shown on a verified/trusted Mostro node"}, + "useDefaultButtonLabel": "Use Default", + "@useDefaultButtonLabel": {"description": "Button label to reset to the default Mostro node"}, + "confirmButtonLabel": "Confirm", + "@confirmButtonLabel": {"description": "Button label to confirm a selection or action"}, + "invalidHexPubkey": "Must be a 64-character hex string", + "@invalidHexPubkey": {"description": "Validation error for an invalid hex pubkey input"}, + + "notificationsScreenTitle": "Notifications", + "@notificationsScreenTitle": {"description": "Screen title for the Notifications screen"}, + "markAllAsReadMenuItem": "Mark all as read", + "@markAllAsReadMenuItem": {"description": "Menu item to mark all notifications as read"}, + "clearAllMenuItem": "Clear all", + "@clearAllMenuItem": {"description": "Menu item to delete all notifications"}, + "youMustBackUpYourAccount": "You must back up your account", + "@youMustBackUpYourAccount": {"description": "Notification title prompting the user to back up their account"}, + "tapToViewAndSaveSecretWords": "Tap to view and save your secret words.", + "@tapToViewAndSaveSecretWords": {"description": "Notification body prompting the user to view and save secret words"}, + "noNotifications": "No notifications", + "@noNotifications": {"description": "Empty state message on the notifications screen"}, + "markAsRead": "Mark as read", + "@markAsRead": {"description": "Contextual action to mark a single notification as read"}, + "deleteNotificationLabel": "Delete", + "@deleteNotificationLabel": {"description": "Contextual action to delete a single notification"}, + + "rateScreenHeader": "RATE", + "@rateScreenHeader": {"description": "Header label on the post-trade rating screen"}, + "successfulOrder": "Successful order", + "@successfulOrder": {"description": "Label shown for a completed order on the rating screen"}, + "submitRatingButton": "SUBMIT", + "@submitRatingButton": {"description": "Button label to submit a trade rating"}, + "closeRatingButton": "CLOSE", + "@closeRatingButton": {"description": "Button label to close the rating screen without rating"}, + + "aboutScreenTitle": "About", + "@aboutScreenTitle": {"description": "Screen title for the About screen"}, + "mostroTagline": "Peer-to-peer Bitcoin trading over Nostr", + "@mostroTagline": {"description": "App tagline shown on the About screen"}, + "viewDocumentationButton": "View Documentation", + "@viewDocumentationButton": {"description": "Button label to open the Mostro documentation"}, + "linkCopiedToClipboard": "Link copied to clipboard", + "@linkCopiedToClipboard": {"description": "Snackbar shown after copying a link to clipboard"}, + "defaultNodeSection": "Default Node", + "@defaultNodeSection": {"description": "Section heading for the default Mostro node info"}, + "pubkeyLabel": "Pubkey", + "@pubkeyLabel": {"description": "Label for a Nostr public key"}, + "relaysLabel": "Relays", + "@relaysLabel": {"description": "Label for the list of Nostr relays"}, + "pubkeyCopiedToClipboard": "Pubkey copied to clipboard", + "@pubkeyCopiedToClipboard": {"description": "Snackbar shown after copying a pubkey to clipboard"}, + "footerTagline": "Open-source. Non-custodial. Private.", + "@footerTagline": {"description": "Footer tagline on the About screen"}, + + "drawerTitle": "MOSTRO", + "@drawerTitle": {"description": "Title shown at the top of the navigation drawer"}, + "betaBadgeLabel": "Beta", + "@betaBadgeLabel": {"description": "Badge label indicating the app is in beta"}, + "drawerAccountMenuItem": "Account", + "@drawerAccountMenuItem": {"description": "Drawer menu item navigating to the Account screen"}, + "drawerSettingsMenuItem": "Settings", + "@drawerSettingsMenuItem": {"description": "Drawer menu item navigating to the Settings screen"}, + "drawerAboutMenuItem": "About", + "@drawerAboutMenuItem": {"description": "Drawer menu item navigating to the About screen"}, + + "navOrderBook": "Order Book", + "@navOrderBook": {"description": "Bottom navigation label for the Order Book tab"}, + "navMyTrades": "My Trades", + "@navMyTrades": {"description": "Bottom navigation label for the My Trades tab"}, + "navChat": "Chat", + "@navChat": {"description": "Bottom navigation label for the Chat tab"} } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a04c70a2..4c315c26 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,6 +1,7 @@ { "@@locale": "es", - "@@last_modified": "2026-03-29", + "@@last_modified": "2026-03-31", + "appName": "Mostro", "loading": "Cargando…", "error": "Error", @@ -8,10 +9,12 @@ "confirm": "Confirmar", "done": "Listo", "skip": "Omitir", + "chatTimestampYesterday": "Ayer", + "disputesEmptyState": "Tus disputas aparecerán aquí", "disputeAttachFile": "Adjuntar archivo", - "disputeWriteMessageHint": "Escribe un mensaje\u2026", + "disputeWriteMessageHint": "Escribe un mensaje…", "disputeSend": "Enviar", "orderDispute": "Disputa de orden", "disputeAdminAssigned": "Se ha asignado un administrador a tu disputa. Se pondrá en contacto contigo aquí en breve.", @@ -32,5 +35,159 @@ "disputeInProgress": "En progreso", "disputeStatusClosed": "Cerrado", "disputeLostFundsToBuyer": "El administrador resolvió la disputa a favor del comprador. Los sats fueron liberados al comprador.", - "disputeLostFundsToSeller": "El administrador canceló la orden y devolvió los sats al vendedor. No recibiste los sats." + "disputeLostFundsToSeller": "El administrador canceló la orden y devolvió los sats al vendedor. No recibiste los sats.", + + "walkthroughSlideOneTitle": "Intercambia Bitcoin libremente — sin KYC", + "walkthroughSlideOneBody": "Mostro es un exchange peer-to-peer que te permite intercambiar Bitcoin por cualquier moneda y método de pago — sin KYC y sin necesidad de dar tus datos a nadie. Está construido sobre Nostr, lo que lo hace resistente a la censura. Nadie puede impedirte operar.", + "walkthroughSlideTwoTitle": "Privacidad por defecto", + "walkthroughSlideTwoBody": "Mostro genera una nueva identidad en cada intercambio, de modo que tus operaciones no pueden vincularse. También puedes decidir cuánta privacidad quieres:\n• Modo reputación – Permite que otros vean tus operaciones exitosas y tu nivel de confianza.\n• Modo privacidad total – No se construye reputación, pero tu actividad es completamente anónima.\nCambia de modo en cualquier momento desde la pantalla de Cuenta, donde también debes guardar tus palabras secretas — son la única forma de recuperar tu cuenta.", + "walkthroughSlideThreeTitle": "Seguridad en cada paso", + "walkthroughSlideThreeBody": "Mostro usa Hold Invoices (facturas retenidas): los sats permanecen en la billetera del vendedor hasta el final del intercambio. Esto protege a ambas partes. La aplicación también está diseñada para ser intuitiva y fácil para todo tipo de usuarios.", + "walkthroughSlideFourTitle": "Chat totalmente cifrado", + "walkthroughSlideFourBody": "Cada operación tiene su propio chat privado, cifrado de extremo a extremo. Solo los dos usuarios involucrados pueden leerlo. En caso de disputa, puedes compartir la clave con un administrador para ayudar a resolver el problema.", + "walkthroughSlideFiveTitle": "Toma una oferta", + "walkthroughSlideFiveBody": "Explora el libro de órdenes, elige una oferta que te convenga y sigue el flujo de la operación paso a paso. Podrás revisar el perfil del otro usuario, chatear de forma segura y completar la operación con facilidad.", + "walkthroughSlideSixTitle": "¿No encuentras lo que necesitas?", + "walkthroughSlideSixBody": "También puedes crear tu propia oferta y esperar a que alguien la tome. Establece el monto y el método de pago preferido — Mostro se encarga del resto.", + + "tabBuyBtc": "COMPRAR BTC", + "tabSellBtc": "VENDER BTC", + "filterButtonLabel": "FILTRAR", + "offersCount": "{count, plural, =1{1 oferta} other{{count} ofertas}}", + "noOrdersAvailable": "No hay órdenes disponibles", + "justNow": "Ahora mismo", + "minutesAgo": "Hace {m}m", + "hoursAgo": "Hace {h}h", + "daysAgo": "Hace {d}d", + + "creatingNewOrderTitle": "CREANDO NUEVA ORDEN", + "youWantToBuyBitcoin": "Quieres comprar Bitcoin", + "youWantToSellBitcoin": "Quieres vender Bitcoin", + "rangeOrderLabel": "Orden por rango", + "payLightningInvoiceTitle": "Pagar Factura Lightning", + "invoiceCopied": "Factura copiada", + "addInvoiceTitle": "Agregar Factura", + "submitButtonLabel": "Enviar", + "orderAlreadyTaken": "La orden ya fue tomada", + "orderIdCopied": "ID de orden copiado", + + "orderDetailsTitle": "DETALLES DE LA ORDEN", + "timeRemainingLabel": "Tiempo restante:", + "fiatSentButtonLabel": "FIAT ENVIADO", + "disputeButtonLabel": "DISPUTAR", + "contactButtonLabel": "CONTACTAR", + "rateButtonLabel": "VALORAR", + "viewDisputeButtonLabel": "VER DISPUTA", + "comingSoonMessage": "Próximamente", + "tradeStatusActive": "Activo", + "tradeStatusFiatSent": "Fiat enviado", + "tradeStatusCompleted": "Completado", + "tradeStatusCancelled": "Cancelado", + "tradeStatusDisputed": "En disputa", + "releaseButtonLabel": "LIBERAR", + + "accountScreenTitle": "Cuenta", + "secretWordsTitle": "Palabras secretas", + "toRestoreYourAccount": "Para restaurar tu cuenta", + "privacyCardTitle": "Privacidad", + "controlPrivacySettings": "Controla tu configuración de privacidad", + "reputationMode": "Modo Reputación", + "reputationModeSubtitle": "Privacidad estándar con reputación", + "fullPrivacyMode": "Modo Privacidad Total", + "fullPrivacyModeSubtitle": "Anonimato máximo", + "generateNewUserButton": "Generar nuevo usuario", + "importMostroUserButton": "Importar usuario de Mostro", + "generateNewUserDialogTitle": "¿Generar nuevo usuario?", + "generateNewUserDialogContent": "Esto creará una identidad completamente nueva. Tus palabras secretas actuales dejarán de funcionar — asegúrate de tenerlas respaldadas antes de continuar.", + "continueButtonLabel": "Continuar", + "importMnemonicDialogTitle": "Importar Mnemónico", + "importMnemonicHintText": "Ingresa tu frase de 12 o 24 palabras…", + "importButtonLabel": "Importar", + "refreshUserDialogTitle": "¿Actualizar usuario?", + "refreshUserDialogContent": "Esto volverá a obtener tus operaciones y órdenes desde la instancia de Mostro. Úsalo si crees que tus datos están desincronizados o faltan órdenes.", + "hideButtonLabel": "Ocultar", + "showButtonLabel": "Mostrar", + + "settingsScreenTitle": "Configuración", + "languageSettingTitle": "Idioma", + "appearanceSettingTitle": "Apariencia", + "appearanceDialogTitle": "Apariencia", + "defaultFiatCurrencyTitle": "Moneda fiat predeterminada", + "allCurrencies": "Todas las monedas", + "lightningAddressSettingTitle": "Dirección Lightning", + "tapToSetSubtitle": "Toca para configurar", + "nwcWalletSettingTitle": "Billetera NWC", + "nwcConnectPrompt": "Conecta tu billetera Lightning mediante NWC", + "relaysSettingTitle": "Relays", + "manageRelayConnections": "Administrar conexiones de relay", + "pushNotificationsSettingTitle": "Notificaciones push", + "manageNotificationPreferences": "Administrar preferencias de notificaciones", + "logReportSettingTitle": "Informe de registros", + "viewDiagnosticLogs": "Ver registros de diagnóstico", + "mostroNodeSettingTitle": "Nodo Mostro", + "themeDark": "Oscuro", + "themeLight": "Claro", + "themeSystemDefault": "Predeterminado del sistema", + "lightningAddressDialogTitle": "Dirección Lightning", + "lightningAddressHintText": "usuario@dominio.com", + "invalidLightningAddressFormat": "Debe tener el formato usuario@dominio", + "clearButtonLabel": "Limpiar", + "saveButtonLabel": "Guardar", + "connectWalletTitle": "Conectar billetera", + "scanQrCodeTitle": "Escanear código QR", + "pasteNwcUri": "Pegar URI NWC", + "selectLanguageTitle": "Seleccionar idioma", + "selectCurrencyDialogTitle": "Seleccionar moneda", + "addRelayDialogTitle": "Agregar relay", + "addButtonLabel": "Agregar", + "relayHintText": "wss://relay.example.com", + "relayErrorMustStartWithWss": "Debe comenzar con wss://", + "relayErrorUrlTooShort": "La URL es demasiado corta", + "relayErrorDuplicate": "La retransmisión ya está en la lista", + "nwcConnectedBalance": "NWC — Conectado. Saldo: {balance}", + "pasteQrCodeHeading": "Pegar contenido del código QR", + "pasteButtonLabel": "Pegar", + "clipboardEmptyError": "El portapapeles está vacío", + "enterValueError": "Por favor ingresa un valor", + "pasteOrScanQrCode": "Pegar o escanear un código QR", + "mostroNodeTitle": "Nodo Mostro", + "currentNodeLabel": "Nodo actual", + "trustedBadgeLabel": "De confianza", + "useDefaultButtonLabel": "Usar predeterminado", + "confirmButtonLabel": "Confirmar", + "invalidHexPubkey": "Debe ser una cadena hexadecimal de 64 caracteres", + + "notificationsScreenTitle": "Notificaciones", + "markAllAsReadMenuItem": "Marcar todo como leído", + "clearAllMenuItem": "Borrar todo", + "youMustBackUpYourAccount": "Debes hacer una copia de seguridad de tu cuenta", + "tapToViewAndSaveSecretWords": "Toca para ver y guardar tus palabras secretas.", + "noNotifications": "Sin notificaciones", + "markAsRead": "Marcar como leído", + "deleteNotificationLabel": "Eliminar", + + "rateScreenHeader": "VALORAR", + "successfulOrder": "Orden exitosa", + "submitRatingButton": "ENVIAR", + "closeRatingButton": "CERRAR", + + "aboutScreenTitle": "Acerca de", + "mostroTagline": "Intercambio de Bitcoin peer-to-peer sobre Nostr", + "viewDocumentationButton": "Ver documentación", + "linkCopiedToClipboard": "Enlace copiado al portapapeles", + "defaultNodeSection": "Nodo predeterminado", + "pubkeyLabel": "Clave pública", + "relaysLabel": "Relays", + "pubkeyCopiedToClipboard": "Clave pública copiada al portapapeles", + "footerTagline": "Código abierto. Sin custodia. Privado.", + + "drawerTitle": "MOSTRO", + "betaBadgeLabel": "Beta", + "drawerAccountMenuItem": "Cuenta", + "drawerSettingsMenuItem": "Configuración", + "drawerAboutMenuItem": "Acerca de", + + "navOrderBook": "Libro de órdenes", + "navMyTrades": "Mis operaciones", + "navChat": "Chat" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index aaa67e79..5b5193ab 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,6 +1,7 @@ { "@@locale": "fr", - "@@last_modified": "2026-03-29", + "@@last_modified": "2026-03-31", + "appName": "Mostro", "loading": "Chargement…", "error": "Erreur", @@ -8,10 +9,12 @@ "confirm": "Confirmer", "done": "Terminé", "skip": "Passer", + "chatTimestampYesterday": "Hier", + "disputesEmptyState": "Vos litiges apparaîtront ici", "disputeAttachFile": "Joindre un fichier", - "disputeWriteMessageHint": "Écrire un message\u2026", + "disputeWriteMessageHint": "Écrire un message…", "disputeSend": "Envoyer", "orderDispute": "Litige de commande", "disputeAdminAssigned": "Un administrateur a été assigné à votre litige. Il vous contactera ici sous peu.", @@ -32,5 +35,159 @@ "disputeInProgress": "En cours", "disputeStatusClosed": "Fermé", "disputeLostFundsToBuyer": "L'administrateur a réglé le litige en faveur de l'acheteur. Les sats ont été libérés à l'acheteur.", - "disputeLostFundsToSeller": "L'administrateur a annulé la commande et retourné les sats au vendeur. Vous n'avez pas reçu les sats." + "disputeLostFundsToSeller": "L'administrateur a annulé la commande et retourné les sats au vendeur. Vous n'avez pas reçu les sats.", + + "walkthroughSlideOneTitle": "Échangez du Bitcoin librement — sans KYC", + "walkthroughSlideOneBody": "Mostro est un exchange pair-à-pair qui vous permet d'échanger du Bitcoin contre n'importe quelle devise et méthode de paiement — sans KYC et sans avoir à communiquer vos données à qui que ce soit. Il est construit sur Nostr, ce qui le rend résistant à la censure. Personne ne peut vous empêcher de trader.", + "walkthroughSlideTwoTitle": "Confidentialité par défaut", + "walkthroughSlideTwoBody": "Mostro génère une nouvelle identité pour chaque échange, de sorte que vos transactions ne peuvent pas être liées. Vous pouvez également décider du niveau de confidentialité souhaité :\n• Mode réputation – Permet aux autres de voir vos échanges réussis et votre niveau de confiance.\n• Mode confidentialité totale – Aucune réputation n'est construite, mais votre activité est totalement anonyme.\nChangez de mode à tout moment depuis l'écran Compte, où vous devriez également sauvegarder vos mots secrets — ils sont le seul moyen de récupérer votre compte.", + "walkthroughSlideThreeTitle": "Sécurité à chaque étape", + "walkthroughSlideThreeBody": "Mostro utilise les Hold Invoices (factures retenues) : les sats restent dans le portefeuille du vendeur jusqu'à la fin de l'échange. Cela protège les deux parties. L'application est également conçue pour être intuitive et facile à utiliser pour tous les types d'utilisateurs.", + "walkthroughSlideFourTitle": "Chat entièrement chiffré", + "walkthroughSlideFourBody": "Chaque transaction dispose de son propre chat privé, chiffré de bout en bout. Seuls les deux utilisateurs impliqués peuvent le lire. En cas de litige, vous pouvez donner la clé partagée à un administrateur pour l'aider à résoudre le problème.", + "walkthroughSlideFiveTitle": "Prenez une offre", + "walkthroughSlideFiveBody": "Parcourez le carnet d'ordres, choisissez une offre qui vous convient et suivez le déroulement de la transaction étape par étape. Vous pourrez consulter le profil de l'autre utilisateur, chatter en toute sécurité et finaliser l'échange facilement.", + "walkthroughSlideSixTitle": "Vous ne trouvez pas ce qu'il vous faut ?", + "walkthroughSlideSixBody": "Vous pouvez également créer votre propre offre et attendre que quelqu'un la prenne. Définissez le montant et la méthode de paiement souhaitée — Mostro s'occupe du reste.", + + "tabBuyBtc": "ACHETER BTC", + "tabSellBtc": "VENDRE BTC", + "filterButtonLabel": "FILTRER", + "offersCount": "{count, plural, =1{1 offre} other{{count} offres}}", + "noOrdersAvailable": "Aucun ordre disponible", + "justNow": "À l'instant", + "minutesAgo": "Il y a {m}m", + "hoursAgo": "Il y a {h}h", + "daysAgo": "Il y a {d}j", + + "creatingNewOrderTitle": "CRÉATION D'UN NOUVEL ORDRE", + "youWantToBuyBitcoin": "Vous voulez acheter du Bitcoin", + "youWantToSellBitcoin": "Vous voulez vendre du Bitcoin", + "rangeOrderLabel": "Ordre à plage", + "payLightningInvoiceTitle": "Payer la facture Lightning", + "invoiceCopied": "Facture copiée", + "addInvoiceTitle": "Ajouter une facture", + "submitButtonLabel": "Soumettre", + "orderAlreadyTaken": "Cet ordre a déjà été pris", + "orderIdCopied": "ID d'ordre copié", + + "orderDetailsTitle": "DÉTAILS DE L'ORDRE", + "timeRemainingLabel": "Temps restant\u00a0:", + "fiatSentButtonLabel": "FIAT ENVOYÉ", + "disputeButtonLabel": "LITIGE", + "contactButtonLabel": "CONTACTER", + "rateButtonLabel": "NOTER", + "viewDisputeButtonLabel": "VOIR LE LITIGE", + "comingSoonMessage": "Bientôt disponible", + "tradeStatusActive": "Actif", + "tradeStatusFiatSent": "Fiat envoyé", + "tradeStatusCompleted": "Terminé", + "tradeStatusCancelled": "Annulé", + "tradeStatusDisputed": "En litige", + "releaseButtonLabel": "LIBÉRER", + + "accountScreenTitle": "Compte", + "secretWordsTitle": "Mots secrets", + "toRestoreYourAccount": "Pour restaurer votre compte", + "privacyCardTitle": "Confidentialité", + "controlPrivacySettings": "Gérez vos paramètres de confidentialité", + "reputationMode": "Mode Réputation", + "reputationModeSubtitle": "Confidentialité standard avec réputation", + "fullPrivacyMode": "Mode Confidentialité Totale", + "fullPrivacyModeSubtitle": "Anonymat maximal", + "generateNewUserButton": "Générer un nouvel utilisateur", + "importMostroUserButton": "Importer un utilisateur Mostro", + "generateNewUserDialogTitle": "Générer un nouvel utilisateur ?", + "generateNewUserDialogContent": "Cela créera une toute nouvelle identité. Vos mots secrets actuels ne fonctionneront plus — assurez-vous de les avoir sauvegardés avant de continuer.", + "continueButtonLabel": "Continuer", + "importMnemonicDialogTitle": "Importer le mnémonique", + "importMnemonicHintText": "Entrez votre phrase de 12 ou 24 mots…", + "importButtonLabel": "Importer", + "refreshUserDialogTitle": "Actualiser l'utilisateur ?", + "refreshUserDialogContent": "Cela va récupérer à nouveau vos transactions et ordres depuis l'instance Mostro. Utilisez cette option si vous pensez que vos données sont désynchronisées ou si des ordres manquent.", + "hideButtonLabel": "Masquer", + "showButtonLabel": "Afficher", + + "settingsScreenTitle": "Paramètres", + "languageSettingTitle": "Langue", + "appearanceSettingTitle": "Apparence", + "appearanceDialogTitle": "Apparence", + "defaultFiatCurrencyTitle": "Devise fiat par défaut", + "allCurrencies": "Toutes les devises", + "lightningAddressSettingTitle": "Adresse Lightning", + "tapToSetSubtitle": "Appuyez pour configurer", + "nwcWalletSettingTitle": "Portefeuille NWC", + "nwcConnectPrompt": "Connectez votre portefeuille Lightning via NWC", + "relaysSettingTitle": "Relais", + "manageRelayConnections": "Gérer les connexions de relais", + "pushNotificationsSettingTitle": "Notifications push", + "manageNotificationPreferences": "Gérer les préférences de notifications", + "logReportSettingTitle": "Rapport de logs", + "viewDiagnosticLogs": "Voir les logs de diagnostic", + "mostroNodeSettingTitle": "Nœud Mostro", + "themeDark": "Sombre", + "themeLight": "Clair", + "themeSystemDefault": "Par défaut du système", + "lightningAddressDialogTitle": "Adresse Lightning", + "lightningAddressHintText": "utilisateur@domaine.com", + "invalidLightningAddressFormat": "Doit être au format utilisateur@domaine", + "clearButtonLabel": "Effacer", + "saveButtonLabel": "Enregistrer", + "connectWalletTitle": "Connecter le portefeuille", + "scanQrCodeTitle": "Scanner le code QR", + "pasteNwcUri": "Coller l'URI NWC", + "selectLanguageTitle": "Sélectionner la langue", + "selectCurrencyDialogTitle": "Sélectionner la devise", + "addRelayDialogTitle": "Ajouter un relais", + "addButtonLabel": "Ajouter", + "relayHintText": "wss://relay.example.com", + "relayErrorMustStartWithWss": "Doit commencer par wss://", + "relayErrorUrlTooShort": "L'URL est trop courte", + "relayErrorDuplicate": "Le relais est déjà dans la liste", + "nwcConnectedBalance": "NWC — Connecté. Solde : {balance}", + "pasteQrCodeHeading": "Coller le contenu du QR code", + "pasteButtonLabel": "Coller", + "clipboardEmptyError": "Le presse-papiers est vide", + "enterValueError": "Veuillez entrer une valeur", + "pasteOrScanQrCode": "Coller ou scanner un QR code", + "mostroNodeTitle": "Nœud Mostro", + "currentNodeLabel": "Nœud actuel", + "trustedBadgeLabel": "De confiance", + "useDefaultButtonLabel": "Utiliser le défaut", + "confirmButtonLabel": "Confirmer", + "invalidHexPubkey": "Doit être une chaîne hexadécimale de 64 caractères", + + "notificationsScreenTitle": "Notifications", + "markAllAsReadMenuItem": "Tout marquer comme lu", + "clearAllMenuItem": "Tout effacer", + "youMustBackUpYourAccount": "Vous devez sauvegarder votre compte", + "tapToViewAndSaveSecretWords": "Appuyez pour afficher et sauvegarder vos mots secrets.", + "noNotifications": "Aucune notification", + "markAsRead": "Marquer comme lu", + "deleteNotificationLabel": "Supprimer", + + "rateScreenHeader": "NOTER", + "successfulOrder": "Ordre réussi", + "submitRatingButton": "SOUMETTRE", + "closeRatingButton": "FERMER", + + "aboutScreenTitle": "À propos", + "mostroTagline": "Trading Bitcoin pair-à-pair sur Nostr", + "viewDocumentationButton": "Voir la documentation", + "linkCopiedToClipboard": "Lien copié dans le presse-papiers", + "defaultNodeSection": "Nœud par défaut", + "pubkeyLabel": "Clé publique", + "relaysLabel": "Relais", + "pubkeyCopiedToClipboard": "Clé publique copiée dans le presse-papiers", + "footerTagline": "Open-source. Non-custodial. Privé.", + + "drawerTitle": "MOSTRO", + "betaBadgeLabel": "Bêta", + "drawerAccountMenuItem": "Compte", + "drawerSettingsMenuItem": "Paramètres", + "drawerAboutMenuItem": "À propos", + + "navOrderBook": "Carnet d'ordres", + "navMyTrades": "Mes transactions", + "navChat": "Chat" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 318a2742..fbddc2cb 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,6 +1,7 @@ { "@@locale": "it", - "@@last_modified": "2026-03-29", + "@@last_modified": "2026-03-31", + "appName": "Mostro", "loading": "Caricamento…", "error": "Errore", @@ -8,10 +9,12 @@ "confirm": "Conferma", "done": "Fine", "skip": "Salta", + "chatTimestampYesterday": "Ieri", + "disputesEmptyState": "Le tue controversie appariranno qui", "disputeAttachFile": "Allega file", - "disputeWriteMessageHint": "Scrivi un messaggio\u2026", + "disputeWriteMessageHint": "Scrivi un messaggio…", "disputeSend": "Invia", "orderDispute": "Disputa ordine", "disputeAdminAssigned": "Un amministratore è stato assegnato alla tua disputa. Ti contatterà qui a breve.", @@ -32,5 +35,159 @@ "disputeInProgress": "In corso", "disputeStatusClosed": "Chiuso", "disputeLostFundsToBuyer": "L'amministratore ha risolto la controversia a favore dell'acquirente. I sats sono stati rilasciati all'acquirente.", - "disputeLostFundsToSeller": "L'amministratore ha annullato l'ordine e restituito i sats al venditore. Non hai ricevuto i sats." + "disputeLostFundsToSeller": "L'amministratore ha annullato l'ordine e restituito i sats al venditore. Non hai ricevuto i sats.", + + "walkthroughSlideOneTitle": "Scambia Bitcoin liberamente — senza KYC", + "walkthroughSlideOneBody": "Mostro è un exchange peer-to-peer che ti consente di scambiare Bitcoin con qualsiasi valuta e metodo di pagamento — senza KYC e senza dover fornire i tuoi dati a nessuno. È costruito su Nostr, il che lo rende resistente alla censura. Nessuno può impedirti di fare trading.", + "walkthroughSlideTwoTitle": "Privacy per impostazione predefinita", + "walkthroughSlideTwoBody": "Mostro genera una nuova identità per ogni scambio, in modo che le tue operazioni non possano essere collegate. Puoi anche decidere quanto vuoi essere privato:\n• Modalità reputazione – Consente agli altri di vedere le tue operazioni riuscite e il tuo livello di fiducia.\n• Modalità privacy totale – Non viene costruita alcuna reputazione, ma la tua attività è completamente anonima.\nCambia modalità in qualsiasi momento dalla schermata Account, dove dovresti anche salvare le tue parole segrete — sono l'unico modo per recuperare il tuo account.", + "walkthroughSlideThreeTitle": "Sicurezza ad ogni passo", + "walkthroughSlideThreeBody": "Mostro utilizza Hold Invoice (fatture trattenute): i sats rimangono nel portafoglio del venditore fino alla fine dello scambio. Questo protegge entrambe le parti. L'app è anche progettata per essere intuitiva e facile da usare per ogni tipo di utente.", + "walkthroughSlideFourTitle": "Chat completamente cifrata", + "walkthroughSlideFourBody": "Ogni operazione ha la propria chat privata, cifrata end-to-end. Solo i due utenti coinvolti possono leggerla. In caso di disputa, puoi fornire la chiave condivisa a un amministratore per aiutare a risolvere il problema.", + "walkthroughSlideFiveTitle": "Prendi un'offerta", + "walkthroughSlideFiveBody": "Sfoglia il book degli ordini, scegli un'offerta adatta a te e segui il flusso dell'operazione passo dopo passo. Potrai controllare il profilo dell'altro utente, chattare in sicurezza e completare l'operazione con facilità.", + "walkthroughSlideSixTitle": "Non trovi quello che cerchi?", + "walkthroughSlideSixBody": "Puoi anche creare la tua offerta e aspettare che qualcuno la accetti. Imposta l'importo e il metodo di pagamento preferito — Mostro pensa al resto.", + + "tabBuyBtc": "COMPRA BTC", + "tabSellBtc": "VENDI BTC", + "filterButtonLabel": "FILTRA", + "offersCount": "{count, plural, =1{1 offerta} other{{count} offerte}}", + "noOrdersAvailable": "Nessun ordine disponibile", + "justNow": "Proprio ora", + "minutesAgo": "{m}m fa", + "hoursAgo": "{h}h fa", + "daysAgo": "{d}g fa", + + "creatingNewOrderTitle": "CREAZIONE NUOVO ORDINE", + "youWantToBuyBitcoin": "Vuoi acquistare Bitcoin", + "youWantToSellBitcoin": "Vuoi vendere Bitcoin", + "rangeOrderLabel": "Ordine a intervallo", + "payLightningInvoiceTitle": "Paga Fattura Lightning", + "invoiceCopied": "Fattura copiata", + "addInvoiceTitle": "Aggiungi Fattura", + "submitButtonLabel": "Invia", + "orderAlreadyTaken": "L'ordine è già stato preso", + "orderIdCopied": "ID ordine copiato", + + "orderDetailsTitle": "DETTAGLI ORDINE", + "timeRemainingLabel": "Tempo rimanente:", + "fiatSentButtonLabel": "FIAT INVIATO", + "disputeButtonLabel": "DISPUTA", + "contactButtonLabel": "CONTATTA", + "rateButtonLabel": "VALUTA", + "viewDisputeButtonLabel": "VEDI DISPUTA", + "comingSoonMessage": "Prossimamente", + "tradeStatusActive": "Attivo", + "tradeStatusFiatSent": "Fiat inviato", + "tradeStatusCompleted": "Completato", + "tradeStatusCancelled": "Annullato", + "tradeStatusDisputed": "In disputa", + "releaseButtonLabel": "RILASCIA", + + "accountScreenTitle": "Account", + "secretWordsTitle": "Parole segrete", + "toRestoreYourAccount": "Per ripristinare il tuo account", + "privacyCardTitle": "Privacy", + "controlPrivacySettings": "Gestisci le impostazioni sulla privacy", + "reputationMode": "Modalità Reputazione", + "reputationModeSubtitle": "Privacy standard con reputazione", + "fullPrivacyMode": "Modalità Privacy Totale", + "fullPrivacyModeSubtitle": "Anonimato massimo", + "generateNewUserButton": "Genera nuovo utente", + "importMostroUserButton": "Importa utente Mostro", + "generateNewUserDialogTitle": "Generare nuovo utente?", + "generateNewUserDialogContent": "Verrà creata una nuova identità. Le tue parole segrete attuali non funzioneranno più — assicurati di averle salvate prima di continuare.", + "continueButtonLabel": "Continua", + "importMnemonicDialogTitle": "Importa Mnemonica", + "importMnemonicHintText": "Inserisci la tua frase da 12 o 24 parole…", + "importButtonLabel": "Importa", + "refreshUserDialogTitle": "Aggiornare utente?", + "refreshUserDialogContent": "Verranno recuperate le tue operazioni e gli ordini dall'istanza Mostro. Usalo se pensi che i tuoi dati non siano sincronizzati o manchino degli ordini.", + "hideButtonLabel": "Nascondi", + "showButtonLabel": "Mostra", + + "settingsScreenTitle": "Impostazioni", + "languageSettingTitle": "Lingua", + "appearanceSettingTitle": "Aspetto", + "appearanceDialogTitle": "Aspetto", + "defaultFiatCurrencyTitle": "Valuta fiat predefinita", + "allCurrencies": "Tutte le valute", + "lightningAddressSettingTitle": "Indirizzo Lightning", + "tapToSetSubtitle": "Tocca per impostare", + "nwcWalletSettingTitle": "Portafoglio NWC", + "nwcConnectPrompt": "Collega il tuo portafoglio Lightning tramite NWC", + "relaysSettingTitle": "Relay", + "manageRelayConnections": "Gestisci connessioni relay", + "pushNotificationsSettingTitle": "Notifiche push", + "manageNotificationPreferences": "Gestisci preferenze notifiche", + "logReportSettingTitle": "Registro diagnostico", + "viewDiagnosticLogs": "Visualizza log diagnostici", + "mostroNodeSettingTitle": "Nodo Mostro", + "themeDark": "Scuro", + "themeLight": "Chiaro", + "themeSystemDefault": "Predefinito di sistema", + "lightningAddressDialogTitle": "Indirizzo Lightning", + "lightningAddressHintText": "utente@dominio.com", + "invalidLightningAddressFormat": "Deve essere nel formato utente@dominio", + "clearButtonLabel": "Cancella", + "saveButtonLabel": "Salva", + "connectWalletTitle": "Collega portafoglio", + "scanQrCodeTitle": "Scansiona codice QR", + "pasteNwcUri": "Incolla URI NWC", + "selectLanguageTitle": "Seleziona lingua", + "selectCurrencyDialogTitle": "Seleziona valuta", + "addRelayDialogTitle": "Aggiungi relay", + "addButtonLabel": "Aggiungi", + "relayHintText": "wss://relay.example.com", + "relayErrorMustStartWithWss": "Deve iniziare con wss://", + "relayErrorUrlTooShort": "L'URL è troppo corta", + "relayErrorDuplicate": "Relay già presente nella lista", + "nwcConnectedBalance": "NWC — Connesso. Saldo: {balance}", + "pasteQrCodeHeading": "Incolla contenuto del codice QR", + "pasteButtonLabel": "Incolla", + "clipboardEmptyError": "Gli appunti sono vuoti", + "enterValueError": "Inserisci un valore", + "pasteOrScanQrCode": "Incolla o scansiona un codice QR", + "mostroNodeTitle": "Nodo Mostro", + "currentNodeLabel": "Nodo attuale", + "trustedBadgeLabel": "Affidabile", + "useDefaultButtonLabel": "Usa predefinito", + "confirmButtonLabel": "Conferma", + "invalidHexPubkey": "Deve essere una stringa esadecimale di 64 caratteri", + + "notificationsScreenTitle": "Notifiche", + "markAllAsReadMenuItem": "Segna tutto come letto", + "clearAllMenuItem": "Cancella tutto", + "youMustBackUpYourAccount": "Devi eseguire il backup del tuo account", + "tapToViewAndSaveSecretWords": "Tocca per visualizzare e salvare le tue parole segrete.", + "noNotifications": "Nessuna notifica", + "markAsRead": "Segna come letto", + "deleteNotificationLabel": "Elimina", + + "rateScreenHeader": "VALUTA", + "successfulOrder": "Ordine riuscito", + "submitRatingButton": "INVIA", + "closeRatingButton": "CHIUDI", + + "aboutScreenTitle": "Informazioni", + "mostroTagline": "Trading Bitcoin peer-to-peer su Nostr", + "viewDocumentationButton": "Visualizza documentazione", + "linkCopiedToClipboard": "Link copiato negli appunti", + "defaultNodeSection": "Nodo predefinito", + "pubkeyLabel": "Chiave pubblica", + "relaysLabel": "Relay", + "pubkeyCopiedToClipboard": "Chiave pubblica copiata negli appunti", + "footerTagline": "Open-source. Non custodiale. Privato.", + + "drawerTitle": "MOSTRO", + "betaBadgeLabel": "Beta", + "drawerAccountMenuItem": "Account", + "drawerSettingsMenuItem": "Impostazioni", + "drawerAboutMenuItem": "Informazioni", + + "navOrderBook": "Book ordini", + "navMyTrades": "Le mie operazioni", + "navChat": "Chat" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index dbc8b8f3..2791af97 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -295,6 +295,864 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The administrator canceled the order and returned the sats to the seller. You did not receive the sats.'** String get disputeLostFundsToSeller; + + /// Title for walkthrough slide 1 + /// + /// In en, this message translates to: + /// **'Trade Bitcoin freely — no KYC'** + String get walkthroughSlideOneTitle; + + /// Body text for walkthrough slide 1 + /// + /// In en, this message translates to: + /// **'Mostro is a peer-to-peer exchange that lets you trade Bitcoin for any currency and payment method — no KYC, and no need to give your data to anyone. It\'s built on Nostr, which makes it censorship-resistant. No one can stop you from trading.'** + String get walkthroughSlideOneBody; + + /// Title for walkthrough slide 2 + /// + /// In en, this message translates to: + /// **'Privacy by default'** + String get walkthroughSlideTwoTitle; + + /// Body text for walkthrough slide 2 + /// + /// In en, this message translates to: + /// **'Mostro generates a new identity for every exchange, so your trades can\'t be linked. You can also decide how private you want to be:\n• Reputation mode – Lets others see your successful trades and trust level.\n• Full privacy mode – No reputation is built, but your activity is completely anonymous.\nSwitch modes anytime from the Account screen, where you should also save your secret words — they\'re the only way to recover your account.'** + String get walkthroughSlideTwoBody; + + /// Title for walkthrough slide 3 + /// + /// In en, this message translates to: + /// **'Security at every step'** + String get walkthroughSlideThreeTitle; + + /// Body text for walkthrough slide 3 + /// + /// In en, this message translates to: + /// **'Mostro uses Hold Invoices: sats stay in the seller\'s wallet until the end of the trade. This protects both sides. The app is also designed to be intuitive and easy for all kinds of users.'** + String get walkthroughSlideThreeBody; + + /// Title for walkthrough slide 4 + /// + /// In en, this message translates to: + /// **'Fully encrypted chat'** + String get walkthroughSlideFourTitle; + + /// Body text for walkthrough slide 4 + /// + /// In en, this message translates to: + /// **'Each trade has its own private chat, end-to-end encrypted. Only the two users involved can read it. In case of a dispute, you can give the shared key to an admin to help resolve the issue.'** + String get walkthroughSlideFourBody; + + /// Title for walkthrough slide 5 + /// + /// In en, this message translates to: + /// **'Take an offer'** + String get walkthroughSlideFiveTitle; + + /// Body text for walkthrough slide 5 + /// + /// In en, this message translates to: + /// **'Browse the order book, choose an offer that works for you, and follow the trade flow step by step. You\'ll be able to check the other user\'s profile, chat securely, and complete the trade with ease.'** + String get walkthroughSlideFiveBody; + + /// Title for walkthrough slide 6 + /// + /// In en, this message translates to: + /// **'Can\'t find what you need?'** + String get walkthroughSlideSixTitle; + + /// Body text for walkthrough slide 6 + /// + /// In en, this message translates to: + /// **'You can also create your own offer and wait for someone to take it. Set the amount and preferred payment method — Mostro handles the rest.'** + String get walkthroughSlideSixBody; + + /// Tab label for the buy Bitcoin order book + /// + /// In en, this message translates to: + /// **'BUY BTC'** + String get tabBuyBtc; + + /// Tab label for the sell Bitcoin order book + /// + /// In en, this message translates to: + /// **'SELL BTC'** + String get tabSellBtc; + + /// Button label to open order book filter options + /// + /// In en, this message translates to: + /// **'FILTER'** + String get filterButtonLabel; + + /// Number of offers shown in the order book + /// + /// In en, this message translates to: + /// **'{count, plural, =1{1 offer} other{{count} offers}}'** + String offersCount(int count); + + /// Empty state message when the order book has no orders + /// + /// In en, this message translates to: + /// **'No orders available'** + String get noOrdersAvailable; + + /// Timestamp label for a very recent event + /// + /// In en, this message translates to: + /// **'Just now'** + String get justNow; + + /// Relative timestamp in minutes + /// + /// In en, this message translates to: + /// **'{m}m ago'** + String minutesAgo(int m); + + /// Relative timestamp in hours + /// + /// In en, this message translates to: + /// **'{h}h ago'** + String hoursAgo(int h); + + /// Relative timestamp in days + /// + /// In en, this message translates to: + /// **'{d}d ago'** + String daysAgo(int d); + + /// Screen title when the user is creating a new order + /// + /// In en, this message translates to: + /// **'CREATING NEW ORDER'** + String get creatingNewOrderTitle; + + /// Label shown when the order type is buy + /// + /// In en, this message translates to: + /// **'You want to buy Bitcoin'** + String get youWantToBuyBitcoin; + + /// Label shown when the order type is sell + /// + /// In en, this message translates to: + /// **'You want to sell Bitcoin'** + String get youWantToSellBitcoin; + + /// Label for a range-amount order toggle + /// + /// In en, this message translates to: + /// **'Range order'** + String get rangeOrderLabel; + + /// Screen title for the pay Lightning invoice step + /// + /// In en, this message translates to: + /// **'Pay Lightning Invoice'** + String get payLightningInvoiceTitle; + + /// Snackbar shown after copying a Lightning invoice to clipboard + /// + /// In en, this message translates to: + /// **'Invoice copied'** + String get invoiceCopied; + + /// Screen title for adding a Lightning invoice + /// + /// In en, this message translates to: + /// **'Add Invoice'** + String get addInvoiceTitle; + + /// Generic submit button label + /// + /// In en, this message translates to: + /// **'Submit'** + String get submitButtonLabel; + + /// Error message when attempting to take an already-taken order + /// + /// In en, this message translates to: + /// **'Order has already been taken'** + String get orderAlreadyTaken; + + /// Snackbar shown after copying an order ID to clipboard + /// + /// In en, this message translates to: + /// **'Order ID copied'** + String get orderIdCopied; + + /// Screen title for the order/trade details screen + /// + /// In en, this message translates to: + /// **'ORDER DETAILS'** + String get orderDetailsTitle; + + /// Label preceding the countdown timer in a trade + /// + /// In en, this message translates to: + /// **'Time remaining:'** + String get timeRemainingLabel; + + /// Button label for the buyer to confirm fiat was sent + /// + /// In en, this message translates to: + /// **'FIAT SENT'** + String get fiatSentButtonLabel; + + /// Button label to open a dispute for a trade + /// + /// In en, this message translates to: + /// **'DISPUTE'** + String get disputeButtonLabel; + + /// Button label to open the trade chat + /// + /// In en, this message translates to: + /// **'CONTACT'** + String get contactButtonLabel; + + /// Button label to rate the trading counterpart + /// + /// In en, this message translates to: + /// **'RATE'** + String get rateButtonLabel; + + /// Button label to view an active dispute + /// + /// In en, this message translates to: + /// **'VIEW DISPUTE'** + String get viewDisputeButtonLabel; + + /// Generic coming-soon placeholder message + /// + /// In en, this message translates to: + /// **'Coming soon'** + String get comingSoonMessage; + + /// Trade status chip label: active + /// + /// In en, this message translates to: + /// **'Active'** + String get tradeStatusActive; + + /// Trade status chip label: fiat sent + /// + /// In en, this message translates to: + /// **'Fiat Sent'** + String get tradeStatusFiatSent; + + /// Trade status chip label: completed + /// + /// In en, this message translates to: + /// **'Completed'** + String get tradeStatusCompleted; + + /// Trade status chip label: cancelled + /// + /// In en, this message translates to: + /// **'Cancelled'** + String get tradeStatusCancelled; + + /// Trade status chip label: disputed + /// + /// In en, this message translates to: + /// **'Disputed'** + String get tradeStatusDisputed; + + /// Button label for the seller to release sats + /// + /// In en, this message translates to: + /// **'RELEASE'** + String get releaseButtonLabel; + + /// Screen title for the Account screen + /// + /// In en, this message translates to: + /// **'Account'** + String get accountScreenTitle; + + /// Section title for the mnemonic backup card + /// + /// In en, this message translates to: + /// **'Secret Words'** + String get secretWordsTitle; + + /// Subtitle under the secret words section heading + /// + /// In en, this message translates to: + /// **'To restore your account'** + String get toRestoreYourAccount; + + /// Section title for the privacy settings card + /// + /// In en, this message translates to: + /// **'Privacy'** + String get privacyCardTitle; + + /// Subtitle under the privacy section heading + /// + /// In en, this message translates to: + /// **'Control your privacy settings'** + String get controlPrivacySettings; + + /// Label for reputation privacy mode option + /// + /// In en, this message translates to: + /// **'Reputation Mode'** + String get reputationMode; + + /// Subtitle for reputation mode option + /// + /// In en, this message translates to: + /// **'Standard privacy with reputation'** + String get reputationModeSubtitle; + + /// Label for full privacy mode option + /// + /// In en, this message translates to: + /// **'Full Privacy Mode'** + String get fullPrivacyMode; + + /// Subtitle for full privacy mode option + /// + /// In en, this message translates to: + /// **'Maximum anonymity'** + String get fullPrivacyModeSubtitle; + + /// Button label to generate a new Mostro identity + /// + /// In en, this message translates to: + /// **'Generate New User'** + String get generateNewUserButton; + + /// Button label to import an existing Mostro identity via mnemonic + /// + /// In en, this message translates to: + /// **'Import Mostro User'** + String get importMostroUserButton; + + /// Confirmation dialog title for generating a new user + /// + /// In en, this message translates to: + /// **'Generate New User?'** + String get generateNewUserDialogTitle; + + /// Confirmation dialog body for generating a new user + /// + /// In en, this message translates to: + /// **'This will create a brand-new identity. Your current secret words will no longer work — make sure they are backed up before continuing.'** + String get generateNewUserDialogContent; + + /// Continue button label + /// + /// In en, this message translates to: + /// **'Continue'** + String get continueButtonLabel; + + /// Dialog title for importing a mnemonic phrase + /// + /// In en, this message translates to: + /// **'Import Mnemonic'** + String get importMnemonicDialogTitle; + + /// Hint text in the mnemonic import text field + /// + /// In en, this message translates to: + /// **'Enter your 12 or 24 word phrase…'** + String get importMnemonicHintText; + + /// Button label to confirm mnemonic import + /// + /// In en, this message translates to: + /// **'Import'** + String get importButtonLabel; + + /// Dialog title for refreshing user data + /// + /// In en, this message translates to: + /// **'Refresh User?'** + String get refreshUserDialogTitle; + + /// Dialog body for refreshing user data + /// + /// In en, this message translates to: + /// **'This will re-fetch your trades and orders from the Mostro instance. Use this if you think your data is out of sync or orders are missing.'** + String get refreshUserDialogContent; + + /// Button label to hide sensitive information + /// + /// In en, this message translates to: + /// **'Hide'** + String get hideButtonLabel; + + /// Button label to reveal sensitive information + /// + /// In en, this message translates to: + /// **'Show'** + String get showButtonLabel; + + /// Screen title for the Settings screen + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsScreenTitle; + + /// Settings list item title for language selection + /// + /// In en, this message translates to: + /// **'Language'** + String get languageSettingTitle; + + /// Settings list item title for appearance/theme + /// + /// In en, this message translates to: + /// **'Appearance'** + String get appearanceSettingTitle; + + /// Dialog title for the appearance/theme picker + /// + /// In en, this message translates to: + /// **'Appearance'** + String get appearanceDialogTitle; + + /// Settings list item title for default fiat currency + /// + /// In en, this message translates to: + /// **'Default Fiat Currency'** + String get defaultFiatCurrencyTitle; + + /// Option label meaning no currency filter is applied + /// + /// In en, this message translates to: + /// **'All currencies'** + String get allCurrencies; + + /// Settings list item title for the user's Lightning address + /// + /// In en, this message translates to: + /// **'Lightning Address'** + String get lightningAddressSettingTitle; + + /// Subtitle shown when a settings value is not yet configured + /// + /// In en, this message translates to: + /// **'Tap to set'** + String get tapToSetSubtitle; + + /// Settings list item title for NWC wallet connection + /// + /// In en, this message translates to: + /// **'NWC Wallet'** + String get nwcWalletSettingTitle; + + /// Subtitle prompting the user to connect a wallet via Nostr Wallet Connect + /// + /// In en, this message translates to: + /// **'Connect your Lightning wallet via NWC'** + String get nwcConnectPrompt; + + /// Settings list item title for Nostr relay management + /// + /// In en, this message translates to: + /// **'Relays'** + String get relaysSettingTitle; + + /// Subtitle for the relays settings entry + /// + /// In en, this message translates to: + /// **'Manage relay connections'** + String get manageRelayConnections; + + /// Settings list item title for push notification preferences + /// + /// In en, this message translates to: + /// **'Push Notifications'** + String get pushNotificationsSettingTitle; + + /// Subtitle for the push notifications settings entry + /// + /// In en, this message translates to: + /// **'Manage notification preferences'** + String get manageNotificationPreferences; + + /// Settings list item title for viewing diagnostic logs + /// + /// In en, this message translates to: + /// **'Log Report'** + String get logReportSettingTitle; + + /// Subtitle for the log report settings entry + /// + /// In en, this message translates to: + /// **'View diagnostic logs'** + String get viewDiagnosticLogs; + + /// Settings list item title for the Mostro node configuration + /// + /// In en, this message translates to: + /// **'Mostro Node'** + String get mostroNodeSettingTitle; + + /// Theme option: dark mode + /// + /// In en, this message translates to: + /// **'Dark'** + String get themeDark; + + /// Theme option: light mode + /// + /// In en, this message translates to: + /// **'Light'** + String get themeLight; + + /// Theme option: follow system setting + /// + /// In en, this message translates to: + /// **'System default'** + String get themeSystemDefault; + + /// Dialog title for editing the Lightning address + /// + /// In en, this message translates to: + /// **'Lightning Address'** + String get lightningAddressDialogTitle; + + /// Placeholder text in the Lightning address input field + /// + /// In en, this message translates to: + /// **'user@domain.com'** + String get lightningAddressHintText; + + /// Validation error for an invalid Lightning address format + /// + /// In en, this message translates to: + /// **'Must be in user@domain format'** + String get invalidLightningAddressFormat; + + /// Button label to clear a field or value + /// + /// In en, this message translates to: + /// **'Clear'** + String get clearButtonLabel; + + /// Button label to save a settings value + /// + /// In en, this message translates to: + /// **'Save'** + String get saveButtonLabel; + + /// Screen or dialog title for the NWC wallet connection flow + /// + /// In en, this message translates to: + /// **'Connect Wallet'** + String get connectWalletTitle; + + /// Screen title for the QR code scanner + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get scanQrCodeTitle; + + /// Hint text for the NWC URI input field / QR scanner fallback + /// + /// In en, this message translates to: + /// **'Paste NWC URI'** + String get pasteNwcUri; + + /// Dialog or screen title for the language picker + /// + /// In en, this message translates to: + /// **'Select Language'** + String get selectLanguageTitle; + + /// Dialog title for the currency picker + /// + /// In en, this message translates to: + /// **'Select Currency'** + String get selectCurrencyDialogTitle; + + /// Dialog title for adding a new Nostr relay + /// + /// In en, this message translates to: + /// **'Add Relay'** + String get addRelayDialogTitle; + + /// Generic add action button label + /// + /// In en, this message translates to: + /// **'Add'** + String get addButtonLabel; + + /// Placeholder hint in the add-relay URL field + /// + /// In en, this message translates to: + /// **'wss://relay.example.com'** + String get relayHintText; + + /// Validation error when relay URL does not start with wss:// + /// + /// In en, this message translates to: + /// **'Must start with wss://'** + String get relayErrorMustStartWithWss; + + /// Validation error when relay URL is too short + /// + /// In en, this message translates to: + /// **'URL is too short'** + String get relayErrorUrlTooShort; + + /// Validation error when relay URL is already added + /// + /// In en, this message translates to: + /// **'Relay already in list'** + String get relayErrorDuplicate; + + /// NWC wallet connected status with balance + /// + /// In en, this message translates to: + /// **'NWC — Connected. Balance: {balance}'** + String nwcConnectedBalance(String balance); + + /// Heading text on the web QR code paste fallback screen + /// + /// In en, this message translates to: + /// **'Paste QR Code Content'** + String get pasteQrCodeHeading; + + /// Button label for paste-from-clipboard action + /// + /// In en, this message translates to: + /// **'Paste'** + String get pasteButtonLabel; + + /// Error shown when clipboard has no text to paste + /// + /// In en, this message translates to: + /// **'Clipboard is empty'** + String get clipboardEmptyError; + + /// Validation error when QR input field is empty + /// + /// In en, this message translates to: + /// **'Please enter a value'** + String get enterValueError; + + /// Default hint text for the QR scanner widget + /// + /// In en, this message translates to: + /// **'Paste or scan a QR code'** + String get pasteOrScanQrCode; + + /// Section title on the Mostro node settings screen + /// + /// In en, this message translates to: + /// **'Mostro Node'** + String get mostroNodeTitle; + + /// Label for the currently active Mostro node + /// + /// In en, this message translates to: + /// **'Current Node'** + String get currentNodeLabel; + + /// Badge shown on a verified/trusted Mostro node + /// + /// In en, this message translates to: + /// **'Trusted'** + String get trustedBadgeLabel; + + /// Button label to reset to the default Mostro node + /// + /// In en, this message translates to: + /// **'Use Default'** + String get useDefaultButtonLabel; + + /// Button label to confirm a selection or action + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirmButtonLabel; + + /// Validation error for an invalid hex pubkey input + /// + /// In en, this message translates to: + /// **'Must be a 64-character hex string'** + String get invalidHexPubkey; + + /// Screen title for the Notifications screen + /// + /// In en, this message translates to: + /// **'Notifications'** + String get notificationsScreenTitle; + + /// Menu item to mark all notifications as read + /// + /// In en, this message translates to: + /// **'Mark all as read'** + String get markAllAsReadMenuItem; + + /// Menu item to delete all notifications + /// + /// In en, this message translates to: + /// **'Clear all'** + String get clearAllMenuItem; + + /// Notification title prompting the user to back up their account + /// + /// In en, this message translates to: + /// **'You must back up your account'** + String get youMustBackUpYourAccount; + + /// Notification body prompting the user to view and save secret words + /// + /// In en, this message translates to: + /// **'Tap to view and save your secret words.'** + String get tapToViewAndSaveSecretWords; + + /// Empty state message on the notifications screen + /// + /// In en, this message translates to: + /// **'No notifications'** + String get noNotifications; + + /// Contextual action to mark a single notification as read + /// + /// In en, this message translates to: + /// **'Mark as read'** + String get markAsRead; + + /// Contextual action to delete a single notification + /// + /// In en, this message translates to: + /// **'Delete'** + String get deleteNotificationLabel; + + /// Header label on the post-trade rating screen + /// + /// In en, this message translates to: + /// **'RATE'** + String get rateScreenHeader; + + /// Label shown for a completed order on the rating screen + /// + /// In en, this message translates to: + /// **'Successful order'** + String get successfulOrder; + + /// Button label to submit a trade rating + /// + /// In en, this message translates to: + /// **'SUBMIT'** + String get submitRatingButton; + + /// Button label to close the rating screen without rating + /// + /// In en, this message translates to: + /// **'CLOSE'** + String get closeRatingButton; + + /// Screen title for the About screen + /// + /// In en, this message translates to: + /// **'About'** + String get aboutScreenTitle; + + /// App tagline shown on the About screen + /// + /// In en, this message translates to: + /// **'Peer-to-peer Bitcoin trading over Nostr'** + String get mostroTagline; + + /// Button label to open the Mostro documentation + /// + /// In en, this message translates to: + /// **'View Documentation'** + String get viewDocumentationButton; + + /// Snackbar shown after copying a link to clipboard + /// + /// In en, this message translates to: + /// **'Link copied to clipboard'** + String get linkCopiedToClipboard; + + /// Section heading for the default Mostro node info + /// + /// In en, this message translates to: + /// **'Default Node'** + String get defaultNodeSection; + + /// Label for a Nostr public key + /// + /// In en, this message translates to: + /// **'Pubkey'** + String get pubkeyLabel; + + /// Label for the list of Nostr relays + /// + /// In en, this message translates to: + /// **'Relays'** + String get relaysLabel; + + /// Snackbar shown after copying a pubkey to clipboard + /// + /// In en, this message translates to: + /// **'Pubkey copied to clipboard'** + String get pubkeyCopiedToClipboard; + + /// Footer tagline on the About screen + /// + /// In en, this message translates to: + /// **'Open-source. Non-custodial. Private.'** + String get footerTagline; + + /// Title shown at the top of the navigation drawer + /// + /// In en, this message translates to: + /// **'MOSTRO'** + String get drawerTitle; + + /// Badge label indicating the app is in beta + /// + /// In en, this message translates to: + /// **'Beta'** + String get betaBadgeLabel; + + /// Drawer menu item navigating to the Account screen + /// + /// In en, this message translates to: + /// **'Account'** + String get drawerAccountMenuItem; + + /// Drawer menu item navigating to the Settings screen + /// + /// In en, this message translates to: + /// **'Settings'** + String get drawerSettingsMenuItem; + + /// Drawer menu item navigating to the About screen + /// + /// In en, this message translates to: + /// **'About'** + String get drawerAboutMenuItem; + + /// Bottom navigation label for the Order Book tab + /// + /// In en, this message translates to: + /// **'Order Book'** + String get navOrderBook; + + /// Bottom navigation label for the My Trades tab + /// + /// In en, this message translates to: + /// **'My Trades'** + String get navMyTrades; + + /// Bottom navigation label for the Chat tab + /// + /// In en, this message translates to: + /// **'Chat'** + String get navChat; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 7152e35d..382f38b8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -49,7 +49,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get disputeAdminAssigned => - 'Ein Administrator wurde Ihrem Streitfall zugewiesen. Er wird sich hier in Kürze bei Ihnen melden.'; + 'Ein Administrator wurde deinem Streitfall zugewiesen. Er wird sich hier in Kürze bei dir melden.'; @override String get disputeChatClosed => @@ -60,7 +60,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get disputeLoadError => - 'Streitfälle konnten nicht geladen werden. Bitte versuchen Sie es erneut.'; + 'Streitfälle konnten nicht geladen werden. Bitte versuche es erneut.'; @override String get disputeMessagingComingSoon => @@ -116,5 +116,464 @@ class AppLocalizationsDe extends AppLocalizations { @override String get disputeLostFundsToSeller => - 'Der Administrator hat die Bestellung storniert und die Sats an den Verkäufer zurückgegeben. Sie haben keine Sats erhalten.'; + 'Der Administrator hat die Bestellung storniert und die Sats an den Verkäufer zurückgegeben. Du hast keine Sats erhalten.'; + + @override + String get walkthroughSlideOneTitle => 'Bitcoin frei handeln — kein KYC'; + + @override + String get walkthroughSlideOneBody => + 'Mostro ist eine Peer-to-Peer-Börse, mit der du Bitcoin gegen jede Währung und Zahlungsmethode tauschen kannst — ohne KYC und ohne deine Daten an irgendjemanden weiterzugeben. Es basiert auf Nostr, was es zensurresistent macht. Niemand kann dich am Handeln hindern.'; + + @override + String get walkthroughSlideTwoTitle => 'Privatsphäre als Standard'; + + @override + String get walkthroughSlideTwoBody => + 'Mostro generiert für jeden Austausch eine neue Identität, sodass deine Trades nicht verknüpft werden können. Du kannst auch selbst entscheiden, wie viel Privatsphäre du möchtest:\n• Reputationsmodus – Andere können deine erfolgreichen Trades und dein Vertrauenslevel sehen.\n• Vollständiger Privatsphäre-Modus – Es wird keine Reputation aufgebaut, aber deine Aktivität ist vollständig anonym.\nWechsle jederzeit den Modus im Konto-Bildschirm, wo du auch deine geheimen Wörter sichern solltest — sie sind die einzige Möglichkeit, dein Konto wiederherzustellen.'; + + @override + String get walkthroughSlideThreeTitle => 'Sicherheit bei jedem Schritt'; + + @override + String get walkthroughSlideThreeBody => + 'Mostro verwendet Hold Invoices (zurückgehaltene Rechnungen): Die Sats verbleiben bis zum Ende des Handels in der Wallet des Verkäufers. Das schützt beide Seiten. Die App ist außerdem so gestaltet, dass sie intuitiv und einfach für alle Arten von Nutzern ist.'; + + @override + String get walkthroughSlideFourTitle => 'Vollständig verschlüsselter Chat'; + + @override + String get walkthroughSlideFourBody => + 'Jeder Trade hat seinen eigenen privaten Chat, der Ende-zu-Ende verschlüsselt ist. Nur die beiden beteiligten Nutzer können ihn lesen. Im Streitfall kannst du den gemeinsamen Schlüssel einem Administrator geben, um bei der Lösung zu helfen.'; + + @override + String get walkthroughSlideFiveTitle => 'Ein Angebot annehmen'; + + @override + String get walkthroughSlideFiveBody => + 'Durchsuche das Orderbuch, wähle ein Angebot, das für dich passt, und folge dem Trade-Ablauf Schritt für Schritt. Du kannst das Profil des anderen Nutzers prüfen, sicher chatten und den Trade problemlos abschließen.'; + + @override + String get walkthroughSlideSixTitle => 'Findest du nicht, was du brauchst?'; + + @override + String get walkthroughSlideSixBody => + 'Du kannst auch dein eigenes Angebot erstellen und warten, bis jemand es annimmt. Lege den Betrag und die bevorzugte Zahlungsmethode fest — Mostro erledigt den Rest.'; + + @override + String get tabBuyBtc => 'BTC KAUFEN'; + + @override + String get tabSellBtc => 'BTC VERKAUFEN'; + + @override + String get filterButtonLabel => 'FILTERN'; + + @override + String offersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Angebote', + one: '1 Angebot', + ); + return '$_temp0'; + } + + @override + String get noOrdersAvailable => 'Keine Bestellungen verfügbar'; + + @override + String get justNow => 'Gerade eben'; + + @override + String minutesAgo(int m) { + return 'Vor ${m}m'; + } + + @override + String hoursAgo(int h) { + return 'Vor ${h}h'; + } + + @override + String daysAgo(int d) { + return 'Vor ${d}T'; + } + + @override + String get creatingNewOrderTitle => 'NEUE BESTELLUNG ERSTELLEN'; + + @override + String get youWantToBuyBitcoin => 'Du möchtest Bitcoin kaufen'; + + @override + String get youWantToSellBitcoin => 'Du möchtest Bitcoin verkaufen'; + + @override + String get rangeOrderLabel => 'Bereichsbestellung'; + + @override + String get payLightningInvoiceTitle => 'Lightning-Rechnung bezahlen'; + + @override + String get invoiceCopied => 'Rechnung kopiert'; + + @override + String get addInvoiceTitle => 'Rechnung hinzufügen'; + + @override + String get submitButtonLabel => 'Absenden'; + + @override + String get orderAlreadyTaken => 'Die Bestellung wurde bereits angenommen'; + + @override + String get orderIdCopied => 'Bestell-ID kopiert'; + + @override + String get orderDetailsTitle => 'BESTELLDETAILS'; + + @override + String get timeRemainingLabel => 'Verbleibende Zeit:'; + + @override + String get fiatSentButtonLabel => 'FIAT GESENDET'; + + @override + String get disputeButtonLabel => 'STREITFALL'; + + @override + String get contactButtonLabel => 'KONTAKT'; + + @override + String get rateButtonLabel => 'BEWERTEN'; + + @override + String get viewDisputeButtonLabel => 'STREITFALL ANZEIGEN'; + + @override + String get comingSoonMessage => 'Demnächst verfügbar'; + + @override + String get tradeStatusActive => 'Aktiv'; + + @override + String get tradeStatusFiatSent => 'Fiat gesendet'; + + @override + String get tradeStatusCompleted => 'Abgeschlossen'; + + @override + String get tradeStatusCancelled => 'Storniert'; + + @override + String get tradeStatusDisputed => 'Strittiger Trade'; + + @override + String get releaseButtonLabel => 'FREIGEBEN'; + + @override + String get accountScreenTitle => 'Konto'; + + @override + String get secretWordsTitle => 'Geheime Wörter'; + + @override + String get toRestoreYourAccount => 'Um dein Konto wiederherzustellen'; + + @override + String get privacyCardTitle => 'Datenschutz'; + + @override + String get controlPrivacySettings => + 'Verwalte deine Datenschutzeinstellungen'; + + @override + String get reputationMode => 'Reputationsmodus'; + + @override + String get reputationModeSubtitle => 'Standard-Datenschutz mit Reputation'; + + @override + String get fullPrivacyMode => 'Vollständiger Privatsphäre-Modus'; + + @override + String get fullPrivacyModeSubtitle => 'Maximale Anonymität'; + + @override + String get generateNewUserButton => 'Neuen Benutzer generieren'; + + @override + String get importMostroUserButton => 'Mostro-Benutzer importieren'; + + @override + String get generateNewUserDialogTitle => 'Neuen Benutzer generieren?'; + + @override + String get generateNewUserDialogContent => + 'Dadurch wird eine brandneue Identität erstellt. Deine aktuellen geheimen Wörter werden nicht mehr funktionieren — stelle sicher, dass du sie gesichert hast, bevor du fortfährst.'; + + @override + String get continueButtonLabel => 'Weiter'; + + @override + String get importMnemonicDialogTitle => 'Mnemonik importieren'; + + @override + String get importMnemonicHintText => 'Gib deine 12- oder 24-Wort-Phrase ein…'; + + @override + String get importButtonLabel => 'Importieren'; + + @override + String get refreshUserDialogTitle => 'Benutzer aktualisieren?'; + + @override + String get refreshUserDialogContent => + 'Dadurch werden deine Trades und Bestellungen von der Mostro-Instanz erneut abgerufen. Verwende dies, wenn du glaubst, dass deine Daten nicht synchron sind oder Bestellungen fehlen.'; + + @override + String get hideButtonLabel => 'Verbergen'; + + @override + String get showButtonLabel => 'Anzeigen'; + + @override + String get settingsScreenTitle => 'Einstellungen'; + + @override + String get languageSettingTitle => 'Sprache'; + + @override + String get appearanceSettingTitle => 'Erscheinungsbild'; + + @override + String get appearanceDialogTitle => 'Erscheinungsbild'; + + @override + String get defaultFiatCurrencyTitle => 'Standard-Fiat-Währung'; + + @override + String get allCurrencies => 'Alle Währungen'; + + @override + String get lightningAddressSettingTitle => 'Lightning-Adresse'; + + @override + String get tapToSetSubtitle => 'Tippen zum Einrichten'; + + @override + String get nwcWalletSettingTitle => 'NWC-Wallet'; + + @override + String get nwcConnectPrompt => 'Verbinde deine Lightning-Wallet über NWC'; + + @override + String get relaysSettingTitle => 'Relays'; + + @override + String get manageRelayConnections => 'Relay-Verbindungen verwalten'; + + @override + String get pushNotificationsSettingTitle => 'Push-Benachrichtigungen'; + + @override + String get manageNotificationPreferences => + 'Benachrichtigungseinstellungen verwalten'; + + @override + String get logReportSettingTitle => 'Protokollbericht'; + + @override + String get viewDiagnosticLogs => 'Diagnoseprotokolle anzeigen'; + + @override + String get mostroNodeSettingTitle => 'Mostro-Knoten'; + + @override + String get themeDark => 'Dunkel'; + + @override + String get themeLight => 'Hell'; + + @override + String get themeSystemDefault => 'Systemstandard'; + + @override + String get lightningAddressDialogTitle => 'Lightning-Adresse'; + + @override + String get lightningAddressHintText => 'benutzer@domain.com'; + + @override + String get invalidLightningAddressFormat => + 'Muss im Format benutzer@domain vorliegen'; + + @override + String get clearButtonLabel => 'Löschen'; + + @override + String get saveButtonLabel => 'Speichern'; + + @override + String get connectWalletTitle => 'Wallet verbinden'; + + @override + String get scanQrCodeTitle => 'QR-Code scannen'; + + @override + String get pasteNwcUri => 'NWC-URI einfügen'; + + @override + String get selectLanguageTitle => 'Sprache auswählen'; + + @override + String get selectCurrencyDialogTitle => 'Währung auswählen'; + + @override + String get addRelayDialogTitle => 'Relay hinzufügen'; + + @override + String get addButtonLabel => 'Hinzufügen'; + + @override + String get relayHintText => 'wss://relay.example.com'; + + @override + String get relayErrorMustStartWithWss => 'Muss mit wss:// beginnen'; + + @override + String get relayErrorUrlTooShort => 'URL ist zu kurz'; + + @override + String get relayErrorDuplicate => 'Relay bereits in der Liste'; + + @override + String nwcConnectedBalance(String balance) { + return 'NWC — Verbunden. Guthaben: $balance'; + } + + @override + String get pasteQrCodeHeading => 'QR-Code-Inhalt einfügen'; + + @override + String get pasteButtonLabel => 'Einfügen'; + + @override + String get clipboardEmptyError => 'Zwischenablage ist leer'; + + @override + String get enterValueError => 'Bitte einen Wert eingeben'; + + @override + String get pasteOrScanQrCode => 'QR-Code einfügen oder scannen'; + + @override + String get mostroNodeTitle => 'Mostro-Knoten'; + + @override + String get currentNodeLabel => 'Aktueller Knoten'; + + @override + String get trustedBadgeLabel => 'Vertrauenswürdig'; + + @override + String get useDefaultButtonLabel => 'Standard verwenden'; + + @override + String get confirmButtonLabel => 'Bestätigen'; + + @override + String get invalidHexPubkey => + 'Muss eine hexadezimale Zeichenfolge mit 64 Zeichen sein'; + + @override + String get notificationsScreenTitle => 'Benachrichtigungen'; + + @override + String get markAllAsReadMenuItem => 'Alle als gelesen markieren'; + + @override + String get clearAllMenuItem => 'Alle löschen'; + + @override + String get youMustBackUpYourAccount => 'Du musst dein Konto sichern'; + + @override + String get tapToViewAndSaveSecretWords => + 'Tippe, um deine geheimen Wörter anzuzeigen und zu speichern.'; + + @override + String get noNotifications => 'Keine Benachrichtigungen'; + + @override + String get markAsRead => 'Als gelesen markieren'; + + @override + String get deleteNotificationLabel => 'Löschen'; + + @override + String get rateScreenHeader => 'BEWERTEN'; + + @override + String get successfulOrder => 'Erfolgreiche Bestellung'; + + @override + String get submitRatingButton => 'ABSENDEN'; + + @override + String get closeRatingButton => 'SCHLIESSEN'; + + @override + String get aboutScreenTitle => 'Über'; + + @override + String get mostroTagline => 'Peer-to-Peer Bitcoin-Handel über Nostr'; + + @override + String get viewDocumentationButton => 'Dokumentation anzeigen'; + + @override + String get linkCopiedToClipboard => 'Link in die Zwischenablage kopiert'; + + @override + String get defaultNodeSection => 'Standardknoten'; + + @override + String get pubkeyLabel => 'Öffentlicher Schlüssel'; + + @override + String get relaysLabel => 'Relays'; + + @override + String get pubkeyCopiedToClipboard => + 'Öffentlicher Schlüssel in die Zwischenablage kopiert'; + + @override + String get footerTagline => 'Open-Source. Nicht-verwahrt. Privat.'; + + @override + String get drawerTitle => 'MOSTRO'; + + @override + String get betaBadgeLabel => 'Beta'; + + @override + String get drawerAccountMenuItem => 'Konto'; + + @override + String get drawerSettingsMenuItem => 'Einstellungen'; + + @override + String get drawerAboutMenuItem => 'Über'; + + @override + String get navOrderBook => 'Orderbuch'; + + @override + String get navMyTrades => 'Meine Trades'; + + @override + String get navChat => 'Chat'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 08ab4732..08e294f3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -114,4 +114,458 @@ class AppLocalizationsEn extends AppLocalizations { @override String get disputeLostFundsToSeller => 'The administrator canceled the order and returned the sats to the seller. You did not receive the sats.'; + + @override + String get walkthroughSlideOneTitle => 'Trade Bitcoin freely — no KYC'; + + @override + String get walkthroughSlideOneBody => + 'Mostro is a peer-to-peer exchange that lets you trade Bitcoin for any currency and payment method — no KYC, and no need to give your data to anyone. It\'s built on Nostr, which makes it censorship-resistant. No one can stop you from trading.'; + + @override + String get walkthroughSlideTwoTitle => 'Privacy by default'; + + @override + String get walkthroughSlideTwoBody => + 'Mostro generates a new identity for every exchange, so your trades can\'t be linked. You can also decide how private you want to be:\n• Reputation mode – Lets others see your successful trades and trust level.\n• Full privacy mode – No reputation is built, but your activity is completely anonymous.\nSwitch modes anytime from the Account screen, where you should also save your secret words — they\'re the only way to recover your account.'; + + @override + String get walkthroughSlideThreeTitle => 'Security at every step'; + + @override + String get walkthroughSlideThreeBody => + 'Mostro uses Hold Invoices: sats stay in the seller\'s wallet until the end of the trade. This protects both sides. The app is also designed to be intuitive and easy for all kinds of users.'; + + @override + String get walkthroughSlideFourTitle => 'Fully encrypted chat'; + + @override + String get walkthroughSlideFourBody => + 'Each trade has its own private chat, end-to-end encrypted. Only the two users involved can read it. In case of a dispute, you can give the shared key to an admin to help resolve the issue.'; + + @override + String get walkthroughSlideFiveTitle => 'Take an offer'; + + @override + String get walkthroughSlideFiveBody => + 'Browse the order book, choose an offer that works for you, and follow the trade flow step by step. You\'ll be able to check the other user\'s profile, chat securely, and complete the trade with ease.'; + + @override + String get walkthroughSlideSixTitle => 'Can\'t find what you need?'; + + @override + String get walkthroughSlideSixBody => + 'You can also create your own offer and wait for someone to take it. Set the amount and preferred payment method — Mostro handles the rest.'; + + @override + String get tabBuyBtc => 'BUY BTC'; + + @override + String get tabSellBtc => 'SELL BTC'; + + @override + String get filterButtonLabel => 'FILTER'; + + @override + String offersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count offers', + one: '1 offer', + ); + return '$_temp0'; + } + + @override + String get noOrdersAvailable => 'No orders available'; + + @override + String get justNow => 'Just now'; + + @override + String minutesAgo(int m) { + return '${m}m ago'; + } + + @override + String hoursAgo(int h) { + return '${h}h ago'; + } + + @override + String daysAgo(int d) { + return '${d}d ago'; + } + + @override + String get creatingNewOrderTitle => 'CREATING NEW ORDER'; + + @override + String get youWantToBuyBitcoin => 'You want to buy Bitcoin'; + + @override + String get youWantToSellBitcoin => 'You want to sell Bitcoin'; + + @override + String get rangeOrderLabel => 'Range order'; + + @override + String get payLightningInvoiceTitle => 'Pay Lightning Invoice'; + + @override + String get invoiceCopied => 'Invoice copied'; + + @override + String get addInvoiceTitle => 'Add Invoice'; + + @override + String get submitButtonLabel => 'Submit'; + + @override + String get orderAlreadyTaken => 'Order has already been taken'; + + @override + String get orderIdCopied => 'Order ID copied'; + + @override + String get orderDetailsTitle => 'ORDER DETAILS'; + + @override + String get timeRemainingLabel => 'Time remaining:'; + + @override + String get fiatSentButtonLabel => 'FIAT SENT'; + + @override + String get disputeButtonLabel => 'DISPUTE'; + + @override + String get contactButtonLabel => 'CONTACT'; + + @override + String get rateButtonLabel => 'RATE'; + + @override + String get viewDisputeButtonLabel => 'VIEW DISPUTE'; + + @override + String get comingSoonMessage => 'Coming soon'; + + @override + String get tradeStatusActive => 'Active'; + + @override + String get tradeStatusFiatSent => 'Fiat Sent'; + + @override + String get tradeStatusCompleted => 'Completed'; + + @override + String get tradeStatusCancelled => 'Cancelled'; + + @override + String get tradeStatusDisputed => 'Disputed'; + + @override + String get releaseButtonLabel => 'RELEASE'; + + @override + String get accountScreenTitle => 'Account'; + + @override + String get secretWordsTitle => 'Secret Words'; + + @override + String get toRestoreYourAccount => 'To restore your account'; + + @override + String get privacyCardTitle => 'Privacy'; + + @override + String get controlPrivacySettings => 'Control your privacy settings'; + + @override + String get reputationMode => 'Reputation Mode'; + + @override + String get reputationModeSubtitle => 'Standard privacy with reputation'; + + @override + String get fullPrivacyMode => 'Full Privacy Mode'; + + @override + String get fullPrivacyModeSubtitle => 'Maximum anonymity'; + + @override + String get generateNewUserButton => 'Generate New User'; + + @override + String get importMostroUserButton => 'Import Mostro User'; + + @override + String get generateNewUserDialogTitle => 'Generate New User?'; + + @override + String get generateNewUserDialogContent => + 'This will create a brand-new identity. Your current secret words will no longer work — make sure they are backed up before continuing.'; + + @override + String get continueButtonLabel => 'Continue'; + + @override + String get importMnemonicDialogTitle => 'Import Mnemonic'; + + @override + String get importMnemonicHintText => 'Enter your 12 or 24 word phrase…'; + + @override + String get importButtonLabel => 'Import'; + + @override + String get refreshUserDialogTitle => 'Refresh User?'; + + @override + String get refreshUserDialogContent => + 'This will re-fetch your trades and orders from the Mostro instance. Use this if you think your data is out of sync or orders are missing.'; + + @override + String get hideButtonLabel => 'Hide'; + + @override + String get showButtonLabel => 'Show'; + + @override + String get settingsScreenTitle => 'Settings'; + + @override + String get languageSettingTitle => 'Language'; + + @override + String get appearanceSettingTitle => 'Appearance'; + + @override + String get appearanceDialogTitle => 'Appearance'; + + @override + String get defaultFiatCurrencyTitle => 'Default Fiat Currency'; + + @override + String get allCurrencies => 'All currencies'; + + @override + String get lightningAddressSettingTitle => 'Lightning Address'; + + @override + String get tapToSetSubtitle => 'Tap to set'; + + @override + String get nwcWalletSettingTitle => 'NWC Wallet'; + + @override + String get nwcConnectPrompt => 'Connect your Lightning wallet via NWC'; + + @override + String get relaysSettingTitle => 'Relays'; + + @override + String get manageRelayConnections => 'Manage relay connections'; + + @override + String get pushNotificationsSettingTitle => 'Push Notifications'; + + @override + String get manageNotificationPreferences => 'Manage notification preferences'; + + @override + String get logReportSettingTitle => 'Log Report'; + + @override + String get viewDiagnosticLogs => 'View diagnostic logs'; + + @override + String get mostroNodeSettingTitle => 'Mostro Node'; + + @override + String get themeDark => 'Dark'; + + @override + String get themeLight => 'Light'; + + @override + String get themeSystemDefault => 'System default'; + + @override + String get lightningAddressDialogTitle => 'Lightning Address'; + + @override + String get lightningAddressHintText => 'user@domain.com'; + + @override + String get invalidLightningAddressFormat => 'Must be in user@domain format'; + + @override + String get clearButtonLabel => 'Clear'; + + @override + String get saveButtonLabel => 'Save'; + + @override + String get connectWalletTitle => 'Connect Wallet'; + + @override + String get scanQrCodeTitle => 'Scan QR Code'; + + @override + String get pasteNwcUri => 'Paste NWC URI'; + + @override + String get selectLanguageTitle => 'Select Language'; + + @override + String get selectCurrencyDialogTitle => 'Select Currency'; + + @override + String get addRelayDialogTitle => 'Add Relay'; + + @override + String get addButtonLabel => 'Add'; + + @override + String get relayHintText => 'wss://relay.example.com'; + + @override + String get relayErrorMustStartWithWss => 'Must start with wss://'; + + @override + String get relayErrorUrlTooShort => 'URL is too short'; + + @override + String get relayErrorDuplicate => 'Relay already in list'; + + @override + String nwcConnectedBalance(String balance) { + return 'NWC — Connected. Balance: $balance'; + } + + @override + String get pasteQrCodeHeading => 'Paste QR Code Content'; + + @override + String get pasteButtonLabel => 'Paste'; + + @override + String get clipboardEmptyError => 'Clipboard is empty'; + + @override + String get enterValueError => 'Please enter a value'; + + @override + String get pasteOrScanQrCode => 'Paste or scan a QR code'; + + @override + String get mostroNodeTitle => 'Mostro Node'; + + @override + String get currentNodeLabel => 'Current Node'; + + @override + String get trustedBadgeLabel => 'Trusted'; + + @override + String get useDefaultButtonLabel => 'Use Default'; + + @override + String get confirmButtonLabel => 'Confirm'; + + @override + String get invalidHexPubkey => 'Must be a 64-character hex string'; + + @override + String get notificationsScreenTitle => 'Notifications'; + + @override + String get markAllAsReadMenuItem => 'Mark all as read'; + + @override + String get clearAllMenuItem => 'Clear all'; + + @override + String get youMustBackUpYourAccount => 'You must back up your account'; + + @override + String get tapToViewAndSaveSecretWords => + 'Tap to view and save your secret words.'; + + @override + String get noNotifications => 'No notifications'; + + @override + String get markAsRead => 'Mark as read'; + + @override + String get deleteNotificationLabel => 'Delete'; + + @override + String get rateScreenHeader => 'RATE'; + + @override + String get successfulOrder => 'Successful order'; + + @override + String get submitRatingButton => 'SUBMIT'; + + @override + String get closeRatingButton => 'CLOSE'; + + @override + String get aboutScreenTitle => 'About'; + + @override + String get mostroTagline => 'Peer-to-peer Bitcoin trading over Nostr'; + + @override + String get viewDocumentationButton => 'View Documentation'; + + @override + String get linkCopiedToClipboard => 'Link copied to clipboard'; + + @override + String get defaultNodeSection => 'Default Node'; + + @override + String get pubkeyLabel => 'Pubkey'; + + @override + String get relaysLabel => 'Relays'; + + @override + String get pubkeyCopiedToClipboard => 'Pubkey copied to clipboard'; + + @override + String get footerTagline => 'Open-source. Non-custodial. Private.'; + + @override + String get drawerTitle => 'MOSTRO'; + + @override + String get betaBadgeLabel => 'Beta'; + + @override + String get drawerAccountMenuItem => 'Account'; + + @override + String get drawerSettingsMenuItem => 'Settings'; + + @override + String get drawerAboutMenuItem => 'About'; + + @override + String get navOrderBook => 'Order Book'; + + @override + String get navMyTrades => 'My Trades'; + + @override + String get navChat => 'Chat'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 686f31a5..7fb92117 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -116,4 +116,464 @@ class AppLocalizationsEs extends AppLocalizations { @override String get disputeLostFundsToSeller => 'El administrador canceló la orden y devolvió los sats al vendedor. No recibiste los sats.'; + + @override + String get walkthroughSlideOneTitle => + 'Intercambia Bitcoin libremente — sin KYC'; + + @override + String get walkthroughSlideOneBody => + 'Mostro es un exchange peer-to-peer que te permite intercambiar Bitcoin por cualquier moneda y método de pago — sin KYC y sin necesidad de dar tus datos a nadie. Está construido sobre Nostr, lo que lo hace resistente a la censura. Nadie puede impedirte operar.'; + + @override + String get walkthroughSlideTwoTitle => 'Privacidad por defecto'; + + @override + String get walkthroughSlideTwoBody => + 'Mostro genera una nueva identidad en cada intercambio, de modo que tus operaciones no pueden vincularse. También puedes decidir cuánta privacidad quieres:\n• Modo reputación – Permite que otros vean tus operaciones exitosas y tu nivel de confianza.\n• Modo privacidad total – No se construye reputación, pero tu actividad es completamente anónima.\nCambia de modo en cualquier momento desde la pantalla de Cuenta, donde también debes guardar tus palabras secretas — son la única forma de recuperar tu cuenta.'; + + @override + String get walkthroughSlideThreeTitle => 'Seguridad en cada paso'; + + @override + String get walkthroughSlideThreeBody => + 'Mostro usa Hold Invoices (facturas retenidas): los sats permanecen en la billetera del vendedor hasta el final del intercambio. Esto protege a ambas partes. La aplicación también está diseñada para ser intuitiva y fácil para todo tipo de usuarios.'; + + @override + String get walkthroughSlideFourTitle => 'Chat totalmente cifrado'; + + @override + String get walkthroughSlideFourBody => + 'Cada operación tiene su propio chat privado, cifrado de extremo a extremo. Solo los dos usuarios involucrados pueden leerlo. En caso de disputa, puedes compartir la clave con un administrador para ayudar a resolver el problema.'; + + @override + String get walkthroughSlideFiveTitle => 'Toma una oferta'; + + @override + String get walkthroughSlideFiveBody => + 'Explora el libro de órdenes, elige una oferta que te convenga y sigue el flujo de la operación paso a paso. Podrás revisar el perfil del otro usuario, chatear de forma segura y completar la operación con facilidad.'; + + @override + String get walkthroughSlideSixTitle => '¿No encuentras lo que necesitas?'; + + @override + String get walkthroughSlideSixBody => + 'También puedes crear tu propia oferta y esperar a que alguien la tome. Establece el monto y el método de pago preferido — Mostro se encarga del resto.'; + + @override + String get tabBuyBtc => 'COMPRAR BTC'; + + @override + String get tabSellBtc => 'VENDER BTC'; + + @override + String get filterButtonLabel => 'FILTRAR'; + + @override + String offersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ofertas', + one: '1 oferta', + ); + return '$_temp0'; + } + + @override + String get noOrdersAvailable => 'No hay órdenes disponibles'; + + @override + String get justNow => 'Ahora mismo'; + + @override + String minutesAgo(int m) { + return 'Hace ${m}m'; + } + + @override + String hoursAgo(int h) { + return 'Hace ${h}h'; + } + + @override + String daysAgo(int d) { + return 'Hace ${d}d'; + } + + @override + String get creatingNewOrderTitle => 'CREANDO NUEVA ORDEN'; + + @override + String get youWantToBuyBitcoin => 'Quieres comprar Bitcoin'; + + @override + String get youWantToSellBitcoin => 'Quieres vender Bitcoin'; + + @override + String get rangeOrderLabel => 'Orden por rango'; + + @override + String get payLightningInvoiceTitle => 'Pagar Factura Lightning'; + + @override + String get invoiceCopied => 'Factura copiada'; + + @override + String get addInvoiceTitle => 'Agregar Factura'; + + @override + String get submitButtonLabel => 'Enviar'; + + @override + String get orderAlreadyTaken => 'La orden ya fue tomada'; + + @override + String get orderIdCopied => 'ID de orden copiado'; + + @override + String get orderDetailsTitle => 'DETALLES DE LA ORDEN'; + + @override + String get timeRemainingLabel => 'Tiempo restante:'; + + @override + String get fiatSentButtonLabel => 'FIAT ENVIADO'; + + @override + String get disputeButtonLabel => 'DISPUTAR'; + + @override + String get contactButtonLabel => 'CONTACTAR'; + + @override + String get rateButtonLabel => 'VALORAR'; + + @override + String get viewDisputeButtonLabel => 'VER DISPUTA'; + + @override + String get comingSoonMessage => 'Próximamente'; + + @override + String get tradeStatusActive => 'Activo'; + + @override + String get tradeStatusFiatSent => 'Fiat enviado'; + + @override + String get tradeStatusCompleted => 'Completado'; + + @override + String get tradeStatusCancelled => 'Cancelado'; + + @override + String get tradeStatusDisputed => 'En disputa'; + + @override + String get releaseButtonLabel => 'LIBERAR'; + + @override + String get accountScreenTitle => 'Cuenta'; + + @override + String get secretWordsTitle => 'Palabras secretas'; + + @override + String get toRestoreYourAccount => 'Para restaurar tu cuenta'; + + @override + String get privacyCardTitle => 'Privacidad'; + + @override + String get controlPrivacySettings => + 'Controla tu configuración de privacidad'; + + @override + String get reputationMode => 'Modo Reputación'; + + @override + String get reputationModeSubtitle => 'Privacidad estándar con reputación'; + + @override + String get fullPrivacyMode => 'Modo Privacidad Total'; + + @override + String get fullPrivacyModeSubtitle => 'Anonimato máximo'; + + @override + String get generateNewUserButton => 'Generar nuevo usuario'; + + @override + String get importMostroUserButton => 'Importar usuario de Mostro'; + + @override + String get generateNewUserDialogTitle => '¿Generar nuevo usuario?'; + + @override + String get generateNewUserDialogContent => + 'Esto creará una identidad completamente nueva. Tus palabras secretas actuales dejarán de funcionar — asegúrate de tenerlas respaldadas antes de continuar.'; + + @override + String get continueButtonLabel => 'Continuar'; + + @override + String get importMnemonicDialogTitle => 'Importar Mnemónico'; + + @override + String get importMnemonicHintText => 'Ingresa tu frase de 12 o 24 palabras…'; + + @override + String get importButtonLabel => 'Importar'; + + @override + String get refreshUserDialogTitle => '¿Actualizar usuario?'; + + @override + String get refreshUserDialogContent => + 'Esto volverá a obtener tus operaciones y órdenes desde la instancia de Mostro. Úsalo si crees que tus datos están desincronizados o faltan órdenes.'; + + @override + String get hideButtonLabel => 'Ocultar'; + + @override + String get showButtonLabel => 'Mostrar'; + + @override + String get settingsScreenTitle => 'Configuración'; + + @override + String get languageSettingTitle => 'Idioma'; + + @override + String get appearanceSettingTitle => 'Apariencia'; + + @override + String get appearanceDialogTitle => 'Apariencia'; + + @override + String get defaultFiatCurrencyTitle => 'Moneda fiat predeterminada'; + + @override + String get allCurrencies => 'Todas las monedas'; + + @override + String get lightningAddressSettingTitle => 'Dirección Lightning'; + + @override + String get tapToSetSubtitle => 'Toca para configurar'; + + @override + String get nwcWalletSettingTitle => 'Billetera NWC'; + + @override + String get nwcConnectPrompt => 'Conecta tu billetera Lightning mediante NWC'; + + @override + String get relaysSettingTitle => 'Relays'; + + @override + String get manageRelayConnections => 'Administrar conexiones de relay'; + + @override + String get pushNotificationsSettingTitle => 'Notificaciones push'; + + @override + String get manageNotificationPreferences => + 'Administrar preferencias de notificaciones'; + + @override + String get logReportSettingTitle => 'Informe de registros'; + + @override + String get viewDiagnosticLogs => 'Ver registros de diagnóstico'; + + @override + String get mostroNodeSettingTitle => 'Nodo Mostro'; + + @override + String get themeDark => 'Oscuro'; + + @override + String get themeLight => 'Claro'; + + @override + String get themeSystemDefault => 'Predeterminado del sistema'; + + @override + String get lightningAddressDialogTitle => 'Dirección Lightning'; + + @override + String get lightningAddressHintText => 'usuario@dominio.com'; + + @override + String get invalidLightningAddressFormat => + 'Debe tener el formato usuario@dominio'; + + @override + String get clearButtonLabel => 'Limpiar'; + + @override + String get saveButtonLabel => 'Guardar'; + + @override + String get connectWalletTitle => 'Conectar billetera'; + + @override + String get scanQrCodeTitle => 'Escanear código QR'; + + @override + String get pasteNwcUri => 'Pegar URI NWC'; + + @override + String get selectLanguageTitle => 'Seleccionar idioma'; + + @override + String get selectCurrencyDialogTitle => 'Seleccionar moneda'; + + @override + String get addRelayDialogTitle => 'Agregar relay'; + + @override + String get addButtonLabel => 'Agregar'; + + @override + String get relayHintText => 'wss://relay.example.com'; + + @override + String get relayErrorMustStartWithWss => 'Debe comenzar con wss://'; + + @override + String get relayErrorUrlTooShort => 'La URL es demasiado corta'; + + @override + String get relayErrorDuplicate => 'La retransmisión ya está en la lista'; + + @override + String nwcConnectedBalance(String balance) { + return 'NWC — Conectado. Saldo: $balance'; + } + + @override + String get pasteQrCodeHeading => 'Pegar contenido del código QR'; + + @override + String get pasteButtonLabel => 'Pegar'; + + @override + String get clipboardEmptyError => 'El portapapeles está vacío'; + + @override + String get enterValueError => 'Por favor ingresa un valor'; + + @override + String get pasteOrScanQrCode => 'Pegar o escanear un código QR'; + + @override + String get mostroNodeTitle => 'Nodo Mostro'; + + @override + String get currentNodeLabel => 'Nodo actual'; + + @override + String get trustedBadgeLabel => 'De confianza'; + + @override + String get useDefaultButtonLabel => 'Usar predeterminado'; + + @override + String get confirmButtonLabel => 'Confirmar'; + + @override + String get invalidHexPubkey => + 'Debe ser una cadena hexadecimal de 64 caracteres'; + + @override + String get notificationsScreenTitle => 'Notificaciones'; + + @override + String get markAllAsReadMenuItem => 'Marcar todo como leído'; + + @override + String get clearAllMenuItem => 'Borrar todo'; + + @override + String get youMustBackUpYourAccount => + 'Debes hacer una copia de seguridad de tu cuenta'; + + @override + String get tapToViewAndSaveSecretWords => + 'Toca para ver y guardar tus palabras secretas.'; + + @override + String get noNotifications => 'Sin notificaciones'; + + @override + String get markAsRead => 'Marcar como leído'; + + @override + String get deleteNotificationLabel => 'Eliminar'; + + @override + String get rateScreenHeader => 'VALORAR'; + + @override + String get successfulOrder => 'Orden exitosa'; + + @override + String get submitRatingButton => 'ENVIAR'; + + @override + String get closeRatingButton => 'CERRAR'; + + @override + String get aboutScreenTitle => 'Acerca de'; + + @override + String get mostroTagline => 'Intercambio de Bitcoin peer-to-peer sobre Nostr'; + + @override + String get viewDocumentationButton => 'Ver documentación'; + + @override + String get linkCopiedToClipboard => 'Enlace copiado al portapapeles'; + + @override + String get defaultNodeSection => 'Nodo predeterminado'; + + @override + String get pubkeyLabel => 'Clave pública'; + + @override + String get relaysLabel => 'Relays'; + + @override + String get pubkeyCopiedToClipboard => 'Clave pública copiada al portapapeles'; + + @override + String get footerTagline => 'Código abierto. Sin custodia. Privado.'; + + @override + String get drawerTitle => 'MOSTRO'; + + @override + String get betaBadgeLabel => 'Beta'; + + @override + String get drawerAccountMenuItem => 'Cuenta'; + + @override + String get drawerSettingsMenuItem => 'Configuración'; + + @override + String get drawerAboutMenuItem => 'Acerca de'; + + @override + String get navOrderBook => 'Libro de órdenes'; + + @override + String get navMyTrades => 'Mis operaciones'; + + @override + String get navChat => 'Chat'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index cfea8e9c..903a8ee2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -117,4 +117,467 @@ class AppLocalizationsFr extends AppLocalizations { @override String get disputeLostFundsToSeller => 'L\'administrateur a annulé la commande et retourné les sats au vendeur. Vous n\'avez pas reçu les sats.'; + + @override + String get walkthroughSlideOneTitle => + 'Échangez du Bitcoin librement — sans KYC'; + + @override + String get walkthroughSlideOneBody => + 'Mostro est un exchange pair-à-pair qui vous permet d\'échanger du Bitcoin contre n\'importe quelle devise et méthode de paiement — sans KYC et sans avoir à communiquer vos données à qui que ce soit. Il est construit sur Nostr, ce qui le rend résistant à la censure. Personne ne peut vous empêcher de trader.'; + + @override + String get walkthroughSlideTwoTitle => 'Confidentialité par défaut'; + + @override + String get walkthroughSlideTwoBody => + 'Mostro génère une nouvelle identité pour chaque échange, de sorte que vos transactions ne peuvent pas être liées. Vous pouvez également décider du niveau de confidentialité souhaité :\n• Mode réputation – Permet aux autres de voir vos échanges réussis et votre niveau de confiance.\n• Mode confidentialité totale – Aucune réputation n\'est construite, mais votre activité est totalement anonyme.\nChangez de mode à tout moment depuis l\'écran Compte, où vous devriez également sauvegarder vos mots secrets — ils sont le seul moyen de récupérer votre compte.'; + + @override + String get walkthroughSlideThreeTitle => 'Sécurité à chaque étape'; + + @override + String get walkthroughSlideThreeBody => + 'Mostro utilise les Hold Invoices (factures retenues) : les sats restent dans le portefeuille du vendeur jusqu\'à la fin de l\'échange. Cela protège les deux parties. L\'application est également conçue pour être intuitive et facile à utiliser pour tous les types d\'utilisateurs.'; + + @override + String get walkthroughSlideFourTitle => 'Chat entièrement chiffré'; + + @override + String get walkthroughSlideFourBody => + 'Chaque transaction dispose de son propre chat privé, chiffré de bout en bout. Seuls les deux utilisateurs impliqués peuvent le lire. En cas de litige, vous pouvez donner la clé partagée à un administrateur pour l\'aider à résoudre le problème.'; + + @override + String get walkthroughSlideFiveTitle => 'Prenez une offre'; + + @override + String get walkthroughSlideFiveBody => + 'Parcourez le carnet d\'ordres, choisissez une offre qui vous convient et suivez le déroulement de la transaction étape par étape. Vous pourrez consulter le profil de l\'autre utilisateur, chatter en toute sécurité et finaliser l\'échange facilement.'; + + @override + String get walkthroughSlideSixTitle => + 'Vous ne trouvez pas ce qu\'il vous faut ?'; + + @override + String get walkthroughSlideSixBody => + 'Vous pouvez également créer votre propre offre et attendre que quelqu\'un la prenne. Définissez le montant et la méthode de paiement souhaitée — Mostro s\'occupe du reste.'; + + @override + String get tabBuyBtc => 'ACHETER BTC'; + + @override + String get tabSellBtc => 'VENDRE BTC'; + + @override + String get filterButtonLabel => 'FILTRER'; + + @override + String offersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count offres', + one: '1 offre', + ); + return '$_temp0'; + } + + @override + String get noOrdersAvailable => 'Aucun ordre disponible'; + + @override + String get justNow => 'À l\'instant'; + + @override + String minutesAgo(int m) { + return 'Il y a ${m}m'; + } + + @override + String hoursAgo(int h) { + return 'Il y a ${h}h'; + } + + @override + String daysAgo(int d) { + return 'Il y a ${d}j'; + } + + @override + String get creatingNewOrderTitle => 'CRÉATION D\'UN NOUVEL ORDRE'; + + @override + String get youWantToBuyBitcoin => 'Vous voulez acheter du Bitcoin'; + + @override + String get youWantToSellBitcoin => 'Vous voulez vendre du Bitcoin'; + + @override + String get rangeOrderLabel => 'Ordre à plage'; + + @override + String get payLightningInvoiceTitle => 'Payer la facture Lightning'; + + @override + String get invoiceCopied => 'Facture copiée'; + + @override + String get addInvoiceTitle => 'Ajouter une facture'; + + @override + String get submitButtonLabel => 'Soumettre'; + + @override + String get orderAlreadyTaken => 'Cet ordre a déjà été pris'; + + @override + String get orderIdCopied => 'ID d\'ordre copié'; + + @override + String get orderDetailsTitle => 'DÉTAILS DE L\'ORDRE'; + + @override + String get timeRemainingLabel => 'Temps restant :'; + + @override + String get fiatSentButtonLabel => 'FIAT ENVOYÉ'; + + @override + String get disputeButtonLabel => 'LITIGE'; + + @override + String get contactButtonLabel => 'CONTACTER'; + + @override + String get rateButtonLabel => 'NOTER'; + + @override + String get viewDisputeButtonLabel => 'VOIR LE LITIGE'; + + @override + String get comingSoonMessage => 'Bientôt disponible'; + + @override + String get tradeStatusActive => 'Actif'; + + @override + String get tradeStatusFiatSent => 'Fiat envoyé'; + + @override + String get tradeStatusCompleted => 'Terminé'; + + @override + String get tradeStatusCancelled => 'Annulé'; + + @override + String get tradeStatusDisputed => 'En litige'; + + @override + String get releaseButtonLabel => 'LIBÉRER'; + + @override + String get accountScreenTitle => 'Compte'; + + @override + String get secretWordsTitle => 'Mots secrets'; + + @override + String get toRestoreYourAccount => 'Pour restaurer votre compte'; + + @override + String get privacyCardTitle => 'Confidentialité'; + + @override + String get controlPrivacySettings => + 'Gérez vos paramètres de confidentialité'; + + @override + String get reputationMode => 'Mode Réputation'; + + @override + String get reputationModeSubtitle => + 'Confidentialité standard avec réputation'; + + @override + String get fullPrivacyMode => 'Mode Confidentialité Totale'; + + @override + String get fullPrivacyModeSubtitle => 'Anonymat maximal'; + + @override + String get generateNewUserButton => 'Générer un nouvel utilisateur'; + + @override + String get importMostroUserButton => 'Importer un utilisateur Mostro'; + + @override + String get generateNewUserDialogTitle => 'Générer un nouvel utilisateur ?'; + + @override + String get generateNewUserDialogContent => + 'Cela créera une toute nouvelle identité. Vos mots secrets actuels ne fonctionneront plus — assurez-vous de les avoir sauvegardés avant de continuer.'; + + @override + String get continueButtonLabel => 'Continuer'; + + @override + String get importMnemonicDialogTitle => 'Importer le mnémonique'; + + @override + String get importMnemonicHintText => 'Entrez votre phrase de 12 ou 24 mots…'; + + @override + String get importButtonLabel => 'Importer'; + + @override + String get refreshUserDialogTitle => 'Actualiser l\'utilisateur ?'; + + @override + String get refreshUserDialogContent => + 'Cela va récupérer à nouveau vos transactions et ordres depuis l\'instance Mostro. Utilisez cette option si vous pensez que vos données sont désynchronisées ou si des ordres manquent.'; + + @override + String get hideButtonLabel => 'Masquer'; + + @override + String get showButtonLabel => 'Afficher'; + + @override + String get settingsScreenTitle => 'Paramètres'; + + @override + String get languageSettingTitle => 'Langue'; + + @override + String get appearanceSettingTitle => 'Apparence'; + + @override + String get appearanceDialogTitle => 'Apparence'; + + @override + String get defaultFiatCurrencyTitle => 'Devise fiat par défaut'; + + @override + String get allCurrencies => 'Toutes les devises'; + + @override + String get lightningAddressSettingTitle => 'Adresse Lightning'; + + @override + String get tapToSetSubtitle => 'Appuyez pour configurer'; + + @override + String get nwcWalletSettingTitle => 'Portefeuille NWC'; + + @override + String get nwcConnectPrompt => + 'Connectez votre portefeuille Lightning via NWC'; + + @override + String get relaysSettingTitle => 'Relais'; + + @override + String get manageRelayConnections => 'Gérer les connexions de relais'; + + @override + String get pushNotificationsSettingTitle => 'Notifications push'; + + @override + String get manageNotificationPreferences => + 'Gérer les préférences de notifications'; + + @override + String get logReportSettingTitle => 'Rapport de logs'; + + @override + String get viewDiagnosticLogs => 'Voir les logs de diagnostic'; + + @override + String get mostroNodeSettingTitle => 'Nœud Mostro'; + + @override + String get themeDark => 'Sombre'; + + @override + String get themeLight => 'Clair'; + + @override + String get themeSystemDefault => 'Par défaut du système'; + + @override + String get lightningAddressDialogTitle => 'Adresse Lightning'; + + @override + String get lightningAddressHintText => 'utilisateur@domaine.com'; + + @override + String get invalidLightningAddressFormat => + 'Doit être au format utilisateur@domaine'; + + @override + String get clearButtonLabel => 'Effacer'; + + @override + String get saveButtonLabel => 'Enregistrer'; + + @override + String get connectWalletTitle => 'Connecter le portefeuille'; + + @override + String get scanQrCodeTitle => 'Scanner le code QR'; + + @override + String get pasteNwcUri => 'Coller l\'URI NWC'; + + @override + String get selectLanguageTitle => 'Sélectionner la langue'; + + @override + String get selectCurrencyDialogTitle => 'Sélectionner la devise'; + + @override + String get addRelayDialogTitle => 'Ajouter un relais'; + + @override + String get addButtonLabel => 'Ajouter'; + + @override + String get relayHintText => 'wss://relay.example.com'; + + @override + String get relayErrorMustStartWithWss => 'Doit commencer par wss://'; + + @override + String get relayErrorUrlTooShort => 'L\'URL est trop courte'; + + @override + String get relayErrorDuplicate => 'Le relais est déjà dans la liste'; + + @override + String nwcConnectedBalance(String balance) { + return 'NWC — Connecté. Solde : $balance'; + } + + @override + String get pasteQrCodeHeading => 'Coller le contenu du QR code'; + + @override + String get pasteButtonLabel => 'Coller'; + + @override + String get clipboardEmptyError => 'Le presse-papiers est vide'; + + @override + String get enterValueError => 'Veuillez entrer une valeur'; + + @override + String get pasteOrScanQrCode => 'Coller ou scanner un QR code'; + + @override + String get mostroNodeTitle => 'Nœud Mostro'; + + @override + String get currentNodeLabel => 'Nœud actuel'; + + @override + String get trustedBadgeLabel => 'De confiance'; + + @override + String get useDefaultButtonLabel => 'Utiliser le défaut'; + + @override + String get confirmButtonLabel => 'Confirmer'; + + @override + String get invalidHexPubkey => + 'Doit être une chaîne hexadécimale de 64 caractères'; + + @override + String get notificationsScreenTitle => 'Notifications'; + + @override + String get markAllAsReadMenuItem => 'Tout marquer comme lu'; + + @override + String get clearAllMenuItem => 'Tout effacer'; + + @override + String get youMustBackUpYourAccount => 'Vous devez sauvegarder votre compte'; + + @override + String get tapToViewAndSaveSecretWords => + 'Appuyez pour afficher et sauvegarder vos mots secrets.'; + + @override + String get noNotifications => 'Aucune notification'; + + @override + String get markAsRead => 'Marquer comme lu'; + + @override + String get deleteNotificationLabel => 'Supprimer'; + + @override + String get rateScreenHeader => 'NOTER'; + + @override + String get successfulOrder => 'Ordre réussi'; + + @override + String get submitRatingButton => 'SOUMETTRE'; + + @override + String get closeRatingButton => 'FERMER'; + + @override + String get aboutScreenTitle => 'À propos'; + + @override + String get mostroTagline => 'Trading Bitcoin pair-à-pair sur Nostr'; + + @override + String get viewDocumentationButton => 'Voir la documentation'; + + @override + String get linkCopiedToClipboard => 'Lien copié dans le presse-papiers'; + + @override + String get defaultNodeSection => 'Nœud par défaut'; + + @override + String get pubkeyLabel => 'Clé publique'; + + @override + String get relaysLabel => 'Relais'; + + @override + String get pubkeyCopiedToClipboard => + 'Clé publique copiée dans le presse-papiers'; + + @override + String get footerTagline => 'Open-source. Non-custodial. Privé.'; + + @override + String get drawerTitle => 'MOSTRO'; + + @override + String get betaBadgeLabel => 'Bêta'; + + @override + String get drawerAccountMenuItem => 'Compte'; + + @override + String get drawerSettingsMenuItem => 'Paramètres'; + + @override + String get drawerAboutMenuItem => 'À propos'; + + @override + String get navOrderBook => 'Carnet d\'ordres'; + + @override + String get navMyTrades => 'Mes transactions'; + + @override + String get navChat => 'Chat'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 58a7abaa..1ec11d8a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -116,4 +116,464 @@ class AppLocalizationsIt extends AppLocalizations { @override String get disputeLostFundsToSeller => 'L\'amministratore ha annullato l\'ordine e restituito i sats al venditore. Non hai ricevuto i sats.'; + + @override + String get walkthroughSlideOneTitle => + 'Scambia Bitcoin liberamente — senza KYC'; + + @override + String get walkthroughSlideOneBody => + 'Mostro è un exchange peer-to-peer che ti consente di scambiare Bitcoin con qualsiasi valuta e metodo di pagamento — senza KYC e senza dover fornire i tuoi dati a nessuno. È costruito su Nostr, il che lo rende resistente alla censura. Nessuno può impedirti di fare trading.'; + + @override + String get walkthroughSlideTwoTitle => 'Privacy per impostazione predefinita'; + + @override + String get walkthroughSlideTwoBody => + 'Mostro genera una nuova identità per ogni scambio, in modo che le tue operazioni non possano essere collegate. Puoi anche decidere quanto vuoi essere privato:\n• Modalità reputazione – Consente agli altri di vedere le tue operazioni riuscite e il tuo livello di fiducia.\n• Modalità privacy totale – Non viene costruita alcuna reputazione, ma la tua attività è completamente anonima.\nCambia modalità in qualsiasi momento dalla schermata Account, dove dovresti anche salvare le tue parole segrete — sono l\'unico modo per recuperare il tuo account.'; + + @override + String get walkthroughSlideThreeTitle => 'Sicurezza ad ogni passo'; + + @override + String get walkthroughSlideThreeBody => + 'Mostro utilizza Hold Invoice (fatture trattenute): i sats rimangono nel portafoglio del venditore fino alla fine dello scambio. Questo protegge entrambe le parti. L\'app è anche progettata per essere intuitiva e facile da usare per ogni tipo di utente.'; + + @override + String get walkthroughSlideFourTitle => 'Chat completamente cifrata'; + + @override + String get walkthroughSlideFourBody => + 'Ogni operazione ha la propria chat privata, cifrata end-to-end. Solo i due utenti coinvolti possono leggerla. In caso di disputa, puoi fornire la chiave condivisa a un amministratore per aiutare a risolvere il problema.'; + + @override + String get walkthroughSlideFiveTitle => 'Prendi un\'offerta'; + + @override + String get walkthroughSlideFiveBody => + 'Sfoglia il book degli ordini, scegli un\'offerta adatta a te e segui il flusso dell\'operazione passo dopo passo. Potrai controllare il profilo dell\'altro utente, chattare in sicurezza e completare l\'operazione con facilità.'; + + @override + String get walkthroughSlideSixTitle => 'Non trovi quello che cerchi?'; + + @override + String get walkthroughSlideSixBody => + 'Puoi anche creare la tua offerta e aspettare che qualcuno la accetti. Imposta l\'importo e il metodo di pagamento preferito — Mostro pensa al resto.'; + + @override + String get tabBuyBtc => 'COMPRA BTC'; + + @override + String get tabSellBtc => 'VENDI BTC'; + + @override + String get filterButtonLabel => 'FILTRA'; + + @override + String offersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count offerte', + one: '1 offerta', + ); + return '$_temp0'; + } + + @override + String get noOrdersAvailable => 'Nessun ordine disponibile'; + + @override + String get justNow => 'Proprio ora'; + + @override + String minutesAgo(int m) { + return '${m}m fa'; + } + + @override + String hoursAgo(int h) { + return '${h}h fa'; + } + + @override + String daysAgo(int d) { + return '${d}g fa'; + } + + @override + String get creatingNewOrderTitle => 'CREAZIONE NUOVO ORDINE'; + + @override + String get youWantToBuyBitcoin => 'Vuoi acquistare Bitcoin'; + + @override + String get youWantToSellBitcoin => 'Vuoi vendere Bitcoin'; + + @override + String get rangeOrderLabel => 'Ordine a intervallo'; + + @override + String get payLightningInvoiceTitle => 'Paga Fattura Lightning'; + + @override + String get invoiceCopied => 'Fattura copiata'; + + @override + String get addInvoiceTitle => 'Aggiungi Fattura'; + + @override + String get submitButtonLabel => 'Invia'; + + @override + String get orderAlreadyTaken => 'L\'ordine è già stato preso'; + + @override + String get orderIdCopied => 'ID ordine copiato'; + + @override + String get orderDetailsTitle => 'DETTAGLI ORDINE'; + + @override + String get timeRemainingLabel => 'Tempo rimanente:'; + + @override + String get fiatSentButtonLabel => 'FIAT INVIATO'; + + @override + String get disputeButtonLabel => 'DISPUTA'; + + @override + String get contactButtonLabel => 'CONTATTA'; + + @override + String get rateButtonLabel => 'VALUTA'; + + @override + String get viewDisputeButtonLabel => 'VEDI DISPUTA'; + + @override + String get comingSoonMessage => 'Prossimamente'; + + @override + String get tradeStatusActive => 'Attivo'; + + @override + String get tradeStatusFiatSent => 'Fiat inviato'; + + @override + String get tradeStatusCompleted => 'Completato'; + + @override + String get tradeStatusCancelled => 'Annullato'; + + @override + String get tradeStatusDisputed => 'In disputa'; + + @override + String get releaseButtonLabel => 'RILASCIA'; + + @override + String get accountScreenTitle => 'Account'; + + @override + String get secretWordsTitle => 'Parole segrete'; + + @override + String get toRestoreYourAccount => 'Per ripristinare il tuo account'; + + @override + String get privacyCardTitle => 'Privacy'; + + @override + String get controlPrivacySettings => 'Gestisci le impostazioni sulla privacy'; + + @override + String get reputationMode => 'Modalità Reputazione'; + + @override + String get reputationModeSubtitle => 'Privacy standard con reputazione'; + + @override + String get fullPrivacyMode => 'Modalità Privacy Totale'; + + @override + String get fullPrivacyModeSubtitle => 'Anonimato massimo'; + + @override + String get generateNewUserButton => 'Genera nuovo utente'; + + @override + String get importMostroUserButton => 'Importa utente Mostro'; + + @override + String get generateNewUserDialogTitle => 'Generare nuovo utente?'; + + @override + String get generateNewUserDialogContent => + 'Verrà creata una nuova identità. Le tue parole segrete attuali non funzioneranno più — assicurati di averle salvate prima di continuare.'; + + @override + String get continueButtonLabel => 'Continua'; + + @override + String get importMnemonicDialogTitle => 'Importa Mnemonica'; + + @override + String get importMnemonicHintText => + 'Inserisci la tua frase da 12 o 24 parole…'; + + @override + String get importButtonLabel => 'Importa'; + + @override + String get refreshUserDialogTitle => 'Aggiornare utente?'; + + @override + String get refreshUserDialogContent => + 'Verranno recuperate le tue operazioni e gli ordini dall\'istanza Mostro. Usalo se pensi che i tuoi dati non siano sincronizzati o manchino degli ordini.'; + + @override + String get hideButtonLabel => 'Nascondi'; + + @override + String get showButtonLabel => 'Mostra'; + + @override + String get settingsScreenTitle => 'Impostazioni'; + + @override + String get languageSettingTitle => 'Lingua'; + + @override + String get appearanceSettingTitle => 'Aspetto'; + + @override + String get appearanceDialogTitle => 'Aspetto'; + + @override + String get defaultFiatCurrencyTitle => 'Valuta fiat predefinita'; + + @override + String get allCurrencies => 'Tutte le valute'; + + @override + String get lightningAddressSettingTitle => 'Indirizzo Lightning'; + + @override + String get tapToSetSubtitle => 'Tocca per impostare'; + + @override + String get nwcWalletSettingTitle => 'Portafoglio NWC'; + + @override + String get nwcConnectPrompt => + 'Collega il tuo portafoglio Lightning tramite NWC'; + + @override + String get relaysSettingTitle => 'Relay'; + + @override + String get manageRelayConnections => 'Gestisci connessioni relay'; + + @override + String get pushNotificationsSettingTitle => 'Notifiche push'; + + @override + String get manageNotificationPreferences => 'Gestisci preferenze notifiche'; + + @override + String get logReportSettingTitle => 'Registro diagnostico'; + + @override + String get viewDiagnosticLogs => 'Visualizza log diagnostici'; + + @override + String get mostroNodeSettingTitle => 'Nodo Mostro'; + + @override + String get themeDark => 'Scuro'; + + @override + String get themeLight => 'Chiaro'; + + @override + String get themeSystemDefault => 'Predefinito di sistema'; + + @override + String get lightningAddressDialogTitle => 'Indirizzo Lightning'; + + @override + String get lightningAddressHintText => 'utente@dominio.com'; + + @override + String get invalidLightningAddressFormat => + 'Deve essere nel formato utente@dominio'; + + @override + String get clearButtonLabel => 'Cancella'; + + @override + String get saveButtonLabel => 'Salva'; + + @override + String get connectWalletTitle => 'Collega portafoglio'; + + @override + String get scanQrCodeTitle => 'Scansiona codice QR'; + + @override + String get pasteNwcUri => 'Incolla URI NWC'; + + @override + String get selectLanguageTitle => 'Seleziona lingua'; + + @override + String get selectCurrencyDialogTitle => 'Seleziona valuta'; + + @override + String get addRelayDialogTitle => 'Aggiungi relay'; + + @override + String get addButtonLabel => 'Aggiungi'; + + @override + String get relayHintText => 'wss://relay.example.com'; + + @override + String get relayErrorMustStartWithWss => 'Deve iniziare con wss://'; + + @override + String get relayErrorUrlTooShort => 'L\'URL è troppo corta'; + + @override + String get relayErrorDuplicate => 'Relay già presente nella lista'; + + @override + String nwcConnectedBalance(String balance) { + return 'NWC — Connesso. Saldo: $balance'; + } + + @override + String get pasteQrCodeHeading => 'Incolla contenuto del codice QR'; + + @override + String get pasteButtonLabel => 'Incolla'; + + @override + String get clipboardEmptyError => 'Gli appunti sono vuoti'; + + @override + String get enterValueError => 'Inserisci un valore'; + + @override + String get pasteOrScanQrCode => 'Incolla o scansiona un codice QR'; + + @override + String get mostroNodeTitle => 'Nodo Mostro'; + + @override + String get currentNodeLabel => 'Nodo attuale'; + + @override + String get trustedBadgeLabel => 'Affidabile'; + + @override + String get useDefaultButtonLabel => 'Usa predefinito'; + + @override + String get confirmButtonLabel => 'Conferma'; + + @override + String get invalidHexPubkey => + 'Deve essere una stringa esadecimale di 64 caratteri'; + + @override + String get notificationsScreenTitle => 'Notifiche'; + + @override + String get markAllAsReadMenuItem => 'Segna tutto come letto'; + + @override + String get clearAllMenuItem => 'Cancella tutto'; + + @override + String get youMustBackUpYourAccount => + 'Devi eseguire il backup del tuo account'; + + @override + String get tapToViewAndSaveSecretWords => + 'Tocca per visualizzare e salvare le tue parole segrete.'; + + @override + String get noNotifications => 'Nessuna notifica'; + + @override + String get markAsRead => 'Segna come letto'; + + @override + String get deleteNotificationLabel => 'Elimina'; + + @override + String get rateScreenHeader => 'VALUTA'; + + @override + String get successfulOrder => 'Ordine riuscito'; + + @override + String get submitRatingButton => 'INVIA'; + + @override + String get closeRatingButton => 'CHIUDI'; + + @override + String get aboutScreenTitle => 'Informazioni'; + + @override + String get mostroTagline => 'Trading Bitcoin peer-to-peer su Nostr'; + + @override + String get viewDocumentationButton => 'Visualizza documentazione'; + + @override + String get linkCopiedToClipboard => 'Link copiato negli appunti'; + + @override + String get defaultNodeSection => 'Nodo predefinito'; + + @override + String get pubkeyLabel => 'Chiave pubblica'; + + @override + String get relaysLabel => 'Relay'; + + @override + String get pubkeyCopiedToClipboard => 'Chiave pubblica copiata negli appunti'; + + @override + String get footerTagline => 'Open-source. Non custodiale. Privato.'; + + @override + String get drawerTitle => 'MOSTRO'; + + @override + String get betaBadgeLabel => 'Beta'; + + @override + String get drawerAccountMenuItem => 'Account'; + + @override + String get drawerSettingsMenuItem => 'Impostazioni'; + + @override + String get drawerAboutMenuItem => 'Informazioni'; + + @override + String get navOrderBook => 'Book ordini'; + + @override + String get navMyTrades => 'Le mie operazioni'; + + @override + String get navChat => 'Chat'; } diff --git a/lib/shared/widgets/bottom_nav_bar.dart b/lib/shared/widgets/bottom_nav_bar.dart index 1b0b0671..95ac0439 100644 --- a/lib/shared/widgets/bottom_nav_bar.dart +++ b/lib/shared/widgets/bottom_nav_bar.dart @@ -20,6 +20,11 @@ class BottomNavBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // On desktop the persistent sidebar provides navigation; hide bottom nav. + if (MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop) { + return const SizedBox.shrink(); + } + final currentIndex = ref.watch(bottomNavIndexProvider); final colors = Theme.of(context).extension(); final green = colors?.mostroGreen ?? const Color(0xFF8CC63F); diff --git a/lib/shared/widgets/countdown_timer.dart b/lib/shared/widgets/countdown_timer.dart new file mode 100644 index 00000000..ed53568d --- /dev/null +++ b/lib/shared/widgets/countdown_timer.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:mostro/core/app_theme.dart'; + +/// Circular countdown timer widget. +/// +/// Displays remaining time as "HH:MM:SS" centred inside a +/// [CircularProgressIndicator]. The progress arc transitions: +/// - green (> 33 % remaining) +/// - yellow (10 %–33 % remaining) +/// - red (≤ 10 % remaining or expired) +/// +/// Used on the Take Order screen (order expiry) and Trade Detail screen +/// (trade step timeout). +class CountdownTimer extends StatefulWidget { + const CountdownTimer({ + super.key, + required this.duration, + this.onExpired, + }); + + /// Total countdown duration. + final Duration duration; + + /// Called once when the timer reaches zero. + final VoidCallback? onExpired; + + @override + State createState() => _CountdownTimerState(); +} + +class _CountdownTimerState extends State { + late Duration _remaining; + Timer? _timer; + + @override + void initState() { + super.initState(); + _remaining = widget.duration; + if (_remaining <= Duration.zero) { + // Fire immediately on the next frame so listeners are attached. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) widget.onExpired?.call(); + }); + } else { + _start(); + } + } + + @override + void didUpdateWidget(CountdownTimer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.duration != oldWidget.duration) { + _timer?.cancel(); + _remaining = widget.duration; + if (_remaining <= Duration.zero) { + _remaining = Duration.zero; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) widget.onExpired?.call(); + }); + } else { + _start(); + } + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _start() { + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + if (!mounted) return; + setState(() { + _remaining -= const Duration(seconds: 1); + if (_remaining <= Duration.zero) { + _remaining = Duration.zero; + _timer?.cancel(); + widget.onExpired?.call(); + } + }); + }); + } + + Color _color(AppColors colors) { + final total = widget.duration.inSeconds; + if (total == 0) return colors.destructiveRed; + final ratio = _remaining.inSeconds / total; + if (ratio > 0.33) return colors.mostroGreen; + if (ratio > 0.10) return const Color(0xFFFFD700); + return colors.destructiveRed; + } + + String _label() { + final d = _remaining.isNegative ? Duration.zero : _remaining; + final h = d.inHours.toString().padLeft(2, '0'); + final m = (d.inMinutes % 60).toString().padLeft(2, '0'); + final s = (d.inSeconds % 60).toString().padLeft(2, '0'); + return '$h:$m:$s'; + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).extension(); + if (colors == null) { + throw StateError('AppColors theme extension must be registered'); + } + final total = widget.duration.inSeconds; + final progress = total > 0 ? _remaining.inSeconds / total : 0.0; + + return SizedBox( + width: 80, + height: 80, + child: Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: progress.clamp(0.0, 1.0), + strokeWidth: 5, + backgroundColor: colors.backgroundCard, + valueColor: AlwaysStoppedAnimation(_color(colors)), + ), + Text( + _label(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/lib/shared/widgets/platform_aware_qr_scanner.dart b/lib/shared/widgets/platform_aware_qr_scanner.dart new file mode 100644 index 00000000..01bf67f7 --- /dev/null +++ b/lib/shared/widgets/platform_aware_qr_scanner.dart @@ -0,0 +1,189 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/l10n/app_localizations.dart'; + +/// Platform-aware QR scanner. +/// +/// On **iOS, Android, and desktop** (non-web): opens the device camera using +/// `mobile_scanner`. +/// On **web**: shows a paste-from-clipboard text field — camera access +/// requires HTTPS and a user gesture that differs across browsers; clipboard +/// paste is the reliable fallback. +/// +/// [onDetected] is called exactly once with the decoded string as soon as a +/// QR code is scanned or the user submits pasted content. +class PlatformAwareQrScanner extends StatefulWidget { + const PlatformAwareQrScanner({ + super.key, + required this.onDetected, + this.hint = 'Paste or scan a QR code', + }); + + /// Called with the raw string value when a QR code is detected or submitted. + final void Function(String value) onDetected; + + /// Placeholder text shown in the paste field (web only). + final String hint; + + @override + State createState() => _PlatformAwareQrScannerState(); +} + +class _PlatformAwareQrScannerState extends State { + final _controller = TextEditingController(); + String? _errorText; + bool _hasEmitted = false; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _emitOnce(String value) { + if (_hasEmitted) return; + _hasEmitted = true; + widget.onDetected(value); + } + + Future _pasteFromClipboard() async { + final data = await Clipboard.getData(Clipboard.kTextPlain); + final text = data?.text?.trim() ?? ''; + if (text.isEmpty) { + if (mounted) setState(() => _errorText = AppLocalizations.of(context).clipboardEmptyError); + return; + } + setState(() => _errorText = null); + _emitOnce(text); + } + + void _submit() { + final text = _controller.text.trim(); + if (text.isEmpty) { + setState(() => _errorText = AppLocalizations.of(context).enterValueError); + return; + } + _emitOnce(text); + } + + @override + Widget build(BuildContext context) { + if (kIsWeb) { + return _WebFallback( + controller: _controller, + errorText: _errorText, + hint: widget.hint, + onChanged: (_) { + if (_errorText != null) setState(() => _errorText = null); + }, + onPaste: _pasteFromClipboard, + onSubmit: _submit, + ); + } + return _CameraScanner(onDetected: widget.onDetected); + } +} + +// ── Camera scanner (native / desktop) ──────────────────────────────────────── + +class _CameraScanner extends StatefulWidget { + const _CameraScanner({required this.onDetected}); + final void Function(String) onDetected; + + @override + State<_CameraScanner> createState() => _CameraScannerState(); +} + +class _CameraScannerState extends State<_CameraScanner> { + bool _detected = false; + + @override + Widget build(BuildContext context) { + return MobileScanner( + onDetect: (capture) { + if (_detected) return; + final raw = capture.barcodes.firstOrNull?.rawValue?.trim(); + if (raw != null && raw.isNotEmpty) { + _detected = true; + widget.onDetected(raw); + } + }, + ); + } +} + +// ── Web fallback (paste / type) ─────────────────────────────────────────────── + +class _WebFallback extends StatelessWidget { + const _WebFallback({ + required this.controller, + required this.errorText, + required this.hint, + required this.onChanged, + required this.onPaste, + required this.onSubmit, + }); + + final TextEditingController controller; + final String? errorText; + final String hint; + final ValueChanged onChanged; + final VoidCallback onPaste; + final VoidCallback onSubmit; + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).extension(); + if (colors == null) { + throw StateError('AppColors theme extension must be registered'); + } + + return Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + AppLocalizations.of(context).pasteQrCodeHeading, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: AppSpacing.md), + TextField( + controller: controller, + decoration: InputDecoration( + hintText: hint, + errorText: errorText, + ), + autocorrect: false, + enableSuggestions: false, + onChanged: onChanged, + ), + const SizedBox(height: AppSpacing.md), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: onPaste, + icon: const Icon(Icons.content_paste), + label: Text(AppLocalizations.of(context).pasteButtonLabel), + ), + ), + const SizedBox(width: AppSpacing.md), + Expanded( + child: FilledButton( + onPressed: onSubmit, + child: Text(AppLocalizations.of(context).submitButtonLabel), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/rust/src/api/nostr.rs b/rust/src/api/nostr.rs index 4f902191..2f2a660c 100644 --- a/rust/src/api/nostr.rs +++ b/rust/src/api/nostr.rs @@ -3,11 +3,13 @@ /// Thin facade over `RelayPool` — keeps all async/relay logic in the pool /// while exposing a flat function interface for the Dart side. use anyhow::Result; +use nostr_sdk::Event; use std::sync::Arc; use tokio::sync::OnceCell; use crate::api::types::{ConnectionState, RelayInfo}; use crate::nostr::relay_pool::RelayPool; +use crate::queue::outbox; /// Global relay pool singleton, initialised once by `initialize()`. static POOL: OnceCell> = OnceCell::const_new(); @@ -37,6 +39,23 @@ pub async fn initialize(relays: Option>) -> Result<()> { // two race past the is_some() guard above. POOL.get_or_try_init(|| async { RelayPool::new(urls).await }) .await?; + + // Spawn a background task that flushes the outbox whenever the relay pool + // transitions to Online. The task exits when the broadcast channel closes. + let pool_ref = POOL.get().unwrap().clone(); + tokio::spawn(async move { + let mut rx = pool_ref.subscribe_connection_state(); + loop { + match rx.recv().await { + Ok(ConnectionState::Online) => { + let _ = flush_message_queue().await; + } + Err(_) => break, + _ => {} + } + } + }); + Ok(()) } @@ -61,12 +80,28 @@ pub async fn get_connection_state() -> Result { } /// Attempt to send all queued offline messages. -/// Returns count of successfully flushed messages. /// -/// Not yet implemented — requires the persistence layer from Phase 7. +/// Iterates the in-memory outbox, publishes each pending event via the relay +/// pool, and applies exponential backoff on failure. Events are pruned once +/// sent or after [`MAX_RETRIES`] failures. +/// +/// Returns the count of messages successfully published in this pass. pub async fn flush_message_queue() -> Result { - let _pool = pool()?; - Err(anyhow::anyhow!("NotImplemented: queue persistence not wired yet")) + let client = pool()?.client(); + let sent = outbox::outbox() + .flush(|event_json| { + let client = client.clone(); + async move { + let event: Event = serde_json::from_str(&event_json)?; + client + .send_event(&event) + .await + .map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(()) + } + }) + .await; + Ok(sent) } // ── Streams ───────────────────────────────────────────────────────────────── @@ -127,6 +162,7 @@ fn default_relays() -> Vec { } /// Provide access to the global pool for other Rust modules (e.g. orders API). +#[allow(dead_code)] pub(crate) fn get_pool() -> Result<&'static Arc> { pool() } diff --git a/rust/src/api/orders.rs b/rust/src/api/orders.rs index b8c0c137..e1c2c94c 100644 --- a/rust/src/api/orders.rs +++ b/rust/src/api/orders.rs @@ -25,6 +25,10 @@ pub struct OrderBook { tx: broadcast::Sender>, } +impl Default for OrderBook { + fn default() -> Self { Self::new() } +} + impl OrderBook { pub fn new() -> Self { let (tx, _) = broadcast::channel(16); diff --git a/rust/src/api/reputation.rs b/rust/src/api/reputation.rs index 0619cf0f..e2a6af58 100644 --- a/rust/src/api/reputation.rs +++ b/rust/src/api/reputation.rs @@ -9,7 +9,7 @@ /// /// All state is held in-memory until the DB persistence layer is wired /// (Phase 12+). -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use std::collections::HashMap; use std::sync::{atomic::{AtomicBool, Ordering}, OnceLock}; use tokio::sync::{broadcast, RwLock}; @@ -115,7 +115,7 @@ fn unix_now() -> i64 { /// /// **Errors**: `InvalidScore`, `PrivacyModeEnabled`, `AlreadyRated`. pub async fn submit_rating(trade_id: String, score: u8) -> Result<()> { - if score < 1 || score > 5 { + if !(1u8..=5).contains(&score) { bail!("InvalidScore: score must be between 1 and 5, got {score}"); } @@ -177,7 +177,7 @@ pub async fn handle_rating_received( score: u8, from_pubkey: String, ) -> Result<()> { - if score < 1 || score > 5 { + if !(1u8..=5).contains(&score) { bail!("InvalidScore: received invalid score {score} for trade {trade_id}"); } diff --git a/rust/src/api/settings.rs b/rust/src/api/settings.rs index f5f395ff..1ab569ca 100644 --- a/rust/src/api/settings.rs +++ b/rust/src/api/settings.rs @@ -45,7 +45,7 @@ impl SettingsStore { F: FnOnce(&mut AppSettings), { let mut guard = self.settings.write().await; - f(&mut *guard); + f(&mut guard); let mut snapshot = guard.clone(); snapshot.privacy_mode = crate::api::reputation::get_privacy_mode(); snapshot @@ -100,17 +100,33 @@ fn validate_fiat_code(code: &str) -> Result<()> { /// Validates a Lightning Address in `user@domain` format. /// -/// Requires exactly one `@` with a non-empty local part and domain. +/// Requires exactly one `@`, a non-empty local part, and a domain that +/// contains at least one dot with no empty labels and only ASCII +/// alphanumeric/hyphen characters. fn validate_lightning_address(address: &str) -> Result<()> { - let parts: Vec<&str> = address.split('@').collect(); + let trimmed = address.trim(); + let parts: Vec<&str> = trimmed.split('@').collect(); if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() { - Ok(()) - } else { - bail!( - "InvalidLightningAddress: '{}' must be in user@domain format", - address - ) + let domain = parts[1]; + let labels: Vec<&str> = domain.split('.').collect(); + let valid_domain = labels.len() >= 2 + && labels.iter().all(|label| { + !label.is_empty() + && label.len() <= 63 + && label + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-') + && !label.starts_with('-') + && !label.ends_with('-') + }); + if valid_domain { + return Ok(()); + } } + bail!( + "InvalidLightningAddress: '{}' must be in user@domain.tld format", + address + ) } // ── Public API ──────────────────────────────────────────────────────────────── @@ -153,11 +169,12 @@ pub async fn set_default_fiat_code(code: Option) -> Result<()> { /// /// **Errors**: `InvalidLightningAddress` if `address` is Some but malformed. pub async fn set_default_lightning_address(address: Option) -> Result<()> { - if let Some(ref a) = address { + let normalized = address.map(|a| a.trim().to_string()); + if let Some(ref a) = normalized { validate_lightning_address(a)?; } let snapshot = store() - .write_with(|s| s.default_lightning_address = address) + .write_with(|s| s.default_lightning_address = normalized) .await; store().notify(snapshot); Ok(()) diff --git a/rust/src/api/types.rs b/rust/src/api/types.rs index 056062b6..4a173aa3 100644 --- a/rust/src/api/types.rs +++ b/rust/src/api/types.rs @@ -125,6 +125,8 @@ pub enum ConnectionState { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum QueuedMessageStatus { Pending, + /// Currently being published — prevents duplicate flush attempts. + InFlight, Sent, Failed, } diff --git a/rust/src/config.rs b/rust/src/config.rs index 9be2b39f..443d9fbf 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,7 +1,7 @@ -/// Default configuration constants for the Mostro network. -/// -/// These are compiled into the app and used on first launch when no -/// user-configured relays or Mostro node exist in the database. +//! Default configuration constants for the Mostro network. +//! +//! These are compiled into the app and used on first launch when no +//! user-configured relays or Mostro node exist in the database. /// Default relay URLs seeded on first launch. pub const DEFAULT_RELAYS: &[&str] = &[ diff --git a/rust/src/crypto/keys.rs b/rust/src/crypto/keys.rs index fd9c9390..8ea10432 100644 --- a/rust/src/crypto/keys.rs +++ b/rust/src/crypto/keys.rs @@ -57,7 +57,7 @@ fn derive_at_index(mnemonic_words: &[String], index: u32) -> Result { .parse() .map_err(|e| anyhow!("derivation path parse: {e}"))?; - let xprv = XPrv::derive_from_path(&seed, &path) + let xprv = XPrv::derive_from_path(seed, &path) .map_err(|e| anyhow!("BIP-32 derive error: {e}"))?; // k256 signing key → raw 32-byte secret diff --git a/rust/src/mostro/session.rs b/rust/src/mostro/session.rs index 9e1314c3..62bd85d9 100644 --- a/rust/src/mostro/session.rs +++ b/rust/src/mostro/session.rs @@ -49,6 +49,10 @@ pub struct SessionManager { sessions: Arc>>, } +impl Default for SessionManager { + fn default() -> Self { Self::new() } +} + impl SessionManager { pub fn new() -> Self { Self { diff --git a/rust/src/nwc/client.rs b/rust/src/nwc/client.rs index 4d9272df..6b0b597d 100644 --- a/rust/src/nwc/client.rs +++ b/rust/src/nwc/client.rs @@ -126,6 +126,7 @@ fn urlencoding_decode(s: &str) -> String { pub struct NwcClient { pub info: NwcWalletInfo, /// NWC client secret key (hex) — used to sign NIP-47 requests. + #[allow(dead_code)] pub(super) secret_hex: String, } @@ -172,7 +173,7 @@ impl NwcClient { /// /// TODO(Phase 15+): Construct and send a signed `pay_invoice` NIP-47 /// request, wait for the response event, and return the preimage. - pub async fn pay_invoice(&self, bolt11: &str) -> Result { + pub async fn pay_invoice(&self, _bolt11: &str) -> Result { if self.info.status != WalletStatus::Connected { return Ok(PaymentResult { success: false, diff --git a/rust/src/queue/outbox.rs b/rust/src/queue/outbox.rs index 5622ac48..afbe3b3a 100644 --- a/rust/src/queue/outbox.rs +++ b/rust/src/queue/outbox.rs @@ -1,6 +1,13 @@ -use crate::api::types::QueuedMessageStatus; +use std::sync::{Mutex, OnceLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Result; use serde::{Deserialize, Serialize}; +use crate::api::types::QueuedMessageStatus; + +// ── QueuedMessage ───────────────────────────────────────────────────────────── + /// A Nostr event waiting to be published when connectivity is restored. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueuedMessage { @@ -11,6 +18,7 @@ pub struct QueuedMessage { pub status: QueuedMessageStatus, pub created_at: i64, pub retry_count: u32, + /// Unix-seconds timestamp before which the message must not be retried. pub next_retry_at: Option, } @@ -26,10 +34,216 @@ impl QueuedMessage { } } - /// Backoff in seconds: 30s × 2^retry_count, capped at 3600s. + /// Backoff in seconds: 30s × 2^retry_count, capped at 3 600s. pub fn next_retry_delay_secs(&self) -> i64 { let base: i64 = 30; let delay = base * (1i64 << self.retry_count.min(7)); delay.min(3600) } } + +// ── MessageOutbox (in-memory) ───────────────────────────────────────────────── + +/// Max retry attempts before a message is considered permanently failed. +const MAX_RETRIES: u32 = 10; + +/// In-memory message outbox. +/// +/// Persistence to SQLite is wired in Phase 18+ (requires DB initialisation +/// to be threaded through to this module). Until then messages survive only +/// for the current app session. +pub struct MessageOutbox { + queue: Mutex>, +} + +impl MessageOutbox { + fn new() -> Self { + Self { + queue: Mutex::new(Vec::new()), + } + } + + /// Add an event to the outbox. + pub fn enqueue(&self, event_json: String) { + let now = unix_now(); + let msg = QueuedMessage::new(event_json, now); + self.queue + .lock() + .unwrap_or_else(|e| e.into_inner()) + .push(msg); + } + + /// Attempt to publish all pending messages. + /// + /// For each `Pending` message whose `next_retry_at` has elapsed: + /// - Calls `publish_fn` with the serialised event JSON. + /// - On success → marks `Sent`. + /// - On failure → increments `retry_count`, schedules next retry with + /// exponential backoff. After [`MAX_RETRIES`] failures → marks `Failed`. + /// + /// After flushing, prunes all `Sent` messages and `Failed` messages older + /// than 24 hours. + /// + /// Returns the count of successfully published messages in this pass. + pub async fn flush(&self, publish_fn: F) -> u32 + where + F: Fn(String) -> Fut, + Fut: std::future::Future>, + { + let now = unix_now(); + + // Snapshot messages that are due for a retry attempt, marking them + // InFlight so concurrent flush() calls skip them. + let pending: Vec = { + let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner()); + let mut snapshot = Vec::new(); + for m in q.iter_mut() { + if m.status == QueuedMessageStatus::Pending + && m.next_retry_at.is_none_or(|t| now >= t) + { + m.status = QueuedMessageStatus::InFlight; + snapshot.push(m.clone()); + } + } + snapshot + }; + + let mut sent = 0u32; + + for mut msg in pending { + match publish_fn(msg.event_json.clone()).await { + Ok(()) => { + msg.status = QueuedMessageStatus::Sent; + sent += 1; + } + Err(_) => { + msg.retry_count += 1; + if msg.retry_count >= MAX_RETRIES { + msg.status = QueuedMessageStatus::Failed; + } else { + msg.status = QueuedMessageStatus::Pending; + msg.next_retry_at = Some(now + msg.next_retry_delay_secs()); + } + } + } + + // Write back the updated entry. + let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(entry) = q.iter_mut().find(|m| m.id == msg.id) { + *entry = msg; + } + } + + // Prune: sent items always; failed items after 24 hours. + // InFlight messages are preserved (another concurrent flush owns them). + let cutoff = now - 86_400; + self.queue.lock().unwrap_or_else(|e| e.into_inner()).retain(|m| { + m.status == QueuedMessageStatus::Pending + || m.status == QueuedMessageStatus::InFlight + || (m.status == QueuedMessageStatus::Failed && m.created_at > cutoff) + }); + + sent + } + + /// Count of messages currently in the queue (all statuses). + pub fn len(&self) -> usize { + self.queue.lock().unwrap_or_else(|e| e.into_inner()).len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +// ── Global singleton ────────────────────────────────────────────────────────── + +static OUTBOX: OnceLock = OnceLock::new(); + +pub fn outbox() -> &'static MessageOutbox { + OUTBOX.get_or_init(MessageOutbox::new) +} + +/// Add a serialised Nostr event to the persistent outbox for deferred delivery. +pub fn queue_message(event_json: String) { + outbox().enqueue(event_json); +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +fn unix_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn fresh_outbox() -> MessageOutbox { + MessageOutbox::new() + } + + #[tokio::test] + async fn enqueue_and_flush_success() { + let ob = fresh_outbox(); + ob.enqueue("{}".to_string()); + assert_eq!(ob.len(), 1); + + let sent = ob.flush(|_| async { Ok(()) }).await; + assert_eq!(sent, 1); + // Sent items are pruned. + assert_eq!(ob.len(), 0); + } + + #[tokio::test] + async fn flush_failure_increments_retry_count() { + let ob = fresh_outbox(); + ob.enqueue("{}".to_string()); + + let sent = ob + .flush(|_| async { Err(anyhow::anyhow!("connection refused")) }) + .await; + assert_eq!(sent, 0); + let q = ob.queue.lock().unwrap(); + assert_eq!(q[0].retry_count, 1); + assert_eq!(q[0].status, QueuedMessageStatus::Pending); + } + + #[tokio::test] + async fn message_marked_failed_after_max_retries() { + let ob = fresh_outbox(); + ob.enqueue("{}".to_string()); + // Force retry_count to MAX_RETRIES - 1 so the next failure tips it over. + { + let mut q = ob.queue.lock().unwrap(); + q[0].retry_count = MAX_RETRIES - 1; + } + + ob.flush(|_| async { Err(anyhow::anyhow!("fail")) }) + .await; + + let q = ob.queue.lock().unwrap(); + assert_eq!(q[0].status, QueuedMessageStatus::Failed); + } + + #[tokio::test] + async fn backoff_delay_is_applied() { + let ob = fresh_outbox(); + ob.enqueue("{}".to_string()); + + // First failure → retry_count = 1, next_retry_at set in the future. + ob.flush(|_| async { Err(anyhow::anyhow!("fail")) }) + .await; + + let q = ob.queue.lock().unwrap(); + let msg = &q[0]; + assert!(msg.next_retry_at.is_some()); + // With retry_count 1 the delay should be 60 s (30 × 2^1). + assert_eq!(msg.next_retry_delay_secs(), 60); + } +} diff --git a/specs/004-mostro-p2p-client/tasks.md b/specs/004-mostro-p2p-client/tasks.md index 7ec18abd..fbb5a405 100644 --- a/specs/004-mostro-p2p-client/tasks.md +++ b/specs/004-mostro-p2p-client/tasks.md @@ -384,14 +384,14 @@ configuration. **Purpose**: Finalize localization, responsive layouts, theme, platform-specific features, offline behavior. -- [ ] T118 Complete all ARB localization files: populate all strings in `assets/l10n/app_en.arb` from all feature screens, then translate to `app_es.arb`, `app_it.arb`, `app_fr.arb`, `app_de.arb`. Include walkthrough highlight terms in all 5 languages for `highlight_config.dart`. Run `flutter gen-l10n`. Zero untranslated strings. -- [ ] T119 Implement responsive layout breakpoints in shared widgets (mobile < 600px, tablet 600–1200px, desktop > 1200px): order book shows 1-column on mobile, 2-column on tablet, 3-column on desktop. Drawer becomes persistent sidebar on desktop. Chat room shows side-panel info on tablet+. All per Constitution Principle V. -- [ ] T120 [P] Wire theme toggle (dark/light/system) throughout: `set_theme()` → `AppTheme.darkTheme` / `AppTheme.lightTheme` / `ThemeMode.system` in `MaterialApp.router`. Defaults to System on first launch (dark appearance on most devices). Switchable from Settings. Both themes fully functional. -- [ ] T121 [P] Implement platform-aware QR scanner wrapper in `lib/shared/widgets/platform_aware_qr_scanner.dart`: on iOS/Android/desktop → camera via `mobile_scanner`. On web → paste-from-clipboard text field (no camera access). Used in Connect Wallet screen and anywhere QR scanning is needed. -- [ ] T122 [P] Implement graceful degradation for platform features: push notifications (FCM/APNs/Web Push) → fallback to polling on unsupported platforms. Camera / QR scan → paste-only fallback on web. Biometric → disabled if hardware unavailable. All per Constitution Principle V. -- [ ] T123 Wire offline message queue flushing: in `rust/src/api/nostr.rs` `flush_message_queue()` called automatically on `on_connection_state_changed(Online)` event. `outbox.rs` retries up to 10 attempts, backs off exponentially, prunes sent items. -- [ ] T124 [P] Implement countdown timer widget in `lib/shared/widgets/countdown_timer.dart`: circular progress indicator showing remaining time as "HH:MM:SS". Color-codes as time runs low (green → yellow → red thresholds). Used on Take Order screen (order expiry) and Trade Detail screen (trade step timeout). -- [ ] T125 Run full quickstart.md validation: build and smoke-test on all 5 platforms (iOS simulator, Android emulator, `flutter run -d chrome`, `flutter run -d macos`, `flutter run -d linux`). Verify `cargo test` passes, `cargo clippy -- -D warnings` clean, `flutter test` passes, `flutter analyze` clean. +- [x] T118 Complete all ARB localization files: populate all strings in `assets/l10n/app_en.arb` from all feature screens, then translate to `app_es.arb`, `app_it.arb`, `app_fr.arb`, `app_de.arb`. Include walkthrough highlight terms in all 5 languages for `highlight_config.dart`. Run `flutter gen-l10n`. Zero untranslated strings. +- [x] T119 Implement responsive layout breakpoints in shared widgets (mobile < 600px, tablet 600–1200px, desktop > 1200px): order book shows 1-column on mobile, 2-column on tablet, 3-column on desktop. Drawer becomes persistent sidebar on desktop. Chat room shows side-panel info on tablet+. All per Constitution Principle V. +- [x] T120 [P] Wire theme toggle (dark/light/system) throughout: `set_theme()` → `AppTheme.darkTheme` / `AppTheme.lightTheme` / `ThemeMode.system` in `MaterialApp.router`. Defaults to System on first launch (dark appearance on most devices). Switchable from Settings. Both themes fully functional. +- [x] T121 [P] Implement platform-aware QR scanner wrapper in `lib/shared/widgets/platform_aware_qr_scanner.dart`: on iOS/Android/desktop → camera via `mobile_scanner`. On web → paste-from-clipboard text field (no camera access). Used in Connect Wallet screen and anywhere QR scanning is needed. +- [x] T122 [P] Implement graceful degradation for platform features: push notifications (FCM/APNs/Web Push) → fallback to polling on unsupported platforms. Camera / QR scan → paste-only fallback on web. Biometric → disabled if hardware unavailable. All per Constitution Principle V. +- [x] T123 Wire offline message queue flushing: in `rust/src/api/nostr.rs` `flush_message_queue()` called automatically on `on_connection_state_changed(Online)` event. `outbox.rs` retries up to 10 attempts, backs off exponentially, prunes sent items. +- [x] T124 [P] Implement countdown timer widget in `lib/shared/widgets/countdown_timer.dart`: circular progress indicator showing remaining time as "HH:MM:SS". Color-codes as time runs low (green → yellow → red thresholds). Used on Take Order screen (order expiry) and Trade Detail screen (trade step timeout). +- [x] T125 Run full quickstart.md validation: build and smoke-test on all 5 platforms (iOS simulator, Android emulator, `flutter run -d chrome`, `flutter run -d macos`, `flutter run -d linux`). Verify `cargo test` passes, `cargo clippy -- -D warnings` clean, `flutter test` passes, `flutter analyze` clean. ---