diff --git a/apps/web/src/app/dashboard/[guildId]/board/page.tsx b/apps/web/src/app/dashboard/[guildId]/board/page.tsx index ef39340..77b9db8 100644 --- a/apps/web/src/app/dashboard/[guildId]/board/page.tsx +++ b/apps/web/src/app/dashboard/[guildId]/board/page.tsx @@ -10,7 +10,7 @@ import { requireUser } from "@/lib/auth"; import { resolveStatus, statusOptions } from "@/lib/forms"; import { getGuildSubmissions, getReviewScope } from "@/lib/guild"; import { signGuildRealtimeToken } from "@/lib/realtime-token"; -import { getDict } from "@/i18n"; +import { getDict, getLocale } from "@/i18n"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -101,8 +101,12 @@ export default async function BoardPage({ params }: { params: Promise<{ guildId: moved: t.board.moved, empty: t.board.columnEmpty, anonymous: t.anonymous, - countOne: t.countSubmission, - countMany: t.countSubmissions, + count: { + one: t.countSubmission, + few: t.countSubmissionsFew, + other: t.countSubmissions, + }, + locale: await getLocale(), saveFailed: t.board.saveFailed, dismiss: dict.kit.dismiss, }} diff --git a/apps/web/src/app/dashboard/[guildId]/submissions/page.tsx b/apps/web/src/app/dashboard/[guildId]/submissions/page.tsx index e19b4f5..95cfb07 100644 --- a/apps/web/src/app/dashboard/[guildId]/submissions/page.tsx +++ b/apps/web/src/app/dashboard/[guildId]/submissions/page.tsx @@ -11,8 +11,9 @@ import { Button } from "@/components/ui/button"; import { requireUser } from "@/lib/auth"; import { resolveStatus, statusOptions } from "@/lib/forms"; import { countArchivedSubmissions, getGuildSubmissions, getReviewScope } from "@/lib/guild"; +import { countForm } from "@/lib/plural"; import { signGuildRealtimeToken } from "@/lib/realtime-token"; -import { getDict } from "@/i18n"; +import { getDict, getLocale } from "@/i18n"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -78,7 +79,11 @@ export default async function GuildSubmissionsPage({ {liveToken && }
diff --git a/apps/web/src/components/board/kanban-board.tsx b/apps/web/src/components/board/kanban-board.tsx index 4c4eada..94eac9e 100644 --- a/apps/web/src/components/board/kanban-board.tsx +++ b/apps/web/src/components/board/kanban-board.tsx @@ -8,6 +8,7 @@ import { useEffect, useId, useRef, useState } from "react"; import { Notice, RowMenu } from "@/components/dashboard-kit"; import { LocalDateTime } from "@/components/public/local-datetime"; import { dotColors } from "@/lib/color-dot"; +import { countForm, type CountForms } from "@/lib/plural"; export interface BoardColumn { key: string; @@ -33,8 +34,9 @@ export interface BoardLabels { moved: string; empty: string; anonymous: string; - countOne: string; - countMany: string; + /** The noun after a column's card count, in the forms the locale needs. */ + count: CountForms; + locale: string; saveFailed: string; dismiss: string; } @@ -150,7 +152,7 @@ export function KanbanBoard({ {cards.length} {" "} - {cards.length === 1 ? labels.countOne : labels.countMany} + {countForm(cards.length, labels.locale, labels.count)} diff --git a/apps/web/src/i18n/dictionaries.ts b/apps/web/src/i18n/dictionaries.ts index 362cafd..bfd70d3 100644 --- a/apps/web/src/i18n/dictionaries.ts +++ b/apps/web/src/i18n/dictionaries.ts @@ -497,6 +497,7 @@ const en = { countForms: "forms", countSubmission: "submission", countSubmissions: "submissions", + countSubmissionsFew: "submissions", newForm: "New form", noForms: "No forms yet. Create your first one.", edit: "Edit", @@ -1842,6 +1843,7 @@ const de: Dictionary = { countForms: "Formulare", countSubmission: "Einreichung", countSubmissions: "Einreichungen", + countSubmissionsFew: "Einreichungen", newForm: "Neues Formular", noForms: "Noch keine Formulare. Erstelle dein erstes.", edit: "Bearbeiten", @@ -3198,6 +3200,7 @@ const hu: Dictionary = { countForms: "űrlap", countSubmission: "beküldés", countSubmissions: "beküldés", + countSubmissionsFew: "beküldés", newForm: "Új űrlap", noForms: "Még nincsenek űrlapok. Hozd létre az elsőt.", edit: "Szerkesztés", @@ -4542,6 +4545,7 @@ const fr: Dictionary = { countForms: "formulaires", countSubmission: "soumission", countSubmissions: "soumissions", + countSubmissionsFew: "soumissions", newForm: "Nouveau formulaire", noForms: "Aucun formulaire. Créez le premier.", edit: "Modifier", @@ -5892,6 +5896,7 @@ const es: Dictionary = { countForms: "formularios", countSubmission: "envío", countSubmissions: "envíos", + countSubmissionsFew: "envíos", newForm: "Nuevo formulario", noForms: "Aún no hay formularios. Crea el primero.", edit: "Editar", @@ -7233,6 +7238,7 @@ const pt: Dictionary = { countForms: "formulários", countSubmission: "candidatura", countSubmissions: "candidaturas", + countSubmissionsFew: "candidaturas", newForm: "Novo formulário", noForms: "Nenhum formulário ainda. Crie o seu primeiro.", edit: "Editar", @@ -8572,6 +8578,7 @@ const pl: Dictionary = { countForms: "formularzy", countSubmission: "zgłoszenie", countSubmissions: "zgłoszeń", + countSubmissionsFew: "zgłoszenia", newForm: "Nowy formularz", noForms: "Brak formularzy. Utwórz pierwszy.", edit: "Edytuj", diff --git a/apps/web/src/lib/plural.test.ts b/apps/web/src/lib/plural.test.ts new file mode 100644 index 0000000..9dfcf17 --- /dev/null +++ b/apps/web/src/lib/plural.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { countForm } from "./plural"; + +const pl = { one: "zgłoszenie", few: "zgłoszenia", other: "zgłoszeń" }; +const en = { one: "submission", few: "submissions", other: "submissions" }; + +describe("countForm", () => { + it("picks the Polish form for 1, 2 to 4 and 5 and up", () => { + expect(countForm(1, "pl", pl)).toBe("zgłoszenie"); + for (const n of [2, 3, 4, 22, 34]) expect(countForm(n, "pl", pl)).toBe("zgłoszenia"); + for (const n of [5, 11, 12, 13, 14, 25, 100]) expect(countForm(n, "pl", pl)).toBe("zgłoszeń"); + }); + + it("keeps singular and plural in English", () => { + expect(countForm(1, "en", en)).toBe("submission"); + expect(countForm(3, "en", en)).toBe("submissions"); + }); +}); diff --git a/apps/web/src/lib/plural.ts b/apps/web/src/lib/plural.ts new file mode 100644 index 0000000..e98a46d --- /dev/null +++ b/apps/web/src/lib/plural.ts @@ -0,0 +1,13 @@ +/** + * A noun in the forms a count can need. `few` is the Polish 2 to 4 (also 22 + * to 24 and so on); languages without that form repeat `other` there. + */ +export type CountForms = { one: string; few: string; other: string }; + +/** The form of a noun that goes with `count` in `locale`, by the CLDR plural rules. */ +export function countForm(count: number, locale: string, forms: CountForms): string { + const category = new Intl.PluralRules(locale).select(count); + if (category === "one") return forms.one; + if (category === "few") return forms.few; + return forms.other; +}