From 671e3aed1b16753aef9ad6359f47560a62558562 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Thu, 29 Oct 2026 06:55:14 +0100 Subject: [PATCH] Use the right Polish plural for submission counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The submission list header and the board columns picked the noun with count === 1, which covers English but not Polish: 2 to 4 (and 22 to 24 and so on) need "zgłoszenia", not "zgłoszeń". countForm() in lib/plural.ts picks the form by Intl.PluralRules, and each dictionary gets a countSubmissionsFew entry ("zgłoszenia" in Polish, the plural everywhere else). As a side effect French and Portuguese now follow CLDR for zero as well. --- .../app/dashboard/[guildId]/board/page.tsx | 10 +++++++--- .../dashboard/[guildId]/submissions/page.tsx | 9 +++++++-- .../web/src/components/board/kanban-board.tsx | 8 +++++--- apps/web/src/i18n/dictionaries.ts | 7 +++++++ apps/web/src/lib/plural.test.ts | 19 +++++++++++++++++++ apps/web/src/lib/plural.ts | 13 +++++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/lib/plural.test.ts create mode 100644 apps/web/src/lib/plural.ts diff --git a/apps/web/src/app/dashboard/[guildId]/board/page.tsx b/apps/web/src/app/dashboard/[guildId]/board/page.tsx index 574be28..1533e8b 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"; @@ -93,8 +93,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 ef85b07..7734245 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"; @@ -74,7 +75,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 a020a61..3ad51b5 100644 --- a/apps/web/src/i18n/dictionaries.ts +++ b/apps/web/src/i18n/dictionaries.ts @@ -491,6 +491,7 @@ const en = { countForms: "forms", countSubmission: "submission", countSubmissions: "submissions", + countSubmissionsFew: "submissions", newForm: "New form", noForms: "No forms yet. Create your first one.", edit: "Edit", @@ -1794,6 +1795,7 @@ const de: Dictionary = { countForms: "Formulare", countSubmission: "Einreichung", countSubmissions: "Einreichungen", + countSubmissionsFew: "Einreichungen", newForm: "Neues Formular", noForms: "Noch keine Formulare. Erstelle dein erstes.", edit: "Bearbeiten", @@ -3109,6 +3111,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", @@ -4412,6 +4415,7 @@ const fr: Dictionary = { countForms: "formulaires", countSubmission: "soumission", countSubmissions: "soumissions", + countSubmissionsFew: "soumissions", newForm: "Nouveau formulaire", noForms: "Aucun formulaire. Créez le premier.", edit: "Modifier", @@ -5720,6 +5724,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", @@ -7020,6 +7025,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", @@ -8319,6 +8325,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; +}