Folders: Support custom emoji in title (#5385)

Co-authored-by: Dmitry Kabanov <dmitrykabanovdev@gmail.com>
Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com>
This commit is contained in:
zubiden 2025-01-03 17:15:30 +01:00 committed by Alexander Zinchuk
parent e42b148721
commit 3b486614df
17 changed files with 146 additions and 68 deletions

View File

@ -26,7 +26,9 @@ import type {
import { pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers';
import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common';
import {
buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId,
} from './common';
import { omitVirtualClassFields } from './helpers';
import {
buildApiEmojiStatus,
@ -419,7 +421,8 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi
pinnedChatIds: filter.pinnedPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
hasMyInvites: filter.hasMyInvites,
isChatList: true,
title: filter.title.text,
noTitleAnimations: filter.titleNoanimate,
title: buildApiFormattedText(filter.title),
};
}
@ -432,7 +435,8 @@ export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFi
pinnedChatIds: filter.pinnedPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
includedChatIds: filter.includePeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
excludedChatIds: filter.excludePeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
title: filter.title.text,
title: buildApiFormattedText(filter.title),
noTitleAnimations: filter.titleNoanimate,
};
}

View File

@ -238,6 +238,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
pinnedChatIds,
includedChatIds,
excludedChatIds,
noTitleAnimations,
} = folder;
const pinnedPeers = pinnedChatIds
@ -255,17 +256,18 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
if (folder.isChatList) {
return new GramJs.DialogFilterChatlist({
id: folder.id,
title: buildInputTextWithEntities({ text: folder.title, entities: [] }),
title: buildInputTextWithEntities(folder.title),
emoticon: emoticon || undefined,
pinnedPeers,
includePeers,
hasMyInvites: folder.hasMyInvites,
titleNoanimate: noTitleAnimations,
});
}
return new GramJs.DialogFilter({
id: folder.id,
title: buildInputTextWithEntities({ text: folder.title, entities: [] }),
title: buildInputTextWithEntities(folder.title),
emoticon: emoticon || undefined,
contacts: contacts || undefined,
nonContacts: nonContacts || undefined,
@ -278,6 +280,7 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
pinnedPeers,
includePeers,
excludePeers,
titleNoanimate: noTitleAnimations,
});
}

View File

