Localization: Add many translations (#1103)

This commit is contained in:
Alexander Zinchuk 2021-05-21 14:44:26 +03:00
parent d2a10a5b8b
commit ffa338d667
43 changed files with 248 additions and 227 deletions

View File

@ -18,6 +18,7 @@ import { buildCollectionByKey } from '../../../util/iteratees';
import localDb from '../localDb'; import localDb from '../localDb';
const MAX_INT_32 = 2 ** 31 - 1; const MAX_INT_32 = 2 ** 31 - 1;
const BETA_LANG_CODES = ['ar', 'fa'];
export function updateProfile({ export function updateProfile({
firstName, firstName,
@ -250,7 +251,10 @@ export async function fetchLanguages(): Promise<ApiLanguage[] | undefined> {
export async function fetchLangPack({ sourceLangPacks, langCode }: { sourceLangPacks: string[]; langCode: string }) { export async function fetchLangPack({ sourceLangPacks, langCode }: { sourceLangPacks: string[]; langCode: string }) {
const results = await Promise.all(sourceLangPacks.map((langPack) => { const results = await Promise.all(sourceLangPacks.map((langPack) => {
return invokeRequest(new GramJs.langpack.GetLangPack({ langPack, langCode })); return invokeRequest(new GramJs.langpack.GetLangPack({
langPack,
langCode: BETA_LANG_CODES.includes(langCode) ? `${langCode}-raw` : langCode,
}));
})); }));
const collections = results const collections = results

View File

@ -16,6 +16,7 @@ import {
} from '../../modules/helpers'; } from '../../modules/helpers';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import renderText from './helpers/renderText';
import Avatar from './Avatar'; import Avatar from './Avatar';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
@ -96,55 +97,52 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
chat={chat} chat={chat}
isSavedMessages={isChatWithSelf} isSavedMessages={isChatWithSelf}
/> />
<h3 className="modal-title">{renderTitle()}</h3> <h3 className="modal-title">{lang(renderTitle())}</h3>
</div> </div>
); );
} }
function renderTitle() { function renderTitle() {
if (isChannel && !chat.isCreator) { if (isChannel && !chat.isCreator) {
return 'Leave Channel?'; return 'LeaveChannel';
} }
if (isChannel && chat.isCreator) { if (isChannel && chat.isCreator) {
return 'Delete and Leave Channel?'; return 'ChannelDelete';
} }
if (isBasicGroup || isSuperGroup) { if (isBasicGroup || isSuperGroup) {
return 'Leave Group?'; return 'Group.LeaveGroup';
} }
return 'Delete Chat?'; return 'DeleteChatUser';
} }
function renderMessage() { function renderMessage() {
if (isChannel && !chat.isCreator) {
return <p>Are you sure you want to leave channel <strong>{chatTitle}</strong>?</p>;
}
if (isChannel && chat.isCreator) { if (isChannel && chat.isCreator) {
return <p>Are you sure you want to delete and leave channel <strong>{chatTitle}</strong>?</p>; return <p>{renderText(lang('ChatList.DeleteAndLeaveGroupConfirmation', chatTitle), ['simple_markdown'])}</p>;
} }
if (isBasicGroup || isSuperGroup) { if ((isChannel && !chat.isCreator) || isBasicGroup || isSuperGroup) {
return <p>Are you sure you want to leave group <strong>{chatTitle}</strong>?</p>; return <p>{renderText(lang('ChannelLeaveAlertWithName', chatTitle), ['simple_markdown'])}</p>;
} }
return <p>Are you sure you want to delete chat with <strong>{contactName}</strong>?</p>; return <p>{renderText(lang('ChatList.DeleteChatConfirmation', contactName), ['simple_markdown'])}</p>;
} }
function renderActionText() { function renderActionText() {
if (isChannel && !chat.isCreator) { if (isChannel && !chat.isCreator) {
return 'Leave Channel'; return 'LeaveChannel';
} }
if (isChannel && chat.isCreator) { if (isChannel && chat.isCreator) {
return 'Delete and Leave Channel'; return 'Chat.Input.Delete';
} }
if (isBasicGroup || isSuperGroup) { if (isBasicGroup || isSuperGroup) {
return 'Leave Group'; return 'Group.LeaveGroup';
} }
return `Delete${canDeleteForAll ? ' just for me' : ''}`; return canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete';
} }
return ( return (
@ -157,11 +155,11 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
{renderMessage()} {renderMessage()}
{canDeleteForAll && ( {canDeleteForAll && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}> <Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
Delete for {contactName ? `me and ${contactName}` : 'Everyone'} {contactName ? lang('ChatList.DeleteForEveryone', contactName) : lang('DeleteForAll')}
</Button> </Button>
)} )}
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}> <Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}>
{renderActionText()} {lang(renderActionText())}
</Button> </Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button> <Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
</Modal> </Modal>

View File

@ -91,19 +91,19 @@ const DeleteMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
> >
<p>{lang('AreYouSureDeleteSingleMessage')}</p> <p>{lang('AreYouSureDeleteSingleMessage')}</p>
{willDeleteForCurrentUserOnly && ( {willDeleteForCurrentUserOnly && (
<p>This will delete it just for you, not for other participants in the chat.</p> <p>{lang('lng_delete_for_me_chat_hint')}</p>
)} )}
{willDeleteForAll && ( {willDeleteForAll && (
<p>This will delete it for everyone in this chat.</p> <p>{lang('lng_delete_for_everyone_hint')}</p>
)} )}
{canDeleteForAll && ( {canDeleteForAll && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}> <Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
Delete for {contactName ? 'me and ' : 'Everyone'} {contactName && lang('Conversation.DeleteMessagesFor', renderText(contactName))}
{contactName && renderText(contactName)} {!contactName && lang('Conversation.DeleteMessagesForEveryone')}
</Button> </Button>
)} )}
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}> <Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
Delete{canDeleteForAll ? ' just for me' : ''} {lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
</Button> </Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button> <Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
</Modal> </Modal>

View File

@ -49,7 +49,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
const pictogramId = message && `sticker-reply-thumb${message.id}`; const pictogramId = message && `sticker-reply-thumb${message.id}`;
const mediaThumbnail = useWebpThumbnail(message); const mediaThumbnail = useWebpThumbnail(message);
useLang(); const lang = useLang();
const senderTitle = sender && getSenderTitle(sender); const senderTitle = sender && getSenderTitle(sender);
@ -68,7 +68,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
) : isActionMessage(message) ? ( ) : isActionMessage(message) ? (
<ActionMessage message={message} isEmbedded /> <ActionMessage message={message} isEmbedded />
) : ( ) : (
renderText(getMessageSummaryText(message, Boolean(mediaThumbnail))) renderText(getMessageSummaryText(lang, message, Boolean(mediaThumbnail)))
)} )}
</p> </p>
</div> </div>

View File

@ -4,7 +4,19 @@
justify-content: center; justify-content: center;
color: var(--color-text-meta); color: var(--color-text-meta);
&.with-description {
flex-direction: column;
}
.AnimatedSticker { .AnimatedSticker {
margin: 0 auto; margin: 0 auto;
} }
.description {
color: var(--color-text-secondary);
font-size: .875rem;
text-align: center;
margin: 1rem 0 0;
unicode-bidi: plaintext;
}
} }

View File

@ -2,21 +2,26 @@ import React, { FC, memo } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import './NothingFound.scss'; import './NothingFound.scss';
interface OwnProps { interface OwnProps {
text?: string; text?: string;
description?: string;
} }
const DEFAULT_TEXT = 'Nothing found.'; const DEFAULT_TEXT = 'Nothing found.';
const NothingFound: FC<OwnProps> = ({ text = DEFAULT_TEXT }) => { const NothingFound: FC<OwnProps> = ({ text = DEFAULT_TEXT, description }) => {
const lang = useLang();
const { transitionClassNames } = useShowTransition(true); const { transitionClassNames } = useShowTransition(true);
return ( return (
<div className={buildClassName('NothingFound', transitionClassNames)}> <div className={buildClassName('NothingFound', transitionClassNames, description && 'with-description')}>
{text} {text}
{description && <p className="description">{renderText(lang(description), ['br'])}</p>}
</div> </div>
); );
}; };

View File

@ -10,6 +10,7 @@ import { STICKER_SIZE_MODAL } from '../../config';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import { selectStickerSet } from '../../modules/selectors'; import { selectStickerSet } from '../../modules/selectors';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -43,6 +44,7 @@ const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const lang = useLang();
const { const {
observe: observeIntersection, observe: observeIntersection,
@ -98,7 +100,11 @@ const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
color={stickerSet.installedDate ? 'danger' : 'primary'} color={stickerSet.installedDate ? 'danger' : 'primary'}
onClick={handleButtonClick} onClick={handleButtonClick}
> >
{`${stickerSet.installedDate ? 'Remove' : 'Add'} ${stickerSet.count} stickers`} {lang(
stickerSet.installedDate ? 'StickerPack.RemoveStickerCount' : 'StickerPack.AddStickerCount',
stickerSet.count,
'i',
)}
</Button> </Button>
</div> </div>
</> </>

View File

@ -7,6 +7,7 @@ import buildClassName from '../../util/buildClassName';
import trimText from '../../util/trimText'; import trimText from '../../util/trimText';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import { formatPastTimeShort } from '../../util/dateFormat'; import { formatPastTimeShort } from '../../util/dateFormat';
import useLang from '../../hooks/useLang';
import Media from './Media'; import Media from './Media';
import Link from '../ui/Link'; import Link from '../ui/Link';
@ -23,13 +24,15 @@ type OwnProps = {
}; };
const WebLink: FC<OwnProps> = ({ message, senderTitle, onMessageClick }) => { const WebLink: FC<OwnProps> = ({ message, senderTitle, onMessageClick }) => {
const lang = useLang();
let linkData: ApiWebPage | undefined = getMessageWebPage(message); let linkData: ApiWebPage | undefined = getMessageWebPage(message);
if (!linkData) { if (!linkData) {
const link = getFirstLinkInMessage(message); const link = getFirstLinkInMessage(message);
if (link) { if (link) {
const { url, domain } = link; const { url, domain } = link;
const messageText = getMessageSummaryText(message); const messageText = getMessageSummaryText(lang, message);
linkData = { linkData = {
siteName: domain.replace(/^www./, ''), siteName: domain.replace(/^www./, ''),

View File

@ -1,6 +1,7 @@
import React from '../../../lib/teact/teact'; import React from '../../../lib/teact/teact';
import { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
import { LangFn } from '../../../hooks/useLang';
import { import {
getChatTitle, getChatTitle,
getMessageContent, getMessageContent,
@ -25,6 +26,7 @@ interface ActionMessageTextOptions {
const NBSP = '\u00A0'; const NBSP = '\u00A0';
export function renderActionMessageText( export function renderActionMessageText(
lang: LangFn,
message: ApiMessage, message: ApiMessage,
actionOrigin?: ApiUser | ApiChat, actionOrigin?: ApiUser | ApiChat,
targetUser?: ApiUser, targetUser?: ApiUser,
@ -66,7 +68,7 @@ export function renderActionMessageText(
unprocessed, unprocessed,
'%message%', '%message%',
targetMessage targetMessage
? renderMessageContent(targetMessage, textOptions) ? renderMessageContent(lang, targetMessage, textOptions)
: 'a message', : 'a message',
); );
unprocessed = processed.pop() as string; unprocessed = processed.pop() as string;
@ -104,8 +106,8 @@ function renderProductContent(message: ApiMessage) {
: 'a product'; : 'a product';
} }
function renderMessageContent(message: ApiMessage, options: ActionMessageTextOptions = {}) { function renderMessageContent(lang: LangFn, message: ApiMessage, options: ActionMessageTextOptions = {}) {
const text = getMessageSummaryText(message); const text = getMessageSummaryText(lang, message);
const { const {
photo, video, document, sticker, photo, video, document, sticker,
} = getMessageContent(message); } = getMessageContent(message);

View File

@ -80,7 +80,7 @@ const NewChatButton: FC<OwnProps> = ({
color="primary" color="primary"
className={isMenuOpen ? 'active' : ''} className={isMenuOpen ? 'active' : ''}
onClick={toggleIsMenuOpen} onClick={toggleIsMenuOpen}
ariaLabel={isMenuOpen ? 'Close' : 'Create new chat'} ariaLabel={lang(isMenuOpen ? 'Close' : 'NewMessageTitle')}
tabIndex={-1} tabIndex={-1}
> >
<i className="icon-new-chat-filled" /> <i className="icon-new-chat-filled" />
@ -95,7 +95,7 @@ const NewChatButton: FC<OwnProps> = ({
> >
<MenuItem icon="channel" onClick={onNewChannel}>{lang('NewChannel')}</MenuItem> <MenuItem icon="channel" onClick={onNewChannel}>{lang('NewChannel')}</MenuItem>
<MenuItem icon="group" onClick={onNewGroup}>{lang('NewGroup')}</MenuItem> <MenuItem icon="group" onClick={onNewGroup}>{lang('NewGroup')}</MenuItem>
<MenuItem icon="user" onClick={onNewPrivateChat}>New Private Chat</MenuItem> <MenuItem icon="user" onClick={onNewPrivateChat}>{lang('NewMessageTitle')}</MenuItem>
</Menu> </Menu>
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn'; import { withGlobal } from '../../../lib/teact/teactn';
import useLang from '../../../hooks/useLang'; import useLang, { LangFn } from '../../../hooks/useLang';
import { GlobalActions, MessageListType } from '../../../global/types'; import { GlobalActions, MessageListType } from '../../../global/types';
import { import {
@ -205,6 +205,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
return ( return (
<p className="last-message"> <p className="last-message">
{renderText(renderActionMessageText( {renderText(renderActionMessageText(
lang,
lastMessage, lastMessage,
actionOrigin, actionOrigin,
actionTargetUser, actionTargetUser,
@ -223,7 +224,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
{senderName && ( {senderName && (
<span className="sender-name">{renderText(senderName)}</span> <span className="sender-name">{renderText(senderName)}</span>
)} )}
{renderMessageSummary(lastMessage!, mediaBlobUrl || mediaThumbnail)} {renderMessageSummary(lang, lastMessage!, mediaBlobUrl || mediaThumbnail)}
</p> </p>
); );
} }
@ -273,16 +274,16 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
); );
}; };
function renderMessageSummary(message: ApiMessage, blobUrl?: string) { function renderMessageSummary(lang: LangFn, message: ApiMessage, blobUrl?: string) {
if (!blobUrl) { if (!blobUrl) {
return renderText(getMessageSummaryText(message)); return renderText(getMessageSummaryText(lang, message));
} }
return ( return (
<span className="media-preview"> <span className="media-preview">
<img src={blobUrl} alt="" /> <img src={blobUrl} alt="" />
{getMessageVideo(message) && <i className="icon-play" />} {getMessageVideo(message) && <i className="icon-play" />}
{renderText(getMessageSummaryText(message, true))} {renderText(getMessageSummaryText(lang, message, true))}
</span> </span>
); );
} }

View File

@ -43,6 +43,8 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null); const transitionRef = useRef<HTMLDivElement>(null);
const lang = useLang();
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
useEffect(() => { useEffect(() => {
@ -84,13 +86,13 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
} }
return [ return [
{ title: 'All' }, { title: lang('FilterAllChats') },
...displayedFolders.map((folder) => ({ ...displayedFolders.map((folder) => ({
title: folder.title, title: folder.title,
...(folderCountersById && folderCountersById[folder.id]), ...(folderCountersById && folderCountersById[folder.id]),
})), })),
]; ];
}, [displayedFolders, folderCountersById]); }, [displayedFolders, folderCountersById, lang]);
const handleSwitchTab = useCallback((index: number) => { const handleSwitchTab = useCallback((index: number) => {
setActiveTab(index); setActiveTab(index);
@ -135,8 +137,6 @@ const ChatFolders: FC<StateProps & DispatchProps> = ({
shouldRender: shouldRenderPlaceholder, transitionClassNames, shouldRender: shouldRenderPlaceholder, transitionClassNames,
} = useShowTransition(!orderedFolderIds, undefined, true); } = useShowTransition(!orderedFolderIds, undefined, true);
const lang = useLang();
function renderCurrentTab() { function renderCurrentTab() {
const activeFolder = Object.values(chatFoldersById) const activeFolder = Object.values(chatFoldersById)
.find(({ title }) => title === folderTabs![activeTab].title); .find(({ title }) => title === folderTabs![activeTab].title);

View File

@ -76,6 +76,7 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
setGlobalSearchDate, setGlobalSearchDate,
setSettingOption, setSettingOption,
}) => { }) => {
const lang = useLang();
const hasMenu = content === LeftColumnContent.ChatList; const hasMenu = content === LeftColumnContent.ChatList;
const clearedDateSearchParam = { date: undefined }; const clearedDateSearchParam = { date: undefined };
const clearedChatSearchParam = { id: undefined }; const clearedChatSearchParam = { id: undefined };
@ -107,12 +108,12 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
color="translucent" color="translucent"
className={isOpen ? 'active' : ''} className={isOpen ? 'active' : ''}
onClick={hasMenu ? onTrigger : () => onReset()} onClick={hasMenu ? onTrigger : () => onReset()}
ariaLabel={hasMenu ? 'Open menu' : 'Return to chat list'} ariaLabel={hasMenu ? lang('AccDescrOpenMenu2') : 'Return to chat list'}
> >
<div className={buildClassName('animated-menu-icon', !hasMenu && 'state-back')} /> <div className={buildClassName('animated-menu-icon', !hasMenu && 'state-back')} />
</Button> </Button>
); );
}, [hasMenu, onReset]); }, [hasMenu, lang, onReset]);
const handleSearchFocus = useCallback(() => { const handleSearchFocus = useCallback(() => {
if (!searchQuery) { if (!searchQuery) {
@ -148,8 +149,6 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
setSettingOption({ animationLevel: newLevel }); setSettingOption({ animationLevel: newLevel });
}, [animationLevel, setSettingOption]); }, [animationLevel, setSettingOption]);
const lang = useLang();
const isSearchFocused = ( const isSearchFocused = (
Boolean(globalSearchChatId) Boolean(globalSearchChatId)
|| content === LeftColumnContent.GlobalSearch || content === LeftColumnContent.GlobalSearch
@ -198,10 +197,10 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
icon="darkmode" icon="darkmode"
onClick={handleDarkModeToggle} onClick={handleDarkModeToggle}
> >
<span className="menu-item-name">Dark Mode</span> <span className="menu-item-name">{lang('lng_menu_night_mode')}</span>
<Switcher <Switcher
id="darkmode" id="darkmode"
label="Toggle Dark Mode" label={lang(theme === 'dark' ? 'lng_settings_disable_night_theme' : 'lng_settings_enable_night_theme')}
checked={theme === 'dark'} checked={theme === 'dark'}
noAnimation noAnimation
/> />
@ -221,7 +220,7 @@ const LeftMainHeader: FC<OwnProps & StateProps & DispatchProps> = ({
icon="help" icon="help"
onClick={openTipsChat} onClick={openTipsChat}
> >
Telegram Features {lang('TelegramFeatures')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="bug" icon="bug"

View File

@ -14,6 +14,7 @@ import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat';
import { getSenderName } from './helpers/getSenderName'; import { getSenderName } from './helpers/getSenderName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import useLang from '../../../hooks/useLang';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Audio from '../../common/Audio'; import Audio from '../../common/Audio';
@ -43,6 +44,7 @@ const AudioResults: FC<OwnProps & StateProps & DispatchProps> = ({
focusMessage, focusMessage,
openAudioPlayer, openAudioPlayer,
}) => { }) => {
const lang = useLang();
const currentType = isVoice ? 'voice' : 'audio'; const currentType = isVoice ? 'voice' : 'audio';
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) { if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
@ -115,7 +117,12 @@ const AudioResults: FC<OwnProps & StateProps & DispatchProps> = ({
noFastList noFastList
> >
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && <NothingFound />} {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </div>

View File

@ -22,7 +22,7 @@ import renderText from '../../common/helpers/renderText';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import { formatPastTimeShort } from '../../../util/dateFormat'; import { formatPastTimeShort } from '../../../util/dateFormat';
import useLang from '../../../hooks/useLang'; import useLang, { LangFn } from '../../../hooks/useLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import VerifiedIcon from '../../common/VerifiedIcon'; import VerifiedIcon from '../../common/VerifiedIcon';
@ -62,7 +62,7 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
focusMessage({ chatId, messageId: message.id }); focusMessage({ chatId, messageId: message.id });
}, [chatId, focusMessage, message.id]); }, [chatId, focusMessage, message.id]);
useLang(); const lang = useLang();
if (!chat) { if (!chat) {
return undefined; return undefined;
@ -96,7 +96,7 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
</div> </div>
<div className="subtitle"> <div className="subtitle">
<div className="message"> <div className="message">
{renderMessageSummary(message, mediaBlobUrl || mediaThumbnail, searchQuery)} {renderMessageSummary(lang, message, mediaBlobUrl || mediaThumbnail, searchQuery)}
</div> </div>
</div> </div>
</div> </div>
@ -104,16 +104,16 @@ const ChatMessage: FC<OwnProps & StateProps & DispatchProps> = ({
); );
}; };
function renderMessageSummary(message: ApiMessage, blobUrl?: string, searchQuery?: string) { function renderMessageSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string) {
if (!blobUrl) { if (!blobUrl) {
return renderText(getMessageSummaryText(message)); return renderText(getMessageSummaryText(lang, message));
} }
return ( return (
<span className="media-preview"> <span className="media-preview">
<img src={blobUrl} alt="" /> <img src={blobUrl} alt="" />
{getMessageVideo(message) && <i className="icon-play" />} {getMessageVideo(message) && <i className="icon-play" />}
{renderText(getMessageSummaryText(message, true), ['emoji', 'highlight'], { highlight: searchQuery })} {renderText(getMessageSummaryText(lang, message, true), ['emoji', 'highlight'], { highlight: searchQuery })}
</span> </span>
); );
} }

View File

@ -11,6 +11,7 @@ import { pick } from '../../../util/iteratees';
import { getMessageSummaryText } from '../../../modules/helpers'; import { getMessageSummaryText } from '../../../modules/helpers';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useLang from '../../../hooks/useLang';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import ChatMessage from './ChatMessage'; import ChatMessage from './ChatMessage';
@ -49,6 +50,7 @@ const ChatMessageResults: FC<OwnProps & StateProps & DispatchProps> = ({
searchMessagesGlobal, searchMessagesGlobal,
onSearchDateSelect, onSearchDateSelect,
}) => { }) => {
const lang = useLang();
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) { if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
runThrottled(() => { runThrottled(() => {
@ -79,7 +81,7 @@ const ChatMessageResults: FC<OwnProps & StateProps & DispatchProps> = ({
}, [foundIds, globalMessagesByChatId]); }, [foundIds, globalMessagesByChatId]);
function renderFoundMessage(message: ApiMessage) { function renderFoundMessage(message: ApiMessage) {
const text = getMessageSummaryText(message); const text = getMessageSummaryText(lang, message);
const chat = chatsById[message.chatId]; const chat = chatsById[message.chatId];
if (!text || !chat) { if (!text || !chat) {
@ -113,7 +115,12 @@ const ChatMessageResults: FC<OwnProps & StateProps & DispatchProps> = ({
/> />
</div> </div>
)} )}
{nothingFound && <NothingFound />} {nothingFound && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{!!foundMessages.length && foundMessages.map(renderFoundMessage)} {!!foundMessages.length && foundMessages.map(renderFoundMessage)}
</InfiniteScroll> </InfiniteScroll>
</div> </div>

View File

@ -159,7 +159,7 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
const lang = useLang(); const lang = useLang();
function renderFoundMessage(message: ApiMessage) { function renderFoundMessage(message: ApiMessage) {
const text = getMessageSummaryText(message); const text = getMessageSummaryText(lang, message);
const chat = chatsById[message.chatId]; const chat = chatsById[message.chatId];
if (!text || !chat) { if (!text || !chat) {
@ -199,7 +199,12 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
/> />
</div> </div>
)} )}
{nothingFound && <NothingFound />} {nothingFound && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{!!localResults.length && ( {!!localResults.length && (
<div className="chat-selection no-selection no-scrollbar"> <div className="chat-selection no-selection no-scrollbar">
{localResults.map((id) => ( {localResults.map((id) => (
@ -215,9 +220,11 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="search-section"> <div className="search-section">
<h3 className="section-heading"> <h3 className="section-heading">
{localResults.length > LESS_LIST_ITEMS_AMOUNT && ( {localResults.length > LESS_LIST_ITEMS_AMOUNT && (
<Link onClick={handleClickShowMoreLocal}>{shouldShowMoreLocal ? 'Show less' : 'Show more'}</Link> <Link onClick={handleClickShowMoreLocal}>
{lang(shouldShowMoreLocal ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')}
</Link>
)} )}
Contacts and Chats {lang('DialogList.SearchSectionDialogs')}
</h3> </h3>
{localResults.map((id, index) => { {localResults.map((id, index) => {
if (!shouldShowMoreLocal && index >= LESS_LIST_ITEMS_AMOUNT) { if (!shouldShowMoreLocal && index >= LESS_LIST_ITEMS_AMOUNT) {
@ -237,9 +244,11 @@ const ChatResults: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="search-section"> <div className="search-section">
<h3 className="section-heading"> <h3 className="section-heading">
{globalResults.length > LESS_LIST_ITEMS_AMOUNT && ( {globalResults.length > LESS_LIST_ITEMS_AMOUNT && (
<Link onClick={handleClickShowMoreGlobal}>{shouldShowMoreGlobal ? 'Show less' : 'Show more'}</Link> <Link onClick={handleClickShowMoreGlobal}>
{lang(shouldShowMoreGlobal ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')}
</Link>
)} )}
Global Search {lang('DialogList.SearchSectionGlobal')}
</h3> </h3>
{globalResults.map((id, index) => { {globalResults.map((id, index) => {
if (!shouldShowMoreGlobal && index >= LESS_LIST_ITEMS_AMOUNT) { if (!shouldShowMoreGlobal && index >= LESS_LIST_ITEMS_AMOUNT) {

View File

@ -16,6 +16,7 @@ import { getSenderName } from './helpers/getSenderName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import { getMessageDocument } from '../../../modules/helpers'; import { getMessageDocument } from '../../../modules/helpers';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import useLang from '../../../hooks/useLang';
import Document from '../../common/Document'; import Document from '../../common/Document';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
@ -43,6 +44,7 @@ const FileResults: FC<OwnProps & StateProps & DispatchProps> = ({
searchMessagesGlobal, searchMessagesGlobal,
focusMessage, focusMessage,
}) => { }) => {
const lang = useLang();
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) { if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
runThrottled(() => { runThrottled(() => {
@ -109,7 +111,12 @@ const FileResults: FC<OwnProps & StateProps & DispatchProps> = ({
noFastList noFastList
> >
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && <NothingFound />} {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </div>

View File

@ -14,6 +14,7 @@ import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat';
import { getSenderName } from './helpers/getSenderName'; import { getSenderName } from './helpers/getSenderName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import useLang from '../../../hooks/useLang';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import WebLink from '../../common/WebLink'; import WebLink from '../../common/WebLink';
@ -41,6 +42,7 @@ const LinkResults: FC<OwnProps & StateProps & DispatchProps> = ({
searchMessagesGlobal, searchMessagesGlobal,
focusMessage, focusMessage,
}) => { }) => {
const lang = useLang();
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) { if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
runThrottled(() => { runThrottled(() => {
@ -103,7 +105,12 @@ const LinkResults: FC<OwnProps & StateProps & DispatchProps> = ({
noFastList noFastList
> >
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && <NothingFound />} {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{canRenderContents && foundIds && foundIds.length > 0 && renderList()} {canRenderContents && foundIds && foundIds.length > 0 && renderList()}
</InfiniteScroll> </InfiniteScroll>
</div> </div>

View File

@ -7,17 +7,18 @@ import { GlobalActions } from '../../../global/types';
import { LoadMoreDirection, MediaViewerOrigin } from '../../../types'; import { LoadMoreDirection, MediaViewerOrigin } from '../../../types';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { createMapStateToProps, StateProps } from './helpers/createMapStateToProps'; import { createMapStateToProps, StateProps } from './helpers/createMapStateToProps';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useLang from '../../../hooks/useLang';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Media from '../../common/Media'; import Media from '../../common/Media';
import ChatMessage from './ChatMessage'; import ChatMessage from './ChatMessage';
import NothingFound from '../../common/NothingFound'; import NothingFound from '../../common/NothingFound';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
export type OwnProps = { export type OwnProps = {
@ -39,6 +40,7 @@ const MediaResults: FC<OwnProps & StateProps & DispatchProps> = ({
searchMessagesGlobal, searchMessagesGlobal,
openMediaViewer, openMediaViewer,
}) => { }) => {
const lang = useLang();
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (lastSyncTime && direction === LoadMoreDirection.Backwards) { if (lastSyncTime && direction === LoadMoreDirection.Backwards) {
runThrottled(() => { runThrottled(() => {
@ -115,7 +117,12 @@ const MediaResults: FC<OwnProps & StateProps & DispatchProps> = ({
noFastList noFastList
> >
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && <NothingFound />} {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{isMediaGrid && renderGallery()} {isMediaGrid && renderGallery()}
{isMessageList && renderSearchResult()} {isMessageList && renderSearchResult()}
</InfiniteScroll> </InfiniteScroll>

View File

@ -199,7 +199,7 @@ const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
/> />
<p className="settings-item-description"> <p className="settings-item-description">
{renderText(lang('BioAbout'), ['br', 'simple_markdown'])} {renderText(lang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
</p> </p>
</div> </div>
@ -219,7 +219,7 @@ const SettingsEditProfile: FC<StateProps & DispatchProps> = ({
</p> </p>
{username && ( {username && (
<p className="settings-item-description"> <p className="settings-item-description">
This link opens a chat with you:<br /> {lang('lng_username_link')}<br />
<span className="username-link">https://t.me/{username}</span> <span className="username-link">https://t.me/{username}</span>
</p> </p>
)} )}

View File

@ -31,11 +31,6 @@ type StateProps = ISettings['byKey'] & {
type DispatchProps = Pick<GlobalActions, 'setSettingOption' | 'loadStickerSets' | 'loadAddedStickers'>; type DispatchProps = Pick<GlobalActions, 'setSettingOption' | 'loadStickerSets' | 'loadAddedStickers'>;
const KEYBOARD_SEND_OPTIONS = !IS_TOUCH_ENV ? [
{ value: 'enter', label: 'Send by Enter', subLabel: 'New line by Shift + Enter' },
{ value: 'ctrl-enter', label: `Send by ${IS_MAC_OS ? 'Cmd' : 'Ctrl'} + Enter`, subLabel: 'New line by Enter' },
] : undefined;
const ANIMATION_LEVEL_OPTIONS = [ const ANIMATION_LEVEL_OPTIONS = [
'Solid and Steady', 'Solid and Steady',
'Nice and Fast', 'Nice and Fast',
@ -67,6 +62,17 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
const [isModalOpen, openModal, closeModal] = useFlag(); const [isModalOpen, openModal, closeModal] = useFlag();
const [sticker, setSticker] = useState<ApiSticker>(); const [sticker, setSticker] = useState<ApiSticker>();
const lang = useLang();
const KEYBOARD_SEND_OPTIONS = !IS_TOUCH_ENV ? [
{ value: 'enter', label: lang('lng_settings_send_enter'), subLabel: 'New line by Shift + Enter' },
{
value: 'ctrl-enter',
label: lang(IS_MAC_OS ? 'lng_settings_send_cmdenter' : 'lng_settings_send_ctrlenter'),
subLabel: 'New line by Enter',
},
] : undefined;
useEffect(() => { useEffect(() => {
loadStickerSets(); loadStickerSets();
}, [loadStickerSets]); }, [loadStickerSets]);
@ -96,9 +102,6 @@ const SettingsGeneral: FC<OwnProps & StateProps & DispatchProps> = ({
openModal(); openModal();
}, [openModal]); }, [openModal]);
const lang = useLang();
const stickerSets = stickerSetIds && stickerSetIds.map((id: string) => { const stickerSets = stickerSetIds && stickerSetIds.map((id: string) => {
return stickerSetsById && stickerSetsById[id] && stickerSetsById[id].installedDate ? stickerSetsById[id] : false; return stickerSetsById && stickerSetsById[id] && stickerSetsById[id].installedDate ? stickerSetsById[id] : false;
}).filter(Boolean); }).filter(Boolean);

View File

@ -83,7 +83,7 @@ const SettingsHeader: FC<OwnProps & DispatchProps> = ({
function renderHeaderContent() { function renderHeaderContent() {
switch (currentScreen) { switch (currentScreen) {
case SettingsScreens.EditProfile: case SettingsScreens.EditProfile:
return <h3>{lang('EditProfile')}</h3>; return <h3>{lang('lng_settings_information')}</h3>;
case SettingsScreens.General: case SettingsScreens.General:
return <h3>{lang('General')}</h3>; return <h3>{lang('General')}</h3>;
case SettingsScreens.Notifications: case SettingsScreens.Notifications:

View File

@ -40,7 +40,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
icon="edit" icon="edit"
onClick={() => onScreenSelect(SettingsScreens.EditProfile)} onClick={() => onScreenSelect(SettingsScreens.EditProfile)}
> >
{lang('EditProfile')} {lang('lng_settings_information')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="folder" icon="folder"

View File

@ -41,7 +41,7 @@ const SenderInfo: FC<OwnProps & StateProps & DispatchProps> = ({
focusMessage({ chatId, messageId }); focusMessage({ chatId, messageId });
}, [chatId, focusMessage, messageId, closeMediaViewer]); }, [chatId, focusMessage, messageId, closeMediaViewer]);
useLang(); const lang = useLang();
if (!sender || (!message && !isAvatar)) { if (!sender || (!message && !isAvatar)) {
return undefined; return undefined;
@ -62,7 +62,7 @@ const SenderInfo: FC<OwnProps & StateProps & DispatchProps> = ({
{senderTitle && renderText(senderTitle)} {senderTitle && renderText(senderTitle)}
</div> </div>
<div className="date"> <div className="date">
{isAvatar ? 'Profile photo' : formatMediaDateTime(message!.date * 1000)} {isAvatar ? lang('lng_mediaview_profile_photo') : formatMediaDateTime(message!.date * 1000)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -65,7 +65,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage); useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight); useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight);
useLang(); const lang = useLang();
const noAppearanceAnimation = appearanceOrder <= 0; const noAppearanceAnimation = appearanceOrder <= 0;
const [isShown, markShown] = useFlag(noAppearanceAnimation); const [isShown, markShown] = useFlag(noAppearanceAnimation);
@ -79,6 +79,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
const content = renderActionMessageText( const content = renderActionMessageText(
lang,
message, message,
sender, sender,
targetUser, targetUser,

View File

@ -165,7 +165,9 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
icon="delete" icon="delete"
onClick={handleDelete} onClick={handleDelete}
> >
{lang(isPrivate ? 'Delete' : (canDeleteChat ? 'Delete and Leave' : 'Leave'))} {lang(isPrivate
? 'Delete'
: (canDeleteChat ? 'GroupInfo.DeleteAndExit' : (isChannel ? 'LeaveChannel' : 'Group.LeaveGroup')))}
</MenuItem> </MenuItem>
)} )}
</Menu> </Menu>

View File

@ -31,10 +31,11 @@ type OwnProps = {
const HeaderPinnedMessage: FC<OwnProps> = ({ const HeaderPinnedMessage: FC<OwnProps> = ({
message, count, index, customTitle, className, onUnpinMessage, onClick, onAllPinnedClick, message, count, index, customTitle, className, onUnpinMessage, onClick, onAllPinnedClick,
}) => { }) => {
const lang = useLang();
const mediaThumbnail = useWebpThumbnail(message); const mediaThumbnail = useWebpThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram')); const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
const text = getMessageSummaryText(message, Boolean(mediaThumbnail)); const text = getMessageSummaryText(lang, message, Boolean(mediaThumbnail));
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag(); const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
const handleUnpinMessage = useCallback(() => { const handleUnpinMessage = useCallback(() => {
@ -45,8 +46,6 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
} }
}, [closeUnpinDialog, onUnpinMessage, message.id]); }, [closeUnpinDialog, onUnpinMessage, message.id]);
const lang = useLang();
return ( return (
<div className={buildClassName('HeaderPinnedMessage-wrapper', className)}> <div className={buildClassName('HeaderPinnedMessage-wrapper', className)}>
{count > 1 && ( {count > 1 && (

View File

@ -8,9 +8,10 @@ import { GlobalActions } from '../../../global/types';
import { pick } from '../../../util/iteratees'; import { pick } from '../../../util/iteratees';
import { isChatPrivate } from '../../../modules/helpers'; import { isChatPrivate } from '../../../modules/helpers';
import { formatInteger, formatIntegerCompact } from '../../../util/textFormat'; import { formatIntegerCompact } from '../../../util/textFormat';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectThreadInfo } from '../../../modules/selectors'; import { selectThreadInfo } from '../../../modules/selectors';
import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
@ -32,6 +33,7 @@ type DispatchProps = Pick<GlobalActions, 'openChat'>;
const CommentButton: FC<OwnProps & StateProps & DispatchProps> = ({ const CommentButton: FC<OwnProps & StateProps & DispatchProps> = ({
disabled, threadInfo, usersById, chatsById, openChat, disabled, threadInfo, usersById, chatsById, openChat,
}) => { }) => {
const lang = useLang();
const { const {
threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds,
} = threadInfo; } = threadInfo;
@ -76,24 +78,14 @@ const CommentButton: FC<OwnProps & StateProps & DispatchProps> = ({
<i className="icon-comments-sticker" /> <i className="icon-comments-sticker" />
{(!recentRepliers || recentRepliers.length === 0) && <i className="icon-comments" />} {(!recentRepliers || recentRepliers.length === 0) && <i className="icon-comments" />}
{renderRecentRepliers()} {renderRecentRepliers()}
<div className="label">{renderLabel(messagesCount)}</div> <div className="label">
{messagesCount ? lang('Comments', messagesCount, 'i') : lang('LeaveAComment')}
</div>
<i className="icon-next" /> <i className="icon-next" />
</div> </div>
); );
}; };
function renderLabel(messagesCount: number) {
if (messagesCount === 0) {
return 'Leave a Comment';
}
if (messagesCount === 1) {
return '1 Comment';
}
return `${formatInteger(messagesCount)} Comments`;
}
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { message }) => { (global, { message }) => {
const { threadId, chatId } = message.threadInfo!; const { threadId, chatId } = message.threadInfo!;

View File

@ -756,7 +756,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
color="translucent-white" color="translucent-white"
round round
size="tiny" size="tiny"
ariaLabel="Forward message" ariaLabel={lang('lng_context_forward_msg')}
onClick={isLastInDocumentGroup ? handleGroupForward : handleForward} onClick={isLastInDocumentGroup ? handleGroupForward : handleForward}
> >
<i className="icon-share-filled" /> <i className="icon-share-filled" />

View File

@ -21,7 +21,7 @@ export function getMessageCopyOptions(
if (canImageBeCopied) { if (canImageBeCopied) {
options.push({ options.push({
label: 'Copy Media', label: 'lng_context_copy_image',
handler: () => { handler: () => {
mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl).then(copyImageToClipboard); mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl).then(copyImageToClipboard);
@ -57,7 +57,7 @@ export function getMessageCopyOptions(
if (onCopyLink) { if (onCopyLink) {
options.push({ options.push({
label: 'CopyMessageLink', label: 'lng_context_copy_message_link',
handler: () => { handler: () => {
onCopyLink(); onCopyLink();
@ -73,11 +73,11 @@ export function getMessageCopyOptions(
function getCopyLabel(hasSelection: boolean, canImageBeCopied: boolean): string { function getCopyLabel(hasSelection: boolean, canImageBeCopied: boolean): string {
if (hasSelection) { if (hasSelection) {
return 'Copy Selected Text'; return 'lng_context_copy_selected';
} }
if (canImageBeCopied) { if (canImageBeCopied) {
return 'Copy Text'; return 'lng_context_copy_text';
} }
return 'Copy'; return 'Copy';

View File

@ -37,6 +37,7 @@ import useProfileViewportIds from './hooks/useProfileViewportIds';
import useProfileState from './hooks/useProfileState'; import useProfileState from './hooks/useProfileState';
import useTransitionFixes from './hooks/useTransitionFixes'; import useTransitionFixes from './hooks/useTransitionFixes';
import useAsyncRendering from './hooks/useAsyncRendering'; import useAsyncRendering from './hooks/useAsyncRendering';
import useLang from '../../hooks/useLang';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import InfiniteScroll from '../ui/InfiniteScroll'; import InfiniteScroll from '../ui/InfiniteScroll';
@ -120,6 +121,8 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const transitionRef = useRef<HTMLDivElement>(null); const transitionRef = useRef<HTMLDivElement>(null);
const lang = useLang();
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const tabs = useMemo(() => ([ const tabs = useMemo(() => ([
@ -228,16 +231,16 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
text = areMembersHidden ? 'You have no access to group members list.' : 'No members found'; text = areMembersHidden ? 'You have no access to group members list.' : 'No members found';
break; break;
case 'documents': case 'documents':
text = 'No documents found.'; text = lang('lng_media_file_empty_search');
break; break;
case 'links': case 'links':
text = 'No links found.'; text = lang('lng_media_link_empty_search');
break; break;
case 'audio': case 'audio':
text = 'No audio found.'; text = lang('lng_media_song_empty_search');
break; break;
default: default:
text = 'No media found.'; text = lang('SharedMedia.EmptyTitle');
} }
return ( return (

View File

@ -235,7 +235,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
return ( return (
<SearchInput <SearchInput
value={stickerSearchQuery} value={stickerSearchQuery}
placeholder="Search Stickers" placeholder={lang('SearchStickersHint')}
onChange={handleStickerSearchQueryChange} onChange={handleStickerSearchQueryChange}
/> />
); );

View File

@ -17,6 +17,7 @@ import {
isChatChannel, isChatChannel,
} from '../../modules/helpers'; } from '../../modules/helpers';
import renderText from '../common/helpers/renderText'; import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang';
import { orderBy, pick } from '../../util/iteratees'; import { orderBy, pick } from '../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
@ -60,6 +61,8 @@ const RightSearch: FC<OwnProps & StateProps & DispatchProps> = ({
searchTextMessagesLocal, searchTextMessagesLocal,
focusMessage, focusMessage,
}) => { }) => {
const lang = useLang();
const foundResults = useMemo(() => { const foundResults = useMemo(() => {
if (!query || !foundIds || !foundIds.length || !messagesById) { if (!query || !foundIds || !foundIds.length || !messagesById) {
return MEMO_EMPTY_ARRAY; return MEMO_EMPTY_ARRAY;
@ -98,7 +101,7 @@ const RightSearch: FC<OwnProps & StateProps & DispatchProps> = ({
message, senderUser, senderChat, onClick, message, senderUser, senderChat, onClick,
}: Result) => { }: Result) => {
const title = senderChat ? getChatTitle(senderChat) : getUserFullName(senderUser); const title = senderChat ? getChatTitle(senderChat) : getUserFullName(senderUser);
const text = getMessageSummaryText(message); const text = getMessageSummaryText(lang, message);
return ( return (
<ListItem className="chat-item-clickable search-result-message m-0" onClick={onClick}> <ListItem className="chat-item-clickable search-result-message m-0" onClick={onClick}>

View File

@ -12,6 +12,7 @@ import { pick } from '../../util/iteratees';
import { selectShouldLoopStickers, selectStickerSet } from '../../modules/selectors'; import { selectShouldLoopStickers, selectStickerSet } from '../../modules/selectors';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useOnChange from '../../hooks/useOnChange'; import useOnChange from '../../hooks/useOnChange';
import useLang from '../../hooks/useLang';
import Button from '../ui/Button'; import Button from '../ui/Button';
import StickerButton from '../common/StickerButton'; import StickerButton from '../common/StickerButton';
@ -37,6 +38,7 @@ const STICKERS_TO_DISPLAY = 5;
const StickerSetResult: FC<OwnProps & StateProps & DispatchProps> = ({ const StickerSetResult: FC<OwnProps & StateProps & DispatchProps> = ({
stickerSetId, observeIntersection, set, shouldPlay, loadStickers, toggleStickerSet, isSomeModalOpen, onModalToggle, stickerSetId, observeIntersection, set, shouldPlay, loadStickers, toggleStickerSet, isSomeModalOpen, onModalToggle,
}) => { }) => {
const lang = useLang();
const isAdded = set && Boolean(set.installedDate); const isAdded = set && Boolean(set.installedDate);
const areStickersLoaded = Boolean(set && set.stickers); const areStickersLoaded = Boolean(set && set.stickers);
@ -79,7 +81,7 @@ const StickerSetResult: FC<OwnProps & StateProps & DispatchProps> = ({
<div className="sticker-set-header"> <div className="sticker-set-header">
<div className="title-wrapper"> <div className="title-wrapper">
<h3 className="title">{set.title}</h3> <h3 className="title">{set.title}</h3>
<p className="count">{set.count} stickers</p> <p className="count">{lang('Stickers', set.count, 'i')}</p>
</div> </div>
<Button <Button
className={isAdded ? 'is-added' : undefined} className={isAdded ? 'is-added' : undefined}
@ -89,7 +91,7 @@ const StickerSetResult: FC<OwnProps & StateProps & DispatchProps> = ({
fluid fluid
onClick={handleAddClick} onClick={handleAddClick}
> >
{isAdded ? 'Added' : 'Add'} {lang(isAdded ? 'Stickers.Installed' : 'Stickers.Install')}
</Button> </Button>
</div> </div>
<div className="sticker-set-main"> <div className="sticker-set-main">

View File

@ -111,7 +111,7 @@ const ManageGroupRecentActions: FC<OwnProps & StateProps> = ({ chat }) => {
</div> </div>
<div className="section not-implemented"> <div className="section not-implemented">
<h3 className="section-heading">Admins</h3> <h3 className="section-heading">{lang('Channel.Management.Title')}</h3>
<div className="ListItem no-selection"> <div className="ListItem no-selection">
<Checkbox <Checkbox

View File

@ -124,7 +124,7 @@ export const MAX_MEDIA_FILES_FOR_ALBUM = 10;
export const MAX_ACTIVE_PINNED_CHATS = 5; export const MAX_ACTIVE_PINNED_CHATS = 5;
export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE; export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE;
export const DEFAULT_LANG_PACK = 'android'; export const DEFAULT_LANG_PACK = 'android';
export const LANG_PACKS = ['android', 'ios']; export const LANG_PACKS = ['android', 'ios', 'tdesktop'];
export const TIPS_USERNAME = 'TelegramTips'; export const TIPS_USERNAME = 'TelegramTips';
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time'; export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
export const DARK_THEME_BG_COLOR = '#0F0F0F'; export const DARK_THEME_BG_COLOR = '#0F0F0F';

View File

@ -1,7 +1,10 @@
import { useMemo } from '../lib/teact/teact'; import { useMemo } from '../lib/teact/teact';
import { getDispatch } from '../lib/teact/teactn'; import { getDispatch } from '../lib/teact/teactn';
import { ApiChat, ApiUser } from '../api/types'; import { ApiChat, ApiUser } from '../api/types';
import { isChatArchived, getCanDeleteChat, isChatPrivate } from '../modules/helpers'; import {
isChatArchived, getCanDeleteChat, isChatPrivate, isChatChannel,
} from '../modules/helpers';
import useLang from './useLang';
export default ({ export default ({
chat, chat,
@ -16,6 +19,8 @@ export default ({
folderId?: number; folderId?: number;
isPinned?: boolean; isPinned?: boolean;
}) => { }) => {
const lang = useLang();
const { const {
toggleChatPinned, toggleChatPinned,
updateChatMutedState, updateChatMutedState,
@ -31,23 +36,39 @@ export default ({
const isChatWithSelf = privateChatUser && privateChatUser.isSelf; const isChatWithSelf = privateChatUser && privateChatUser.isSelf;
const actionUnreadMark = chat.unreadCount || chat.hasUnreadMark const actionUnreadMark = chat.unreadCount || chat.hasUnreadMark
? { title: 'Mark as Read', icon: 'readchats', handler: () => toggleChatUnread({ id: chat.id }) } ? { title: lang('MarkAsRead'), icon: 'readchats', handler: () => toggleChatUnread({ id: chat.id }) }
: { title: 'Mark as Unread', icon: 'unread', handler: () => toggleChatUnread({ id: chat.id }) }; : { title: lang('MarkAsUnread'), icon: 'unread', handler: () => toggleChatUnread({ id: chat.id }) };
const actionPin = isPinned const actionPin = isPinned
? { title: 'Unpin', icon: 'unpin', handler: () => toggleChatPinned({ id: chat.id, folderId }) } ? {
: { title: 'Pin', icon: 'pin', handler: () => toggleChatPinned({ id: chat.id, folderId }) }; title: lang('UnpinFromTop'),
icon: 'unpin',
handler: () => toggleChatPinned({ id: chat.id, folderId }),
}
: { title: lang('PinToTop'), icon: 'pin', handler: () => toggleChatPinned({ id: chat.id, folderId }) };
const actionMute = chat.isMuted const actionMute = chat.isMuted
? { title: 'Unmute', icon: 'unmute', handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }) } ? {
: { title: 'Mute', icon: 'mute', handler: () => updateChatMutedState({ chatId: chat.id, isMuted: true }) }; title: lang('ChatList.Unmute'),
icon: 'unmute',
handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }),
}
: {
title: lang('ChatList.Mute'),
icon: 'mute',
handler: () => updateChatMutedState({ chatId: chat.id, isMuted: true }),
};
const actionArchive = isChatArchived(chat) const actionArchive = isChatArchived(chat)
? { title: 'Unarchive', icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) } ? { title: lang('Unarchive'), icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) }
: { title: 'Archive', icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) }; : { title: lang('Archive'), icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) };
const actionDelete = { const actionDelete = {
title: isChatPrivate(chat.id) ? 'Delete' : (getCanDeleteChat(chat) ? 'Delete and Leave' : 'Leave'), title: isChatPrivate(chat.id)
? lang('Delete')
: lang(getCanDeleteChat(chat)
? 'DeleteChat'
: (isChatChannel(chat) ? 'LeaveChannel' : 'Group.LeaveGroup')),
icon: 'delete', icon: 'delete',
destructive: true, destructive: true,
handler: handleDelete, handler: handleDelete,
@ -63,7 +84,7 @@ export default ({
actionDelete, actionDelete,
]; ];
}, [ }, [
chat, privateChatUser, handleDelete, folderId, isPinned, chat, privateChatUser, lang, isPinned, handleDelete, toggleChatUnread, toggleChatPinned, folderId,
toggleChatPinned, updateChatMutedState, toggleChatArchived, toggleChatUnread, updateChatMutedState, toggleChatArchived,
]); ]);
}; };

View File

@ -428,7 +428,7 @@ export function getMessageSenderName(chatId: number, sender?: ApiUser) {
} }
if (sender.isSelf) { if (sender.isSelf) {
return 'You'; return getTranslation('FromYou');
} }
return getUserFirstOrLastName(sender); return getUserFirstOrLastName(sender);

View File

@ -1,6 +1,7 @@
import { import {
ApiChat, ApiMessage, ApiMessageEntityTypes, ApiUser, ApiChat, ApiMessage, ApiMessageEntityTypes, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { LangFn } from '../../hooks/useLang';
import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID, RE_LINK_TEMPLATE } from '../../config'; import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID, RE_LINK_TEMPLATE } from '../../config';
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString'; import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
@ -26,62 +27,34 @@ export function getMessageOriginalId(message: ApiMessage) {
return message.previousLocalId || message.id; return message.previousLocalId || message.id;
} }
export function getMessageSummaryText(message: ApiMessage, noEmoji = false) { export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji = false) {
const { const {
text, photo, video, audio, voice, document, sticker, contact, poll, invoice, text, photo, video, audio, voice, document, sticker, contact, poll, invoice,
} = message.content; } = message.content;
if (message.groupedId) { if (message.groupedId) {
if (text) { return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('lng_in_dlg_album')}`;
return `${noEmoji ? '' : '🖼 '}${text.text}`;
}
return 'Album';
} }
if (photo) { if (photo) {
if (text) { return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('AttachPhoto')}`;
return `${noEmoji ? '' : '🖼 '}${text.text}`;
}
return 'Photo';
} }
if (video) { if (video) {
if (video.isGif) { return `${noEmoji ? '' : '📹 '}${text ? text.text : lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`;
if (text) {
return `${noEmoji ? '' : 'GIF '}${text.text}`;
}
return 'GIF';
} else {
if (text) {
return `${noEmoji ? '' : '📹 '}${text.text}`;
}
return 'Video';
}
} }
if (sticker) { if (sticker) {
return `${sticker.emoji} Sticker`; return `${sticker.emoji} ${lang('AttachSticker')} `;
} }
if (audio) { if (audio) {
const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text); const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text);
if (caption) { return `${noEmoji ? '' : '🎧 '}${caption || lang('AttachMusic')}`;
return `🎧 ${caption}`;
}
return 'Audio';
} }
if (voice) { if (voice) {
if (text) { return `${noEmoji ? '' : '🎤 '}${text ? text.text : lang('AttachAudio')}`;
return `${noEmoji ? '' : '🎤 '}${text.text}`;
}
return 'Voice Message';
} }
if (document) { if (document) {
@ -89,11 +62,11 @@ export function getMessageSummaryText(message: ApiMessage, noEmoji = false) {
} }
if (contact) { if (contact) {
return 'Contact'; return lang('AttachContact');
} }
if (poll) { if (poll) {
return `📊 ${poll.summary.question}`; return `${noEmoji ? '' : '📊 '}${poll.summary.question}`;
} }
if (invoice) { if (invoice) {
@ -107,60 +80,6 @@ export function getMessageSummaryText(message: ApiMessage, noEmoji = false) {
return CONTENT_NOT_SUPPORTED; return CONTENT_NOT_SUPPORTED;
} }
export function getNotificationText(message: ApiMessage) {
const {
text, photo, video, audio, voice, document, sticker, contact, poll, invoice,
} = message.content;
if (message.groupedId) {
return `🖼 ${text ? text.text : 'Album'}`;
}
if (photo) {
return `🖼 ${text ? text.text : 'Photo'}`;
}
if (video) {
return `📹 ${text ? text.text : video.isGif ? 'GIF' : 'Video'}`;
}
if (sticker) {
return `${sticker.emoji} Sticker `;
}
if (audio) {
const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text);
return `🎧 ${caption || 'Audio'}`;
}
if (voice) {
return `🎤 ${text ? text.text : 'Voice Message'}`;
}
if (document) {
return `📎 ${text ? text.text : document.fileName}`;
}
if (contact) {
return 'Contact';
}
if (poll) {
return `📊 ${poll.summary.question}`;
}
if (invoice) {
return 'Invoice';
}
if (text) {
return text.text;
}
return CONTENT_NOT_SUPPORTED;
}
export function getMessageText(message: ApiMessage) { export function getMessageText(message: ApiMessage) {
const { const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice,

View File

@ -70,7 +70,7 @@ export function getUserStatus(user: ApiUser, lang: LangFn) {
} }
if (user.type && user.type === 'userTypeBot') { if (user.type && user.type === 'userTypeBot') {
return 'bot'; return lang('Bot');
} }
if (!user.status) { if (!user.status) {

View File

@ -7,11 +7,12 @@ import {
getChatTitle, getChatTitle,
getMessageAction, getMessageAction,
getMessageSenderName, getMessageSenderName,
getNotificationText, getMessageSummaryText,
getPrivateChatUserId, getPrivateChatUserId,
isActionMessage, isActionMessage,
isChatChannel, isChatChannel,
} from '../modules/helpers'; } from '../modules/helpers';
import { getTranslation } from './langProvider';
import { replaceSettings } from '../modules/reducers'; import { replaceSettings } from '../modules/reducers';
import { selectChatMessage, selectUser } from '../modules/selectors'; import { selectChatMessage, selectUser } from '../modules/selectors';
import { IS_SERVICE_WORKER_SUPPORTED } from './environment'; import { IS_SERVICE_WORKER_SUPPORTED } from './environment';
@ -225,6 +226,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
? chat ? chat
: messageSender; : messageSender;
body = renderActionMessageText( body = renderActionMessageText(
getTranslation,
message, message,
actionOrigin, actionOrigin,
actionTargetUser, actionTargetUser,
@ -234,7 +236,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
) as string; ) as string;
} else { } else {
const senderName = getMessageSenderName(chat.id, messageSender); const senderName = getMessageSenderName(chat.id, messageSender);
const summary = getNotificationText(message); const summary = getMessageSummaryText(getTranslation, message);
body = senderName ? `${senderName}: ${summary}` : summary; body = senderName ? `${senderName}: ${summary}` : summary;
} }

View File

@ -137,7 +137,7 @@ export function expectCommentButton(
expect(button.querySelector('.label')).toHaveTextContent(`${commentsCount} Comments`); expect(button.querySelector('.label')).toHaveTextContent(`${commentsCount} Comments`);
expect(button.querySelectorAll('.Avatar')).toHaveLength(Math.min(authorsCount, 3)); expect(button.querySelectorAll('.Avatar')).toHaveLength(Math.min(authorsCount, 3));
} else { } else {
expect(button.querySelector('.label')).toHaveTextContent('Leave a Comment'); expect(button.querySelector('.label')).toHaveTextContent('Leave a comment');
expect(button.querySelectorAll('.Avatar')).toHaveLength(0); expect(button.querySelectorAll('.Avatar')).toHaveLength(0);
} }