diff --git a/web/src/lib/components/home/SocialPost.svelte b/web/src/lib/components/home/SocialPost.svelte
index 6d70d5244..324a5d00c 100644
--- a/web/src/lib/components/home/SocialPost.svelte
+++ b/web/src/lib/components/home/SocialPost.svelte
@@ -4,6 +4,7 @@
import Repeat2 from "$icon/repeat-2";
import Avatar from "$lib/components/ui/Avatar.svelte";
import type { SocialPostData } from "$lib/components/home/content";
+ import { plural } from "$lib/format";
interface Props {
post: SocialPostData;
@@ -13,9 +14,13 @@
let { post, class: className = "" }: Props = $props();
const stats = $derived([
- { icon: MessageCircle, count: post.replies, label: "replies" },
- { icon: Repeat2, count: post.reposts, label: "reposts" },
- { icon: Heart, count: post.likes, label: "likes" }
+ {
+ icon: MessageCircle,
+ count: post.replies,
+ label: plural(post.replies, "reply", "replies")
+ },
+ { icon: Repeat2, count: post.reposts, label: plural(post.reposts, "repost", "reposts") },
+ { icon: Heart, count: post.likes, label: plural(post.likes, "like", "likes") }
]);
diff --git a/web/src/lib/components/import/GitHubRepoSelectStep.svelte b/web/src/lib/components/import/GitHubRepoSelectStep.svelte
index f21120cc0..a86cf671c 100644
--- a/web/src/lib/components/import/GitHubRepoSelectStep.svelte
+++ b/web/src/lib/components/import/GitHubRepoSelectStep.svelte
@@ -6,6 +6,7 @@
import type { Draft } from "./gitHubImportState.svelte";
import Button from "$lib/components/ui/Button.svelte";
import FormStep from "$lib/components/ui/FormStep.svelte";
+ import { plural } from "$lib/format";
import RepoConfig from "./RepoConfig.svelte";
import RepoRow from "./RepoRow.svelte";
import { isMigratable } from "./gitHubImportState.svelte";
@@ -132,7 +133,7 @@
diff --git a/web/src/lib/components/profile/FollowCardContent.svelte b/web/src/lib/components/profile/FollowCardContent.svelte
index 9985c4b6d..76cc6e7d3 100644
--- a/web/src/lib/components/profile/FollowCardContent.svelte
+++ b/web/src/lib/components/profile/FollowCardContent.svelte
@@ -6,7 +6,7 @@
import type { FollowChange, PersonData } from "$lib/components/profile/types";
import { createOptimisticCount } from "$lib/optimistic.svelte";
import Separator from "$lib/components/ui/Separator.svelte";
- import { formatCount } from "$lib/format";
+ import { formatCount, pluralize } from "$lib/format";
let {
person,
@@ -60,7 +60,7 @@
>{formatCount(followerCount.value)} followers{pluralize(followerCount.value, "follower", "followers")}
diff --git a/web/src/lib/components/profile/FollowerFollowing.svelte b/web/src/lib/components/profile/FollowerFollowing.svelte
index 16f5841ca..3eafa0607 100644
--- a/web/src/lib/components/profile/FollowerFollowing.svelte
+++ b/web/src/lib/components/profile/FollowerFollowing.svelte
@@ -2,7 +2,7 @@
import { resolve } from "$app/paths";
import Users from "$icon/users";
import Separator from "$lib/components/ui/Separator.svelte";
- import { formatCount } from "$lib/format";
+ import { formatCount, pluralize } from "$lib/format";
interface Props {
handle: string;
@@ -20,7 +20,7 @@
{formatCount(followers)} followers{pluralize(followers, "follower", "followers")}
diff --git a/web/src/lib/components/profile/NetworkVouches.svelte b/web/src/lib/components/profile/NetworkVouches.svelte
index c8518af1e..84db32a7c 100644
--- a/web/src/lib/components/profile/NetworkVouches.svelte
+++ b/web/src/lib/components/profile/NetworkVouches.svelte
@@ -4,6 +4,7 @@
import AvatarList from "$lib/components/profile/AvatarList.svelte";
import type { NetworkVouch } from "$lib/components/profile/types";
import Link from "$lib/components/ui/Link.svelte";
+ import { plural } from "$lib/format";
interface Props {
// handle of the profile these vouches are about
@@ -19,8 +20,6 @@
const denouncers = $derived(vouches.filter((v) => v.kind === "denounce"));
const href = $derived(resolve(`/${subjectHandle}?tab=vouches` as "/"));
- const plural = (n: number) => (n === 1 ? "user" : "users");
-
const both = $derived(vouchers.length > 0 && denouncers.length > 0);
const vouchBorder = $derived(
both
@@ -46,7 +45,7 @@
Vouched for by {vouchers.length}
- {plural(vouchers.length)}
+ {plural(vouchers.length, "user", "users")}
@@ -62,7 +61,7 @@
class="typography-paragraph-regular text-red-800 hover:no-underline dark:text-red-200"
>
Denounced by {denouncers.length}
- {plural(denouncers.length)}
+ {plural(denouncers.length, "user", "users")}
diff --git a/web/src/lib/components/profile/ProfileActivityChart.svelte b/web/src/lib/components/profile/ProfileActivityChart.svelte
index 53e2391c2..f529dfa2e 100644
--- a/web/src/lib/components/profile/ProfileActivityChart.svelte
+++ b/web/src/lib/components/profile/ProfileActivityChart.svelte
@@ -10,7 +10,7 @@
import GitCommit from "$icon/git-commit-horizontal";
import GitPullRequest from "$icon/git-pull-request";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
- import { formatCount } from "$lib/format";
+ import { formatCount, plural } from "$lib/format";
import {
ACTIVITY_METRICS,
layoutActivityChart,
@@ -53,7 +53,7 @@
const valueOf = (month: ProfileActivityMonth, key: ActivityMetric) =>
key === "commits" ? month.commits : month[key].length;
const nameOf = (value: number, metric: (typeof metrics)[number]) =>
- value === 1 ? metric.singular : metric.label;
+ plural(value, metric.singular, metric.label);
const values = $derived(
Object.fromEntries(
ACTIVITY_METRICS.map((metric) => [
diff --git a/web/src/lib/components/profile/ProfileSocialLinks.svelte b/web/src/lib/components/profile/ProfileSocialLinks.svelte
index 133e4bb22..f621e2c2d 100644
--- a/web/src/lib/components/profile/ProfileSocialLinks.svelte
+++ b/web/src/lib/components/profile/ProfileSocialLinks.svelte
@@ -5,7 +5,7 @@
import UsersRound from "$icon/users-round";
import type { ProfileRecord } from "$lib/api/records";
import Link, { link } from "$lib/components/ui/Link.svelte";
- import { formatCount } from "$lib/format";
+ import { formatCount, pluralize } from "$lib/format";
interface Props {
identity: { did: string; handle: string };
@@ -39,7 +39,7 @@
@@ -50,7 +50,7 @@
variant="inherit"
size="small"
>
- {formatCount(followers)} followers
+ {pluralize(followers, "follower", "followers")}
{#if following !== undefined}
ยท
diff --git a/web/src/lib/components/repo/BlobHeader.svelte b/web/src/lib/components/repo/BlobHeader.svelte
index 73df26e2f..66186ceaf 100644
--- a/web/src/lib/components/repo/BlobHeader.svelte
+++ b/web/src/lib/components/repo/BlobHeader.svelte
@@ -17,6 +17,7 @@
import RefChip from "$lib/components/repo/RefChip.svelte";
import RepoCrumbs from "$lib/components/repo/RepoCrumbs.svelte";
import { baseName } from "$lib/components/repo/urls";
+ import { plural } from "$lib/format";
interface Props {
path: string;
@@ -109,7 +110,7 @@
{/if}
{#if showLines}
- {lines} {lines === 1 ? "line" : "lines"}
+ {lines} {plural(lines, "line", "lines")}
{/if}
diff --git a/web/src/lib/components/repo/DiffView.svelte b/web/src/lib/components/repo/DiffView.svelte
index 0e6b72676..55e69f76d 100644
--- a/web/src/lib/components/repo/DiffView.svelte
+++ b/web/src/lib/components/repo/DiffView.svelte
@@ -29,6 +29,7 @@
import repoDiffviewSkeletonFixtureCollapsed from "$lib/bones/repo-diffview--skeleton-fixture-collapsed.bones.json";
import Bones from "$lib/components/ui/Bones.svelte";
import ResizeHandle from "$lib/components/ui/ResizeHandle.svelte";
+ import { plural } from "$lib/format";
import { browser } from "$app/environment";
import { tick, untrack } from "svelte";
import { tv } from "tailwind-variants";
@@ -230,9 +231,11 @@
{#if diff}
- {diff.files.length} changed {diff.files.length === 1
- ? "file"
- : "files"}
+ {diff.files.length} changed {plural(
+ diff.files.length,
+ "file",
+ "files"
+ )}
{/if}
diff --git a/web/src/lib/components/repo/LabelPanel.svelte b/web/src/lib/components/repo/LabelPanel.svelte
index 38f80f6b0..ecd2ad544 100644
--- a/web/src/lib/components/repo/LabelPanel.svelte
+++ b/web/src/lib/components/repo/LabelPanel.svelte
@@ -30,6 +30,7 @@
import Link from "$lib/components/ui/Link.svelte";
import Combobox from "$lib/components/ui/Combobox.svelte";
import type { SelectOption } from "$lib/components/ui/selectField";
+ import { plural } from "$lib/format";
interface Props {
direction?: "horizontal" | "vertical";
@@ -214,7 +215,8 @@
onclick={() => (expanded = true)}
icon={TagPlus}
>
- Display all {labels.length} labels
+ Display all {labels.length}
+ {plural(labels.length, "label", "labels")}
{/if}
diff --git a/web/src/lib/components/repo/StarButton.stories.svelte b/web/src/lib/components/repo/StarButton.stories.svelte
index 4c77898da..da7782558 100644
--- a/web/src/lib/components/repo/StarButton.stories.svelte
+++ b/web/src/lib/components/repo/StarButton.stories.svelte
@@ -1,7 +1,6 @@
@@ -27,13 +27,13 @@
{#if folders > 0}
- {plural(folders, "folder")}
+ {folders} {plural(folders, "folder", "folders")}
{/if}
{#if folders > 0 && files > 0}
{/if}
{#if files > 0}
- {plural(files, "file")}
+ {files} {plural(files, "file", "files")}
{/if}
diff --git a/web/src/lib/components/repo/pipelines/PipelineToolbar.svelte b/web/src/lib/components/repo/pipelines/PipelineToolbar.svelte
index 674291bbd..4447efe04 100644
--- a/web/src/lib/components/repo/pipelines/PipelineToolbar.svelte
+++ b/web/src/lib/components/repo/pipelines/PipelineToolbar.svelte
@@ -13,7 +13,7 @@
import bonesLoadingprimitivesCount from "$lib/bones/bones-loadingprimitives--count.bones.json";
import type { PipelineFilter } from "$lib/components/repo/pipelines/pipeline";
import PipelineSearch from "$lib/components/repo/pipelines/PipelineSearch.svelte";
- import { formatCount } from "$lib/format";
+ import { formatCount, plural } from "$lib/format";
interface Props {
ownerHandle: string;
@@ -80,7 +80,7 @@
{#if value > 0}
- {formatCount(value)} pipeline {value === 1 ? "run" : "runs"}
+ {formatCount(value)} pipeline {plural(value, "run", "runs")}
{/if}
diff --git a/web/src/lib/components/repo/pipelines/logs.ts b/web/src/lib/components/repo/pipelines/logs.ts
index 4a57e0cde..ba2d5ce2a 100644
--- a/web/src/lib/components/repo/pipelines/logs.ts
+++ b/web/src/lib/components/repo/pipelines/logs.ts
@@ -4,6 +4,7 @@ import { AnsiUp } from "ansi_up";
import type { LogFrame } from "$lib/api/spindleLogs";
import type { PipelineStatus } from "$lib/components/repo/types";
import { isLive, STATUS_LABELS } from "$lib/components/repo/pipelines/pipeline";
+import { plural } from "$lib/format";
export interface LogStep {
id: number;
@@ -152,8 +153,7 @@ export const narrate = (steps: LogStep[], workflow: PipelineStatus): string => {
if (errorIndex !== -1) return `${STATUS_LABELS[workflow]} at ${steps[errorIndex].name}`;
if (workflow === "success" && statuses.every((status) => status === "done")) {
- const step = steps.length === 1 ? "step" : "steps";
- return `All ${steps.length} ${step} complete`;
+ return `All ${steps.length} ${plural(steps.length, "step", "steps")} complete`;
}
return "";
diff --git a/web/src/lib/components/repo/tickets/TicketCardContent.svelte b/web/src/lib/components/repo/tickets/TicketCardContent.svelte
index 757030de5..ae28e79f1 100644
--- a/web/src/lib/components/repo/tickets/TicketCardContent.svelte
+++ b/web/src/lib/components/repo/tickets/TicketCardContent.svelte
@@ -3,7 +3,7 @@
import TicketInfoBar from "$lib/components/repo/tickets/TicketInfoBar.svelte";
import Separator from "$lib/components/ui/Separator.svelte";
import type { IssueSummary, PullSummary } from "$lib/components/repo/types";
- import { formatCount } from "$lib/format";
+ import { pluralize } from "$lib/format";
interface Props {
ownerHandle: string;
@@ -38,8 +38,7 @@
{#snippet extraInfo()}
- {formatCount(ticket.commentCount)}
- {ticket.commentCount === 1 ? "comment" : "comments"}
+ {pluralize(ticket.commentCount, "comment", "comments")}
{/snippet}
diff --git a/web/src/lib/components/search/CodeSearchView.svelte b/web/src/lib/components/search/CodeSearchView.svelte
index 66de23716..324b822e2 100644
--- a/web/src/lib/components/search/CodeSearchView.svelte
+++ b/web/src/lib/components/search/CodeSearchView.svelte
@@ -15,6 +15,7 @@
import { searchUrl } from "$lib/components/search/SearchTabs.svelte";
import { matchCount } from "$lib/components/search/chunks";
import { createCodeSearch } from "$lib/components/search/codeSearch.svelte";
+ import { plural } from "$lib/format";
const HINT = "rounded-md bg-background-subtle px-2xs py-3xs typography-monospace-small";
@@ -130,8 +131,8 @@
class="border-t border-border-default py-xs typography-paragraph-small text-foreground-muted"
>
Found {matches}
- {matches === 1 ? "match" : "matches"} in {search.results.length}
- {search.results.length === 1 ? "file" : "files"}
+ {plural(matches, "match", "matches")} in {search.results.length}
+ {plural(search.results.length, "file", "files")}
{/if}
{/snippet}
diff --git a/web/src/lib/components/search/FileMatch.svelte b/web/src/lib/components/search/FileMatch.svelte
index 979ec1f66..b109806b9 100644
--- a/web/src/lib/components/search/FileMatch.svelte
+++ b/web/src/lib/components/search/FileMatch.svelte
@@ -13,6 +13,7 @@
import FileCardHeader from "$lib/components/repo/FileCardHeader.svelte";
import { chunkLines, type ThemedToken } from "$lib/components/search/chunks";
import { highlightChunks, resolveLanguage } from "$lib/components/search/highlight";
+ import { plural } from "$lib/format";
const VISIBLE_CHUNKS = 3;
@@ -142,9 +143,11 @@
{:else}
Show {hiddenMatches} more {hiddenMatches === 1
- ? "match"
- : "matches"}Show {hiddenMatches} more {plural(
+ hiddenMatches,
+ "match",
+ "matches"
+ )}
{/if}
diff --git a/web/src/lib/components/strings/StringForm.svelte b/web/src/lib/components/strings/StringForm.svelte
index 48e86865c..e4659594f 100644
--- a/web/src/lib/components/strings/StringForm.svelte
+++ b/web/src/lib/components/strings/StringForm.svelte
@@ -20,6 +20,7 @@
import { createAction } from "$lib/action.svelte";
import Field from "$lib/components/ui/Field.svelte";
import PierreEditor from "$lib/components/editor/PierreEditor.svelte";
+ import { plural } from "$lib/format";
interface Props {
mode?: "create" | "edit";
@@ -131,11 +132,11 @@
{stats.lines} line{stats.lines !== 1 ? "s" : ""}{stats.lines} {plural(stats.lines, "line", "lines")}
{stats.bytes} byte{stats.bytes !== 1 ? "s" : ""}{stats.bytes} {plural(stats.bytes, "byte", "bytes")}
diff --git a/web/src/lib/components/timeline/TimelineGroup.svelte b/web/src/lib/components/timeline/TimelineGroup.svelte
index a190dd3cf..f48244557 100644
--- a/web/src/lib/components/timeline/TimelineGroup.svelte
+++ b/web/src/lib/components/timeline/TimelineGroup.svelte
@@ -6,6 +6,7 @@
import TimeAgo from "$lib/components/ui/TimeAgo.svelte";
import TimelineCard from "$lib/components/timeline/TimelineCard.svelte";
import type { TimelineEvent } from "$lib/components/timeline/group";
+ import { plural as pluralFor } from "$lib/format";
const COLLAPSED_COUNT = 3;
@@ -65,7 +66,7 @@
{#if hidden > 0}
{/if}
diff --git a/web/src/lib/components/timeline/TimelineTargetHeader.svelte b/web/src/lib/components/timeline/TimelineTargetHeader.svelte
index b6e3b4960..5e4fc7900 100644
--- a/web/src/lib/components/timeline/TimelineTargetHeader.svelte
+++ b/web/src/lib/components/timeline/TimelineTargetHeader.svelte
@@ -3,6 +3,7 @@
import AvatarList from "$lib/components/profile/AvatarList.svelte";
import TimeAgo from "$lib/components/ui/TimeAgo.svelte";
import type * as ShTangledActorDefs from "$lib/api/lexicons/types/sh/tangled/actor/defs";
+ import { plural } from "$lib/format";
const STACK_COUNT = 5;
@@ -22,7 +23,7 @@
-
{people.length} users
+
{people.length} {plural(people.length, "user", "users")}
{verb}
{@render children()}
diff --git a/web/src/lib/components/ui/CodeBlockToolbar.svelte b/web/src/lib/components/ui/CodeBlockToolbar.svelte
index 86d6484e6..12dcbefd2 100644
--- a/web/src/lib/components/ui/CodeBlockToolbar.svelte
+++ b/web/src/lib/components/ui/CodeBlockToolbar.svelte
@@ -5,6 +5,7 @@
import { MediaQuery } from "svelte/reactivity";
import Button from "$lib/components/ui/Button.svelte";
import CopyButton from "$lib/components/ui/CopyButton.svelte";
+ import { plural } from "$lib/format";
interface Props {
block: HTMLElement;
@@ -48,7 +49,8 @@
variant="ghost"
/>
- {lines} lines
+ {lines}
+ {plural(lines, "line", "lines")}
diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts
index a5f697e4c..407d9a8e7 100644
--- a/web/src/lib/format.ts
+++ b/web/src/lib/format.ts
@@ -85,3 +85,13 @@ export const formatCount = (n: number | undefined | null): string => {
// truncate so 999,999 reads as 999.9k rather than rounding up to 1000k
return `${Math.floor(scaled * 10) / 10}${suffix}`;
};
+
+const pluralRules = new Intl.PluralRules("en");
+
+// the noun alone, for markup that renders the number separately
+export const plural = (n: number, one: string, other: string): string =>
+ pluralRules.select(n) === "one" ? one : other;
+
+// "1 follower", "1.2k followers"
+export const pluralize = (n: number, one: string, other: string): string =>
+ `${formatCount(n)} ${plural(n, one, other)}`;
diff --git a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
index 51d37fb50..dc44d66a2 100644
--- a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
+++ b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
@@ -64,6 +64,7 @@
import SubjectLabelPanel from "$lib/components/repo/SubjectLabelPanel.svelte";
import type { PageData } from "./$types";
import { CROSSFADE_DURATION, CROSSFADE_EASING } from "$lib/motion/spring";
+ import { plural } from "$lib/format";
type PullViewData = PageData &
Exclude
, { redirectTo: string }> & {
@@ -806,7 +807,7 @@
{conflicts.length}
- {conflicts.length === 1 ? "file" : "files"}
@@ -854,7 +855,11 @@
class="w-full"
icon={ArrowDown}
>
- Load {hiddenConflictCount} more conflicts
+ Load {hiddenConflictCount} more {plural(
+ hiddenConflictCount,
+ "conflict",
+ "conflicts"
+ )}
{/if}
diff --git a/web/src/routes/[handle]/settings/members/InviteMembers.svelte b/web/src/routes/[handle]/settings/members/InviteMembers.svelte
index 185c7ebca..b7d5ee31b 100644
--- a/web/src/routes/[handle]/settings/members/InviteMembers.svelte
+++ b/web/src/routes/[handle]/settings/members/InviteMembers.svelte
@@ -16,6 +16,7 @@
import Input from "$lib/components/ui/Input.svelte";
import type { InputTag } from "$lib/components/ui/inputField";
import User from "$lib/components/ui/User.svelte";
+ import { plural } from "$lib/format";
import Plus from "$icon/plus";
import X from "$icon/x";
@@ -143,7 +144,7 @@
onclick={invite.run}
>
Invite {tags.length}
- {tags.length === 1 ? "member" : "members"}
+ {plural(tags.length, "member", "members")}
diff --git a/web/src/routes/dev/components/Shell.svelte b/web/src/routes/dev/components/Shell.svelte
index 8cf30aaf9..35091883a 100644
--- a/web/src/routes/dev/components/Shell.svelte
+++ b/web/src/routes/dev/components/Shell.svelte
@@ -10,6 +10,7 @@
import Button from "$lib/components/ui/Button.svelte";
import Input from "$lib/components/ui/Input.svelte";
import Logo from "$lib/components/ui/Logo.svelte";
+ import { plural } from "$lib/format";
interface Entry {
name: string;
@@ -205,7 +206,8 @@
- {total} components
+ {total}
+ {plural(total, "component", "components")}