diff --git a/apps/web/components/automations/runs-section.test.tsx b/apps/web/components/automations/runs-section.test.tsx index b4b2767079..c8edac7e70 100644 --- a/apps/web/components/automations/runs-section.test.tsx +++ b/apps/web/components/automations/runs-section.test.tsx @@ -46,7 +46,9 @@ function setup(runs: AutomationRun[]) { }); render(); // The runs table only renders once the "Recent Runs" disclosure is expanded. - fireEvent.click(screen.getByText(/Recent Runs/)); + // The heading is count-aware ("Recent Run (1)" / "Recent Runs (2)"), so the + // matcher must tolerate both forms rather than pinning the plural. + fireEvent.click(screen.getByText(/Recent Runs?\s*\(/)); } describe("RunsSection status badges", () => { diff --git a/apps/web/components/github/my-github/pr-status-badges.test.tsx b/apps/web/components/github/my-github/pr-status-badges.test.tsx new file mode 100644 index 0000000000..aae7e45a11 --- /dev/null +++ b/apps/web/components/github/my-github/pr-status-badges.test.tsx @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; + +import { PRStatusBadges } from "./pr-status-badges"; +import type { GitHubPR, GitHubPRStatus } from "@/lib/types/github"; + +afterEach(cleanup); + +function makePR(): GitHubPR { + return { + number: 1, + title: "Test PR", + url: "https://api.github.com/repos/o/r/pulls/1", + html_url: "https://github.com/o/r/pull/1", + state: "open", + head_branch: "feature", + base_branch: "main", + author_login: "octocat", + repo_owner: "o", + repo_name: "r", + draft: false, + mergeable: true, + additions: 1, + deletions: 0, + requested_reviewers: [], + created_at: "2026-08-06T00:00:00Z", + updated_at: "2026-08-06T00:00:00Z", + merged_at: null, + closed_at: null, + }; +} + +function makeStatus(pendingReviewCount: number): GitHubPRStatus { + return { + pr: makePR(), + review_state: "", + checks_state: "", + mergeable_state: "clean", + review_count: 0, + pending_review_count: pendingReviewCount, + checks_total: 0, + checks_passing: 0, + }; +} + +describe("PRStatusBadges review chip tooltip", () => { + // The tooltip used to be `pendingReviewS`, interpolating {{pending}} into a + // hardcoded "review(s)". i18next cannot select a plural from a value that is + // not `count`, so the parenthetical was the only way English could stay + // truthful — and every translation inherited it. These two cases pin the + // plural-aware replacement. + // The query is deliberately loose and the assertion exact: a wrong plural then + // fails on the value ("expected '1 pending reviews' to be '1 pending review'") + // rather than on the lookup, which would only report a missing element. + it("uses the singular form for exactly one pending review", () => { + render(); + expect(screen.getByTitle(/pending review/).getAttribute("title")).toBe("1 pending review"); + }); + + it("uses the plural form for more than one pending review", () => { + render(); + expect(screen.getByTitle(/pending review/).getAttribute("title")).toBe("3 pending reviews"); + }); +}); diff --git a/apps/web/components/github/my-github/pr-status-badges.tsx b/apps/web/components/github/my-github/pr-status-badges.tsx index 9d26e9a9fe..ac6a5c7517 100644 --- a/apps/web/components/github/my-github/pr-status-badges.tsx +++ b/apps/web/components/github/my-github/pr-status-badges.tsx @@ -72,7 +72,7 @@ function ReviewChip({ Icon={IconClockHour4} label={t("github:pending", { pending })} tone="pending" - title={t("github:pendingReviewS", { pending })} + title={t("github:pendingReviews", { count: pending })} /> ); return null; diff --git a/apps/web/src/locales/en/agents.json b/apps/web/src/locales/en/agents.json index 6bdaeca308..33f55e3ac2 100644 --- a/apps/web/src/locales/en/agents.json +++ b/apps/web/src/locales/en/agents.json @@ -18,7 +18,7 @@ "authRequiredOpenShell": "Authentication required - click to open a shell and sign in", "authenticationRequired": "Authentication required", "automaticallyAvailable": "Automatically available", - "availableCommandsCount_one": "Available commands ({{count}})", + "availableCommandsCount_one": "Available command ({{count}})", "availableCommandsCount_other": "Available commands ({{count}})", "availableSlashCommands": "Available slash commands", "availableSlashCommandsHelp": "Type these during a session chat to invoke them - e.g. <0>{{example}}.", diff --git a/apps/web/src/locales/en/automations.json b/apps/web/src/locales/en/automations.json index a499e35408..19c5a36fe7 100644 --- a/apps/web/src/locales/en/automations.json +++ b/apps/web/src/locales/en/automations.json @@ -104,7 +104,7 @@ "prTriggerRepositoryHelp": "PR triggers always use the PR's own repository.", "pushTriggerHelp": "Triggers when code is pushed to matching branches", "recentRuns": "Recent runs", - "recentRuns_one": "Recent Runs ({{count}})", + "recentRuns_one": "Recent Run ({{count}})", "recentRuns_other": "Recent Runs ({{count}})", "refresh": "Refresh", "removeRepository": "Remove repository", diff --git a/apps/web/src/locales/en/github.json b/apps/web/src/locales/en/github.json index 32fc6c274e..49c70fd673 100644 --- a/apps/web/src/locales/en/github.json +++ b/apps/web/src/locales/en/github.json @@ -352,7 +352,8 @@ "pendingReview": "Pending review", "pendingReviewCount_one": "({{count}} pending)", "pendingReviewCount_other": "({{count}} pending)", - "pendingReviewS": "{{pending}} pending review(s)", + "pendingReviews_one": "{{count}} pending review", + "pendingReviews_other": "{{count}} pending reviews", "permissionsAvailableToKandevThroughThis": "Permissions available to Kandev through this workspace's GitHub App installation.", "personalAccessToken": "Personal access token", "personalAccount": "Personal account", diff --git a/apps/web/src/locales/en/sentry.json b/apps/web/src/locales/en/sentry.json index f27f96dac0..6699c40b52 100644 --- a/apps/web/src/locales/en/sentry.json +++ b/apps/web/src/locales/en/sentry.json @@ -30,7 +30,7 @@ "enterNameAndTokenBeforeSaving": "Enter an instance name and auth token before saving.", "environment": "Environment", "environmentHelp": "Optional — restrict to one environment.", - "eventsCount_one": "events {{count}}", + "eventsCount_one": "event {{count}}", "eventsCount_other": "events {{count}}", "executorProfile": "Executor Profile", "failedToLoadInstances": "Failed to load Sentry instances: {{error}}", @@ -161,7 +161,7 @@ "update": "Update", "updateFailed": "Update failed: {{error}}", "useStepDefault": "(use step default)", - "usersCount_one": "users {{count}}", + "usersCount_one": "user {{count}}", "usersCount_other": "users {{count}}", "watchDialogDescription": "Poll Sentry with a structured filter and auto-create a Kandev task for each newly-matching issue. Optionally bind a repository so each task runs against that codebase, or leave it unset to run with no repository.", "watcherCreated": "Watcher created", diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json index 29434a580e..9c63b9dda6 100644 --- a/apps/web/src/locales/en/task.json +++ b/apps/web/src/locales/en/task.json @@ -358,7 +358,7 @@ "dropFilesHere": "Drop files here", "duration": "duration", "durationValue": "Duration: {{duration}}", - "earlierUpdates_one": "Earlier updates ({{count}})", + "earlierUpdates_one": "Earlier update ({{count}})", "earlierUpdates_other": "Earlier updates ({{count}})", "editBranch": "Edit branch {{branch}}", "editBranch2": "Edit branch", @@ -1138,7 +1138,7 @@ "sidebarFilters": "Sidebar filters", "skipAllQuestions": "Skip all questions", "skipped": "Skipped", - "snapshotMessageCount_one": "{{count}} messages", + "snapshotMessageCount_one": "{{count}} message", "snapshotMessageCount_other": "{{count}} messages", "softReset": "Soft Reset", "someone": "Someone", @@ -1248,7 +1248,7 @@ "taskTreeRestored": "Task tree restored", "taskTreeResumed": "Task tree resumed", "tasks": "Tasks", - "tasksAffected_one": "{{count}} tasks affected", + "tasksAffected_one": "{{count}} task affected", "tasksAffected_other": "{{count}} tasks affected", "tcpPortsWithActiveListenersInside": "TCP ports with active listeners inside the remote executor. Click refresh to re-scan.", "technicalDetails": "Technical details", @@ -1379,7 +1379,7 @@ "versionNumber": "Version {{version}}", "viaSession": "via session", "view": "View:", - "viewLimitReached_one": "You can save up to {{count}} views.", + "viewLimitReached_one": "You can save up to {{count}} view.", "viewLimitReached_other": "You can save up to {{count}} views.", "viewName": "View name", "viewPlanHistory": "View plan history", @@ -1404,7 +1404,7 @@ "websocketClientNotAvailable": "WebSocket client not available", "whatShouldTheAgentWorkOn": "What should the agent work on?", "workdir": "Workdir", - "workedRanCommands_one": "Worked -- ran {{count}} commands", + "workedRanCommands_one": "Worked -- ran {{count}} command", "workedRanCommands_other": "Worked -- ran {{count}} commands", "workflow": "workflow", "workflowCount_one": "{{count}} workflow", diff --git a/apps/web/src/locales/pseudo/agents.json b/apps/web/src/locales/pseudo/agents.json index b09ba3a34f..6dfce12107 100644 --- a/apps/web/src/locales/pseudo/agents.json +++ b/apps/web/src/locales/pseudo/agents.json @@ -18,7 +18,7 @@ "authRequiredOpenShell": "Àũţĥēńţĩćàţĩōń ŕēqũĩŕēď - ćĺĩćķ ţō ōƥēń à śĥēĺĺ àńď śĩĝń ĩń", "authenticationRequired": "Àũţĥēńţĩćàţĩōń ŕēqũĩŕēď", "automaticallyAvailable": "Àũţōḿàţĩćàĺĺŷ àvàĩĺàƀĺē", - "availableCommandsCount_one": "Àvàĩĺàƀĺē ćōḿḿàńďś ({{count}})", + "availableCommandsCount_one": "Àvàĩĺàƀĺē ćōḿḿàńď ({{count}})", "availableCommandsCount_other": "Àvàĩĺàƀĺē ćōḿḿàńďś ({{count}})", "availableSlashCommands": "Àvàĩĺàƀĺē śĺàśĥ ćōḿḿàńďś", "availableSlashCommandsHelp": "Ţŷƥē ţĥēśē ďũŕĩńĝ à śēśśĩōń ćĥàţ ţō ĩńvōķē ţĥēḿ - ē.ĝ. <0>{{example}}.", diff --git a/apps/web/src/locales/pseudo/automations.json b/apps/web/src/locales/pseudo/automations.json index b333e23b8d..c9b85b4c4d 100644 --- a/apps/web/src/locales/pseudo/automations.json +++ b/apps/web/src/locales/pseudo/automations.json @@ -104,7 +104,7 @@ "prTriggerRepositoryHelp": "ƤŔ ţŕĩĝĝēŕś àĺŵàŷś ũśē ţĥē ƤŔ'ś ōŵń ŕēƥōśĩţōŕŷ.", "pushTriggerHelp": "Ţŕĩĝĝēŕś ŵĥēń ćōďē ĩś ƥũśĥēď ţō ḿàţćĥĩńĝ ƀŕàńćĥēś", "recentRuns": "Ŕēćēńţ ŕũńś", - "recentRuns_one": "Ŕēćēńţ Ŕũńś ({{count}})", + "recentRuns_one": "Ŕēćēńţ Ŕũń ({{count}})", "recentRuns_other": "Ŕēćēńţ Ŕũńś ({{count}})", "refresh": "Ŕēƒŕēśĥ", "removeRepository": "Ŕēḿōvē ŕēƥōśĩţōŕŷ", diff --git a/apps/web/src/locales/pseudo/github.json b/apps/web/src/locales/pseudo/github.json index 13d6a4c5db..1847a7dae9 100644 --- a/apps/web/src/locales/pseudo/github.json +++ b/apps/web/src/locales/pseudo/github.json @@ -352,7 +352,8 @@ "pendingReview": "Ƥēńďĩńĝ ŕēvĩēŵ", "pendingReviewCount_one": "({{count}} ƥēńďĩńĝ)", "pendingReviewCount_other": "({{count}} ƥēńďĩńĝ)", - "pendingReviewS": "{{pending}} ƥēńďĩńĝ ŕēvĩēŵ(ś)", + "pendingReviews_one": "{{count}} ƥēńďĩńĝ ŕēvĩēŵ", + "pendingReviews_other": "{{count}} ƥēńďĩńĝ ŕēvĩēŵś", "permissionsAvailableToKandevThroughThis": "Ƥēŕḿĩśśĩōńś àvàĩĺàƀĺē ţō Ķàńďēv ţĥŕōũĝĥ ţĥĩś ŵōŕķśƥàćē'ś ĜĩţĤũƀ Àƥƥ ĩńśţàĺĺàţĩōń.", "personalAccessToken": "Ƥēŕśōńàĺ àććēśś ţōķēń", "personalAccount": "Ƥēŕśōńàĺ àććōũńţ", diff --git a/apps/web/src/locales/pseudo/sentry.json b/apps/web/src/locales/pseudo/sentry.json index a8a6caece3..c27249c4f6 100644 --- a/apps/web/src/locales/pseudo/sentry.json +++ b/apps/web/src/locales/pseudo/sentry.json @@ -30,7 +30,7 @@ "enterNameAndTokenBeforeSaving": "Ēńţēŕ àń ĩńśţàńćē ńàḿē àńď àũţĥ ţōķēń ƀēƒōŕē śàvĩńĝ.", "environment": "Ēńvĩŕōńḿēńţ", "environmentHelp": "Ōƥţĩōńàĺ — ŕēśţŕĩćţ ţō ōńē ēńvĩŕōńḿēńţ.", - "eventsCount_one": "ēvēńţś {{count}}", + "eventsCount_one": "ēvēńţ {{count}}", "eventsCount_other": "ēvēńţś {{count}}", "executorProfile": "Ēxēćũţōŕ Ƥŕōƒĩĺē", "failedToLoadInstances": "Ƒàĩĺēď ţō ĺōàď Śēńţŕŷ ĩńśţàńćēś: {{error}}", @@ -161,7 +161,7 @@ "update": "Ũƥďàţē", "updateFailed": "Ũƥďàţē ƒàĩĺēď: {{error}}", "useStepDefault": "(ũśē śţēƥ ďēƒàũĺţ)", - "usersCount_one": "ũśēŕś {{count}}", + "usersCount_one": "ũśēŕ {{count}}", "usersCount_other": "ũśēŕś {{count}}", "watchDialogDescription": "Ƥōĺĺ Śēńţŕŷ ŵĩţĥ à śţŕũćţũŕēď ƒĩĺţēŕ àńď àũţō-ćŕēàţē à Ķàńďēv ţàśķ ƒōŕ ēàćĥ ńēŵĺŷ-ḿàţćĥĩńĝ ĩśśũē. Ōƥţĩōńàĺĺŷ ƀĩńď à ŕēƥōśĩţōŕŷ śō ēàćĥ ţàśķ ŕũńś àĝàĩńśţ ţĥàţ ćōďēƀàśē, ōŕ ĺēàvē ĩţ ũńśēţ ţō ŕũń ŵĩţĥ ńō ŕēƥōśĩţōŕŷ.", "watcherCreated": "Ŵàţćĥēŕ ćŕēàţēď", diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json index cd86922f0e..5bc78ad239 100644 --- a/apps/web/src/locales/pseudo/task.json +++ b/apps/web/src/locales/pseudo/task.json @@ -358,7 +358,7 @@ "dropFilesHere": "Ďŕōƥ ƒĩĺēś ĥēŕē", "duration": "ďũŕàţĩōń", "durationValue": "Ďũŕàţĩōń: {{duration}}", - "earlierUpdates_one": "Ēàŕĺĩēŕ ũƥďàţēś ({{count}})", + "earlierUpdates_one": "Ēàŕĺĩēŕ ũƥďàţē ({{count}})", "earlierUpdates_other": "Ēàŕĺĩēŕ ũƥďàţēś ({{count}})", "editBranch": "Ēďĩţ ƀŕàńćĥ {{branch}}", "editBranch2": "Ēďĩţ ƀŕàńćĥ", @@ -1138,7 +1138,7 @@ "sidebarFilters": "Śĩďēƀàŕ ƒĩĺţēŕś", "skipAllQuestions": "Śķĩƥ àĺĺ qũēśţĩōńś", "skipped": "Śķĩƥƥēď", - "snapshotMessageCount_one": "{{count}} ḿēśśàĝēś", + "snapshotMessageCount_one": "{{count}} ḿēśśàĝē", "snapshotMessageCount_other": "{{count}} ḿēśśàĝēś", "softReset": "Śōƒţ Ŕēśēţ", "someone": "Śōḿēōńē", @@ -1248,7 +1248,7 @@ "taskTreeRestored": "Ţàśķ ţŕēē ŕēśţōŕēď", "taskTreeResumed": "Ţàśķ ţŕēē ŕēśũḿēď", "tasks": "Ţàśķś", - "tasksAffected_one": "{{count}} ţàśķś àƒƒēćţēď", + "tasksAffected_one": "{{count}} ţàśķ àƒƒēćţēď", "tasksAffected_other": "{{count}} ţàśķś àƒƒēćţēď", "tcpPortsWithActiveListenersInside": "ŢĆƤ ƥōŕţś ŵĩţĥ àćţĩvē ĺĩśţēńēŕś ĩńśĩďē ţĥē ŕēḿōţē ēxēćũţōŕ. Ćĺĩćķ ŕēƒŕēśĥ ţō ŕē-śćàń.", "technicalDetails": "Ţēćĥńĩćàĺ ďēţàĩĺś", @@ -1379,7 +1379,7 @@ "versionNumber": "Vēŕśĩōń {{version}}", "viaSession": "vĩà śēśśĩōń", "view": "Vĩēŵ:", - "viewLimitReached_one": "Ŷōũ ćàń śàvē ũƥ ţō {{count}} vĩēŵś.", + "viewLimitReached_one": "Ŷōũ ćàń śàvē ũƥ ţō {{count}} vĩēŵ.", "viewLimitReached_other": "Ŷōũ ćàń śàvē ũƥ ţō {{count}} vĩēŵś.", "viewName": "Vĩēŵ ńàḿē", "viewPlanHistory": "Vĩēŵ ƥĺàń ĥĩśţōŕŷ", @@ -1404,7 +1404,7 @@ "websocketClientNotAvailable": "ŴēƀŚōćķēţ ćĺĩēńţ ńōţ àvàĩĺàƀĺē", "whatShouldTheAgentWorkOn": "Ŵĥàţ śĥōũĺď ţĥē àĝēńţ ŵōŕķ ōń?", "workdir": "Ŵōŕķďĩŕ", - "workedRanCommands_one": "Ŵōŕķēď -- ŕàń {{count}} ćōḿḿàńďś", + "workedRanCommands_one": "Ŵōŕķēď -- ŕàń {{count}} ćōḿḿàńď", "workedRanCommands_other": "Ŵōŕķēď -- ŕàń {{count}} ćōḿḿàńďś", "workflow": "ŵōŕķƒĺōŵ", "workflowCount_one": "{{count}} ŵōŕķƒĺōŵ", diff --git a/apps/web/src/locales/pt-pt/agents.json b/apps/web/src/locales/pt-pt/agents.json index 8227f66cf9..7261e3a83d 100644 --- a/apps/web/src/locales/pt-pt/agents.json +++ b/apps/web/src/locales/pt-pt/agents.json @@ -18,7 +18,7 @@ "authRequiredOpenShell": "É necessária autenticação - clique para abrir uma shell e iniciar sessão", "authenticationRequired": "É necessária autenticação", "automaticallyAvailable": "Disponível automaticamente", - "availableCommandsCount_one": "Comandos disponíveis ({{count}})", + "availableCommandsCount_one": "Comando disponível ({{count}})", "availableCommandsCount_other": "Comandos disponíveis ({{count}})", "availableSlashCommands": "Comandos slash disponíveis", "availableSlashCommandsHelp": "Escreva-os durante o chat de uma sessão para os invocar - ex.: <0>{{example}}.", diff --git a/apps/web/src/locales/pt-pt/automations.json b/apps/web/src/locales/pt-pt/automations.json index 8ad6f07b96..d1d73cc087 100644 --- a/apps/web/src/locales/pt-pt/automations.json +++ b/apps/web/src/locales/pt-pt/automations.json @@ -104,7 +104,7 @@ "prTriggerRepositoryHelp": "Os acionadores de PR usam sempre o repositório do próprio PR.", "pushTriggerHelp": "Aciona quando é feito push de código para branches correspondentes", "recentRuns": "Execuções recentes", - "recentRuns_one": "Execuções recentes ({{count}})", + "recentRuns_one": "Execução recente ({{count}})", "recentRuns_other": "Execuções recentes ({{count}})", "refresh": "Atualizar", "removeRepository": "Remover repositório", diff --git a/apps/web/src/locales/pt-pt/github.json b/apps/web/src/locales/pt-pt/github.json index b9a9f3b4e8..53cd1414ed 100644 --- a/apps/web/src/locales/pt-pt/github.json +++ b/apps/web/src/locales/pt-pt/github.json @@ -352,7 +352,8 @@ "pendingReview": "Revisão pendente", "pendingReviewCount_one": "({{count}} pendente)", "pendingReviewCount_other": "({{count}} pendentes)", - "pendingReviewS": "{{pending}} revisão(ões) pendente(s)", + "pendingReviews_one": "{{count}} revisão pendente", + "pendingReviews_other": "{{count}} revisões pendentes", "permissionsAvailableToKandevThroughThis": "Permissões disponíveis para o Kandev através da instalação da App do GitHub nesta área de trabalho.", "personalAccessToken": "Token de acesso pessoal", "personalAccount": "Conta pessoal", diff --git a/apps/web/src/locales/pt-pt/sentry.json b/apps/web/src/locales/pt-pt/sentry.json index 5732c4fc8f..49105fb53b 100644 --- a/apps/web/src/locales/pt-pt/sentry.json +++ b/apps/web/src/locales/pt-pt/sentry.json @@ -30,7 +30,7 @@ "enterNameAndTokenBeforeSaving": "Introduza um nome de instância e um token de autenticação antes de guardar.", "environment": "Ambiente", "environmentHelp": "Opcional — restringir a um ambiente.", - "eventsCount_one": "eventos {{count}}", + "eventsCount_one": "evento {{count}}", "eventsCount_other": "eventos {{count}}", "executorProfile": "Perfil de executor", "failedToLoadInstances": "Não foi possível carregar as instâncias do Sentry: {{error}}", @@ -161,7 +161,7 @@ "update": "Atualizar", "updateFailed": "Não foi possível atualizar: {{error}}", "useStepDefault": "(usar a pré-definição do passo)", - "usersCount_one": "utilizadores {{count}}", + "usersCount_one": "utilizador {{count}}", "usersCount_other": "utilizadores {{count}}", "watchDialogDescription": "Sonde o Sentry com um filtro estruturado e crie automaticamente uma tarefa do Kandev para cada nova issue correspondente. Opcionalmente, associe um repositório para que cada tarefa seja executada sobre essa base de código, ou deixe por definir para executar sem repositório.", "watcherCreated": "Vigilância criada", diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json index 1b566b7a15..5fd7f242fd 100644 --- a/apps/web/src/locales/pt-pt/task.json +++ b/apps/web/src/locales/pt-pt/task.json @@ -195,7 +195,7 @@ "draftCreated": "Rascunho de {{longName}} criado", "duration": "duração", "durationValue": "Duração: {{duration}}", - "earlierUpdates_one": "Atualizações anteriores ({{count}})", + "earlierUpdates_one": "Atualização anterior ({{count}})", "earlierUpdates_other": "Atualizações anteriores ({{count}})", "editComment": "Editar comentário", "empty": "(vazio)",