Message Translate: Support Tone (#6849)

This commit is contained in:
Alexander Zinchuk 2026-04-17 13:38:08 +02:00
parent df5c8292ed
commit 889d07823d
29 changed files with 543 additions and 145 deletions

View File

@ -6,6 +6,7 @@ import type {
ForwardMessagesParams,
SendMessageParams,
ThreadId,
TranslationTone,
} from '../../../types';
import type {
ApiAttachment,
@ -128,7 +129,7 @@ type TranslateTextParams = ({
messageIds: number[];
}) & {
toLanguageCode: string;
tone?: string;
tone?: TranslationTone;
};
type SearchResults = {
@ -2462,20 +2463,24 @@ export async function transcribeAudio({
export async function translateText(params: TranslateTextParams) {
let result;
const isMessageTranslation = 'chat' in params;
const { toLanguageCode, tone } = params;
const apiTone = tone === 'neutral' ? undefined : tone;
if (isMessageTranslation) {
const { chat, messageIds, toLanguageCode, tone } = params;
const { chat, messageIds } = params;
result = await invokeRequest(new GramJs.messages.TranslateText({
peer: buildInputPeer(chat.id, chat.accessHash),
id: messageIds,
toLang: toLanguageCode,
tone,
tone: apiTone,
}));
} else {
const { text, toLanguageCode, tone } = params;
const { text } = params;
result = await invokeRequest(new GramJs.messages.TranslateText({
text: text.map((t) => buildInputTextWithEntities(t)),
toLang: toLanguageCode,
tone,
tone: apiTone,
}));
}
@ -2486,6 +2491,7 @@ export async function translateText(params: TranslateTextParams) {
chatId: params.chat.id,
messageIds: params.messageIds,
toLanguageCode: params.toLanguageCode,
tone,
});
}
return undefined;
@ -2500,6 +2506,7 @@ export async function translateText(params: TranslateTextParams) {
messageIds: params.messageIds,
translations: formattedText,
toLanguageCode: params.toLanguageCode,
tone,
});
}

View File

@ -6,7 +6,7 @@ import type {
VideoRotation,
VideoState,
} from '../../lib/secret-sauce';
import type { ThreadId, ThreadReadState } from '../../types';
import type { ThreadId, ThreadReadState, TranslationTone } from '../../types';
import type { RegularLangFnParameters } from '../../util/localization';
import type { ApiBotCommand, ApiBotMenuButton } from './bots';
import type {
@ -778,6 +778,7 @@ export type ApiUpdateMessageTranslations = {
messageIds: number[];
translations: ApiFormattedText[];
toLanguageCode: string;
tone?: TranslationTone;
};
export type ApiUpdateFailedMessageTranslations = {
@ -785,6 +786,7 @@ export type ApiUpdateFailedMessageTranslations = {
chatId: string;
messageIds: number[];
toLanguageCode: string;
tone?: TranslationTone;
};
export type ApiUpdateFetchingDifference = {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M29.05 15.293a2.444 2.444 0 0 0-3.457-3.456c-.082.081-.147.172-.214.262l-.003-.003-7.06 8.746a1.71 1.71 0 0 1-1.273.635l-.294.01a15 15 0 0 1-2.103-.076l-3.667-.384-.385-3.667a15 15 0 0 1-.075-2.104l.01-.294a1.71 1.71 0 0 1 .635-1.273l8.746-7.06-.003-.002c.09-.068.18-.133.262-.215a2.444 2.444 0 0 0-3.457-3.456c-.082.081-.147.172-.214.262l-.024-.024-6.886 8.53a3.16 3.16 0 0 0-.7 1.883v.029q-.036 1.043.072 2.082l.491 4.752a.7.7 0 0 1-.286.636l-5.793 4.148q-.054.037-.107.077l-.056.04.003.002c-.09.068-.18.133-.262.215a2.444 2.444 0 0 0 3.457 3.456c.082-.081.147-.172.214-.262l.024.024 4.253-5.972a.7.7 0 0 1 .638-.288l4.741.497q1.05.11 2.103.075h.019a3.16 3.16 0 0 0 1.882-.7l8.53-6.886-.023-.024c.09-.068.18-.133.262-.215"/></svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@ -2775,6 +2775,10 @@
"Transfer" = "Transfer";
"TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}";
"TranslateMenuCocoonLinkText" = "How does it work?";
"TranslationTone" = "Translation Tone";
"TranslationToneNeutral" = "Neutral";
"TranslationToneFormal" = "Formal";
"TranslationToneCasual" = "Casual";
"CocoonTitle" = "Cocoon";
"CocoonDescription" = "Cocoon (**Co**nfidential **Co**mpute **O**pen **N**etwork)\nhandles AI tasks safely and efficiently.";
"CocoonFeature1Title" = "Private";

View File

@ -6,7 +6,7 @@ import type {
ApiMessage, ApiPeer, ApiReplyInfo, MediaContainer,
} from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatTranslatedMessages } from '../../../types';
import type { ChatTranslatedMessages, TranslationTone } from '../../../types';
import type { IconName } from '../../../types/icons';
import { TON_CURRENCY_CODE } from '../../../config';
@ -54,6 +54,7 @@ type OwnProps = {
isInComposer?: boolean;
chatTranslations?: ChatTranslatedMessages;
requestedChatTranslationLanguage?: string;
requestedChatTranslationTone?: TranslationTone;
isOpen?: boolean;
isMediaNsfw?: boolean;
noCaptions?: boolean;
@ -83,6 +84,7 @@ const EmbeddedMessage = ({
noUserColors,
chatTranslations,
requestedChatTranslationLanguage,
requestedChatTranslationTone,
isMediaNsfw,
noCaptions,
pictogramActionIcon,
@ -110,7 +112,8 @@ const EmbeddedMessage = ({
const shouldTranslate = message && isMessageTranslatable(message);
const { translatedText } = useMessageTranslation(
chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined, requestedChatTranslationLanguage,
chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined,
requestedChatTranslationLanguage, requestedChatTranslationTone,
);
const oldLang = useOldLang();

View File

@ -5,11 +5,14 @@ import {
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { TranslationTone } from '../../types';
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../config';
import {
selectLanguageCode,
selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage,
selectRequestedMessageTranslationTone,
selectTabState,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
@ -39,6 +42,7 @@ type StateProps = {
messageId?: number;
activeTranslationLanguage?: string;
currentLanguageCode: string;
currentTone?: TranslationTone;
};
const ChatLanguageModal: FC<OwnProps & StateProps> = ({
@ -47,6 +51,7 @@ const ChatLanguageModal: FC<OwnProps & StateProps> = ({
messageId,
activeTranslationLanguage,
currentLanguageCode,
currentTone,
}) => {
const {
requestMessageTranslation,
@ -62,7 +67,7 @@ const ChatLanguageModal: FC<OwnProps & StateProps> = ({
if (!chatId) return;
if (messageId) {
requestMessageTranslation({ chatId, id: messageId, toLanguageCode: langCode });
requestMessageTranslation({ chatId, id: messageId, toLanguageCode: langCode, tone: currentTone });
} else {
setSettingOption({ translationLanguage: langCode });
requestChatTranslation({ chatId, toLanguageCode: langCode });
@ -157,11 +162,16 @@ export default memo(withGlobal<OwnProps>(
: selectRequestedChatTranslationLanguage(global, chatId)
: undefined;
const currentTone = chatId && messageId
? selectRequestedMessageTranslationTone(global, chatId, messageId)
: undefined;
return {
chatId,
messageId,
activeTranslationLanguage,
currentLanguageCode,
currentTone,
};
},
)(ChatLanguageModal));

View File

@ -4,7 +4,7 @@ import {
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { IAnchorPosition, MessageListType, ThreadId } from '../../types';
import type { IAnchorPosition, MessageListType, ThreadId, TranslationTone } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { ManagementScreens } from '../../types';
@ -31,6 +31,7 @@ import {
selectIsUserBlocked,
selectLanguageCode,
selectRequestedChatTranslationLanguage,
selectRequestedChatTranslationTone,
selectTranslationLanguage,
selectUserFullInfo,
} from '../../global/selectors';
@ -44,11 +45,13 @@ import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import CustomEmoji from '../common/CustomEmoji';
import Icon from '../common/icons/Icon';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
import Link from '../ui/Link';
import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator';
import NestedMenuItem from '../ui/NestedMenuItem';
import HeaderMenuContainer from './HeaderMenuContainer.async';
interface OwnProps {
@ -91,6 +94,7 @@ interface StateProps {
detectedChatLanguage?: string;
doNotTranslate: string[];
isAccountFrozen?: boolean;
currentTone?: TranslationTone;
}
const HeaderActions: FC<OwnProps & StateProps> = ({
@ -128,6 +132,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
detectedChatLanguage,
doNotTranslate,
isAccountFrozen,
currentTone,
onTopicSearch,
}) => {
const {
@ -140,6 +145,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
showNotification,
openChat,
requestChatTranslation,
setChatTranslationTone,
togglePeerTranslations,
openChatLanguageModal,
setSettingOption,
@ -293,6 +299,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) });
});
const handleSetTone = useLastCallback((tone: TranslationTone) => {
setChatTranslationTone({ chatId, tone });
setSettingOption({ translationTone: tone });
});
useHotkeys(useMemo(() => ({
'Mod+F': handleHotkeySearchClick,
}), []));
@ -326,6 +337,37 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
<MenuItem icon="replace" onClick={handleChangeLanguage}>
{oldLang('Chat.Translate.Menu.To')}
</MenuItem>
<NestedMenuItem
icon="tone"
submenuClassName="translation-tone-menu"
submenu={(
<>
<MenuItem
icon={currentTone === 'neutral' ? 'message-succeeded' : undefined}
customIcon={currentTone !== 'neutral' ? <Icon name="placeholder" /> : undefined}
onClick={() => handleSetTone('neutral')}
>
{lang('TranslationToneNeutral')}
</MenuItem>
<MenuItem
icon={currentTone === 'formal' ? 'message-succeeded' : undefined}
customIcon={currentTone !== 'formal' ? <Icon name="placeholder" /> : undefined}
onClick={() => handleSetTone('formal')}
>
{lang('TranslationToneFormal')}
</MenuItem>
<MenuItem
icon={currentTone === 'casual' ? 'message-succeeded' : undefined}
customIcon={currentTone !== 'casual' ? <Icon name="placeholder" /> : undefined}
onClick={() => handleSetTone('casual')}
>
{lang('TranslationToneCasual')}
</MenuItem>
</>
)}
>
{lang('TranslationTone')}
</NestedMenuItem>
<MenuSeparator />
{detectedChatLanguage
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
@ -494,7 +536,7 @@ export default memo(withGlobal<OwnProps>(
const language = selectLanguageCode(global);
const translationLanguage = selectTranslationLanguage(global);
const isPrivate = isUserId(chatId);
const { doNotTranslate } = global.settings.byKey;
const { doNotTranslate, translationTone } = global.settings.byKey;
const isRestricted = selectIsChatRestricted(global, chatId);
if (!chat || isRestricted || selectIsInSelectMode(global)) {
@ -503,6 +545,7 @@ export default memo(withGlobal<OwnProps>(
language,
translationLanguage,
doNotTranslate,
currentTone: translationTone,
} as Complete<StateProps>;
}
@ -545,6 +588,7 @@ export default memo(withGlobal<OwnProps>(
const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId));
const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled;
const isAccountFrozen = selectIsCurrentUserFrozen(global);
const currentTone = selectRequestedChatTranslationTone(global, chatId);
const channelMonoforumId = isChatChannel(chat) ? chat.linkedMonoforumId : undefined;
@ -578,6 +622,7 @@ export default memo(withGlobal<OwnProps>(
canUnblock,
isAccountFrozen,
channelMonoforumId,
currentTone,
};
},
)(HeaderActions));

View File

@ -22,3 +22,11 @@
}
}
}
.translation-tone-menu .bubble {
min-width: 0;
.icon {
margin-inline: 0.25rem;
}
}

View File

@ -23,6 +23,7 @@ import type {
IAnchorPosition,
MessageListType,
ThreadId,
TranslationTone,
} from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
@ -64,6 +65,7 @@ import {
selectPeerStory,
selectPollFromMessage,
selectRequestedChatTranslationLanguage,
selectRequestedChatTranslationTone,
selectRequestedMessageTranslationLanguage,
selectStickerSet,
selectTopic,
@ -77,6 +79,7 @@ import { selectSavedDialogIdFromMessage, selectThreadInfo } from '../../../globa
import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard';
import { isUserId } from '../../../util/entities/ids';
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../../util/keys/translationKey';
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
@ -138,6 +141,8 @@ type StateProps = {
canShowOriginal?: boolean;
isMessageTranslated?: boolean;
canSelectLanguage?: boolean;
currentTranslationTone?: TranslationTone;
translationRequestLanguage?: string;
isPrivate?: boolean;
isCurrentUserPremium?: boolean;
hasFullInfo?: boolean;
@ -227,6 +232,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
isMessageTranslated,
canShowOriginal,
canSelectLanguage,
currentTranslationTone,
translationRequestLanguage,
isReactionPickerOpen,
isInSavedMessages,
canReplyInChat,
@ -278,6 +285,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
reportMessages,
openTodoListModal,
showNotification,
setSettingOption,
} = getActions();
const oldLang = useOldLang();
@ -655,6 +663,20 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
requestMessageTranslation({
chatId: message.chatId,
id: message.id,
tone: currentTranslationTone,
});
closeMenu();
});
const handleTranslateWithTone = useLastCallback((tone: TranslationTone) => {
const { languageCode } = parseTranslationCacheKey(translationRequestLanguage!);
setSettingOption({ translationTone: tone });
requestMessageTranslation({
chatId: message.chatId,
id: message.id,
toLanguageCode: languageCode,
tone,
});
closeMenu();
});
@ -736,6 +758,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canTranslate={canTranslate}
canShowOriginal={canShowOriginal}
canSelectLanguage={canSelectLanguage}
currentTranslationTone={currentTranslationTone}
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
shouldRenderShowWhen={shouldRenderShowWhen}
canLoadReadDate={canLoadReadDate}
@ -777,6 +800,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
onShowReactors={handleOpenReactorListModal}
onReactionPickerOpen={handleReactionPickerOpen}
onTranslate={handleTranslate}
onTranslateWithTone={handleTranslateWithTone}
onShowOriginal={handleShowOriginal}
onSelectLanguage={handleSelectLanguage}
userFullName={userFullName}
@ -901,11 +925,26 @@ export default memo(withGlobal<OwnProps>(
? customEmojiSetsNotFiltered : undefined;
const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id);
const hasTranslation = translationRequestLanguage
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text)
const chatTranslationLanguage = selectRequestedChatTranslationLanguage(global, message.chatId);
const chatTranslationTone = selectRequestedChatTranslationTone(global, message.chatId);
const isManualMessageTranslation = !chatTranslationLanguage && translationRequestLanguage;
const { tone: manualMessageTone } = isManualMessageTranslation
? parseTranslationCacheKey(translationRequestLanguage)
: { tone: undefined };
const globalTone = global.settings.byKey.translationTone;
const currentTranslationTone = manualMessageTone || chatTranslationTone || globalTone;
const translationCacheKey = chatTranslationLanguage
? getTranslationCacheKey(chatTranslationLanguage, currentTranslationTone)
: translationRequestLanguage;
const messageTranslation = translationCacheKey
? selectMessageTranslations(global, message.chatId, translationCacheKey)[message.id]
: undefined;
const hasTranslation = Boolean(messageTranslation?.text);
const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage);
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId);
const isChatTranslated = chatTranslationLanguage;
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
@ -966,6 +1005,8 @@ export default memo(withGlobal<OwnProps>(
canShowOriginal: hasTranslation && !isChatTranslated,
canSelectLanguage: hasTranslation && !isChatTranslated,
isMessageTranslated: hasTranslation,
currentTranslationTone,
translationRequestLanguage,
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
isReactionPickerOpen: selectIsReactionPickerOpen(global),
isInSavedMessages,

View File

@ -40,6 +40,7 @@ import type {
TextSummary,
ThemeKey,
ThreadId,
TranslationTone,
} from '../../../types';
import type { Signal } from '../../../util/signals';
import { MAIN_THREAD_ID } from '../../../api/types';
@ -106,6 +107,7 @@ import {
selectPollFromMessage,
selectReplyMessage,
selectRequestedChatTranslationLanguage,
selectRequestedChatTranslationTone,
selectRequestedMessageTranslationLanguage,
selectSender,
selectSenderFromHeader,
@ -131,6 +133,7 @@ import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { isUserId } from '../../../util/entities/ids';
import { getMessageKey } from '../../../util/keys/messageKey';
import { parseTranslationCacheKey } from '../../../util/keys/translationKey';
import { getServerTime } from '../../../util/serverTime';
import stopEvent from '../../../util/stopEvent';
import { isElementInViewport } from '../../../util/visibility/isElementInViewport';
@ -313,6 +316,7 @@ type StateProps = {
shouldDetectChatLanguage?: boolean;
requestedTranslationLanguage?: string;
requestedChatTranslationLanguage?: string;
requestedTranslationTone?: TranslationTone;
withAnimatedEffects?: boolean;
canAnimateTextStreaming?: boolean;
webPageStory?: ApiTypeStory;
@ -442,6 +446,7 @@ const Message = ({
shouldDetectChatLanguage,
requestedTranslationLanguage,
requestedChatTranslationLanguage,
requestedTranslationTone,
withAnimatedEffects,
canAnimateTextStreaming,
webPageStory,
@ -827,12 +832,19 @@ const Message = ({
useDetectChatLanguage(message, detectedLanguage, !shouldDetectChatLanguage, getIsMessageListReady);
const shouldTranslate = isMessageTranslatable(message, !requestedChatTranslationLanguage);
const isManualMessageTranslation = !requestedChatTranslationLanguage && requestedTranslationLanguage;
const parsedManualTranslation = isManualMessageTranslation
? parseTranslationCacheKey(requestedTranslationLanguage) : undefined;
const translationLanguageForHook = parsedManualTranslation?.languageCode || requestedChatTranslationLanguage;
const translationToneForHook = parsedManualTranslation?.tone || requestedTranslationTone;
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
chatTranslations, chatId, shouldTranslate ? messageId : undefined, requestedTranslationLanguage,
chatTranslations, chatId, shouldTranslate ? messageId : undefined, translationLanguageForHook,
translationToneForHook,
);
const isSummaryPending = Boolean(summary?.isPending);
const isNewTextPending = isTranslationPending || isSummaryPending;
// Used to display previous result while new one is loading
const previousTranslatedText = usePreviousDeprecated(translatedText, Boolean(shouldTranslate));
useEffectWithPrevDeps(([prevIsShowingSummary]) => {
@ -1224,6 +1236,7 @@ const Message = ({
chatTranslations={chatTranslations}
isMediaNsfw={isReplyMediaNsfw}
requestedChatTranslationLanguage={requestedChatTranslationLanguage}
requestedChatTranslationTone={requestedTranslationTone}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
onClick={handleReplyClick}
@ -2149,6 +2162,7 @@ export default memo(withGlobal<OwnProps>(
const requestedTranslationLanguage = selectRequestedMessageTranslationLanguage(global, chatId, message.id);
const requestedChatTranslationLanguage = selectRequestedChatTranslationLanguage(global, chatId);
const requestedTranslationTone = selectRequestedChatTranslationTone(global, chatId);
const areTranslationsEnabled = IS_TRANSLATION_SUPPORTED && global.settings.byKey.canTranslate
&& !requestedChatTranslationLanguage; // Stop separate language detection if chat translation is requested
@ -2250,6 +2264,7 @@ export default memo(withGlobal<OwnProps>(
shouldDetectChatLanguage: selectShouldDetectChatLanguage(global, chatId),
requestedTranslationLanguage,
requestedChatTranslationLanguage,
requestedTranslationTone,
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
withAnimatedEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
canAnimateTextStreaming: selectPerformanceSettingsValue(global, 'textStreaming'),

View File

@ -87,3 +87,11 @@
min-width: 12rem;
}
}
.translation-tone-menu .bubble {
min-width: 0;
.icon {
margin-inline: 0.25rem;
}
}

View File

@ -18,7 +18,7 @@ import type {
ApiUser,
ApiWebPage,
} from '../../../api/types';
import type { IAnchorPosition } from '../../../types';
import type { IAnchorPosition, TranslationTone } from '../../../types';
import {
getUserFullName,
@ -38,9 +38,11 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AvatarList from '../../common/AvatarList';
import Icon from '../../common/icons/Icon';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import MenuSeparator from '../../ui/MenuSeparator';
import NestedMenuItem from '../../ui/NestedMenuItem';
import Skeleton from '../../ui/placeholder/Skeleton';
import LastEditTimeMenuItem from './LastEditTimeMenuItem';
import ReactionSelector from './reactions/ReactionSelector';
@ -86,6 +88,7 @@ type OwnProps = {
canTranslate?: boolean;
canShowOriginal?: boolean;
canSelectLanguage?: boolean;
currentTranslationTone?: TranslationTone;
isPrivate?: boolean;
isCurrentUserPremium?: boolean;
canDownload?: boolean;
@ -128,6 +131,7 @@ type OwnProps = {
onShowSeenBy?: NoneToVoidFunction;
onShowReactors?: NoneToVoidFunction;
onTranslate?: NoneToVoidFunction;
onTranslateWithTone?: (tone: TranslationTone) => void;
onShowOriginal?: NoneToVoidFunction;
onSelectLanguage?: NoneToVoidFunction;
onToggleReaction?: (reaction: ApiReaction) => void;
@ -185,6 +189,7 @@ const MessageContextMenu: FC<OwnProps> = ({
canTranslate,
canShowOriginal,
canSelectLanguage,
currentTranslationTone,
isDownloading,
repliesThreadInfo,
canShowSeenBy,
@ -227,6 +232,7 @@ const MessageContextMenu: FC<OwnProps> = ({
onCopyMessages,
onReactionPickerOpen,
onTranslate,
onTranslateWithTone,
onShowOriginal,
onSelectLanguage,
userFullName,
@ -435,12 +441,47 @@ const MessageContextMenu: FC<OwnProps> = ({
{canUnfaveSticker && (
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{oldLang('Stickers.RemoveFromFavorites')}</MenuItem>
)}
{canTranslate && <MenuItem icon="language" onClick={onTranslate}>{oldLang('TranslateMessage')}</MenuItem>}
{canTranslate && (
<MenuItem icon="language" onClick={() => onTranslate?.()}>{oldLang('TranslateMessage')}</MenuItem>
)}
{canShowOriginal && (
<MenuItem icon="language" onClick={onShowOriginal}>
{oldLang('ShowOriginalButton')}
</MenuItem>
)}
{canShowOriginal && (
<NestedMenuItem
icon="tone"
submenuClassName="translation-tone-menu"
submenu={(
<>
<MenuItem
icon={currentTranslationTone === 'neutral' ? 'message-succeeded' : undefined}
customIcon={currentTranslationTone !== 'neutral' ? <Icon name="placeholder" /> : undefined}
onClick={() => onTranslateWithTone?.('neutral')}
>
{lang('TranslationToneNeutral')}
</MenuItem>
<MenuItem
icon={currentTranslationTone === 'formal' ? 'message-succeeded' : undefined}
customIcon={currentTranslationTone !== 'formal' ? <Icon name="placeholder" /> : undefined}
onClick={() => onTranslateWithTone?.('formal')}
>
{lang('TranslationToneFormal')}
</MenuItem>
<MenuItem
icon={currentTranslationTone === 'casual' ? 'message-succeeded' : undefined}
customIcon={currentTranslationTone !== 'casual' ? <Icon name="placeholder" /> : undefined}
onClick={() => onTranslateWithTone?.('casual')}
>
{lang('TranslationToneCasual')}
</MenuItem>
</>
)}
>
{lang('TranslationTone')}
</NestedMenuItem>
)}
{canSelectLanguage && (
<MenuItem icon="web" onClick={onSelectLanguage}>{oldLang('lng_settings_change_lang')}</MenuItem>
)}

View File

@ -1,8 +1,9 @@
import { useEffect } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
import type { ChatTranslatedMessages } from '../../../../types';
import type { ChatTranslatedMessages, TranslationTone } from '../../../../types';
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../../../util/keys/translationKey';
import { throttle } from '../../../../util/schedulers';
const MESSAGE_LIMIT_PER_REQUEST = 20;
@ -14,19 +15,21 @@ export default function useMessageTranslation(
chatId?: string,
messageId?: number,
requestedLanguageCode?: string,
tone?: TranslationTone,
) {
const messageTranslation = requestedLanguageCode && messageId
? chatTranslations?.byLangCode[requestedLanguageCode]?.[messageId] : undefined;
const cacheKey = requestedLanguageCode ? getTranslationCacheKey(requestedLanguageCode, tone) : undefined;
const messageTranslation = cacheKey && messageId
? chatTranslations?.byLangCode[cacheKey]?.[messageId] : undefined;
const { isPending, text } = messageTranslation || {};
useEffect(() => {
if (!chatId || !messageId) return;
if (!chatId || !messageId || !cacheKey || !requestedLanguageCode) return;
if (!text && isPending === undefined && requestedLanguageCode) {
addPendingTranslation(chatId, messageId, requestedLanguageCode);
if (!text && isPending === undefined) {
addPendingTranslation(chatId, messageId, requestedLanguageCode, tone);
}
}, [chatId, text, isPending, messageId, requestedLanguageCode]);
}, [chatId, text, isPending, messageId, cacheKey, requestedLanguageCode, tone]);
if (!chatId || !messageId) {
return {
@ -46,7 +49,10 @@ const throttledProcessPending = throttle(processPending, THROTTLE_DELAY);
function processPending() {
const { translateMessages } = getActions();
let hasUnprocessed = false;
PENDING_TRANSLATIONS.forEach((chats, toLanguageCode) => {
PENDING_TRANSLATIONS.forEach((chats, cacheKey) => {
const { languageCode, tone } = parseTranslationCacheKey(cacheKey);
chats.forEach((messageIds, chatId) => {
const messageIdsToTranslate = messageIds.slice(0, MESSAGE_LIMIT_PER_REQUEST);
@ -54,9 +60,9 @@ function processPending() {
hasUnprocessed = true;
}
translateMessages({ chatId, messageIds: messageIdsToTranslate, toLanguageCode });
translateMessages({ chatId, messageIds: messageIdsToTranslate, toLanguageCode: languageCode, tone });
removePendingTranslations(chatId, messageIdsToTranslate, toLanguageCode);
removePendingTranslations(chatId, messageIdsToTranslate, cacheKey);
});
});
@ -69,8 +75,10 @@ function addPendingTranslation(
chatId: string,
messageId: number,
toLanguageCode: string,
tone?: TranslationTone,
) {
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode) || new Map<string, number[]>();
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
const languageTranslations = PENDING_TRANSLATIONS.get(cacheKey) || new Map<string, number[]>();
const messageIds = languageTranslations.get(chatId) || [];
if (messageIds.includes(messageId)) {
@ -80,9 +88,9 @@ function addPendingTranslation(
messageIds.push(messageId);
languageTranslations.set(chatId, messageIds);
PENDING_TRANSLATIONS.set(toLanguageCode, languageTranslations);
PENDING_TRANSLATIONS.set(cacheKey, languageTranslations);
getActions().markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
getActions().markMessagesTranslationPending({ chatId, messageIds, toLanguageCode, tone });
throttledProcessPending();
}
@ -90,11 +98,11 @@ function addPendingTranslation(
function removePendingTranslations(
chatId: string,
messageIds: number[],
toLanguageCode: string,
cacheKey: string,
) {
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode);
const languageTranslations = PENDING_TRANSLATIONS.get(cacheKey);
if (!languageTranslations?.size) {
PENDING_TRANSLATIONS.delete(toLanguageCode);
PENDING_TRANSLATIONS.delete(cacheKey);
return;
}
@ -109,7 +117,7 @@ function removePendingTranslations(
if (!newMessageIds?.length) {
languageTranslations.delete(chatId);
if (!languageTranslations.size) {
PENDING_TRANSLATIONS.delete(toLanguageCode);
PENDING_TRANSLATIONS.delete(cacheKey);
}
return;
}

View File

@ -55,6 +55,7 @@ import {
uniqueByField,
} from '../../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
import { parseTranslationCacheKey } from '../../../util/keys/translationKey';
import { getTranslationFn, type RegularLangFnParameters } from '../../../util/localization';
import { formatStarsAsText } from '../../../util/localization/format';
import { oldTranslate } from '../../../util/oldLangProvider';
@ -2879,13 +2880,16 @@ addActionHandler('forwardStory', (global, actions, payload): ActionReturnType =>
addActionHandler('requestMessageTranslation', (global, actions, payload): ActionReturnType => {
const {
chatId, id, toLanguageCode = selectTranslationLanguage(global), tabId = getCurrentTabId(),
chatId, id, toLanguageCode = selectTranslationLanguage(global), tone, tabId = getCurrentTabId(),
} = payload;
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tabId);
global = replaceSettings(global, {
translationLanguage: toLanguageCode,
});
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tone, tabId);
if (!tone) {
global = replaceSettings(global, {
translationLanguage: toLanguageCode,
});
}
return global;
});
@ -2902,13 +2906,13 @@ addActionHandler('showOriginalMessage', (global, actions, payload): ActionReturn
addActionHandler('markMessagesTranslationPending', (global, actions, payload): ActionReturnType => {
const {
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
chatId, messageIds, toLanguageCode = selectLanguageCode(global), tone,
} = payload;
messageIds.forEach((id) => {
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
isPending: true,
});
}, tone);
});
return global;
@ -2916,18 +2920,19 @@ addActionHandler('markMessagesTranslationPending', (global, actions, payload): A
addActionHandler('translateMessages', (global, actions, payload): ActionReturnType => {
const {
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
chatId, messageIds, toLanguageCode = selectLanguageCode(global), tone,
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return undefined;
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode, tone });
callApi('translateText', {
chat,
messageIds,
toLanguageCode,
tone,
});
return global;
@ -2938,6 +2943,11 @@ addActionHandler('summarizeMessage', async (global, actions, payload): Promise<v
const chat = selectChat(global, chatId);
if (!chat) return;
const { languageCode, tone } = toLanguageCode
? parseTranslationCacheKey(toLanguageCode)
: { languageCode: undefined, tone: undefined };
const apiTone = tone === 'neutral' ? undefined : tone;
const placeholderSummary: TextSummary = {
isPending: true,
text: undefined,
@ -2946,10 +2956,9 @@ addActionHandler('summarizeMessage', async (global, actions, payload): Promise<v
global = updateMessageSummary(global, chatId, id, placeholderSummary, toLanguageCode);
setGlobal(global);
const result = await callApi('fetchMessageSummary', { chat, id, toLanguageCode });
const result = await callApi('fetchMessageSummary', { chat, id, toLanguageCode: languageCode, tone: apiTone });
if (!result) {
global = getGlobal();
// Disable summary to prevent endless loading
global = updateChatMessage(global, chatId, id, { summaryLanguageCode: undefined });
global = clearMessageSummary(global, chatId, id);
setGlobal(global);

View File

@ -945,19 +945,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateMessageTranslations': {
const {
chatId, messageIds, toLanguageCode, translations,
chatId, messageIds, toLanguageCode, translations, tone,
} = update;
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, translations);
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, translations, tone);
setGlobal(global);
break;
}
case 'failedMessageTranslations': {
const { chatId, messageIds, toLanguageCode } = update;
const { chatId, messageIds, toLanguageCode, tone } = update;
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, []);
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, [], tone);
setGlobal(global);
break;

View File

@ -7,13 +7,15 @@ import { createMessageHashUrl } from '../../../util/routing';
import { addActionHandler, execAfterActions, getGlobal, setGlobal } from '../../index';
import {
closeMiddleSearch,
exitMessageSelectMode, updateCurrentMessageList, updateRequestedChatTranslation,
exitMessageSelectMode,
updateChatTranslationTone,
updateCurrentMessageList,
updateMessageTranslationTone,
updateRequestedChatTranslation,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { replaceTabThreadParam } from '../../reducers/threads';
import {
selectChat, selectCurrentMessageList, selectTabState,
} from '../../selectors';
import { selectChat, selectCurrentMessageList, selectTabState } from '../../selectors';
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const {
@ -241,7 +243,20 @@ addActionHandler('closeChatlistModal', (global, actions, payload): ActionReturnT
addActionHandler('requestChatTranslation', (global, actions, payload): ActionReturnType => {
const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload;
return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId);
const tabState = selectTabState(global, tabId);
const existingTone = tabState.requestedTranslations.byChatId[chatId]?.tone;
const tone = existingTone || global.settings.byKey.translationTone;
return updateRequestedChatTranslation(global, chatId, toLanguageCode, tone, tabId);
});
addActionHandler('setChatTranslationTone', (global, actions, payload): ActionReturnType => {
const { chatId, tone, tabId = getCurrentTabId() } = payload;
return updateChatTranslationTone(global, chatId, tone, tabId);
});
addActionHandler('setMessageTranslationTone', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, tone, tabId = getCurrentTabId() } = payload;
return updateMessageTranslationTone(global, chatId, messageId, tone, tabId);
});
addActionHandler('closeChatInviteModal', (global, actions, payload): ActionReturnType => {

View File

@ -313,6 +313,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
canTranslate: false,
canTranslateChats: true,
doNotTranslate: [],
translationTone: 'neutral',
},
privacy: {},
botVerificationShownPeerIds: [],

View File

@ -1,16 +1,23 @@
import type { ApiFormattedText } from '../../api/types';
import type { TextSummary, TranslatedMessage } from '../../types';
import type { TextSummary, TranslatedMessage, TranslationTone } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { omit } from '../../util/iteratees';
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../util/keys/translationKey';
import { selectMessageTranslations, selectTabState } from '../selectors';
import { updateTabState } from './tabs';
export function updateMessageTranslation<T extends GlobalState>(
global: T, chatId: string, messageId: number, toLanguageCode: string, translation: Partial<TranslatedMessage>,
global: T,
chatId: string,
messageId: number,
toLanguageCode: string,
translation: Partial<TranslatedMessage>,
tone?: TranslationTone,
) {
const translatedMessages = selectMessageTranslations(global, chatId, toLanguageCode);
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
const translatedMessages = selectMessageTranslations(global, chatId, cacheKey);
return {
...global,
@ -22,7 +29,7 @@ export function updateMessageTranslation<T extends GlobalState>(
...global.translations.byChatId[chatId],
byLangCode: {
...global.translations.byChatId[chatId]?.byLangCode,
[toLanguageCode]: {
[cacheKey]: {
...translatedMessages,
[messageId]: {
...translatedMessages[messageId],
@ -68,30 +75,65 @@ export function clearMessageTranslation<T extends GlobalState>(
}
export function updateMessageTranslations<T extends GlobalState>(
global: T, chatId: string, messageIds: number[], toLanguageCode: string, translations: ApiFormattedText[],
global: T,
chatId: string,
messageIds: number[],
toLanguageCode: string,
translations: ApiFormattedText[],
tone?: TranslationTone,
) {
messageIds.forEach((messageId, index) => {
const text = translations[index];
global = updateMessageTranslation(global, chatId, messageId, toLanguageCode, {
text: text.text.length ? text : undefined,
text: text?.text?.length ? text : undefined,
isPending: false,
});
}, tone);
});
return global;
}
export function clearChatTranslations<T extends GlobalState>(
global: T, chatId: string, toLanguageCode: string,
) {
const chatTranslations = global.translations.byChatId[chatId];
if (!chatTranslations) return global;
const filteredByLangCode = Object.fromEntries(
Object.entries(chatTranslations.byLangCode).filter(([cacheKey]) => {
return parseTranslationCacheKey(cacheKey).languageCode !== toLanguageCode;
}),
);
return {
...global,
translations: {
...global.translations,
byChatId: {
...global.translations.byChatId,
[chatId]: {
...chatTranslations,
byLangCode: filteredByLangCode,
},
},
},
};
}
export function updateRequestedChatTranslation<T extends GlobalState>(
global: T, chatId: string, toLanguageCode?: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
global: T, chatId: string, toLanguageCode?: string, tone?: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
const existingChat = tabState.requestedTranslations.byChatId[chatId];
global = updateTabState(global, {
requestedTranslations: {
...tabState.requestedTranslations,
byChatId: {
...tabState.requestedTranslations.byChatId,
[chatId]: {
...existingChat,
toLanguage: toLanguageCode,
tone: tone !== undefined ? tone : existingChat?.tone,
},
},
},
@ -115,10 +157,38 @@ export function removeRequestedChatTranslation<T extends GlobalState>(
return global;
}
export function updateRequestedMessageTranslation<T extends GlobalState>(
global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
export function updateChatTranslationTone<T extends GlobalState>(
global: T, chatId: string, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
const existingChat = tabState.requestedTranslations.byChatId[chatId];
global = updateTabState(global, {
requestedTranslations: {
...tabState.requestedTranslations,
byChatId: {
...tabState.requestedTranslations.byChatId,
[chatId]: {
...existingChat,
tone,
},
},
},
}, tabId);
return global;
}
export function updateMessageTranslationTone<T extends GlobalState>(
global: T, chatId: string, messageId: number, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
const existingCacheKey = tabState.requestedTranslations.byChatId[chatId]?.manualMessages?.[messageId];
if (!existingCacheKey) return global;
const { languageCode } = parseTranslationCacheKey(existingCacheKey);
const newCacheKey = getTranslationCacheKey(languageCode, tone);
global = updateTabState(global, {
requestedTranslations: {
...tabState.requestedTranslations,
@ -128,7 +198,37 @@ export function updateRequestedMessageTranslation<T extends GlobalState>(
...tabState.requestedTranslations.byChatId[chatId],
manualMessages: {
...tabState.requestedTranslations.byChatId[chatId]?.manualMessages,
[messageId]: toLanguageCode,
[messageId]: newCacheKey,
},
},
},
},
}, tabId);
return global;
}
export function updateRequestedMessageTranslation<T extends GlobalState>(
global: T,
chatId: string,
messageId: number,
toLanguageCode: string,
tone?: TranslationTone,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const tabState = selectTabState(global, tabId);
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
global = updateTabState(global, {
requestedTranslations: {
...tabState.requestedTranslations,
byChatId: {
...tabState.requestedTranslations.byChatId,
[chatId]: {
...tabState.requestedTranslations.byChatId[chatId],
manualMessages: {
...tabState.requestedTranslations.byChatId[chatId]?.manualMessages,
[messageId]: cacheKey,
},
},
},

View File

@ -313,6 +313,15 @@ export function selectRequestedChatTranslationLanguage<T extends GlobalState>(
return requestedTranslations.byChatId[chatId]?.toLanguage;
}
export function selectRequestedChatTranslationTone<T extends GlobalState>(
global: T, chatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const { requestedTranslations } = selectTabState(global, tabId);
return requestedTranslations.byChatId[chatId]?.tone || global.settings.byKey.translationTone || 'neutral';
}
export function selectSimilarChannelIds<T extends GlobalState>(
global: T,
chatId: string,

View File

@ -14,6 +14,7 @@ import type {
MessageListType,
TextSummary,
ThreadId,
TranslationTone,
} from '../../types';
import type { IAllowedAttachmentOptions } from '../helpers';
import type {
@ -31,6 +32,7 @@ import { isUserId } from '../../util/entities/ids';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { findLast } from '../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey';
import { parseTranslationCacheKey } from '../../util/keys/translationKey';
import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { getServerTime } from '../../util/serverTime';
@ -1364,9 +1366,9 @@ export function selectChatTranslations<T extends GlobalState>(
}
export function selectMessageTranslations<T extends GlobalState>(
global: T, chatId: string, toLanguageCode: string,
global: T, chatId: string, cacheKey: string,
) {
return selectChatTranslations(global, chatId)?.byLangCode[toLanguageCode] || {};
return selectChatTranslations(global, chatId)?.byLangCode[cacheKey] || {};
}
export function selectRequestedMessageTranslationLanguage<T extends GlobalState>(
@ -1375,6 +1377,23 @@ export function selectRequestedMessageTranslationLanguage<T extends GlobalState>
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId];
}
export function selectRequestedMessageTranslationTone<T extends GlobalState>(
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
): TranslationTone | undefined {
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
if (requestedInChat?.toLanguage) {
return requestedInChat.tone || 'neutral';
}
const cacheKey = requestedInChat?.manualMessages?.[messageId];
if (!cacheKey) return undefined;
const { tone } = parseTranslationCacheKey(cacheKey);
return tone;
}
export function selectReplyCanBeSentToChat<T extends GlobalState>(
global: T,
toChatId: string,

View File

@ -107,6 +107,7 @@ import type {
StoryViewerOrigin,
ThemeKey,
ThreadId,
TranslationTone,
WebPageMediaSize,
} from '../../types';
import type { WebApp, WebAppModalStateType, WebAppOutboundEvent } from '../../types/webapp';
@ -1417,6 +1418,17 @@ export interface ActionPayloads {
isEnabled: boolean;
};
setChatTranslationTone: {
chatId: string;
tone: TranslationTone;
} & WithTabId;
setMessageTranslationTone: {
chatId: string;
messageId: number;
tone: TranslationTone;
} & WithTabId;
// Messages
setEditingDraft: {
text?: ApiFormattedText;
@ -1540,6 +1552,7 @@ export interface ActionPayloads {
chatId: string;
id: number;
toLanguageCode?: string;
tone?: TranslationTone;
} & WithTabId;
showOriginalMessage: {
@ -1551,11 +1564,13 @@ export interface ActionPayloads {
chatId: string;
messageIds: number[];
toLanguageCode?: string;
tone?: TranslationTone;
};
translateMessages: {
chatId: string;
messageIds: number[];
toLanguageCode?: string;
tone?: TranslationTone;
};
summarizeMessage: {
chatId: string;

View File

@ -3,8 +3,8 @@
font-weight: normal;
font-style: normal;
font-display: block;
src: url("./icons.woff2?2cfe26f033ba58e2ce3494fb1bfb31b7") format("woff2"),
url("./icons.woff?2cfe26f033ba58e2ce3494fb1bfb31b7") format("woff");
src: url("./icons.woff2?04b18437fcd7ee960708328d5fdc1333") format("woff2"),
url("./icons.woff?04b18437fcd7ee960708328d5fdc1333") format("woff");
}
.icon-char::before {
@ -915,114 +915,117 @@ url("./icons.woff?2cfe26f033ba58e2ce3494fb1bfb31b7") format("woff");
.icon-toncoin::before {
content: "\f22a";
}
.icon-tools::before {
.icon-tone::before {
content: "\f22b";
}
.icon-topic-new::before {
.icon-tools::before {
content: "\f22c";
}
.icon-trade::before {
.icon-topic-new::before {
content: "\f22d";
}
.icon-transcribe::before {
.icon-trade::before {
content: "\f22e";
}
.icon-truck::before {
.icon-transcribe::before {
content: "\f22f";
}
.icon-unarchive::before {
.icon-truck::before {
content: "\f230";
}
.icon-underlined::before {
.icon-unarchive::before {
content: "\f231";
}
.icon-understood::before {
.icon-underlined::before {
content: "\f232";
}
.icon-undo::before {
.icon-understood::before {
content: "\f233";
}
.icon-unique-profile::before {
.icon-undo::before {
content: "\f234";
}
.icon-unlist-outline::before {
.icon-unique-profile::before {
content: "\f235";
}
.icon-unlist::before {
.icon-unlist-outline::before {
content: "\f236";
}
.icon-unlock-badge::before {
.icon-unlist::before {
content: "\f237";
}
.icon-unlock::before {
.icon-unlock-badge::before {
content: "\f238";
}
.icon-unmute::before {
.icon-unlock::before {
content: "\f239";
}
.icon-unpin::before {
.icon-unmute::before {
content: "\f23a";
}
.icon-unread::before {
.icon-unpin::before {
content: "\f23b";
}
.icon-up::before {
.icon-unread::before {
content: "\f23c";
}
.icon-user-filled::before {
.icon-up::before {
content: "\f23d";
}
.icon-user-online::before {
.icon-user-filled::before {
content: "\f23e";
}
.icon-user-stars::before {
.icon-user-online::before {
content: "\f23f";
}
.icon-user-tag::before {
.icon-user-stars::before {
content: "\f240";
}
.icon-user::before {
.icon-user-tag::before {
content: "\f241";
}
.icon-video-outlined::before {
.icon-user::before {
content: "\f242";
}
.icon-video-stop::before {
.icon-video-outlined::before {
content: "\f243";
}
.icon-video::before {
.icon-video-stop::before {
content: "\f244";
}
.icon-view-once::before {
.icon-video::before {
content: "\f245";
}
.icon-voice-chat::before {
.icon-view-once::before {
content: "\f246";
}
.icon-volume-1::before {
.icon-voice-chat::before {
content: "\f247";
}
.icon-volume-2::before {
.icon-volume-1::before {
content: "\f248";
}
.icon-volume-3::before {
.icon-volume-2::before {
content: "\f249";
}
.icon-warning::before {
.icon-volume-3::before {
content: "\f24a";
}
.icon-web::before {
.icon-warning::before {
content: "\f24b";
}
.icon-webapp::before {
.icon-web::before {
content: "\f24c";
}
.icon-word-wrap::before {
.icon-webapp::before {
content: "\f24d";
}
.icon-zoom-in::before {
.icon-word-wrap::before {
content: "\f24e";
}
.icon-zoom-out::before {
.icon-zoom-in::before {
content: "\f24f";
}
.icon-zoom-out::before {
content: "\f250";
}

View File

@ -314,41 +314,42 @@ $icons-map: (
"tag": "\f228",
"timer": "\f229",
"toncoin": "\f22a",
"tools": "\f22b",
"topic-new": "\f22c",
"trade": "\f22d",
"transcribe": "\f22e",
"truck": "\f22f",
"unarchive": "\f230",
"underlined": "\f231",
"understood": "\f232",
"undo": "\f233",
"unique-profile": "\f234",
"unlist-outline": "\f235",
"unlist": "\f236",
"unlock-badge": "\f237",
"unlock": "\f238",
"unmute": "\f239",
"unpin": "\f23a",
"unread": "\f23b",
"up": "\f23c",
"user-filled": "\f23d",
"user-online": "\f23e",
"user-stars": "\f23f",
"user-tag": "\f240",
"user": "\f241",
"video-outlined": "\f242",
"video-stop": "\f243",
"video": "\f244",
"view-once": "\f245",
"voice-chat": "\f246",
"volume-1": "\f247",
"volume-2": "\f248",
"volume-3": "\f249",
"warning": "\f24a",
"web": "\f24b",
"webapp": "\f24c",
"word-wrap": "\f24d",
"zoom-in": "\f24e",
"zoom-out": "\f24f",
"tone": "\f22b",
"tools": "\f22c",
"topic-new": "\f22d",
"trade": "\f22e",
"transcribe": "\f22f",
"truck": "\f230",
"unarchive": "\f231",
"underlined": "\f232",
"understood": "\f233",
"undo": "\f234",
"unique-profile": "\f235",
"unlist-outline": "\f236",
"unlist": "\f237",
"unlock-badge": "\f238",
"unlock": "\f239",
"unmute": "\f23a",
"unpin": "\f23b",
"unread": "\f23c",
"up": "\f23d",
"user-filled": "\f23e",
"user-online": "\f23f",
"user-stars": "\f240",
"user-tag": "\f241",
"user": "\f242",
"video-outlined": "\f243",
"video-stop": "\f244",
"video": "\f245",
"view-once": "\f246",
"voice-chat": "\f247",
"volume-1": "\f248",
"volume-2": "\f249",
"volume-3": "\f24a",
"warning": "\f24b",
"web": "\f24c",
"webapp": "\f24d",
"word-wrap": "\f24e",
"zoom-in": "\f24f",
"zoom-out": "\f250",
);

Binary file not shown.

Binary file not shown.

View File

@ -297,6 +297,7 @@ export type FontIconName =
| 'tag'
| 'timer'
| 'toncoin'
| 'tone'
| 'tools'
| 'topic-new'
| 'trade'

View File

@ -169,6 +169,7 @@ export interface AccountSettings {
canTranslateChats: boolean;
translationLanguage?: string;
doNotTranslate: string[];
translationTone?: TranslationTone;
shouldPaidMessageAutoApprove: boolean;
}
@ -683,6 +684,9 @@ export type TranslatedMessage = {
summary?: TextSummary;
};
export const TRANSLATION_TONES = ['neutral', 'formal', 'casual'] as const;
export type TranslationTone = typeof TRANSLATION_TONES[number];
export type TextSummary = {
isPending?: false;
text: ApiFormattedText;
@ -698,6 +702,7 @@ export type ChatTranslatedMessages = {
export type ChatRequestedTranslations = {
toLanguage?: string;
manualMessages?: Record<number, string>;
tone?: TranslationTone;
};
export type SimilarBotsInfo = {

View File

@ -2033,6 +2033,10 @@ export interface LangPair {
'EnterPasswordDescription': undefined;
'Transfer': undefined;
'TranslateMenuCocoonLinkText': undefined;
'TranslationTone': undefined;
'TranslationToneNeutral': undefined;
'TranslationToneFormal': undefined;
'TranslationToneCasual': undefined;
'CocoonTitle': undefined;
'CocoonDescription': undefined;
'CocoonFeature1Title': undefined;
@ -2095,7 +2099,6 @@ export interface LangPair {
'TextShowLess': undefined;
'AiMessageEditorFrom': undefined;
'AiMessageEditorTo': undefined;
'TranslationToneNeutral': undefined;
'ButtonHelp': undefined;
}

View File

@ -0,0 +1,24 @@
import type { TranslationTone } from '../../types';
import { TRANSLATION_TONES } from '../../types';
export function getTranslationCacheKey(languageCode: string, tone: TranslationTone = 'neutral'): string {
return `${languageCode}_${tone}`;
}
export function parseTranslationCacheKey(cacheKey: string): { languageCode: string; tone: TranslationTone } {
const separatorIndex = cacheKey.lastIndexOf('_');
if (separatorIndex === -1) {
return { languageCode: cacheKey, tone: 'neutral' };
}
const languageCode = cacheKey.slice(0, separatorIndex);
const tone = cacheKey.slice(separatorIndex + 1);
const isValidTone = (TRANSLATION_TONES as readonly string[]).includes(tone);
if (!isValidTone) {
return { languageCode: cacheKey, tone: 'neutral' };
}
return { languageCode, tone: tone as TranslationTone };
}