Format numbers internationally.
This commit is contained in:
parent
c5b7cc5ac4
commit
5319232918
12 changed files with 64 additions and 59 deletions
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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={() => {
|
||||||
|
|
|
@ -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) && (
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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" />}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (shortFormat) {
|
||||||
if (precision <= 7) {
|
const formatter = new Intl.NumberFormat(undefined, {
|
||||||
if (shortFormat) {
|
minimumFractionDigits: safePrecision,
|
||||||
actualAmount = actualAmount / 1000000;
|
maximumFractionDigits: safePrecision,
|
||||||
suffix = 'M';
|
roundingIncrement: 5,
|
||||||
} else {
|
// Display suffix (M, K, etc.)
|
||||||
actualPrecision -= 7;
|
notation: 'compact',
|
||||||
}
|
compactDisplay: 'short',
|
||||||
}
|
});
|
||||||
} else if (actualAmount >= 1000) {
|
return formatter.format(actualAmount);
|
||||||
if (precision <= 4) {
|
|
||||||
if (shortFormat) {
|
|
||||||
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) {
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue