diff --git a/components/typography/font-filters.tsx b/components/typography/font-filters.tsx index 7212290..f51fbf5 100644 --- a/components/typography/font-filters.tsx +++ b/components/typography/font-filters.tsx @@ -1,7 +1,9 @@ "use client"; import { Heart, Search, X } from "lucide-react"; -import { useRef } from "react"; +import { useEffect, useRef, useState } from "react"; + +import { useI18n } from "@/components/layout/locale-provider"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -31,7 +33,13 @@ const sorts: { value: FontSort; label: string }[] = [ ]; export function FontFilters({ resultCount }: { resultCount: number }) { + const { plural } = useI18n(); const filters = useTypographyStore((state) => state.filters); + const [announcedCount, setAnnouncedCount] = useState(resultCount); + useEffect(() => { + const timeout = window.setTimeout(() => setAnnouncedCount(resultCount), 300); + return () => window.clearTimeout(timeout); + }, [resultCount]); const setFilters = useTypographyStore((state) => state.setFilters); const inputRef = useRef(null); useHotkey("f", () => inputRef.current?.focus()); @@ -118,8 +126,11 @@ export function FontFilters({ resultCount }: { resultCount: number }) { Favorites -

- {resultCount} {resultCount === 1 ? "family" : "families"} +

+ {plural("typography.fonts.count", resultCount)} +

+

+ {plural("typography.fonts.count", announcedCount)}

diff --git a/lib/i18n/messages.test.ts b/lib/i18n/messages.test.ts index 4a7e85c..bb14987 100644 --- a/lib/i18n/messages.test.ts +++ b/lib/i18n/messages.test.ts @@ -65,6 +65,21 @@ describe("translate", () => { expect(t("projects.card.openNamed", { name: "Acme" })).toBe("Open Acme"); }); + it("translates the font result count in every locale", () => { + const cases = [ + [catalogs.en, "en", "1 font", "2 fonts"], + [catalogs.es, "es", "1 fuente", "2 fuentes"], + [catalogs.fr, "fr", "1 police", "2 polices"], + [catalogs.hi, "hi", "1 फ़ॉन्ट", "2 फ़ॉन्ट"], + ] as const; + + for (const [catalog, locale, one, other] of cases) { + const { plural } = createTranslator(catalog, locale); + expect(plural("typography.fonts.count", 1)).toBe(one); + expect(plural("typography.fonts.count", 2)).toBe(other); + } + }); + it("picks plural forms by count", () => { const { plural } = createTranslator(catalogs.es, "es"); expect(plural("projects.toast.exported", 1)).toBe("Se exportó 1 proyecto"); diff --git a/lib/i18n/messages/en.ts b/lib/i18n/messages/en.ts index 2450791..2685406 100644 --- a/lib/i18n/messages/en.ts +++ b/lib/i18n/messages/en.ts @@ -92,6 +92,8 @@ export const en = { "nav.skip": "Skip to content", "nav.skipPreview": "Skip to preview", "nav.skipOutput": "Skip to code and exports", + "typography.fonts.count.one": "{count} font", + "typography.fonts.count.other": "{count} fonts", "typography.pair.layout": "Layout", "typography.pair.layout.article": "Blog article", "typography.pair.layout.pricing": "Pricing card", diff --git a/lib/i18n/messages/es.ts b/lib/i18n/messages/es.ts index 13a4b8c..64bdbce 100644 --- a/lib/i18n/messages/es.ts +++ b/lib/i18n/messages/es.ts @@ -89,6 +89,8 @@ export const es = { "nav.skip": "Saltar al contenido", "nav.skipPreview": "Saltar a la vista previa", "nav.skipOutput": "Saltar al código y las exportaciones", + "typography.fonts.count.one": "{count} fuente", + "typography.fonts.count.other": "{count} fuentes", "typography.pair.layout": "Diseño", "typography.pair.layout.article": "Artículo de blog", "typography.pair.layout.pricing": "Tarjeta de precios", diff --git a/lib/i18n/messages/fr.ts b/lib/i18n/messages/fr.ts index 986ffa5..f7ff34e 100644 --- a/lib/i18n/messages/fr.ts +++ b/lib/i18n/messages/fr.ts @@ -90,6 +90,8 @@ export const fr = { "nav.skip": "Aller au contenu", "nav.skipPreview": "Aller à l'aperçu", "nav.skipOutput": "Aller au code et aux exports", + "typography.fonts.count.one": "{count} police", + "typography.fonts.count.other": "{count} polices", "typography.pair.layout": "Mise en page", "typography.pair.layout.article": "Article de blog", "typography.pair.layout.pricing": "Carte tarifaire", diff --git a/lib/i18n/messages/hi.ts b/lib/i18n/messages/hi.ts index 93dfe20..06fd595 100644 --- a/lib/i18n/messages/hi.ts +++ b/lib/i18n/messages/hi.ts @@ -88,6 +88,8 @@ export const hi = { "nav.skip": "सामग्री पर जाएँ", "nav.skipPreview": "पूर्वावलोकन पर जाएँ", "nav.skipOutput": "कोड और एक्सपोर्ट पर जाएँ", + "typography.fonts.count.one": "{count} फ़ॉन्ट", + "typography.fonts.count.other": "{count} फ़ॉन्ट", "typography.pair.layout": "लेआउट", "typography.pair.layout.article": "ब्लॉग लेख", "typography.pair.layout.pricing": "प्राइसिंग कार्ड", diff --git a/tests/e2e/font-browser.spec.ts b/tests/e2e/font-browser.spec.ts index 079def4..825d9ca 100644 --- a/tests/e2e/font-browser.spec.ts +++ b/tests/e2e/font-browser.spec.ts @@ -36,3 +36,14 @@ test("the whole catalog is listed, but only the rows near the view are in the pa // The target is 50 ms; the bound leaves room for a busy CI machine. await expectUnder(testInfo, "longest-task", longest, 300); }); + +test("announces the filtered font count after search input settles", async ({ page }) => { + await page.goto("/typography"); + const search = page.getByRole("searchbox", { name: "Search fonts" }); + const announcement = page.locator('[aria-live="polite"][aria-atomic="true"]'); + await expect(announcement).toHaveText(/\d+ fonts?/); + + await search.fill("open sans"); + await expect(page.getByRole("option")).toHaveCount(1); + await expect(announcement).toHaveText("1 font"); +});