diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index aae1a2c6d..cbef58e8b 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -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, }); } diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 46295549a..9d0afca5a 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -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 = { diff --git a/src/assets/font-icons/tone.svg b/src/assets/font-icons/tone.svg new file mode 100644 index 000000000..b30bc314e --- /dev/null +++ b/src/assets/font-icons/tone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 909a6b1aa..230f2e1ef 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index 6e866d559..092233847 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -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(); diff --git a/src/components/middle/ChatLanguageModal.tsx b/src/components/middle/ChatLanguageModal.tsx index 1e0023d81..72454b8f4 100644 --- a/src/components/middle/ChatLanguageModal.tsx +++ b/src/components/middle/ChatLanguageModal.tsx @@ -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 = ({ @@ -47,6 +51,7 @@ const ChatLanguageModal: FC = ({ messageId, activeTranslationLanguage, currentLanguageCode, + currentTone, }) => { const { requestMessageTranslation, @@ -62,7 +67,7 @@ const ChatLanguageModal: FC = ({ 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( : selectRequestedChatTranslationLanguage(global, chatId) : undefined; + const currentTone = chatId && messageId + ? selectRequestedMessageTranslationTone(global, chatId, messageId) + : undefined; + return { chatId, messageId, activeTranslationLanguage, currentLanguageCode, + currentTone, }; }, )(ChatLanguageModal)); diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index 6a27a3f6e..a98c65d7b 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -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 = ({ @@ -128,6 +132,7 @@ const HeaderActions: FC = ({ detectedChatLanguage, doNotTranslate, isAccountFrozen, + currentTone, onTopicSearch, }) => { const { @@ -140,6 +145,7 @@ const HeaderActions: FC = ({ showNotification, openChat, requestChatTranslation, + setChatTranslationTone, togglePeerTranslations, openChatLanguageModal, setSettingOption, @@ -293,6 +299,11 @@ const HeaderActions: FC = ({ 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 = ({ {oldLang('Chat.Translate.Menu.To')} + + : undefined} + onClick={() => handleSetTone('neutral')} + > + {lang('TranslationToneNeutral')} + + : undefined} + onClick={() => handleSetTone('formal')} + > + {lang('TranslationToneFormal')} + + : undefined} + onClick={() => handleSetTone('casual')} + > + {lang('TranslationToneCasual')} + + + )} + > + {lang('TranslationTone')} + {detectedChatLanguage && {doNotTranslateText}} @@ -494,7 +536,7 @@ export default memo(withGlobal( 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( language, translationLanguage, doNotTranslate, + currentTone: translationTone, } as Complete; } @@ -545,6 +588,7 @@ export default memo(withGlobal( 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( canUnblock, isAccountFrozen, channelMonoforumId, + currentTone, }; }, )(HeaderActions)); diff --git a/src/components/middle/HeaderMenuContainer.scss b/src/components/middle/HeaderMenuContainer.scss index 1d9f71316..5854544d5 100644 --- a/src/components/middle/HeaderMenuContainer.scss +++ b/src/components/middle/HeaderMenuContainer.scss @@ -22,3 +22,11 @@ } } } + +.translation-tone-menu .bubble { + min-width: 0; + + .icon { + margin-inline: 0.25rem; + } +} diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 4528523f4..829a49b15 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -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 = ({ isMessageTranslated, canShowOriginal, canSelectLanguage, + currentTranslationTone, + translationRequestLanguage, isReactionPickerOpen, isInSavedMessages, canReplyInChat, @@ -278,6 +285,7 @@ const ContextMenuContainer: FC = ({ reportMessages, openTodoListModal, showNotification, + setSettingOption, } = getActions(); const oldLang = useOldLang(); @@ -655,6 +663,20 @@ const ContextMenuContainer: FC = ({ 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 = ({ canTranslate={canTranslate} canShowOriginal={canShowOriginal} canSelectLanguage={canSelectLanguage} + currentTranslationTone={currentTranslationTone} canPlayAnimatedEmojis={canPlayAnimatedEmojis} shouldRenderShowWhen={shouldRenderShowWhen} canLoadReadDate={canLoadReadDate} @@ -777,6 +800,7 @@ const ContextMenuContainer: FC = ({ onShowReactors={handleOpenReactorListModal} onReactionPickerOpen={handleReactionPickerOpen} onTranslate={handleTranslate} + onTranslateWithTone={handleTranslateWithTone} onShowOriginal={handleShowOriginal} onSelectLanguage={handleSelectLanguage} userFullName={userFullName} @@ -901,11 +925,26 @@ export default memo(withGlobal( ? 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( canShowOriginal: hasTranslation && !isChatTranslated, canSelectLanguage: hasTranslation && !isChatTranslated, isMessageTranslated: hasTranslation, + currentTranslationTone, + translationRequestLanguage, canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), isReactionPickerOpen: selectIsReactionPickerOpen(global), isInSavedMessages, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index fa3ae2f49..a5245afbd 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -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( 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( shouldDetectChatLanguage: selectShouldDetectChatLanguage(global, chatId), requestedTranslationLanguage, requestedChatTranslationLanguage, + requestedTranslationTone, hasLinkedChat: Boolean(chatFullInfo?.linkedChatId), withAnimatedEffects: selectPerformanceSettingsValue(global, 'stickerEffects'), canAnimateTextStreaming: selectPerformanceSettingsValue(global, 'textStreaming'), diff --git a/src/components/middle/message/MessageContextMenu.scss b/src/components/middle/message/MessageContextMenu.scss index 49d1b399b..7a2eb0da5 100644 --- a/src/components/middle/message/MessageContextMenu.scss +++ b/src/components/middle/message/MessageContextMenu.scss @@ -87,3 +87,11 @@ min-width: 12rem; } } + +.translation-tone-menu .bubble { + min-width: 0; + + .icon { + margin-inline: 0.25rem; + } +} diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 6cee520c4..7aaa32a96 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -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 = ({ canTranslate, canShowOriginal, canSelectLanguage, + currentTranslationTone, isDownloading, repliesThreadInfo, canShowSeenBy, @@ -227,6 +232,7 @@ const MessageContextMenu: FC = ({ onCopyMessages, onReactionPickerOpen, onTranslate, + onTranslateWithTone, onShowOriginal, onSelectLanguage, userFullName, @@ -435,12 +441,47 @@ const MessageContextMenu: FC = ({ {canUnfaveSticker && ( {oldLang('Stickers.RemoveFromFavorites')} )} - {canTranslate && {oldLang('TranslateMessage')}} + {canTranslate && ( + onTranslate?.()}>{oldLang('TranslateMessage')} + )} {canShowOriginal && ( {oldLang('ShowOriginalButton')} )} + {canShowOriginal && ( + + : undefined} + onClick={() => onTranslateWithTone?.('neutral')} + > + {lang('TranslationToneNeutral')} + + : undefined} + onClick={() => onTranslateWithTone?.('formal')} + > + {lang('TranslationToneFormal')} + + : undefined} + onClick={() => onTranslateWithTone?.('casual')} + > + {lang('TranslationToneCasual')} + + + )} + > + {lang('TranslationTone')} + + )} {canSelectLanguage && ( {oldLang('lng_settings_change_lang')} )} diff --git a/src/components/middle/message/hooks/useMessageTranslation.ts b/src/components/middle/message/hooks/useMessageTranslation.ts index e4ebf2849..dc934dbaf 100644 --- a/src/components/middle/message/hooks/useMessageTranslation.ts +++ b/src/components/middle/message/hooks/useMessageTranslation.ts @@ -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(); + const cacheKey = getTranslationCacheKey(toLanguageCode, tone); + const languageTranslations = PENDING_TRANSLATIONS.get(cacheKey) || new Map(); 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; } diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 87c3d9c44..ac7b73952 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -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 { 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; diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index c8092384d..13b20ebe6 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -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 => { diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 861ed213c..dbf71f385 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -313,6 +313,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { canTranslate: false, canTranslateChats: true, doNotTranslate: [], + translationTone: 'neutral', }, privacy: {}, botVerificationShownPeerIds: [], diff --git a/src/global/reducers/translations.ts b/src/global/reducers/translations.ts index d073f7fdf..9606116ba 100644 --- a/src/global/reducers/translations.ts +++ b/src/global/reducers/translations.ts @@ -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( - global: T, chatId: string, messageId: number, toLanguageCode: string, translation: Partial, + global: T, + chatId: string, + messageId: number, + toLanguageCode: string, + translation: Partial, + 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( ...global.translations.byChatId[chatId], byLangCode: { ...global.translations.byChatId[chatId]?.byLangCode, - [toLanguageCode]: { + [cacheKey]: { ...translatedMessages, [messageId]: { ...translatedMessages[messageId], @@ -68,30 +75,65 @@ export function clearMessageTranslation( } export function updateMessageTranslations( - 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( + 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( - global: T, chatId: string, toLanguageCode?: string, ...[tabId = getCurrentTabId()]: TabArgs + global: T, chatId: string, toLanguageCode?: string, tone?: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs ) { 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( return global; } -export function updateRequestedMessageTranslation( - global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs +export function updateChatTranslationTone( + global: T, chatId: string, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs ) { 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( + global: T, chatId: string, messageId: number, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs +) { + 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( ...tabState.requestedTranslations.byChatId[chatId], manualMessages: { ...tabState.requestedTranslations.byChatId[chatId]?.manualMessages, - [messageId]: toLanguageCode, + [messageId]: newCacheKey, + }, + }, + }, + }, + }, tabId); + + return global; +} + +export function updateRequestedMessageTranslation( + global: T, + chatId: string, + messageId: number, + toLanguageCode: string, + tone?: TranslationTone, + ...[tabId = getCurrentTabId()]: TabArgs +) { + 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, }, }, }, diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 55ef308d7..68ec63692 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -313,6 +313,15 @@ export function selectRequestedChatTranslationLanguage( return requestedTranslations.byChatId[chatId]?.toLanguage; } +export function selectRequestedChatTranslationTone( + global: T, chatId: string, + ...[tabId = getCurrentTabId()]: TabArgs +) { + const { requestedTranslations } = selectTabState(global, tabId); + + return requestedTranslations.byChatId[chatId]?.tone || global.settings.byKey.translationTone || 'neutral'; +} + export function selectSimilarChannelIds( global: T, chatId: string, diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 264419df9..684c23300 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -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( } export function selectMessageTranslations( - 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( @@ -1375,6 +1377,23 @@ export function selectRequestedMessageTranslationLanguage const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId]; return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId]; } + +export function selectRequestedMessageTranslationTone( + global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs +): 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( global: T, toChatId: string, diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index e8096708f..4e2f5802e 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; diff --git a/src/styles/icons.css b/src/styles/icons.css index bc590e150..c850cdb2f 100644 --- a/src/styles/icons.css +++ b/src/styles/icons.css @@ -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"; +} diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 0beb99f50..ade74a53e 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -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", ); diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 375905156..7f9c12572 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index dc597bbfb..02b8f94a0 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index 04f08da96..8c07df095 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -297,6 +297,7 @@ export type FontIconName = | 'tag' | 'timer' | 'toncoin' + | 'tone' | 'tools' | 'topic-new' | 'trade' diff --git a/src/types/index.ts b/src/types/index.ts index 084872f14..5f41cdb2d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; + tone?: TranslationTone; }; export type SimilarBotsInfo = { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 2469e5970..5f96ec5ad 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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; } diff --git a/src/util/keys/translationKey.ts b/src/util/keys/translationKey.ts new file mode 100644 index 000000000..c8f727995 --- /dev/null +++ b/src/util/keys/translationKey.ts @@ -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 }; +}