Message Translate: Support Tone (#6849)
This commit is contained in:
parent
df5c8292ed
commit
889d07823d
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
1
src/assets/font-icons/tone.svg
Normal file
1
src/assets/font-icons/tone.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M29.05 15.293a2.444 2.444 0 0 0-3.457-3.456c-.082.081-.147.172-.214.262l-.003-.003-7.06 8.746a1.71 1.71 0 0 1-1.273.635l-.294.01a15 15 0 0 1-2.103-.076l-3.667-.384-.385-3.667a15 15 0 0 1-.075-2.104l.01-.294a1.71 1.71 0 0 1 .635-1.273l8.746-7.06-.003-.002c.09-.068.18-.133.262-.215a2.444 2.444 0 0 0-3.457-3.456c-.082.081-.147.172-.214.262l-.024-.024-6.886 8.53a3.16 3.16 0 0 0-.7 1.883v.029q-.036 1.043.072 2.082l.491 4.752a.7.7 0 0 1-.286.636l-5.793 4.148q-.054.037-.107.077l-.056.04.003.002c-.09.068-.18.133-.262.215a2.444 2.444 0 0 0 3.457 3.456c.082-.081.147-.172.214-.262l.024.024 4.253-5.972a.7.7 0 0 1 .638-.288l4.741.497q1.05.11 2.103.075h.019a3.16 3.16 0 0 0 1.882-.7l8.53-6.886-.023-.024c.09-.068.18-.133.262-.215"/></svg>
|
||||
|
After Width: | Height: | Size: 822 B |
@ -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";
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -5,11 +5,14 @@ import {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { TranslationTone } from '../../types';
|
||||
|
||||
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../config';
|
||||
import {
|
||||
selectLanguageCode,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectRequestedMessageTranslationTone,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -39,6 +42,7 @@ type StateProps = {
|
||||
messageId?: number;
|
||||
activeTranslationLanguage?: string;
|
||||
currentLanguageCode: string;
|
||||
currentTone?: TranslationTone;
|
||||
};
|
||||
|
||||
const ChatLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
@ -47,6 +51,7 @@ const ChatLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
messageId,
|
||||
activeTranslationLanguage,
|
||||
currentLanguageCode,
|
||||
currentTone,
|
||||
}) => {
|
||||
const {
|
||||
requestMessageTranslation,
|
||||
@ -62,7 +67,7 @@ const ChatLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
if (!chatId) return;
|
||||
|
||||
if (messageId) {
|
||||
requestMessageTranslation({ chatId, id: messageId, toLanguageCode: langCode });
|
||||
requestMessageTranslation({ chatId, id: messageId, toLanguageCode: langCode, tone: currentTone });
|
||||
} else {
|
||||
setSettingOption({ translationLanguage: langCode });
|
||||
requestChatTranslation({ chatId, toLanguageCode: langCode });
|
||||
@ -157,11 +162,16 @@ export default memo(withGlobal<OwnProps>(
|
||||
: selectRequestedChatTranslationLanguage(global, chatId)
|
||||
: undefined;
|
||||
|
||||
const currentTone = chatId && messageId
|
||||
? selectRequestedMessageTranslationTone(global, chatId, messageId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
chatId,
|
||||
messageId,
|
||||
activeTranslationLanguage,
|
||||
currentLanguageCode,
|
||||
currentTone,
|
||||
};
|
||||
},
|
||||
)(ChatLanguageModal));
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { IAnchorPosition, MessageListType, ThreadId } from '../../types';
|
||||
import type { IAnchorPosition, MessageListType, ThreadId, TranslationTone } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { ManagementScreens } from '../../types';
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
selectIsUserBlocked,
|
||||
selectLanguageCode,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedChatTranslationTone,
|
||||
selectTranslationLanguage,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
@ -44,11 +45,13 @@ import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import CustomEmoji from '../common/CustomEmoji';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import Link from '../ui/Link';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
import NestedMenuItem from '../ui/NestedMenuItem';
|
||||
import HeaderMenuContainer from './HeaderMenuContainer.async';
|
||||
|
||||
interface OwnProps {
|
||||
@ -91,6 +94,7 @@ interface StateProps {
|
||||
detectedChatLanguage?: string;
|
||||
doNotTranslate: string[];
|
||||
isAccountFrozen?: boolean;
|
||||
currentTone?: TranslationTone;
|
||||
}
|
||||
|
||||
const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
@ -128,6 +132,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
detectedChatLanguage,
|
||||
doNotTranslate,
|
||||
isAccountFrozen,
|
||||
currentTone,
|
||||
onTopicSearch,
|
||||
}) => {
|
||||
const {
|
||||
@ -140,6 +145,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
showNotification,
|
||||
openChat,
|
||||
requestChatTranslation,
|
||||
setChatTranslationTone,
|
||||
togglePeerTranslations,
|
||||
openChatLanguageModal,
|
||||
setSettingOption,
|
||||
@ -293,6 +299,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) });
|
||||
});
|
||||
|
||||
const handleSetTone = useLastCallback((tone: TranslationTone) => {
|
||||
setChatTranslationTone({ chatId, tone });
|
||||
setSettingOption({ translationTone: tone });
|
||||
});
|
||||
|
||||
useHotkeys(useMemo(() => ({
|
||||
'Mod+F': handleHotkeySearchClick,
|
||||
}), []));
|
||||
@ -326,6 +337,37 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
<MenuItem icon="replace" onClick={handleChangeLanguage}>
|
||||
{oldLang('Chat.Translate.Menu.To')}
|
||||
</MenuItem>
|
||||
<NestedMenuItem
|
||||
icon="tone"
|
||||
submenuClassName="translation-tone-menu"
|
||||
submenu={(
|
||||
<>
|
||||
<MenuItem
|
||||
icon={currentTone === 'neutral' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTone !== 'neutral' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => handleSetTone('neutral')}
|
||||
>
|
||||
{lang('TranslationToneNeutral')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={currentTone === 'formal' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTone !== 'formal' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => handleSetTone('formal')}
|
||||
>
|
||||
{lang('TranslationToneFormal')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={currentTone === 'casual' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTone !== 'casual' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => handleSetTone('casual')}
|
||||
>
|
||||
{lang('TranslationToneCasual')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{lang('TranslationTone')}
|
||||
</NestedMenuItem>
|
||||
<MenuSeparator />
|
||||
{detectedChatLanguage
|
||||
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
|
||||
@ -494,7 +536,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const language = selectLanguageCode(global);
|
||||
const translationLanguage = selectTranslationLanguage(global);
|
||||
const isPrivate = isUserId(chatId);
|
||||
const { doNotTranslate } = global.settings.byKey;
|
||||
const { doNotTranslate, translationTone } = global.settings.byKey;
|
||||
|
||||
const isRestricted = selectIsChatRestricted(global, chatId);
|
||||
if (!chat || isRestricted || selectIsInSelectMode(global)) {
|
||||
@ -503,6 +545,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
language,
|
||||
translationLanguage,
|
||||
doNotTranslate,
|
||||
currentTone: translationTone,
|
||||
} as Complete<StateProps>;
|
||||
}
|
||||
|
||||
@ -545,6 +588,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId));
|
||||
const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled;
|
||||
const isAccountFrozen = selectIsCurrentUserFrozen(global);
|
||||
const currentTone = selectRequestedChatTranslationTone(global, chatId);
|
||||
|
||||
const channelMonoforumId = isChatChannel(chat) ? chat.linkedMonoforumId : undefined;
|
||||
|
||||
@ -578,6 +622,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canUnblock,
|
||||
isAccountFrozen,
|
||||
channelMonoforumId,
|
||||
currentTone,
|
||||
};
|
||||
},
|
||||
)(HeaderActions));
|
||||
|
||||
@ -22,3 +22,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.translation-tone-menu .bubble {
|
||||
min-width: 0;
|
||||
|
||||
.icon {
|
||||
margin-inline: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import type {
|
||||
IAnchorPosition,
|
||||
MessageListType,
|
||||
ThreadId,
|
||||
TranslationTone,
|
||||
} from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
@ -64,6 +65,7 @@ import {
|
||||
selectPeerStory,
|
||||
selectPollFromMessage,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedChatTranslationTone,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectStickerSet,
|
||||
selectTopic,
|
||||
@ -77,6 +79,7 @@ import { selectSavedDialogIdFromMessage, selectThreadInfo } from '../../../globa
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../../util/keys/translationKey';
|
||||
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
|
||||
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
|
||||
|
||||
@ -138,6 +141,8 @@ type StateProps = {
|
||||
canShowOriginal?: boolean;
|
||||
isMessageTranslated?: boolean;
|
||||
canSelectLanguage?: boolean;
|
||||
currentTranslationTone?: TranslationTone;
|
||||
translationRequestLanguage?: string;
|
||||
isPrivate?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
hasFullInfo?: boolean;
|
||||
@ -227,6 +232,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isMessageTranslated,
|
||||
canShowOriginal,
|
||||
canSelectLanguage,
|
||||
currentTranslationTone,
|
||||
translationRequestLanguage,
|
||||
isReactionPickerOpen,
|
||||
isInSavedMessages,
|
||||
canReplyInChat,
|
||||
@ -278,6 +285,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
reportMessages,
|
||||
openTodoListModal,
|
||||
showNotification,
|
||||
setSettingOption,
|
||||
} = getActions();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
@ -655,6 +663,20 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
requestMessageTranslation({
|
||||
chatId: message.chatId,
|
||||
id: message.id,
|
||||
tone: currentTranslationTone,
|
||||
});
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleTranslateWithTone = useLastCallback((tone: TranslationTone) => {
|
||||
const { languageCode } = parseTranslationCacheKey(translationRequestLanguage!);
|
||||
|
||||
setSettingOption({ translationTone: tone });
|
||||
requestMessageTranslation({
|
||||
chatId: message.chatId,
|
||||
id: message.id,
|
||||
toLanguageCode: languageCode,
|
||||
tone,
|
||||
});
|
||||
closeMenu();
|
||||
});
|
||||
@ -736,6 +758,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canTranslate={canTranslate}
|
||||
canShowOriginal={canShowOriginal}
|
||||
canSelectLanguage={canSelectLanguage}
|
||||
currentTranslationTone={currentTranslationTone}
|
||||
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
||||
shouldRenderShowWhen={shouldRenderShowWhen}
|
||||
canLoadReadDate={canLoadReadDate}
|
||||
@ -777,6 +800,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
onShowReactors={handleOpenReactorListModal}
|
||||
onReactionPickerOpen={handleReactionPickerOpen}
|
||||
onTranslate={handleTranslate}
|
||||
onTranslateWithTone={handleTranslateWithTone}
|
||||
onShowOriginal={handleShowOriginal}
|
||||
onSelectLanguage={handleSelectLanguage}
|
||||
userFullName={userFullName}
|
||||
@ -901,11 +925,26 @@ export default memo(withGlobal<OwnProps>(
|
||||
? customEmojiSetsNotFiltered : undefined;
|
||||
|
||||
const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id);
|
||||
const hasTranslation = translationRequestLanguage
|
||||
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text)
|
||||
const chatTranslationLanguage = selectRequestedChatTranslationLanguage(global, message.chatId);
|
||||
const chatTranslationTone = selectRequestedChatTranslationTone(global, message.chatId);
|
||||
|
||||
const isManualMessageTranslation = !chatTranslationLanguage && translationRequestLanguage;
|
||||
const { tone: manualMessageTone } = isManualMessageTranslation
|
||||
? parseTranslationCacheKey(translationRequestLanguage)
|
||||
: { tone: undefined };
|
||||
const globalTone = global.settings.byKey.translationTone;
|
||||
const currentTranslationTone = manualMessageTone || chatTranslationTone || globalTone;
|
||||
|
||||
const translationCacheKey = chatTranslationLanguage
|
||||
? getTranslationCacheKey(chatTranslationLanguage, currentTranslationTone)
|
||||
: translationRequestLanguage;
|
||||
|
||||
const messageTranslation = translationCacheKey
|
||||
? selectMessageTranslations(global, message.chatId, translationCacheKey)[message.id]
|
||||
: undefined;
|
||||
const hasTranslation = Boolean(messageTranslation?.text);
|
||||
const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage);
|
||||
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId);
|
||||
const isChatTranslated = chatTranslationLanguage;
|
||||
|
||||
const isInSavedMessages = selectIsChatWithSelf(global, message.chatId);
|
||||
|
||||
@ -966,6 +1005,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
canShowOriginal: hasTranslation && !isChatTranslated,
|
||||
canSelectLanguage: hasTranslation && !isChatTranslated,
|
||||
isMessageTranslated: hasTranslation,
|
||||
currentTranslationTone,
|
||||
translationRequestLanguage,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||
isInSavedMessages,
|
||||
|
||||
@ -40,6 +40,7 @@ import type {
|
||||
TextSummary,
|
||||
ThemeKey,
|
||||
ThreadId,
|
||||
TranslationTone,
|
||||
} from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
@ -106,6 +107,7 @@ import {
|
||||
selectPollFromMessage,
|
||||
selectReplyMessage,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedChatTranslationTone,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectSender,
|
||||
selectSenderFromHeader,
|
||||
@ -131,6 +133,7 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||
import { parseTranslationCacheKey } from '../../../util/keys/translationKey';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { isElementInViewport } from '../../../util/visibility/isElementInViewport';
|
||||
@ -313,6 +316,7 @@ type StateProps = {
|
||||
shouldDetectChatLanguage?: boolean;
|
||||
requestedTranslationLanguage?: string;
|
||||
requestedChatTranslationLanguage?: string;
|
||||
requestedTranslationTone?: TranslationTone;
|
||||
withAnimatedEffects?: boolean;
|
||||
canAnimateTextStreaming?: boolean;
|
||||
webPageStory?: ApiTypeStory;
|
||||
@ -442,6 +446,7 @@ const Message = ({
|
||||
shouldDetectChatLanguage,
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
requestedTranslationTone,
|
||||
withAnimatedEffects,
|
||||
canAnimateTextStreaming,
|
||||
webPageStory,
|
||||
@ -827,12 +832,19 @@ const Message = ({
|
||||
useDetectChatLanguage(message, detectedLanguage, !shouldDetectChatLanguage, getIsMessageListReady);
|
||||
|
||||
const shouldTranslate = isMessageTranslatable(message, !requestedChatTranslationLanguage);
|
||||
|
||||
const isManualMessageTranslation = !requestedChatTranslationLanguage && requestedTranslationLanguage;
|
||||
const parsedManualTranslation = isManualMessageTranslation
|
||||
? parseTranslationCacheKey(requestedTranslationLanguage) : undefined;
|
||||
const translationLanguageForHook = parsedManualTranslation?.languageCode || requestedChatTranslationLanguage;
|
||||
const translationToneForHook = parsedManualTranslation?.tone || requestedTranslationTone;
|
||||
|
||||
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
|
||||
chatTranslations, chatId, shouldTranslate ? messageId : undefined, requestedTranslationLanguage,
|
||||
chatTranslations, chatId, shouldTranslate ? messageId : undefined, translationLanguageForHook,
|
||||
translationToneForHook,
|
||||
);
|
||||
const isSummaryPending = Boolean(summary?.isPending);
|
||||
const isNewTextPending = isTranslationPending || isSummaryPending;
|
||||
// Used to display previous result while new one is loading
|
||||
const previousTranslatedText = usePreviousDeprecated(translatedText, Boolean(shouldTranslate));
|
||||
|
||||
useEffectWithPrevDeps(([prevIsShowingSummary]) => {
|
||||
@ -1224,6 +1236,7 @@ const Message = ({
|
||||
chatTranslations={chatTranslations}
|
||||
isMediaNsfw={isReplyMediaNsfw}
|
||||
requestedChatTranslationLanguage={requestedChatTranslationLanguage}
|
||||
requestedChatTranslationTone={requestedTranslationTone}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleReplyClick}
|
||||
@ -2149,6 +2162,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const requestedTranslationLanguage = selectRequestedMessageTranslationLanguage(global, chatId, message.id);
|
||||
const requestedChatTranslationLanguage = selectRequestedChatTranslationLanguage(global, chatId);
|
||||
const requestedTranslationTone = selectRequestedChatTranslationTone(global, chatId);
|
||||
|
||||
const areTranslationsEnabled = IS_TRANSLATION_SUPPORTED && global.settings.byKey.canTranslate
|
||||
&& !requestedChatTranslationLanguage; // Stop separate language detection if chat translation is requested
|
||||
@ -2250,6 +2264,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldDetectChatLanguage: selectShouldDetectChatLanguage(global, chatId),
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
requestedTranslationTone,
|
||||
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
||||
withAnimatedEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
||||
canAnimateTextStreaming: selectPerformanceSettingsValue(global, 'textStreaming'),
|
||||
|
||||
@ -87,3 +87,11 @@
|
||||
min-width: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
.translation-tone-menu .bubble {
|
||||
min-width: 0;
|
||||
|
||||
.icon {
|
||||
margin-inline: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import type {
|
||||
ApiUser,
|
||||
ApiWebPage,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
import type { IAnchorPosition, TranslationTone } from '../../../types';
|
||||
|
||||
import {
|
||||
getUserFullName,
|
||||
@ -38,9 +38,11 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Menu from '../../ui/Menu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import MenuSeparator from '../../ui/MenuSeparator';
|
||||
import NestedMenuItem from '../../ui/NestedMenuItem';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
import LastEditTimeMenuItem from './LastEditTimeMenuItem';
|
||||
import ReactionSelector from './reactions/ReactionSelector';
|
||||
@ -86,6 +88,7 @@ type OwnProps = {
|
||||
canTranslate?: boolean;
|
||||
canShowOriginal?: boolean;
|
||||
canSelectLanguage?: boolean;
|
||||
currentTranslationTone?: TranslationTone;
|
||||
isPrivate?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
canDownload?: boolean;
|
||||
@ -128,6 +131,7 @@ type OwnProps = {
|
||||
onShowSeenBy?: NoneToVoidFunction;
|
||||
onShowReactors?: NoneToVoidFunction;
|
||||
onTranslate?: NoneToVoidFunction;
|
||||
onTranslateWithTone?: (tone: TranslationTone) => void;
|
||||
onShowOriginal?: NoneToVoidFunction;
|
||||
onSelectLanguage?: NoneToVoidFunction;
|
||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||
@ -185,6 +189,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
canTranslate,
|
||||
canShowOriginal,
|
||||
canSelectLanguage,
|
||||
currentTranslationTone,
|
||||
isDownloading,
|
||||
repliesThreadInfo,
|
||||
canShowSeenBy,
|
||||
@ -227,6 +232,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
onCopyMessages,
|
||||
onReactionPickerOpen,
|
||||
onTranslate,
|
||||
onTranslateWithTone,
|
||||
onShowOriginal,
|
||||
onSelectLanguage,
|
||||
userFullName,
|
||||
@ -435,12 +441,47 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
{canUnfaveSticker && (
|
||||
<MenuItem icon="favorite" onClick={onUnfaveSticker}>{oldLang('Stickers.RemoveFromFavorites')}</MenuItem>
|
||||
)}
|
||||
{canTranslate && <MenuItem icon="language" onClick={onTranslate}>{oldLang('TranslateMessage')}</MenuItem>}
|
||||
{canTranslate && (
|
||||
<MenuItem icon="language" onClick={() => onTranslate?.()}>{oldLang('TranslateMessage')}</MenuItem>
|
||||
)}
|
||||
{canShowOriginal && (
|
||||
<MenuItem icon="language" onClick={onShowOriginal}>
|
||||
{oldLang('ShowOriginalButton')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canShowOriginal && (
|
||||
<NestedMenuItem
|
||||
icon="tone"
|
||||
submenuClassName="translation-tone-menu"
|
||||
submenu={(
|
||||
<>
|
||||
<MenuItem
|
||||
icon={currentTranslationTone === 'neutral' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTranslationTone !== 'neutral' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => onTranslateWithTone?.('neutral')}
|
||||
>
|
||||
{lang('TranslationToneNeutral')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={currentTranslationTone === 'formal' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTranslationTone !== 'formal' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => onTranslateWithTone?.('formal')}
|
||||
>
|
||||
{lang('TranslationToneFormal')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={currentTranslationTone === 'casual' ? 'message-succeeded' : undefined}
|
||||
customIcon={currentTranslationTone !== 'casual' ? <Icon name="placeholder" /> : undefined}
|
||||
onClick={() => onTranslateWithTone?.('casual')}
|
||||
>
|
||||
{lang('TranslationToneCasual')}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{lang('TranslationTone')}
|
||||
</NestedMenuItem>
|
||||
)}
|
||||
{canSelectLanguage && (
|
||||
<MenuItem icon="web" onClick={onSelectLanguage}>{oldLang('lng_settings_change_lang')}</MenuItem>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useEffect } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ChatTranslatedMessages } from '../../../../types';
|
||||
import type { ChatTranslatedMessages, TranslationTone } from '../../../../types';
|
||||
|
||||
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../../../util/keys/translationKey';
|
||||
import { throttle } from '../../../../util/schedulers';
|
||||
|
||||
const MESSAGE_LIMIT_PER_REQUEST = 20;
|
||||
@ -14,19 +15,21 @@ export default function useMessageTranslation(
|
||||
chatId?: string,
|
||||
messageId?: number,
|
||||
requestedLanguageCode?: string,
|
||||
tone?: TranslationTone,
|
||||
) {
|
||||
const messageTranslation = requestedLanguageCode && messageId
|
||||
? chatTranslations?.byLangCode[requestedLanguageCode]?.[messageId] : undefined;
|
||||
const cacheKey = requestedLanguageCode ? getTranslationCacheKey(requestedLanguageCode, tone) : undefined;
|
||||
const messageTranslation = cacheKey && messageId
|
||||
? chatTranslations?.byLangCode[cacheKey]?.[messageId] : undefined;
|
||||
|
||||
const { isPending, text } = messageTranslation || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatId || !messageId) return;
|
||||
if (!chatId || !messageId || !cacheKey || !requestedLanguageCode) return;
|
||||
|
||||
if (!text && isPending === undefined && requestedLanguageCode) {
|
||||
addPendingTranslation(chatId, messageId, requestedLanguageCode);
|
||||
if (!text && isPending === undefined) {
|
||||
addPendingTranslation(chatId, messageId, requestedLanguageCode, tone);
|
||||
}
|
||||
}, [chatId, text, isPending, messageId, requestedLanguageCode]);
|
||||
}, [chatId, text, isPending, messageId, cacheKey, requestedLanguageCode, tone]);
|
||||
|
||||
if (!chatId || !messageId) {
|
||||
return {
|
||||
@ -46,7 +49,10 @@ const throttledProcessPending = throttle(processPending, THROTTLE_DELAY);
|
||||
function processPending() {
|
||||
const { translateMessages } = getActions();
|
||||
let hasUnprocessed = false;
|
||||
PENDING_TRANSLATIONS.forEach((chats, toLanguageCode) => {
|
||||
|
||||
PENDING_TRANSLATIONS.forEach((chats, cacheKey) => {
|
||||
const { languageCode, tone } = parseTranslationCacheKey(cacheKey);
|
||||
|
||||
chats.forEach((messageIds, chatId) => {
|
||||
const messageIdsToTranslate = messageIds.slice(0, MESSAGE_LIMIT_PER_REQUEST);
|
||||
|
||||
@ -54,9 +60,9 @@ function processPending() {
|
||||
hasUnprocessed = true;
|
||||
}
|
||||
|
||||
translateMessages({ chatId, messageIds: messageIdsToTranslate, toLanguageCode });
|
||||
translateMessages({ chatId, messageIds: messageIdsToTranslate, toLanguageCode: languageCode, tone });
|
||||
|
||||
removePendingTranslations(chatId, messageIdsToTranslate, toLanguageCode);
|
||||
removePendingTranslations(chatId, messageIdsToTranslate, cacheKey);
|
||||
});
|
||||
});
|
||||
|
||||
@ -69,8 +75,10 @@ function addPendingTranslation(
|
||||
chatId: string,
|
||||
messageId: number,
|
||||
toLanguageCode: string,
|
||||
tone?: TranslationTone,
|
||||
) {
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode) || new Map<string, number[]>();
|
||||
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(cacheKey) || new Map<string, number[]>();
|
||||
const messageIds = languageTranslations.get(chatId) || [];
|
||||
|
||||
if (messageIds.includes(messageId)) {
|
||||
@ -80,9 +88,9 @@ function addPendingTranslation(
|
||||
|
||||
messageIds.push(messageId);
|
||||
languageTranslations.set(chatId, messageIds);
|
||||
PENDING_TRANSLATIONS.set(toLanguageCode, languageTranslations);
|
||||
PENDING_TRANSLATIONS.set(cacheKey, languageTranslations);
|
||||
|
||||
getActions().markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
|
||||
getActions().markMessagesTranslationPending({ chatId, messageIds, toLanguageCode, tone });
|
||||
|
||||
throttledProcessPending();
|
||||
}
|
||||
@ -90,11 +98,11 @@ function addPendingTranslation(
|
||||
function removePendingTranslations(
|
||||
chatId: string,
|
||||
messageIds: number[],
|
||||
toLanguageCode: string,
|
||||
cacheKey: string,
|
||||
) {
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode);
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(cacheKey);
|
||||
if (!languageTranslations?.size) {
|
||||
PENDING_TRANSLATIONS.delete(toLanguageCode);
|
||||
PENDING_TRANSLATIONS.delete(cacheKey);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,7 +117,7 @@ function removePendingTranslations(
|
||||
if (!newMessageIds?.length) {
|
||||
languageTranslations.delete(chatId);
|
||||
if (!languageTranslations.size) {
|
||||
PENDING_TRANSLATIONS.delete(toLanguageCode);
|
||||
PENDING_TRANSLATIONS.delete(cacheKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ import {
|
||||
uniqueByField,
|
||||
} from '../../../util/iteratees';
|
||||
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
|
||||
import { parseTranslationCacheKey } from '../../../util/keys/translationKey';
|
||||
import { getTranslationFn, type RegularLangFnParameters } from '../../../util/localization';
|
||||
import { formatStarsAsText } from '../../../util/localization/format';
|
||||
import { oldTranslate } from '../../../util/oldLangProvider';
|
||||
@ -2879,13 +2880,16 @@ addActionHandler('forwardStory', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
addActionHandler('requestMessageTranslation', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, id, toLanguageCode = selectTranslationLanguage(global), tabId = getCurrentTabId(),
|
||||
chatId, id, toLanguageCode = selectTranslationLanguage(global), tone, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tabId);
|
||||
global = replaceSettings(global, {
|
||||
translationLanguage: toLanguageCode,
|
||||
});
|
||||
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tone, tabId);
|
||||
|
||||
if (!tone) {
|
||||
global = replaceSettings(global, {
|
||||
translationLanguage: toLanguageCode,
|
||||
});
|
||||
}
|
||||
|
||||
return global;
|
||||
});
|
||||
@ -2902,13 +2906,13 @@ addActionHandler('showOriginalMessage', (global, actions, payload): ActionReturn
|
||||
|
||||
addActionHandler('markMessagesTranslationPending', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global), tone,
|
||||
} = payload;
|
||||
|
||||
messageIds.forEach((id) => {
|
||||
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
|
||||
isPending: true,
|
||||
});
|
||||
}, tone);
|
||||
});
|
||||
|
||||
return global;
|
||||
@ -2916,18 +2920,19 @@ addActionHandler('markMessagesTranslationPending', (global, actions, payload): A
|
||||
|
||||
addActionHandler('translateMessages', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global), tone,
|
||||
} = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
|
||||
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode, tone });
|
||||
|
||||
callApi('translateText', {
|
||||
chat,
|
||||
messageIds,
|
||||
toLanguageCode,
|
||||
tone,
|
||||
});
|
||||
|
||||
return global;
|
||||
@ -2938,6 +2943,11 @@ addActionHandler('summarizeMessage', async (global, actions, payload): Promise<v
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const { languageCode, tone } = toLanguageCode
|
||||
? parseTranslationCacheKey(toLanguageCode)
|
||||
: { languageCode: undefined, tone: undefined };
|
||||
const apiTone = tone === 'neutral' ? undefined : tone;
|
||||
|
||||
const placeholderSummary: TextSummary = {
|
||||
isPending: true,
|
||||
text: undefined,
|
||||
@ -2946,10 +2956,9 @@ addActionHandler('summarizeMessage', async (global, actions, payload): Promise<v
|
||||
global = updateMessageSummary(global, chatId, id, placeholderSummary, toLanguageCode);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchMessageSummary', { chat, id, toLanguageCode });
|
||||
const result = await callApi('fetchMessageSummary', { chat, id, toLanguageCode: languageCode, tone: apiTone });
|
||||
if (!result) {
|
||||
global = getGlobal();
|
||||
// Disable summary to prevent endless loading
|
||||
global = updateChatMessage(global, chatId, id, { summaryLanguageCode: undefined });
|
||||
global = clearMessageSummary(global, chatId, id);
|
||||
setGlobal(global);
|
||||
|
||||
@ -945,19 +945,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
case 'updateMessageTranslations': {
|
||||
const {
|
||||
chatId, messageIds, toLanguageCode, translations,
|
||||
chatId, messageIds, toLanguageCode, translations, tone,
|
||||
} = update;
|
||||
|
||||
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, translations);
|
||||
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, translations, tone);
|
||||
|
||||
setGlobal(global);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'failedMessageTranslations': {
|
||||
const { chatId, messageIds, toLanguageCode } = update;
|
||||
const { chatId, messageIds, toLanguageCode, tone } = update;
|
||||
|
||||
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, []);
|
||||
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, [], tone);
|
||||
|
||||
setGlobal(global);
|
||||
break;
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -313,6 +313,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
canTranslate: false,
|
||||
canTranslateChats: true,
|
||||
doNotTranslate: [],
|
||||
translationTone: 'neutral',
|
||||
},
|
||||
privacy: {},
|
||||
botVerificationShownPeerIds: [],
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
import type { ApiFormattedText } from '../../api/types';
|
||||
import type { TextSummary, TranslatedMessage } from '../../types';
|
||||
import type { TextSummary, TranslatedMessage, TranslationTone } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { omit } from '../../util/iteratees';
|
||||
import { getTranslationCacheKey, parseTranslationCacheKey } from '../../util/keys/translationKey';
|
||||
import { selectMessageTranslations, selectTabState } from '../selectors';
|
||||
import { updateTabState } from './tabs';
|
||||
|
||||
export function updateMessageTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, toLanguageCode: string, translation: Partial<TranslatedMessage>,
|
||||
global: T,
|
||||
chatId: string,
|
||||
messageId: number,
|
||||
toLanguageCode: string,
|
||||
translation: Partial<TranslatedMessage>,
|
||||
tone?: TranslationTone,
|
||||
) {
|
||||
const translatedMessages = selectMessageTranslations(global, chatId, toLanguageCode);
|
||||
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
|
||||
const translatedMessages = selectMessageTranslations(global, chatId, cacheKey);
|
||||
|
||||
return {
|
||||
...global,
|
||||
@ -22,7 +29,7 @@ export function updateMessageTranslation<T extends GlobalState>(
|
||||
...global.translations.byChatId[chatId],
|
||||
byLangCode: {
|
||||
...global.translations.byChatId[chatId]?.byLangCode,
|
||||
[toLanguageCode]: {
|
||||
[cacheKey]: {
|
||||
...translatedMessages,
|
||||
[messageId]: {
|
||||
...translatedMessages[messageId],
|
||||
@ -68,30 +75,65 @@ export function clearMessageTranslation<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function updateMessageTranslations<T extends GlobalState>(
|
||||
global: T, chatId: string, messageIds: number[], toLanguageCode: string, translations: ApiFormattedText[],
|
||||
global: T,
|
||||
chatId: string,
|
||||
messageIds: number[],
|
||||
toLanguageCode: string,
|
||||
translations: ApiFormattedText[],
|
||||
tone?: TranslationTone,
|
||||
) {
|
||||
messageIds.forEach((messageId, index) => {
|
||||
const text = translations[index];
|
||||
global = updateMessageTranslation(global, chatId, messageId, toLanguageCode, {
|
||||
text: text.text.length ? text : undefined,
|
||||
text: text?.text?.length ? text : undefined,
|
||||
isPending: false,
|
||||
});
|
||||
}, tone);
|
||||
});
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function clearChatTranslations<T extends GlobalState>(
|
||||
global: T, chatId: string, toLanguageCode: string,
|
||||
) {
|
||||
const chatTranslations = global.translations.byChatId[chatId];
|
||||
if (!chatTranslations) return global;
|
||||
|
||||
const filteredByLangCode = Object.fromEntries(
|
||||
Object.entries(chatTranslations.byLangCode).filter(([cacheKey]) => {
|
||||
return parseTranslationCacheKey(cacheKey).languageCode !== toLanguageCode;
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...global,
|
||||
translations: {
|
||||
...global.translations,
|
||||
byChatId: {
|
||||
...global.translations.byChatId,
|
||||
[chatId]: {
|
||||
...chatTranslations,
|
||||
byLangCode: filteredByLangCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRequestedChatTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, toLanguageCode?: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
global: T, chatId: string, toLanguageCode?: string, tone?: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const existingChat = tabState.requestedTranslations.byChatId[chatId];
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
byChatId: {
|
||||
...tabState.requestedTranslations.byChatId,
|
||||
[chatId]: {
|
||||
...existingChat,
|
||||
toLanguage: toLanguageCode,
|
||||
tone: tone !== undefined ? tone : existingChat?.tone,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -115,10 +157,38 @@ export function removeRequestedChatTranslation<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateRequestedMessageTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
export function updateChatTranslationTone<T extends GlobalState>(
|
||||
global: T, chatId: string, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const existingChat = tabState.requestedTranslations.byChatId[chatId];
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
byChatId: {
|
||||
...tabState.requestedTranslations.byChatId,
|
||||
[chatId]: {
|
||||
...existingChat,
|
||||
tone,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateMessageTranslationTone<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, tone: TranslationTone, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const existingCacheKey = tabState.requestedTranslations.byChatId[chatId]?.manualMessages?.[messageId];
|
||||
if (!existingCacheKey) return global;
|
||||
|
||||
const { languageCode } = parseTranslationCacheKey(existingCacheKey);
|
||||
const newCacheKey = getTranslationCacheKey(languageCode, tone);
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
@ -128,7 +198,37 @@ export function updateRequestedMessageTranslation<T extends GlobalState>(
|
||||
...tabState.requestedTranslations.byChatId[chatId],
|
||||
manualMessages: {
|
||||
...tabState.requestedTranslations.byChatId[chatId]?.manualMessages,
|
||||
[messageId]: toLanguageCode,
|
||||
[messageId]: newCacheKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateRequestedMessageTranslation<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
messageId: number,
|
||||
toLanguageCode: string,
|
||||
tone?: TranslationTone,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const cacheKey = getTranslationCacheKey(toLanguageCode, tone);
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
byChatId: {
|
||||
...tabState.requestedTranslations.byChatId,
|
||||
[chatId]: {
|
||||
...tabState.requestedTranslations.byChatId[chatId],
|
||||
manualMessages: {
|
||||
...tabState.requestedTranslations.byChatId[chatId]?.manualMessages,
|
||||
[messageId]: cacheKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -313,6 +313,15 @@ export function selectRequestedChatTranslationLanguage<T extends GlobalState>(
|
||||
return requestedTranslations.byChatId[chatId]?.toLanguage;
|
||||
}
|
||||
|
||||
export function selectRequestedChatTranslationTone<T extends GlobalState>(
|
||||
global: T, chatId: string,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const { requestedTranslations } = selectTabState(global, tabId);
|
||||
|
||||
return requestedTranslations.byChatId[chatId]?.tone || global.settings.byKey.translationTone || 'neutral';
|
||||
}
|
||||
|
||||
export function selectSimilarChannelIds<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
MessageListType,
|
||||
TextSummary,
|
||||
ThreadId,
|
||||
TranslationTone,
|
||||
} from '../../types';
|
||||
import type { IAllowedAttachmentOptions } from '../helpers';
|
||||
import type {
|
||||
@ -31,6 +32,7 @@ import { isUserId } from '../../util/entities/ids';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { findLast } from '../../util/iteratees';
|
||||
import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey';
|
||||
import { parseTranslationCacheKey } from '../../util/keys/translationKey';
|
||||
import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
@ -1364,9 +1366,9 @@ export function selectChatTranslations<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function selectMessageTranslations<T extends GlobalState>(
|
||||
global: T, chatId: string, toLanguageCode: string,
|
||||
global: T, chatId: string, cacheKey: string,
|
||||
) {
|
||||
return selectChatTranslations(global, chatId)?.byLangCode[toLanguageCode] || {};
|
||||
return selectChatTranslations(global, chatId)?.byLangCode[cacheKey] || {};
|
||||
}
|
||||
|
||||
export function selectRequestedMessageTranslationLanguage<T extends GlobalState>(
|
||||
@ -1375,6 +1377,23 @@ export function selectRequestedMessageTranslationLanguage<T extends GlobalState>
|
||||
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
|
||||
return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId];
|
||||
}
|
||||
|
||||
export function selectRequestedMessageTranslationTone<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): TranslationTone | undefined {
|
||||
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
|
||||
|
||||
if (requestedInChat?.toLanguage) {
|
||||
return requestedInChat.tone || 'neutral';
|
||||
}
|
||||
|
||||
const cacheKey = requestedInChat?.manualMessages?.[messageId];
|
||||
if (!cacheKey) return undefined;
|
||||
|
||||
const { tone } = parseTranslationCacheKey(cacheKey);
|
||||
return tone;
|
||||
}
|
||||
|
||||
export function selectReplyCanBeSentToChat<T extends GlobalState>(
|
||||
global: T,
|
||||
toChatId: string,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -314,41 +314,42 @@ $icons-map: (
|
||||
"tag": "\f228",
|
||||
"timer": "\f229",
|
||||
"toncoin": "\f22a",
|
||||
"tools": "\f22b",
|
||||
"topic-new": "\f22c",
|
||||
"trade": "\f22d",
|
||||
"transcribe": "\f22e",
|
||||
"truck": "\f22f",
|
||||
"unarchive": "\f230",
|
||||
"underlined": "\f231",
|
||||
"understood": "\f232",
|
||||
"undo": "\f233",
|
||||
"unique-profile": "\f234",
|
||||
"unlist-outline": "\f235",
|
||||
"unlist": "\f236",
|
||||
"unlock-badge": "\f237",
|
||||
"unlock": "\f238",
|
||||
"unmute": "\f239",
|
||||
"unpin": "\f23a",
|
||||
"unread": "\f23b",
|
||||
"up": "\f23c",
|
||||
"user-filled": "\f23d",
|
||||
"user-online": "\f23e",
|
||||
"user-stars": "\f23f",
|
||||
"user-tag": "\f240",
|
||||
"user": "\f241",
|
||||
"video-outlined": "\f242",
|
||||
"video-stop": "\f243",
|
||||
"video": "\f244",
|
||||
"view-once": "\f245",
|
||||
"voice-chat": "\f246",
|
||||
"volume-1": "\f247",
|
||||
"volume-2": "\f248",
|
||||
"volume-3": "\f249",
|
||||
"warning": "\f24a",
|
||||
"web": "\f24b",
|
||||
"webapp": "\f24c",
|
||||
"word-wrap": "\f24d",
|
||||
"zoom-in": "\f24e",
|
||||
"zoom-out": "\f24f",
|
||||
"tone": "\f22b",
|
||||
"tools": "\f22c",
|
||||
"topic-new": "\f22d",
|
||||
"trade": "\f22e",
|
||||
"transcribe": "\f22f",
|
||||
"truck": "\f230",
|
||||
"unarchive": "\f231",
|
||||
"underlined": "\f232",
|
||||
"understood": "\f233",
|
||||
"undo": "\f234",
|
||||
"unique-profile": "\f235",
|
||||
"unlist-outline": "\f236",
|
||||
"unlist": "\f237",
|
||||
"unlock-badge": "\f238",
|
||||
"unlock": "\f239",
|
||||
"unmute": "\f23a",
|
||||
"unpin": "\f23b",
|
||||
"unread": "\f23c",
|
||||
"up": "\f23d",
|
||||
"user-filled": "\f23e",
|
||||
"user-online": "\f23f",
|
||||
"user-stars": "\f240",
|
||||
"user-tag": "\f241",
|
||||
"user": "\f242",
|
||||
"video-outlined": "\f243",
|
||||
"video-stop": "\f244",
|
||||
"video": "\f245",
|
||||
"view-once": "\f246",
|
||||
"voice-chat": "\f247",
|
||||
"volume-1": "\f248",
|
||||
"volume-2": "\f249",
|
||||
"volume-3": "\f24a",
|
||||
"warning": "\f24b",
|
||||
"web": "\f24c",
|
||||
"webapp": "\f24d",
|
||||
"word-wrap": "\f24e",
|
||||
"zoom-in": "\f24f",
|
||||
"zoom-out": "\f250",
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -297,6 +297,7 @@ export type FontIconName =
|
||||
| 'tag'
|
||||
| 'timer'
|
||||
| 'toncoin'
|
||||
| 'tone'
|
||||
| 'tools'
|
||||
| 'topic-new'
|
||||
| 'trade'
|
||||
|
||||
@ -169,6 +169,7 @@ export interface AccountSettings {
|
||||
canTranslateChats: boolean;
|
||||
translationLanguage?: string;
|
||||
doNotTranslate: string[];
|
||||
translationTone?: TranslationTone;
|
||||
shouldPaidMessageAutoApprove: boolean;
|
||||
}
|
||||
|
||||
@ -683,6 +684,9 @@ export type TranslatedMessage = {
|
||||
summary?: TextSummary;
|
||||
};
|
||||
|
||||
export const TRANSLATION_TONES = ['neutral', 'formal', 'casual'] as const;
|
||||
export type TranslationTone = typeof TRANSLATION_TONES[number];
|
||||
|
||||
export type TextSummary = {
|
||||
isPending?: false;
|
||||
text: ApiFormattedText;
|
||||
@ -698,6 +702,7 @@ export type ChatTranslatedMessages = {
|
||||
export type ChatRequestedTranslations = {
|
||||
toLanguage?: string;
|
||||
manualMessages?: Record<number, string>;
|
||||
tone?: TranslationTone;
|
||||
};
|
||||
|
||||
export type SimilarBotsInfo = {
|
||||
|
||||
5
src/types/language.d.ts
vendored
5
src/types/language.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
||||
24
src/util/keys/translationKey.ts
Normal file
24
src/util/keys/translationKey.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { TranslationTone } from '../../types';
|
||||
import { TRANSLATION_TONES } from '../../types';
|
||||
|
||||
export function getTranslationCacheKey(languageCode: string, tone: TranslationTone = 'neutral'): string {
|
||||
return `${languageCode}_${tone}`;
|
||||
}
|
||||
|
||||
export function parseTranslationCacheKey(cacheKey: string): { languageCode: string; tone: TranslationTone } {
|
||||
const separatorIndex = cacheKey.lastIndexOf('_');
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
return { languageCode: cacheKey, tone: 'neutral' };
|
||||
}
|
||||
|
||||
const languageCode = cacheKey.slice(0, separatorIndex);
|
||||
const tone = cacheKey.slice(separatorIndex + 1);
|
||||
const isValidTone = (TRANSLATION_TONES as readonly string[]).includes(tone);
|
||||
|
||||
if (!isValidTone) {
|
||||
return { languageCode: cacheKey, tone: 'neutral' };
|
||||
}
|
||||
|
||||
return { languageCode, tone: tone as TranslationTone };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user