Localization: Respect locale number format for floats (#5863)

This commit is contained in:
zubiden 2025-04-23 18:59:49 +02:00 committed by Alexander Zinchuk
parent b3c6ae2089
commit 29ec767eb2
16 changed files with 85 additions and 59 deletions

View File

@ -6,6 +6,7 @@ import { getAllNotificationsCount } from '../../util/folderManager';
import { formatIntegerCompact } from '../../util/textFormat';
import { useFolderManagerForUnreadCounters } from '../../hooks/useFolderManager';
import useLang from '../../hooks/useLang';
interface OwnProps {
isForAppBadge?: boolean;
@ -15,6 +16,8 @@ const UnreadCounter: FC<OwnProps> = ({ isForAppBadge }) => {
useFolderManagerForUnreadCounters();
const unreadNotificationsCount = getAllNotificationsCount();
const lang = useLang();
useEffect(() => {
if (isForAppBadge) {
updateAppBadge(unreadNotificationsCount);
@ -26,7 +29,7 @@ const UnreadCounter: FC<OwnProps> = ({ isForAppBadge }) => {
}
return (
<div className="unread-count active">{formatIntegerCompact(unreadNotificationsCount)}</div>
<div className="unread-count active">{formatIntegerCompact(lang, unreadNotificationsCount)}</div>
);
};

View File

@ -69,7 +69,7 @@ const SavedGift = ({
const ribbonText = gift.isPinned && gift.gift.type === 'starGiftUnique'
? lang('GiftSavedNumber', { number: gift.gift.number })
: totalIssued
? lang('ActionStarGiftLimitedRibbon', { total: formatIntegerCompact(totalIssued) })
? lang('ActionStarGiftLimitedRibbon', { total: formatIntegerCompact(lang, totalIssued) })
: undefined;
const {

View File

@ -118,7 +118,7 @@ const Archive: FC<OwnProps> = ({
</div>
<Badge
className={styles.unreadCount}
text={archiveUnreadCount ? formatIntegerCompact(archiveUnreadCount) : undefined}
text={archiveUnreadCount ? formatIntegerCompact(lang, archiveUnreadCount) : undefined}
/>
</div>
</div>
@ -143,7 +143,7 @@ const Archive: FC<OwnProps> = ({
</div>
<Badge
className={styles.unreadCount}
text={archiveUnreadCount ? formatIntegerCompact(archiveUnreadCount) : undefined}
text={archiveUnreadCount ? formatIntegerCompact(lang, archiveUnreadCount) : undefined}
/>
</div>
</div>

View File

@ -12,6 +12,7 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import useDerivedState from '../../../hooks/useDerivedState';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@ -52,6 +53,7 @@ const ChatBadge: FC<OwnProps> = ({
const { requestMainWebView } = getActions();
const oldLang = useOldLang();
const lang = useLang();
const {
unreadMentionsCount = 0, unreadReactionsCount = 0,
@ -136,7 +138,7 @@ const ChatBadge: FC<OwnProps> = ({
const unreadCountElement = (hasUnreadMark || unreadCount) ? (
<div className={className}>
{!hasUnreadMark && <AnimatedCounter text={formatIntegerCompact(unreadCount!)} />}
{!hasUnreadMark && <AnimatedCounter text={formatIntegerCompact(lang, unreadCount!)} />}
</div>
) : undefined;

View File

@ -20,6 +20,7 @@ import { formatIntegerCompact } from '../../util/textFormat';
import useFlag from '../../hooks/useFlag';
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
@ -67,7 +68,8 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
const chatsById = getGlobal().chats.byId;
const usersById = getGlobal().users.byId;
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const [isClosing, startClosing, stopClosing] = useFlag(false);
const [chosenTab, setChosenTab] = useState<ApiReaction | undefined>(undefined);
const canShowFilters = reactors && reactions && reactors.count >= MIN_REACTIONS_COUNT_FOR_FILTERS
@ -143,11 +145,11 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
isOpen={isOpen && !isClosing}
onClose={handleClose}
className="ReactorListModal narrow"
title={lang('Reactions')}
title={oldLang('Reactions')}
onCloseAnimationEnd={handleCloseAnimationEnd}
>
{canShowFilters && (
<div className="Reactions" dir={lang.isRtl ? 'rtl' : undefined}>
<div className="Reactions" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
className={buildClassName(!chosenTab && 'chosen')}
size="tiny"
@ -156,7 +158,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
onClick={() => setChosenTab(undefined)}
>
<Icon name="heart" />
{Boolean(reactors?.count) && formatIntegerCompact(reactors.count)}
{Boolean(reactors?.count) && formatIntegerCompact(lang, reactors.count)}
</Button>
{allReactions.map((reaction) => {
const count = reactions?.results
@ -175,14 +177,14 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
className="reaction-filter-emoji"
availableReactions={availableReactions}
/>
{Boolean(count) && formatIntegerCompact(count)}
{Boolean(count) && formatIntegerCompact(lang, count)}
</Button>
);
})}
</div>
)}
<div dir={lang.isRtl ? 'rtl' : undefined} className="reactor-list-wrapper">
<div dir={oldLang.isRtl ? 'rtl' : undefined} className="reactor-list-wrapper">
{viewportIds?.length ? (
<InfiniteScroll
className="reactor-list custom-scroll"
@ -212,7 +214,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
<FullNameTitle peer={peer} withEmojiStatus />
<span className="status" dir="auto">
<Icon name="heart-outline" className="status-icon" />
{formatDateAtTime(lang, r.addedDate * 1000)}
{formatDateAtTime(oldLang, r.addedDate * 1000)}
</span>
</div>
{r.reaction && (
@ -238,7 +240,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
userId={peerId}
noStatusOrTyping
avatarSize="medium"
status={seenByUser ? formatDateAtTime(lang, seenByUser * 1000) : undefined}
status={seenByUser ? formatDateAtTime(oldLang, seenByUser * 1000) : undefined}
statusIcon="message-read"
/>
</ListItem>,
@ -255,7 +257,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
isText
onClick={handleClose}
>
{lang('Close')}
{oldLang('Close')}
</Button>
</Modal>
);

View File

@ -7,6 +7,7 @@ import buildClassName from '../../util/buildClassName';
import { formatIntegerCompact } from '../../util/textFormat';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon';
@ -33,7 +34,8 @@ const ScrollDownButton: FC<OwnProps> = ({
onReadAll,
className,
}) => {
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
@ -52,11 +54,11 @@ const ScrollDownButton: FC<OwnProps> = ({
className={styles.button}
onClick={onClick}
onContextMenu={handleContextMenu}
ariaLabel={lang(ariaLabelLang)}
ariaLabel={oldLang(ariaLabelLang)}
>
<Icon name={icon} className={styles.icon} />
</Button>
{Boolean(unreadCount) && <div className={styles.unreadCount}>{formatIntegerCompact(unreadCount)}</div>}
{Boolean(unreadCount) && <div className={styles.unreadCount}>{formatIntegerCompact(lang, unreadCount)}</div>}
{onReadAll && (
<Menu
isOpen={isContextMenuOpen}
@ -66,7 +68,7 @@ const ScrollDownButton: FC<OwnProps> = ({
positionX="right"
positionY="bottom"
>
<MenuItem icon="readchats" onClick={onReadAll}>{lang('MarkAllAsRead')}</MenuItem>
<MenuItem icon="readchats" onClick={onReadAll}>{oldLang('MarkAllAsRead')}</MenuItem>
</Menu>
)}
</div>

View File

@ -8,6 +8,7 @@ import { selectIsCurrentUserFrozen, selectPeer } from '../../../global/selectors
import buildClassName from '../../../util/buildClassName';
import { formatIntegerCompact } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
@ -40,7 +41,8 @@ const CommentButton: FC<OwnProps> = ({
const shouldRenderLoading = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const {
originMessageId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
} = threadInfo;
@ -76,7 +78,7 @@ const CommentButton: FC<OwnProps> = ({
function renderRecentRepliers() {
return (
Boolean(recentRepliers?.length) && (
<div className="recent-repliers" dir={lang.isRtl ? 'rtl' : 'ltr'}>
<div className="recent-repliers" dir={oldLang.isRtl ? 'rtl' : 'ltr'}>
{recentRepliers!.map((peer) => (
<Avatar
key={peer.id}
@ -91,16 +93,16 @@ const CommentButton: FC<OwnProps> = ({
const hasUnread = Boolean(lastReadInboxMessageId && lastMessageId && lastReadInboxMessageId < lastMessageId);
const commentsText = messagesCount ? (lang('CommentsCount', '%COMMENTS_COUNT%', undefined, messagesCount) as string)
const commentsText = messagesCount ? (oldLang('CommentsCount', '%COMMENTS_COUNT%', undefined, messagesCount))
.split('%')
.map((s) => {
return (s === 'COMMENTS_COUNT' ? <AnimatedCounter text={formatIntegerCompact(messagesCount)} /> : s);
return (s === 'COMMENTS_COUNT' ? <AnimatedCounter text={formatIntegerCompact(lang, messagesCount)} /> : s);
})
: undefined;
return (
<div
data-cnt={formatIntegerCompact(messagesCount)}
data-cnt={formatIntegerCompact(lang, messagesCount)}
className={buildClassName(
'CommentButton',
hasUnread && 'has-unread',
@ -109,7 +111,7 @@ const CommentButton: FC<OwnProps> = ({
isLoading && 'loading',
asActionButton && 'as-action-button',
)}
dir={lang.isRtl ? 'rtl' : 'ltr'}
dir={oldLang.isRtl ? 'rtl' : 'ltr'}
onClick={handleClick}
role="button"
tabIndex={0}
@ -124,7 +126,7 @@ const CommentButton: FC<OwnProps> = ({
{!recentRepliers?.length && <Icon name="comments" />}
{renderRecentRepliers()}
<div className="label" dir="auto">
{messagesCount ? commentsText : lang('LeaveAComment')}
{messagesCount ? commentsText : oldLang('LeaveAComment')}
</div>
<div className="CommentButton_right">
{isLoading && (

View File

@ -112,10 +112,14 @@ const MessageMeta: FC<OwnProps> = ({
const viewsTitle = useMemo(() => {
if (!message.viewsCount) return undefined;
let text = lang('MessageTooltipViews', { count: message.viewsCount }, { pluralValue: message.viewsCount });
let text = lang('MessageTooltipViews', {
count: lang.number(message.viewsCount),
}, { pluralValue: message.viewsCount });
if (message.forwardsCount) {
text += '\n';
text += lang('MessageTooltipForwards', { count: message.forwardsCount }, { pluralValue: message.forwardsCount });
text += lang('MessageTooltipForwards', {
count: lang.number(message.forwardsCount),
}, { pluralValue: message.forwardsCount });
}
return text;
@ -160,7 +164,7 @@ const MessageMeta: FC<OwnProps> = ({
{Boolean(message.viewsCount) && (
<>
<span className="message-views" title={viewsTitle}>
{formatIntegerCompact(message.viewsCount!)}
{formatIntegerCompact(lang, message.viewsCount!)}
</span>
<Icon name="channelviews" />
</>
@ -168,7 +172,7 @@ const MessageMeta: FC<OwnProps> = ({
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
<span onClick={handleOpenThread} className="message-replies-wrapper" title={repliesTitle}>
<span className="message-replies">
<AnimatedCounter text={formatIntegerCompact(repliesThreadInfo!.messagesCount!)} />
<AnimatedCounter text={formatIntegerCompact(lang, repliesThreadInfo!.messagesCount!)} />
</span>
<Icon name="reply-filled" />
</span>

View File

@ -19,6 +19,7 @@ import useTimeout from '../../../hooks/schedulers/useTimeout';
import useAverageColor from '../../../hooks/useAverageColor';
import useFlag from '../../../hooks/useFlag';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@ -203,13 +204,14 @@ const SimilarChannels = ({
function SimilarChannel({ channel }: { channel: ApiChat }) {
const { openChat } = getActions();
const color = useAverageColor(channel, DEFAULT_BADGE_COLOR);
const lang = useLang();
return (
<div className={styles.item} onClick={() => openChat({ id: channel.id })}>
<Avatar className={styles.avatar} key={channel.id} size="large" peer={channel} />
<div style={`background: ${color}`} className={styles.badge}>
<Icon name="user-filled" className={styles.icon} />
<span className={styles.membersCount}>{formatIntegerCompact(channel?.membersCount || 0)}
<span className={styles.membersCount}>{formatIntegerCompact(lang, channel?.membersCount || 0)}
</span>
</div>
<span className={styles.channelTitle}>{channel.title}</span>

View File

@ -145,7 +145,9 @@ const StarGiftAction = ({
{action.gift.availabilityTotal && (
<GiftRibbon
color={backgroundColor || 'blue'}
text={lang('ActionStarGiftLimitedRibbon', { total: formatIntegerCompact(action.gift.availabilityTotal) })}
text={lang('ActionStarGiftLimitedRibbon', {
total: formatIntegerCompact(lang, action.gift.availabilityTotal),
})}
/>
)}
<div className={styles.info}>

View File

@ -15,6 +15,7 @@ import { REM } from '../../../common/helpers/mediaDimensions';
import useSelector from '../../../../hooks/data/useSelector';
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import usePrevious from '../../../../hooks/usePrevious';
import useShowTransition from '../../../../hooks/useShowTransition';
@ -76,6 +77,8 @@ const ReactionButton = ({
const counterRef = useRef<HTMLSpanElement>(null);
const animationRef = useRef<Animation>();
const lang = useLang();
const isPaid = reaction.reaction.type === 'paid';
const starsState = useSelector(selectStarsState);
@ -198,7 +201,7 @@ const ReactionButton = ({
{shouldRenderPaidCounter && (
<AnimatedCounter
ref={counterRef}
text={`+${formatIntegerCompact(reaction.localAmount || prevAmount!)}`}
text={`+${formatIntegerCompact(lang, reaction.localAmount || prevAmount!)}`}
className={styles.paidCounter}
/>
)}
@ -216,7 +219,7 @@ const ReactionButton = ({
<AvatarList size="mini" peers={recentReactors} />
) : (
<AnimatedCounter
text={formatIntegerCompact(reaction.count + (reaction.localAmount || 0))}
text={formatIntegerCompact(lang, reaction.count + (reaction.localAmount || 0))}
className={styles.counter}
/>
)}

View File

@ -12,6 +12,7 @@ import buildClassName from '../../../util/buildClassName';
import { formatIntegerCompact } from '../../../util/textFormat';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PeerBadge from '../../common/PeerBadge';
@ -33,6 +34,8 @@ function WebAppGridItem({ user, isPopularApp }: OwnProps & StateProps) {
requestMainWebView,
} = getActions();
const lang = useLang();
const handleClick = useLastCallback(() => {
if (!user) {
return;
@ -55,7 +58,7 @@ function WebAppGridItem({ user, isPopularApp }: OwnProps & StateProps) {
const title = user?.firstName;
const activeUserCount = user?.botActiveUsers;
const badgeText = activeUserCount && isPopularApp ? formatIntegerCompact(activeUserCount) : undefined;
const badgeText = activeUserCount && isPopularApp ? formatIntegerCompact(lang, activeUserCount) : undefined;
return (
<div

View File

@ -11,6 +11,7 @@ import buildClassName from '../../../util/buildClassName';
import { formatFullDate } from '../../../util/dates/dateFormat';
import { formatInteger, formatIntegerCompact } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
@ -121,7 +122,8 @@ const StatisticsOverview: FC<OwnProps> = ({
className,
subtitle,
}) => {
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
if (!change) {
@ -132,7 +134,9 @@ const StatisticsOverview: FC<OwnProps> = ({
return (
<span className={buildClassName(styles.value, isChangeNegative && styles.negative)}>
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
{isChangeNegative
? `-${formatIntegerCompact(lang, Math.abs(change))}`
: `+${formatIntegerCompact(lang, change)}`}
{percentage && (
<>
{' '}
@ -156,7 +160,7 @@ const StatisticsOverview: FC<OwnProps> = ({
<span className={styles.tableHeading}>
${integerUsdPart}<span className={styles.decimalUsdPart}>.{decimalUsdPart}</span>
</span>
<h3 className={styles.tableHeading}>{lang(text)}</h3>
<h3 className={styles.tableHeading}>{oldLang(text)}</h3>
</div>
);
};
@ -177,7 +181,7 @@ const StatisticsOverview: FC<OwnProps> = ({
{period && (
<div className={styles.caption}>
{formatFullDate(lang, period.minDate * 1000)} {formatFullDate(lang, period.maxDate * 1000)}
{formatFullDate(oldLang, period.minDate * 1000)} {formatFullDate(oldLang, period.maxDate * 1000)}
</div>
)}
</div>
@ -202,7 +206,7 @@ const StatisticsOverview: FC<OwnProps> = ({
<b className={styles.tableValue}>
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
</b>
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
</td>
);
}
@ -218,7 +222,7 @@ const StatisticsOverview: FC<OwnProps> = ({
<span className={cell.withAbsoluteValue ? styles.tableSecondaryValue : styles.tableValue}>
{field.percentage}%
</span>
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
</td>
);
}
@ -226,11 +230,11 @@ const StatisticsOverview: FC<OwnProps> = ({
return (
<td className={styles.tableCell}>
<b className={styles.tableValue}>
{formatIntegerCompact(field.current)}
{formatIntegerCompact(lang, field.current)}
</b>
{' '}
{renderOverviewItemValue(field)}
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
</td>
);
})}

View File

@ -4,6 +4,7 @@ import type { StatisticsMessageInteractionCounter, StatisticsStoryInteractionCou
import { formatIntegerCompact } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
@ -15,25 +16,26 @@ interface OwnProps {
}
function StatisticsRecentPostMeta({ postStatistic }: OwnProps) {
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
return (
<div className={styles.meta}>
{postStatistic.reactionsCount > 0 && (
<span className={styles.metaWithIcon}>
<Icon name="heart-outline" className={styles.metaIcon} />
{formatIntegerCompact(postStatistic.reactionsCount)}
{formatIntegerCompact(lang, postStatistic.reactionsCount)}
</span>
)}
{postStatistic.forwardsCount > 0 && (
<span className={styles.metaWithIcon}>
<Icon name="forward" className={styles.metaIcon} />
{formatIntegerCompact(postStatistic.forwardsCount)}
{formatIntegerCompact(lang, postStatistic.forwardsCount)}
</span>
)}
{!postStatistic.forwardsCount && !postStatistic.reactionsCount
&& lang('ChannelStats.SharesCount_ZeroValueHolder')}
&& oldLang('ChannelStats.SharesCount_ZeroValueHolder')}
</div>
);
}

View File

@ -1,6 +1,8 @@
export const clamp = (num: number, min: number, max: number) => (Math.min(max, Math.max(min, num)));
export const isBetween = (num: number, min: number, max: number) => (num >= min && num <= max);
export const round = (num: number, decimals: number = 0) => Math.round(num * 10 ** decimals) / 10 ** decimals;
export const ceil = (num: number, decimals: number = 0) => Math.ceil(num * 10 ** decimals) / 10 ** decimals;
export const floor = (num: number, decimals: number = 0) => Math.floor(num * 10 ** decimals) / 10 ** decimals;
export const lerp = (start: number, end: number, interpolationRatio: number) => {
return (1 - interpolationRatio) * start + interpolationRatio * end;
};

View File

@ -1,32 +1,25 @@
import type { OldLangFn } from '../hooks/useOldLang';
import type { LangFn } from './localization';
import EMOJI_REGEX from '../lib/twemojiRegex';
import fixNonStandardEmoji from './emoji/fixNonStandardEmoji';
import { floor } from './math';
import withCache from './withCache';
export function formatInteger(value: number) {
return String(value).replace(/\d(?=(\d{3})+$)/g, '$& ');
}
function formatFixedNumber(number: number) {
const fixed = String(number.toFixed(1));
if (fixed.substr(-2) === '.0') {
return Math.floor(number);
}
return number.toFixed(1).replace('.', ',');
}
export function formatIntegerCompact(views: number) {
export function formatIntegerCompact(lang: LangFn, views: number) {
if (views < 1e3) {
return views.toString();
return lang.number(views);
}
if (views < 1e6) {
return `${formatFixedNumber(views / 1e3)}K`;
return `${lang.number(floor(views / 1e3, 1))}K`;
}
return `${formatFixedNumber(views / 1e6)}M`;
return `${lang.number(floor(views / 1e6, 1))}M`;
}
export function formatPercent(value: number, fractionDigits = 1) {