@ -207,7 +207,8 @@ export interface ApiRestrictionReason {
export interface ApiChatFolder {
id: number;
title: string;
title: ApiFormattedText;
noTitleAnimations?: true;
description?: string;
emoticon?: string;
contacts?: true;

View File

@ -1406,3 +1406,6 @@
"StarsSubscribeBotText_one" = "Do you want to subscribe to **{name}** in **{bot}** for **{amount}** star per month?"
"StarsSubscribeBotText_other" = "Do you want to subscribe to **{name}** in **{bot}** for **{amount}** stars per month?"
"StarsSubscribeBotButtonMonth" = "Subscribe for {amount} / month";
"FolderLinkTitleDescription" = "Anyone with this link can add {folder} folder and {chats} selected below.";
"FolderLinkTitleDescriptionChats_one" = "the chat";
"FolderLinkTitleDescriptionChats_other" = "the {count} chats";

View File

@ -46,6 +46,7 @@ export function renderTextWithEntities({
sharedCanvasHqRef,
cacheBuster,
forcePlayback,
noCustomEmojiPlayback,
focusedQuote,
isInSelectMode,
}: {
@ -65,6 +66,7 @@ export function renderTextWithEntities({
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
cacheBuster?: string;
forcePlayback?: boolean;
noCustomEmojiPlayback?: boolean;
focusedQuote?: string;
isInSelectMode?: boolean;
}) {
@ -173,6 +175,7 @@ export function renderTextWithEntities({
sharedCanvasHqRef,
cacheBuster,
forcePlayback,
noCustomEmojiPlayback,
isInSelectMode,
});
@ -388,6 +391,7 @@ function processEntity({
sharedCanvasHqRef,
cacheBuster,
forcePlayback,
noCustomEmojiPlayback,
isInSelectMode,
} : {
entity: ApiMessageEntity;
@ -407,6 +411,7 @@ function processEntity({
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
cacheBuster?: string;
forcePlayback?: boolean;
noCustomEmojiPlayback?: boolean;
isInSelectMode?: boolean;
}) {
const entityText = typeof entityContent === 'string' && entityContent;
@ -447,6 +452,7 @@ function processEntity({
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumb={withTranslucentThumbs}
forceAlways={forcePlayback}
noPlay={noCustomEmojiPlayback}
/>
);
}
@ -581,6 +587,7 @@ function processEntity({
observeIntersectionForPlaying={observeIntersectionForPlaying}
withTranslucentThumb={withTranslucentThumbs}
forceAlways={forcePlayback}
noPlay={noCustomEmojiPlayback}
/>
);
default:

View File

@ -8,6 +8,7 @@ import type { ApiChatFolder } from '../../api/types';
import { ALL_FOLDER_ID } from '../../config';
import buildClassName from '../../util/buildClassName';
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
import useOldLang from '../../hooks/useOldLang';
@ -59,10 +60,19 @@ const ChatFolderModal: FC<OwnProps & StateProps> = ({
const [selectedFolderIds, setSelectedFolderIds] = useState<string[]>(initialSelectedFolderIds);
const folders = useMemo(() => {
return folderOrderedIds?.filter((folderId) => folderId !== ALL_FOLDER_ID).map((folderId) => ({
label: foldersById ? foldersById[folderId].title : '',
value: String(folderId),
})) || [];
return folderOrderedIds?.filter((folderId) => folderId !== ALL_FOLDER_ID)
.map((folderId) => {
const folder = foldersById ? foldersById[folderId] : undefined;
const label = folder ? renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
}) : '';
return {
label,
value: String(folderId),
};
}) || [];
}, [folderOrderedIds, foldersById]);
const handleSubmit = useCallback(() => {

View File

@ -19,6 +19,7 @@ import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useDerivedState from '../../../hooks/useDerivedState';
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
@ -114,7 +115,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
const allChatsFolder: ApiChatFolder = useMemo(() => {
return {
id: ALL_FOLDER_ID,
title: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats'),
title: { text: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats') },
includedChatIds: MEMO_EMPTY_ARRAY,
excludedChatIds: MEMO_EMPTY_ARRAY,
} satisfies ApiChatFolder;
@ -197,7 +198,11 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
return {
id,
title,
title: renderTextWithEntities({
text: title.text,
entities: title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
}),
badgeCount: folderCountersById[id]?.chatsCount,
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
isBlocked,
@ -335,7 +340,6 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
tabs={folderTabs}
activeTab={activeChatFolder}
onSwitchTab={handleSwitchTab}
areFolders
/>
) : shouldRenderPlaceholder ? (
<div ref={placeholderRef} className="tabs-placeholder" />

View File

@ -298,7 +298,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
<InputText
className="mb-0"
label={lang('FilterNameHint')}
value={state.folder.title}
value={state.folder.title.text}
onChange={handleChange}
error={state.error && state.error === ERROR_NO_TITLE ? ERROR_NO_TITLE : undefined}
/>

View File

@ -14,7 +14,7 @@ import { isBetween } from '../../../../util/math';
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
import { throttle } from '../../../../util/schedulers';
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
import renderText from '../../../common/helpers/renderText';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import { useFolderManagerForChatsCount } from '../../../../hooks/useFolderManager';
import useHistoryBack from '../../../../hooks/useHistoryBack';
@ -133,7 +133,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
if (id === ALL_FOLDER_ID) {
return {
id,
title: lang('FilterAllChats'),
title: {
text: lang('FilterAllChats'),
entities: [],
},
};
}
@ -142,6 +145,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
title: folder.title,
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
isChatList: folder.isChatList,
noTitleAnimations: folder.noTitleAnimations,
};
});
}, [folderIds, foldersById, lang, chatsCountByFolderId]);
@ -252,7 +256,11 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
allowSelection
>
<span className="title">
{folder.title}
{renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
})}
</span>
<span className="subtitle">{lang('FoldersAllChatsDesc')}</span>
</ListItem>
@ -297,7 +305,11 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
}}
>
<span className="title">
{renderText(folder.title, ['emoji'])}
{renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
})}
{isBlocked && <i className="icon icon-lock-badge settings-folders-blocked-icon" />}
</span>
<span className="subtitle">
@ -330,7 +342,13 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
>
<div className="settings-folders-recommended-item">
<div className="multiline-item">
<span className="title">{renderText(folder.title, ['emoji'])}</span>
<span className="title">
{renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
})}
</span>
<span className="subtitle">{folder.description}</span>
</div>

View File

@ -4,6 +4,8 @@ import React, {
} from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { ApiChatFolder } from '../../../../api/types';
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
import { isChatChannel, isUserBot } from '../../../../global/helpers';
import {
@ -13,10 +15,11 @@ import {
} from '../../../../global/selectors';
import { partition } from '../../../../util/iteratees';
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
import renderText from '../../../common/helpers/renderText';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import useHistoryBack from '../../../../hooks/useHistoryBack';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
@ -33,9 +36,7 @@ type OwnProps = {
type StateProps = {
folderId?: number;
title?: string;
includedChatIds?: string[];
pinnedChatIds?: string[];
folder?: ApiChatFolder;
peerIds?: string[];
url?: string;
isLoading?: boolean;
@ -45,9 +46,7 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
isActive,
onReset,
folderId,
title,
includedChatIds,
pinnedChatIds,
folder,
peerIds,
url,
isLoading,
@ -55,7 +54,9 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
const {
createChatlistInvite, deleteChatlistInvite, editChatlistInvite, showNotification,
} = getActions();
const lang = useOldLang();
const lang = useLang();
const oldLang = useOldLang();
const [isTouched, setIsTouched] = useState(false);
@ -84,8 +85,8 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
});
const itemIds = useMemo(() => {
return (includedChatIds || []).concat(pinnedChatIds || []);
}, [includedChatIds, pinnedChatIds]);
return (folder?.includedChatIds || []).concat(folder?.pinnedChatIds || []);
}, [folder?.includedChatIds, folder?.pinnedChatIds]);
const [unlockedIds, lockedIds] = useMemo(() => {
const global = getGlobal();
@ -114,19 +115,19 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
const chat = selectChat(global, id);
if (user && isUserBot(user)) {
showNotification({
message: lang('FolderLinkScreen.AlertTextUnavailableBot'),
message: oldLang('FolderLinkScreen.AlertTextUnavailableBot'),
});
} else if (user) {
showNotification({
message: lang('FolderLinkScreen.AlertTextUnavailableUser'),
message: oldLang('FolderLinkScreen.AlertTextUnavailableUser'),
});
} else if (chat && isChatChannel(chat)) {
showNotification({
message: lang('FolderLinkScreen.AlertTextUnavailablePublicChannel'),
message: oldLang('FolderLinkScreen.AlertTextUnavailablePublicChannel'),
});
} else {
showNotification({
message: lang('FolderLinkScreen.AlertTextUnavailablePublicGroup'),
message: oldLang('FolderLinkScreen.AlertTextUnavailablePublicGroup'),
});
}
});
@ -153,15 +154,26 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
className="settings-content-icon"
/>
<p className="settings-item-description mb-3" dir="auto">
{renderText(lang('FolderLinkScreen.TitleDescriptionSelected', [title, chatsCount]),
['simple_markdown'])}
</p>
{folder && (
<p className="settings-item-description mb-3" dir="auto">
{lang('FolderLinkTitleDescription', {
folder: renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
}),
chats: lang('FolderLinkTitleDescriptionChats', { count: chatsCount }, { pluralValue: chatsCount }),
}, {
withMarkdown: true,
withNodes: true,
})}
</p>
)}
</div>
<LinkField
className="settings-item"
link={!url ? lang('Loading') : url}
link={!url ? oldLang('Loading') : url}
withShare
onRevoke={handleRevoke}
isDisabled={!chatsCount || isTouched}
@ -204,9 +216,7 @@ export default memo(withGlobal<OwnProps>(
return {
folderId,
title: folder?.title,
includedChatIds: folder?.includedChatIds,
pinnedChatIds: folder?.pinnedChatIds,
folder,
url,
isLoading,
peerIds: invite?.peerIds,

View File

@ -1,4 +1,4 @@
import type { FC } from '../../../lib/teact/teact';
import type { FC, TeactNode } from '../../../lib/teact/teact';
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -6,6 +6,7 @@ import type { ApiChatFolder } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { selectChatFolder } from '../../../global/selectors';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import useOldLang from '../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
@ -55,7 +56,13 @@ const ChatlistInviteModal: FC<OwnProps & StateProps> = ({
}, [lang, renderingInfo]);
const renderingFolderTitle = useMemo(() => {
if (renderingFolder) return renderingFolder.title;
if (renderingFolder) {
return renderTextWithEntities({
text: renderingFolder.title.text,
entities: renderingFolder.title.entities,
noCustomEmojiPlayback: renderingFolder.noTitleAnimations,
});
}
if (renderingInfo?.invite && 'title' in renderingInfo.invite) return renderingInfo.invite.title;
return undefined;
}, [renderingFolder, renderingInfo]);
@ -66,12 +73,18 @@ const ChatlistInviteModal: FC<OwnProps & StateProps> = ({
return undefined;
}, [renderingInfo]);
function renderFolders(folderTitle: string) {
function renderFolders(folderTitle: TeactNode) {
return (
<div className={styles.foldersWrapper}>
<div className={styles.folders}>
<Tab className={styles.folder} title={lang('FolderLinkPreviewLeft')} />
<Tab className={styles.folder} isActive badgeCount={folderTabNumber} isBadgeActive title={folderTitle} />
<Tab
className={styles.folder}
isActive
badgeCount={folderTabNumber}
isBadgeActive
title={folderTitle}
/>
<Tab className={styles.folder} title={lang('FolderLinkPreviewRight')} />
</div>
</div>

View File

@ -49,6 +49,7 @@
display: flex;
align-items: center;
white-space: nowrap;
gap: 1px; // Prevent custom emoji sticking to the text
}
.badge {

View File

@ -1,4 +1,4 @@
import type { FC } from '../../lib/teact/teact';
import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact';
import type { MenuItemContextAction } from './ListItem';
@ -21,7 +21,7 @@ import './Tab.scss';
type OwnProps = {
className?: string;
title: string;
title: TeactNode;
isActive?: boolean;
isBlocked?: boolean;
badgeCount?: number;
@ -139,7 +139,7 @@ const Tab: FC<OwnProps> = ({
ref={tabRef}
>
<span className="Tab_inner">
{renderText(title)}
{typeof title === 'string' ? renderText(title) : title}
{Boolean(badgeCount) && (
<span className={buildClassName('badge', isBadgeActive && classNames.badgeActive)}>{badgeCount}</span>
)}

View File

@ -1,9 +1,8 @@
import type { FC } from '../../lib/teact/teact';
import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
import type { MenuItemContextAction } from './ListItem';
import { ALL_FOLDER_ID } from '../../config';
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
import buildClassName from '../../util/buildClassName';
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
@ -18,7 +17,7 @@ import './TabList.scss';
export type TabWithProperties = {
id?: number;
title: string;
title: TeactNode;
badgeCount?: number;
isBlocked?: boolean;
isBadgeActive?: boolean;
@ -27,7 +26,6 @@ export type TabWithProperties = {
type OwnProps = {
tabs: readonly TabWithProperties[];
areFolders?: boolean;
activeTab: number;
className?: string;
tabClassName?: string;
@ -40,7 +38,7 @@ const TAB_SCROLL_THRESHOLD_PX = 16;
const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
const TabList: FC<OwnProps> = ({
tabs, areFolders, activeTab, onSwitchTab,
tabs, activeTab, onSwitchTab,
contextRootElementSelector, className, tabClassName,
}) => {
// eslint-disable-next-line no-null/no-null
@ -83,9 +81,8 @@ const TabList: FC<OwnProps> = ({
>
{tabs.map((tab, i) => (
<Tab
key={tab.id ?? tab.title}
// TODO Remove dependency on usage context
title={(!areFolders || tab.id === ALL_FOLDER_ID) ? lang(tab.title) : tab.title}
key={tab.id}
title={tab.title}
isActive={i === activeTab}
isBlocked={tab.isBlocked}
badgeCount={tab.badgeCount}

View File

@ -124,7 +124,7 @@ const INITIAL_STATE: FoldersState = {
mode: 'create',
chatFilter: '',
folder: {
title: '',
title: { text: '' },
includedChatIds: [],
excludedChatIds: [],
},
@ -133,14 +133,14 @@ const INITIAL_STATE: FoldersState = {
const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
state,
action,
) => {
): FoldersState => {
switch (action.type) {
case 'setTitle':
return {
...state,
folder: {
...state.folder,
title: action.payload,
title: { text: action.payload },
},
isTouched: true,
};
@ -184,7 +184,7 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
...state,
folder: {
...omit(state.folder, INCLUDE_FILTER_FIELDS),
title: state.folder.title ? state.folder.title : getSuggestedFolderName(state.includeFilters),
title: state.folder.title ? state.folder.title : { text: getSuggestedFolderName(state.includeFilters) },
...state.includeFilters,
},
includeFilters: undefined,

View File

@ -1574,6 +1574,10 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'StarsSubscribeBotButtonMonth': {
'amount': V;
};
'FolderLinkTitleDescription': {
'folder': V;
'chats': V;
};
}
export interface LangPairPlural {
@ -1755,6 +1759,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'bot': V;
'amount': V;
};
'FolderLinkTitleDescriptionChats': {
'count': V;
};
}
export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;

View File

@ -122,11 +122,11 @@ export type LangFn = {
<K extends PluralLangKey = PluralLangKey>(
key: K, variables: undefined, options: LangFnOptionsWithPlural,
): string;
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
key: K, variables: V, options?: LangFnOptions,
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables>(
key: K, variables: LangPairWithVariables[K], options?: LangFnOptions,
): string;
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables, V = LangPairPluralWithVariables[K]>(
key: K, variables: V, options: LangFnOptionsWithPlural,
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables>(
key: K, variables: LangPairPluralWithVariables[K], options: LangFnOptionsWithPlural,
): string;
<K extends RegularLangKey = RegularLangKey>(
@ -135,11 +135,11 @@ export type LangFn = {
<K extends PluralLangKey = PluralLangKey>(
key: K, variables: undefined, options: AdvancedLangFnOptionsWithPlural,
): TeactNode;
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
key: K, variables: V, options: AdvancedLangFnOptions,
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables>(
key: K, variables: LangPairWithVariables<TeactNode | undefined>[K], options: AdvancedLangFnOptions,
): TeactNode;
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables, V = LangPairPluralWithVariables[K]>(
key: K, variables: V, options: AdvancedLangFnOptionsWithPlural,
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables>(
key: K, variables: LangPairPluralWithVariables<TeactNode | undefined>[K], options: AdvancedLangFnOptionsWithPlural,
): TeactNode;
with: (params: LangFnParameters) => TeactNode;