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 00000000000..487edcd1e6e --- /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/agents/new-session-configure-form.test.ts b/apps/mobile/src/components/agents/new-session-configure-form.test.ts index bb25e7b8286..458a2346c36 100644 --- a/apps/mobile/src/components/agents/new-session-configure-form.test.ts +++ b/apps/mobile/src/components/agents/new-session-configure-form.test.ts @@ -51,6 +51,7 @@ const keyboardSubscribers = vi.hoisted(() => ({ })); vi.mock('@/components/ui/activity-indicator', () => ({ ActivityIndicator: 'ActivityIndicator' })); +vi.mock('@/components/ui/skeleton', () => ({ Skeleton: 'Skeleton' })); vi.mock('react-native', () => ({ ActivityIndicator: 'ActivityIndicator', Keyboard: { diff --git a/apps/mobile/src/components/notifications-screen.mounted.test.tsx b/apps/mobile/src/components/notifications-screen.mounted.test.tsx index f78a73426f2..b55942d1ac9 100644 --- a/apps/mobile/src/components/notifications-screen.mounted.test.tsx +++ b/apps/mobile/src/components/notifications-screen.mounted.test.tsx @@ -152,6 +152,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(); @@ -598,6 +677,7 @@ describe('NotificationsScreen category availability', () => { sessionStatus: true, kiloclawActivity: true, balanceAlerts: true, + spendAlerts: true, securityFindings: true, agentPushEnabled: true, notificationPreviews: 'generic', @@ -607,6 +687,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 e7e24911098..f4116b7d206 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'; @@ -148,6 +148,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', @@ -174,11 +184,16 @@ type CategoryMeta = (typeof CATEGORY_META)[number]; /** * Per-category availability from the preferences response `capabilities` map. * The same entry carries the server's `unavailableReason`: an English-only - * sentence that is deliberately never rendered, because the screen is localized - * and the server cannot translate it. `CategoryRow` reads the reason from - * `CATEGORY_UNAVAILABLE_SUBTITLE_KEYS` instead. + * sentence that must never render where a catalog key exists, because the + * screen is localized and the server cannot translate it. `CategoryRow` reads + * the reason from `CATEGORY_UNAVAILABLE_SUBTITLE_KEYS` for those categories, + * and falls back to this field only for one that has no key yet (the spend + * row). */ -type NotificationCategoryCapability = Readonly<{ available: boolean }>; +type NotificationCategoryCapability = Readonly<{ + available: boolean; + unavailableReason: string | null; +}>; type CategoryRowProps = Readonly<{ meta: CategoryMeta; @@ -213,17 +228,25 @@ function CategoryRow({ const editable = deriveAgentPushEditable({ hasData: preferences != null, isPending }); // An unavailable category is a terminal, non-retryable state: the switch is // disabled and the row renders its own catalog copy for the reason. The - // server's `unavailableReason` is English prose and must never render — it - // cannot be translated — so the unavailable state is carried by the muted - // title and the disabled switch, and the reason comes from - // `CATEGORY_UNAVAILABLE_SUBTITLE_KEYS`. A missing entry (the - // `noUncheckedIndexedAccess` widening) defaults to available. + // server's `unavailableReason` is English prose and cannot be translated, so + // the unavailable state is carried by the muted title and the disabled + // switch, and the reason comes from `CATEGORY_UNAVAILABLE_SUBTITLE_KEYS`. A + // category without a catalog key (the spend row) falls back to the reason the + // server sent. A missing entry (the `noUncheckedIndexedAccess` widening) + // defaults to available. 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); + // A category with catalog copy renders that copy; the spend row has none yet, + // so it falls back to the reason the server sent. const unavailableSubtitleKey = CATEGORY_UNAVAILABLE_SUBTITLE_KEYS.get(meta.key); - const subtitle = - unavailable && unavailableSubtitleKey != null ? t(unavailableSubtitleKey) : t(meta.subtitleKey); + const unavailableReason = + unavailableSubtitleKey != null ? t(unavailableSubtitleKey) : capability?.unavailableReason; + const subtitle = unavailable ? (unavailableReason ?? t(meta.subtitleKey)) : t(meta.subtitleKey); return ( @@ -242,7 +265,7 @@ function CategoryRow({ { if (isDisabled) { @@ -266,8 +289,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` @@ -605,7 +628,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. */} @@ -795,7 +818,10 @@ export function NotificationsScreen() { // backend that predates the field returns none. The guard // keeps the old response on the always-available path. // eslint-disable-next-line typescript-eslint/no-unnecessary-condition - preferences.capabilities?.[meta.key] ?? { available: true } + preferences.capabilities?.[meta.key] ?? { + available: true, + unavailableReason: null, + } } disabled={!notificationsEnabled} isPending={pendingCategories.has(meta.key)} diff --git a/apps/mobile/src/components/organization/hub-screen.mounted.test.tsx b/apps/mobile/src/components/organization/hub-screen.mounted.test.tsx index 8b3fe075cc3..13a307f98ee 100644 --- a/apps/mobile/src/components/organization/hub-screen.mounted.test.tsx +++ b/apps/mobile/src/components/organization/hub-screen.mounted.test.tsx @@ -116,6 +116,7 @@ vi.mock('@/components/ui/icons', () => ({ Pencil: 'Pencil', Receipt: 'Receipt', Users: 'Users', + Wallet: 'Wallet', })); vi.mock('@/components/ui/kv-row', () => ({ KvRow: 'KvRow' })); diff --git a/apps/mobile/src/components/organization/hub-screen.tsx b/apps/mobile/src/components/organization/hub-screen.tsx index cc31be70509..4640ced5633 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'; @@ -199,11 +199,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 00000000000..80dc290d2d0 --- /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 00000000000..5550e775d49 --- /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 00000000000..28958d612aa --- /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 00000000000..6a7161b8e9b --- /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 f88af6f0f9a..75a385ad2ce 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 47c761cebf2..ff37cd92fe7 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 7aeb9d30bef..95c4865561d 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/components/profile-screen.signout.mounted.test.tsx b/apps/mobile/src/components/profile-screen.signout.mounted.test.tsx index 57e0524a48d..f21e55d042e 100644 --- a/apps/mobile/src/components/profile-screen.signout.mounted.test.tsx +++ b/apps/mobile/src/components/profile-screen.signout.mounted.test.tsx @@ -32,6 +32,9 @@ vi.mock('react-native-reanimated', () => ({ LinearTransition: {}, })); +// The Profile screen reads its landscape side insets through `@/lib/screen-insets`, +// whose real module loads the native safe-area package. The node project cannot +// load that native module, so the screen's own tests stub the hook. // The screen reads its side insets through `@/lib/screen-insets`, which imports // this native module; its untransformed source breaks the mounted project. vi.mock('react-native-safe-area-context', () => ({ diff --git a/apps/mobile/src/components/settings-screens-a11y.mounted.test.tsx b/apps/mobile/src/components/settings-screens-a11y.mounted.test.tsx index 59aca1209b5..add7a985aa7 100644 --- a/apps/mobile/src/components/settings-screens-a11y.mounted.test.tsx +++ b/apps/mobile/src/components/settings-screens-a11y.mounted.test.tsx @@ -75,6 +75,7 @@ vi.mock('@/components/ui/icons', () => ({ Shield: 'Shield', SlidersHorizontal: 'SlidersHorizontal', Smartphone: 'Smartphone', + Wallet: 'Wallet', WandSparkles: 'WandSparkles', })); vi.mock('@/components/ui/directional-icons', () => ({ diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index b52d54feaa1..8879b01ef8c 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "waarskuwings oor die organisasie se lae saldo", "balanceAlertsUnavailable": "Sluit by 'n organisasie aan om waarskuwings oor lae saldo te kry.", "securityFindingsSubtitle": "nuwe bevindings en SLA-herinneringe", - "securityFindingsUnavailable": "Aktiveer Kilo-sekuriteitsagent op 'n bestek om sekuriteitsbevindings te kry." + "securityFindingsUnavailable": "Aktiveer Kilo-sekuriteitsagent op 'n bestek om sekuriteitsbevindings te kry.", + "spendAlertsSubtitle": "oorskryding van drempels en uurlikse pieke", + "spendAlertsToggle": "Kategorie vir bestedingswaarskuwings" }, "action": { "openSession": "Maak sessie oop" @@ -3200,5 +3203,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 e63b657f967..b555bcc7a39 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": "በውይይቶችዎ ውስጥ የሚሰጡ ምላሾች", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ዝቅተኛ የድርጅት ቀሪ ሂሳብ ማስጠንቀቂያዎች", "balanceAlertsUnavailable": "የቀሪ ሂሳብ ማስጠንቀቂያዎችን ለማግኘት ድርጅት ይቀላቀሉ።", "securityFindingsSubtitle": "አዳዲስ ግኝቶችና የSLA ማስታወሻዎች", - "securityFindingsUnavailable": "የደህንነት ግኝቶችን ለማግኘት የKilo የደህንነት ወኪልን በአንድ ወሰን ያንቁ።" + "securityFindingsUnavailable": "የደህንነት ግኝቶችን ለማግኘት የKilo የደህንነት ወኪልን በአንድ ወሰን ያንቁ።", + "spendAlertsSubtitle": "የገደብ መሻገሪያዎች እና የሰዓት ጭማሪዎች", + "spendAlertsToggle": "የወጪ ማስጠንቀቂያ ምድብ" }, "action": { "openSession": "ክፍለ ጊዜውን ክፈት" @@ -3200,5 +3203,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 cdacb931cf9..334cd0f32ee 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": "الردود في محادثاتك", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "تحذيرات انخفاض رصيد المؤسسة", "balanceAlertsUnavailable": "انضم إلى مؤسسة للحصول على تنبيهات الرصيد.", "securityFindingsSubtitle": "نتائج جديدة وتذكيرات SLA", - "securityFindingsUnavailable": "فعّل وكيل أمان Kilo على نطاق للحصول على ملاحظات الأمان." + "securityFindingsUnavailable": "فعّل وكيل أمان Kilo على نطاق للحصول على ملاحظات الأمان.", + "spendAlertsSubtitle": "تجاوزات الحدود والارتفاعات الساعية", + "spendAlertsToggle": "فئة تنبيهات الإنفاق" }, "action": { "openSession": "فتح الجلسة" @@ -3372,5 +3375,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 3cbb25cf7a0..e8856c4d491 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "təşkilatın balansının azalması barədə xəbərdarlıqlar", "balanceAlertsUnavailable": "Balans xəbərdarlıqlarını almaq üçün təşkilata qoşulun.", "securityFindingsSubtitle": "yeni aşkarlamalar və SLA xatırlatmaları", - "securityFindingsUnavailable": "Təhlükəsizlik problemlərini almaq üçün Kilo Təhlükəsizlik agentini bir əhatə dairəsində aktivləşdirin." + "securityFindingsUnavailable": "Təhlükəsizlik problemlərini almaq üçün Kilo Təhlükəsizlik agentini bir əhatə dairəsində aktivləşdirin.", + "spendAlertsSubtitle": "hədd aşmaları və saatlıq sıçrayışlar", + "spendAlertsToggle": "Xərc bildirişləri kateqoriyası" }, "action": { "openSession": "Sessiyanı aç" @@ -3200,5 +3203,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 1827845d431..d286b0176a0 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": "адказы ў вашых размовах", @@ -874,7 +875,9 @@ "balanceAlertsSubtitle": "папярэджанні аб нізкім балансе арганізацыі", "balanceAlertsUnavailable": "Далучыцеся да арганізацыі, каб атрымліваць папярэджанні аб нізкім балансе.", "securityFindingsSubtitle": "новыя праблемы бяспекі і напамінкі аб SLA", - "securityFindingsUnavailable": "Уключыце агента бяспекі Kilo у пэўным абсягу, каб атрымліваць выяўленыя праблемы бяспекі." + "securityFindingsUnavailable": "Уключыце агента бяспекі Kilo у пэўным абсягу, каб атрымліваць выяўленыя праблемы бяспекі.", + "spendAlertsSubtitle": "перасячэнні парогаў і гадзінныя ўсплёскі", + "spendAlertsToggle": "Катэгорыя апавяшчэнняў аб выдатаках" }, "action": { "openSession": "Адкрыць сеанс" @@ -3286,5 +3289,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 355e1e87e27..26dd123e7a9 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": "отговори в разговорите ти", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "предупреждения за ниска наличност на организацията", "balanceAlertsUnavailable": "Присъедини се към организация, за да получаваш предупреждения за нисък баланс.", "securityFindingsSubtitle": "нови открити проблеми и напомняния за SLA", - "securityFindingsUnavailable": "Активирай агента по сигурността на Kilo за даден обхват, за да получаваш проблеми със сигурността." + "securityFindingsUnavailable": "Активирай агента по сигурността на Kilo за даден обхват, за да получаваш проблеми със сигурността.", + "spendAlertsSubtitle": "преминаване на прагове и часови скокове", + "spendAlertsToggle": "Категория известия за разходи" }, "action": { "openSession": "Отваряне на сесия" @@ -3200,5 +3203,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 f756c6134ef..ef97c2ed8c7 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": "আপনার কথোপকথনে উত্তর", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "সংস্থার ব্যালেন্স কমে যাওয়ার সতর্কতা", "balanceAlertsUnavailable": "ব্যালেন্স সতর্কতা পেতে একটি সংস্থায় যোগ দিন।", "securityFindingsSubtitle": "নতুন নিরাপত্তা সমস্যা ও SLA মনে করিয়ে দেওয়ার বিজ্ঞপ্তি", - "securityFindingsUnavailable": "নিরাপত্তা সমস্যা পেতে একটি পরিসরে Kilo নিরাপত্তা এজেন্ট চালু করুন।" + "securityFindingsUnavailable": "নিরাপত্তা সমস্যা পেতে একটি পরিসরে Kilo নিরাপত্তা এজেন্ট চালু করুন।", + "spendAlertsSubtitle": "সীমা অতিক্রম ও ঘণ্টাভিত্তিক বৃদ্ধি", + "spendAlertsToggle": "খরচের সতর্কতার বিভাগ" }, "action": { "openSession": "সেশন খুলুন" @@ -3200,5 +3203,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 7a31990ca47..7c439c4ba74 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", "balanceAlertsUnavailable": "Pridruži se organizaciji da bi dobio upozorenja o niskom saldu.", "securityFindingsSubtitle": "novi nalazi i SLA podsjetnici", - "securityFindingsUnavailable": "Uključi Kilo sigurnosnog agenta na opsegu da bi dobio sigurnosne nalaze." + "securityFindingsUnavailable": "Uključi Kilo sigurnosnog agenta na opsegu da bi dobio sigurnosne nalaze.", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi na sat", + "spendAlertsToggle": "Kategorija upozorenja o potrošnji" }, "action": { "openSession": "Otvori sesiju" @@ -3243,5 +3246,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 6a080b159fd..a867cecf0f9 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "avisos de saldo baix de l'organització", "balanceAlertsUnavailable": "Uneix-te a una organització per rebre avisos de saldo baix.", "securityFindingsSubtitle": "noves deteccions i recordatoris d'SLA", - "securityFindingsUnavailable": "Activa l'agent de seguretat de Kilo en un àmbit per rebre problemes de seguretat." + "securityFindingsUnavailable": "Activa l'agent de seguretat de Kilo en un àmbit per rebre problemes de seguretat.", + "spendAlertsSubtitle": "superacions de llindars i pics horaris", + "spendAlertsToggle": "Categoria d'alertes de despesa" }, "action": { "openSession": "Obre la sessió" @@ -3243,5 +3246,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 a8a0f3e7886..84c65811be3 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": "وەڵامەکان لە گفتوگۆکانتدا", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ئاگادارکردنەوەکانی کەمی باڵانسی ڕێکخراوە", "balanceAlertsUnavailable": "بەشداری ڕێکخراوێک بکە بۆ وەرگرتنی ئاگادارکردنەوەکانی باڵانس.", "securityFindingsSubtitle": "دۆزینەوە نوێکان و بیرخستنەوەکانی SLA", - "securityFindingsUnavailable": "بریکاری ئاسایشی Kilo لە مەودایەکدا چالاک بکە بۆ وەرگرتنی دۆزینەوەکانی ئاسایش." + "securityFindingsUnavailable": "بریکاری ئاسایشی Kilo لە مەودایەکدا چالاک بکە بۆ وەرگرتنی دۆزینەوەکانی ئاسایش.", + "spendAlertsSubtitle": "تێپەڕینی سنوورەکان و بەرزبوونەوەکانی کاتژمێری", + "spendAlertsToggle": "پۆلی ئاگادارکردنەوەکانی خەرجی" }, "action": { "openSession": "کردنەوەی سێشن" @@ -3200,5 +3203,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 c64ae582b29..0bebc28d915 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", @@ -874,7 +875,9 @@ "balanceAlertsSubtitle": "upozornění na nízký zůstatek organizace", "balanceAlertsUnavailable": "Přidejte se k organizaci, abyste dostávali upozornění na nízký zůstatek.", "securityFindingsSubtitle": "nové nálezy a připomenutí lhůt SLA", - "securityFindingsUnavailable": "Zapněte bezpečnostního agenta Kilo v určitém rozsahu, abyste dostávali bezpečnostní zjištění." + "securityFindingsUnavailable": "Zapněte bezpečnostního agenta Kilo v určitém rozsahu, abyste dostávali bezpečnostní zjištění.", + "spendAlertsSubtitle": "překročení prahů a hodinové špičky", + "spendAlertsToggle": "Kategorie upozornění na útratu" }, "action": { "openSession": "Otevřít relaci" @@ -3286,5 +3289,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 50e7f266d0d..d08684d79ae 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", @@ -906,7 +907,9 @@ "balanceAlertsSubtitle": "rhybuddion am falans isel y sefydliad", "balanceAlertsUnavailable": "Ymunwch â sefydliad i gael rhybuddion balans isel.", "securityFindingsSubtitle": "canfyddiadau newydd a nodiadau atgoffa SLA", - "securityFindingsUnavailable": "Galluogwch asiant diogelwch Kilo ar gwmpas i gael canfyddiadau diogelwch." + "securityFindingsUnavailable": "Galluogwch asiant diogelwch Kilo ar gwmpas i gael canfyddiadau diogelwch.", + "spendAlertsSubtitle": "croesi trothwyon a sbigau bob awr", + "spendAlertsToggle": "Categori rhybuddion gwariant" }, "action": { "openSession": "Agor y sesiwn" @@ -3372,5 +3375,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 f03d1afa43e..463697e3a1b 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "advarsler om lav saldo i organisationen", "balanceAlertsUnavailable": "Tilmeld dig en organisation for at få advarsler om lav saldo.", "securityFindingsSubtitle": "nye fund og SLA-påmindelser", - "securityFindingsUnavailable": "Aktivér Kilo-sikkerhedsagenten i et område for at få sikkerhedsfund." + "securityFindingsUnavailable": "Aktivér Kilo-sikkerhedsagenten i et område for at få sikkerhedsfund.", + "spendAlertsSubtitle": "grænseoverskridelser og spikes pr. time", + "spendAlertsToggle": "Kategorien forbrugsadvarsler" }, "action": { "openSession": "Åbn session" @@ -3200,5 +3203,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 2efd7f23e90..f9516d7a737 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", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "warnungen bei niedrigem Organisationsguthaben", "balanceAlertsUnavailable": "Tritt einer Organisation bei, um Warnungen bei niedrigem Guthaben zu erhalten.", "securityFindingsSubtitle": "neue Sicherheitsbefunde und SLA-Erinnerungen", - "securityFindingsUnavailable": "Aktiviere den Kilo-Sicherheitsagenten für einen Bereich, um Sicherheitsbefunde zu erhalten." + "securityFindingsUnavailable": "Aktiviere den Kilo-Sicherheitsagenten für einen Bereich, um Sicherheitsbefunde zu erhalten.", + "spendAlertsSubtitle": "Schwellenwertüberschreitungen und stündliche Spitzen", + "spendAlertsToggle": "Kategorie „Ausgaben-Warnungen“" }, "action": { "openSession": "Sitzung öffnen" @@ -3200,5 +3203,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 42b616328db..6cc3e885513 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": "απαντήσεις στις συνομιλίες σου", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "προειδοποιήσεις χαμηλού υπολοίπου οργανισμού", "balanceAlertsUnavailable": "Γίνε μέλος ενός οργανισμού για να λαμβάνεις ειδοποιήσεις χαμηλού υπολοίπου.", "securityFindingsSubtitle": "νέα ευρήματα και υπενθυμίσεις SLA", - "securityFindingsUnavailable": "Ενεργοποίησε τον πράκτορα ασφάλειας Kilo σε ένα πεδίο για να λαμβάνεις ευρήματα ασφάλειας." + "securityFindingsUnavailable": "Ενεργοποίησε τον πράκτορα ασφάλειας Kilo σε ένα πεδίο για να λαμβάνεις ευρήματα ασφάλειας.", + "spendAlertsSubtitle": "υπερβάσεις ορίων και ωριαίες κορυφώσεις", + "spendAlertsToggle": "Κατηγορία ειδοποιήσεων δαπανών" }, "action": { "openSession": "Άνοιγμα συνεδρίας" @@ -3200,5 +3203,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 5fc4de9196c..a7565f12782 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "low organization balance warnings", "balanceAlertsUnavailable": "Join an organization to get balance alerts.", "securityFindingsSubtitle": "new findings and SLA reminders", - "securityFindingsUnavailable": "Enable Kilo Security Agent on a scope to get security findings." + "securityFindingsUnavailable": "Enable Kilo Security Agent on a scope to get security findings.", + "spendAlertsSubtitle": "threshold crossings and hourly spikes", + "spendAlertsToggle": "Spend alerts category" }, "action": { "openSession": "Open session" @@ -3200,5 +3203,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 46c8edc2d3d..335982e6cbc 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", @@ -310,7 +311,9 @@ "balanceAlertsSubtitle": "avisos de saldo bajo de la organización", "balanceAlertsUnavailable": "Únete a una organización para recibir alertas de saldo bajo.", "securityFindingsSubtitle": "nuevos hallazgos y recordatorios de SLA", - "securityFindingsUnavailable": "Activa el agente de seguridad de Kilo en un ámbito para recibir hallazgos de seguridad." + "securityFindingsUnavailable": "Activa el agente de seguridad de Kilo en un ámbito para recibir hallazgos de seguridad.", + "spendAlertsSubtitle": "superaciones de umbrales y picos por hora", + "spendAlertsToggle": "Categoría de alertas de gasto" }, "liveActivities": "Actividades en directo", "liveActivitySubtitle": "Mostrar los agentes activos en la pantalla de bloqueo", @@ -3243,5 +3246,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 32700dd04d1..63a8a61eee3 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "organisatsiooni madala saldo hoiatused", "balanceAlertsUnavailable": "Liitu organisatsiooniga, et saada madala saldo hoiatusi.", "securityFindingsSubtitle": "uued leiud ja SLA meeldetuletused", - "securityFindingsUnavailable": "Lülita Kilo turvaagent kindlas ulatuses sisse, et saada turvaleide." + "securityFindingsUnavailable": "Lülita Kilo turvaagent kindlas ulatuses sisse, et saada turvaleide.", + "spendAlertsSubtitle": "lävendi ületamised ja tunnipõhised tõusud", + "spendAlertsToggle": "Kulutushoiatuste kategooria" }, "action": { "openSession": "Ava seanss" @@ -3200,5 +3203,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 cc6748d808f..bf5986aad0d 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "erakundearen saldo baxuaren abisuak", "balanceAlertsUnavailable": "Egin bat erakunde batekin saldo baxuko abisuak jasotzeko.", "securityFindingsSubtitle": "aurkikuntza berriak eta SLAren gogorarazpenak", - "securityFindingsUnavailable": "Gaitu Kilo segurtasun-agentea esparru batean segurtasun-arazoak jasotzeko." + "securityFindingsUnavailable": "Gaitu Kilo segurtasun-agentea esparru batean segurtasun-arazoak jasotzeko.", + "spendAlertsSubtitle": "atalaseen gainditzeak eta orduko gorakadak", + "spendAlertsToggle": "Gastu-alerten kategoria" }, "action": { "openSession": "Ireki saioa" @@ -3200,5 +3203,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 80c42b2376e..67148f982e7 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": "پاسخ‌های گفت‌وگوهای شما", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "هشدارهای موجودی کم سازمان", "balanceAlertsUnavailable": "برای دریافت هشدارهای موجودی کم، به یک سازمان بپیوندید.", "securityFindingsSubtitle": "یافته‌های جدید و یادآوری‌های SLA", - "securityFindingsUnavailable": "برای دریافت یافته‌های امنیتی، عامل امنیتی Kilo را در یک محدوده فعال کنید." + "securityFindingsUnavailable": "برای دریافت یافته‌های امنیتی، عامل امنیتی Kilo را در یک محدوده فعال کنید.", + "spendAlertsSubtitle": "عبور از آستانه‌ها و جهش‌های ساعتی", + "spendAlertsToggle": "دستهٔ هشدارهای هزینه" }, "action": { "openSession": "باز کردن نشست" @@ -3200,5 +3203,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 86da0e01af1..5a9e522454b 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "varoitukset organisaation alhaisesta saldosta", "balanceAlertsUnavailable": "Liity organisaatioon saadaksesi saldohälytyksiä.", "securityFindingsSubtitle": "uudet löydökset ja SLA-muistutukset", - "securityFindingsUnavailable": "Ota Kilo-tietoturva-agentti käyttöön laajuudessa saadaksesi tietoturvahavaintoja." + "securityFindingsUnavailable": "Ota Kilo-tietoturva-agentti käyttöön laajuudessa saadaksesi tietoturvahavaintoja.", + "spendAlertsSubtitle": "raja-arvojen ylitykset ja tunneittaiset piikit", + "spendAlertsToggle": "Kulutusvaroitusten luokka" }, "action": { "openSession": "Avaa istunto" @@ -3200,5 +3203,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 ff2895e26f1..355f7ff24d4 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "mga babala sa mababang balanse ng organisasyon", "balanceAlertsUnavailable": "Sumali sa isang organisasyon para makakuha ng mga babala sa mababang balanse.", "securityFindingsSubtitle": "mga bagong natuklasan at paalala sa SLA", - "securityFindingsUnavailable": "Paganahin ang Kilo security agent sa isang saklaw para makakuha ng mga natukoy na isyu sa seguridad." + "securityFindingsUnavailable": "Paganahin ang Kilo security agent sa isang saklaw para makakuha ng mga natukoy na isyu sa seguridad.", + "spendAlertsSubtitle": "mga paglampas sa hangganan at pagtaas bawat oras", + "spendAlertsToggle": "Kategorya ng mga babala sa gastos" }, "action": { "openSession": "Buksan ang sesyon" @@ -3200,5 +3203,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 44ececbbef9..eb584c68555 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", @@ -274,7 +275,9 @@ "balanceAlertsSubtitle": "alertes de solde faible pour l'organisation", "balanceAlertsUnavailable": "Rejoignez une organisation pour recevoir les alertes de solde faible.", "securityFindingsSubtitle": "nouvelles vulnérabilités et rappels SLA", - "securityFindingsUnavailable": "Activez l'agent de sécurité Kilo sur un périmètre pour recevoir les problèmes de sécurité." + "securityFindingsUnavailable": "Activez l'agent de sécurité Kilo sur un périmètre pour recevoir les problèmes de sécurité.", + "spendAlertsSubtitle": "franchissements de seuils et pics horaires", + "spendAlertsToggle": "Catégorie « Alertes de dépenses »" }, "liveActivities": "Activités en direct", "liveActivitySubtitle": "Afficher les agents actifs sur l’écran verrouillé", @@ -3243,5 +3246,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 17229ad69cb..ead56844740 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", @@ -890,7 +891,9 @@ "balanceAlertsSubtitle": "rabhaidh faoi iarmhéid íseal na heagraíochta", "balanceAlertsUnavailable": "Bí páirteach in eagraíocht chun foláirimh faoi iarmhéid íseal a fháil.", "securityFindingsSubtitle": "torthaí nua agus meabhrúcháin SLA", - "securityFindingsUnavailable": "Cumasaigh gníomhaire slándála Kilo ar raon chun torthaí slándála a fháil." + "securityFindingsUnavailable": "Cumasaigh gníomhaire slándála Kilo ar raon chun torthaí slándála a fháil.", + "spendAlertsSubtitle": "sáruithe tairsí agus spuaiceanna uaire", + "spendAlertsToggle": "Catagóir foláirimh chaiteachais" }, "action": { "openSession": "Oscail an seisiún" @@ -3329,5 +3332,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 d0f20c5c500..0fbe88db8de 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "avisos de saldo baixo da organización", "balanceAlertsUnavailable": "Únete a unha organización para recibir alertas de saldo baixo.", "securityFindingsSubtitle": "novos problemas detectados e recordatorios de SLA", - "securityFindingsUnavailable": "Activa o axente de seguridade de Kilo nun ámbito para recibir problemas de seguridade." + "securityFindingsUnavailable": "Activa o axente de seguridade de Kilo nun ámbito para recibir problemas de seguridade.", + "spendAlertsSubtitle": "superacións de limiares e picos por hora", + "spendAlertsToggle": "Categoría de alertas de gasto" }, "action": { "openSession": "Abrir a sesión" @@ -3200,5 +3203,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 aa34ad8a8c7..bf7093d6aa1 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": "તમારી વાતચીતમાં જવાબો", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "સંસ્થાનું બેલેન્સ ઓછું થવાની ચેતવણીઓ", "balanceAlertsUnavailable": "બેલેન્સ ચેતવણીઓ મેળવવા માટે સંસ્થામાં જોડાઓ.", "securityFindingsSubtitle": "નવા તારણો અને SLA રીમાઇન્ડર", - "securityFindingsUnavailable": "સુરક્ષા તારણો મેળવવા માટે વ્યાપમાં Kilo સુરક્ષા એજન્ટ ચાલુ કરો." + "securityFindingsUnavailable": "સુરક્ષા તારણો મેળવવા માટે વ્યાપમાં Kilo સુરક્ષા એજન્ટ ચાલુ કરો.", + "spendAlertsSubtitle": "મર્યાદા ઓળંગવી અને કલાકદીઠ ઉછાળા", + "spendAlertsToggle": "ખર્ચની ચેતવણીની શ્રેણી" }, "action": { "openSession": "સેશન ખોલો" @@ -3200,5 +3203,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 1062b34539c..309659507ef 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "gargaɗin ƙarancin kuɗin ƙungiya", "balanceAlertsUnavailable": "Shiga ƙungiya don samun gargaɗin ƙarancin kuɗi.", "securityFindingsSubtitle": "sabbin sakamakon bincike da tunatarwar SLA", - "securityFindingsUnavailable": "Kunna wakilin tsaro na Kilo a wani yanki don samun sakamakon binciken tsaro." + "securityFindingsUnavailable": "Kunna wakilin tsaro na Kilo a wani yanki don samun sakamakon binciken tsaro.", + "spendAlertsSubtitle": "wucewar iyaka da hauhawar sa'a", + "spendAlertsToggle": "Rukunin faɗakarwar kashe kuɗi" }, "action": { "openSession": "Buɗe zaman" @@ -3200,5 +3203,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 99850caf302..e36ee72d893 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": "תשובות בשיחות שלך", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "אזהרות על יתרה נמוכה בארגון", "balanceAlertsUnavailable": "הצטרף לארגון כדי לקבל התראות על יתרה נמוכה.", "securityFindingsSubtitle": "ממצאים חדשים ותזכורות SLA", - "securityFindingsUnavailable": "הפעל את סוכן האבטחה של Kilo בהיקף כדי לקבל ממצאי אבטחה." + "securityFindingsUnavailable": "הפעל את סוכן האבטחה של Kilo בהיקף כדי לקבל ממצאי אבטחה.", + "spendAlertsSubtitle": "חציית ספים וזינוקים שעתיים", + "spendAlertsToggle": "קטגוריית התראות הוצאות" }, "action": { "openSession": "פתיחת סשן" @@ -3243,5 +3246,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 d99b875dab6..3d4a2a290f8 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": "आपकी बातचीत में उत्तर", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "संगठन का बैलेंस कम होने की चेतावनियाँ", "balanceAlertsUnavailable": "बैलेंस अलर्ट पाने के लिए किसी संगठन से जुड़ें।", "securityFindingsSubtitle": "नई सुरक्षा समस्याएँ और SLA के रिमाइंडर", - "securityFindingsUnavailable": "सुरक्षा समस्याएँ पाने के लिए किसी दायरे में Kilo सुरक्षा एजेंट चालू करें।" + "securityFindingsUnavailable": "सुरक्षा समस्याएँ पाने के लिए किसी दायरे में Kilo सुरक्षा एजेंट चालू करें।", + "spendAlertsSubtitle": "सीमा पार होना और हर घंटे की बढ़त", + "spendAlertsToggle": "खर्च की चेतावनी श्रेणी" }, "action": { "openSession": "सत्र खोलें" @@ -3200,5 +3203,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 481e8381bc8..cdb92aae145 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", "balanceAlertsUnavailable": "Pridruži se organizaciji da bi dobio upozorenja o niskom saldu.", "securityFindingsSubtitle": "novi nalazi i podsjetnici na SLA", - "securityFindingsUnavailable": "Uključi Kilo sigurnosnog agenta na opsegu da bi dobio sigurnosne nalaze." + "securityFindingsUnavailable": "Uključi Kilo sigurnosnog agenta na opsegu da bi dobio sigurnosne nalaze.", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi po satu", + "spendAlertsToggle": "Kategorija upozorenja o potrošnji" }, "action": { "openSession": "Otvori sesiju" @@ -3243,5 +3246,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 7f0ee5e9cce..49ab2a873df 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "avètisman lè balans òganizasyon an ba", "balanceAlertsUnavailable": "Antre nan yon òganizasyon pou jwenn alèt sou balans ki ba.", "securityFindingsSubtitle": "nouvo rezilta ak rapèl SLA", - "securityFindingsUnavailable": "Aktive ajan sekirite Kilo sou yon domèn pou jwenn pwoblèm sekirite detekte." + "securityFindingsUnavailable": "Aktive ajan sekirite Kilo sou yon domèn pou jwenn pwoblèm sekirite detekte.", + "spendAlertsSubtitle": "depasman papòt ak ogmantasyon chak èdtan", + "spendAlertsToggle": "Kategori alèt depans" }, "action": { "openSession": "Louvri sesyon an" @@ -3200,5 +3203,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 a93b731a00b..88a365bb52b 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "figyelmeztetések a szervezet alacsony egyenlegére", "balanceAlertsUnavailable": "Csatlakozz egy szervezethez, hogy figyelmeztetéseket kapj alacsony egyenlegről.", "securityFindingsSubtitle": "új biztonsági problémák és SLA-emlékeztetők", - "securityFindingsUnavailable": "Kapcsold be a Kilo biztonsági ügynököt egy hatókörben, hogy biztonsági találatokat kapj." + "securityFindingsUnavailable": "Kapcsold be a Kilo biztonsági ügynököt egy hatókörben, hogy biztonsági találatokat kapj.", + "spendAlertsSubtitle": "küszöbátlépések és óránkénti kiugrások", + "spendAlertsToggle": "Költésfigyelmeztetések kategória" }, "action": { "openSession": "Munkamenet megnyitása" @@ -3200,5 +3203,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 1b1241aa23e..ba799aaaa48 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": "պատասխաններ ձեր զրույցներում", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "կազմակերպության ցածր մնացորդի նախազգուշացումներ", "balanceAlertsUnavailable": "Միացիր կազմակերպությանը՝ ցածր մնացորդի ծանուցումներ ստանալու համար։", "securityFindingsSubtitle": "հայտնաբերված նոր խնդիրներ և SLA հիշեցումներ", - "securityFindingsUnavailable": "Միացրու Kilo անվտանգության գործակալը շրջանակում՝ անվտանգության խնդիրներ ստանալու համար։" + "securityFindingsUnavailable": "Միացրու Kilo անվտանգության գործակալը շրջանակում՝ անվտանգության խնդիրներ ստանալու համար։", + "spendAlertsSubtitle": "շեմերի հատումներ և ժամային ցատկեր", + "spendAlertsToggle": "Ծախսերի ծանուցումների կատեգորիա" }, "action": { "openSession": "Բացել նիստը" @@ -3200,5 +3203,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 04e361ef7a5..86d95b68021 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", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "peringatan saat saldo organisasi rendah", "balanceAlertsUnavailable": "Gabung ke organisasi untuk mendapatkan peringatan saldo rendah.", "securityFindingsSubtitle": "temuan baru dan pengingat SLA", - "securityFindingsUnavailable": "Aktifkan agen keamanan Kilo pada cakupan untuk mendapatkan temuan keamanan." + "securityFindingsUnavailable": "Aktifkan agen keamanan Kilo pada cakupan untuk mendapatkan temuan keamanan.", + "spendAlertsSubtitle": "melewati ambang batas dan lonjakan per jam", + "spendAlertsToggle": "Kategori peringatan pengeluaran" }, "action": { "openSession": "Buka sesi" @@ -3200,5 +3203,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 a9f2087fe17..9801891cbff 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ị", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ịdọ aka na ntị mgbe ego ụlọ ọrụ fọdụrụ dị ntakịrị", "balanceAlertsUnavailable": "Sonye n'òtù iji nweta ịdọ aka na ntị maka ego fọdụrụ.", "securityFindingsSubtitle": "nchọpụta ọhụrụ na ncheta SLA", - "securityFindingsUnavailable": "Gbanye onye enyemaka nchebe Kilo na mpaghara iji nweta ihe achọpụtara gbasara nchebe." + "securityFindingsUnavailable": "Gbanye onye enyemaka nchebe Kilo na mpaghara iji nweta ihe achọpụtara gbasara nchebe.", + "spendAlertsSubtitle": "ịgafe oke na mmụba kwa awa", + "spendAlertsToggle": "Otu ọkwa gbasara mmefu" }, "action": { "openSession": "Mepee nnọkọ" @@ -3200,5 +3203,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 9516c8ebd4b..846a9aa8905 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "viðvaranir um lága inneign félagsins", "balanceAlertsUnavailable": "Skráðu þig í fyrirtæki til að fá viðvaranir um lága inneign.", "securityFindingsSubtitle": "nýjar niðurstöður og SLA-áminningar", - "securityFindingsUnavailable": "Virkjaðu Kilo öryggiserindreka á sviði til að fá öryggisniðurstöður." + "securityFindingsUnavailable": "Virkjaðu Kilo öryggiserindreka á sviði til að fá öryggisniðurstöður.", + "spendAlertsSubtitle": "yfirferðir þröskulda og uppsveiflur á klukkustund", + "spendAlertsToggle": "Flokkur útgjaldaviðvarana" }, "action": { "openSession": "Opna lotu" @@ -3200,5 +3203,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 95630842d45..4d8c2a969dd 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", @@ -274,7 +275,9 @@ "balanceAlertsSubtitle": "avvisi di saldo basso dell'organizzazione", "balanceAlertsUnavailable": "Unisciti a un'organizzazione per ricevere avvisi di saldo basso.", "securityFindingsSubtitle": "nuovi problemi di sicurezza e promemoria SLA", - "securityFindingsUnavailable": "Attiva l'agente di sicurezza Kilo su un ambito per ricevere problemi di sicurezza." + "securityFindingsUnavailable": "Attiva l'agente di sicurezza Kilo su un ambito per ricevere problemi di sicurezza.", + "spendAlertsSubtitle": "superamenti di soglia e picchi orari", + "spendAlertsToggle": "Categoria avvisi di spesa" }, "liveActivities": "Attività in tempo reale", "liveActivitySubtitle": "Mostra gli agenti attivi nella schermata di blocco", @@ -3243,5 +3246,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 9d85074c79d..f09677cf7f4 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": "会話内の返信", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "組織の残高低下に関する警告", "balanceAlertsUnavailable": "残高アラートを受け取るには、組織に参加してください。", "securityFindingsSubtitle": "新たな検出結果とSLAのリマインダー", - "securityFindingsUnavailable": "セキュリティの検出事項を受け取るには、スコープでKiloセキュリティエージェントを有効にしてください。" + "securityFindingsUnavailable": "セキュリティの検出事項を受け取るには、スコープでKiloセキュリティエージェントを有効にしてください。", + "spendAlertsSubtitle": "しきい値の超過と毎時の急増", + "spendAlertsToggle": "支出アラートのカテゴリ" }, "action": { "openSession": "セッションを開く" @@ -3200,5 +3203,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 ad7c699c342..8f20d820e03 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": "პასუხები თქვენს საუბრებში", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ორგანიზაციის დაბალი ბალანსის გაფრთხილებები", "balanceAlertsUnavailable": "შეუერთდი ორგანიზაციას, რომ მიიღო დაბალი ბალანსის გაფრთხილებები.", "securityFindingsSubtitle": "ახლად აღმოჩენილი პრობლემები და SLA-ის შეხსენებები", - "securityFindingsUnavailable": "ჩართე Kilo-ს უსაფრთხოების აგენტი სფეროში, რომ მიიღო უსაფრთხოების პრობლემები." + "securityFindingsUnavailable": "ჩართე Kilo-ს უსაფრთხოების აგენტი სფეროში, რომ მიიღო უსაფრთხოების პრობლემები.", + "spendAlertsSubtitle": "ზღვრების გადაცილება და საათობრივი ნახტომები", + "spendAlertsToggle": "ხარჯვის შეტყობინებების კატეგორია" }, "action": { "openSession": "სესიის გახსნა" @@ -3200,5 +3203,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 94a4c30ea14..6062099dfff 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": "сөйлесулеріңіздегі жауаптар", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ұйым балансының азайғаны туралы ескертулер", "balanceAlertsUnavailable": "Баланс ескертулерін алу үшін ұйымға қосылыңыз.", "securityFindingsSubtitle": "жаңа осалдықтар және SLA туралы еске салулар", - "securityFindingsUnavailable": "Қауіпсіздік мәселелерін алу үшін Kilo Қауіпсіздік агентін ауқымда қосыңыз." + "securityFindingsUnavailable": "Қауіпсіздік мәселелерін алу үшін Kilo Қауіпсіздік агентін ауқымда қосыңыз.", + "spendAlertsSubtitle": "шектерден асу және сағаттық секірістер", + "spendAlertsToggle": "Шығыс ескертулері санаты" }, "action": { "openSession": "Сессияны ашу" @@ -3200,5 +3203,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 9a83e69365c..3ecfd51b8fa 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": "ការឆ្លើយតបនៅក្នុងការសន្ទនារបស់អ្នក", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ការព្រមានពេលសមតុល្យរបស់អង្គការទាប", "balanceAlertsUnavailable": "ចូលរួមស្ថាប័នដើម្បីទទួលបានការជូនដំណឹងពេលសមតុល្យទាប។", "securityFindingsSubtitle": "ការរកឃើញថ្មី និងការរំលឹក SLA", - "securityFindingsUnavailable": "បើកភ្នាក់ងារសុវត្ថិភាព Kilo នៅក្នុងវិសាលភាពដើម្បីទទួលបានបញ្ហាសុវត្ថិភាពដែលរកឃើញ។" + "securityFindingsUnavailable": "បើកភ្នាក់ងារសុវត្ថិភាព Kilo នៅក្នុងវិសាលភាពដើម្បីទទួលបានបញ្ហាសុវត្ថិភាពដែលរកឃើញ។", + "spendAlertsSubtitle": "ការឆ្លងកាត់កម្រិត និងការកើនឡើងប្រចាំម៉ោង", + "spendAlertsToggle": "ប្រភេទនៃការជូនដំណឹងអំពីការចំណាយ" }, "action": { "openSession": "បើកវគ្គ" @@ -3200,5 +3203,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 c195e2b3b12..b6e5cdee678 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": "ನಿಮ್ಮ ಸಂಭಾಷಣೆಗಳಲ್ಲಿನ ಉತ್ತರಗಳು", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ಸಂಸ್ಥೆಯ ಉಳಿಕೆ ಕಡಿಮೆಯಾದಾಗ ಎಚ್ಚರಿಕೆಗಳು", "balanceAlertsUnavailable": "ಬ್ಯಾಲೆನ್ಸ್ ಎಚ್ಚರಿಕೆಗಳನ್ನು ಪಡೆಯಲು ಸಂಸ್ಥೆಗೆ ಸೇರಿ.", "securityFindingsSubtitle": "ಹೊಸದಾಗಿ ಪತ್ತೆಯಾದ ಸಮಸ್ಯೆಗಳು ಮತ್ತು SLA ಜ್ಞಾಪನೆಗಳು", - "securityFindingsUnavailable": "ಭದ್ರತಾ ಸಮಸ್ಯೆಗಳನ್ನು ಪಡೆಯಲು ಒಂದು ವ್ಯಾಪ್ತಿಯಲ್ಲಿ Kilo ಭದ್ರತಾ ಏಜೆಂಟ್ ಸಕ್ರಿಯಗೊಳಿಸಿ." + "securityFindingsUnavailable": "ಭದ್ರತಾ ಸಮಸ್ಯೆಗಳನ್ನು ಪಡೆಯಲು ಒಂದು ವ್ಯಾಪ್ತಿಯಲ್ಲಿ Kilo ಭದ್ರತಾ ಏಜೆಂಟ್ ಸಕ್ರಿಯಗೊಳಿಸಿ.", + "spendAlertsSubtitle": "ಮಿತಿ ದಾಟುವಿಕೆ ಮತ್ತು ಪ್ರತಿ ಗಂಟೆಯ ಏರಿಕೆಗಳು", + "spendAlertsToggle": "ಖರ್ಚಿನ ಎಚ್ಚರಿಕೆಗಳ ವರ್ಗ" }, "action": { "openSession": "ಸೆಷನ್ ತೆರೆಯಿರಿ" @@ -3200,5 +3203,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 de817c7f300..831b767cf10 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": "대화에 달린 답장", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "조직 잔액 부족 경고", "balanceAlertsUnavailable": "잔액 부족 알림을 받으려면 조직에 참여하세요.", "securityFindingsSubtitle": "새 보안 문제 및 SLA 알림", - "securityFindingsUnavailable": "보안 진단 결과를 받으려면 범위에서 Kilo 보안 에이전트를 활성화하세요." + "securityFindingsUnavailable": "보안 진단 결과를 받으려면 범위에서 Kilo 보안 에이전트를 활성화하세요.", + "spendAlertsSubtitle": "임계값 초과 및 시간당 급증", + "spendAlertsToggle": "지출 알림 카테고리" }, "action": { "openSession": "세션 열기" @@ -3200,5 +3203,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 dfe16c218a6..433e0fe72fb 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": "ຄຳຕອບໃນການສົນທະນາຂອງທ່ານ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ຄຳເຕືອນເມື່ອຍອດເງິນຂອງອົງກອນຕໍ່າ", "balanceAlertsUnavailable": "ເຂົ້າຮ່ວມອົງກອນ ເພື່ອຮັບຄຳເຕືອນເມື່ອຍອດເງິນຕໍ່າ.", "securityFindingsSubtitle": "ບັນຫາທີ່ພົບໃໝ່ ແລະ ການແຈ້ງເຕືອນກ່ຽວກັບ SLA", - "securityFindingsUnavailable": "ເປີດເອເຈນດ້ານຄວາມປອດໄພ Kilo ໃນຂອບເຂດ ເພື່ອຮັບບັນຫາດ້ານຄວາມປອດໄພທີ່ກວດພົບ." + "securityFindingsUnavailable": "ເປີດເອເຈນດ້ານຄວາມປອດໄພ Kilo ໃນຂອບເຂດ ເພື່ອຮັບບັນຫາດ້ານຄວາມປອດໄພທີ່ກວດພົບ.", + "spendAlertsSubtitle": "ການຂ້າມຂີດຈຳກັດ ແລະ ການເພີ່ມຂຶ້ນຕໍ່ຊົ່ວໂມງ", + "spendAlertsToggle": "ໝວດການແຈ້ງເຕືອນການໃຊ້ຈ່າຍ" }, "action": { "openSession": "ເປີດເຊສຊັນ" @@ -3200,5 +3203,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 80764b60006..376a3b9cc15 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", @@ -874,7 +875,9 @@ "balanceAlertsSubtitle": "įspėjimai apie mažą organizacijos likutį", "balanceAlertsUnavailable": "Prisijunkite prie organizacijos, kad gautumėte mažo likučio įspėjimus.", "securityFindingsSubtitle": "nauji radiniai ir SLA priminimai", - "securityFindingsUnavailable": "Įjunkite Kilo saugos agentą apimtyje, kad gautumėte saugos problemas." + "securityFindingsUnavailable": "Įjunkite Kilo saugos agentą apimtyje, kad gautumėte saugos problemas.", + "spendAlertsSubtitle": "slenksčių viršijimai ir valandiniai šuoliai", + "spendAlertsToggle": "Išlaidų įspėjimų kategorija" }, "action": { "openSession": "Atidaryti sesiją" @@ -3286,5 +3289,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 46204e4b87a..31bfa5cd076 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "brīdinājumi par mazu organizācijas kredītu atlikumu", "balanceAlertsUnavailable": "Pievienojies organizācijai, lai saņemtu brīdinājumus par mazu atlikumu.", "securityFindingsSubtitle": "jaunas konstatētās problēmas un SLA atgādinājumi", - "securityFindingsUnavailable": "Iespējo Kilo drošības aģentu tvērumā, lai saņemtu drošības konstatējumus." + "securityFindingsUnavailable": "Iespējo Kilo drošības aģentu tvērumā, lai saņemtu drošības konstatējumus.", + "spendAlertsSubtitle": "sliekšņu pārsniegumi un stundas lēcieni", + "spendAlertsToggle": "Izdevumu brīdinājumu kategorija" }, "action": { "openSession": "Atvērt sesiju" @@ -3243,5 +3246,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 87930c6ca05..8665b866093 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "fampitandremana rehefa kely ny ambim-bolan'ny fikambanana", "balanceAlertsUnavailable": "Midira ao amin'ny fikambanana hahazoana fampitandremana momba ny ambim-bola kely.", "securityFindingsSubtitle": "olana vaovao hita sy fampahatsiahivana SLA", - "securityFindingsUnavailable": "Alefaso ny mpanampy amin'ny fiarovana Kilo ao amin'ny faritra iray hahazoana ny olana hita amin'ny fiarovana." + "securityFindingsUnavailable": "Alefaso ny mpanampy amin'ny fiarovana Kilo ao amin'ny faritra iray hahazoana ny olana hita amin'ny fiarovana.", + "spendAlertsSubtitle": "fihoaram-bidy sy fitsambikinana isan'ora", + "spendAlertsToggle": "Sokajin'ny fampitandremana momba ny fandaniana" }, "action": { "openSession": "Sokafy ny fotoam-piasana" @@ -3200,5 +3203,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 2ce757562f1..cb0ad5ba377 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ngā whakatūpato mō te toenga iti o te rōpū", "balanceAlertsUnavailable": "Whakaurua ki tētahi whakahaerenga kia whiwhi ai i ngā whakatūpato toenga iti.", "securityFindingsSubtitle": "ngā kitenga hōu me ngā whakamaumahara SLA", - "securityFindingsUnavailable": "Whakahohea te kaiāwhina haumarutanga Kilo i runga i tētahi whānuitanga kia whiwhi ai i ngā kitenga haumarutanga." + "securityFindingsUnavailable": "Whakahohea te kaiāwhina haumarutanga Kilo i runga i tētahi whānuitanga kia whiwhi ai i ngā kitenga haumarutanga.", + "spendAlertsSubtitle": "ngā whitinga paepae me ngā pikinga ia hāora", + "spendAlertsToggle": "Kāwai matohi whakapaunga" }, "action": { "openSession": "Whakatuwheratia te wātū" @@ -3200,5 +3203,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 74ea1236108..d410b7f1173 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": "одговори во твоите разговори", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "предупредувања за ниска состојба на организацијата", "balanceAlertsUnavailable": "Приклучи се кон организација за да добиеш предупредувања за ниска состојба.", "securityFindingsSubtitle": "нови наоди и потсетници за SLA", - "securityFindingsUnavailable": "Вклучи го безбедносниот агент на Kilo во опсег за да добиеш безбедносни наоди." + "securityFindingsUnavailable": "Вклучи го безбедносниот агент на Kilo во опсег за да добиеш безбедносни наоди.", + "spendAlertsSubtitle": "преминувања на прагови и часовни скокови", + "spendAlertsToggle": "Категорија предупредувања за трошење" }, "action": { "openSession": "Отвори сесија" @@ -3200,5 +3203,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 f5adf3807f1..cd2c44243cf 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": "നിങ്ങളുടെ സംഭാഷണങ്ങളിലെ മറുപടികൾ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ഓർഗനൈസേഷന്റെ ബാലൻസ് കുറയുമ്പോഴുള്ള മുന്നറിയിപ്പുകൾ", "balanceAlertsUnavailable": "ബാലൻസ് മുന്നറിയിപ്പുകൾ ലഭിക്കാൻ ഒരു സ്ഥാപനത്തിൽ ചേരുക.", "securityFindingsSubtitle": "പുതിയ കണ്ടെത്തലുകളും SLA ഓർമ്മപ്പെടുത്തലുകളും", - "securityFindingsUnavailable": "സുരക്ഷാ കണ്ടെത്തലുകൾ ലഭിക്കാൻ ഒരു പരിധിയിൽ Kilo സുരക്ഷാ ഏജന്റ് പ്രവർത്തനക്ഷമമാക്കുക." + "securityFindingsUnavailable": "സുരക്ഷാ കണ്ടെത്തലുകൾ ലഭിക്കാൻ ഒരു പരിധിയിൽ Kilo സുരക്ഷാ ഏജന്റ് പ്രവർത്തനക്ഷമമാക്കുക.", + "spendAlertsSubtitle": "പരിധി ലംഘനങ്ങളും മണിക്കൂർ തോറുമുള്ള വർധനകളും", + "spendAlertsToggle": "ചെലവ് മുന്നറിയിപ്പ് വിഭാഗം" }, "action": { "openSession": "സെഷൻ തുറക്കുക" @@ -3200,5 +3203,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 135f73bbc71..30577764d6a 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": "харилцан яриан дахь хариултууд", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "байгууллагын үлдэгдэл багасах анхааруулга", "balanceAlertsUnavailable": "Балансын анхааруулга авахын тулд байгууллагад нэгдэнэ үү.", "securityFindingsSubtitle": "шинээр илэрсэн эмзэг байдал болон SLA сануулга", - "securityFindingsUnavailable": "Аюулгүй байдлын илэрсэн асуудал авахын тулд Kilo Аюулгүй байдлын агентыг хүрээнд идэвхжүүлнэ үү." + "securityFindingsUnavailable": "Аюулгүй байдлын илэрсэн асуудал авахын тулд Kilo Аюулгүй байдлын агентыг хүрээнд идэвхжүүлнэ үү.", + "spendAlertsSubtitle": "босго давсан болон цагийн огцом өсөлт", + "spendAlertsToggle": "Зарцуулалтын анхааруулгын ангилал" }, "action": { "openSession": "Сесс нээх" @@ -3200,5 +3203,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 d387d6cf66a..426427bc8e5 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": "तुमच्या संभाषणांमधील उत्तरे", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "संस्थेची शिल्लक कमी झाल्याच्या सूचना", "balanceAlertsUnavailable": "बॅलन्स सूचना मिळवण्यासाठी संस्थेत सामील व्हा.", "securityFindingsSubtitle": "नवीन निष्कर्ष आणि SLA स्मरणपत्रे", - "securityFindingsUnavailable": "सुरक्षा निष्कर्ष मिळवण्यासाठी एका व्याप्तीत Kilo सुरक्षा एजंट सुरू करा." + "securityFindingsUnavailable": "सुरक्षा निष्कर्ष मिळवण्यासाठी एका व्याप्तीत Kilo सुरक्षा एजंट सुरू करा.", + "spendAlertsSubtitle": "मर्यादा ओलांडणे आणि प्रति तास वाढ", + "spendAlertsToggle": "खर्चाच्या सूचना श्रेणी" }, "action": { "openSession": "सत्र उघडा" @@ -3200,5 +3203,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 d2e498121e9..30031ef68c7 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "amaran baki rendah untuk organisasi", "balanceAlertsUnavailable": "Sertai organisasi untuk mendapat amaran baki rendah.", "securityFindingsSubtitle": "penemuan baharu dan peringatan SLA", - "securityFindingsUnavailable": "Dayakan ejen keselamatan Kilo pada skop untuk mendapat dapatan keselamatan." + "securityFindingsUnavailable": "Dayakan ejen keselamatan Kilo pada skop untuk mendapat dapatan keselamatan.", + "spendAlertsSubtitle": "melepasi ambang dan lonjakan setiap jam", + "spendAlertsToggle": "Kategori amaran perbelanjaan" }, "action": { "openSession": "Buka sesi" @@ -3200,5 +3203,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 9ff2b221735..01cd395e940 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", @@ -890,7 +891,9 @@ "balanceAlertsSubtitle": "twissijiet ta' bilanċ baxx tal-organizzazzjoni", "balanceAlertsUnavailable": "Ingħaqad ma' organizzazzjoni biex tikseb twissijiet ta' bilanċ baxx.", "securityFindingsSubtitle": "sejbiet ġodda u tfakkiriet tal-SLA", - "securityFindingsUnavailable": "Attiva l-aġent tas-sigurtà ta' Kilo fuq ambitu biex tikseb sejbiet tas-sigurtà." + "securityFindingsUnavailable": "Attiva l-aġent tas-sigurtà ta' Kilo fuq ambitu biex tikseb sejbiet tas-sigurtà.", + "spendAlertsSubtitle": "qbiż ta' limiti u żidiet fis-siegħa", + "spendAlertsToggle": "Kategorija tat-twissijiet tal-infiq" }, "action": { "openSession": "Iftaħ is-sessjoni" @@ -3329,5 +3332,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 5873b2f3525..61e4655a417 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": "သင့်စကားဝိုင်းများရှိ တုံ့ပြန်ချက်များ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "အဖွဲ့အစည်း၏ လက်ကျန်နည်းချိန် သတိပေးချက်များ", "balanceAlertsUnavailable": "လက်ကျန်နည်း သတိပေးချက်များ ရရှိရန် အဖွဲ့အစည်းတစ်ခုသို့ ပါဝင်ပါ။", "securityFindingsSubtitle": "တွေ့ရှိချက်အသစ်များနှင့် SLA သတိပေးချက်များ", - "securityFindingsUnavailable": "လုံခြုံရေးတွေ့ရှိချက်များ ရရှိရန် နယ်ပယ်တစ်ခုတွင် Kilo လုံခြုံရေးအေးဂျင့်ကို ဖွင့်ပါ။" + "securityFindingsUnavailable": "လုံခြုံရေးတွေ့ရှိချက်များ ရရှိရန် နယ်ပယ်တစ်ခုတွင် Kilo လုံခြုံရေးအေးဂျင့်ကို ဖွင့်ပါ။", + "spendAlertsSubtitle": "သတ်မှတ်ချက် ကျော်လွန်မှုနှင့် နာရီအလိုက် မြင့်တက်မှု", + "spendAlertsToggle": "အသုံးစရိတ် သတိပေးချက် အမျိုးအစား" }, "action": { "openSession": "စက်ရှင်ကို ဖွင့်ပါ" @@ -3200,5 +3203,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 502e8da63e9..e8e6cd2f1b6 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "advarsler om lav organisasjonssaldo", "balanceAlertsUnavailable": "Bli med i en organisasjon for å få varsler om lav saldo.", "securityFindingsSubtitle": "nye funn og SLA-påminnelser", - "securityFindingsUnavailable": "Aktiver Kilo-sikkerhetsagenten i et omfang for å få sikkerhetsfunn." + "securityFindingsUnavailable": "Aktiver Kilo-sikkerhetsagenten i et omfang for å få sikkerhetsfunn.", + "spendAlertsSubtitle": "grenseoverskridelser og topper per time", + "spendAlertsToggle": "Kategorien forbruksvarsler" }, "action": { "openSession": "Åpne økt" @@ -3200,5 +3203,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 88928fe8e12..94f08110b66 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": "तपाईंका कुराकानीमा आएका उत्तरहरू", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "संस्थाको मौज्दात कम भएको चेतावनी", "balanceAlertsUnavailable": "ब्यालेन्स सूचना पाउन कुनै संस्थामा सामेल हुनुहोस्।", "securityFindingsSubtitle": "फेला परेका नयाँ सुरक्षा समस्या र SLA का सम्झना सूचनाहरू", - "securityFindingsUnavailable": "सुरक्षा समस्या पाउन कुनै दायरामा Kilo सुरक्षा एजेन्ट चालु गर्नुहोस्।" + "securityFindingsUnavailable": "सुरक्षा समस्या पाउन कुनै दायरामा Kilo सुरक्षा एजेन्ट चालु गर्नुहोस्।", + "spendAlertsSubtitle": "सीमा नाघ्ने र प्रति घण्टा वृद्धि", + "spendAlertsToggle": "खर्चका चेतावनी श्रेणी" }, "action": { "openSession": "सत्र खोल्नुहोस्" @@ -3200,5 +3203,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 1e0735d946e..ac14d5caf27 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", @@ -310,7 +311,9 @@ "balanceAlertsSubtitle": "waarschuwingen voor laag organisatiesaldo", "balanceAlertsUnavailable": "Word lid van een organisatie om waarschuwingen voor laag saldo te ontvangen.", "securityFindingsSubtitle": "nieuwe bevindingen en SLA-herinneringen", - "securityFindingsUnavailable": "Schakel de Kilo-beveiligingsagent in op een bereik om beveiligingsbevindingen te ontvangen." + "securityFindingsUnavailable": "Schakel de Kilo-beveiligingsagent in op een bereik om beveiligingsbevindingen te ontvangen.", + "spendAlertsSubtitle": "drempeloverschrijdingen en uurlijkse pieken", + "spendAlertsToggle": "Categorie bestedingswaarschuwingen" }, "liveActivities": "Liveactiviteiten", "liveActivitySubtitle": "Actieve agents op het toegangsscherm tonen", @@ -3200,5 +3203,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 b2029461903..04eb60ef41b 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "akeekkachiisa haftee dhaabbataa xiqqaa", "balanceAlertsUnavailable": "Akeekkachiisa haftee xiqqaa argachuuf dhaabbata makami.", "securityFindingsSubtitle": "argannoowwan haaraa fi yaadachiisa SLA", - "securityFindingsUnavailable": "Argannoowwan nageenyaa argachuuf daangaa irratti eejentii nageenyaa Kilo bani." + "securityFindingsUnavailable": "Argannoowwan nageenyaa argachuuf daangaa irratti eejentii nageenyaa Kilo bani.", + "spendAlertsSubtitle": "daarbii daangaa fi olka'iinsa sa'aatii", + "spendAlertsToggle": "Ramaddii akeekkachiisa baasii" }, "action": { "openSession": "Turtii bani" @@ -3200,5 +3203,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 ed4ab901d3d..71d77654772 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": "ଆପଣଙ୍କ ବାର୍ତ୍ତାଳାପର ଉତ୍ତର", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ସଂଗଠନର କମ୍ ବ୍ୟାଲାନ୍ସ ସତର୍କତା", "balanceAlertsUnavailable": "ବ୍ୟାଲାନ୍ସ ସତର୍କତା ପାଇବାକୁ ଏକ ସଂଗଠନରେ ଯୋଗ ଦିଅନ୍ତୁ।", "securityFindingsSubtitle": "ନୂଆ ସୁରକ୍ଷା ସମସ୍ୟା ଓ SLA ସ୍ମାରକ", - "securityFindingsUnavailable": "ସୁରକ୍ଷା ସମସ୍ୟା ପାଇବାକୁ ଏକ ପରିସରରେ Kilo ସୁରକ୍ଷା ଏଜେଣ୍ଟ ଚାଲୁ କରନ୍ତୁ।" + "securityFindingsUnavailable": "ସୁରକ୍ଷା ସମସ୍ୟା ପାଇବାକୁ ଏକ ପରିସରରେ Kilo ସୁରକ୍ଷା ଏଜେଣ୍ଟ ଚାଲୁ କରନ୍ତୁ।", + "spendAlertsSubtitle": "ସୀମା ଅତିକ୍ରମ ଏବଂ ଘଣ୍ଟାପ୍ରତି ବୃଦ୍ଧି", + "spendAlertsToggle": "ଖର୍ଚ୍ଚ ସତର୍କତା ବର୍ଗ" }, "action": { "openSession": "ସେସନ ଖୋଲନ୍ତୁ" @@ -3200,5 +3203,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 bd6171120e0..a244d211126 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": "ਤੁਹਾਡੀਆਂ ਗੱਲਬਾਤਾਂ ਵਿੱਚ ਜਵਾਬ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "ਸੰਸਥਾ ਦਾ ਬੈਲੰਸ ਘੱਟ ਹੋਣ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ", "balanceAlertsUnavailable": "ਬੈਲੰਸ ਚੇਤਾਵਨੀਆਂ ਪਾਉਣ ਲਈ ਇੱਕ ਸੰਸਥਾ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ।", "securityFindingsSubtitle": "ਨਵੀਆਂ ਸੁਰੱਖਿਆ ਸਮੱਸਿਆਵਾਂ ਅਤੇ SLA ਲਈ ਯਾਦ-ਦਹਾਨੀਆਂ", - "securityFindingsUnavailable": "ਸੁਰੱਖਿਆ ਨਤੀਜੇ ਪਾਉਣ ਲਈ ਇੱਕ ਦਾਇਰੇ ਵਿੱਚ Kilo ਸੁਰੱਖਿਆ ਏਜੰਟ ਚਾਲੂ ਕਰੋ।" + "securityFindingsUnavailable": "ਸੁਰੱਖਿਆ ਨਤੀਜੇ ਪਾਉਣ ਲਈ ਇੱਕ ਦਾਇਰੇ ਵਿੱਚ Kilo ਸੁਰੱਖਿਆ ਏਜੰਟ ਚਾਲੂ ਕਰੋ।", + "spendAlertsSubtitle": "ਸੀਮਾ ਟੱਪਣਾ ਅਤੇ ਹਰ ਘੰਟੇ ਵਾਧਾ", + "spendAlertsToggle": "ਖਰਚੇ ਦੀਆਂ ਚੇਤਾਵਨੀਆਂ ਦੀ ਸ਼੍ਰੇਣੀ" }, "action": { "openSession": "ਸੈਸ਼ਨ ਖੋਲ੍ਹੋ" @@ -3200,5 +3203,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 8ce7851cf2f..c674ceac6b9 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", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "ostrzeżenia o niskim saldzie organizacji", "balanceAlertsUnavailable": "Dołącz do organizacji, aby otrzymywać powiadomienia o niskim saldzie.", "securityFindingsSubtitle": "nowe wykryte problemy i przypomnienia o terminach SLA", - "securityFindingsUnavailable": "Włącz agenta bezpieczeństwa Kilo w zakresie, aby otrzymywać wykryte problemy z bezpieczeństwem." + "securityFindingsUnavailable": "Włącz agenta bezpieczeństwa Kilo w zakresie, aby otrzymywać wykryte problemy z bezpieczeństwem.", + "spendAlertsSubtitle": "przekroczenia progów i skoki godzinowe", + "spendAlertsToggle": "Kategoria alertów o wydatkach" }, "action": { "openSession": "Otwórz sesję" @@ -3286,5 +3289,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 e6f151500ff..798557aa942 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": "ستاسو په خبرو اترو کې ځوابونه", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "د سازمان د کم بیلانس خبرداري", "balanceAlertsUnavailable": "د بیلانس خبرتیاوې ترلاسه کولو لپاره یو سازمان ته وګډون وکړئ.", "securityFindingsSubtitle": "نوې موندنې او د SLA یادونې", - "securityFindingsUnavailable": "امنیتي موندنې ترلاسه کولو لپاره په یوه ساحه کې د Kilo امنیتي اجنټ فعال کړئ." + "securityFindingsUnavailable": "امنیتي موندنې ترلاسه کولو لپاره په یوه ساحه کې د Kilo امنیتي اجنټ فعال کړئ.", + "spendAlertsSubtitle": "د حدونو تېرېدل او ساعتي زیاتوالی", + "spendAlertsToggle": "د لګښت خبرتیاوو کټګوري" }, "action": { "openSession": "ناسته پرانیزئ" @@ -3200,5 +3203,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 678bae4845f..ebab3f1d5d2 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", @@ -302,7 +303,9 @@ "balanceAlertsSubtitle": "avisos de saldo baixo da organização", "balanceAlertsUnavailable": "Entre em uma organização para receber alertas de saldo baixo.", "securityFindingsSubtitle": "novos problemas de segurança e lembretes de SLA", - "securityFindingsUnavailable": "Ative o agente de segurança do Kilo em um âmbito para receber problemas de segurança." + "securityFindingsUnavailable": "Ative o agente de segurança do Kilo em um âmbito para receber problemas de segurança.", + "spendAlertsSubtitle": "ultrapassagens de limites e picos por hora", + "spendAlertsToggle": "Categoria de alertas de gastos" }, "action": { "openSession": "Abrir sessão" @@ -3243,5 +3246,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 223fb86c705..20c2aedf329 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "avisos de saldo baixo da organização", "balanceAlertsUnavailable": "Entra numa organização para receber alertas de saldo baixo.", "securityFindingsSubtitle": "novas ocorrências e lembretes de SLA", - "securityFindingsUnavailable": "Ativa o agente de segurança Kilo num âmbito para receber os problemas de segurança." + "securityFindingsUnavailable": "Ativa o agente de segurança Kilo num âmbito para receber os problemas de segurança.", + "spendAlertsSubtitle": "ultrapassagens de limites e picos horários", + "spendAlertsToggle": "Categoria de alertas de gastos" }, "action": { "openSession": "Abrir sessão" @@ -3243,5 +3246,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 5b4426e5127..f2d6809f2d4 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "avertismente de sold scăzut al organizației", "balanceAlertsUnavailable": "Alătură-te unei organizații pentru a primi alerte de sold scăzut.", "securityFindingsSubtitle": "constatări noi și mementouri SLA", - "securityFindingsUnavailable": "Activează agentul de securitate Kilo într-un domeniu pentru a primi probleme de securitate detectate." + "securityFindingsUnavailable": "Activează agentul de securitate Kilo într-un domeniu pentru a primi probleme de securitate detectate.", + "spendAlertsSubtitle": "depășiri de praguri și vârfuri orare", + "spendAlertsToggle": "Categoria alerte de cheltuieli" }, "action": { "openSession": "Deschide sesiunea" @@ -3243,5 +3246,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 6a11b3e547c..74b4387dab3 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": "ответы в вашей переписке", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "предупреждения о низком балансе организации", "balanceAlertsUnavailable": "Присоединитесь к организации, чтобы получать оповещения о низком балансе.", "securityFindingsSubtitle": "новые уязвимости и напоминания о сроках SLA", - "securityFindingsUnavailable": "Включите Агент безопасности Kilo в области, чтобы получать замечания по безопасности." + "securityFindingsUnavailable": "Включите Агент безопасности Kilo в области, чтобы получать замечания по безопасности.", + "spendAlertsSubtitle": "превышения порогов и почасовые всплески", + "spendAlertsToggle": "Категория «Оповещения о расходах»" }, "action": { "openSession": "Открыть сеанс" @@ -3286,5 +3289,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 ff1729acc7e..34ee63c4e9d 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": "ඔබේ සංවාදවල පිළිතුරු", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "සංවිධානයේ ශේෂය අඩු වීමේ දැනුම්දීම්", "balanceAlertsUnavailable": "ශේෂය අඩු වීමේ දැනුම්දීම් ලබා ගැනීමට සංවිධානයකට සම්බන්ධ වන්න.", "securityFindingsSubtitle": "නව සොයාගැනීම් සහ SLA මතක් කිරීම්", - "securityFindingsUnavailable": "ආරක්ෂක ගැටලු ලබා ගැනීමට විෂය පථයක Kilo ආරක්ෂක නියෝජිතයා සක්‍රීය කරන්න." + "securityFindingsUnavailable": "ආරක්ෂක ගැටලු ලබා ගැනීමට විෂය පථයක Kilo ආරක්ෂක නියෝජිතයා සක්‍රීය කරන්න.", + "spendAlertsSubtitle": "සීමා ඉක්මවීම් සහ පැයකට ඇති උච්චතා", + "spendAlertsToggle": "වියදම් දැනුම්දීම් ප්‍රවර්ගය" }, "action": { "openSession": "සැසිය විවෘත කරන්න" @@ -3200,5 +3203,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 2f89f19ac68..cceb4c60752 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", @@ -874,7 +875,9 @@ "balanceAlertsSubtitle": "upozornenia na nízky zostatok organizácie", "balanceAlertsUnavailable": "Pripojte sa k organizácii, aby ste dostávali upozornenia na nízky zostatok.", "securityFindingsSubtitle": "nové zistenia a pripomienky SLA", - "securityFindingsUnavailable": "Zapnite bezpečnostného agenta Kilo v rozsahu, aby ste dostávali bezpečnostné zistenia." + "securityFindingsUnavailable": "Zapnite bezpečnostného agenta Kilo v rozsahu, aby ste dostávali bezpečnostné zistenia.", + "spendAlertsSubtitle": "prekročenia prahov a hodinové špičky", + "spendAlertsToggle": "Kategória upozornení na výdavky" }, "action": { "openSession": "Otvoriť reláciu" @@ -3286,5 +3289,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 122f46be010..6a05c0bff5c 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", @@ -874,7 +875,9 @@ "balanceAlertsSubtitle": "opozorila o nizkem stanju organizacije", "balanceAlertsUnavailable": "Pridruži se organizaciji, da prejemaš opozorila o nizkem stanju.", "securityFindingsSubtitle": "nove ugotovitve in opomniki SLA", - "securityFindingsUnavailable": "Vklopi varnostnega agenta Kilo v obsegu, da prejemaš varnostne ugotovitve." + "securityFindingsUnavailable": "Vklopi varnostnega agenta Kilo v obsegu, da prejemaš varnostne ugotovitve.", + "spendAlertsSubtitle": "preseganja pragov in urni skoki", + "spendAlertsToggle": "Kategorija opozoril o porabi" }, "action": { "openSession": "Odpri sejo" @@ -3286,5 +3289,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 db47f86541a..d2b05f6d345 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "digniinaha hadhaaga yar ee ururka", "balanceAlertsUnavailable": "Ku biir urur si aad u hesho digniinaha hadhaaga yar.", "securityFindingsSubtitle": "natiijooyin cusub iyo xusuusinno SLA", - "securityFindingsUnavailable": "Shid wakiilka amniga Kilo baaxad si aad u hesho natiijooyinka amniga." + "securityFindingsUnavailable": "Shid wakiilka amniga Kilo baaxad si aad u hesho natiijooyinka amniga.", + "spendAlertsSubtitle": "dhaafitaanka xadka iyo kor u kaca saacadlaha", + "spendAlertsToggle": "Qaybta digniinaha kharashka" }, "action": { "openSession": "Fur kalfadhi" @@ -3200,5 +3203,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 a91cce5d844..7262d8bbbcb 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "paralajmërime për gjendje të ulët të organizatës", "balanceAlertsUnavailable": "Bashkohu me një organizatë për të marrë njoftime për gjendje të ulët.", "securityFindingsSubtitle": "gjetje të reja dhe kujtesa për SLA", - "securityFindingsUnavailable": "Aktivizo agjentin e sigurisë Kilo në një fushëveprim për të marrë gjetjet e sigurisë." + "securityFindingsUnavailable": "Aktivizo agjentin e sigurisë Kilo në një fushëveprim për të marrë gjetjet e sigurisë.", + "spendAlertsSubtitle": "tejkalime pragjesh dhe rritje për orë", + "spendAlertsToggle": "Kategoria e njoftimeve për shpenzimet" }, "action": { "openSession": "Hap seancën" @@ -3200,5 +3203,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 c0a00b41f5b..ddc76c9dcd4 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", @@ -858,7 +859,9 @@ "balanceAlertsSubtitle": "upozorenja o niskom saldu organizacije", "balanceAlertsUnavailable": "Pridruži se organizaciji da dobiješ upozorenja o niskom saldu.", "securityFindingsSubtitle": "novi nalazi i podsetnici za SLA", - "securityFindingsUnavailable": "Uključi Kilo bezbednosnog agenta na opsegu da dobiješ bezbednosne nalaze." + "securityFindingsUnavailable": "Uključi Kilo bezbednosnog agenta na opsegu da dobiješ bezbednosne nalaze.", + "spendAlertsSubtitle": "prekoračenja pragova i skokovi po satu", + "spendAlertsToggle": "Kategorija obaveštenja o potrošnji" }, "action": { "openSession": "Otvori sesiju" @@ -3243,5 +3246,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 53ba774fd9f..6cd0fb65698 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "varningar om lågt organisationssaldo", "balanceAlertsUnavailable": "Gå med i en organisation för att få varningar om lågt saldo.", "securityFindingsSubtitle": "nya säkerhetsfynd och SLA-påminnelser", - "securityFindingsUnavailable": "Aktivera Kilo-säkerhetsagenten i en omfattning för att få säkerhetsanmärkningar." + "securityFindingsUnavailable": "Aktivera Kilo-säkerhetsagenten i en omfattning för att få säkerhetsanmärkningar.", + "spendAlertsSubtitle": "gränsöverskridanden och toppar per timme", + "spendAlertsToggle": "Kategorin utgiftsvarningar" }, "action": { "openSession": "Öppna session" @@ -3200,5 +3203,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 b1c7f8ee9a1..1fac6d519dd 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "maonyo ya salio la chini la shirika", "balanceAlertsUnavailable": "Jiunge na shirika ili kupata maonyo ya salio la chini.", "securityFindingsSubtitle": "matokeo mapya na vikumbusho vya SLA", - "securityFindingsUnavailable": "Washa wakala wa usalama wa Kilo kwenye wigo ili kupata matokeo ya ukaguzi wa usalama." + "securityFindingsUnavailable": "Washa wakala wa usalama wa Kilo kwenye wigo ili kupata matokeo ya ukaguzi wa usalama.", + "spendAlertsSubtitle": "kuvuka vizingiti na kuongezeka kwa saa", + "spendAlertsToggle": "Aina ya tahadhari za matumizi" }, "action": { "openSession": "Fungua kikao" @@ -3200,5 +3203,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 d0c3959713b..35f1d36aeff 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": "உங்கள் உரையாடல்களில் வரும் பதில்கள்", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "நிறுவனத்தின் இருப்பு குறையும்போது வரும் எச்சரிக்கைகள்", "balanceAlertsUnavailable": "இருப்பு எச்சரிக்கைகளைப் பெற ஒரு நிறுவனத்தில் சேரவும்.", "securityFindingsSubtitle": "புதிய கண்டறிதல்கள் மற்றும் SLA நினைவூட்டல்கள்", - "securityFindingsUnavailable": "பாதுகாப்புச் சிக்கல்களைப் பெற ஒரு வரம்பில் Kilo பாதுகாப்பு முகவரை இயக்கவும்." + "securityFindingsUnavailable": "பாதுகாப்புச் சிக்கல்களைப் பெற ஒரு வரம்பில் Kilo பாதுகாப்பு முகவரை இயக்கவும்.", + "spendAlertsSubtitle": "வரம்பு மீறல்கள் மற்றும் மணிநேர உயர்வுகள்", + "spendAlertsToggle": "செலவு எச்சரிக்கை வகை" }, "action": { "openSession": "அமர்வைத் திறக்கவும்" @@ -3200,5 +3203,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 d523543d83a..74960a55b38 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": "మీ సంభాషణలలో ప్రత్యుత్తరాలు", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "సంస్థ బ్యాలెన్స్ తగ్గినప్పుడు హెచ్చరికలు", "balanceAlertsUnavailable": "బ్యాలెన్స్ హెచ్చరికలను పొందడానికి ఒక సంస్థలో చేరండి.", "securityFindingsSubtitle": "కొత్త భద్రతా సమస్యలు, SLA రిమైండర్లు", - "securityFindingsUnavailable": "భద్రతా సమస్యలను పొందడానికి ఒక పరిధిలో Kilo భద్రతా ఏజెంట్‌ను ప్రారంభించండి." + "securityFindingsUnavailable": "భద్రతా సమస్యలను పొందడానికి ఒక పరిధిలో Kilo భద్రతా ఏజెంట్‌ను ప్రారంభించండి.", + "spendAlertsSubtitle": "పరిమితి దాటడం మరియు గంటకు పెరుగుదల", + "spendAlertsToggle": "ఖర్చు హెచ్చరికల వర్గం" }, "action": { "openSession": "సెషన్‌ను తెరవండి" @@ -3200,5 +3203,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 ab886252c2a..43eadd50beb 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": "การตอบกลับในการสนทนาของคุณ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "คำเตือนเมื่อยอดคงเหลือขององค์กรต่ำ", "balanceAlertsUnavailable": "เข้าร่วมองค์กรเพื่อรับการแจ้งเตือนยอดคงเหลือต่ำ", "securityFindingsSubtitle": "ปัญหาที่ตรวจพบใหม่และการเตือนตาม SLA", - "securityFindingsUnavailable": "เปิดเอเจนต์ความปลอดภัย Kilo ในขอบเขตเพื่อรับข้อค้นพบด้านความปลอดภัย" + "securityFindingsUnavailable": "เปิดเอเจนต์ความปลอดภัย Kilo ในขอบเขตเพื่อรับข้อค้นพบด้านความปลอดภัย", + "spendAlertsSubtitle": "การเกินเกณฑ์และการพุ่งขึ้นรายชั่วโมง", + "spendAlertsToggle": "หมวดหมู่การแจ้งเตือนการใช้จ่าย" }, "action": { "openSession": "เปิดเซสชัน" @@ -3200,5 +3203,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 430209eab0b..e6bf6476b11 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", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "düşük kuruluş bakiyesi uyarıları", "balanceAlertsUnavailable": "Düşük bakiye uyarıları almak için bir kuruluşa katıl.", "securityFindingsSubtitle": "yeni bulgular ve SLA hatırlatmaları", - "securityFindingsUnavailable": "Güvenlik bulguları almak için bir kapsamda Kilo Güvenlik ajanını etkinleştir." + "securityFindingsUnavailable": "Güvenlik bulguları almak için bir kapsamda Kilo Güvenlik ajanını etkinleştir.", + "spendAlertsSubtitle": "eşik aşımları ve saatlik artışlar", + "spendAlertsToggle": "Harcama uyarıları kategorisi" }, "action": { "openSession": "Oturumu aç" @@ -3200,5 +3203,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 4dff1f33ced..999e0ccc6af 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": "відповіді у ваших розмовах", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "попередження про низький баланс організації", "balanceAlertsUnavailable": "Приєднайтеся до організації, щоб отримувати сповіщення про низький баланс.", "securityFindingsSubtitle": "нові виявлені проблеми та нагадування про SLA", - "securityFindingsUnavailable": "Увімкніть Агента безпеки Kilo в області, щоб отримувати виявлені проблеми безпеки." + "securityFindingsUnavailable": "Увімкніть Агента безпеки Kilo в області, щоб отримувати виявлені проблеми безпеки.", + "spendAlertsSubtitle": "перевищення порогів і погодинні сплески", + "spendAlertsToggle": "Категорія «Сповіщення про витрати»" }, "action": { "openSession": "Відкрити сеанс" @@ -3286,5 +3289,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 5dc68c9a141..a94d807b422 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": "آپ کی گفتگو میں جوابات", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "تنظیم کا بیلنس کم ہونے کے انتباہات", "balanceAlertsUnavailable": "بیلنس کے انتباہات حاصل کرنے کے لیے کسی تنظیم میں شامل ہوں۔", "securityFindingsSubtitle": "نئی دریافتیں اور SLA کی یاد دہانیاں", - "securityFindingsUnavailable": "سیکیورٹی مسائل حاصل کرنے کے لیے ایک دائرہ کار میں Kilo سیکیورٹی ایجنٹ فعال کریں۔" + "securityFindingsUnavailable": "سیکیورٹی مسائل حاصل کرنے کے لیے ایک دائرہ کار میں Kilo سیکیورٹی ایجنٹ فعال کریں۔", + "spendAlertsSubtitle": "حدوں سے تجاوز اور فی گھنٹہ اضافہ", + "spendAlertsToggle": "اخراجات کی تنبیہات کی قسم" }, "action": { "openSession": "سیشن کھولیں" @@ -3200,5 +3203,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 a7958fbbe16..c8e889e068b 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "tashkilot balansi kamaygani haqida ogohlantirishlar", "balanceAlertsUnavailable": "Balans ogohlantirishlarini olish uchun tashkilotga qo'shiling.", "securityFindingsSubtitle": "aniqlangan yangi zaifliklar va SLA eslatmalari", - "securityFindingsUnavailable": "Xavfsizlik muammolarini olish uchun Kilo Xavfsizlik agentini qamrovda yoqing." + "securityFindingsUnavailable": "Xavfsizlik muammolarini olish uchun Kilo Xavfsizlik agentini qamrovda yoqing.", + "spendAlertsSubtitle": "chegaradan oshish va soatlik ko'tarilishlar", + "spendAlertsToggle": "Xarajat bildirishnomalari toifasi" }, "action": { "openSession": "Sessiyani ochish" @@ -3200,5 +3203,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 04f4c38747b..5d00459aa6b 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", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "cảnh báo số dư tổ chức thấp", "balanceAlertsUnavailable": "Tham gia một tổ chức để nhận cảnh báo số dư thấp.", "securityFindingsSubtitle": "vấn đề bảo mật mới và lời nhắc về SLA", - "securityFindingsUnavailable": "Bật Trợ lý bảo mật Kilo trên một phạm vi để nhận vấn đề bảo mật." + "securityFindingsUnavailable": "Bật Trợ lý bảo mật Kilo trên một phạm vi để nhận vấn đề bảo mật.", + "spendAlertsSubtitle": "vượt ngưỡng và tăng vọt theo giờ", + "spendAlertsToggle": "Danh mục cảnh báo chi tiêu" }, "action": { "openSession": "Mở phiên" @@ -3200,5 +3203,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 104ef79167c..5b30d66c74e 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ẹ", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "àwọn ìkìlọ̀ pé owó àjọ tó kù ti dín kù", "balanceAlertsUnavailable": "Dara pọ̀ mọ́ àjọ kan láti gba àwọn ìkìlọ̀ owó tó kù díẹ̀.", "securityFindingsSubtitle": "àwọn àwárí tuntun àti ìránnilétí SLA", - "securityFindingsUnavailable": "Mú Aṣojú ààbò Kilo ṣiṣẹ́ ní ìwọ̀n kan láti gba àwọn àwárí ààbò." + "securityFindingsUnavailable": "Mú Aṣojú ààbò Kilo ṣiṣẹ́ ní ìwọ̀n kan láti gba àwọn àwárí ààbò.", + "spendAlertsSubtitle": "àwọn ìkọjá ààlà àti ìgbésókè lójú wákàtí", + "spendAlertsToggle": "Ẹ̀ka àwọn ìkìlọ̀ ìnáwó" }, "action": { "openSession": "Ṣí ìgbà iṣẹ́" @@ -3200,5 +3203,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 617fff88cba..e0a589ad801 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": "对话中的回复", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "组织余额不足警告", "balanceAlertsUnavailable": "加入组织即可获取余额不足提醒。", "securityFindingsSubtitle": "新发现的安全问题和 SLA 提醒", - "securityFindingsUnavailable": "在范围内启用 Kilo 安全智能体即可获取安全问题。" + "securityFindingsUnavailable": "在范围内启用 Kilo 安全智能体即可获取安全问题。", + "spendAlertsSubtitle": "阈值突破与每小时激增", + "spendAlertsToggle": "支出提醒类别" }, "action": { "openSession": "打开会话" @@ -3200,5 +3203,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 59649e5276f..50ce9dbdf40 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": "對話中的回覆", @@ -338,7 +339,9 @@ "balanceAlertsSubtitle": "組織餘額不足警告", "balanceAlertsUnavailable": "加入組織即可取得餘額不足提醒。", "securityFindingsSubtitle": "新發現的安全問題與 SLA 提醒", - "securityFindingsUnavailable": "在範圍內啟用 Kilo 資安代理程式即可取得資安問題。" + "securityFindingsUnavailable": "在範圍內啟用 Kilo 資安代理程式即可取得資安問題。", + "spendAlertsSubtitle": "門檻突破與每小時激增", + "spendAlertsToggle": "支出提醒類別" }, "action": { "openSession": "開啟工作階段" @@ -3200,5 +3203,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 f85b52fee45..87b376cf406 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", @@ -842,7 +843,9 @@ "balanceAlertsSubtitle": "izexwayiso zebhalansi ephansi yenhlangano", "balanceAlertsUnavailable": "Joyina inhlangano ukuze uthole izexwayiso zebhalansi ephansi.", "securityFindingsSubtitle": "okutholakele okusha nezikhumbuzo ze-SLA", - "securityFindingsUnavailable": "Vula i-ejenti yezokuphepha ye-Kilo kububanzi ukuze uthole okutholakele kwezokuphepha." + "securityFindingsUnavailable": "Vula i-ejenti yezokuphepha ye-Kilo kububanzi ukuze uthole okutholakele kwezokuphepha.", + "spendAlertsSubtitle": "ukweqa imingcele nokukhuphuka ngehora", + "spendAlertsToggle": "Isigaba sezexwayiso zokusebenzisa imali" }, "action": { "openSession": "Vula iseshini" @@ -3200,5 +3203,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 51243da267d..94ff2d357cc 100644 --- a/apps/mobile/src/lib/auth/passkey-client.test.ts +++ b/apps/mobile/src/lib/auth/passkey-client.test.ts @@ -133,6 +133,16 @@ describe('classifyPasskeyError', () => { } ); + 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', 'message', 'code'])( 'classifies uppercase protocol errors in %s without locale-dependent casing', field => { 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 ea1f4341179..645789d4317 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 159f93b6305..507d019ad02 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 2ce0be95a0c..d24fc48b75f 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 00000000000..08eee6c8060 --- /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 18c556336e6..6506d5b370b 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'; @@ -133,6 +132,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({ @@ -188,127 +206,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 01f7dc3a048..c9696dd300a 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 00000000000..0befb24877c --- /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 00000000000..a7a68f3c6cf --- /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 00000000000..2c920dc4ef5 --- /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 + + +