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 = ({
+
+ : undefined}
+ onClick={() => handleSetTone('neutral')}
+ >
+ {lang('TranslationToneNeutral')}
+
+ : undefined}
+ onClick={() => handleSetTone('formal')}
+ >
+ {lang('TranslationToneFormal')}
+
+ : undefined}
+ onClick={() => handleSetTone('casual')}
+ >
+ {lang('TranslationToneCasual')}
+
+ >
+ )}
+ >
+ {lang('TranslationTone')}
+
{detectedChatLanguage
&& }
@@ -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 && (
)}
- {canTranslate && }
+ {canTranslate && (
+
+ )}
{canShowOriginal && (
)}
+ {canShowOriginal && (
+
+ : undefined}
+ onClick={() => onTranslateWithTone?.('neutral')}
+ >
+ {lang('TranslationToneNeutral')}
+
+ : undefined}
+ onClick={() => onTranslateWithTone?.('formal')}
+ >
+ {lang('TranslationToneFormal')}
+
+ : undefined}
+ onClick={() => onTranslateWithTone?.('casual')}
+ >
+ {lang('TranslationToneCasual')}
+
+ >
+ )}
+ >
+ {lang('TranslationTone')}
+
+ )}
{canSelectLanguage && (
)}
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 };
+}