From b6a7503c62f8eff8e3cf1292963a27d62005f969 Mon Sep 17 00:00:00 2001 From: ROCKET Date: Wed, 25 Feb 2026 17:14:27 -0600 Subject: [PATCH] Preserve hyperlink entities in alternative chat translation --- .../messenger/TranslateController.java | 18 +-- .../ui/Components/TranslateAlert2.java | 112 +++++++++++++++++- 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/TranslateController.java b/TMessagesProj/src/main/java/org/telegram/messenger/TranslateController.java index 19a903eff1f..f780ebf62d2 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/TranslateController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/TranslateController.java @@ -30,7 +30,6 @@ import org.telegram.tgnet.TLRPC; import org.telegram.tgnet.Vector; import org.spacegram.SpaceGramConfig; -import org.spacegram.translator.SpaceGramTranslator; import org.telegram.tgnet.tl.TL_stories; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Components.Bulletin; @@ -1061,14 +1060,11 @@ private void pushToTranslate( for (int i = 0; i < pendingTranslation1.messageIds.size(); ++i) { final int id = pendingTranslation1.messageIds.get(i); final Utilities.Callback4 _callback = pendingTranslation1.callbacks.get(i); - final String _text = pendingTranslation1.messageTexts.get(i).text; final TLRPC.TL_textWithEntities sourceEntity = pendingTranslation1.messageTexts.get(i); - - SpaceGramTranslator.getInstance().translate(_text, null, toLanguage, (result, rateLimit) -> { + + TranslateAlert2.alternativeTranslateWithEntities(sourceEntity, null, toLanguage, (result, rateLimit) -> { if (result != null) { - final TLRPC.TL_textWithEntities resultWithEntities = new TLRPC.TL_textWithEntities(); - resultWithEntities.text = result; - _callback.run(isTranscription, id, TranslateAlert2.preprocess(sourceEntity, resultWithEntities), toLanguage); + _callback.run(isTranscription, id, result, toLanguage); } else { toggleTranslatingDialog(dialogId, false); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.showBulletin, Bulletin.TYPE_ERROR, getString(rateLimit ? R.string.TranslationFailedAlert1 : R.string.TranslationFailedAlert2)); @@ -1129,12 +1125,10 @@ private void pushToTranslate( for (int i = 0; i < ids.size(); ++i) { final int id = ids.get(i); final Utilities.Callback4 _callback = callbacks.get(i); - final String _text = texts.get(i).text; - TranslateAlert2.alternativeTranslate(_text, null, toLanguage, (result, rateLimit) -> { + final TLRPC.TL_textWithEntities sourceEntity = texts.get(i); + TranslateAlert2.alternativeTranslateWithEntities(sourceEntity, null, toLanguage, (result, rateLimit) -> { if (result != null) { - final TLRPC.TL_textWithEntities resultWithEntities = new TLRPC.TL_textWithEntities(); - resultWithEntities.text = result; - _callback.run(isTranscription, id, resultWithEntities, toLanguage); + _callback.run(isTranscription, id, result, toLanguage); } else { toggleTranslatingDialog(dialogId, false); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.showBulletin, Bulletin.TYPE_ERROR, getString(rateLimit ? R.string.TranslationFailedAlert1 : R.string.TranslationFailedAlert2)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/TranslateAlert2.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/TranslateAlert2.java index ec3d81bd398..6cc335e1099 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/TranslateAlert2.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/TranslateAlert2.java @@ -78,6 +78,8 @@ import java.net.HttpURLConnection; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -405,10 +407,15 @@ private void translateAlt() { } final String toLng = _toLng; - alternativeTranslate(text, fromLng, toLng, (res, rateLimit) -> { + TLRPC.TL_textWithEntities source = new TLRPC.TL_textWithEntities(); + source.text = text; + source.entities = reqMessageEntities; + alternativeTranslateWithEntities(source, fromLng, toLng, (res, rateLimit) -> { if (res != null) { firstTranslation = false; - textView.setText(preprocessText(res)); + CharSequence translated = SpannableStringBuilder.valueOf(res.text); + MessageObject.addEntitiesToText(translated, res.entities, false, true, false, false); + textView.setText(preprocessText(translated)); adapter.updateMainView(textViewContainer); } else { if (isDismissed()) return; @@ -433,6 +440,107 @@ private static int lastIndexOfSafe(String text, String target, int start, int en return (idx >= start) ? idx : -1; } + private static class PlaceholderLink { + final String placeholder; + final String visibleText; + final TLRPC.MessageEntity source; + + PlaceholderLink(String placeholder, String visibleText, TLRPC.MessageEntity source) { + this.placeholder = placeholder; + this.visibleText = visibleText; + this.source = source; + } + } + + private static boolean isLinkEntity(TLRPC.MessageEntity entity) { + return entity instanceof TLRPC.TL_messageEntityTextUrl || + entity instanceof TLRPC.TL_messageEntityUrl || + entity instanceof TLRPC.TL_messageEntityEmail || + entity instanceof TLRPC.TL_messageEntityMentionName; + } + + private static TLRPC.MessageEntity cloneLinkEntity(TLRPC.MessageEntity entity, int offset, int length) { + if (entity == null) { + return null; + } + TLRPC.MessageEntity cloned; + if (entity instanceof TLRPC.TL_messageEntityTextUrl) { + TLRPC.TL_messageEntityTextUrl textUrl = new TLRPC.TL_messageEntityTextUrl(); + textUrl.url = entity.url; + cloned = textUrl; + } else if (entity instanceof TLRPC.TL_messageEntityUrl) { + cloned = new TLRPC.TL_messageEntityUrl(); + } else if (entity instanceof TLRPC.TL_messageEntityEmail) { + cloned = new TLRPC.TL_messageEntityEmail(); + } else if (entity instanceof TLRPC.TL_messageEntityMentionName) { + TLRPC.TL_messageEntityMentionName mentionName = new TLRPC.TL_messageEntityMentionName(); + mentionName.user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + cloned = mentionName; + } else { + return null; + } + cloned.offset = offset; + cloned.length = length; + return cloned; + } + + public static void alternativeTranslateWithEntities(TLRPC.TL_textWithEntities source, String fromLng, String toLng, Utilities.Callback2 done) { + if (done == null) { + return; + } + if (source == null || TextUtils.isEmpty(source.text)) { + TLRPC.TL_textWithEntities empty = new TLRPC.TL_textWithEntities(); + empty.text = ""; + done.run(empty, false); + return; + } + + final String originalText = source.text; + final ArrayList placeholders = new ArrayList<>(); + String maskedText = originalText; + if (source.entities != null && !source.entities.isEmpty()) { + ArrayList sorted = new ArrayList<>(source.entities); + sorted.sort(Comparator.comparingInt(a -> -a.offset)); + for (int i = 0; i < sorted.size(); ++i) { + TLRPC.MessageEntity entity = sorted.get(i); + if (!isLinkEntity(entity)) { + continue; + } + if (entity.offset < 0 || entity.length <= 0 || entity.offset + entity.length > originalText.length()) { + continue; + } + String visible = originalText.substring(entity.offset, entity.offset + entity.length); + String token = " __SG_LINK_" + placeholders.size() + "__ "; + placeholders.add(new PlaceholderLink(token, visible, entity)); + maskedText = maskedText.substring(0, entity.offset) + token + maskedText.substring(entity.offset + entity.length); + } + Collections.reverse(placeholders); + } + + alternativeTranslate(maskedText, fromLng, toLng, (translated, rateLimit) -> { + if (translated == null) { + done.run(null, rateLimit); + return; + } + TLRPC.TL_textWithEntities result = new TLRPC.TL_textWithEntities(); + result.text = translated; + result.entities = new ArrayList<>(); + for (int i = 0; i < placeholders.size(); ++i) { + PlaceholderLink link = placeholders.get(i); + int index = result.text.indexOf(link.placeholder); + if (index < 0) { + continue; + } + result.text = result.text.substring(0, index) + link.visibleText + result.text.substring(index + link.placeholder.length()); + TLRPC.MessageEntity linkEntity = cloneLinkEntity(link.source, index, link.visibleText.length()); + if (linkEntity != null) { + result.entities.add(linkEntity); + } + } + done.run(preprocess(source, result), false); + }); + } + public static ArrayList cut(String encodedText, int maxLength) { ArrayList result = new ArrayList<>(); int start = 0;