diff --git a/src/components/ContributionHeatmap.tsx b/src/components/ContributionHeatmap.tsx index 6c759ea6..727ac7dc 100644 --- a/src/components/ContributionHeatmap.tsx +++ b/src/components/ContributionHeatmap.tsx @@ -6,6 +6,7 @@ import { TEXT_OPACITY, scrollbarSx, } from '../theme'; +import { pluralize } from '../utils/format'; interface ContributionData { date: string; @@ -146,7 +147,7 @@ const ContributionHeatmap: React.FC = ({ onClick={() => onDayClick?.(activity.date)} style={{ cursor: 'pointer' }} role="button" - aria-label={`View ${activity.count} contribution${activity.count !== 1 ? 's' : ''} on ${activity.date}`} + aria-label={`View ${pluralize(activity.count, 'contribution')} on ${activity.date}`} > {highlighted} @@ -155,7 +156,7 @@ const ContributionHeatmap: React.FC = ({ ); return ( = ({ {[ baseScore > 0 && `base ${baseScore.toFixed(2)}`, `+${pr.additions} / -${pr.deletions}`, - `${pr.commitCount} commit${pr.commitCount !== 1 ? 's' : ''}`, + pluralize(pr.commitCount, 'commit'), pr.tokenScore != null && `tokens ${parseNumber(pr.tokenScore).toFixed(2)}`, pr.totalNodesScored != null && diff --git a/src/pages/RepositoriesPage.tsx b/src/pages/RepositoriesPage.tsx index 195b8f3c..63454777 100644 --- a/src/pages/RepositoriesPage.tsx +++ b/src/pages/RepositoriesPage.tsx @@ -11,6 +11,7 @@ import { useAllPrs, useAllMiners, useReposAndWeights } from '../api'; import { type CommitLog } from '../api/models/Dashboard'; import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; import { buildRepoDiscoveryRollupFromMiners } from '../utils/ExplorerUtils'; +import { pluralize } from '../utils/format'; import { isMergedPr } from '../utils/prStatus'; const FONTS = { mono: '"JetBrains Mono", monospace' } as const; @@ -509,8 +510,8 @@ const RepositoriesPage: React.FC = () => { whiteSpace: 'nowrap', }} > - {repo.collateral.toFixed(1)} ({repo.openPRs} PR - {repo.openPRs !== 1 ? 's' : ''}) + {repo.collateral.toFixed(1)} ( + {pluralize(repo.openPRs, 'PR')}) } /> diff --git a/src/pages/RepositoryDetailsPage.tsx b/src/pages/RepositoryDetailsPage.tsx index 5dd932cf..f4915dc4 100644 --- a/src/pages/RepositoryDetailsPage.tsx +++ b/src/pages/RepositoryDetailsPage.tsx @@ -8,6 +8,7 @@ import React, { } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; +import { pluralize } from '../utils/format'; import { Alert, Box, @@ -272,7 +273,7 @@ const RepositoryDetailsPage: React.FC = () => { lineHeight: 1.4, }} > - {openBounties} {openBounties === 1 ? 'bounty' : 'bounties'} + {pluralize(openBounties, 'bounty', 'bounties')} ) : null} diff --git a/src/pages/dashboard/views/FeaturedWorkRepoCard.tsx b/src/pages/dashboard/views/FeaturedWorkRepoCard.tsx index 51ac2502..d3850f90 100644 --- a/src/pages/dashboard/views/FeaturedWorkRepoCard.tsx +++ b/src/pages/dashboard/views/FeaturedWorkRepoCard.tsx @@ -3,6 +3,7 @@ import GitHubIcon from '@mui/icons-material/GitHub'; import { Avatar, Box, Stack, Tooltip, Typography } from '@mui/material'; import { type Theme } from '@mui/material/styles'; import { getGithubAvatarSrc } from '../../../utils'; +import { pluralize } from '../../../utils/format'; import { type FeaturedWorkRepo, type FeaturedWorkPr } from '../dashboardData'; import FeaturedWorkPrRow from './FeaturedWorkPrRow'; import { @@ -25,8 +26,7 @@ interface FeaturedWorkRepoCardProps { const extractOwner = (repository: string): string => repository.split('/')[0] || ''; -const formatPrCount = (count: number): string => - `${count} PR${count !== 1 ? 's' : ''}`; +const formatPrCount = (count: number): string => pluralize(count, 'PR'); const formatTotalScore = (score: number): string => score.toFixed(1); diff --git a/src/pages/search/SearchPage.tsx b/src/pages/search/SearchPage.tsx index 41368b98..cd49b9e9 100644 --- a/src/pages/search/SearchPage.tsx +++ b/src/pages/search/SearchPage.tsx @@ -3,6 +3,7 @@ import { Alert, Box, Stack, Tab, Tabs, Typography } from '@mui/material'; import { useLocation, useSearchParams } from 'react-router-dom'; import { BackButton, SEO } from '../../components'; import { GlobalSearchBar, Page } from '../../components/layout'; +import { pluralize } from '../../utils/format'; import IssuesTab from './IssuesTab'; import MinerTab from './MinerTab'; import PullRequestsTab from './PullRequestsTab'; @@ -209,7 +210,7 @@ const SearchPage: React.FC = () => { {isAnySectionLoading && totalResults === 0 ? `Loading search results for "${query}"...` - : `${activeResultCount} result${activeResultCount === 1 ? '' : 's'} in ${activeTabLabel} for "${query}"`} + : `${pluralize(activeResultCount, 'result')} in ${activeTabLabel} for "${query}"`} { export const getLowerText = (value: string | null | undefined): string => (value ?? '').toLowerCase(); + +/** + * `1 issue` / `2 issues`. Pass an explicit `plural` for irregular forms, + * e.g. `pluralize(n, 'repository', 'repositories')`. + */ +export const pluralize = ( + count: number, + singular: string, + plural?: string, +): string => + count === 1 ? `${count} ${singular}` : `${count} ${plural ?? `${singular}s`}`; diff --git a/src/utils/repoConfig.ts b/src/utils/repoConfig.ts index 07165510..4f48698f 100644 --- a/src/utils/repoConfig.ts +++ b/src/utils/repoConfig.ts @@ -10,6 +10,7 @@ * to the default below. */ import type { RepositoryConfig } from '../api/models/Dashboard'; +import { pluralize } from './format'; type RepoConfigFormat = | 'integer' @@ -347,9 +348,9 @@ export function formatRepoConfigValue( case 'multiplier': return `${value}×`; case 'days': - return `${value} ${value === 1 ? 'day' : 'days'}`; + return pluralize(value, 'day'); case 'hours': - return `${value} ${value === 1 ? 'hr' : 'hrs'}`; + return pluralize(value, 'hr'); case 'integer': case 'score': case 'decimal':