Skip to content

Commit

Permalink
Merge pull request dubinc#1645 from nabarvn/fix/dynamic-label-plurali…
Browse files Browse the repository at this point in the history
…zation

Add pluralize utility function for dynamic word forms
  • Loading branch information
steven-tey authored Oct 28, 2024
2 parents b3921f4 + a63011a commit 0c772c1
Show file tree
Hide file tree
Showing 13 changed files with 61 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import { CursorRays, LinkBroken } from "@dub/ui/src/icons";
import { ToggleGroup } from "@dub/ui/src/toggle-group";
import { InfoTooltip, TooltipContent } from "@dub/ui/src/tooltip";
import { capitalize } from "@dub/utils";
import { capitalize, pluralize } from "@dub/utils";
import { ChevronDown, Crown } from "lucide-react";
import { useEffect, useState } from "react";
import { DefaultDomains } from "./default-domains";
Expand Down Expand Up @@ -79,9 +79,10 @@ export default function WorkspaceDomainsClient() {

const disabledTooltip = exceededDomains ? (
<TooltipContent
title={`You can only add up to ${domainsLimit} domain${
domainsLimit === 1 ? "" : "s"
} on the ${capitalize(plan)} plan. Upgrade to add more domains`}
title={`You can only add up to ${domainsLimit} ${pluralize(
"domain",
domainsLimit || 0,
)} on the ${capitalize(plan)} plan. Upgrade to add more domains`}
cta="Upgrade"
onClick={() => {
queryParams({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
LoadingSpinner,
PenWriting,
} from "@dub/ui/src/icons";
import { cn, nFormatter } from "@dub/utils";
import { cn, nFormatter, pluralize } from "@dub/utils";
import Link from "next/link";
import { useContext, useState } from "react";
import { toast } from "sonner";
Expand Down Expand Up @@ -116,7 +116,7 @@ export function TagCard({
href={`/${slug}?tagIds=${tag.id}`}
className="whitespace-nowrap rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-sm text-gray-800 transition-colors hover:bg-gray-100"
>
{nFormatter(linksCount || 0)} link{linksCount !== 1 && "s"}
{nFormatter(linksCount || 0)} {pluralize("link", linksCount || 0)}
</Link>
)}
<Popover
Expand Down
6 changes: 3 additions & 3 deletions apps/web/emails/links-imported.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DUB_WORDMARK, linkConstructor, timeAgo } from "@dub/utils";
import { DUB_WORDMARK, linkConstructor, pluralize, timeAgo } from "@dub/utils";
import {
Body,
Column,
Expand Down Expand Up @@ -94,7 +94,7 @@ export default function LinksImported({
>
{workspaceName}
</Link>{" "}
, for the domain{domains.length > 1 ? "s" : ""}{" "}
, for the {pluralize("domain", domains.length)}{" "}
<strong>{domains.join(", ")}</strong>.
</Text>
{links.length > 0 && (
Expand Down Expand Up @@ -144,7 +144,7 @@ export default function LinksImported({
href="https://dub.co/help/article/how-to-add-custom-domain#step-2-configure-your-domain"
className="font-medium text-blue-600 no-underline"
>
configured your domain{domains.length > 1 ? "s" : ""}
configured your {pluralize("domain", domains.length)}
</Link>
, you will need to do it before you can start using your links.
</Text>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/api/links/process-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isValidUrl,
log,
parseDateTime,
pluralize,
} from "@dub/utils";
import { combineTagIds, keyChecks, processKey } from "./utils";

Expand Down Expand Up @@ -200,7 +201,7 @@ export async function processLink<T extends Record<string, any>>({
) {
return {
link: payload,
error: `Invalid destination URL. You can only create ${domain} short links for URLs with the domain${allowedHostnames.length > 1 ? "s" : ""} ${allowedHostnames
error: `Invalid destination URL. You can only create ${domain} short links for URLs with the ${pluralize("domain", allowedHostnames.length)} ${allowedHostnames
.map((d) => `"${d}"`)
.join(", ")}.`,
code: "unprocessable_entity",
Expand Down
5 changes: 3 additions & 2 deletions apps/web/ui/integrations/integration-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ShieldCheck,
} from "@dub/ui/src/icons";
import { Tooltip } from "@dub/ui/src/tooltip";
import { pluralize } from "@dub/utils";
import Link from "next/link";

export default function IntegrationCard(
Expand Down Expand Up @@ -77,8 +78,8 @@ export default function IntegrationCard(
<div className="flex items-center justify-end gap-1 text-gray-500">
<Download className="size-4" />
<span className="text-sm">
{integration.installations} install
{integration.installations === 1 ? "" : "s"}
{integration.installations}{" "}
{pluralize("install", integration.installations)}
</span>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/ui/links/archived-links-hint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import useLinksCount from "@/lib/swr/use-links-count";
import { Button } from "@dub/ui";
import { BoxArchive } from "@dub/ui/src/icons";
import { Tooltip } from "@dub/ui/src/tooltip";
import { pluralize } from "@dub/utils";
import { useSearchParams } from "next/navigation";
import { useContext } from "react";
import { LinksDisplayContext } from "./links-display-provider";
Expand Down Expand Up @@ -33,7 +34,7 @@ function ArchivedLinksHintHelper() {
<span className="font-medium text-gray-950">
{archivedCount}
</span>{" "}
archived link{archivedCount !== 1 && "s"} that match
archived {pluralize("link", archivedCount)} that match
{archivedCount === 1 && "es"} the applied filters
</span>
<div>
Expand Down
20 changes: 15 additions & 5 deletions apps/web/ui/links/link-details-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@ import { TagProps } from "@/lib/types";
import {
Button,
CardList,
CursorRays,
InvoiceDollar,
Tooltip,
useMediaQuery,
UserCheck,
useRouterStuff,
} from "@dub/ui";
import { CursorRays, InvoiceDollar, UserCheck } from "@dub/ui/src/icons";
import { cn, currencyFormatter, nFormatter, timeAgo } from "@dub/utils";
import {
cn,
currencyFormatter,
nFormatter,
pluralize,
timeAgo,
} from "@dub/utils";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
Expand Down Expand Up @@ -136,7 +144,9 @@ function AnalyticsBadge({ link }: { link: ResponseLink }) {
id: "clicks",
icon: CursorRays,
value: link.clicks,
label: trackConversion ? undefined : (p) => (p ? "clicks" : "click"),
label: trackConversion
? undefined
: (value) => pluralize("click", value),
},
...(trackConversion
? [
Expand Down Expand Up @@ -187,7 +197,7 @@ function AnalyticsBadge({ link }: { link: ResponseLink }) {
? currencyFormatter(value / 100)
: nFormatter(value, { full: value < 1000000000 })}
</span>{" "}
{value !== 1 ? tab : tab.slice(0, -1)}
{pluralize(tab.slice(0, -1), value)}
</div>
))}
<p className="text-xs leading-none text-gray-400">
Expand Down Expand Up @@ -231,7 +241,7 @@ function AnalyticsBadge({ link }: { link: ResponseLink }) {
: nFormatter(value)}
{label && (
<span className="hidden md:inline-block">
&nbsp;{label(value !== 1)}
&nbsp;{label(value)}
</span>
)}
</span>
Expand Down
6 changes: 2 additions & 4 deletions apps/web/ui/modals/add-edit-domain-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Modal,
TooltipContent,
} from "@dub/ui";
import { capitalize } from "@dub/utils";
import { capitalize, pluralize } from "@dub/utils";
import { useParams } from "next/navigation";
import {
Dispatch,
Expand Down Expand Up @@ -85,9 +85,7 @@ function AddDomainButton({
disabledTooltip={
exceededDomains ? (
<TooltipContent
title={`You can only add up to ${domainsLimit} domain${
domainsLimit === 1 ? "" : "s"
} on the ${capitalize(plan)} plan. Upgrade to add more domains`}
title={`You can only add up to ${domainsLimit} ${pluralize("domain", domainsLimit || 0)} on the ${capitalize(plan)} plan. Upgrade to add more domains`}
cta="Upgrade"
href={`/${slug}/upgrade`}
/>
Expand Down
6 changes: 2 additions & 4 deletions apps/web/ui/modals/add-edit-tag-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TooltipContent,
useMediaQuery,
} from "@dub/ui";
import { capitalize, cn } from "@dub/utils";
import { capitalize, cn, pluralize } from "@dub/utils";
import posthog from "posthog-js";
import {
Dispatch,
Expand Down Expand Up @@ -231,9 +231,7 @@ function AddTagButton({
disabledTooltip={
exceededTags ? (
<TooltipContent
title={`You can only add up to ${tagsLimit} tag${
tagsLimit === 1 ? "" : "s"
} on the ${capitalize(plan)} plan. Upgrade to add more tags`}
title={`You can only add up to ${tagsLimit} ${pluralize("tag", tagsLimit || 0)} on the ${capitalize(plan)} plan. Upgrade to add more tags`}
cta="Upgrade"
href={`/${slug}/upgrade`}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/ui/modals/link-builder/targeting-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
COUNTRIES,
getParamsFromURL,
isValidUrl,
pluralize,
} from "@dub/utils";
import {
Dispatch,
Expand Down Expand Up @@ -420,7 +421,7 @@ export function getTargetingLabel({

// Geo
if (countries.length === 1 && countries[0]) return countries[0];
return `${countries.length} Target${countries.length === 1 ? "" : "s"}`;
return `${countries.length} ${pluralize("Target", countries.length)}`;
}

return `${count + (countries.length > 1 ? countries.length - 1 : 0)} Targets`;
Expand Down
18 changes: 8 additions & 10 deletions apps/web/ui/workspaces/invite-teammates-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Role, roles } from "@/lib/types";
import { Invite } from "@/lib/zod/schemas/invites";
import { Button, useMediaQuery } from "@dub/ui";
import { Trash } from "@dub/ui/src/icons";
import { capitalize, cn } from "@dub/utils";
import { capitalize, cn, pluralize } from "@dub/utils";
import { Plus } from "lucide-react";
import posthog from "posthog-js";
import { useFieldArray, useForm } from "react-hook-form";
Expand Down Expand Up @@ -68,13 +68,11 @@ export function InviteTeammatesForm({

if (saveOnly) {
toast.custom(
() => <InviteSavedToast plural={teammates.length !== 1} />,
() => <InviteSavedToast teammates={teammates.length} />,
{ duration: 7000 },
);
} else
toast.success(
`Invitation${teammates.length !== 1 ? "s" : ""} sent!`,
);
toast.success(`${pluralize("Invitation", teammates.length)} sent!`);

if (!saveOnly) {
teammates.forEach(({ email }) =>
Expand Down Expand Up @@ -111,7 +109,7 @@ export function InviteTeammatesForm({
<label>
{index === 0 && (
<span className="mb-2 block text-sm font-medium text-gray-700">
Email{fields.length !== 1 ? "s" : ""}
{pluralize("Email", fields.length)}
</span>
)}
<div className="relative flex rounded-md shadow-sm">
Expand Down Expand Up @@ -164,20 +162,20 @@ export function InviteTeammatesForm({
<Button
loading={isSubmitting || isSubmitSuccessful}
text={
saveOnly ? "Continue" : `Send invite${fields.length !== 1 ? "s" : ""}`
saveOnly ? "Continue" : `Send ${pluralize("invite", fields.length)}`
}
/>
</form>
);
}

function InviteSavedToast({ plural }: { plural: boolean }) {
function InviteSavedToast({ teammates }: { teammates: number }) {
return (
<div className="flex items-center gap-1.5 rounded-lg bg-white p-4 text-sm shadow-[0_4px_12px_#0000001a]">
<CheckCircleFill className="size-5 shrink-0 text-black" />
<p className="text-[13px] font-medium text-gray-900">
Invitation{plural ? "s" : ""} saved. You'll need a pro plan to invite
teammates.{" "}
{pluralize("Invitation", teammates)} saved. You'll need a pro plan to
invite teammates.{" "}
<a
href="https://dub.co/help/article/how-to-invite-teammates"
target="_blank"
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./link-constructor";
export * from "./log";
export * from "./nanoid";
export * from "./nformatter";
export * from "./pluralize";
export * from "./punycode";
export * from "./random-value";
export * from "./smart-truncate";
Expand Down
14 changes: 14 additions & 0 deletions packages/utils/src/functions/pluralize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const pluralize = (
word: string,
count: number,
options: {
plural?: string;
} = {},
) => {
if (count === 1) {
return word;
}

// Use custom plural form if provided, otherwise add 's'
return options.plural || `${word}s`;
};

0 comments on commit 0c772c1

Please sign in to comment.