feat(i18n): implement strict pluralization for bulk action notifications

Removes generic '(s)' syntax from backend responses. Adds proper _one and _other pluralization keys to all 16 locales with native grammar translations, resolving 0-count edge cases.

Fixes #3602
This commit is contained in:
harsh-aghara committed 2026-05-02 14:28:15 +05:30
1 parent 1b0b17b589
commit 657a35e948
18 files changed
+256 -57

No files matched your search

+41 -19
View File
@@ -1,9 +1,14 @@
import { useState, useEffect } from "react";
import { usePost } from "@/Hooks/UseApi";
import { post } from "@/Utils/ApiClient";
import { useToast } from "@/Hooks/UseToast";
import { useTranslation } from "react-i18next";
import { logger } from "@/Utils/logger";
import type { Monitor } from "@/Types/Monitor";
interface BulkPauseBody {
monitorIds: string[];
pause: boolean;
interface ApiResponse {
success: boolean;
msg: string;
data: Monitor[];
}
interface UseBulkMonitorActionsReturn {
@@ -19,33 +24,50 @@ export const useBulkMonitorActions = (
page?: number
): UseBulkMonitorActionsReturn => {
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const { post: bulkPost } = usePost<BulkPauseBody>();
const { toastSuccess, toastError, toastInfo } = useToast();
const { t } = useTranslation();
// Clear selection when page changes
useEffect(() => {
setSelectedRows([]);
}, [page]);
const handleBulkPause = async () => {
const result = await bulkPost("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause: true,
});
if (result) {
const executeBulkAction = async (pause: boolean) => {
try {
const res = await post<ApiResponse>("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause,
});
const affectedCount = res.data?.data?.length ?? 0;
if (affectedCount === 0) {
const key = pause
? "pages.common.monitors.bulkPause.alreadyPaused"
: "pages.common.monitors.bulkPause.alreadyRunning";
toastInfo(t(key, { count: selectedRows.length }));
} else {
const key = pause
? "pages.common.monitors.bulkPause.paused"
: "pages.common.monitors.bulkPause.resumed";
toastSuccess(t(key, { count: affectedCount }));
}
setSelectedRows([]);
refetch();
} catch (err: any) {
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
logger.error("Bulk pause/resume failed", err, { pause });
toastError(errMsg);
}
};
const handleBulkPause = async () => {
await executeBulkAction(true);
};
const handleBulkResume = async () => {
const result = await bulkPost("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause: false,
});
if (result) {
setSelectedRows([]);
refetch();
}
await executeBulkAction(false);
};
const handleCancelSelection = () => {
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "فتح الموقع",
"pause": "إيقاف مؤقت",
"resume": "استئناف",
"bulkSelected": "تم تحديد {{count}} جهاز(أجهزة) مراقبة",
"selectAll": "تحديد جميع أجهزة المراقبة",
"selectMonitor": "تحديد {{name}}"
"selectAll": "تحديد جميع الشاشات",
"selectMonitor": "تحديد {{name}}",
"bulkSelected_one": "{{count}} شاشة محددة",
"bulkSelected_other": "{{count}} شاشات محددة"
},
"statBoxes": {
"activeFor": "نشط منذ",
@@ -452,6 +453,16 @@
"optionHardware": "البنية التحتية",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "تم إيقاف {{count}} شاشة مؤقتاً",
"paused_other": "تم إيقاف {{count}} شاشات مؤقتاً",
"resumed_one": "تم استئناف {{count}} شاشة",
"resumed_other": "تم استئناف {{count}} شاشات",
"alreadyPaused_one": "الشاشة المحددة متوقفة مؤقتاً بالفعل",
"alreadyPaused_other": "جميع الشاشات المحددة متوقفة مؤقتاً بالفعل",
"alreadyRunning_one": "الشاشة المحددة قيد التشغيل بالفعل",
"alreadyRunning_other": "جميع الشاشات المحددة قيد التشغيل بالفعل"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Otevřít stránku",
"pause": "Pozastavit",
"resume": "Obnovit",
"bulkSelected": "{{count}} monitor(ů) vybráno",
"selectAll": "Vybrat všechny monitory",
"selectMonitor": "Vybrat {{name}}"
"selectMonitor": "Vybrat {{name}}",
"bulkSelected_one": "Vybrán {{count}} monitor",
"bulkSelected_other": "Vybráno {{count}} monitorů"
},
"statBoxes": {
"activeFor": "Aktivní po dobu",
@@ -452,6 +453,16 @@
"optionHardware": "Infrastruktura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} monitor pozastaven",
"paused_other": "{{count}} monitorů pozastaveno",
"resumed_one": "{{count}} monitor obnoven",
"resumed_other": "{{count}} monitorů obnoveno",
"alreadyPaused_one": "Vybraný monitor je již pozastaven",
"alreadyPaused_other": "Všechny vybrané monitory jsou již pozastaveny",
"alreadyRunning_one": "Vybraný monitor již běží",
"alreadyRunning_other": "Všechny vybrané monitory již běží"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Seite öffnen",
"pause": "Pausieren",
"resume": "Fortsetzen",
"bulkSelected": "{{count}} Monitor(e) ausgewählt",
"selectAll": "Alle Monitore auswählen",
"selectMonitor": "{{name}} auswählen"
"selectMonitor": "{{name}} auswählen",
"bulkSelected_one": "{{count}} Monitor ausgewählt",
"bulkSelected_other": "{{count}} Monitore ausgewählt"
},
"statBoxes": {
"activeFor": "Aktiv seit",
@@ -452,6 +453,16 @@
"optionHardware": "Infrastruktur",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} Monitor pausiert",
"paused_other": "{{count}} Monitore pausiert",
"resumed_one": "{{count}} Monitor fortgesetzt",
"resumed_other": "{{count}} Monitore fortgesetzt",
"alreadyPaused_one": "Der ausgewählte Monitor ist bereits pausiert",
"alreadyPaused_other": "Alle ausgewählten Monitore sind bereits pausiert",
"alreadyRunning_one": "Der ausgewählte Monitor wird bereits ausgeführt",
"alreadyRunning_other": "Alle ausgewählten Monitore werden bereits ausgeführt"
}
}
},
+12 -1
View File
@@ -422,10 +422,21 @@
"openSite": "Open site",
"pause": "Pause",
"resume": "Resume",
"bulkSelected": "{{count}} Monitor(s) selected",
"bulkSelected_one": "{{count}} monitor selected",
"bulkSelected_other": "{{count}} monitors selected",
"selectAll": "Select all monitors",
"selectMonitor": "Select {{name}}"
},
"bulkPause": {
"alreadyPaused_one": "The selected monitor is already paused",
"alreadyPaused_other": "All selected monitors are already paused",
"alreadyRunning_one": "The selected monitor is already running",
"alreadyRunning_other": "All selected monitors are already running",
"paused_one": "{{count}} monitor paused",
"paused_other": "{{count}} monitors paused",
"resumed_one": "{{count}} monitor resumed",
"resumed_other": "{{count}} monitors resumed"
},
"statBoxes": {
"activeFor": "Active for",
"certificateExpiry": "Certificate expiry",
+13 -2
View File
@@ -429,9 +429,10 @@
"openSite": "Abrir sitio",
"pause": "Pausar",
"resume": "Reanudar",
"bulkSelected": "{{count}} monitor(es) seleccionado(s)",
"selectAll": "Seleccionar todos los monitores",
"selectMonitor": "Seleccionar {{name}}"
"selectMonitor": "Seleccionar {{name}}",
"bulkSelected_one": "{{count}} monitor seleccionado",
"bulkSelected_other": "{{count}} monitores seleccionados"
},
"statBoxes": {
"activeFor": "Activo desde hace",
@@ -459,6 +460,16 @@
"optionHardware": "Infraestructura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} monitor pausado",
"paused_other": "{{count}} monitores pausados",
"resumed_one": "{{count}} monitor reanudado",
"resumed_other": "{{count}} monitores reanudados",
"alreadyPaused_one": "El monitor seleccionado ya está pausado",
"alreadyPaused_other": "Todos los monitores seleccionados ya están pausados",
"alreadyRunning_one": "El monitor seleccionado ya se está ejecutando",
"alreadyRunning_other": "Todos los monitores seleccionados ya se están ejecutando"
}
}
},
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "Avaa sivusto",
"pause": "Keskeytä",
"resume": "Jatka",
"bulkSelected": "{{count}} monitori(a) valittu",
"selectAll": "Valitse kaikki monitorit",
"selectMonitor": "Valitse {{name}}"
"selectAll": "Valitse kaikki näytöt",
"selectMonitor": "Valitse {{name}}",
"bulkSelected_one": "{{count}} näyttö valittu",
"bulkSelected_other": "{{count}} näyttöä valittu"
},
"statBoxes": {
"activeFor": "Aktiivinen",
@@ -452,6 +453,16 @@
"optionHardware": "Infrastruktuuri",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} näyttö keskeytetty",
"paused_other": "{{count}} näyttöä keskeytetty",
"resumed_one": "{{count}} näyttö jatkettu",
"resumed_other": "{{count}} näyttöä jatkettu",
"alreadyPaused_one": "Valittu näyttö on jo keskeytetty",
"alreadyPaused_other": "Kaikki valitut näytöt on jo keskeytetty",
"alreadyRunning_one": "Valittu näyttö on jo käynnissä",
"alreadyRunning_other": "Kaikki valitut näytöt ovat jo käynnissä"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Ouvrir le site",
"pause": "Pause",
"resume": "Reprendre",
"bulkSelected": "{{count}} moniteur(s) sélectionné(s)",
"selectAll": "Sélectionner tous les moniteurs",
"selectMonitor": "Sélectionner {{name}}"
"selectMonitor": "Sélectionner {{name}}",
"bulkSelected_one": "{{count}} moniteur sélectionné",
"bulkSelected_other": "{{count}} moniteurs sélectionnés"
},
"statBoxes": {
"activeFor": "Actif depuis",
@@ -452,6 +453,16 @@
"optionHardware": "Infrastructure",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} moniteur en pause",
"paused_other": "{{count}} moniteurs en pause",
"resumed_one": "{{count}} moniteur repris",
"resumed_other": "{{count}} moniteurs repris",
"alreadyPaused_one": "Le moniteur sélectionné est déjà en pause",
"alreadyPaused_other": "Tous les moniteurs sélectionnés sont déjà en pause",
"alreadyRunning_one": "Le moniteur sélectionné est déjà en cours d'exécution",
"alreadyRunning_other": "Tous les moniteurs sélectionnés sont déjà en cours d'exécution"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "サイトを開く",
"pause": "一時停止",
"resume": "再開",
"bulkSelected": "{{count}} 件のモニターを選択中",
"selectAll": "すべてのモニターを選択",
"selectMonitor": "{{name}} を選択"
"selectMonitor": "{{name}} を選択",
"bulkSelected_one": "{{count}} 個のモニターが選択されました",
"bulkSelected_other": "{{count}} 個のモニターが選択されました"
},
"statBoxes": {
"activeFor": "稼働期間",
@@ -452,6 +453,16 @@
"optionHardware": "インフラストラクチャ",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} 個のモニターを一時停止しました",
"paused_other": "{{count}} 個のモニターを一時停止しました",
"resumed_one": "{{count}} 個のモニターを再開しました",
"resumed_other": "{{count}} 個のモニターを再開しました",
"alreadyPaused_one": "選択したモニターはすでに一時停止されています",
"alreadyPaused_other": "選択したすべてのモニターはすでに一時停止されています",
"alreadyRunning_one": "選択したモニターはすでに実行中です",
"alreadyRunning_other": "選択したすべてのモニターはすでに実行中です"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Abrir site",
"pause": "Pausar",
"resume": "Retomar",
"bulkSelected": "{{count}} monitor(es) selecionado(s)",
"selectAll": "Selecionar todos os monitores",
"selectMonitor": "Selecionar {{name}}"
"selectMonitor": "Selecionar {{name}}",
"bulkSelected_one": "{{count}} monitor selecionado",
"bulkSelected_other": "{{count}} monitores selecionados"
},
"statBoxes": {
"activeFor": "Ativo há",
@@ -452,6 +453,16 @@
"optionHardware": "Infraestrutura",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} monitor pausado",
"paused_other": "{{count}} monitores pausados",
"resumed_one": "{{count}} monitor retomado",
"resumed_other": "{{count}} monitores retomados",
"alreadyPaused_one": "O monitor selecionado já está pausado",
"alreadyPaused_other": "Todos os monitores selecionados já estão pausados",
"alreadyRunning_one": "O monitor selecionado já está em execução",
"alreadyRunning_other": "Todos os monitores selecionados já estão em execução"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Открыть сайт",
"pause": "Пауза",
"resume": "Продолжить",
"bulkSelected": "Выбрано мониторов: {{count}}",
"selectAll": "Выбрать все мониторы",
"selectMonitor": "Выбрать {{name}}"
"selectMonitor": "Выбрать {{name}}",
"bulkSelected_one": "Выбран {{count}} монитор",
"bulkSelected_other": "Выбрано {{count}} мониторов"
},
"statBoxes": {
"activeFor": "Активен",
@@ -452,6 +453,16 @@
"optionHardware": "Инфраструктура",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} монитор приостановлен",
"paused_other": "{{count}} мониторов приостановлено",
"resumed_one": "{{count}} монитор возобновлен",
"resumed_other": "{{count}} мониторов возобновлено",
"alreadyPaused_one": "Выбранный монитор уже приостановлен",
"alreadyPaused_other": "Все выбранные мониторы уже приостановлены",
"alreadyRunning_one": "Выбранный монитор уже запущен",
"alreadyRunning_other": "Все выбранные мониторы уже запущены"
}
}
},
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "เปิดเว็บไซต์",
"pause": "หยุดชั่วคราว",
"resume": "ดำเนินการต่อ",
"bulkSelected": "เลือก {{count}} มอนิเตอร์แล้ว",
"selectAll": "เลือกมอนิเตอร์ทั้งหมด",
"selectMonitor": "เลือก {{name}}"
"selectAll": "เลือกจอภาพทั้งหมด",
"selectMonitor": "เลือก {{name}}",
"bulkSelected_one": "เลือกแล้ว {{count}} จอภาพ",
"bulkSelected_other": "เลือกแล้ว {{count}} จอภาพ"
},
"statBoxes": {
"activeFor": "ทำงานมาแล้ว",
@@ -452,6 +453,16 @@
"optionHardware": "โครงสร้างพื้นฐาน",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "หยุดจอภาพชั่วคราว {{count}} จอ",
"paused_other": "หยุดจอภาพชั่วคราว {{count}} จอ",
"resumed_one": "กลับมาทำงานต่อ {{count}} จอ",
"resumed_other": "กลับมาทำงานต่อ {{count}} จอ",
"alreadyPaused_one": "จอภาพที่เลือกถูกหยุดชั่วคราวแล้ว",
"alreadyPaused_other": "จอภาพที่เลือกทั้งหมดถูกหยุดชั่วคราวแล้ว",
"alreadyRunning_one": "จอภาพที่เลือกกำลังทำงานอยู่แล้ว",
"alreadyRunning_other": "จอภาพที่เลือกทั้งหมดกำลังทำงานอยู่แล้ว"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Siteyi aç",
"pause": "Duraklat",
"resume": "Devam et",
"bulkSelected": "{{count}} monitör seçildi",
"selectAll": "Tüm monitörleri seç",
"selectMonitor": "{{name}} seç"
"selectMonitor": "{{name}}'i seç",
"bulkSelected_one": "{{count}} monitör seçildi",
"bulkSelected_other": "{{count}} monitör seçildi"
},
"statBoxes": {
"activeFor": "Aktif süresi",
@@ -452,6 +453,16 @@
"optionHardware": "Altyapı",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} monitör duraklatıldı",
"paused_other": "{{count}} monitör duraklatıldı",
"resumed_one": "{{count}} monitör sürdürüldü",
"resumed_other": "{{count}} monitör sürdürüldü",
"alreadyPaused_one": "Seçili monitör zaten duraklatıldı",
"alreadyPaused_other": "Seçili tüm monitörler zaten duraklatıldı",
"alreadyRunning_one": "Seçili monitör zaten çalışıyor",
"alreadyRunning_other": "Seçili tüm monitörler zaten çalışıyor"
}
}
},
+13 -2
View File
@@ -422,9 +422,10 @@
"openSite": "Відкрити сайт",
"pause": "Призупинити",
"resume": "Відновити",
"bulkSelected": "Вибрано моніторів: {{count}}",
"selectAll": "Вибрати всі монітори",
"selectMonitor": "Вибрати {{name}}"
"selectMonitor": "Вибрати {{name}}",
"bulkSelected_one": "Вибрано {{count}} монітор",
"bulkSelected_other": "Вибрано {{count}} моніторів"
},
"statBoxes": {
"activeFor": "Активний протягом",
@@ -452,6 +453,16 @@
"optionHardware": "Інфраструктура",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "{{count}} монітор призупинено",
"paused_other": "{{count}} моніторів призупинено",
"resumed_one": "{{count}} монітор відновлено",
"resumed_other": "{{count}} моніторів відновлено",
"alreadyPaused_one": "Вибраний монітор уже призупинено",
"alreadyPaused_other": "Всі вибрані монітори уже призупинено",
"alreadyRunning_one": "Вибраний монітор уже запущено",
"alreadyRunning_other": "Всі вибрані монітори уже запущено"
}
}
},
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "Mở trang web",
"pause": "Tạm dừng",
"resume": "Tiếp tục",
"bulkSelected": "Đã chọn {{count}} monitor",
"selectAll": "Chọn tất cả monitor",
"selectMonitor": "Chọn {{name}}"
"selectAll": "Chọn tất cả màn hình",
"selectMonitor": "Chọn {{name}}",
"bulkSelected_one": "Đã chọn {{count}} màn hình",
"bulkSelected_other": "Đã chọn {{count}} màn hình"
},
"statBoxes": {
"activeFor": "Hoạt động từ",
@@ -452,6 +453,16 @@
"optionHardware": "Hạ tầng",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "Đã tạm dừng {{count}} màn hình",
"paused_other": "Đã tạm dừng {{count}} màn hình",
"resumed_one": "Đã tiếp tục {{count}} màn hình",
"resumed_other": "Đã tiếp tục {{count}} màn hình",
"alreadyPaused_one": "Màn hình đã chọn đã được tạm dừng",
"alreadyPaused_other": "Tất cả các màn hình đã chọn đã được tạm dừng",
"alreadyRunning_one": "Màn hình đã chọn đã đang chạy",
"alreadyRunning_other": "Tất cả các màn hình đã chọn đã đang chạy"
}
}
},
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "打开站点",
"pause": "暂停",
"resume": "恢复",
"bulkSelected": "已选择 {{count}} 个监控",
"selectAll": "选择所有监控",
"selectMonitor": "选择 {{name}}"
"selectAll": "选择所有监视器",
"selectMonitor": "选择 {{name}}",
"bulkSelected_one": "已选择 {{count}} 个监视器",
"bulkSelected_other": "已选择 {{count}} 个监视器"
},
"statBoxes": {
"activeFor": "已活跃",
@@ -452,6 +453,16 @@
"optionHardware": "基础设施",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "已暂停 {{count}} 个监视器",
"paused_other": "已暂停 {{count}} 个监视器",
"resumed_one": "已恢复 {{count}} 个监视器",
"resumed_other": "已恢复 {{count}} 个监视器",
"alreadyPaused_one": "所选监视器已暂停",
"alreadyPaused_other": "所有选定的监视器均已暂停",
"alreadyRunning_one": "所选监视器已经在运行",
"alreadyRunning_other": "所有选定的监视器均已在运行"
}
}
},
+14 -3
View File
@@ -422,9 +422,10 @@
"openSite": "開啟網站",
"pause": "暫停",
"resume": "恢復",
"bulkSelected": "已選擇 {{count}} 個監控",
"selectAll": "選擇所有監控",
"selectMonitor": "選擇 {{name}}"
"selectAll": "選擇所有監視器",
"selectMonitor": "選擇 {{name}}",
"bulkSelected_one": "已選擇 {{count}} 個監視器",
"bulkSelected_other": "已選擇 {{count}} 個監視器"
},
"statBoxes": {
"activeFor": "已啟用",
@@ -452,6 +453,16 @@
"optionHardware": "基礎架構",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
},
"bulkPause": {
"paused_one": "已暫停 {{count}} 個監視器",
"paused_other": "已暫停 {{count}} 個監視器",
"resumed_one": "已恢復 {{count}} 個監視器",
"resumed_other": "已恢復 {{count}} 個監視器",
"alreadyPaused_one": "所選監視器已暫停",
"alreadyPaused_other": "所有選定的監視器均已暫停",
"alreadyRunning_one": "所選監視器已經在運行",
"alreadyRunning_other": "所有選定的監視器均已在運行"
}
}
},
+2 -1
View File
@@ -318,10 +318,11 @@ class MonitorController implements IMonitorController {
const monitors = await this.monitorService.bulkPauseMonitors({ teamId, monitorIds, pause });
const action = pause ? "paused" : "resumed";
const monitorStr = monitors.length === 1 ? "monitor" : "monitors";
return res.status(200).json({
success: true,
msg: `${monitors.length} monitor(s) ${action} successfully`,
msg: `${monitors.length} ${monitorStr} ${action} successfully`,
data: monitors,
});
} catch (error) {