fix: add support for more keys for translations

This commit is contained in:
particle-box
2026-02-06 23:29:57 +05:30
parent 4692987baf
commit 3744c5b422
25 changed files with 156 additions and 129 deletions

View File

@@ -121,7 +121,7 @@ class BridgeService : Service() {
val pendingTask = remoteSideContext.taskManager.createPendingTask(
Task(
type = TaskType.DOWNLOAD,
title = "Media conversion",
title = remoteSideContext.translation["task_media_conversion_title"],
author = null,
hash = taskId
)

View File

@@ -120,7 +120,7 @@ class CallDownloadSessionImpl(
val pendingTask = context.taskManager.createPendingTask(
Task(
type = TaskType.DOWNLOAD,
title = "Call Recording $author",
title = context.translation.format("task_call_recording_title", "author" to author),
author = author,
hash = UUID.randomUUID().toString()
)
@@ -188,4 +188,4 @@ class CallDownloadSessionImpl(
context.log.verbose("ending call")
}
}
}
}

View File

@@ -132,16 +132,16 @@ class MainActivity : ComponentActivity() {
if (shouldShowAbiWarning) {
AestheticDialog(
onDismissRequest = {},
title = "Wrong APK installed",
title = managerContext.translation["wrong_apk_title"],
text = "",
icon = Icons.Filled.Warning,
confirmButtonText = "Close",
confirmButtonText = managerContext.translation["common.close"],
onConfirm = { (context as? Activity)?.finishAffinity() },
showCloseButton = false,
opaque = true,
customContent = {
Text(
text = "Your device is armv8, please download the armv8 apk, not armv7.",
text = managerContext.translation["wrong_apk_message"],
color = PurrfectPalette.textSecondary,
lineHeight = 18.sp
)

View File

@@ -143,7 +143,7 @@ class ManageReposSection: Routes.Route() {
addRepo(url)
}.onFailure {
context.log.error("Failed to add repository", it)
context.shortToast(translation.format("add_repo_failed", "message" to (it.message ?: "Unknown")))
context.shortToast(translation.format("add_repo_failed", "message" to (it.message ?: context.translation["common.unknown"])))
}
loading = false
}

View File

@@ -138,7 +138,7 @@ class ConfigExportSummaryScreen : Routes.Route() {
override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = {
val exportSensitiveData = it.arguments?.getString("exportSensitiveData")?.toBoolean() ?: false
val includeSavedLocations = it.arguments?.getString("includeSavedLocations")?.toBoolean() ?: false
val exportLabel = context.translation["manager.sections.features.export_option"] ?: "Export"
val exportLabel = context.translation["manager.sections.features.export_option"]
val parser = remember { ConfigParser() }
val savedLocations = remember {
if (includeSavedLocations) context.database.getLocationCoordinates() else null
@@ -196,14 +196,14 @@ class ConfigExportSummaryScreen : Routes.Route() {
tint = Color.White,
modifier = Modifier.padding(end = 6.dp)
)
Text(context.translation["common.back"] ?: "Back")
Text(context.translation["common.back"])
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = "Summary",
text = translation["title"],
color = Color.White,
fontWeight = FontWeight.ExtraBold,
fontSize = 18.sp

View File

@@ -232,7 +232,7 @@ class ConfigImportConfirmationScreen : Routes.Route() {
routes.configJsonForImport?.let { parser.parse(it) } ?: emptyMap()
}
val expandedState = remember { mutableStateMapOf<String, Boolean>() }
val importLabel = translation["confirm_button"] ?: "Import"
val importLabel = translation["confirm_button"]
Box(
modifier = Modifier
@@ -281,14 +281,14 @@ class ConfigImportConfirmationScreen : Routes.Route() {
tint = Color.White,
modifier = Modifier.padding(end = 6.dp)
)
Text(context.translation["common.back"] ?: "Back")
Text(context.translation["common.back"])
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = "Summary",
text = translation["title"],
color = Color.White,
fontWeight = FontWeight.ExtraBold,
fontSize = 18.sp
@@ -308,7 +308,7 @@ class ConfigImportConfirmationScreen : Routes.Route() {
context.longToast(
context.translation.format(
"config_import_failure_toast",
"error" to (err.message ?: "Unknown error")
"error" to (err.message ?: context.translation["common.unknown_error"])
)
)
}

View File

@@ -112,7 +112,7 @@ class FeaturesRootSection : Routes.Route() {
} ?: routeInfo.translatedKey?.value
}
SEARCH_FEATURE_ROUTE -> {
translation["search_button"] ?: "Search"
translation["search_button"]
}
else -> {
routeInfo.translatedKey?.value
@@ -668,7 +668,7 @@ class FeaturesRootSection : Routes.Route() {
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
) {
Text(
text = "$messageCount messages",
text = translation.format("search_results_count", "count" to messageCount.toString()),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.bodyMedium,
color = Color.White
@@ -1014,7 +1014,7 @@ class FeaturesRootSection : Routes.Route() {
},
placeholder = {
Text(
text = translation["search_button"] ?: "Search",
text = translation["search_button"],
color = Color(0xFFE0DCFF)
)
},
@@ -1109,7 +1109,7 @@ class FeaturesRootSection : Routes.Route() {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Include Saved Locations",
text = context.translation["include_saved_locations"],
style = MaterialTheme.typography.bodyMedium,
color = Color.White
)
@@ -1310,7 +1310,7 @@ class FeaturesRootSection : Routes.Route() {
val headerTitle = activeSectionTitle ?: translation["manager.routes.features"]
val subtitleText = when {
isSearchResults -> translation["search_button"] ?: "Search"
isSearchResults -> translation["search_button"]
!activeSectionSubtitle.isNullOrBlank() -> activeSectionSubtitle
else -> translation["manager.sections.features.subtitle"] ?: ""
}
@@ -1364,7 +1364,7 @@ class FeaturesRootSection : Routes.Route() {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = translation["button.back"] ?: "Back",
contentDescription = context.translation["common.back"],
tint = Color.White
)
}
@@ -1385,7 +1385,7 @@ class FeaturesRootSection : Routes.Route() {
.weight(1f)
.focusRequester(focusRequester),
singleLine = true,
placeholder = { Text(text = translation["search_button"] ?: "Search", color = Color(0xFFE0DCFF)) },
placeholder = { Text(text = translation["search_button"], color = Color(0xFFE0DCFF)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
@@ -1543,7 +1543,7 @@ class FeaturesRootSection : Routes.Route() {
) {
Icon(Icons.Filled.Delete, contentDescription = null, tint = Color.White.copy(alpha = 0.85f))
Spacer(Modifier.width(6.dp))
Text(text = translation["clear_history"] ?: "Clear history", color = Color.White)
Text(text = translation["clear_history"], color = Color.White)
}
}
}

View File

@@ -203,8 +203,8 @@ class ManageRuleFeature : Routes.Route() {
title = translation["clear_list_button"],
text = translation["dialog_clear_confirmation_text"],
icon = Icons.Default.DeleteSweep,
confirmButtonText = context.translation["clear"],
dismissButtonText = context.translation["button.cancel"],
confirmButtonText = translation["dialog_clear_confirm_button"],
dismissButtonText = translation["dialog_clear_cancel_button"],
onDismiss = { confirmationDialog = false },
onConfirm = {
context.database.clearRuleIds(currentRuleType.key)
@@ -390,7 +390,7 @@ class ManageRuleFeature : Routes.Route() {
contentColor = Color.White
)
) {
Text(text = context.translation["clear"])
Text(text = translation["dialog_clear_confirm_button"])
}
}
}

View File

@@ -315,7 +315,7 @@ class HomeLogs : Routes.Route() {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, tint = Color.White)
}
Text(
text = routeInfo.translatedKey?.value ?: translation["manager.routes.home_logs"] ?: "Logs",
text = routeInfo.translatedKey?.value ?: translation["manager.routes.home_logs"],
color = PurrfectPalette.textPrimary,
fontSize = 18.sp,
fontWeight = FontWeight.ExtraBold
@@ -412,7 +412,7 @@ class HomeLogs : Routes.Route() {
textAlign = TextAlign.Center
)
Text(
text = "Pull to refresh or trigger an action to see new entries.",
text = translation["refresh_hint"],
color = PurrfectPalette.textSecondary,
fontSize = 13.sp,
textAlign = TextAlign.Center,

View File

@@ -154,8 +154,8 @@ class HomeSettings : Routes.Route() {
var value by remember { mutableStateOf(sharedPreferences.getBoolean(realKey, defaultValue)) }
var showDisableDialog by remember { mutableStateOf(false) }
val hapticFeedback = LocalHapticFeedback.current
val positiveLabel = context.translation["button.positive"] ?: "Yes"
val negativeLabel = context.translation["button.negative"] ?: "No"
val positiveLabel = context.translation["button.positive"]
val negativeLabel = context.translation["button.negative"]
LaunchedEffect(realKey) {
if (!sharedPreferences.contains(realKey)) {
@@ -167,7 +167,7 @@ class HomeSettings : Routes.Route() {
if (showDisableDialog) {
AestheticDialog(
onDismissRequest = { showDisableDialog = false },
title = confirmDisableTitle ?: "Are you sure?",
title = confirmDisableTitle ?: translation["reset_setup_dialog_title"],
text = confirmDisableText.orEmpty(),
icon = Icons.Filled.Warning,
confirmButtonText = positiveLabel,
@@ -313,9 +313,9 @@ class HomeSettings : Routes.Route() {
val contextC = LocalContext.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val positiveLabel = context.translation["button.positive"] ?: "Yes"
val negativeLabel = context.translation["button.negative"] ?: "No"
val importLabel = context.translation["button.import"] ?: "Import"
val positiveLabel = context.translation["button.positive"]
val negativeLabel = context.translation["button.negative"]
val importLabel = context.translation["button.import"]
val sharedButtonColors = ButtonDefaults.buttonColors(
containerColor = Color.White.copy(alpha = 0.12f),
contentColor = Color.White

View File

@@ -127,7 +127,11 @@ class BetterLocationRoot : Routes.Route() {
overflow = TextOverflow.Ellipsis
)
Text(
text = "Lat ${friendLocation.latitude.toFloat()}, Lng ${friendLocation.longitude.toFloat()}",
text = context.translation.format(
"spoofed_coordinates_title",
"latitude" to friendLocation.latitude.toFloat().toString(),
"longitude" to friendLocation.longitude.toFloat().toString()
),
fontSize = 11.sp,
fontWeight = FontWeight.Light,
color = Color.White.copy(alpha = 0.8f)

View File

@@ -255,7 +255,7 @@ class ManageScriptReposSection : Routes.Route() {
}
}.onFailure {
context.log.error("Failed to add repository", it)
context.shortToast(translation.format("add_repo_failed_toast", "message" to (it.message ?: "Unknown")))
context.shortToast(translation.format("add_repo_failed_toast", "message" to (it.message ?: context.translation["common.unknown"])))
}
loading = false
}
@@ -292,7 +292,7 @@ class ManageScriptReposSection : Routes.Route() {
.background(PurrfectPalette.backgroundGradient)
) {
FloatingTopBar(
title = routeInfo.translatedKey?.value ?: (translation["title"] ?: "Repositories"),
title = routeInfo.translatedKey?.value ?: translation["title"],
onBack = { routes.navController.popBackStack() },
modifier = Modifier
.zIndex(2f)

View File

@@ -162,7 +162,7 @@ fun ScriptCatalog(root: ScriptingRootSection) {
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.shortToast(translation.format("error", "message" to (e.localizedMessage ?: "Unknown")))
context.shortToast(translation.format("error", "message" to (e.localizedMessage ?: context.translation["common.unknown"])))
}
}
}
@@ -238,7 +238,7 @@ fun ScriptCatalog(root: ScriptingRootSection) {
color = Color.White
)
Text(
text = translation["loading"] ?: "Loading...",
text = translation["loading"],
color = Color.White,
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp
@@ -348,7 +348,7 @@ fun ScriptCatalog(root: ScriptingRootSection) {
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
) {
Text(
text = translation.format("version", "version" to (entry.version ?: "N/A")),
text = translation.format("version", "version" to (entry.version ?: context.translation["common.not_available"])),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
fontWeight = FontWeight.Medium,
fontSize = 11.sp,

View File

@@ -140,7 +140,7 @@ class ScriptingRootSection : Routes.Route() {
return@launch
}.onFailure {
context.log.error("Failed to import script", it)
context.shortToast(translation.format("import_failed", "message" to (it.message ?: "Unknown")))
context.shortToast(translation.format("import_failed", "message" to (it.message ?: context.translation["common.unknown"])))
}
isLoading = false
}
@@ -431,7 +431,7 @@ class ScriptingRootSection : Routes.Route() {
@Composable
private fun SelectFolderButton(onClick: () -> Unit) {
val label = translation.getOrNull("select_folder_button") ?: "Select folder"
val label = translation["select_folder_button"]
Box(
modifier = Modifier
.fillMaxWidth()
@@ -439,13 +439,13 @@ class ScriptingRootSection : Routes.Route() {
contentAlignment = Alignment.Center
) {
Surface(
modifier = Modifier.size(68.dp),
modifier = Modifier.size(78.dp),
shape = CircleShape,
color = Color.White.copy(alpha = 0.1f),
color = PurrfectPalette.cardOverlayColor.copy(alpha = 0.9f),
tonalElevation = 0.dp,
shadowElevation = 10.dp,
shadowElevation = 14.dp,
border = BorderStroke(
1.dp,
1.5.dp,
Brush.linearGradient(
listOf(
PurrfectPalette.glowPrimary.copy(alpha = 0.7f),
@@ -456,17 +456,18 @@ class ScriptingRootSection : Routes.Route() {
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(6.dp)
.size(66.dp)
.clip(CircleShape)
.background(
Brush.radialGradient(
colors = listOf(
PurrfectPalette.glowPrimary.copy(alpha = 0.335f),
PurrfectPalette.glowSecondary.copy(alpha = 0.25f),
Color.Transparent
PurrfectPalette.glowPrimary.copy(alpha = 0.42f),
PurrfectPalette.glowSecondary.copy(alpha = 0.34f)
)
)
)
.border(1.dp, Color.White.copy(alpha = 0.14f), CircleShape)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
@@ -474,7 +475,7 @@ class ScriptingRootSection : Routes.Route() {
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = label,
tint = Color.White,
modifier = Modifier.size(32.dp)
modifier = Modifier.size(34.dp)
)
}
}
@@ -627,7 +628,7 @@ class ScriptingRootSection : Routes.Route() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(260.dp),
.heightIn(min = 260.dp),
contentAlignment = Alignment.Center
) {
Surface(
@@ -743,7 +744,7 @@ class ScriptingRootSection : Routes.Route() {
title = context.translation["manager.dialogs.scripting_warning.title"],
text = context.translation["manager.dialogs.scripting_warning.content"],
icon = Icons.Default.Warning,
confirmButtonText = translation["button.ok"] ?: "OK",
confirmButtonText = translation["button.ok"],
onConfirm = { if (timeout == 0) scriptingWarning = false },
loading = timeout > 0,
showCloseButton = false,
@@ -875,7 +876,7 @@ class ScriptingRootSection : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = translation["manager.routes.scripts"] ?: "Scripts",
text = translation["manager.routes.scripts"],
color = Color.White,
fontWeight = FontWeight.ExtraBold,
fontSize = 18.sp

View File

@@ -375,8 +375,7 @@ class AddFriendDialog(
enabled = !allFriendsSelected
) {
Text(
text = context.translation["manager.dialogs.messaging_action.select_all_button"]
?: "Select All",
text = context.translation["manager.dialogs.messaging_action.select_all_button"],
color = if (allFriendsSelected) {
Color.White.copy(alpha = 0.45f)
} else {

View File

@@ -91,7 +91,7 @@ class ManageScope: Routes.Route() {
title = translation.format("delete_scope_confirm_dialog_title", "scope" to context.translation["scopes.${scope.key}"]),
text = "",
icon = Icons.Rounded.DeleteForever,
confirmButtonText = translation["delete_button"] ?: "Delete",
confirmButtonText = translation["delete_button"],
dismissButtonText = context.translation["button.cancel"],
onDismiss = { deleteConfirmDialog = false },
onConfirm = {
@@ -109,7 +109,7 @@ class ManageScope: Routes.Route() {
.background(PurrfectPalette.backgroundGradient)
) {
FloatingTopBar(
title = titleText ?: "Manage",
title = titleText ?: translation["manage_scope_title"],
onBack = { routes.navController.popBackStack() },
actions = {
IconButton(onClick = { deleteConfirmDialog = true }) {

View File

@@ -63,7 +63,7 @@ import me.eternal.purrfectsnap.ui.util.Dialog
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
class MessagingPreview: Routes.Route() {
override val translation by lazy { context.translation.getCategory("manager.sections.social.messaging_preview.messaging_preview") }
override val translation by lazy { context.translation.getCategory("manager.sections.social.messaging_preview") }
private lateinit var coroutineScope: CoroutineScope
private lateinit var previewScrollState: LazyListState
@@ -79,21 +79,6 @@ class MessagingPreview: Routes.Route() {
else selectedMessages.add(messageId)
}
private fun tr(key: String, fallback: String? = null): String {
fun normalizeCandidate(candidate: String?): String? {
if (candidate.isNullOrBlank()) return null
if (candidate == key) return null
if (candidate.endsWith(".$key")) return null
return candidate
}
return normalizeCandidate(translation.getOrNull(key))
?: normalizeCandidate(context.translation.getOrNull("manager.sections.social.messaging_preview.$key"))
?: normalizeCandidate(context.translation.getOrNull("manager.social.messaging_preview.$key"))
?: fallback
?: key
}
@Composable
private fun ActionsSheetItem(
title: String,
@@ -338,20 +323,20 @@ class MessagingPreview: Routes.Route() {
val senderDisplayName by rememberAsyncMutableState<String?>(null, keys = arrayOf(senderId, myUserId, scope.key, scopeId, friendDisplayName)) {
when {
senderId == null -> "Unknown"
senderId == myUserId -> "You"
senderId == null -> translation["sender_unknown"]
senderId == myUserId -> translation["sender_you"]
scope == SocialScope.FRIEND -> friendDisplayName
?: context.database.getFriendInfo(scopeId)?.displayName
?: context.database.getFriendInfo(scopeId)?.mutableUsername
?: "Friend"
?: translation["sender_friend"]
else -> context.database.getFriendInfo(senderId)?.displayName
?: context.database.getFriendInfo(senderId)?.mutableUsername
?: "Unknown"
?: translation["sender_unknown"]
}
}
val contentTypeLabel = remember(contentType) {
contentType?.let { contentTypeTranslation.getOrNull(it.name) ?: it.name } ?: "Unknown"
contentType?.let { contentTypeTranslation.getOrNull(it.name) ?: it.name } ?: translation["sender_unknown"]
}
val bodyText = remember(message.contentType) { messageReader.getString(2, 1)?.trim().orEmpty() }
@@ -408,7 +393,7 @@ class MessagingPreview: Routes.Route() {
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start
) {
Text(
text = senderDisplayName ?: "Unknown",
text = senderDisplayName ?: translation["sender_unknown"],
color = Color.White.copy(alpha = 0.82f),
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
@@ -487,7 +472,7 @@ class MessagingPreview: Routes.Route() {
.padding(40.dp),
horizontalArrangement = Arrangement.Center
) {
Text(tr("no_message_hint", "No messages"), color = PurrfectPalette.textSecondary)
Text(translation["no_message_hint"], color = PurrfectPalette.textSecondary)
}
}
Spacer(modifier = Modifier.height(20.dp))
@@ -564,7 +549,7 @@ class MessagingPreview: Routes.Route() {
translation.format("processed_message_toast", "count" to processMessageCount.intValue.toString())
} ?: translation.getOrNull("processed_messages_toast")?.let {
translation.format("processed_messages_toast", "count" to processMessageCount.intValue.toString())
} ?: "Processed ${processMessageCount.intValue} messages"
} ?: translation.format("processed_messages_toast", "count" to processMessageCount.intValue.toString())
context.longToast(toastText)
}
}
@@ -579,7 +564,7 @@ class MessagingPreview: Routes.Route() {
context.longToast(
translation.getOrNull("bridge_connection_error")
?: translation.getOrNull("bridge_connection_failed")
?: "Failed to connect to bridge"
?: translation["bridge_connection_error"]
)
return
}
@@ -640,7 +625,7 @@ class MessagingPreview: Routes.Route() {
) {
val processedText = translation.getOrNull("processed_messages_text")?.let {
translation.format("processed_messages_text", "count" to processMessageCount.intValue.toString())
} ?: "Processed ${processMessageCount.intValue}"
} ?: translation.format("processed_messages_text", "count" to processMessageCount.intValue.toString())
Text(processedText)
if (activeTask?.hasFixedGoal() == true) {
LinearProgressIndicator(
@@ -679,7 +664,7 @@ class MessagingPreview: Routes.Route() {
}.onFailure {
context.log.error("Failed to fetch messages", it)
context.shortToast(
translation.getOrNull("message_fetch_failed") ?: "Failed to fetch messages"
translation.getOrNull("message_fetch_failed") ?: translation["message_fetch_failed"]
)
}
}
@@ -710,7 +695,7 @@ class MessagingPreview: Routes.Route() {
fetchNewMessages()
}.onFailure {
context.longToast(
translation.getOrNull("bridge_init_failed") ?: "Failed to initialize messaging bridge"
translation.getOrNull("bridge_init_failed") ?: translation["bridge_init_failed"]
)
context.log.error("Failed to initialize messaging bridge", it)
}
@@ -751,11 +736,11 @@ class MessagingPreview: Routes.Route() {
.background(PurrfectPalette.backgroundGradient)
) {
FloatingTopBar(
title = titleText ?: translation["title"] ?: "Preview",
title = titleText ?: translation["title"],
subtitle = if (selectedMessages.isNotEmpty()) {
"${selectedMessages.size} selected"
} else {
tr("subtitle", "Hold to select")
translation["subtitle"]
.substringBefore("")
.substringBefore("·")
.substringBefore("|")
@@ -798,7 +783,7 @@ class MessagingPreview: Routes.Route() {
Text(
translation.getOrNull("bridge_connection_error")
?: translation.getOrNull("bridge_connection_failed")
?: "Failed to connect to bridge",
?: translation["bridge_connection_error"],
modifier = Modifier.padding(16.dp),
color = Color.White
)
@@ -831,7 +816,7 @@ class MessagingPreview: Routes.Route() {
val selectionSubtitle = if (hasSelection) {
"${selectedMessages.size} selected"
} else {
"Choose message types"
translation["choose_message_types_subtitle"]
}
Column(
@@ -862,7 +847,7 @@ class MessagingPreview: Routes.Route() {
)
Spacer(Modifier.height(14.dp))
Text(
text = tr("actions_title", "Conversation Actions"),
text = translation["actions_title"],
color = Color.White,
fontWeight = FontWeight.ExtraBold,
fontSize = 18.sp,
@@ -886,31 +871,31 @@ class MessagingPreview: Routes.Route() {
val deleteKey = if (hasSelection) "delete_selection_option" else "delete_all_option"
ActionsSheetItem(
title = tr(saveKey, if (hasSelection) "Save Selection" else "Save All"),
subtitle = if (hasSelection) "Save selected messages" else "Save by content type",
title = translation[saveKey],
subtitle = if (hasSelection) translation["save_selected_messages_subtitle"] else translation["save_by_content_type_subtitle"],
icon = Icons.Rounded.BookmarkAdded
) {
launchMessagingTask(MessagingTaskType.SAVE)
if (hasSelection) runCurrentTask() else selectConstraintsDialog = true
}
ActionsSheetItem(
title = tr(unsaveKey, if (hasSelection) "Unsave Selection" else "Unsave All"),
subtitle = if (hasSelection) "Unsave selected messages" else "Unsave by content type",
title = translation[unsaveKey],
subtitle = if (hasSelection) translation["unsave_selected_messages_subtitle"] else translation["unsave_by_content_type_subtitle"],
icon = Icons.Rounded.BookmarkBorder
) {
launchMessagingTask(MessagingTaskType.UNSAVE)
if (hasSelection) runCurrentTask() else selectConstraintsDialog = true
}
ActionsSheetItem(
title = tr(markKey, if (hasSelection) "Mark selected as seen" else "Mark all as seen"),
subtitle = "Marks snaps as seen",
title = translation[markKey],
subtitle = translation["mark_as_seen_subtitle"],
icon = Icons.Rounded.RemoveRedEye
) {
if (messagingBridge == null) {
context.longToast(
translation.getOrNull("bridge_connection_error")
?: translation.getOrNull("bridge_connection_failed")
?: "Failed to connect to bridge"
?: translation["bridge_connection_error"]
)
return@ActionsSheetItem
}
@@ -924,8 +909,8 @@ class MessagingPreview: Routes.Route() {
runCurrentTask()
}
ActionsSheetItem(
title = tr(deleteKey, if (hasSelection) "Delete Selection" else "Delete All"),
subtitle = if (hasSelection) "Delete selected messages" else "Delete by content type",
title = translation[deleteKey],
subtitle = if (hasSelection) translation["delete_selected_messages_subtitle"] else translation["delete_by_content_type_subtitle"],
icon = Icons.Rounded.DeleteForever,
danger = true
) {
@@ -933,7 +918,7 @@ class MessagingPreview: Routes.Route() {
context.longToast(
translation.getOrNull("bridge_connection_error")
?: translation.getOrNull("bridge_connection_failed")
?: "Failed to connect to bridge"
?: translation["bridge_connection_error"]
)
return@ActionsSheetItem
}

View File

@@ -237,7 +237,7 @@ class SocialRootSection : Routes.Route() {
}
)
if (searchActive) {
val searchHint = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search"
val searchHint = context.translation["manager.dialogs.add_friend.search_hint"]
val searchShape = RoundedCornerShape(18.dp)
val searchBorder = Brush.linearGradient(
listOf(
@@ -520,7 +520,7 @@ class SocialRootSection : Routes.Route() {
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = translation["manager.routes.social"] ?: "Social",
text = translation["manager.routes.social"],
color = Color.White,
fontWeight = FontWeight.ExtraBold,
fontSize = 18.sp
@@ -631,7 +631,7 @@ class SocialRootSection : Routes.Route() {
fontSize = 15.sp
)
Text(
text = translation["social_empty_hint"] ?: "Tap the + button to sync friends or groups.",
text = translation["social_empty_hint"],
color = PurrfectPalette.textSecondary,
fontSize = 12.sp
)

View File

@@ -148,7 +148,7 @@ class FriendTrackerCatalog : Routes.Route() {
contentColor = Color.White
)
) {
Text(translation["manage_repos_description"] ?: (context.translation["manager.routes.manage_friend_tracker_repos"] ?: "Manage repositories"))
Text(translation["manage_repos_description"] ?: context.translation["manager.routes.manage_friend_tracker_repos"])
}
}
)
@@ -176,7 +176,7 @@ class FriendTrackerCatalog : Routes.Route() {
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
context.shortToast(translation.format("error", "message" to (e.localizedMessage ?: "Unknown")))
context.shortToast(translation.format("error", "message" to (e.localizedMessage ?: context.translation["common.unknown"])))
}
}
}
@@ -341,7 +341,7 @@ class FriendTrackerCatalog : Routes.Route() {
.background(PurrfectPalette.backgroundGradient)
) {
FloatingTopBar(
title = translation["title"] ?: (routeInfo.translatedKey?.value ?: "Catalog"),
title = translation["title"],
onBack = { routes.navController.popBackStack() },
actions = {
IconButton(onClick = { routes.manageFriendTrackerRepos.navigate() }) {

View File

@@ -115,7 +115,7 @@ class FriendTrackerConfigExportScreen : Routes.Route() {
}
}
}.onFailure {
context.longToast(translation.format("export_failed_toast", "message" to (it.message ?: "Unknown")))
context.longToast(translation.format("export_failed_toast", "message" to (it.message ?: context.translation["common.unknown"])))
}
}
}) {

View File

@@ -78,7 +78,7 @@ class FriendTrackerConfigImportScreen : Routes.Route() {
routes.onRuleImported?.invoke()
routes.navController.popBackStack()
}.onFailure {
context.longToast(translation.format("import_failed_toast", "message" to (it.message ?: "Unknown")))
context.longToast(translation.format("import_failed_toast", "message" to (it.message ?: context.translation["common.unknown"])))
}
}) {
Text(translation["confirm_button"])

View File

@@ -245,7 +245,7 @@ class ManageFriendTrackerReposSection: Routes.Route() {
}
}.onFailure {
context.log.error("Failed to add repository", it)
context.shortToast(translation.format("add_repo_failed_toast", "message" to (it.message ?: "Unknown")))
context.shortToast(translation.format("add_repo_failed_toast", "message" to (it.message ?: context.translation["common.unknown"])))
}
loading = false
}
@@ -282,7 +282,7 @@ class ManageFriendTrackerReposSection: Routes.Route() {
.background(PurrfectPalette.backgroundGradient)
) {
FloatingTopBar(
title = routeInfo.translatedKey?.value ?: (translation["title"] ?: "Repositories"),
title = routeInfo.translatedKey?.value ?: translation["title"],
onBack = { routes.navController.popBackStack() },
modifier = Modifier
.zIndex(2f)

View File

@@ -16,10 +16,14 @@ class TrackerConfigParser(private val context: Routes.Route) {
fun parse(configJson: String): Map<String, List<ImportedFeature>> {
val featureMap = mutableMapOf<String, MutableList<ImportedFeature>>()
val exportedData = context.context.gson.fromJson(configJson, ExportedTrackerData::class.java)
val authorLabel = context.translation["tracker_author_label"]
val enabledLabel = context.translation["tracker_enabled_label"]
val enabledValue = context.translation["tracker_enabled_value"]
val disabledValue = context.translation["tracker_disabled_value"]
exportedData.rules.forEach { rule ->
val features = mutableListOf<ImportedFeature>()
features.add(ImportedFeature(rule.name, "Author", "author", rule.author ?: "Unknown", 0))
features.add(ImportedFeature(rule.name, "Enabled", "enabled", rule.enabled, 0))
features.add(ImportedFeature(rule.name, authorLabel, "author", rule.author ?: context.context.translation["common.unknown"], 0))
features.add(ImportedFeature(rule.name, enabledLabel, "enabled", if (rule.enabled) enabledValue else disabledValue, 0))
rule.events?.forEach { event ->
features.add(ImportedFeature(rule.name, context.context.translation["tracker_events.${event.eventType}"], event.eventType, event.actions.joinToString(", ") { context.context.translation["tracker_actions.${it.key}"] }, 1))
}
@@ -30,7 +34,7 @@ class TrackerConfigParser(private val context: Routes.Route) {
fun parseValue(featureKey: String, value: Any): Any {
return when (value) {
is Boolean -> if (value) "Enabled" else "Disabled"
is Boolean -> if (value) context.translation["tracker_enabled_value"] else context.translation["tracker_disabled_value"]
is JSONArray -> {
val list = mutableListOf<String>()
for (i in 0 until value.length()) {

View File

@@ -499,7 +499,7 @@ class AlertDialogs(
currentColor = null
}
},
label = { Text(text = "Hex Color") },
label = { Text(text = translation["dialogs.hex_color_label"]) },
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
@@ -816,7 +816,7 @@ class AlertDialogs(
color = Color.White
)
Text(
text = "Search or tap on the map",
text = betterLocationTranslation["search_or_tap_map_hint"],
fontSize = 12.sp,
color = PurrfectPalette.textSecondary
)
@@ -870,11 +870,11 @@ class AlertDialogs(
search()
}
},
placeholder = { Text(text = "Search location...") },
placeholder = { Text(text = betterLocationTranslation["search_location_placeholder"]) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search",
contentDescription = betterLocationTranslation["search_icon_description"],
tint = Color.White
)
},
@@ -994,7 +994,7 @@ class AlertDialogs(
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Searching...",
text = betterLocationTranslation["searching_label"],
style = MaterialTheme.typography.bodyMedium,
color = Color.White
)
@@ -1142,7 +1142,7 @@ class AlertDialogs(
.padding(horizontal = 6.dp),
value = lat.value,
onValueChange = { lat.value = it },
label = { Text(text = "Latitude") },
label = { Text(text = translation["latitude_dialog_hint"]) },
leadingIcon = { Icon(Icons.Filled.MyLocation, contentDescription = null) },
singleLine = true,
shape = RoundedCornerShape(14.dp),
@@ -1162,7 +1162,7 @@ class AlertDialogs(
.padding(horizontal = 6.dp),
value = lon.value,
onValueChange = { lon.value = it },
label = { Text(text = "Longitude") },
label = { Text(text = translation["longitude_dialog_hint"]) },
leadingIcon = { Icon(Icons.Filled.Navigation, contentDescription = null) },
singleLine = true,
shape = RoundedCornerShape(14.dp),

View File

@@ -28,7 +28,8 @@
"dialogs": {
"select_language": "Select Language",
"save_folder": "Choose where to save downloads",
"select_save_folder_button": "Select Folder"
"select_save_folder_button": "Select Folder",
"hex_color_label": "Hex Color"
},
"install_mode": {
"confirm_timeout": "I understand ({seconds}s)",
@@ -196,7 +197,7 @@
},
"sections": {
"home": {
"version_title": "v{versionName} \u00b7 by Eternal",
"version_title": "v{versionName} \u00b7 by ΞTΞRNAL",
"update_title": "PurrfectSnap Update",
"update_content": "Version {version} is available!",
"update_button": "Download",
@@ -246,7 +247,7 @@
"about_tagline": "An Xposed Module meant to enhance your Snapchat experience!",
"about_lead_developers_title": "Lead Developers",
"about_story_title": "Our Story",
"about_story": "PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by IzTIzRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer <RSR/> joined the team, and this app soon became a huge success. We received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place. We would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him. Lastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, SUJI>L, Zain & scrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.",
"about_story": "PurrfectSnap was founded on 2nd of October, 2025, as a fork of SnapEnhance by ΞTΞRNAL with a vision to provide users the quality Snapchat experience they deserve. This app was just meant to be a minor update in the SnapEnhance repository, but it soon became a separate app wherein the contributors kept adding features. Then the developer <RSR/> joined the team, and this app soon became a huge success. We received much love and support and gained 1K+ downloads in just two days! We thank all users and contributors; without your support, we wouldn't have reached this place. We would also like to convey our huge thanks to rhunk, the lead developer of SnapEnhance, as without him, this app wouldn't even exist. We are immensely grateful to him. Lastly, we would like to thank all our admins, notably: CLASSIC GENIUS, Harry, Sujal, Zain & schrodingerspet, who were right there with us from the very beginning. We would also like to thank all testers, notably Leo & Toxic, who tested and reported bugs continuously. We are immensely grateful for your contribution.",
"about_thanks_title": "With love, PurrfectSnap Team",
"about_magic_toast": "Tap 5 times in this screen to see some magic 😉!",
"github_button": "GitHub",
@@ -254,6 +255,7 @@
},
"home_logs": {
"no_logs_hint": "No logs available",
"refresh_hint": "Pull to refresh or trigger an action to see new entries.",
"clear_logs_button": "Clear Logs",
"export_logs_button": "Export Logs",
"saving_logs_toast": "Saving logs, this may take a while ...",
@@ -341,7 +343,7 @@
"remove_selected_tasks_confirm": "Remove {count} tasks?",
"remove_all_tasks_confirm": "Remove all tasks?"
},
"features": {
"features": {
"disabled": "Disabled",
"export_option": "Export",
"import_option": "Import",
@@ -353,10 +355,11 @@
"saved_config_snackbar": "Config saved",
"older_required": "This feature requires Snapchat v{version} or older to work correctly",
"newer_required": "This feature requires Snapchat v{version} or newer to work correctly",
"search_button": "Search",
"clear_history": "Clear search history",
"subtitle": "Search and manage features"
},
"search_button": "Search",
"search_results_count": "{count} messages",
"clear_history": "Clear search history",
"subtitle": "Search and manage features"
},
"bypass_status": {
"active": "PurrAura Active",
"inactive": "PurrAura Inactive"
@@ -371,7 +374,9 @@
"blacklist_state_subtext": "Everyone except {count} friends/groups will be affected by this rule",
"blacklist_state_button": "Select excluded friends/groups",
"clear_list_button": "Clear friends/groups list",
"dialog_clear_confirmation_text": "Are you sure you want to clear the list?"
"dialog_clear_confirmation_text": "Are you sure you want to clear the list?",
"dialog_clear_confirm_button": "Clear",
"dialog_clear_cancel_button": "Cancel"
},
"social": {
"friends_tab": "Friends",
@@ -387,6 +392,7 @@
"social_empty_hint": "Tap the + button to sync friends or groups."
},
"manage_scope": {
"manage_scope_title": "Manage",
"logged_stories_button": "Show Logged Stories",
"e2ee_title": "End-to-End Encryption",
"e2ee_subtitle": "Manage your shared key for this friend.",
@@ -417,16 +423,27 @@
"bridge_init_failed": "Failed to initialize messaging bridge. Make sure Snapchat is running in the background",
"message_fetch_failed": "Failed to fetch messages",
"no_message_hint": "No message",
"sender_unknown": "Unknown",
"sender_you": "You",
"sender_friend": "Friend",
"subtitle": "Hold to select",
"actions_title": "Conversation Actions",
"choose_message_types_subtitle": "Choose message types",
"save_selection_option": "Save Selection",
"save_all_option": "Save All",
"save_selected_messages_subtitle": "Save selected messages",
"save_by_content_type_subtitle": "Save by content type",
"unsave_selection_option": "Unsave Selection",
"unsave_all_option": "Unsave All",
"unsave_selected_messages_subtitle": "Unsave selected messages",
"unsave_by_content_type_subtitle": "Unsave by content type",
"mark_selection_as_seen_option": "Mark selected Snap as seen",
"mark_all_as_seen_option": "Mark all Snaps as seen",
"mark_as_seen_subtitle": "Marks snaps as seen",
"delete_selection_option": "Delete Selection",
"delete_all_option": "Delete All",
"delete_selected_messages_subtitle": "Delete selected messages",
"delete_by_content_type_subtitle": "Delete by content type",
"processed_message_toast": "Processed {count} messages",
"processed_messages_toast": "Processed {count} messages",
"processed_messages_text": "Processed {count}",
@@ -459,6 +476,10 @@
"longitude_dialog_hint": "Longitude",
"save_dialog_button": "Save",
"choose_location_button": "Pick a Location",
"search_or_tap_map_hint": "Search or tap on the map",
"search_location_placeholder": "Search location...",
"search_icon_description": "Search",
"searching_label": "Searching...",
"manual_coordinates_hint": "Fine-tune the coordinates manually.",
"saved_coordinates_subtitle": "Manage your saved spoof locations",
"teleport_to_friend_button": "Teleport to Friend",
@@ -660,6 +681,10 @@
"save_button": "Save",
"back_button_description": "Go back",
"expand_button_description": "Expand or collapse category",
"tracker_author_label": "Author",
"tracker_enabled_label": "Enabled",
"tracker_enabled_value": "Enabled",
"tracker_disabled_value": "Disabled",
"exported_toast": "Tracker configuration exported",
"export_failed_toast": "Failed to export tracker: {message}"
},
@@ -668,6 +693,10 @@
"confirm_button": "Import",
"back_button_description": "Go back",
"expand_button_description": "Expand or collapse category",
"tracker_author_label": "Author",
"tracker_enabled_label": "Enabled",
"tracker_enabled_value": "Enabled",
"tracker_disabled_value": "Disabled",
"imported_toast": "Tracker imported",
"import_failed_toast": "Failed to import tracker: {message}"
},
@@ -3444,17 +3473,22 @@
},
"common": {
"cancel": "Cancel",
"close": "Close",
"add": "Add",
"ok": "OK",
"quit": "Quit",
"done": "Done",
"back": "Back",
"unknown": "Unknown",
"unknown_error": "Unknown error",
"not_available": "N/A",
"added": "Added",
"no_friends_found": "No friends found",
"exporting_memories": "Exporting memories... ({failed} failed)"
},
"clear_friend_feed": "Clear Friend Feed",
"task_media_conversion_title": "Media conversion",
"task_call_recording_title": "Call Recording {author}",
"select_date": "Select Date",
"schedule_scheduled_for": "Scheduled for {name} in {time}",
"schedule_sending_in": "Sending in {time}",