diff --git a/app/localization_plan.txt b/app/localization_plan.txt new file mode 100644 index 000000000..fe356a443 --- /dev/null +++ b/app/localization_plan.txt @@ -0,0 +1,36 @@ +Localization Plan + +1) Inventory and extraction +- Identify all user-visible strings across UI: labels, placeholders, button texts, dialogs, toasts, headings, contentDescription for icons and images. +- Replace hardcoded literals with references to resources in res/values/strings.xml. + +2) Resource structuring +- Use clear, stable keys (prefix with context where helpful: screen_, action_, content_desc_). +- Add for count-dependent text and for option lists where needed. +- Mark non-translatable items with translatable="false" (e.g., symbols like "/"). + +3) Compose usage +- Use stringResource(R.string.key) in composables. +- For non-composable helpers, pass localized strings in as parameters or compute at call sites. + +4) Locale-specific resources +- Add res/values-/strings.xml (e.g., values-es, values-fr) for target locales. +- Keep keys identical; only translate values. + +5) Android 13+ per-app language (optional) +- Create res/xml/locales_config.xml listing supported locales. +- Reference in AndroidManifest.xml via android:localeConfig="@xml/locales_config". +- For in-app language picker, call AppCompatDelegate.setApplicationLocales(...) and persist selection. + +6) Locale-sensitive formatting +- Prefer DateFormat / NumberFormat for locale-aware formatting and respect 12/24h settings. + +7) Accessibility +- Ensure all Icon/IconButton/Image have meaningful contentDescription using string resources. +- Use semantics for non-textual interactive elements where needed. + +8) QA +- Test by switching device language; verify truncation, RTL mirroring, and TalkBack. +- Maintain a translation workflow (spreadsheet/service) with key ownership and review. + + diff --git a/app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt b/app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt index aed974b6b..f7792d975 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt @@ -5,11 +5,11 @@ package com.bitchat.android.geohash * Direct port from iOS implementation for 100% compatibility */ enum class GeohashChannelLevel(val precision: Int, val displayName: String) { - BLOCK(7, "Block"), - NEIGHBORHOOD(6, "Neighborhood"), - CITY(5, "City"), - PROVINCE(4, "Province"), - REGION(2, "REGION"); + BLOCK(7, "block"), + NEIGHBORHOOD(6, "neighborhood"), + CITY(5, "city"), + PROVINCE(4, "province"), + REGION(2, "region"); companion object { fun allCases(): List = values().toList() diff --git a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt index a026d47e3..fbb183969 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt @@ -98,7 +98,7 @@ private fun BatteryOptimizationEnabledContent( Icon( imageVector = Icons.Outlined.BatteryAlert, - contentDescription = "Battery Optimization", + contentDescription = stringResource(id = R.string.content_desc_battery_optimization), modifier = Modifier.size(64.dp), tint = colorScheme.error ) @@ -231,7 +231,7 @@ private fun BatteryOptimizationCheckingContent( Icon( imageVector = Icons.Filled.BatteryStd, - contentDescription = "Checking Battery Optimization", + contentDescription = stringResource(id = R.string.content_desc_checking_battery_optimization), modifier = Modifier .size(64.dp) .rotate(rotation), @@ -277,7 +277,7 @@ private fun BatteryOptimizationNotSupportedContent( Icon( imageVector = Icons.Filled.CheckCircle, - contentDescription = "Battery Optimization Not Supported", + contentDescription = stringResource(id = R.string.content_desc_battery_optimization_not_supported), modifier = Modifier.size(64.dp), tint = colorScheme.primary ) diff --git a/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt index 25dfd0d5a..95f9115eb 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/BluetoothCheckScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R /** * Screen shown when checking Bluetooth status or requesting Bluetooth enable @@ -69,7 +71,7 @@ private fun BluetoothDisabledContent( // Bluetooth icon - using Bluetooth outlined icon in app's green color Icon( imageVector = Icons.Outlined.Bluetooth, - contentDescription = "Bluetooth", + contentDescription = stringResource(id = R.string.content_desc_bluetooth), modifier = Modifier.size(64.dp), tint = Color(0xFF00C851) // App's main green color ) @@ -184,7 +186,7 @@ private fun BluetoothNotSupportedContent( } Text( - text = "Bluetooth Not Supported", + text = stringResource(id = R.string.bt_not_supported), style = MaterialTheme.typography.headlineSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -201,7 +203,7 @@ private fun BluetoothNotSupportedContent( elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Text( - text = "This device doesn't support Bluetooth Low Energy (BLE), which is required for bitchat to function.\n\nbitchat needs BLE to create mesh networks and communicate with nearby devices without internet.", + text = stringResource(id = R.string.bt_not_supported_paragraph), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface @@ -222,7 +224,7 @@ private fun BluetoothCheckingContent( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "bitchat", + text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.headlineLarge.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -234,7 +236,7 @@ private fun BluetoothCheckingContent( BluetoothLoadingIndicator() Text( - text = "Checking Bluetooth status...", + text = stringResource(id = R.string.checking_bluetooth_status), style = MaterialTheme.typography.bodyLarge.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) diff --git a/app/src/main/java/com/bitchat/android/onboarding/InitializingScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/InitializingScreen.kt index 24a0a9533..570033ae7 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/InitializingScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/InitializingScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.unit.dp /** @@ -59,7 +61,7 @@ fun InitializingScreen(modifier: Modifier) { ) { // App title Text( - text = "bitchat", + text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.headlineLarge.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -88,7 +90,7 @@ fun InitializingScreen(modifier: Modifier) { horizontalArrangement = Arrangement.Center ) { Text( - text = "Initializing mesh network", + text = stringResource(id = R.string.initializing_mesh_network), style = MaterialTheme.typography.bodyLarge.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) @@ -123,7 +125,7 @@ fun InitializingScreen(modifier: Modifier) { horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "Setting up Bluetooth mesh networking...", + text = stringResource(id = R.string.setting_up_bluetooth_mesh), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.8f) @@ -132,7 +134,7 @@ fun InitializingScreen(modifier: Modifier) { ) Text( - text = "This should only take a few seconds", + text = stringResource(id = R.string.initializing_note_seconds), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.6f) @@ -180,7 +182,7 @@ fun InitializationErrorScreen( } Text( - text = "Setup Not Complete", + text = stringResource(id = R.string.setup_not_complete), style = MaterialTheme.typography.headlineSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -216,7 +218,7 @@ fun InitializationErrorScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Try Again", + text = stringResource(id = R.string.try_again), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold @@ -230,7 +232,7 @@ fun InitializationErrorScreen( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Open Settings", + text = stringResource(id = R.string.open_settings), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace ), diff --git a/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt index 5a544e002..e2cd0fce7 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/LocationCheckScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R /** * Screen shown when checking location services status or requesting location services enable @@ -70,13 +72,13 @@ private fun LocationDisabledContent( // Location icon - using LocationOn outlined icon in app's green color Icon( imageVector = Icons.Outlined.LocationOn, - contentDescription = "Location Services", + contentDescription = stringResource(id = R.string.content_desc_location_services), modifier = Modifier.size(64.dp), tint = Color(0xFF00C851) // App's main green color ) Text( - text = "Location Services Required", + text = stringResource(id = R.string.location_services_required), style = MaterialTheme.typography.headlineSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -103,13 +105,13 @@ private fun LocationDisabledContent( ) { Icon( imageVector = Icons.Filled.Security, - contentDescription = "Privacy", + contentDescription = stringResource(id = R.string.content_desc_privacy), tint = Color(0xFF4CAF50), modifier = Modifier.size(20.dp) ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Privacy First", + text = stringResource(id = R.string.privacy_first), style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Bold, color = colorScheme.onSurface @@ -118,7 +120,7 @@ private fun LocationDisabledContent( } Text( - text = "bitchat does NOT track your location.\n\nLocation services are required for Bluetooth scanning and for the Geohash chat feature.", + text = stringResource(id = R.string.loc_bitchat_does_not_track), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.8f) @@ -128,7 +130,7 @@ private fun LocationDisabledContent( Spacer(modifier = Modifier.height(4.dp)) Text( - text = "bitchat needs location services for:", + text = stringResource(id = R.string.loc_needs), style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Medium, color = colorScheme.onSurface @@ -138,10 +140,10 @@ private fun LocationDisabledContent( ) Text( - text = "• Bluetooth device scanning\n" + - "• Discovering nearby users on mesh network\n" + - "• Geohash chat feature\n" + - "• No tracking or location collection", + text = stringResource(id = R.string.loc_bullet_1) + "\n" + + stringResource(id = R.string.loc_bullet_2) + "\n" + + stringResource(id = R.string.loc_bullet_3) + "\n" + + stringResource(id = R.string.loc_bullet_4), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.8f) @@ -165,7 +167,7 @@ private fun LocationDisabledContent( ) ) { Text( - text = "Open Location Settings", + text = stringResource(id = R.string.open_location_settings), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold @@ -179,7 +181,7 @@ private fun LocationDisabledContent( modifier = Modifier.fillMaxWidth() ) { Text( - text = "Check Again", + text = stringResource(id = R.string.check_again), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace ), @@ -202,13 +204,13 @@ private fun LocationNotAvailableContent( // Error icon Icon( imageVector = Icons.Filled.ErrorOutline, - contentDescription = "Error", + contentDescription = stringResource(id = R.string.content_desc_error), modifier = Modifier.size(64.dp), tint = colorScheme.error ) Text( - text = "Location Services Unavailable", + text = stringResource(id = R.string.loc_unavailable), style = MaterialTheme.typography.headlineSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -225,7 +227,7 @@ private fun LocationNotAvailableContent( elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Text( - text = "Location services are not available on this device. This is unusual as location services are standard on Android devices.\n\nbitchat needs location services for Bluetooth scanning to work properly (Android requirement). Without this, the app cannot discover nearby users.", + text = stringResource(id = R.string.loc_unavailable_paragraph), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index a925567e5..1290d25cf 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R /** * Permission explanation screen shown before requesting permissions @@ -46,7 +48,7 @@ fun PermissionExplanationScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = "Welcome to bitchat", + text = stringResource(id = R.string.welcome_to_app, stringResource(id = R.string.app_name)), style = MaterialTheme.typography.headlineMedium.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -58,7 +60,7 @@ fun PermissionExplanationScreen( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Decentralized mesh messaging over Bluetooth", + text = stringResource(id = R.string.decentralized_mesh_bluetooth), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) @@ -91,7 +93,7 @@ fun PermissionExplanationScreen( modifier = Modifier.size(20.dp) ) Text( - text = "Your Privacy is Protected", + text = stringResource(id = R.string.privacy_protected), style = MaterialTheme.typography.titleSmall.copy( fontWeight = FontWeight.Bold, color = colorScheme.onSurface @@ -100,10 +102,7 @@ fun PermissionExplanationScreen( } Text( - text = "• bitchat doesn't track you or collect personal data\n" + - "• Bluetooth mesh chats are fully offline and require no internet\n" + - "• Geohash chats use the internet but your location is generalized\n" + - "• Your messages stay on your device and peer devices only", + text = stringResource(id = R.string.privacy_points, stringResource(id = R.string.app_name)), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.8f) @@ -115,7 +114,7 @@ fun PermissionExplanationScreen( Spacer(modifier = Modifier.height(8.dp)) Text( - text = "To work properly, bitchat needs these permissions:", + text = stringResource(id = R.string.needs_permissions_intro, stringResource(id = R.string.app_name)), style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Medium, color = colorScheme.onSurface @@ -220,7 +219,7 @@ private fun PermissionCategoryCard( modifier = Modifier.size(16.dp) ) Text( - text = "bitchat does NOT track your location", + text = stringResource(id = R.string.does_not_track_location, stringResource(id = R.string.app_name)), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 3caccf322..5cfdd6121 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bitchat.android.nostr.NostrProofOfWork @@ -84,7 +86,7 @@ fun AboutSheet( verticalAlignment = Alignment.Bottom ) { Text( - text = "bitchat", + text = stringResource(id = R.string.app_name), fontSize = 18.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, @@ -103,7 +105,7 @@ fun AboutSheet( } Text( - text = "decentralized mesh messaging with end-to-end encryption", + text = stringResource(id = R.string.about_tagline), fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) @@ -117,24 +119,24 @@ fun AboutSheet( FeatureCard( icon = Icons.Filled.Bluetooth, iconColor = standardBlue, - title = "offline mesh chat", - description = "communicate directly via bluetooth le without internet or servers. messages relay through nearby devices to extend range.", + title = stringResource(id = R.string.about_feature_offline_mesh_title), + description = stringResource(id = R.string.about_feature_offline_mesh_desc), modifier = Modifier.fillMaxWidth() ) FeatureCard( icon = Icons.Filled.Public, iconColor = standardGreen, - title = "online geohash channels", - description = "connect with people in your area using geohash-based channels. extend the mesh using public internet relays.", + title = stringResource(id = R.string.about_feature_geohash_title), + description = stringResource(id = R.string.about_feature_geohash_desc), modifier = Modifier.fillMaxWidth() ) FeatureCard( icon = Icons.Filled.Lock, iconColor = if (isDark) Color(0xFFFFD60A) else Color(0xFFF5A623), - title = "end-to-end encryption", - description = "private messages are encrypted. channel messages are public.", + title = stringResource(id = R.string.about_feature_e2e_title), + description = stringResource(id = R.string.about_feature_e2e_desc), modifier = Modifier.fillMaxWidth() ) } @@ -148,7 +150,7 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "appearance", + text = stringResource(id = R.string.appearance), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, @@ -158,17 +160,17 @@ fun AboutSheet( FilterChip( selected = themePref.isSystem, onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) }, - label = { Text("system", fontFamily = FontFamily.Monospace) } + label = { Text(stringResource(id = R.string.system_theme), fontFamily = FontFamily.Monospace) } ) FilterChip( selected = themePref.isLight, onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Light) }, - label = { Text("light", fontFamily = FontFamily.Monospace) } + label = { Text(stringResource(id = R.string.light_theme), fontFamily = FontFamily.Monospace) } ) FilterChip( selected = themePref.isDark, onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Dark) }, - label = { Text("dark", fontFamily = FontFamily.Monospace) } + label = { Text(stringResource(id = R.string.dark_theme), fontFamily = FontFamily.Monospace) } ) } } @@ -191,7 +193,7 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "proof of work", + text = stringResource(id = R.string.about_proof_of_work), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, @@ -205,7 +207,7 @@ fun AboutSheet( FilterChip( selected = !powEnabled, onClick = { PoWPreferenceManager.setPowEnabled(false) }, - label = { Text("pow off", fontFamily = FontFamily.Monospace) } + label = { Text(stringResource(id = R.string.about_pow_off), fontFamily = FontFamily.Monospace) } ) FilterChip( selected = powEnabled, @@ -215,7 +217,7 @@ fun AboutSheet( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { - Text("pow on", fontFamily = FontFamily.Monospace) + Text(stringResource(id = R.string.about_pow_on), fontFamily = FontFamily.Monospace) // Show current difficulty if (powEnabled) { Surface( @@ -229,7 +231,7 @@ fun AboutSheet( } Text( - text = "add proof of work to geohash messages for spam deterrence.", + text = stringResource(id = R.string.about_pow_desc), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.6f) @@ -242,7 +244,7 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "difficulty: $powDifficulty bits (~${NostrProofOfWork.estimateMiningTime(powDifficulty)})", + text = stringResource(id = R.string.about_difficulty_bits, powDifficulty, NostrProofOfWork.estimateMiningTime(powDifficulty)), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) @@ -270,20 +272,20 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = "difficulty $powDifficulty requires ~${NostrProofOfWork.estimateWork(powDifficulty)} hash attempts", + text = stringResource(id = R.string.about_difficulty_hash_attempts, powDifficulty, NostrProofOfWork.estimateWork(powDifficulty)), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) ) Text( text = when { - powDifficulty == 0 -> "no proof of work required" - powDifficulty <= 8 -> "very low - minimal spam protection" - powDifficulty <= 12 -> "low - basic spam protection" - powDifficulty <= 16 -> "medium - good spam protection" - powDifficulty <= 20 -> "high - strong spam protection" - powDifficulty <= 24 -> "very high - may cause delays" - else -> "extreme - significant computation required" + powDifficulty == 0 -> stringResource(id = R.string.about_difficulty_none) + powDifficulty <= 8 -> stringResource(id = R.string.about_difficulty_very_low) + powDifficulty <= 12 -> stringResource(id = R.string.about_difficulty_low) + powDifficulty <= 16 -> stringResource(id = R.string.about_difficulty_medium) + powDifficulty <= 20 -> stringResource(id = R.string.about_difficulty_high) + powDifficulty <= 24 -> stringResource(id = R.string.about_difficulty_very_high) + else -> stringResource(id = R.string.about_difficulty_extreme) }, fontSize = 10.sp, fontFamily = FontFamily.Monospace, @@ -306,7 +308,7 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "network", + text = stringResource(id = R.string.about_network), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, @@ -319,7 +321,7 @@ fun AboutSheet( torMode.value = com.bitchat.android.net.TorMode.OFF com.bitchat.android.net.TorPreferenceManager.set(ctx, torMode.value) }, - label = { Text("tor off", fontFamily = FontFamily.Monospace) } + label = { Text(stringResource(id = R.string.about_tor_off), fontFamily = FontFamily.Monospace) } ) FilterChip( selected = torMode.value == com.bitchat.android.net.TorMode.ON, @@ -332,7 +334,7 @@ fun AboutSheet( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { - Text("tor on", fontFamily = FontFamily.Monospace) + Text(stringResource(id = R.string.about_tor_on), fontFamily = FontFamily.Monospace) // Status indicator (red/orange/green) moved inside the "tor on" button val statusColor = when { torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500) @@ -348,7 +350,7 @@ fun AboutSheet( ) } Text( - text = "route internet over tor for enhanced privacy.", + text = stringResource(id = R.string.about_tor_desc), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.6f) @@ -366,7 +368,7 @@ fun AboutSheet( ) { Text( text = "tor status: " + - (if (torStatus.running) "running" else "stopped") + + (if (torStatus.running) stringResource(id = R.string.about_tor_status_running) else stringResource(id = R.string.about_tor_status_stopped)) + ", bootstrap=" + torStatus.bootstrapPercent + "%", fontSize = 11.sp, fontFamily = FontFamily.Monospace, @@ -375,7 +377,7 @@ fun AboutSheet( val last = torStatus.lastLogLine if (last.isNotEmpty()) { Text( - text = "last: " + last.take(160), + text = stringResource(id = R.string.about_last) + " " + last.take(160), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.6f) @@ -400,14 +402,14 @@ fun AboutSheet( ) { Icon( imageVector = Icons.Filled.Warning, - contentDescription = "Warning", + contentDescription = stringResource(id = R.string.content_desc_warning), tint = Color(0xFFBF1A1A), modifier = Modifier.size(16.dp) ) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Text( - text = "emergency data deletion", + text = stringResource(id = R.string.about_emergency_title), fontSize = 12.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, @@ -415,7 +417,7 @@ fun AboutSheet( ) Text( - text = "tip: triple-click the app title to emergency delete all stored data including messages, keys, and settings.", + text = stringResource(id = R.string.about_emergency_tip), fontSize = 11.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.7f) @@ -457,7 +459,7 @@ fun AboutSheet( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "open source • privacy first • decentralized", + text = stringResource(id = R.string.about_footer), fontSize = 10.sp, fontFamily = FontFamily.Monospace, color = colorScheme.onSurface.copy(alpha = 0.5f) @@ -564,7 +566,7 @@ fun PasswordPromptDialog( onDismissRequest = onDismiss, title = { Text( - text = "Enter Channel Password", + text = stringResource(id = R.string.enter_channel_password_title), style = MaterialTheme.typography.titleMedium, color = colorScheme.onSurface ) @@ -572,7 +574,7 @@ fun PasswordPromptDialog( text = { Column { Text( - text = "Channel $channelName is password protected. Enter the password to join.", + text = stringResource(id = R.string.enter_channel_password_body, channelName), style = MaterialTheme.typography.bodyMedium, color = colorScheme.onSurface ) @@ -581,7 +583,7 @@ fun PasswordPromptDialog( OutlinedTextField( value = passwordInput, onValueChange = onPasswordChange, - label = { Text("Password", style = MaterialTheme.typography.bodyMedium) }, + label = { Text(stringResource(id = R.string.channel_password_hint), style = MaterialTheme.typography.bodyMedium) }, textStyle = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace ), @@ -595,7 +597,7 @@ fun PasswordPromptDialog( confirmButton = { TextButton(onClick = onConfirm) { Text( - text = "Join", + text = stringResource(id = R.string.join), style = MaterialTheme.typography.bodyMedium, color = colorScheme.primary ) @@ -604,7 +606,7 @@ fun PasswordPromptDialog( dismissButton = { TextButton(onClick = onDismiss) { Text( - text = "Cancel", + text = stringResource(id = R.string.cancel), style = MaterialTheme.typography.bodyMedium, color = colorScheme.onSurface ) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index bb1619534..ee2143d59 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -62,7 +64,7 @@ fun TorStatusIcon( } Icon( imageVector = Icons.Outlined.Cable, - contentDescription = "Tor status", + contentDescription = stringResource(id = R.string.content_desc_tor_status), modifier = modifier, tint = cableColor ) @@ -209,7 +211,7 @@ fun PeerCounter( // Filled mail icon to match sidebar style Icon( imageVector = Icons.Filled.Email, - contentDescription = "Unread private messages", + contentDescription = stringResource(id = R.string.content_desc_unread_private_messages), modifier = Modifier.size(16.dp), tint = Color(0xFFFF9500) // Orange to match private message theme ) @@ -384,7 +386,7 @@ private fun PrivateChatHeader( ) { Icon( imageVector = Icons.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(id = R.string.content_desc_back), modifier = Modifier.size(16.dp), tint = colorScheme.primary ) @@ -416,7 +418,7 @@ private fun PrivateChatHeader( if (showGlobe) { Icon( imageVector = Icons.Outlined.Public, - contentDescription = "Nostr reachable", + contentDescription = stringResource(id = R.string.content_desc_nostr_reachable), modifier = Modifier.size(14.dp), tint = Color(0xFF9B59B6) // Purple like iOS ) @@ -474,7 +476,7 @@ private fun ChannelHeader( ) { Icon( imageVector = Icons.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = stringResource(id = R.string.content_desc_back), modifier = Modifier.size(16.dp), tint = colorScheme.primary ) @@ -637,7 +639,7 @@ private fun LocationChannelsButton( Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = Icons.Default.PinDrop, - contentDescription = "Teleported", + contentDescription = stringResource(id = R.string.content_desc_teleported), modifier = Modifier.size(12.dp), tint = badgeColor ) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 6c0a5555f..7c01483f3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -9,6 +9,8 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.Alignment import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward @@ -262,7 +264,7 @@ fun ChatScreen(viewModel: ChatViewModel) { IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) { Icon( imageVector = Icons.Filled.ArrowDownward, - contentDescription = "Scroll to bottom", + contentDescription = stringResource(id = R.string.content_desc_scroll_to_bottom), tint = Color(0xFF00C851) ) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 74302cb99..89404be17 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -16,6 +16,7 @@ import com.bitchat.android.util.NotificationIntervalManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Date +import com.bitchat.android.R import kotlin.random.Random /** @@ -48,7 +49,7 @@ class ChatViewModel( } val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate) - private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager) + private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager, application.applicationContext) private val notificationManager = NotificationManager( application.applicationContext, NotificationManagerCompat.from(application.applicationContext), @@ -191,7 +192,7 @@ class ChatViewModel( if (state.getConnectedPeersValue().isEmpty() && state.getMessagesValue().isEmpty()) { val welcomeMessage = BitchatMessage( sender = "system", - content = "get people around you to download bitchat and chat with them here!", + content = getApplication().getString(R.string.welcome_warmup), timestamp = Date(), isRelay = false ) diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index d36f67b43..b630679a0 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -1,5 +1,7 @@ package com.bitchat.android.ui +import android.content.Context +import com.bitchat.android.R import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.BitchatMessage import java.util.Date @@ -11,20 +13,21 @@ class CommandProcessor( private val state: ChatState, private val messageManager: MessageManager, private val channelManager: ChannelManager, - private val privateChatManager: PrivateChatManager + private val privateChatManager: PrivateChatManager, + private val context: Context ) { // Available commands list private val baseCommands = listOf( - CommandSuggestion("/block", emptyList(), "[nickname]", "block or list blocked peers"), - CommandSuggestion("/channels", emptyList(), null, "show all discovered channels"), - CommandSuggestion("/clear", emptyList(), null, "clear chat messages"), - CommandSuggestion("/hug", emptyList(), "", "send someone a warm hug"), - CommandSuggestion("/j", listOf("/join"), "", "join or create a channel"), - CommandSuggestion("/m", listOf("/msg"), " [message]", "send private message"), - CommandSuggestion("/slap", emptyList(), "", "slap someone with a trout"), - CommandSuggestion("/unblock", emptyList(), "", "unblock a peer"), - CommandSuggestion("/w", emptyList(), null, "see who's online") + CommandSuggestion("/block", emptyList(), "[nickname]", context.getString(R.string.app_cmd_block_desc)), + CommandSuggestion("/channels", emptyList(), null, context.getString(R.string.app_cmd_channels_desc)), + CommandSuggestion("/clear", emptyList(), null, context.getString(R.string.app_cmd_clear_desc)), + CommandSuggestion("/hug", emptyList(), "", context.getString(R.string.app_cmd_hug_desc)), + CommandSuggestion("/j", listOf("/join"), "", context.getString(R.string.app_cmd_join_desc)), + CommandSuggestion("/m", listOf("/msg"), " [message]", context.getString(R.string.app_cmd_msg_desc)), + CommandSuggestion("/slap", emptyList(), "", context.getString(R.string.app_cmd_slap_desc)), + CommandSuggestion("/unblock", emptyList(), "", context.getString(R.string.app_cmd_unblock_desc)), + CommandSuggestion("/w", emptyList(), null, context.getString(R.string.app_cmd_w_desc)) ) // MARK: - Command Processing @@ -69,7 +72,7 @@ class CommandProcessor( } else { val systemMessage = BitchatMessage( sender = "system", - content = "usage: /join ", + content = context.getString(R.string.usage_join), timestamp = Date(), isRelay = false ) @@ -102,7 +105,7 @@ class CommandProcessor( } else { val systemMessage = BitchatMessage( sender = "system", - content = "started private chat with $targetName", + content = context.getString(R.string.started_private_chat_fmt, targetName), timestamp = Date(), isRelay = false ) @@ -112,7 +115,7 @@ class CommandProcessor( } else { val systemMessage = BitchatMessage( sender = "system", - content = "user '$targetName' not found. they may be offline or using a different nickname.", + content = context.getString(R.string.user_not_found_fmt, targetName), timestamp = Date(), isRelay = false ) @@ -172,11 +175,8 @@ class CommandProcessor( val systemMessage = BitchatMessage( sender = "system", - content = if (peerList.isEmpty()) { - "no one else is around right now." - } else { - "$contextDescription: $peerList" - }, + content = if (peerList.isEmpty()) context.getString(R.string.no_one_else_around) + else context.getString(R.string.context_list_fmt, contextDescription, peerList), timestamp = Date(), isRelay = false ) @@ -208,7 +208,7 @@ class CommandProcessor( if (currentChannel == null) { val systemMessage = BitchatMessage( sender = "system", - content = "you must be in a channel to set a password.", + content = context.getString(R.string.must_be_in_channel), timestamp = Date(), isRelay = false ) diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt index 5ce03bf16..d68fd0c4c 100644 --- a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt +++ b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt @@ -8,6 +8,8 @@ import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.LocationOn import androidx.compose.material3.* +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -182,7 +184,7 @@ private fun GeohashPersonItem( // Unread DM indicator (orange envelope) Icon( imageVector = Icons.Filled.Email, - contentDescription = "Unread message", + contentDescription = stringResource(id = R.string.content_desc_unread_message), modifier = Modifier.size(12.dp), tint = Color(0xFFFF9500) // iOS orange ) diff --git a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt index 6ebeb21da..5803b1a8a 100644 --- a/app/src/main/java/com/bitchat/android/ui/InputComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/InputComponents.kt @@ -200,7 +200,7 @@ fun MessageInput( // Show placeholder when there's no text if (value.text.isEmpty()) { Text( - text = "type a message...", + text = stringResource(id = R.string.message_hint), style = MaterialTheme.typography.bodyMedium.copy( fontFamily = FontFamily.Monospace ), @@ -221,7 +221,7 @@ fun MessageInput( modifier = Modifier.size(32.dp) ) { Text( - text = "/", + text = stringResource(id = R.string.slash), textAlign = TextAlign.Center ) } @@ -408,7 +408,7 @@ fun MentionSuggestionItem( Spacer(modifier = Modifier.weight(1f)) Text( - text = "mention", + text = stringResource(id = R.string.mention_label), style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace ), diff --git a/app/src/main/java/com/bitchat/android/ui/LinkPreviewPill.kt b/app/src/main/java/com/bitchat/android/ui/LinkPreviewPill.kt index 25d00bd74..995fa3d4b 100644 --- a/app/src/main/java/com/bitchat/android/ui/LinkPreviewPill.kt +++ b/app/src/main/java/com/bitchat/android/ui/LinkPreviewPill.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bitchat.android.ui.theme.BASE_FONT_SIZE @@ -56,7 +58,7 @@ fun LinkPreviewPill( null } - val displayTitle = title ?: parsedUrl?.host ?: "Link" + val displayTitle = title ?: parsedUrl?.host ?: stringResource(id = R.string.link) val displayHost = parsedUrl?.host ?: url Surface( @@ -94,7 +96,7 @@ fun LinkPreviewPill( ) { Icon( imageVector = Icons.Outlined.Link, - contentDescription = "Link", + contentDescription = stringResource(id = R.string.content_desc_link), modifier = Modifier.size(24.dp), tint = Color.Blue ) diff --git a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt index e21dd8f5c..af60de17e 100644 --- a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt @@ -33,6 +33,8 @@ import com.bitchat.android.geohash.LocationChannelManager import java.util.* import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R /** * Location Channels Sheet for selecting geohash-based location channels @@ -381,7 +383,7 @@ fun LocationChannelsSheet( // iOS has a face.dashed icon, use closest Material equivalent Icon( imageVector = Icons.Filled.PinDrop, - contentDescription = "Teleport", + contentDescription = stringResource(id = R.string.content_desc_teleport), modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSurface ) @@ -569,10 +571,11 @@ private fun splitTitleAndCount(title: String): Pair { } } +@Composable private fun meshTitleWithCount(viewModel: ChatViewModel): String { val meshCount = meshCount(viewModel) - val noun = if (meshCount == 1) "person" else "people" - return "mesh [$meshCount $noun]" + val noun = if (meshCount == 1) stringResource(id = R.string.person_one) else stringResource(id = R.string.people_many) + return stringResource(id = R.string.mesh_label) + " [" + meshCount + " " + noun + "]" } private fun meshCount(viewModel: ChatViewModel): Int { @@ -582,8 +585,9 @@ private fun meshCount(viewModel: ChatViewModel): Int { } ?: 0 } +@Composable private fun geohashTitleWithCount(channel: GeohashChannel, participantCount: Int): String { - val noun = if (participantCount == 1) "person" else "people" + val noun = if (participantCount == 1) stringResource(id = R.string.person_one) else stringResource(id = R.string.people_many) return "${channel.level.displayName.lowercase()} [$participantCount $noun]" } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index c8452c3b1..d4b067688 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.platform.LocalContext @@ -26,6 +27,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import android.content.Intent import android.net.Uri import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.R import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.mesh.BluetoothMeshService import java.text.SimpleDateFormat @@ -289,7 +291,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) { when (status) { is DeliveryStatus.Sending -> { Text( - text = "○", + text = stringResource(id = R.string.delivery_status_sending_symbol), fontSize = 10.sp, color = colorScheme.primary.copy(alpha = 0.6f) ) @@ -297,7 +299,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) { is DeliveryStatus.Sent -> { // Use a subtle hollow marker for Sent; single check is reserved for Delivered (iOS parity) Text( - text = "○", + text = stringResource(id = R.string.delivery_status_sent_symbol), fontSize = 10.sp, color = colorScheme.primary.copy(alpha = 0.6f) ) @@ -305,14 +307,14 @@ fun DeliveryStatusIcon(status: DeliveryStatus) { is DeliveryStatus.Delivered -> { // Single check for Delivered (matches iOS expectations) Text( - text = "✓", + text = stringResource(id = R.string.delivery_status_delivered_symbol), fontSize = 10.sp, color = colorScheme.primary.copy(alpha = 0.8f) ) } is DeliveryStatus.Read -> { Text( - text = "✓✓", + text = stringResource(id = R.string.delivery_status_read_symbol), fontSize = 10.sp, color = Color(0xFF007AFF), // Blue fontWeight = FontWeight.Bold @@ -320,7 +322,7 @@ fun DeliveryStatusIcon(status: DeliveryStatus) { } is DeliveryStatus.Failed -> { Text( - text = "⚠", + text = stringResource(id = R.string.delivery_status_failed_symbol), fontSize = 10.sp, color = Color.Red.copy(alpha = 0.8f) ) diff --git a/app/src/main/java/com/bitchat/android/ui/NotificationManager.kt b/app/src/main/java/com/bitchat/android/ui/NotificationManager.kt index a36e71fef..10486829b 100644 --- a/app/src/main/java/com/bitchat/android/ui/NotificationManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/NotificationManager.kt @@ -93,8 +93,8 @@ class NotificationManager( private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // DM notifications channel - val dmName = "Direct Messages" - val dmDescriptionText = "Notifications for private messages from other users" + val dmName = context.getString(R.string.channel_name_dm) + val dmDescriptionText = context.getString(R.string.channel_desc_dm) val dmImportance = NotificationManager.IMPORTANCE_HIGH val dmChannel = NotificationChannel(CHANNEL_ID, dmName, dmImportance).apply { description = dmDescriptionText @@ -104,8 +104,8 @@ class NotificationManager( systemNotificationManager.createNotificationChannel(dmChannel) // Geohash notifications channel - val geohashName = "Geohash Chats" - val geohashDescriptionText = "Notifications for mentions and messages in geohash location channels" + val geohashName = context.getString(R.string.channel_name_geohash) + val geohashDescriptionText = context.getString(R.string.channel_desc_geohash) val geohashImportance = NotificationManager.IMPORTANCE_HIGH val geohashChannel = NotificationChannel(GEOHASH_CHANNEL_ID, geohashName, geohashImportance).apply { description = geohashDescriptionText @@ -258,7 +258,7 @@ class NotificationManager( } if (messageCount > 5) { - style.setSummaryText("and ${messageCount - 5} more") + style.setSummaryText(context.getString(R.string.notif_and_more, messageCount - 5)) } builder.setStyle(style) @@ -291,11 +291,11 @@ class NotificationManager( ) // Build notification content - val contentTitle = "👥 bitchatters nearby!" + val contentTitle = context.getString(R.string.notif_title_active_peers) val contentText = if (peersSize == 1) { - "1 person around" + context.getString(R.string.notif_people_around_one) } else { - "$peersSize people around" + context.getString(R.string.notif_people_around_many, peersSize) } val builder = NotificationCompat.Builder(context, CHANNEL_ID) @@ -331,8 +331,8 @@ class NotificationManager( val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) - .setContentTitle("bitchat") - .setContentText("$totalMessages messages from $senderCount people") + .setContentTitle(context.getString(R.string.notif_app_name)) + .setContentText(context.getString(R.string.notif_summary_messages_from_people, totalMessages, senderCount)) .setContentIntent(pendingIntent) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) @@ -342,7 +342,7 @@ class NotificationManager( // Add inbox style showing recent senders val style = NotificationCompat.InboxStyle() - .setBigContentTitle("New Messages") + .setBigContentTitle(context.getString(R.string.notif_new_messages)) pendingNotifications.entries.take(5).forEach { (peerID, notifications) -> val latestNotif = notifications.last() @@ -356,7 +356,7 @@ class NotificationManager( } if (pendingNotifications.size > 5) { - style.setSummaryText("and ${pendingNotifications.size - 5} more conversations") + style.setSummaryText(context.getString(R.string.notif_and_more_conversations, pendingNotifications.size - 5)) } builder.setStyle(style) @@ -503,7 +503,7 @@ class NotificationManager( } if (messageCount > 5) { - style.setSummaryText("and ${messageCount - 5} more") + style.setSummaryText(context.getString(R.string.notif_and_more, messageCount - 5)) } builder.setStyle(style) @@ -543,12 +543,12 @@ class NotificationManager( ) val contentTitle = if (totalMentions > 0) { - "bitchat - $totalMentions mentions" + context.getString(R.string.notif_geohash_mentions_title, totalMentions) } else { - "bitchat - location chats" + context.getString(R.string.notif_geohash_location_chats) } - val contentText = "$totalMessages messages from $geohashCount locations" + val contentText = context.getString(R.string.notif_geohash_summary_messages_from_locations, totalMessages, geohashCount) val builder = NotificationCompat.Builder(context, GEOHASH_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) @@ -563,7 +563,7 @@ class NotificationManager( // Add inbox style showing recent geohashes val style = NotificationCompat.InboxStyle() - .setBigContentTitle("New Location Messages") + .setBigContentTitle(context.getString(R.string.notif_new_location_messages)) pendingGeohashNotifications.entries.take(5).forEach { (geohash, notifications) -> val mentionCount = notifications.count { it.isMention } @@ -579,7 +579,7 @@ class NotificationManager( } if (pendingGeohashNotifications.size > 5) { - style.setSummaryText("and ${pendingGeohashNotifications.size - 5} more locations") + style.setSummaryText(context.getString(R.string.notif_and_more_locations, pendingGeohashNotifications.size - 5)) } builder.setStyle(style) @@ -710,7 +710,7 @@ class NotificationManager( } if (messageCount > 5) { - style.setSummaryText("and ${messageCount - 5} more") + style.setSummaryText(context.getString(R.string.notif_and_more, messageCount - 5)) } builder.setStyle(style) diff --git a/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt b/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt index 55a3e694d..f8971cd76 100644 --- a/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt +++ b/app/src/main/java/com/bitchat/android/ui/PoWStatusIndicator.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import com.bitchat.android.R import androidx.compose.ui.unit.sp import com.bitchat.android.nostr.NostrProofOfWork import com.bitchat.android.nostr.PoWPreferenceManager @@ -54,7 +56,7 @@ fun PoWStatusIndicator( Icon( imageVector = Icons.Filled.Security, - contentDescription = "Mining PoW", + contentDescription = stringResource(id = R.string.content_desc_pow_mining), tint = Color(0xFFFF9500), // Orange for mining modifier = Modifier .size(12.dp) @@ -63,7 +65,7 @@ fun PoWStatusIndicator( } else { Icon( imageVector = Icons.Filled.Security, - contentDescription = "PoW Enabled", + contentDescription = stringResource(id = R.string.content_desc_pow_enabled), tint = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D), // Green when ready modifier = Modifier.size(12.dp) ) @@ -85,7 +87,7 @@ fun PoWStatusIndicator( // PoW icon Icon( imageVector = Icons.Filled.Security, - contentDescription = "Proof of Work", + contentDescription = stringResource(id = R.string.content_desc_proof_of_work), tint = if (isMining) Color(0xFFFF9500) else { if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) }, diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 502e63cbe..92af8dd72 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -186,7 +186,7 @@ fun ChannelsSection( ) { Icon( imageVector = Icons.Default.Person, // Using Person icon as placeholder - contentDescription = null, + contentDescription = stringResource(id = R.string.content_desc_people_icon), modifier = Modifier.size(10.dp), tint = colorScheme.onSurface.copy(alpha = 0.6f) ) @@ -236,7 +236,7 @@ fun ChannelsSection( ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Leave channel", + contentDescription = stringResource(id = R.string.content_desc_leave_channel), modifier = Modifier.size(14.dp), tint = colorScheme.onSurface.copy(alpha = 0.5f) ) @@ -258,6 +258,7 @@ fun PeopleSection( onPrivateChatStart: (String) -> Unit ) { Column { + val youLabel = stringResource(id = R.string.label_you) Row( modifier = Modifier .fillMaxWidth() @@ -266,7 +267,7 @@ fun PeopleSection( ) { Icon( imageVector = Icons.Default.Group, // Using Person icon for people - contentDescription = null, + contentDescription = stringResource(id = R.string.content_desc_people_icon), modifier = Modifier.size(12.dp), tint = colorScheme.onSurface.copy(alpha = 0.6f) ) @@ -317,7 +318,7 @@ fun PeopleSection( compareBy { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first .thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long) .thenBy { !(peerFavoriteStates[it] ?: false) } // Favorites first - .thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical + .thenBy { (if (it == nickname) youLabel else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical ) // Build a map of base name counts across all people shown in the list (connected + offline + nostr) @@ -325,7 +326,7 @@ fun PeopleSection( // Helper to compute display name used for a given key fun computeDisplayNameForPeerId(key: String): String { - return if (key == nickname) "You" else (peerNicknames[key] ?: (privateChats[key]?.lastOrNull()?.sender ?: key.take(12))) + return if (key == nickname) youLabel else (peerNicknames[key] ?: (privateChats[key]?.lastOrNull()?.sender ?: key.take(12))) } @@ -336,7 +337,7 @@ fun PeopleSection( sortedPeers.forEach { pid -> val dn = computeDisplayNameForPeerId(pid) val (b, _) = com.bitchat.android.ui.splitSuffix(dn) - if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + if (b != youLabel) baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 } // Offline favorites (exclude ones mapped to connected) @@ -347,7 +348,7 @@ fun PeopleSection( if (!isMappedToConnected) { val dn = peerNicknames[favPeerID] ?: fav.peerNickname val (b, _) = com.bitchat.android.ui.splitSuffix(dn) - if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + if (b != youLabel) baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 } } @@ -363,7 +364,7 @@ fun PeopleSection( .forEach { convKey -> val dn = peerNicknames[convKey] ?: (privateChats[convKey]?.lastOrNull()?.sender ?: convKey.take(12)) val (b, _) = com.bitchat.android.ui.splitSuffix(dn) - if (b != "You") baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 + if (b != youLabel) baseNameCounts[b] = (baseNameCounts[b] ?: 0) + 1 } sortedPeers.forEach { peerID -> @@ -380,7 +381,7 @@ fun PeopleSection( if (noiseHex != null) privateChats[noiseHex]?.count { msg -> msg.sender != nickname && nostrUnread } ?: 0 else 0 ) - val displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: (privateChats[peerID]?.lastOrNull()?.sender ?: peerID.take(12))) + val displayName = if (peerID == nickname) youLabel else (peerNicknames[peerID] ?: (privateChats[peerID]?.lastOrNull()?.sender ?: peerID.take(12))) val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName) val showHash = (baseNameCounts[bName] ?: 0) > 1 @@ -501,7 +502,7 @@ private fun PeerItem( val (baseNameRaw, suffixRaw) = com.bitchat.android.ui.splitSuffix(displayName) val baseName = truncateNickname(baseNameRaw) val suffix = if (showHashSuffix) suffixRaw else "" - val isMe = displayName == "You" || peerID == viewModel.nickname.value + val isMe = displayName == stringResource(id = R.string.label_you) || peerID == viewModel.nickname.value // Get consistent peer color (iOS-compatible) val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f @@ -524,7 +525,7 @@ private fun PeerItem( // Show mail icon for unread DMs (iOS orange) Icon( imageVector = Icons.Filled.Email, - contentDescription = "Unread message", + contentDescription = stringResource(id = R.string.content_desc_unread_message), modifier = Modifier.size(16.dp), tint = Color(0xFFFF9500) // iOS orange ) @@ -534,14 +535,14 @@ private fun PeerItem( // Purple globe to indicate Nostr availability Icon( imageVector = Icons.Filled.Public, - contentDescription = "Reachable via Nostr", + contentDescription = stringResource(id = R.string.content_desc_reachable_via_nostr), modifier = Modifier.size(16.dp), tint = Color(0xFF9C27B0) // Purple ) } else { Icon( imageVector = if (isDirect) Icons.Outlined.SettingsInputAntenna else Icons.Filled.Route, - contentDescription = if (isDirect) "Direct Bluetooth" else "Routed", + contentDescription = if (isDirect) stringResource(id = R.string.content_desc_direct_bluetooth) else stringResource(id = R.string.content_desc_routed), modifier = Modifier.size(16.dp), tint = colorScheme.onSurface.copy(alpha = 0.8f) ) @@ -588,7 +589,7 @@ private fun PeerItem( ) { Icon( imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, - contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites", + contentDescription = if (isFavorite) stringResource(id = R.string.content_desc_favorite_remove) else stringResource(id = R.string.content_desc_favorite_add), modifier = Modifier.size(16.dp), tint = if (isFavorite) Color(0xFFFFD700) else Color(0xFF4CAF50) ) @@ -643,7 +644,7 @@ private fun UnreadBadge( contentAlignment = Alignment.Center ) { Text( - text = if (count > 99) "99+" else count.toString(), + text = if (count > 99) stringResource(id = R.string.unread_overflow_99_plus) else count.toString(), style = MaterialTheme.typography.labelSmall.copy( fontSize = 10.sp, fontWeight = FontWeight.Bold diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..165c70032 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,247 @@ + + + bitchat + L’autorisation Bluetooth est requise pour la messagerie pair à pair sans Internet. + L’autorisation de localisation est requise pour découvrir les appareils à proximité via Bluetooth. + L’autorisation de notification est requise pour vous alerter des nouveaux messages. + pseudo + écrire un message… + Mot de passe + Rejoindre le canal + Quitter + Envoyer + Afficher les commandes + Retour + Personnes + Canaux + Utilisateurs en ligne + Personne connecté + Touchez trois fois pour effacer toutes les données + Réseau + + + Optimisation de la batterie détectée + Optimisation de la batterie désactivée + Optimisation de la batterie non requise + Vérification de l’optimisation de la batterie + Pourquoi désactiver l’optimisation de la batterie ? + bitchat fonctionne en arrière-plan pour maintenir les connexions du réseau maillé avec les appareils à proximité. L’optimisation de la batterie peut interrompre ces connexions, entraînant des messages retardés ou manqués.\n\nDésactiver l’optimisation de la batterie garantit une messagerie pair à pair fiable. + Désactiver l’optimisation de la batterie + Remarque : vous pouvez modifier ce paramètre plus tard dans Paramètres Android > Applications > bitchat > Batterie + Votre appareil n’a pas besoin de paramètres d’optimisation de la batterie. bitchat fonctionnera normalement. + Votre appareil n’a pas besoin de paramètres d’optimisation de la batterie. bitchat fonctionnera normalement. + bitchat peut fonctionner de manière fiable en arrière-plan + • Garantit une livraison fiable des messages\n• Maintient la connectivité du réseau maillé\n• Autorise la retransmission des messages en arrière-plan\n• Empêche les coupures de connexion + Vérifier à nouveau + Passer pour l’instant + Continuer + Réessayer + Passer + + + Vous + mention + / + + + Canaux + Personnes + Quitter le canal + Message non lu + Joignable via Nostr + Bluetooth direct + Routé + Ajouter aux favoris + Retirer des favoris + Optimisation de la batterie + Vérification de l’optimisation de la batterie + Optimisation de la batterie non prise en charge + + + + + + ✓✓ + + + + Lien + apparence + système + clair + sombre + + + Minage PoW + PoW activé + Preuve de travail + Téléporter + Lien + Aller en bas + Statut Tor + Messages privés non lus + Retour + Joignable via Nostr + Téléporté + Avertissement + Services de localisation + Confidentialité + Erreur + Bluetooth + + + 99+ + + + messagerie maillée décentralisée avec chiffrement de bout en bout + messagerie maillée hors ligne + communiquez directement via bluetooth le sans internet ni serveurs. les messages sont relayés par les appareils à proximité pour étendre la portée. + canaux geohash en ligne + connectez-vous avec des personnes de votre région via des canaux basés sur geohash. étendez le maillage à l’aide de relais internet publics. + chiffrement de bout en bout + les messages privés sont chiffrés. les messages des canaux sont publics. + preuve de travail + pow désactivé + pow activé + ajoutez une preuve de travail aux messages geohash pour dissuader le spam. + difficulté : %1$d bits (~%2$s) + difficulté %1$d nécessite ~%2$s tentatives de hachage + aucune preuve de travail requise + très faible - protection anti-spam minimale + faible - protection anti-spam basique + moyenne - bonne protection anti-spam + élevée - protection anti-spam forte + très élevée - peut causer des retards + extrême - calcul important requis + réseau + tor désactivé + tor activé + redirigez internet via tor pour une confidentialité renforcée. + en cours d’exécution + arrêté + dernier : + suppression d’urgence des données + astuce : appuyez trois fois sur le titre de l’application pour supprimer d’urgence toutes les données stockées, y compris les messages, les clés et les paramètres. + open source • confidentialité d’abord • décentralisé + + + Entrer le mot de passe du canal + Le canal %1$s est protégé par mot de passe. Entrez le mot de passe pour le rejoindre. + Rejoindre + Annuler + + + #canaux de localisation + discutez avec des personnes proches via des canaux geohash. seul un geohash approximatif est partagé, jamais le GPS exact. ne faites pas de capture d’écran et ne partagez pas cet écran pour protéger votre confidentialité. + autoriser l’accès à la localisation + autorisation de localisation refusée. activez-la dans les paramètres pour utiliser les canaux de localisation. + ✓ autorisation de localisation accordée + vérification des autorisations… + recherche des canaux à proximité… + geohash + téléporter + geohash invalide + désactiver les services de localisation + activer les services de localisation + + + Messages directs + Notifications pour les messages privés d’autres utilisateurs + Discussions Geohash + Notifications pour les mentions et messages dans les canaux de localisation geohash + et %1$d de plus + et %1$d conversations de plus + et %1$d emplacements de plus + Nouveaux messages + Nouveaux messages de localisation + 👥 utilisateurs bitchat à proximité ! + 1 personne autour + %1$d personnes autour + bitchat + %1$d messages de %2$d personnes + Mention dans le chat maillé + %1$d mentions dans le chat maillé + bitchat - %1$d mentions + bitchat - discussions par localisation + %1$d messages de %2$d emplacements + + + Initialisation du réseau maillé + Configuration du réseau maillé Bluetooth… + Cela ne devrait prendre que quelques secondes + Configuration incomplète + Réessayer + Ouvrir les paramètres + + + Bienvenue sur %1$s + Messagerie maillée décentralisée via Bluetooth + Votre confidentialité est protégée + • %1$s ne vous piste pas et ne collecte pas de données personnelles\n• Les conversations maillées Bluetooth sont entièrement hors ligne et sans internet\n• Les canaux geohash utilisent internet mais votre position est généralisée\n• Vos messages restent sur votre appareil et ceux des pairs uniquement + Pour fonctionner correctement, %1$s a besoin de ces autorisations : + %1$s ne suit PAS votre position + + + bloquer ou lister les pairs bloqués + afficher tous les canaux découverts + effacer les messages du chat + envoyer un câlin + rejoindre ou créer un canal + envoyer un message privé + gifler quelqu’un avec une truite + débloquer un pair + voir qui est en ligne + + + bloc + quartier + ville + province + région + personne + personnes + maillage + Services de localisation requis + Confidentialité d\’abord + Bluetooth requis + bitchat a besoin du Bluetooth pour : + • Découvrir les utilisateurs à proximité + • Créer des connexions de réseau maillé + • Envoyer et recevoir des messages + • Fonctionner sans internet ni serveurs + Activer le Bluetooth + Bluetooth non pris en charge + Cet appareil ne prend pas en charge Bluetooth Low Energy (BLE), requis pour le fonctionnement de bitchat.\n\nbitchat a besoin de BLE pour créer des réseaux maillés et communiquer avec des appareils à proximité sans internet. + Vérification de l\’état du Bluetooth… + bitchat ne piste PAS votre position.\n\nLes services de localisation sont requis pour l\’analyse Bluetooth et pour la messagerie geohash. + bitchat a besoin des services de localisation pour : + • Analyse des appareils Bluetooth + • Découvrir les utilisateurs proches sur le réseau maillé + • Fonction geohash + • Aucun pistage ni collecte de position + Ouvrir les paramètres de localisation + Vérifier à nouveau + Services de localisation indisponibles + Les services de localisation ne sont pas disponibles sur cet appareil. C\’est inhabituel car ils sont généralement présents sur Android.\n\nbitchat a besoin des services de localisation pour que l\’analyse Bluetooth fonctionne correctement (exigence Android). Sans cela, l\’app ne peut pas découvrir les utilisateurs à proximité. + Vérification des services de localisation… + inconnu + + + invitez les personnes autour de vous à télécharger bitchat et discutez ici avec elles ! + + + utilisation : /join <canal> + discussion privée démarrée avec %1$s + utilisateur « %1$s » introuvable. il peut être hors ligne ou utiliser un autre pseudo. + personne d’autre n’est présent pour le moment. + %1$s : %2$s + vous devez être dans un canal pour définir un mot de passe. + vous devez être le créateur du canal pour définir un mot de passe. + mot de passe modifié pour le canal %1$s + utilisation : /msg <pseudo> [message] + utilisation : /pass <mot de passe> + aucun canal rejoint + canaux rejoints : %1$s + commande inconnue : %1$s. tapez / pour voir les commandes disponibles. + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 783120f28..8cbd26e20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,4 +37,211 @@ Continue Retry Skip + + + You + mention + / + + + Channels + People + Leave channel + Unread message + Reachable via Nostr + Direct Bluetooth + Routed + Add to favorites + Remove from favorites + Battery Optimization + Checking Battery Optimization + Battery Optimization Not Supported + + + + + + ✓✓ + + + + Link + appearance + system + light + dark + + + Mining PoW + PoW Enabled + Proof of Work + Teleport + Link + Scroll to bottom + Tor status + Unread private messages + Back + Nostr reachable + Teleported + Warning + Location Services + Privacy + Error + Bluetooth + + + 99+ + + + decentralized mesh messaging with end-to-end encryption + offline mesh chat + communicate directly via bluetooth le without internet or servers. messages relay through nearby devices to extend range. + online geohash channels + connect with people in your area using geohash-based channels. extend the mesh using public internet relays. + end-to-end encryption + private messages are encrypted. channel messages are public. + proof of work + pow off + pow on + add proof of work to geohash messages for spam deterrence. + difficulty: %1$d bits (~%2$s) + difficulty %1$d requires ~%2$s hash attempts + no proof of work required + very low - minimal spam protection + low - basic spam protection + medium - good spam protection + high - strong spam protection + very high - may cause delays + extreme - significant computation required + network + tor off + tor on + route internet over tor for enhanced privacy. + running + stopped + last: + emergency data deletion + tip: triple-click the app title to emergency delete all stored data including messages, keys, and settings. + open source • privacy first • decentralized + + + Enter Channel Password + Channel %1$s is password protected. Enter the password to join. + Join + Cancel + + + #location channels + chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy. + grant location permission + location permission denied. enable in settings to use location channels. + + ✓ location permission granted + checking permissions... + finding nearby channels… + geohash + teleport + invalid geohash + disable location services + enable location services + + + Direct Messages + Notifications for private messages from other users + Geohash Chats + Notifications for mentions and messages in geohash location channels + and %1$d more + and %1$d more conversations + and %1$d more locations + New Messages + New Location Messages + 👥 bitchatters nearby! + 1 person around + %1$d people around + bitchat + %1$d messages from %2$d people + Mentioned in Mesh Chat + %1$d mentions in Mesh Chat + bitchat - %1$d mentions + bitchat - location chats + %1$d messages from %2$d locations + + + Initializing mesh network + Setting up Bluetooth mesh networking... + This should only take a few seconds + Setup Not Complete + Try Again + Open Settings + + + Welcome to %1$s + Decentralized mesh messaging over Bluetooth + Your Privacy is Protected + • %1$s doesn’t track you or collect personal data\n• Bluetooth mesh chats are fully offline and require no internet\n• Geohash chats use the internet but your location is generalized\n• Your messages stay on your device and peer devices only + To work properly, %1$s needs these permissions: + %1$s does NOT track your location + + + block or list blocked peers + show all discovered channels + clear chat messages + send someone a warm hug + join or create a channel + send private message + slap someone with a trout + unblock a peer + see who\'s online + + + block + neighborhood + city + province + region + person + people + mesh + Location Services Required + Privacy First + Bluetooth Required + bitchat needs Bluetooth to: + • Discover nearby users + • Create mesh network connections + • Send and receive messages + • Work without internet or servers + Enable Bluetooth + Bluetooth Not Supported + This device doesn\'t support Bluetooth Low Energy (BLE), which is required for bitchat to function.\n\nbitchat needs BLE to create mesh networks and communicate with nearby devices without internet. + Checking Bluetooth status... + bitchat does NOT track your location.\n\nLocation services are required for Bluetooth scanning and for the Geohash chat feature. + bitchat needs location services for: + • Bluetooth device scanning + • Discovering nearby users on mesh network + • Geohash chat feature + • No tracking or location collection + Open Location Settings + Check Again + Location Services Unavailable + Location services are not available on this device. This is unusual as location services are standard on Android devices.\n\nbitchat needs location services for Bluetooth scanning to work properly (Android requirement). Without this, the app cannot discover nearby users. + Checking location services... + unknown + + + get people around you to download bitchat and chat with them here! + + + usage: /join <channel> + started private chat with %1$s + user "%1$s" not found. they may be offline or using a different nickname. + no one else is around right now. + %1$s: %2$s + you must be in a channel to set a password. + you must be the channel creator to set a password. + password changed for channel %1$s + usage: /msg <nickname> [message] + usage: /pass <password> + no channels joined + joined channels: %1$s + unknown command: %1$s. type / to see available commands.