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

View File

@ -6,7 +6,7 @@ import type {
VideoRotation, VideoRotation,
VideoState, VideoState,
} from '../../lib/secret-sauce'; } 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 { RegularLangFnParameters } from '../../util/localization';
import type { ApiBotCommand, ApiBotMenuButton } from './bots'; import type { ApiBotCommand, ApiBotMenuButton } from './bots';
import type { import type {
@ -778,6 +778,7 @@ export type ApiUpdateMessageTranslations = {
messageIds: number[]; messageIds: number[];
translations: ApiFormattedText[]; translations: ApiFormattedText[];
toLanguageCode: string; toLanguageCode: string;
tone?: TranslationTone;
}; };
export type ApiUpdateFailedMessageTranslations = { export type ApiUpdateFailedMessageTranslations = {
@ -785,6 +786,7 @@ export type ApiUpdateFailedMessageTranslations = {
chatId: string; chatId: string;
messageIds: number[]; messageIds: number[];
toLanguageCode: string; toLanguageCode: string;
tone?: TranslationTone;
}; };
export type ApiUpdateFetchingDifference = { 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"; "Transfer" = "Transfer";
"TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}"; "TranslateMenuCocoon" = "Translations are powered by 🥚 **Cocoon**. {link}";
"TranslateMenuCocoonLinkText" = "How does it work?"; "TranslateMenuCocoonLinkText" = "How does it work?";
"TranslationTone" = "Translation Tone";
"TranslationToneNeutral" = "Neutral";
"TranslationToneFormal" = "Formal";
"TranslationToneCasual" = "Casual";
"CocoonTitle" = "Cocoon"; "CocoonTitle" = "Cocoon";
"CocoonDescription" = "Cocoon (**Co**nfidential **Co**mpute **O**pen **N**etwork)\nhandles AI tasks safely and efficiently."; "CocoonDescription" = "Cocoon (**Co**nfidential **Co**mpute **O**pen **N**etwork)\nhandles AI tasks safely and efficiently.";
"CocoonFeature1Title" = "Private"; "CocoonFeature1Title" = "Private";

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; 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 { MAIN_THREAD_ID } from '../../api/types';
import { ManagementScreens } from '../../types'; import { ManagementScreens } from '../../types';
@ -31,6 +31,7 @@ import {
selectIsUserBlocked, selectIsUserBlocked,
selectLanguageCode, selectLanguageCode,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedChatTranslationTone,
selectTranslationLanguage, selectTranslationLanguage,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
@ -44,11 +45,13 @@ import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import CustomEmoji from '../common/CustomEmoji'; import CustomEmoji from '../common/CustomEmoji';
import Icon from '../common/icons/Icon';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import Link from '../ui/Link'; import Link from '../ui/Link';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import MenuSeparator from '../ui/MenuSeparator'; import MenuSeparator from '../ui/MenuSeparator';
import NestedMenuItem from '../ui/NestedMenuItem';
import HeaderMenuContainer from './HeaderMenuContainer.async'; import HeaderMenuContainer from './HeaderMenuContainer.async';
interface OwnProps { interface OwnProps {
@ -91,6 +94,7 @@ interface StateProps {
detectedChatLanguage?: string; detectedChatLanguage?: string;
doNotTranslate: string[]; doNotTranslate: string[];
isAccountFrozen?: boolean; isAccountFrozen?: boolean;
currentTone?: TranslationTone;
} }
const HeaderActions: FC<OwnProps & StateProps> = ({ const HeaderActions: FC<OwnProps & StateProps> = ({
@ -128,6 +132,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
detectedChatLanguage, detectedChatLanguage,
doNotTranslate, doNotTranslate,
isAccountFrozen, isAccountFrozen,
currentTone,
onTopicSearch, onTopicSearch,
}) => { }) => {
const { const {
@ -140,6 +145,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
showNotification, showNotification,
openChat, openChat,
requestChatTranslation, requestChatTranslation,
setChatTranslationTone,
togglePeerTranslations, togglePeerTranslations,
openChatLanguageModal, openChatLanguageModal,
setSettingOption, setSettingOption,
@ -293,6 +299,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) }); showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) });
}); });
const handleSetTone = useLastCallback((tone: TranslationTone) => {
setChatTranslationTone({ chatId, tone });
setSettingOption({ translationTone: tone });
});
useHotkeys(useMemo(() => ({ useHotkeys(useMemo(() => ({
'Mod+F': handleHotkeySearchClick, 'Mod+F': handleHotkeySearchClick,
}), [])); }), []));
@ -326,6 +337,37 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
<MenuItem icon="replace" onClick={handleChangeLanguage}> <MenuItem icon="replace" onClick={handleChangeLanguage}>
{oldLang('Chat.Translate.Menu.To')} {oldLang('Chat.Translate.Menu.To')}
</MenuItem> </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 /> <MenuSeparator />
{detectedChatLanguage {detectedChatLanguage
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>} && <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
@ -494,7 +536,7 @@ export default memo(withGlobal<OwnProps>(
const language = selectLanguageCode(global); const language = selectLanguageCode(global);
const translationLanguage = selectTranslationLanguage(global); const translationLanguage = selectTranslationLanguage(global);
const isPrivate = isUserId(chatId); const isPrivate = isUserId(chatId);
const { doNotTranslate } = global.settings.byKey; const { doNotTranslate, translationTone } = global.settings.byKey;
const isRestricted = selectIsChatRestricted(global, chatId); const isRestricted = selectIsChatRestricted(global, chatId);
if (!chat || isRestricted || selectIsInSelectMode(global)) { if (!chat || isRestricted || selectIsInSelectMode(global)) {
@ -503,6 +545,7 @@ export default memo(withGlobal<OwnProps>(
language, language,
translationLanguage, translationLanguage,
doNotTranslate, doNotTranslate,
currentTone: translationTone,
} as Complete<StateProps>; } as Complete<StateProps>;
} }
@ -545,6 +588,7 @@ export default memo(withGlobal<OwnProps>(
const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId)); const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId));
const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled; const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled;
const isAccountFrozen = selectIsCurrentUserFrozen(global); const isAccountFrozen = selectIsCurrentUserFrozen(global);
const currentTone = selectRequestedChatTranslationTone(global, chatId);
const channelMonoforumId = isChatChannel(chat) ? chat.linkedMonoforumId : undefined; const channelMonoforumId = isChatChannel(chat) ? chat.linkedMonoforumId : undefined;
@ -578,6 +622,7 @@ export default memo(withGlobal<OwnProps>(
canUnblock, canUnblock,
isAccountFrozen, isAccountFrozen,
channelMonoforumId, channelMonoforumId,
currentTone,
}; };
}, },
)(HeaderActions)); )(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, IAnchorPosition,
MessageListType, MessageListType,
ThreadId, ThreadId,
TranslationTone,
} from '../../../types'; } from '../../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -64,6 +65,7 @@ import {
selectPeerStory, selectPeerStory,
selectPollFromMessage, selectPollFromMessage,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedChatTranslationTone,
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
selectStickerSet, selectStickerSet,
selectTopic, selectTopic,
@ -77,6 +79,7 @@ import { selectSavedDialogIdFromMessage, selectThreadInfo } from '../../../globa
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { isUserId } from '../../../util/entities/ids'; import { isUserId } from '../../../util/entities/ids';
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../../util/keys/translationKey';
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText'; import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage'; import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
@ -138,6 +141,8 @@ type StateProps = {
canShowOriginal?: boolean; canShowOriginal?: boolean;
isMessageTranslated?: boolean; isMessageTranslated?: boolean;
canSelectLanguage?: boolean; canSelectLanguage?: boolean;
currentTranslationTone?: TranslationTone;
translationRequestLanguage?: string;
isPrivate?: boolean; isPrivate?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
hasFullInfo?: boolean; hasFullInfo?: boolean;
@ -227,6 +232,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
isMessageTranslated, isMessageTranslated,
canShowOriginal, canShowOriginal,
canSelectLanguage, canSelectLanguage,
currentTranslationTone,
translationRequestLanguage,
isReactionPickerOpen, isReactionPickerOpen,
isInSavedMessages, isInSavedMessages,
canReplyInChat, canReplyInChat,
@ -278,6 +285,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
reportMessages, reportMessages,
openTodoListModal, openTodoListModal,
showNotification, showNotification,
setSettingOption,
} = getActions(); } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
@ -655,6 +663,20 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
requestMessageTranslation({ requestMessageTranslation({
chatId: message.chatId, chatId: message.chatId,
id: message.id, 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(); closeMenu();
}); });
@ -736,6 +758,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canTranslate={canTranslate} canTranslate={canTranslate}
canShowOriginal={canShowOriginal} canShowOriginal={canShowOriginal}
canSelectLanguage={canSelectLanguage} canSelectLanguage={canSelectLanguage}
currentTranslationTone={currentTranslationTone}
canPlayAnimatedEmojis={canPlayAnimatedEmojis} canPlayAnimatedEmojis={canPlayAnimatedEmojis}
shouldRenderShowWhen={shouldRenderShowWhen} shouldRenderShowWhen={shouldRenderShowWhen}
canLoadReadDate={canLoadReadDate} canLoadReadDate={canLoadReadDate}
@ -777,6 +800,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
onShowReactors={handleOpenReactorListModal} onShowReactors={handleOpenReactorListModal}
onReactionPickerOpen={handleReactionPickerOpen} onReactionPickerOpen={handleReactionPickerOpen}
onTranslate={handleTranslate} onTranslate={handleTranslate}
onTranslateWithTone={handleTranslateWithTone}
onShowOriginal={handleShowOriginal} onShowOriginal={handleShowOriginal}
onSelectLanguage={handleSelectLanguage} onSelectLanguage={handleSelectLanguage}
userFullName={userFullName} userFullName={userFullName}
@ -901,11 +925,26 @@ export default memo(withGlobal<OwnProps>(
? customEmojiSetsNotFiltered : undefined; ? customEmojiSetsNotFiltered : undefined;
const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id); const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id);
const hasTranslation = translationRequestLanguage const chatTranslationLanguage = selectRequestedChatTranslationLanguage(global, message.chatId);
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text) 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; : undefined;
const hasTranslation = Boolean(messageTranslation?.text);
const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage); const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage);
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId); const isChatTranslated = chatTranslationLanguage;
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId); const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
@ -966,6 +1005,8 @@ export default memo(withGlobal<OwnProps>(
canShowOriginal: hasTranslation && !isChatTranslated, canShowOriginal: hasTranslation && !isChatTranslated,
canSelectLanguage: hasTranslation && !isChatTranslated, canSelectLanguage: hasTranslation && !isChatTranslated,
isMessageTranslated: hasTranslation, isMessageTranslated: hasTranslation,
currentTranslationTone,
translationRequestLanguage,
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
isReactionPickerOpen: selectIsReactionPickerOpen(global), isReactionPickerOpen: selectIsReactionPickerOpen(global),
isInSavedMessages, isInSavedMessages,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,15 @@ import { createMessageHashUrl } from '../../../util/routing';
import { addActionHandler, execAfterActions, getGlobal, setGlobal } from '../../index'; import { addActionHandler, execAfterActions, getGlobal, setGlobal } from '../../index';
import { import {
closeMiddleSearch, closeMiddleSearch,
exitMessageSelectMode, updateCurrentMessageList, updateRequestedChatTranslation, exitMessageSelectMode,
updateChatTranslationTone,
updateCurrentMessageList,
updateMessageTranslationTone,
updateRequestedChatTranslation,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { replaceTabThreadParam } from '../../reducers/threads'; import { replaceTabThreadParam } from '../../reducers/threads';
import { import { selectChat, selectCurrentMessageList, selectTabState } from '../../selectors';
selectChat, selectCurrentMessageList, selectTabState,
} from '../../selectors';
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => { addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const { const {
@ -241,7 +243,20 @@ addActionHandler('closeChatlistModal', (global, actions, payload): ActionReturnT
addActionHandler('requestChatTranslation', (global, actions, payload): ActionReturnType => { addActionHandler('requestChatTranslation', (global, actions, payload): ActionReturnType => {
const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload; 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 => { addActionHandler('closeChatInviteModal', (global, actions, payload): ActionReturnType => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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