Localize key ChatScreen labels and media badges
Some checks failed
Android CI / android (push) Has started running
CI / test (push) Has been cancelled
Android Release / release (push) Has started running

This commit is contained in:
2026-03-11 21:45:48 +03:00
parent e5e4fd653e
commit d649cf1cb4
3 changed files with 78 additions and 20 deletions

View File

@@ -386,7 +386,8 @@ fun ChatScreen(
val senderNameByUserId = remember(state.messages, state.chatMembers) { val senderNameByUserId = remember(state.messages, state.chatMembers) {
val fromMembers = state.chatMembers.associate { member -> val fromMembers = state.chatMembers.associate { member ->
val resolved = member.name.ifBlank { val resolved = member.name.ifBlank {
member.username?.takeIf { it.isNotBlank() }?.let { "@$it" } ?: "User #${member.userId}" member.username?.takeIf { it.isNotBlank() }?.let { "@$it" }
?: context.getString(R.string.chat_user_fallback_with_id, member.userId)
} }
member.userId to resolved member.userId to resolved
} }
@@ -694,7 +695,9 @@ fun ChatScreen(
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" }, text = state.chatTitle.ifBlank {
stringResource(id = R.string.chat_title_fallback, state.chatId)
},
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
@@ -1287,7 +1290,9 @@ fun ChatScreen(
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = state.chatTitle.ifBlank { "Chat #${state.chatId}" }, text = state.chatTitle.ifBlank {
stringResource(id = R.string.chat_title_fallback, state.chatId)
},
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
@@ -1977,7 +1982,7 @@ fun ChatScreen(
.clickable { emojiPickerTab = tab }, .clickable { emojiPickerTab = tab },
) { ) {
Text( Text(
text = tab.title, text = stringResource(id = tab.titleRes),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
) )
@@ -2170,6 +2175,7 @@ private fun MessageBubble(
onClick: () -> Unit, onClick: () -> Unit,
onLongPress: () -> Unit, onLongPress: () -> Unit,
) { ) {
val context = LocalContext.current
val isOutgoing = message.isOutgoing val isOutgoing = message.isOutgoing
val renderAsChannelPost = isChannelChat val renderAsChannelPost = isChannelChat
val alignAsOutgoing = isOutgoing && !renderAsChannelPost val alignAsOutgoing = isOutgoing && !renderAsChannelPost
@@ -2360,7 +2366,11 @@ private fun MessageBubble(
if (isLegacyImageUrlMessage) { if (isLegacyImageUrlMessage) {
val isLegacyStickerMessage = isStickerAsset(fileUrl = textUrl, fileType = "image/url") val isLegacyStickerMessage = isStickerAsset(fileUrl = textUrl, fileType = "image/url")
val badgeLabel = if (isLegacyStickerMessage) null else mediaBadgeLabel(fileType = "image/url", url = textUrl) val badgeLabel = if (isLegacyStickerMessage) {
null
} else {
mediaBadgeLabel(fileType = "image/url", url = textUrl, context = context)
}
val openable = !isLegacyStickerMessage && badgeLabel == null val openable = !isLegacyStickerMessage && badgeLabel == null
Box( Box(
modifier = Modifier modifier = Modifier
@@ -2434,7 +2444,11 @@ private fun MessageBubble(
if (imageAttachments.size == 1) { if (imageAttachments.size == 1) {
val single = imageAttachments.first() val single = imageAttachments.first()
val isStickerImage = isStickerAsset(fileUrl = single.fileUrl, fileType = single.fileType) val isStickerImage = isStickerAsset(fileUrl = single.fileUrl, fileType = single.fileType)
val badgeLabel = if (isStickerImage) null else mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl) val badgeLabel = if (isStickerImage) {
null
} else {
mediaBadgeLabel(fileType = single.fileType, url = single.fileUrl, context = context)
}
val openable = !isStickerImage && badgeLabel == null val openable = !isStickerImage && badgeLabel == null
Box( Box(
modifier = Modifier modifier = Modifier
@@ -2475,7 +2489,11 @@ private fun MessageBubble(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
rowItems.forEach { image -> rowItems.forEach { image ->
val badgeLabel = mediaBadgeLabel(fileType = image.fileType, url = image.fileUrl) val badgeLabel = mediaBadgeLabel(
fileType = image.fileType,
url = image.fileUrl,
context = context,
)
val openable = badgeLabel == null val openable = badgeLabel == null
Box( Box(
modifier = Modifier modifier = Modifier
@@ -2533,9 +2551,15 @@ private fun MessageBubble(
playbackTitle = message.senderDisplayName?.ifBlank { null } playbackTitle = message.senderDisplayName?.ifBlank { null }
?: extractFileName(attachment.fileUrl), ?: extractFileName(attachment.fileUrl),
playbackSubtitle = if (message.type.contains("voice", ignoreCase = true)) { playbackSubtitle = if (message.type.contains("voice", ignoreCase = true)) {
"Voice message • ${formatMessageTime(message.createdAt)}" stringResource(
id = R.string.chat_playback_subtitle_voice,
formatMessageTime(message.createdAt),
)
} else { } else {
"Audio • ${formatMessageTime(message.createdAt)}" stringResource(
id = R.string.chat_playback_subtitle_audio,
formatMessageTime(message.createdAt),
)
}, },
messageId = message.id, messageId = message.id,
onPlaybackChanged = onAudioPlaybackChanged, onPlaybackChanged = onAudioPlaybackChanged,
@@ -2741,10 +2765,10 @@ private sealed interface ChatTimelineItem {
) : ChatTimelineItem ) : ChatTimelineItem
} }
private enum class ComposerPickerTab(val title: String) { private enum class ComposerPickerTab(@StringRes val titleRes: Int) {
Emoji("Эмодзи"), Emoji(R.string.chat_picker_tab_emoji),
Gif("GIF"), Gif(R.string.chat_picker_tab_gif),
Sticker("Стикеры"), Sticker(R.string.chat_picker_tab_stickers),
} }
private data class RemotePickerItem( private data class RemotePickerItem(
@@ -3882,12 +3906,19 @@ private fun ChatMembersTabContent(
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = member.name.ifBlank { member.username?.let { "@$it" } ?: "User #${member.userId}" }, text = member.name.ifBlank {
member.username?.let { "@$it" }
?: stringResource(id = R.string.chat_user_fallback_with_id, member.userId)
},
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
Text( Text(
text = "${member.role} • id ${member.userId}", text = stringResource(
id = R.string.chat_member_role_with_id,
member.role,
member.userId,
),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -3993,12 +4024,15 @@ private fun ChatMembersTabContent(
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = ban.name.ifBlank { ban.username?.let { "@$it" } ?: "User #${ban.userId}" }, text = ban.name.ifBlank {
ban.username?.let { "@$it" }
?: stringResource(id = R.string.chat_user_fallback_with_id, ban.userId)
},
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
Text( Text(
text = "id ${ban.userId}", text = stringResource(id = R.string.chat_member_id, ban.userId),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -4115,11 +4149,15 @@ private fun isStickerAsset(fileUrl: String, fileType: String): Boolean {
return isStickerLikeUrl(fileUrl) return isStickerLikeUrl(fileUrl)
} }
private fun mediaBadgeLabel(fileType: String, url: String): String? { private fun mediaBadgeLabel(
fileType: String,
url: String,
context: Context,
): String? {
val normalizedType = fileType.lowercase(Locale.getDefault()) val normalizedType = fileType.lowercase(Locale.getDefault())
return when { return when {
normalizedType.contains("gif") || isGifLikeUrl(url) -> "GIF" normalizedType.contains("gif") || isGifLikeUrl(url) -> context.getString(R.string.chat_media_badge_gif)
normalizedType.contains("webp") || isStickerLikeUrl(url) -> "Sticker" normalizedType.contains("webp") || isStickerLikeUrl(url) -> context.getString(R.string.chat_media_badge_sticker)
else -> null else -> null
} }
} }

View File

@@ -147,6 +147,16 @@
<string name="chat_member_action_kick">Кикнуть</string> <string name="chat_member_action_kick">Кикнуть</string>
<string name="chat_member_action_unban">Разбанить</string> <string name="chat_member_action_unban">Разбанить</string>
<string name="chat_media_badge_video">Видео</string> <string name="chat_media_badge_video">Видео</string>
<string name="chat_media_badge_gif">GIF</string>
<string name="chat_media_badge_sticker">Стикер</string>
<string name="chat_picker_tab_emoji">Эмодзи</string>
<string name="chat_picker_tab_gif">GIF</string>
<string name="chat_picker_tab_stickers">Стикеры</string>
<string name="chat_playback_subtitle_voice">Голосовое сообщение • %1$s</string>
<string name="chat_playback_subtitle_audio">Аудио • %1$s</string>
<string name="chat_user_fallback_with_id">Пользователь #%1$d</string>
<string name="chat_member_role_with_id">%1$s • id %2$d</string>
<string name="chat_member_id">id %1$d</string>
<string name="chat_member_dialog_demote_title">Понизить админа</string> <string name="chat_member_dialog_demote_title">Понизить админа</string>
<string name="chat_member_dialog_demote_body">Понизить %1$s до участника?</string> <string name="chat_member_dialog_demote_body">Понизить %1$s до участника?</string>
<string name="chat_member_dialog_transfer_title">Передача owner</string> <string name="chat_member_dialog_transfer_title">Передача owner</string>

View File

@@ -147,6 +147,16 @@
<string name="chat_member_action_kick">Kick</string> <string name="chat_member_action_kick">Kick</string>
<string name="chat_member_action_unban">Unban</string> <string name="chat_member_action_unban">Unban</string>
<string name="chat_media_badge_video">Video</string> <string name="chat_media_badge_video">Video</string>
<string name="chat_media_badge_gif">GIF</string>
<string name="chat_media_badge_sticker">Sticker</string>
<string name="chat_picker_tab_emoji">Emoji</string>
<string name="chat_picker_tab_gif">GIF</string>
<string name="chat_picker_tab_stickers">Stickers</string>
<string name="chat_playback_subtitle_voice">Voice message • %1$s</string>
<string name="chat_playback_subtitle_audio">Audio • %1$s</string>
<string name="chat_user_fallback_with_id">User #%1$d</string>
<string name="chat_member_role_with_id">%1$s • id %2$d</string>
<string name="chat_member_id">id %1$d</string>
<string name="chat_member_dialog_demote_title">Demote admin</string> <string name="chat_member_dialog_demote_title">Demote admin</string>
<string name="chat_member_dialog_demote_body">Demote %1$s to member?</string> <string name="chat_member_dialog_demote_body">Demote %1$s to member?</string>
<string name="chat_member_dialog_transfer_title">Transfer ownership</string> <string name="chat_member_dialog_transfer_title">Transfer ownership</string>