Format numbers internationally.

This commit is contained in:
Franco Montenegro 2022-04-26 12:28:23 -03:00 committed by jessopb
parent c5b7cc5ac4
commit 5319232918
12 changed files with 64 additions and 59 deletions

View file

@ -6,6 +6,7 @@ import CreditAmount from 'component/common/credit-amount';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
import YoutubeBadge from 'component/youtubeBadge'; import YoutubeBadge from 'component/youtubeBadge';
import SUPPORTED_LANGUAGES from 'constants/supported_languages'; import SUPPORTED_LANGUAGES from 'constants/supported_languages';
import { formatNumber } from 'util/number';
type Props = { type Props = {
claim: ChannelClaim, claim: ChannelClaim,
@ -74,7 +75,7 @@ function ChannelAbout(props: Props) {
</div> </div>
<label>{__('Total Uploads')}</label> <label>{__('Total Uploads')}</label>
<div className="media__info-text">{claim.meta.claims_in_channel}</div> <div className="media__info-text">{formatNumber(claim.meta.claims_in_channel || 0, 2, true)}</div>
<label>{__('Last Updated')}</label> <label>{__('Last Updated')}</label>
<div className="media__info-text"> <div className="media__info-text">

View file

@ -9,7 +9,7 @@ import * as COLLECTIONS_CONSTS from 'constants/collections';
import { isChannelClaim } from 'util/claim'; import { isChannelClaim } from 'util/claim';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import { toCompactNotation } from 'util/string'; import { formatNumber } from 'util/number';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import FileThumbnail from 'component/fileThumbnail'; import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
@ -138,7 +138,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
indexInContainer, indexInContainer,
channelSubCount, channelSubCount,
swipeLayout = false, swipeLayout = false,
lang,
showEdit, showEdit,
dragHandleProps, dragHandleProps,
unavailableUris, unavailableUris,
@ -162,8 +161,8 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
if (channelSubCount === undefined) { if (channelSubCount === undefined) {
return <span />; return <span />;
} }
const formattedSubCount = toCompactNotation(channelSubCount, lang, 10000); const formattedSubCount = formatNumber(channelSubCount, 2, true);
const formattedSubCountLocale = Number(channelSubCount).toLocaleString(); const formattedSubCountLocale = formatNumber(channelSubCount, 2, false);
return ( return (
<div className="media__subtitle"> <div className="media__subtitle">
<Tooltip title={formattedSubCountLocale} followCursor placement="top"> <Tooltip title={formattedSubCountLocale} followCursor placement="top">

View file

@ -5,6 +5,7 @@ import DateTime from 'component/dateTime';
import Button from 'component/button'; import Button from 'component/button';
import FileViewCountInline from 'component/fileViewCountInline'; import FileViewCountInline from 'component/fileViewCountInline';
import { parseURI } from 'util/lbryURI'; import { parseURI } from 'util/lbryURI';
import { formatNumber } from 'util/number';
type Props = { type Props = {
uri: string, uri: string,
@ -34,7 +35,7 @@ function ClaimPreviewSubtitle(props: Props) {
<> <>
{isChannel && {isChannel &&
type !== 'inline' && type !== 'inline' &&
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`} `${formatNumber(claimsInChannel, 2, true)} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
{!isChannel && ( {!isChannel && (
<> <>

View file

@ -8,6 +8,7 @@ import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { SITE_NAME, ENABLE_COMMENT_REACTIONS } from 'config'; import { SITE_NAME, ENABLE_COMMENT_REACTIONS } from 'config';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { parseURI } from 'util/lbryURI'; import { parseURI } from 'util/lbryURI';
import { formatNumber } from 'util/number';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
import Button from 'component/button'; import Button from 'component/button';
import Expandable from 'component/expandable'; import Expandable from 'component/expandable';
@ -384,7 +385,7 @@ function CommentView(props: Props) {
label={ label={
numDirectReplies < 2 numDirectReplies < 2
? __('Show reply') ? __('Show reply')
: __('Show %count% replies', { count: numDirectReplies }) : __('Show %count% replies', { count: formatNumber(numDirectReplies, 2, true) })
} }
button="link" button="link"
onClick={() => { onClick={() => {

View file

@ -9,6 +9,7 @@ import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import { formatNumber } from 'util/number';
type Props = { type Props = {
myReacts: Array<string>, myReacts: Array<string>,
@ -109,7 +110,11 @@ export default function CommentReactions(props: Props) {
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE), 'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
})} })}
onClick={handleCommentLike} onClick={handleCommentLike}
label={<span className="comment__reaction-count">{getCountForReact(REACTION_TYPES.LIKE)}</span>} label={
<span className="comment__reaction-count">
{formatNumber(getCountForReact(REACTION_TYPES.LIKE), 2, true)}
</span>
}
/> />
<Button <Button
title={__('Downvote')} title={__('Downvote')}
@ -119,7 +124,11 @@ export default function CommentReactions(props: Props) {
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE), 'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
})} })}
onClick={handleCommentDislike} onClick={handleCommentDislike}
label={<span className="comment__reaction-count">{getCountForReact(REACTION_TYPES.DISLIKE)}</span>} label={
<span className="comment__reaction-count">
{formatNumber(getCountForReact(REACTION_TYPES.DISLIKE), 2, true)}
</span>
}
/> />
{!shouldHide && ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && ( {!shouldHide && ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && (

View file

@ -4,7 +4,7 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Button from 'component/button'; import Button from 'component/button';
import { formatNumberWithCommas } from 'util/number'; import { formatNumber } from 'util/number';
import NudgeFloating from 'component/nudgeFloating'; import NudgeFloating from 'component/nudgeFloating';
type Props = { type Props = {
claim: StreamClaim, claim: StreamClaim,
@ -18,16 +18,8 @@ type Props = {
}; };
function FileReactions(props: Props) { function FileReactions(props: Props) {
const { const { claim, uri, doFetchReactions, doReactionLike, doReactionDislike, myReaction, likeCount, dislikeCount } =
claim, props;
uri,
doFetchReactions,
doReactionLike,
doReactionDislike,
myReaction,
likeCount,
dislikeCount,
} = props;
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const channel = claim && claim.signing_channel && claim.signing_channel.name; const channel = claim && claim.signing_channel && claim.signing_channel.name;
@ -59,7 +51,7 @@ function FileReactions(props: Props) {
className={classnames('button--file-action button-like', { className={classnames('button--file-action button-like', {
'button--file-action-active': myReaction === REACTION_TYPES.LIKE, 'button--file-action-active': myReaction === REACTION_TYPES.LIKE,
})} })}
label={<>{formatNumberWithCommas(likeCount, 0)}</>} label={<>{formatNumber(likeCount, 2, true)}</>}
iconSize={18} iconSize={18}
icon={likeIcon} icon={likeIcon}
onClick={() => doReactionLike(uri)} onClick={() => doReactionLike(uri)}
@ -70,7 +62,7 @@ function FileReactions(props: Props) {
className={classnames('button--file-action button-dislike', { className={classnames('button--file-action button-dislike', {
'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE, 'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE,
})} })}
label={<>{formatNumberWithCommas(dislikeCount, 0)}</>} label={<>{formatNumber(dislikeCount, 2, true)}</>}
iconSize={18} iconSize={18}
icon={dislikeIcon} icon={dislikeIcon}
onClick={() => doReactionDislike(uri)} onClick={() => doReactionDislike(uri)}

View file

@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import HelpLink from 'component/common/help-link'; import HelpLink from 'component/common/help-link';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import { toCompactNotation } from 'util/string'; import { formatNumber } from 'util/number';
type Props = { type Props = {
claimId: ?string, // this claimId: ?string, // this
@ -14,8 +14,8 @@ type Props = {
function FileViewCount(props: Props) { function FileViewCount(props: Props) {
const { claimId, uri, fetchViewCount, viewCount } = props; // claimId const { claimId, uri, fetchViewCount, viewCount } = props; // claimId
const countCompact = toCompactNotation(viewCount); const countCompact = formatNumber(Number(viewCount), 2, true);
const countFullResolution = Number(viewCount).toLocaleString(); const countFullResolution = formatNumber(Number(viewCount), 2, false);
React.useEffect(() => { React.useEffect(() => {
if (claimId) { if (claimId) {
@ -24,7 +24,7 @@ function FileViewCount(props: Props) {
}, [fetchViewCount, uri, claimId]); }, [fetchViewCount, uri, claimId]);
return ( return (
<Tooltip label={countFullResolution}> <Tooltip title={`${countFullResolution}`}>
<span className="media__subtitle--centered"> <span className="media__subtitle--centered">
{viewCount !== 1 ? __('%view_count% views', { view_count: countCompact }) : __('1 view')} {viewCount !== 1 ? __('%view_count% views', { view_count: countCompact }) : __('1 view')}
{<HelpLink href="https://lbry.com/faq/views" />} {<HelpLink href="https://lbry.com/faq/views" />}

View file

@ -1,4 +1,6 @@
// @flow // @flow
import { formatCredits } from 'util/format-credits';
type Props = { type Props = {
uri: ?string, uri: ?string,
isResolvingUri: boolean, isResolvingUri: boolean,
@ -23,7 +25,7 @@ function BidHelpText(props: Props) {
bidHelpText = __( bidHelpText = __(
'If you bid more than %amount% LBRY Credits, when someone navigates to %uri%, it will load your published content. However, you can get a longer version of this URL for any bid.', 'If you bid more than %amount% LBRY Credits, when someone navigates to %uri%, it will load your published content. However, you can get a longer version of this URL for any bid.',
{ {
amount: amountNeededForTakeover, amount: formatCredits(amountNeededForTakeover, 2, true),
uri: uri, uri: uri,
} }
); );

View file

@ -3,6 +3,7 @@ import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React from 'react'; import React from 'react';
import { parseURI } from 'util/lbryURI'; import { parseURI } from 'util/lbryURI';
import { formatNumber } from 'util/number';
import { YOUTUBE_STATUSES } from 'lbryinc'; import { YOUTUBE_STATUSES } from 'lbryinc';
import Page from 'component/page'; import Page from 'component/page';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
@ -93,7 +94,7 @@ function ChannelPage(props: Props) {
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
const { permanent_url: permanentUrl } = claim; const { permanent_url: permanentUrl } = claim;
const claimId = claim.claim_id; const claimId = claim.claim_id;
const formattedSubCount = Number(subCount).toLocaleString(); const formattedSubCount = formatNumber(subCount, 2, true);
const isBlocked = claim && blockedChannels.includes(claim.permanent_url); const isBlocked = claim && blockedChannels.includes(claim.permanent_url);
const isMuted = claim && mutedChannels.includes(claim.permanent_url); const isMuted = claim && mutedChannels.includes(claim.permanent_url);
const isMyYouTubeChannel = const isMyYouTubeChannel =

View file

@ -2,6 +2,7 @@
import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment'; import { SORT_COMMENTS_NEW, SORT_COMMENTS_BEST, SORT_COMMENTS_CONTROVERSIAL } from 'constants/comment';
import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers'; import { FREE_GLOBAL_STICKERS, PAID_GLOBAL_STICKERS } from 'constants/stickers';
import * as REACTION_TYPES from 'constants/reactions'; import * as REACTION_TYPES from 'constants/reactions';
import { formatNumber } from 'util/number';
const ALL_VALID_STICKERS = [...FREE_GLOBAL_STICKERS, ...PAID_GLOBAL_STICKERS]; const ALL_VALID_STICKERS = [...FREE_GLOBAL_STICKERS, ...PAID_GLOBAL_STICKERS];
const stickerRegex = /(<stkr>:[A-Z0-9_]+:<stkr>)/; const stickerRegex = /(<stkr>:[A-Z0-9_]+:<stkr>)/;
@ -118,7 +119,7 @@ export function getCommentsListTitle(totalComments: number) {
const title = const title =
(totalComments === 0 && __('Leave a comment')) || (totalComments === 0 && __('Leave a comment')) ||
(totalComments === 1 && __('1 comment')) || (totalComments === 1 && __('1 comment')) ||
__('%total_comments% comments', { total_comments: totalComments }); __('%total_comments% comments', { total_comments: formatNumber(totalComments, 2, true) });
return title; return title;
} }

View file

@ -1,39 +1,27 @@
function numberWithCommas(x) {
var parts = x.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
}
export function formatCredits(amount, precision, shortFormat = false) { export function formatCredits(amount, precision, shortFormat = false) {
let actualAmount = parseFloat(amount); const actualAmount = Number(amount);
let actualPrecision = parseFloat(precision); const safePrecision = Math.min(20, Math.max(1, precision));
let suffix = '';
if (Number.isNaN(actualAmount) || actualAmount === 0) return '0'; if (Number.isNaN(actualAmount) || actualAmount === 0) return '0';
if (actualAmount >= 1000000) {
if (precision <= 7) {
if (shortFormat) { if (shortFormat) {
actualAmount = actualAmount / 1000000; const formatter = new Intl.NumberFormat(undefined, {
suffix = 'M'; minimumFractionDigits: safePrecision,
} else { maximumFractionDigits: safePrecision,
actualPrecision -= 7; roundingIncrement: 5,
} // Display suffix (M, K, etc.)
} notation: 'compact',
} else if (actualAmount >= 1000) { compactDisplay: 'short',
if (precision <= 4) { });
if (shortFormat) { return formatter.format(actualAmount);
actualAmount = actualAmount / 1000;
suffix = 'K';
} else {
actualPrecision -= 4;
}
}
} }
return ( const formatter = new Intl.NumberFormat(undefined, {
numberWithCommas(actualAmount.toFixed(actualPrecision >= 0 ? actualPrecision : 1).replace(/\.*0+$/, '')) + suffix minimumFractionDigits: safePrecision,
); maximumFractionDigits: safePrecision,
roundingIncrement: 5,
});
return formatter.format(actualAmount);
} }
export function formatFullPrice(amount, precision = 1) { export function formatFullPrice(amount, precision = 1) {

View file

@ -1,5 +1,15 @@
// @flow // @flow
export function formatNumber(num: number, numberOfDigits?: number, short: boolean = false): string {
const safePrecision = Math.min(20, numberOfDigits || 0);
const formatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: safePrecision,
notation: short ? 'compact' : 'standard',
compactDisplay: 'short',
});
return formatter.format(num);
}
export function formatNumberWithCommas(num: number, numberOfDigits?: number): string { export function formatNumberWithCommas(num: number, numberOfDigits?: number): string {
return num.toLocaleString('en', { minimumFractionDigits: numberOfDigits !== undefined ? numberOfDigits : 8 }); return num.toLocaleString('en', { minimumFractionDigits: numberOfDigits !== undefined ? numberOfDigits : 8 });
} }