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/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/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.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/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/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..89fc5777 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;
@@ -29,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/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..5d130bb3
--- /dev/null
+++ b/frontend/arah.app/lib/core/widgets/arah_brand_header.dart
@@ -0,0 +1,83 @@
+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 logo e wordmark alinhados 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;
+
+ double get _logoSize => switch (size) {
+ ArahBrandHeaderSize.large => 48,
+ ArahBrandHeaderSize.medium => 40,
+ _ => 32,
+ };
+
+ @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: [
+ 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),
+ 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/alerts/presentation/screens/alerts_screen.dart b/frontend/arah.app/lib/features/alerts/presentation/screens/alerts_screen.dart
index f7e38c91..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
@@ -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,28 +56,29 @@ 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: [
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,
),
],
),
actions: [
- TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
+ TextButton(onPressed: () => Navigator.pop(ctx), child: Text(l10n.cancel)),
FilledButton(
onPressed: () async {
try {
@@ -84,18 +88,18 @@ 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,
);
}
}
},
- 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(),
@@ -113,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: [
@@ -126,14 +131,14 @@ 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,
),
),
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..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
@@ -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 {
@@ -70,18 +74,18 @@ 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,
);
}
}
},
- 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(),
@@ -104,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,
),
),
@@ -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(
@@ -128,18 +133,18 @@ 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),
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)),
],
],
),
@@ -155,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') {
@@ -162,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/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/chat/presentation/screens/chat_conversation_screen.dart b/frontend/arah.app/lib/features/chat/presentation/screens/chat_conversation_screen.dart
index 19994f77..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
@@ -5,6 +5,8 @@ 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 '../providers/chat_provider.dart';
class ChatConversationScreen extends ConsumerStatefulWidget {
@@ -35,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 {
@@ -47,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 Scaffold(
- appBar: AppBar(title: Text(widget.title ?? 'Conversa')),
+ return ArahScaffold(
+ appBar: AppBar(title: Text(widget.title ?? l10n.conversation)),
body: Column(
children: [
Expanded(
@@ -94,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 e080ea9d..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
@@ -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,20 +43,21 @@ 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(
- labelText: 'Nome do grupo',
+ decoration: InputDecoration(
+ labelText: l10n.groupName,
border: OutlineInputBorder(),
),
),
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();
@@ -63,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)}');
}
@@ -72,12 +75,12 @@ 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,
);
}
}
},
- child: const Text('Criar'),
+ child: Text(l10n.create),
),
],
),
@@ -86,25 +89,26 @@ 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 [
- Tab(text: 'Canais'),
- Tab(text: 'Grupos'),
+ tabs: [
+ Tab(text: l10n.channelsTab),
+ Tab(text: l10n.groupsTab),
],
),
),
@@ -129,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(),
@@ -144,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,
),
),
@@ -160,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,
),
),
),
@@ -178,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 4c5c73d6..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
@@ -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(),
@@ -32,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,
@@ -41,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,
);
}
}
@@ -58,6 +62,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(),
@@ -68,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: [
@@ -78,7 +83,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,18 +101,18 @@ 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(
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,
),
@@ -148,6 +153,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet>
}
Future _loadSuggestions() async {
+ final l10n = AppLocalizations.of(context)!;
setState(() {
_loading = true;
_error = null;
@@ -158,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 {
@@ -167,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;
@@ -181,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 [];
});
}
@@ -192,6 +199,7 @@ class _ConnectionSearchSheetState extends ConsumerState<_ConnectionSearchSheet>
@override
Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.85,
@@ -202,12 +210,12 @@ 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,
decoration: InputDecoration(
- hintText: 'Nome de exibição',
+ hintText: l10n.displayNameHint,
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
suffixIcon: IconButton(
@@ -234,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,
@@ -255,7 +263,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 +289,17 @@ 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}'),
+ title: Text(
+ isPending
+ ? (item.isIncoming ? l10n.connectionRequestIncoming : l10n.connectionRequestOutgoing)
+ : l10n.activeConnection,
+ ),
+ subtitle: Text(l10n.statusLabel(item.status)),
trailing: isPending && item.isIncoming
? Row(
mainAxisSize: MainAxisSize.min,
@@ -300,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,
);
}
}
@@ -315,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,
);
}
}
@@ -333,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/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..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
@@ -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';
@@ -44,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);
@@ -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: [
@@ -206,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 e3659140..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
@@ -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,15 +42,15 @@ 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: [
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,
),
@@ -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);
@@ -297,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: [
@@ -313,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,
),
@@ -355,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?);
@@ -412,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(
@@ -427,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/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..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
@@ -5,6 +5,8 @@ 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';
@@ -16,8 +18,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.
@@ -36,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;
@@ -48,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(
@@ -78,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,
),
@@ -98,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(
@@ -107,9 +108,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,
),
),
@@ -132,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),
),
],
),
@@ -152,7 +153,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/marketplace/presentation/screens/marketplace_screen.dart b/frontend/arah.app/lib/features/marketplace/presentation/screens/marketplace_screen.dart
index 013639d0..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
@@ -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,25 +46,26 @@ 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 [
- Tab(text: 'Buscar'),
- Tab(text: 'Minha loja'),
+ tabs: [
+ Tab(text: l10n.searchTab),
+ Tab(text: l10n.myStore),
],
),
actions: [
@@ -71,18 +74,18 @@ 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,
);
}
}
},
icon: const Icon(Icons.shopping_cart_checkout),
- label: Text('Checkout (${_cartItemCount(state.cart)})'),
+ label: Text(l10n.checkoutWithCount(_cartItemCount(state.cart))),
),
],
),
@@ -121,6 +124,7 @@ class _SearchTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
return Column(
children: [
Padding(
@@ -128,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(
@@ -145,6 +149,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());
}
@@ -153,12 +158,12 @@ class _SearchTab extends StatelessWidget {
child: Text(
state.error is ApiException
? (state.error as ApiException).userMessage
- : 'Erro ao buscar itens.',
+ : l10n.errorSearchItems,
),
);
}
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),
@@ -169,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,
);
}
}
@@ -234,6 +242,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,28 +255,28 @@ 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),
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,
),
@@ -276,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 {
@@ -289,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 2dc3e36f..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
@@ -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(),
@@ -58,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: [
@@ -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!)),
],
],
),
@@ -107,45 +111,45 @@ 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,
);
}
}
},
icon: const Icon(Icons.home_work_outlined),
- label: const Text('Solicitar residência'),
+ label: Text(l10n.requestResidency),
),
const SizedBox(height: AppConstants.spacingMd),
OutlinedButton.icon(
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,
);
}
}
},
icon: const Icon(Icons.location_on_outlined),
- label: const Text('Verificar por localização'),
+ label: Text(l10n.verifyByLocation),
),
] 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 9c9b894e..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
@@ -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,27 +44,28 @@ 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 [
- Tab(text: 'Fila'),
- Tab(text: 'Casos'),
- Tab(text: 'Evidências'),
+ tabs: [
+ Tab(text: l10n.moderationQueueTab),
+ Tab(text: l10n.moderationCasesTab),
+ Tab(text: l10n.moderationEvidencesTab),
],
),
),
@@ -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(),
@@ -101,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,
),
),
@@ -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(
@@ -125,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),
@@ -136,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'),
),
@@ -164,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'),
),
@@ -185,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,
);
}
}
@@ -203,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/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 e7810158..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
@@ -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';
@@ -124,7 +126,7 @@ class _OnboardingScreenState extends ConsumerState {
});
}
- return Scaffold(
+ return ArahScaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
@@ -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({
@@ -379,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(
@@ -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..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
@@ -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';
@@ -18,37 +20,27 @@ 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 Scaffold(
- appBar: AppBar(title: Text(AppLocalizations.of(context)!.profile)),
+ return ArahScaffold(
+ appBar: AppBar(title: Text(l10n.profile)),
body: Center(
child: Padding(
padding: const EdgeInsets.all(AppConstants.spacingLg),
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: 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),
),
],
),
@@ -60,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),
@@ -113,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),
),
],
),
@@ -131,6 +123,7 @@ class ProfileScreen extends ConsumerWidget {
}
void _showSettingsSheet(BuildContext context) {
+ final l10n = AppLocalizations.of(context)!;
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
@@ -142,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');
@@ -150,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');
@@ -158,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');
@@ -166,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');
@@ -174,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');
@@ -182,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');
@@ -190,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');
@@ -198,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..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
@@ -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(),
@@ -43,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,
),
),
@@ -63,33 +67,33 @@ 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 {
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,
);
}
}
},
- 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(
@@ -102,17 +106,17 @@ 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,
);
}
}
},
- child: const Text('Assinar'),
+ child: Text(l10n.subscribe),
),
),
),
diff --git a/frontend/arah.app/lib/l10n/app_en.arb b/frontend/arah.app/lib/l10n/app_en.arb
index 9d7d2fe4..35d57f50 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",
@@ -107,5 +108,189 @@
"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"}
+ }
+ },
+ "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 942cf82e..6846a334 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:
@@ -739,6 +745,816 @@ 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);
+
+ /// 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 d2af6ac0..7cf2ac7e 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';
@@ -339,4 +342,429 @@ 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';
+ }
+
+ @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 c3578d2f..0110935f 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';
@@ -341,4 +344,432 @@ 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';
+ }
+
+ @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 21e7f3b4..d2cf4d53 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",
@@ -107,5 +108,189 @@
"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"}
+ }
+ },
+ "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/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 02c9fd64..6c89179c 100644
--- a/frontend/arah.app/pubspec.yaml
+++ b/frontend/arah.app/pubspec.yaml
@@ -23,6 +23,8 @@ dependencies:
# UI
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)
@@ -46,3 +48,4 @@ flutter:
generate: true
assets:
- assets/images/
+ - assets/images/arah-icon.svg
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 ""
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);
+ });
+}
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": [