From efa61de6bf8b7c03dd12918ab2354d439f927450 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 02:51:42 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(app):=20alinhar=20design=20system=20Ar?= =?UTF-8?q?ah=20e=20rebrand=20Ar=C3=A1=20=E2=86=92=20Arah?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tokens dark alinhados a design-tokens.css (#0a0e12, #141a21, links, erro) - Tipografia Inter (google_fonts) com escala 1.125 e peso visual reforçado - ArahBrandHeader e ArahScaffold (gradiente sutil, cards com borda) - Login, shell, onboarding e perfil com presença de marca Arah - Web manifest/index.html com cores e nome da marca - Mapa/onboarding sem cores hardcoded (territoryBoundary, locationPin) Co-authored-by: Rapha --- frontend/arah.app/README.md | 4 +- frontend/arah.app/lib/app.dart | 3 +- .../lib/core/config/brand_config.dart | 10 + .../arah.app/lib/core/config/constants.dart | 1 + .../lib/core/theme/app_design_tokens.dart | 179 ++++++++++++++---- .../arah.app/lib/core/theme/app_theme.dart | 157 ++++++++++++--- .../lib/core/widgets/arah_brand_header.dart | 81 ++++++++ .../lib/core/widgets/arah_scaffold.dart | 45 +++++ .../presentation/screens/login_screen.dart | 23 +-- .../presentation/widgets/feed_post_card.dart | 2 +- .../screens/main_shell_screen.dart | 4 +- .../map/presentation/screens/map_screen.dart | 9 +- .../screens/onboarding_screen.dart | 17 +- .../presentation/screens/profile_screen.dart | 21 +- frontend/arah.app/lib/l10n/app_en.arb | 3 +- .../arah.app/lib/l10n/app_localizations.dart | 8 +- .../lib/l10n/app_localizations_en.dart | 5 +- .../lib/l10n/app_localizations_pt.dart | 5 +- frontend/arah.app/lib/l10n/app_pt.arb | 3 +- frontend/arah.app/pubspec.yaml | 1 + .../auth/presentation/login_screen_test.dart | 5 +- .../presentation/profile_screen_test.dart | 2 + frontend/arah.app/web/index.html | 7 +- frontend/arah.app/web/manifest.json | 10 +- 24 files changed, 476 insertions(+), 129 deletions(-) create mode 100644 frontend/arah.app/lib/core/config/brand_config.dart create mode 100644 frontend/arah.app/lib/core/widgets/arah_brand_header.dart create mode 100644 frontend/arah.app/lib/core/widgets/arah_scaffold.dart diff --git a/frontend/arah.app/README.md b/frontend/arah.app/README.md index db236e9e..23cd16c8 100644 --- a/frontend/arah.app/README.md +++ b/frontend/arah.app/README.md @@ -1,6 +1,6 @@ -# Ará (App Flutter) +# Arah (App Flutter) -App mobile **território-first** e **comunidade-first**. Nome: **Ará**. Interface clean, fundo escuro padrão. Conecta ao **BFF** do repositório. +App mobile **território-first** e **comunidade-first**. Marca: **Arah**. Interface alinhada ao design system (fundo escuro, tipografia Inter, tokens compartilhados). Conecta ao **BFF** do repositório. **Fluxo:** Login → Onboarding (escolha de território) ou Home → Feed / Explorar / Publicar / Perfil. diff --git a/frontend/arah.app/lib/app.dart b/frontend/arah.app/lib/app.dart index 2dc348f8..8e7bc7c3 100644 --- a/frontend/arah.app/lib/app.dart +++ b/frontend/arah.app/lib/app.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app_router.dart'; import 'core/services/device_registration_listener.dart'; +import 'core/config/brand_config.dart'; import 'core/theme/app_theme.dart'; import 'l10n/app_localizations.dart'; @@ -13,7 +14,7 @@ class ArahApp extends ConsumerWidget { final router = ref.watch(goRouterProvider); return DeviceRegistrationListener( child: MaterialApp.router( - title: 'Ará', + title: BrandConfig.name, debugShowCheckedModeBanner: false, theme: AppTheme.light, darkTheme: AppTheme.dark, diff --git a/frontend/arah.app/lib/core/config/brand_config.dart b/frontend/arah.app/lib/core/config/brand_config.dart new file mode 100644 index 00000000..ba719b43 --- /dev/null +++ b/frontend/arah.app/lib/core/config/brand_config.dart @@ -0,0 +1,10 @@ +/// Marca e metadados do ecossistema Arah (espelho de frontend/shared/config/brand.ts). +class BrandConfig { + BrandConfig._(); + + static const String name = 'Arah'; + static const String tagline = 'Território primeiro. Comunidade primeiro.'; + static const String description = + 'Plataforma digital comunitária orientada ao território.'; + static const String siteUrl = 'https://arah.app'; +} diff --git a/frontend/arah.app/lib/core/config/constants.dart b/frontend/arah.app/lib/core/config/constants.dart index d7ffb76c..3b990bfe 100644 --- a/frontend/arah.app/lib/core/config/constants.dart +++ b/frontend/arah.app/lib/core/config/constants.dart @@ -7,6 +7,7 @@ class AppConstants { static const double spacingMd = 16.0; static const double spacingLg = 24.0; static const double spacingXl = 32.0; + static const double spacing2xl = 48.0; static const double radiusSm = 8.0; static const double radiusMd = 12.0; static const double radiusLg = 16.0; diff --git a/frontend/arah.app/lib/core/theme/app_design_tokens.dart b/frontend/arah.app/lib/core/theme/app_design_tokens.dart index b4a63244..76882b27 100644 --- a/frontend/arah.app/lib/core/theme/app_design_tokens.dart +++ b/frontend/arah.app/lib/core/theme/app_design_tokens.dart @@ -2,81 +2,152 @@ import 'package:flutter/material.dart'; import '../config/constants.dart'; -/// Tokens de design em um único lugar para manutenção fácil. -/// Alterar cores, radius ou tipografia aqui reflete em todo o app via [AppTheme]. -/// Ver docs/DESIGN_SYSTEM.md. +/// Tokens alinhados a [frontend/shared/styles/design-tokens.css]. class AppDesignTokens { AppDesignTokens._(); // --------------------------------------------------------------------------- - // Paleta Arah – alinhada a frontend/shared/styles/design-tokens.css + // Paleta – dark mode (padrão do app) // --------------------------------------------------------------------------- - static const Color primaryDark = Color(0xFF4DD4A8); - static const Color linkDark = Color(0xFF7DD3FF); - static const Color surfaceDark = Color(0xFF0F172A); - static const Color surfaceVariantDark = Color(0xFF1E293B); - static const Color onSurfaceDark = Color(0xFFF1F5F9); - static const Color onSurfaceVariantDark = Color(0xFF94A3B8); - static const Color outlineDark = Color(0xFF334155); + static const Color primary = Color(0xFF4DD4A8); + static const Color primaryHover = Color(0xFF3BC495); + static const Color link = Color(0xFF7DD3FF); + static const Color linkHover = Color(0xFF9DE3FF); + static const Color surface = Color(0xFF0A0E12); + static const Color surfaceElevated = Color(0xFF0F1419); + static const Color surfaceCard = Color(0xFF141A21); + static const Color onSurface = Color(0xFFE8EDF2); + static const Color onSurfaceMuted = Color(0xFFB8C5D2); + static const Color onSurfaceSubtle = Color(0xFF8A97A4); + static const Color outline = Color(0xFF25303A); + static const Color outlineSubtle = Color(0x1A6496B4); // rgba(100,150,180,0.1) + + static const Color error = Color(0xFFF26D6D); + static const Color warning = Color(0xFFF5C842); + static const Color textOnAccent = Color(0xFF0A0E12); + + static const Color accentSubtle = Color(0x264DD4A8); // rgba(77,212,168,0.15) + static const Color accentBorderSoft = Color(0x404DD4A8); // rgba(77,212,168,0.25) + static const Color territoryBoundary = Color(0xFF16A34A); // primary-700 + static const Color locationPin = Color(0xFFF5C842); + + // Light mode static const Color primaryLight = Color(0xFF3BC495); static const Color surfaceLight = Color(0xFFFAFAFA); + static const Color surfaceCardLight = Color(0xFFF5F5F5); static const Color onSurfaceLight = Color(0xFF1C1C1C); + static const Color onSurfaceMutedLight = Color(0xFF5C5C5C); + static const Color outlineLight = Color(0xFFE0E0E0); // Feed – cores semânticas por tipo de post - static const Color feedTypeAlert = Color(0xFFF5C842); - static const Color feedTypeEvent = Color(0xFF7DD3FF); - static const Color feedTypeTip = Color(0xFF4DD4A8); + static const Color feedTypeAlert = warning; + static const Color feedTypeEvent = link; + static const Color feedTypeTip = primary; static const Color feedTypeGeneral = Color(0xFF9CA3AF); + // Tipografia – escala 1.125 (CSS) + static const double fontSizeXs = 12; + static const double fontSizeSm = 14; + static const double fontSizeBase = 16; + static const double fontSizeLg = 18; + static const double fontSizeXl = 20; + static const double fontSize2xl = 24; + static const double fontSize3xl = 30; + static const double fontSize4xl = 36; + + static const double letterSpacingTight = -0.5; + static const double letterSpacingWide = 0.5; + static const double feedCardTypeBorderWidth = 4; - static const EdgeInsets cardPadding = EdgeInsets.all(16); - static const double fontSizeBodySmall = 12; + static const EdgeInsets cardPadding = EdgeInsets.all(AppConstants.spacingMd); static const double fontSizeSnackBar = 15; - // --------------------------------------------------------------------------- - // Componentes (usar AppConstants quando já existir) - // --------------------------------------------------------------------------- - static double get radiusCard => AppConstants.radiusMd; - static double get radiusSnackBar => AppConstants.radiusSm; - static EdgeInsets get snackBarInsets => const EdgeInsets.symmetric(horizontal: AppConstants.spacingMd); + static double get radiusCard => AppConstants.radiusLg; + static double get radiusSnackBar => AppConstants.radiusMd; + static double get radiusButton => AppConstants.radiusMd; + static EdgeInsets get snackBarInsets => + const EdgeInsets.symmetric(horizontal: AppConstants.spacingMd); + + /// Gradiente sutil de fundo (equivalente a --bg-gradient-accent). + static const List scaffoldGradientColors = [ + Color(0x084DD4A8), + Color(0x000A0E12), + Color(0x087DD3FF), + Color(0x000A0E12), + ]; } -/// Extensão do tema para acessar tokens e cores do app de forma semântica. -/// Uso: `context.appColors.primary` ou `Theme.of(context).extension()!`. +/// Extensão do tema para cores semânticas do Arah. class AppColors extends ThemeExtension { const AppColors({ required this.primary, + required this.link, required this.surface, + required this.surfaceElevated, required this.surfaceContainer, required this.onSurface, required this.onSurfaceVariant, + required this.onSurfaceSubtle, required this.outline, + required this.outlineSubtle, + required this.error, + required this.accentSubtle, + required this.accentBorder, + required this.territoryBoundary, + required this.locationPin, }); final Color primary; + final Color link; final Color surface; + final Color surfaceElevated; final Color surfaceContainer; final Color onSurface; final Color onSurfaceVariant; + final Color onSurfaceSubtle; final Color outline; + final Color outlineSubtle; + final Color error; + final Color accentSubtle; + final Color accentBorder; + final Color territoryBoundary; + final Color locationPin; @override ThemeExtension copyWith({ Color? primary, + Color? link, Color? surface, + Color? surfaceElevated, Color? surfaceContainer, Color? onSurface, Color? onSurfaceVariant, + Color? onSurfaceSubtle, Color? outline, + Color? outlineSubtle, + Color? error, + Color? accentSubtle, + Color? accentBorder, + Color? territoryBoundary, + Color? locationPin, }) { return AppColors( primary: primary ?? this.primary, + link: link ?? this.link, surface: surface ?? this.surface, + surfaceElevated: surfaceElevated ?? this.surfaceElevated, surfaceContainer: surfaceContainer ?? this.surfaceContainer, onSurface: onSurface ?? this.onSurface, onSurfaceVariant: onSurfaceVariant ?? this.onSurfaceVariant, + onSurfaceSubtle: onSurfaceSubtle ?? this.onSurfaceSubtle, outline: outline ?? this.outline, + outlineSubtle: outlineSubtle ?? this.outlineSubtle, + error: error ?? this.error, + accentSubtle: accentSubtle ?? this.accentSubtle, + accentBorder: accentBorder ?? this.accentBorder, + territoryBoundary: territoryBoundary ?? this.territoryBoundary, + locationPin: locationPin ?? this.locationPin, ); } @@ -85,36 +156,60 @@ class AppColors extends ThemeExtension { if (other is! AppColors) return this; return AppColors( primary: Color.lerp(primary, other.primary, t)!, + link: Color.lerp(link, other.link, t)!, surface: Color.lerp(surface, other.surface, t)!, + surfaceElevated: Color.lerp(surfaceElevated, other.surfaceElevated, t)!, surfaceContainer: Color.lerp(surfaceContainer, other.surfaceContainer, t)!, onSurface: Color.lerp(onSurface, other.onSurface, t)!, onSurfaceVariant: Color.lerp(onSurfaceVariant, other.onSurfaceVariant, t)!, + onSurfaceSubtle: Color.lerp(onSurfaceSubtle, other.onSurfaceSubtle, t)!, outline: Color.lerp(outline, other.outline, t)!, + outlineSubtle: Color.lerp(outlineSubtle, other.outlineSubtle, t)!, + error: Color.lerp(error, other.error, t)!, + accentSubtle: Color.lerp(accentSubtle, other.accentSubtle, t)!, + accentBorder: Color.lerp(accentBorder, other.accentBorder, t)!, + territoryBoundary: Color.lerp(territoryBoundary, other.territoryBoundary, t)!, + locationPin: Color.lerp(locationPin, other.locationPin, t)!, ); } - /// Cores para tema escuro. static const AppColors dark = AppColors( - primary: AppDesignTokens.primaryDark, - surface: AppDesignTokens.surfaceDark, - surfaceContainer: AppDesignTokens.surfaceVariantDark, - onSurface: AppDesignTokens.onSurfaceDark, - onSurfaceVariant: AppDesignTokens.onSurfaceVariantDark, - outline: AppDesignTokens.outlineDark, + primary: AppDesignTokens.primary, + link: AppDesignTokens.link, + surface: AppDesignTokens.surface, + surfaceElevated: AppDesignTokens.surfaceElevated, + surfaceContainer: AppDesignTokens.surfaceCard, + onSurface: AppDesignTokens.onSurface, + onSurfaceVariant: AppDesignTokens.onSurfaceMuted, + onSurfaceSubtle: AppDesignTokens.onSurfaceSubtle, + outline: AppDesignTokens.outline, + outlineSubtle: AppDesignTokens.outlineSubtle, + error: AppDesignTokens.error, + accentSubtle: AppDesignTokens.accentSubtle, + accentBorder: AppDesignTokens.accentBorderSoft, + territoryBoundary: AppDesignTokens.territoryBoundary, + locationPin: AppDesignTokens.locationPin, ); - /// Cores para tema claro (surfaceContainer = surfaceVariant implícito no light). - static AppColors get light => const AppColors( - primary: AppDesignTokens.primaryLight, - surface: AppDesignTokens.surfaceLight, - surfaceContainer: Color(0xFFF5F5F5), - onSurface: AppDesignTokens.onSurfaceLight, - onSurfaceVariant: Color(0xFF5C5C5C), - outline: Color(0xFFE0E0E0), - ); + static const AppColors light = AppColors( + primary: AppDesignTokens.primaryLight, + link: AppDesignTokens.link, + surface: AppDesignTokens.surfaceLight, + surfaceElevated: AppDesignTokens.surfaceLight, + surfaceContainer: AppDesignTokens.surfaceCardLight, + onSurface: AppDesignTokens.onSurfaceLight, + onSurfaceVariant: AppDesignTokens.onSurfaceMutedLight, + onSurfaceSubtle: AppDesignTokens.onSurfaceMutedLight, + outline: AppDesignTokens.outlineLight, + outlineSubtle: Color(0x1A000000), + error: AppDesignTokens.error, + accentSubtle: Color(0x1A3BC495), + accentBorder: Color(0x333BC495), + territoryBoundary: AppDesignTokens.territoryBoundary, + locationPin: AppDesignTokens.locationPin, + ); } -/// Acesso rápido: `context.appColors`. extension AppColorsExtension on BuildContext { - AppColors get appColors => Theme.of(this).extension()!; + AppColors get appColors => Theme.of(this).extension() ?? AppColors.dark; } diff --git a/frontend/arah.app/lib/core/theme/app_theme.dart b/frontend/arah.app/lib/core/theme/app_theme.dart index ea0a659c..3a77ae37 100644 --- a/frontend/arah.app/lib/core/theme/app_theme.dart +++ b/frontend/arah.app/lib/core/theme/app_theme.dart @@ -1,62 +1,155 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'app_design_tokens.dart'; import '../config/constants.dart'; -/// Tema Ará: clean, leve, harmônico. Fundo escuro como padrão. -/// Cores e componentes vêm de [AppDesignTokens] para manutenção em um só lugar. -/// Ver docs/DESIGN_SYSTEM.md. +/// Tema Arah: identidade visual alinhada a design-tokens.css (dark padrão). class AppTheme { AppTheme._(); + static TextTheme _textTheme(Brightness brightness) { + final base = brightness == Brightness.dark + ? ThemeData.dark().textTheme + : ThemeData.light().textTheme; + final inter = GoogleFonts.interTextTheme(base); + final onSurface = brightness == Brightness.dark + ? AppDesignTokens.onSurface + : AppDesignTokens.onSurfaceLight; + final onMuted = brightness == Brightness.dark + ? AppDesignTokens.onSurfaceMuted + : AppDesignTokens.onSurfaceMutedLight; + + return inter.copyWith( + displaySmall: inter.displaySmall?.copyWith( + fontSize: AppDesignTokens.fontSize4xl, + fontWeight: FontWeight.w700, + letterSpacing: AppDesignTokens.letterSpacingTight, + color: onSurface, + ), + headlineMedium: inter.headlineMedium?.copyWith( + fontSize: AppDesignTokens.fontSize2xl, + fontWeight: FontWeight.w600, + letterSpacing: AppDesignTokens.letterSpacingTight, + color: onSurface, + ), + titleLarge: inter.titleLarge?.copyWith( + fontSize: AppDesignTokens.fontSizeXl, + fontWeight: FontWeight.w600, + color: onSurface, + ), + titleMedium: inter.titleMedium?.copyWith( + fontSize: AppDesignTokens.fontSizeLg, + fontWeight: FontWeight.w600, + color: onSurface, + ), + bodyLarge: inter.bodyLarge?.copyWith( + fontSize: AppDesignTokens.fontSizeBase, + height: 1.5, + color: onSurface, + ), + bodyMedium: inter.bodyMedium?.copyWith( + fontSize: AppDesignTokens.fontSizeSm, + height: 1.5, + color: onMuted, + ), + bodySmall: inter.bodySmall?.copyWith( + fontSize: AppDesignTokens.fontSizeXs, + height: 1.4, + color: onMuted, + ), + labelLarge: inter.labelLarge?.copyWith( + fontSize: AppDesignTokens.fontSizeSm, + fontWeight: FontWeight.w600, + letterSpacing: AppDesignTokens.letterSpacingWide, + ), + ); + } + static ThemeData _baseTheme(Brightness brightness) { final isDark = brightness == Brightness.dark; final colors = isDark ? AppColors.dark : AppColors.light; + final textTheme = _textTheme(brightness); + return ThemeData( useMaterial3: true, brightness: brightness, + fontFamily: GoogleFonts.inter().fontFamily, + textTheme: textTheme, colorScheme: ColorScheme( brightness: brightness, primary: colors.primary, - onPrimary: isDark ? AppDesignTokens.onSurfaceDark : Colors.white, - secondary: colors.primary, - onSecondary: isDark ? AppDesignTokens.onSurfaceDark : Colors.white, + onPrimary: AppDesignTokens.textOnAccent, + secondary: colors.link, + onSecondary: AppDesignTokens.textOnAccent, + tertiary: colors.primary, surface: colors.surface, onSurface: colors.onSurface, surfaceContainerHighest: colors.surfaceContainer, onSurfaceVariant: colors.onSurfaceVariant, outline: colors.outline, - error: Colors.red.shade400, - onError: Colors.white, + error: colors.error, + onError: AppDesignTokens.textOnAccent, ), extensions: [colors], appBarTheme: AppBarTheme( centerTitle: true, elevation: 0, - scrolledUnderElevation: isDark ? 0.5 : 0, - backgroundColor: colors.surface, + scrolledUnderElevation: 0, + backgroundColor: colors.surfaceElevated.withValues(alpha: 0.92), foregroundColor: colors.onSurface, + surfaceTintColor: Colors.transparent, + titleTextStyle: textTheme.titleLarge, ), scaffoldBackgroundColor: colors.surface, + navigationBarTheme: NavigationBarThemeData( + elevation: 0, + height: 72, + backgroundColor: colors.surfaceElevated.withValues(alpha: 0.95), + indicatorColor: colors.accentSubtle, + labelTextStyle: WidgetStateProperty.resolveWith((states) { + final selected = states.contains(WidgetState.selected); + return textTheme.labelSmall?.copyWith( + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? colors.primary : colors.onSurfaceSubtle, + ); + }), + iconTheme: WidgetStateProperty.resolveWith((states) { + final selected = states.contains(WidgetState.selected); + return IconThemeData( + color: selected ? colors.primary : colors.onSurfaceSubtle, + size: AppConstants.iconSizeMd, + ); + }), + ), bottomNavigationBarTheme: BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, selectedItemColor: colors.primary, - unselectedItemColor: colors.onSurfaceVariant, - backgroundColor: colors.surface, + unselectedItemColor: colors.onSurfaceSubtle, + backgroundColor: colors.surfaceElevated, ), cardTheme: CardThemeData( - color: colors.surfaceContainer, + color: colors.surfaceContainer.withValues(alpha: 0.9), elevation: 0, + margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDesignTokens.radiusCard), + side: BorderSide(color: colors.outlineSubtle), ), ), + dividerTheme: DividerThemeData( + color: colors.outlineSubtle, + thickness: 1, + space: 1, + ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, + backgroundColor: colors.surfaceContainer, + contentTextStyle: textTheme.bodyMedium?.copyWith(color: colors.onSurface), insetPadding: AppDesignTokens.snackBarInsets, - contentTextStyle: const TextStyle(fontSize: AppDesignTokens.fontSizeSnackBar), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDesignTokens.radiusSnackBar), + side: BorderSide(color: colors.accentBorder), ), ), iconButtonTheme: IconButtonThemeData( @@ -70,7 +163,13 @@ class AppTheme { minimumSize: const Size(88, AppConstants.minTouchTargetSize), padding: const EdgeInsets.symmetric( horizontal: AppConstants.spacingLg, - vertical: AppConstants.spacingSm, + vertical: AppConstants.spacingSm + 2, + ), + backgroundColor: colors.primary, + foregroundColor: AppDesignTokens.textOnAccent, + textStyle: textTheme.labelLarge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppDesignTokens.radiusButton), ), ), ), @@ -78,22 +177,36 @@ class AppTheme { style: TextButton.styleFrom( minimumSize: const Size(64, AppConstants.minTouchTargetSize), padding: const EdgeInsets.symmetric(horizontal: AppConstants.spacingMd), + foregroundColor: colors.link, + textStyle: textTheme.labelLarge?.copyWith(color: colors.link), ), ), - inputDecorationTheme: const InputDecorationTheme( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric( + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colors.surfaceContainer.withValues(alpha: 0.6), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.radiusMd), + borderSide: BorderSide(color: colors.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.radiusMd), + borderSide: BorderSide(color: colors.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.radiusMd), + borderSide: BorderSide(color: colors.primary, width: 1.5), + ), + contentPadding: const EdgeInsets.symmetric( horizontal: AppConstants.spacingMd, - vertical: AppConstants.spacingSm, + vertical: AppConstants.spacingSm + 4, ), - filled: true, + labelStyle: textTheme.bodyMedium, + hintStyle: textTheme.bodyMedium?.copyWith(color: colors.onSurfaceSubtle), ), ); } - /// Tema claro. static ThemeData get light => _baseTheme(Brightness.light); - /// Tema escuro (padrão). static ThemeData get dark => _baseTheme(Brightness.dark); } diff --git a/frontend/arah.app/lib/core/widgets/arah_brand_header.dart b/frontend/arah.app/lib/core/widgets/arah_brand_header.dart new file mode 100644 index 00000000..a09250d0 --- /dev/null +++ b/frontend/arah.app/lib/core/widgets/arah_brand_header.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +import '../config/brand_config.dart'; +import '../config/constants.dart'; +import '../theme/app_design_tokens.dart'; + +/// Cabeçalho de marca com presença visual alinhada ao portal/wiki. +class ArahBrandHeader extends StatelessWidget { + const ArahBrandHeader({ + super.key, + this.subtitle, + this.size = ArahBrandHeaderSize.large, + this.center = true, + }); + + final String? subtitle; + final ArahBrandHeaderSize size; + final bool center; + + @override + Widget build(BuildContext context) { + final colors = context.appColors; + final titleStyle = switch (size) { + ArahBrandHeaderSize.large => Theme.of(context).textTheme.displaySmall?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w700, + letterSpacing: AppDesignTokens.letterSpacingTight, + ), + ArahBrandHeaderSize.medium => Theme.of(context).textTheme.headlineMedium?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w700, + letterSpacing: AppDesignTokens.letterSpacingTight, + ), + ArahBrandHeaderSize.compact => Theme.of(context).textTheme.titleLarge?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w600, + ), + }; + + return Column( + crossAxisAlignment: center ? CrossAxisAlignment.center : CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: center ? MainAxisSize.min : MainAxisSize.max, + mainAxisAlignment: center ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + Container( + width: size == ArahBrandHeaderSize.large ? 48 : 40, + height: size == ArahBrandHeaderSize.large ? 48 : 40, + decoration: BoxDecoration( + color: colors.accentSubtle, + borderRadius: BorderRadius.circular(AppConstants.radiusMd), + border: Border.all(color: colors.accentBorder), + ), + child: Icon( + Icons.terrain_outlined, + color: colors.primary, + size: size == ArahBrandHeaderSize.large ? 28 : 22, + ), + ), + const SizedBox(width: AppConstants.spacingSm + 4), + Text(BrandConfig.name, style: titleStyle), + ], + ), + if (subtitle != null) ...[ + SizedBox(height: size == ArahBrandHeaderSize.large ? AppConstants.spacingMd : AppConstants.spacingSm), + Text( + subtitle!, + textAlign: center ? TextAlign.center : TextAlign.start, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: colors.onSurfaceVariant, + height: 1.5, + ), + ), + ], + ], + ); + } +} + +enum ArahBrandHeaderSize { large, medium, compact } diff --git a/frontend/arah.app/lib/core/widgets/arah_scaffold.dart b/frontend/arah.app/lib/core/widgets/arah_scaffold.dart new file mode 100644 index 00000000..d79ef9ad --- /dev/null +++ b/frontend/arah.app/lib/core/widgets/arah_scaffold.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_design_tokens.dart'; + +/// Scaffold com fundo em gradiente sutil do design system. +class ArahScaffold extends StatelessWidget { + const ArahScaffold({ + super.key, + required this.body, + this.appBar, + this.bottomNavigationBar, + this.floatingActionButton, + this.extendBody = false, + }); + + final Widget body; + final PreferredSizeWidget? appBar; + final Widget? bottomNavigationBar; + final Widget? floatingActionButton; + final bool extendBody; + + @override + Widget build(BuildContext context) { + final surface = context.appColors.surface; + return DecoratedBox( + decoration: BoxDecoration( + color: surface, + gradient: RadialGradient( + center: const Alignment(-0.6, -0.4), + radius: 1.2, + colors: AppDesignTokens.scaffoldGradientColors, + stops: const [0.0, 0.45, 0.75, 1.0], + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + extendBody: extendBody, + appBar: appBar, + body: body, + bottomNavigationBar: bottomNavigationBar, + floatingActionButton: floatingActionButton, + ), + ); + } +} diff --git a/frontend/arah.app/lib/features/auth/presentation/screens/login_screen.dart b/frontend/arah.app/lib/features/auth/presentation/screens/login_screen.dart index 1fb4d9c1..eb48e122 100644 --- a/frontend/arah.app/lib/features/auth/presentation/screens/login_screen.dart +++ b/frontend/arah.app/lib/features/auth/presentation/screens/login_screen.dart @@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_brand_header.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../providers/auth_state_provider.dart'; @@ -131,7 +133,7 @@ class _LoginScreenState extends ConsumerState { final auth = ref.watch(authStateProvider); final l10n = AppLocalizations.of(context)!; - return Scaffold( + return ArahScaffold( body: SafeArea( child: Center( child: SingleChildScrollView( @@ -151,27 +153,14 @@ class _LoginScreenState extends ConsumerState { label: Text(l10n.back), ), ), - Text( - l10n.appTitle, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppConstants.spacingSm), - Text( - _step == LoginStep.email + ArahBrandHeader( + subtitle: _step == LoginStep.email ? l10n.loginSubtitle : _step == LoginStep.password ? l10n.enterPassword : l10n.signUpSubtitle, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), ), - const SizedBox(height: AppConstants.spacingXl + AppConstants.spacingSm), + const SizedBox(height: AppConstants.spacing2xl), if (_step == LoginStep.email) ..._buildEmailStep(l10n), if (_step == LoginStep.password) ..._buildPasswordStep(l10n, auth.isLoading), if (_step == LoginStep.signup) ..._buildSignUpStep(l10n, auth.isLoading), diff --git a/frontend/arah.app/lib/features/feed/presentation/widgets/feed_post_card.dart b/frontend/arah.app/lib/features/feed/presentation/widgets/feed_post_card.dart index 4ced211e..db34c4b8 100644 --- a/frontend/arah.app/lib/features/feed/presentation/widgets/feed_post_card.dart +++ b/frontend/arah.app/lib/features/feed/presentation/widgets/feed_post_card.dart @@ -127,7 +127,7 @@ class FeedPostCard extends StatelessWidget { (authorInitial ?? (title.isNotEmpty ? title[0] : '?')).toUpperCase(), style: TextStyle( color: theme.colorScheme.onPrimaryContainer, - fontSize: AppDesignTokens.fontSizeBodySmall, + fontSize: AppDesignTokens.fontSizeXs, fontWeight: FontWeight.w600, ), ), diff --git a/frontend/arah.app/lib/features/home/presentation/screens/main_shell_screen.dart b/frontend/arah.app/lib/features/home/presentation/screens/main_shell_screen.dart index c8edd1df..359ad188 100644 --- a/frontend/arah.app/lib/features/home/presentation/screens/main_shell_screen.dart +++ b/frontend/arah.app/lib/features/home/presentation/screens/main_shell_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/geo/geo_location_provider.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../feed/presentation/screens/feed_screen.dart'; import '../../../feed/presentation/screens/create_post_screen.dart'; @@ -38,7 +39,8 @@ class _MainShellScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( + return ArahScaffold( + extendBody: true, body: IndexedStack( index: _currentIndex, children: _buildScreens(), diff --git a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart index 97c09779..243e3b60 100644 --- a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart +++ b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart @@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/providers/territory_provider.dart'; +import '../../../../core/theme/app_design_tokens.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../core/geo/geo_location_provider.dart'; import '../../../territories/data/repositories/territories_repository.dart'; @@ -16,8 +17,6 @@ import '../../data/models/map_pin.dart'; const LatLng _defaultCenter = LatLng(-14.2, -51.9); const double _defaultZoom = 4.0; const double _territoryZoom = 13.0; -/// Cor do contorno do polígono e do pin do território (verde floresta). -const _territoryBoundaryColorMap = Color(0xFF228B22); /// Tela de mapa com pins do território (entidades, posts, eventos, etc.). /// Usa flutter_map + OpenStreetMap; opcionalmente configurável para tiles Mapbox. @@ -107,9 +106,9 @@ class _MapScreenState extends ConsumerState { point: LatLng(geo.latitude, geo.longitude), width: 40, height: 40, - child: const Icon( + child: Icon( Icons.person_pin_circle, - color: Colors.orange, + color: context.appColors.locationPin, size: 40, ), ), @@ -152,7 +151,7 @@ class _MapScreenState extends ConsumerState { } Widget _buildTerritoryBoundaryLayer(BuildContext context, TerritoryDetail detail) { - const color = _territoryBoundaryColorMap; + final color = context.appColors.territoryBoundary; if (detail.boundaryPolygon != null && detail.boundaryPolygon!.length >= 3) { final points = detail.boundaryPolygon!.map((p) => LatLng(p.lat, p.lng)).toList(); return PolygonLayer( diff --git a/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart b/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart index e7810158..a05fe055 100644 --- a/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart +++ b/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart @@ -8,7 +8,9 @@ import '../../../../core/config/constants.dart'; import '../../../../core/geo/geo_location_provider.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; +import '../../../../core/theme/app_design_tokens.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../auth/presentation/providers/auth_state_provider.dart'; import '../../../territories/data/repositories/territories_repository.dart'; @@ -157,9 +159,9 @@ class _OnboardingScreenState extends ConsumerState { children: [ Row( children: [ - const Icon( + Icon( Icons.location_on, - color: Colors.orange, + color: context.appColors.locationPin, size: AppConstants.iconSizeLg, ), const SizedBox(width: AppConstants.spacingSm), @@ -339,9 +341,6 @@ class _ContinueButton extends StatelessWidget { } } -/// Cor do contorno do polígono e do pin do território (verde floresta). -const _territoryBoundaryColor = Color(0xFF228B22); - /// Mapa compacto: sempre visível; contorno do território selecionado (ou mais próximo). class _OnboardingMap extends ConsumerWidget { const _OnboardingMap({ @@ -394,9 +393,9 @@ class _OnboardingMap extends ConsumerWidget { point: LatLng(centerLat, centerLng), width: 40, height: 40, - child: const Icon( + child: Icon( Icons.person_pin_circle, - color: Colors.orange, + color: context.appColors.locationPin, size: 40, ), ), @@ -413,7 +412,7 @@ class _OnboardingMap extends ConsumerWidget { height: 32, child: Icon( Icons.place, - color: _territoryBoundaryColor, + color: context.appColors.territoryBoundary, size: 32, ), ); @@ -429,7 +428,7 @@ class _OnboardingMap extends ConsumerWidget { } Widget _boundaryLayer(BuildContext context, TerritoryDetail detail) { - const color = _territoryBoundaryColor; + final color = context.appColors.territoryBoundary; if (detail.boundaryPolygon != null && detail.boundaryPolygon!.length >= 3) { final points = detail.boundaryPolygon!.map((p) => LatLng(p.lat, p.lng)).toList(); return PolygonLayer( diff --git a/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart b/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart index 77ca648c..cb9a7727 100644 --- a/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart +++ b/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart @@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_brand_header.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../data/models/me_profile.dart'; import '../../../auth/presentation/providers/auth_state_provider.dart'; @@ -22,7 +24,7 @@ class ProfileScreen extends ConsumerWidget { final session = auth.valueOrNull; if (session == null) { - return Scaffold( + return ArahScaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.profile)), body: Center( child: Padding( @@ -30,20 +32,9 @@ class ProfileScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Ará', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppConstants.spacingMd), - Text( - 'Entre na sua conta para acessar perfil, publicar e notificações.', - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + ArahBrandHeader( + subtitle: 'Entre na sua conta para acessar perfil, publicar e notificações.', + size: ArahBrandHeaderSize.medium, ), const SizedBox(height: AppConstants.spacingLg), FilledButton( diff --git a/frontend/arah.app/lib/l10n/app_en.arb b/frontend/arah.app/lib/l10n/app_en.arb index 9d7d2fe4..74c33f4c 100644 --- a/frontend/arah.app/lib/l10n/app_en.arb +++ b/frontend/arah.app/lib/l10n/app_en.arb @@ -1,6 +1,7 @@ { "@@locale": "en", - "appTitle": "Ará", + "appTitle": "Arah", + "brandTagline": "Territory first. Community first.", "login": "Log in", "loginSubtitle": "Sign in to your account", "email": "Email", diff --git a/frontend/arah.app/lib/l10n/app_localizations.dart b/frontend/arah.app/lib/l10n/app_localizations.dart index 942cf82e..b1ddb47f 100644 --- a/frontend/arah.app/lib/l10n/app_localizations.dart +++ b/frontend/arah.app/lib/l10n/app_localizations.dart @@ -101,9 +101,15 @@ abstract class AppLocalizations { /// No description provided for @appTitle. /// /// In pt, this message translates to: - /// **'Ará'** + /// **'Arah'** String get appTitle; + /// No description provided for @brandTagline. + /// + /// In pt, this message translates to: + /// **'Território primeiro. Comunidade primeiro.'** + String get brandTagline; + /// No description provided for @login. /// /// In pt, this message translates to: diff --git a/frontend/arah.app/lib/l10n/app_localizations_en.dart b/frontend/arah.app/lib/l10n/app_localizations_en.dart index d2af6ac0..884203bb 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_en.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_en.dart @@ -9,7 +9,10 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get appTitle => 'Ará'; + String get appTitle => 'Arah'; + + @override + String get brandTagline => 'Territory first. Community first.'; @override String get login => 'Log in'; diff --git a/frontend/arah.app/lib/l10n/app_localizations_pt.dart b/frontend/arah.app/lib/l10n/app_localizations_pt.dart index c3578d2f..adf89f4d 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_pt.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_pt.dart @@ -9,7 +9,10 @@ class AppLocalizationsPt extends AppLocalizations { AppLocalizationsPt([String locale = 'pt']) : super(locale); @override - String get appTitle => 'Ará'; + String get appTitle => 'Arah'; + + @override + String get brandTagline => 'Território primeiro. Comunidade primeiro.'; @override String get login => 'Entrar'; diff --git a/frontend/arah.app/lib/l10n/app_pt.arb b/frontend/arah.app/lib/l10n/app_pt.arb index 21e7f3b4..16b3d3b8 100644 --- a/frontend/arah.app/lib/l10n/app_pt.arb +++ b/frontend/arah.app/lib/l10n/app_pt.arb @@ -1,6 +1,7 @@ { "@@locale": "pt", - "appTitle": "Ará", + "appTitle": "Arah", + "brandTagline": "Território primeiro. Comunidade primeiro.", "login": "Entrar", "loginSubtitle": "Entre na sua conta", "email": "E-mail", diff --git a/frontend/arah.app/pubspec.yaml b/frontend/arah.app/pubspec.yaml index 02c9fd64..55a8da70 100644 --- a/frontend/arah.app/pubspec.yaml +++ b/frontend/arah.app/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: # UI cached_network_image: ^3.3.1 image_picker: ^1.1.2 + google_fonts: ^6.2.1 # Geolocalização (convergência com território observado) geolocator: ^13.0.2 # Mapa (open source: OpenStreetMap; opcionalmente tiles Mapbox) diff --git a/frontend/arah.app/test/features/auth/presentation/login_screen_test.dart b/frontend/arah.app/test/features/auth/presentation/login_screen_test.dart index 5ef3baba..e8cab775 100644 --- a/frontend/arah.app/test/features/auth/presentation/login_screen_test.dart +++ b/frontend/arah.app/test/features/auth/presentation/login_screen_test.dart @@ -1,3 +1,4 @@ +import 'package:arah_app/core/theme/app_theme.dart'; import 'package:arah_app/core/config/app_config.dart'; import 'package:arah_app/core/storage/secure_storage_service.dart'; import 'package:arah_app/features/auth/data/repositories/auth_repository.dart'; @@ -54,6 +55,7 @@ void main() { authStateProvider.overrideWith(() => _FakeAuthStateNotifier()), ], child: MaterialApp( + theme: AppTheme.dark, locale: const Locale('pt'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, @@ -63,7 +65,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Ará'), findsOneWidget); + expect(find.text('Arah'), findsOneWidget); expect(find.text('Continuar'), findsOneWidget); expect(find.byType(TextFormField), findsWidgets); }); @@ -80,6 +82,7 @@ void main() { authRepositoryProvider.overrideWithValue(fakeRepo), ], child: MaterialApp( + theme: AppTheme.dark, locale: const Locale('pt'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/frontend/arah.app/test/features/profile/presentation/profile_screen_test.dart b/frontend/arah.app/test/features/profile/presentation/profile_screen_test.dart index 63702c66..2bac3acd 100644 --- a/frontend/arah.app/test/features/profile/presentation/profile_screen_test.dart +++ b/frontend/arah.app/test/features/profile/presentation/profile_screen_test.dart @@ -1,3 +1,4 @@ +import 'package:arah_app/core/theme/app_theme.dart'; import 'package:arah_app/features/auth/data/models/auth_models.dart'; import 'package:arah_app/features/auth/data/repositories/auth_repository.dart'; import 'package:arah_app/features/auth/presentation/providers/auth_state_provider.dart'; @@ -34,6 +35,7 @@ void main() { meProfileProvider.overrideWith((ref) async => profile), ], child: MaterialApp( + theme: AppTheme.dark, locale: const Locale('pt'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/frontend/arah.app/web/index.html b/frontend/arah.app/web/index.html index 0566c7a0..52846371 100644 --- a/frontend/arah.app/web/index.html +++ b/frontend/arah.app/web/index.html @@ -18,18 +18,19 @@ - + - + + - arah_app + Arah diff --git a/frontend/arah.app/web/manifest.json b/frontend/arah.app/web/manifest.json index 59abae04..3e44540c 100644 --- a/frontend/arah.app/web/manifest.json +++ b/frontend/arah.app/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "arah_app", - "short_name": "arah_app", + "name": "Arah", + "short_name": "Arah", "start_url": ".", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", + "background_color": "#0A0E12", + "theme_color": "#4DD4A8", + "description": "Plataforma digital comunitária orientada ao território.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ From 3a27dcc24960f14fbbdad87d9a911de8b2753efd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 09:15:44 +0000 Subject: [PATCH 2/4] feat(arah.app): localize feature screens and use ArahScaffold - Replace Scaffold with ArahScaffold across all feature screens - Add AppLocalizations for mapped UI strings - Use AppConstants.mapUserAgentPackage in map and onboarding screens - Regenerate l10n bindings for new keys Co-authored-by: Rapha --- .../presentation/screens/alerts_screen.dart | 25 +- .../presentation/screens/assets_screen.dart | 35 +- .../screens/chat_conversation_screen.dart | 3 +- .../screens/chat_list_screen.dart | 20 +- .../screens/connections_screen.dart | 24 +- .../presentation/screens/events_screen.dart | 3 +- .../presentation/screens/explore_screen.dart | 20 +- .../screens/create_post_screen.dart | 3 +- .../presentation/screens/feed_screen.dart | 24 +- .../map/presentation/screens/map_screen.dart | 14 +- .../screens/marketplace_screen.dart | 21 +- .../screens/membership_screen.dart | 24 +- .../screens/moderation_screen.dart | 16 +- .../screens/notifications_screen.dart | 3 +- .../screens/onboarding_screen.dart | 4 +- .../presentation/screens/profile_screen.dart | 32 +- .../screens/subscriptions_screen.dart | 18 +- .../arah.app/lib/l10n/app_localizations.dart | 312 ++++++++++++++++++ .../lib/l10n/app_localizations_en.dart | 168 ++++++++++ .../lib/l10n/app_localizations_pt.dart | 169 ++++++++++ 20 files changed, 819 insertions(+), 119 deletions(-) diff --git a/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart b/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart index f7e38c91..76a2004a 100644 --- a/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart +++ b/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart @@ -6,6 +6,8 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../providers/alerts_provider.dart'; @@ -20,19 +22,20 @@ class AlertsScreen extends ConsumerStatefulWidget { class _AlertsScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(alertsProvider); final notifier = ref.read(alertsProvider.notifier); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Alertas')), - body: const Center(child: Text('Escolha um território para ver alertas.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.alertsTitle)), + body: Center(child: Text(l10n.chooseTerritoryForAlerts)), ); } - return Scaffold( - appBar: AppBar(title: const Text('Alertas')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.alertsTitle)), floatingActionButton: FloatingActionButton( onPressed: () => _showCreateDialog(context), child: const Icon(Icons.add), @@ -53,12 +56,13 @@ class _AlertsScreenState extends ConsumerState { } Future _showCreateDialog(BuildContext context) async { + final l10n = AppLocalizations.of(context)!; final titleController = TextEditingController(); final descController = TextEditingController(); await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Reportar alerta'), + title: Text(l10n.reportAlert), content: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -74,7 +78,7 @@ class _AlertsScreenState extends ConsumerState { ], ), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), + TextButton(onPressed: () => Navigator.pop(ctx), child: Text(l10n.cancel)), FilledButton( onPressed: () async { try { @@ -95,7 +99,7 @@ class _AlertsScreenState extends ConsumerState { } } }, - child: const Text('Enviar'), + child: Text(l10n.send), ), ], ), @@ -103,6 +107,7 @@ class _AlertsScreenState extends ConsumerState { } Widget _buildBody(BuildContext context, AlertsState state, AlertsNotifier notifier) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.items.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -133,7 +138,7 @@ class _AlertsScreenState extends ConsumerState { ), ), const SizedBox(height: AppConstants.spacingMd), - FilledButton.tonal(onPressed: () => notifier.refresh(), child: const Text('Tentar novamente')), + FilledButton.tonal(onPressed: () => notifier.refresh(), child: Text(l10n.tryAgain)), ], ), ), @@ -157,7 +162,7 @@ class _AlertsScreenState extends ConsumerState { color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), ), const SizedBox(height: AppConstants.spacingMd), - Text('Nenhum alerta ativo', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.noAlertsActive, style: Theme.of(context).textTheme.titleMedium), ], ), ), diff --git a/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart b/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart index 0641a338..b3e2fe40 100644 --- a/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart +++ b/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart @@ -5,6 +5,8 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../../data/models/asset_item.dart'; import '../providers/assets_provider.dart'; @@ -14,19 +16,20 @@ class AssetsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(assetsProvider); final notifier = ref.read(assetsProvider.notifier); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Assets')), - body: const Center(child: Text('Escolha um território primeiro.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.assetsTitle)), + body: Center(child: Text(l10n.chooseTerritoryFirst)), ); } - return Scaffold( - appBar: AppBar(title: const Text('Assets')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.assetsTitle)), floatingActionButton: FloatingActionButton( onPressed: () => _showCreateDialog(context, ref), child: const Icon(Icons.add), @@ -46,21 +49,22 @@ class AssetsScreen extends ConsumerWidget { } Future _showCreateDialog(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context)!; final nameController = TextEditingController(); final typeController = TextEditingController(text: 'Infrastructure'); await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Novo asset'), + title: Text(l10n.newAsset), content: Column( mainAxisSize: MainAxisSize.min, children: [ - TextField(controller: nameController, decoration: const InputDecoration(labelText: 'Nome')), - TextField(controller: typeController, decoration: const InputDecoration(labelText: 'Tipo')), + TextField(controller: nameController, decoration: InputDecoration(labelText: l10n.assetName)), + TextField(controller: typeController, decoration: InputDecoration(labelText: l10n.assetType)), ], ), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), + TextButton(onPressed: () => Navigator.pop(ctx), child: Text(l10n.cancel)), FilledButton( onPressed: () async { try { @@ -81,7 +85,7 @@ class AssetsScreen extends ConsumerWidget { } } }, - child: const Text('Criar'), + child: Text(l10n.create), ), ], ), @@ -89,6 +93,7 @@ class AssetsScreen extends ConsumerWidget { } Widget _buildBody(BuildContext context, WidgetRef ref, AssetsState state) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.items.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -114,7 +119,7 @@ class AssetsScreen extends ConsumerWidget { if (state.items.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), - children: [SizedBox(height: 200, child: Center(child: Text('Nenhum asset cadastrado.')))], + children: [SizedBox(height: 200, child: Center(child: Text(l10n.noAssetsRegistered)))], ); } return ListView.builder( @@ -134,12 +139,12 @@ class AssetsScreen extends ConsumerWidget { onSelected: (value) => _onAssetAction(context, ref, asset, value), itemBuilder: (context) => [ if (asset.canValidate) - const PopupMenuItem(value: 'validate', child: Text('Validar')), + PopupMenuItem(value: 'validate', child: Text(l10n.validate)), if (asset.canArchive) - const PopupMenuItem(value: 'archive', child: Text('Arquivar')), + PopupMenuItem(value: 'archive', child: Text(l10n.archive)), if (asset.canCurate) ...[ - const PopupMenuItem(value: 'approve', child: Text('Aprovar (curador)')), - const PopupMenuItem(value: 'reject', child: Text('Rejeitar (curador)')), + PopupMenuItem(value: 'approve', child: Text(l10n.approveCurator)), + PopupMenuItem(value: 'reject', child: Text(l10n.rejectCurator)), ], ], ), diff --git a/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart b/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart index 19994f77..918c10bb 100644 --- a/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart +++ b/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../providers/chat_provider.dart'; class ChatConversationScreen extends ConsumerStatefulWidget { @@ -50,7 +51,7 @@ class _ChatConversationScreenState extends ConsumerState final state = ref.watch(chatConversationProvider(widget.conversationId)); final timeFormat = DateFormat('HH:mm'); - return Scaffold( + return ArahScaffold( appBar: AppBar(title: Text(widget.title ?? 'Conversa')), body: Column( children: [ diff --git a/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart b/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart index e080ea9d..0246f1d2 100644 --- a/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart +++ b/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart @@ -6,6 +6,8 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../providers/chat_provider.dart'; @@ -41,11 +43,12 @@ class _ChatListScreenState extends ConsumerState with SingleTick } Future _showCreateGroupDialog() async { + final l10n = AppLocalizations.of(context)!; final nameController = TextEditingController(); await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Novo grupo'), + title: Text(l10n.newGroup), content: TextField( controller: nameController, decoration: const InputDecoration( @@ -54,7 +57,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick ), ), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), + TextButton(onPressed: () => Navigator.pop(ctx), child: Text(l10n.cancel)), FilledButton( onPressed: () async { final name = nameController.text.trim(); @@ -77,7 +80,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick } } }, - child: const Text('Criar'), + child: Text(l10n.create), ), ], ), @@ -86,20 +89,21 @@ class _ChatListScreenState extends ConsumerState with SingleTick @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(chatListProvider); final notifier = ref.read(chatListProvider.notifier); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Chat')), - body: const Center(child: Text('Escolha um território primeiro.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.chat)), + body: Center(child: Text(l10n.chooseTerritoryFirst)), ); } - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: const Text('Chat'), + title: Text(l10n.chat), bottom: TabBar( controller: _tabController, tabs: const [ diff --git a/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart b/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart index 4c5c73d6..5cf447e6 100644 --- a/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart +++ b/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../data/models/connection_item.dart'; import '../../data/models/connection_user.dart'; import '../providers/connections_provider.dart'; @@ -14,15 +16,16 @@ class ConnectionsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final state = ref.watch(connectionsProvider); final notifier = ref.read(connectionsProvider.notifier); - return Scaffold( - appBar: AppBar(title: const Text('Conexões')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.connections)), floatingActionButton: FloatingActionButton.extended( onPressed: () => _openSearchSheet(context, ref), icon: const Icon(Icons.person_add_outlined), - label: const Text('Adicionar'), + label: Text(l10n.add), ), body: RefreshIndicator( onRefresh: () => notifier.refresh(), @@ -58,6 +61,7 @@ class ConnectionsScreen extends ConsumerWidget { } Widget _buildBody(BuildContext context, ConnectionsState state, ConnectionsNotifier notifier) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.accepted.isEmpty && state.pending.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -78,7 +82,7 @@ class ConnectionsScreen extends ConsumerWidget { children: [ Text(message, textAlign: TextAlign.center), const SizedBox(height: AppConstants.spacingMd), - FilledButton.tonal(onPressed: () => notifier.refresh(), child: const Text('Tentar novamente')), + FilledButton.tonal(onPressed: () => notifier.refresh(), child: Text(l10n.tryAgain)), ], ), ), @@ -96,12 +100,12 @@ class ConnectionsScreen extends ConsumerWidget { ), children: [ if (state.pending.isNotEmpty) ...[ - Text('Pendentes', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.pending, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppConstants.spacingSm), ...state.pending.map((item) => _ConnectionTile(item: item, notifier: notifier, isPending: true)), const SizedBox(height: AppConstants.spacingLg), ], - Text('Conexões', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.connections, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppConstants.spacingSm), if (state.accepted.isEmpty) Padding( @@ -192,6 +196,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return DraggableScrollableSheet( expand: false, initialChildSize: 0.85, @@ -202,7 +207,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Buscar pessoas', style: Theme.of(context).textTheme.titleLarge), + Text(l10n.searchPeople, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: AppConstants.spacingMd), TextField( controller: _queryController, @@ -255,7 +260,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> title: Text(user.displayName), trailing: FilledButton.tonal( onPressed: () => widget.onRequest(user.id), - child: const Text('Conectar'), + child: Text(l10n.connect), ), ); }, @@ -281,12 +286,13 @@ class _ConnectionTile extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Card( margin: const EdgeInsets.only(bottom: AppConstants.spacingSm), child: ListTile( leading: CircleAvatar(child: Text(item.isIncoming ? '←' : '→')), title: Text(isPending ? 'Solicitação ${item.isIncoming ? 'recebida' : 'enviada'}' : 'Conexão ativa'), - subtitle: Text('Status: ${item.status}'), + subtitle: Text(l10n.statusLabel(item.status)), trailing: isPending && item.isIncoming ? Row( mainAxisSize: MainAxisSize.min, diff --git a/frontend/arah.app/lib/features/events/presentation/screens/events_screen.dart b/frontend/arah.app/lib/features/events/presentation/screens/events_screen.dart index 60658f70..e5942ea6 100644 --- a/frontend/arah.app/lib/features/events/presentation/screens/events_screen.dart +++ b/frontend/arah.app/lib/features/events/presentation/screens/events_screen.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../data/models/event_item.dart'; @@ -25,7 +26,7 @@ class EventsScreen extends ConsumerWidget { final hasTerritory = effectiveTerritoryId != null && effectiveTerritoryId.isNotEmpty; - return Scaffold( + return ArahScaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.events), ), diff --git a/frontend/arah.app/lib/features/explore/presentation/screens/explore_screen.dart b/frontend/arah.app/lib/features/explore/presentation/screens/explore_screen.dart index f96bc459..237965ab 100644 --- a/frontend/arah.app/lib/features/explore/presentation/screens/explore_screen.dart +++ b/frontend/arah.app/lib/features/explore/presentation/screens/explore_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/providers/territory_provider.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../../../territories/presentation/widgets/territory_selector.dart'; @@ -14,37 +15,38 @@ class ExploreScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final hasTerritory = territoryId != null && territoryId.isNotEmpty; - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.explore), + title: Text(l10n.explore), actions: [ if (hasTerritory) ...[ IconButton( icon: const Icon(Icons.event_outlined), - tooltip: AppLocalizations.of(context)!.events, + tooltip: l10n.events, onPressed: () => context.push('/events?territoryId=$territoryId'), ), IconButton( icon: const Icon(Icons.map_outlined), - tooltip: AppLocalizations.of(context)!.viewOnMap, + tooltip: l10n.viewOnMap, onPressed: () => context.push('/map?territoryId=$territoryId'), ), IconButton( icon: const Icon(Icons.warning_amber_outlined), - tooltip: 'Alertas', + tooltip: l10n.alertsTitle, onPressed: () => context.push('/alerts'), ), IconButton( icon: const Icon(Icons.storefront_outlined), - tooltip: 'Marketplace', + tooltip: l10n.marketplace, onPressed: () => context.push('/marketplace'), ), IconButton( icon: const Icon(Icons.chat_outlined), - tooltip: 'Chat', + tooltip: l10n.chat, onPressed: () => context.push('/chat'), ), ], @@ -62,7 +64,7 @@ class ExploreScreen extends ConsumerWidget { AppConstants.spacingSm, ), child: Text( - AppLocalizations.of(context)!.territories, + l10n.territories, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -71,7 +73,7 @@ class ExploreScreen extends ConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: AppConstants.spacingMd), child: Text( - AppLocalizations.of(context)!.territoriesSubtitle, + l10n.territoriesSubtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), diff --git a/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart b/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart index d9c6e250..5256289b 100644 --- a/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart +++ b/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart @@ -8,6 +8,7 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../providers/feed_provider.dart'; @@ -88,7 +89,7 @@ class _CreatePostScreenState extends ConsumerState { final territoryId = ref.watch(selectedTerritoryIdValueProvider); final hasTerritory = territoryId != null && territoryId.isNotEmpty; - return Scaffold( + return ArahScaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.createPost), actions: [ diff --git a/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart b/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart index e3659140..f6326f01 100644 --- a/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart +++ b/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart @@ -5,6 +5,7 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/shimmer_skeleton.dart'; import '../../../../core/providers/territory_provider.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../../../territories/presentation/widgets/territory_selector.dart'; @@ -33,6 +34,7 @@ class _FeedScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final feedState = ref.watch(feedNotifierProvider(territoryId)); final notifier = ref.read(feedNotifierProvider(territoryId).notifier); @@ -40,8 +42,8 @@ class _FeedScreenState extends ConsumerState { final filterType = ref.watch(filterFeedTypeProvider); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Início')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.home)), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -60,16 +62,16 @@ class _FeedScreenState extends ConsumerState { ); } - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.home), + title: Text(l10n.home), actions: [ IconButton( icon: Icon( filterByInterests ? Icons.filter_list : Icons.filter_list_off, color: filterByInterests ? Theme.of(context).colorScheme.primary : null, ), - tooltip: AppLocalizations.of(context)!.filterByInterests, + tooltip: l10n.filterByInterests, onPressed: () { ref.read(filterFeedByInterestsProvider.notifier).state = !filterByInterests; ref.invalidate(feedNotifierProvider(territoryId)); @@ -226,14 +228,15 @@ class _FeedListState extends ConsumerState<_FeedList> { } Future _confirmDelete(String postId) async { + final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Excluir post'), - content: const Text('Esta ação não pode ser desfeita.'), + title: Text(l10n.deletePost), + content: Text(l10n.deletePostConfirm), actions: [ - TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar')), - FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Excluir')), + TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(l10n.cancel)), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(l10n.delete)), ], ), ); @@ -242,6 +245,7 @@ class _FeedListState extends ConsumerState<_FeedList> { } void _showPostMenu({required String postId, required bool canDelete}) { + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, builder: (ctx) => SafeArea( @@ -251,7 +255,7 @@ class _FeedListState extends ConsumerState<_FeedList> { if (canDelete) ListTile( leading: Icon(Icons.delete_outline, color: Theme.of(ctx).colorScheme.error), - title: const Text('Excluir post'), + title: Text(l10n.deletePost), onTap: () { Navigator.pop(ctx); _confirmDelete(postId); diff --git a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart index 243e3b60..28d9f0a2 100644 --- a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart +++ b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart @@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/theme/app_design_tokens.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../core/geo/geo_location_provider.dart'; import '../../../territories/data/repositories/territories_repository.dart'; @@ -35,6 +36,7 @@ class _MapScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; // Sempre priorizar o território atualmente selecionado para que a alternância no Explorar // reflita no mapa e apenas um contorno seja exibido por vez. final territoryId = ref.watch(selectedTerritoryIdValueProvider) ?? widget.territoryId; @@ -47,9 +49,9 @@ class _MapScreenState extends ConsumerState { final initialCenter = _initialCenter(geo, pinsAsync.valueOrNull, territoryDetail); final initialZoom = _initialZoom(geo, pinsAsync.valueOrNull); - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.map), + title: Text(l10n.map), actions: [ if (geo != null) IconButton( @@ -77,7 +79,7 @@ class _MapScreenState extends ConsumerState { ), const SizedBox(height: AppConstants.spacingMd), Text( - AppLocalizations.of(context)!.chooseTerritory, + l10n.chooseTerritory, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), @@ -97,7 +99,7 @@ class _MapScreenState extends ConsumerState { children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.araponga.app', + userAgentPackageName: AppConstants.mapUserAgentPackage, ), if (geo != null) MarkerLayer( @@ -131,8 +133,8 @@ class _MapScreenState extends ConsumerState { loading: () => const SizedBox.shrink(), error: (_, __) => const SizedBox.shrink(), ), - const SimpleAttributionWidget( - source: Text('OpenStreetMap contributors'), + SimpleAttributionWidget( + source: Text(l10n.openStreetMapAttribution), ), ], ), diff --git a/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart b/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart index 013639d0..cb28e97e 100644 --- a/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart +++ b/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart @@ -5,6 +5,8 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../providers/marketplace_provider.dart'; @@ -44,20 +46,21 @@ class _MarketplaceScreenState extends ConsumerState @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(marketplaceProvider); final notifier = ref.read(marketplaceProvider.notifier); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Marketplace')), - body: const Center(child: Text('Escolha um território primeiro.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.marketplace)), + body: Center(child: Text(l10n.chooseTerritoryFirst)), ); } - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: const Text('Marketplace'), + title: Text(l10n.marketplace), bottom: TabBar( controller: _tabController, tabs: const [ @@ -82,7 +85,7 @@ class _MarketplaceScreenState extends ConsumerState } }, icon: const Icon(Icons.shopping_cart_checkout), - label: Text('Checkout (${_cartItemCount(state.cart)})'), + label: Text(l10n.checkoutWithCount(_cartItemCount(state.cart))), ), ], ), @@ -145,6 +148,7 @@ class _SearchTab extends StatelessWidget { } Widget _buildList(BuildContext context) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.items.isEmpty) { return const Center(child: CircularProgressIndicator()); } @@ -158,7 +162,7 @@ class _SearchTab extends StatelessWidget { ); } if (state.items.isEmpty) { - return const Center(child: Text('Nenhum item encontrado.')); + return Center(child: Text(l10n.noItemsFound)); } return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: AppConstants.spacingMd), @@ -234,6 +238,7 @@ class _MyStoreTabState extends State<_MyStoreTab> { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; if (widget.state.isStoreLoading && widget.state.myStore == null) { return const Center(child: CircularProgressIndicator()); } @@ -246,7 +251,7 @@ class _MyStoreTabState extends State<_MyStoreTab> { child: ListTile( leading: const Icon(Icons.storefront_outlined), title: Text(widget.state.myStore!.displayName), - subtitle: Text('Status: ${widget.state.myStore!.status}'), + subtitle: Text(l10n.statusLabel(widget.state.myStore!.status)), ), ), const SizedBox(height: AppConstants.spacingMd), diff --git a/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart b/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart index 2dc3e36f..8b62aafe 100644 --- a/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart +++ b/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart @@ -6,6 +6,8 @@ import '../../../../core/geo/geo_location_provider.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../providers/membership_provider.dart'; @@ -14,19 +16,20 @@ class MembershipScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(membershipProvider); final notifier = ref.read(membershipProvider.notifier); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Membership')), - body: const Center(child: Text('Escolha um território primeiro.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.membership)), + body: Center(child: Text(l10n.chooseTerritoryFirst)), ); } - return Scaffold( - appBar: AppBar(title: const Text('Membership')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.membership)), body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -48,6 +51,7 @@ class MembershipScreen extends ConsumerWidget { MembershipState state, MembershipNotifier notifier, ) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.membership == null && state.error == null) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -68,7 +72,7 @@ class MembershipScreen extends ConsumerWidget { children: [ Text(msg, textAlign: TextAlign.center), const SizedBox(height: AppConstants.spacingMd), - FilledButton.tonal(onPressed: () => notifier.refresh(), child: const Text('Tentar novamente')), + FilledButton.tonal(onPressed: () => notifier.refresh(), child: Text(l10n.tryAgain)), ], ), ), @@ -90,12 +94,12 @@ class MembershipScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Seu papel', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.yourRole, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppConstants.spacingSm), Text(role, style: Theme.of(context).textTheme.headlineSmall), if (membership?.residencyVerification != null) ...[ const SizedBox(height: AppConstants.spacingSm), - Text('Verificação: ${membership!.residencyVerification}'), + Text(l10n.verificationLabel(membership!.residencyVerification!)), ], ], ), @@ -118,7 +122,7 @@ class MembershipScreen extends ConsumerWidget { } }, icon: const Icon(Icons.home_work_outlined), - label: const Text('Solicitar residência'), + label: Text(l10n.requestResidency), ), const SizedBox(height: AppConstants.spacingMd), OutlinedButton.icon( @@ -141,7 +145,7 @@ class MembershipScreen extends ConsumerWidget { } }, icon: const Icon(Icons.location_on_outlined), - label: const Text('Verificar por localização'), + label: Text(l10n.verifyByLocation), ), ] else Text( diff --git a/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart b/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart index 9c9b894e..534da9e2 100644 --- a/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart +++ b/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart @@ -6,6 +6,8 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/providers/territory_provider.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../../territories/presentation/widgets/territory_indicator_bar.dart'; import '../../data/models/work_item.dart'; import '../providers/moderation_provider.dart'; @@ -42,21 +44,22 @@ class _ModerationScreenState extends ConsumerState with Single @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final territoryId = ref.watch(selectedTerritoryIdValueProvider); final state = ref.watch(moderationProvider); final notifier = ref.read(moderationProvider.notifier); final dateFormat = DateFormat('dd/MM/yyyy HH:mm'); if (territoryId == null || territoryId.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('Moderação')), - body: const Center(child: Text('Escolha um território primeiro.')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.moderation)), + body: Center(child: Text(l10n.chooseTerritoryFirst)), ); } - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: const Text('Moderação'), + title: Text(l10n.moderation), bottom: TabBar( controller: _tabController, tabs: const [ @@ -86,6 +89,7 @@ class _ModerationScreenState extends ConsumerState with Single DateFormat dateFormat, ModerationNotifier notifier, ) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.items.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -111,7 +115,7 @@ class _ModerationScreenState extends ConsumerState with Single if (state.items.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), - children: [SizedBox(height: 200, child: Center(child: Text('Nenhum item nesta fila.')))], + children: [SizedBox(height: 200, child: Center(child: Text(l10n.noQueueItems)))], ); } return ListView.builder( diff --git a/frontend/arah.app/lib/features/notifications/presentation/screens/notifications_screen.dart b/frontend/arah.app/lib/features/notifications/presentation/screens/notifications_screen.dart index e00466eb..3c0215c8 100644 --- a/frontend/arah.app/lib/features/notifications/presentation/screens/notifications_screen.dart +++ b/frontend/arah.app/lib/features/notifications/presentation/screens/notifications_screen.dart @@ -4,6 +4,7 @@ import 'package:intl/intl.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; import '../../../../l10n/app_localizations.dart'; import '../../data/models/notification_item.dart'; import '../providers/notifications_provider.dart'; @@ -17,7 +18,7 @@ class NotificationsScreen extends ConsumerWidget { final state = ref.watch(notificationsProvider); final notifier = ref.read(notificationsProvider.notifier); - return Scaffold( + return ArahScaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.notifications), ), diff --git a/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart b/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart index a05fe055..43b8d8a3 100644 --- a/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart +++ b/frontend/arah.app/lib/features/onboarding/presentation/screens/onboarding_screen.dart @@ -126,7 +126,7 @@ class _OnboardingScreenState extends ConsumerState { }); } - return Scaffold( + return ArahScaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -378,7 +378,7 @@ class _OnboardingMap extends ConsumerWidget { children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.araponga.app', + userAgentPackageName: AppConstants.mapUserAgentPackage, ), // Contorno do território mais próximo (polígono ou círculo) detailAsync.when( diff --git a/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart b/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart index cb9a7727..299c8460 100644 --- a/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart +++ b/frontend/arah.app/lib/features/profile/presentation/screens/profile_screen.dart @@ -20,12 +20,13 @@ class ProfileScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final auth = ref.watch(authStateProvider); final session = auth.valueOrNull; if (session == null) { return ArahScaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.profile)), + appBar: AppBar(title: Text(l10n.profile)), body: Center( child: Padding( padding: const EdgeInsets.all(AppConstants.spacingLg), @@ -33,13 +34,13 @@ class ProfileScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ ArahBrandHeader( - subtitle: 'Entre na sua conta para acessar perfil, publicar e notificações.', + subtitle: l10n.enterToAccess, size: ArahBrandHeaderSize.medium, ), const SizedBox(height: AppConstants.spacingLg), FilledButton( onPressed: () => context.push('/login'), - child: Text(AppLocalizations.of(context)!.login), + child: Text(l10n.login), ), ], ), @@ -51,9 +52,9 @@ class ProfileScreen extends ConsumerWidget { final profileAsync = ref.watch(meProfileProvider); final authUser = session.user; - return Scaffold( + return ArahScaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.profile), + title: Text(l10n.profile), actions: [ IconButton( icon: const Icon(Icons.settings), @@ -104,14 +105,14 @@ class ProfileScreen extends ConsumerWidget { Icon(Icons.error_outline, size: AppConstants.iconSizeLg, color: Theme.of(context).colorScheme.error), const SizedBox(height: AppConstants.spacingMd), Text( - err is ApiException ? err.userMessage : AppLocalizations.of(context)!.errorLoad, + err is ApiException ? err.userMessage : l10n.errorLoad, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: AppConstants.spacingMd), FilledButton.tonal( onPressed: () => ref.invalidate(meProfileProvider), - child: Text(AppLocalizations.of(context)!.tryAgain), + child: Text(l10n.tryAgain), ), ], ), @@ -122,6 +123,7 @@ class ProfileScreen extends ConsumerWidget { } void _showSettingsSheet(BuildContext context) { + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( @@ -133,7 +135,7 @@ class ProfileScreen extends ConsumerWidget { children: [ ListTile( leading: const Icon(Icons.people_outline), - title: const Text('Conexões'), + title: Text(l10n.connections), onTap: () { Navigator.of(ctx).pop(); context.push('/connections'); @@ -141,7 +143,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.home_work_outlined), - title: const Text('Membership'), + title: Text(l10n.membership), onTap: () { Navigator.of(ctx).pop(); context.push('/membership'); @@ -149,7 +151,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.storefront_outlined), - title: const Text('Marketplace'), + title: Text(l10n.marketplace), onTap: () { Navigator.of(ctx).pop(); context.push('/marketplace'); @@ -157,7 +159,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.chat_outlined), - title: const Text('Chat'), + title: Text(l10n.chat), onTap: () { Navigator.of(ctx).pop(); context.push('/chat'); @@ -165,7 +167,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.warning_amber_outlined), - title: const Text('Alertas'), + title: Text(l10n.alertsTitle), onTap: () { Navigator.of(ctx).pop(); context.push('/alerts'); @@ -173,7 +175,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.gavel_outlined), - title: const Text('Moderação'), + title: Text(l10n.moderation), onTap: () { Navigator.of(ctx).pop(); context.push('/moderation'); @@ -181,7 +183,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.inventory_2_outlined), - title: const Text('Assets'), + title: Text(l10n.assetsTitle), onTap: () { Navigator.of(ctx).pop(); context.push('/assets'); @@ -189,7 +191,7 @@ class ProfileScreen extends ConsumerWidget { ), ListTile( leading: const Icon(Icons.card_membership_outlined), - title: const Text('Assinaturas'), + title: Text(l10n.subscriptions), onTap: () { Navigator.of(ctx).pop(); context.push('/subscriptions'); diff --git a/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart b/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart index 3f9e3b49..e62de351 100644 --- a/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart +++ b/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; +import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../providers/subscriptions_provider.dart'; class SubscriptionsScreen extends ConsumerWidget { @@ -11,11 +13,12 @@ class SubscriptionsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context)!; final state = ref.watch(subscriptionsProvider); final notifier = ref.read(subscriptionsProvider.notifier); - return Scaffold( - appBar: AppBar(title: const Text('Assinaturas')), + return ArahScaffold( + appBar: AppBar(title: Text(l10n.subscriptions)), body: RefreshIndicator( onRefresh: () => notifier.refresh(), child: _buildBody(context, state, notifier), @@ -28,6 +31,7 @@ class SubscriptionsScreen extends ConsumerWidget { SubscriptionsState state, SubscriptionsNotifier notifier, ) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.plans.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -63,8 +67,8 @@ class SubscriptionsScreen extends ConsumerWidget { Card( child: ListTile( leading: const Icon(Icons.verified_outlined), - title: const Text('Minha assinatura'), - subtitle: Text('Status: ${state.mySubscription!.status}'), + title: Text(l10n.mySubscription), + subtitle: Text(l10n.statusLabel(state.mySubscription!.status)), trailing: hasPaidSubscription ? TextButton( onPressed: () async { @@ -82,14 +86,14 @@ class SubscriptionsScreen extends ConsumerWidget { } } }, - child: const Text('Cancelar'), + child: Text(l10n.cancelSubscription), ) : null, ), ), const SizedBox(height: AppConstants.spacingMd), ], - Text('Planos disponíveis', style: Theme.of(context).textTheme.titleMedium), + Text(l10n.availablePlans, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: AppConstants.spacingSm), ...state.plans.map( (plan) => Card( @@ -112,7 +116,7 @@ class SubscriptionsScreen extends ConsumerWidget { } } }, - child: const Text('Assinar'), + child: Text(l10n.subscribe), ), ), ), diff --git a/frontend/arah.app/lib/l10n/app_localizations.dart b/frontend/arah.app/lib/l10n/app_localizations.dart index b1ddb47f..aa861285 100644 --- a/frontend/arah.app/lib/l10n/app_localizations.dart +++ b/frontend/arah.app/lib/l10n/app_localizations.dart @@ -745,6 +745,318 @@ abstract class AppLocalizations { /// In pt, this message translates to: /// **'As senhas não coincidem'** String get passwordsDontMatch; + + /// No description provided for @chooseTerritoryFirst. + /// + /// In pt, this message translates to: + /// **'Escolha um território primeiro.'** + String get chooseTerritoryFirst; + + /// No description provided for @cancel. + /// + /// In pt, this message translates to: + /// **'Cancelar'** + String get cancel; + + /// No description provided for @create. + /// + /// In pt, this message translates to: + /// **'Criar'** + String get create; + + /// No description provided for @send. + /// + /// In pt, this message translates to: + /// **'Enviar'** + String get send; + + /// No description provided for @delete. + /// + /// In pt, this message translates to: + /// **'Excluir'** + String get delete; + + /// No description provided for @deletePost. + /// + /// In pt, this message translates to: + /// **'Excluir post'** + String get deletePost; + + /// No description provided for @deletePostConfirm. + /// + /// In pt, this message translates to: + /// **'Esta ação não pode ser desfeita.'** + String get deletePostConfirm; + + /// No description provided for @marketplace. + /// + /// In pt, this message translates to: + /// **'Marketplace'** + String get marketplace; + + /// No description provided for @chat. + /// + /// In pt, this message translates to: + /// **'Chat'** + String get chat; + + /// No description provided for @alertsTitle. + /// + /// In pt, this message translates to: + /// **'Alertas'** + String get alertsTitle; + + /// No description provided for @moderation. + /// + /// In pt, this message translates to: + /// **'Moderação'** + String get moderation; + + /// No description provided for @assetsTitle. + /// + /// In pt, this message translates to: + /// **'Assets'** + String get assetsTitle; + + /// No description provided for @membership. + /// + /// In pt, this message translates to: + /// **'Membership'** + String get membership; + + /// No description provided for @subscriptions. + /// + /// In pt, this message translates to: + /// **'Assinaturas'** + String get subscriptions; + + /// No description provided for @connections. + /// + /// In pt, this message translates to: + /// **'Conexões'** + String get connections; + + /// No description provided for @add. + /// + /// In pt, this message translates to: + /// **'Adicionar'** + String get add; + + /// No description provided for @newGroup. + /// + /// In pt, this message translates to: + /// **'Novo grupo'** + String get newGroup; + + /// No description provided for @newAsset. + /// + /// In pt, this message translates to: + /// **'Novo asset'** + String get newAsset; + + /// No description provided for @assetName. + /// + /// In pt, this message translates to: + /// **'Nome'** + String get assetName; + + /// No description provided for @assetType. + /// + /// In pt, this message translates to: + /// **'Tipo'** + String get assetType; + + /// No description provided for @validate. + /// + /// In pt, this message translates to: + /// **'Validar'** + String get validate; + + /// No description provided for @archive. + /// + /// In pt, this message translates to: + /// **'Arquivar'** + String get archive; + + /// No description provided for @approveCurator. + /// + /// In pt, this message translates to: + /// **'Aprovar (curador)'** + String get approveCurator; + + /// No description provided for @rejectCurator. + /// + /// In pt, this message translates to: + /// **'Rejeitar (curador)'** + String get rejectCurator; + + /// No description provided for @noAssetsRegistered. + /// + /// In pt, this message translates to: + /// **'Nenhum asset cadastrado.'** + String get noAssetsRegistered; + + /// No description provided for @noItemsFound. + /// + /// In pt, this message translates to: + /// **'Nenhum item encontrado.'** + String get noItemsFound; + + /// No description provided for @noQueueItems. + /// + /// In pt, this message translates to: + /// **'Nenhum item nesta fila.'** + String get noQueueItems; + + /// No description provided for @noAlertsActive. + /// + /// In pt, this message translates to: + /// **'Nenhum alerta ativo'** + String get noAlertsActive; + + /// No description provided for @reportAlert. + /// + /// In pt, this message translates to: + /// **'Reportar alerta'** + String get reportAlert; + + /// No description provided for @chooseTerritoryForAlerts. + /// + /// In pt, this message translates to: + /// **'Escolha um território para ver alertas.'** + String get chooseTerritoryForAlerts; + + /// No description provided for @pending. + /// + /// In pt, this message translates to: + /// **'Pendentes'** + String get pending; + + /// No description provided for @connect. + /// + /// In pt, this message translates to: + /// **'Conectar'** + String get connect; + + /// No description provided for @searchPeople. + /// + /// In pt, this message translates to: + /// **'Buscar pessoas'** + String get searchPeople; + + /// No description provided for @statusLabel. + /// + /// In pt, this message translates to: + /// **'Status: {status}'** + String statusLabel(String status); + + /// No description provided for @checkoutWithCount. + /// + /// In pt, this message translates to: + /// **'Checkout ({count})'** + String checkoutWithCount(int count); + + /// No description provided for @myStore. + /// + /// In pt, this message translates to: + /// **'Minha loja'** + String get myStore; + + /// No description provided for @itemsTab. + /// + /// In pt, this message translates to: + /// **'Itens'** + String get itemsTab; + + /// No description provided for @availablePlans. + /// + /// In pt, this message translates to: + /// **'Planos disponíveis'** + String get availablePlans; + + /// No description provided for @mySubscription. + /// + /// In pt, this message translates to: + /// **'Minha assinatura'** + String get mySubscription; + + /// No description provided for @subscribe. + /// + /// In pt, this message translates to: + /// **'Assinar'** + String get subscribe; + + /// No description provided for @cancelSubscription. + /// + /// In pt, this message translates to: + /// **'Cancelar'** + String get cancelSubscription; + + /// No description provided for @yourRole. + /// + /// In pt, this message translates to: + /// **'Seu papel'** + String get yourRole; + + /// No description provided for @verificationLabel. + /// + /// In pt, this message translates to: + /// **'Verificação: {value}'** + String verificationLabel(String value); + + /// No description provided for @requestResidency. + /// + /// In pt, this message translates to: + /// **'Solicitar residência'** + String get requestResidency; + + /// No description provided for @verifyByLocation. + /// + /// In pt, this message translates to: + /// **'Verificar por localização'** + String get verifyByLocation; + + /// No description provided for @channelsTab. + /// + /// In pt, this message translates to: + /// **'Canais'** + String get channelsTab; + + /// No description provided for @groupsTab. + /// + /// In pt, this message translates to: + /// **'Grupos'** + String get groupsTab; + + /// No description provided for @groupName. + /// + /// In pt, this message translates to: + /// **'Nome do grupo'** + String get groupName; + + /// No description provided for @openStreetMapAttribution. + /// + /// In pt, this message translates to: + /// **'OpenStreetMap contributors'** + String get openStreetMapAttribution; + + /// No description provided for @priceLabel. + /// + /// In pt, this message translates to: + /// **'{currency} {amount}'** + String priceLabel(String currency, String amount); + + /// No description provided for @storeAndPrice. + /// + /// In pt, this message translates to: + /// **'{store} · {price}'** + String storeAndPrice(String store, String price); + + /// No description provided for @conversationMeta. + /// + /// In pt, this message translates to: + /// **'{kind} · {status}'** + String conversationMeta(String kind, String status); } class _AppLocalizationsDelegate diff --git a/frontend/arah.app/lib/l10n/app_localizations_en.dart b/frontend/arah.app/lib/l10n/app_localizations_en.dart index 884203bb..37bfba66 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_en.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_en.dart @@ -342,4 +342,172 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordsDontMatch => 'Passwords do not match'; + + @override + String get chooseTerritoryFirst => 'Choose a territory first.'; + + @override + String get cancel => 'Cancel'; + + @override + String get create => 'Create'; + + @override + String get send => 'Send'; + + @override + String get delete => 'Delete'; + + @override + String get deletePost => 'Delete post'; + + @override + String get deletePostConfirm => 'This action cannot be undone.'; + + @override + String get marketplace => 'Marketplace'; + + @override + String get chat => 'Chat'; + + @override + String get alertsTitle => 'Alerts'; + + @override + String get moderation => 'Moderation'; + + @override + String get assetsTitle => 'Assets'; + + @override + String get membership => 'Membership'; + + @override + String get subscriptions => 'Subscriptions'; + + @override + String get connections => 'Connections'; + + @override + String get add => 'Add'; + + @override + String get newGroup => 'New group'; + + @override + String get newAsset => 'New asset'; + + @override + String get assetName => 'Name'; + + @override + String get assetType => 'Type'; + + @override + String get validate => 'Validate'; + + @override + String get archive => 'Archive'; + + @override + String get approveCurator => 'Approve (curator)'; + + @override + String get rejectCurator => 'Reject (curator)'; + + @override + String get noAssetsRegistered => 'No assets registered.'; + + @override + String get noItemsFound => 'No items found.'; + + @override + String get noQueueItems => 'No items in this queue.'; + + @override + String get noAlertsActive => 'No active alerts'; + + @override + String get reportAlert => 'Report alert'; + + @override + String get chooseTerritoryForAlerts => 'Choose a territory to see alerts.'; + + @override + String get pending => 'Pending'; + + @override + String get connect => 'Connect'; + + @override + String get searchPeople => 'Search people'; + + @override + String statusLabel(String status) { + return 'Status: $status'; + } + + @override + String checkoutWithCount(int count) { + return 'Checkout ($count)'; + } + + @override + String get myStore => 'My store'; + + @override + String get itemsTab => 'Items'; + + @override + String get availablePlans => 'Available plans'; + + @override + String get mySubscription => 'My subscription'; + + @override + String get subscribe => 'Subscribe'; + + @override + String get cancelSubscription => 'Cancel'; + + @override + String get yourRole => 'Your role'; + + @override + String verificationLabel(String value) { + return 'Verification: $value'; + } + + @override + String get requestResidency => 'Request residency'; + + @override + String get verifyByLocation => 'Verify by location'; + + @override + String get channelsTab => 'Channels'; + + @override + String get groupsTab => 'Groups'; + + @override + String get groupName => 'Group name'; + + @override + String get openStreetMapAttribution => 'OpenStreetMap contributors'; + + @override + String priceLabel(String currency, String amount) { + return '$currency $amount'; + } + + @override + String storeAndPrice(String store, String price) { + return '$store · $price'; + } + + @override + String conversationMeta(String kind, String status) { + return '$kind · $status'; + } } diff --git a/frontend/arah.app/lib/l10n/app_localizations_pt.dart b/frontend/arah.app/lib/l10n/app_localizations_pt.dart index adf89f4d..baf49263 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_pt.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_pt.dart @@ -344,4 +344,173 @@ class AppLocalizationsPt extends AppLocalizations { @override String get passwordsDontMatch => 'As senhas não coincidem'; + + @override + String get chooseTerritoryFirst => 'Escolha um território primeiro.'; + + @override + String get cancel => 'Cancelar'; + + @override + String get create => 'Criar'; + + @override + String get send => 'Enviar'; + + @override + String get delete => 'Excluir'; + + @override + String get deletePost => 'Excluir post'; + + @override + String get deletePostConfirm => 'Esta ação não pode ser desfeita.'; + + @override + String get marketplace => 'Marketplace'; + + @override + String get chat => 'Chat'; + + @override + String get alertsTitle => 'Alertas'; + + @override + String get moderation => 'Moderação'; + + @override + String get assetsTitle => 'Assets'; + + @override + String get membership => 'Membership'; + + @override + String get subscriptions => 'Assinaturas'; + + @override + String get connections => 'Conexões'; + + @override + String get add => 'Adicionar'; + + @override + String get newGroup => 'Novo grupo'; + + @override + String get newAsset => 'Novo asset'; + + @override + String get assetName => 'Nome'; + + @override + String get assetType => 'Tipo'; + + @override + String get validate => 'Validar'; + + @override + String get archive => 'Arquivar'; + + @override + String get approveCurator => 'Aprovar (curador)'; + + @override + String get rejectCurator => 'Rejeitar (curador)'; + + @override + String get noAssetsRegistered => 'Nenhum asset cadastrado.'; + + @override + String get noItemsFound => 'Nenhum item encontrado.'; + + @override + String get noQueueItems => 'Nenhum item nesta fila.'; + + @override + String get noAlertsActive => 'Nenhum alerta ativo'; + + @override + String get reportAlert => 'Reportar alerta'; + + @override + String get chooseTerritoryForAlerts => + 'Escolha um território para ver alertas.'; + + @override + String get pending => 'Pendentes'; + + @override + String get connect => 'Conectar'; + + @override + String get searchPeople => 'Buscar pessoas'; + + @override + String statusLabel(String status) { + return 'Status: $status'; + } + + @override + String checkoutWithCount(int count) { + return 'Checkout ($count)'; + } + + @override + String get myStore => 'Minha loja'; + + @override + String get itemsTab => 'Itens'; + + @override + String get availablePlans => 'Planos disponíveis'; + + @override + String get mySubscription => 'Minha assinatura'; + + @override + String get subscribe => 'Assinar'; + + @override + String get cancelSubscription => 'Cancelar'; + + @override + String get yourRole => 'Seu papel'; + + @override + String verificationLabel(String value) { + return 'Verificação: $value'; + } + + @override + String get requestResidency => 'Solicitar residência'; + + @override + String get verifyByLocation => 'Verificar por localização'; + + @override + String get channelsTab => 'Canais'; + + @override + String get groupsTab => 'Grupos'; + + @override + String get groupName => 'Nome do grupo'; + + @override + String get openStreetMapAttribution => 'OpenStreetMap contributors'; + + @override + String priceLabel(String currency, String amount) { + return '$currency $amount'; + } + + @override + String storeAndPrice(String store, String price) { + return '$store · $price'; + } + + @override + String conversationMeta(String kind, String status) { + return '$kind · $status'; + } } From 314ee184d28615260e0db54d29f57cd87ede0e73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 09:17:34 +0000 Subject: [PATCH 3/4] =?UTF-8?q?feat(app):=20logo=20Arah,=20migra=C3=A7?= =?UTF-8?q?=C3=A3o=20storage=20arah=5F*=20e=20l10n=20ampliado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Logo SVG oficial em ArahBrandHeader (flutter_svg) - StorageMigration: araponga_* → arah_* na inicialização - Chaves arah_* e user-agent com.arah.app - +50 strings l10n (features, ações comuns) - Teste de migração de território legado Co-authored-by: Rapha --- frontend/arah.app/assets/images/arah-icon.svg | 13 +++ .../arah.app/lib/core/config/constants.dart | 11 ++- .../lib/core/storage/storage_migration.dart | 49 +++++++++++ .../lib/core/widgets/arah_brand_header.dart | 28 +++--- frontend/arah.app/lib/l10n/app_en.arb | 87 ++++++++++++++++++- frontend/arah.app/lib/l10n/app_pt.arb | 87 ++++++++++++++++++- frontend/arah.app/lib/main.dart | 2 + frontend/arah.app/pubspec.yaml | 2 + .../core/storage/storage_migration_test.dart | 21 +++++ 9 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 frontend/arah.app/assets/images/arah-icon.svg create mode 100644 frontend/arah.app/lib/core/storage/storage_migration.dart create mode 100644 frontend/arah.app/test/core/storage/storage_migration_test.dart diff --git a/frontend/arah.app/assets/images/arah-icon.svg b/frontend/arah.app/assets/images/arah-icon.svg new file mode 100644 index 00000000..7df4f6cc --- /dev/null +++ b/frontend/arah.app/assets/images/arah-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/arah.app/lib/core/config/constants.dart b/frontend/arah.app/lib/core/config/constants.dart index 3b990bfe..89fc5777 100644 --- a/frontend/arah.app/lib/core/config/constants.dart +++ b/frontend/arah.app/lib/core/config/constants.dart @@ -30,8 +30,11 @@ class AppConstants { static const double minTouchTargetSize = 44.0; static const int defaultPageSize = 20; static const int maxPageSize = 50; - static const String keyAccessToken = 'araponga_access_token'; - static const String keyRefreshToken = 'araponga_refresh_token'; - static const String keyTokenExpiry = 'araponga_token_expiry'; - static const String keySelectedTerritoryId = 'araponga_selected_territory_id'; + static const String keyAccessToken = 'arah_access_token'; + static const String keyRefreshToken = 'arah_refresh_token'; + static const String keyTokenExpiry = 'arah_token_expiry'; + static const String keySelectedTerritoryId = 'arah_selected_territory_id'; + + /// Pacote para user-agent de mapas (OpenStreetMap). + static const String mapUserAgentPackage = 'com.arah.app'; } diff --git a/frontend/arah.app/lib/core/storage/storage_migration.dart b/frontend/arah.app/lib/core/storage/storage_migration.dart new file mode 100644 index 00000000..67cbe266 --- /dev/null +++ b/frontend/arah.app/lib/core/storage/storage_migration.dart @@ -0,0 +1,49 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../config/constants.dart'; + +/// Migra chaves legadas `araponga_*` para `arah_*` (uma vez por instalação). +class StorageMigration { + StorageMigration._(); + + static const _migrationFlag = 'arah_storage_migrated_v1'; + + static const _secureKeyPairs = <({String legacy, String current})>[ + (legacy: 'araponga_access_token', current: AppConstants.keyAccessToken), + (legacy: 'araponga_refresh_token', current: AppConstants.keyRefreshToken), + (legacy: 'araponga_token_expiry', current: AppConstants.keyTokenExpiry), + ]; + + static Future migrateIfNeeded() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.getBool(_migrationFlag) == true) return; + + const secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); + + try { + for (final pair in _secureKeyPairs) { + final legacyValue = await secureStorage.read(key: pair.legacy); + if (legacyValue == null) continue; + final currentValue = await secureStorage.read(key: pair.current); + if (currentValue == null) { + await secureStorage.write(key: pair.current, value: legacyValue); + } + await secureStorage.delete(key: pair.legacy); + } + } catch (_) { + // Secure storage indisponível (ex.: testes unitários sem plugin). + } + + const legacyTerritoryKey = 'araponga_selected_territory_id'; + final legacyTerritory = prefs.getString(legacyTerritoryKey); + if (legacyTerritory != null && prefs.getString(AppConstants.keySelectedTerritoryId) == null) { + await prefs.setString(AppConstants.keySelectedTerritoryId, legacyTerritory); + } + await prefs.remove(legacyTerritoryKey); + + await prefs.setBool(_migrationFlag, true); + } +} diff --git a/frontend/arah.app/lib/core/widgets/arah_brand_header.dart b/frontend/arah.app/lib/core/widgets/arah_brand_header.dart index a09250d0..5d130bb3 100644 --- a/frontend/arah.app/lib/core/widgets/arah_brand_header.dart +++ b/frontend/arah.app/lib/core/widgets/arah_brand_header.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../config/brand_config.dart'; import '../config/constants.dart'; import '../theme/app_design_tokens.dart'; -/// Cabeçalho de marca com presença visual alinhada ao portal/wiki. +/// Cabeçalho de marca com logo e wordmark alinhados ao portal/wiki. class ArahBrandHeader extends StatelessWidget { const ArahBrandHeader({ super.key, @@ -17,6 +18,12 @@ class ArahBrandHeader extends StatelessWidget { final ArahBrandHeaderSize size; final bool center; + double get _logoSize => switch (size) { + ArahBrandHeaderSize.large => 48, + ArahBrandHeaderSize.medium => 40, + _ => 32, + }; + @override Widget build(BuildContext context) { final colors = context.appColors; @@ -44,18 +51,13 @@ class ArahBrandHeader extends StatelessWidget { mainAxisSize: center ? MainAxisSize.min : MainAxisSize.max, mainAxisAlignment: center ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ - Container( - width: size == ArahBrandHeaderSize.large ? 48 : 40, - height: size == ArahBrandHeaderSize.large ? 48 : 40, - decoration: BoxDecoration( - color: colors.accentSubtle, - borderRadius: BorderRadius.circular(AppConstants.radiusMd), - border: Border.all(color: colors.accentBorder), - ), - child: Icon( - Icons.terrain_outlined, - color: colors.primary, - size: size == ArahBrandHeaderSize.large ? 28 : 22, + ClipRRect( + borderRadius: BorderRadius.circular(AppConstants.radiusMd), + child: SvgPicture.asset( + 'assets/images/arah-icon.svg', + width: _logoSize, + height: _logoSize, + semanticsLabel: BrandConfig.name, ), ), const SizedBox(width: AppConstants.spacingSm + 4), diff --git a/frontend/arah.app/lib/l10n/app_en.arb b/frontend/arah.app/lib/l10n/app_en.arb index 74c33f4c..7160b6b5 100644 --- a/frontend/arah.app/lib/l10n/app_en.arb +++ b/frontend/arah.app/lib/l10n/app_en.arb @@ -108,5 +108,90 @@ "back": "Back", "accountCreated": "Account created. Welcome!", "passwordMinLength": "At least 6 characters", - "passwordsDontMatch": "Passwords do not match" + "passwordsDontMatch": "Passwords do not match", + "chooseTerritoryFirst": "Choose a territory first.", + "cancel": "Cancel", + "create": "Create", + "send": "Send", + "delete": "Delete", + "deletePost": "Delete post", + "deletePostConfirm": "This action cannot be undone.", + "marketplace": "Marketplace", + "chat": "Chat", + "alertsTitle": "Alerts", + "moderation": "Moderation", + "assetsTitle": "Assets", + "membership": "Membership", + "subscriptions": "Subscriptions", + "connections": "Connections", + "add": "Add", + "newGroup": "New group", + "newAsset": "New asset", + "assetName": "Name", + "assetType": "Type", + "validate": "Validate", + "archive": "Archive", + "approveCurator": "Approve (curator)", + "rejectCurator": "Reject (curator)", + "noAssetsRegistered": "No assets registered.", + "noItemsFound": "No items found.", + "noQueueItems": "No items in this queue.", + "noAlertsActive": "No active alerts", + "reportAlert": "Report alert", + "chooseTerritoryForAlerts": "Choose a territory to see alerts.", + "pending": "Pending", + "connect": "Connect", + "searchPeople": "Search people", + "statusLabel": "Status: {status}", + "@statusLabel": { + "placeholders": { + "status": {"type": "String"} + } + }, + "checkoutWithCount": "Checkout ({count})", + "@checkoutWithCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "myStore": "My store", + "itemsTab": "Items", + "availablePlans": "Available plans", + "mySubscription": "My subscription", + "subscribe": "Subscribe", + "cancelSubscription": "Cancel", + "yourRole": "Your role", + "verificationLabel": "Verification: {value}", + "@verificationLabel": { + "placeholders": { + "value": {"type": "String"} + } + }, + "requestResidency": "Request residency", + "verifyByLocation": "Verify by location", + "channelsTab": "Channels", + "groupsTab": "Groups", + "groupName": "Group name", + "openStreetMapAttribution": "OpenStreetMap contributors", + "priceLabel": "{currency} {amount}", + "@priceLabel": { + "placeholders": { + "currency": {"type": "String"}, + "amount": {"type": "String"} + } + }, + "storeAndPrice": "{store} · {price}", + "@storeAndPrice": { + "placeholders": { + "store": {"type": "String"}, + "price": {"type": "String"} + } + }, + "conversationMeta": "{kind} · {status}", + "@conversationMeta": { + "placeholders": { + "kind": {"type": "String"}, + "status": {"type": "String"} + } + } } diff --git a/frontend/arah.app/lib/l10n/app_pt.arb b/frontend/arah.app/lib/l10n/app_pt.arb index 16b3d3b8..7a7b0a7d 100644 --- a/frontend/arah.app/lib/l10n/app_pt.arb +++ b/frontend/arah.app/lib/l10n/app_pt.arb @@ -108,5 +108,90 @@ "back": "Voltar", "accountCreated": "Conta criada. Bem-vindo!", "passwordMinLength": "Mín. 6 caracteres", - "passwordsDontMatch": "As senhas não coincidem" + "passwordsDontMatch": "As senhas não coincidem", + "chooseTerritoryFirst": "Escolha um território primeiro.", + "cancel": "Cancelar", + "create": "Criar", + "send": "Enviar", + "delete": "Excluir", + "deletePost": "Excluir post", + "deletePostConfirm": "Esta ação não pode ser desfeita.", + "marketplace": "Marketplace", + "chat": "Chat", + "alertsTitle": "Alertas", + "moderation": "Moderação", + "assetsTitle": "Assets", + "membership": "Membership", + "subscriptions": "Assinaturas", + "connections": "Conexões", + "add": "Adicionar", + "newGroup": "Novo grupo", + "newAsset": "Novo asset", + "assetName": "Nome", + "assetType": "Tipo", + "validate": "Validar", + "archive": "Arquivar", + "approveCurator": "Aprovar (curador)", + "rejectCurator": "Rejeitar (curador)", + "noAssetsRegistered": "Nenhum asset cadastrado.", + "noItemsFound": "Nenhum item encontrado.", + "noQueueItems": "Nenhum item nesta fila.", + "noAlertsActive": "Nenhum alerta ativo", + "reportAlert": "Reportar alerta", + "chooseTerritoryForAlerts": "Escolha um território para ver alertas.", + "pending": "Pendentes", + "connect": "Conectar", + "searchPeople": "Buscar pessoas", + "statusLabel": "Status: {status}", + "@statusLabel": { + "placeholders": { + "status": {"type": "String"} + } + }, + "checkoutWithCount": "Checkout ({count})", + "@checkoutWithCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "myStore": "Minha loja", + "itemsTab": "Itens", + "availablePlans": "Planos disponíveis", + "mySubscription": "Minha assinatura", + "subscribe": "Assinar", + "cancelSubscription": "Cancelar", + "yourRole": "Seu papel", + "verificationLabel": "Verificação: {value}", + "@verificationLabel": { + "placeholders": { + "value": {"type": "String"} + } + }, + "requestResidency": "Solicitar residência", + "verifyByLocation": "Verificar por localização", + "channelsTab": "Canais", + "groupsTab": "Grupos", + "groupName": "Nome do grupo", + "openStreetMapAttribution": "OpenStreetMap contributors", + "priceLabel": "{currency} {amount}", + "@priceLabel": { + "placeholders": { + "currency": {"type": "String"}, + "amount": {"type": "String"} + } + }, + "storeAndPrice": "{store} · {price}", + "@storeAndPrice": { + "placeholders": { + "store": {"type": "String"}, + "price": {"type": "String"} + } + }, + "conversationMeta": "{kind} · {status}", + "@conversationMeta": { + "placeholders": { + "kind": {"type": "String"}, + "status": {"type": "String"} + } + } } diff --git a/frontend/arah.app/lib/main.dart b/frontend/arah.app/lib/main.dart index 3d4703b1..20f99d40 100644 --- a/frontend/arah.app/lib/main.dart +++ b/frontend/arah.app/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app.dart'; +import 'core/storage/storage_migration.dart'; void main() async { // Em debug: mesma zone para ensureInitialized e runApp (evita "Zone mismatch"). @@ -33,6 +34,7 @@ Future _runApp() async { } catch (_) { // Firebase não configurado (ex.: Web sem options, ou google-services.json ausente). } + await StorageMigration.migrateIfNeeded(); runApp( const ProviderScope( child: ArahApp(), diff --git a/frontend/arah.app/pubspec.yaml b/frontend/arah.app/pubspec.yaml index 55a8da70..6c89179c 100644 --- a/frontend/arah.app/pubspec.yaml +++ b/frontend/arah.app/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: cached_network_image: ^3.3.1 image_picker: ^1.1.2 google_fonts: ^6.2.1 + flutter_svg: ^2.0.10+1 # Geolocalização (convergência com território observado) geolocator: ^13.0.2 # Mapa (open source: OpenStreetMap; opcionalmente tiles Mapbox) @@ -47,3 +48,4 @@ flutter: generate: true assets: - assets/images/ + - assets/images/arah-icon.svg diff --git a/frontend/arah.app/test/core/storage/storage_migration_test.dart b/frontend/arah.app/test/core/storage/storage_migration_test.dart new file mode 100644 index 00000000..ccf914d9 --- /dev/null +++ b/frontend/arah.app/test/core/storage/storage_migration_test.dart @@ -0,0 +1,21 @@ +import 'package:arah_app/core/config/constants.dart'; +import 'package:arah_app/core/storage/storage_migration.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('StorageMigration copies legacy territory id to arah key', () async { + SharedPreferences.setMockInitialValues({ + 'araponga_selected_territory_id': 'territory-legacy', + }); + + await StorageMigration.migrateIfNeeded(); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString(AppConstants.keySelectedTerritoryId), 'territory-legacy'); + expect(prefs.getString('araponga_selected_territory_id'), isNull); + expect(prefs.getBool('arah_storage_migrated_v1'), isTrue); + }); +} From e42c5a1bd0c8980543c77b4a00d4ed4101b31a49 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 16:25:22 +0000 Subject: [PATCH 4/4] feat(app): complete l10n coverage and design fidelity gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Localize remaining hardcoded strings across all feature screens - Add 90+ l10n keys (comments, chat, marketplace, connections, etc.) - Use ArahScaffold on splash redirect screen - Rebrand Ará → Arah in app docs and run-app-local script Co-authored-by: Rapha --- frontend/arah.app/docs/ARCHITECTURE.md | 2 +- frontend/arah.app/docs/MAPA_PINS.md | 2 +- frontend/arah.app/docs/README.md | 2 +- frontend/arah.app/lib/app_router.dart | 3 +- .../presentation/screens/alerts_screen.dart | 12 +- .../presentation/screens/assets_screen.dart | 19 +- .../screens/chat_conversation_screen.dart | 11 +- .../screens/chat_list_screen.dart | 23 +- .../screens/connections_screen.dart | 31 +- .../screens/create_post_screen.dart | 6 +- .../presentation/screens/feed_screen.dart | 21 +- .../widgets/feed_comments_sheet.dart | 17 +- .../screens/marketplace_screen.dart | 46 +- .../screens/membership_screen.dart | 14 +- .../screens/moderation_screen.dart | 33 +- .../screens/subscriptions_screen.dart | 10 +- frontend/arah.app/lib/l10n/app_en.arb | 101 +++- .../arah.app/lib/l10n/app_localizations.dart | 498 ++++++++++++++++++ .../lib/l10n/app_localizations_en.dart | 257 +++++++++ .../lib/l10n/app_localizations_pt.dart | 259 +++++++++ frontend/arah.app/lib/l10n/app_pt.arb | 101 +++- frontend/arah.app/scripts/run-app-local.ps1 | 2 +- 22 files changed, 1355 insertions(+), 115 deletions(-) diff --git a/frontend/arah.app/docs/ARCHITECTURE.md b/frontend/arah.app/docs/ARCHITECTURE.md index a7e05f0e..ceb1ad3c 100644 --- a/frontend/arah.app/docs/ARCHITECTURE.md +++ b/frontend/arah.app/docs/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Arquitetura – Ará App (Flutter) +# Arquitetura – Arah App (Flutter) Visão da arquitetura do app: rede, estado, rotas e internacionalização. diff --git a/frontend/arah.app/docs/MAPA_PINS.md b/frontend/arah.app/docs/MAPA_PINS.md index 2c06d06b..0a5418b8 100644 --- a/frontend/arah.app/docs/MAPA_PINS.md +++ b/frontend/arah.app/docs/MAPA_PINS.md @@ -1,4 +1,4 @@ -# Mapa e pins – App Ará +# Mapa e pins – App Arah Implementação e opções de tiles (OpenStreetMap, Mapbox). diff --git a/frontend/arah.app/docs/README.md b/frontend/arah.app/docs/README.md index f5c6b463..112982eb 100644 --- a/frontend/arah.app/docs/README.md +++ b/frontend/arah.app/docs/README.md @@ -1,4 +1,4 @@ -# Documentação do app Ará +# Documentação do app Arah Documentação interna do app Flutter (frontend/Arah.app). diff --git a/frontend/arah.app/lib/app_router.dart b/frontend/arah.app/lib/app_router.dart index 9808da98..34b09738 100644 --- a/frontend/arah.app/lib/app_router.dart +++ b/frontend/arah.app/lib/app_router.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'core/config/app_config.dart'; import 'core/providers/territory_provider.dart'; +import 'core/widgets/arah_scaffold.dart'; import 'features/auth/presentation/screens/login_screen.dart'; import 'features/auth/presentation/providers/auth_state_provider.dart'; import 'features/home/presentation/screens/main_shell_screen.dart'; @@ -134,7 +135,7 @@ class _SplashOrRedirect extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( + return const ArahScaffold( body: Center(child: CircularProgressIndicator()), ); } diff --git a/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart b/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart index 76a2004a..5f2c7086 100644 --- a/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart +++ b/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart @@ -68,11 +68,11 @@ class _AlertsScreenState extends ConsumerState { children: [ TextField( controller: titleController, - decoration: const InputDecoration(labelText: 'Título'), + decoration: InputDecoration(labelText: l10n.title), ), TextField( controller: descController, - decoration: const InputDecoration(labelText: 'Descrição'), + decoration: InputDecoration(labelText: l10n.descriptionLabel), maxLines: 3, ), ], @@ -88,13 +88,13 @@ class _AlertsScreenState extends ConsumerState { ); if (ctx.mounted) { Navigator.pop(ctx); - showSuccessSnackBar(ctx, 'Alerta criado.'); + showSuccessSnackBar(ctx, l10n.alertCreated); } } catch (e) { if (ctx.mounted) { showErrorSnackBar( ctx, - e is ApiException ? e.userMessage : 'Erro ao criar alerta.', + e is ApiException ? e.userMessage : l10n.errorCreateAlert, ); } } @@ -118,7 +118,7 @@ class _AlertsScreenState extends ConsumerState { if (state.error != null && state.items.isEmpty) { final message = state.error is ApiException ? (state.error as ApiException).userMessage - : 'Não foi possível carregar alertas.'; + : l10n.errorLoadAlerts; return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ @@ -131,7 +131,7 @@ class _AlertsScreenState extends ConsumerState { Text(message, textAlign: TextAlign.center), const SizedBox(height: AppConstants.spacingSm), Text( - 'Alertas do território exigem residência ou curadoria.', + l10n.alertsRequireResidency, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart b/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart index b3e2fe40..dfe77e84 100644 --- a/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart +++ b/frontend/arah.app/lib/features/assets/presentation/screens/assets_screen.dart @@ -74,13 +74,13 @@ class AssetsScreen extends ConsumerWidget { ); if (ctx.mounted) { Navigator.pop(ctx); - showSuccessSnackBar(ctx, 'Asset criado.'); + showSuccessSnackBar(ctx, l10n.assetCreated); } } catch (e) { if (ctx.mounted) { showErrorSnackBar( ctx, - e is ApiException ? e.userMessage : 'Erro ao criar asset.', + e is ApiException ? e.userMessage : l10n.errorCreateAsset, ); } } @@ -109,7 +109,7 @@ class AssetsScreen extends ConsumerWidget { child: Text( state.error is ApiException ? (state.error as ApiException).userMessage - : 'Erro ao carregar assets.', + : l10n.errorLoadAssets, textAlign: TextAlign.center, ), ), @@ -133,7 +133,7 @@ class AssetsScreen extends ConsumerWidget { title: Text(asset.name), subtitle: Text( '${asset.type} · ${asset.status}' - '${asset.validationsCount > 0 ? ' · ${asset.validationsCount} validações (${asset.validationPct.toStringAsFixed(0)}%)' : ''}', + '${asset.validationsCount > 0 ? ' · ${l10n.assetValidationsMeta(asset.validationsCount, asset.validationPct.toStringAsFixed(0))}' : ''}', ), trailing: PopupMenuButton( onSelected: (value) => _onAssetAction(context, ref, asset, value), @@ -160,6 +160,7 @@ class AssetsScreen extends ConsumerWidget { AssetItem asset, String action, ) async { + final l10n = AppLocalizations.of(context)!; final notifier = ref.read(assetsProvider.notifier); try { if (action == 'validate') { @@ -167,24 +168,24 @@ class AssetsScreen extends ConsumerWidget { if (context.mounted) { showSuccessSnackBar( context, - 'Validação registrada (${result.validationPct.toStringAsFixed(0)}% da comunidade).', + l10n.validationRegistered(result.validationPct.toStringAsFixed(0)), ); } } else if (action == 'archive') { await notifier.archiveAsset(asset.id); - if (context.mounted) showSuccessSnackBar(context, 'Asset arquivado.'); + if (context.mounted) showSuccessSnackBar(context, l10n.assetArchived); } else if (action == 'approve') { await notifier.curateAsset(asset.id, outcome: 'APPROVED'); - if (context.mounted) showSuccessSnackBar(context, 'Asset aprovado.'); + if (context.mounted) showSuccessSnackBar(context, l10n.assetApproved); } else if (action == 'reject') { await notifier.curateAsset(asset.id, outcome: 'REJECTED'); - if (context.mounted) showSuccessSnackBar(context, 'Asset rejeitado.'); + if (context.mounted) showSuccessSnackBar(context, l10n.assetRejected); } } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Não foi possível concluir a ação.', + e is ApiException ? e.userMessage : l10n.errorCompleteAction, ); } } diff --git a/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart b/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart index 918c10bb..94a6ca82 100644 --- a/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart +++ b/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart @@ -6,6 +6,7 @@ import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../core/widgets/app_snackbar.dart'; import '../../../../core/widgets/arah_scaffold.dart'; +import '../../../../l10n/app_localizations.dart'; import '../providers/chat_provider.dart'; class ChatConversationScreen extends ConsumerStatefulWidget { @@ -36,9 +37,10 @@ class _ChatConversationScreenState extends ConsumerState _controller.clear(); } catch (e) { if (mounted) { + final l10n = AppLocalizations.of(context)!; showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao enviar mensagem.', + e is ApiException ? e.userMessage : l10n.errorSendMessage, ); } } finally { @@ -48,11 +50,12 @@ class _ChatConversationScreenState extends ConsumerState @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final state = ref.watch(chatConversationProvider(widget.conversationId)); final timeFormat = DateFormat('HH:mm'); return ArahScaffold( - appBar: AppBar(title: Text(widget.title ?? 'Conversa')), + appBar: AppBar(title: Text(widget.title ?? l10n.conversation)), body: Column( children: [ Expanded( @@ -95,8 +98,8 @@ class _ChatConversationScreenState extends ConsumerState Expanded( child: TextField( controller: _controller, - decoration: const InputDecoration( - hintText: 'Mensagem', + decoration: InputDecoration( + hintText: l10n.messageHint, border: OutlineInputBorder(), ), onSubmitted: (_) => _send(), diff --git a/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart b/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart index 0246f1d2..fd08be0b 100644 --- a/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart +++ b/frontend/arah.app/lib/features/chat/presentation/screens/chat_list_screen.dart @@ -51,8 +51,8 @@ class _ChatListScreenState extends ConsumerState with SingleTick title: Text(l10n.newGroup), content: TextField( controller: nameController, - decoration: const InputDecoration( - labelText: 'Nome do grupo', + decoration: InputDecoration( + labelText: l10n.groupName, border: OutlineInputBorder(), ), ), @@ -66,7 +66,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick final group = await ref.read(chatListProvider.notifier).createGroup(name); if (ctx.mounted) { Navigator.pop(ctx); - showSuccessSnackBar(context, 'Grupo criado.'); + showSuccessSnackBar(context, l10n.groupCreated); if (mounted) { context.push('/chat/${group.id}?title=${Uri.encodeComponent(group.name)}'); } @@ -75,7 +75,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick if (ctx.mounted) { showErrorSnackBar( ctx, - e is ApiException ? e.userMessage : 'Erro ao criar grupo.', + e is ApiException ? e.userMessage : l10n.errorCreateGroup, ); } } @@ -106,9 +106,9 @@ class _ChatListScreenState extends ConsumerState with SingleTick title: Text(l10n.chat), bottom: TabBar( controller: _tabController, - tabs: const [ - Tab(text: 'Canais'), - Tab(text: 'Grupos'), + tabs: [ + Tab(text: l10n.channelsTab), + Tab(text: l10n.groupsTab), ], ), ), @@ -133,6 +133,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick } Widget _buildBody(BuildContext context, ChatListState state) { + final l10n = AppLocalizations.of(context)!; if (state.isLoading && state.activeItems.isEmpty) { return ListView( physics: AlwaysScrollableScrollPhysics(), @@ -148,7 +149,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick child: Text( state.error is ApiException ? (state.error as ApiException).userMessage - : 'Erro ao carregar conversas.', + : l10n.errorLoadConversations, textAlign: TextAlign.center, ), ), @@ -164,8 +165,8 @@ class _ChatListScreenState extends ConsumerState with SingleTick child: Center( child: Text( state.tab == ChatListTab.channels - ? 'Nenhum canal disponível.' - : 'Nenhum grupo ainda. Toque + para criar.', + ? l10n.noChannelsAvailable + : l10n.noGroupsYet, ), ), ), @@ -182,7 +183,7 @@ class _ChatListScreenState extends ConsumerState with SingleTick state.tab == ChatListTab.channels ? Icons.chat_bubble_outline : Icons.group_outlined, ), title: Text(conversation.name), - subtitle: Text('${conversation.kind} · ${conversation.status}'), + subtitle: Text(l10n.conversationMeta(conversation.kind, conversation.status)), onTap: () => context.push('/chat/${conversation.id}?title=${Uri.encodeComponent(conversation.name)}'), ); }, diff --git a/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart b/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart index 5cf447e6..f5dc1bfe 100644 --- a/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart +++ b/frontend/arah.app/lib/features/connections/presentation/screens/connections_screen.dart @@ -35,6 +35,7 @@ class ConnectionsScreen extends ConsumerWidget { } Future _openSearchSheet(BuildContext context, WidgetRef ref) async { + final l10n = AppLocalizations.of(context)!; await showModalBottomSheet( context: context, isScrollControlled: true, @@ -44,14 +45,14 @@ class ConnectionsScreen extends ConsumerWidget { try { await ref.read(connectionsProvider.notifier).sendRequest(userId); if (ctx.mounted) { - showSuccessSnackBar(ctx, 'Solicitação enviada.'); + showSuccessSnackBar(ctx, l10n.requestSent); Navigator.pop(ctx); } } catch (e) { if (ctx.mounted) { showErrorSnackBar( ctx, - e is ApiException ? e.userMessage : 'Erro ao enviar solicitação.', + e is ApiException ? e.userMessage : l10n.errorSendRequest, ); } } @@ -72,7 +73,7 @@ class ConnectionsScreen extends ConsumerWidget { if (state.error != null && state.accepted.isEmpty && state.pending.isEmpty) { final message = state.error is ApiException ? (state.error as ApiException).userMessage - : 'Não foi possível carregar conexões.'; + : l10n.errorLoadConnections; return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ @@ -111,7 +112,7 @@ class ConnectionsScreen extends ConsumerWidget { Padding( padding: const EdgeInsets.symmetric(vertical: AppConstants.spacingLg), child: Text( - 'Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.', + l10n.noConnectionsYet, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -152,6 +153,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> } Future _loadSuggestions() async { + final l10n = AppLocalizations.of(context)!; setState(() { _loading = true; _error = null; @@ -162,7 +164,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> } catch (e) { if (mounted) { setState(() { - _error = e is ApiException ? e.userMessage : 'Erro ao carregar sugestões.'; + _error = e is ApiException ? e.userMessage : l10n.errorLoadSuggestions; }); } } finally { @@ -171,6 +173,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> } Future _search(String query) async { + final l10n = AppLocalizations.of(context)!; if (query.trim().length < 2) { await _loadSuggestions(); return; @@ -185,7 +188,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> } catch (e) { if (mounted) { setState(() { - _error = e is ApiException ? e.userMessage : 'Erro na busca.'; + _error = e is ApiException ? e.userMessage : l10n.errorSearch; _results = const []; }); } @@ -212,7 +215,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> TextField( controller: _queryController, decoration: InputDecoration( - hintText: 'Nome de exibição', + hintText: l10n.displayNameHint, prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), suffixIcon: IconButton( @@ -239,7 +242,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet> child: _results.isEmpty && !_loading ? Center( child: Text( - 'Digite ao menos 2 caracteres ou veja sugestões acima.', + l10n.searchMinCharsHint, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -291,7 +294,11 @@ class _ConnectionTile extends StatelessWidget { margin: const EdgeInsets.only(bottom: AppConstants.spacingSm), child: ListTile( leading: CircleAvatar(child: Text(item.isIncoming ? '←' : '→')), - title: Text(isPending ? 'Solicitação ${item.isIncoming ? 'recebida' : 'enviada'}' : 'Conexão ativa'), + title: Text( + isPending + ? (item.isIncoming ? l10n.connectionRequestIncoming : l10n.connectionRequestOutgoing) + : l10n.activeConnection, + ), subtitle: Text(l10n.statusLabel(item.status)), trailing: isPending && item.isIncoming ? Row( @@ -306,7 +313,7 @@ class _ConnectionTile extends StatelessWidget { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao aceitar.', + e is ApiException ? e.userMessage : l10n.errorAccept, ); } } @@ -321,7 +328,7 @@ class _ConnectionTile extends StatelessWidget { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao rejeitar.', + e is ApiException ? e.userMessage : l10n.errorReject, ); } } @@ -339,7 +346,7 @@ class _ConnectionTile extends StatelessWidget { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao remover.', + e is ApiException ? e.userMessage : l10n.errorRemove, ); } } diff --git a/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart b/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart index 5256289b..85138554 100644 --- a/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart +++ b/frontend/arah.app/lib/features/feed/presentation/screens/create_post_screen.dart @@ -45,7 +45,7 @@ class _CreatePostScreenState extends ConsumerState { if (!(_formKey.currentState?.validate() ?? false)) return; final territoryId = ref.read(selectedTerritoryIdValueProvider); if (territoryId == null || territoryId.isEmpty) { - if (mounted) showErrorSnackBar(context, 'Escolha um território antes de publicar.'); + if (mounted) showErrorSnackBar(context, AppLocalizations.of(context)!.chooseTerritoryBeforePost); return; } setState(() => _submitting = true); @@ -207,7 +207,9 @@ class _CreatePostScreenState extends ConsumerState { OutlinedButton.icon( onPressed: _submitting ? null : _pickImage, icon: const Icon(Icons.image_outlined), - label: Text(_selectedImagePath == null ? 'Adicionar imagem' : 'Trocar imagem'), + label: Text(_selectedImagePath == null + ? AppLocalizations.of(context)!.addImage + : AppLocalizations.of(context)!.changeImage), ), if (_selectedImagePath != null) ...[ const SizedBox(height: AppConstants.spacingSm), diff --git a/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart b/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart index f6326f01..134f7ee1 100644 --- a/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart +++ b/frontend/arah.app/lib/features/feed/presentation/screens/feed_screen.dart @@ -50,7 +50,7 @@ class _FeedScreenState extends ConsumerState { Padding( padding: const EdgeInsets.all(AppConstants.spacingMd), child: Text( - 'Escolha um território para ver o feed da região', + l10n.chooseTerritory, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -301,6 +301,7 @@ class _FeedListState extends ConsumerState<_FeedList> { final isLoadingMore = widget.isLoadingMore; final onLoadMore = widget.onLoadMore; if (items.isEmpty) { + final l10n = AppLocalizations.of(context)!; return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ @@ -317,12 +318,12 @@ class _FeedListState extends ConsumerState<_FeedList> { ), const SizedBox(height: AppConstants.spacingMd), Text( - 'Nenhum post nesta região', + l10n.noPostsHere, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppConstants.spacingSm), Text( - 'Seja o primeiro a publicar aqui.', + l10n.beFirstToPost, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -359,7 +360,7 @@ class _FeedListState extends ConsumerState<_FeedList> { final item = items[index] as Map?; final post = item?['post'] as Map?; final postId = post?['id']?.toString() ?? ''; - final title = post?['title']?.toString() ?? 'Post'; + final title = post?['title']?.toString() ?? AppLocalizations.of(context)!.postDefaultTitle; final content = post?['content']?.toString() ?? ''; final postType = post?['type']?.toString(); final counts = FeedPostCounts.fromJson(item?['counts'] as Map?); @@ -416,14 +417,16 @@ class _FeedTypeFilterBar extends StatelessWidget { final String? selectedType; final ValueChanged onTypeSelected; - static const _options = { - null: 'Todos', - 'general': 'Geral', - 'alert': 'Alerta', + static Map _options(AppLocalizations l10n) => { + null: l10n.filterAll, + 'general': l10n.general, + 'alert': l10n.alert, }; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final options = _options(l10n); return SingleChildScrollView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric( @@ -431,7 +434,7 @@ class _FeedTypeFilterBar extends StatelessWidget { vertical: AppConstants.spacingSm, ), child: Row( - children: _options.entries.map((entry) { + children: options.entries.map((entry) { final selected = selectedType?.toLowerCase() == entry.key?.toLowerCase() || (selectedType == null && entry.key == null); return Padding( diff --git a/frontend/arah.app/lib/features/feed/presentation/widgets/feed_comments_sheet.dart b/frontend/arah.app/lib/features/feed/presentation/widgets/feed_comments_sheet.dart index 6ead1fc2..ddc9647a 100644 --- a/frontend/arah.app/lib/features/feed/presentation/widgets/feed_comments_sheet.dart +++ b/frontend/arah.app/lib/features/feed/presentation/widgets/feed_comments_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/config/constants.dart'; import '../../../../core/network/api_exception.dart'; +import '../../../../l10n/app_localizations.dart'; import '../../domain/feed_comment.dart'; import '../../domain/feed_interaction.dart'; import '../providers/feed_provider.dart'; @@ -99,7 +100,7 @@ class _FeedCommentsSheetState extends ConsumerState { if (!mounted) return; setState(() { _isLoading = false; - _error = e is ApiException ? e.userMessage : 'Não foi possível carregar comentários.'; + _error = e is ApiException ? e.userMessage : AppLocalizations.of(context)!.errorLoadComments; }); } } @@ -120,7 +121,7 @@ class _FeedCommentsSheetState extends ConsumerState { } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e is ApiException ? e.userMessage : 'Erro ao comentar.')), + SnackBar(content: Text(e is ApiException ? e.userMessage : AppLocalizations.of(context)!.errorComment)), ); } finally { if (mounted) setState(() => _isSubmitting = false); @@ -130,6 +131,7 @@ class _FeedCommentsSheetState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; return DraggableScrollableSheet( expand: false, initialChildSize: 0.75, @@ -161,7 +163,7 @@ class _FeedCommentsSheetState extends ConsumerState { : _comments.isEmpty ? Center( child: Text( - 'Nenhum comentário ainda.', + l10n.noCommentsYet, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -175,7 +177,7 @@ class _FeedCommentsSheetState extends ConsumerState { if (index >= _comments.length) { return TextButton( onPressed: () => _loadComments(reset: false), - child: const Text('Carregar mais'), + child: Text(l10n.loadMore), ); } final comment = _comments[index]; @@ -193,8 +195,8 @@ class _FeedCommentsSheetState extends ConsumerState { controller: _controller, minLines: 1, maxLines: 3, - decoration: const InputDecoration( - hintText: 'Escreva um comentário', + decoration: InputDecoration( + hintText: l10n.commentHint, border: OutlineInputBorder(), isDense: true, ), @@ -270,6 +272,7 @@ class _ErrorState extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Center( child: Padding( padding: const EdgeInsets.all(AppConstants.spacingLg), @@ -278,7 +281,7 @@ class _ErrorState extends StatelessWidget { children: [ Text(message, textAlign: TextAlign.center), const SizedBox(height: AppConstants.spacingMd), - FilledButton.tonal(onPressed: onRetry, child: const Text('Tentar novamente')), + FilledButton.tonal(onPressed: onRetry, child: Text(l10n.tryAgain)), ], ), ), diff --git a/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart b/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart index cb28e97e..ecfbc3fa 100644 --- a/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart +++ b/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart @@ -63,9 +63,9 @@ class _MarketplaceScreenState extends ConsumerState title: Text(l10n.marketplace), bottom: TabBar( controller: _tabController, - tabs: const [ - Tab(text: 'Buscar'), - Tab(text: 'Minha loja'), + tabs: [ + Tab(text: l10n.searchTab), + Tab(text: l10n.myStore), ], ), actions: [ @@ -74,12 +74,12 @@ class _MarketplaceScreenState extends ConsumerState onPressed: () async { try { await notifier.checkout(message: 'Checkout via app'); - if (mounted) showSuccessSnackBar(context, 'Pedido enviado.'); + if (mounted) showSuccessSnackBar(context, l10n.orderSent); } catch (e) { if (mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro no checkout.', + e is ApiException ? e.userMessage : l10n.errorCheckout, ); } } @@ -124,6 +124,7 @@ class _SearchTab extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Column( children: [ Padding( @@ -131,7 +132,7 @@ class _SearchTab extends StatelessWidget { child: TextField( controller: queryController, decoration: InputDecoration( - hintText: 'Buscar itens', + hintText: l10n.searchItemsHint, prefixIcon: const Icon(Icons.search), border: const OutlineInputBorder(), suffixIcon: IconButton( @@ -157,7 +158,7 @@ class _SearchTab extends StatelessWidget { child: Text( state.error is ApiException ? (state.error as ApiException).userMessage - : 'Erro ao buscar itens.', + : l10n.errorSearchItems, ), ); } @@ -173,18 +174,21 @@ class _SearchTab extends StatelessWidget { margin: const EdgeInsets.only(bottom: AppConstants.spacingSm), child: ListTile( title: Text(item.title), - subtitle: Text('${item.storeName} · ${item.currency} ${item.priceAmount.toStringAsFixed(2)}'), + subtitle: Text(l10n.storeAndPrice( + item.storeName, + l10n.priceLabel(item.currency, item.priceAmount.toStringAsFixed(2)), + )), trailing: IconButton( icon: const Icon(Icons.add_shopping_cart_outlined), onPressed: () async { try { await notifier.addToCart(item.id); - if (context.mounted) showSuccessSnackBar(context, 'Adicionado ao carrinho.'); + if (context.mounted) showSuccessSnackBar(context, l10n.addedToCart); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao adicionar.', + e is ApiException ? e.userMessage : l10n.errorAddToCart, ); } } @@ -256,23 +260,23 @@ class _MyStoreTabState extends State<_MyStoreTab> { ), const SizedBox(height: AppConstants.spacingMd), Text( - widget.state.myStore == null ? 'Criar minha loja' : 'Atualizar loja', + widget.state.myStore == null ? l10n.createMyStore : l10n.updateStore, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: AppConstants.spacingSm), TextField( controller: _nameController, - decoration: const InputDecoration( - labelText: 'Nome da loja', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: l10n.storeNameLabel, + border: const OutlineInputBorder(), ), ), const SizedBox(height: AppConstants.spacingSm), TextField( controller: _descriptionController, - decoration: const InputDecoration( - labelText: 'Descrição', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: l10n.descriptionLabel, + border: const OutlineInputBorder(), ), maxLines: 3, ), @@ -281,7 +285,7 @@ class _MyStoreTabState extends State<_MyStoreTab> { onPressed: () async { final name = _nameController.text.trim(); if (name.isEmpty) { - showErrorSnackBar(context, 'Informe o nome da loja.'); + showErrorSnackBar(context, l10n.informStoreName); return; } try { @@ -294,19 +298,19 @@ class _MyStoreTabState extends State<_MyStoreTab> { if (context.mounted) { showSuccessSnackBar( context, - widget.state.myStore == null ? 'Loja criada.' : 'Loja atualizada.', + widget.state.myStore == null ? l10n.storeCreated : l10n.storeUpdated, ); } } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao salvar loja.', + e is ApiException ? e.userMessage : l10n.errorSaveStore, ); } } }, - child: Text(widget.state.myStore == null ? 'Criar loja' : 'Salvar alterações'), + child: Text(widget.state.myStore == null ? l10n.createStore : l10n.saveChanges), ), ], ); diff --git a/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart b/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart index 8b62aafe..be6637fe 100644 --- a/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart +++ b/frontend/arah.app/lib/features/membership/presentation/screens/membership_screen.dart @@ -62,7 +62,7 @@ class MembershipScreen extends ConsumerWidget { if (state.error != null && state.membership == null) { final msg = state.error is ApiException ? (state.error as ApiException).userMessage - : 'Erro ao carregar membership.'; + : l10n.errorLoadMembership; return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ @@ -111,12 +111,12 @@ class MembershipScreen extends ConsumerWidget { onPressed: () async { try { await notifier.becomeResident(message: 'Solicitação via app'); - if (context.mounted) showSuccessSnackBar(context, 'Solicitação enviada.'); + if (context.mounted) showSuccessSnackBar(context, l10n.requestSent); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao solicitar residência.', + e is ApiException ? e.userMessage : l10n.errorRequestResidency, ); } } @@ -129,17 +129,17 @@ class MembershipScreen extends ConsumerWidget { onPressed: () async { final geo = ref.read(geoLocationStateProvider); if (geo == null) { - if (context.mounted) showErrorSnackBar(context, 'Ative a localização primeiro.'); + if (context.mounted) showErrorSnackBar(context, l10n.enableLocationFirst); return; } try { await notifier.verifyByGeo(geo.latitude, geo.longitude); - if (context.mounted) showSuccessSnackBar(context, 'Residência verificada por geo.'); + if (context.mounted) showSuccessSnackBar(context, l10n.residencyVerifiedByGeo); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro na verificação.', + e is ApiException ? e.userMessage : l10n.errorResidencyVerification, ); } } @@ -149,7 +149,7 @@ class MembershipScreen extends ConsumerWidget { ), ] else Text( - 'Você já é morador neste território.', + l10n.alreadyResident, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), diff --git a/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart b/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart index 534da9e2..1c478a50 100644 --- a/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart +++ b/frontend/arah.app/lib/features/moderation/presentation/screens/moderation_screen.dart @@ -62,10 +62,10 @@ class _ModerationScreenState extends ConsumerState with Single title: Text(l10n.moderation), bottom: TabBar( controller: _tabController, - tabs: const [ - Tab(text: 'Fila'), - Tab(text: 'Casos'), - Tab(text: 'Evidências'), + tabs: [ + Tab(text: l10n.moderationQueueTab), + Tab(text: l10n.moderationCasesTab), + Tab(text: l10n.moderationEvidencesTab), ], ), ), @@ -105,7 +105,7 @@ class _ModerationScreenState extends ConsumerState with Single child: Text( state.error is ApiException ? (state.error as ApiException).userMessage - : 'Sem permissão ou erro ao carregar.', + : l10n.noPermissionOrError, textAlign: TextAlign.center, ), ), @@ -129,7 +129,7 @@ class _ModerationScreenState extends ConsumerState with Single title: Text(item.type), subtitle: Text( '${item.status} · ${item.subjectType}' - '${item.evidenceId != null ? ' · evidência' : ''}' + '${item.evidenceId != null ? ' · ${l10n.moderationEvidenceSuffix}' : ''}' ' · ${dateFormat.format(item.createdAtUtc.toLocal())}', ), trailing: _buildActions(context, notifier, item), @@ -140,23 +140,24 @@ class _ModerationScreenState extends ConsumerState with Single } Widget? _buildActions(BuildContext context, ModerationNotifier notifier, WorkItem item) { + final l10n = AppLocalizations.of(context)!; if (!item.isPending) return null; if (item.isResidencyVerification && item.evidenceId != null) { return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( - tooltip: 'Baixar evidência', + tooltip: l10n.downloadEvidenceTooltip, icon: const Icon(Icons.download_outlined), onPressed: () => _downloadEvidence(context, notifier, item), ), IconButton( - tooltip: 'Aprovar', + tooltip: l10n.approveTooltip, icon: const Icon(Icons.check_circle_outline), onPressed: () => _decide(context, notifier, item, 'APPROVED'), ), IconButton( - tooltip: 'Rejeitar', + tooltip: l10n.rejectTooltip, icon: const Icon(Icons.cancel_outlined), onPressed: () => _decide(context, notifier, item, 'REJECTED'), ), @@ -168,12 +169,12 @@ class _ModerationScreenState extends ConsumerState with Single mainAxisSize: MainAxisSize.min, children: [ IconButton( - tooltip: 'Aprovar', + tooltip: l10n.approveTooltip, icon: const Icon(Icons.check_circle_outline), onPressed: () => _decide(context, notifier, item, 'APPROVED'), ), IconButton( - tooltip: 'Rejeitar', + tooltip: l10n.rejectTooltip, icon: const Icon(Icons.cancel_outlined), onPressed: () => _decide(context, notifier, item, 'REJECTED'), ), @@ -189,14 +190,15 @@ class _ModerationScreenState extends ConsumerState with Single WorkItem item, String outcome, ) async { + final l10n = AppLocalizations.of(context)!; try { await notifier.decideItem(item, outcome); - if (context.mounted) showSuccessSnackBar(context, 'Decisão registrada.'); + if (context.mounted) showSuccessSnackBar(context, l10n.decisionRegistered); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao decidir item.', + e is ApiException ? e.userMessage : l10n.errorDecideItem, ); } } @@ -207,14 +209,15 @@ class _ModerationScreenState extends ConsumerState with Single ModerationNotifier notifier, WorkItem item, ) async { + final l10n = AppLocalizations.of(context)!; try { final size = await notifier.downloadEvidence(item); - if (context.mounted) showSuccessSnackBar(context, 'Evidência baixada ($size bytes).'); + if (context.mounted) showSuccessSnackBar(context, l10n.evidenceDownloaded(size)); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao baixar evidência.', + e is ApiException ? e.userMessage : l10n.errorDownloadEvidence, ); } } diff --git a/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart b/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart index e62de351..bb591f92 100644 --- a/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart +++ b/frontend/arah.app/lib/features/subscriptions/presentation/screens/subscriptions_screen.dart @@ -47,7 +47,7 @@ class SubscriptionsScreen extends ConsumerWidget { child: Text( state.error is ApiException ? (state.error as ApiException).userMessage - : 'Erro ao carregar planos.', + : l10n.errorLoadPlans, textAlign: TextAlign.center, ), ), @@ -75,13 +75,13 @@ class SubscriptionsScreen extends ConsumerWidget { try { await notifier.cancelMySubscription(); if (context.mounted) { - showSuccessSnackBar(context, 'Assinatura cancelada.'); + showSuccessSnackBar(context, l10n.subscriptionCancelled); } } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao cancelar.', + e is ApiException ? e.userMessage : l10n.errorCancelSubscription, ); } } @@ -106,12 +106,12 @@ class SubscriptionsScreen extends ConsumerWidget { onPressed: () async { try { await notifier.subscribeToPlan(plan.id); - if (context.mounted) showSuccessSnackBar(context, 'Assinatura ativada.'); + if (context.mounted) showSuccessSnackBar(context, l10n.subscriptionActivated); } catch (e) { if (context.mounted) { showErrorSnackBar( context, - e is ApiException ? e.userMessage : 'Erro ao assinar.', + e is ApiException ? e.userMessage : l10n.errorSubscribe, ); } } diff --git a/frontend/arah.app/lib/l10n/app_en.arb b/frontend/arah.app/lib/l10n/app_en.arb index 7160b6b5..35d57f50 100644 --- a/frontend/arah.app/lib/l10n/app_en.arb +++ b/frontend/arah.app/lib/l10n/app_en.arb @@ -193,5 +193,104 @@ "kind": {"type": "String"}, "status": {"type": "String"} } - } + }, + "noCommentsYet": "No comments yet.", + "commentHint": "Write a comment", + "errorLoadComments": "Could not load comments.", + "errorComment": "Error posting comment.", + "groupCreated": "Group created.", + "errorCreateGroup": "Error creating group.", + "errorLoadConversations": "Error loading conversations.", + "noChannelsAvailable": "No channels available.", + "noGroupsYet": "No groups yet. Tap + to create one.", + "conversation": "Conversation", + "messageHint": "Message", + "errorSendMessage": "Error sending message.", + "searchTab": "Search", + "searchItemsHint": "Search items", + "orderSent": "Order sent.", + "errorCheckout": "Checkout error.", + "errorSearchItems": "Error searching items.", + "addedToCart": "Added to cart.", + "errorAddToCart": "Error adding to cart.", + "createMyStore": "Create my store", + "updateStore": "Update store", + "storeNameLabel": "Store name", + "descriptionLabel": "Description", + "informStoreName": "Enter the store name.", + "storeCreated": "Store created.", + "storeUpdated": "Store updated.", + "errorSaveStore": "Error saving store.", + "createStore": "Create store", + "saveChanges": "Save changes", + "requestSent": "Request sent.", + "errorSendRequest": "Error sending request.", + "errorLoadConnections": "Could not load connections.", + "noConnectionsYet": "No connections yet. Tap Add to find people.", + "errorLoadSuggestions": "Error loading suggestions.", + "errorSearch": "Search error.", + "searchMinCharsHint": "Type at least 2 characters or see suggestions above.", + "connectionRequestIncoming": "Incoming request", + "connectionRequestOutgoing": "Outgoing request", + "activeConnection": "Active connection", + "errorAccept": "Error accepting.", + "errorReject": "Error rejecting.", + "errorRemove": "Error removing.", + "assetCreated": "Asset created.", + "errorCreateAsset": "Error creating asset.", + "errorLoadAssets": "Error loading assets.", + "validationRegistered": "Validation recorded ({percent}% of community).", + "@validationRegistered": { + "placeholders": { + "percent": {"type": "String"} + } + }, + "assetArchived": "Asset archived.", + "assetApproved": "Asset approved.", + "assetRejected": "Asset rejected.", + "errorCompleteAction": "Could not complete the action.", + "assetValidationsMeta": "{count} validations ({percent}%)", + "@assetValidationsMeta": { + "placeholders": { + "count": {"type": "int"}, + "percent": {"type": "String"} + } + }, + "alertCreated": "Alert created.", + "errorCreateAlert": "Error creating alert.", + "errorLoadAlerts": "Could not load alerts.", + "alertsRequireResidency": "Territory alerts require residency or curator role.", + "filterAll": "All", + "postDefaultTitle": "Post", + "chooseTerritoryBeforePost": "Choose a territory before posting.", + "addImage": "Add image", + "changeImage": "Change image", + "moderationQueueTab": "Queue", + "moderationCasesTab": "Cases", + "moderationEvidencesTab": "Evidence", + "noPermissionOrError": "No permission or error loading.", + "downloadEvidenceTooltip": "Download evidence", + "approveTooltip": "Approve", + "rejectTooltip": "Reject", + "decisionRegistered": "Decision recorded.", + "errorDecideItem": "Error deciding item.", + "evidenceDownloaded": "Evidence downloaded ({size} bytes).", + "@evidenceDownloaded": { + "placeholders": { + "size": {"type": "int"} + } + }, + "errorDownloadEvidence": "Error downloading evidence.", + "errorLoadMembership": "Error loading membership.", + "enableLocationFirst": "Enable location first.", + "residencyVerifiedByGeo": "Residency verified by location.", + "errorResidencyVerification": "Verification error.", + "alreadyResident": "You are already a resident in this territory.", + "errorLoadPlans": "Error loading plans.", + "subscriptionCancelled": "Subscription cancelled.", + "errorCancelSubscription": "Error cancelling.", + "subscriptionActivated": "Subscription activated.", + "errorSubscribe": "Error subscribing.", + "errorRequestResidency": "Error requesting residency.", + "moderationEvidenceSuffix": "evidence" } diff --git a/frontend/arah.app/lib/l10n/app_localizations.dart b/frontend/arah.app/lib/l10n/app_localizations.dart index aa861285..6846a334 100644 --- a/frontend/arah.app/lib/l10n/app_localizations.dart +++ b/frontend/arah.app/lib/l10n/app_localizations.dart @@ -1057,6 +1057,504 @@ abstract class AppLocalizations { /// In pt, this message translates to: /// **'{kind} · {status}'** String conversationMeta(String kind, String status); + + /// No description provided for @noCommentsYet. + /// + /// In pt, this message translates to: + /// **'Nenhum comentário ainda.'** + String get noCommentsYet; + + /// No description provided for @commentHint. + /// + /// In pt, this message translates to: + /// **'Escreva um comentário'** + String get commentHint; + + /// No description provided for @errorLoadComments. + /// + /// In pt, this message translates to: + /// **'Não foi possível carregar comentários.'** + String get errorLoadComments; + + /// No description provided for @errorComment. + /// + /// In pt, this message translates to: + /// **'Erro ao comentar.'** + String get errorComment; + + /// No description provided for @groupCreated. + /// + /// In pt, this message translates to: + /// **'Grupo criado.'** + String get groupCreated; + + /// No description provided for @errorCreateGroup. + /// + /// In pt, this message translates to: + /// **'Erro ao criar grupo.'** + String get errorCreateGroup; + + /// No description provided for @errorLoadConversations. + /// + /// In pt, this message translates to: + /// **'Erro ao carregar conversas.'** + String get errorLoadConversations; + + /// No description provided for @noChannelsAvailable. + /// + /// In pt, this message translates to: + /// **'Nenhum canal disponível.'** + String get noChannelsAvailable; + + /// No description provided for @noGroupsYet. + /// + /// In pt, this message translates to: + /// **'Nenhum grupo ainda. Toque + para criar.'** + String get noGroupsYet; + + /// No description provided for @conversation. + /// + /// In pt, this message translates to: + /// **'Conversa'** + String get conversation; + + /// No description provided for @messageHint. + /// + /// In pt, this message translates to: + /// **'Mensagem'** + String get messageHint; + + /// No description provided for @errorSendMessage. + /// + /// In pt, this message translates to: + /// **'Erro ao enviar mensagem.'** + String get errorSendMessage; + + /// No description provided for @searchTab. + /// + /// In pt, this message translates to: + /// **'Buscar'** + String get searchTab; + + /// No description provided for @searchItemsHint. + /// + /// In pt, this message translates to: + /// **'Buscar itens'** + String get searchItemsHint; + + /// No description provided for @orderSent. + /// + /// In pt, this message translates to: + /// **'Pedido enviado.'** + String get orderSent; + + /// No description provided for @errorCheckout. + /// + /// In pt, this message translates to: + /// **'Erro no checkout.'** + String get errorCheckout; + + /// No description provided for @errorSearchItems. + /// + /// In pt, this message translates to: + /// **'Erro ao buscar itens.'** + String get errorSearchItems; + + /// No description provided for @addedToCart. + /// + /// In pt, this message translates to: + /// **'Adicionado ao carrinho.'** + String get addedToCart; + + /// No description provided for @errorAddToCart. + /// + /// In pt, this message translates to: + /// **'Erro ao adicionar.'** + String get errorAddToCart; + + /// No description provided for @createMyStore. + /// + /// In pt, this message translates to: + /// **'Criar minha loja'** + String get createMyStore; + + /// No description provided for @updateStore. + /// + /// In pt, this message translates to: + /// **'Atualizar loja'** + String get updateStore; + + /// No description provided for @storeNameLabel. + /// + /// In pt, this message translates to: + /// **'Nome da loja'** + String get storeNameLabel; + + /// No description provided for @descriptionLabel. + /// + /// In pt, this message translates to: + /// **'Descrição'** + String get descriptionLabel; + + /// No description provided for @informStoreName. + /// + /// In pt, this message translates to: + /// **'Informe o nome da loja.'** + String get informStoreName; + + /// No description provided for @storeCreated. + /// + /// In pt, this message translates to: + /// **'Loja criada.'** + String get storeCreated; + + /// No description provided for @storeUpdated. + /// + /// In pt, this message translates to: + /// **'Loja atualizada.'** + String get storeUpdated; + + /// No description provided for @errorSaveStore. + /// + /// In pt, this message translates to: + /// **'Erro ao salvar loja.'** + String get errorSaveStore; + + /// No description provided for @createStore. + /// + /// In pt, this message translates to: + /// **'Criar loja'** + String get createStore; + + /// No description provided for @saveChanges. + /// + /// In pt, this message translates to: + /// **'Salvar alterações'** + String get saveChanges; + + /// No description provided for @requestSent. + /// + /// In pt, this message translates to: + /// **'Solicitação enviada.'** + String get requestSent; + + /// No description provided for @errorSendRequest. + /// + /// In pt, this message translates to: + /// **'Erro ao enviar solicitação.'** + String get errorSendRequest; + + /// No description provided for @errorLoadConnections. + /// + /// In pt, this message translates to: + /// **'Não foi possível carregar conexões.'** + String get errorLoadConnections; + + /// No description provided for @noConnectionsYet. + /// + /// In pt, this message translates to: + /// **'Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.'** + String get noConnectionsYet; + + /// No description provided for @errorLoadSuggestions. + /// + /// In pt, this message translates to: + /// **'Erro ao carregar sugestões.'** + String get errorLoadSuggestions; + + /// No description provided for @errorSearch. + /// + /// In pt, this message translates to: + /// **'Erro na busca.'** + String get errorSearch; + + /// No description provided for @searchMinCharsHint. + /// + /// In pt, this message translates to: + /// **'Digite ao menos 2 caracteres ou veja sugestões acima.'** + String get searchMinCharsHint; + + /// No description provided for @connectionRequestIncoming. + /// + /// In pt, this message translates to: + /// **'Solicitação recebida'** + String get connectionRequestIncoming; + + /// No description provided for @connectionRequestOutgoing. + /// + /// In pt, this message translates to: + /// **'Solicitação enviada'** + String get connectionRequestOutgoing; + + /// No description provided for @activeConnection. + /// + /// In pt, this message translates to: + /// **'Conexão ativa'** + String get activeConnection; + + /// No description provided for @errorAccept. + /// + /// In pt, this message translates to: + /// **'Erro ao aceitar.'** + String get errorAccept; + + /// No description provided for @errorReject. + /// + /// In pt, this message translates to: + /// **'Erro ao rejeitar.'** + String get errorReject; + + /// No description provided for @errorRemove. + /// + /// In pt, this message translates to: + /// **'Erro ao remover.'** + String get errorRemove; + + /// No description provided for @assetCreated. + /// + /// In pt, this message translates to: + /// **'Asset criado.'** + String get assetCreated; + + /// No description provided for @errorCreateAsset. + /// + /// In pt, this message translates to: + /// **'Erro ao criar asset.'** + String get errorCreateAsset; + + /// No description provided for @errorLoadAssets. + /// + /// In pt, this message translates to: + /// **'Erro ao carregar assets.'** + String get errorLoadAssets; + + /// No description provided for @validationRegistered. + /// + /// In pt, this message translates to: + /// **'Validação registrada ({percent}% da comunidade).'** + String validationRegistered(String percent); + + /// No description provided for @assetArchived. + /// + /// In pt, this message translates to: + /// **'Asset arquivado.'** + String get assetArchived; + + /// No description provided for @assetApproved. + /// + /// In pt, this message translates to: + /// **'Asset aprovado.'** + String get assetApproved; + + /// No description provided for @assetRejected. + /// + /// In pt, this message translates to: + /// **'Asset rejeitado.'** + String get assetRejected; + + /// No description provided for @errorCompleteAction. + /// + /// In pt, this message translates to: + /// **'Não foi possível concluir a ação.'** + String get errorCompleteAction; + + /// No description provided for @assetValidationsMeta. + /// + /// In pt, this message translates to: + /// **'{count} validações ({percent}%)'** + String assetValidationsMeta(int count, String percent); + + /// No description provided for @alertCreated. + /// + /// In pt, this message translates to: + /// **'Alerta criado.'** + String get alertCreated; + + /// No description provided for @errorCreateAlert. + /// + /// In pt, this message translates to: + /// **'Erro ao criar alerta.'** + String get errorCreateAlert; + + /// No description provided for @errorLoadAlerts. + /// + /// In pt, this message translates to: + /// **'Não foi possível carregar alertas.'** + String get errorLoadAlerts; + + /// No description provided for @alertsRequireResidency. + /// + /// In pt, this message translates to: + /// **'Alertas do território exigem residência ou curadoria.'** + String get alertsRequireResidency; + + /// No description provided for @filterAll. + /// + /// In pt, this message translates to: + /// **'Todos'** + String get filterAll; + + /// No description provided for @postDefaultTitle. + /// + /// In pt, this message translates to: + /// **'Post'** + String get postDefaultTitle; + + /// No description provided for @chooseTerritoryBeforePost. + /// + /// In pt, this message translates to: + /// **'Escolha um território antes de publicar.'** + String get chooseTerritoryBeforePost; + + /// No description provided for @addImage. + /// + /// In pt, this message translates to: + /// **'Adicionar imagem'** + String get addImage; + + /// No description provided for @changeImage. + /// + /// In pt, this message translates to: + /// **'Trocar imagem'** + String get changeImage; + + /// No description provided for @moderationQueueTab. + /// + /// In pt, this message translates to: + /// **'Fila'** + String get moderationQueueTab; + + /// No description provided for @moderationCasesTab. + /// + /// In pt, this message translates to: + /// **'Casos'** + String get moderationCasesTab; + + /// No description provided for @moderationEvidencesTab. + /// + /// In pt, this message translates to: + /// **'Evidências'** + String get moderationEvidencesTab; + + /// No description provided for @noPermissionOrError. + /// + /// In pt, this message translates to: + /// **'Sem permissão ou erro ao carregar.'** + String get noPermissionOrError; + + /// No description provided for @downloadEvidenceTooltip. + /// + /// In pt, this message translates to: + /// **'Baixar evidência'** + String get downloadEvidenceTooltip; + + /// No description provided for @approveTooltip. + /// + /// In pt, this message translates to: + /// **'Aprovar'** + String get approveTooltip; + + /// No description provided for @rejectTooltip. + /// + /// In pt, this message translates to: + /// **'Rejeitar'** + String get rejectTooltip; + + /// No description provided for @decisionRegistered. + /// + /// In pt, this message translates to: + /// **'Decisão registrada.'** + String get decisionRegistered; + + /// No description provided for @errorDecideItem. + /// + /// In pt, this message translates to: + /// **'Erro ao decidir item.'** + String get errorDecideItem; + + /// No description provided for @evidenceDownloaded. + /// + /// In pt, this message translates to: + /// **'Evidência baixada ({size} bytes).'** + String evidenceDownloaded(int size); + + /// No description provided for @errorDownloadEvidence. + /// + /// In pt, this message translates to: + /// **'Erro ao baixar evidência.'** + String get errorDownloadEvidence; + + /// No description provided for @errorLoadMembership. + /// + /// In pt, this message translates to: + /// **'Erro ao carregar membership.'** + String get errorLoadMembership; + + /// No description provided for @enableLocationFirst. + /// + /// In pt, this message translates to: + /// **'Ative a localização primeiro.'** + String get enableLocationFirst; + + /// No description provided for @residencyVerifiedByGeo. + /// + /// In pt, this message translates to: + /// **'Residência verificada por geo.'** + String get residencyVerifiedByGeo; + + /// No description provided for @errorResidencyVerification. + /// + /// In pt, this message translates to: + /// **'Erro na verificação.'** + String get errorResidencyVerification; + + /// No description provided for @alreadyResident. + /// + /// In pt, this message translates to: + /// **'Você já é morador neste território.'** + String get alreadyResident; + + /// No description provided for @errorLoadPlans. + /// + /// In pt, this message translates to: + /// **'Erro ao carregar planos.'** + String get errorLoadPlans; + + /// No description provided for @subscriptionCancelled. + /// + /// In pt, this message translates to: + /// **'Assinatura cancelada.'** + String get subscriptionCancelled; + + /// No description provided for @errorCancelSubscription. + /// + /// In pt, this message translates to: + /// **'Erro ao cancelar.'** + String get errorCancelSubscription; + + /// No description provided for @subscriptionActivated. + /// + /// In pt, this message translates to: + /// **'Assinatura ativada.'** + String get subscriptionActivated; + + /// No description provided for @errorSubscribe. + /// + /// In pt, this message translates to: + /// **'Erro ao assinar.'** + String get errorSubscribe; + + /// No description provided for @errorRequestResidency. + /// + /// In pt, this message translates to: + /// **'Erro ao solicitar residência.'** + String get errorRequestResidency; + + /// No description provided for @moderationEvidenceSuffix. + /// + /// In pt, this message translates to: + /// **'evidência'** + String get moderationEvidenceSuffix; } class _AppLocalizationsDelegate diff --git a/frontend/arah.app/lib/l10n/app_localizations_en.dart b/frontend/arah.app/lib/l10n/app_localizations_en.dart index 37bfba66..7cf2ac7e 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_en.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_en.dart @@ -510,4 +510,261 @@ class AppLocalizationsEn extends AppLocalizations { String conversationMeta(String kind, String status) { return '$kind · $status'; } + + @override + String get noCommentsYet => 'No comments yet.'; + + @override + String get commentHint => 'Write a comment'; + + @override + String get errorLoadComments => 'Could not load comments.'; + + @override + String get errorComment => 'Error posting comment.'; + + @override + String get groupCreated => 'Group created.'; + + @override + String get errorCreateGroup => 'Error creating group.'; + + @override + String get errorLoadConversations => 'Error loading conversations.'; + + @override + String get noChannelsAvailable => 'No channels available.'; + + @override + String get noGroupsYet => 'No groups yet. Tap + to create one.'; + + @override + String get conversation => 'Conversation'; + + @override + String get messageHint => 'Message'; + + @override + String get errorSendMessage => 'Error sending message.'; + + @override + String get searchTab => 'Search'; + + @override + String get searchItemsHint => 'Search items'; + + @override + String get orderSent => 'Order sent.'; + + @override + String get errorCheckout => 'Checkout error.'; + + @override + String get errorSearchItems => 'Error searching items.'; + + @override + String get addedToCart => 'Added to cart.'; + + @override + String get errorAddToCart => 'Error adding to cart.'; + + @override + String get createMyStore => 'Create my store'; + + @override + String get updateStore => 'Update store'; + + @override + String get storeNameLabel => 'Store name'; + + @override + String get descriptionLabel => 'Description'; + + @override + String get informStoreName => 'Enter the store name.'; + + @override + String get storeCreated => 'Store created.'; + + @override + String get storeUpdated => 'Store updated.'; + + @override + String get errorSaveStore => 'Error saving store.'; + + @override + String get createStore => 'Create store'; + + @override + String get saveChanges => 'Save changes'; + + @override + String get requestSent => 'Request sent.'; + + @override + String get errorSendRequest => 'Error sending request.'; + + @override + String get errorLoadConnections => 'Could not load connections.'; + + @override + String get noConnectionsYet => 'No connections yet. Tap Add to find people.'; + + @override + String get errorLoadSuggestions => 'Error loading suggestions.'; + + @override + String get errorSearch => 'Search error.'; + + @override + String get searchMinCharsHint => + 'Type at least 2 characters or see suggestions above.'; + + @override + String get connectionRequestIncoming => 'Incoming request'; + + @override + String get connectionRequestOutgoing => 'Outgoing request'; + + @override + String get activeConnection => 'Active connection'; + + @override + String get errorAccept => 'Error accepting.'; + + @override + String get errorReject => 'Error rejecting.'; + + @override + String get errorRemove => 'Error removing.'; + + @override + String get assetCreated => 'Asset created.'; + + @override + String get errorCreateAsset => 'Error creating asset.'; + + @override + String get errorLoadAssets => 'Error loading assets.'; + + @override + String validationRegistered(String percent) { + return 'Validation recorded ($percent% of community).'; + } + + @override + String get assetArchived => 'Asset archived.'; + + @override + String get assetApproved => 'Asset approved.'; + + @override + String get assetRejected => 'Asset rejected.'; + + @override + String get errorCompleteAction => 'Could not complete the action.'; + + @override + String assetValidationsMeta(int count, String percent) { + return '$count validations ($percent%)'; + } + + @override + String get alertCreated => 'Alert created.'; + + @override + String get errorCreateAlert => 'Error creating alert.'; + + @override + String get errorLoadAlerts => 'Could not load alerts.'; + + @override + String get alertsRequireResidency => + 'Territory alerts require residency or curator role.'; + + @override + String get filterAll => 'All'; + + @override + String get postDefaultTitle => 'Post'; + + @override + String get chooseTerritoryBeforePost => 'Choose a territory before posting.'; + + @override + String get addImage => 'Add image'; + + @override + String get changeImage => 'Change image'; + + @override + String get moderationQueueTab => 'Queue'; + + @override + String get moderationCasesTab => 'Cases'; + + @override + String get moderationEvidencesTab => 'Evidence'; + + @override + String get noPermissionOrError => 'No permission or error loading.'; + + @override + String get downloadEvidenceTooltip => 'Download evidence'; + + @override + String get approveTooltip => 'Approve'; + + @override + String get rejectTooltip => 'Reject'; + + @override + String get decisionRegistered => 'Decision recorded.'; + + @override + String get errorDecideItem => 'Error deciding item.'; + + @override + String evidenceDownloaded(int size) { + return 'Evidence downloaded ($size bytes).'; + } + + @override + String get errorDownloadEvidence => 'Error downloading evidence.'; + + @override + String get errorLoadMembership => 'Error loading membership.'; + + @override + String get enableLocationFirst => 'Enable location first.'; + + @override + String get residencyVerifiedByGeo => 'Residency verified by location.'; + + @override + String get errorResidencyVerification => 'Verification error.'; + + @override + String get alreadyResident => 'You are already a resident in this territory.'; + + @override + String get errorLoadPlans => 'Error loading plans.'; + + @override + String get subscriptionCancelled => 'Subscription cancelled.'; + + @override + String get errorCancelSubscription => 'Error cancelling.'; + + @override + String get subscriptionActivated => 'Subscription activated.'; + + @override + String get errorSubscribe => 'Error subscribing.'; + + @override + String get errorRequestResidency => 'Error requesting residency.'; + + @override + String get moderationEvidenceSuffix => 'evidence'; } diff --git a/frontend/arah.app/lib/l10n/app_localizations_pt.dart b/frontend/arah.app/lib/l10n/app_localizations_pt.dart index baf49263..0110935f 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_pt.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_pt.dart @@ -513,4 +513,263 @@ class AppLocalizationsPt extends AppLocalizations { String conversationMeta(String kind, String status) { return '$kind · $status'; } + + @override + String get noCommentsYet => 'Nenhum comentário ainda.'; + + @override + String get commentHint => 'Escreva um comentário'; + + @override + String get errorLoadComments => 'Não foi possível carregar comentários.'; + + @override + String get errorComment => 'Erro ao comentar.'; + + @override + String get groupCreated => 'Grupo criado.'; + + @override + String get errorCreateGroup => 'Erro ao criar grupo.'; + + @override + String get errorLoadConversations => 'Erro ao carregar conversas.'; + + @override + String get noChannelsAvailable => 'Nenhum canal disponível.'; + + @override + String get noGroupsYet => 'Nenhum grupo ainda. Toque + para criar.'; + + @override + String get conversation => 'Conversa'; + + @override + String get messageHint => 'Mensagem'; + + @override + String get errorSendMessage => 'Erro ao enviar mensagem.'; + + @override + String get searchTab => 'Buscar'; + + @override + String get searchItemsHint => 'Buscar itens'; + + @override + String get orderSent => 'Pedido enviado.'; + + @override + String get errorCheckout => 'Erro no checkout.'; + + @override + String get errorSearchItems => 'Erro ao buscar itens.'; + + @override + String get addedToCart => 'Adicionado ao carrinho.'; + + @override + String get errorAddToCart => 'Erro ao adicionar.'; + + @override + String get createMyStore => 'Criar minha loja'; + + @override + String get updateStore => 'Atualizar loja'; + + @override + String get storeNameLabel => 'Nome da loja'; + + @override + String get descriptionLabel => 'Descrição'; + + @override + String get informStoreName => 'Informe o nome da loja.'; + + @override + String get storeCreated => 'Loja criada.'; + + @override + String get storeUpdated => 'Loja atualizada.'; + + @override + String get errorSaveStore => 'Erro ao salvar loja.'; + + @override + String get createStore => 'Criar loja'; + + @override + String get saveChanges => 'Salvar alterações'; + + @override + String get requestSent => 'Solicitação enviada.'; + + @override + String get errorSendRequest => 'Erro ao enviar solicitação.'; + + @override + String get errorLoadConnections => 'Não foi possível carregar conexões.'; + + @override + String get noConnectionsYet => + 'Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.'; + + @override + String get errorLoadSuggestions => 'Erro ao carregar sugestões.'; + + @override + String get errorSearch => 'Erro na busca.'; + + @override + String get searchMinCharsHint => + 'Digite ao menos 2 caracteres ou veja sugestões acima.'; + + @override + String get connectionRequestIncoming => 'Solicitação recebida'; + + @override + String get connectionRequestOutgoing => 'Solicitação enviada'; + + @override + String get activeConnection => 'Conexão ativa'; + + @override + String get errorAccept => 'Erro ao aceitar.'; + + @override + String get errorReject => 'Erro ao rejeitar.'; + + @override + String get errorRemove => 'Erro ao remover.'; + + @override + String get assetCreated => 'Asset criado.'; + + @override + String get errorCreateAsset => 'Erro ao criar asset.'; + + @override + String get errorLoadAssets => 'Erro ao carregar assets.'; + + @override + String validationRegistered(String percent) { + return 'Validação registrada ($percent% da comunidade).'; + } + + @override + String get assetArchived => 'Asset arquivado.'; + + @override + String get assetApproved => 'Asset aprovado.'; + + @override + String get assetRejected => 'Asset rejeitado.'; + + @override + String get errorCompleteAction => 'Não foi possível concluir a ação.'; + + @override + String assetValidationsMeta(int count, String percent) { + return '$count validações ($percent%)'; + } + + @override + String get alertCreated => 'Alerta criado.'; + + @override + String get errorCreateAlert => 'Erro ao criar alerta.'; + + @override + String get errorLoadAlerts => 'Não foi possível carregar alertas.'; + + @override + String get alertsRequireResidency => + 'Alertas do território exigem residência ou curadoria.'; + + @override + String get filterAll => 'Todos'; + + @override + String get postDefaultTitle => 'Post'; + + @override + String get chooseTerritoryBeforePost => + 'Escolha um território antes de publicar.'; + + @override + String get addImage => 'Adicionar imagem'; + + @override + String get changeImage => 'Trocar imagem'; + + @override + String get moderationQueueTab => 'Fila'; + + @override + String get moderationCasesTab => 'Casos'; + + @override + String get moderationEvidencesTab => 'Evidências'; + + @override + String get noPermissionOrError => 'Sem permissão ou erro ao carregar.'; + + @override + String get downloadEvidenceTooltip => 'Baixar evidência'; + + @override + String get approveTooltip => 'Aprovar'; + + @override + String get rejectTooltip => 'Rejeitar'; + + @override + String get decisionRegistered => 'Decisão registrada.'; + + @override + String get errorDecideItem => 'Erro ao decidir item.'; + + @override + String evidenceDownloaded(int size) { + return 'Evidência baixada ($size bytes).'; + } + + @override + String get errorDownloadEvidence => 'Erro ao baixar evidência.'; + + @override + String get errorLoadMembership => 'Erro ao carregar membership.'; + + @override + String get enableLocationFirst => 'Ative a localização primeiro.'; + + @override + String get residencyVerifiedByGeo => 'Residência verificada por geo.'; + + @override + String get errorResidencyVerification => 'Erro na verificação.'; + + @override + String get alreadyResident => 'Você já é morador neste território.'; + + @override + String get errorLoadPlans => 'Erro ao carregar planos.'; + + @override + String get subscriptionCancelled => 'Assinatura cancelada.'; + + @override + String get errorCancelSubscription => 'Erro ao cancelar.'; + + @override + String get subscriptionActivated => 'Assinatura ativada.'; + + @override + String get errorSubscribe => 'Erro ao assinar.'; + + @override + String get errorRequestResidency => 'Erro ao solicitar residência.'; + + @override + String get moderationEvidenceSuffix => 'evidência'; } diff --git a/frontend/arah.app/lib/l10n/app_pt.arb b/frontend/arah.app/lib/l10n/app_pt.arb index 7a7b0a7d..d2cf4d53 100644 --- a/frontend/arah.app/lib/l10n/app_pt.arb +++ b/frontend/arah.app/lib/l10n/app_pt.arb @@ -193,5 +193,104 @@ "kind": {"type": "String"}, "status": {"type": "String"} } - } + }, + "noCommentsYet": "Nenhum comentário ainda.", + "commentHint": "Escreva um comentário", + "errorLoadComments": "Não foi possível carregar comentários.", + "errorComment": "Erro ao comentar.", + "groupCreated": "Grupo criado.", + "errorCreateGroup": "Erro ao criar grupo.", + "errorLoadConversations": "Erro ao carregar conversas.", + "noChannelsAvailable": "Nenhum canal disponível.", + "noGroupsYet": "Nenhum grupo ainda. Toque + para criar.", + "conversation": "Conversa", + "messageHint": "Mensagem", + "errorSendMessage": "Erro ao enviar mensagem.", + "searchTab": "Buscar", + "searchItemsHint": "Buscar itens", + "orderSent": "Pedido enviado.", + "errorCheckout": "Erro no checkout.", + "errorSearchItems": "Erro ao buscar itens.", + "addedToCart": "Adicionado ao carrinho.", + "errorAddToCart": "Erro ao adicionar.", + "createMyStore": "Criar minha loja", + "updateStore": "Atualizar loja", + "storeNameLabel": "Nome da loja", + "descriptionLabel": "Descrição", + "informStoreName": "Informe o nome da loja.", + "storeCreated": "Loja criada.", + "storeUpdated": "Loja atualizada.", + "errorSaveStore": "Erro ao salvar loja.", + "createStore": "Criar loja", + "saveChanges": "Salvar alterações", + "requestSent": "Solicitação enviada.", + "errorSendRequest": "Erro ao enviar solicitação.", + "errorLoadConnections": "Não foi possível carregar conexões.", + "noConnectionsYet": "Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.", + "errorLoadSuggestions": "Erro ao carregar sugestões.", + "errorSearch": "Erro na busca.", + "searchMinCharsHint": "Digite ao menos 2 caracteres ou veja sugestões acima.", + "connectionRequestIncoming": "Solicitação recebida", + "connectionRequestOutgoing": "Solicitação enviada", + "activeConnection": "Conexão ativa", + "errorAccept": "Erro ao aceitar.", + "errorReject": "Erro ao rejeitar.", + "errorRemove": "Erro ao remover.", + "assetCreated": "Asset criado.", + "errorCreateAsset": "Erro ao criar asset.", + "errorLoadAssets": "Erro ao carregar assets.", + "validationRegistered": "Validação registrada ({percent}% da comunidade).", + "@validationRegistered": { + "placeholders": { + "percent": {"type": "String"} + } + }, + "assetArchived": "Asset arquivado.", + "assetApproved": "Asset aprovado.", + "assetRejected": "Asset rejeitado.", + "errorCompleteAction": "Não foi possível concluir a ação.", + "assetValidationsMeta": "{count} validações ({percent}%)", + "@assetValidationsMeta": { + "placeholders": { + "count": {"type": "int"}, + "percent": {"type": "String"} + } + }, + "alertCreated": "Alerta criado.", + "errorCreateAlert": "Erro ao criar alerta.", + "errorLoadAlerts": "Não foi possível carregar alertas.", + "alertsRequireResidency": "Alertas do território exigem residência ou curadoria.", + "filterAll": "Todos", + "postDefaultTitle": "Post", + "chooseTerritoryBeforePost": "Escolha um território antes de publicar.", + "addImage": "Adicionar imagem", + "changeImage": "Trocar imagem", + "moderationQueueTab": "Fila", + "moderationCasesTab": "Casos", + "moderationEvidencesTab": "Evidências", + "noPermissionOrError": "Sem permissão ou erro ao carregar.", + "downloadEvidenceTooltip": "Baixar evidência", + "approveTooltip": "Aprovar", + "rejectTooltip": "Rejeitar", + "decisionRegistered": "Decisão registrada.", + "errorDecideItem": "Erro ao decidir item.", + "evidenceDownloaded": "Evidência baixada ({size} bytes).", + "@evidenceDownloaded": { + "placeholders": { + "size": {"type": "int"} + } + }, + "errorDownloadEvidence": "Erro ao baixar evidência.", + "errorLoadMembership": "Erro ao carregar membership.", + "enableLocationFirst": "Ative a localização primeiro.", + "residencyVerifiedByGeo": "Residência verificada por geo.", + "errorResidencyVerification": "Erro na verificação.", + "alreadyResident": "Você já é morador neste território.", + "errorLoadPlans": "Erro ao carregar planos.", + "subscriptionCancelled": "Assinatura cancelada.", + "errorCancelSubscription": "Erro ao cancelar.", + "subscriptionActivated": "Assinatura ativada.", + "errorSubscribe": "Erro ao assinar.", + "errorRequestResidency": "Erro ao solicitar residência.", + "moderationEvidenceSuffix": "evidência" } diff --git a/frontend/arah.app/scripts/run-app-local.ps1 b/frontend/arah.app/scripts/run-app-local.ps1 index 4b1793fe..35f1aeb6 100644 --- a/frontend/arah.app/scripts/run-app-local.ps1 +++ b/frontend/arah.app/scripts/run-app-local.ps1 @@ -37,7 +37,7 @@ function Get-FlutterPath { function Show-Help { Write-Host "" - Write-Info "=== Ará - Rodar app contra BFF local ===" + Write-Info "=== Arah - Rodar app contra BFF local ===" Write-Host "" Write-Host " Por padrão roda no Chrome (web). Use -Device para outro alvo." Write-Host ""