From 89d37f5ef37686fd4f5f667d039021c7dbb1b296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Sun, 20 Sep 2026 20:12:05 +0200 Subject: [PATCH] feat(spend-alerts): add spend alerts on web and mobile https://github.com/Kilo-Org/cloud/pull/6291 --- .../(app)/(tabs)/(3_profile)/spend-alerts.tsx | 8 + .../notifications-screen.mounted.test.tsx | 81 + .../src/components/notifications-screen.tsx | 26 +- .../components/organization/hub-screen.tsx | 13 +- .../spend-alert-validators.test.ts | 101 + .../organization/spend-alert-validators.ts | 91 + .../spend-alerts-screen.mounted.test.tsx | 664 + .../organization/spend-alerts-screen.tsx | 670 + ...nces-screen.feature-flags.mounted.test.tsx | 1 + .../preferences-screen.mounted.test.tsx | 10 +- .../src/components/preferences-screen.tsx | 11 +- apps/mobile/src/i18n/locales/af.json | 28 +- apps/mobile/src/i18n/locales/am.json | 28 +- apps/mobile/src/i18n/locales/ar.json | 28 +- apps/mobile/src/i18n/locales/az.json | 28 +- apps/mobile/src/i18n/locales/be.json | 28 +- apps/mobile/src/i18n/locales/bg.json | 28 +- apps/mobile/src/i18n/locales/bn.json | 28 +- apps/mobile/src/i18n/locales/bs.json | 28 +- apps/mobile/src/i18n/locales/ca.json | 28 +- apps/mobile/src/i18n/locales/ckb.json | 28 +- apps/mobile/src/i18n/locales/cs.json | 28 +- apps/mobile/src/i18n/locales/cy.json | 28 +- apps/mobile/src/i18n/locales/da.json | 28 +- apps/mobile/src/i18n/locales/de.json | 28 +- apps/mobile/src/i18n/locales/el.json | 28 +- apps/mobile/src/i18n/locales/en.json | 28 +- apps/mobile/src/i18n/locales/es.json | 28 +- apps/mobile/src/i18n/locales/et.json | 28 +- apps/mobile/src/i18n/locales/eu.json | 28 +- apps/mobile/src/i18n/locales/fa.json | 28 +- apps/mobile/src/i18n/locales/fi.json | 28 +- apps/mobile/src/i18n/locales/fil.json | 28 +- apps/mobile/src/i18n/locales/fr.json | 28 +- apps/mobile/src/i18n/locales/ga.json | 28 +- apps/mobile/src/i18n/locales/gl.json | 28 +- apps/mobile/src/i18n/locales/gu.json | 28 +- apps/mobile/src/i18n/locales/ha.json | 28 +- apps/mobile/src/i18n/locales/he.json | 28 +- apps/mobile/src/i18n/locales/hi.json | 28 +- apps/mobile/src/i18n/locales/hr.json | 28 +- apps/mobile/src/i18n/locales/ht.json | 28 +- apps/mobile/src/i18n/locales/hu.json | 28 +- apps/mobile/src/i18n/locales/hy.json | 28 +- apps/mobile/src/i18n/locales/id.json | 28 +- apps/mobile/src/i18n/locales/ig.json | 28 +- apps/mobile/src/i18n/locales/is.json | 28 +- apps/mobile/src/i18n/locales/it.json | 28 +- apps/mobile/src/i18n/locales/ja.json | 28 +- apps/mobile/src/i18n/locales/ka.json | 28 +- apps/mobile/src/i18n/locales/kk.json | 28 +- apps/mobile/src/i18n/locales/km.json | 28 +- apps/mobile/src/i18n/locales/kn.json | 28 +- apps/mobile/src/i18n/locales/ko.json | 28 +- apps/mobile/src/i18n/locales/lo.json | 28 +- apps/mobile/src/i18n/locales/lt.json | 28 +- apps/mobile/src/i18n/locales/lv.json | 28 +- apps/mobile/src/i18n/locales/mg.json | 28 +- apps/mobile/src/i18n/locales/mi.json | 28 +- apps/mobile/src/i18n/locales/mk.json | 28 +- apps/mobile/src/i18n/locales/ml.json | 28 +- apps/mobile/src/i18n/locales/mn.json | 28 +- apps/mobile/src/i18n/locales/mr.json | 28 +- apps/mobile/src/i18n/locales/ms.json | 28 +- apps/mobile/src/i18n/locales/mt.json | 28 +- apps/mobile/src/i18n/locales/my.json | 28 +- apps/mobile/src/i18n/locales/nb.json | 28 +- apps/mobile/src/i18n/locales/ne.json | 28 +- apps/mobile/src/i18n/locales/nl.json | 28 +- apps/mobile/src/i18n/locales/om.json | 28 +- apps/mobile/src/i18n/locales/or.json | 28 +- apps/mobile/src/i18n/locales/pa.json | 28 +- apps/mobile/src/i18n/locales/pl.json | 28 +- apps/mobile/src/i18n/locales/ps.json | 28 +- apps/mobile/src/i18n/locales/pt-BR.json | 28 +- apps/mobile/src/i18n/locales/pt.json | 28 +- apps/mobile/src/i18n/locales/ro.json | 28 +- apps/mobile/src/i18n/locales/ru.json | 28 +- apps/mobile/src/i18n/locales/si.json | 28 +- apps/mobile/src/i18n/locales/sk.json | 28 +- apps/mobile/src/i18n/locales/sl.json | 28 +- apps/mobile/src/i18n/locales/so.json | 28 +- apps/mobile/src/i18n/locales/sq.json | 28 +- apps/mobile/src/i18n/locales/sr.json | 28 +- apps/mobile/src/i18n/locales/sv.json | 28 +- apps/mobile/src/i18n/locales/sw.json | 28 +- apps/mobile/src/i18n/locales/ta.json | 28 +- apps/mobile/src/i18n/locales/te.json | 28 +- apps/mobile/src/i18n/locales/th.json | 28 +- apps/mobile/src/i18n/locales/tr.json | 28 +- apps/mobile/src/i18n/locales/uk.json | 28 +- apps/mobile/src/i18n/locales/ur.json | 28 +- apps/mobile/src/i18n/locales/uz.json | 28 +- apps/mobile/src/i18n/locales/vi.json | 28 +- apps/mobile/src/i18n/locales/yo.json | 28 +- apps/mobile/src/i18n/locales/zh-Hans.json | 28 +- apps/mobile/src/i18n/locales/zh-Hant.json | 28 +- apps/mobile/src/i18n/locales/zu.json | 28 +- .../src/lib/auth/passkey-client.test.ts | 9 + .../agent-push-preference.optimistic.test.ts | 4 + .../lib/hooks/agent-push-preference.test.ts | 32 +- .../src/lib/hooks/agent-push-preference.ts | 3 + .../src/lib/notification-path-schema.test.ts | 128 + apps/mobile/src/lib/notification-path.test.ts | 144 +- apps/mobile/src/lib/notification-path.ts | 7 + .../cron/dispatch-spend-alerts/route.test.ts | 112 + .../api/cron/dispatch-spend-alerts/route.ts | 80 + .../spend-alerts/SpendAlertsPanel.tsx | 457 + .../spendAlertsPanelState.test.ts | 696 + .../spend-alerts/spendAlertsPanelState.ts | 503 + .../usage-analytics/BreakdownBarChart.tsx | 44 +- .../usage-analytics/MetricCard.test.ts | 45 + .../components/usage-analytics/MetricCard.tsx | 10 +- .../UsageAnalyticsDashboard.tsx | 13 + apps/web/src/emails/AGENTS.md | 1 + apps/web/src/emails/spendAlert.html | 200 + .../ai-gateway/openai-chatgpt/routing.test.ts | 71 +- .../lib/ai-gateway/openai-chatgpt/routing.ts | 10 +- apps/web/src/lib/email.test.ts | 112 + apps/web/src/lib/email.ts | 95 + .../lib/notifications-worker-client.test.ts | 162 + .../src/lib/notifications-worker-client.ts | 63 +- .../web/src/lib/spend-alerts/delivery.test.ts | 481 + apps/web/src/lib/spend-alerts/delivery.ts | 536 + .../web/src/lib/spend-alerts/settings.test.ts | 388 + apps/web/src/lib/spend-alerts/settings.ts | 534 + apps/web/src/lib/spend-alerts/sweep.test.ts | 624 + apps/web/src/lib/spend-alerts/sweep.ts | 716 + apps/web/src/lib/user/index.test.ts | 89 + apps/web/src/lib/user/index.ts | 52 + .../admin/email-testing-router.test.ts | 22 + .../src/routers/admin/email-testing-router.ts | 43 +- apps/web/src/routers/root-router.ts | 2 + .../routers/spend-alert-router.schema.test.ts | 37 + .../src/routers/spend-alert-router.test.ts | 234 + apps/web/src/routers/spend-alert-router.ts | 298 + apps/web/src/routers/user-router.test.ts | 66 +- apps/web/src/routers/user-router.ts | 17 +- apps/web/vercel.json | 4 + packages/db/jest.config.mjs | 34 + packages/db/package.json | 5 +- packages/db/src/client.test.ts | 9 +- packages/db/src/migration-journal.test.ts | 44 + .../migrations/0254_charming_iron_patriot.sql | 70 + .../db/src/migrations/meta/0254_snapshot.json | 42874 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema.test.ts | 9 +- packages/db/src/schema.ts | 152 + packages/db/src/spend-alerts-schema.test.ts | 161 + packages/notifications/src/locales/af.json | 5 + packages/notifications/src/locales/am.json | 5 + packages/notifications/src/locales/ar.json | 5 + packages/notifications/src/locales/az.json | 5 + packages/notifications/src/locales/be.json | 5 + packages/notifications/src/locales/bg.json | 5 + packages/notifications/src/locales/bn.json | 5 + packages/notifications/src/locales/bs.json | 5 + packages/notifications/src/locales/ca.json | 5 + packages/notifications/src/locales/ckb.json | 5 + packages/notifications/src/locales/cs.json | 5 + packages/notifications/src/locales/cy.json | 5 + packages/notifications/src/locales/da.json | 5 + packages/notifications/src/locales/de.json | 5 + packages/notifications/src/locales/el.json | 5 + packages/notifications/src/locales/en.json | 5 + packages/notifications/src/locales/es.json | 5 + packages/notifications/src/locales/et.json | 5 + packages/notifications/src/locales/eu.json | 5 + packages/notifications/src/locales/fa.json | 5 + packages/notifications/src/locales/fi.json | 5 + packages/notifications/src/locales/fil.json | 5 + packages/notifications/src/locales/fr.json | 5 + packages/notifications/src/locales/ga.json | 5 + packages/notifications/src/locales/gl.json | 5 + packages/notifications/src/locales/gu.json | 5 + packages/notifications/src/locales/ha.json | 5 + packages/notifications/src/locales/he.json | 5 + packages/notifications/src/locales/hi.json | 5 + packages/notifications/src/locales/hr.json | 5 + packages/notifications/src/locales/ht.json | 5 + packages/notifications/src/locales/hu.json | 5 + packages/notifications/src/locales/hy.json | 5 + packages/notifications/src/locales/id.json | 5 + packages/notifications/src/locales/ig.json | 5 + packages/notifications/src/locales/is.json | 5 + packages/notifications/src/locales/it.json | 5 + packages/notifications/src/locales/ja.json | 5 + packages/notifications/src/locales/ka.json | 5 + packages/notifications/src/locales/kk.json | 5 + packages/notifications/src/locales/km.json | 5 + packages/notifications/src/locales/kn.json | 5 + packages/notifications/src/locales/ko.json | 5 + packages/notifications/src/locales/lo.json | 5 + packages/notifications/src/locales/lt.json | 5 + packages/notifications/src/locales/lv.json | 5 + packages/notifications/src/locales/mg.json | 5 + packages/notifications/src/locales/mi.json | 5 + packages/notifications/src/locales/mk.json | 5 + packages/notifications/src/locales/ml.json | 5 + packages/notifications/src/locales/mn.json | 5 + packages/notifications/src/locales/mr.json | 5 + packages/notifications/src/locales/ms.json | 5 + packages/notifications/src/locales/mt.json | 5 + packages/notifications/src/locales/my.json | 5 + packages/notifications/src/locales/nb.json | 5 + packages/notifications/src/locales/ne.json | 5 + packages/notifications/src/locales/nl.json | 5 + packages/notifications/src/locales/om.json | 5 + packages/notifications/src/locales/or.json | 5 + packages/notifications/src/locales/pa.json | 5 + packages/notifications/src/locales/pl.json | 5 + packages/notifications/src/locales/ps.json | 5 + packages/notifications/src/locales/pt-BR.json | 5 + packages/notifications/src/locales/pt.json | 5 + packages/notifications/src/locales/ro.json | 5 + packages/notifications/src/locales/ru.json | 5 + packages/notifications/src/locales/si.json | 5 + packages/notifications/src/locales/sk.json | 5 + packages/notifications/src/locales/sl.json | 5 + packages/notifications/src/locales/so.json | 5 + packages/notifications/src/locales/sq.json | 5 + packages/notifications/src/locales/sr.json | 5 + packages/notifications/src/locales/sv.json | 5 + packages/notifications/src/locales/sw.json | 5 + packages/notifications/src/locales/ta.json | 5 + packages/notifications/src/locales/te.json | 5 + packages/notifications/src/locales/th.json | 5 + packages/notifications/src/locales/tr.json | 5 + packages/notifications/src/locales/uk.json | 5 + packages/notifications/src/locales/ur.json | 5 + packages/notifications/src/locales/uz.json | 5 + packages/notifications/src/locales/vi.json | 5 + packages/notifications/src/locales/yo.json | 5 + .../notifications/src/locales/zh-Hans.json | 5 + .../notifications/src/locales/zh-Hant.json | 5 + packages/notifications/src/locales/zu.json | 5 + packages/notifications/src/push-data.test.ts | 20 + packages/notifications/src/push-data.ts | 8 + .../src/push-presentation.test.ts | 14 + .../notifications/src/push-presentation.ts | 11 + .../notifications/src/rpc-schemas.test.ts | 54 + packages/notifications/src/rpc-schemas.ts | 24 + packages/trpc/src/mobile.ts | 2 + pnpm-lock.yaml | 6 + services/kilo-mcp/catalog.json | 136 +- .../send-push-for-conversation.test.ts | 1 + services/notifications/src/index.ts | 6 +- .../agent-session-notification-push.test.ts | 1 + .../src/lib/cloud-agent-session-push.ts | 2 + .../src/lib/internal-dispatch-push.test.ts | 200 + .../src/lib/internal-dispatch-push.ts | 50 +- .../notifications-service-cloud-agent.test.ts | 1 + 252 files changed, 55309 insertions(+), 283 deletions(-) create mode 100644 apps/mobile/src/app/(app)/(tabs)/(3_profile)/spend-alerts.tsx create mode 100644 apps/mobile/src/components/organization/spend-alert-validators.test.ts create mode 100644 apps/mobile/src/components/organization/spend-alert-validators.ts create mode 100644 apps/mobile/src/components/organization/spend-alerts-screen.mounted.test.tsx create mode 100644 apps/mobile/src/components/organization/spend-alerts-screen.tsx create mode 100644 apps/mobile/src/lib/notification-path-schema.test.ts create mode 100644 apps/web/src/app/api/cron/dispatch-spend-alerts/route.test.ts create mode 100644 apps/web/src/app/api/cron/dispatch-spend-alerts/route.ts create mode 100644 apps/web/src/components/spend-alerts/SpendAlertsPanel.tsx create mode 100644 apps/web/src/components/spend-alerts/spendAlertsPanelState.test.ts create mode 100644 apps/web/src/components/spend-alerts/spendAlertsPanelState.ts create mode 100644 apps/web/src/components/usage-analytics/MetricCard.test.ts create mode 100644 apps/web/src/emails/spendAlert.html create mode 100644 apps/web/src/lib/notifications-worker-client.test.ts create mode 100644 apps/web/src/lib/spend-alerts/delivery.test.ts create mode 100644 apps/web/src/lib/spend-alerts/delivery.ts create mode 100644 apps/web/src/lib/spend-alerts/settings.test.ts create mode 100644 apps/web/src/lib/spend-alerts/settings.ts create mode 100644 apps/web/src/lib/spend-alerts/sweep.test.ts create mode 100644 apps/web/src/lib/spend-alerts/sweep.ts create mode 100644 apps/web/src/routers/admin/email-testing-router.test.ts create mode 100644 apps/web/src/routers/spend-alert-router.schema.test.ts create mode 100644 apps/web/src/routers/spend-alert-router.test.ts create mode 100644 apps/web/src/routers/spend-alert-router.ts create mode 100644 packages/db/jest.config.mjs create mode 100644 packages/db/src/migration-journal.test.ts create mode 100644 packages/db/src/migrations/0254_charming_iron_patriot.sql create mode 100644 packages/db/src/migrations/meta/0254_snapshot.json create mode 100644 packages/db/src/spend-alerts-schema.test.ts diff --git a/apps/mobile/src/app/(app)/(tabs)/(3_profile)/spend-alerts.tsx b/apps/mobile/src/app/(app)/(tabs)/(3_profile)/spend-alerts.tsx new file mode 100644 index 0000000000..487edcd1e6 --- /dev/null +++ b/apps/mobile/src/app/(app)/(tabs)/(3_profile)/spend-alerts.tsx @@ -0,0 +1,8 @@ +import { useLocalSearchParams } from 'expo-router'; + +import { SpendAlertsScreen } from '@/components/organization/spend-alerts-screen'; + +export default function SpendAlertsRoute() { + const { org } = useLocalSearchParams<{ org?: string }>(); + return ; +} diff --git a/apps/mobile/src/components/notifications-screen.mounted.test.tsx b/apps/mobile/src/components/notifications-screen.mounted.test.tsx index 2d196c2f8e..d15f594893 100644 --- a/apps/mobile/src/components/notifications-screen.mounted.test.tsx +++ b/apps/mobile/src/components/notifications-screen.mounted.test.tsx @@ -149,6 +149,7 @@ function fullCapabilities(overrides: Record = {}): Record = {}): Record { + beforeEach(() => { + vi.clearAllMocks(); + getNotificationPermissionStatus.mockResolvedValue('granted'); + getDevicePushToken.mockResolvedValue('device-token'); + pushTokensQueryFn.mockResolvedValue([{ token: 'device-token', platform: 'android' }]); + setPreferenceMutationFn.mockResolvedValue({}); + registerTokenMutationFn.mockResolvedValue({ success: true }); + }); + + it('happy: renders the row with its capability and flipping it sends exactly spendAlerts', async () => { + prefsQueryFn.mockResolvedValue(fullPrefs()); + const { renderer } = await renderScreen(); + await waitForEnabledSwitch(renderer, 'Spend alerts category'); + + // The row renders beside balance alerts, enabled because the server + // reported the spend capability available. + expect(textWithChildren(renderer.root, 'Spend alerts').length).toBe(1); + expect(switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.value).toBe(true); + expect(switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.disabled).toBe(false); + + // Exactly one category key per call: the payload is what the spend view's + // own column write must agree with. + prefsQueryFn.mockResolvedValue(fullPrefs({ spendAlerts: false })); + act(() => { + switchOnValueChange(renderer.root, 'Spend alerts category')?.(false); + }); + await waitFor(() => setPreferenceMutationFn.mock.calls.length === 1); + expect(setPreferenceMutationFn.mock.calls[0]?.[0]).toEqual({ spendAlerts: false }); + await waitFor( + () => switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.value === false + ); + expect(toastError).not.toHaveBeenCalled(); + }); + + it('non-retryable unhappy: an unavailable spend capability disables the row and shows the server reason', async () => { + prefsQueryFn.mockResolvedValue( + fullPrefs({ + capabilities: fullCapabilities({ + spendAlerts: { + available: false, + unavailableReason: 'Spend alerts are unavailable.', + }, + }), + }) + ); + const { renderer } = await renderScreen(); + + // Waiting on an available sibling proves the master gate settled, so the + // spend row's disabled state below is the server capability, not the gate. + await waitForEnabledSwitch(renderer, 'Chat messages'); + + expect(switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.disabled).toBe(true); + expect(textWithChildren(renderer.root, 'Spend alerts are unavailable.').length).toBe(1); + }); + + it('retryable unhappy: a spend toggle failure rolls back the optimistic flip', async () => { + prefsQueryFn.mockResolvedValue(fullPrefs()); + setPreferenceMutationFn.mockRejectedValue({ + data: { code: 'INTERNAL_SERVER_ERROR' }, + message: 'boom', + }); + const { renderer } = await renderScreen(); + await waitForEnabledSwitch(renderer, 'Spend alerts category'); + + act(() => { + switchOnValueChange(renderer.root, 'Spend alerts category')?.(false); + }); + await waitFor(() => setPreferenceMutationFn.mock.calls.length === 1); + await waitFor(() => activityIndicators(renderer.root).length === 0); + + // The switch returns to the unchanged server value and the error surfaces. + expect(switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.value).toBe(true); + expect(toastError).toHaveBeenCalledWith('boom'); + }); +}); + describe('NotificationsScreen category availability', () => { beforeEach(() => { vi.clearAllMocks(); @@ -512,6 +591,7 @@ describe('NotificationsScreen category availability', () => { sessionStatus: true, kiloclawActivity: true, balanceAlerts: true, + spendAlerts: true, securityFindings: true, agentPushEnabled: true, notificationPreviews: 'generic', @@ -521,6 +601,7 @@ describe('NotificationsScreen category availability', () => { expect(switchesByLabel(renderer.root, 'Chat messages')[0]?.props.disabled).toBe(false); expect(switchesByLabel(renderer.root, 'Balance alerts')[0]?.props.disabled).toBe(false); + expect(switchesByLabel(renderer.root, 'Spend alerts category')[0]?.props.disabled).toBe(false); expect(switchesByLabel(renderer.root, 'KiloClaw activity')[0]?.props.disabled).toBe(false); }); diff --git a/apps/mobile/src/components/notifications-screen.tsx b/apps/mobile/src/components/notifications-screen.tsx index ed934d1fa8..88801e68a0 100644 --- a/apps/mobile/src/components/notifications-screen.tsx +++ b/apps/mobile/src/components/notifications-screen.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-lines -- The dedicated Notifications screen composes the master - * OS-permission gate, the push-token registration flow, and 7 per-category toggles + * OS-permission gate, the push-token registration flow, and 8 per-category toggles * with their optimistic-mutation + retry + loading patterns. CATEGORY_META still - * has seven keys; the KiloClaw row is hidden when useKiloClawTabVisible is false. + * has eight keys; the KiloClaw row is hidden when useKiloClawTabVisible is false. * Extracting subcomponents would re-encode the same hooks. The screen stays a * single rendered surface. */ import { hashKey, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -147,6 +147,16 @@ const CATEGORY_META = [ subtitleKey: 'notifications.category.balanceAlertsSubtitle', icon: Wallet, }, + { + key: 'spendAlerts', + titleKey: 'notifications.channel.spend', + subtitleKey: 'notifications.category.spendAlertsSubtitle', + icon: Wallet, + // A dedicated label for the device scene: the spend view owns a switch with + // the same visible wording, so this row's control must be addressable on + // its own. The low-balance sheet uses the same `*A11y` pattern. + accessibilityLabelKey: 'notifications.category.spendAlertsToggle', + }, { key: 'securityFindings', titleKey: 'notifications.channel.security', @@ -200,6 +210,10 @@ function CategoryRow({ const unavailable = capability?.available === false; const isDisabled = disabled || !editable || unavailable; const title = t(meta.titleKey); + // Most rows take the visible title as their control label; the spend row + // carries its own so the device scene can address it unambiguously. + const accessibilityLabel = + 'accessibilityLabelKey' in meta ? t(meta.accessibilityLabelKey) : t(meta.titleKey); const subtitle = unavailable ? (capability.unavailableReason ?? t(meta.subtitleKey)) : t(meta.subtitleKey); @@ -221,7 +235,7 @@ function CategoryRow({ { if (isDisabled) { @@ -245,8 +259,8 @@ export function NotificationsScreen() { const [isTogglingPermission, setIsTogglingPermission] = useState(false); const [isRegisteringToken, setIsRegisteringToken] = useState(false); - // The `setNotificationPreferences` mutation object is shared across all five - // rows, and its `isPending` is a single flag for the whole procedure. Two + // The `setNotificationPreferences` mutation object is shared across every + // category row, and its `isPending` is a single flag for the whole procedure. Two // category flips can therefore be in flight at once, so we track the set of // in-flight categories explicitly and scope each row's busy state to its own // key. Each mutation callback resolves its own category from `variables` @@ -584,7 +598,7 @@ export function NotificationsScreen() { showsVerticalScrollIndicator={false} > {/* The glanceable surface. First on the screen because it is what the - user sees without opening the app, and it must not sit below seven + user sees without opening the app, and it must not sit below eight category rows. Each platform names it the way its own OS does: a Live Activity on iOS, a Live Update on Android. */} diff --git a/apps/mobile/src/components/organization/hub-screen.tsx b/apps/mobile/src/components/organization/hub-screen.tsx index a9d8ab19b6..e5cb5ba96a 100644 --- a/apps/mobile/src/components/organization/hub-screen.tsx +++ b/apps/mobile/src/components/organization/hub-screen.tsx @@ -1,7 +1,7 @@ import { fromMicrodollars } from '@kilocode/app-shared/utils'; import * as Haptics from 'expo-haptics'; import { type Href, useRouter } from 'expo-router'; -import { Bell, FileText, Pencil, Receipt, Users } from '@/components/ui/icons'; +import { Bell, FileText, Pencil, Receipt, Users, Wallet } from '@/components/ui/icons'; import { DirectionalChevronRight } from '@/components/ui/directional-icons'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -201,11 +201,20 @@ export function OrganizationHubScreen({ organizationIdOverride }: OrganizationHu icon={Bell} title={t('organization.lowBalanceAlert.title')} subtitle={lowBalanceSubtitle} - last onPress={() => { router.push('/(app)/(tabs)/(3_profile)/organization/low-balance-alert' as Href); }} /> + { + router.push( + `/(app)/(tabs)/(3_profile)/spend-alerts?org=${organizationId}` as Href + ); + }} + /> )} diff --git a/apps/mobile/src/components/organization/spend-alert-validators.test.ts b/apps/mobile/src/components/organization/spend-alert-validators.test.ts new file mode 100644 index 0000000000..80dc290d2d --- /dev/null +++ b/apps/mobile/src/components/organization/spend-alert-validators.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; + +import { + DEFAULT_MULTIPLIER, + DEFAULT_WINDOW_HOURS, + DISABLED_MULTIPLIER, + DISABLED_THRESHOLD_USD, + multiplierBasisPoints, + multiplierError, + parseMultiplier, + parseThreshold, + thresholdError, + toWindowHours, +} from '@/components/organization/spend-alert-validators'; + +// `pnpm check:i18n` owns the copy; these assert the bounds, not the wording. +const THRESHOLD_CASES: [string, number | null][] = [ + ['', null], + [' ', null], + ['0', null], + ['-5', null], + ['not a number', null], + ['25', 25], + ['25.50', 25.5], + ['1,000', 1000], + ['1000000', 1_000_000], + ['1000000.01', null], + // Below one microdollar the wire's `round(usd * 1_000_000)` stores a zero + // threshold, which fires on any spend and never clears. + ['0.000001', 0.000_001], + ['0.0000001', null], +]; + +const MULTIPLIER_CASES: [string, number | null][] = [ + ['', null], + ['0.9', null], + ['0', null], + ['-2', null], + ['not a number', null], + ['1', 1], + ['2.5', 2.5], + ['50', 50], + ['50.01', null], +]; + +describe('parseThreshold', () => { + it.each(THRESHOLD_CASES)('parses %s to %s', (value, expected) => { + expect(parseThreshold(value)).toBe(expected); + }); + + it('reports an inline error only while the limit is out of range', () => { + expect(thresholdError('25')).toBeNull(); + expect(thresholdError('0')).not.toBeNull(); + expect(thresholdError('1000000.01')).not.toBeNull(); + }); +}); + +describe('parseMultiplier', () => { + it.each(MULTIPLIER_CASES)('parses %s to %s', (value, expected) => { + expect(parseMultiplier(value)).toBe(expected); + }); + + it('reports an inline error only while the multiplier is below 1x or above the cap', () => { + expect(multiplierError('2')).toBeNull(); + expect(multiplierError('0.5')).not.toBeNull(); + expect(multiplierError('51')).not.toBeNull(); + }); +}); + +describe('disabled-kind stand-ins', () => { + it('are the smallest values the wire accepts', () => { + // A switched-off kind submits these instead of gating Save on a field the + // owner deliberately left empty. + expect(parseThreshold(String(DISABLED_THRESHOLD_USD))).toBe(DISABLED_THRESHOLD_USD); + expect(parseMultiplier(String(DISABLED_MULTIPLIER))).toBe(DISABLED_MULTIPLIER); + expect(multiplierBasisPoints(DISABLED_MULTIPLIER)).toBe(100); + }); +}); + +describe('multiplierBasisPoints', () => { + it('converts a multiplier to the wire basis points', () => { + expect(multiplierBasisPoints(1)).toBe(100); + expect(multiplierBasisPoints(DEFAULT_MULTIPLIER)).toBe(200); + expect(multiplierBasisPoints(1.5)).toBe(150); + }); +}); + +describe('toWindowHours', () => { + it('defaults an unsaved or unknown window to the 24-hour default', () => { + expect(DEFAULT_WINDOW_HOURS).toBe(24); + expect(toWindowHours(null)).toBe(24); + expect(toWindowHours(undefined)).toBe(24); + expect(toWindowHours(999)).toBe(24); + }); + + it('keeps the windows the wire accepts', () => { + expect(toWindowHours(24)).toBe(24); + expect(toWindowHours(168)).toBe(168); + expect(toWindowHours(720)).toBe(720); + }); +}); diff --git a/apps/mobile/src/components/organization/spend-alert-validators.ts b/apps/mobile/src/components/organization/spend-alert-validators.ts new file mode 100644 index 0000000000..5550e775d4 --- /dev/null +++ b/apps/mobile/src/components/organization/spend-alert-validators.ts @@ -0,0 +1,91 @@ +import { i18n } from '@/i18n'; +import { parseLocalizedNumber } from '@/lib/format'; + +/** + * Bounds the save payload's schema enforces, mirrored here so the form shows an + * inline error instead of a rejected save: one million dollars and 50x (the + * wire's basis-point cap of 5000) are the schema's own limits. + */ +const MAX_THRESHOLD_USD = 1_000_000; +const MAX_MULTIPLIER = 50; + +/** + * Smallest limit that survives the wire's conversion to microdollars. The + * router stores `round(threshold * 1_000_000)`, so a smaller positive value + * would round to a zero-microdollar threshold: the rule then fires on any + * spend and its 95% hysteresis band is zero, so it never clears. + */ +const MIN_THRESHOLD_USD = 0.000_001; + +/** The rolling windows the wire accepts, in hours. */ +export type SpendAlertWindowHours = 24 | 168 | 720; + +/** A scope that has never chosen a window alerts on the trailing 24 hours. */ +export const DEFAULT_WINDOW_HOURS: SpendAlertWindowHours = 24; + +/** A scope that has never chosen a multiplier starts at double its usual rate. */ +export const DEFAULT_MULTIPLIER = 2; + +/** + * The value the save payload carries for an alert kind the owner has switched + * off. The wire requires a limit and a multiplier whichever kinds are enabled, + * so a switched-off kind submits the smallest value its schema accepts rather + * than making the owner fill in a field for an alert that will not fire. + */ +export const DISABLED_THRESHOLD_USD = 1; +export const DISABLED_MULTIPLIER = 1; + +/** + * Parse a USD limit: an empty, unparsable, zero, negative, sub-microdollar, or + * above-cap value is `null` (no limit), never a clamped number. + */ +export function parseThreshold(value: string): number | null { + const trimmed = value.trim(); + const parsed = parseLocalizedNumber(trimmed, i18n.language); + if ( + trimmed === '' || + parsed === null || + parsed < MIN_THRESHOLD_USD || + parsed > MAX_THRESHOLD_USD + ) { + return null; + } + return parsed; +} + +/** Parse the anomaly multiplier: at least 1x, at most the wire's 50x cap. */ +export function parseMultiplier(value: string): number | null { + const trimmed = value.trim(); + const parsed = parseLocalizedNumber(trimmed, i18n.language); + if (trimmed === '' || parsed === null || parsed < 1 || parsed > MAX_MULTIPLIER) { + return null; + } + return parsed; +} + +/** The wire stores the multiplier in basis points, where 100 is 1x. */ +export function multiplierBasisPoints(multiplier: number): number { + return Math.round(multiplier * 100); +} + +/** A window read back from the wire, narrowed to the three the wire accepts. */ +export function toWindowHours(value: number | null | undefined): SpendAlertWindowHours { + if (value === 168 || value === 720) { + return value; + } + return DEFAULT_WINDOW_HOURS; +} + +/** Inline error for the limit field, or `null` while the value is valid. */ +export function thresholdError(value: string): string | null { + // Reuses the billing alert's copy: the same sentence, so the two fields can + // never drift apart in a translation. + return parseThreshold(value) == null + ? i18n.t('organization.lowBalanceAlert.thresholdError') + : null; +} + +/** Inline error for the multiplier field, or `null` while the value is valid. */ +export function multiplierError(value: string): string | null { + return parseMultiplier(value) == null ? i18n.t('spendAlerts.multiplierError') : null; +} diff --git a/apps/mobile/src/components/organization/spend-alerts-screen.mounted.test.tsx b/apps/mobile/src/components/organization/spend-alerts-screen.mounted.test.tsx new file mode 100644 index 0000000000..28958d612a --- /dev/null +++ b/apps/mobile/src/components/organization/spend-alerts-screen.mounted.test.tsx @@ -0,0 +1,664 @@ +/* eslint-disable max-lines -- one mounted suite for the spend view's state contract: four load states, the push agreement, and the validators' wiring share one mock harness. */ + +// The spend view's state contract, driven through the screen's own JSX with the +// tRPC layer mocked: the persisted draft, the saved confirmation, the load and +// save errors (retryable, draft kept), permission denial, the off state, and +// the push channel's agreement with the Notifications screen. + +import { createElement } from 'react'; +import { act, type ReactTestInstance, type ReactTestRenderer } from '@/test/renderer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { i18n } from '@/i18n'; +import { SpendAlertsScreen } from '@/components/organization/spend-alerts-screen'; +import { thresholdError } from '@/components/organization/spend-alert-validators'; +import { formatList } from '@/lib/format'; +import { useRouteForegroundRefresh } from '@/lib/hooks/use-route-foreground-refresh'; +import { renderWithProviders, waitFor } from '@/test/render-with-providers'; + +const getQueryFn = vi.hoisted(() => vi.fn()); +const saveMutationFn = vi.hoisted(() => vi.fn()); +const announced = vi.hoisted(() => ({ success: vi.fn() })); +const router = vi.hoisted(() => ({ back: vi.fn(), push: vi.fn() })); +const boundary = vi.hoisted(() => ({ + organizationId: undefined as string | undefined, + role: 'owner', + org: null as { organizationId: string } | null, + isResolving: false, +})); + +vi.mock('@/lib/trpc', () => ({ + useTRPC: () => ({ + spendAlerts: { + get: { + queryOptions: (input: Record, options: Record = {}) => ({ + queryKey: ['spendAlerts', 'get', input], + queryFn: getQueryFn, + ...options, + }), + pathFilter: () => ({ queryKey: [['spendAlerts', 'get']] }), + }, + save: { + mutationOptions: (options: Record = {}) => ({ + ...options, + mutationFn: saveMutationFn, + mutationKey: ['spendAlerts', 'save'], + }), + }, + }, + }), +})); +vi.mock('@/lib/a11y/announcing-toast', () => ({ announcingToast: announced })); +vi.mock('expo-router', () => ({ useRouter: () => router })); +vi.mock('expo-haptics', () => ({ + selectionAsync: vi.fn(), + notificationAsync: vi.fn(), + NotificationFeedbackType: { Success: 'success' }, +})); +vi.mock('react-native', () => ({ + Pressable: 'Pressable', + ScrollView: 'ScrollView', + Switch: 'Switch', + TextInput: 'TextInput', + View: 'View', +})); +vi.mock('@/components/organization/organization-boundary', () => ({ + OrganizationBoundary: 'OrganizationBoundary', +})); +vi.mock('@/components/organization/permission-denied', () => ({ + PermissionDenied: 'PermissionDenied', +})); +vi.mock('@/components/query-error', () => ({ QueryError: 'QueryError' })); +vi.mock('@/components/screen-header', () => ({ ScreenHeader: 'ScreenHeader' })); +vi.mock('@/components/tab-screen', () => ({ TabScreenScrollView: 'TabScreenScrollView' })); +vi.mock('@/components/ui/accessible-status', () => ({ AccessibleStatus: 'AccessibleStatus' })); +vi.mock('@/components/ui/button', () => ({ Button: 'Button' })); +vi.mock('@/components/ui/directional-icons', () => ({ + DirectionalChevronRight: 'DirectionalChevronRight', +})); +vi.mock('@/components/ui/form-field', () => ({ FormField: 'FormField' })); +vi.mock('@/components/ui/kv-row', () => ({ KvRow: 'KvRow' })); +vi.mock('@/components/ui/segmented-control', () => ({ SegmentedControl: 'SegmentedControl' })); +vi.mock('@/components/ui/skeleton', () => ({ Skeleton: 'Skeleton' })); +vi.mock('@/components/ui/text', () => ({ Text: 'Text' })); +vi.mock('@/lib/hooks/use-organization-queries', () => ({ useOrgBoundary: () => boundary })); +vi.mock('@/lib/hooks/use-route-foreground-refresh', () => ({ + useRouteForegroundRefresh: vi.fn(), +})); +vi.mock('@/lib/hooks/use-theme-colors', () => ({ + useThemeColors: () => ({ mutedForeground: '#666666' }), +})); +// Everything the validators need from the formatter, in the en-US shape the +// tests write values in. +vi.mock('@/lib/format', () => ({ + formatMoney: (amount: number) => `$${amount}`, + formatNumber: String, + formatList: (values: readonly string[]) => values.join(' and '), + parseLocalizedNumber: (value: string) => { + const normalized = value.trim().replaceAll(',', ''); + if (normalized === '') { + return null; + } + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : null; + }, +})); + +type RuleFixture = { + kind: 'threshold' | 'anomaly'; + enabled: boolean; + threshold: number | null; + windowHours: number | null; + multiplierBasisPoints: number | null; + emailEnabled: boolean; + pushEnabled: boolean; + firing: boolean; +}; + +function thresholdRule(overrides: Partial = {}): RuleFixture { + return { + kind: 'threshold', + enabled: true, + threshold: 25, + windowHours: 24, + multiplierBasisPoints: null, + emailEnabled: true, + pushEnabled: false, + firing: false, + ...overrides, + }; +} + +function anomalyRule(overrides: Partial = {}): RuleFixture { + return { + kind: 'anomaly', + enabled: true, + threshold: null, + windowHours: null, + multiplierBasisPoints: 200, + emailEnabled: true, + pushEnabled: false, + firing: false, + ...overrides, + }; +} + +function settingsFixture(overrides: Record = {}): Record { + return { + scope: 'personal', + scopeName: 'Ada', + canManage: true, + pushCategoryEnabled: true, + pushChannelBlocked: false, + enabled: true, + rules: [thresholdRule(), anomalyRule()], + spend: { + spend24hMicrodollars: 1_500_000, + spend7dMicrodollars: 2_000_000, + baselineHourlyMicrodollars: null, + }, + ...overrides, + }; +} + +async function mountScreen(organizationId?: string) { + const view = await renderWithProviders( + createElement(SpendAlertsScreen, organizationId == null ? {} : { organizationId }) + ); + return view; +} + +function collectText(node: unknown): string[] { + if (node == null || typeof node === 'boolean') { + return []; + } + if (typeof node === 'string' || typeof node === 'number') { + return [String(node)]; + } + if (Array.isArray(node)) { + return node.flatMap(item => collectText(item)); + } + if (typeof node === 'object' && 'children' in node) { + return collectText((node as { children?: unknown }).children); + } + return []; +} + +function texts(renderer: ReactTestRenderer): string[] { + return collectText(renderer.toJSON()); +} + +function byType(renderer: ReactTestRenderer, nodeType: string): ReactTestInstance[] { + return renderer.root.findAll(node => node.type === nodeType); +} + +function switchByLabel(renderer: ReactTestRenderer, label: string): ReactTestInstance | undefined { + return byType(renderer, 'Switch').find(node => node.props.accessibilityLabel === label); +} + +/** + * The Email or Push switches of both cards, in render order. Their label is the + * channel composed with the card's alert kind, so the lookup matches the + * channel substring and keeps the cards distinguishable. + */ +function channelSwitches(renderer: ReactTestRenderer, channelKey: string): ReactTestInstance[] { + const channelLabel = i18n.t(channelKey); + return byType(renderer, 'Switch').filter(node => + String(node.props.accessibilityLabel).includes(channelLabel) + ); +} + +function fieldByLabel(renderer: ReactTestRenderer, label: string): ReactTestInstance { + const field = byType(renderer, 'FormField').find(node => node.props.label === label); + if (!field) { + throw new Error(`FormField for ${label} not found`); + } + return field; +} + +function buttonByVariant(renderer: ReactTestRenderer, variant?: string): ReactTestInstance { + const button = byType(renderer, 'Button').find(node => node.props.variant === variant); + if (!button) { + throw new Error(`Button for ${String(variant)} not found`); + } + return button; +} + +function press(instance: ReactTestInstance | undefined): void { + if (!instance) { + throw new Error('pressable not found'); + } + act(() => { + (instance.props as { onPress: () => void }).onPress(); + }); +} + +function retry(instance: ReactTestInstance | undefined): void { + if (!instance) { + throw new Error('retry control not found'); + } + act(() => { + (instance.props as { onRetry: () => void }).onRetry(); + }); +} + +function toggle(instance: ReactTestInstance | undefined, value: boolean): void { + if (!instance) { + throw new Error('switch not found'); + } + act(() => { + (instance.props as { onValueChange: (value: boolean) => void }).onValueChange(value); + }); +} + +function type(renderer: ReactTestRenderer, label: string, value: string): void { + const field = fieldByLabel(renderer, label); + act(() => { + (field.props as { onChangeText: (value: string) => void }).onChangeText(value); + }); +} + +const LIMIT = i18n.t('spendAlerts.limitLabel'); +const MULTIPLIER = i18n.t('spendAlerts.multiplierLabel'); + +async function mountLoaded(overrides: Record = {}) { + getQueryFn.mockResolvedValue(settingsFixture(overrides)); + const view = await mountScreen(); + await waitFor(() => texts(view.renderer).includes(i18n.t('spendAlerts.subtitle'))); + return view; +} + +beforeEach(() => { + vi.clearAllMocks(); + boundary.organizationId = undefined; + boundary.org = null; + boundary.isResolving = false; + saveMutationFn.mockResolvedValue(settingsFixture()); +}); + +describe('SpendAlertsScreen happy state', () => { + it('renders the persisted values, the scope spend, and saves the draft', async () => { + const { renderer } = await mountLoaded(); + + expect(fieldByLabel(renderer, LIMIT).props.defaultValue).toBe('25'); + expect(fieldByLabel(renderer, MULTIPLIER).props.defaultValue).toBe('2'); + expect(byType(renderer, 'KvRow')[0]?.props.value).toBe('$1.5'); + expect(switchByLabel(renderer, i18n.t('spendAlerts.enable'))?.props.value).toBe(true); + expect(switchByLabel(renderer, i18n.t('spendAlerts.thresholdTitle'))?.props.value).toBe(true); + expect(byType(renderer, 'SegmentedControl')[0]?.props.value).toBe('24'); + + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + expect(saveMutationFn.mock.calls[0]?.[0]).toEqual({ + enabled: true, + rules: [ + { + kind: 'threshold', + enabled: true, + threshold: 25, + windowHours: 24, + emailEnabled: true, + pushEnabled: false, + }, + { + kind: 'anomaly', + enabled: true, + multiplierBasisPoints: 200, + emailEnabled: true, + pushEnabled: false, + }, + ], + }); + // The confirmation is a real Text node on the form, and the screen stays + // put so it describes the values on screen: a sonner-native toast alone is + // drawn outside the accessibility hierarchy, so assistive tech (and the + // on-device digest) never sees it. + await waitFor( + () => byType(renderer, 'AccessibleStatus')[0]?.props.message === i18n.t('spendAlerts.saved') + ); + expect(byType(renderer, 'AccessibleStatus')[0]?.props.tone).toBe('status'); + // The inline status is the single announcement owner: the mutation must + // not also announce through a toast, or one save speaks (and draws) the + // confirmation twice. + expect(announced.success).not.toHaveBeenCalled(); + expect(router.back).not.toHaveBeenCalled(); + + // A draft edit clears it: the message never describes superseded values. + type(renderer, LIMIT, '30'); + expect(byType(renderer, 'AccessibleStatus')[0]?.props.message).toBeNull(); + }); + + it('labels each channel switch with its alert kind so the two cards stay distinct', async () => { + const { renderer } = await mountLoaded(); + + const labels = byType(renderer, 'Switch').map(node => String(node.props.accessibilityLabel)); + expect(labels).toContain( + formatList([i18n.t('spendAlerts.thresholdTitle'), i18n.t('common.email')], i18n.language) + ); + expect(labels).toContain( + formatList( + [i18n.t('spendAlerts.thresholdTitle'), i18n.t('notifications.push')], + i18n.language + ) + ); + expect(labels).toContain( + formatList([i18n.t('spendAlerts.anomalyTitle'), i18n.t('common.email')], i18n.language) + ); + expect(labels).toContain( + formatList([i18n.t('spendAlerts.anomalyTitle'), i18n.t('notifications.push')], i18n.language) + ); + // The OS hierarchy does not group a switch with its card, so no two controls + // may share a label. + expect(new Set(labels).size).toBe(labels.length); + }); + + it('shows no confirmation before the first save', async () => { + const { renderer } = await mountLoaded(); + + expect(byType(renderer, 'AccessibleStatus')[0]?.props.message).toBeNull(); + }); + + it('shows the spend summary empty copy when the scope has no spend', async () => { + const { renderer } = await mountLoaded({ + spend: { spend24hMicrodollars: 0, spend7dMicrodollars: 0, baselineHourlyMicrodollars: null }, + }); + + expect(byType(renderer, 'KvRow')[0]?.props.value).toBe(i18n.t('spendAlerts.noSpend')); + }); +}); + +describe('SpendAlertsScreen foreground refresh', () => { + it('refreshes the spend-alerts query with the tRPC-nested key prefix', async () => { + await mountLoaded(); + + // The stored key is `[['spendAlerts','get'], …]`; the flat `['spendAlerts']` + // form compares a string against the first element's array and never + // matches, so the refresh would be a silent no-op. + expect(vi.mocked(useRouteForegroundRefresh)).toHaveBeenCalledWith([[['spendAlerts']]]); + }); +}); + +describe('SpendAlertsScreen retryable states', () => { + it('renders the load error with a retry and fills the form in place after it clears', async () => { + getQueryFn.mockRejectedValue(new Error('offline')); + const { renderer } = await mountScreen(); + + await waitFor(() => byType(renderer, 'QueryError').length === 1); + const error = byType(renderer, 'QueryError')[0]; + expect(error?.props.message).toBe(i18n.t('spendAlerts.loadError')); + expect(typeof error?.props.onRetry).toBe('function'); + expect(byType(renderer, 'Skeleton')).toHaveLength(0); + + getQueryFn.mockResolvedValue(settingsFixture()); + retry(error); + + await waitFor(() => byType(renderer, 'FormField').length === 2); + expect(fieldByLabel(renderer, LIMIT).props.defaultValue).toBe('25'); + }); + + it('keeps the draft and retries after a failed save', async () => { + const { renderer } = await mountLoaded(); + saveMutationFn.mockRejectedValue(new Error('boom')); + + type(renderer, LIMIT, '30'); + press(buttonByVariant(renderer)); + + await waitFor(() => byType(renderer, 'AccessibleStatus')[0]?.props.message != null); + expect(byType(renderer, 'AccessibleStatus')[0]?.props.message).toBe( + i18n.t('spendAlerts.saveError') + ); + + const retryButton = buttonByVariant(renderer, 'outline'); + expect(retryButton.props.accessibilityLabel).toBe('Retry'); + + saveMutationFn.mockResolvedValue(settingsFixture()); + press(retryButton); + + await waitFor(() => saveMutationFn.mock.calls.length === 2); + const retried = saveMutationFn.mock.calls[1]?.[0] as { rules: { threshold: number }[] }; + expect(retried.rules[0]?.threshold).toBe(30); + }); + + it('disables Retry when an edit leaves the draft unsubmittable', async () => { + const { renderer } = await mountLoaded(); + saveMutationFn.mockRejectedValue(new Error('boom')); + + type(renderer, LIMIT, '30'); + press(buttonByVariant(renderer)); + + await waitFor(() => byType(renderer, 'AccessibleStatus')[0]?.props.message != null); + expect(buttonByVariant(renderer, 'outline').props.disabled).toBe(false); + + // The edit revalidates but keeps the failure showing. Retry posts whatever + // is on screen, so it must not stay tappable for a draft `onSave` would + // refuse to send: an enabled-looking control that does nothing. + type(renderer, LIMIT, '0'); + expect(buttonByVariant(renderer, 'outline').props.disabled).toBe(true); + + type(renderer, LIMIT, '40'); + expect(buttonByVariant(renderer, 'outline').props.disabled).toBe(false); + }); +}); + +describe('SpendAlertsScreen non-retryable states', () => { + it('renders permission denial for an organization scope the viewer cannot manage', async () => { + boundary.org = { organizationId: 'org-1' }; + getQueryFn.mockResolvedValue( + settingsFixture({ scope: 'organization', canManage: false, enabled: undefined }) + ); + + const { renderer } = await mountScreen('org-1'); + + await waitFor(() => byType(renderer, 'PermissionDenied').length === 1); + expect(byType(renderer, 'Button')).toHaveLength(0); + expect(byType(renderer, 'Switch')).toHaveLength(0); + expect(byType(renderer, 'FormField')).toHaveLength(0); + }); + + it('disables Save and wires the inline error for an out-of-range limit', async () => { + const { renderer } = await mountLoaded(); + + expect(fieldByLabel(renderer, LIMIT).props.validate).toBe(thresholdError); + expect((fieldByLabel(renderer, LIMIT).props.validate as (value: string) => string)('0')).toBe( + i18n.t('organization.lowBalanceAlert.thresholdError') + ); + + type(renderer, LIMIT, '0'); + + expect(buttonByVariant(renderer).props.disabled).toBe(true); + type(renderer, LIMIT, '40'); + expect(buttonByVariant(renderer).props.disabled).toBe(false); + }); +}); + +describe('SpendAlertsScreen with an alert kind switched off', () => { + it('lets Save pass with a blank limit once the threshold kind is off', async () => { + const { renderer } = await mountLoaded(); + + type(renderer, LIMIT, ''); + expect(buttonByVariant(renderer).props.disabled).toBe(true); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.thresholdTitle')), false); + + // A switched-off kind stops validating its field and stops gating Save. + expect(fieldByLabel(renderer, LIMIT).props.validate).toBeUndefined(); + expect(buttonByVariant(renderer).props.disabled).toBe(false); + + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + const input = saveMutationFn.mock.calls[0]?.[0] as { + rules: { kind: string; enabled: boolean; threshold: number }[]; + }; + const savedThreshold = input.rules.find(rule => rule.kind === 'threshold'); + expect(savedThreshold?.enabled).toBe(false); + // The wire still needs a positive limit for the kind: the stand-in fills in. + expect(savedThreshold?.threshold).toBeGreaterThan(0); + }); + + it('lets Save pass with a blank multiplier once the anomaly kind is off', async () => { + const { renderer } = await mountLoaded(); + + type(renderer, MULTIPLIER, ''); + expect(buttonByVariant(renderer).props.disabled).toBe(true); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.anomalyTitle')), false); + + expect(fieldByLabel(renderer, MULTIPLIER).props.validate).toBeUndefined(); + expect(buttonByVariant(renderer).props.disabled).toBe(false); + + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + const input = saveMutationFn.mock.calls[0]?.[0] as { + rules: { kind: string; enabled: boolean; multiplierBasisPoints: number }[]; + }; + const anomaly = input.rules.find(rule => rule.kind === 'anomaly'); + expect(anomaly?.enabled).toBe(false); + expect(anomaly?.multiplierBasisPoints).toBeGreaterThanOrEqual(100); + }); + + it('keeps a valid typed limit when the threshold kind is switched off', async () => { + const { renderer } = await mountLoaded(); + + type(renderer, LIMIT, '30'); + toggle(switchByLabel(renderer, i18n.t('spendAlerts.thresholdTitle')), false); + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + const input = saveMutationFn.mock.calls[0]?.[0] as { rules: { threshold: number }[] }; + expect(input.rules[0]?.threshold).toBe(30); + }); +}); + +describe('SpendAlertsScreen empty state', () => { + it('shows the off copy with the master switch as the only call to action', async () => { + const { renderer } = await mountLoaded({ enabled: false }); + + expect(texts(renderer)).toContain(i18n.t('spendAlerts.empty')); + expect(byType(renderer, 'Button')).toHaveLength(0); + expect(byType(renderer, 'FormField')).toHaveLength(0); + expect(byType(renderer, 'Switch')).toHaveLength(1); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.enable')), true); + + expect(texts(renderer)).not.toContain(i18n.t('spendAlerts.empty')); + expect(byType(renderer, 'FormField')).toHaveLength(2); + expect(byType(renderer, 'Button')).toHaveLength(1); + }); + + it('keeps Save with the switch when it is turned off, so the disable can be posted', async () => { + const { renderer } = await mountLoaded(); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.enable')), false); + + expect(texts(renderer)).toContain(i18n.t('spendAlerts.empty')); + expect(byType(renderer, 'FormField')).toHaveLength(0); + + const saveButton = buttonByVariant(renderer); + expect(saveButton.props.disabled).toBe(false); + press(saveButton); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + expect(saveMutationFn.mock.calls[0]?.[0]).toMatchObject({ enabled: false }); + }); + + it('keeps the saved confirmation after a disable save lands', async () => { + const { renderer, queryClient } = await mountLoaded(); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.enable')), false); + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + await waitFor( + () => byType(renderer, 'AccessibleStatus')[0]?.props.message === i18n.t('spendAlerts.saved') + ); + + // The save's own refetch lands with the stored value now false, so the + // draft no longer differs and the off branch is the only branch left. The + // confirmation must survive that: a disable-save is a supported flow, so it + // gets the same stable 'settings saved' line as the enable path. + act(() => { + queryClient.setQueryData(['spendAlerts', 'get', {}], settingsFixture({ enabled: false })); + }); + + expect(texts(renderer)).toContain(i18n.t('spendAlerts.empty')); + expect(byType(renderer, 'AccessibleStatus')[0]?.props.message).toBe( + i18n.t('spendAlerts.saved') + ); + expect(byType(renderer, 'Button')).toHaveLength(1); + + // A draft edit clears it again, so the message never outlives its values. + toggle(switchByLabel(renderer, i18n.t('spendAlerts.enable')), true); + expect(byType(renderer, 'AccessibleStatus')[0]?.props.message).toBeNull(); + }); +}); + +describe('SpendAlertsScreen master switch over an invalid hidden field', () => { + it('lets Save pass when the switch hides an invalid limit, keeping the stored value', async () => { + const { renderer } = await mountLoaded(); + + // A blank, enabled limit disables Save while its card is on screen. + type(renderer, LIMIT, ''); + expect(buttonByVariant(renderer).props.disabled).toBe(true); + + toggle(switchByLabel(renderer, i18n.t('spendAlerts.enable')), false); + + // The rule cards are gone, so no hidden field can gate Save any more. + expect(byType(renderer, 'FormField')).toHaveLength(0); + expect(buttonByVariant(renderer).props.disabled).toBe(false); + + press(buttonByVariant(renderer)); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + const input = saveMutationFn.mock.calls[0]?.[0] as { + enabled: boolean; + rules: { kind: string; threshold?: number; multiplierBasisPoints?: number }[]; + }; + expect(input.enabled).toBe(false); + // The unusable hidden limit falls back to the last saved value, so turning + // the feature off does not clobber the configured threshold. + expect(input.rules.find(rule => rule.kind === 'threshold')?.threshold).toBe(25); + expect(input.rules.find(rule => rule.kind === 'anomaly')?.multiplierBasisPoints).toBe(200); + }); +}); + +describe('SpendAlertsScreen push agreement', () => { + it('saves the rule and the caller category when a push channel is turned on', async () => { + const { renderer } = await mountLoaded(); + + // [0] threshold push, [1] anomaly push + toggle(channelSwitches(renderer, i18n.t('notifications.push'))[0], true); + + await waitFor(() => saveMutationFn.mock.calls.length === 1); + const input = saveMutationFn.mock.calls[0]?.[0] as { + rules: { kind: string; pushEnabled: boolean }[]; + }; + expect(input.rules).toContainEqual( + expect.objectContaining({ kind: 'threshold', pushEnabled: true }) + ); + expect(input.rules).toContainEqual( + expect.objectContaining({ kind: 'anomaly', pushEnabled: false }) + ); + }); + + it('points the push row at Notifications when the category is off', async () => { + const { renderer } = await mountLoaded({ pushCategoryEnabled: false }); + + expect(texts(renderer)).toContain(i18n.t('spendAlerts.pushOffByCategory')); + expect(channelSwitches(renderer, i18n.t('notifications.push'))).toHaveLength(0); + expect(channelSwitches(renderer, i18n.t('common.email'))).toHaveLength(2); + + press(byType(renderer, 'Pressable')[0]); + + expect(router.push).toHaveBeenCalledWith('/(app)/(tabs)/(3_profile)/notifications'); + }); + + it('points the push row at Notifications when the viewer has no device', async () => { + const { renderer } = await mountLoaded({ pushChannelBlocked: true }); + + expect(channelSwitches(renderer, i18n.t('notifications.push'))).toHaveLength(0); + expect(byType(renderer, 'Pressable')).toHaveLength(2); + }); +}); diff --git a/apps/mobile/src/components/organization/spend-alerts-screen.tsx b/apps/mobile/src/components/organization/spend-alerts-screen.tsx new file mode 100644 index 0000000000..6a7161b8e9 --- /dev/null +++ b/apps/mobile/src/components/organization/spend-alerts-screen.tsx @@ -0,0 +1,670 @@ +/* eslint-disable max-lines -- The spend view composes the scope boundary, the four +load states, and two rule cards with their channel rows in one surface; splitting +the pieces would re-encode the same draft state across files. */ + +import { fromMicrodollars } from '@kilocode/app-shared/utils'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import * as Haptics from 'expo-haptics'; +import { type Href, useRouter } from 'expo-router'; +import { type ReactNode, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Pressable, ScrollView, Switch, View } from 'react-native'; + +import { OrganizationBoundary } from '@/components/organization/organization-boundary'; +import { PermissionDenied } from '@/components/organization/permission-denied'; +import { + DEFAULT_MULTIPLIER, + DISABLED_MULTIPLIER, + DISABLED_THRESHOLD_USD, + multiplierBasisPoints, + multiplierError, + parseMultiplier, + parseThreshold, + thresholdError, + toWindowHours, +} from '@/components/organization/spend-alert-validators'; +import { QueryError } from '@/components/query-error'; +import { ScreenHeader } from '@/components/screen-header'; +import { TabScreenScrollView } from '@/components/tab-screen'; +import { AccessibleStatus } from '@/components/ui/accessible-status'; +import { Button } from '@/components/ui/button'; +import { DirectionalChevronRight } from '@/components/ui/directional-icons'; +import { FormField } from '@/components/ui/form-field'; +import { KvRow } from '@/components/ui/kv-row'; +import { SegmentedControl } from '@/components/ui/segmented-control'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Text } from '@/components/ui/text'; +import { i18n } from '@/i18n'; +import { formatList, formatMoney, formatNumber } from '@/lib/format'; +import { useOrgBoundary } from '@/lib/hooks/use-organization-queries'; +import { useRouteForegroundRefresh } from '@/lib/hooks/use-route-foreground-refresh'; +import { useThemeColors } from '@/lib/hooks/use-theme-colors'; +import { useTRPC } from '@/lib/trpc'; + +/** The Notifications screen owns the push gate the channel rows point at. */ +const NOTIFICATIONS_HREF = '/(app)/(tabs)/(3_profile)/notifications' as Href; + +const WINDOW_LABEL_KEYS = { + '24': 'spendAlerts.window24h', + '168': 'spendAlerts.window7d', + '720': 'spendAlerts.window30d', +} as const; + +type WindowOption = keyof typeof WINDOW_LABEL_KEYS; + +/** The option matching a stored window; an unsaved scope starts at 24 hours. */ +function windowOptionFor(value: number | null | undefined): WindowOption { + if (value === 168) { + return '168'; + } + if (value === 720) { + return '720'; + } + return '24'; +} + +/** The scope's settings: no `organizationId` is the caller's personal scope. */ +function useSpendAlertSettings(organizationId: string | undefined, enabled: boolean) { + const trpc = useTRPC(); + return useQuery( + trpc.spendAlerts.get.queryOptions(organizationId == null ? {} : { organizationId }, { enabled }) + ); +} + +/** + * The save mutation. It deliberately has no error toast: the form renders + * `spendAlerts.saveError` inline with a Retry and keeps the draft, the + * inline-error pattern (P2) the low-balance sheet uses for org settings. + */ +function useSaveSpendAlerts() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const settingsFilter = trpc.spendAlerts.get.pathFilter(); + return useMutation( + trpc.spendAlerts.save.mutationOptions({ + onSuccess: async () => { + await queryClient.invalidateQueries(settingsFilter); + }, + }) + ); +} + +// Every type here is derived from the procedure, so the form cannot drift from +// the wire contract. +type SpendAlertSaveInput = Parameters['mutate']>[0]; +type SpendAlertRuleKind = SpendAlertSaveInput['rules'][number]['kind']; +type SpendAlertSettings = NonNullable['data']>; +type SpendAlertRules = SpendAlertSettings['rules']; +type SpendAlertSpend = SpendAlertSettings['spend']; + +type PushOverride = Readonly<{ target: SpendAlertRuleKind; value: boolean }>; + +type SpendAlertsScreenProps = Readonly<{ + /** Organization scope from the route; absent is the caller's personal scope. */ + organizationId?: string; +}>; + +/** + * The spend view the spend-alert push opens: the scope's rolling spend, the + * master switch, and one card per alert kind saved through `spendAlerts.save`. + * The same screen serves the personal scope and an organization scope — the + * route decides, never the platform. + */ +export function SpendAlertsScreen({ organizationId }: SpendAlertsScreenProps) { + const { t } = useTranslation(); + const isOrgScope = organizationId != null; + const boundary = useOrgBoundary(organizationId); + // A push or a deep link can name an organization the viewer is not a member + // of: the boundary owns that case, so the settings query never fires until + // membership is confirmed. + const scopeResolved = !isOrgScope || (!boundary.isResolving && boundary.org != null); + // The tRPC-nested prefix form: the stored key is `[['spendAlerts','get'], …]`, + // whose first element is the procedure path array. The flat `['spendAlerts']` + // form compares the string against that array and never matches. + useRouteForegroundRefresh([[['spendAlerts']]]); + const query = useSpendAlertSettings(organizationId, scopeResolved); + + if (!scopeResolved) { + return ( + + ); + } + + if (query.isPending) { + return ; + } + + // `data` is kept across a background refetch, so after an error clears the + // loaded values are still in the resolved branch: a retry never blanks them. + if (query.isError && query.data == null) { + return ( + + + void query.refetch()} + isRetrying={query.isFetching} + /> + + ); + } + + if (!query.data.canManage) { + return ; + } + + return ( + + + + + + + ); +} + +type SpendAlertsFormProps = Readonly<{ + organizationId?: string; + enabled: boolean; + rules: SpendAlertRules; + spend: SpendAlertSpend; + pushCategoryEnabled: boolean; + pushChannelBlocked: boolean; +}>; + +function SpendAlertsForm({ + organizationId, + enabled: storedEnabled, + rules, + spend, + pushCategoryEnabled, + pushChannelBlocked, +}: SpendAlertsFormProps) { + const router = useRouter(); + const { t } = useTranslation(); + const save = useSaveSpendAlerts(); + + const threshold = rules?.find(rule => rule.kind === 'threshold'); + const anomaly = rules?.find(rule => rule.kind === 'anomaly'); + + // Text lives in refs (iOS: never control an input with state); the switches + // and the window picker are state because they render their own value. + const thresholdRef = useRef( + threshold?.threshold == null + ? '' + : formatNumber(threshold.threshold, i18n.language, { useGrouping: false }) + ); + const multiplierRef = useRef( + anomaly?.multiplierBasisPoints == null + ? String(DEFAULT_MULTIPLIER) + : formatNumber(anomaly.multiplierBasisPoints / 100, i18n.language, { useGrouping: false }) + ); + + const [enabled, setEnabled] = useState(storedEnabled); + const [thresholdEnabled, setThresholdEnabled] = useState(threshold?.enabled ?? true); + const [thresholdEmail, setThresholdEmail] = useState(threshold?.emailEnabled ?? true); + const [thresholdPush, setThresholdPush] = useState(threshold?.pushEnabled ?? false); + const [windowOption, setWindowOption] = useState(() => + windowOptionFor(threshold?.windowHours) + ); + const [anomalyEnabled, setAnomalyEnabled] = useState(anomaly?.enabled ?? true); + const [anomalyEmail, setAnomalyEmail] = useState(anomaly?.emailEnabled ?? true); + const [anomalyPush, setAnomalyPush] = useState(anomaly?.pushEnabled ?? false); + // The saved confirmation. It is rendered as a real Text node on this screen, + // not left to the toast alone: sonner-native draws outside the accessibility + // hierarchy, so a toast-only confirmation never reaches assistive tech (and + // the on-device hierarchy digest). Every draft edit clears it so it can only + // ever describe the values on screen. + const [saved, setSaved] = useState(false); + + // The wire requires a limit and a multiplier for both kinds whether or not the + // kind is enabled, but a kind the owner switched off is not being edited: it + // does not gate Save, and its payload falls back to a schema-valid stand-in + // (buildInput below). + const canSubmitWith = (thresholdOn: boolean, anomalyOn: boolean) => + (!thresholdOn || parseThreshold(thresholdRef.current) != null) && + (!anomalyOn || parseMultiplier(multiplierRef.current) != null); + // The master switch off hides the rule cards entirely: no field is on screen + // to repair, so a draft left invalid before the switch was turned off must not + // hold Save disabled, or the feature could never be turned off. + const canSubmit = () => !enabled || canSubmitWith(thresholdEnabled, anomalyEnabled); + const [canSave, setCanSave] = useState(canSubmit); + const revalidate = () => { + setSaved(false); + setCanSave(canSubmit()); + }; + + // Push reaches the viewer only when the rule asks for it, the viewer's own + // category is on, and the viewer has a registered device. Both gaps are fixed + // on the Notifications screen (the server writes the category in the same + // transaction as the rule), so the row sends them there instead of offering a + // switch that cannot deliver. + const pushOffByCategory = !pushCategoryEnabled || pushChannelBlocked; + + // The master switch is a draft like every other control: the empty state is + // only final when it matches what is stored. Once it differs, Save (or Retry) + // renders with it, otherwise `enabled: false` could never be posted and the + // feature could never be switched off. + const masterDirty = enabled !== storedEnabled; + + const spend24h = spend?.spend24hMicrodollars ?? 0; + const spend7d = spend?.spend7dMicrodollars ?? 0; + const spendValue = + spend24h > 0 || spend7d > 0 + ? formatMoney(fromMicrodollars(spend24h), i18n.language) + : t('spendAlerts.noSpend'); + + const windowOptions: readonly { value: WindowOption; label: string }[] = [ + { value: '24', label: t(WINDOW_LABEL_KEYS['24']) }, + { value: '168', label: t(WINDOW_LABEL_KEYS['168']) }, + { value: '720', label: t(WINDOW_LABEL_KEYS['720']) }, + ]; + + // What a hidden rule field falls back to when its draft is unusable: the last + // saved value, kept rather than clobbered by the stand-in. The server's schema + // already bounded both, so the checks only guard a legacy zero limit. + const storedThresholdUsd = + threshold?.threshold != null && threshold.threshold > 0 + ? threshold.threshold + : DISABLED_THRESHOLD_USD; + const storedMultiplierTimes = + anomaly?.multiplierBasisPoints != null && anomaly.multiplierBasisPoints >= 100 + ? anomaly.multiplierBasisPoints / 100 + : DISABLED_MULTIPLIER; + + const buildInput = (pushOverride?: PushOverride): SpendAlertSaveInput | null => { + const limit = parseThreshold(thresholdRef.current); + const multiplier = parseMultiplier(multiplierRef.current); + // A kind the owner switched off — or any kind while the master switch is + // off and its card is hidden — need not hold a valid value; the wire still + // wants one per kind, so it falls back to the stored value and then to the + // smallest value the schema accepts. + const thresholdHidden = !enabled || !thresholdEnabled; + const anomalyHidden = !enabled || !anomalyEnabled; + const thresholdUsd = limit ?? (thresholdHidden ? storedThresholdUsd : null); + const multiplierTimes = multiplier ?? (anomalyHidden ? storedMultiplierTimes : null); + if (thresholdUsd == null || multiplierTimes == null) { + return null; + } + return { + ...(organizationId == null ? {} : { organizationId }), + enabled, + rules: [ + { + kind: 'threshold', + enabled: thresholdEnabled, + threshold: thresholdUsd, + windowHours: toWindowHours(Number(windowOption)), + emailEnabled: thresholdEmail, + pushEnabled: pushOverride?.target === 'threshold' ? pushOverride.value : thresholdPush, + }, + { + kind: 'anomaly', + enabled: anomalyEnabled, + multiplierBasisPoints: multiplierBasisPoints(multiplierTimes), + emailEnabled: anomalyEmail, + pushEnabled: pushOverride?.target === 'anomaly' ? pushOverride.value : anomalyPush, + }, + ], + }; + }; + + const onSave = () => { + const input = buildInput(); + if (input == null) { + return; + } + save.mutate(input, { + onSuccess: () => { + void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + // No toast here: the confirmation below the fields is the single + // announcement owner (AccessibleStatus), and it describes the values + // that are still on screen. A toast would announce the same sentence a + // second time and draw a second copy of it. The owner leaves through + // the header's back control. + setSaved(true); + }, + }); + }; + + const onPushChange = ( + target: SpendAlertRuleKind, + next: boolean, + apply: (value: boolean) => void + ) => { + void Haptics.selectionAsync(); + setSaved(false); + apply(next); + // Toggling push also writes the caller's notification category (the + // server's agreement between the rule channel and the category), so the + // Notifications screen cannot disagree. Only a valid draft can be sent; an + // invalid one waits for Save, which owns the inline field errors. + const input = buildInput({ target, value: next }); + if (input != null) { + save.mutate(input); + } + }; + + // save has no mutation toast — success or failure (inline error pattern P2) + // — so AccessibleStatus is the single announcement owner on both platforms: + // on success it carries the confirmation as a real Text node, on failure the + // error, and a failure keeps the draft and offers Retry. A failure outranks + // the last confirmation. + const confirmedMessage = saved ? t('spendAlerts.saved') : null; + const saveMessage = save.isError ? t('spendAlerts.saveError') : confirmedMessage; + + const saveControls = ( + <> + {/* The slot is reserved whether or not a message shows: the confirmation + and the error swap in place, so the Save button below never moves + when the save settles. */} + + + + + {save.isError && ( + + )} + + + + ); + + return ( + <> + + {t('spendAlerts.subtitle')} + + + + + + + + {t('common.enabled')} + { + void Haptics.selectionAsync(); + setSaved(false); + setEnabled(value); + // Off hides the rule fields, so they stop gating Save; on restores + // their gate with whatever the fields currently hold. + setCanSave(value ? canSubmitWith(thresholdEnabled, anomalyEnabled) : true); + }} + /> + + + {enabled ? ( + <> + { + void Haptics.selectionAsync(); + setSaved(false); + setThresholdEnabled(value); + // Switching the kind off drops its field from the Save gate. + setCanSave(canSubmitWith(value, anomalyEnabled)); + }} + emailEnabled={thresholdEmail} + onEmailChange={value => { + setSaved(false); + setThresholdEmail(value); + }} + pushEnabled={thresholdPush} + onPushChange={next => { + onPushChange('threshold', next, setThresholdPush); + }} + pushOffByCategory={pushOffByCategory} + onOpenNotifications={() => { + router.push(NOTIFICATIONS_HREF); + }} + > + { + thresholdRef.current = value; + revalidate(); + }} + /> + + + {t('spendAlerts.windowLabel')} + + + accessibilityLabel={t('spendAlerts.windowLabel')} + options={windowOptions} + value={windowOption} + onChange={value => { + setSaved(false); + setWindowOption(value); + }} + /> + + + + { + void Haptics.selectionAsync(); + setSaved(false); + setAnomalyEnabled(value); + // Switching the kind off drops its field from the Save gate. + setCanSave(canSubmitWith(thresholdEnabled, value)); + }} + emailEnabled={anomalyEmail} + onEmailChange={value => { + setSaved(false); + setAnomalyEmail(value); + }} + pushEnabled={anomalyPush} + onPushChange={next => { + onPushChange('anomaly', next, setAnomalyPush); + }} + pushOffByCategory={pushOffByCategory} + onOpenNotifications={() => { + router.push(NOTIFICATIONS_HREF); + }} + > + { + multiplierRef.current = value; + revalidate(); + }} + /> + + + {/* save has no mutation toast (inline error pattern P2), so + AccessibleStatus is the single announcement owner here. */} + {saveControls} + + ) : ( + <> + + {t('spendAlerts.empty')} + + {/* A disable save lands with the stored value now false, so + `masterDirty` clears on its own refetch. Keep the controls while a + confirmation is live, or the saved line (and its reserved slot) + would vanish the moment the save settles. An edit clears `saved` + (setSaved(false)), which returns this branch to the master switch + alone. */} + {masterDirty || saved ? saveControls : null} + + )} + + ); +} + +type RuleCardProps = Readonly<{ + title: string; + subtitle: string; + enabled: boolean; + onEnabledChange: (value: boolean) => void; + emailEnabled: boolean; + onEmailChange: (value: boolean) => void; + pushEnabled: boolean; + onPushChange: (value: boolean) => void; + pushOffByCategory: boolean; + onOpenNotifications: () => void; + children: ReactNode; +}>; + +/** One alert kind: its enable switch, its kind-specific fields, its channels. */ +function RuleCard({ + title, + subtitle, + enabled, + onEnabledChange, + emailEnabled, + onEmailChange, + pushEnabled, + onPushChange, + pushOffByCategory, + onOpenNotifications, + children, +}: RuleCardProps) { + const { t } = useTranslation(); + const colors = useThemeColors(); + + // The OS accessibility hierarchy does not group a switch with the card above + // it, and both cards render an Email and a Push switch. Naming the kind + // alongside the channel keeps each control addressable on its own, like the + // Notifications screen's dedicated `spendAlertsToggle` label. + const emailLabel = formatList([title, t('common.email')], i18n.language); + const pushLabel = formatList([title, t('notifications.push')], i18n.language); + + return ( + + + + {title} + + {subtitle} + + + + + + {children} + + + + {t('common.email')} + {/* The row's own label names the channel; the card's kind is composed + into the switch label, so the two Email rows stay distinct. */} + + + {pushOffByCategory ? ( + + {t('notifications.push')} + + + {t('spendAlerts.pushOffByCategory')} + + + + + ) : ( + + {t('notifications.push')} + + + )} + + + ); +} + +function RuleCardSkeleton() { + return ( + + + + + + + + + ); +} + +/** Skeleton at the loaded rows' size, so the swap moves nothing. */ +function SpendAlertsSkeleton() { + const { t } = useTranslation(); + return ( + + + + + + + + + + + + ); +} diff --git a/apps/mobile/src/components/preferences-screen.feature-flags.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.feature-flags.mounted.test.tsx index f88af6f0f9..75a385ad2c 100644 --- a/apps/mobile/src/components/preferences-screen.feature-flags.mounted.test.tsx +++ b/apps/mobile/src/components/preferences-screen.feature-flags.mounted.test.tsx @@ -66,6 +66,7 @@ vi.mock('@/components/ui/icons', () => ({ Shield: 'Shield', SlidersHorizontal: 'SlidersHorizontal', Smartphone: 'Smartphone', + Wallet: 'Wallet', WandSparkles: 'WandSparkles', })); vi.mock('@/components/language-picker-sheet', () => ({ diff --git a/apps/mobile/src/components/preferences-screen.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.mounted.test.tsx index 47c761cebf..ff37cd92fe 100644 --- a/apps/mobile/src/components/preferences-screen.mounted.test.tsx +++ b/apps/mobile/src/components/preferences-screen.mounted.test.tsx @@ -1,7 +1,7 @@ import { act, type ReactTestInstance, type ReactTestRenderer } from '@/test/renderer'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import '@/i18n'; +import { i18n } from '@/i18n'; import { PreferencesScreen } from '@/components/preferences-screen'; import { renderWithProviders } from '@/test/render-with-providers'; @@ -25,6 +25,7 @@ vi.mock('@/components/ui/icons', () => ({ Globe: 'Globe', Mic: 'Mic', SlidersHorizontal: 'SlidersHorizontal', + Wallet: 'Wallet', WandSparkles: 'WandSparkles', })); vi.mock('@/components/screen-header', () => ({ ScreenHeader: () => null })); @@ -72,7 +73,7 @@ describe('PreferencesScreen hub', () => { it('renders one navigation row per settings group with its title and subtitle', async () => { const renderer = await mountPreferences(); - expect(hubRows(renderer)).toHaveLength(5); + expect(hubRows(renderer)).toHaveLength(6); expect(row(renderer, 'General').props).toMatchObject({ icon: 'SlidersHorizontal', last: true }); expect(row(renderer, 'Voice input').props).toMatchObject({ icon: 'Mic', @@ -96,6 +97,10 @@ describe('PreferencesScreen hub', () => { subtitle: 'Push preferences', last: true, }); + expect(row(renderer, i18n.t('notifications.channel.spend')).props).toMatchObject({ + icon: 'Wallet', + last: true, + }); }); it.each([ @@ -104,6 +109,7 @@ describe('PreferencesScreen hub', () => { ['Translate tool summaries', '/(app)/(tabs)/(3_profile)/tool-summary-translation'], ['Account', '/(app)/(tabs)/(3_profile)/account'], ['Notifications', '/(app)/(tabs)/(3_profile)/notifications'], + [i18n.t('notifications.channel.spend'), '/(app)/(tabs)/(3_profile)/spend-alerts'], ])('pushes the %s subpage from its row', async (title, route) => { const renderer = await mountPreferences(); diff --git a/apps/mobile/src/components/preferences-screen.tsx b/apps/mobile/src/components/preferences-screen.tsx index 7aeb9d30be..95c4865561 100644 --- a/apps/mobile/src/components/preferences-screen.tsx +++ b/apps/mobile/src/components/preferences-screen.tsx @@ -1,5 +1,5 @@ import { type Href, useRouter } from 'expo-router'; -import { Bell, Globe, Mic, SlidersHorizontal, WandSparkles } from '@/components/ui/icons'; +import { Bell, Globe, Mic, SlidersHorizontal, Wallet, WandSparkles } from '@/components/ui/icons'; import { View } from 'react-native'; import { useTranslation } from 'react-i18next'; @@ -102,6 +102,15 @@ export function PreferencesScreen() { router.push('/(app)/(tabs)/(3_profile)/notifications' as Href); }} /> + { + router.push('/(app)/(tabs)/(3_profile)/spend-alerts' as Href); + }} + /> ); diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index dcb30e8b03..ce21c47385 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -827,7 +827,8 @@ "chat": "Kletsboodskappe", "kiloclaw": "KiloClaw-aktiwiteit", "balance": "Saldowaarskuwings", - "security": "Sekuriteitsbevindings" + "security": "Sekuriteitsbevindings", + "spend": "Bestedingswaarskuwings" }, "category": { "chatMessagesSubtitle": "antwoorde in jou gesprekke", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "klaar / misluk / gereed om te beheer", "kiloclawActivitySubtitle": "instansie gereed/misluk, geskeduleerde aksies", "balanceAlertsSubtitle": "waarskuwings oor die organisasie se lae saldo", + "spendAlertsSubtitle": "oorskryding van drempels en uurlikse pieke", + "spendAlertsToggle": "Kategorie vir bestedingswaarskuwings", "securityFindingsSubtitle": "nuwe bevindings en SLA-herinneringe" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Maak laaste sessie oop" + }, + "spendAlerts": { + "subtitle": "E-pos of stoot 'n waarskuwing na die faktuurkontakte wanneer hierdie omvang se besteding 'n limiet oorskry of bo sy gewone uurlikse tempo styg.", + "enable": "Aktiveer bestedingswaarskuwings", + "thresholdTitle": "Oorskryding van drempel", + "thresholdSubtitle": "Lopende besteding oorskry jou limiet", + "anomalyTitle": "Uurlikse piek", + "anomalySubtitle": "'n Uur is ver bo hierdie omvang se gewone tempo", + "limitLabel": "Limiet (USD)", + "windowLabel": "Venster", + "window24h": "24 uur", + "window7d": "7 dae", + "window30d": "30 dae", + "multiplierLabel": "Piekvermenigvuldiger", + "saved": "Instellings vir bestedingswaarskuwings gestoor", + "loadError": "Kon nie bestedingswaarskuwings laai nie.", + "saveError": "Kon nie bestedingswaarskuwings stoor nie.", + "empty": "Bestedingswaarskuwings is af. Skakel dit aan om hierdie omvang se faktuurkontakte te waarsku.", + "noSpend": "Nog geen besteding aangeteken nie", + "spend24h": "Besteding in die afgelope 24 uur", + "multiplierError": "Voer 'n vermenigvuldiger van 1 of meer in", + "permissionDenied": "Jy het nie toestemming om bestedingswaarskuwings te bestuur nie.", + "pushOffByCategory": "Af in Kennisgewings" } } diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index f07fb6012b..c5148a50b3 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -827,7 +827,8 @@ "chat": "የውይይት መልእክቶች", "kiloclaw": "የKiloClaw እንቅስቃሴ", "balance": "የቀሪ ሂሳብ ማስጠንቀቂያዎች", - "security": "የደህንነት ግኝቶች" + "security": "የደህንነት ግኝቶች", + "spend": "የወጪ ማስጠንቀቂያዎች" }, "category": { "chatMessagesSubtitle": "በውይይቶችዎ ውስጥ የሚሰጡ ምላሾች", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "የተጠናቀቀ / ያልተሳካ / ለመቆጣጠር ዝግጁ", "kiloclawActivitySubtitle": "ዝግጁ የሆነ/ያልተሳካ ሂደት፣ መርሐግብር የተያዘላቸው ተግባሮች", "balanceAlertsSubtitle": "ዝቅተኛ የድርጅት ቀሪ ሂሳብ ማስጠንቀቂያዎች", + "spendAlertsSubtitle": "የገደብ መሻገሪያዎች እና የሰዓት ጭማሪዎች", + "spendAlertsToggle": "የወጪ ማስጠንቀቂያ ምድብ", "securityFindingsSubtitle": "አዳዲስ ግኝቶችና የSLA ማስታወሻዎች" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "የመጨረሻውን ክፍለ ጊዜ ክፈት" + }, + "spendAlerts": { + "subtitle": "የዚህ አካባቢ ወጪ ገደብ ሲያልፍ ወይም ከተለመደው የሰዓት መጠኑ በላይ ሲጨምር ለሂሳብ ተጠሪዎች በኢሜይል ወይም በግፋ ማስጠንቀቂያ ላክ።", + "enable": "የወጪ ማስጠንቀቂያዎችን አንቃ", + "thresholdTitle": "የገደብ መሻገር", + "thresholdSubtitle": "ተንቀሳቃሽ ወጪ ገደብህን ያልፋል", + "anomalyTitle": "የሰዓት ጭማሪ", + "anomalySubtitle": "አንድ ሰዓት ከዚህ አካባቢ የተለመደው መጠን በጣም ከፍ ይላል", + "limitLabel": "ገደብ (USD)", + "windowLabel": "የጊዜ መስኮት", + "window24h": "24 ሰዓታት", + "window7d": "7 ቀናት", + "window30d": "30 ቀናት", + "multiplierLabel": "የጭማሪ ብዜት", + "saved": "የወጪ ማስጠንቀቂያ ቅንብሮች ተቀምጠዋል", + "loadError": "የወጪ ማስጠንቀቂያዎችን መጫን አልተቻለም።", + "saveError": "የወጪ ማስጠንቀቂያዎችን ማስቀመጥ አልተቻለም።", + "empty": "የወጪ ማስጠንቀቂያዎች ጠፍተዋል። የዚህን አካባቢ የሂሳብ ተጠሪዎች ለማስጠንቀቅ ያብራቸው።", + "noSpend": "እስካሁን ምንም ወጪ አልተመዘገበም", + "spend24h": "ባለፉት 24 ሰዓታት የወጣ ወጪ", + "multiplierError": "ከ1 ወይም ከዚያ በላይ የሆነ ብዜት ያስገቡ", + "permissionDenied": "የወጪ ማስጠንቀቂያዎችን ለማስተዳደር ፈቃድ የለዎትም።", + "pushOffByCategory": "በማሳወቂያዎች ውስጥ ተሰናክሏል" } } diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index dafac39351..e26573dd1a 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -323,7 +323,8 @@ "chat": "رسائل الدردشة", "kiloclaw": "نشاط KiloClaw", "balance": "تنبيهات الرصيد", - "security": "ملاحظات الأمان" + "security": "ملاحظات الأمان", + "spend": "تنبيهات الإنفاق" }, "category": { "chatMessagesSubtitle": "الردود في محادثاتك", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "مكتملة / تعذّر إكمالها / جاهزة للتحكم", "kiloclawActivitySubtitle": "جاهزية المثيل أو فشله، وإجراءات مجدولة", "balanceAlertsSubtitle": "تحذيرات انخفاض رصيد المؤسسة", + "spendAlertsSubtitle": "تجاوزات الحدود والارتفاعات الساعية", + "spendAlertsToggle": "فئة تنبيهات الإنفاق", "securityFindingsSubtitle": "نتائج جديدة وتذكيرات SLA" }, "action": { @@ -3371,5 +3374,28 @@ }, "launcher": { "openLastSession": "فتح آخر جلسة" + }, + "spendAlerts": { + "subtitle": "أرسل تنبيهًا بالبريد الإلكتروني أو بالإشعارات إلى جهات اتصال الفوترة عندما يتجاوز إنفاق هذا النطاق حدًا معينًا أو يرتفع فوق معدله الساعي المعتاد.", + "enable": "تفعيل تنبيهات الإنفاق", + "thresholdTitle": "تجاوز الحد", + "thresholdSubtitle": "الإنفاق المتحرك يتجاوز حدك", + "anomalyTitle": "ارتفاع ساعي", + "anomalySubtitle": "الساعة أعلى بكثير من المعدل المعتاد لهذا النطاق", + "limitLabel": "الحد (USD)", + "windowLabel": "النافذة الزمنية", + "window24h": "24 ساعة", + "window7d": "7 أيام", + "window30d": "30 يومًا", + "multiplierLabel": "مضاعف الارتفاع", + "saved": "تم حفظ إعدادات تنبيهات الإنفاق", + "loadError": "تعذّر تحميل تنبيهات الإنفاق.", + "saveError": "تعذّر حفظ تنبيهات الإنفاق.", + "empty": "تنبيهات الإنفاق متوقفة. فعّلها لتنبيه جهات اتصال الفوترة لهذا النطاق.", + "noSpend": "لم يُسجَّل أي إنفاق بعد", + "spend24h": "الإنفاق خلال آخر 24 ساعة", + "multiplierError": "أدخل مضاعفًا يساوي 1 أو أكثر", + "permissionDenied": "ليست لديك صلاحية لإدارة تنبيهات الإنفاق.", + "pushOffByCategory": "معطّل في الإشعارات" } } diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index b1c68265e5..1d54fa4b69 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -827,7 +827,8 @@ "chat": "Söhbət mesajları", "kiloclaw": "KiloClaw fəaliyyəti", "balance": "Balans bildirişləri", - "security": "Təhlükəsizlik problemləri" + "security": "Təhlükəsizlik problemləri", + "spend": "Xərc bildirişləri" }, "category": { "chatMessagesSubtitle": "söhbətlərindəki cavablar", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "tamamlanıb / uğursuz olub / idarəetməyə hazırdır", "kiloclawActivitySubtitle": "nüsxə hazırdır/uğursuz olub, planlaşdırılmış əməliyyatlar", "balanceAlertsSubtitle": "təşkilatın balansının azalması barədə xəbərdarlıqlar", + "spendAlertsSubtitle": "hədd aşmaları və saatlıq sıçrayışlar", + "spendAlertsToggle": "Xərc bildirişləri kateqoriyası", "securityFindingsSubtitle": "yeni aşkarlamalar və SLA xatırlatmaları" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Son sessiyanı aç" + }, + "spendAlerts": { + "subtitle": "Bu əhatənin xərci həddi aşdıqda və ya adi saatlıq sürətindən yuxarı sıçradıqda ödəniş əlaqələrinə e-poçt və ya push ilə bildiriş göndər.", + "enable": "Xərc bildirişlərini aktivləşdir", + "thresholdTitle": "Həddin aşılması", + "thresholdSubtitle": "Yığılan xərc limitini aşır", + "anomalyTitle": "Saatlıq sıçrayış", + "anomalySubtitle": "Bir saat bu əhatənin adi sürətindən xeyli yuxarıdır", + "limitLabel": "Limit (USD)", + "windowLabel": "Pəncərə", + "window24h": "24 saat", + "window7d": "7 gün", + "window30d": "30 gün", + "multiplierLabel": "Sıçrayış əmsalı", + "saved": "Xərc bildirişi parametrləri yadda saxlanıldı", + "loadError": "Xərc bildirişlərini yükləmək mümkün olmadı.", + "saveError": "Xərc bildirişlərini yadda saxlamaq mümkün olmadı.", + "empty": "Xərc bildirişləri söndürülüb. Bu əhatənin ödəniş əlaqələrini xəbərdar etmək üçün onları yandır.", + "noSpend": "Hələ heç bir xərc qeydə alınmayıb", + "spend24h": "Son 24 saatda xərc", + "multiplierError": "1 və ya daha böyük əmsal daxil et", + "permissionDenied": "Xərc bildirişlərini idarə etmək icazən yoxdur.", + "pushOffByCategory": "Bildirişlərdə söndürülüb" } } diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 64286528b5..6e5fc6280f 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -859,7 +859,8 @@ "chat": "Паведамленні ў чаце", "kiloclaw": "Актыўнасць KiloClaw", "balance": "Апавяшчэнні аб балансе", - "security": "Выяўленыя праблемы бяспекі" + "security": "Выяўленыя праблемы бяспекі", + "spend": "Апавяшчэнні аб выдатаках" }, "category": { "chatMessagesSubtitle": "адказы ў вашых размовах", @@ -871,6 +872,8 @@ "sessionStatusSubtitle": "завершаны / памылка / гатовы да кіравання", "kiloclawActivitySubtitle": "экзэмпляр гатовы або завяршыўся з памылкай, запланаваныя дзеянні", "balanceAlertsSubtitle": "папярэджанні аб нізкім балансе арганізацыі", + "spendAlertsSubtitle": "перасячэнні парогаў і гадзінныя ўсплёскі", + "spendAlertsToggle": "Катэгорыя апавяшчэнняў аб выдатаках", "securityFindingsSubtitle": "новыя праблемы бяспекі і напамінкі аб SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Адкрыць апошні сеанс" + }, + "spendAlerts": { + "subtitle": "Дасылайце апавяшчэнне па электроннай пошце або пуш-паведамленнем кантактам па аплаце, калі выдатакі гэтай сферы перавышаюць ліміт або рэзка ўзрастаюць вышэй за звычайную гадзінную хуткасць.", + "enable": "Уключыць апавяшчэнні аб выдатаках", + "thresholdTitle": "Перасячэнне парога", + "thresholdSubtitle": "Бягучыя выдатакі перавышаюць ваш ліміт", + "anomalyTitle": "Гадзінны ўсплёск", + "anomalySubtitle": "Гадзіна значна вышэй за звычайную хуткасць гэтай сферы", + "limitLabel": "Ліміт (USD)", + "windowLabel": "Акно", + "window24h": "24 гадзіны", + "window7d": "7 дзён", + "window30d": "30 дзён", + "multiplierLabel": "Множнік усплёску", + "saved": "Налады апавяшчэнняў аб выдатаках захаваны", + "loadError": "Не ўдалося загрузіць апавяшчэнні аб выдатаках.", + "saveError": "Не ўдалося захаваць апавяшчэнні аб выдатаках.", + "empty": "Апавяшчэнні аб выдатаках выключаны. Уключыце іх, каб папярэджваць кантакты па аплаце гэтай сферы.", + "noSpend": "Выдаткі яшчэ не зафіксаваны", + "spend24h": "Выдаткі за апошнія 24 гадзіны", + "multiplierError": "Увядзіце множнік 1 або больш", + "permissionDenied": "У вас няма дазволу кіраваць апавяшчэннямі аб выдатаках.", + "pushOffByCategory": "Выключана ў апавяшчэннях" } } diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index d7f20f8d38..8f4a9ee8c9 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -827,7 +827,8 @@ "chat": "Съобщения в чата", "kiloclaw": "Активност на KiloClaw", "balance": "Известия за салдото", - "security": "Проблеми със сигурността" + "security": "Проблеми със сигурността", + "spend": "Известия за разходи" }, "category": { "chatMessagesSubtitle": "отговори в разговорите ти", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "завършена / неуспешна / готова за управление", "kiloclawActivitySubtitle": "готова инстанция или грешка, планирани действия", "balanceAlertsSubtitle": "предупреждения за ниска наличност на организацията", + "spendAlertsSubtitle": "преминаване на прагове и часови скокове", + "spendAlertsToggle": "Категория известия за разходи", "securityFindingsSubtitle": "нови открити проблеми и напомняния за SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Отвори последната сесия" + }, + "spendAlerts": { + "subtitle": "Изпращай известие по имейл или push до контактите за фактуриране, когато разходите на този обхват надхвърлят лимит или скочат над обичайната си часова норма.", + "enable": "Включи известията за разходи", + "thresholdTitle": "Преминаване на праг", + "thresholdSubtitle": "Текущите разходи надхвърлят лимита ти", + "anomalyTitle": "Часов скок", + "anomalySubtitle": "Един час е далеч над обичайната норма за този обхват", + "limitLabel": "Лимит (USD)", + "windowLabel": "Прозорец", + "window24h": "24 часа", + "window7d": "7 дни", + "window30d": "30 дни", + "multiplierLabel": "Множител на скока", + "saved": "Настройките за известия за разходи са запазени", + "loadError": "Известията за разходи не можаха да се заредят.", + "saveError": "Известията за разходи не можаха да се запазят.", + "empty": "Известията за разходи са изключени. Включи ги, за да предупреждаваш контактите за фактуриране на този обхват.", + "noSpend": "Още няма записани разходи", + "spend24h": "Разходи през последните 24 часа", + "multiplierError": "Въведи множител 1 или повече", + "permissionDenied": "Нямаш разрешение да управляваш известията за разходи.", + "pushOffByCategory": "Изключено в „Известия“" } } diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 7a6f0cc613..73f8fb66ec 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -827,7 +827,8 @@ "chat": "চ্যাটের বার্তা", "kiloclaw": "KiloClaw-এর কার্যকলাপ", "balance": "ব্যালেন্সের সতর্কতা", - "security": "শনাক্ত নিরাপত্তা সমস্যা" + "security": "শনাক্ত নিরাপত্তা সমস্যা", + "spend": "খরচের সতর্কতা" }, "category": { "chatMessagesSubtitle": "আপনার কথোপকথনে উত্তর", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "সমাপ্ত / ব্যর্থ / নিয়ন্ত্রণের জন্য প্রস্তুত", "kiloclawActivitySubtitle": "ইনস্ট্যান্স প্রস্তুত বা ব্যর্থ, নির্ধারিত কাজ", "balanceAlertsSubtitle": "সংস্থার ব্যালেন্স কমে যাওয়ার সতর্কতা", + "spendAlertsSubtitle": "সীমা অতিক্রম ও ঘণ্টাভিত্তিক বৃদ্ধি", + "spendAlertsToggle": "খরচের সতর্কতার বিভাগ", "securityFindingsSubtitle": "নতুন নিরাপত্তা সমস্যা ও SLA মনে করিয়ে দেওয়ার বিজ্ঞপ্তি" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "শেষ সেশন খুলুন" + }, + "spendAlerts": { + "subtitle": "এই স্কোপের খরচ একটি সীমা ছাড়িয়ে গেলে বা স্বাভাবিক ঘণ্টাভিত্তিক হারের চেয়ে বেড়ে গেলে বিলিং পরিচিতিদের ইমেল বা পুশের মাধ্যমে সতর্কতা পাঠান।", + "enable": "খরচের সতর্কতা চালু করুন", + "thresholdTitle": "সীমা অতিক্রম", + "thresholdSubtitle": "চলমান খরচ আপনার সীমা ছাড়িয়ে যাচ্ছে", + "anomalyTitle": "ঘণ্টাভিত্তিক বৃদ্ধি", + "anomalySubtitle": "এক ঘণ্টার খরচ এই স্কোপের স্বাভাবিক হারের চেয়ে অনেক বেশি", + "limitLabel": "সীমা (USD)", + "windowLabel": "সময়সীমা", + "window24h": "24 ঘণ্টা", + "window7d": "7 দিন", + "window30d": "30 দিন", + "multiplierLabel": "বৃদ্ধির গুণক", + "saved": "খরচের সতর্কতার সেটিংস সংরক্ষিত হয়েছে", + "loadError": "খরচের সতর্কতা লোড করা যায়নি।", + "saveError": "খরচের সতর্কতা সংরক্ষণ করা যায়নি।", + "empty": "খরচের সতর্কতা বন্ধ আছে। এই স্কোপের বিলিং পরিচিতিদের সতর্ক করতে এটি চালু করুন।", + "noSpend": "এখনও কোনো খরচ নথিভুক্ত হয়নি", + "spend24h": "গত 24 ঘণ্টার খরচ", + "multiplierError": "1 বা তার বেশি গুণক লিখুন", + "permissionDenied": "খরচের সতর্কতা পরিচালনার অনুমতি আপনার নেই।", + "pushOffByCategory": "বিজ্ঞপ্তিতে বন্ধ" } } diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 8d55e3570f..69e3fce6eb 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -843,7 +843,8 @@ "chat": "Poruke u razgovoru", "kiloclaw": "Aktivnost u KiloClawu", "balance": "Upozorenja o stanju", - "security": "Sigurnosni nalazi" + "security": "Sigurnosni nalazi", + "spend": "Upozorenja o potrošnji" }, "category": { "chatMessagesSubtitle": "odgovori u tvojim razgovorima", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "završeno / neuspjelo / spremno za upravljanje", "kiloclawActivitySubtitle": "instance spremne/neuspjele, zakazane radnje", "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi na sat", + "spendAlertsToggle": "Kategorija upozorenja o potrošnji", "securityFindingsSubtitle": "novi nalazi i SLA podsjetnici" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Otvori posljednju sesiju" + }, + "spendAlerts": { + "subtitle": "Pošalji upozorenje e-poštom ili push porukom kontaktima za naplatu kada potrošnja ovog obuhvata pređe limit ili skoči iznad uobičajene satne stope.", + "enable": "Omogući upozorenja o potrošnji", + "thresholdTitle": "Prekoračenje praga", + "thresholdSubtitle": "Tekuća potrošnja prelazi tvoj limit", + "anomalyTitle": "Satni skok", + "anomalySubtitle": "Jedan sat je daleko iznad uobičajene stope ovog obuhvata", + "limitLabel": "Limit (USD)", + "windowLabel": "Prozor", + "window24h": "24 sata", + "window7d": "7 dana", + "window30d": "30 dana", + "multiplierLabel": "Množilac skoka", + "saved": "Postavke upozorenja o potrošnji su sačuvane", + "loadError": "Upozorenja o potrošnji nisu učitana.", + "saveError": "Upozorenja o potrošnji nisu sačuvana.", + "empty": "Upozorenja o potrošnji su isključena. Uključi ih da upozoriš kontakte za naplatu ovog obuhvata.", + "noSpend": "Još nema zabilježene potrošnje", + "spend24h": "Potrošnja u posljednja 24 sata", + "multiplierError": "Unesi množilac 1 ili veći", + "permissionDenied": "Nemaš dozvolu za upravljanje upozorenjima o potrošnji.", + "pushOffByCategory": "Isključeno u Obavijestima" } } diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 0f965c4eaf..94a3f23448 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -843,7 +843,8 @@ "chat": "Missatges de xat", "kiloclaw": "Activitat de KiloClaw", "balance": "Alertes de saldo", - "security": "Problemes de seguretat" + "security": "Problemes de seguretat", + "spend": "Alertes de despesa" }, "category": { "chatMessagesSubtitle": "respostes a les teves converses", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "finalitzada / fallida / a punt per controlar", "kiloclawActivitySubtitle": "instància a punt/fallida, accions programades", "balanceAlertsSubtitle": "avisos de saldo baix de l'organització", + "spendAlertsSubtitle": "superacions de llindars i pics horaris", + "spendAlertsToggle": "Categoria d'alertes de despesa", "securityFindingsSubtitle": "noves deteccions i recordatoris d'SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Obre l'última sessió" + }, + "spendAlerts": { + "subtitle": "Envia una alerta per correu o notificació push als contactes de facturació quan la despesa d'aquest àmbit superi un límit o augmenti per sobre de la seva taxa horària habitual.", + "enable": "Activa les alertes de despesa", + "thresholdTitle": "Superació del llindar", + "thresholdSubtitle": "La despesa acumulada supera el teu límit", + "anomalyTitle": "Pic horari", + "anomalySubtitle": "Una hora està molt per sobre de la taxa habitual d'aquest àmbit", + "limitLabel": "Límit (USD)", + "windowLabel": "Finestra", + "window24h": "24 hores", + "window7d": "7 dies", + "window30d": "30 dies", + "multiplierLabel": "Multiplicador del pic", + "saved": "S'ha desat la configuració de les alertes de despesa", + "loadError": "No s'han pogut carregar les alertes de despesa.", + "saveError": "No s'han pogut desar les alertes de despesa.", + "empty": "Les alertes de despesa estan desactivades. Activa-les per avisar els contactes de facturació d'aquest àmbit.", + "noSpend": "Encara no s'ha registrat cap despesa", + "spend24h": "Despesa de les últimes 24 hores", + "multiplierError": "Introdueix un multiplicador d'1 o més", + "permissionDenied": "No tens permís per gestionar les alertes de despesa.", + "pushOffByCategory": "Desactivat a Notificacions" } } diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 983ed782a8..10ccf344ba 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -827,7 +827,8 @@ "chat": "نامەکانی گفتوگۆ", "kiloclaw": "چالاکییەکانی KiloClaw", "balance": "ئاگادارکردنەوەکانی باڵانس", - "security": "دۆزینەوەکانی ئاسایش" + "security": "دۆزینەوەکانی ئاسایش", + "spend": "ئاگادارکردنەوەکانی خەرجی" }, "category": { "chatMessagesSubtitle": "وەڵامەکان لە گفتوگۆکانتدا", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "تەواوبوو / سەرنەکەوتوو / ئامادە بۆ کۆنترۆڵکردن", "kiloclawActivitySubtitle": "نموونەی ئامادە یان سەرنەکەوتوو، کردارە کاتدیاریکراوەکان", "balanceAlertsSubtitle": "ئاگادارکردنەوەکانی کەمی باڵانسی ڕێکخراوە", + "spendAlertsSubtitle": "تێپەڕینی سنوورەکان و بەرزبوونەوەکانی کاتژمێری", + "spendAlertsToggle": "پۆلی ئاگادارکردنەوەکانی خەرجی", "securityFindingsSubtitle": "دۆزینەوە نوێکان و بیرخستنەوەکانی SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "کردنەوەی دوایین دانیشتن" + }, + "spendAlerts": { + "subtitle": "کاتێک خەرجی ئەم بووارە تێپەڕی سنوورێک بکات یان لە ڕێژەی کاتژمێری ئاساییدا بەرز ببێتەوە، ئاگادارکردنەوەیەک بە ئیمەیڵ یان پاڵنەر بۆ پەیوەندییەکانی پارەدان بنێرە.", + "enable": "چالاککردنی ئاگادارکردنەوەکانی خەرجی", + "thresholdTitle": "تێپەڕینی سنوور", + "thresholdSubtitle": "خەرجی جاری لە سنوورەکەت تێدەپەڕێت", + "anomalyTitle": "بەرزبوونەوەی کاتژمێری", + "anomalySubtitle": "یەک کاتژمێر زۆر لە ڕێژەی ئاسایی ئەم بووارە بەرزترە", + "limitLabel": "سنوور (USD)", + "windowLabel": "ماوە", + "window24h": "24 کاتژمێر", + "window7d": "7 ڕۆژ", + "window30d": "30 ڕۆژ", + "multiplierLabel": "لێکدەری بەرزبوونەوە", + "saved": "ڕێکخستنەکانی ئاگادارکردنەوەی خەرجی پاشەکەوتکران", + "loadError": "ئاگادارکردنەوەکانی خەرجی بار نەکران.", + "saveError": "ئاگادارکردنەوەکانی خەرجی پاشەکەوت نەکران.", + "empty": "ئاگادارکردنەوەکانی خەرجی ناچالاکن. چالاکیان بکە بۆ ئاگادارکردنی پەیوەندییەکانی پارەدانی ئەم بووارە.", + "noSpend": "هێشتا هیچ خەرجییەک تۆمار نەکراوە", + "spend24h": "خەرجی لە 24 کاتژمێری ڕابردوودا", + "multiplierError": "لێکدەرێکی 1 یان زیاتر بنووسە", + "permissionDenied": "مۆڵەتی بەڕێوەبردنی ئاگادارکردنەوەکانی خەرجیت نییە.", + "pushOffByCategory": "لە ئاگادارکردنەوەکاندا ناچالاکە" } } diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 84a5bee2d8..315feb7ea1 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -859,7 +859,8 @@ "chat": "Zprávy v konverzaci", "kiloclaw": "Aktivita KiloClaw", "balance": "Upozornění na zůstatek", - "security": "Bezpečnostní zjištění" + "security": "Bezpečnostní zjištění", + "spend": "Upozornění na útratu" }, "category": { "chatMessagesSubtitle": "odpovědi ve vašich konverzacích", @@ -871,6 +872,8 @@ "sessionStatusSubtitle": "dokončeno / selhalo / připraveno k ovládání", "kiloclawActivitySubtitle": "instance připravena/selhala, naplánované akce", "balanceAlertsSubtitle": "upozornění na nízký zůstatek organizace", + "spendAlertsSubtitle": "překročení prahů a hodinové špičky", + "spendAlertsToggle": "Kategorie upozornění na útratu", "securityFindingsSubtitle": "nové nálezy a připomenutí lhůt SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Otevřít poslední relaci" + }, + "spendAlerts": { + "subtitle": "Pošli e-mailem nebo push oznámením upozornění fakturačním kontaktům, když útrata tohoto rozsahu překročí limit nebo vyskočí nad obvyklou hodinovou sazbu.", + "enable": "Povolit upozornění na útratu", + "thresholdTitle": "Překročení prahu", + "thresholdSubtitle": "Průběžná útrata překračuje tvůj limit", + "anomalyTitle": "Hodinová špička", + "anomalySubtitle": "Hodina je výrazně nad obvyklou sazbou tohoto rozsahu", + "limitLabel": "Limit (USD)", + "windowLabel": "Okno", + "window24h": "24 hodin", + "window7d": "7 dní", + "window30d": "30 dní", + "multiplierLabel": "Násobitel špičky", + "saved": "Nastavení upozornění na útratu bylo uloženo", + "loadError": "Upozornění na útratu se nepodařilo načíst.", + "saveError": "Upozornění na útratu se nepodařilo uložit.", + "empty": "Upozornění na útratu jsou vypnutá. Zapni je, abys upozornil fakturační kontakty tohoto rozsahu.", + "noSpend": "Zatím není zaznamenána žádná útrata", + "spend24h": "Útrata za posledních 24 hodin", + "multiplierError": "Zadej násobitel 1 nebo větší", + "permissionDenied": "Nemáš oprávnění spravovat upozornění na útratu.", + "pushOffByCategory": "Vypnuto v Oznámeních" } } diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index 06c0b67e29..d05437c106 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -891,7 +891,8 @@ "chat": "Negeseuon sgwrs", "kiloclaw": "Gweithgaredd KiloClaw", "balance": "Rhybuddion balans", - "security": "Canfyddiadau diogelwch" + "security": "Canfyddiadau diogelwch", + "spend": "Rhybuddion gwariant" }, "category": { "chatMessagesSubtitle": "atebion yn eich sgyrsiau", @@ -903,6 +904,8 @@ "sessionStatusSubtitle": "wedi gorffen / wedi methu / yn barod i'w reoli", "kiloclawActivitySubtitle": "enghraifft yn barod neu wedi methu, gweithredoedd wedi'u hamserlennu", "balanceAlertsSubtitle": "rhybuddion am falans isel y sefydliad", + "spendAlertsSubtitle": "croesi trothwyon a sbigau bob awr", + "spendAlertsToggle": "Categori rhybuddion gwariant", "securityFindingsSubtitle": "canfyddiadau newydd a nodiadau atgoffa SLA" }, "action": { @@ -3371,5 +3374,28 @@ }, "launcher": { "openLastSession": "Agor y sesiwn ddiwethaf" + }, + "spendAlerts": { + "subtitle": "Anfon rhybudd drwy e-bost neu wthiad at y cysylltiadau bilio pan fo gwariant y cwmpas hwn yn croesi terfyn neu'n codi uwchlaw ei gyfradd awr arferol.", + "enable": "Galluogi rhybuddion gwariant", + "thresholdTitle": "Croesi trothwy", + "thresholdSubtitle": "Mae gwariant crynodedig yn croesi dy derfyn", + "anomalyTitle": "Sbig awr", + "anomalySubtitle": "Mae awr ymhell uwchlaw cyfradd arferol y cwmpas hwn", + "limitLabel": "Terfyn (USD)", + "windowLabel": "Ffenestr", + "window24h": "24 awr", + "window7d": "7 diwrnod", + "window30d": "30 diwrnod", + "multiplierLabel": "Lluosydd y sbig", + "saved": "Wedi cadw gosodiadau rhybuddion gwariant", + "loadError": "Methwyd llwytho rhybuddion gwariant.", + "saveError": "Methwyd cadw rhybuddion gwariant.", + "empty": "Mae rhybuddion gwariant i ffwrdd. Tro nhw ymlaen i rybuddio cysylltiadau bilio'r cwmpas hwn.", + "noSpend": "Dim gwariant wedi'i gofnodi eto", + "spend24h": "Gwariant yn y 24 awr diwethaf", + "multiplierError": "Nodwch luosydd o 1 neu fwy", + "permissionDenied": "Nid oes gennyt ganiatâd i reoli rhybuddion gwariant.", + "pushOffByCategory": "I ffwrdd yn Hysbysiadau" } } diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index b69797eb45..8fa4c13963 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -827,7 +827,8 @@ "chat": "Chatbeskeder", "kiloclaw": "KiloClaw-aktivitet", "balance": "Advarsler om saldo", - "security": "Sikkerhedsfund" + "security": "Sikkerhedsfund", + "spend": "Forbrugsadvarsler" }, "category": { "chatMessagesSubtitle": "svar i dine samtaler", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "afsluttet / mislykket / klar til styring", "kiloclawActivitySubtitle": "instans klar eller mislykket, planlagte handlinger", "balanceAlertsSubtitle": "advarsler om lav saldo i organisationen", + "spendAlertsSubtitle": "grænseoverskridelser og spikes pr. time", + "spendAlertsToggle": "Kategorien forbrugsadvarsler", "securityFindingsSubtitle": "nye fund og SLA-påmindelser" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Åbn seneste session" + }, + "spendAlerts": { + "subtitle": "Send en advarsel via e-mail eller push til faktureringskontakterne, når dette områdes forbrug overskrider en grænse eller stiger over den sædvanlige timesats.", + "enable": "Aktivér forbrugsadvarsler", + "thresholdTitle": "Grænseoverskridelse", + "thresholdSubtitle": "Løbende forbrug overskrider din grænse", + "anomalyTitle": "Spike pr. time", + "anomalySubtitle": "En time er langt over dette områdes sædvanlige sats", + "limitLabel": "Grænse (USD)", + "windowLabel": "Vindue", + "window24h": "24 timer", + "window7d": "7 dage", + "window30d": "30 dage", + "multiplierLabel": "Spike-multiplikator", + "saved": "Indstillinger for forbrugsadvarsler er gemt", + "loadError": "Forbrugsadvarsler kunne ikke indlæses.", + "saveError": "Forbrugsadvarsler kunne ikke gemmes.", + "empty": "Forbrugsadvarsler er slået fra. Slå dem til for at advare dette områdes faktureringskontakter.", + "noSpend": "Der er endnu ikke registreret noget forbrug", + "spend24h": "Forbrug de seneste 24 timer", + "multiplierError": "Angiv en multiplikator på 1 eller mere", + "permissionDenied": "Du har ikke tilladelse til at administrere forbrugsadvarsler.", + "pushOffByCategory": "Fra i Notifikationer" } } diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 5bbb75042f..5c67fe7d8f 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -323,7 +323,8 @@ "chat": "Chat-Nachrichten", "kiloclaw": "KiloClaw-Aktivitäten", "balance": "Guthaben-Warnungen", - "security": "Sicherheitsbefunde" + "security": "Sicherheitsbefunde", + "spend": "Ausgaben-Warnungen" }, "category": { "chatMessagesSubtitle": "antworten in deinen Unterhaltungen", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "beendet / fehlgeschlagen / bereit zur Steuerung", "kiloclawActivitySubtitle": "instanz bereit/fehlgeschlagen, geplante Aktionen", "balanceAlertsSubtitle": "warnungen bei niedrigem Organisationsguthaben", + "spendAlertsSubtitle": "Schwellenwertüberschreitungen und stündliche Spitzen", + "spendAlertsToggle": "Kategorie „Ausgaben-Warnungen“", "securityFindingsSubtitle": "neue Sicherheitsbefunde und SLA-Erinnerungen" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Letzte Sitzung öffnen" + }, + "spendAlerts": { + "subtitle": "Sende den Abrechnungskontakten eine E-Mail- oder Push-Warnung, wenn die Ausgaben dieses Bereichs ein Limit überschreiten oder über ihre übliche Stundenrate steigen.", + "enable": "Ausgaben-Warnungen aktivieren", + "thresholdTitle": "Schwellenwertüberschreitung", + "thresholdSubtitle": "Laufende Ausgaben überschreiten dein Limit", + "anomalyTitle": "Stündliche Spitze", + "anomalySubtitle": "Eine Stunde liegt weit über der üblichen Rate dieses Bereichs", + "limitLabel": "Limit (USD)", + "windowLabel": "Zeitfenster", + "window24h": "24 Stunden", + "window7d": "7 Tage", + "window30d": "30 Tage", + "multiplierLabel": "Spitzen-Multiplikator", + "saved": "Einstellungen für Ausgaben-Warnungen gespeichert", + "loadError": "Ausgaben-Warnungen konnten nicht geladen werden.", + "saveError": "Ausgaben-Warnungen konnten nicht gespeichert werden.", + "empty": "Ausgaben-Warnungen sind aus. Schalte sie ein, um die Abrechnungskontakte dieses Bereichs zu warnen.", + "noSpend": "Noch keine Ausgaben erfasst", + "spend24h": "Ausgaben der letzten 24 Stunden", + "multiplierError": "Gib einen Multiplikator von 1 oder mehr ein", + "permissionDenied": "Du hast keine Berechtigung, Ausgaben-Warnungen zu verwalten.", + "pushOffByCategory": "In Benachrichtigungen aus" } } diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index 1a97fb6401..a4b39f1ba6 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -827,7 +827,8 @@ "chat": "Μηνύματα συνομιλίας", "kiloclaw": "Δραστηριότητα KiloClaw", "balance": "Ειδοποιήσεις υπολοίπου", - "security": "Ευρήματα ασφάλειας" + "security": "Ευρήματα ασφάλειας", + "spend": "Ειδοποιήσεις δαπανών" }, "category": { "chatMessagesSubtitle": "απαντήσεις στις συνομιλίες σου", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ολοκληρώθηκε / απέτυχε / έτοιμη για έλεγχο", "kiloclawActivitySubtitle": "περιβάλλον έτοιμο ή με αποτυχία, προγραμματισμένες ενέργειες", "balanceAlertsSubtitle": "προειδοποιήσεις χαμηλού υπολοίπου οργανισμού", + "spendAlertsSubtitle": "υπερβάσεις ορίων και ωριαίες κορυφώσεις", + "spendAlertsToggle": "Κατηγορία ειδοποιήσεων δαπανών", "securityFindingsSubtitle": "νέα ευρήματα και υπενθυμίσεις SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Άνοιγμα τελευταίας συνεδρίας" + }, + "spendAlerts": { + "subtitle": "Στείλε ειδοποίηση με email ή push στις επαφές χρέωσης όταν η δαπάνη αυτού του πεδίου ξεπερνά ένα όριο ή αυξάνεται πάνω από τη συνήθη ωριαία τιμή του.", + "enable": "Ενεργοποίηση ειδοποιήσεων δαπανών", + "thresholdTitle": "Υπέρβαση ορίου", + "thresholdSubtitle": "Η τρέχουσα δαπάνη ξεπερνά το όριό σου", + "anomalyTitle": "Ωριαία κορύφωση", + "anomalySubtitle": "Μια ώρα είναι πολύ πάνω από τη συνήθη τιμή αυτού του πεδίου", + "limitLabel": "Όριο (USD)", + "windowLabel": "Παράθυρο", + "window24h": "24 ώρες", + "window7d": "7 ημέρες", + "window30d": "30 ημέρες", + "multiplierLabel": "Πολλαπλασιαστής κορύφωσης", + "saved": "Οι ρυθμίσεις ειδοποιήσεων δαπανών αποθηκεύτηκαν", + "loadError": "Δεν ήταν δυνατή η φόρτωση των ειδοποιήσεων δαπανών.", + "saveError": "Δεν ήταν δυνατή η αποθήκευση των ειδοποιήσεων δαπανών.", + "empty": "Οι ειδοποιήσεις δαπανών είναι ανενεργές. Ενεργοποίησέ τις για να ειδοποιείς τις επαφές χρέωσης αυτού του πεδίου.", + "noSpend": "Δεν έχει καταγραφεί ακόμη καμία δαπάνη", + "spend24h": "Δαπάνη τις τελευταίες 24 ώρες", + "multiplierError": "Εισήγαγε πολλαπλασιαστή 1 ή μεγαλύτερο", + "permissionDenied": "Δεν έχεις δικαίωμα να διαχειρίζεσαι τις ειδοποιήσεις δαπανών.", + "pushOffByCategory": "Ανενεργό στις Ειδοποιήσεις" } } diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 2d5ca6de43..c23caf92ac 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -827,7 +827,8 @@ "chat": "Chat messages", "kiloclaw": "KiloClaw activity", "balance": "Balance alerts", - "security": "Security findings" + "security": "Security findings", + "spend": "Spend alerts" }, "category": { "chatMessagesSubtitle": "replies in your conversations", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "finished / failed / ready to control", "kiloclawActivitySubtitle": "instance ready/failed, scheduled actions", "balanceAlertsSubtitle": "low organization balance warnings", + "spendAlertsSubtitle": "threshold crossings and hourly spikes", + "spendAlertsToggle": "Spend alerts category", "securityFindingsSubtitle": "new findings and SLA reminders" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Open last session" + }, + "spendAlerts": { + "subtitle": "Email or push an alert to the billing contacts when this scope's spend crosses a limit or spikes above its usual hourly rate.", + "enable": "Enable spend alerts", + "thresholdTitle": "Threshold crossing", + "thresholdSubtitle": "Rolling spend crosses your limit", + "anomalyTitle": "Hourly spike", + "anomalySubtitle": "An hour is far above this scope's usual rate", + "limitLabel": "Limit (USD)", + "windowLabel": "Window", + "window24h": "24 hours", + "window7d": "7 days", + "window30d": "30 days", + "multiplierLabel": "Spike multiplier", + "saved": "Spend alert settings saved", + "loadError": "Couldn't load spend alerts.", + "saveError": "Couldn't save spend alerts.", + "empty": "Spend alerts are off. Turn them on to alert this scope's billing contacts.", + "noSpend": "No spend recorded yet", + "spend24h": "Spend in the last 24 hours", + "multiplierError": "Enter a multiplier of 1 or more", + "permissionDenied": "You don't have permission to manage spend alerts.", + "pushOffByCategory": "Off in Notifications" } } diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index f8e5177f5a..b29038ba0d 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -295,7 +295,8 @@ "chat": "Mensajes de chat", "kiloclaw": "Actividad de KiloClaw", "balance": "Alertas de saldo", - "security": "Hallazgos de seguridad" + "security": "Hallazgos de seguridad", + "spend": "Alertas de gasto" }, "category": { "chatMessagesSubtitle": "respuestas en tus conversaciones", @@ -307,6 +308,8 @@ "sessionStatusSubtitle": "terminada / fallida / lista para controlar", "kiloclawActivitySubtitle": "instancia lista/fallida, acciones programadas", "balanceAlertsSubtitle": "avisos de saldo bajo de la organización", + "spendAlertsSubtitle": "superaciones de umbrales y picos por hora", + "spendAlertsToggle": "Categoría de alertas de gasto", "securityFindingsSubtitle": "nuevos hallazgos y recordatorios de SLA" }, "liveActivities": "Actividades en directo", @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Abrir última sesión" + }, + "spendAlerts": { + "subtitle": "Envía un aviso por correo o notificación push a los contactos de facturación cuando el gasto de este ámbito supere un límite o se dispare por encima de su ritmo horario habitual.", + "enable": "Activar las alertas de gasto", + "thresholdTitle": "Superación del umbral", + "thresholdSubtitle": "El gasto acumulado supera tu límite", + "anomalyTitle": "Pico por hora", + "anomalySubtitle": "Una hora está muy por encima del ritmo habitual de este ámbito", + "limitLabel": "Límite (USD)", + "windowLabel": "Ventana", + "window24h": "24 horas", + "window7d": "7 días", + "window30d": "30 días", + "multiplierLabel": "Multiplicador del pico", + "saved": "Ajustes de alertas de gasto guardados", + "loadError": "No se pudieron cargar las alertas de gasto.", + "saveError": "No se pudieron guardar las alertas de gasto.", + "empty": "Las alertas de gasto están desactivadas. Actívalas para avisar a los contactos de facturación de este ámbito.", + "noSpend": "Aún no se ha registrado ningún gasto", + "spend24h": "Gasto en las últimas 24 horas", + "multiplierError": "Introduce un multiplicador de 1 o más", + "permissionDenied": "No tienes permiso para gestionar las alertas de gasto.", + "pushOffByCategory": "Desactivado en Notificaciones" } } diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index 434e82516d..8a2a7090da 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -827,7 +827,8 @@ "chat": "Vestlussõnumid", "kiloclaw": "KiloClaw'i tegevus", "balance": "Saldohoiatused", - "security": "Turvaleiud" + "security": "Turvaleiud", + "spend": "Kulutushoiatused" }, "category": { "chatMessagesSubtitle": "vastused sinu vestlustes", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "lõppenud / ebaõnnestunud / juhtimiseks valmis", "kiloclawActivitySubtitle": "eksemplar valmis/ebaõnnestunud, ajastatud toimingud", "balanceAlertsSubtitle": "organisatsiooni madala saldo hoiatused", + "spendAlertsSubtitle": "lävendi ületamised ja tunnipõhised tõusud", + "spendAlertsToggle": "Kulutushoiatuste kategooria", "securityFindingsSubtitle": "uued leiud ja SLA meeldetuletused" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Ava viimane seanss" + }, + "spendAlerts": { + "subtitle": "Saada faktureerimiskontaktidele e-posti või push-teavitus, kui selle piirkonna kulutus ületab limiidi või tõuseb üle tavapärase tunnimäära.", + "enable": "Luba kulutushoiatused", + "thresholdTitle": "Lävendi ületamine", + "thresholdSubtitle": "Jooksev kulutus ületab sinu limiidi", + "anomalyTitle": "Tunnipõhine tõus", + "anomalySubtitle": "Üks tund on selle piirkonna tavapärasest määrast tunduvalt kõrgem", + "limitLabel": "Limiit (USD)", + "windowLabel": "Aken", + "window24h": "24 tundi", + "window7d": "7 päeva", + "window30d": "30 päeva", + "multiplierLabel": "Tõusu kordaja", + "saved": "Kulutushoiatuste seaded on salvestatud", + "loadError": "Kulutushoiatuste laadimine ebaõnnestus.", + "saveError": "Kulutushoiatuste salvestamine ebaõnnestus.", + "empty": "Kulutushoiatused on välja lülitatud. Lülita need sisse, et hoiatada selle piirkonna faktureerimiskontakte.", + "noSpend": "Kulutusi pole veel registreeritud", + "spend24h": "Viimase 24 tunni kulutus", + "multiplierError": "Sisesta kordaja 1 või suurem", + "permissionDenied": "Sul puudub õigus kulutushoiatusi hallata.", + "pushOffByCategory": "Teavitustes väljas" } } diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 2f11e53d85..02cad73415 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -827,7 +827,8 @@ "chat": "Txat-mezuak", "kiloclaw": "KiloClaw-en jarduera", "balance": "Saldoari buruzko alertak", - "security": "Antzemandako segurtasun-arazoak" + "security": "Antzemandako segurtasun-arazoak", + "spend": "Gastu-alertak" }, "category": { "chatMessagesSubtitle": "zure elkarrizketetako erantzunak", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "amaituta / huts eginda / kontrolatzeko prest", "kiloclawActivitySubtitle": "instantzia prest edo huts eginda, programatutako ekintzak", "balanceAlertsSubtitle": "erakundearen saldo baxuaren abisuak", + "spendAlertsSubtitle": "atalaseen gainditzeak eta orduko gorakadak", + "spendAlertsToggle": "Gastu-alerten kategoria", "securityFindingsSubtitle": "aurkikuntza berriak eta SLAren gogorarazpenak" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Ireki azken saioa" + }, + "spendAlerts": { + "subtitle": "Bidali alerta bat posta elektronikoz edo push bidez fakturatze-kontaktuei, eremu honen gastuak muga bat gainditzen duenean edo bere ohiko ordu-tasaren gainetik igotzen denean.", + "enable": "Gaitu gastu-alertak", + "thresholdTitle": "Atalasaren gainditzea", + "thresholdSubtitle": "Metatutako gastuak zure muga gainditzen du", + "anomalyTitle": "Orduko gorakada", + "anomalySubtitle": "Ordu bat eremu honen ohiko tasatik oso gainetik dago", + "limitLabel": "Muga (USD)", + "windowLabel": "Leihoa", + "window24h": "24 ordu", + "window7d": "7 egun", + "window30d": "30 egun", + "multiplierLabel": "Gorakadaren biderkatzailea", + "saved": "Gastu-alerten ezarpenak gorde dira", + "loadError": "Ezin izan dira gastu-alertak kargatu.", + "saveError": "Ezin izan dira gastu-alertak gorde.", + "empty": "Gastu-alertak itzalita daude. Piztu itzazu eremu honetako fakturatze-kontaktuei abisatzeko.", + "noSpend": "Oraindik ez da gasturik erregistratu", + "spend24h": "Azken 24 orduetako gastua", + "multiplierError": "Sartu 1 edo handiagoa den biderkatzaile bat", + "permissionDenied": "Ez duzu gastu-alertak kudeatzeko baimenik.", + "pushOffByCategory": "Jakinarazpenetan itzalita" } } diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index d50d849373..3d8e9931d1 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -827,7 +827,8 @@ "chat": "پیام‌های گفت‌وگو", "kiloclaw": "فعالیت KiloClaw", "balance": "هشدارهای موجودی", - "security": "یافته‌های امنیتی" + "security": "یافته‌های امنیتی", + "spend": "هشدارهای هزینه" }, "category": { "chatMessagesSubtitle": "پاسخ‌های گفت‌وگوهای شما", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "تمام‌شده / ناموفق / آمادهٔ کنترل", "kiloclawActivitySubtitle": "نمونه آماده/ناموفق، اقدامات زمان‌بندی‌شده", "balanceAlertsSubtitle": "هشدارهای موجودی کم سازمان", + "spendAlertsSubtitle": "عبور از آستانه‌ها و جهش‌های ساعتی", + "spendAlertsToggle": "دستهٔ هشدارهای هزینه", "securityFindingsSubtitle": "یافته‌های جدید و یادآوری‌های SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "باز کردن آخرین نشست" + }, + "spendAlerts": { + "subtitle": "وقتی هزینهٔ این حوزه از حدی فراتر می‌رود یا از نرخ ساعتی معمول آن بالاتر می‌جهد، هشداری را با ایمیل یا نوتیفیکیشن برای مخاطبان صورتحساب بفرست.", + "enable": "فعال‌سازی هشدارهای هزینه", + "thresholdTitle": "عبور از آستانه", + "thresholdSubtitle": "هزینهٔ جاری از حد شما فراتر می‌رود", + "anomalyTitle": "جهش ساعتی", + "anomalySubtitle": "یک ساعت بسیار بالاتر از نرخ معمول این حوزه است", + "limitLabel": "حد (USD)", + "windowLabel": "بازهٔ زمانی", + "window24h": "۲۴ ساعت", + "window7d": "۷ روز", + "window30d": "۳۰ روز", + "multiplierLabel": "ضریب جهش", + "saved": "تنظیمات هشدار هزینه ذخیره شد", + "loadError": "هشدارهای هزینه بارگذاری نشد.", + "saveError": "هشدارهای هزینه ذخیره نشد.", + "empty": "هشدارهای هزینه خاموش است. برای هشدار دادن به مخاطبان صورتحساب این حوزه آن‌ها را روشن کن.", + "noSpend": "هنوز هیچ هزینه‌ای ثبت نشده است", + "spend24h": "هزینه در ۲۴ ساعت گذشته", + "multiplierError": "ضریبی برابر ۱ یا بیشتر وارد کن", + "permissionDenied": "شما اجازهٔ مدیریت هشدارهای هزینه را ندارید.", + "pushOffByCategory": "در اعلان‌ها خاموش" } } diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index e2bd041078..912c42b4a4 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -827,7 +827,8 @@ "chat": "Keskusteluviestit", "kiloclaw": "KiloClaw-tapahtumat", "balance": "Saldoilmoitukset", - "security": "Tietoturvahavainnot" + "security": "Tietoturvahavainnot", + "spend": "Kulutusvaroitukset" }, "category": { "chatMessagesSubtitle": "vastaukset keskusteluissasi", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "valmis / epäonnistui / valmis hallittavaksi", "kiloclawActivitySubtitle": "instanssi valmis/epäonnistui, ajoitetut toiminnot", "balanceAlertsSubtitle": "varoitukset organisaation alhaisesta saldosta", + "spendAlertsSubtitle": "raja-arvojen ylitykset ja tunneittaiset piikit", + "spendAlertsToggle": "Kulutusvaroitusten luokka", "securityFindingsSubtitle": "uudet löydökset ja SLA-muistutukset" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Avaa viimeisin istunto" + }, + "spendAlerts": { + "subtitle": "Lähetä laskutusyhteyshenkilöille sähköposti- tai push-varoitus, kun tämän alueen kulutus ylittää rajan tai nousee tavanomaisen tuntitahdin yläpuolelle.", + "enable": "Ota kulutusvaroitukset käyttöön", + "thresholdTitle": "Rajan ylitys", + "thresholdSubtitle": "Juokseva kulutus ylittää rajasi", + "anomalyTitle": "Tunneittainen piikki", + "anomalySubtitle": "Yksi tunti on selvästi tämän alueen tavanomaista tahtia korkeampi", + "limitLabel": "Raja (USD)", + "windowLabel": "Aikaikkuna", + "window24h": "24 tuntia", + "window7d": "7 päivää", + "window30d": "30 päivää", + "multiplierLabel": "Piikin kerroin", + "saved": "Kulutusvaroitusten asetukset tallennettu", + "loadError": "Kulutusvaroituksia ei voitu ladata.", + "saveError": "Kulutusvaroituksia ei voitu tallentaa.", + "empty": "Kulutusvaroitukset ovat pois päältä. Ota ne käyttöön, jotta voit varoittaa tämän alueen laskutusyhteyshenkilöitä.", + "noSpend": "Kulutusta ei ole vielä kirjattu", + "spend24h": "Kulutus viimeisten 24 tunnin aikana", + "multiplierError": "Anna kerroin, joka on 1 tai suurempi", + "permissionDenied": "Sinulla ei ole oikeutta hallita kulutusvaroituksia.", + "pushOffByCategory": "Pois päältä Ilmoituksissa" } } diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 250448c209..e74e9028e3 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -827,7 +827,8 @@ "chat": "Mga mensahe sa usapan", "kiloclaw": "Aktibidad ng KiloClaw", "balance": "Mga babala sa balanse", - "security": "Mga natukoy na isyu sa seguridad" + "security": "Mga natukoy na isyu sa seguridad", + "spend": "Mga babala sa gastos" }, "category": { "chatMessagesSubtitle": "mga tugon sa iyong mga pag-uusap", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "tapos / nabigo / handa nang kontrolin", "kiloclawActivitySubtitle": "handa o nabigo ang instance, mga nakaiskedyul na aksyon", "balanceAlertsSubtitle": "mga babala sa mababang balanse ng organisasyon", + "spendAlertsSubtitle": "mga paglampas sa hangganan at pagtaas bawat oras", + "spendAlertsToggle": "Kategorya ng mga babala sa gastos", "securityFindingsSubtitle": "mga bagong natuklasan at paalala sa SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Buksan ang huling sesyon" + }, + "spendAlerts": { + "subtitle": "Magpadala ng babala sa email o push sa mga contact sa pagsingil kapag ang gastos ng saklaw na ito ay lumampas sa limitasyon o tumaas nang higit sa karaniwang rate bawat oras.", + "enable": "Paganahin ang mga babala sa gastos", + "thresholdTitle": "Paglampas sa hangganan", + "thresholdSubtitle": "Ang kasalukuyang gastos ay lumampas sa iyong limitasyon", + "anomalyTitle": "Pagtaas bawat oras", + "anomalySubtitle": "Ang isang oras ay malayo sa itaas ng karaniwang rate ng saklaw na ito", + "limitLabel": "Limitasyon (USD)", + "windowLabel": "Panahon", + "window24h": "24 na oras", + "window7d": "7 araw", + "window30d": "30 araw", + "multiplierLabel": "Multiplier ng pagtaas", + "saved": "Nai-save ang mga setting ng babala sa gastos", + "loadError": "Hindi ma-load ang mga babala sa gastos.", + "saveError": "Hindi mai-save ang mga babala sa gastos.", + "empty": "Naka-off ang mga babala sa gastos. I-on ang mga ito para abisuhan ang mga contact sa pagsingil ng saklaw na ito.", + "noSpend": "Wala pang naitalang gastos", + "spend24h": "Gastos sa nakalipas na 24 na oras", + "multiplierError": "Maglagay ng multiplier na 1 o higit pa", + "permissionDenied": "Wala kang pahintulot na pamahalaan ang mga babala sa gastos.", + "pushOffByCategory": "Naka-off sa Mga abiso" } } diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 4dee1a2751..2459906307 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -259,7 +259,8 @@ "chat": "Messages de discussion", "kiloclaw": "Activité KiloClaw", "balance": "Alertes sur le solde", - "security": "Problèmes de sécurité" + "security": "Problèmes de sécurité", + "spend": "Alertes de dépenses" }, "category": { "chatMessagesSubtitle": "réponses dans vos conversations", @@ -271,6 +272,8 @@ "sessionStatusSubtitle": "terminée / en échec / prête à être contrôlée", "kiloclawActivitySubtitle": "instance prête ou en échec, actions planifiées", "balanceAlertsSubtitle": "alertes de solde faible pour l'organisation", + "spendAlertsSubtitle": "franchissements de seuils et pics horaires", + "spendAlertsToggle": "Catégorie « Alertes de dépenses »", "securityFindingsSubtitle": "nouvelles vulnérabilités et rappels SLA" }, "liveActivities": "Activités en direct", @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Ouvrir la dernière session" + }, + "spendAlerts": { + "subtitle": "Envoyez une alerte par e-mail ou notification push aux contacts de facturation lorsque les dépenses de ce périmètre dépassent une limite ou s'envolent au-dessus de leur rythme horaire habituel.", + "enable": "Activer les alertes de dépenses", + "thresholdTitle": "Franchissement du seuil", + "thresholdSubtitle": "Les dépenses cumulées dépassent votre limite", + "anomalyTitle": "Pic horaire", + "anomalySubtitle": "Une heure est bien au-dessus du rythme habituel de ce périmètre", + "limitLabel": "Limite (USD)", + "windowLabel": "Fenêtre", + "window24h": "24 heures", + "window7d": "7 jours", + "window30d": "30 jours", + "multiplierLabel": "Multiplicateur du pic", + "saved": "Paramètres des alertes de dépenses enregistrés", + "loadError": "Impossible de charger les alertes de dépenses.", + "saveError": "Impossible d'enregistrer les alertes de dépenses.", + "empty": "Les alertes de dépenses sont désactivées. Activez-les pour prévenir les contacts de facturation de ce périmètre.", + "noSpend": "Aucune dépense enregistrée pour l'instant", + "spend24h": "Dépenses des dernières 24 heures", + "multiplierError": "Saisissez un multiplicateur de 1 ou plus", + "permissionDenied": "Vous n'avez pas l'autorisation de gérer les alertes de dépenses.", + "pushOffByCategory": "Désactivé dans les notifications" } } diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 942f3b1aa8..06e2be1904 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -875,7 +875,8 @@ "chat": "Teachtaireachtaí comhrá", "kiloclaw": "Gníomhaíocht KiloClaw", "balance": "Foláirimh iarmhéid", - "security": "Torthaí slándála" + "security": "Torthaí slándála", + "spend": "Foláirimh chaiteachais" }, "category": { "chatMessagesSubtitle": "freagraí i do chomhráite", @@ -887,6 +888,8 @@ "sessionStatusSubtitle": "críochnaithe / teipthe / réidh le rialú", "kiloclawActivitySubtitle": "ásc réidh nó teipthe, gníomhartha sceidealaithe", "balanceAlertsSubtitle": "rabhaidh faoi iarmhéid íseal na heagraíochta", + "spendAlertsSubtitle": "sáruithe tairsí agus spuaiceanna uaire", + "spendAlertsToggle": "Catagóir foláirimh chaiteachais", "securityFindingsSubtitle": "torthaí nua agus meabhrúcháin SLA" }, "action": { @@ -3328,5 +3331,28 @@ }, "launcher": { "openLastSession": "Oscail an seisiún deireanach" + }, + "spendAlerts": { + "subtitle": "Seol foláireamh trí ríomhphost nó trí push chuig na teagmhálaithe billeála nuair a sháraíonn caiteachas an raoin seo teorainn nó nuair a ardaíonn sé os cionn a ghnáthráta uaire.", + "enable": "Cumasaigh foláirimh chaiteachais", + "thresholdTitle": "Sárú tairsí", + "thresholdSubtitle": "Sáraíonn caiteachas reatha do theorainn", + "anomalyTitle": "Spuaic uaire", + "anomalySubtitle": "Tá uair an chloig i bhfad os cionn gnáthráta an raoin seo", + "limitLabel": "Teorainn (USD)", + "windowLabel": "Fuinneog", + "window24h": "24 uair an chloig", + "window7d": "7 lá", + "window30d": "30 lá", + "multiplierLabel": "Iolraitheoir na spuaice", + "saved": "Sábháladh socruithe foláirimh chaiteachais", + "loadError": "Níorbh fhéidir foláirimh chaiteachais a lódáil.", + "saveError": "Níorbh fhéidir foláirimh chaiteachais a shábháil.", + "empty": "Tá foláirimh chaiteachais as. Cuir ar siúl iad chun teagmhálaithe billeála an raoin seo a chur ar an eolas.", + "noSpend": "Níor taifeadadh aon chaiteachas fós", + "spend24h": "Caiteachas sna 24 uair an chloig seo caite", + "multiplierError": "Iontráil iolraitheoir 1 nó níos mó", + "permissionDenied": "Níl cead agat foláirimh chaiteachais a bhainistiú.", + "pushOffByCategory": "As i bhFógraí" } } diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index cedb432901..767224258b 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -827,7 +827,8 @@ "chat": "Mensaxes de conversa", "kiloclaw": "Actividade de KiloClaw", "balance": "Alertas de saldo", - "security": "Problemas de seguridade" + "security": "Problemas de seguridade", + "spend": "Alertas de gasto" }, "category": { "chatMessagesSubtitle": "respostas nas túas conversas", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "rematada / con erros / lista para controlar", "kiloclawActivitySubtitle": "instancia lista ou con erros, accións programadas", "balanceAlertsSubtitle": "avisos de saldo baixo da organización", + "spendAlertsSubtitle": "superacións de limiares e picos por hora", + "spendAlertsToggle": "Categoría de alertas de gasto", "securityFindingsSubtitle": "novos problemas detectados e recordatorios de SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Abrir a última sesión" + }, + "spendAlerts": { + "subtitle": "Envía unha alerta por correo ou notificación push aos contactos de facturación cando o gasto deste ámbito supere un límite ou se dispare por riba do seu ritmo horario habitual.", + "enable": "Activar as alertas de gasto", + "thresholdTitle": "Superación do limiar", + "thresholdSubtitle": "O gasto acumulado supera o teu límite", + "anomalyTitle": "Pico por hora", + "anomalySubtitle": "Unha hora está moi por riba do ritmo habitual deste ámbito", + "limitLabel": "Límite (USD)", + "windowLabel": "Ventá", + "window24h": "24 horas", + "window7d": "7 días", + "window30d": "30 días", + "multiplierLabel": "Multiplicador do pico", + "saved": "Gardáronse os axustes das alertas de gasto", + "loadError": "Non se puideron cargar as alertas de gasto.", + "saveError": "Non se puideron gardar as alertas de gasto.", + "empty": "As alertas de gasto están desactivadas. Actívaas para avisar os contactos de facturación deste ámbito.", + "noSpend": "Aínda non se rexistrou ningún gasto", + "spend24h": "Gasto nas últimas 24 horas", + "multiplierError": "Introduce un multiplicador de 1 ou máis", + "permissionDenied": "Non tes permiso para xestionar as alertas de gasto.", + "pushOffByCategory": "Desactivado en Notificacións" } } diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 19214d8501..9437213dd8 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -827,7 +827,8 @@ "chat": "ચેટના સંદેશા", "kiloclaw": "KiloClawની પ્રવૃત્તિ", "balance": "બેલેન્સ અંગેની ચેતવણીઓ", - "security": "સુરક્ષા તપાસનાં તારણો" + "security": "સુરક્ષા તપાસનાં તારણો", + "spend": "ખર્ચની ચેતવણીઓ" }, "category": { "chatMessagesSubtitle": "તમારી વાતચીતમાં જવાબો", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "પૂર્ણ / નિષ્ફળ / નિયંત્રિત કરવા તૈયાર", "kiloclawActivitySubtitle": "ઇન્સ્ટન્સ તૈયાર/નિષ્ફળ, નિર્ધારિત ક્રિયાઓ", "balanceAlertsSubtitle": "સંસ્થાનું બેલેન્સ ઓછું થવાની ચેતવણીઓ", + "spendAlertsSubtitle": "મર્યાદા ઓળંગવી અને કલાકદીઠ ઉછાળા", + "spendAlertsToggle": "ખર્ચની ચેતવણીની શ્રેણી", "securityFindingsSubtitle": "નવા તારણો અને SLA રીમાઇન્ડર" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "છેલ્લું સત્ર ખોલો" + }, + "spendAlerts": { + "subtitle": "આ સ્કોપનો ખર્ચ મર્યાદા ઓળંગે ત્યારે અથવા તેના સામાન્ય કલાકદીઠ દરથી ઉપર ઉછાળે ત્યારે બિલિંગ સંપર્કોને ઇમેઇલ અથવા પુશ દ્વારા ચેતવણી મોકલો.", + "enable": "ખર્ચની ચેતવણીઓ ચાલુ કરો", + "thresholdTitle": "મર્યાદા ઓળંગવી", + "thresholdSubtitle": "ચાલુ ખર્ચ તમારી મર્યાદા ઓળંગે છે", + "anomalyTitle": "કલાકદીઠ ઉછાળો", + "anomalySubtitle": "એક કલાક આ સ્કોપના સામાન્ય દરથી ઘણો ઊંચો છે", + "limitLabel": "મર્યાદા (USD)", + "windowLabel": "સમયગાળો", + "window24h": "24 કલાક", + "window7d": "7 દિવસ", + "window30d": "30 દિવસ", + "multiplierLabel": "ઉછાળ ગુણક", + "saved": "ખર્ચની ચેતવણીની સેટિંગ્સ સાચવી", + "loadError": "ખર્ચની ચેતવણીઓ લોડ કરી શકાઈ નથી.", + "saveError": "ખર્ચની ચેતવણીઓ સાચવી શકાઈ નથી.", + "empty": "ખર્ચની ચેતવણીઓ બંધ છે. આ સ્કોપના બિલિંગ સંપર્કોને ચેતવવા માટે તેમને ચાલુ કરો.", + "noSpend": "હજી કોઈ ખર્ચ નોંધાયો નથી", + "spend24h": "છેલ્લા 24 કલાકનો ખર્ચ", + "multiplierError": "1 કે તેથી વધુ ગુણક દાખલ કરો", + "permissionDenied": "તમને ખર્ચની ચેતવણીઓનું સંચાલન કરવાની પરવાનગી નથી.", + "pushOffByCategory": "સૂચનાઓમાં બંધ" } } diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 58a10664f6..13b3ca71e4 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -827,7 +827,8 @@ "chat": "Saƙonnin tattaunawa", "kiloclaw": "Ayyukan KiloClaw", "balance": "Faɗakarwar ragowar kuɗi", - "security": "Sakamakon binciken tsaro" + "security": "Sakamakon binciken tsaro", + "spend": "Faɗakarwar kashe kuɗi" }, "category": { "chatMessagesSubtitle": "amsoshi a cikin tattaunawarka", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "an gama / an kasa / a shirye don sarrafawa", "kiloclawActivitySubtitle": "kwafin shirin ya shirya ko ya gaza, ayyukan da aka tsara", "balanceAlertsSubtitle": "gargaɗin ƙarancin kuɗin ƙungiya", + "spendAlertsSubtitle": "wucewar iyaka da hauhawar sa'a", + "spendAlertsToggle": "Rukunin faɗakarwar kashe kuɗi", "securityFindingsSubtitle": "sabbin sakamakon bincike da tunatarwar SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Buɗe zaman ƙarshe" + }, + "spendAlerts": { + "subtitle": "Aika faɗakarwa ta imel ko turawa zuwa ga abokan hulɗar biyan kuɗi lokacin da kashe kuɗin wannan yanki ya wuce iyaka ko ya haura fiye da yawan sa'ar da ya saba.", + "enable": "Kunna faɗakarwar kashe kuɗi", + "thresholdTitle": "Wucewar iyaka", + "thresholdSubtitle": "Kashe kuɗin da ake yi ya wuce iyakarka", + "anomalyTitle": "Hauhawar sa'a", + "anomalySubtitle": "Sa'a ɗaya ta fi yawan da wannan yanki ya saba nisa", + "limitLabel": "Iyaka (USD)", + "windowLabel": "Taga", + "window24h": "Sa'o'i 24", + "window7d": "Kwanaki 7", + "window30d": "Kwanaki 30", + "multiplierLabel": "Mai ninka hauhawa", + "saved": "An adana saitunan faɗakarwar kashe kuɗi", + "loadError": "An kasa loda faɗakarwar kashe kuɗi.", + "saveError": "An kasa adana faɗakarwar kashe kuɗi.", + "empty": "Faɗakarwar kashe kuɗi a kashe take. Kunna ta domin faɗakar da abokan hulɗar biyan kuɗi na wannan yanki.", + "noSpend": "Ba a rubuta wani kashe kuɗi ba tukuna", + "spend24h": "Kashe kuɗi a cikin sa'o'i 24 da suka gabata", + "multiplierError": "Shigar da mai ninka 1 ko fiye", + "permissionDenied": "Ba ka da izinin sarrafa faɗakarwar kashe kuɗi.", + "pushOffByCategory": "A kashe a cikin Sanarwa" } } diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index b99202f384..3b72327e38 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -323,7 +323,8 @@ "chat": "הודעות צ׳אט", "kiloclaw": "פעילות KiloClaw", "balance": "התראות על היתרה", - "security": "ממצאי אבטחה" + "security": "ממצאי אבטחה", + "spend": "התראות הוצאות" }, "category": { "chatMessagesSubtitle": "תשובות בשיחות שלך", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "הסתיים / נכשל / מוכן לשליטה", "kiloclawActivitySubtitle": "מופע מוכן או שנכשל, פעולות מתוזמנות", "balanceAlertsSubtitle": "אזהרות על יתרה נמוכה בארגון", + "spendAlertsSubtitle": "חציית ספים וזינוקים שעתיים", + "spendAlertsToggle": "קטגוריית התראות הוצאות", "securityFindingsSubtitle": "ממצאים חדשים ותזכורות SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "פתיחת הסשן האחרון" + }, + "spendAlerts": { + "subtitle": "שלח התראה בדוא״ל או בהתראת push לאנשי הקשר לחיוב כאשר ההוצאות של תחום זה חוצות מגבלה או מזנקות מעל הקצב השעתי הרגיל.", + "enable": "הפעל התראות הוצאות", + "thresholdTitle": "חציית סף", + "thresholdSubtitle": "ההוצאה המצטברת חוצה את המגבלה שלך", + "anomalyTitle": "זינוק שעתי", + "anomalySubtitle": "שעה אחת גבוהה בהרבה מהקצב הרגיל של תחום זה", + "limitLabel": "מגבלה (USD)", + "windowLabel": "חלון", + "window24h": "24 שעות", + "window7d": "7 ימים", + "window30d": "30 ימים", + "multiplierLabel": "מכפיל הזינוק", + "saved": "הגדרות התראות ההוצאות נשמרו", + "loadError": "לא ניתן לטעון את התראות ההוצאות.", + "saveError": "לא ניתן לשמור את התראות ההוצאות.", + "empty": "התראות ההוצאות כבויות. הפעל אותן כדי ליידע את אנשי הקשר לחיוב של תחום זה.", + "noSpend": "עדיין לא נרשמה הוצאה", + "spend24h": "הוצאה ב־24 השעות האחרונות", + "multiplierError": "הזן מכפיל של 1 או יותר", + "permissionDenied": "אין לך הרשאה לנהל התראות הוצאות.", + "pushOffByCategory": "כבוי בהתראות" } } diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 7102693bd4..dc6a4e1284 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -323,7 +323,8 @@ "chat": "चैट के संदेश", "kiloclaw": "KiloClaw की गतिविधि", "balance": "बैलेंस की चेतावनियाँ", - "security": "सुरक्षा संबंधी समस्याएँ" + "security": "सुरक्षा संबंधी समस्याएँ", + "spend": "खर्च की चेतावनियाँ" }, "category": { "chatMessagesSubtitle": "आपकी बातचीत में उत्तर", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "समाप्त / विफल / नियंत्रण के लिए तैयार", "kiloclawActivitySubtitle": "इंस्टेंस तैयार/विफल, तय समय पर होने वाले कार्य", "balanceAlertsSubtitle": "संगठन का बैलेंस कम होने की चेतावनियाँ", + "spendAlertsSubtitle": "सीमा पार होना और हर घंटे की बढ़त", + "spendAlertsToggle": "खर्च की चेतावनी श्रेणी", "securityFindingsSubtitle": "नई सुरक्षा समस्याएँ और SLA के रिमाइंडर" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "पिछला सत्र खोलें" + }, + "spendAlerts": { + "subtitle": "जब इस स्कोप का खर्च किसी सीमा को पार करे या अपनी सामान्य प्रति घंटा दर से ऊपर बढ़े, तो बिलिंग संपर्कों को ईमेल या पुश द्वारा चेतावनी भेजें।", + "enable": "खर्च की चेतावनियाँ चालू करें", + "thresholdTitle": "सीमा पार होना", + "thresholdSubtitle": "चालू खर्च आपकी सीमा पार कर रहा है", + "anomalyTitle": "प्रति घंटा बढ़त", + "anomalySubtitle": "एक घंटा इस स्कोप की सामान्य दर से कहीं अधिक है", + "limitLabel": "सीमा (USD)", + "windowLabel": "अवधि", + "window24h": "24 घंटे", + "window7d": "7 दिन", + "window30d": "30 दिन", + "multiplierLabel": "बढ़त गुणक", + "saved": "खर्च की चेतावनी की सेटिंग्स सहेजी गईं", + "loadError": "खर्च की चेतावनियाँ लोड नहीं हो सकीं।", + "saveError": "खर्च की चेतावनियाँ सहेजी नहीं जा सकीं।", + "empty": "खर्च की चेतावनियाँ बंद हैं। इस स्कोप के बिलिंग संपर्कों को सचेत करने के लिए इन्हें चालू करें।", + "noSpend": "अभी तक कोई खर्च दर्ज नहीं हुआ", + "spend24h": "पिछले 24 घंटों का खर्च", + "multiplierError": "1 या उससे अधिक का गुणक दर्ज करें", + "permissionDenied": "आपको खर्च की चेतावनियाँ प्रबंधित करने की अनुमति नहीं है।", + "pushOffByCategory": "सूचनाओं में बंद" } } diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 152b4c61a5..8011d48cc3 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -843,7 +843,8 @@ "chat": "Poruke u razgovoru", "kiloclaw": "Aktivnost u KiloClawu", "balance": "Upozorenja o stanju računa", - "security": "Sigurnosni nalazi" + "security": "Sigurnosni nalazi", + "spend": "Upozorenja o potrošnji" }, "category": { "chatMessagesSubtitle": "odgovori u tvojim razgovorima", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "dovršeno / neuspješno / spremno za upravljanje", "kiloclawActivitySubtitle": "spremnost ili pogreška instance, zakazane radnje", "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi po satu", + "spendAlertsToggle": "Kategorija upozorenja o potrošnji", "securityFindingsSubtitle": "novi nalazi i podsjetnici na SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Otvori posljednju sesiju" + }, + "spendAlerts": { + "subtitle": "Pošalji upozorenje e-poštom ili push porukom kontaktima za naplatu kada potrošnja ovog opsega prijeđe limit ili skoči iznad uobičajene satne stope.", + "enable": "Omogući upozorenja o potrošnji", + "thresholdTitle": "Prekoračenje praga", + "thresholdSubtitle": "Tekuća potrošnja prelazi tvoj limit", + "anomalyTitle": "Satni skok", + "anomalySubtitle": "Jedan sat je daleko iznad uobičajene stope ovog opsega", + "limitLabel": "Limit (USD)", + "windowLabel": "Prozor", + "window24h": "24 sata", + "window7d": "7 dana", + "window30d": "30 dana", + "multiplierLabel": "Množitelj skoka", + "saved": "Postavke upozorenja o potrošnji su spremljene", + "loadError": "Upozorenja o potrošnji nisu učitana.", + "saveError": "Upozorenja o potrošnji nisu spremljena.", + "empty": "Upozorenja o potrošnji su isključena. Uključi ih da upozoriš kontakte za naplatu ovog opsega.", + "noSpend": "Još nema zabilježene potrošnje", + "spend24h": "Potrošnja u posljednja 24 sata", + "multiplierError": "Unesi množitelj 1 ili veći", + "permissionDenied": "Nemaš dopuštenje za upravljanje upozorenjima o potrošnji.", + "pushOffByCategory": "Isključeno u Obavijestima" } } diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index cde2a0e32d..83406796c9 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -827,7 +827,8 @@ "chat": "Mesaj diskisyon", "kiloclaw": "Aktivite KiloClaw", "balance": "Alèt balans", - "security": "Pwoblèm sekirite detekte" + "security": "Pwoblèm sekirite detekte", + "spend": "Alèt depans" }, "category": { "chatMessagesSubtitle": "repons nan konvèsasyon ou yo", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "fini / echwe / pare pou kontwole", "kiloclawActivitySubtitle": "enstans pare/echwe, aksyon pwograme", "balanceAlertsSubtitle": "avètisman lè balans òganizasyon an ba", + "spendAlertsSubtitle": "depasman papòt ak ogmantasyon chak èdtan", + "spendAlertsToggle": "Kategori alèt depans", "securityFindingsSubtitle": "nouvo rezilta ak rapèl SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Louvri dènye sesyon an" + }, + "spendAlerts": { + "subtitle": "Voye yon alèt pa imèl oswa push bay kontak fakti yo lè depans sektè sa a depase yon limit oswa monte pi wo pase vitès èdtan abityèl li.", + "enable": "Aktive alèt depans yo", + "thresholdTitle": "Depasman papòt", + "thresholdSubtitle": "Depans aktyèl la depase limit ou", + "anomalyTitle": "Ogmantasyon chak èdtan", + "anomalySubtitle": "Yon èdtan byen pi wo pase vitès abityèl sektè sa a", + "limitLabel": "Limit (USD)", + "windowLabel": "Fenèt", + "window24h": "24 èdtan", + "window7d": "7 jou", + "window30d": "30 jou", + "multiplierLabel": "Miltiplikatè ogmantasyon", + "saved": "Paramèt alèt depans yo anrejistre", + "loadError": "Pa t kapab chaje alèt depans yo.", + "saveError": "Pa t kapab anrejistre alèt depans yo.", + "empty": "Alèt depans yo fèmen. Limen yo pou avèti kontak fakti sektè sa a.", + "noSpend": "Poko gen depans ki anrejistre", + "spend24h": "Depans nan 24 èdtan ki sot pase yo", + "multiplierError": "Antre yon miltiplikatè 1 oswa plis", + "permissionDenied": "Ou pa gen pèmisyon pou jere alèt depans yo.", + "pushOffByCategory": "Fèmen nan Notifikasyon" } } diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index ef1de20663..a1c87cc1db 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -827,7 +827,8 @@ "chat": "Csevegőüzenetek", "kiloclaw": "KiloClaw-tevékenység", "balance": "Egyenlegértesítések", - "security": "Biztonsági találatok" + "security": "Biztonsági találatok", + "spend": "Költésfigyelmeztetések" }, "category": { "chatMessagesSubtitle": "válaszok a beszélgetéseidben", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "befejezett / sikertelen / vezérelhető", "kiloclawActivitySubtitle": "elkészült vagy meghibásodott példányok, ütemezett műveletek", "balanceAlertsSubtitle": "figyelmeztetések a szervezet alacsony egyenlegére", + "spendAlertsSubtitle": "küszöbátlépések és óránkénti kiugrások", + "spendAlertsToggle": "Költésfigyelmeztetések kategória", "securityFindingsSubtitle": "új biztonsági problémák és SLA-emlékeztetők" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Utolsó munkamenet megnyitása" + }, + "spendAlerts": { + "subtitle": "Küldj e-mailes vagy push értesítést a számlázási kapcsolatoknak, amikor ennek a hatókörnek a költése átlép egy limitet, vagy a szokásos óránkénti üteme fölé ugrik.", + "enable": "Költésfigyelmeztetések engedélyezése", + "thresholdTitle": "Küszöbátlépés", + "thresholdSubtitle": "A folyó költés átlépi a limited", + "anomalyTitle": "Óránkénti kiugrás", + "anomalySubtitle": "Egy óra messze a hatókör szokásos üteme felett van", + "limitLabel": "Limit (USD)", + "windowLabel": "Ablak", + "window24h": "24 óra", + "window7d": "7 nap", + "window30d": "30 nap", + "multiplierLabel": "Kiugrási szorzó", + "saved": "A költésfigyelmeztetési beállítások elmentve", + "loadError": "A költésfigyelmeztetéseket nem sikerült betölteni.", + "saveError": "A költésfigyelmeztetéseket nem sikerült elmenteni.", + "empty": "A költésfigyelmeztetések ki vannak kapcsolva. Kapcsold be őket, hogy értesítsd a hatókör számlázási kapcsolatait.", + "noSpend": "Még nincs rögzített költés", + "spend24h": "Költés az elmúlt 24 órában", + "multiplierError": "Adj meg 1 vagy nagyobb szorzót", + "permissionDenied": "Nincs jogosultságod a költésfigyelmeztetések kezelésére.", + "pushOffByCategory": "Értesítésekben kikapcsolva" } } diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 59bf0219fc..57a6429b4d 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -827,7 +827,8 @@ "chat": "Զրույցի հաղորդագրություններ", "kiloclaw": "KiloClaw-ի ակտիվություն", "balance": "Մնացորդի մասին զգուշացումներ", - "security": "Հայտնաբերված անվտանգության խնդիրներ" + "security": "Հայտնաբերված անվտանգության խնդիրներ", + "spend": "Ծախսերի ծանուցումներ" }, "category": { "chatMessagesSubtitle": "պատասխաններ ձեր զրույցներում", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ավարտված / ձախողված / պատրաստ է կառավարման", "kiloclawActivitySubtitle": "միջավայրը պատրաստ է կամ ձախողվել է, պլանավորված գործողություններ", "balanceAlertsSubtitle": "կազմակերպության ցածր մնացորդի նախազգուշացումներ", + "spendAlertsSubtitle": "շեմերի հատումներ և ժամային ցատկեր", + "spendAlertsToggle": "Ծախսերի ծանուցումների կատեգորիա", "securityFindingsSubtitle": "հայտնաբերված նոր խնդիրներ և SLA հիշեցումներ" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Բացել վերջին աշխատաշրջանը" + }, + "spendAlerts": { + "subtitle": "Ուղարկիր ծանուցում էլ. փոստով կամ push-ով վճարումների կոնտակտներին, երբ այս տիրույթի ծախսը հատի սահմանը կամ բարձրանա սովորական ժամային տեմպից։", + "enable": "Միացնել ծախսերի ծանուցումները", + "thresholdTitle": "Շեմի հատում", + "thresholdSubtitle": "Ընթացիկ ծախսը հատում է քո սահմանը", + "anomalyTitle": "Ժամային ցատկ", + "anomalySubtitle": "Մեկ ժամը շատ ավելի բարձր է այս տիրույթի սովորական տեմպից", + "limitLabel": "Սահման (USD)", + "windowLabel": "Պատուհան", + "window24h": "24 ժամ", + "window7d": "7 օր", + "window30d": "30 օր", + "multiplierLabel": "Ցատկի բազմապատկիչ", + "saved": "Ծախսերի ծանուցման կարգավորումները պահպանվեցին", + "loadError": "Ծախսերի ծանուցումները չհաջողվեց բեռնել։", + "saveError": "Ծախսերի ծանուցումները չհաջողվեց պահպանել։", + "empty": "Ծախսերի ծանուցումներն անջատված են։ Միացրու դրանք այս տիրույթի վճարումների կոնտակտներին զգուշացնելու համար։", + "noSpend": "Ծախս դեռ չի գրանցվել", + "spend24h": "Ծախսը վերջին 24 ժամում", + "multiplierError": "Մուտքագրիր 1 կամ ավելի բազմապատկիչ", + "permissionDenied": "Դուք ծախսերի ծանուցումները կառավարելու թույլտվություն չունեք։", + "pushOffByCategory": "Անջատված է Ծանուցումներում" } } diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index de020fe95c..b7aa46f454 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -323,7 +323,8 @@ "chat": "Pesan obrolan", "kiloclaw": "Aktivitas KiloClaw", "balance": "Peringatan saldo", - "security": "Temuan keamanan" + "security": "Temuan keamanan", + "spend": "Peringatan pengeluaran" }, "category": { "chatMessagesSubtitle": "balasan dalam percakapanmu", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "selesai / gagal / siap dikendalikan", "kiloclawActivitySubtitle": "instans siap/gagal, tindakan terjadwal", "balanceAlertsSubtitle": "peringatan saat saldo organisasi rendah", + "spendAlertsSubtitle": "melewati ambang batas dan lonjakan per jam", + "spendAlertsToggle": "Kategori peringatan pengeluaran", "securityFindingsSubtitle": "temuan baru dan pengingat SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Buka sesi terakhir" + }, + "spendAlerts": { + "subtitle": "Kirim peringatan lewat email atau push ke kontak penagihan saat pengeluaran cakupan ini melewati batas atau melonjak di atas laju per jam biasanya.", + "enable": "Aktifkan peringatan pengeluaran", + "thresholdTitle": "Melewati ambang batas", + "thresholdSubtitle": "Pengeluaran berjalan melewati batas kamu", + "anomalyTitle": "Lonjakan per jam", + "anomalySubtitle": "Satu jam jauh di atas laju biasanya cakupan ini", + "limitLabel": "Batas (USD)", + "windowLabel": "Jendela", + "window24h": "24 jam", + "window7d": "7 hari", + "window30d": "30 hari", + "multiplierLabel": "Pengali lonjakan", + "saved": "Setelan peringatan pengeluaran disimpan", + "loadError": "Tidak dapat memuat peringatan pengeluaran.", + "saveError": "Tidak dapat menyimpan peringatan pengeluaran.", + "empty": "Peringatan pengeluaran nonaktif. Aktifkan untuk memberi tahu kontak penagihan cakupan ini.", + "noSpend": "Belum ada pengeluaran yang tercatat", + "spend24h": "Pengeluaran dalam 24 jam terakhir", + "multiplierError": "Masukkan pengali 1 atau lebih", + "permissionDenied": "Kamu tidak memiliki izin untuk mengelola peringatan pengeluaran.", + "pushOffByCategory": "Nonaktif di Notifikasi" } } diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 399b23bc98..6c56385b94 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -827,7 +827,8 @@ "chat": "Ozi nkata", "kiloclaw": "Ihe omume KiloClaw", "balance": "Ọkwa gbasara ego fọdụrụ", - "security": "Ihe achọpụtara gbasara nchebe" + "security": "Ihe achọpụtara gbasara nchebe", + "spend": "Ọkwa gbasara mmefu" }, "category": { "chatMessagesSubtitle": "azịza na mkparịta ụka gị", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "agwụla / agaghị nke ọma / dị njikere ka ị chịkwaa ya", "kiloclawActivitySubtitle": "usoro dị njikere ma ọ bụ agaghị nke ọma, omume a haziri oge ha", "balanceAlertsSubtitle": "ịdọ aka na ntị mgbe ego ụlọ ọrụ fọdụrụ dị ntakịrị", + "spendAlertsSubtitle": "ịgafe oke na mmụba kwa awa", + "spendAlertsToggle": "Otu ọkwa gbasara mmefu", "securityFindingsSubtitle": "nchọpụta ọhụrụ na ncheta SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Mepee nnọkọ ikpeazụ" + }, + "spendAlerts": { + "subtitle": "Zipu ọkwa site na ozi-e ma ọ bụ push nye ndị kọntaktị ịgba ụgwọ mgbe mmefu nke mpaghara a gafere oke ma ọ bụ rịgoro karịa ọnụ ọgụgụ awa ya.", + "enable": "Gbanye ọkwa gbasara mmefu", + "thresholdTitle": "Ịgafe oke", + "thresholdSubtitle": "Mmefu na-aga n'ihu na-agafe oke gị", + "anomalyTitle": "Mmụba kwa awa", + "anomalySubtitle": "Otu awa dị nnọọ elu karịa ọnụ ọgụgụ mpaghara a", + "limitLabel": "Oke (USD)", + "windowLabel": "Oge", + "window24h": "Awa 24", + "window7d": "Ụbọchị 7", + "window30d": "Ụbọchị 30", + "multiplierLabel": "Ọnụọgụ mmụba", + "saved": "Echekwara ntọala ọkwa gbasara mmefu", + "loadError": "Enweghị ike ibudata ọkwa gbasara mmefu.", + "saveError": "Enweghị ike ichekwa ọkwa gbasara mmefu.", + "empty": "Ọkwa gbasara mmefu gbanyụrụ. Gbanye ya iji kwaa ndị kọntaktị ịgba ụgwọ nke mpaghara a aka na ntị.", + "noSpend": "Edebebeghị mmefu ọ bụla", + "spend24h": "Mmefu n'ime awa 24 gara aga", + "multiplierError": "Tinye ọnụọgụ mmụba nke 1 ma ọ bụ karịa", + "permissionDenied": "Ị nweghị ikike ijikwa ọkwa gbasara mmefu.", + "pushOffByCategory": "Gbanyụrụ n'ime Ọkwa" } } diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 4ded353269..7f43484ded 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -827,7 +827,8 @@ "chat": "Spjallskilaboð", "kiloclaw": "Virkni í KiloClaw", "balance": "Inneignarviðvaranir", - "security": "Öryggisniðurstöður" + "security": "Öryggisniðurstöður", + "spend": "Útgjaldaviðvaranir" }, "category": { "chatMessagesSubtitle": "svör í samtölunum þínum", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "lokið / mistókst / tilbúin til stýringar", "kiloclawActivitySubtitle": "tilvik tilbúið eða bilað, tímasettar aðgerðir", "balanceAlertsSubtitle": "viðvaranir um lága inneign félagsins", + "spendAlertsSubtitle": "yfirferðir þröskulda og uppsveiflur á klukkustund", + "spendAlertsToggle": "Flokkur útgjaldaviðvarana", "securityFindingsSubtitle": "nýjar niðurstöður og SLA-áminningar" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Opna síðustu lotu" + }, + "spendAlerts": { + "subtitle": "Sendu viðvörun í tölvupósti eða push til innheimtutengiliða þegar útgjöld þessa svæðis fara yfir mörk eða stökkva yfir venjulegan klukkustundarhraða þess.", + "enable": "Virkja útgjaldaviðvaranir", + "thresholdTitle": "Yfirferð þröskulds", + "thresholdSubtitle": "Safnað útgjöld fara yfir mörkin þín", + "anomalyTitle": "Uppsveifla á klukkustund", + "anomalySubtitle": "Klukkustund er langt yfir venjulegum hraða þessa svæðis", + "limitLabel": "Mörk (USD)", + "windowLabel": "Gluggi", + "window24h": "24 klukkustundir", + "window7d": "7 dagar", + "window30d": "30 dagar", + "multiplierLabel": "Margfaldari uppsveiflunnar", + "saved": "Stillingar útgjaldaviðvarana vistaðar", + "loadError": "Ekki tókst að hlaða útgjaldaviðvörunum.", + "saveError": "Ekki tókst að vista útgjaldaviðvaranir.", + "empty": "Útgjaldaviðvaranir eru af. Kveiktu á þeim til að vara innheimtutengiliði þessa svæðis við.", + "noSpend": "Engin útgjöld skráð ennþá", + "spend24h": "Útgjöld síðustu 24 klukkustundir", + "multiplierError": "Sláðu inn margfaldara sem er 1 eða meira", + "permissionDenied": "Þú hefur ekki heimild til að stjórna útgjaldaviðvörunum.", + "pushOffByCategory": "Af í Tilkynningum" } } diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 640660e4ab..9bb5b697a3 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -259,7 +259,8 @@ "chat": "Messaggi di chat", "kiloclaw": "Attività di KiloClaw", "balance": "Avvisi sul saldo", - "security": "Problemi di sicurezza" + "security": "Problemi di sicurezza", + "spend": "Avvisi di spesa" }, "category": { "chatMessagesSubtitle": "risposte nelle tue conversazioni", @@ -271,6 +272,8 @@ "sessionStatusSubtitle": "terminata / non riuscita / pronta per il controllo", "kiloclawActivitySubtitle": "istanza pronta o in errore, azioni pianificate", "balanceAlertsSubtitle": "avvisi di saldo basso dell'organizzazione", + "spendAlertsSubtitle": "superamenti di soglia e picchi orari", + "spendAlertsToggle": "Categoria avvisi di spesa", "securityFindingsSubtitle": "nuovi problemi di sicurezza e promemoria SLA" }, "liveActivities": "Attività in tempo reale", @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Apri l'ultima sessione" + }, + "spendAlerts": { + "subtitle": "Invia un avviso via email o push ai contatti di fatturazione quando la spesa di questo ambito supera un limite o schizza sopra il suo ritmo orario abituale.", + "enable": "Attiva gli avvisi di spesa", + "thresholdTitle": "Superamento della soglia", + "thresholdSubtitle": "La spesa progressiva supera il tuo limite", + "anomalyTitle": "Picco orario", + "anomalySubtitle": "Un'ora è molto sopra il ritmo abituale di questo ambito", + "limitLabel": "Limite (USD)", + "windowLabel": "Finestra", + "window24h": "24 ore", + "window7d": "7 giorni", + "window30d": "30 giorni", + "multiplierLabel": "Moltiplicatore del picco", + "saved": "Impostazioni degli avvisi di spesa salvate", + "loadError": "Impossibile caricare gli avvisi di spesa.", + "saveError": "Impossibile salvare gli avvisi di spesa.", + "empty": "Gli avvisi di spesa sono disattivati. Attivali per avvisare i contatti di fatturazione di questo ambito.", + "noSpend": "Nessuna spesa registrata finora", + "spend24h": "Spesa nelle ultime 24 ore", + "multiplierError": "Inserisci un moltiplicatore di 1 o più", + "permissionDenied": "Non hai l'autorizzazione per gestire gli avvisi di spesa.", + "pushOffByCategory": "Disattivato in Notifiche" } } diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index f9bcfade9e..dce02cf3ec 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -323,7 +323,8 @@ "chat": "チャットメッセージ", "kiloclaw": "KiloClawのアクティビティ", "balance": "残高アラート", - "security": "セキュリティの検出事項" + "security": "セキュリティの検出事項", + "spend": "支出アラート" }, "category": { "chatMessagesSubtitle": "会話内の返信", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "完了 / 失敗 / 操作可能", "kiloclawActivitySubtitle": "インスタンスの準備完了や失敗、予定された操作", "balanceAlertsSubtitle": "組織の残高低下に関する警告", + "spendAlertsSubtitle": "しきい値の超過と毎時の急増", + "spendAlertsToggle": "支出アラートのカテゴリ", "securityFindingsSubtitle": "新たな検出結果とSLAのリマインダー" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "最後のセッションを開く" + }, + "spendAlerts": { + "subtitle": "このスコープの支出が上限を超えたとき、または通常の毎時のペースを大きく上回ったときに、請求担当者にメールまたはプッシュでアラートを送信します。", + "enable": "支出アラートを有効にする", + "thresholdTitle": "しきい値の超過", + "thresholdSubtitle": "累計支出が上限を超えています", + "anomalyTitle": "毎時の急増", + "anomalySubtitle": "1時間の支出がこのスコープの通常のペースを大きく上回っています", + "limitLabel": "上限 (USD)", + "windowLabel": "期間", + "window24h": "24時間", + "window7d": "7日間", + "window30d": "30日間", + "multiplierLabel": "急増の倍率", + "saved": "支出アラートの設定を保存しました", + "loadError": "支出アラートを読み込めませんでした。", + "saveError": "支出アラートを保存できませんでした。", + "empty": "支出アラートはオフです。このスコープの請求担当者に通知するにはオンにしてください。", + "noSpend": "まだ支出が記録されていません", + "spend24h": "過去24時間の支出", + "multiplierError": "1以上の倍率を入力してください", + "permissionDenied": "支出アラートを管理する権限がありません。", + "pushOffByCategory": "通知でオフ" } } diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 38febf6690..51e6dde077 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -827,7 +827,8 @@ "chat": "ჩატის შეტყობინებები", "kiloclaw": "KiloClaw-ის აქტივობა", "balance": "ბალანსის შესახებ გაფრთხილებები", - "security": "უსაფრთხოების პრობლემები" + "security": "უსაფრთხოების პრობლემები", + "spend": "ხარჯვის შეტყობინებები" }, "category": { "chatMessagesSubtitle": "პასუხები თქვენს საუბრებში", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "დასრულდა / ჩაიშალა / სამართავად მზადაა", "kiloclawActivitySubtitle": "ინსტანცია მზადაა / ჩაიშალა, დაგეგმილი მოქმედებები", "balanceAlertsSubtitle": "ორგანიზაციის დაბალი ბალანსის გაფრთხილებები", + "spendAlertsSubtitle": "ზღვრების გადაცილება და საათობრივი ნახტომები", + "spendAlertsToggle": "ხარჯვის შეტყობინებების კატეგორია", "securityFindingsSubtitle": "ახლად აღმოჩენილი პრობლემები და SLA-ის შეხსენებები" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "ბოლო სესიის გახსნა" + }, + "spendAlerts": { + "subtitle": "გაუგზავნე შეტყობინება ელფოსტით ან push-ით ბილინგის კონტაქტებს, როცა ამ სფეროს ხარჯვა გადააჭარბებს ლიმიტს ან ავა მის ჩვეულ საათობრივ ტემპს.", + "enable": "ხარჯვის შეტყობინებების ჩართვა", + "thresholdTitle": "ზღვრის გადაცილება", + "thresholdSubtitle": "მიმდინარე ხარჯვა აჭარბებს შენს ლიმიტს", + "anomalyTitle": "საათობრივი ნახტომი", + "anomalySubtitle": "ერთი საათი ბევრად აღემატება ამ სფეროს ჩვეულ ტემპს", + "limitLabel": "ლიმიტი (USD)", + "windowLabel": "ფანჯარა", + "window24h": "24 საათი", + "window7d": "7 დღე", + "window30d": "30 დღე", + "multiplierLabel": "ნახტომის მამრავლი", + "saved": "ხარჯვის შეტყობინების პარამეტრები შენახულია", + "loadError": "ხარჯვის შეტყობინებები ვერ ჩაიტვირთა.", + "saveError": "ხარჯვის შეტყობინებები ვერ შეინახა.", + "empty": "ხარჯვის შეტყობინებები გამორთულია. ჩართე ისინი ამ სფეროს ბილინგის კონტაქტების გასაფრთხილებლად.", + "noSpend": "ხარჯვა ჯერ არ არის დაფიქსირებული", + "spend24h": "ხარჯვა ბოლო 24 საათში", + "multiplierError": "შეიყვანე 1 ან მეტი მამრავლი", + "permissionDenied": "ხარჯვის შეტყობინებების მართვის უფლება არ გაქვთ.", + "pushOffByCategory": "გამორთულია შეტყობინებებში" } } diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 135857110d..23a9046b90 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -827,7 +827,8 @@ "chat": "Чат хабарламалары", "kiloclaw": "KiloClaw белсенділігі", "balance": "Баланс туралы ескертулер", - "security": "Анықталған қауіпсіздік мәселелері" + "security": "Анықталған қауіпсіздік мәселелері", + "spend": "Шығыс ескертулері" }, "category": { "chatMessagesSubtitle": "сөйлесулеріңіздегі жауаптар", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "аяқталды / сәтсіз аяқталды / басқаруға дайын", "kiloclawActivitySubtitle": "дана дайын / істен шықты, жоспарланған әрекеттер", "balanceAlertsSubtitle": "ұйым балансының азайғаны туралы ескертулер", + "spendAlertsSubtitle": "шектерден асу және сағаттық секірістер", + "spendAlertsToggle": "Шығыс ескертулері санаты", "securityFindingsSubtitle": "жаңа осалдықтар және SLA туралы еске салулар" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Соңғы сессияны ашу" + }, + "spendAlerts": { + "subtitle": "Осы ауқымның шығысы шектен асып кеткенде немесе әдеттегі сағаттық қарқынынан жоғары көтерілгенде, төлем бойынша байланыстарға электрондық пошта немесе push арқылы ескерту жібер.", + "enable": "Шығыс ескертулерін қосу", + "thresholdTitle": "Шектен асу", + "thresholdSubtitle": "Ағымдағы шығыс шегіңізден асады", + "anomalyTitle": "Сағаттық секіріс", + "anomalySubtitle": "Бір сағат осы ауқымның әдеттегі қарқынынан әлдеқайда жоғары", + "limitLabel": "Шек (USD)", + "windowLabel": "Терезе", + "window24h": "24 сағат", + "window7d": "7 күн", + "window30d": "30 күн", + "multiplierLabel": "Секіріс көбейткіші", + "saved": "Шығыс ескертуінің параметрлері сақталды", + "loadError": "Шығыс ескертулері жүктелмеді.", + "saveError": "Шығыс ескертулері сақталмады.", + "empty": "Шығыс ескертулері өшірулі. Осы ауқымның төлем бойынша байланыстарын ескерту үшін оларды қос.", + "noSpend": "Әзірге шығыс тіркелмеген", + "spend24h": "Соңғы 24 сағаттағы шығыс", + "multiplierError": "1 немесе одан үлкен көбейткіш енгіз", + "permissionDenied": "Шығыс ескертулерін басқаруға рұқсатыңыз жоқ.", + "pushOffByCategory": "Хабарландыруларда өшірулі" } } diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 2a9e928397..c7bebf57ca 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -827,7 +827,8 @@ "chat": "សារជជែក", "kiloclaw": "សកម្មភាព KiloClaw", "balance": "ការជូនដំណឹងអំពីសមតុល្យ", - "security": "បញ្ហាសុវត្ថិភាពដែលរកឃើញ" + "security": "បញ្ហាសុវត្ថិភាពដែលរកឃើញ", + "spend": "ការជូនដំណឹងអំពីការចំណាយ" }, "category": { "chatMessagesSubtitle": "ការឆ្លើយតបនៅក្នុងការសន្ទនារបស់អ្នក", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "បានបញ្ចប់ / បានបរាជ័យ / រួចរាល់សម្រាប់ឱ្យអ្នកគ្រប់គ្រង", "kiloclawActivitySubtitle": "ម៉ាស៊ីនរួចរាល់/បរាជ័យ សកម្មភាពតាមកាលវិភាគ", "balanceAlertsSubtitle": "ការព្រមានពេលសមតុល្យរបស់អង្គការទាប", + "spendAlertsSubtitle": "ការឆ្លងកាត់កម្រិត និងការកើនឡើងប្រចាំម៉ោង", + "spendAlertsToggle": "ប្រភេទនៃការជូនដំណឹងអំពីការចំណាយ", "securityFindingsSubtitle": "ការរកឃើញថ្មី និងការរំលឹក SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "បើកវគ្គចុងក្រោយ" + }, + "spendAlerts": { + "subtitle": "ផ្ញើការជូនដំណឹងតាមអ៊ីមែល ឬ push ទៅអ្នកទំនាក់ទំនងវិក្កយបត្រ នៅពេលការចំណាយរបស់វិសាលភាពនេះឆ្លងកាត់កម្រិត ឬកើនឡើងលើសពីអត្រាប្រចាំម៉ោងធម្មតា។", + "enable": "បើកការជូនដំណឹងអំពីការចំណាយ", + "thresholdTitle": "ការឆ្លងកាត់កម្រិត", + "thresholdSubtitle": "ការចំណាយបច្ចុប្បន្នឆ្លងកាត់កម្រិតរបស់អ្នក", + "anomalyTitle": "ការកើនឡើងប្រចាំម៉ោង", + "anomalySubtitle": "មួយម៉ោងខ្ពស់ជាងអត្រាធម្មតារបស់វិសាលភាពនេះច្រើន", + "limitLabel": "កម្រិត (USD)", + "windowLabel": "រយៈពេល", + "window24h": "24 ម៉ោង", + "window7d": "7 ថ្ងៃ", + "window30d": "30 ថ្ងៃ", + "multiplierLabel": "តម្លៃគុណនៃការកើនឡើង", + "saved": "បានរក្សាទុកការកំណត់នៃការជូនដំណឹងអំពីការចំណាយ", + "loadError": "មិនអាចផ្ទុកការជូនដំណឹងអំពីការចំណាយបានទេ។", + "saveError": "មិនអាចរក្សាទុកការជូនដំណឹងអំពីការចំណាយបានទេ។", + "empty": "ការជូនដំណឹងអំពីការចំណាយបិទ។ បើកវាដើម្បីជូនដំណឹងអ្នកទំនាក់ទំនងវិក្កយបត្ររបស់វិសាលភាពនេះ។", + "noSpend": "មិនទាន់មានការចំណាយត្រូវបានកត់ត្រាទេ", + "spend24h": "ការចំណាយក្នុងរយៈពេល 24 ម៉ោងចុងក្រោយ", + "multiplierError": "បញ្ចូលតម្លៃគុណ 1 ឬច្រើនជាងនេះ", + "permissionDenied": "អ្នកមិនមានសិទ្ធិគ្រប់គ្រងការជូនដំណឹងអំពីការចំណាយទេ។", + "pushOffByCategory": "បិទក្នុងការជូនដំណឹង" } } diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 0d243633a8..4f95f740d7 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -827,7 +827,8 @@ "chat": "ಚಾಟ್ ಸಂದೇಶಗಳು", "kiloclaw": "KiloClaw ಚಟುವಟಿಕೆ", "balance": "ಉಳಿಕೆ ಮೊತ್ತದ ಎಚ್ಚರಿಕೆಗಳು", - "security": "ಪತ್ತೆಯಾದ ಭದ್ರತಾ ಸಮಸ್ಯೆಗಳು" + "security": "ಪತ್ತೆಯಾದ ಭದ್ರತಾ ಸಮಸ್ಯೆಗಳು", + "spend": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳು" }, "category": { "chatMessagesSubtitle": "ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳಲ್ಲಿನ ಉತ್ತರಗಳು", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ಮುಗಿದಿದೆ / ವಿಫಲವಾಗಿದೆ / ನಿಯಂತ್ರಿಸಲು ಸಿದ್ಧ", "kiloclawActivitySubtitle": "ಇನ್‌ಸ್ಟನ್ಸ್ ಸಿದ್ಧವಾಗಿದೆ/ವಿಫಲವಾಗಿದೆ, ನಿಗದಿತ ಕಾರ್ಯಗಳು", "balanceAlertsSubtitle": "ಸಂಸ್ಥೆಯ ಉಳಿಕೆ ಕಡಿಮೆಯಾದಾಗ ಎಚ್ಚರಿಕೆಗಳು", + "spendAlertsSubtitle": "ಮಿತಿ ದಾಟುವಿಕೆ ಮತ್ತು ಪ್ರತಿ ಗಂಟೆಯ ಏರಿಕೆಗಳು", + "spendAlertsToggle": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳ ವರ್ಗ", "securityFindingsSubtitle": "ಹೊಸದಾಗಿ ಪತ್ತೆಯಾದ ಸಮಸ್ಯೆಗಳು ಮತ್ತು SLA ಜ್ಞಾಪನೆಗಳು" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "ಕೊನೆಯ ಸೆಷನ್ ತೆರೆಯಿರಿ" + }, + "spendAlerts": { + "subtitle": "ಈ ವ್ಯಾಪ್ತಿಯ ಖರ್ಚು ಮಿತಿಯನ್ನು ದಾಟಿದಾಗ ಅಥವಾ ಅದರ ಸಾಮಾನ್ಯ ಗಂಟೆಯ ದರಕ್ಕಿಂತ ಮೇಲೇರಿದಾಗ ಬಿಲ್ಲಿಂಗ್ ಸಂಪರ್ಕಗಳಿಗೆ ಇಮೇಲ್ ಅಥವಾ ಪುಷ್ ಮೂಲಕ ಎಚ್ಚರಿಕೆ ಕಳುಹಿಸಿ.", + "enable": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "thresholdTitle": "ಮಿತಿ ದಾಟುವಿಕೆ", + "thresholdSubtitle": "ಪ್ರಸ್ತುತ ಖರ್ಚು ನಿಮ್ಮ ಮಿತಿಯನ್ನು ದಾಟುತ್ತಿದೆ", + "anomalyTitle": "ಪ್ರತಿ ಗಂಟೆಯ ಏರಿಕೆ", + "anomalySubtitle": "ಒಂದು ಗಂಟೆ ಈ ವ್ಯಾಪ್ತಿಯ ಸಾಮಾನ್ಯ ದರಕ್ಕಿಂತ ಬಹಳ ಹೆಚ್ಚಿದೆ", + "limitLabel": "ಮಿತಿ (USD)", + "windowLabel": "ಅವಧಿ", + "window24h": "24 ಗಂಟೆಗಳು", + "window7d": "7 ದಿನಗಳು", + "window30d": "30 ದಿನಗಳು", + "multiplierLabel": "ಏರಿಕೆಯ ಗುಣಕ", + "saved": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಉಳಿಸಲಾಗಿದೆ", + "loadError": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳನ್ನು ಲೋಡ್ ಮಾಡಲಾಗಲಿಲ್ಲ.", + "saveError": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳನ್ನು ಉಳಿಸಲಾಗಲಿಲ್ಲ.", + "empty": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳು ಆಫ್ ಆಗಿವೆ. ಈ ವ್ಯಾಪ್ತಿಯ ಬಿಲ್ಲಿಂಗ್ ಸಂಪರ್ಕಗಳಿಗೆ ಎಚ್ಚರಿಸಲು ಅವನ್ನು ಆನ್ ಮಾಡಿ.", + "noSpend": "ಇನ್ನೂ ಯಾವುದೇ ಖರ್ಚು ದಾಖಲಾಗಿಲ್ಲ", + "spend24h": "ಕಳೆದ 24 ಗಂಟೆಗಳ ಖರ್ಚು", + "multiplierError": "1 ಅಥವಾ ಹೆಚ್ಚಿನ ಗುಣಕವನ್ನು ನಮೂದಿಸಿ", + "permissionDenied": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳನ್ನು ನಿರ್ವಹಿಸಲು ನಿಮಗೆ ಅನುಮತಿ ಇಲ್ಲ.", + "pushOffByCategory": "ಅಧಿಸೂಚನೆಗಳಲ್ಲಿ ಆಫ್" } } diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 1d127929fc..5d1152c050 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -323,7 +323,8 @@ "chat": "채팅 메시지", "kiloclaw": "KiloClaw 활동", "balance": "잔액 알림", - "security": "보안 진단 결과" + "security": "보안 진단 결과", + "spend": "지출 알림" }, "category": { "chatMessagesSubtitle": "대화에 달린 답장", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "완료 / 실패 / 제어 준비 완료", "kiloclawActivitySubtitle": "인스턴스 준비 완료 및 실패, 예약된 작업", "balanceAlertsSubtitle": "조직 잔액 부족 경고", + "spendAlertsSubtitle": "임계값 초과 및 시간당 급증", + "spendAlertsToggle": "지출 알림 카테고리", "securityFindingsSubtitle": "새 보안 문제 및 SLA 알림" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "마지막 세션 열기" + }, + "spendAlerts": { + "subtitle": "이 범위의 지출이 한도를 넘거나 평소 시간당 속도를 크게 웃돌면 청구 담당자에게 이메일 또는 푸시로 알림을 보냅니다.", + "enable": "지출 알림 켜기", + "thresholdTitle": "임계값 초과", + "thresholdSubtitle": "누적 지출이 한도를 초과합니다", + "anomalyTitle": "시간당 급증", + "anomalySubtitle": "한 시간의 지출이 이 범위의 평소 속도를 크게 웃돕니다", + "limitLabel": "한도 (USD)", + "windowLabel": "기간", + "window24h": "24시간", + "window7d": "7일", + "window30d": "30일", + "multiplierLabel": "급증 배수", + "saved": "지출 알림 설정이 저장되었습니다", + "loadError": "지출 알림을 불러오지 못했습니다.", + "saveError": "지출 알림을 저장하지 못했습니다.", + "empty": "지출 알림이 꺼져 있습니다. 이 범위의 청구 담당자에게 알리려면 켜세요.", + "noSpend": "아직 기록된 지출이 없습니다", + "spend24h": "최근 24시간 지출", + "multiplierError": "1 이상의 배수를 입력하세요", + "permissionDenied": "지출 알림을 관리할 권한이 없습니다.", + "pushOffByCategory": "알림에서 꺼짐" } } diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 88740d75f3..e48d553ef1 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -827,7 +827,8 @@ "chat": "ຂໍ້ຄວາມສົນທະນາ", "kiloclaw": "ກິດຈະກຳ KiloClaw", "balance": "ການແຈ້ງເຕືອນຍອດເງິນ", - "security": "ບັນຫາດ້ານຄວາມປອດໄພທີ່ກວດພົບ" + "security": "ບັນຫາດ້ານຄວາມປອດໄພທີ່ກວດພົບ", + "spend": "ການແຈ້ງເຕືອນການໃຊ້ຈ່າຍ" }, "category": { "chatMessagesSubtitle": "ຄຳຕອບໃນການສົນທະນາຂອງທ່ານ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ສຳເລັດ / ບໍ່ສຳເລັດ / ພ້ອມໃຫ້ຄວບຄຸມ", "kiloclawActivitySubtitle": "ອິນສະແຕນພ້ອມໃຊ້ງານ/ຂັດຂ້ອງ, ວຽກທີ່ຕັ້ງເວລາໄວ້", "balanceAlertsSubtitle": "ຄຳເຕືອນເມື່ອຍອດເງິນຂອງອົງກອນຕໍ່າ", + "spendAlertsSubtitle": "ການຂ້າມຂີດຈຳກັດ ແລະ ການເພີ່ມຂຶ້ນຕໍ່ຊົ່ວໂມງ", + "spendAlertsToggle": "ໝວດການແຈ້ງເຕືອນການໃຊ້ຈ່າຍ", "securityFindingsSubtitle": "ບັນຫາທີ່ພົບໃໝ່ ແລະ ການແຈ້ງເຕືອນກ່ຽວກັບ SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "ເປີດເຊສຊັນລ່າສຸດ" + }, + "spendAlerts": { + "subtitle": "ສົ່ງການແຈ້ງເຕືອນຜ່ານອີເມວ ຫຼື push ໄປຫາຜູ້ຕິດຕໍ່ການຮຽກເກັບເງິນ ເມື່ອການໃຊ້ຈ່າຍຂອງຂອບເຂດນີ້ຂ້າມຂີດຈຳກັດ ຫຼື ເພີ່ມຂຶ້ນເໜືອອັດຕາຕໍ່ຊົ່ວໂມງປົກກະຕິ.", + "enable": "ເປີດການແຈ້ງເຕືອນການໃຊ້ຈ່າຍ", + "thresholdTitle": "ການຂ້າມຂີດຈຳກັດ", + "thresholdSubtitle": "ການໃຊ້ຈ່າຍປັດຈຸບັນຂ້າມຂີດຈຳກັດຂອງທ່ານ", + "anomalyTitle": "ການເພີ່ມຂຶ້ນຕໍ່ຊົ່ວໂມງ", + "anomalySubtitle": "ໜຶ່ງຊົ່ວໂມງສູງກວ່າອັດຕາປົກກະຕິຂອງຂອບເຂດນີ້ຫຼາຍ", + "limitLabel": "ຂີດຈຳກັດ (USD)", + "windowLabel": "ໄລຍະເວລາ", + "window24h": "24 ຊົ່ວໂມງ", + "window7d": "7 ວັນ", + "window30d": "30 ວັນ", + "multiplierLabel": "ຕົວຄູນການເພີ່ມຂຶ້ນ", + "saved": "ບັນທຶກການຕັ້ງຄ່າການແຈ້ງເຕືອນການໃຊ້ຈ່າຍແລ້ວ", + "loadError": "ບໍ່ສາມາດໂຫຼດການແຈ້ງເຕືອນການໃຊ້ຈ່າຍໄດ້.", + "saveError": "ບໍ່ສາມາດບັນທຶກການແຈ້ງເຕືອນການໃຊ້ຈ່າຍໄດ້.", + "empty": "ການແຈ້ງເຕືອນການໃຊ້ຈ່າຍປິດຢູ່. ເປີດມັນເພື່ອແຈ້ງເຕືອນຜູ້ຕິດຕໍ່ການຮຽກເກັບເງິນຂອງຂອບເຂດນີ້.", + "noSpend": "ຍັງບໍ່ມີການບັນທຶກການໃຊ້ຈ່າຍ", + "spend24h": "ການໃຊ້ຈ່າຍໃນ 24 ຊົ່ວໂມງຜ່ານມາ", + "multiplierError": "ໃສ່ຕົວຄູນ 1 ຫຼື ຫຼາຍກວ່າ", + "permissionDenied": "ທ່ານບໍ່ມີສິດຈັດການການແຈ້ງເຕືອນການໃຊ້ຈ່າຍ.", + "pushOffByCategory": "ປິດໃນການແຈ້ງເຕືອນ" } } diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 5f26bc7bb8..55ad71784b 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -859,7 +859,8 @@ "chat": "Pokalbių žinutės", "kiloclaw": "KiloClaw veikla", "balance": "Įspėjimai apie likutį", - "security": "Saugos problemos" + "security": "Saugos problemos", + "spend": "Išlaidų įspėjimai" }, "category": { "chatMessagesSubtitle": "atsakymai jūsų pokalbiuose", @@ -871,6 +872,8 @@ "sessionStatusSubtitle": "baigta / nepavyko / paruošta valdyti", "kiloclawActivitySubtitle": "egzemplioriaus parengtis ar klaida, suplanuoti veiksmai", "balanceAlertsSubtitle": "įspėjimai apie mažą organizacijos likutį", + "spendAlertsSubtitle": "slenksčių viršijimai ir valandiniai šuoliai", + "spendAlertsToggle": "Išlaidų įspėjimų kategorija", "securityFindingsSubtitle": "nauji radiniai ir SLA priminimai" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Atidaryti paskutinę sesiją" + }, + "spendAlerts": { + "subtitle": "Siųsk įspėjimą el. paštu arba push pranešimu atsiskaitymo kontaktams, kai šios srities išlaidos viršija limitą arba pakyla virš įprasto valandinio tempo.", + "enable": "Įjungti išlaidų įspėjimus", + "thresholdTitle": "Slenksčio viršijimas", + "thresholdSubtitle": "Einamosios išlaidos viršija tavo limitą", + "anomalyTitle": "Valandinis šuolis", + "anomalySubtitle": "Viena valanda gerokai viršija įprastą šios srities tempą", + "limitLabel": "Limitas (USD)", + "windowLabel": "Langas", + "window24h": "24 valandos", + "window7d": "7 dienos", + "window30d": "30 dienų", + "multiplierLabel": "Šuolio daugiklis", + "saved": "Išlaidų įspėjimų nustatymai išsaugoti", + "loadError": "Nepavyko įkelti išlaidų įspėjimų.", + "saveError": "Nepavyko išsaugoti išlaidų įspėjimų.", + "empty": "Išlaidų įspėjimai išjungti. Įjunk juos, kad įspėtum šios srities atsiskaitymo kontaktus.", + "noSpend": "Išlaidų dar neužregistruota", + "spend24h": "Išlaidos per paskutines 24 valandas", + "multiplierError": "Įvesk daugiklį, lygų 1 arba didesnį", + "permissionDenied": "Neturite leidimo valdyti išlaidų įspėjimų.", + "pushOffByCategory": "Išjungta pranešimuose" } } diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 8195fce169..c5818b44a5 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -843,7 +843,8 @@ "chat": "Tērzēšanas ziņojumi", "kiloclaw": "KiloClaw darbība", "balance": "Brīdinājumi par atlikumu", - "security": "Drošības konstatējumi" + "security": "Drošības konstatējumi", + "spend": "Izdevumu brīdinājumi" }, "category": { "chatMessagesSubtitle": "atbildes tavās sarunās", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "sesija pabeigta / kļūda / gatava vadīšanai", "kiloclawActivitySubtitle": "instance gatava / kļūda, ieplānotas darbības", "balanceAlertsSubtitle": "brīdinājumi par mazu organizācijas kredītu atlikumu", + "spendAlertsSubtitle": "sliekšņu pārsniegumi un stundas lēcieni", + "spendAlertsToggle": "Izdevumu brīdinājumu kategorija", "securityFindingsSubtitle": "jaunas konstatētās problēmas un SLA atgādinājumi" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Atvērt pēdējo sesiju" + }, + "spendAlerts": { + "subtitle": "Sūti brīdinājumu pa e-pastu vai push ziņojumu norēķinu kontaktpersonām, kad šīs jomas izdevumi pārsniedz limitu vai pārlec virs ierastā stundas tempa.", + "enable": "Iespējot izdevumu brīdinājumus", + "thresholdTitle": "Sliekšņa pārsniegums", + "thresholdSubtitle": "Tekošie izdevumi pārsniedz tavu limitu", + "anomalyTitle": "Stundas lēciens", + "anomalySubtitle": "Viena stunda ir krietni virs šīs jomas ierastā tempa", + "limitLabel": "Limits (USD)", + "windowLabel": "Logs", + "window24h": "24 stundas", + "window7d": "7 dienas", + "window30d": "30 dienas", + "multiplierLabel": "Lēciena reizinātājs", + "saved": "Izdevumu brīdinājumu iestatījumi saglabāti", + "loadError": "Neizdevās ielādēt izdevumu brīdinājumus.", + "saveError": "Neizdevās saglabāt izdevumu brīdinājumus.", + "empty": "Izdevumu brīdinājumi ir izslēgti. Ieslēdz tos, lai brīdinātu šīs jomas norēķinu kontaktpersonas.", + "noSpend": "Izdevumi vēl nav reģistrēti", + "spend24h": "Izdevumi pēdējo 24 stundu laikā", + "multiplierError": "Ievadi reizinātāju, kas ir 1 vai lielāks", + "permissionDenied": "Tev nav atļaujas pārvaldīt izdevumu brīdinājumus.", + "pushOffByCategory": "Izslēgts paziņojumos" } } diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index d29226c379..dc3b9f9b68 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -827,7 +827,8 @@ "chat": "Hafatra amin'ny resaka", "kiloclaw": "Hetsika ao amin'ny KiloClaw", "balance": "Fampitandremana momba ny vola sisa", - "security": "Olana hita amin'ny fiarovana" + "security": "Olana hita amin'ny fiarovana", + "spend": "Fampitandremana momba ny fandaniana" }, "category": { "chatMessagesSubtitle": "valiny amin'ny resakao", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "vita / tsy nahomby / vonona hofehezina", "kiloclawActivitySubtitle": "dika mandeha vonona na tsy nahomby, hetsika voalahatra", "balanceAlertsSubtitle": "fampitandremana rehefa kely ny ambim-bolan'ny fikambanana", + "spendAlertsSubtitle": "fihoaram-bidy sy fitsambikinana isan'ora", + "spendAlertsToggle": "Sokajin'ny fampitandremana momba ny fandaniana", "securityFindingsSubtitle": "olana vaovao hita sy fampahatsiahivana SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Sokafy ny fotoam-piasana farany" + }, + "spendAlerts": { + "subtitle": "Alefaso fampitandremana amin'ny mailaka na push ho an'ny mpifandray amin'ny faktiora rehefa mihoatra fetra ny fandaniana amin'ity faritra ity na miakatra ambonin'ny taha isan'ora mahazatra.", + "enable": "Velomy ny fampitandremana momba ny fandaniana", + "thresholdTitle": "Fihoaram-bidy", + "thresholdSubtitle": "Mihoatra ny fetranao ny fandaniana mitohy", + "anomalyTitle": "Fitsambikinana isan'ora", + "anomalySubtitle": "Ny adiny iray dia ambony lavitra noho ny taha mahazatra amin'ity faritra ity", + "limitLabel": "Fetra (USD)", + "windowLabel": "Fe-potoana", + "window24h": "Adiny 24", + "window7d": "7 andro", + "window30d": "30 andro", + "multiplierLabel": "Mpamaro ny fitsambikinana", + "saved": "Voatahiry ny fikirakirana ny fampitandremana momba ny fandaniana", + "loadError": "Tsy afaka nampiditra ny fampitandremana momba ny fandaniana.", + "saveError": "Tsy afaka nitahiry ny fampitandremana momba ny fandaniana.", + "empty": "Mihidy ny fampitandremana momba ny fandaniana. Velomy izy ireo hampandre ny mpifandray amin'ny faktiora amin'ity faritra ity.", + "noSpend": "Mbola tsy misy fandaniana voarakitra", + "spend24h": "Fandaniana tao anatin'ny 24 ora farany", + "multiplierError": "Ampidiro mpamaro 1 na mihoatra", + "permissionDenied": "Tsy manana alalana hitantana ny fampitandremana momba ny fandaniana ianao.", + "pushOffByCategory": "Mihidy ao amin'ny Fampandrenesana" } } diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 90b667fde4..66613d3efe 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -827,7 +827,8 @@ "chat": "Ngā karere kōrerorero", "kiloclaw": "Ngā mahi KiloClaw", "balance": "Ngā matohi mō te toenga pūtea", - "security": "Ngā kitenga haumarutanga" + "security": "Ngā kitenga haumarutanga", + "spend": "Ngā matohi whakapaunga" }, "category": { "chatMessagesSubtitle": "ngā whakautu i ō kōrerorero", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "kua oti / i rahua / kua rite hei whakahaere māu", "kiloclawActivitySubtitle": "kua rite te tāruatanga / i rahua, ngā mahi kua whakaritea", "balanceAlertsSubtitle": "ngā whakatūpato mō te toenga iti o te rōpū", + "spendAlertsSubtitle": "ngā whitinga paepae me ngā pikinga ia hāora", + "spendAlertsToggle": "Kāwai matohi whakapaunga", "securityFindingsSubtitle": "ngā kitenga hōu me ngā whakamaumahara SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Whakatuwheratia te wātū whakamutunga" + }, + "spendAlerts": { + "subtitle": "Tukuna he matohi mā te īmēra, mā te push rānei ki ngā hoapā nama ina whiti te whakapaunga o tēnei rohe i tētahi paepae, ina piki rānei ki runga ake i tōna tere ia hāora.", + "enable": "Whakahohea ngā matohi whakapaunga", + "thresholdTitle": "Whitinga paepae", + "thresholdSubtitle": "Kei te whiti te whakapaunga haere tonu i tō paepae", + "anomalyTitle": "Pikinga ia hāora", + "anomalySubtitle": "Kei runga rawa tētahi hāora i te tere noa o tēnei rohe", + "limitLabel": "Paepae (USD)", + "windowLabel": "Matapihi", + "window24h": "24 hāora", + "window7d": "7 rā", + "window30d": "30 rā", + "multiplierLabel": "Whakarea pikinga", + "saved": "Kua tiakina ngā tautuhinga matohi whakapaunga", + "loadError": "Kāore i taea ngā matohi whakapaunga te uta.", + "saveError": "Kāore i taea ngā matohi whakapaunga te tiaki.", + "empty": "Kua weto ngā matohi whakapaunga. Whakahohea kia mōhio ai ngā hoapā nama o tēnei rohe.", + "noSpend": "Kāore anō kia tuhia he whakapaunga", + "spend24h": "Whakapaunga i ngā 24 hāora kua hipa", + "multiplierError": "Whakaurua he whakarea 1, nui ake rānei", + "permissionDenied": "Kāore ō whakaaetanga ki te whakahaere i ngā matohi whakapaunga.", + "pushOffByCategory": "Weto i roto i Ngā whakamōhiotanga" } } diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 1fa6aff998..0618dbda2d 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -827,7 +827,8 @@ "chat": "Пораки во разговорите", "kiloclaw": "Активност на KiloClaw", "balance": "Предупредувања за салдо", - "security": "Безбедносни наоди" + "security": "Безбедносни наоди", + "spend": "Предупредувања за трошење" }, "category": { "chatMessagesSubtitle": "одговори во твоите разговори", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "завршено / неуспешно / подготвено за контрола", "kiloclawActivitySubtitle": "инстанца подготвена/неуспешна, закажани дејства", "balanceAlertsSubtitle": "предупредувања за ниска состојба на организацијата", + "spendAlertsSubtitle": "преминувања на прагови и часовни скокови", + "spendAlertsToggle": "Категорија предупредувања за трошење", "securityFindingsSubtitle": "нови наоди и потсетници за SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Отвори ја последната сесија" + }, + "spendAlerts": { + "subtitle": "Испрати предупредување по е-пошта или push до контактите за наплата кога трошењето на овој опсег ќе премине лимит или ќе скокне над вообичаеното часовно темпо.", + "enable": "Овозможи предупредувања за трошење", + "thresholdTitle": "Преминување на праг", + "thresholdSubtitle": "Тековното трошење го надминува твојот лимит", + "anomalyTitle": "Часовен скок", + "anomalySubtitle": "Еден час е далеку над вообичаеното темпо на овој опсег", + "limitLabel": "Лимит (USD)", + "windowLabel": "Прозорец", + "window24h": "24 часа", + "window7d": "7 дена", + "window30d": "30 дена", + "multiplierLabel": "Множител на скокот", + "saved": "Поставките за предупредувања за трошење се зачувани", + "loadError": "Предупредувањата за трошење не можеа да се вчитаат.", + "saveError": "Предупредувањата за трошење не можеа да се зачуваат.", + "empty": "Предупредувањата за трошење се исклучени. Вклучи ги за да ги предупредиш контактите за наплата на овој опсег.", + "noSpend": "Сè уште нема евидентирано трошење", + "spend24h": "Трошење во последните 24 часа", + "multiplierError": "Внеси множител од 1 или повеќе", + "permissionDenied": "Немаш дозвола да управуваш со предупредувањата за трошење.", + "pushOffByCategory": "Исклучено во Известувања" } } diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 7ee7f29c50..f8d26d26f8 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -827,7 +827,8 @@ "chat": "ചാറ്റ് സന്ദേശങ്ങൾ", "kiloclaw": "KiloClaw പ്രവർത്തനം", "balance": "ബാലൻസ് മുന്നറിയിപ്പുകൾ", - "security": "സുരക്ഷാ കണ്ടെത്തലുകൾ" + "security": "സുരക്ഷാ കണ്ടെത്തലുകൾ", + "spend": "ചെലവ് മുന്നറിയിപ്പുകൾ" }, "category": { "chatMessagesSubtitle": "നിങ്ങളുടെ സംഭാഷണങ്ങളിലെ മറുപടികൾ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "പൂർത്തിയായി / പരാജയപ്പെട്ടു / നിയന്ത്രിക്കാൻ തയ്യാർ", "kiloclawActivitySubtitle": "ഇൻസ്റ്റൻസ് തയ്യാറാകൽ/പരാജയപ്പെടൽ, മുൻകൂട്ടി നിശ്ചയിച്ച പ്രവർത്തനങ്ങൾ", "balanceAlertsSubtitle": "ഓർഗനൈസേഷന്റെ ബാലൻസ് കുറയുമ്പോഴുള്ള മുന്നറിയിപ്പുകൾ", + "spendAlertsSubtitle": "പരിധി ലംഘനങ്ങളും മണിക്കൂർ തോറുമുള്ള വർധനകളും", + "spendAlertsToggle": "ചെലവ് മുന്നറിയിപ്പ് വിഭാഗം", "securityFindingsSubtitle": "പുതിയ കണ്ടെത്തലുകളും SLA ഓർമ്മപ്പെടുത്തലുകളും" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "അവസാന സെഷൻ തുറക്കുക" + }, + "spendAlerts": { + "subtitle": "ഈ സ്കോപ്പിന്റെ ചെലവ് ഒരു പരിധി കടക്കുമ്പോഴോ അതിന്റെ സാധാരണ മണിക്കൂർ നിരക്കിന് മുകളിൽ കയറുമ്പോഴോ ബില്ലിംഗ് കോൺടാക്റ്റുകൾക്ക് ഇമെയിൽ അല്ലെങ്കിൽ പുഷ് വഴി മുന്നറിയിപ്പ് അയയ്ക്കുക.", + "enable": "ചെലവ് മുന്നറിയിപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കുക", + "thresholdTitle": "പരിധി ലംഘനം", + "thresholdSubtitle": "നിലവിലെ ചെലവ് നിങ്ങളുടെ പരിധി കടക്കുന്നു", + "anomalyTitle": "മണിക്കൂർ വർധന", + "anomalySubtitle": "ഒരു മണിക്കൂർ ഈ സ്കോപ്പിന്റെ സാധാരണ നിരക്കിനേക്കാൾ വളരെ ഉയരത്തിലാണ്", + "limitLabel": "പരിധി (USD)", + "windowLabel": "കാലയളവ്", + "window24h": "24 മണിക്കൂർ", + "window7d": "7 ദിവസം", + "window30d": "30 ദിവസം", + "multiplierLabel": "വർധന ഗുണിതം", + "saved": "ചെലവ് മുന്നറിയിപ്പ് ക്രമീകരണങ്ങൾ സംരക്ഷിച്ചു", + "loadError": "ചെലവ് മുന്നറിയിപ്പുകൾ ലോഡ് ചെയ്യാനായില്ല.", + "saveError": "ചെലവ് മുന്നറിയിപ്പുകൾ സംരക്ഷിക്കാനായില്ല.", + "empty": "ചെലവ് മുന്നറിയിപ്പുകൾ ഓഫ് ആണ്. ഈ സ്കോപ്പിന്റെ ബില്ലിംഗ് കോൺടാക്റ്റുകളെ അറിയിക്കാൻ അവ ഓൺ ചെയ്യുക.", + "noSpend": "ഇതുവരെ ചെലവൊന്നും രേഖപ്പെടുത്തിയിട്ടില്ല", + "spend24h": "കഴിഞ്ഞ 24 മണിക്കൂറിലെ ചെലവ്", + "multiplierError": "1 അല്ലെങ്കിൽ അതിൽ കൂടുതലുള്ള ഗുണിതം നൽകുക", + "permissionDenied": "ചെലവ് മുന്നറിയിപ്പുകൾ നിയന്ത്രിക്കാൻ നിങ്ങൾക്ക് അനുമതിയില്ല.", + "pushOffByCategory": "അറിയിപ്പുകളിൽ ഓഫ്" } } diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 3e02c89ddc..69ae77b3ee 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -827,7 +827,8 @@ "chat": "Чатын мессеж", "kiloclaw": "KiloClaw-ийн үйл ажиллагаа", "balance": "Үлдэгдлийн анхааруулга", - "security": "Аюулгүй байдлын илэрсэн асуудал" + "security": "Аюулгүй байдлын илэрсэн асуудал", + "spend": "Зарцуулалтын анхааруулга" }, "category": { "chatMessagesSubtitle": "харилцан яриан дахь хариултууд", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "дууссан / амжилтгүй / удирдахад бэлэн", "kiloclawActivitySubtitle": "инстанц бэлэн/амжилтгүй, төлөвлөсөн үйлдлүүд", "balanceAlertsSubtitle": "байгууллагын үлдэгдэл багасах анхааруулга", + "spendAlertsSubtitle": "босго давсан болон цагийн огцом өсөлт", + "spendAlertsToggle": "Зарцуулалтын анхааруулгын ангилал", "securityFindingsSubtitle": "шинээр илэрсэн эмзэг байдал болон SLA сануулга" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Сүүлийн сессийг нээх" + }, + "spendAlerts": { + "subtitle": "Энэ хүрээний зарцуулалт хязгаараас хэтэрсэн эсвэл ердийн цагийн хурдаас дээш өссөн үед төлбөрийн холбоо барих хаягуудад имэйл эсвэл push-ээр анхааруулга илгээ.", + "enable": "Зарцуулалтын анхааруулгыг идэвхжүүлэх", + "thresholdTitle": "Босго давсан", + "thresholdSubtitle": "Гүйлтийн зарцуулалт таны хязгаараас хэтэрч байна", + "anomalyTitle": "Цагийн огцом өсөлт", + "anomalySubtitle": "Нэг цаг энэ хүрээний ердийн хурдаас хамаагүй өндөр байна", + "limitLabel": "Хязгаар (USD)", + "windowLabel": "Цонх", + "window24h": "24 цаг", + "window7d": "7 хоног", + "window30d": "30 хоног", + "multiplierLabel": "Огцом өсөлтийн үржүүлэгч", + "saved": "Зарцуулалтын анхааруулгын тохиргоо хадгалагдлаа", + "loadError": "Зарцуулалтын анхааруулгыг ачаалж чадсангүй.", + "saveError": "Зарцуулалтын анхааруулгыг хадгалж чадсангүй.", + "empty": "Зарцуулалтын анхааруулга унтарсан байна. Энэ хүрээний төлбөрийн хаягуудад мэдэгдэхийн тулд асаа.", + "noSpend": "Одоогоор зарцуулалт бүртгэгдээгүй", + "spend24h": "Сүүлийн 24 цагийн зарцуулалт", + "multiplierError": "1 буюу түүнээс их үржүүлэгч оруул", + "permissionDenied": "Зарцуулалтын анхааруулгыг удирдах эрх танд байхгүй.", + "pushOffByCategory": "Мэдэгдэлд унтарсан" } } diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index e1efac1047..0eaced0fda 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -827,7 +827,8 @@ "chat": "चॅटमधील संदेश", "kiloclaw": "KiloClaw मधील घडामोडी", "balance": "शिल्लक रकमेच्या सूचना", - "security": "सुरक्षा निष्कर्ष" + "security": "सुरक्षा निष्कर्ष", + "spend": "खर्चाच्या सूचना" }, "category": { "chatMessagesSubtitle": "तुमच्या संभाषणांमधील उत्तरे", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "पूर्ण / अयशस्वी / नियंत्रणासाठी तयार", "kiloclawActivitySubtitle": "इन्स्टन्स तयार/अयशस्वी, नियोजित क्रिया", "balanceAlertsSubtitle": "संस्थेची शिल्लक कमी झाल्याच्या सूचना", + "spendAlertsSubtitle": "मर्यादा ओलांडणे आणि प्रति तास वाढ", + "spendAlertsToggle": "खर्चाच्या सूचना श्रेणी", "securityFindingsSubtitle": "नवीन निष्कर्ष आणि SLA स्मरणपत्रे" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "शेवटचे सत्र उघडा" + }, + "spendAlerts": { + "subtitle": "या स्कोपचा खर्च मर्यादा ओलांडतो किंवा त्याच्या नेहमीच्या प्रति तास दरापेक्षा वर जातो तेव्हा बिलिंग संपर्कांना ईमेल किंवा पुशद्वारे सूचना पाठवा.", + "enable": "खर्चाच्या सूचना चालू करा", + "thresholdTitle": "मर्यादा ओलांडणे", + "thresholdSubtitle": "चालू खर्च तुमची मर्यादा ओलांडत आहे", + "anomalyTitle": "प्रति तास वाढ", + "anomalySubtitle": "एक तास या स्कोपच्या नेहमीच्या दरापेक्षा खूपच जास्त आहे", + "limitLabel": "मर्यादा (USD)", + "windowLabel": "कालावधी", + "window24h": "24 तास", + "window7d": "7 दिवस", + "window30d": "30 दिवस", + "multiplierLabel": "वाढीचा गुणक", + "saved": "खर्चाच्या सूचना सेटिंग्ज जतन केल्या", + "loadError": "खर्चाच्या सूचना लोड करता आल्या नाहीत.", + "saveError": "खर्चाच्या सूचना जतन करता आल्या नाहीत.", + "empty": "खर्चाच्या सूचना बंद आहेत. या स्कोपच्या बिलिंग संपर्कांना सूचित करण्यासाठी त्या चालू करा.", + "noSpend": "अजून कोणताही खर्च नोंदवला गेलेला नाही", + "spend24h": "गेल्या 24 तासांतील खर्च", + "multiplierError": "1 किंवा त्याहून मोठा गुणक टाका", + "permissionDenied": "तुम्हाला खर्चाच्या सूचना व्यवस्थापित करण्याची परवानगी नाही.", + "pushOffByCategory": "सूचनांमध्ये बंद" } } diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index d54a2f1e07..305fd62b7c 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -827,7 +827,8 @@ "chat": "Mesej sembang", "kiloclaw": "Aktiviti KiloClaw", "balance": "Amaran baki", - "security": "Dapatan keselamatan" + "security": "Dapatan keselamatan", + "spend": "Amaran perbelanjaan" }, "category": { "chatMessagesSubtitle": "balasan dalam perbualan anda", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "selesai / gagal / sedia untuk dikawal", "kiloclawActivitySubtitle": "instans sedia/gagal, tindakan berjadual", "balanceAlertsSubtitle": "amaran baki rendah untuk organisasi", + "spendAlertsSubtitle": "melepasi ambang dan lonjakan setiap jam", + "spendAlertsToggle": "Kategori amaran perbelanjaan", "securityFindingsSubtitle": "penemuan baharu dan peringatan SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Buka sesi terakhir" + }, + "spendAlerts": { + "subtitle": "Hantar amaran melalui e-mel atau push kepada kenalan pengebilan apabila perbelanjaan skop ini melepasi had atau melonjak melebihi kadar setiap jam biasanya.", + "enable": "Dayakan amaran perbelanjaan", + "thresholdTitle": "Melepasi ambang", + "thresholdSubtitle": "Perbelanjaan berjalan melepasi had anda", + "anomalyTitle": "Lonjakan setiap jam", + "anomalySubtitle": "Satu jam jauh melebihi kadar biasa skop ini", + "limitLabel": "Had (USD)", + "windowLabel": "Tetingkap", + "window24h": "24 jam", + "window7d": "7 hari", + "window30d": "30 hari", + "multiplierLabel": "Pengganda lonjakan", + "saved": "Tetapan amaran perbelanjaan disimpan", + "loadError": "Tidak dapat memuatkan amaran perbelanjaan.", + "saveError": "Tidak dapat menyimpan amaran perbelanjaan.", + "empty": "Amaran perbelanjaan dimatikan. Hidupkannya untuk memberitahu kenalan pengebilan skop ini.", + "noSpend": "Belum ada perbelanjaan direkodkan", + "spend24h": "Perbelanjaan dalam 24 jam terakhir", + "multiplierError": "Masukkan pengganda 1 atau lebih", + "permissionDenied": "Anda tidak mempunyai kebenaran untuk mengurus amaran perbelanjaan.", + "pushOffByCategory": "Dimatikan dalam Pemberitahuan" } } diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index d46f1c3382..fe9bffb251 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -875,7 +875,8 @@ "chat": "Messaġġi tat-taħdit", "kiloclaw": "Attività ta' KiloClaw", "balance": "Twissijiet tal-bilanċ", - "security": "Sejbiet tas-sigurtà" + "security": "Sejbiet tas-sigurtà", + "spend": "Twissijiet tal-infiq" }, "category": { "chatMessagesSubtitle": "tweġibiet fil-konversazzjonijiet tiegħek", @@ -887,6 +888,8 @@ "sessionStatusSubtitle": "tlestiet / falliet / lesta biex tikkontrollaha", "kiloclawActivitySubtitle": "istanza lesta jew falliet, azzjonijiet skedati", "balanceAlertsSubtitle": "twissijiet ta' bilanċ baxx tal-organizzazzjoni", + "spendAlertsSubtitle": "qbiż ta' limiti u żidiet fis-siegħa", + "spendAlertsToggle": "Kategorija tat-twissijiet tal-infiq", "securityFindingsSubtitle": "sejbiet ġodda u tfakkiriet tal-SLA" }, "action": { @@ -3328,5 +3331,28 @@ }, "launcher": { "openLastSession": "Iftaħ l-aħħar sessjoni" + }, + "spendAlerts": { + "subtitle": "Ibgħat twissija bil-posta elettronika jew push lill-kuntatti tal-ħlasijiet meta l-infiq ta' dan il-kamp jinqabeż limitu jew jogħla 'l fuq mir-rata tas-siegħa tas-soltu.", + "enable": "Attiva t-twissijiet tal-infiq", + "thresholdTitle": "Qbiż tal-limitu", + "thresholdSubtitle": "L-infiq attwali jaqbeż il-limitu tiegħek", + "anomalyTitle": "Żieda fis-siegħa", + "anomalySubtitle": "Siegħa hija 'l fuq ħafna mir-rata tas-soltu ta' dan il-kamp", + "limitLabel": "Limitu (USD)", + "windowLabel": "Tieqa", + "window24h": "24 siegħa", + "window7d": "7 ijiem", + "window30d": "30 jum", + "multiplierLabel": "Multiplikatur taż-żieda", + "saved": "Is-settings tat-twissijiet tal-infiq ġew salvati", + "loadError": "It-twissijiet tal-infiq ma setgħux jitgħabbew.", + "saveError": "It-twissijiet tal-infiq ma setgħux jiġu salvati.", + "empty": "It-twissijiet tal-infiq huma mitfija. Xegħelhom biex tavża lill-kuntatti tal-ħlasijiet ta' dan il-kamp.", + "noSpend": "Għadu ma ġie rreġistrat l-ebda infiq", + "spend24h": "Infiq fl-aħħar 24 siegħa", + "multiplierError": "Daħħal multiplikatur ta' 1 jew aktar", + "permissionDenied": "M'għandekx permess biex timmaniġġja t-twissijiet tal-infiq.", + "pushOffByCategory": "Mitfi fin-Notifiki" } } diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index bf67fc01e0..f539cde269 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -827,7 +827,8 @@ "chat": "စကားဝိုင်း မက်ဆေ့ချ်များ", "kiloclaw": "KiloClaw လုပ်ဆောင်ချက်များ", "balance": "လက်ကျန်သတိပေးချက်များ", - "security": "လုံခြုံရေးတွေ့ရှိချက်များ" + "security": "လုံခြုံရေးတွေ့ရှိချက်များ", + "spend": "အသုံးစရိတ် သတိပေးချက်များ" }, "category": { "chatMessagesSubtitle": "သင့်စကားဝိုင်းများရှိ တုံ့ပြန်ချက်များ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ပြီးဆုံး / မအောင်မြင် / ထိန်းချုပ်ရန် အသင့်", "kiloclawActivitySubtitle": "စနစ်အသင့်ဖြစ်မှု/မအောင်မြင်မှု၊ အချိန်သတ်မှတ်ထားသော လုပ်ဆောင်ချက်များ", "balanceAlertsSubtitle": "အဖွဲ့အစည်း၏ လက်ကျန်နည်းချိန် သတိပေးချက်များ", + "spendAlertsSubtitle": "သတ်မှတ်ချက် ကျော်လွန်မှုနှင့် နာရီအလိုက် မြင့်တက်မှု", + "spendAlertsToggle": "အသုံးစရိတ် သတိပေးချက် အမျိုးအစား", "securityFindingsSubtitle": "တွေ့ရှိချက်အသစ်များနှင့် SLA သတိပေးချက်များ" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "နောက်ဆုံးဆက်ရှင်ကို ဖွင့်ပါ" + }, + "spendAlerts": { + "subtitle": "ဤနယ်ပယ်၏ အသုံးစရိတ်သည် သတ်မှတ်ချက်တစ်ခုကို ကျော်လွန်သောအခါ သို့မဟုတ် ပုံမှန် နာရီအလိုက် နှုန်းထက် မြင့်တက်သောအခါ ငွေတောင်းခံမှု အဆက်အသွယ်များထံ အီးမေးလ် သို့မဟုတ် push ဖြင့် သတိပေးချက် ပို့ပါ။", + "enable": "အသုံးစရိတ် သတိပေးချက်များကို ဖွင့်ပါ", + "thresholdTitle": "သတ်မှတ်ချက် ကျော်လွန်မှု", + "thresholdSubtitle": "လက်ရှိ အသုံးစရိတ်သည် သင့် သတ်မှတ်ချက်ကို ကျော်လွန်နေသည်", + "anomalyTitle": "နာရီအလိုက် မြင့်တက်မှု", + "anomalySubtitle": "တစ်နာရီသည် ဤနယ်ပယ်၏ ပုံမှန် နှုန်းထက် အလွန်မြင့်နေသည်", + "limitLabel": "သတ်မှတ်ချက် (USD)", + "windowLabel": "အချိန်ကာလ", + "window24h": "24 နာရီ", + "window7d": "7 ရက်", + "window30d": "30 ရက်", + "multiplierLabel": "မြင့်တက်မှု အမြှောက်", + "saved": "အသုံးစရိတ် သတိပေးချက် ဆက်တင်များ သိမ်းဆည်းပြီးပါပြီ", + "loadError": "အသုံးစရိတ် သတိပေးချက်များကို ဖွင့်၍မရပါ။", + "saveError": "အသုံးစရိတ် သတိပေးချက်များကို သိမ်း၍မရပါ။", + "empty": "အသုံးစရိတ် သတိပေးချက်များ ပိတ်ထားသည်။ ဤနယ်ပယ်၏ ငွေတောင်းခံမှု အဆက်အသွယ်များကို သတိပေးရန် ၎င်းတို့ကို ဖွင့်ပါ။", + "noSpend": "အသုံးစရိတ် မှတ်တမ်းတင်ထားခြင်း မရှိသေးပါ", + "spend24h": "လွန်ခဲ့သော 24 နာရီအတွင်း အသုံးစရိတ်", + "multiplierError": "1 သို့မဟုတ် ထိုထက်ကြီးသော အမြှောက်ကို ထည့်ပါ", + "permissionDenied": "အသုံးစရိတ် သတိပေးချက်များကို စီမံရန် သင့်တွင် ခွင့်ပြုချက် မရှိပါ။", + "pushOffByCategory": "အသိပေးချက်များတွင် ပိတ်ထားသည်" } } diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 17afc3b1e8..ec35673234 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -827,7 +827,8 @@ "chat": "Samtalemeldinger", "kiloclaw": "KiloClaw-aktivitet", "balance": "Saldovarsler", - "security": "Sikkerhetsfunn" + "security": "Sikkerhetsfunn", + "spend": "Forbruksvarsler" }, "category": { "chatMessagesSubtitle": "svar i samtalene dine", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "fullført / mislykket / klar for fjernstyring", "kiloclawActivitySubtitle": "instans klar eller mislykket, planlagte handlinger", "balanceAlertsSubtitle": "advarsler om lav organisasjonssaldo", + "spendAlertsSubtitle": "grenseoverskridelser og topper per time", + "spendAlertsToggle": "Kategorien forbruksvarsler", "securityFindingsSubtitle": "nye funn og SLA-påminnelser" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Åpne siste økt" + }, + "spendAlerts": { + "subtitle": "Send et varsel på e-post eller push til faktureringskontaktene når forbruket i dette området overskrider en grense eller stiger over den vanlige timesraten.", + "enable": "Aktiver forbruksvarsler", + "thresholdTitle": "Grenseoverskridelse", + "thresholdSubtitle": "Løpende forbruk overskrider grensen din", + "anomalyTitle": "Topp per time", + "anomalySubtitle": "Én time er langt over dette områdets vanlige rate", + "limitLabel": "Grense (USD)", + "windowLabel": "Vindu", + "window24h": "24 timer", + "window7d": "7 dager", + "window30d": "30 dager", + "multiplierLabel": "Topp-multiplikator", + "saved": "Innstillingene for forbruksvarsler er lagret", + "loadError": "Forbruksvarslene kunne ikke lastes.", + "saveError": "Forbruksvarslene kunne ikke lagres.", + "empty": "Forbruksvarsler er av. Slå dem på for å varsle faktureringskontaktene i dette området.", + "noSpend": "Ingen forbruk registrert ennå", + "spend24h": "Forbruk de siste 24 timene", + "multiplierError": "Angi en multiplikator på 1 eller mer", + "permissionDenied": "Du har ikke tillatelse til å administrere forbruksvarsler.", + "pushOffByCategory": "Av i Varsler" } } diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 2e9bad6e62..36fdeb3649 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -827,7 +827,8 @@ "chat": "कुराकानीका सन्देशहरू", "kiloclaw": "KiloClaw का गतिविधिहरू", "balance": "मौज्दातसम्बन्धी चेतावनीहरू", - "security": "भेटिएका सुरक्षा समस्या" + "security": "भेटिएका सुरक्षा समस्या", + "spend": "खर्चका चेतावनीहरू" }, "category": { "chatMessagesSubtitle": "तपाईंका कुराकानीमा आएका उत्तरहरू", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "समाप्त / असफल / नियन्त्रण गर्न तयार", "kiloclawActivitySubtitle": "इन्स्ट्यान्स तयार वा असफल, निर्धारित कार्यहरू", "balanceAlertsSubtitle": "संस्थाको मौज्दात कम भएको चेतावनी", + "spendAlertsSubtitle": "सीमा नाघ्ने र प्रति घण्टा वृद्धि", + "spendAlertsToggle": "खर्चका चेतावनी श्रेणी", "securityFindingsSubtitle": "फेला परेका नयाँ सुरक्षा समस्या र SLA का सम्झना सूचनाहरू" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "पछिल्लो सत्र खोल्नुहोस्" + }, + "spendAlerts": { + "subtitle": "यो स्कोपको खर्च कुनै सीमा नाघ्दा वा यसको सामान्य प्रति घण्टाको दरभन्दा माथि बढ्दा बिलिङ सम्पर्कहरूलाई इमेल वा पुशमार्फत चेतावनी पठाउनुहोस्।", + "enable": "खर्चका चेतावनीहरू खोल्नुहोस्", + "thresholdTitle": "सीमा नाघ्नु", + "thresholdSubtitle": "चालु खर्च तपाईंको सीमा नाघिरहेको छ", + "anomalyTitle": "प्रति घण्टा वृद्धि", + "anomalySubtitle": "एक घण्टा यो स्कोपको सामान्य दरभन्दा धेरै माथि छ", + "limitLabel": "सीमा (USD)", + "windowLabel": "अवधि", + "window24h": "24 घण्टा", + "window7d": "7 दिन", + "window30d": "30 दिन", + "multiplierLabel": "वृद्धि गुणक", + "saved": "खर्च चेतावनीका सेटिङहरू सुरक्षित भए", + "loadError": "खर्चका चेतावनीहरू लोड गर्न सकिएन।", + "saveError": "खर्चका चेतावनीहरू सुरक्षित गर्न सकिएन।", + "empty": "खर्चका चेतावनीहरू बन्द छन्। यो स्कोपका बिलिङ सम्पर्कहरूलाई जानकारी दिन तिनीहरू खोल्नुहोस्।", + "noSpend": "अहिलेसम्म कुनै खर्च अभिलेख भएको छैन", + "spend24h": "पछिल्लो 24 घण्टाको खर्च", + "multiplierError": "1 वा त्यसभन्दा बढीको गुणक लेख्नुहोस्", + "permissionDenied": "तपाईंसँग खर्चका चेतावनीहरू व्यवस्थापन गर्ने अनुमति छैन।", + "pushOffByCategory": "सूचनाहरूमा बन्द" } } diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 60123d7ce6..00adbedc59 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -295,7 +295,8 @@ "chat": "Chatberichten", "kiloclaw": "KiloClaw-activiteit", "balance": "Saldowaarschuwingen", - "security": "Beveiligingsbevindingen" + "security": "Beveiligingsbevindingen", + "spend": "Bestedingswaarschuwingen" }, "category": { "chatMessagesSubtitle": "antwoorden in je gesprekken", @@ -307,6 +308,8 @@ "sessionStatusSubtitle": "voltooid / mislukt / klaar voor bediening", "kiloclawActivitySubtitle": "instantie klaar of mislukt, geplande acties", "balanceAlertsSubtitle": "waarschuwingen voor laag organisatiesaldo", + "spendAlertsSubtitle": "drempeloverschrijdingen en uurlijkse pieken", + "spendAlertsToggle": "Categorie bestedingswaarschuwingen", "securityFindingsSubtitle": "nieuwe bevindingen en SLA-herinneringen" }, "liveActivities": "Liveactiviteiten", @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Laatste sessie openen" + }, + "spendAlerts": { + "subtitle": "Stuur een e-mail- of pushmelding naar de factuurcontacten wanneer de besteding van dit bereik een limiet overschrijdt of boven het gebruikelijke uurtarief uitstijgt.", + "enable": "Bestedingswaarschuwingen inschakelen", + "thresholdTitle": "Drempeloverschrijding", + "thresholdSubtitle": "Lopende besteding overschrijdt je limiet", + "anomalyTitle": "Uurlijkse piek", + "anomalySubtitle": "Een uur ligt ver boven het gebruikelijke tarief van dit bereik", + "limitLabel": "Limiet (USD)", + "windowLabel": "Venster", + "window24h": "24 uur", + "window7d": "7 dagen", + "window30d": "30 dagen", + "multiplierLabel": "Piekvermenigvuldiger", + "saved": "Instellingen voor bestedingswaarschuwingen opgeslagen", + "loadError": "Bestedingswaarschuwingen konden niet worden geladen.", + "saveError": "Bestedingswaarschuwingen konden niet worden opgeslagen.", + "empty": "Bestedingswaarschuwingen staan uit. Zet ze aan om de factuurcontacten van dit bereik te waarschuwen.", + "noSpend": "Nog geen besteding geregistreerd", + "spend24h": "Besteding in de afgelopen 24 uur", + "multiplierError": "Voer een vermenigvuldiger van 1 of meer in", + "permissionDenied": "Je hebt geen toestemming om bestedingswaarschuwingen te beheren.", + "pushOffByCategory": "Uit in Meldingen" } } diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 997a05f80c..8ae16e0c12 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -827,7 +827,8 @@ "chat": "Ergaawwan marii", "kiloclaw": "Sochii KiloClaw", "balance": "Akeekkachiisa haftee maallaqaa", - "security": "Argannoowwan nageenyaa" + "security": "Argannoowwan nageenyaa", + "spend": "Akeekkachiisa baasii" }, "category": { "chatMessagesSubtitle": "deebiiwwan mariiwwan kee keessatti", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "xumurame / hin milkoofne / to'achuuf qophaa'e", "kiloclawActivitySubtitle": "fakkeenyi qophaa'e/hin milkoofne, gochaalee sagantaa qaban", "balanceAlertsSubtitle": "akeekkachiisa haftee dhaabbataa xiqqaa", + "spendAlertsSubtitle": "daarbii daangaa fi olka'iinsa sa'aatii", + "spendAlertsToggle": "Ramaddii akeekkachiisa baasii", "securityFindingsSubtitle": "argannoowwan haaraa fi yaadachiisa SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Turtii dhumaa bani" + }, + "spendAlerts": { + "subtitle": "Yeroo baasiin naannoo kanaa daangaa darbu ykn sadarkaa sa'aatii baramaa isaa ol ka'u, qunnamtiiwwan kaffaltiitti imeelii ykn push ergaa.", + "enable": "Akeekkachiisa baasii dandeessisi", + "thresholdTitle": "Daangaa darbuu", + "thresholdSubtitle": "Baasiin yeroo ammaa daangaa kee darbaa jira", + "anomalyTitle": "Olka'iinsa sa'aatii", + "anomalySubtitle": "Sa'aanni tokko sadarkaa baramaa naannoo kanaa caalaa baay'ee ol jira", + "limitLabel": "Daangaa (USD)", + "windowLabel": "Yeroo", + "window24h": "Sa'aatii 24", + "window7d": "Guyyaa 7", + "window30d": "Guyyaa 30", + "multiplierLabel": "Baay'isaa olka'iinsaa", + "saved": "Qindaa'inni akeekkachiisa baasii olkaa'ame", + "loadError": "Akeekkachiisa baasii fiduun hin danda'amne.", + "saveError": "Akeekkachiisa baasii olkaa'uun hin danda'amne.", + "empty": "Akeekkachiisni baasii cufaa jira. Qunnamtiiwwan kaffaltii naannoo kanaa akeekkachiisuuf isa bansi.", + "noSpend": "Amma ammaatti baasiin galmeeffame hin jiru", + "spend24h": "Baasii sa'aatii 24 darban keessatti", + "multiplierError": "Baay'isaa 1 ykn isaa ol galchi", + "permissionDenied": "Akeekkachiisa baasii bulchuuf hayyamni sitti hin jiru.", + "pushOffByCategory": "Beeksisota keessatti cufaa" } } diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 8f08e325aa..7fa12318a9 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -827,7 +827,8 @@ "chat": "ଚାଟ୍ ସନ୍ଦେଶ", "kiloclaw": "KiloClaw କାର୍ଯ୍ୟକଳାପ", "balance": "ଅବଶିଷ୍ଟ ରାଶି ସମ୍ବନ୍ଧୀୟ ସତର୍କ ସୂଚନା", - "security": "ଚିହ୍ନଟ ସୁରକ୍ଷା ସମସ୍ୟା" + "security": "ଚିହ୍ନଟ ସୁରକ୍ଷା ସମସ୍ୟା", + "spend": "ଖର୍ଚ୍ଚ ସତର୍କତା" }, "category": { "chatMessagesSubtitle": "ଆପଣଙ୍କ ବାର୍ତ୍ତାଳାପର ଉତ୍ତର", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ସମାପ୍ତ / ବିଫଳ / ନିୟନ୍ତ୍ରଣ ପାଇଁ ପ୍ରସ୍ତୁତ", "kiloclawActivitySubtitle": "ଇନଷ୍ଟାନ୍ସ ପ୍ରସ୍ତୁତ/ବିଫଳ, ନିର୍ଦ୍ଧାରିତ କାର୍ଯ୍ୟ", "balanceAlertsSubtitle": "ସଂଗଠନର କମ୍ ବ୍ୟାଲାନ୍ସ ସତର୍କତା", + "spendAlertsSubtitle": "ସୀମା ଅତିକ୍ରମ ଏବଂ ଘଣ୍ଟାପ୍ରତି ବୃଦ୍ଧି", + "spendAlertsToggle": "ଖର୍ଚ୍ଚ ସତର୍କତା ବର୍ଗ", "securityFindingsSubtitle": "ନୂଆ ସୁରକ୍ଷା ସମସ୍ୟା ଓ SLA ସ୍ମାରକ" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "ଶେଷ ସେସନ୍ ଖୋଲନ୍ତୁ" + }, + "spendAlerts": { + "subtitle": "ଏହି ସ୍କୋପର ଖର୍ଚ୍ଚ ଏକ ସୀମା ଅତିକ୍ରମ କଲେ କିମ୍ବା ଏହାର ସାଧାରଣ ଘଣ୍ଟାପ୍ରତି ହାରଠାରୁ ଉପରକୁ ଗଲେ ବିଲିଂ ସମ୍ପର୍କମାନଙ୍କୁ ଇମେଲ କିମ୍ବା ପୁଶ୍ ମାଧ୍ୟମରେ ସତର୍କତା ପଠାନ୍ତୁ।", + "enable": "ଖର୍ଚ୍ଚ ସତର୍କତା ଚାଲୁ କରନ୍ତୁ", + "thresholdTitle": "ସୀମା ଅତିକ୍ରମ", + "thresholdSubtitle": "ଚାଲୁ ଖର୍ଚ୍ଚ ଆପଣଙ୍କ ସୀମା ଅତିକ୍ରମ କରୁଛି", + "anomalyTitle": "ଘଣ୍ଟାପ୍ରତି ବୃଦ୍ଧି", + "anomalySubtitle": "ଏକ ଘଣ୍ଟା ଏହି ସ୍କୋପର ସାଧାରଣ ହାରଠାରୁ ବହୁତ ଉପରେ", + "limitLabel": "ସୀମା (USD)", + "windowLabel": "ଅବଧି", + "window24h": "24 ଘଣ୍ଟା", + "window7d": "7 ଦିନ", + "window30d": "30 ଦିନ", + "multiplierLabel": "ବୃଦ୍ଧି ଗୁଣକ", + "saved": "ଖର୍ଚ୍ଚ ସତର୍କତା ସେଟିଂସ୍ ସଞ୍ଚିତ ହେଲା", + "loadError": "ଖର୍ଚ୍ଚ ସତର୍କତା ଲୋଡ୍ ହୋଇପାରିଲା ନାହିଁ।", + "saveError": "ଖର୍ଚ୍ଚ ସତର୍କତା ସଞ୍ଚିତ ହୋଇପାରିଲା ନାହିଁ।", + "empty": "ଖର୍ଚ୍ଚ ସତର୍କତା ବନ୍ଦ ଅଛି। ଏହି ସ୍କୋପର ବିଲିଂ ସମ୍ପର୍କମାନଙ୍କୁ ସୂଚିତ କରିବାକୁ ସେଗୁଡ଼ିକୁ ଚାଲୁ କରନ୍ତୁ।", + "noSpend": "ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଖର୍ଚ୍ଚ ରେକର୍ଡ ହୋଇନାହିଁ", + "spend24h": "ଗତ 24 ଘଣ୍ଟାର ଖର୍ଚ୍ଚ", + "multiplierError": "1 କିମ୍ବା ତାହାଠାରୁ ଅଧିକ ଗୁଣକ ଦିଅନ୍ତୁ", + "permissionDenied": "ଖର୍ଚ୍ଚ ସତର୍କତା ପରିଚାଳନା କରିବାକୁ ଆପଣଙ୍କର ଅନୁମତି ନାହିଁ।", + "pushOffByCategory": "ବିଜ୍ଞପ୍ତିରେ ବନ୍ଦ" } } diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 045805ddcf..54501e1d21 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -827,7 +827,8 @@ "chat": "ਚੈਟ ਦੇ ਸੁਨੇਹੇ", "kiloclaw": "KiloClaw ਦੀ ਗਤੀਵਿਧੀ", "balance": "ਬਚੀ ਰਕਮ ਬਾਰੇ ਚੇਤਾਵਨੀਆਂ", - "security": "ਸੁਰੱਖਿਆ ਜਾਂਚ ਦੇ ਨਤੀਜੇ" + "security": "ਸੁਰੱਖਿਆ ਜਾਂਚ ਦੇ ਨਤੀਜੇ", + "spend": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ" }, "category": { "chatMessagesSubtitle": "ਤੁਹਾਡੀਆਂ ਗੱਲਬਾਤਾਂ ਵਿੱਚ ਜਵਾਬ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ਪੂਰਾ ਹੋਇਆ / ਅਸਫਲ / ਕੰਟਰੋਲ ਕਰਨ ਲਈ ਤਿਆਰ", "kiloclawActivitySubtitle": "ਇੰਸਟੈਂਸ ਤਿਆਰ/ਅਸਫਲ, ਨਿਰਧਾਰਤ ਕਾਰਵਾਈਆਂ", "balanceAlertsSubtitle": "ਸੰਸਥਾ ਦਾ ਬੈਲੰਸ ਘੱਟ ਹੋਣ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ", + "spendAlertsSubtitle": "ਸੀਮਾ ਟੱਪਣਾ ਅਤੇ ਹਰ ਘੰਟੇ ਵਾਧਾ", + "spendAlertsToggle": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਦੀ ਸ਼੍ਰੇਣੀ", "securityFindingsSubtitle": "ਨਵੀਆਂ ਸੁਰੱਖਿਆ ਸਮੱਸਿਆਵਾਂ ਅਤੇ SLA ਲਈ ਯਾਦ-ਦਹਾਨੀਆਂ" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "ਆਖਰੀ ਸੈਸ਼ਨ ਖੋਲ੍ਹੋ" + }, + "spendAlerts": { + "subtitle": "ਜਦੋਂ ਇਸ ਸਕੋਪ ਦਾ ਖਰਚਾ ਕੋਈ ਸੀਮਾ ਟੱਪ ਜਾਵੇ ਜਾਂ ਆਪਣੀ ਆਮ ਪ੍ਰਤੀ ਘੰਟਾ ਦਰ ਤੋਂ ਉੱਪਰ ਚੜ੍ਹ ਜਾਵੇ, ਤਾਂ ਬਿਲਿੰਗ ਸੰਪਰਕਾਂ ਨੂੰ ਈਮੇਲ ਜਾਂ ਪੁਸ਼ ਰਾਹੀਂ ਚੇਤਾਵਨੀ ਭੇਜੋ।", + "enable": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਚਾਲੂ ਕਰੋ", + "thresholdTitle": "ਸੀਮਾ ਟੱਪਣਾ", + "thresholdSubtitle": "ਚਾਲੂ ਖਰਚਾ ਤੁਹਾਡੀ ਸੀਮਾ ਟੱਪ ਰਿਹਾ ਹੈ", + "anomalyTitle": "ਪ੍ਰਤੀ ਘੰਟਾ ਵਾਧਾ", + "anomalySubtitle": "ਇੱਕ ਘੰਟਾ ਇਸ ਸਕੋਪ ਦੀ ਆਮ ਦਰ ਤੋਂ ਬਹੁਤ ਉੱਪਰ ਹੈ", + "limitLabel": "ਸੀਮਾ (USD)", + "windowLabel": "ਸਮਾਂ", + "window24h": "24 ਘੰਟੇ", + "window7d": "7 ਦਿਨ", + "window30d": "30 ਦਿਨ", + "multiplierLabel": "ਵਾਧੇ ਦਾ ਗੁਣਕ", + "saved": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀ ਸੈਟਿੰਗਾਂ ਸੰਭਾਲੀਆਂ ਗਈਆਂ", + "loadError": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਲੋਡ ਨਹੀਂ ਹੋ ਸਕੀਆਂ।", + "saveError": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਸੰਭਾਲੀਆਂ ਨਹੀਂ ਜਾ ਸਕੀਆਂ।", + "empty": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਬੰਦ ਹਨ। ਇਸ ਸਕੋਪ ਦੇ ਬਿਲਿੰਗ ਸੰਪਰਕਾਂ ਨੂੰ ਸੂਚਿਤ ਕਰਨ ਲਈ ਇਨ੍ਹਾਂ ਨੂੰ ਚਾਲੂ ਕਰੋ।", + "noSpend": "ਹਾਲੇ ਕੋਈ ਖਰਚਾ ਦਰਜ ਨਹੀਂ ਹੋਇਆ", + "spend24h": "ਪਿਛਲੇ 24 ਘੰਟਿਆਂ ਦਾ ਖਰਚਾ", + "multiplierError": "1 ਜਾਂ ਵੱਧ ਦਾ ਗੁਣਕ ਦਾਖਲ ਕਰੋ", + "permissionDenied": "ਤੁਹਾਨੂੰ ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਪ੍ਰਬੰਧਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।", + "pushOffByCategory": "ਸੂਚਨਾਵਾਂ ਵਿੱਚ ਬੰਦ" } } diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index a337fa6bcc..839d2535ec 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -323,7 +323,8 @@ "chat": "Wiadomości na czacie", "kiloclaw": "Aktywność KiloClaw", "balance": "Powiadomienia o saldzie", - "security": "Wykryte problemy z bezpieczeństwem" + "security": "Wykryte problemy z bezpieczeństwem", + "spend": "Alerty o wydatkach" }, "category": { "chatMessagesSubtitle": "odpowiedzi w twoich rozmowach", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "zakończona / nieudana / gotowa do sterowania", "kiloclawActivitySubtitle": "gotowość lub błąd instancji, zaplanowane działania", "balanceAlertsSubtitle": "ostrzeżenia o niskim saldzie organizacji", + "spendAlertsSubtitle": "przekroczenia progów i skoki godzinowe", + "spendAlertsToggle": "Kategoria alertów o wydatkach", "securityFindingsSubtitle": "nowe wykryte problemy i przypomnienia o terminach SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Otwórz ostatnią sesję" + }, + "spendAlerts": { + "subtitle": "Wyślij alert e-mailem lub powiadomieniem push do kontaktów rozliczeniowych, gdy wydatki tego zakresu przekroczą limit lub wzrosną powyżej zwykłego tempa godzinowego.", + "enable": "Włącz alerty o wydatkach", + "thresholdTitle": "Przekroczenie progu", + "thresholdSubtitle": "Bieżące wydatki przekraczają twój limit", + "anomalyTitle": "Skok godzinowy", + "anomalySubtitle": "Godzina jest znacznie powyżej zwykłego tempa tego zakresu", + "limitLabel": "Limit (USD)", + "windowLabel": "Okno", + "window24h": "24 godziny", + "window7d": "7 dni", + "window30d": "30 dni", + "multiplierLabel": "Mnożnik skoku", + "saved": "Zapisano ustawienia alertów o wydatkach", + "loadError": "Nie udało się wczytać alertów o wydatkach.", + "saveError": "Nie udało się zapisać alertów o wydatkach.", + "empty": "Alerty o wydatkach są wyłączone. Włącz je, aby ostrzegać kontakty rozliczeniowe tego zakresu.", + "noSpend": "Nie zarejestrowano jeszcze żadnych wydatków", + "spend24h": "Wydatki z ostatnich 24 godzin", + "multiplierError": "Podaj mnożnik równy 1 lub większy", + "permissionDenied": "Nie masz uprawnień do zarządzania alertami o wydatkach.", + "pushOffByCategory": "Wyłączone w Powiadomieniach" } } diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 5ca0b60f1a..c4fa77cbc3 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -827,7 +827,8 @@ "chat": "د خبرو اترو پیغامونه", "kiloclaw": "د KiloClaw فعالیت", "balance": "د بیلانس خبرتیاوې", - "security": "امنیتي موندنې" + "security": "امنیتي موندنې", + "spend": "د لګښت خبرتیاوې" }, "category": { "chatMessagesSubtitle": "ستاسو په خبرو اترو کې ځوابونه", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "بشپړ شوی / ناکام شوی / د کنټرول لپاره چمتو", "kiloclawActivitySubtitle": "چمتو یا ناکام انسټانس، مهالویش شوې کړنې", "balanceAlertsSubtitle": "د سازمان د کم بیلانس خبرداري", + "spendAlertsSubtitle": "د حدونو تېرېدل او ساعتي زیاتوالی", + "spendAlertsToggle": "د لګښت خبرتیاوو کټګوري", "securityFindingsSubtitle": "نوې موندنې او د SLA یادونې" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "وروستۍ ناسته پرانیزئ" + }, + "spendAlerts": { + "subtitle": "کله چې د دې ساحې لګښت له یو حد څخه تېر شي یا له خپل عادي ساعتي کچه پورته ووځي، د بلونو اړیکو ته د بریښنالیک یا push له لارې خبرتیا ولېږه.", + "enable": "د لګښت خبرتیاوې فعاله کړه", + "thresholdTitle": "د حد تېرېدل", + "thresholdSubtitle": "روان لګښت ستاسو حد تېروي", + "anomalyTitle": "ساعتي زیاتوالی", + "anomalySubtitle": "یوه ساعته د دې ساحې له عادي کچې ډېره لوړه ده", + "limitLabel": "حد (USD)", + "windowLabel": "کړکۍ", + "window24h": "24 ساعته", + "window7d": "7 ورځې", + "window30d": "30 ورځې", + "multiplierLabel": "د زیاتوالي ضریب", + "saved": "د لګښت خبرتیاوو تنظیمات خوندي شول", + "loadError": "د لګښت خبرتیاوې پورته نه شوې.", + "saveError": "د لګښت خبرتیاوې خوندي نه شوې.", + "empty": "د لګښت خبرتیاوې بندې دي. د دې ساحې د بلونو اړیکو ته د خبر ورکولو لپاره یې فعاله کړه.", + "noSpend": "تر دې دمه هیڅ لګښت ثبت شوی نه دی", + "spend24h": "د تېرو 24 ساعتونو لګښت", + "multiplierError": "د 1 یا ډېر ضریب ولیکه", + "permissionDenied": "تاسو د لګښت خبرتیاوو د اداره کولو اجازه نه لرئ.", + "pushOffByCategory": "په خبرتیاوو کې بند" } } diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 2cf6da5771..1df40ac579 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -259,7 +259,8 @@ "chat": "Mensagens da conversa", "kiloclaw": "Atividade do KiloClaw", "balance": "Alertas de saldo", - "security": "Problemas de segurança" + "security": "Problemas de segurança", + "spend": "Alertas de gastos" }, "liveActivities": "Atividades ao vivo", "liveActivitySubtitle": "Mostrar agentes ativos na tela de bloqueio", @@ -299,6 +300,8 @@ "sessionStatusSubtitle": "sessão concluída, com falha ou pronta para você controlar", "kiloclawActivitySubtitle": "instância pronta ou com falha, ações agendadas", "balanceAlertsSubtitle": "avisos de saldo baixo da organização", + "spendAlertsSubtitle": "ultrapassagens de limites e picos por hora", + "spendAlertsToggle": "Categoria de alertas de gastos", "securityFindingsSubtitle": "novos problemas de segurança e lembretes de SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Abrir última sessão" + }, + "spendAlerts": { + "subtitle": "Envie um alerta por e-mail ou notificação push aos contatos de cobrança quando os gastos deste escopo ultrapassarem um limite ou subirem acima do seu ritmo por hora habitual.", + "enable": "Ativar os alertas de gastos", + "thresholdTitle": "Ultrapassagem do limite", + "thresholdSubtitle": "Os gastos acumulados ultrapassam o seu limite", + "anomalyTitle": "Pico por hora", + "anomalySubtitle": "Uma hora está muito acima do ritmo habitual deste escopo", + "limitLabel": "Limite (USD)", + "windowLabel": "Janela", + "window24h": "24 horas", + "window7d": "7 dias", + "window30d": "30 dias", + "multiplierLabel": "Multiplicador do pico", + "saved": "Configurações dos alertas de gastos salvas", + "loadError": "Não foi possível carregar os alertas de gastos.", + "saveError": "Não foi possível salvar os alertas de gastos.", + "empty": "Os alertas de gastos estão desativados. Ative-os para avisar os contatos de cobrança deste escopo.", + "noSpend": "Nenhum gasto registrado ainda", + "spend24h": "Gastos nas últimas 24 horas", + "multiplierError": "Insira um multiplicador de 1 ou mais", + "permissionDenied": "Você não tem permissão para gerenciar os alertas de gastos.", + "pushOffByCategory": "Desativado em Notificações" } } diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 6a208f86cf..dd13c4bd5e 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -843,7 +843,8 @@ "chat": "Mensagens da conversa", "kiloclaw": "Atividade do KiloClaw", "balance": "Alertas de saldo", - "security": "Ocorrências de segurança" + "security": "Ocorrências de segurança", + "spend": "Alertas de gastos" }, "category": { "chatMessagesSubtitle": "respostas nas suas conversas", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "concluída / com erro / pronta para controlo", "kiloclawActivitySubtitle": "instância pronta ou com erro, ações agendadas", "balanceAlertsSubtitle": "avisos de saldo baixo da organização", + "spendAlertsSubtitle": "ultrapassagens de limites e picos horários", + "spendAlertsToggle": "Categoria de alertas de gastos", "securityFindingsSubtitle": "novas ocorrências e lembretes de SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Abrir última sessão" + }, + "spendAlerts": { + "subtitle": "Envie um alerta por e-mail ou notificação push aos contactos de faturação quando os gastos deste âmbito ultrapassarem um limite ou subirem acima do seu ritmo horário habitual.", + "enable": "Ativar os alertas de gastos", + "thresholdTitle": "Ultrapassagem do limite", + "thresholdSubtitle": "Os gastos acumulados ultrapassam o seu limite", + "anomalyTitle": "Pico horário", + "anomalySubtitle": "Uma hora está muito acima do ritmo habitual deste âmbito", + "limitLabel": "Limite (USD)", + "windowLabel": "Janela", + "window24h": "24 horas", + "window7d": "7 dias", + "window30d": "30 dias", + "multiplierLabel": "Multiplicador do pico", + "saved": "Definições dos alertas de gastos guardadas", + "loadError": "Não foi possível carregar os alertas de gastos.", + "saveError": "Não foi possível guardar os alertas de gastos.", + "empty": "Os alertas de gastos estão desativados. Ative-os para avisar os contactos de faturação deste âmbito.", + "noSpend": "Ainda não há gastos registados", + "spend24h": "Gastos nas últimas 24 horas", + "multiplierError": "Introduza um multiplicador de 1 ou mais", + "permissionDenied": "Não tem permissão para gerir os alertas de gastos.", + "pushOffByCategory": "Desativado em Notificações" } } diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index e156be479b..46f5d1321f 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -843,7 +843,8 @@ "chat": "Mesaje din conversații", "kiloclaw": "Activitate KiloClaw", "balance": "Alerte privind soldul", - "security": "Probleme de securitate detectate" + "security": "Probleme de securitate detectate", + "spend": "Alerte de cheltuieli" }, "category": { "chatMessagesSubtitle": "răspunsuri în conversațiile tale", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "finalizată / eșuată / pregătită pentru control", "kiloclawActivitySubtitle": "instanță pregătită sau eșuată, acțiuni programate", "balanceAlertsSubtitle": "avertismente de sold scăzut al organizației", + "spendAlertsSubtitle": "depășiri de praguri și vârfuri orare", + "spendAlertsToggle": "Categoria alerte de cheltuieli", "securityFindingsSubtitle": "constatări noi și mementouri SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Deschide ultima sesiune" + }, + "spendAlerts": { + "subtitle": "Trimite o alertă prin e-mail sau push către contactele de facturare când cheltuielile acestui domeniu depășesc o limită sau sar peste ritmul orar obișnuit.", + "enable": "Activează alertele de cheltuieli", + "thresholdTitle": "Depășirea pragului", + "thresholdSubtitle": "Cheltuielile cumulate depășesc limita ta", + "anomalyTitle": "Vârf orar", + "anomalySubtitle": "O oră este mult peste ritmul obișnuit al acestui domeniu", + "limitLabel": "Limită (USD)", + "windowLabel": "Fereastră", + "window24h": "24 de ore", + "window7d": "7 zile", + "window30d": "30 de zile", + "multiplierLabel": "Multiplicatorul vârfului", + "saved": "Setările alertelor de cheltuieli au fost salvate", + "loadError": "Alertele de cheltuieli nu au putut fi încărcate.", + "saveError": "Alertele de cheltuieli nu au putut fi salvate.", + "empty": "Alertele de cheltuieli sunt oprite. Activează-le pentru a-i avertiza pe contactele de facturare ale acestui domeniu.", + "noSpend": "Încă nu s-a înregistrat nicio cheltuială", + "spend24h": "Cheltuieli din ultimele 24 de ore", + "multiplierError": "Introdu un multiplicator de 1 sau mai mare", + "permissionDenied": "Nu ai permisiunea de a gestiona alertele de cheltuieli.", + "pushOffByCategory": "Oprit în Notificări" } } diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index d6771ee3ae..bc3a1fd88b 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -323,7 +323,8 @@ "chat": "Сообщения чата", "kiloclaw": "Активность KiloClaw", "balance": "Оповещения о балансе", - "security": "Замечания по безопасности" + "security": "Замечания по безопасности", + "spend": "Оповещения о расходах" }, "category": { "chatMessagesSubtitle": "ответы в вашей переписке", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "завершён / ошибка / готов к управлению", "kiloclawActivitySubtitle": "готовность и ошибки экземпляра, запланированные действия", "balanceAlertsSubtitle": "предупреждения о низком балансе организации", + "spendAlertsSubtitle": "превышения порогов и почасовые всплески", + "spendAlertsToggle": "Категория «Оповещения о расходах»", "securityFindingsSubtitle": "новые уязвимости и напоминания о сроках SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Открыть последний сеанс" + }, + "spendAlerts": { + "subtitle": "Отправляй оповещение по почте или push-уведомлением контактам для оплаты, когда расходы этой области превышают лимит или подскакивают выше обычного почасового темпа.", + "enable": "Включить оповещения о расходах", + "thresholdTitle": "Превышение порога", + "thresholdSubtitle": "Текущие расходы превышают ваш лимит", + "anomalyTitle": "Почасовой всплеск", + "anomalySubtitle": "Час значительно выше обычного темпа этой области", + "limitLabel": "Лимит (USD)", + "windowLabel": "Окно", + "window24h": "24 часа", + "window7d": "7 дней", + "window30d": "30 дней", + "multiplierLabel": "Множитель всплеска", + "saved": "Настройки оповещений о расходах сохранены", + "loadError": "Не удалось загрузить оповещения о расходах.", + "saveError": "Не удалось сохранить оповещения о расходах.", + "empty": "Оповещения о расходах выключены. Включите их, чтобы предупреждать контакты для оплаты этой области.", + "noSpend": "Расходы ещё не зафиксированы", + "spend24h": "Расходы за последние 24 часа", + "multiplierError": "Введите множитель 1 или больше", + "permissionDenied": "У вас нет прав на управление оповещениями о расходах.", + "pushOffByCategory": "Выключено в уведомлениях" } } diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index e368461bb5..9fd4688767 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -827,7 +827,8 @@ "chat": "කතාබස් පණිවිඩ", "kiloclaw": "KiloClaw ක්‍රියාකාරකම්", "balance": "ශේෂය පිළිබඳ ඇඟවීම්", - "security": "හඳුනාගත් ආරක්ෂක ගැටලු" + "security": "හඳුනාගත් ආරක්ෂක ගැටලු", + "spend": "වියදම් දැනුම්දීම්" }, "category": { "chatMessagesSubtitle": "ඔබේ සංවාදවල පිළිතුරු", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "අවසන් / අසාර්ථක / පාලනයට සූදානම්", "kiloclawActivitySubtitle": "පිටපත සූදානම් හෝ අසාර්ථක වීම, කාලසටහන්ගත ක්‍රියා", "balanceAlertsSubtitle": "සංවිධානයේ ශේෂය අඩු වීමේ දැනුම්දීම්", + "spendAlertsSubtitle": "සීමා ඉක්මවීම් සහ පැයකට ඇති උච්චතා", + "spendAlertsToggle": "වියදම් දැනුම්දීම් ප්‍රවර්ගය", "securityFindingsSubtitle": "නව සොයාගැනීම් සහ SLA මතක් කිරීම්" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "අවසන් සැසිය විවෘත කරන්න" + }, + "spendAlerts": { + "subtitle": "මෙම පරාසයේ වියදම සීමාවක් ඉක්මවන විට හෝ එහි සාමාන්‍ය පැයක අනුපාතයට වඩා ඉහළ ගිය විට බිල්පත් සම්බන්ධතාවලට ඊමේල් හෝ push මගින් දැනුම්දීමක් යවන්න.", + "enable": "වියදම් දැනුම්දීම් සක්‍රීය කරන්න", + "thresholdTitle": "සීමාව ඉක්මවීම", + "thresholdSubtitle": "වත්මන් වියදම ඔබේ සීමාව ඉක්මවයි", + "anomalyTitle": "පැයකට උච්චතාව", + "anomalySubtitle": "එක් පැයක් මෙම පරාසයේ සාමාන්‍ය අනුපාතයට වඩා බෙහෙවින් ඉහළ ය", + "limitLabel": "සීමාව (USD)", + "windowLabel": "කාල පරාසය", + "window24h": "පැය 24", + "window7d": "දින 7", + "window30d": "දින 30", + "multiplierLabel": "උච්චතා ගුණකය", + "saved": "වියදම් දැනුම්දීම් සැකසුම් සුරාගත්තා", + "loadError": "වියදම් දැනුම්දීම් පූරණය කළ නොහැකි විය.", + "saveError": "වියදම් දැනුම්දීම් සුරැකිය නොහැකි විය.", + "empty": "වියදම් දැනුම්දීම් ක්‍රියා විරහිතයි. මෙම පරාසයේ බිල්පත් සම්බන්ධතාවලට දැනුම් දීමට ඒවා ක්‍රියාත්මක කරන්න.", + "noSpend": "තවම වියදමක් වාර්තා වී නැත", + "spend24h": "පසුගිය පැය 24 තුළ වියදම", + "multiplierError": "1 හෝ ඊට වැඩි ගුණකයක් ඇතුළත් කරන්න", + "permissionDenied": "වියදම් දැනුම්දීම් කළමනාකරණය කිරීමට ඔබට අවසර නැත.", + "pushOffByCategory": "දැනුම්දීම් තුළ ක්‍රියා විරහිතයි" } } diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index c06611292d..02d9c0ac39 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -859,7 +859,8 @@ "chat": "Správy v čete", "kiloclaw": "Aktivita KiloClaw", "balance": "Upozornenia na zostatok", - "security": "Bezpečnostné zistenia" + "security": "Bezpečnostné zistenia", + "spend": "Upozornenia na výdavky" }, "category": { "chatMessagesSubtitle": "odpovede v tvojich konverzáciách", @@ -871,6 +872,8 @@ "sessionStatusSubtitle": "dokončená / zlyhala / pripravená na ovládanie", "kiloclawActivitySubtitle": "pripravená inštancia, zlyhanie inštancie, naplánované akcie", "balanceAlertsSubtitle": "upozornenia na nízky zostatok organizácie", + "spendAlertsSubtitle": "prekročenia prahov a hodinové špičky", + "spendAlertsToggle": "Kategória upozornení na výdavky", "securityFindingsSubtitle": "nové zistenia a pripomienky SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Otvoriť poslednú reláciu" + }, + "spendAlerts": { + "subtitle": "Pošli e-mailom alebo push oznámením upozornenie fakturačným kontaktom, keď výdavky tohto rozsahu prekročia limit alebo vyskočia nad obvyklú hodinovú sadzbu.", + "enable": "Povoliť upozornenia na výdavky", + "thresholdTitle": "Prekročenie prahu", + "thresholdSubtitle": "Priebežné výdavky prekračujú tvoj limit", + "anomalyTitle": "Hodinová špička", + "anomalySubtitle": "Hodina je výrazne nad obvyklou sadzbou tohto rozsahu", + "limitLabel": "Limit (USD)", + "windowLabel": "Okno", + "window24h": "24 hodín", + "window7d": "7 dní", + "window30d": "30 dní", + "multiplierLabel": "Násobiteľ špičky", + "saved": "Nastavenia upozornení na výdavky boli uložené", + "loadError": "Upozornenia na výdavky sa nepodarilo načítať.", + "saveError": "Upozornenia na výdavky sa nepodarilo uložiť.", + "empty": "Upozornenia na výdavky sú vypnuté. Zapni ich, aby si upozornil fakturačné kontakty tohto rozsahu.", + "noSpend": "Zatiaľ nie sú zaznamenané žiadne výdavky", + "spend24h": "Výdavky za posledných 24 hodín", + "multiplierError": "Zadaj násobiteľ 1 alebo väčší", + "permissionDenied": "Nemáš povolenie spravovať upozornenia na výdavky.", + "pushOffByCategory": "Vypnuté v Upozorneniach" } } diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index bbd34f2851..8b423cf862 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -859,7 +859,8 @@ "chat": "Sporočila v klepetu", "kiloclaw": "Dejavnost v KiloClaw", "balance": "Opozorila o stanju dobroimetja", - "security": "Varnostne ugotovitve" + "security": "Varnostne ugotovitve", + "spend": "Opozorila o porabi" }, "category": { "chatMessagesSubtitle": "odgovori v tvojih pogovorih", @@ -871,6 +872,8 @@ "sessionStatusSubtitle": "končano / neuspešno / pripravljeno za upravljanje", "kiloclawActivitySubtitle": "instanca pripravljena ali neuspešna, načrtovana dejanja", "balanceAlertsSubtitle": "opozorila o nizkem stanju organizacije", + "spendAlertsSubtitle": "preseganja pragov in urni skoki", + "spendAlertsToggle": "Kategorija opozoril o porabi", "securityFindingsSubtitle": "nove ugotovitve in opomniki SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Odpri zadnjo sejo" + }, + "spendAlerts": { + "subtitle": "Pošlji opozorilo po e-pošti ali push sporočilu obračunskim stikom, ko poraba tega obsega preseže omejitev ali skoči nad običajno urno stopnjo.", + "enable": "Omogoči opozorila o porabi", + "thresholdTitle": "Preseganje praga", + "thresholdSubtitle": "Tekoča poraba presega tvojo omejitev", + "anomalyTitle": "Urni skok", + "anomalySubtitle": "Ena ura je daleč nad običajno stopnjo tega obsega", + "limitLabel": "Omejitev (USD)", + "windowLabel": "Okno", + "window24h": "24 ur", + "window7d": "7 dni", + "window30d": "30 dni", + "multiplierLabel": "Množitelj skoka", + "saved": "Nastavitve opozoril o porabi so shranjene", + "loadError": "Opozoril o porabi ni bilo mogoče naložiti.", + "saveError": "Opozoril o porabi ni bilo mogoče shraniti.", + "empty": "Opozorila o porabi so izklopljena. Vklopi jih, da opozoriš obračunske stike tega obsega.", + "noSpend": "Poraba še ni zabeležena", + "spend24h": "Poraba v zadnjih 24 urah", + "multiplierError": "Vnesi množitelj 1 ali večji", + "permissionDenied": "Nimaš dovoljenja za upravljanje opozoril o porabi.", + "pushOffByCategory": "Izklopljeno v Obvestilih" } } diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index e295109d05..2349ff4af5 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -827,7 +827,8 @@ "chat": "Fariimaha sheekaysiga", "kiloclaw": "Hawlaha KiloClaw", "balance": "Digniinaha haraaga", - "security": "Natiijooyinka amniga" + "security": "Natiijooyinka amniga", + "spend": "Digniinaha kharashka" }, "category": { "chatMessagesSubtitle": "jawaabaha wadahadalladaada", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "dhammaaday / fashilmay / diyaar u ah in la xakameeyo", "kiloclawActivitySubtitle": "nuqul diyaar ah ama fashilmay, ficillo la jadwaleeyay", "balanceAlertsSubtitle": "digniinaha hadhaaga yar ee ururka", + "spendAlertsSubtitle": "dhaafitaanka xadka iyo kor u kaca saacadlaha", + "spendAlertsToggle": "Qaybta digniinaha kharashka", "securityFindingsSubtitle": "natiijooyin cusub iyo xusuusinno SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Fur kalfadhigii ugu dambeeyay" + }, + "spendAlerts": { + "subtitle": "U dir digniin iimayl ama push xiriirka biilasha marka kharashka goobtan uu dhaafo xad ama uu ka sarreeyo heerka saacadlaha caadiga ah.", + "enable": "Daar digniinaha kharashka", + "thresholdTitle": "Dhaafitaanka xadka", + "thresholdSubtitle": "Kharashka socda ayaa dhaafaya xadkaaga", + "anomalyTitle": "Kor u kaca saacadlaha", + "anomalySubtitle": "Hal saac aad ayuu uga sarreeyaa heerka caadiga ah ee goobtan", + "limitLabel": "Xad (USD)", + "windowLabel": "Muddo", + "window24h": "24 saac", + "window7d": "7 maalmood", + "window30d": "30 maalmood", + "multiplierLabel": "Isku-dhufashada kor u kaca", + "saved": "Dejinta digniinaha kharashka waa la kaydiyay", + "loadError": "Digniinaha kharashka lama soo dejin karin.", + "saveError": "Digniinaha kharashka lama kaydin karin.", + "empty": "Digniinaha kharashka waa damman yihiin. Daar si aad uga digniiso xiriirka biilasha goobtan.", + "noSpend": "Weli lama diiwaangelin kharash", + "spend24h": "Kharashka 24-kii saacadood ee ugu dambeeyay", + "multiplierError": "Geli isku-dhufasho 1 ama ka badan", + "permissionDenied": "Ma lihid oggolaansho aad ku maamusho digniinaha kharashka.", + "pushOffByCategory": "Waa damman Ogeysiisyada" } } diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 7878341478..33b31c7672 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -827,7 +827,8 @@ "chat": "Mesazhet e bisedës", "kiloclaw": "Aktiviteti i KiloClaw", "balance": "Njoftimet për gjendjen e llogarisë", - "security": "Gjetjet e sigurisë" + "security": "Gjetjet e sigurisë", + "spend": "Njoftime për shpenzimet" }, "category": { "chatMessagesSubtitle": "përgjigje në bisedat e tua", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "e përfunduar / e dështuar / gati për kontroll", "kiloclawActivitySubtitle": "instancë gati / e dështuar, veprime të planifikuara", "balanceAlertsSubtitle": "paralajmërime për gjendje të ulët të organizatës", + "spendAlertsSubtitle": "tejkalime pragjesh dhe rritje për orë", + "spendAlertsToggle": "Kategoria e njoftimeve për shpenzimet", "securityFindingsSubtitle": "gjetje të reja dhe kujtesa për SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Hap sesionin e fundit" + }, + "spendAlerts": { + "subtitle": "Dërgo një njoftim me email ose push te kontaktet e faturimit kur shpenzimet e këtij fushëveprimi kalojnë një kufi ose hidhen mbi ritmin e zakonshëm për orë.", + "enable": "Aktivizo njoftimet për shpenzimet", + "thresholdTitle": "Tejkalimi i pragut", + "thresholdSubtitle": "Shpenzimet rrjedhëse tejkalojnë kufirin tënd", + "anomalyTitle": "Rritje për orë", + "anomalySubtitle": "Një orë është shumë mbi ritmin e zakonshëm të këtij fushëveprimi", + "limitLabel": "Kufi (USD)", + "windowLabel": "Dritare", + "window24h": "24 orë", + "window7d": "7 ditë", + "window30d": "30 ditë", + "multiplierLabel": "Shumëzuesi i rritjes", + "saved": "Cilësimet e njoftimeve për shpenzimet u ruajtën", + "loadError": "Njoftimet për shpenzimet nuk u ngarkuan dot.", + "saveError": "Njoftimet për shpenzimet nuk u ruajtën dot.", + "empty": "Njoftimet për shpenzimet janë të fikura. Ndizni ato për të paralajmëruar kontaktet e faturimit të këtij fushëveprimi.", + "noSpend": "Nuk është regjistruar ende asnjë shpenzim", + "spend24h": "Shpenzimet në 24 orët e fundit", + "multiplierError": "Vendos një shumëzues 1 ose më shumë", + "permissionDenied": "Nuk ke leje për të menaxhuar njoftimet për shpenzimet.", + "pushOffByCategory": "Fikur në Njoftimet" } } diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 62a8ef081b..195488277a 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -843,7 +843,8 @@ "chat": "Poruke u ćaskanju", "kiloclaw": "Aktivnosti u KiloClaw-u", "balance": "Upozorenja o stanju kredita", - "security": "Bezbednosni nalazi" + "security": "Bezbednosni nalazi", + "spend": "Obaveštenja o potrošnji" }, "category": { "chatMessagesSubtitle": "odgovori u tvojim razgovorima", @@ -855,6 +856,8 @@ "sessionStatusSubtitle": "završeno / neuspešno / spremno za upravljanje", "kiloclawActivitySubtitle": "spremnost ili greška instance, zakazane radnje", "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi po satu", + "spendAlertsToggle": "Kategorija obaveštenja o potrošnji", "securityFindingsSubtitle": "novi nalazi i podsetnici za SLA" }, "action": { @@ -3242,5 +3245,28 @@ }, "launcher": { "openLastSession": "Otvori poslednju sesiju" + }, + "spendAlerts": { + "subtitle": "Pošalji obaveštenje e-poštom ili push porukom kontaktima za naplatu kada potrošnja ovog opsega pređe limit ili skoči iznad uobičajene satne stope.", + "enable": "Omogući obaveštenja o potrošnji", + "thresholdTitle": "Prekoračenje praga", + "thresholdSubtitle": "Tekuća potrošnja prelazi tvoj limit", + "anomalyTitle": "Satni skok", + "anomalySubtitle": "Jedan sat je daleko iznad uobičajene stope ovog opsega", + "limitLabel": "Limit (USD)", + "windowLabel": "Prozor", + "window24h": "24 sata", + "window7d": "7 dana", + "window30d": "30 dana", + "multiplierLabel": "Množilac skoka", + "saved": "Podešavanja obaveštenja o potrošnji su sačuvana", + "loadError": "Obaveštenja o potrošnji nisu učitana.", + "saveError": "Obaveštenja o potrošnji nisu sačuvana.", + "empty": "Obaveštenja o potrošnji su isključena. Uključi ih da upozoriš kontakte za naplatu ovog opsega.", + "noSpend": "Još nema zabeležene potrošnje", + "spend24h": "Potrošnja u poslednja 24 sata", + "multiplierError": "Unesi množilac 1 ili veći", + "permissionDenied": "Nemaš dozvolu da upravljaš obaveštenjima o potrošnji.", + "pushOffByCategory": "Isključeno u Obaveštenjima" } } diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index b5b7469753..c3744b9617 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -827,7 +827,8 @@ "chat": "Chattmeddelanden", "kiloclaw": "KiloClaw-aktivitet", "balance": "Saldovarningar", - "security": "Säkerhetsanmärkningar" + "security": "Säkerhetsanmärkningar", + "spend": "Utgiftsvarningar" }, "category": { "chatMessagesSubtitle": "svar i dina konversationer", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "avslutad / misslyckad / redo att styras", "kiloclawActivitySubtitle": "instans klar eller misslyckad, schemalagda åtgärder", "balanceAlertsSubtitle": "varningar om lågt organisationssaldo", + "spendAlertsSubtitle": "gränsöverskridanden och toppar per timme", + "spendAlertsToggle": "Kategorin utgiftsvarningar", "securityFindingsSubtitle": "nya säkerhetsfynd och SLA-påminnelser" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Öppna senaste sessionen" + }, + "spendAlerts": { + "subtitle": "Skicka en varning via e-post eller push till faktureringskontakterna när detta områdes utgifter överskrider en gräns eller stiger över den vanliga timtakten.", + "enable": "Aktivera utgiftsvarningar", + "thresholdTitle": "Gränsöverskridande", + "thresholdSubtitle": "Löpande utgifter överskrider din gräns", + "anomalyTitle": "Topp per timme", + "anomalySubtitle": "En timme ligger långt över detta områdes vanliga takt", + "limitLabel": "Gräns (USD)", + "windowLabel": "Fönster", + "window24h": "24 timmar", + "window7d": "7 dagar", + "window30d": "30 dagar", + "multiplierLabel": "Toppmultiplikator", + "saved": "Inställningarna för utgiftsvarningar har sparats", + "loadError": "Utgiftsvarningarna kunde inte läsas in.", + "saveError": "Utgiftsvarningarna kunde inte sparas.", + "empty": "Utgiftsvarningar är av. Slå på dem för att varna detta områdes faktureringskontakter.", + "noSpend": "Ingen utgift registrerad ännu", + "spend24h": "Utgifter de senaste 24 timmarna", + "multiplierError": "Ange en multiplikator på 1 eller mer", + "permissionDenied": "Du har inte behörighet att hantera utgiftsvarningar.", + "pushOffByCategory": "Av i Aviseringar" } } diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index ba314877cc..7a0391a4e3 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -827,7 +827,8 @@ "chat": "Ujumbe wa mazungumzo", "kiloclaw": "Shughuli za KiloClaw", "balance": "Tahadhari za salio", - "security": "Matokeo ya ukaguzi wa usalama" + "security": "Matokeo ya ukaguzi wa usalama", + "spend": "Tahadhari za matumizi" }, "category": { "chatMessagesSubtitle": "majibu kwenye mazungumzo yako", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "kimekamilika / kimeshindikana / kiko tayari kudhibitiwa", "kiloclawActivitySubtitle": "nakala iko tayari au imeshindikana, vitendo vilivyoratibiwa", "balanceAlertsSubtitle": "maonyo ya salio la chini la shirika", + "spendAlertsSubtitle": "kuvuka vizingiti na kuongezeka kwa saa", + "spendAlertsToggle": "Aina ya tahadhari za matumizi", "securityFindingsSubtitle": "matokeo mapya na vikumbusho vya SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Fungua kikao cha mwisho" + }, + "spendAlerts": { + "subtitle": "Tuma tahadhari kwa barua pepe au push kwa anwani za bili wakati matumizi ya wigo huu yanapita kikomo au yanapanda juu ya kiwango chake cha kawaida cha saa.", + "enable": "Washa tahadhari za matumizi", + "thresholdTitle": "Kuvuka kizingiti", + "thresholdSubtitle": "Matumizi yanayoendelea yanapita kikomo chako", + "anomalyTitle": "Kuongezeka kwa saa", + "anomalySubtitle": "Saa moja iko juu sana kuliko kiwango cha kawaida cha wigo huu", + "limitLabel": "Kikomo (USD)", + "windowLabel": "Kipindi", + "window24h": "Saa 24", + "window7d": "Siku 7", + "window30d": "Siku 30", + "multiplierLabel": "Kizidishi cha kuongezeka", + "saved": "Mipangilio ya tahadhari za matumizi imehifadhiwa", + "loadError": "Tahadhari za matumizi hazikuweza kupakiwa.", + "saveError": "Tahadhari za matumizi hazikuweza kuhifadhiwa.", + "empty": "Tahadhari za matumizi zimezimwa. Ziwasha ili kuwatahadharisha anwani za bili za wigo huu.", + "noSpend": "Hakuna matumizi yaliyorekodiwa bado", + "spend24h": "Matumizi katika saa 24 zilizopita", + "multiplierError": "Weka kizidishi cha 1 au zaidi", + "permissionDenied": "Huna ruhusa ya kusimamia tahadhari za matumizi.", + "pushOffByCategory": "Imezimwa katika Arifa" } } diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index de1d75ffe9..b68837a039 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -827,7 +827,8 @@ "chat": "அரட்டைச் செய்திகள்", "kiloclaw": "KiloClaw செயல்பாடு", "balance": "கணக்கு இருப்பு எச்சரிக்கைகள்", - "security": "கண்டறிந்த பாதுகாப்புச் சிக்கல்கள்" + "security": "கண்டறிந்த பாதுகாப்புச் சிக்கல்கள்", + "spend": "செலவு எச்சரிக்கைகள்" }, "category": { "chatMessagesSubtitle": "உங்கள் உரையாடல்களில் வரும் பதில்கள்", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "முடிந்தது / தோல்வியடைந்தது / கட்டுப்படுத்தத் தயார்", "kiloclawActivitySubtitle": "இயக்கப் பிரதி தயார் / தோல்வி, திட்டமிட்ட செயல்கள்", "balanceAlertsSubtitle": "நிறுவனத்தின் இருப்பு குறையும்போது வரும் எச்சரிக்கைகள்", + "spendAlertsSubtitle": "வரம்பு மீறல்கள் மற்றும் மணிநேர உயர்வுகள்", + "spendAlertsToggle": "செலவு எச்சரிக்கை வகை", "securityFindingsSubtitle": "புதிய கண்டறிதல்கள் மற்றும் SLA நினைவூட்டல்கள்" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "கடைசி அமர்வைத் திறக்கவும்" + }, + "spendAlerts": { + "subtitle": "இந்த நோக்கத்தின் செலவு ஒரு வரம்பை மீறும்போது அல்லது அதன் வழக்கமான மணிநேர விகிதத்திற்கு மேல் உயரும்போது பில்லிங் தொடர்புகளுக்கு மின்னஞ்சல் அல்லது புஷ் மூலம் எச்சரிக்கை அனுப்பு.", + "enable": "செலவு எச்சரிக்கைகளை இயக்கு", + "thresholdTitle": "வரம்பு மீறல்", + "thresholdSubtitle": "தற்போதைய செலவு உங்கள் வரம்பை மீறுகிறது", + "anomalyTitle": "மணிநேர உயர்வு", + "anomalySubtitle": "ஒரு மணிநேரம் இந்த நோக்கத்தின் வழக்கமான விகிதத்தை விட மிக அதிகம்", + "limitLabel": "வரம்பு (USD)", + "windowLabel": "கால அளவு", + "window24h": "24 மணிநேரம்", + "window7d": "7 நாட்கள்", + "window30d": "30 நாட்கள்", + "multiplierLabel": "உயர்வு பெருக்கி", + "saved": "செலவு எச்சரிக்கை அமைப்புகள் சேமிக்கப்பட்டன", + "loadError": "செலவு எச்சரிக்கைகளை ஏற்ற முடியவில்லை.", + "saveError": "செலவு எச்சரிக்கைகளை சேமிக்க முடியவில்லை.", + "empty": "செலவு எச்சரிக்கைகள் அணைக்கப்பட்டுள்ளன. இந்த நோக்கத்தின் பில்லிங் தொடர்புகளுக்கு எச்சரிக்க அவற்றை இயக்கு.", + "noSpend": "இதுவரை எந்த செலவும் பதிவாகவில்லை", + "spend24h": "கடந்த 24 மணிநேர செலவு", + "multiplierError": "1 அல்லது அதற்கு மேற்பட்ட பெருக்கியை உள்ளிடு", + "permissionDenied": "செலவு எச்சரிக்கைகளை நிர்வகிக்க உங்களுக்கு அனுமதி இல்லை.", + "pushOffByCategory": "அறிவிப்புகளில் அணைக்கப்பட்டது" } } diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 74009f25bf..f503e51278 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -827,7 +827,8 @@ "chat": "చాట్ సందేశాలు", "kiloclaw": "KiloClaw కార్యకలాపాలు", "balance": "బ్యాలెన్స్ హెచ్చరికలు", - "security": "గుర్తించిన భద్రతా సమస్యలు" + "security": "గుర్తించిన భద్రతా సమస్యలు", + "spend": "ఖర్చు హెచ్చరికలు" }, "category": { "chatMessagesSubtitle": "మీ సంభాషణలలో ప్రత్యుత్తరాలు", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "పూర్తయింది / విఫలమైంది / నియంత్రించడానికి సిద్ధం", "kiloclawActivitySubtitle": "ఇన్‌స్టాన్స్ సిద్ధం/విఫలం, నిర్ణీత సమయాల్లో జరిగే చర్యలు", "balanceAlertsSubtitle": "సంస్థ బ్యాలెన్స్ తగ్గినప్పుడు హెచ్చరికలు", + "spendAlertsSubtitle": "పరిమితి దాటడం మరియు గంటకు పెరుగుదల", + "spendAlertsToggle": "ఖర్చు హెచ్చరికల వర్గం", "securityFindingsSubtitle": "కొత్త భద్రతా సమస్యలు, SLA రిమైండర్లు" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "చివరి సెషన్ను తెరవండి" + }, + "spendAlerts": { + "subtitle": "ఈ పరిధి ఖర్చు పరిమితిని దాటినప్పుడు లేదా దాని సాధారణ గంటవారీ రేటు కంటే పైకి ఎగిసినప్పుడు బిల్లింగ్ పరిచయాలకు ఇమెయిల్ లేదా పుష్ ద్వారా హెచ్చరిక పంపండి.", + "enable": "ఖర్చు హెచ్చరికలను ప్రారంభించండి", + "thresholdTitle": "పరిమితి దాటడం", + "thresholdSubtitle": "ప్రస్తుత ఖర్చు మీ పరిమితిని దాటుతోంది", + "anomalyTitle": "గంటవారీ పెరుగుదల", + "anomalySubtitle": "ఒక గంట ఈ పరిధి సాధారణ రేటు కంటే చాలా ఎక్కువ", + "limitLabel": "పరిమితి (USD)", + "windowLabel": "కాలవ్యవధి", + "window24h": "24 గంటలు", + "window7d": "7 రోజులు", + "window30d": "30 రోజులు", + "multiplierLabel": "పెరుగుదల గుణకం", + "saved": "ఖర్చు హెచ్చరిక సెట్టింగ్‌లు సేవ్ చేయబడ్డాయి", + "loadError": "ఖర్చు హెచ్చరికలు లోడ్ కాలేదు.", + "saveError": "ఖర్చు హెచ్చరికలు సేవ్ కాలేదు.", + "empty": "ఖర్చు హెచ్చరికలు ఆఫ్‌లో ఉన్నాయి. ఈ పరిధి బిల్లింగ్ పరిచయాలను హెచ్చరించడానికి వాటిని ఆన్ చేయండి.", + "noSpend": "ఇంకా ఖర్చు నమోదు కాలేదు", + "spend24h": "గత 24 గంటల ఖర్చు", + "multiplierError": "1 లేదా అంతకంటే ఎక్కువ గుణకాన్ని నమోదు చేయండి", + "permissionDenied": "ఖర్చు హెచ్చరికలను నిర్వహించడానికి మీకు అనుమతి లేదు.", + "pushOffByCategory": "నోటిఫికేషన్లలో ఆఫ్" } } diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index e37cfed0ea..e8bf2dee05 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -827,7 +827,8 @@ "chat": "ข้อความแชท", "kiloclaw": "กิจกรรม KiloClaw", "balance": "การแจ้งเตือนยอดเงิน", - "security": "ข้อค้นพบด้านความปลอดภัย" + "security": "ข้อค้นพบด้านความปลอดภัย", + "spend": "การแจ้งเตือนการใช้จ่าย" }, "category": { "chatMessagesSubtitle": "การตอบกลับในการสนทนาของคุณ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "เสร็จสิ้น / ล้มเหลว / พร้อมควบคุม", "kiloclawActivitySubtitle": "อินสแตนซ์พร้อมใช้งานหรือทำงานล้มเหลว และการดำเนินการตามกำหนดเวลา", "balanceAlertsSubtitle": "คำเตือนเมื่อยอดคงเหลือขององค์กรต่ำ", + "spendAlertsSubtitle": "การเกินเกณฑ์และการพุ่งขึ้นรายชั่วโมง", + "spendAlertsToggle": "หมวดหมู่การแจ้งเตือนการใช้จ่าย", "securityFindingsSubtitle": "ปัญหาที่ตรวจพบใหม่และการเตือนตาม SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "เปิดเซสชันล่าสุด" + }, + "spendAlerts": { + "subtitle": "ส่งการแจ้งเตือนทางอีเมลหรือพุชให้ผู้ติดต่อด้านการเรียกเก็บเงินเมื่อการใช้จ่ายของขอบเขตนี้เกินขีดจำกัดหรือพุ่งสูงกว่าอัตรารายชั่วโมงปกติ", + "enable": "เปิดใช้การแจ้งเตือนการใช้จ่าย", + "thresholdTitle": "การเกินเกณฑ์", + "thresholdSubtitle": "การใช้จ่ายสะสมเกินขีดจำกัดของคุณ", + "anomalyTitle": "การพุ่งขึ้นรายชั่วโมง", + "anomalySubtitle": "หนึ่งชั่วโมงสูงกว่าอัตราปกติของขอบเขตนี้มาก", + "limitLabel": "ขีดจำกัด (USD)", + "windowLabel": "ช่วงเวลา", + "window24h": "24 ชั่วโมง", + "window7d": "7 วัน", + "window30d": "30 วัน", + "multiplierLabel": "ตัวคูณการพุ่งขึ้น", + "saved": "บันทึกการตั้งค่าการแจ้งเตือนการใช้จ่ายแล้ว", + "loadError": "ไม่สามารถโหลดการแจ้งเตือนการใช้จ่ายได้", + "saveError": "ไม่สามารถบันทึกการแจ้งเตือนการใช้จ่ายได้", + "empty": "การแจ้งเตือนการใช้จ่ายปิดอยู่ เปิดใช้งานเพื่อแจ้งเตือนผู้ติดต่อด้านการเรียกเก็บเงินของขอบเขตนี้", + "noSpend": "ยังไม่มีการบันทึกการใช้จ่าย", + "spend24h": "การใช้จ่ายใน 24 ชั่วโมงที่ผ่านมา", + "multiplierError": "ป้อนตัวคูณ 1 หรือมากกว่า", + "permissionDenied": "คุณไม่มีสิทธิ์จัดการการแจ้งเตือนการใช้จ่าย", + "pushOffByCategory": "ปิดในการแจ้งเตือน" } } diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index a9e511f3ca..bae5b4e9b5 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -323,7 +323,8 @@ "chat": "Sohbet mesajları", "kiloclaw": "KiloClaw etkinliği", "balance": "Bakiye uyarıları", - "security": "Güvenlik bulguları" + "security": "Güvenlik bulguları", + "spend": "Harcama uyarıları" }, "category": { "chatMessagesSubtitle": "konuşmalarındaki yanıtlar", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "bitti / başarısız / kontrol edilmeye hazır", "kiloclawActivitySubtitle": "örnek hazır/başarısız, zamanlanmış eylemler", "balanceAlertsSubtitle": "düşük kuruluş bakiyesi uyarıları", + "spendAlertsSubtitle": "eşik aşımları ve saatlik artışlar", + "spendAlertsToggle": "Harcama uyarıları kategorisi", "securityFindingsSubtitle": "yeni bulgular ve SLA hatırlatmaları" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Son oturumu aç" + }, + "spendAlerts": { + "subtitle": "Bu kapsamın harcaması bir sınırı aştığında veya olağan saatlik hızının üzerine çıktığında faturalandırma kişilerine e-posta veya push ile uyarı gönder.", + "enable": "Harcama uyarılarını etkinleştir", + "thresholdTitle": "Eşik aşımı", + "thresholdSubtitle": "Cari harcama limitini aşıyor", + "anomalyTitle": "Saatlik artış", + "anomalySubtitle": "Bir saat bu kapsamın olağan hızının çok üzerinde", + "limitLabel": "Limit (USD)", + "windowLabel": "Pencere", + "window24h": "24 saat", + "window7d": "7 gün", + "window30d": "30 gün", + "multiplierLabel": "Artış çarpanı", + "saved": "Harcama uyarısı ayarları kaydedildi", + "loadError": "Harcama uyarıları yüklenemedi.", + "saveError": "Harcama uyarıları kaydedilemedi.", + "empty": "Harcama uyarıları kapalı. Bu kapsamın faturalandırma kişilerini uyarmak için aç.", + "noSpend": "Henüz harcama kaydedilmedi", + "spend24h": "Son 24 saatteki harcama", + "multiplierError": "1 veya daha büyük bir çarpan gir", + "permissionDenied": "Harcama uyarılarını yönetme iznin yok.", + "pushOffByCategory": "Bildirimler'de kapalı" } } diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index d04b5f2208..973d2ca34a 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -323,7 +323,8 @@ "chat": "Повідомлення в чаті", "kiloclaw": "Активність KiloClaw", "balance": "Сповіщення про баланс", - "security": "Виявлені проблеми безпеки" + "security": "Виявлені проблеми безпеки", + "spend": "Сповіщення про витрати" }, "category": { "chatMessagesSubtitle": "відповіді у ваших розмовах", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "завершено / помилка / готовність до керування", "kiloclawActivitySubtitle": "готовність або помилка екземпляра, заплановані дії", "balanceAlertsSubtitle": "попередження про низький баланс організації", + "spendAlertsSubtitle": "перевищення порогів і погодинні сплески", + "spendAlertsToggle": "Категорія «Сповіщення про витрати»", "securityFindingsSubtitle": "нові виявлені проблеми та нагадування про SLA" }, "action": { @@ -3285,5 +3288,28 @@ }, "launcher": { "openLastSession": "Відкрити останній сеанс" + }, + "spendAlerts": { + "subtitle": "Надсилай сповіщення електронною поштою або push-повідомленням контактам для оплати, коли витрати цієї області перевищують ліміт або стрибають вище звичайного погодинного темпу.", + "enable": "Увімкнути сповіщення про витрати", + "thresholdTitle": "Перевищення порогу", + "thresholdSubtitle": "Поточні витрати перевищують ваш ліміт", + "anomalyTitle": "Погодинний сплеск", + "anomalySubtitle": "Година значно вище звичайного темпу цієї області", + "limitLabel": "Ліміт (USD)", + "windowLabel": "Вікно", + "window24h": "24 години", + "window7d": "7 днів", + "window30d": "30 днів", + "multiplierLabel": "Множник сплеску", + "saved": "Налаштування сповіщень про витрати збережено", + "loadError": "Не вдалося завантажити сповіщення про витрати.", + "saveError": "Не вдалося зберегти сповіщення про витрати.", + "empty": "Сповіщення про витрати вимкнено. Увімкніть їх, щоб попереджати контакти для оплати цієї області.", + "noSpend": "Витрати ще не зафіксовано", + "spend24h": "Витрати за останні 24 години", + "multiplierError": "Введіть множник 1 або більше", + "permissionDenied": "У вас немає дозволу керувати сповіщеннями про витрати.", + "pushOffByCategory": "Вимкнено у сповіщеннях" } } diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 2c09768559..b438ee509c 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -827,7 +827,8 @@ "chat": "چیٹ کے پیغامات", "kiloclaw": "KiloClaw کی سرگرمی", "balance": "بیلنس کی تنبیہات", - "security": "شناخت شدہ سیکیورٹی مسائل" + "security": "شناخت شدہ سیکیورٹی مسائل", + "spend": "اخراجات کی تنبیہات" }, "category": { "chatMessagesSubtitle": "آپ کی گفتگو میں جوابات", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "مکمل / ناکام / کنٹرول کے لیے تیار", "kiloclawActivitySubtitle": "انسٹینس تیار یا ناکام، مقررہ وقت کے کام", "balanceAlertsSubtitle": "تنظیم کا بیلنس کم ہونے کے انتباہات", + "spendAlertsSubtitle": "حدوں سے تجاوز اور فی گھنٹہ اضافہ", + "spendAlertsToggle": "اخراجات کی تنبیہات کی قسم", "securityFindingsSubtitle": "نئی دریافتیں اور SLA کی یاد دہانیاں" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "آخری سیشن کھولیں" + }, + "spendAlerts": { + "subtitle": "جب اس دائرے کا خرچ کسی حد سے تجاوز کرے یا اپنی معمول کی فی گھنٹہ شرح سے بڑھ جائے تو بلنگ رابطوں کو ای میل یا پش کے ذریعے تنبیہ بھیجیں۔", + "enable": "اخراجات کی تنبیہات فعال کریں", + "thresholdTitle": "حد سے تجاوز", + "thresholdSubtitle": "جاری خرچ آپ کی حد سے تجاوز کر رہا ہے", + "anomalyTitle": "فی گھنٹہ اضافہ", + "anomalySubtitle": "ایک گھنٹہ اس دائرے کی معمول کی شرح سے بہت زیادہ ہے", + "limitLabel": "حد (USD)", + "windowLabel": "وقت کی حد", + "window24h": "24 گھنٹے", + "window7d": "7 دن", + "window30d": "30 دن", + "multiplierLabel": "اضافے کا ضرب", + "saved": "اخراجات کی تنبیہ کی ترتیبات محفوظ ہو گئیں", + "loadError": "اخراجات کی تنبیہات لوڈ نہیں ہو سکیں۔", + "saveError": "اخراجات کی تنبیہات محفوظ نہیں ہو سکیں۔", + "empty": "اخراجات کی تنبیہات بند ہیں۔ اس دائرے کے بلنگ رابطوں کو آگاہ کرنے کے لیے انہیں آن کریں۔", + "noSpend": "ابھی تک کوئی خرچ درج نہیں ہوا", + "spend24h": "گزشتہ 24 گھنٹوں کا خرچ", + "multiplierError": "1 یا اس سے زیادہ کا ضرب درج کریں", + "permissionDenied": "آپ کو اخراجات کی تنبیہات کا انتظام کرنے کی اجازت نہیں ہے۔", + "pushOffByCategory": "اطلاعات میں بند" } } diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index b0c3cc09f0..4a86998994 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -827,7 +827,8 @@ "chat": "Suhbat xabarlari", "kiloclaw": "KiloClaw faoliyati", "balance": "Balans haqida ogohlantirishlar", - "security": "Aniqlangan xavfsizlik muammolari" + "security": "Aniqlangan xavfsizlik muammolari", + "spend": "Xarajat bildirishnomalari" }, "category": { "chatMessagesSubtitle": "suhbatlaringizdagi javoblar", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "tugadi / xatolik yuz berdi / boshqarishga tayyor", "kiloclawActivitySubtitle": "nusxa tayyor yoki xatolik yuz berdi, rejalashtirilgan amallar", "balanceAlertsSubtitle": "tashkilot balansi kamaygani haqida ogohlantirishlar", + "spendAlertsSubtitle": "chegaradan oshish va soatlik ko'tarilishlar", + "spendAlertsToggle": "Xarajat bildirishnomalari toifasi", "securityFindingsSubtitle": "aniqlangan yangi zaifliklar va SLA eslatmalari" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Oxirgi seansni ochish" + }, + "spendAlerts": { + "subtitle": "Bu sohaning xarajati chegaradan oshganda yoki odatdagi soatlik sur'atidan yuqoriga ko'tarilganda to'lov bo'yicha kontaktlarga email yoki push orqali bildirishnoma yubor.", + "enable": "Xarajat bildirishnomalarini yoqish", + "thresholdTitle": "Chegaradan oshish", + "thresholdSubtitle": "Joriy xarajat limitingizdan oshib ketdi", + "anomalyTitle": "Soatlik ko'tarilish", + "anomalySubtitle": "Bir soat bu sohaning odatdagi sur'atidan ancha yuqori", + "limitLabel": "Limit (USD)", + "windowLabel": "Oyna", + "window24h": "24 soat", + "window7d": "7 kun", + "window30d": "30 kun", + "multiplierLabel": "Ko'tarilish koeffitsiyenti", + "saved": "Xarajat bildirishnomasi sozlamalari saqlandi", + "loadError": "Xarajat bildirishnomalarini yuklab bo'lmadi.", + "saveError": "Xarajat bildirishnomalarini saqlab bo'lmadi.", + "empty": "Xarajat bildirishnomalari o'chirilgan. Bu sohaning to'lov kontaktlarini ogohlantirish uchun ularni yoqing.", + "noSpend": "Hozircha xarajat qayd etilmagan", + "spend24h": "Oxirgi 24 soatdagi xarajat", + "multiplierError": "1 yoki undan katta koeffitsiyent kiriting", + "permissionDenied": "Xarajat bildirishnomalarini boshqarishga ruxsatingiz yo'q.", + "pushOffByCategory": "Bildirishnomalarda o'chirilgan" } } diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 32bc29c800..7e0909aa91 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -323,7 +323,8 @@ "chat": "Tin nhắn trò chuyện", "kiloclaw": "Hoạt động KiloClaw", "balance": "Cảnh báo số dư", - "security": "Vấn đề bảo mật" + "security": "Vấn đề bảo mật", + "spend": "Cảnh báo chi tiêu" }, "category": { "chatMessagesSubtitle": "câu trả lời trong các cuộc trò chuyện của bạn", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "hoàn tất / thất bại / sẵn sàng điều khiển", "kiloclawActivitySubtitle": "máy sẵn sàng hoặc gặp lỗi, thao tác đã lên lịch", "balanceAlertsSubtitle": "cảnh báo số dư tổ chức thấp", + "spendAlertsSubtitle": "vượt ngưỡng và tăng vọt theo giờ", + "spendAlertsToggle": "Danh mục cảnh báo chi tiêu", "securityFindingsSubtitle": "vấn đề bảo mật mới và lời nhắc về SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Mở phiên gần nhất" + }, + "spendAlerts": { + "subtitle": "Gửi cảnh báo qua email hoặc push tới các liên hệ thanh toán khi chi tiêu của phạm vi này vượt một giới hạn hoặc tăng vọt trên mức thường lệ theo giờ.", + "enable": "Bật cảnh báo chi tiêu", + "thresholdTitle": "Vượt ngưỡng", + "thresholdSubtitle": "Chi tiêu tích lũy vượt giới hạn của bạn", + "anomalyTitle": "Tăng vọt theo giờ", + "anomalySubtitle": "Một giờ cao hơn hẳn mức thường lệ của phạm vi này", + "limitLabel": "Giới hạn (USD)", + "windowLabel": "Khoảng thời gian", + "window24h": "24 giờ", + "window7d": "7 ngày", + "window30d": "30 ngày", + "multiplierLabel": "Hệ số tăng vọt", + "saved": "Đã lưu cài đặt cảnh báo chi tiêu", + "loadError": "Không thể tải cảnh báo chi tiêu.", + "saveError": "Không thể lưu cảnh báo chi tiêu.", + "empty": "Cảnh báo chi tiêu đang tắt. Bật chúng để thông báo cho các liên hệ thanh toán của phạm vi này.", + "noSpend": "Chưa ghi nhận chi tiêu nào", + "spend24h": "Chi tiêu trong 24 giờ qua", + "multiplierError": "Nhập hệ số từ 1 trở lên", + "permissionDenied": "Bạn không có quyền quản lý cảnh báo chi tiêu.", + "pushOffByCategory": "Đã tắt trong Thông báo" } } diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 252eb1f075..35aec73306 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -827,7 +827,8 @@ "chat": "Àwọn ìfiránṣẹ́ ìfọ̀rọ̀wérọ̀", "kiloclaw": "Àwọn ìgbòkègbodò KiloClaw", "balance": "Àwọn ìkìlọ̀ nípa owó tó kù", - "security": "Àwọn àwárí ààbò" + "security": "Àwọn àwárí ààbò", + "spend": "Àwọn ìkìlọ̀ ìnáwó" }, "category": { "chatMessagesSubtitle": "àwọn ìdáhùn nínú ìjíròrò rẹ", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "ó parí / ó kùnà / ó ti ṣe tán fún ìṣàkóso", "kiloclawActivitySubtitle": "ẹ̀rọ iṣẹ́ ti ṣe tán/ó kùnà, àwọn ìṣe tí a ṣètò àkókò wọn", "balanceAlertsSubtitle": "àwọn ìkìlọ̀ pé owó àjọ tó kù ti dín kù", + "spendAlertsSubtitle": "àwọn ìkọjá ààlà àti ìgbésókè lójú wákàtí", + "spendAlertsToggle": "Ẹ̀ka àwọn ìkìlọ̀ ìnáwó", "securityFindingsSubtitle": "àwọn àwárí tuntun àti ìránnilétí SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Ṣí ìgbà iṣẹ́ tó kẹ́yìn" + }, + "spendAlerts": { + "subtitle": "Fi ìkìlọ̀ ránṣẹ́ nípasẹ̀ ímeèlì tàbí push sí àwọn olùbáṣepọ̀ ìsanwó nígbà tí ìnáwó ẹ̀ka yìí bá kọjá ààlà tàbí gòkè ju ìwọ̀n wákàtí tó máa ń ṣe lọ.", + "enable": "Tan àwọn ìkìlọ̀ ìnáwó", + "thresholdTitle": "Ìkọjá ààlà", + "thresholdSubtitle": "Ìnáwó tí ń lọ kọjá ààlà rẹ", + "anomalyTitle": "Ìgbésókè lójú wákàtí", + "anomalySubtitle": "Wákàtí kan ga ju ìwọ̀n ìṣe ẹ̀ka yìí lọ", + "limitLabel": "Ààlà (USD)", + "windowLabel": "Àkókò", + "window24h": "Wákàtí 24", + "window7d": "Ọjọ́ 7", + "window30d": "Ọjọ́ 30", + "multiplierLabel": "Ìlọ́po ìgbésókè", + "saved": "A ti fipamọ́ àwọn ètò ìkìlọ̀ ìnáwó", + "loadError": "A kò lè gbé àwọn ìkìlọ̀ ìnáwó wọlé.", + "saveError": "A kò lè fipamọ́ àwọn ìkìlọ̀ ìnáwó.", + "empty": "Àwọn ìkìlọ̀ ìnáwó ti pa. Tan wọ́n kí o lè kìlọ̀ fún àwọn olùbáṣepọ̀ ìsanwó ẹ̀ka yìí.", + "noSpend": "A kò tíì ṣe àkọsílẹ̀ ìnáwó kankan", + "spend24h": "Ìnáwó ní wákàtí 24 sẹ́yìn", + "multiplierError": "Tẹ ìlọ́po 1 tàbí jù bẹ́ẹ̀ lọ", + "permissionDenied": "O kò ní ìyọ̀ǹda láti ṣàkóso àwọn ìkìlọ̀ ìnáwó.", + "pushOffByCategory": "Ti pa nínú Àwọn ìfitónilétí" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 13d6df8dee..5531147ad7 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -323,7 +323,8 @@ "chat": "聊天消息", "kiloclaw": "KiloClaw 活动", "balance": "余额提醒", - "security": "安全问题" + "security": "安全问题", + "spend": "支出提醒" }, "category": { "chatMessagesSubtitle": "对话中的回复", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "已完成 / 已失败 / 可接管", "kiloclawActivitySubtitle": "实例就绪/失败、计划操作", "balanceAlertsSubtitle": "组织余额不足警告", + "spendAlertsSubtitle": "阈值突破与每小时激增", + "spendAlertsToggle": "支出提醒类别", "securityFindingsSubtitle": "新发现的安全问题和 SLA 提醒" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "打开上次会话" + }, + "spendAlerts": { + "subtitle": "当此范围的支出超过限额或高于其平时每小时速率时,通过邮件或推送向账单联系人发送提醒。", + "enable": "启用支出提醒", + "thresholdTitle": "阈值突破", + "thresholdSubtitle": "累计支出超过你的限额", + "anomalyTitle": "每小时激增", + "anomalySubtitle": "某一小时的支出远高于此范围的平时速率", + "limitLabel": "限额 (USD)", + "windowLabel": "时间窗口", + "window24h": "24 小时", + "window7d": "7 天", + "window30d": "30 天", + "multiplierLabel": "激增倍数", + "saved": "支出提醒设置已保存", + "loadError": "无法加载支出提醒。", + "saveError": "无法保存支出提醒。", + "empty": "支出提醒已关闭。开启后会通知此范围的账单联系人。", + "noSpend": "尚无支出记录", + "spend24h": "过去 24 小时的支出", + "multiplierError": "请输入 1 或更大的倍数", + "permissionDenied": "你没有管理支出提醒的权限。", + "pushOffByCategory": "已在通知中关闭" } } diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index a6f05bf1e6..f1f3cc9f12 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -323,7 +323,8 @@ "chat": "聊天訊息", "kiloclaw": "KiloClaw 活動", "balance": "餘額提醒", - "security": "資安問題" + "security": "資安問題", + "spend": "支出提醒" }, "category": { "chatMessagesSubtitle": "對話中的回覆", @@ -335,6 +336,8 @@ "sessionStatusSubtitle": "已完成/失敗/可開始控制", "kiloclawActivitySubtitle": "執行個體就緒或失敗、排程動作", "balanceAlertsSubtitle": "組織餘額不足警告", + "spendAlertsSubtitle": "門檻突破與每小時激增", + "spendAlertsToggle": "支出提醒類別", "securityFindingsSubtitle": "新發現的安全問題與 SLA 提醒" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "開啟上次工作階段" + }, + "spendAlerts": { + "subtitle": "當此範圍的支出超過限額或高於其平時每小時速率時,透過電子郵件或推播向帳單聯絡人發送提醒。", + "enable": "啟用支出提醒", + "thresholdTitle": "門檻突破", + "thresholdSubtitle": "累計支出超過你的限額", + "anomalyTitle": "每小時激增", + "anomalySubtitle": "某一小時的支出遠高於此範圍的平時速率", + "limitLabel": "限額 (USD)", + "windowLabel": "時間視窗", + "window24h": "24 小時", + "window7d": "7 天", + "window30d": "30 天", + "multiplierLabel": "激增倍數", + "saved": "支出提醒設定已儲存", + "loadError": "無法載入支出提醒。", + "saveError": "無法儲存支出提醒。", + "empty": "支出提醒已關閉。開啟後會通知此範圍的帳單聯絡人。", + "noSpend": "尚無支出記錄", + "spend24h": "過去 24 小時的支出", + "multiplierError": "請輸入 1 或更大的倍數", + "permissionDenied": "你沒有管理支出提醒的權限。", + "pushOffByCategory": "已在通知中關閉" } } diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 8db9f288fe..3e59accbdb 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -827,7 +827,8 @@ "chat": "Imilayezo yengxoxo", "kiloclaw": "Umsebenzi we-KiloClaw", "balance": "Izexwayiso zebhalansi", - "security": "Okutholakele kwezokuphepha" + "security": "Okutholakele kwezokuphepha", + "spend": "Izexwayiso zokusebenzisa imali" }, "category": { "chatMessagesSubtitle": "izimpendulo ezingxoxweni zakho", @@ -839,6 +840,8 @@ "sessionStatusSubtitle": "kuqediwe / kwehlulekile / kulungele ukulawulwa", "kiloclawActivitySubtitle": "uhlelo selulungile/lwehlulekile, izenzo ezihleliwe", "balanceAlertsSubtitle": "izexwayiso zebhalansi ephansi yenhlangano", + "spendAlertsSubtitle": "ukweqa imingcele nokukhuphuka ngehora", + "spendAlertsToggle": "Isigaba sezexwayiso zokusebenzisa imali", "securityFindingsSubtitle": "okutholakele okusha nezikhumbuzo ze-SLA" }, "action": { @@ -3199,5 +3202,28 @@ }, "launcher": { "openLastSession": "Vula iseshini yokugcina" + }, + "spendAlerts": { + "subtitle": "Thumela isexwayiso nge-imeyili noma nge-push kwabathintwayo bokukhokha lapho ukusetshenziswa kwemali kwalesi sikhathi kweqa umkhawulo noma kukhuphuka ngaphezu kwezinga elivamile lehora.", + "enable": "Vula izexwayiso zokusebenzisa imali", + "thresholdTitle": "Ukweqa umkhawulo", + "thresholdSubtitle": "Ukusetshenziswa kwemali okwamanje kweqa umkhawulo wakho", + "anomalyTitle": "Ukukhuphuka ngehora", + "anomalySubtitle": "Ihora elilodwa liphakeme kakhulu kunezinga elivamile lalesi sikhathi", + "limitLabel": "Umkhawulo (USD)", + "windowLabel": "Isikhathi", + "window24h": "Amahora angu-24", + "window7d": "Izinsuku ezingu-7", + "window30d": "Izinsuku ezingu-30", + "multiplierLabel": "Isiphindaphindi sokukhuphuka", + "saved": "Izilungiselelo zexwayiso lokusetshenziswa kwemali zilondoloziwe", + "loadError": "Ayikwazanga ukulayisha izexwayiso zokusetshenziswa kwemali.", + "saveError": "Ayikwazanga ukulondoloza izexwayiso zokusetshenziswa kwemali.", + "empty": "Izexwayiso zokusetshenziswa kwemali zivaliwe. Zivule ukuze uxwayise abathintwayo bokukhokha balesi sikhathi.", + "noSpend": "Akukho kusetshenziswa kwemali okurekhodiwe okwamanje", + "spend24h": "Ukusetshenziswa kwemali emahoreni angu-24 edlule", + "multiplierError": "Faka isiphindaphindi esingu-1 noma ngaphezulu", + "permissionDenied": "Awunayo imvume yokuphatha izexwayiso zokusetshenziswa kwemali.", + "pushOffByCategory": "Kucishiwe kuzaziso" } } diff --git a/apps/mobile/src/lib/auth/passkey-client.test.ts b/apps/mobile/src/lib/auth/passkey-client.test.ts index d1ba26c619..2898ac3a79 100644 --- a/apps/mobile/src/lib/auth/passkey-client.test.ts +++ b/apps/mobile/src/lib/auth/passkey-client.test.ts @@ -115,6 +115,15 @@ describe('classifyPasskeyError', () => { expect(classifyPasskeyError({ message: 'nocredentials' })).toBe('no-passkey'); }); + it.each(['name', 'message', 'code'])( + 'classifies ASCII identifiers in %s without changing the localized copy key', + field => { + const failure = classifyPasskeyError({ [field]: 'NOTCONFIGURED' }); + expect(failure).toBe('unsupported'); + expect(passkeyFailureKey(failure)).toBe('login.passkeyUnsupported'); + } + ); + it.each([ [{ name: 'USERCANCELLEDEXCEPTION' }, 'login.passkeyCancelled'], [{ message: 'NOCREDENTIALS' }, 'login.passkeyNotFound'], diff --git a/apps/mobile/src/lib/hooks/agent-push-preference.optimistic.test.ts b/apps/mobile/src/lib/hooks/agent-push-preference.optimistic.test.ts index ea1f434117..645789d431 100644 --- a/apps/mobile/src/lib/hooks/agent-push-preference.optimistic.test.ts +++ b/apps/mobile/src/lib/hooks/agent-push-preference.optimistic.test.ts @@ -34,6 +34,7 @@ function fullRow(overrides: Partial = {}): Notification sessionStatus: DEFAULT_NOTIFICATION_PREFERENCE, kiloclawActivity: DEFAULT_NOTIFICATION_PREFERENCE, balanceAlerts: DEFAULT_NOTIFICATION_PREFERENCE, + spendAlerts: DEFAULT_NOTIFICATION_PREFERENCE, securityFindings: DEFAULT_NOTIFICATION_PREFERENCE, agentPushEnabled: DEFAULT_NOTIFICATION_PREFERENCE, ...overrides, @@ -69,6 +70,7 @@ describe('applyAgentPushOptimistic + rollbackAgentPushOptimistic (per-category)' sessionStatus: false, kiloclawActivity: true, balanceAlerts: true, + spendAlerts: true, securityFindings: true, agentPushEnabled: true, } as const satisfies NotificationPreferences; @@ -88,6 +90,7 @@ describe('applyAgentPushOptimistic + rollbackAgentPushOptimistic (per-category)' expect(after.agentUpdates).toBe(true); expect(after.kiloclawActivity).toBe(true); expect(after.balanceAlerts).toBe(true); + expect(after.spendAlerts).toBe(true); expect(after.securityFindings).toBe(true); expect(after.agentPushEnabled).toBe(true); @@ -200,6 +203,7 @@ describe('per-category flip flow (each category in turn)', () => { { category: 'sessionStatus', next: true }, { category: 'kiloclawActivity', next: false }, { category: 'balanceAlerts', next: false }, + { category: 'spendAlerts', next: false }, { category: 'securityFindings', next: true }, ]; diff --git a/apps/mobile/src/lib/hooks/agent-push-preference.test.ts b/apps/mobile/src/lib/hooks/agent-push-preference.test.ts index 159f93b630..507d019ad0 100644 --- a/apps/mobile/src/lib/hooks/agent-push-preference.test.ts +++ b/apps/mobile/src/lib/hooks/agent-push-preference.test.ts @@ -2,6 +2,7 @@ import { QueryClient } from '@tanstack/react-query'; import { describe, expect, it } from 'vitest'; import { + applyAgentPushOptimistic, DEFAULT_NOTIFICATION_PREFERENCE, deriveAgentPushEditable, deriveGateSettled, @@ -10,6 +11,7 @@ import { type NotificationPreferences, readAgentPushPreference, readAgentPushPreferenceIfLoaded, + rollbackAgentPushOptimistic, } from './agent-push-preference'; const key = ['user', 'getNotificationPreferences'] as const; @@ -26,6 +28,7 @@ function fullRow(overrides: Partial = {}): Notification sessionStatus: DEFAULT_NOTIFICATION_PREFERENCE, kiloclawActivity: DEFAULT_NOTIFICATION_PREFERENCE, balanceAlerts: DEFAULT_NOTIFICATION_PREFERENCE, + spendAlerts: DEFAULT_NOTIFICATION_PREFERENCE, securityFindings: DEFAULT_NOTIFICATION_PREFERENCE, agentPushEnabled: DEFAULT_NOTIFICATION_PREFERENCE, ...overrides, @@ -39,7 +42,7 @@ describe('DEFAULT_NOTIFICATION_PREFERENCE', () => { }); describe('NOTIFICATION_CATEGORY_KEYS', () => { - it('lists the categories rendered on the dedicated screen including balance and security', () => { + it('lists the categories rendered on the dedicated screen including balance, spend and security', () => { expect([...NOTIFICATION_CATEGORY_KEYS]).toEqual([ 'chatMessages', 'agentAttention', @@ -47,6 +50,7 @@ describe('NOTIFICATION_CATEGORY_KEYS', () => { 'sessionStatus', 'kiloclawActivity', 'balanceAlerts', + 'spendAlerts', 'securityFindings', ]); }); @@ -133,6 +137,7 @@ describe('readAgentPushPreference', () => { sessionStatus: false, kiloclawActivity: true, balanceAlerts: false, + spendAlerts: false, securityFindings: true, agentPushEnabled: true, }); @@ -142,6 +147,7 @@ describe('readAgentPushPreference', () => { expect(readAgentPushPreference(qc, key, 'sessionStatus')).toBe(false); expect(readAgentPushPreference(qc, key, 'kiloclawActivity')).toBe(true); expect(readAgentPushPreference(qc, key, 'balanceAlerts')).toBe(false); + expect(readAgentPushPreference(qc, key, 'spendAlerts')).toBe(false); expect(readAgentPushPreference(qc, key, 'securityFindings')).toBe(true); }); @@ -153,6 +159,7 @@ describe('readAgentPushPreference', () => { // legacy field is present. expect(readAgentPushPreference(qc, key, 'chatMessages')).toBe(DEFAULT_NOTIFICATION_PREFERENCE); expect(readAgentPushPreference(qc, key, 'balanceAlerts')).toBe(DEFAULT_NOTIFICATION_PREFERENCE); + expect(readAgentPushPreference(qc, key, 'spendAlerts')).toBe(DEFAULT_NOTIFICATION_PREFERENCE); expect(readAgentPushPreference(qc, key, 'securityFindings')).toBe( DEFAULT_NOTIFICATION_PREFERENCE ); @@ -165,6 +172,29 @@ describe('readAgentPushPreference', () => { }); }); +describe('spendAlerts optimistic flip (agreement with the spend view column)', () => { + it('applies the optimistic flip to spendAlerts and rolls back on error', async () => { + const qc = makeQueryClient(); + qc.setQueryData(key, fullRow({ spendAlerts: true, balanceAlerts: true })); + + const context = await applyAgentPushOptimistic({ + queryClient: qc, + queryKey: key, + next: false, + category: 'spendAlerts', + }); + // The flip lands on spendAlerts only: the sibling balance row is untouched. + expect(readAgentPushPreference(qc, key, 'spendAlerts')).toBe(false); + expect(readAgentPushPreference(qc, key, 'balanceAlerts')).toBe(true); + + rollbackAgentPushOptimistic({ queryClient: qc, queryKey: key, context }); + // Rollback restores the previous snapshot, so a failed save cannot leave + // the screen disagreeing with the column the spend view writes. + expect(readAgentPushPreference(qc, key, 'spendAlerts')).toBe(true); + expect(qc.getQueryData(key)).toEqual(fullRow({ spendAlerts: true, balanceAlerts: true })); + }); +}); + describe('readAgentPushPreferenceIfLoaded', () => { it('reports the row as unknown while the cache is empty', () => { const qc = makeQueryClient(); diff --git a/apps/mobile/src/lib/hooks/agent-push-preference.ts b/apps/mobile/src/lib/hooks/agent-push-preference.ts index 2ce0be95a0..d24fc48b75 100644 --- a/apps/mobile/src/lib/hooks/agent-push-preference.ts +++ b/apps/mobile/src/lib/hooks/agent-push-preference.ts @@ -22,6 +22,7 @@ export const NOTIFICATION_CATEGORY_KEYS = [ 'sessionStatus', 'kiloclawActivity', 'balanceAlerts', + 'spendAlerts', 'securityFindings', ] as const; @@ -35,6 +36,7 @@ export type NotificationPreferences = Readonly<{ sessionStatus: boolean; kiloclawActivity: boolean; balanceAlerts: boolean; + spendAlerts: boolean; securityFindings: boolean; agentPushEnabled: boolean; notificationPreviews?: 'generic' | 'full'; @@ -111,6 +113,7 @@ function defaultPreferences( sessionStatus: DEFAULT_NOTIFICATION_PREFERENCE, kiloclawActivity: DEFAULT_NOTIFICATION_PREFERENCE, balanceAlerts: DEFAULT_NOTIFICATION_PREFERENCE, + spendAlerts: DEFAULT_NOTIFICATION_PREFERENCE, securityFindings: DEFAULT_NOTIFICATION_PREFERENCE, agentPushEnabled: DEFAULT_NOTIFICATION_PREFERENCE, ...overrides, diff --git a/apps/mobile/src/lib/notification-path-schema.test.ts b/apps/mobile/src/lib/notification-path-schema.test.ts new file mode 100644 index 0000000000..08eee6c806 --- /dev/null +++ b/apps/mobile/src/lib/notification-path-schema.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from 'vitest'; +import { pushDataSchema } from '@kilocode/notifications'; + +import { notificationPathForData } from './notification-path'; + +describe('pushDataSchema', () => { + it('rejects empty chat notification IDs', () => { + expect( + pushDataSchema.safeParse({ + type: 'chat.message', + sandboxId: '', + conversationId: 'conversation-1', + messageId: 'message-1', + }).success + ).toBe(false); + expect( + pushDataSchema.safeParse({ + type: 'chat.message', + sandboxId: 'sandbox-1', + conversationId: '', + messageId: 'message-1', + }).success + ).toBe(false); + expect( + pushDataSchema.safeParse({ + type: 'chat.message', + sandboxId: 'sandbox-1', + conversationId: 'conversation-1', + messageId: '', + }).success + ).toBe(false); + }); + + it('accepts valid chat, lifecycle, and cloud agent notification data', () => { + expect( + pushDataSchema.safeParse({ + type: 'chat.message', + sandboxId: 'sandbox-1', + conversationId: 'conversation-1', + messageId: 'message-1', + }).success + ).toBe(true); + expect( + pushDataSchema.safeParse({ + type: 'instance-lifecycle', + event: 'ready', + sandboxId: 'sandbox-1', + }).success + ).toBe(true); + expect( + pushDataSchema.safeParse({ + type: 'cloud_agent_session', + cliSessionId: 'ses_1', + }).success + ).toBe(true); + }); + + it('accepts valid low_balance and security_finding notification data', () => { + expect( + pushDataSchema.safeParse({ + type: 'low_balance', + organizationId: 'org-abc', + }).success + ).toBe(true); + expect( + pushDataSchema.safeParse({ + type: 'security_finding', + findingId: 'finding-1', + scope: 'personal', + }).success + ).toBe(true); + expect( + pushDataSchema.safeParse({ + type: 'security_finding', + findingId: 'finding-2', + scope: 'org-xyz', + }).success + ).toBe(true); + }); + + it('parses low_balance and security_finding through the schema into notification paths', () => { + const lowBalance = pushDataSchema.parse({ + type: 'low_balance', + organizationId: 'org-parsed', + }); + expect(notificationPathForData(lowBalance)).toBe( + '/(app)/(tabs)/(3_profile)/organization/credit-activity?org=org-parsed&via=push' + ); + + const personalFinding = pushDataSchema.parse({ + type: 'security_finding', + findingId: 'f-parsed', + scope: 'personal', + }); + expect(notificationPathForData(personalFinding)).toBe( + '/(app)/(tabs)/(3_profile)/security-agent/personal/findings/f-parsed?via=push' + ); + + const orgFinding = pushDataSchema.parse({ + type: 'security_finding', + findingId: 'f-org', + scope: 'org-99', + }); + expect(notificationPathForData(orgFinding)).toBe( + '/(app)/(tabs)/(3_profile)/security-agent/org-99/findings/f-org?via=push' + ); + }); + + it('rejects empty cloud agent session IDs', () => { + expect( + pushDataSchema.safeParse({ + type: 'cloud_agent_session', + cliSessionId: '', + }).success + ).toBe(false); + }); + + it('rejects a security_lifecycle payload with an unknown event value', () => { + expect( + pushDataSchema.safeParse({ + type: 'security_lifecycle', + event: 'sla_warning', + findingId: 'finding-1', + scope: 'org-xyz', + }).success + ).toBe(false); + }); +}); diff --git a/apps/mobile/src/lib/notification-path.test.ts b/apps/mobile/src/lib/notification-path.test.ts index 9c39a7d885..602d724863 100644 --- a/apps/mobile/src/lib/notification-path.test.ts +++ b/apps/mobile/src/lib/notification-path.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import { pushDataSchema } from '@kilocode/notifications'; import { notificationPathForData, prPathForData } from './notification-path'; @@ -131,6 +130,25 @@ describe('notificationPathForData', () => { ).toBe('/(app)/(tabs)/(3_profile)/organization/credit-activity?org=org-abc&via=push'); }); + it('routes spend_alert notifications for the personal scope to the spend view with via=push', () => { + expect( + notificationPathForData({ + type: 'spend_alert', + scope: 'personal', + }) + ).toBe('/(app)/(tabs)/(3_profile)/spend-alerts?via=push'); + }); + + it('routes spend_alert notifications for an organization scope to its spend view', () => { + expect( + notificationPathForData({ + type: 'spend_alert', + scope: 'organization', + organizationId: 'org-spend', + }) + ).toBe('/(app)/(tabs)/(3_profile)/spend-alerts?org=org-spend&via=push'); + }); + it('routes security_finding notifications for personal scope', () => { expect( notificationPathForData({ @@ -186,127 +204,3 @@ describe('notificationPathForData', () => { ).toBe('/(app)/(tabs)/(3_profile)/security-agent/org-xyz/findings/finding-4?via=push'); }); }); - -describe('pushDataSchema', () => { - it('rejects empty chat notification IDs', () => { - expect( - pushDataSchema.safeParse({ - type: 'chat.message', - sandboxId: '', - conversationId: 'conversation-1', - messageId: 'message-1', - }).success - ).toBe(false); - expect( - pushDataSchema.safeParse({ - type: 'chat.message', - sandboxId: 'sandbox-1', - conversationId: '', - messageId: 'message-1', - }).success - ).toBe(false); - expect( - pushDataSchema.safeParse({ - type: 'chat.message', - sandboxId: 'sandbox-1', - conversationId: 'conversation-1', - messageId: '', - }).success - ).toBe(false); - }); - - it('accepts valid chat, lifecycle, and cloud agent notification data', () => { - expect( - pushDataSchema.safeParse({ - type: 'chat.message', - sandboxId: 'sandbox-1', - conversationId: 'conversation-1', - messageId: 'message-1', - }).success - ).toBe(true); - expect( - pushDataSchema.safeParse({ - type: 'instance-lifecycle', - event: 'ready', - sandboxId: 'sandbox-1', - }).success - ).toBe(true); - expect( - pushDataSchema.safeParse({ - type: 'cloud_agent_session', - cliSessionId: 'ses_1', - }).success - ).toBe(true); - }); - - it('accepts valid low_balance and security_finding notification data', () => { - expect( - pushDataSchema.safeParse({ - type: 'low_balance', - organizationId: 'org-abc', - }).success - ).toBe(true); - expect( - pushDataSchema.safeParse({ - type: 'security_finding', - findingId: 'finding-1', - scope: 'personal', - }).success - ).toBe(true); - expect( - pushDataSchema.safeParse({ - type: 'security_finding', - findingId: 'finding-2', - scope: 'org-xyz', - }).success - ).toBe(true); - }); - - it('parses low_balance and security_finding through the schema into notification paths', () => { - const lowBalance = pushDataSchema.parse({ - type: 'low_balance', - organizationId: 'org-parsed', - }); - expect(notificationPathForData(lowBalance)).toBe( - '/(app)/(tabs)/(3_profile)/organization/credit-activity?org=org-parsed&via=push' - ); - - const personalFinding = pushDataSchema.parse({ - type: 'security_finding', - findingId: 'f-parsed', - scope: 'personal', - }); - expect(notificationPathForData(personalFinding)).toBe( - '/(app)/(tabs)/(3_profile)/security-agent/personal/findings/f-parsed?via=push' - ); - - const orgFinding = pushDataSchema.parse({ - type: 'security_finding', - findingId: 'f-org', - scope: 'org-99', - }); - expect(notificationPathForData(orgFinding)).toBe( - '/(app)/(tabs)/(3_profile)/security-agent/org-99/findings/f-org?via=push' - ); - }); - - it('rejects empty cloud agent session IDs', () => { - expect( - pushDataSchema.safeParse({ - type: 'cloud_agent_session', - cliSessionId: '', - }).success - ).toBe(false); - }); - - it('rejects a security_lifecycle payload with an unknown event value', () => { - expect( - pushDataSchema.safeParse({ - type: 'security_lifecycle', - event: 'sla_warning', - findingId: 'finding-1', - scope: 'org-xyz', - }).success - ).toBe(false); - }); -}); diff --git a/apps/mobile/src/lib/notification-path.ts b/apps/mobile/src/lib/notification-path.ts index 01f7dc3a04..c9696dd300 100644 --- a/apps/mobile/src/lib/notification-path.ts +++ b/apps/mobile/src/lib/notification-path.ts @@ -38,6 +38,13 @@ export function notificationPathForData(data: PushData): string { case 'low_balance': { return `/(app)/(tabs)/(3_profile)/organization/credit-activity?org=${data.organizationId}&via=push`; } + case 'spend_alert': { + // The alert's single CTA opens the scope's spend view: the caller's own + // screen, or the named organization's. + return data.scope === 'organization' && data.organizationId + ? `/(app)/(tabs)/(3_profile)/spend-alerts?org=${data.organizationId}&via=push` + : '/(app)/(tabs)/(3_profile)/spend-alerts?via=push'; + } case 'security_finding': case 'security_lifecycle': { // getSecurityAgentPath returns Href; coerce to string for query append (cast style of security-agent.ts). diff --git a/apps/web/src/app/api/cron/dispatch-spend-alerts/route.test.ts b/apps/web/src/app/api/cron/dispatch-spend-alerts/route.test.ts new file mode 100644 index 0000000000..0befb24877 --- /dev/null +++ b/apps/web/src/app/api/cron/dispatch-spend-alerts/route.test.ts @@ -0,0 +1,112 @@ +import { NextRequest } from 'next/server'; + +jest.mock('@/lib/config.server', () => ({ CRON_SECRET: 'cron-secret' })); +jest.mock('@/lib/spend-alerts/sweep', () => ({ + createSpendAlertSweepStore: jest.fn(() => ({ loadScopeSnapshot: jest.fn() })), + runSpendAlertSweep: jest.fn(), +})); +jest.mock('@/lib/spend-alerts/delivery', () => ({ + drainPendingSpendAlertDeliveries: jest.fn(), + spendAlertDeliveryDeps: { sendEmail: jest.fn(), dispatchPush: jest.fn() }, +})); +const mockSentryLog = jest.fn(); +jest.mock('@/lib/utils.server', () => ({ sentryLogger: jest.fn(() => mockSentryLog) })); + +import { runSpendAlertSweep } from '@/lib/spend-alerts/sweep'; +import { drainPendingSpendAlertDeliveries } from '@/lib/spend-alerts/delivery'; +import { GET, maxDuration } from './route'; + +const mockRunSpendAlertSweep = jest.mocked(runSpendAlertSweep); +const mockDrainPendingSpendAlertDeliveries = jest.mocked(drainPendingSpendAlertDeliveries); + +function request(secret: string): NextRequest { + return new NextRequest('http://localhost:3000/api/cron/dispatch-spend-alerts', { + headers: { authorization: `Bearer ${secret}` }, + }); +} + +describe('GET /api/cron/dispatch-spend-alerts', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRunSpendAlertSweep.mockResolvedValue({ + sweptScopes: 4, + candidateScopes: 2, + fired: 3, + cleared: 1, + deliveriesEnqueued: 3, + }); + mockDrainPendingSpendAlertDeliveries.mockResolvedValue({ + claimed: 3, + delivered: 3, + failed: [], + }); + }); + + test('rejects invalid cron authorization', async () => { + const response = await GET(request('wrong-secret')); + + expect(response.status).toBe(401); + await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' }); + expect(mockRunSpendAlertSweep).not.toHaveBeenCalled(); + expect(mockDrainPendingSpendAlertDeliveries).not.toHaveBeenCalled(); + }); + + test('sweeps then drains with a bounded limit and reports the summary', async () => { + const response = await GET(request('cron-secret')); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + success: true, + partialFailure: false, + summary: { scopesTouched: 2, alertsFired: 3, delivered: 3, failed: 0 }, + }); + expect(mockRunSpendAlertSweep).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ store: expect.anything() }), + expect.objectContaining({ now: expect.any(Date) }) + ); + expect(mockDrainPendingSpendAlertDeliveries).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + { limit: 50 } + ); + expect(mockRunSpendAlertSweep.mock.invocationCallOrder[0]).toBeLessThan( + mockDrainPendingSpendAlertDeliveries.mock.invocationCallOrder[0] + ); + expect(mockSentryLog).toHaveBeenCalledWith( + 'Spend alert sweep and delivery drain completed', + expect.objectContaining({ scopesTouched: 2, alertsFired: 3, delivered: 3, failed: 0 }) + ); + }); + + test('surfaces partialFailure when a delivery fails', async () => { + mockDrainPendingSpendAlertDeliveries.mockResolvedValue({ + claimed: 3, + delivered: 2, + failed: [ + { + deliveryId: '00000000-0000-4000-8000-000000000001', + channel: 'email', + error: 'spend_alert_email_delivery_failed', + }, + ], + }); + + const response = await GET(request('cron-secret')); + + expect(response.status).toBe(500); + await expect(response.json()).resolves.toMatchObject({ + success: false, + partialFailure: true, + summary: { delivered: 2, failed: 1 }, + }); + expect(mockSentryLog).toHaveBeenCalledWith( + 'Spend alert delivery completed with partial failures', + expect.objectContaining({ failedCount: 1 }) + ); + }); + + test('exports a bounded function duration', () => { + expect(maxDuration).toBe(300); + }); +}); diff --git a/apps/web/src/app/api/cron/dispatch-spend-alerts/route.ts b/apps/web/src/app/api/cron/dispatch-spend-alerts/route.ts new file mode 100644 index 0000000000..a7a68f3c6c --- /dev/null +++ b/apps/web/src/app/api/cron/dispatch-spend-alerts/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server'; + +import { db } from '@/lib/drizzle'; +import { CRON_SECRET } from '@/lib/config.server'; +import { createSpendAlertSweepStore, runSpendAlertSweep } from '@/lib/spend-alerts/sweep'; +import { + drainPendingSpendAlertDeliveries, + spendAlertDeliveryDeps, +} from '@/lib/spend-alerts/delivery'; +import { isCronAuthorizationValid } from '@/lib/cron-auth'; +import { sentryLogger } from '@/lib/utils.server'; + +if (!CRON_SECRET) { + throw new Error('CRON_SECRET is not configured in environment variables'); +} + +/** + * Bounded per-run send fan-out. A crossing enqueues at most one row per channel + * and rule, and the pending index orders the claim, so the limit bounds alerts + * sent per run rather than scopes observed. + */ +const SPEND_ALERT_DELIVERY_LIMIT = 50; + +export const maxDuration = 300; + +/** + * One spend-alert tick: re-derive the hourly buckets for the usage delta, + * decide those scopes (plus any rule still firing), then drain the alert + * outbox. The sweep never walks the owner population, so an idle fleet costs a + * single empty range scan and an empty claim. + */ +export async function GET(request: Request) { + const authHeader = request.headers.get('authorization'); + if (!isCronAuthorizationValid(authHeader, CRON_SECRET)) { + sentryLogger( + 'cron', + 'warning' + )( + 'SECURITY: Invalid dispatch-spend-alerts CRON authorization attempt: ' + + (authHeader ? 'Invalid authorization header' : 'Missing authorization header') + ); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const sweep = await runSpendAlertSweep( + db, + { store: createSpendAlertSweepStore(db) }, + { now: new Date() } + ); + const deliveries = await drainPendingSpendAlertDeliveries(db, spendAlertDeliveryDeps, { + limit: SPEND_ALERT_DELIVERY_LIMIT, + }); + + const summary = { + scopesTouched: sweep.candidateScopes, + alertsFired: sweep.fired, + delivered: deliveries.delivered, + failed: deliveries.failed.length, + }; + + sentryLogger('cron', 'info')('Spend alert sweep and delivery drain completed', summary); + + const hasFailures = summary.failed > 0; + if (hasFailures) { + sentryLogger('cron', 'error')('Spend alert delivery completed with partial failures', { + failedCount: summary.failed, + failures: deliveries.failed, + }); + } + + return NextResponse.json( + { + success: !hasFailures, + partialFailure: hasFailures, + summary, + timestamp: new Date().toISOString(), + }, + { status: hasFailures ? 500 : 200 } + ); +} diff --git a/apps/web/src/components/spend-alerts/SpendAlertsPanel.tsx b/apps/web/src/components/spend-alerts/SpendAlertsPanel.tsx new file mode 100644 index 0000000000..2c920dc4ef --- /dev/null +++ b/apps/web/src/components/spend-alerts/SpendAlertsPanel.tsx @@ -0,0 +1,457 @@ +'use client'; + +import { useEffect, useId, useState, type ReactNode } from 'react'; +import { skipToken, useMutation, useQuery } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { AlertCircle, ExternalLink } from 'lucide-react'; +import { useTRPC } from '@/lib/trpc/utils'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { + canManageOrganizationBilling, + type OrganizationRole, +} from '@kilocode/app-shared/organizations'; +import { cn } from '@/lib/utils'; +import { + SPEND_ALERTS_OFF_COPY, + SPEND_ALERTS_PANEL_SLOT_CLASS, + SPEND_ALERTS_SAVED, + SPEND_ALERT_WINDOW_HOURS, + WINDOW_LABELS, + derivePanelView, + draftAfterSave, + panelControlsVisible, + pushChannelNote, + pushControlDisabled, + toDraft, + toSaveInput, + type SpendAlertFieldErrors, + type SpendAlertRuleDraft, + type SpendAlertRuleKind, + type SpendAlertsDraft, + type SpendAlertWindowHours, +} from './spendAlertsPanelState'; + +/** + * Every state renders into this slot, and every state gets at least as much + * room as the ready form needs at that width, so the settings arriving (or + * failing) never moves the dashboard below. + * + * The ready form's height follows the card's own width, not the viewport's. + * The card is the dashboard's content column: the app sidebar narrows it at + * `md`, and the organization usage-details view adds a second sidebar at `lg`, + * which squeezes the card to ~385px while the viewport is still wide enough for + * the two-column rule fields. The rule descriptions and the channel notes then + * wrap, and the fields themselves collapse to one column below `sm`. The + * reserved heights per band, and the worst ready-form height each one covers, + * live next to the class in `spendAlertsPanelState.ts`; the unit test there + * holds the two together. + * + * The bands are element queries rather than media queries because the card + * width, not the viewport, is what wraps the form: the personal spend view has + * no organization sidebar, so the same viewport width gives a wide card there + * and a narrow one on the organization view. Both the skeleton (717px of + * blocks) and the form render into this same slot, so the two phases are the + * same height and nothing below moves. + */ +const PANEL_SLOT_CLASS = SPEND_ALERTS_PANEL_SLOT_CLASS; + +/** + * The container that {@link PANEL_SLOT_CLASS}'s element query measures. A + * container query can only style descendants, so the Card that owns the slot + * has to sit inside this wrapper rather than carry `@container` itself. The + * wrapper spans the same width as the Card, so the query sees the card's width. + */ +function PanelSlot({ children }: { children: ReactNode }) { + return
{children}
; +} + +export type SpendAlertsPanelProps = { + /** Organization scope when set; personal scope when omitted. */ + organizationId?: string; + /** + * Caller's role in `organizationId`. A role that cannot manage billing gets + * no panel at all; omitted in personal scope, which the owner always manages. + */ + callerRole?: OrganizationRole; +}; + +/** + * Spend-alert settings, mounted above the usage summary of the spend views. + * Enabling the feature, choosing which alerts fire, at which thresholds, and on + * which channels all happen here; the mobile app's notification category is the + * push gate and agrees with the push choices saved here. + */ +export function SpendAlertsPanel({ organizationId, callerRole }: SpendAlertsPanelProps) { + const trpc = useTRPC(); + const fieldId = useId(); + const [draft, setDraft] = useState(null); + + // A non-billing caller never has settings to read: skip the query rather than + // asking the server for an answer the panel will not render. + const roleCanManage = callerRole === undefined || canManageOrganizationBilling(callerRole); + + const query = useQuery( + trpc.spendAlerts.get.queryOptions( + roleCanManage ? (organizationId ? { organizationId } : {}) : skipToken + ) + ); + + const saveMutation = useMutation( + trpc.spendAlerts.save.mutationOptions({ + onSuccess: () => { + toast.success(SPEND_ALERTS_SAVED); + // The save also writes the viewer's push category column, so re-read it + // to keep the channel-agreement note honest. + void query.refetch(); + }, + }) + ); + + // Seed the editable draft once; a background refetch must not discard edits. + useEffect(() => { + const data = query.data; + if (data === undefined) return; + setDraft(current => current ?? toDraft(data)); + }, [query.data]); + + const view = derivePanelView( + { isLoading: query.isLoading, isError: query.isError, data: query.data }, + callerRole, + { isPending: saveMutation.isPending, isError: saveMutation.isError } + ); + + if (view.status === 'hidden') return null; + + if (view.status === 'loading') { + return ( + + + Loading spend alerts + + Spend alerts + + +