diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx
index 39b9f27..c5590b7 100644
--- a/src/app/(app)/_layout.tsx
+++ b/src/app/(app)/_layout.tsx
@@ -400,7 +400,8 @@ export default function TabLayout() {
zIndex: 100,
backgroundColor: 'transparent',
borderTopWidth: 0.5,
- borderTopColor: 'rgba(0, 0, 0, 0.1)',
+ // Mid-gray rather than black-alpha so the divider still shows on the dark tab bar.
+ borderTopColor: 'rgba(127, 127, 127, 0.2)',
},
}),
[isLandscape, insets.bottom, insets.top]
@@ -413,12 +414,12 @@ export default function TabLayout() {
const handleCloseNotifications = useCallback(() => setIsNotificationsOpen(false), []);
// Memoize per-screen tab bar icon renderers to prevent new functions every render
- const mapIcon = useCallback(({ color }: { color: ColorValue }) => , []);
- const callsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
- const incidentsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
- const commandIcon = useCallback(({ color }: { color: ColorValue }) => , []);
- const routesIcon = useCallback(({ color }: { color: ColorValue }) => , []);
- const weatherAlertsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const mapIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const callsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const incidentsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const commandIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const routesIcon = useCallback(({ color }: { color: ColorValue }) => , []);
+ const weatherAlertsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
const settingsIcon = useCallback(({ color }: { color: ColorValue }) => , []);
// Memoize header left/right renders
@@ -609,7 +610,7 @@ export default function TabLayout() {
{/* Loading overlay during initialization — shown on top of Tabs so the navigator stays mounted */}
{!isInitComplete ? (
-
+
) : null}
@@ -768,7 +769,6 @@ const styles = StyleSheet.create({
...(StyleSheet.absoluteFill as object),
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: Platform.OS === 'web' ? '#ffffff' : 'rgba(255,255,255,0.95)',
zIndex: 1000,
},
});
diff --git a/src/app/(app)/chat.tsx b/src/app/(app)/chat.tsx
index f487ed6..eb381f0 100644
--- a/src/app/(app)/chat.tsx
+++ b/src/app/(app)/chat.tsx
@@ -64,7 +64,7 @@ function ChannelRow({ channel, onPress }: { channel: ChatChannelResultData; onPr
) : null}
{unread ? (
-
+
{channel.UnreadCount > 99 ? '99+' : String(channel.UnreadCount)}
) : null}
diff --git a/src/app/(app)/chatbot.tsx b/src/app/(app)/chatbot.tsx
index 836b973..397ea95 100644
--- a/src/app/(app)/chatbot.tsx
+++ b/src/app/(app)/chatbot.tsx
@@ -1,5 +1,6 @@
import { Redirect, useFocusEffect } from 'expo-router';
import { RefreshCw, Send, Sparkles } from 'lucide-react-native';
+import { useColorScheme } from 'nativewind';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, useWindowDimensions } from 'react-native';
@@ -28,6 +29,7 @@ import { useToastStore } from '@/stores/toast/store';
export default function ChatbotScreen() {
const { t } = useTranslation();
+ const { colorScheme } = useColorScheme();
// The assistant is a hidden tab, so the tab bar still sits below it and the keyboard
// already covers that strip — pad for the remainder only.
const insets = useSafeAreaInsets();
@@ -114,7 +116,7 @@ export default function ChatbotScreen() {
{t('chatbot.subtitle')}
useChatStore.getState().newChatbotSession()} accessibilityLabel={t('chatbot.new_session')}>
-
+
{t('chatbot.new_session')}
@@ -145,7 +147,7 @@ export default function ChatbotScreen() {
-
+
diff --git a/src/app/(app)/command.tsx b/src/app/(app)/command.tsx
index 6fbb077..7e09779 100644
--- a/src/app/(app)/command.tsx
+++ b/src/app/(app)/command.tsx
@@ -72,6 +72,7 @@ import { useDirectMessage } from '@/hooks/use-direct-message';
import { useRecordsContext } from '@/hooks/use-records-context';
import { getIncidentRoleName } from '@/lib/incident-command-utils';
import { isWeb } from '@/lib/platform';
+import { readableColors } from '@/lib/utils';
import { ChatChannelType } from '@/models/v4/chat';
import { type IncidentNeedStatus, type ResourceAssignment, ResourceAssignmentKind } from '@/models/v4/incidentCommand/incidentCommandModels';
import { useCoreStore } from '@/stores/app/core-store';
@@ -647,6 +648,7 @@ export default function CommandBoard() {
? { latitude: callLatitude, longitude: callLongitude, isIcp: false }
: null;
const summaryPriority = activeCall?.CallId === boardState.callId ? activePriority : null;
+ const summaryPriorityColors = summaryPriority?.Color ? readableColors(summaryPriority.Color) : null;
return (
@@ -685,12 +687,13 @@ export default function CommandBoard() {
{boardState.isProvisional ? (
- {t('command.provisional_badge')}
+ {t('command.provisional_badge')}
) : null}
{summaryPriority ? (
-
- {summaryPriority.Name}
+
+ {/* A priority colour is fixed in both themes; pick black or white text for it rather than a fixed white. */}
+ {summaryPriority.Name}
) : null}
{/* Master scene timer — elapsed time since the call was logged */}
diff --git a/src/app/(app)/poi/[id].tsx b/src/app/(app)/poi/[id].tsx
index 5a319cc..d9171cb 100644
--- a/src/app/(app)/poi/[id].tsx
+++ b/src/app/(app)/poi/[id].tsx
@@ -98,8 +98,8 @@ export default function PoiDetailScreen() {
diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx
index 2a31b17..f9c9e51 100644
--- a/src/app/(app)/settings.tsx
+++ b/src/app/(app)/settings.tsx
@@ -124,7 +124,7 @@ export default function Settings() {
- setShowServerUrl(true)} textStyle="text-info-600" />
- setShowLoginInfo(true)} textStyle="text-info-600" />
-
- router.push('/account-security' as unknown as Href)} textStyle="text-info-600" />
+
- router.push('/settings/account-security' as unknown as Href)} textStyle="text-info-600" />
- router.push('/login/shared-device' as unknown as Href)} textStyle="text-info-600" />
- setShowLogoutConfirm(true)} textStyle="text-error-600" />
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 741400b..a1f3deb 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -31,7 +31,7 @@ import { PushNotificationModal } from '@/components/push-notification/push-notif
import { ToastContainer } from '@/components/toast/toast-container';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import { loadKeepAliveState } from '@/lib/hooks/use-keep-alive';
-import { loadSelectedTheme } from '@/lib/hooks/use-selected-theme';
+import { loadSelectedTheme, useSelectedTheme } from '@/lib/hooks/use-selected-theme';
import { logger } from '@/lib/logging';
import { registerNavigationReadyCheck } from '@/lib/navigation';
import { getDeviceUuid, setDeviceUuid } from '@/lib/storage/app';
@@ -211,10 +211,13 @@ function Providers({ children }: { children: React.ReactNode }) {
// nativewind's hook (not react-native's) so manually selected themes are
// reflected on web, where Appearance only tracks the system preference.
const { colorScheme } = useColorScheme();
+ // The provider gets the user's choice, not the resolved scheme: handing it the resolved 'light' or 'dark'
+ // pinned Appearance to that value, so a 'system' choice stopped following the OS until a restart.
+ const { selectedTheme } = useSelectedTheme();
const renderContent = () => (
-
+
{children}
diff --git a/src/app/call/[id].tsx b/src/app/call/[id].tsx
index bb13ffa..e9d6eeb 100644
--- a/src/app/call/[id].tsx
+++ b/src/app/call/[id].tsx
@@ -1,22 +1,6 @@
import { format, isValid } from 'date-fns';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
-import {
- BuildingIcon,
- ClockIcon,
- FileTextIcon,
- HistoryIcon,
- ImageIcon,
- InfoIcon,
- LoaderIcon,
- MessageSquareIcon,
- PaperclipIcon,
- RouteIcon,
- ShieldCheckIcon,
- TimerIcon,
- UserIcon,
- UsersIcon,
- VideoIcon,
-} from 'lucide-react-native';
+import { BuildingIcon, ClockIcon, FileTextIcon, HistoryIcon, ImageIcon, InfoIcon, MessageSquareIcon, PaperclipIcon, RouteIcon, ShieldCheckIcon, TimerIcon, UserIcon, UsersIcon, VideoIcon } from 'lucide-react-native';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
@@ -27,6 +11,7 @@ import { CallSiteInfoTabPanel } from '@/components/calls/call-site-info-tab-pane
import { LocationHistoryPanel } from '@/components/calls/location-history-panel';
import { UnitReadinessPanel } from '@/components/calls/unit-readiness-panel';
import { CheckInTabContent } from '@/components/check-in-timers/check-in-tab-content';
+import { isOverdueCheckInStatus } from '@/components/check-in-timers/check-in-timer-card';
import { MessageCommanderSheet } from '@/components/command/message-commander-sheet';
import { ReopenCommandSheet } from '@/components/command/reopen-command-sheet';
import { StartCommandSheet } from '@/components/command/start-command-sheet';
@@ -45,6 +30,7 @@ import { Heading } from '@/components/ui/heading';
import { HStack } from '@/components/ui/hstack';
import { HtmlRenderer } from '@/components/ui/html-renderer';
import { SharedTabs, type TabItem } from '@/components/ui/shared-tabs';
+import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { useAnalytics } from '@/hooks/use-analytics';
@@ -488,10 +474,10 @@ export default function CallDetail() {
{dispatched.Name}
-
+
{t('call_detail.group')}: {dispatched.Group}
-
+
{t('call_detail.type')}: {dispatched.Type}
@@ -518,7 +504,7 @@ export default function CallDetail() {
{event.StatusText}
-
+
{event.Name} - {event.Group}
{isValid(new Date(event.Timestamp)) ? new Date(event.Timestamp).toLocaleString() : ''}
@@ -570,7 +556,7 @@ export default function CallDetail() {
// Conditionally add check-in tab
if (call?.CheckInTimersEnabled) {
- const overdueCount = timerStatuses.filter((t) => t.Status === 'Overdue').length;
+ const overdueCount = timerStatuses.filter((timer) => isOverdueCheckInStatus(timer.Status)).length;
tabs.push({
key: 'checkin',
title: t('check_in.tab_title'),
@@ -635,7 +621,7 @@ export default function CallDetail() {
disabled={isSettingActive}
className={`${isSettingActive ? 'bg-primary-400 opacity-80' : 'bg-primary-500'} shadow-lg`}
>
- {isSettingActive ? : null}
+ {isSettingActive ? : null}
{isSettingActive ? t('command.starting') : hasCommandBoard ? t('command.open_board') : t('command.start_command')}
diff --git a/src/app/call/new/index.tsx b/src/app/call/new/index.tsx
index 8ebee81..0f6f5c9 100644
--- a/src/app/call/new/index.tsx
+++ b/src/app/call/new/index.tsx
@@ -732,7 +732,7 @@ export default function NewCall() {
{t('common.cancel')}
diff --git a/src/app/chat/[channelId].tsx b/src/app/chat/[channelId].tsx
index 62a15c9..a33073f 100644
--- a/src/app/chat/[channelId].tsx
+++ b/src/app/chat/[channelId].tsx
@@ -384,9 +384,9 @@ export default function ChannelConversationScreen() {
{/* IC delta: identity chip — messages in command channels post as the Incident Commander. */}
{isCommandChannel ? (
-
+
- {t('chat.chatting_as_ic')}
+ {t('chat.chatting_as_ic')}
) : null}
diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx
index a31f1df..8c5df22 100644
--- a/src/app/login/login-form.tsx
+++ b/src/app/login/login-form.tsx
@@ -211,7 +211,7 @@ export const LoginForm = ({ onSubmit = () => {}, isLoading = false, error = unde
) : null}
{onSsoPress ? (
) : null}
@@ -220,7 +220,7 @@ export const LoginForm = ({ onSubmit = () => {}, isLoading = false, error = unde
{/* Shared device: every sign-in here starts a shared session that locks between operators */}
{onSharedDevicePress ? (
{item.error ? (
-
+
{item.error}
) : null}
- {exhausted ? {t('offline_queue.retries_exhausted')} : null}
+ {exhausted ? {t('offline_queue.retries_exhausted')} : null}
{item.status === QueuedEventStatus.FAILED ? (
diff --git a/src/components/auth/login-mfa-sheet.tsx b/src/components/auth/login-mfa-sheet.tsx
index 1f39102..c04c8fb 100644
--- a/src/components/auth/login-mfa-sheet.tsx
+++ b/src/components/auth/login-mfa-sheet.tsx
@@ -229,7 +229,7 @@ export const LoginMfaSheet: React.FC = ({ isOpen, onLostFact
{!approval && showsCodeInput ? (
- {busy ? : {t('mfa.login.verify')}}
+ {busy ? : {t('mfa.login.verify')}}
) : null}
diff --git a/src/components/auth/login-otp-modal.tsx b/src/components/auth/login-otp-modal.tsx
index 8181d07..3680b09 100644
--- a/src/components/auth/login-otp-modal.tsx
+++ b/src/components/auth/login-otp-modal.tsx
@@ -86,7 +86,7 @@ export const LoginOtpModal: React.FC = ({ isOpen, isSubmitti
{t('common.cancel', 'Cancel')}
- {isSubmitting ? : {t('login.otp_verify', 'Verify')}}
+ {isSubmitting ? : {t('login.otp_verify', 'Verify')}}
diff --git a/src/components/call-video-feeds/video-feed-form-sheet.tsx b/src/components/call-video-feeds/video-feed-form-sheet.tsx
index 319aae5..b7f303e 100644
--- a/src/components/call-video-feeds/video-feed-form-sheet.tsx
+++ b/src/components/call-video-feeds/video-feed-form-sheet.tsx
@@ -152,14 +152,14 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* Name */}
- {t('video_feeds.name')} *
+ {t('video_feeds.name')} *
(
-
+
)}
/>
@@ -167,14 +167,14 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* URL */}
- {t('video_feeds.url')} *
+ {t('video_feeds.url')} *
(
- handleUrlBlur(value)} placeholder="https://" autoCapitalize="none" keyboardType="url" />
+ handleUrlBlur(value)} placeholder="https://" autoCapitalize="none" keyboardType="url" />
)}
/>
@@ -182,7 +182,7 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* Feed Type */}
- {t('video_feeds.feed_type')}
+ {t('video_feeds.feed_type')}
{FEED_TYPES.map((type) => (
setValue('FeedType', type.value)} className="mb-1">
@@ -194,7 +194,7 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* Feed Format */}
- {t('video_feeds.feed_format')}
+ {t('video_feeds.feed_format')}
{FEED_FORMATS.map((fmt) => (
setValue('FeedFormat', fmt.value)} className="mb-1">
@@ -206,13 +206,21 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* Description */}
- {t('video_feeds.description')}
+ {t('video_feeds.description')}
(
-
+
)}
/>
@@ -221,25 +229,25 @@ export const VideoFeedFormSheet: React.FC = ({ isOpen,
{/* Latitude / Longitude */}
- {t('video_feeds.latitude')}
+ {t('video_feeds.latitude')}
(
-
+
)}
/>
- {t('video_feeds.longitude')}
+ {t('video_feeds.longitude')}
(
-
+
)}
/>
diff --git a/src/components/calls/__tests__/location-history-panel.test.tsx b/src/components/calls/__tests__/location-history-panel.test.tsx
index 51e3354..8649806 100644
--- a/src/components/calls/__tests__/location-history-panel.test.tsx
+++ b/src/components/calls/__tests__/location-history-panel.test.tsx
@@ -30,6 +30,12 @@ jest.mock('lucide-react-native', () => {
return { ChevronDownIcon: View, ChevronUpIcon: View, HistoryIcon: View, InfoIcon: View, LockIcon: View, MapPinIcon: View };
});
+// ProtectedText draws its lock with the themed icon set.
+jest.mock('@/components/ui/lucide-icons', () => {
+ const { View } = jest.requireActual('react-native');
+ return { Lock: View };
+});
+
jest.mock('@/lib/logging', () => ({
logger: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() },
}));
@@ -126,6 +132,20 @@ describe('LocationHistoryPanel', () => {
expect(mockTrackEvent).toHaveBeenCalledWith('location_history_viewed', expect.objectContaining({ kind: 'call', id: '42', callCount: 2 }));
});
+ it('pairs each priority badge with text that reads on it, falling back to grey for a colour it cannot read', async () => {
+ const yellow = { ...baseHistory.Calls[0], CallId: '7100', PriorityText: 'Caution', PriorityColor: 'rgb(255, 255, 0)' };
+ const unreadable = { ...baseHistory.Calls[0], CallId: '7200', PriorityText: 'Odd', PriorityColor: 'hsl(60, 100%, 50%)' };
+ mockCallHistory.mockResolvedValueOnce({ Data: { ...baseHistory, Calls: [yellow, unreadable] } } as never);
+
+ render();
+
+ await waitFor(() => expect(screen.getByTestId('location-history-priority-7100')).toBeTruthy());
+ expect(screen.getByTestId('location-history-priority-7100')).toHaveStyle({ backgroundColor: 'rgb(255, 255, 0)' });
+ expect(screen.getByText('Caution')).toHaveStyle({ color: '#000000' });
+ expect(screen.getByTestId('location-history-priority-7200')).toHaveStyle({ backgroundColor: '#6b7280' });
+ expect(screen.getByText('Odd')).toHaveStyle({ color: '#FFFFFF' });
+ });
+
it('shows a lock instead of the REDACTED sentinel for protected call text', async () => {
mockCallHistory.mockResolvedValueOnce({ Data: baseHistory } as never);
diff --git a/src/components/calls/call-card.tsx b/src/components/calls/call-card.tsx
index 6abedb4..aac83d4 100644
--- a/src/components/calls/call-card.tsx
+++ b/src/components/calls/call-card.tsx
@@ -10,7 +10,7 @@ import { HtmlRenderer } from '@/components/ui/html-renderer';
import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
-import { getTimeAgoUtc, invertColor } from '@/lib/utils';
+import { getTimeAgoUtc, readableColors } from '@/lib/utils';
import { type CallPriorityResultData } from '@/models/v4/callPriorities/callPriorityResultData';
import type { CallResultData } from '@/models/v4/calls/callResultData';
import type { DispatchedEventResultData } from '@/models/v4/calls/dispatchedEventResultData';
@@ -43,7 +43,7 @@ interface CallCardProps {
export const CallCard: React.FC = ({ call, priority, showTimerIcon = false, isTimerOverdue = false, dispatches, onStartCommand, isCommandCall = false, hasCommand = false }) => {
const { t } = useTranslation();
- const textColor = invertColor(getColor(call, priority), true);
+ const { backgroundColor, textColor } = readableColors(getColor(call, priority));
const pulseAnim = useRef(new Animated.Value(1)).current;
const destinationLabel = call.DestinationName || call.DestinationAddress || '';
@@ -62,14 +62,14 @@ export const CallCard: React.FC = ({ call, priority, showTimerIco
return (
{/* Header with Call Number and Priority */}
-
+
= ({ call, priority, showTimerIco
{hasCommand ? t('command.open_board') : t('command.start_command')}
{isCommandCall ? (
-
+
{t('command.active_badge')}
) : null}
diff --git a/src/components/calls/call-detail-menu.tsx b/src/components/calls/call-detail-menu.tsx
index 94c2d9a..43084c4 100644
--- a/src/components/calls/call-detail-menu.tsx
+++ b/src/components/calls/call-detail-menu.tsx
@@ -1,4 +1,5 @@
import { EditIcon, MoreVerticalIcon, XIcon } from 'lucide-react-native';
+import { useColorScheme } from 'nativewind';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal, Pressable as RNPressable, StyleSheet, View } from 'react-native';
@@ -31,6 +32,8 @@ const HeaderRightMenuButton = ({ onPress }: HeaderRightMenuButtonProps) => {
export const useCallDetailMenu = ({ onEditCall, onCloseCall, canUserCreateCalls = false }: CallDetailMenuProps) => {
const { t } = useTranslation();
const { trackEvent } = useAnalytics();
+ const { colorScheme } = useColorScheme();
+ const isDark = colorScheme === 'dark';
const [isKebabMenuOpen, setIsKebabMenuOpen] = useState(false);
const closeMenu = () => setIsKebabMenuOpen(false);
@@ -81,7 +84,7 @@ export const useCallDetailMenu = ({ onEditCall, onCloseCall, canUserCreateCalls
supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
>
-
+
-
+
{t('call_detail.edit_call')}
@@ -115,7 +118,7 @@ export const useCallDetailMenu = ({ onEditCall, onCloseCall, canUserCreateCalls
);
- }, [isKebabMenuOpen, canUserCreateCalls, t, onEditCall, onCloseCall]);
+ }, [isKebabMenuOpen, canUserCreateCalls, t, onEditCall, onCloseCall, isDark]);
return {
HeaderRightMenu,
@@ -139,6 +142,9 @@ const menuStyles = StyleSheet.create({
paddingBottom: 34,
paddingTop: 8,
},
+ sheetDark: {
+ backgroundColor: '#1F2937',
+ },
handle: {
width: 36,
height: 4,
diff --git a/src/components/calls/call-files-modal.tsx b/src/components/calls/call-files-modal.tsx
index db45221..2874b28 100644
--- a/src/components/calls/call-files-modal.tsx
+++ b/src/components/calls/call-files-modal.tsx
@@ -273,7 +273,7 @@ export const CallFilesModal: React.FC = ({ isOpen, onClose,
{t('calls.files.title')}
-
+
diff --git a/src/components/calls/call-site-info-tab-panel.tsx b/src/components/calls/call-site-info-tab-panel.tsx
index a5f6410..11dc1f1 100644
--- a/src/components/calls/call-site-info-tab-panel.tsx
+++ b/src/components/calls/call-site-info-tab-panel.tsx
@@ -36,7 +36,7 @@ const SiteContactCard: React.FC = ({ site, callId }) => {
return (
-
+
{nameRedacted ? : site.ContactType === 1 ? : }
@@ -55,7 +55,7 @@ const SiteContactCard: React.FC = ({ site, callId }) => {
Linking.openURL(`tel:${site.PhoneNumber}`).catch(() => {})} className="mb-2" testID={`site-contact-phone-${site.ContactId}`}>
- {site.PhoneNumber}
+ {site.PhoneNumber}
) : null}
diff --git a/src/components/calls/location-history-panel.tsx b/src/components/calls/location-history-panel.tsx
index a816111..2270424 100644
--- a/src/components/calls/location-history-panel.tsx
+++ b/src/components/calls/location-history-panel.tsx
@@ -14,7 +14,7 @@ import { VStack } from '@/components/ui/vstack';
import { useAnalytics } from '@/hooks/use-analytics';
import { useProtectedGrantRefresh } from '@/hooks/use-protected-grant-refresh';
import { ProtectedFieldIds } from '@/lib/data-protection/redacted';
-import { formatDateForDisplay, parseUtcMs } from '@/lib/utils';
+import { formatDateForDisplay, parseUtcMs, readableColors } from '@/lib/utils';
import { type LocationHistoryCallData, type LocationHistoryMatch } from '@/models/v4/calls/locationHistoryResult';
import { locationHistoryKey, type LocationHistorySource, useLocationHistoryStore } from '@/stores/calls/location-history-store';
@@ -49,6 +49,7 @@ const HistoryCallCard: React.FC = ({ call, onOpenCall }) =
const loggedOn = call.LoggedOn || formatUtcTimestamp(call.LoggedOnUtc);
const handleOpen = useCallback(() => onOpenCall(call.CallId), [onOpenCall, call.CallId]);
const toggleNotes = useCallback(() => setExpanded((value) => !value), []);
+ const priorityColors = readableColors(call.PriorityColor, '#6b7280');
return (
@@ -56,9 +57,11 @@ const HistoryCallCard: React.FC = ({ call, onOpenCall }) =
- {call.Number}
-
- {call.PriorityText || t('location_history.unknown_priority')}
+ {call.Number}
+
+
+ {call.PriorityText || t('location_history.unknown_priority')}
+
{t(`location_history.state.${call.State}`, String(call.State))}
@@ -97,7 +100,7 @@ const HistoryCallCard: React.FC = ({ call, onOpenCall }) =
{expanded ? : }
- {expanded ? t('location_history.hide_notes') : t('location_history.show_notes', { count: call.Notes.length })}
+ {expanded ? t('location_history.hide_notes') : t('location_history.show_notes', { count: call.Notes.length })}
{expanded ? (
diff --git a/src/components/chat/ack-banner.tsx b/src/components/chat/ack-banner.tsx
index 50b999f..e710665 100644
--- a/src/components/chat/ack-banner.tsx
+++ b/src/components/chat/ack-banner.tsx
@@ -1,9 +1,9 @@
-import { AlertTriangle } from 'lucide-react-native';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button, ButtonText } from '@/components/ui/button';
import { HStack } from '@/components/ui/hstack';
+import { AlertTriangle } from '@/components/ui/lucide-icons';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { type ChatAckResultData } from '@/models/v4/chat';
@@ -23,7 +23,7 @@ export function AckBanner({ acks, onAcknowledge }: AckBannerProps) {
return (
-
+
{t('chat.ack_required')}
{acks.length > 1 ? t('chat.ack_pending_count', { count: acks.length }) : t('chat.ack_pending_one')}
diff --git a/src/components/chat/message-bubble.tsx b/src/components/chat/message-bubble.tsx
index 4851bed..9c0f255 100644
--- a/src/components/chat/message-bubble.tsx
+++ b/src/components/chat/message-bubble.tsx
@@ -1,5 +1,5 @@
import { Image } from 'expo-image';
-import { AlertTriangle, Clock, MapPin, MessageSquare, Pin, RefreshCw } from 'lucide-react-native';
+import { Clock, MapPin, MessageSquare, Pin, RefreshCw } from 'lucide-react-native';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking } from 'react-native';
@@ -8,6 +8,7 @@ import { getChatAttachmentImageSource } from '@/api/chat/chat';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { Box } from '@/components/ui/box';
import { HStack } from '@/components/ui/hstack';
+import { AlertTriangle } from '@/components/ui/lucide-icons';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
@@ -63,7 +64,7 @@ export function MessageBubble({ message, isOwn, showSender, currentUserId, onLon
const isPending = message._localStatus === 'pending';
const isFailed = message._localStatus === 'failed';
- const bubbleTone = isOwn ? 'bg-primary-600' : 'bg-background-100';
+ const bubbleTone = isOwn ? 'bg-primary-600 dark:bg-primary-400' : 'bg-background-100';
const textTone = isOwn ? 'text-white' : 'text-typography-900';
const urgentClasses = isUrgent && !isOwn ? 'border-2 border-error-500 bg-error-50' : isUrgent && isOwn ? 'border-2 border-error-300' : '';
@@ -142,7 +143,7 @@ export function MessageBubble({ message, isOwn, showSender, currentUserId, onLon
{isUrgent && !isDeleted ? (
-
+
{t('chat.urgent')}
) : null}
diff --git a/src/components/chat/message-composer.tsx b/src/components/chat/message-composer.tsx
index 51ad4c8..72d6f24 100644
--- a/src/components/chat/message-composer.tsx
+++ b/src/components/chat/message-composer.tsx
@@ -148,7 +148,7 @@ export function MessageComposer({ onSendText, onSendImage, onSendLocation, onOpe
) : null}
-
+
diff --git a/src/components/chat/new-conversation-sheet.tsx b/src/components/chat/new-conversation-sheet.tsx
index 654e0f7..16f6dfc 100644
--- a/src/components/chat/new-conversation-sheet.tsx
+++ b/src/components/chat/new-conversation-sheet.tsx
@@ -7,7 +7,7 @@ import { getRecipients } from '@/api/messaging/messages';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '@/components/ui/actionsheet';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { Box } from '@/components/ui/box';
-import { Button, ButtonText } from '@/components/ui/button';
+import { Button, ButtonIcon, ButtonText } from '@/components/ui/button';
import { Center } from '@/components/ui/center';
import { HStack } from '@/components/ui/hstack';
import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input';
@@ -210,7 +210,7 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
) : null}
{mode === 'group' && isSelected ? (
-
+
) : null}
@@ -223,8 +223,8 @@ export function NewConversationSheet({ isOpen, onClose, mode, onCreated }: NewCo
{mode === 'group' ? (
-
- {t('chat.create_group_with', { count: selected.size })}
+
+ {t('chat.create_group_with', { count: selected.size })}
) : null}
diff --git a/src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx b/src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx
index be8b9f0..a45009a 100644
--- a/src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx
+++ b/src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx
@@ -3,7 +3,7 @@ import { render, fireEvent } from '@testing-library/react-native';
import type { CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData';
-import { CheckInTimerCard } from '../check-in-timer-card';
+import { CheckInTimerCard, isOverdueCheckInStatus } from '../check-in-timer-card';
jest.mock('react-i18next', () => ({
useTranslation: () => ({
@@ -128,4 +128,36 @@ describe('CheckInTimerCard', () => {
expect(getByText('check_in.status_overdue')).toBeTruthy();
});
+
+ it.each([
+ ['Green', 'check_in.status_green'],
+ ['Warning', 'check_in.status_warning'],
+ ['Critical', 'check_in.status_critical'],
+ ])('should render the translated label for server status %s', (status, labelKey) => {
+ const timer = createMockTimer({ Status: status });
+
+ const { getByText, unmount } = render();
+
+ expect(getByText(labelKey)).toBeTruthy();
+ unmount();
+ });
+
+ it('should show the raw status instead of an i18n key for an unknown status', () => {
+ const timer = createMockTimer({ Status: 'NoTimer' });
+
+ const { getByText, queryByText, unmount } = render();
+
+ expect(getByText('NoTimer')).toBeTruthy();
+ expect(queryByText('check_in.status_notimer')).toBeNull();
+ unmount();
+ });
+});
+
+describe('isOverdueCheckInStatus', () => {
+ it('treats Critical and legacy Overdue as overdue', () => {
+ expect(isOverdueCheckInStatus('Critical')).toBe(true);
+ expect(isOverdueCheckInStatus('Overdue')).toBe(true);
+ expect(isOverdueCheckInStatus('Warning')).toBe(false);
+ expect(isOverdueCheckInStatus('Green')).toBe(false);
+ });
});
diff --git a/src/components/check-in-timers/check-in-bottom-sheet.tsx b/src/components/check-in-timers/check-in-bottom-sheet.tsx
index e64e5e7..646299d 100644
--- a/src/components/check-in-timers/check-in-bottom-sheet.tsx
+++ b/src/components/check-in-timers/check-in-bottom-sheet.tsx
@@ -73,7 +73,7 @@ export const CheckInBottomSheet: React.FC = ({ isOpen,
{/* Type selector */}
- {t('check_in.select_type')}
+ {t('check_in.select_type')}
{CHECK_IN_TYPES.map((type) => (
setSelectedType(type.value)} className="mb-1">
@@ -85,9 +85,9 @@ export const CheckInBottomSheet: React.FC = ({ isOpen,
{/* Note input */}
- {t('check_in.add_note')}
+ {t('check_in.add_note')}
-
+
diff --git a/src/components/check-in-timers/check-in-timer-card.tsx b/src/components/check-in-timers/check-in-timer-card.tsx
index 5b5ef48..a106643 100644
--- a/src/components/check-in-timers/check-in-timer-card.tsx
+++ b/src/components/check-in-timers/check-in-timer-card.tsx
@@ -12,12 +12,25 @@ import { VStack } from '@/components/ui/vstack';
import { parseUtcMs } from '@/lib/utils';
import type { CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData';
+// The server reports Green / Warning / Critical; Ok / Overdue are the legacy names for the same states.
const STATUS_COLORS: Record = {
Ok: '#22C55E',
+ Green: '#22C55E',
Warning: '#F59E0B',
Overdue: '#EF4444',
+ Critical: '#EF4444',
};
+const STATUS_LABEL_KEYS: Record = {
+ Ok: 'check_in.status_ok',
+ Green: 'check_in.status_green',
+ Warning: 'check_in.status_warning',
+ Overdue: 'check_in.status_overdue',
+ Critical: 'check_in.status_critical',
+};
+
+export const isOverdueCheckInStatus = (status: string): boolean => status === 'Overdue' || status === 'Critical';
+
interface CheckInTimerCardProps {
timer: CheckInTimerStatusResultData;
onCheckIn: () => void;
@@ -43,7 +56,7 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe
// Pulse animation for overdue
useEffect(() => {
- if (timer.Status === 'Overdue') {
+ if (isOverdueCheckInStatus(timer.Status)) {
const animation = Animated.loop(
Animated.sequence([Animated.timing(pulseAnim, { toValue: 0.5, duration: 800, useNativeDriver: true }), Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true })])
);
@@ -57,7 +70,7 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe
const statusColor = STATUS_COLORS[timer.Status] ?? '#808080';
const duration = timer.DurationMinutes ? Number(timer.DurationMinutes) : 0;
const progress = duration > 0 ? Math.min(localElapsed / duration, 1) : 0;
- const safeStatusLower = typeof timer.Status === 'string' ? timer.Status.toLowerCase() : '';
+ const statusLabelKey = STATUS_LABEL_KEYS[timer.Status];
// LastCheckIn is a zone-less UTC string; parseUtcMs pins it so the gap is right off-UTC.
const lastCheckInMs = timer.LastCheckIn ? parseUtcMs(timer.LastCheckIn) : null;
const minutesSinceLastCheckIn = lastCheckInMs === null ? 0 : differenceInMinutes(new Date(), lastCheckInMs);
@@ -76,13 +89,13 @@ export const CheckInTimerCard: React.FC = ({ timer, onChe
- {t(`check_in.status_${safeStatusLower}`)}
+ {statusLabelKey ? t(statusLabelKey) : timer.Status}
{/* Progress bar */}
-
+
diff --git a/src/components/command/__tests__/command-board.test.tsx b/src/components/command/__tests__/command-board.test.tsx
index 1cb55a2..fc10f7c 100644
--- a/src/components/command/__tests__/command-board.test.tsx
+++ b/src/components/command/__tests__/command-board.test.tsx
@@ -43,4 +43,25 @@ describe('CommandBoard', () => {
expect(screen.getByText('Jane Doe')).toBeTruthy();
expect(screen.getByText('Critical')).toBeTruthy();
});
+
+ it('renders a member once when accountability contains duplicate rows for them', () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const duplicated = {
+ ...board,
+ Accountability: [
+ { UserId: 'u1', FullName: 'Jane Doe', Status: 'Critical' },
+ { UserId: 'u2', FullName: 'John Roe', Status: 'Green' },
+ { UserId: 'u1', FullName: 'Jane Doe', Status: 'Critical' },
+ ],
+ } as unknown as IncidentCommandBoard;
+
+ const { unmount } = render();
+
+ expect(screen.getAllByText('Jane Doe')).toHaveLength(1);
+ expect(screen.getByText('John Roe')).toBeTruthy();
+ expect(consoleErrorSpy.mock.calls.some(([message]) => String(message).includes('same key'))).toBe(false);
+
+ unmount();
+ consoleErrorSpy.mockRestore();
+ });
});
diff --git a/src/components/command/__tests__/resource-cards.test.tsx b/src/components/command/__tests__/resource-cards.test.tsx
index 5e38e30..994f52d 100644
--- a/src/components/command/__tests__/resource-cards.test.tsx
+++ b/src/components/command/__tests__/resource-cards.test.tsx
@@ -95,4 +95,16 @@ describe('PersonnelResourceCard', () => {
unmount();
});
+
+ it('pairs each status badge with text that reads on it, swapping a colour it cannot read for grey', () => {
+ const tinted = { ...person, StatusColor: '#ffff0080', StaffingColor: 'rgb(255, 255, 0)' } as PersonnelInfoResultData;
+ const { getByText, getByTestId, unmount } = render();
+
+ expect(getByTestId('card-4-status')).toHaveStyle({ backgroundColor: '#6b7280' });
+ expect(getByText('Available')).toHaveStyle({ color: '#FFFFFF' });
+ expect(getByTestId('card-4-staffing')).toHaveStyle({ backgroundColor: 'rgb(255, 255, 0)' });
+ expect(getByText('On Shift')).toHaveStyle({ color: '#000000' });
+
+ unmount();
+ });
});
diff --git a/src/components/command/accountability-section.tsx b/src/components/command/accountability-section.tsx
index d178d0e..0440322 100644
--- a/src/components/command/accountability-section.tsx
+++ b/src/components/command/accountability-section.tsx
@@ -182,7 +182,7 @@ export const AccountabilitySection: React.FC = ({ ca
{timersEnabled ? (
-
+
{t('common.refresh')}
) : null}
@@ -209,7 +209,7 @@ export const AccountabilitySection: React.FC = ({ ca
- {entry.Status === 'Green' ? t('command.par_green') : entry.Status === 'Warning' ? t('command.par_warning') : t('command.par_critical')}
+ {entry.Status === 'Green' ? t('command.par_green') : entry.Status === 'Warning' ? t('command.par_warning') : t('command.par_critical')}
handlePersonnelCheckIn(entry.UserId)} isDisabled={isCheckingIn} testID={`accountability-check-in-${entry.UserId}`}>
{t('check_in.perform_check_in')}
@@ -233,9 +233,7 @@ export const AccountabilitySection: React.FC = ({ ca
-
- {timer.Status === 'Green' || timer.Status === 'Ok' ? t('command.par_green') : timer.Status === 'Warning' ? t('command.par_warning') : t('command.par_critical')}
-
+ {timer.Status === 'Green' || timer.Status === 'Ok' ? t('command.par_green') : timer.Status === 'Warning' ? t('command.par_warning') : t('command.par_critical')}
handleTimerCheckIn(timer)} isDisabled={isCheckingIn || target === null} testID={`accountability-timer-check-in-${timer.TargetType}-${timer.TargetEntityId}`}>
{t('check_in.perform_check_in')}
diff --git a/src/components/command/command-board.tsx b/src/components/command/command-board.tsx
index efc2053..46530fb 100644
--- a/src/components/command/command-board.tsx
+++ b/src/components/command/command-board.tsx
@@ -8,6 +8,7 @@ import { Heading } from '@/components/ui/heading';
import { HStack } from '@/components/ui/hstack';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
+import { dedupeByUserId } from '@/lib/incident-command-utils';
import { type IncidentCommandBoard } from '@/models/v4/incidentCommand/incidentCommandBoard';
import { CommandNodeType, TacticalObjectiveStatus } from '@/models/v4/incidentCommand/incidentCommandEnums';
import { type PersonnelCallCheckInStatus } from '@/models/v4/incidentCommand/personnelCallCheckInStatus';
@@ -29,6 +30,7 @@ export const CommandBoard: React.FC = ({ board }) => {
const liveNodes = board.Nodes.filter((n) => !n.DeletedOn).sort((a, b) => a.SortOrder - b.SortOrder);
const liveAssignments = board.Assignments.filter((a) => !a.ReleasedOn);
const objectives = board.Objectives.slice().sort((a, b) => a.SortOrder - b.SortOrder);
+ const accountability = dedupeByUserId(board.Accountability);
return (
@@ -78,10 +80,10 @@ export const CommandBoard: React.FC = ({ board }) => {
{/* Personnel accountability / PAR */}
{t('incidents.accountability')}
- {board.Accountability.length === 0 ? (
+ {accountability.length === 0 ? (
{t('incidents.unassigned')}
) : (
- board.Accountability.map((person) => (
+ accountability.map((person) => (
{person.FullName ?? person.UserId}
diff --git a/src/components/command/landscape-structure-board.tsx b/src/components/command/landscape-structure-board.tsx
index c3a3492..84f687a 100644
--- a/src/components/command/landscape-structure-board.tsx
+++ b/src/components/command/landscape-structure-board.tsx
@@ -67,10 +67,10 @@ export const WorkTimeLight: React.FC<{ assignedOn?: string | null; rotationAfter
return (
- {`${minutes}m`}
+ {`${minutes}m`}
{isRotationDue ? (
- {t('command.rotation_due')}
+ {t('command.rotation_due')}
) : null}
@@ -234,7 +234,7 @@ const DraggableResourceCard: React.FC = React.memo(
accessibilityHint={t('command.drag_move_hint')}
accessibilityLabel={name}
accessibilityRole={containerButtonRole}
- className={`rounded-lg border px-3 py-2 ${isSelected || isDragging ? 'border-primary-500 bg-primary-50 dark:bg-primary-950' : assignment.RequirementsWarning ? 'border-2 border-amber-500 bg-white dark:bg-gray-900' : 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'}`}
+ className={`rounded-lg border px-3 py-2 ${isSelected || isDragging ? 'border-primary-500 bg-primary-50' : assignment.RequirementsWarning ? 'border-2 border-amber-500 bg-white dark:bg-gray-900' : 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900'}`}
delayLongPress={LONG_PRESS_DELAY_MS}
onLongPress={handleLongPress}
onPress={handlePress}
@@ -264,12 +264,12 @@ const DraggableResourceCard: React.FC = React.memo(
{assignment.ResourceAssignmentId.startsWith('local-') ? : null}
{assignment.RequirementsWarning ? (
- {t('command.requirements_warning')}
+ {t('command.requirements_warning')}
) : null}
{isSelected ? (
- {t('command.selected')}
+ {t('command.selected')}
) : null}
@@ -373,7 +373,7 @@ export const LandscapeStructureBoard: React.FC = (
-
+
{selectedAssignment ? t('command.move_selected_hint', { resource: resolveResourceName(selectedAssignment.ResourceKind, selectedAssignment.ResourceId) }) : t('command.drag_move_hint')}
@@ -401,7 +401,7 @@ export const LandscapeStructureBoard: React.FC = (
}
accessibilityRole={containerButtonRole}
style={node.Color ? { borderTopWidth: 4, borderTopColor: node.Color } : undefined}
- className={`h-full rounded-xl border p-3 ${isMoveTarget ? 'border-primary-400 bg-primary-50 dark:bg-primary-950' : 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900'}`}
+ className={`h-full rounded-xl border p-3 ${isMoveTarget ? 'border-primary-400 bg-primary-50' : 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900'}`}
onPress={() => handleLanePress(node.CommandStructureNodeId)}
testID={`landscape-lane-${node.CommandStructureNodeId}`}
>
@@ -418,7 +418,7 @@ export const LandscapeStructureBoard: React.FC = (
{isUnderstaffed ? (
- {t('command.lane_understaffed', { count: laneUnitCount, min: node.MinUnits })}
+ {t('command.lane_understaffed', { count: laneUnitCount, min: node.MinUnits })}
) : null}
{node.CommandStructureNodeId.startsWith('local-') ? : null}
diff --git a/src/components/command/need-details-sheet.tsx b/src/components/command/need-details-sheet.tsx
index c922faf..d32292d 100644
--- a/src/components/command/need-details-sheet.tsx
+++ b/src/components/command/need-details-sheet.tsx
@@ -158,7 +158,7 @@ export const NeedDetailsSheet: React.FC = ({ isOpen, onCl
{need.Name}
- {getNeedStatusName(t, need.Status)}
+ {getNeedStatusName(t, need.Status)}
diff --git a/src/components/command/need-entity-status-list.tsx b/src/components/command/need-entity-status-list.tsx
index 45a231c..5381066 100644
--- a/src/components/command/need-entity-status-list.tsx
+++ b/src/components/command/need-entity-status-list.tsx
@@ -8,6 +8,7 @@ import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { logger } from '@/lib/logging';
+import { readableTextColor } from '@/lib/utils';
import { type IncidentNeedEntity, NeedEntityKind } from '@/models/v4/incidentCommand/incidentCommandModels';
import { type PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData';
import { type UnitStatusResultData } from '@/models/v4/unitStatus/unitStatusResultData';
@@ -17,11 +18,15 @@ const FALLBACK_COLOR = '#6b7280';
const asHexColor = (value?: string | null): string | null => (value && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim()) ? value.trim() : null);
/** Solid chip tinted with a server-provided status color. */
-const StatusChip: React.FC<{ label: string; color?: string | null; testID?: string }> = ({ label, color, testID }) => (
-
- {label}
-
-);
+const StatusChip: React.FC<{ label: string; color?: string | null; testID?: string }> = ({ label, color, testID }) => {
+ const background = asHexColor(color) ?? FALLBACK_COLOR;
+ // Status colours run from dark to pastel, so a fixed white label vanishes on the lighter ones.
+ return (
+
+ {label}
+
+ );
+};
interface NeedEntityStatusListProps {
incidentNeedId: string;
diff --git a/src/components/command/needs-section.tsx b/src/components/command/needs-section.tsx
index 9c79ef8..381f886 100644
--- a/src/components/command/needs-section.tsx
+++ b/src/components/command/needs-section.tsx
@@ -114,7 +114,7 @@ export const NeedsSection: React.FC = ({ needs, onAdd, onSetS
- {getNeedStatusName(t, need.Status)}
+ {getNeedStatusName(t, need.Status)}
{isMet ? (
onSetStatus(need.IncidentNeedId, IncidentNeedStatus.Open)} className="p-2" testID={`need-reopen-${need.IncidentNeedId}`}>
diff --git a/src/components/command/notes-section.tsx b/src/components/command/notes-section.tsx
index 7656f7e..4817c59 100644
--- a/src/components/command/notes-section.tsx
+++ b/src/components/command/notes-section.tsx
@@ -89,7 +89,7 @@ export const NotesSection: React.FC = ({ notes, onAdd }) => {
setVisibility(IncidentContentVisibility.Internal)}
- className={`flex-1 flex-row items-center justify-center rounded-lg border px-3 py-2 ${visibility === IncidentContentVisibility.Internal ? 'border-primary-500 bg-primary-50 dark:bg-primary-950' : 'border-gray-300 dark:border-gray-700'}`}
+ className={`flex-1 flex-row items-center justify-center rounded-lg border px-3 py-2 ${visibility === IncidentContentVisibility.Internal ? 'border-primary-500 bg-primary-50' : 'border-gray-300 dark:border-gray-700'}`}
testID="command-note-visibility-internal"
>
@@ -97,7 +97,7 @@ export const NotesSection: React.FC = ({ notes, onAdd }) => {
setVisibility(IncidentContentVisibility.Public)}
- className={`flex-1 flex-row items-center justify-center rounded-lg border px-3 py-2 ${visibility === IncidentContentVisibility.Public ? 'border-primary-500 bg-primary-50 dark:bg-primary-950' : 'border-gray-300 dark:border-gray-700'}`}
+ className={`flex-1 flex-row items-center justify-center rounded-lg border px-3 py-2 ${visibility === IncidentContentVisibility.Public ? 'border-primary-500 bg-primary-50' : 'border-gray-300 dark:border-gray-700'}`}
testID="command-note-visibility-public"
>
diff --git a/src/components/command/objective-details-sheet.tsx b/src/components/command/objective-details-sheet.tsx
index a61d0a4..d78fb1d 100644
--- a/src/components/command/objective-details-sheet.tsx
+++ b/src/components/command/objective-details-sheet.tsx
@@ -102,7 +102,7 @@ export const ObjectiveDetailsSheet: React.FC = ({ is
{isComplete ? (
- {t(getObjectiveOutcomeKey(objective.Outcome ?? 0))}
+ {t(getObjectiveOutcomeKey(objective.Outcome ?? 0))}
) : (
diff --git a/src/components/command/objectives-section.tsx b/src/components/command/objectives-section.tsx
index e852dc5..e018ca1 100644
--- a/src/components/command/objectives-section.tsx
+++ b/src/components/command/objectives-section.tsx
@@ -94,7 +94,7 @@ export const ObjectivesSection: React.FC = ({ objectives
{isComplete ? (
- {(objective.Outcome ?? 0) !== 0 ? t(getObjectiveOutcomeKey(objective.Outcome ?? 0)) : t('command.objective_completed')}
+ {(objective.Outcome ?? 0) !== 0 ? t(getObjectiveOutcomeKey(objective.Outcome ?? 0)) : t('command.objective_completed')}
) : null}
diff --git a/src/components/command/resource-cards.tsx b/src/components/command/resource-cards.tsx
index 608015b..b958660 100644
--- a/src/components/command/resource-cards.tsx
+++ b/src/components/command/resource-cards.tsx
@@ -12,6 +12,7 @@ import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { VStack } from '@/components/ui/vstack';
import { isWeb } from '@/lib/platform';
+import { readableColors } from '@/lib/utils';
import type { PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData';
import type { ActiveUnitRoleResultData } from '@/models/v4/unitRoles/activeUnitRoleResultData';
import type { UnitResultData } from '@/models/v4/units/unitResultData';
@@ -24,10 +25,13 @@ const asHexColor = (value?: string | null) => (value && /^#[0-9a-fA-F]{3,8}$/.te
const oneLine = isWeb ? ({ isTruncated: true } as const) : ({ numberOfLines: 1 } as const);
const ServerColorBadge: React.FC<{ label: string; color?: string | null; testID?: string }> = ({ label, color, testID }) => {
- const hex = asHexColor(color);
+ const { backgroundColor, textColor } = readableColors(color, '#6b7280');
return (
-
- {label}
+
+ {/* Status colours run from dark to pastel, so pick black or white text for each rather than a fixed white. */}
+
+ {label}
+
);
};
@@ -89,7 +93,7 @@ export const UnitResourceCard: React.FC = ({ name, unit,
return (
-
+
@@ -156,8 +160,8 @@ export const PersonnelResourceCard: React.FC = ({ na
return (
-
- {initials}
+
+ {initials}
{statusDot ? : null}
@@ -187,9 +191,6 @@ export const PersonnelResourceCard: React.FC = ({ na
};
const styles = StyleSheet.create({
- badgeFallback: {
- backgroundColor: '#6b7280',
- },
statusDot: {
position: 'absolute',
right: -1,
diff --git a/src/components/command/resource-details-sheet.tsx b/src/components/command/resource-details-sheet.tsx
index 1403431..423563f 100644
--- a/src/components/command/resource-details-sheet.tsx
+++ b/src/components/command/resource-details-sheet.tsx
@@ -16,7 +16,7 @@ import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { VStack } from '@/components/ui/vstack';
-import { parseUtcMs } from '@/lib/utils';
+import { parseUtcMs, readableColors } from '@/lib/utils';
import type { MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
import type { PersonnelInfoResultData } from '@/models/v4/personnel/personnelInfoResultData';
import type { ActiveUnitRoleResultData } from '@/models/v4/unitRoles/activeUnitRoleResultData';
@@ -48,10 +48,13 @@ const parseCoords = (lat?: number | string | null, lon?: number | string | null)
};
const ColorBadge: React.FC<{ label: string; color?: string | null; testID?: string }> = ({ label, color, testID }) => {
- const hex = asHexColor(color);
+ const { backgroundColor, textColor } = readableColors(color, '#6b7280');
return (
-
- {label}
+
+ {/* Status colours run from dark to pastel, so pick black or white text for each rather than a fixed white. */}
+
+ {label}
+
);
};
@@ -322,9 +325,6 @@ export const ResourceDetailsSheet: React.FC = ({
};
const styles = StyleSheet.create({
- badgeFallback: {
- backgroundColor: '#6b7280',
- },
laneDot: {
width: 10,
height: 10,
diff --git a/src/components/command/structure-section.tsx b/src/components/command/structure-section.tsx
index 8ba54c3..ce265af 100644
--- a/src/components/command/structure-section.tsx
+++ b/src/components/command/structure-section.tsx
@@ -85,7 +85,7 @@ export const StructureSection: React.FC = ({
{activeAssignments.length > 0 ? (
-
+
{selectedAssignment ? t('command.move_selected_hint', { resource: resolveResourceName(selectedAssignment.ResourceKind, selectedAssignment.ResourceId) }) : t('command.drag_move_hint')}
) : null}
@@ -103,7 +103,7 @@ export const StructureSection: React.FC = ({
key={node.CommandStructureNodeId}
accessibilityHint={selectedAssignment ? t('command.move_resource_to_lane', { resource: resolveResourceName(selectedAssignment.ResourceKind, selectedAssignment.ResourceId), lane: node.Name }) : undefined}
style={node.Color ? { borderLeftWidth: 4, borderLeftColor: node.Color } : undefined}
- className={`rounded-lg border p-3 ${selectedAssignment && selectedAssignment.CommandStructureNodeId !== node.CommandStructureNodeId ? 'border-primary-400 bg-primary-50 dark:bg-primary-950' : 'border-gray-200 dark:border-gray-700'}`}
+ className={`rounded-lg border p-3 ${selectedAssignment && selectedAssignment.CommandStructureNodeId !== node.CommandStructureNodeId ? 'border-primary-400 bg-primary-50' : 'border-gray-200 dark:border-gray-700'}`}
onPress={() => void handleLanePress(node.CommandStructureNodeId)}
testID={`lane-${node.CommandStructureNodeId}`}
>
@@ -113,7 +113,7 @@ export const StructureSection: React.FC = ({
{node.Name}
{isUnderstaffed ? (
- {t('command.lane_understaffed', { count: laneUnitCount, min: node.MinUnits })}
+ {t('command.lane_understaffed', { count: laneUnitCount, min: node.MinUnits })}
) : null}
{node.CommandStructureNodeId.startsWith('local-') ? : null}
@@ -164,7 +164,7 @@ export const StructureSection: React.FC = ({
setSelectedAssignmentId(assignment.ResourceAssignmentId)}
onPress={() => handleSelectResource(assignment.ResourceAssignmentId)}
testID={`lane-resource-${assignment.ResourceAssignmentId}`}
@@ -175,12 +175,12 @@ export const StructureSection: React.FC = ({
{assignment.ResourceAssignmentId.startsWith('local-') ? : null}
{assignment.RequirementsWarning ? (
- {t('command.requirements_warning')}
+ {t('command.requirements_warning')}
) : null}
{isSelected ? (
- {t('command.selected')}
+ {t('command.selected')}
) : null}
diff --git a/src/components/command/timeline-section.tsx b/src/components/command/timeline-section.tsx
index 558a279..2aa9181 100644
--- a/src/components/command/timeline-section.tsx
+++ b/src/components/command/timeline-section.tsx
@@ -94,7 +94,7 @@ export const SceneClock: React.FC = ({ startedOn }) => {
const text = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
return (
-
+
{text}
);
diff --git a/src/components/command/timers-section.tsx b/src/components/command/timers-section.tsx
index f3c7121..13e162f 100644
--- a/src/components/command/timers-section.tsx
+++ b/src/components/command/timers-section.tsx
@@ -102,16 +102,12 @@ export const TimersSection: React.FC = ({ timers, onStartTim
const remainingSeconds = dueMs !== null ? Math.round((dueMs - nowMs) / 1000) : null;
const isOverdue = remainingSeconds !== null && remainingSeconds <= 0;
return (
-
+
{timer.Name}
-
+
{remainingSeconds === null ? '' : isOverdue ? t('command.timer_overdue_by', { time: formatClock(remainingSeconds) }) : t('command.timer_due_in', { time: formatClock(remainingSeconds) })}
@@ -119,7 +115,7 @@ export const TimersSection: React.FC = ({ timers, onStartTim
{isOverdue ? (
- {t('command.timer_overdue')}
+ {t('command.timer_overdue')}
) : null}
= ({ callId, channels, tr
{channel.Name}
{isConnected ? (
- {t('command.channel_connected')}
+ {t('command.channel_connected')}
) : null}
diff --git a/src/components/common/__tests__/header-back-button.test.tsx b/src/components/common/__tests__/header-back-button.test.tsx
new file mode 100644
index 0000000..1ec394b
--- /dev/null
+++ b/src/components/common/__tests__/header-back-button.test.tsx
@@ -0,0 +1,54 @@
+import { fireEvent, render, screen } from '@testing-library/react-native';
+import React from 'react';
+
+import { HeaderBackButton } from '../header-back-button';
+
+jest.mock('lucide-react-native', () => ({
+ ArrowLeftIcon: () => null,
+}));
+
+describe('HeaderBackButton', () => {
+ it('renders with back-button testID', () => {
+ render();
+
+ expect(screen.getByTestId('back-button')).toBeTruthy();
+ });
+
+ it('calls onPress when pressed', () => {
+ const onPress = jest.fn();
+ render();
+
+ fireEvent.press(screen.getByTestId('back-button'));
+
+ expect(onPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('has a fixed 40x40 wrapper so iOS 26 liquid glass headers cannot stretch it', () => {
+ render();
+
+ const button = screen.getByTestId('back-button-container');
+ const flatStyle = Array.isArray(button.props.style) ? Object.assign({}, ...button.props.style.flat().filter(Boolean)) : button.props.style;
+
+ expect(flatStyle).toEqual(
+ expect.objectContaining({
+ width: 40,
+ height: 40,
+ })
+ );
+ });
+
+ it('fills the wrapper and centres the icon, so the arrow sits in the middle of the glass', () => {
+ render();
+
+ const pressable = screen.getByTestId('back-button');
+ const flatStyle = Array.isArray(pressable.props.style) ? Object.assign({}, ...pressable.props.style.flat().filter(Boolean)) : pressable.props.style;
+
+ expect(flatStyle).toEqual(
+ expect.objectContaining({
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ })
+ );
+ });
+});
diff --git a/src/components/common/date-time-field.tsx b/src/components/common/date-time-field.tsx
index 98940b9..0cd915e 100644
--- a/src/components/common/date-time-field.tsx
+++ b/src/components/common/date-time-field.tsx
@@ -80,7 +80,7 @@ export const DateTimeField = ({ value, onChange, label, mode = 'date', disabled
accessibilityLabel={accessibilityLabel}
accessibilityState={{ selected: chosen }}
onPress={onPress}
- className={chosen ? 'min-h-11 items-center justify-center rounded bg-primary-600 px-2 py-3' : 'min-h-11 items-center justify-center rounded px-2 py-3'}
+ className={chosen ? 'min-h-11 items-center justify-center rounded bg-primary-600 px-2 py-3 dark:bg-primary-400' : 'min-h-11 items-center justify-center rounded px-2 py-3'}
>
{text}
diff --git a/src/components/common/header-back-button.tsx b/src/components/common/header-back-button.tsx
index caee154..8f22c4f 100644
--- a/src/components/common/header-back-button.tsx
+++ b/src/components/common/header-back-button.tsx
@@ -14,12 +14,14 @@ interface HeaderBackButtonProps {
// frame after the first native commit, so the glass capsule caches a stretched
// constraint on re-entry — the wrapper guarantees a fixed frame from the first commit.
// collapsable={false} keeps the native view (and its frame) from being optimized away.
+// The Pressable fills the wrapper from the stylesheet too: NativeWind's rem is 14px, so a
+// `size-10` class comes out at 35pt and leaves the icon off-centre in the glass.
export const HeaderBackButton: React.FC = ({ onPress }) => {
const { colorScheme } = useColorScheme();
return (
-
+
{/* lucide icons draw with `stroke="currentColor"`, which react-native-svg resolves
to black — a `className` text colour never reaches them. Pass `color` instead,
or the arrow is invisible against the dark header. */}
@@ -34,4 +36,9 @@ const styles = StyleSheet.create({
width: 40,
height: 40,
},
+ pressable: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
});
diff --git a/src/components/common/loading.tsx b/src/components/common/loading.tsx
index 437e304..84e431e 100644
--- a/src/components/common/loading.tsx
+++ b/src/components/common/loading.tsx
@@ -66,7 +66,12 @@ export const Loading: React.FC = ({ text, fullscreen = false, size
);
case 'icon':
- return ;
+ // Spin a wrapping View, not the SVG: Reanimated 4's CSS animations can't interpolate `transform` on an
{label}
- {line}
+ {line}
diff --git a/src/components/contacts/contact-details-sheet.tsx b/src/components/contacts/contact-details-sheet.tsx
index 0726a23..c45af90 100644
--- a/src/components/contacts/contact-details-sheet.tsx
+++ b/src/components/contacts/contact-details-sheet.tsx
@@ -56,7 +56,7 @@ const Section: React.FC = ({ title, icon, children, isCollapsible
setIsExpanded(!isExpanded) : undefined} className="flex-row items-center justify-between" disabled={!isCollapsible}>
- {icon}
+ {icon}
{title}
{isCollapsible ? isExpanded ? : : null}
@@ -157,7 +157,7 @@ const ContactField: React.FC = ({ label, value, icon, isLink,
{icon ? {icon} : null}
{label}
- {displayValue}
+ {displayValue}
);
@@ -297,7 +297,7 @@ export const ContactDetailsSheet: React.FC = () => {
) : (
- {selectedContact.ContactType === ContactType.Person ? : }
+ {selectedContact.ContactType === ContactType.Person ? : }
)}
@@ -309,38 +309,26 @@ export const ContactDetailsSheet: React.FC = () => {
{selectedContact.ContactType === ContactType.Person ? t('contacts.person') : t('contacts.company')}
{selectedContact.OtherName ? ({selectedContact.OtherName}) : null}
- {selectedContact.CategoryName || selectedContact.Category?.Name ? (
- {selectedContact.CategoryName || selectedContact.Category?.Name}
- ) : null}
+ {selectedContact.CategoryName || selectedContact.Category?.Name ? {selectedContact.CategoryName || selectedContact.Category?.Name} : null}
{/* Tab Navigation */}
setActiveTab('details')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'details' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}>
-
- {t('contacts.tabs.details')}
-
+ {t('contacts.tabs.details')}
setActiveTab('notes')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'notes' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}>
-
- {t('contacts.tabs.notes')}
-
+ {t('contacts.tabs.notes')}
setActiveTab('preplan')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'preplan' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}>
-
- {t('contacts.tabs.preplan')}
-
+ {t('contacts.tabs.preplan')}
setActiveTab('files')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'files' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}>
-
- {t('contacts.tabs.files')}
-
+ {t('contacts.tabs.files')}
setActiveTab('calls')} className={`flex-1 rounded-md ${isLandscape ? 'px-4 py-2' : 'px-3 py-1.5'} ${activeTab === 'calls' ? 'bg-white shadow-xs dark:bg-gray-700' : ''}`}>
-
- {t('contacts.tabs.calls')}
-
+ {t('contacts.tabs.calls')}
diff --git a/src/components/contacts/contact-notes-list.tsx b/src/components/contacts/contact-notes-list.tsx
index 25eebf4..c96f692 100644
--- a/src/components/contacts/contact-notes-list.tsx
+++ b/src/components/contacts/contact-notes-list.tsx
@@ -47,7 +47,7 @@ const ContactNoteCard: React.FC = ({ note }) => {
{/* Header with type and indicators */}
- {note.NoteType ? {note.NoteType} : null}
+ {note.NoteType ? {note.NoteType} : null}
{note.ShouldAlert ? (
diff --git a/src/components/data-protection/protected-reveal-bar.tsx b/src/components/data-protection/protected-reveal-bar.tsx
index bf896ad..a55de64 100644
--- a/src/components/data-protection/protected-reveal-bar.tsx
+++ b/src/components/data-protection/protected-reveal-bar.tsx
@@ -1,9 +1,10 @@
-import { EyeIcon, EyeOffIcon, ShieldIcon } from 'lucide-react-native';
+import { EyeIcon, EyeOffIcon } from 'lucide-react-native';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, ButtonIcon, ButtonText } from '@/components/ui/button';
import { HStack } from '@/components/ui/hstack';
+import { ShieldIcon } from '@/components/ui/lucide-icons';
import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { useProtectedReveal } from '@/hooks/use-protected-reveal';
@@ -53,7 +54,7 @@ export const ProtectedRevealBar: React.FC = ({ onRefres
return (
-
+
{isRevealed ? t('data_protection.revealed_notice', 'Protected information is visible.') : t('data_protection.protected_notice', 'Some information on this screen is protected.')}
@@ -65,7 +66,7 @@ export const ProtectedRevealBar: React.FC = ({ onRefres
) : (
{isRequesting ? (
-
+
) : (
<>
diff --git a/src/components/data-protection/protected-text.tsx b/src/components/data-protection/protected-text.tsx
index 9608c21..569af71 100644
--- a/src/components/data-protection/protected-text.tsx
+++ b/src/components/data-protection/protected-text.tsx
@@ -1,8 +1,8 @@
-import { LockIcon } from 'lucide-react-native';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { HStack } from '@/components/ui/hstack';
+import { Lock } from '@/components/ui/lucide-icons';
import { Text } from '@/components/ui/text';
import { isFieldRedacted } from '@/lib/data-protection/redacted';
@@ -37,7 +37,7 @@ export const ProtectedText: React.FC = ({ value, fieldId, re
if (isFieldRedacted(redactedFields, fieldId, value)) {
return (
-
+
{t('data_protection.protected_value', 'Protected')}
diff --git a/src/components/data-protection/step-up-modal.tsx b/src/components/data-protection/step-up-modal.tsx
index f5e21d0..29b2e91 100644
--- a/src/components/data-protection/step-up-modal.tsx
+++ b/src/components/data-protection/step-up-modal.tsx
@@ -189,7 +189,7 @@ export const StepUpModal: React.FC = ({ isOpen, onClose, onVer
{offersCode && !approval ? (
- {isVerifying ? : {t('data_protection.step_up_verify', 'Verify')}}
+ {isVerifying ? : {t('data_protection.step_up_verify', 'Verify')}}
) : null}
diff --git a/src/components/livekit/livekit-bottom-sheet.tsx b/src/components/livekit/livekit-bottom-sheet.tsx
index 4555d2c..ba95cc0 100644
--- a/src/components/livekit/livekit-bottom-sheet.tsx
+++ b/src/components/livekit/livekit-bottom-sheet.tsx
@@ -197,8 +197,8 @@ export const LiveKitBottomSheet = () => {
{isTalking && {t('livekit.speaking')}}
{/* Audio Device Info */}
-
- {t('livekit.audio_devices')}
+
+ {t('livekit.audio_devices')}
{t('livekit.microphone')}
diff --git a/src/components/maps/pin-detail-modal.tsx b/src/components/maps/pin-detail-modal.tsx
index b44b51a..2e3e2eb 100644
--- a/src/components/maps/pin-detail-modal.tsx
+++ b/src/components/maps/pin-detail-modal.tsx
@@ -85,7 +85,7 @@ export const PinDetailModal: React.FC = ({ pin, isOpen, onC
-
+
{pin.Latitude.toFixed(6)}, {pin.Longitude.toFixed(6)}
@@ -99,28 +99,28 @@ export const PinDetailModal: React.FC = ({ pin, isOpen, onC
{pin.Address ? (
{t('map.pin_address')}
- {pin.Address}
+ {pin.Address}
) : null}
{pin.Note ? (
{t('map.pin_note')}
- {pin.Note}
+ {pin.Note}
) : null}
{pin.PoiTypeName ? (
{t('map.pin_type')}
- {pin.PoiTypeName}
+ {pin.PoiTypeName}
) : null}
{pin.Color && (
- {t('map.pin_color')}
+ {t('map.pin_color')}
)}
diff --git a/src/components/mfa/account-verify-modal.tsx b/src/components/mfa/account-verify-modal.tsx
index 9bd1cb6..266f44f 100644
--- a/src/components/mfa/account-verify-modal.tsx
+++ b/src/components/mfa/account-verify-modal.tsx
@@ -151,7 +151,7 @@ export const AccountVerifyModal: React.FC = ({ proof, o
{(reauth && !useProvider) || offersCode ? (
- {busy ? : {t('mfa.login.verify')}}
+ {busy ? : {t('mfa.login.verify')}}
) : null}
diff --git a/src/components/notifications/NotificationButton.tsx b/src/components/notifications/NotificationButton.tsx
index ee16eb4..f92a032 100644
--- a/src/components/notifications/NotificationButton.tsx
+++ b/src/components/notifications/NotificationButton.tsx
@@ -22,7 +22,7 @@ export const NotificationButton = ({ onPress }: NotificationButtonProps) => {
return (
-
+
{counts?.[0]?.count && counts?.[0]?.count > 0 ? (
diff --git a/src/components/notifications/NotificationDetail.tsx b/src/components/notifications/NotificationDetail.tsx
index 7738c50..fe23d8f 100644
--- a/src/components/notifications/NotificationDetail.tsx
+++ b/src/components/notifications/NotificationDetail.tsx
@@ -106,7 +106,7 @@ export const NotificationDetail = ({ notification, onClose, onDelete, onNavigate
-
+
{t('notifications.notification')}
diff --git a/src/components/notifications/NotificationInbox.tsx b/src/components/notifications/NotificationInbox.tsx
index 768c607..75bc09d 100644
--- a/src/components/notifications/NotificationInbox.tsx
+++ b/src/components/notifications/NotificationInbox.tsx
@@ -125,7 +125,7 @@ const NotificationRow = React.memo(({ item, isSelectionMod
{isSelectionMode ? (
- {isSelected ? : }
+ {isSelected ? : }
) : null}
@@ -140,7 +140,7 @@ const NotificationRow = React.memo(({ item, isSelectionMod
notification.referenceType && notification.referenceId ? (
-
+
@@ -395,10 +395,10 @@ export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) =
{t('notifications.title')}
-
+
-
+
>
diff --git a/src/components/push-notification/push-notification-modal.tsx b/src/components/push-notification/push-notification-modal.tsx
index 670e303..45fec22 100644
--- a/src/components/push-notification/push-notification-modal.tsx
+++ b/src/components/push-notification/push-notification-modal.tsx
@@ -16,13 +16,13 @@ const NotificationIcon = ({ type }: { type: NotificationType }) => {
switch (type) {
case 'call':
- return ;
+ return ;
case 'message':
- return ;
+ return ;
case 'chat':
- return ;
+ return ;
case 'group-chat':
- return ;
+ return ;
default:
return ;
}
@@ -113,9 +113,9 @@ export const PushNotificationModal: React.FC = () => {
) : null}
{notification.type === 'unknown' ? (
-
-
- {t('push_notifications.unknown_type_warning')}
+
+
+ {t('push_notifications.unknown_type_warning')}
) : null}
diff --git a/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx b/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx
index fee17c1..f55dc65 100644
--- a/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx
+++ b/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx
@@ -168,13 +168,13 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
handleDeviceSelect(item)}
disabled={!!connectingDeviceId}
- className={`mb-2 rounded-lg border p-4 ${isSelected ? 'border-primary-500 bg-primary-50 dark:bg-primary-950' : 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800'} ${!!connectingDeviceId ? 'opacity-70' : ''}`}
+ className={`mb-2 rounded-lg border p-4 ${isSelected ? 'border-primary-500 bg-primary-50' : 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800'} ${!!connectingDeviceId ? 'opacity-70' : ''}`}
>
- {item.name || t('bluetooth.unknown_device')}
+ {item.name || t('bluetooth.unknown_device')}
{isConnected && }
@@ -187,7 +187,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
) : isSelected ? (
- {t('bluetooth.selected')}
+ {t('bluetooth.selected')}
{isConnected && {t('bluetooth.connected')}}
) : null}
@@ -270,19 +270,19 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo
}
}}
disabled={!!connectingDeviceId}
- className={`rounded-lg border p-4 ${preferredDevice?.id === 'system-audio' ? 'border-primary-500 bg-primary-50 dark:bg-primary-950' : 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800'} ${!!connectingDeviceId ? 'opacity-70' : ''}`}
+ className={`rounded-lg border p-4 ${preferredDevice?.id === 'system-audio' ? 'border-primary-500 bg-primary-50' : 'border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800'} ${!!connectingDeviceId ? 'opacity-70' : ''}`}
>
- {t('bluetooth.system_audio')}
+ {t('bluetooth.system_audio')}
{t('bluetooth.system_audio_description')}
{preferredDevice?.id === 'system-audio' && (
- {t('bluetooth.selected')}
+ {t('bluetooth.selected')}
)}
diff --git a/src/components/settings/item.tsx b/src/components/settings/item.tsx
index 06aeeda..0162b7c 100644
--- a/src/components/settings/item.tsx
+++ b/src/components/settings/item.tsx
@@ -1,7 +1,7 @@
-import { ArrowRight } from 'lucide-react-native';
import * as React from 'react';
import { Pressable, View } from '@/components/ui';
+import { ArrowRight } from '@/components/ui/lucide-icons';
import { Text } from '@/components/ui/text';
type ItemProps = {
@@ -26,7 +26,7 @@ export const Item = ({ text, value, icon, onPress, textStyle }: ItemProps) => {
{truncatedValue}
{isPressable && (
-
+
)}
diff --git a/src/components/shared-session/shared-session-bar.tsx b/src/components/shared-session/shared-session-bar.tsx
index 25b91e3..4016485 100644
--- a/src/components/shared-session/shared-session-bar.tsx
+++ b/src/components/shared-session/shared-session-bar.tsx
@@ -50,7 +50,7 @@ export const SharedSessionBar: React.FC = () => {
};
return (
-
+
{installationLabel ? t('shared_session.bar_label', { label: installationLabel }) : t('shared_session.bar')}
@@ -72,7 +72,7 @@ export const SharedSessionBar: React.FC = () => {
) : (
<>
void lockSharedSession('explicit')} testID="shared-bar-lock">
-
+
{t('shared_session.lock')}
setConfirmEnd(true)} testID="shared-bar-end">
@@ -84,7 +84,7 @@ export const SharedSessionBar: React.FC = () => {
{idleWarning || shiftEndsSoon ? (
-
+
{[
idleWarning ? t('shared_session.idle_warning', { time: clockText(remaining ?? 0) }) : null,
shiftEndsSoon && shiftEndsAt !== null ? t('shared_session.shift_ends_soon', { time: new Date(shiftEndsAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }) : null,
diff --git a/src/components/shared-session/shared-session-lock-screen.tsx b/src/components/shared-session/shared-session-lock-screen.tsx
index 4e725aa..979d37a 100644
--- a/src/components/shared-session/shared-session-lock-screen.tsx
+++ b/src/components/shared-session/shared-session-lock-screen.tsx
@@ -248,11 +248,12 @@ export const SharedSessionLockScreen: React.FC = () => {
return (
undefined} testID="shared-lock-screen">
-
+ {/* An opaque Modal paints its own white container, so the page needs a themed background of its own. */}
+
-
+
{t('shared_session.locked_title')}
@@ -291,7 +292,7 @@ export const SharedSessionLockScreen: React.FC = () => {
/>
- {busy ? : {t('shared_session.unlock')}}
+ {busy ? : {t('shared_session.unlock')}}
>
) : null}
diff --git a/src/components/sidebar/sidebar-content.tsx b/src/components/sidebar/sidebar-content.tsx
index 8e22cc0..eefa03e 100644
--- a/src/components/sidebar/sidebar-content.tsx
+++ b/src/components/sidebar/sidebar-content.tsx
@@ -70,7 +70,7 @@ const Sidebar = ({ onClose }: SidebarProps) => {
{menuItems.map((item) => (
handleNavigate(item.href)}>
-
+
{t(item.labelKey)}
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
index 10a5ea9..3058868 100644
--- a/src/components/toast/toast.tsx
+++ b/src/components/toast/toast.tsx
@@ -1,9 +1,9 @@
-import { X } from 'lucide-react-native';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { type GestureResponderEvent, Pressable } from 'react-native';
import { HStack } from '@/components/ui/hstack';
+import { X } from '@/components/ui/lucide-icons';
import { VStack } from '@/components/ui/vstack';
import { type ToastItem, useToastStore } from '../../stores/toast/store';
@@ -24,15 +24,16 @@ export const ToastMessage: React.FC = ({ id, type, title, message, on
};
return (
+ // typography-0 flips against the toast backgrounds (dark in light mode, pastel in dark mode); a fixed white vanishes in dark mode.
- {title ? {t(title)} : null}
- {t(message)}
+ {title ? {t(title)} : null}
+ {t(message)}
-
+
diff --git a/src/components/ui/button/index.tsx b/src/components/ui/button/index.tsx
index c76b1bc..511cc2c 100644
--- a/src/components/ui/button/index.tsx
+++ b/src/components/ui/button/index.tsx
@@ -140,7 +140,7 @@ const buttonTextStyle = tva({
{
variant: 'outline',
action: 'primary',
- class: 'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
+ class: 'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
},
{
variant: 'outline',
diff --git a/src/components/ui/focus-aware-status-bar.tsx b/src/components/ui/focus-aware-status-bar.tsx
index 0764676..f564d7f 100644
--- a/src/components/ui/focus-aware-status-bar.tsx
+++ b/src/components/ui/focus-aware-status-bar.tsx
@@ -61,7 +61,7 @@ function FocusAwareStatusBarInner({ hidden = false }: Props) {
if (Platform.OS === 'web') return null;
// Only render SystemBars when focused and on supported platforms
- return isFocused && (Platform.OS === 'android' || Platform.OS === 'ios') ? : null;
+ return isFocused && (Platform.OS === 'android' || Platform.OS === 'ios') ? : null;
}
export const FocusAwareStatusBar = (props: Props) => {
diff --git a/src/components/ui/lucide-icons.tsx b/src/components/ui/lucide-icons.tsx
index 49e2f67..1be72dd 100644
--- a/src/components/ui/lucide-icons.tsx
+++ b/src/components/ui/lucide-icons.tsx
@@ -3,6 +3,7 @@ import {
AlertCircle as RawAlertCircle,
AlertTriangle as RawAlertTriangle,
ArrowLeft as RawArrowLeft,
+ ArrowRight as RawArrowRight,
Bell as RawBell,
BellIcon as RawBellIcon,
BluetoothIcon as RawBluetoothIcon,
@@ -45,6 +46,7 @@ import {
SearchIcon as RawSearchIcon,
SettingsIcon as RawSettingsIcon,
ShieldCheck as RawShieldCheck,
+ ShieldIcon as RawShieldIcon,
SmartphoneIcon as RawSmartphoneIcon,
Speaker as RawSpeaker,
StarIcon as RawStarIcon,
@@ -94,6 +96,7 @@ export const AlarmClock = themed(RawAlarmClock);
export const AlertCircle = themed(RawAlertCircle);
export const AlertTriangle = themed(RawAlertTriangle);
export const ArrowLeft = themed(RawArrowLeft);
+export const ArrowRight = themed(RawArrowRight);
export const Bell = themed(RawBell);
export const BellIcon = themed(RawBellIcon);
export const BluetoothIcon = themed(RawBluetoothIcon);
@@ -135,6 +138,7 @@ export const RefreshCwIcon = themed(RawRefreshCwIcon);
export const SearchIcon = themed(RawSearchIcon);
export const SettingsIcon = themed(RawSettingsIcon);
export const ShieldCheck = themed(RawShieldCheck);
+export const ShieldIcon = themed(RawShieldIcon);
export const Truck = themed(RawTruck);
export const SmartphoneIcon = themed(RawSmartphoneIcon);
export const Speaker = themed(RawSpeaker);
diff --git a/src/components/ui/progress/index.tsx b/src/components/ui/progress/index.tsx
index fb64f87..da1c2e4 100644
--- a/src/components/ui/progress/index.tsx
+++ b/src/components/ui/progress/index.tsx
@@ -11,7 +11,7 @@ export const UIProgress = createProgress({
});
const progressStyle = tva({
- base: 'bg-background-300 rounded-full w-full',
+ base: 'bg-background-200 rounded-full w-full',
variants: {
size: {
xs: 'h-1',
@@ -24,7 +24,7 @@ const progressStyle = tva({
},
});
const progressFilledTrackStyle = tva({
- base: 'bg-primary-500 rounded-full',
+ base: 'bg-primary-600 rounded-full',
parentVariants: {
size: {
xs: 'h-1',
diff --git a/src/components/ui/shared-tabs.tsx b/src/components/ui/shared-tabs.tsx
index 90b8017..3dd3207 100644
--- a/src/components/ui/shared-tabs.tsx
+++ b/src/components/ui/shared-tabs.tsx
@@ -77,10 +77,21 @@ export const SharedTabs: React.FC = ({
);
// Get appropriate text color based on theme
- const getTextColor = () => {
+ // Pills and segmented tabs fill the active tab with primary, so its label and icon go white.
+ const isFilledActive = (index: number) => index === currentIndex && (variant === 'pills' || variant === 'segmented');
+
+ const getTextColor = (index: number) => {
+ if (isFilledActive(index)) return 'text-white';
return colorScheme === 'dark' ? 'text-gray-200' : 'text-gray-800';
};
+ // Tab icons are usually raw lucide icons, whose default stroke is black; give them the label colour unless the caller chose one.
+ const renderIcon = (icon: React.ReactNode, index: number) => {
+ if (!React.isValidElement<{ color?: string }>(icon) || icon.props.color !== undefined) return icon;
+ const color = isFilledActive(index) ? '#ffffff' : colorScheme === 'dark' ? '#e5e7eb' : '#1f2937';
+ return React.cloneElement(icon, { color });
+ };
+
// Determine tab styles based on variant and size
const getTabStyles = (index: number) => {
const isActive = index === currentIndex;
@@ -93,9 +104,9 @@ export const SharedTabs: React.FC = ({
}[size];
const variantStyles = {
- default: isActive ? 'border-b-2 border-primary-500 text-primary-500' : `border-b-2 border-transparent ${colorScheme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`,
+ default: isActive ? 'border-b-2 border-primary-600 text-primary-600' : `border-b-2 border-transparent ${colorScheme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`,
pills: isActive ? 'bg-primary-500 text-white rounded-full' : `bg-transparent ${colorScheme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`,
- underlined: isActive ? 'border-b-2 border-primary-500 text-primary-500' : `border-b-2 border-transparent ${colorScheme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`,
+ underlined: isActive ? 'border-b-2 border-primary-600 text-primary-600' : `border-b-2 border-transparent ${colorScheme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`,
segmented: isActive ? 'bg-primary-500 text-white' : `${colorScheme === 'dark' ? 'bg-gray-800 text-gray-400' : 'bg-gray-100 text-gray-500'}`,
}[variant];
@@ -144,13 +155,13 @@ export const SharedTabs: React.FC = ({
{tabs.map((tab, index) => (
handleTabPress(index)}>
- {tab.icon && {tab.icon}}
+ {tab.icon ? {renderIcon(tab.icon, index)} : null}
{typeof tab.title === 'string' ? (
-
+
{t(tab.title)}
) : (
-
+
{tab.title}
)}
@@ -170,13 +181,13 @@ export const SharedTabs: React.FC = ({
{tabs.map((tab, index) => (
handleTabPress(index)}>
- {tab.icon && {tab.icon}}
+ {tab.icon ? {renderIcon(tab.icon, index)} : null}
{typeof tab.title === 'string' ? (
-
+
{t(tab.title)}
) : (
-
+
{tab.title}
)}
diff --git a/src/components/ui/textarea/index.tsx b/src/components/ui/textarea/index.tsx
index 4a6d8d6..13b274d 100644
--- a/src/components/ui/textarea/index.tsx
+++ b/src/components/ui/textarea/index.tsx
@@ -28,7 +28,7 @@ const textareaStyle = tva({
});
const textareaInputStyle = tva({
- base: 'p-2 web:outline-0 web:outline-none flex-1 color-typography-900 align-text-top placeholder:text-typography-500 web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
+ base: 'p-2 web:outline-0 web:outline-none flex-1 text-typography-900 align-text-top placeholder:text-typography-500 web:cursor-text web:data-[disabled=true]:cursor-not-allowed',
parentVariants: {
size: {
sm: 'text-sm',
diff --git a/src/lib/__tests__/readable-text-color.test.ts b/src/lib/__tests__/readable-text-color.test.ts
new file mode 100644
index 0000000..8db01f7
--- /dev/null
+++ b/src/lib/__tests__/readable-text-color.test.ts
@@ -0,0 +1,61 @@
+import { readableColors, readableTextColor } from '../utils';
+
+describe('readableTextColor', () => {
+ it('uses white on dark backgrounds', () => {
+ expect(readableTextColor('#000000')).toBe('#FFFFFF');
+ expect(readableTextColor('#262626')).toBe('#FFFFFF');
+ expect(readableTextColor('#475569')).toBe('#FFFFFF');
+ });
+
+ it('uses black on light backgrounds', () => {
+ expect(readableTextColor('#ffffff')).toBe('#000000');
+ expect(readableTextColor('#f8ac59')).toBe('#000000');
+ });
+
+ it('uses black on mid-tone status colours where white would fall below 3:1', () => {
+ expect(readableTextColor('#23c6c8')).toBe('#000000');
+ expect(readableTextColor('#449d44')).toBe('#000000');
+ expect(readableTextColor('#ED5565')).toBe('#000000');
+ });
+
+ it('accepts 3-digit hex and a missing #', () => {
+ expect(readableTextColor('#fff')).toBe('#000000');
+ expect(readableTextColor('000')).toBe('#FFFFFF');
+ });
+
+ it('reads opaque rgb() and rgba() colours', () => {
+ expect(readableTextColor('rgb(0,0,0)')).toBe('#FFFFFF');
+ expect(readableTextColor('rgb(200, 0, 0)')).toBe('#FFFFFF');
+ expect(readableTextColor('RGB(255, 255, 0)')).toBe('#000000');
+ expect(readableTextColor('rgba(30, 58, 138, 1)')).toBe('#FFFFFF');
+ expect(readableTextColor(' rgba(255,255,255,1.0) ')).toBe('#000000');
+ });
+
+ it('returns undefined for values whose contrast it cannot work out', () => {
+ expect(readableTextColor('')).toBeUndefined();
+ expect(readableTextColor('label-default')).toBeUndefined();
+ expect(readableTextColor('red')).toBeUndefined();
+ expect(readableTextColor('hsl(0, 100%, 50%)')).toBeUndefined();
+ expect(readableTextColor('rgba(0, 0, 0, 0.5)')).toBeUndefined();
+ expect(readableTextColor('rgb(256, 0, 0)')).toBeUndefined();
+ expect(readableTextColor('#12345')).toBeUndefined();
+ });
+});
+
+describe('readableColors', () => {
+ it('keeps a colour it can read and pairs it with the readable text colour', () => {
+ expect(readableColors('#1e3a8a')).toEqual({ backgroundColor: '#1e3a8a', textColor: '#FFFFFF' });
+ expect(readableColors('rgb(255, 255, 0)')).toEqual({ backgroundColor: 'rgb(255, 255, 0)', textColor: '#000000' });
+ });
+
+ it('adds the # React Native needs to draw bare hex', () => {
+ expect(readableColors(' ff0000 ')).toEqual({ backgroundColor: '#ff0000', textColor: '#000000' });
+ });
+
+ it('swaps a colour it cannot read for the fallback and its text colour', () => {
+ expect(readableColors('hsl(0, 100%, 25%)')).toEqual({ backgroundColor: '#808080', textColor: '#000000' });
+ expect(readableColors('rgba(255, 255, 255, 0.2)', '#6b7280')).toEqual({ backgroundColor: '#6b7280', textColor: '#FFFFFF' });
+ expect(readableColors(undefined, '#6b7280')).toEqual({ backgroundColor: '#6b7280', textColor: '#FFFFFF' });
+ expect(readableColors(null)).toEqual({ backgroundColor: '#808080', textColor: '#000000' });
+ });
+});
diff --git a/src/lib/incident-command-utils.ts b/src/lib/incident-command-utils.ts
index 2f2d229..8f80999 100644
--- a/src/lib/incident-command-utils.ts
+++ b/src/lib/incident-command-utils.ts
@@ -187,3 +187,18 @@ export const getParBadgeAction = (status: string): 'success' | 'warning' | 'erro
return 'muted';
}
};
+
+/**
+ * Collapse PAR/accountability rows to one per member. Older servers emit a row per CallDispatch, so a member
+ * dispatched more than once on a call appears repeatedly (duplicate React keys, inflated PAR counts).
+ */
+export const dedupeByUserId = (rows: readonly T[]): T[] => {
+ const seen = new Set();
+ return rows.filter((row) => {
+ if (seen.has(row.UserId)) {
+ return false;
+ }
+ seen.add(row.UserId);
+ return true;
+ });
+};
diff --git a/src/lib/operations/__tests__/time.test.ts b/src/lib/operations/__tests__/time.test.ts
index c168f0b..6a53977 100644
--- a/src/lib/operations/__tests__/time.test.ts
+++ b/src/lib/operations/__tests__/time.test.ts
@@ -138,6 +138,8 @@ it('maps server failures to short reason codes', () => {
expect(operationsError({ response: { status: 401 } })).toBe('denied');
expect(operationsError({ response: { status: 400, headers: { 'x-resgrid-reason': 'timereports_subject_covered' } } })).toBe('timereports_subject_covered');
expect(operationsError({ response: { status: 400 } })).toBe('validation');
+ // A reason with no message of its own must not surface as a raw key or as the misleading "could not reach the server".
+ expect(operationsError({ response: { status: 400, headers: { 'x-resgrid-reason': 'workforce_amount_invalid' } } })).toBe('validation');
expect(operationsError({ response: { status: 200, data: { type: 'protected_data_required' } } })).toBe('locked');
expect(operationsError(new Error('boom'))).toBe('retry');
});
diff --git a/src/lib/operations/time.ts b/src/lib/operations/time.ts
index 90983fa..ce24173 100644
--- a/src/lib/operations/time.ts
+++ b/src/lib/operations/time.ts
@@ -238,13 +238,26 @@ export const documentNumberLabel = (number: string) => (/^\d+$/.test(number) ? `
export const isReportEditable = (report: TimeReport | null) => !!report && report.Status === TimeReportStatus.Draft && report.CanAct;
+/** Server rejection reasons that have their own operations.errors message; any other reason reads as a generic validation error. */
+const OPERATIONS_ERROR_REASONS = new Set([
+ 'deployments_attachment_too_large',
+ 'deployments_closed',
+ 'expenses_receipt_invalid',
+ 'timereports_date_exists',
+ 'timereports_locked',
+ 'timereports_scope_invalid',
+ 'timereports_status_transition_invalid',
+ 'timereports_subject_covered',
+ 'timereports_time_invalid',
+]);
+
export const operationsError = (error: unknown): string => {
const response = (error as { response?: { status?: number; headers?: Record; data?: { type?: string } } })?.response;
const reason = String(response?.headers?.['x-resgrid-reason'] ?? '');
if (response?.data?.type === 'protected_data_required') return 'locked';
if (reason.endsWith('_disabled')) return 'disabled';
if (response?.status === 401 || response?.status === 403 || response?.status === 404) return 'denied';
- if (response?.status === 400) return reason || 'validation';
+ if (response?.status === 400) return OPERATIONS_ERROR_REASONS.has(reason) ? reason : 'validation';
if (error instanceof Error && ['denied', 'disabled', 'locked', 'validation'].includes(error.message)) return error.message;
return 'retry';
};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index d7d840a..caf42ea 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -42,6 +42,7 @@ export function onSortOptions(a: any, b: any) {
return 0;
}
+/** For black-or-white text on a coloured background prefer `readableTextColor`; the `bw` cutoff here picks white on many mid-tones. */
export function invertColor(hex: string, bw: boolean): string {
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
@@ -68,6 +69,56 @@ export function invertColor(hex: string, bw: boolean): string {
return '#' + padZero(r2, 2) + padZero(g2, 2) + padZero(b2, 2);
}
+/** The 0-255 [r, g, b] of a 3- or 6-digit hex colour (the # optional) or an opaque rgb()/rgba(). */
+function parseOpaqueRgb(color: string): number[] | undefined {
+ const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);
+ if (hex) {
+ const digits = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
+ return [0, 2, 4].map((i) => parseInt(digits.slice(i, i + 2), 16));
+ }
+ // A translucent colour's contrast depends on what is drawn behind it, so only an alpha of 1 counts.
+ const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d*\.?\d+)\s*)?\)$/i.exec(color);
+ if (!rgb || (rgb[4] !== undefined && Number(rgb[4]) < 1)) return undefined;
+ const channels = [rgb[1], rgb[2], rgb[3]].map(Number);
+ return channels.every((c) => c <= 255) ? channels : undefined;
+}
+
+/**
+ * Black or white, whichever has the higher WCAG contrast against `color`. Returns undefined for a
+ * value that isn't a 3- or 6-digit hex colour or an opaque rgb()/rgba(), so callers can fall back;
+ * `readableColors` does that by swapping in a known background.
+ * Unlike `invertColor(hex, true)`, this picks black on mid-tones such as orange, teal and green.
+ */
+export function readableTextColor(color: string): '#000000' | '#FFFFFF' | undefined {
+ const channels = parseOpaqueRgb(color.trim());
+ if (!channels) return undefined;
+ const [r, g, b] = channels.map((channel) => {
+ const c = channel / 255;
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
+ });
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ // Contrast with black is (L + 0.05) / 0.05 and with white 1.05 / (L + 0.05); they are equal at L ≈ 0.179.
+ return luminance > 0.179 ? '#000000' : '#FFFFFF';
+}
+
+interface ReadableColors {
+ backgroundColor: string;
+ textColor: '#000000' | '#FFFFFF';
+}
+
+/**
+ * The background to draw for a server-supplied `color` and the black or white text that reads on it.
+ * A colour `readableTextColor` can't read (named, hsl(), translucent, invalid) is replaced by `fallback`,
+ * so text never sits in a fixed colour on a background it wasn't picked for.
+ */
+export function readableColors(color: string | null | undefined, fallback = '#808080'): ReadableColors {
+ const value = color?.trim() ?? '';
+ const textColor = readableTextColor(value);
+ if (!textColor) return { backgroundColor: fallback, textColor: readableTextColor(fallback) ?? '#000000' };
+ // React Native only draws a hex colour that has its leading #.
+ return { backgroundColor: /^[0-9a-f]+$/i.test(value) ? `#${value}` : value, textColor };
+}
+
export function padZero(str: string, len: number): string {
len = len || 2;
const zeros = new Array(len).join('0');
diff --git a/src/models/v4/calls/locationHistoryResult.ts b/src/models/v4/calls/locationHistoryResult.ts
index 23c8a15..0ed9d7e 100644
--- a/src/models/v4/calls/locationHistoryResult.ts
+++ b/src/models/v4/calls/locationHistoryResult.ts
@@ -24,7 +24,7 @@ export interface LocationHistoryCallData {
Priority: number;
PriorityText?: string | null;
PriorityColor?: string | null;
- /** CallStates: 0 Active, 1 Closed, 2 Cancelled, 3 Unfounded, 4 Founded, 5 Minor */
+ /** CallStates: 0 Active, 1 Closed, 2 Cancelled, 3 Unfounded, 4 Founded, 5 Minor, 6 Transferred, 7 False Alarm, 8 Pending */
State: number;
LoggedOnUtc: string;
/** Formatted in the department's time zone */
diff --git a/src/services/incident-assistant/__tests__/answerers.test.ts b/src/services/incident-assistant/__tests__/answerers.test.ts
index 14d1af7..0845b71 100644
--- a/src/services/incident-assistant/__tests__/answerers.test.ts
+++ b/src/services/incident-assistant/__tests__/answerers.test.ts
@@ -154,6 +154,17 @@ describe('incident assistant answers', () => {
expect(answer).toContain('6 min overdue');
});
+ it('counts a member once when the server returns duplicate accountability rows for them', () => {
+ const context = buildContext();
+ const [green, critical] = context.board!.Accountability;
+ context.board!.Accountability = [green, critical, { ...critical }];
+
+ const answer = answerPar(context, t);
+
+ expect(answer).toContain('2 personnel tracked');
+ expect(answer).toContain('1 green, 0 approaching, 1 overdue');
+ });
+
it('says plainly when nothing is being tracked rather than implying everyone is fine', () => {
const context = buildContext();
context.board!.Accountability = [];
diff --git a/src/services/incident-assistant/answerers.ts b/src/services/incident-assistant/answerers.ts
index c3f90b5..59e6c51 100644
--- a/src/services/incident-assistant/answerers.ts
+++ b/src/services/incident-assistant/answerers.ts
@@ -11,7 +11,7 @@
import { type TFunction } from 'i18next';
-import { getCommandNodeTypeName, getIncidentRoleName, getNeedCategoryName } from '@/lib/incident-command-utils';
+import { dedupeByUserId, getCommandNodeTypeName, getIncidentRoleName, getNeedCategoryName } from '@/lib/incident-command-utils';
import { parseUtcMs } from '@/lib/utils';
import { IncidentTimerStatus } from '@/models/v4/incidentCommand/incidentCommandEnums';
import {
@@ -102,7 +102,7 @@ const isCriticalPar = (row: PersonnelCallCheckInStatus): boolean => row.Status =
const isWarningPar = (row: PersonnelCallCheckInStatus): boolean => !isCriticalPar(row) && row.Status === 'Warning';
const parBuckets = (board: IncidentCommandBoard) => {
- const rows = board.Accountability ?? [];
+ const rows = dedupeByUserId(board.Accountability ?? []);
return { total: rows.length, warning: rows.filter(isWarningPar).length, critical: rows.filter(isCriticalPar).length };
};
@@ -249,7 +249,7 @@ export const answerStatus = (context: IncidentAnswerContext, t: TFunction): stri
};
export const answerPar = (context: IncidentAnswerContext, t: TFunction): string => {
- const rows = context.board!.Accountability ?? [];
+ const rows = dedupeByUserId(context.board!.Accountability ?? []);
if (rows.length === 0) {
return t('incident_assistant.par_none', { incident: incidentLabel(context, t) });
}
diff --git a/src/stores/check-in-timers/__tests__/store.test.ts b/src/stores/check-in-timers/__tests__/store.test.ts
index 39b88c7..9586b0a 100644
--- a/src/stores/check-in-timers/__tests__/store.test.ts
+++ b/src/stores/check-in-timers/__tests__/store.test.ts
@@ -97,6 +97,30 @@ describe('useCheckInTimerStore', () => {
expect(useCheckInTimerStore.getState().personnelStatuses.map((status) => status.UserId)).toEqual(['critical', 'safe']);
expect(useCheckInTimerStore.getState().hasActivePersonnelTimer).toBe(true);
});
+
+ it('keeps one row per user when the server returns a row per dispatch', async () => {
+ const critical = { UserId: 'critical', FullName: 'Critical User', LastCheckIn: null, NeedsCheckIn: true, MinutesRemaining: -2, Status: 'Critical' };
+ mockGetCallPersonnelCheckInStatuses.mockResolvedValue({
+ CallId: 1,
+ HasActivePersonnelTimer: true,
+ DurationMinutes: 20,
+ WarningThresholdMinutes: 5,
+ Data: [critical, { UserId: 'safe', FullName: 'Safe User', LastCheckIn: null, NeedsCheckIn: false, MinutesRemaining: 12, Status: 'Green' }, { ...critical }],
+ PageSize: 3,
+ Timestamp: '',
+ Version: '',
+ Node: '',
+ RequestId: '',
+ Status: '',
+ Environment: '',
+ });
+
+ await act(async () => {
+ await useCheckInTimerStore.getState().fetchPersonnelStatuses(1);
+ });
+
+ expect(useCheckInTimerStore.getState().personnelStatuses.map((status) => status.UserId)).toEqual(['critical', 'safe']);
+ });
});
describe('fetchTimerStatuses', () => {
diff --git a/src/stores/check-in-timers/store.ts b/src/stores/check-in-timers/store.ts
index 2f3aca3..b2606d6 100644
--- a/src/stores/check-in-timers/store.ts
+++ b/src/stores/check-in-timers/store.ts
@@ -3,6 +3,7 @@ import { create } from 'zustand';
import type { PerformCheckInInput } from '@/api/check-in-timers/check-in-timers';
import { getCallPersonnelCheckInStatuses, getCheckInHistory, getTimersForCall, getTimerStatuses, performCheckIn, toggleCallTimers } from '@/api/check-in-timers/check-in-timers';
+import { dedupeByUserId } from '@/lib/incident-command-utils';
import { logger } from '@/lib/logging';
import type { CallPersonnelCheckInStatusResultData } from '@/models/v4/checkIn/callPersonnelCheckInStatusResultData';
import type { CheckInRecordResultData } from '@/models/v4/checkIn/checkInRecordResultData';
@@ -81,8 +82,8 @@ export const useCheckInTimerStore = create((set, get) => ({
fetchPersonnelStatuses: async (callId: number) => {
try {
const result = await getCallPersonnelCheckInStatuses(callId);
- const data = Array.isArray(result.Data) ? result.Data : [];
- const sorted = [...data].sort((a, b) => (STATUS_SEVERITY[a.Status] ?? 3) - (STATUS_SEVERITY[b.Status] ?? 3));
+ const data = Array.isArray(result.Data) ? dedupeByUserId(result.Data) : [];
+ const sorted = data.sort((a, b) => (STATUS_SEVERITY[a.Status] ?? 3) - (STATUS_SEVERITY[b.Status] ?? 3));
set({ personnelStatuses: sorted, hasActivePersonnelTimer: result.HasActivePersonnelTimer === true });
} catch (error) {
logger.error({ message: 'Failed to fetch personnel check-in statuses', context: { error, callId } });
diff --git a/src/stores/operations/__tests__/store.test.ts b/src/stores/operations/__tests__/store.test.ts
index aa49597..b91743b 100644
--- a/src/stores/operations/__tests__/store.test.ts
+++ b/src/stores/operations/__tests__/store.test.ts
@@ -251,7 +251,14 @@ it('stamps usage readings with the open deployment and surfaces server refusals
expect(server.addResourceUsage).toHaveBeenCalledWith(expect.objectContaining({ DeploymentId: 'dep-1', CallId: 9, UnitId: 12 }));
expect(useOperationsStore.getState().usage.map((reading) => reading.Id)).toEqual(['u-1']);
- server.addResourceUsage.mockRejectedValue({ response: { status: 400, headers: { 'x-resgrid-reason': 'usage_unit_not_rostered' } } });
+ // FieldCost/AddResourceUsage refuses a unit that is not on the member's deployment with a 403.
+ server.addResourceUsage.mockRejectedValue({ response: { status: 403, headers: { 'x-resgrid-reason': 'workforce_unit_not_on_deployment' } } });
expect(await useOperationsStore.getState().addUsage({ UnitId: 99, UsageDate: '2026-09-19T00:00:00', Phase: 2 })).toBe(false);
- expect(useOperationsStore.getState().error).toBe('usage_unit_not_rostered');
+ expect(useOperationsStore.getState().error).toBe('denied');
+
+ // A 400 reason with no operations.errors message of its own reads as the generic validation error, not a raw key.
+ server.addResourceUsage.mockRejectedValue({ response: { status: 400, headers: { 'x-resgrid-reason': 'workforce_usage_invalid' } } });
+ expect(await useOperationsStore.getState().addUsage({ UnitId: 12, UsageDate: '2026-09-19T00:00:00', Phase: 2 })).toBe(false);
+ expect(useOperationsStore.getState().error).toBe('validation');
+ expect(useOperationsStore.getState().usage.map((reading) => reading.Id)).toEqual(['u-1']);
});
diff --git a/src/stores/records/__tests__/deployments-store.test.ts b/src/stores/records/__tests__/deployments-store.test.ts
index b48b9a3..887f82b 100644
--- a/src/stores/records/__tests__/deployments-store.test.ts
+++ b/src/stores/records/__tests__/deployments-store.test.ts
@@ -106,7 +106,7 @@ describe('Deployments store conformance', () => {
const list = await useDeploymentsStore.getState().fetchDeployments();
expect(list).toHaveLength(1);
- expect(useDeploymentsStore.getState().error).toBe('offline');
+ expect(useDeploymentsStore.getState().error).toBe('load_failed');
expect(useDeploymentsStore.getState().isLoading).toBe(false);
});
diff --git a/src/stores/records/deployments-store.ts b/src/stores/records/deployments-store.ts
index a2f8929..db5e4ff 100644
--- a/src/stores/records/deployments-store.ts
+++ b/src/stores/records/deployments-store.ts
@@ -51,7 +51,9 @@ const messageOf = (error: unknown, fallback: string): string => {
if (status === 404) {
return 'not_found';
}
- return error instanceof Error && error.message ? error.message : fallback;
+ // Never the transport's own text ("Network Error", "Request failed with status code 500"): the
+ // result is used as a records.* translation key.
+ return fallback;
};
// Fetches can overlap (a screen's focus refresh and a connector run's re-read), and each answer would
diff --git a/src/translations/ar.json b/src/translations/ar.json
index abd3c54..a9a5e34 100644
--- a/src/translations/ar.json
+++ b/src/translations/ar.json
@@ -8,7 +8,7 @@
}
},
"app": {
- "title": "وحدة ريسغريد"
+ "title": "Resgrid IC"
},
"audio_streams": {
"buffering": "التخزين المؤقت",
@@ -339,7 +339,7 @@
"acknowledge": "تأكيد",
"add_gif": "إضافة GIF",
"add_image": "إضافة صورة",
- "all_leads": "All Leads",
+ "all_leads": "جميع المسؤولين",
"are_typing": "{{count}} أشخاص يكتبون الآن...",
"assistant": "المساعد",
"attachment_failed": "فشل رفع المرفق",
@@ -353,13 +353,13 @@
"delete": "حذف",
"delete_failed": "تعذّر حذف الرسالة",
"direct_message": "رسالة مباشرة",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "تعذّر فتح تلك المحادثة.",
+ "dm_unavailable": "ليس لدى جهة الاتصال هذه حساب Resgrid لمراسلتها.",
"edit": "تعديل",
"edit_failed": "تعذّر تعديل الرسالة",
"edit_message": "تعديل الرسالة",
"edited": "(معدّلة)",
- "emoji": "Emoji",
+ "emoji": "رموز تعبيرية",
"empty": "لا توجد محادثات بعد. ابدأ رسالة مباشرة أو أنشئ مجموعة.",
"failed_tap_retry": "فشل الإرسال - انقر لإعادة المحاولة",
"flag": "إبلاغ",
@@ -371,14 +371,14 @@
"flag_reason": "ما سبب الإبلاغ عن هذا؟",
"flag_sensitive": "معلومات حساسة",
"flag_spam": "رسائل غير مرغوب فيها",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "هذا الحادث مغلق. تُحفظ المحادثة كسجل لحظة زمنية محددة — لا رسائل أو تعديلات جديدة، لكن لا يزال بإمكانك الإبلاغ عن المحتوى.",
"group_name": "اسم المجموعة (اختياري)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "دردشة القيادة",
+ "incident_lane_channel": "دردشة المسار",
"is_typing": "{{name}} يكتب الآن...",
"load_people_failed": "تعذّر تحميل الأشخاص",
"message_deleted": "تم حذف هذه الرسالة",
- "message_person": "Message {{name}}",
+ "message_person": "مراسلة {{name}}",
"moderator_delete": "إزالة (مشرف)",
"moderator_removed": "تمت الإزالة بواسطة المشرف",
"new_direct_message": "رسالة مباشرة جديدة",
@@ -386,8 +386,8 @@
"no_gifs": "لم يتم العثور على ملفات GIF",
"no_people": "لم يتم العثور على أشخاص",
"open_assistant": "فتح المساعد",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "دردشة القيادة",
+ "open_lane_chat": "دردشة المسار",
"original_message": "الرسالة الأصلية",
"permission_location_denied": "تم رفض إذن الوصول إلى الموقع",
"permission_photos_denied": "تم رفض إذن الوصول إلى مكتبة الصور",
@@ -440,6 +440,8 @@
"queued_offline": "تم وضع التسجيل في قائمة الانتظار لحين استعادة الاتصال",
"quick_check_in": "تسجيل سريع",
"select_type": "اختر نوع التسجيل",
+ "status_critical": "حرج",
+ "status_green": "جيد",
"status_ok": "جيد",
"status_overdue": "متأخر",
"status_warning": "تحذير",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "مثال: تكتيكي 1",
"channel_preset_command": "القيادة",
"channel_preset_tactical": "تكتيكي",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "تعذّر تحميل قنوات الدردشة لهذا الحادث. اضغط مرة أخرى لإعادة المحاولة.",
"close_call_failed": "انتهت القيادة، لكن تعذّر إغلاق المكالمة.",
"close_call_queued": "تمت إضافة إغلاق المكالمة إلى قائمة الانتظار وسيُرسل تلقائيًا.",
"close_call_skipped": "تُركت المكالمة مفتوحة لأن القيادة لم تُغلق.",
"close_channels": "إغلاق الكل",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "دردشة القيادة",
+ "command_chat_short": "القيادة",
+ "command_chat_unavailable": "لم يتم إنشاء قناة دردشة القيادة لهذا الحادث بعد.",
"command_details": "تفاصيل القيادة",
"crew_placeholder": "اسم الشخص أو الفريق",
"current_commander": "قائد الحادث",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "نقل إلى الموارد",
"delete_lane_release_resources": "إخلاء من الحادث",
"delete_lane_resources_message": "يحتوي هذا المسار على {{count}} موارد معيّنة. انقلها إلى قائمة الموارد أو أخلها من الحادث.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "التفاصيل",
+ "dispatch_chat": "مركز الإرسال",
+ "dispatch_chat_unavailable": "لم يتم إنشاء قناة مركز الإرسال لهذا الحادث بعد.",
"drag_move_hint": "اضغط مطولاً على المورد لسحبه، أو اضغط عليه ثم اضغط على مسار.",
"edit_lane": "تعديل المسار",
"edit_lane_title": "تعديل {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "إرسال أيضًا إلى نواب القائد",
"info_save_error": "فشل حفظ معلومات الحادث",
"info_save_success": "تم حفظ معلومات الحادث",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "دردشة المسار",
+ "lane_chat_unavailable": "لم يتم إنشاء قناة دردشة لهذا المسار بعد.",
"lane_color_label": "لون المسار (اختياري — يلوّن علاماته على الخريطة)",
"lane_limits_label": "حدود المسار (اختياري)",
"lane_name_label": "اسم المسار",
@@ -622,8 +624,8 @@
"lead_external_label": "جهة اتصال خارجية",
"lead_name_placeholder": "اسم جهة الاتصال",
"lead_phone_placeholder": "الهاتف",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "جميع المسؤولين",
+ "leads_chat_unavailable": "لم يتم إنشاء قناة جميع المسؤولين لهذا الحادث بعد.",
"limit_max_riding": "الحد الأقصى للطاقم",
"limit_max_time": "تناوب بعد (دقيقة)",
"limit_max_units": "الحد الأقصى للوحدات",
@@ -650,8 +652,8 @@
"message_commander": "رسالة إلى القائد",
"message_commander_hint": "تُرسل مباشرة إلى قائد الحادث عبر قنوات الإشعارات الخاصة به.",
"message_commander_hint_named": "تُرسل مباشرة إلى {{name}} عبر قنوات الإشعارات الخاصة به.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "مراسلة المسؤول",
+ "message_role_holder": "مراسلة",
"message_send_error": "فشل إرسال الرسالة",
"message_send_success": "تم إرسال الرسالة إلى القيادة",
"message_subject_placeholder": "الموضوع (اختياري)",
@@ -850,7 +852,7 @@
"transfer_command": "نقل القيادة",
"transfer_command_hint": "سلّم قيادة الحادث إلى مستخدم آخر. تبقى اللوحة مشتركة.",
"transfer_error": "فشل نقل القيادة",
- "transfer_short": "Transfer",
+ "transfer_short": "نقل",
"transfer_success": "تم نقل القيادة",
"transmission_log": "سجل الإرسال",
"transmission_seconds": "{{count}} ث",
@@ -870,6 +872,7 @@
"back": "رجوع",
"cancel": "إلغاء",
"clear": "مسح",
+ "clear_search": "مسح البحث",
"close": "إغلاق",
"confirm": "تأكيد",
"confirm_location": "تأكيد الموقع",
@@ -1079,144 +1082,144 @@
"required": "هذا الحقل مطلوب"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "المساءلة",
+ "briefing_action_plan": "خطة العمل: {{text}}",
+ "briefing_address": "الموقع: {{address}}",
+ "briefing_command": "القيادة",
+ "briefing_commander": "قائد الحادث: {{name}}",
+ "briefing_established": "تأسيس القيادة: {{time}} (منذ {{duration}})",
+ "briefing_header": "إحاطة نقل القيادة — {{incident}}",
+ "briefing_icp": "مركز القيادة: {{location}}",
+ "briefing_important": "معلومات مهمة: {{text}}",
+ "briefing_lane_row": "{{lane}} (المسؤول: {{lead}}) — {{count}} موارد: {{names}}",
+ "briefing_needs": "الاحتياجات المعلّقة",
+ "briefing_no_lanes": "- لم يتم إنشاء مسارات",
+ "briefing_no_needs": "- لا يوجد",
+ "briefing_no_objectives": "- لم تُسجَّل أهداف",
+ "briefing_no_par": "لا تجري متابعة مساءلة الأفراد.",
+ "briefing_objectives": "الأهداف",
+ "briefing_organization": "التنظيم والموارد",
+ "briefing_rehab": "منطقة الاستراحة: {{location}}",
+ "briefing_situation": "الوضع",
+ "briefing_staging": "منطقة التجمع: {{location}}",
+ "briefing_type": "نوع الحادث: {{type}}",
+ "check_action_plan": "تسجيل خطة عمل أو أهداف",
+ "check_command": "تأسيس القيادة مع تسمية قائد الحادث",
+ "check_icp": "تحديد موقع مركز القيادة",
+ "check_par": "تفعيل المساءلة / تسجيل الحضور",
+ "check_safety": "تعيين ضابط السلامة",
+ "check_staging": "تحديد منطقة التجمع",
+ "checklist_confirm": "بنود {{type}} القياسية الواجب تأكيدها:",
+ "checklist_disclaimer": "هذه إرشادات ICS عامة وليست سياسة قسمك — إجراءات التشغيل القياسية لديك وتقديرك الشخصي يأتيان أولاً.",
+ "checklist_done": "تم إنجازه بالفعل على اللوحة: {{items}}.",
+ "checklist_header": "قائمة تحقق {{type}} — {{incident}}.",
+ "checklist_outstanding": "لم يظهر على اللوحة بعد:",
+ "clear": "مسح المحادثة",
+ "command_post": "مركز القيادة: {{location}}.",
+ "commander": "قائد الحادث: {{name}}.",
+ "elapsed": "القيادة قائمة منذ {{duration}}.",
+ "empty": "اسأل عن هذا الحادث — المساءلة أو الموارد أو الأهداف أو الاحتياجات أو المناصب أو ما قد يفوتك.",
+ "error": "حدث خطأ أثناء الإجابة. حاول مرة أخرى.",
+ "estimated_end": "الانتهاء المتوقع: {{time}}.",
+ "external_resources": "- موارد خارجية / مساعدة متبادلة قيد المتابعة: {{count}}",
+ "important": "مهم: {{text}}",
+ "lane_empty": "لا يوجد شيء معيّن لهذا المسار.",
+ "lane_header": "{{lane}} ({{type}}): {{count}} موارد.",
+ "lane_lead": "المسؤول: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "لا أجد مسارًا باسم \"{{lane}}\". المسارات على اللوحة: {{lanes}}",
+ "lane_objective": "الهدف الرئيسي: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "معالم {{type}} الشائعة غير الموجودة على اللوحة بعد: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — مفتوح منذ {{age}}",
+ "needs_all_met": "تمت تلبية كل ما طُلب.",
+ "needs_header": "{{incident}}: {{outstanding}} احتياجات معلّقة ({{met}} ملبّاة، {{cancelled}} ملغاة).",
+ "no_answer": "تعذّر عليّ الإجابة عن ذلك.",
+ "no_board": "لم يتم تحميل لوحة قيادة لهذا الحادث بعد.",
+ "no_lanes": "لا يوجد بعد",
+ "no_lanes_yet": "لم يتم إنشاء أي مسارات في {{incident}} بعد، لذا لا يوجد نطاق إشراف للتحقق منه.",
+ "no_lead": "بلا مسؤول",
+ "no_needs": "لم تُسجَّل أي احتياجات في {{incident}}.",
+ "no_notes": "لم تُسجَّل أي ملاحظات حالة في {{incident}}.",
+ "no_objectives": "لم يتم تحديد أهداف تكتيكية في {{incident}} بعد.",
+ "no_resources": "لا يوجد شيء معيّن إلى {{incident}} بعد.",
+ "no_rit": "لا أرى مسار فريق تدخل سريع (RIT/RIC) في {{incident}}. في حادث نشط، يجدر تعيينه قبل دخول الطواقم إلى الداخل.",
+ "no_timers": "لا توجد مؤقّتات تعمل في {{incident}}.",
+ "no_unassigned": "كل ما تتم متابعته في {{incident}} موضوع في مسار.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — ملاحظات الحالة ({{count}}):",
+ "objective_complete": "مكتمل",
+ "objective_in_progress": "قيد التنفيذ",
+ "objective_overdue": " [تجاوز الموعد المستهدف]",
+ "objective_pending": "قيد الانتظار",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
- "offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "objective_summary": "الأهداف: {{complete}} من {{total}} مكتملة.",
+ "objectives_all_complete": "جميع الأهداف على اللوحة مكتملة.",
+ "objectives_header": "{{incident}}: {{complete}} من {{total}} أهداف مكتملة. لا تزال مفتوحة:",
+ "offline_badge": "غير متصل",
+ "offline_cannot_answer": "لا يمكنني الإجابة عن ذلك دون اتصال. دون اتصال لا يزال بإمكاني تزويدك بـ PAR والموارد والأهداف والاحتياجات ومناصب ICS والمؤقّتات وسجل الحادث وإحاطة وقائمة التحقق لنوع هذا الحادث.",
+ "offline_hint": "لا يوجد اتصال — الإجابات مأخوذة من اللوحة المخزّنة مؤقتًا على هذا الجهاز.",
+ "open_needs": "{{count}} احتياجات لا تزال مفتوحة.",
+ "par_all_good": "تم حصر جميع الأفراد.",
+ "par_counts": "{{green}} بحالة جيدة، {{warning}} يقترب موعدهم، {{critical}} متأخرون.",
+ "par_critical_header": "متأخرون — لم يتم حصرهم:",
+ "par_due_row": "- {{name}}: يستحق خلال {{minutes}} دقيقة",
+ "par_header": "PAR في {{incident}}: متابعة {{count}} من الأفراد.",
+ "par_none": "لا تجري متابعة مساءلة الأفراد في {{incident}} — لم يسجّل أحد حضوره ولا يعمل أي مؤقّت لتسجيل الحضور.",
+ "par_overdue_row": "- {{name}}: متأخر {{minutes}} دقيقة",
+ "par_summary": "PAR: {{total}} قيد المتابعة، {{critical}} متأخرون، {{warning}} يقترب موعدهم.",
+ "par_warning_header": "يقترب موعد تسجيل الحضور:",
+ "placeholder": "اسأل عن هذا الحادث",
+ "resource_counts": "{{units}} وحدات و{{personnel}} أفراد موزّعون على {{lanes}} مسارات ({{unassigned}} غير معيّن).",
+ "resources_header": "{{incident}}: {{units}} وحدات و{{personnel}} أفراد قيد العمل.",
+ "rit_found": "{{lane}} في وضع الاستعداد ومعه {{count}} من الموارد.",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "لم يتم تعيين {{role}} في {{incident}}.",
+ "role_unknown": "لا أتعرف على المنصب \"{{role}}\". اطلب أدوار ICS لعرض جميع التعيينات.",
+ "roles_header": "{{incident}}: تم تعيين {{count}} من مناصب ICS.",
+ "roles_unfilled": "مناصب شاغرة يحتاجها حادث {{type}} عادةً: {{roles}}",
+ "send": "إرسال",
+ "source_device": "تمت الإجابة على هذا الجهاز",
+ "source_server": "تمت الإجابة بواسطة Resgrid",
+ "span_all_good": "يبدو نطاق الإشراف معقولًا — كل مسار ضمن حدوده ولا يتجاوز أي مسار {{ceiling}} موارد.",
+ "span_header": "{{incident}}: {{lanes}} مسارات، {{resources}} موارد معيّنة.",
+ "span_no_lead": "- لا يوجد مسؤول معيّن: {{lanes}}",
+ "span_over_row": "- {{lane}} يضم {{count}} موارد (الحد {{limit}}) — فكّر في تقسيمه أو إضافة مشرف.",
+ "span_under_row": "- {{lane}} لديه {{count}} موارد مقابل حد أدنى قدره {{minimum}}.",
+ "staging": "منطقة التجمع: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "نقل القيادة",
+ "missing": "ما الذي يفوتني؟",
+ "on_scene": "ما الموجود في الموقع؟",
+ "open_needs": "الاحتياجات المفتوحة",
+ "open_objectives": "الأهداف المفتوحة",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "آخر 30 دقيقة",
+ "rit": "هل لدي فريق تدخل سريع؟",
+ "safety": "ضابط السلامة",
+ "span": "نطاق الإشراف",
+ "status": "حالة الحادث",
+ "timers": "المؤقّتات",
+ "unassigned": "غير معيّن",
+ "unfilled_roles": "مناصب ICS الشاغرة",
+ "wind": "الرياح والطقس"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "جارٍ التحقق من اللوحة…",
+ "this_incident": "هذا الحادث",
+ "time_in_lane": "({{duration}} في المسار)",
+ "timeline_empty": "لم يُسجَّل أي شيء في {{incident}} بعد.",
+ "timeline_empty_window": "لم يُسجَّل أي شيء خلال آخر {{minutes}} دقيقة في {{incident}}.",
+ "timeline_header": "{{incident}} — آخر {{count}} إدخالات في السجل:",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — آخر {{minutes}} دقيقة ({{count}} إدخالات):",
+ "timer_due_row": "- {{name}}: مستحق الآن",
+ "timer_no_due_row": "- {{name}}: قيد التشغيل",
+ "timer_running_row": "- {{name}}: يستحق خلال {{remaining}}",
+ "timers_header": "{{incident}}: {{count}} مؤقّت(ات).",
+ "title": "مساعد الحادث",
+ "unassigned_header": "غير معيّن في {{incident}} ({{count}}):",
+ "unassigned_line": "- مجموعة غير المعيّنين: {{count}}",
+ "unknown": "غير معروف"
},
"incidents": {
"accountability": "متابعة الأفراد",
@@ -1304,13 +1307,16 @@
"2": "ملغاة",
"3": "غير مؤكدة",
"4": "مؤكدة",
- "5": "بسيطة"
+ "5": "بسيطة",
+ "6": "محولة",
+ "7": "إنذار كاذب",
+ "8": "قيد الانتظار"
},
"unknown_priority": "غير معروفة",
"unknown_user": "مستخدم غير معروف"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "غير مصرّح لك باستخدام تطبيق IC. تواصل مع مسؤول قسمك.",
"errorModal": {
"confirmButton": "موافق",
"message": "يرجى التحقق من اسم المستخدم وكلمة المرور والمحاولة مرة أخرى.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "تسجيل الدخول...",
"login_button_success": "تم تسجيل الدخول بنجاح",
"or_sign_in_with_password": "أو تسجيل الدخول بكلمة المرور",
+ "otp_body": "تم تفعيل المصادقة الثنائية لحسابك. أدخل الرمز الحالي من تطبيق المصادقة لإكمال تسجيل الدخول.",
+ "otp_invalid": "هذا الرمز غير صالح أو انتهت صلاحيته. أدخل الرمز الحالي من تطبيق المصادقة.",
+ "otp_placeholder": "رمز من 6 أرقام",
+ "otp_title": "التحقق بخطوتين",
+ "otp_verify": "تحقق",
"password": "كلمة المرور",
"password_incorrect": "كانت كلمة المرور غير صحيحة",
"password_placeholder": "أدخل كلمة المرور الخاصة بك",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "يوجد بالفعل تقرير لهذا الطاقم أو الشخص في ذلك اليوم.",
"timereports_locked": "لم يعد بالإمكان تغيير هذا التقرير.",
"timereports_scope_invalid": "هذا الطاقم أو الشخص غير نشط في عملية النشر هذه.",
+ "timereports_status_transition_invalid": "تم تغيير هذا التقرير بالفعل، ربما من جهاز آخر. حدّث الصفحة وحاول مرة أخرى.",
"timereports_subject_covered": "لهذا الطاقم أو الشخص وقت مسجل بالفعل في تقرير آخر لهذا اليوم.",
"timereports_time_invalid": "أدخل الأوقات بصيغة HH:mm.",
"validation": "رفض الخادم التغيير."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "جميع أنواع نقاط الاهتمام",
"poi_filter_placeholder": "تصفية حسب النوع",
"poi_filter_type": "نوع نقطة الاهتمام",
- "poi_filters": "POI Filters",
+ "poi_filters": "عوامل التصفية",
"poi_not_found": "لم يتم العثور على نقطة الاهتمام",
"poi_not_found_description": "تعذر العثور على نقطة الاهتمام هذه.",
"poi_note": "ملاحظة",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "ترتيب حسب",
"poi_sort_display": "الاسم",
"poi_sort_placeholder": "فرز نقاط الاهتمام",
"poi_sort_type": "النوع",
diff --git a/src/translations/de.json b/src/translations/de.json
index 2e63ca4..88b7e84 100644
--- a/src/translations/de.json
+++ b/src/translations/de.json
@@ -339,7 +339,7 @@
"acknowledge": "Bestätigen",
"add_gif": "GIF hinzufügen",
"add_image": "Bild hinzufügen",
- "all_leads": "All Leads",
+ "all_leads": "Alle Führungskräfte",
"are_typing": "{{count}} Personen tippen...",
"assistant": "Assistent",
"attachment_failed": "Hochladen des Anhangs fehlgeschlagen",
@@ -353,8 +353,8 @@
"delete": "Löschen",
"delete_failed": "Nachricht konnte nicht gelöscht werden",
"direct_message": "Direktnachricht",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Diese Unterhaltung konnte nicht geöffnet werden.",
+ "dm_unavailable": "Dieser Kontakt hat kein Resgrid-Konto, an das Nachrichten gesendet werden können.",
"edit": "Bearbeiten",
"edit_failed": "Nachricht konnte nicht bearbeitet werden",
"edit_message": "Nachricht bearbeiten",
@@ -371,14 +371,14 @@
"flag_reason": "Warum melden Sie dies?",
"flag_sensitive": "Sensible Informationen",
"flag_spam": "Spam",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Dieser Einsatz ist abgeschlossen. Die Unterhaltung bleibt als Momentaufnahme erhalten — keine neuen Nachrichten oder Bearbeitungen, Inhalte können aber weiterhin gemeldet werden.",
"group_name": "Gruppenname (optional)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Führungs-Chat",
+ "incident_lane_channel": "Abschnitts-Chat",
"is_typing": "{{name}} tippt...",
"load_people_failed": "Personen konnten nicht geladen werden",
"message_deleted": "Diese Nachricht wurde gelöscht",
- "message_person": "Message {{name}}",
+ "message_person": "Nachricht an {{name}}",
"moderator_delete": "Entfernen (Moderator)",
"moderator_removed": "Vom Moderator entfernt",
"new_direct_message": "Neue Direktnachricht",
@@ -386,8 +386,8 @@
"no_gifs": "Keine GIFs gefunden",
"no_people": "Keine Personen gefunden",
"open_assistant": "Assistent öffnen",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Führungs-Chat",
+ "open_lane_chat": "Abschnitts-Chat",
"original_message": "Ursprüngliche Nachricht",
"permission_location_denied": "Standortberechtigung verweigert",
"permission_photos_denied": "Zugriff auf die Fotomediathek verweigert",
@@ -440,6 +440,8 @@
"queued_offline": "Check-In wird bei Verbindung nachgeholt",
"quick_check_in": "Schnell-Check-In",
"select_type": "Check-In-Typ wählen",
+ "status_critical": "Kritisch",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Überfällig",
"status_warning": "Warnung",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "z. B. Taktik 1",
"channel_preset_command": "Führung",
"channel_preset_tactical": "Taktik",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Die Chat-Kanäle dieses Einsatzes konnten nicht geladen werden. Zum Wiederholen erneut tippen.",
"close_call_failed": "Die Einsatzleitung wurde beendet, der Anruf konnte jedoch nicht geschlossen werden.",
"close_call_queued": "Das Schließen des Anrufs ist in der Warteschlange und wird automatisch gesendet.",
"close_call_skipped": "Der Anruf bleibt offen, weil die Einsatzleitung nicht beendet wurde.",
"close_channels": "Alle schließen",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Führungs-Chat",
+ "command_chat_short": "Führung",
+ "command_chat_unavailable": "Für diesen Einsatz wurde noch kein Führungs-Chat-Kanal erstellt.",
"command_details": "Führungsdetails",
"crew_placeholder": "Name der Person oder des Trupps",
"current_commander": "EL",
@@ -507,8 +509,8 @@
"delete_lane_release_resources": "Vom Einsatz lösen",
"delete_lane_resources_message": "Dieser Spur sind {{count}} Ressourcen zugewiesen. Zurück in die Ressourcenliste verschieben oder vom Einsatz lösen.",
"details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "dispatch_chat": "Leitstelle",
+ "dispatch_chat_unavailable": "Für diesen Einsatz wurde noch kein Leitstellen-Kanal erstellt.",
"drag_move_hint": "Halten Sie eine Ressource gedrückt, um sie zu ziehen, oder tippen Sie sie und anschließend eine Spur an.",
"edit_lane": "Abschnitt bearbeiten",
"edit_lane_title": "{{lane}} bearbeiten",
@@ -598,8 +600,8 @@
"include_deputies": "Auch an stellvertretende Einsatzleiter senden",
"info_save_error": "Speichern der Einsatzinformationen fehlgeschlagen",
"info_save_success": "Einsatzinformationen gespeichert",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Abschnitts-Chat",
+ "lane_chat_unavailable": "Für diesen Abschnitt wurde noch kein Chat-Kanal erstellt.",
"lane_color_label": "Spurfarbe (optional — färbt die Marker dieser Spur auf der Karte)",
"lane_limits_label": "Spur-Limits (optional)",
"lane_name_label": "Abschnittsname",
@@ -622,8 +624,8 @@
"lead_external_label": "Externer Kontakt",
"lead_name_placeholder": "Kontaktname",
"lead_phone_placeholder": "Telefon",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Alle Führungskräfte",
+ "leads_chat_unavailable": "Der Kanal „Alle Führungskräfte“ wurde für diesen Einsatz noch nicht erstellt.",
"limit_max_riding": "Max. Besatzung",
"limit_max_time": "Ablösen nach (Min)",
"limit_max_units": "Max. Einheiten",
@@ -650,8 +652,8 @@
"message_commander": "Nachricht an Einsatzleiter",
"message_commander_hint": "Geht direkt an den Einsatzleiter über dessen Benachrichtigungskanäle.",
"message_commander_hint_named": "Geht direkt an {{name}} über dessen Benachrichtigungskanäle.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Nachricht an Leiter",
+ "message_role_holder": "Nachricht",
"message_send_error": "Nachricht konnte nicht gesendet werden",
"message_send_success": "Nachricht an Einsatzleitung gesendet",
"message_subject_placeholder": "Betreff (optional)",
@@ -719,7 +721,7 @@
"node_sector": "Sektor",
"node_staging": "Bereitstellungsraum",
"node_strike_team": "Einsatztrupp",
- "node_task_force": "Task Force",
+ "node_task_force": "Taskforce",
"node_unified_command": "Gemeinsame Führung",
"none_option": "Keins",
"not_on_board": "Nicht auf dem Board",
@@ -850,7 +852,7 @@
"transfer_command": "Führung übergeben",
"transfer_command_hint": "Übergib die Einsatzleitung an einen anderen Benutzer. Das Board bleibt geteilt.",
"transfer_error": "Führungsübergabe fehlgeschlagen",
- "transfer_short": "Transfer",
+ "transfer_short": "Übergeben",
"transfer_success": "Führung übergeben",
"transmission_log": "Sendeprotokoll",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Zurück",
"cancel": "Abbrechen",
"clear": "Löschen",
+ "clear_search": "Suche löschen",
"close": "Schließen",
"confirm": "Bestätigen",
"confirm_location": "Standort bestätigen",
@@ -1079,144 +1082,144 @@
"required": "Dieses Feld ist erforderlich"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "PERSONALÜBERSICHT",
+ "briefing_action_plan": "Maßnahmenplan: {{text}}",
+ "briefing_address": "Einsatzort: {{address}}",
+ "briefing_command": "FÜHRUNG",
+ "briefing_commander": "Einsatzleiter: {{name}}",
+ "briefing_established": "Einsatzleitung eingerichtet: {{time}} (seit {{duration}})",
+ "briefing_header": "BRIEFING ZUR FÜHRUNGSÜBERGABE — {{incident}}",
+ "briefing_icp": "Befehlsstelle: {{location}}",
+ "briefing_important": "Wichtige Informationen: {{text}}",
+ "briefing_lane_row": "{{lane}} (Leiter: {{lead}}) — {{count}} Ressourcen: {{names}}",
+ "briefing_needs": "OFFENE BEDARFE",
+ "briefing_no_lanes": "- Keine Abschnitte eingerichtet",
+ "briefing_no_needs": "- Keine",
+ "briefing_no_objectives": "- Keine Ziele erfasst",
+ "briefing_no_par": "Es wird keine Personalübersicht geführt.",
+ "briefing_objectives": "ZIELE",
+ "briefing_organization": "ORGANISATION UND RESSOURCEN",
+ "briefing_rehab": "Erholungsbereich: {{location}}",
+ "briefing_situation": "LAGE",
+ "briefing_staging": "Bereitstellungsraum: {{location}}",
+ "briefing_type": "Einsatzart: {{type}}",
+ "check_action_plan": "Maßnahmenplan oder Ziele erfasst",
+ "check_command": "Einsatzleitung mit benanntem Einsatzleiter eingerichtet",
+ "check_icp": "Standort der Befehlsstelle festgelegt",
+ "check_par": "Personalübersicht / Check-In läuft",
+ "check_safety": "Sicherheitsbeauftragter zugewiesen",
+ "check_staging": "Bereitstellungsraum festgelegt",
+ "checklist_confirm": "Zu bestätigende Standardpunkte für {{type}}:",
+ "checklist_disclaimer": "Dies ist eine allgemeine ICS-Empfehlung, nicht die Vorgabe Ihrer Organisation — Ihre Dienstanweisungen und Ihr Urteilsvermögen haben Vorrang.",
+ "checklist_done": "Bereits auf dem Board erledigt: {{items}}.",
+ "checklist_header": "{{incident}}: Checkliste {{type}}.",
+ "checklist_outstanding": "Noch nicht auf dem Board:",
+ "clear": "Unterhaltung löschen",
+ "command_post": "Befehlsstelle: {{location}}.",
+ "commander": "EL: {{name}}.",
+ "elapsed": "Einsatzleitung seit {{duration}}.",
+ "empty": "Fragen Sie zu diesem Einsatz — Personalübersicht, Ressourcen, Ziele, Bedarfe, Rollen oder was Ihnen möglicherweise fehlt.",
+ "error": "Bei der Beantwortung ist ein Fehler aufgetreten. Bitte erneut versuchen.",
+ "estimated_end": "Voraussichtliches Ende: {{time}}.",
+ "external_resources": "- Erfasste externe / überörtliche Ressourcen: {{count}}",
+ "important": "Wichtig: {{text}}",
+ "lane_empty": "Diesem Abschnitt ist nichts zugewiesen.",
+ "lane_header": "{{lane}} ({{type}}): {{count}} Ressourcen.",
+ "lane_lead": "Leiter: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Einen Abschnitt namens „{{lane}}“ finde ich nicht. Abschnitte auf dem Board: {{lanes}}",
+ "lane_objective": "Hauptziel: {{name}} ({{progress}} %).",
+ "missing_benchmarks": "Übliche Meilensteine für {{type}}, die noch nicht auf dem Board stehen: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — offen seit {{age}}",
+ "needs_all_met": "Alle Anforderungen wurden erfüllt.",
+ "needs_header": "{{incident}}: {{outstanding}} offene Bedarfe ({{met}} erfüllt, {{cancelled}} geschlossen).",
+ "no_answer": "Darauf konnte ich keine Antwort finden.",
+ "no_board": "Für diesen Einsatz ist noch keine Führungstafel geladen.",
+ "no_lanes": "noch keine",
+ "no_lanes_yet": "{{incident}}: Noch keine Abschnitte angelegt, daher gibt es keine Führungsspanne zu prüfen.",
+ "no_lead": "kein Leiter",
+ "no_needs": "{{incident}}: Keine Bedarfe erfasst.",
+ "no_notes": "{{incident}}: Keine Statusnotizen erfasst.",
+ "no_objectives": "{{incident}}: Noch keine taktischen Ziele festgelegt.",
+ "no_resources": "{{incident}}: Noch nichts zugewiesen.",
+ "no_rit": "{{incident}}: Kein Abschnitt für einen Sicherheitstrupp (RIT/RIC) vorhanden. Bei einem laufenden Einsatz sollte er zugewiesen werden, bevor Trupps zum Innenangriff vorgehen.",
+ "no_timers": "{{incident}}: Keine Timer aktiv.",
+ "no_unassigned": "{{incident}}: Alle erfassten Ressourcen sind einem Abschnitt zugeordnet.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — {{count}} Statusnotiz(en):",
+ "objective_complete": "abgeschlossen",
+ "objective_in_progress": "in Arbeit",
+ "objective_overdue": " [Zielzeit überschritten]",
+ "objective_pending": "ausstehend",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
+ "objective_summary": "Ziele: {{complete}} von {{total}} abgeschlossen.",
+ "objectives_all_complete": "Alle Ziele auf dem Board sind abgeschlossen.",
+ "objectives_header": "{{incident}}: {{complete}} von {{total}} Zielen abgeschlossen. Noch offen:",
"offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "offline_cannot_answer": "Ohne Verbindung kann ich das nicht beantworten. Offline kann ich Ihnen weiterhin PAR, Ressourcen, Ziele, Bedarfe, ICS-Rollen, Timer, das Einsatzprotokoll, ein Briefing und die Checkliste für diese Einsatzart liefern.",
+ "offline_hint": "Keine Verbindung — Antworten basieren auf dem auf diesem Gerät zwischengespeicherten Board.",
+ "open_needs": "{{count}} Bedarfe noch offen.",
+ "par_all_good": "Alle Einsatzkräfte sind erfasst.",
+ "par_counts": "{{green}} grün, {{warning}} bald fällig, {{critical}} überfällig.",
+ "par_critical_header": "Überfällig — nicht erfasst:",
+ "par_due_row": "- {{name}}: fällig in {{minutes}} Min.",
+ "par_header": "PAR – {{incident}}: {{count}} Einsatzkräfte erfasst.",
+ "par_none": "{{incident}}: Es wird keine Personalübersicht geführt — niemand hat eingecheckt und kein Check-In-Timer läuft.",
+ "par_overdue_row": "- {{name}}: {{minutes}} Min. überfällig",
+ "par_summary": "PAR: {{total}} erfasst, {{critical}} überfällig, {{warning}} bald fällig.",
+ "par_warning_header": "Check-In bald fällig:",
+ "placeholder": "Frage zu diesem Einsatz",
+ "resource_counts": "{{units}} Einheiten und {{personnel}} Einsatzkräfte in {{lanes}} Abschnitten ({{unassigned}} nicht zugewiesen).",
+ "resources_header": "{{incident}}: {{units}} Einheiten und {{personnel}} Einsatzkräfte im Einsatz.",
+ "rit_found": "{{lane}} steht mit {{count}} Ressource(n) bereit.",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "{{incident}}: {{role}} ist nicht besetzt.",
+ "role_unknown": "Die Rolle „{{role}}“ kenne ich nicht. Fragen Sie nach ICS-Rollen, um alle Zuweisungen zu sehen.",
+ "roles_header": "{{incident}}: {{count}} ICS-Rolle(n) besetzt.",
+ "roles_unfilled": "Unbesetzte Rollen, die bei {{type}} üblicherweise benötigt werden: {{roles}}",
+ "send": "Senden",
+ "source_device": "Auf diesem Gerät beantwortet",
+ "source_server": "Von Resgrid beantwortet",
+ "span_all_good": "Die Führungsspanne ist angemessen — alle Abschnitte liegen innerhalb ihrer Grenzen und kein Abschnitt hat mehr als {{ceiling}} Ressourcen.",
+ "span_header": "{{incident}}: {{lanes}} Abschnitte, {{resources}} Ressourcen zugewiesen.",
+ "span_no_lead": "- Kein Leiter zugewiesen: {{lanes}}",
+ "span_over_row": "- {{lane}} hat {{count}} Ressourcen (Limit {{limit}}) — ggf. aufteilen oder eine zusätzliche Führungskraft einsetzen.",
+ "span_under_row": "- {{lane}} hat {{count}} Ressourcen bei einem Minimum von {{minimum}}.",
+ "staging": "Bereitstellungsraum: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Führungsübergabe",
+ "missing": "Was fehlt mir noch?",
+ "on_scene": "Was ist vor Ort?",
+ "open_needs": "Offene Bedarfe",
+ "open_objectives": "Offene Ziele",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "Letzte 30 Minuten",
+ "rit": "Habe ich einen Sicherheitstrupp?",
+ "safety": "Sicherheitsbeauftragter",
+ "span": "Führungsspanne",
+ "status": "Einsatzstatus",
+ "timers": "Timer",
+ "unassigned": "Nicht zugewiesen",
+ "unfilled_roles": "Unbesetzte ICS-Rollen",
+ "wind": "Wind und Wetter"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Board wird geprüft…",
+ "this_incident": "Dieser Einsatz",
+ "time_in_lane": "({{duration}} im Abschnitt)",
+ "timeline_empty": "{{incident}}: Noch keine Protokolleinträge.",
+ "timeline_empty_window": "{{incident}}: In den letzten {{minutes}} Minuten wurde nichts protokolliert.",
+ "timeline_header": "{{incident}} — letzte {{count}} Protokolleinträge:",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — letzte {{minutes}} Minuten ({{count}} Einträge):",
+ "timer_due_row": "- {{name}}: JETZT FÄLLIG",
+ "timer_no_due_row": "- {{name}}: läuft",
+ "timer_running_row": "- {{name}}: fällig in {{remaining}}",
+ "timers_header": "{{incident}}: {{count}} Timer.",
+ "title": "Einsatz-Assistent",
+ "unassigned_header": "{{incident}} – nicht zugewiesen ({{count}}):",
+ "unassigned_line": "- Nicht zugewiesene Ressourcen: {{count}}",
+ "unknown": "unbekannt"
},
"incidents": {
"accountability": "Anwesenheitskontrolle",
@@ -1304,13 +1307,16 @@
"2": "Abgebrochen",
"3": "Unbegründet",
"4": "Begründet",
- "5": "Geringfügig"
+ "5": "Geringfügig",
+ "6": "Übergeben",
+ "7": "Fehlalarm",
+ "8": "Ausstehend"
},
"unknown_priority": "Unbekannt",
"unknown_user": "Unbekannter Benutzer"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Sie sind nicht berechtigt, die IC-App zu verwenden. Wenden Sie sich an den Administrator Ihrer Abteilung.",
"errorModal": {
"confirmButton": "OK",
"message": "Bitte Benutzername und Passwort überprüfen und erneut versuchen.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Anmelden...",
"login_button_success": "Erfolgreich angemeldet",
"or_sign_in_with_password": "oder mit Passwort anmelden",
+ "otp_body": "Für Ihr Konto ist die Zwei-Faktor-Authentifizierung aktiviert. Geben Sie den aktuellen Code aus Ihrer Authenticator-App ein, um die Anmeldung abzuschließen.",
+ "otp_invalid": "Dieser Code ist ungültig oder abgelaufen. Geben Sie den aktuellen Code aus Ihrer Authenticator-App ein.",
+ "otp_placeholder": "6-stelliger Code",
+ "otp_title": "Zwei-Faktor-Verifizierung",
+ "otp_verify": "Bestätigen",
"password": "Passwort",
"password_incorrect": "Passwort war falsch",
"password_placeholder": "Passwort eingeben",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Für diese Besatzung oder Person gibt es an diesem Tag bereits einen Bericht.",
"timereports_locked": "Dieser Bericht kann nicht mehr geändert werden.",
"timereports_scope_invalid": "Diese Besatzung oder Person ist in diesem Einsatz nicht aktiv.",
+ "timereports_status_transition_invalid": "Dieser Bericht wurde bereits geändert, möglicherweise auf einem anderen Gerät. Bitte aktualisieren und erneut versuchen.",
"timereports_subject_covered": "Für diese Besatzung oder Person ist an diesem Tag bereits Zeit in einem anderen Bericht erfasst.",
"timereports_time_invalid": "Geben Sie Zeiten als HH:mm ein.",
"validation": "Der Server hat die Änderung abgelehnt."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Alle POI-Typen",
"poi_filter_placeholder": "Nach Typ filtern",
"poi_filter_type": "POI-Typ",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filter",
"poi_not_found": "POI nicht gefunden",
"poi_not_found_description": "Dieser POI konnte nicht gefunden werden.",
"poi_note": "Notiz",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Sortieren nach",
"poi_sort_display": "Name",
"poi_sort_placeholder": "POIs sortieren",
"poi_sort_type": "Typ",
@@ -2156,7 +2168,7 @@
"skip_reason_placeholder": "Grund für das Überspringen dieses Stopps eingeben",
"skipped": "Übersprungen",
"skipped_by_driver": "Vom Fahrer übersprungen",
- "start": "Start",
+ "start": "Starten",
"start_route": "Route starten",
"step_of": "Schritt {{current}} von {{total}}",
"stop_contact": "Stoppkontakt",
diff --git a/src/translations/el.json b/src/translations/el.json
index 23656c4..9a465d8 100644
--- a/src/translations/el.json
+++ b/src/translations/el.json
@@ -440,6 +440,8 @@
"queued_offline": "Η δήλωση παρουσίας τέθηκε σε ουρά για όταν αποκατασταθεί η σύνδεση",
"quick_check_in": "Γρήγορη Δήλωση Παρουσίας",
"select_type": "Επιλέξτε τύπο παρουσίας",
+ "status_critical": "Κρίσιμο",
+ "status_green": "Εντάξει",
"status_ok": "Εντάξει",
"status_overdue": "Εκπρόθεσμο",
"status_warning": "Προειδοποίηση",
@@ -870,6 +872,7 @@
"back": "Πίσω",
"cancel": "Ακύρωση",
"clear": "Καθαρισμός",
+ "clear_search": "Εκκαθάριση αναζήτησης",
"close": "Κλείσιμο",
"confirm": "Επιβεβαίωση",
"confirm_location": "Επιβεβαίωση Τοποθεσίας",
@@ -1304,7 +1307,10 @@
"2": "Ακυρωμένη",
"3": "Αβάσιμη",
"4": "Βάσιμη",
- "5": "Μικρή"
+ "5": "Μικρή",
+ "6": "Μεταβιβάστηκε",
+ "7": "Ψευδής Συναγερμός",
+ "8": "Εκκρεμεί"
},
"unknown_priority": "Άγνωστη",
"unknown_user": "Άγνωστος χρήστης"
@@ -1323,6 +1329,11 @@
"login_button_loading": "Σύνδεση...",
"login_button_success": "Επιτυχής σύνδεση",
"or_sign_in_with_password": "ή συνδεθείτε με κωδικό πρόσβασης",
+ "otp_body": "Ο λογαριασμός σας έχει ενεργοποιημένο έλεγχο ταυτότητας δύο παραγόντων. Εισαγάγετε τον τρέχοντα κωδικό από την εφαρμογή ελέγχου ταυτότητας για να ολοκληρώσετε τη σύνδεση.",
+ "otp_invalid": "Ο κωδικός δεν είναι έγκυρος ή έχει λήξει. Εισαγάγετε τον τρέχοντα κωδικό από την εφαρμογή ελέγχου ταυτότητας.",
+ "otp_placeholder": "Εξαψήφιος κωδικός",
+ "otp_title": "Επαλήθευση δύο παραγόντων",
+ "otp_verify": "Επαλήθευση",
"password": "Κωδικός Πρόσβασης",
"password_incorrect": "Ο κωδικός πρόσβασης ήταν λανθασμένος",
"password_placeholder": "Εισαγάγετε τον κωδικό πρόσβασής σας",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Υπάρχει ήδη αναφορά για αυτό το πλήρωμα ή πρόσωπο εκείνη την ημέρα.",
"timereports_locked": "Αυτή η αναφορά δεν μπορεί πλέον να αλλάξει.",
"timereports_scope_invalid": "Αυτό το πλήρωμα ή πρόσωπο δεν είναι ενεργό σε αυτή την ανάπτυξη.",
+ "timereports_status_transition_invalid": "Αυτή η αναφορά έχει ήδη αλλάξει, πιθανώς από άλλη συσκευή. Ανανεώστε και δοκιμάστε ξανά.",
"timereports_subject_covered": "Αυτό το πλήρωμα ή πρόσωπο έχει ήδη χρόνο σε άλλη αναφορά για αυτή την ημέρα.",
"timereports_time_invalid": "Εισαγάγετε ώρες ως HH:mm.",
"validation": "Ο διακομιστής απέρριψε την αλλαγή."
diff --git a/src/translations/en.json b/src/translations/en.json
index 95b6e53..0534c06 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -440,6 +440,8 @@
"queued_offline": "Check-in queued for when connection is restored",
"quick_check_in": "Quick Check-In",
"select_type": "Select check-in type",
+ "status_critical": "Critical",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Overdue",
"status_warning": "Warning",
@@ -870,6 +872,7 @@
"back": "Back",
"cancel": "Cancel",
"clear": "Clear",
+ "clear_search": "Clear search",
"close": "Close",
"confirm": "Confirm",
"confirm_location": "Confirm Location",
@@ -1304,7 +1307,10 @@
"2": "Cancelled",
"3": "Unfounded",
"4": "Founded",
- "5": "Minor"
+ "5": "Minor",
+ "6": "Transferred",
+ "7": "False Alarm",
+ "8": "Pending"
},
"unknown_priority": "Unknown",
"unknown_user": "Unknown user"
@@ -1323,6 +1329,11 @@
"login_button_loading": "Signing in...",
"login_button_success": "Logged in successfully",
"or_sign_in_with_password": "or sign in with password",
+ "otp_body": "Your account has two-factor authentication enabled. Enter the current code from your authenticator app to finish signing in.",
+ "otp_invalid": "That code is invalid or has expired. Enter the current code from your authenticator app.",
+ "otp_placeholder": "6-digit code",
+ "otp_title": "Two-factor verification",
+ "otp_verify": "Verify",
"password": "Password",
"password_incorrect": "Password was incorrect",
"password_placeholder": "Enter your password",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "A report for this crew or person already exists for that day.",
"timereports_locked": "This report can no longer be changed.",
"timereports_scope_invalid": "That crew or person is not active on this deployment.",
+ "timereports_status_transition_invalid": "This report was already changed, possibly on another device. Refresh and try again.",
"timereports_subject_covered": "That crew or person already has time on another report for this day.",
"timereports_time_invalid": "Enter times as HH:mm.",
"validation": "The server rejected the change."
diff --git a/src/translations/es.json b/src/translations/es.json
index c6e0916..9b13f4c 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -339,7 +339,7 @@
"acknowledge": "Confirmar",
"add_gif": "Agregar GIF",
"add_image": "Agregar imagen",
- "all_leads": "All Leads",
+ "all_leads": "Todos los líderes",
"are_typing": "{{count}} personas están escribiendo...",
"assistant": "Asistente",
"attachment_failed": "Error al subir el archivo adjunto",
@@ -353,8 +353,8 @@
"delete": "Eliminar",
"delete_failed": "No se pudo eliminar el mensaje",
"direct_message": "Mensaje directo",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "No se pudo abrir esa conversación.",
+ "dm_unavailable": "Ese contacto no tiene una cuenta de Resgrid para enviarle mensajes.",
"edit": "Editar",
"edit_failed": "No se pudo editar el mensaje",
"edit_message": "Editar mensaje",
@@ -371,14 +371,14 @@
"flag_reason": "¿Por qué lo denuncias?",
"flag_sensitive": "Información confidencial",
"flag_spam": "Spam",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Este incidente está cerrado. La conversación se conserva como registro de ese momento: no se permiten mensajes nuevos ni ediciones, pero aún puedes denunciar contenido.",
"group_name": "Nombre del grupo (opcional)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Chat de mando",
+ "incident_lane_channel": "Chat del carril",
"is_typing": "{{name}} está escribiendo...",
"load_people_failed": "No se pudieron cargar las personas",
"message_deleted": "Este mensaje fue eliminado",
- "message_person": "Message {{name}}",
+ "message_person": "Enviar mensaje a {{name}}",
"moderator_delete": "Eliminar (moderador)",
"moderator_removed": "Eliminado por el moderador",
"new_direct_message": "Nuevo mensaje directo",
@@ -386,8 +386,8 @@
"no_gifs": "No se encontraron GIF",
"no_people": "No se encontraron personas",
"open_assistant": "Abrir asistente",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Chat de mando",
+ "open_lane_chat": "Chat del carril",
"original_message": "Mensaje original",
"permission_location_denied": "Permiso de ubicación denegado",
"permission_photos_denied": "Permiso de la galería de fotos denegado",
@@ -440,6 +440,8 @@
"queued_offline": "Registro en cola para cuando se restaure la conexión",
"quick_check_in": "Registro rápido",
"select_type": "Seleccionar tipo de registro",
+ "status_critical": "Crítico",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Vencido",
"status_warning": "Advertencia",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "p. ej. Táctico 1",
"channel_preset_command": "Mando",
"channel_preset_tactical": "Táctico",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "No se pudieron cargar los canales de chat de este incidente. Toca de nuevo para reintentar.",
"close_call_failed": "El comando se finalizó, pero no se pudo cerrar la llamada.",
"close_call_queued": "El cierre de la llamada está en cola y se enviará automáticamente.",
"close_call_skipped": "La llamada sigue abierta porque el comando no se cerró.",
"close_channels": "Cerrar todo",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Chat de mando",
+ "command_chat_short": "Mando",
+ "command_chat_unavailable": "Aún no se ha creado un canal de chat de mando para este incidente.",
"command_details": "Detalles del comando",
"crew_placeholder": "Nombre de la persona o del equipo",
"current_commander": "CI",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Mover a recursos",
"delete_lane_release_resources": "Dar de baja del incidente",
"delete_lane_resources_message": "Este carril tiene {{count}} recursos asignados. Muévalos a la lista de recursos o délos de baja del incidente.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Detalles",
+ "dispatch_chat": "Despacho",
+ "dispatch_chat_unavailable": "Aún no se ha creado un canal de despacho para este incidente.",
"drag_move_hint": "Mantén pulsado un recurso para arrastrarlo, o tócalo y luego toca un carril.",
"edit_lane": "Editar carril",
"edit_lane_title": "Editar {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Enviar también a los comandantes adjuntos",
"info_save_error": "No se pudo guardar la información",
"info_save_success": "Información guardada",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Chat del carril",
+ "lane_chat_unavailable": "Aún no se ha creado un canal de chat para este carril.",
"lane_color_label": "Color del carril (opcional — colorea sus marcadores en el mapa)",
"lane_limits_label": "Límites del carril (opcional)",
"lane_name_label": "Nombre del carril",
@@ -622,8 +624,8 @@
"lead_external_label": "Contacto externo",
"lead_name_placeholder": "Nombre del contacto",
"lead_phone_placeholder": "Teléfono",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Todos los líderes",
+ "leads_chat_unavailable": "Aún no se ha creado el canal Todos los líderes para este incidente.",
"limit_max_riding": "Máx. a bordo",
"limit_max_time": "Rotar tras (min)",
"limit_max_units": "Máx. unidades",
@@ -650,8 +652,8 @@
"message_commander": "Enviar mensaje al comandante",
"message_commander_hint": "Se envía directamente al comandante del incidente por sus canales de notificación.",
"message_commander_hint_named": "Se envía directamente a {{name}} por sus canales de notificación.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Mensaje al líder",
+ "message_role_holder": "Mensaje",
"message_send_error": "Error al enviar el mensaje",
"message_send_success": "Mensaje enviado al mando",
"message_subject_placeholder": "Asunto (opcional)",
@@ -850,7 +852,7 @@
"transfer_command": "Transferir el mando",
"transfer_command_hint": "Entrega el mando del incidente a otro usuario. El tablero sigue compartido.",
"transfer_error": "No se pudo transferir el mando",
- "transfer_short": "Transfer",
+ "transfer_short": "Transferir",
"transfer_success": "Mando transferido",
"transmission_log": "Registro de transmisiones",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Atrás",
"cancel": "Cancelar",
"clear": "Limpiar",
+ "clear_search": "Borrar búsqueda",
"close": "Cerrar",
"confirm": "Confirmar",
"confirm_location": "Confirmar ubicación",
@@ -1079,144 +1082,144 @@
"required": "Este campo es obligatorio"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "RENDICIÓN DE CUENTAS",
+ "briefing_action_plan": "Plan de acción: {{text}}",
+ "briefing_address": "Ubicación: {{address}}",
+ "briefing_command": "MANDO",
+ "briefing_commander": "Comandante del incidente: {{name}}",
+ "briefing_established": "Mando establecido: {{time}} (activo desde hace {{duration}})",
+ "briefing_header": "INFORME DE TRANSFERENCIA DE MANDO — {{incident}}",
+ "briefing_icp": "Puesto de mando: {{location}}",
+ "briefing_important": "Información importante: {{text}}",
+ "briefing_lane_row": "{{lane}} (líder: {{lead}}) — {{count}} recursos: {{names}}",
+ "briefing_needs": "NECESIDADES PENDIENTES",
+ "briefing_no_lanes": "- No hay carriles establecidos",
+ "briefing_no_needs": "- Ninguna",
+ "briefing_no_objectives": "- No hay objetivos registrados",
+ "briefing_no_par": "No se está controlando la rendición de cuentas del personal.",
+ "briefing_objectives": "OBJETIVOS",
+ "briefing_organization": "ORGANIZACIÓN Y RECURSOS",
+ "briefing_rehab": "Rehabilitación: {{location}}",
+ "briefing_situation": "SITUACIÓN",
+ "briefing_staging": "Área de espera: {{location}}",
+ "briefing_type": "Tipo de incidente: {{type}}",
+ "check_action_plan": "Plan de acción u objetivos registrados",
+ "check_command": "Mando establecido con un CI designado",
+ "check_icp": "Ubicación del puesto de mando definida",
+ "check_par": "Rendición de cuentas / registro en curso",
+ "check_safety": "Oficial de seguridad asignado",
+ "check_staging": "Área de espera designada",
+ "checklist_confirm": "Elementos estándar de {{type}} por confirmar:",
+ "checklist_disclaimer": "Esta es una guía general del ICS, no la política de tu departamento — tus procedimientos operativos y tu criterio van primero.",
+ "checklist_done": "Ya hecho en el tablero: {{items}}.",
+ "checklist_header": "Lista de verificación de {{type}} para {{incident}}.",
+ "checklist_outstanding": "Aún no aparece en el tablero:",
+ "clear": "Borrar conversación",
+ "command_post": "PC: {{location}}.",
+ "commander": "CI: {{name}}.",
+ "elapsed": "Mando activo desde hace {{duration}}.",
+ "empty": "Pregunta sobre este incidente: rendición de cuentas, recursos, objetivos, necesidades, puestos o lo que podrías estar pasando por alto.",
+ "error": "Algo salió mal al responder. Inténtalo de nuevo.",
+ "estimated_end": "Fin estimado: {{time}}.",
+ "external_resources": "- Recursos externos / de ayuda mutua registrados: {{count}}",
+ "important": "Importante: {{text}}",
+ "lane_empty": "No hay nada asignado a este carril.",
+ "lane_header": "{{lane}} ({{type}}): {{count}} recursos.",
+ "lane_lead": "Líder: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "No veo ningún carril llamado \"{{lane}}\". Carriles en el tablero: {{lanes}}",
+ "lane_objective": "Objetivo principal: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Hitos habituales de {{type}} que aún no están en el tablero: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — abierta hace {{age}}",
+ "needs_all_met": "Todo lo solicitado se ha cubierto.",
+ "needs_header": "{{incident}}: {{outstanding}} necesidades pendientes ({{met}} cubiertas, {{cancelled}} canceladas).",
+ "no_answer": "No pude responder a eso.",
+ "no_board": "Aún no hay un tablero de mando cargado para este incidente.",
+ "no_lanes": "ninguno todavía",
+ "no_lanes_yet": "Aún no se han creado carriles en {{incident}}, así que no hay alcance de control que revisar.",
+ "no_lead": "sin líder",
+ "no_needs": "No se han registrado necesidades en {{incident}}.",
+ "no_notes": "No se han registrado notas de estado en {{incident}}.",
+ "no_objectives": "Aún no se han definido objetivos tácticos en {{incident}}.",
+ "no_resources": "Aún no hay nada asignado a {{incident}}.",
+ "no_rit": "No veo un carril RIT/RIC en {{incident}}. En un incidente activo, conviene asignarlo antes de que los equipos entren al interior.",
+ "no_timers": "No hay temporizadores en marcha en {{incident}}.",
+ "no_unassigned": "Todo lo registrado en {{incident}} está ubicado en un carril.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — {{count}} nota(s) de estado:",
+ "objective_complete": "completado",
+ "objective_in_progress": "en curso",
+ "objective_overdue": " [fuera de plazo]",
+ "objective_pending": "pendiente",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
- "offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "objective_summary": "Objetivos: {{complete}} de {{total}} completados.",
+ "objectives_all_complete": "Todos los objetivos del tablero están completados.",
+ "objectives_header": "{{incident}}: {{complete}} de {{total}} objetivos completados. Aún abiertos:",
+ "offline_badge": "Sin conexión",
+ "offline_cannot_answer": "No puedo responder a eso sin conexión. Sin conexión aún puedo darte el PAR, los recursos, los objetivos, las necesidades, los puestos ICS, los temporizadores, el registro del incidente, un informe y la lista de verificación de este tipo de incidente.",
+ "offline_hint": "Sin conexión — las respuestas provienen del tablero guardado en este dispositivo.",
+ "open_needs": "{{count}} necesidades aún abiertas.",
+ "par_all_good": "Todo el personal está contabilizado.",
+ "par_counts": "{{green}} en verde, {{warning}} por vencer, {{critical}} vencidos.",
+ "par_critical_header": "Vencidos — sin contabilizar:",
+ "par_due_row": "- {{name}}: vence en {{minutes}} min",
+ "par_header": "PAR de {{incident}}: {{count}} personas en seguimiento.",
+ "par_none": "No se está controlando la rendición de cuentas del personal en {{incident}} — nadie se ha registrado y no hay ningún temporizador de registro en marcha.",
+ "par_overdue_row": "- {{name}}: vencido hace {{minutes}} min",
+ "par_summary": "PAR: {{total}} en seguimiento, {{critical}} vencidos, {{warning}} por vencer.",
+ "par_warning_header": "Registro próximo a vencer:",
+ "placeholder": "Pregunta sobre este incidente",
+ "resource_counts": "{{units}} unidades y {{personnel}} personas en {{lanes}} carriles ({{unassigned}} sin asignar).",
+ "resources_header": "{{incident}}: {{units}} unidades y {{personnel}} personas trabajando.",
+ "rit_found": "{{lane}} está en espera con {{count}} recurso(s).",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "El puesto {{role}} no está asignado en {{incident}}.",
+ "role_unknown": "No reconozco el puesto \"{{role}}\". Pregunta por los roles ICS para ver todas las asignaciones.",
+ "roles_header": "{{incident}}: {{count}} puesto(s) ICS asignado(s).",
+ "roles_unfilled": "Puestos sin cubrir que suele requerir un incidente de tipo {{type}}: {{roles}}",
+ "send": "Enviar",
+ "source_device": "Respondido en este dispositivo",
+ "source_server": "Respondido por Resgrid",
+ "span_all_good": "El alcance de control parece razonable — todos los carriles están dentro de sus límites y ninguno supera los {{ceiling}} recursos.",
+ "span_header": "{{incident}}: {{lanes}} carriles, {{resources}} recursos asignados.",
+ "span_no_lead": "- Sin líder asignado: {{lanes}}",
+ "span_over_row": "- {{lane}} tiene {{count}} recursos (límite {{limit}}) — considera dividirlo o añadir un supervisor.",
+ "span_under_row": "- {{lane}} tiene {{count}} recursos frente a un mínimo de {{minimum}}.",
+ "staging": "Área de espera: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Transferencia de mando",
+ "missing": "¿Qué me falta?",
+ "on_scene": "¿Qué hay en escena?",
+ "open_needs": "Necesidades abiertas",
+ "open_objectives": "Objetivos abiertos",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "Últimos 30 minutos",
+ "rit": "¿Tengo un RIT?",
+ "safety": "Oficial de seguridad",
+ "span": "Alcance de control",
+ "status": "Estado del incidente",
+ "timers": "Temporizadores",
+ "unassigned": "Sin asignar",
+ "unfilled_roles": "Puestos ICS sin cubrir",
+ "wind": "Viento y clima"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Consultando el tablero…",
+ "this_incident": "este incidente",
+ "time_in_lane": "({{duration}} en el carril)",
+ "timeline_empty": "Aún no se ha registrado nada en {{incident}}.",
+ "timeline_empty_window": "No se ha registrado nada en los últimos {{minutes}} minutos en {{incident}}.",
+ "timeline_header": "{{incident}} — últimas {{count}} entradas del registro:",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — últimos {{minutes}} minutos ({{count}} entradas):",
+ "timer_due_row": "- {{name}}: VENCE AHORA",
+ "timer_no_due_row": "- {{name}}: en marcha",
+ "timer_running_row": "- {{name}}: vence en {{remaining}}",
+ "timers_header": "{{incident}}: {{count}} temporizador(es).",
+ "title": "Asistente de incidentes",
+ "unassigned_header": "Sin asignar en {{incident}} ({{count}}):",
+ "unassigned_line": "- Recursos sin asignar: {{count}}",
+ "unknown": "desconocido"
},
"incidents": {
"accountability": "Control de personal",
@@ -1304,13 +1307,16 @@
"2": "Cancelada",
"3": "Infundada",
"4": "Fundada",
- "5": "Menor"
+ "5": "Menor",
+ "6": "Transferida",
+ "7": "Falsa alarma",
+ "8": "Pendiente"
},
"unknown_priority": "Desconocida",
"unknown_user": "Usuario desconocido"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "No tienes autorización para usar la app IC. Contacta con el administrador de tu departamento.",
"errorModal": {
"confirmButton": "Aceptar",
"message": "Por favor, comprueba tu nombre de usuario y contraseña e inténtalo de nuevo.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Iniciando sesión...",
"login_button_success": "Sesión iniciada con éxito",
"or_sign_in_with_password": "o inicia sesión con contraseña",
+ "otp_body": "Tu cuenta tiene activada la autenticación en dos pasos. Introduce el código actual de tu aplicación de autenticación para terminar de iniciar sesión.",
+ "otp_invalid": "Ese código no es válido o ha caducado. Introduce el código actual de tu aplicación de autenticación.",
+ "otp_placeholder": "Código de 6 dígitos",
+ "otp_title": "Verificación en dos pasos",
+ "otp_verify": "Verificar",
"password": "Contraseña",
"password_incorrect": "La contraseña era incorrecta",
"password_placeholder": "Introduce tu contraseña",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Ya existe un parte para esa tripulación o persona ese día.",
"timereports_locked": "Este parte ya no se puede modificar.",
"timereports_scope_invalid": "Esa tripulación o persona no está activa en este despliegue.",
+ "timereports_status_transition_invalid": "Este parte ya se modificó, posiblemente desde otro dispositivo. Actualice e inténtelo de nuevo.",
"timereports_subject_covered": "Esa tripulación o persona ya tiene horas en otro parte de este día.",
"timereports_time_invalid": "Introduzca las horas como HH:mm.",
"validation": "El servidor rechazó el cambio."
@@ -1689,7 +1701,7 @@
"0": "Dieta de comidas",
"1": "Alojamiento",
"2": "Alojamiento privado",
- "3": "Ferry",
+ "3": "Ferri",
"4": "Combustible",
"5": "Reposición de material",
"6": "Otro"
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Todos los tipos de POI",
"poi_filter_placeholder": "Filtrar por tipo",
"poi_filter_type": "Tipo de POI",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filtros",
"poi_not_found": "POI no encontrado",
"poi_not_found_description": "No se pudo encontrar este POI.",
"poi_note": "Nota",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Ordenar por",
"poi_sort_display": "Nombre",
"poi_sort_placeholder": "Ordenar POIs",
"poi_sort_type": "Tipo",
diff --git a/src/translations/fr.json b/src/translations/fr.json
index e00d326..30d2034 100644
--- a/src/translations/fr.json
+++ b/src/translations/fr.json
@@ -195,7 +195,7 @@
"contact": "Contact",
"dispatched": "Envoyé",
"history": "Historique",
- "info": "Info",
+ "info": "Infos",
"protocols": "Protocoles",
"site": "Infos site",
"timeline": "Activité"
@@ -339,7 +339,7 @@
"acknowledge": "Acquitter",
"add_gif": "Ajouter un GIF",
"add_image": "Ajouter une image",
- "all_leads": "All Leads",
+ "all_leads": "Tous les responsables",
"are_typing": "{{count}} personnes sont en train d'écrire...",
"assistant": "Assistant",
"attachment_failed": "Échec de l'envoi de la pièce jointe",
@@ -353,8 +353,8 @@
"delete": "Supprimer",
"delete_failed": "Impossible de supprimer le message",
"direct_message": "Message direct",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Impossible d'ouvrir cette conversation.",
+ "dm_unavailable": "Ce contact n'a pas de compte Resgrid pour recevoir des messages.",
"edit": "Modifier",
"edit_failed": "Impossible de modifier le message",
"edit_message": "Modifier le message",
@@ -371,14 +371,14 @@
"flag_reason": "Pourquoi le signalez-vous ?",
"flag_sensitive": "Informations sensibles",
"flag_spam": "Spam",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Cet incident est clos. La conversation est conservée en l'état comme archive — plus de nouveaux messages ni de modifications, mais vous pouvez toujours signaler du contenu.",
"group_name": "Nom du groupe (facultatif)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Chat du commandement",
+ "incident_lane_channel": "Chat de la voie",
"is_typing": "{{name}} est en train d'écrire...",
"load_people_failed": "Impossible de charger les personnes",
"message_deleted": "Ce message a été supprimé",
- "message_person": "Message {{name}}",
+ "message_person": "Écrire à {{name}}",
"moderator_delete": "Supprimer (modérateur)",
"moderator_removed": "Supprimé par un modérateur",
"new_direct_message": "Nouveau message direct",
@@ -386,8 +386,8 @@
"no_gifs": "Aucun GIF trouvé",
"no_people": "Aucune personne trouvée",
"open_assistant": "Ouvrir l'assistant",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Chat du commandement",
+ "open_lane_chat": "Chat de la voie",
"original_message": "Message d'origine",
"permission_location_denied": "Accès à la localisation refusé",
"permission_photos_denied": "Accès à la photothèque refusé",
@@ -440,6 +440,8 @@
"queued_offline": "Pointage mis en file d'attente pour la restauration de la connexion",
"quick_check_in": "Pointage rapide",
"select_type": "Sélectionner le type de pointage",
+ "status_critical": "Critique",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "En retard",
"status_warning": "Avertissement",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "ex. Tactique 1",
"channel_preset_command": "Commandement",
"channel_preset_tactical": "Tactique",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Impossible de charger les canaux de discussion de cet incident. Touchez à nouveau pour réessayer.",
"close_call_failed": "Le commandement est terminé, mais l'appel n'a pas pu être clôturé.",
"close_call_queued": "La clôture de l'appel est en file d'attente et sera envoyée automatiquement.",
"close_call_skipped": "L'appel reste ouvert car le commandement n'a pas été clôturé.",
"close_channels": "Tout fermer",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Chat du commandement",
+ "command_chat_short": "Commandement",
+ "command_chat_unavailable": "Aucun canal de discussion du commandement n'a encore été créé pour cet incident.",
"command_details": "Détails du commandement",
"crew_placeholder": "Nom de la personne ou de l'équipe",
"current_commander": "CI",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Déplacer vers les ressources",
"delete_lane_release_resources": "Libérer de l'incident",
"delete_lane_resources_message": "Cette voie compte {{count}} ressources affectées. Déplacez-les vers la liste des ressources ou libérez-les de l'incident.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Détails",
+ "dispatch_chat": "Régulation",
+ "dispatch_chat_unavailable": "Aucun canal de régulation n'a encore été créé pour cet incident.",
"drag_move_hint": "Appuyez longuement sur une ressource pour la faire glisser, ou touchez-la puis touchez une voie.",
"edit_lane": "Modifier la voie",
"edit_lane_title": "Modifier {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Envoyer aussi aux commandants adjoints",
"info_save_error": "Échec de l'enregistrement des informations",
"info_save_success": "Informations enregistrées",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Chat de la voie",
+ "lane_chat_unavailable": "Aucun canal de discussion n'a encore été créé pour cette voie.",
"lane_color_label": "Couleur de la voie (optionnel — colore ses marqueurs sur la carte)",
"lane_limits_label": "Limites de la voie (optionnel)",
"lane_name_label": "Nom de la voie",
@@ -622,8 +624,8 @@
"lead_external_label": "Contact externe",
"lead_name_placeholder": "Nom du contact",
"lead_phone_placeholder": "Téléphone",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Tous les responsables",
+ "leads_chat_unavailable": "Le canal Tous les responsables n'a pas encore été créé pour cet incident.",
"limit_max_riding": "Max à bord",
"limit_max_time": "Relève après (min)",
"limit_max_units": "Max unités",
@@ -650,8 +652,8 @@
"message_commander": "Message au commandant",
"message_commander_hint": "Envoyé directement au commandant de l'incident via ses canaux de notification.",
"message_commander_hint_named": "Envoyé directement à {{name}} via ses canaux de notification.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Écrire au responsable",
+ "message_role_holder": "Envoyer un message",
"message_send_error": "Échec de l'envoi du message",
"message_send_success": "Message envoyé au commandement",
"message_subject_placeholder": "Sujet (facultatif)",
@@ -850,7 +852,7 @@
"transfer_command": "Transférer le commandement",
"transfer_command_hint": "Confiez le commandement de l’incident à un autre utilisateur. Le tableau reste partagé.",
"transfer_error": "Échec du transfert du commandement",
- "transfer_short": "Transfer",
+ "transfer_short": "Transférer",
"transfer_success": "Commandement transféré",
"transmission_log": "Journal des transmissions",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Retour",
"cancel": "Annuler",
"clear": "Effacer",
+ "clear_search": "Effacer la recherche",
"close": "Fermer",
"confirm": "Confirmer",
"confirm_location": "Confirmer la localisation",
@@ -1079,144 +1082,144 @@
"required": "Ce champ est obligatoire"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
+ "briefing_accountability": "CONTRÔLE DES EFFECTIFS",
+ "briefing_action_plan": "Plan d'action : {{text}}",
+ "briefing_address": "Lieu : {{address}}",
+ "briefing_command": "COMMANDEMENT",
+ "briefing_commander": "Commandant de l'incident : {{name}}",
+ "briefing_established": "Commandement établi : {{time}} (depuis {{duration}})",
+ "briefing_header": "BRIEFING DE TRANSFERT DE COMMANDEMENT — {{incident}}",
+ "briefing_icp": "Poste de commandement : {{location}}",
+ "briefing_important": "Informations importantes : {{text}}",
+ "briefing_lane_row": "{{lane}} (responsable : {{lead}}) — {{count}} ressources : {{names}}",
+ "briefing_needs": "BESOINS EN SUSPENS",
+ "briefing_no_lanes": "- Aucune voie établie",
+ "briefing_no_needs": "- Aucun",
+ "briefing_no_objectives": "- Aucun objectif enregistré",
+ "briefing_no_par": "Aucun contrôle des effectifs n'est en cours.",
+ "briefing_objectives": "OBJECTIFS",
+ "briefing_organization": "ORGANISATION ET RESSOURCES",
+ "briefing_rehab": "Réhabilitation : {{location}}",
"briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_staging": "Zone d'attente : {{location}}",
+ "briefing_type": "Type d'incident : {{type}}",
+ "check_action_plan": "Plan d'action ou objectifs enregistrés",
+ "check_command": "Commandement établi avec un commandant désigné",
+ "check_icp": "Emplacement du poste de commandement défini",
+ "check_par": "Contrôle des effectifs / pointage en cours",
+ "check_safety": "Officier de sécurité désigné",
+ "check_staging": "Zone d'attente désignée",
+ "checklist_confirm": "Points standard à confirmer ({{type}}) :",
+ "checklist_disclaimer": "Il s'agit de recommandations ICS générales, et non de la politique de votre service — vos procédures opérationnelles et votre jugement priment.",
+ "checklist_done": "Déjà fait sur le tableau : {{items}}.",
+ "checklist_header": "Liste de contrôle {{type}} pour {{incident}}.",
+ "checklist_outstanding": "Pas encore sur le tableau :",
+ "clear": "Effacer la conversation",
+ "command_post": "PC : {{location}}.",
+ "commander": "CI: {{name}}.",
+ "elapsed": "Commandement en cours depuis {{duration}}.",
+ "empty": "Posez une question sur cet incident — effectifs, ressources, objectifs, besoins, postes, ou ce que vous auriez pu oublier.",
+ "error": "Une erreur s'est produite lors de la réponse. Réessayez.",
+ "estimated_end": "Fin estimée : {{time}}.",
+ "external_resources": "- Ressources externes / d'entraide suivies : {{count}}",
+ "important": "Important : {{text}}",
+ "lane_empty": "Rien n'est affecté à cette voie.",
+ "lane_header": "{{lane}} ({{type}}) : {{count}} ressources.",
+ "lane_lead": "Responsable : {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Je ne trouve aucune voie nommée \"{{lane}}\". Voies sur le tableau : {{lanes}}",
+ "lane_objective": "Objectif principal : {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Jalons habituels ({{type}}) pas encore sur le tableau : {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — ouvert depuis {{age}}",
+ "needs_all_met": "Toutes les demandes ont été satisfaites.",
+ "needs_header": "{{incident}} : {{outstanding}} besoins en suspens ({{met}} satisfaits, {{cancelled}} annulés).",
+ "no_answer": "Je n'ai pas pu répondre à cette question.",
+ "no_board": "Aucun tableau de commandement n'est encore chargé pour cet incident.",
+ "no_lanes": "aucune pour le moment",
+ "no_lanes_yet": "Aucune voie n'a encore été créée sur {{incident}} : il n'y a donc pas d'étendue de contrôle à vérifier.",
+ "no_lead": "aucun responsable",
+ "no_needs": "Aucun besoin n'a été enregistré sur {{incident}}.",
+ "no_notes": "Aucune note de situation n'a été enregistrée sur {{incident}}.",
+ "no_objectives": "Aucun objectif tactique n'a encore été défini sur {{incident}}.",
+ "no_resources": "Rien n'est encore affecté à {{incident}}.",
+ "no_rit": "Je ne vois pas de voie RIT/RIC sur {{incident}}. Sur une intervention active, il vaut mieux en désigner une avant que les équipes n'entrent à l'intérieur.",
+ "no_timers": "Aucun minuteur n'est actif sur {{incident}}.",
+ "no_unassigned": "Toutes les ressources suivies sur {{incident}} sont placées dans une voie.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — {{count}} note(s) de situation :",
+ "objective_complete": "terminé",
+ "objective_in_progress": "en cours",
+ "objective_overdue": " [échéance dépassée]",
+ "objective_pending": "en attente",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
- "offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "objective_summary": "Objectifs : {{complete}} sur {{total}} terminés.",
+ "objectives_all_complete": "Tous les objectifs du tableau sont terminés.",
+ "objectives_header": "{{incident}} : {{complete}} objectifs terminés sur {{total}}. Encore ouverts :",
+ "offline_badge": "Hors ligne",
+ "offline_cannot_answer": "Je ne peux pas répondre à cela sans connexion. Hors ligne, je peux toujours vous donner le PAR, les ressources, les objectifs, les besoins, les postes ICS, les minuteurs, le journal de l'incident, un briefing et la liste de contrôle pour ce type d'incident.",
+ "offline_hint": "Pas de connexion — les réponses proviennent du tableau en cache sur cet appareil.",
+ "open_needs": "{{count}} besoins encore ouverts.",
+ "par_all_good": "Tout le personnel est comptabilisé.",
+ "par_counts": "{{green}} au vert, {{warning}} bientôt à échéance, {{critical}} en retard.",
+ "par_critical_header": "En retard — non pointés :",
+ "par_due_row": "- {{name}} : échéance dans {{minutes}} min",
+ "par_header": "PAR pour {{incident}} : {{count}} personnes suivies.",
+ "par_none": "Aucun contrôle des effectifs n'est en cours sur {{incident}} — personne n'a pointé et aucun minuteur de pointage n'est actif.",
+ "par_overdue_row": "- {{name}} : en retard de {{minutes}} min",
+ "par_summary": "PAR : {{total}} suivis, {{critical}} en retard, {{warning}} bientôt à échéance.",
+ "par_warning_header": "Pointage bientôt dû :",
+ "placeholder": "Posez une question sur cet incident",
+ "resource_counts": "{{units}} unités et {{personnel}} personnes réparties sur {{lanes}} voies ({{unassigned}} non affectées).",
+ "resources_header": "{{incident}} : {{units}} unités et {{personnel}} personnes engagées.",
+ "rit_found": "{{lane}} est en attente avec {{count}} ressource(s).",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "Le poste {{role}} n'est pas pourvu sur {{incident}}.",
+ "role_unknown": "Je ne reconnais pas le poste \"{{role}}\". Demandez les rôles ICS pour voir toutes les affectations.",
+ "roles_header": "{{incident}} : {{count}} poste(s) ICS pourvu(s).",
+ "roles_unfilled": "Postes non pourvus généralement nécessaires pour ce type d'incident ({{type}}) : {{roles}}",
+ "send": "Envoyer",
+ "source_device": "Réponse générée sur cet appareil",
+ "source_server": "Réponse fournie par Resgrid",
+ "span_all_good": "L'étendue de contrôle semble raisonnable — chaque voie respecte ses limites et aucune ne dépasse {{ceiling}} ressources.",
+ "span_header": "{{incident}} : {{lanes}} voies, {{resources}} ressources affectées.",
+ "span_no_lead": "- Aucun responsable désigné : {{lanes}}",
+ "span_over_row": "- {{lane}} compte {{count}} ressources (limite {{limit}}) — envisagez de la scinder ou d'ajouter un superviseur.",
+ "span_under_row": "- {{lane}} compte {{count}} ressources pour un minimum de {{minimum}}.",
+ "staging": "Zone d'attente : {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Transfert de commandement",
+ "missing": "Qu'est-ce que j'oublie ?",
+ "on_scene": "Moyens sur place",
+ "open_needs": "Besoins ouverts",
+ "open_objectives": "Objectifs ouverts",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "30 dernières minutes",
+ "rit": "Ai-je une équipe RIT ?",
+ "safety": "Officier de sécurité",
+ "span": "Étendue de contrôle",
+ "status": "État de l'incident",
+ "timers": "Minuteurs",
+ "unassigned": "Non affectés",
+ "unfilled_roles": "Postes ICS non pourvus",
+ "wind": "Vent et météo"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Consultation du tableau…",
+ "this_incident": "cet incident",
+ "time_in_lane": "({{duration}} dans la voie)",
+ "timeline_empty": "Rien n'a encore été consigné sur {{incident}}.",
+ "timeline_empty_window": "Rien n'a été consigné sur {{incident}} au cours des {{minutes}} dernières minutes.",
+ "timeline_header": "{{incident}} — {{count}} dernières entrées du journal :",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — {{minutes}} dernières minutes ({{count}} entrées) :",
+ "timer_due_row": "- {{name}} : ÉCHÉANCE ATTEINTE",
+ "timer_no_due_row": "- {{name}} : en cours",
+ "timer_running_row": "- {{name}} : échéance dans {{remaining}}",
+ "timers_header": "{{incident}} : {{count}} minuteur(s).",
+ "title": "Assistant d'incident",
+ "unassigned_header": "Non affectés sur {{incident}} ({{count}}) :",
+ "unassigned_line": "- Réserve non affectée : {{count}}",
+ "unknown": "inconnu"
},
"incidents": {
"accountability": "Contrôle du personnel",
@@ -1304,13 +1307,16 @@
"2": "Annulé",
"3": "Non fondé",
"4": "Fondé",
- "5": "Mineur"
+ "5": "Mineur",
+ "6": "Transféré",
+ "7": "Fausse alarme",
+ "8": "En attente"
},
"unknown_priority": "Inconnue",
"unknown_user": "Utilisateur inconnu"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Vous n'êtes pas autorisé à utiliser l'application IC. Contactez l'administrateur de votre service.",
"errorModal": {
"confirmButton": "OK",
"message": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe et réessayer.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Connexion en cours...",
"login_button_success": "Connexion réussie",
"or_sign_in_with_password": "ou se connecter avec un mot de passe",
+ "otp_body": "L'authentification à deux facteurs est activée sur votre compte. Saisissez le code actuel de votre application d'authentification pour terminer la connexion.",
+ "otp_invalid": "Ce code est invalide ou a expiré. Saisissez le code actuel de votre application d'authentification.",
+ "otp_placeholder": "Code à 6 chiffres",
+ "otp_title": "Vérification en deux étapes",
+ "otp_verify": "Vérifier",
"password": "Mot de passe",
"password_incorrect": "Le mot de passe était incorrect",
"password_placeholder": "Saisir votre mot de passe",
@@ -1538,7 +1549,7 @@
"created": "Créé",
"delete": "Supprimer",
"edit": "Modifier",
- "tags": "Tags",
+ "tags": "Étiquettes",
"title": "Détails de la note",
"updated": "Mis à jour"
},
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Un rapport existe déjà pour cet équipage ou cette personne ce jour-là.",
"timereports_locked": "Ce rapport ne peut plus être modifié.",
"timereports_scope_invalid": "Cet équipage ou cette personne n'est pas actif sur ce déploiement.",
+ "timereports_status_transition_invalid": "Ce rapport a déjà été modifié, peut-être depuis un autre appareil. Actualisez et réessayez.",
"timereports_subject_covered": "Cet équipage ou cette personne a déjà du temps sur un autre rapport ce jour-là.",
"timereports_time_invalid": "Saisissez les heures au format HH:mm.",
"validation": "Le serveur a refusé la modification."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Tous les types de POI",
"poi_filter_placeholder": "Filtrer par type",
"poi_filter_type": "Type de POI",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filtres",
"poi_not_found": "POI introuvable",
"poi_not_found_description": "Ce POI est introuvable.",
"poi_note": "Note",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Trier par",
"poi_sort_display": "Nom",
"poi_sort_placeholder": "Trier les POI",
"poi_sort_type": "Type",
@@ -2252,7 +2264,7 @@
"share": "Partager",
"spanish": "Espagnol",
"status_page": "État du système",
- "support": "Support",
+ "support": "Assistance",
"support_us": "Soutenez-nous",
"swedish": "Suédois",
"sync_section": "Synchronisation et file hors ligne",
diff --git a/src/translations/it.json b/src/translations/it.json
index 76699e1..4803d96 100644
--- a/src/translations/it.json
+++ b/src/translations/it.json
@@ -339,7 +339,7 @@
"acknowledge": "Conferma",
"add_gif": "Aggiungi GIF",
"add_image": "Aggiungi immagine",
- "all_leads": "All Leads",
+ "all_leads": "Tutti i responsabili",
"are_typing": "{{count}} persone stanno scrivendo...",
"assistant": "Assistente",
"attachment_failed": "Caricamento allegato non riuscito",
@@ -353,8 +353,8 @@
"delete": "Elimina",
"delete_failed": "Impossibile eliminare il messaggio",
"direct_message": "Messaggio diretto",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Impossibile aprire la conversazione.",
+ "dm_unavailable": "Questo contatto non ha un account Resgrid a cui scrivere.",
"edit": "Modifica",
"edit_failed": "Impossibile modificare il messaggio",
"edit_message": "Modifica messaggio",
@@ -371,14 +371,14 @@
"flag_reason": "Perché lo stai segnalando?",
"flag_sensitive": "Informazioni sensibili",
"flag_spam": "Spam",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Questo incidente è chiuso. La conversazione è conservata come registrazione storica — niente nuovi messaggi o modifiche, ma puoi ancora segnalare i contenuti.",
"group_name": "Nome del gruppo (facoltativo)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Chat di comando",
+ "incident_lane_channel": "Chat della corsia",
"is_typing": "{{name}} sta scrivendo...",
"load_people_failed": "Impossibile caricare le persone",
"message_deleted": "Questo messaggio è stato eliminato",
- "message_person": "Message {{name}}",
+ "message_person": "Scrivi a {{name}}",
"moderator_delete": "Rimuovi (moderatore)",
"moderator_removed": "Rimosso dal moderatore",
"new_direct_message": "Nuovo messaggio diretto",
@@ -386,8 +386,8 @@
"no_gifs": "Nessuna GIF trovata",
"no_people": "Nessuna persona trovata",
"open_assistant": "Apri assistente",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Chat di comando",
+ "open_lane_chat": "Chat della corsia",
"original_message": "Messaggio originale",
"permission_location_denied": "Autorizzazione posizione negata",
"permission_photos_denied": "Autorizzazione libreria foto negata",
@@ -440,6 +440,8 @@
"queued_offline": "Check-in in coda per quando la connessione sarà ripristinata",
"quick_check_in": "Check-In rapido",
"select_type": "Seleziona tipo di check-in",
+ "status_critical": "Critico",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Scaduto",
"status_warning": "Avviso",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "es. Tattico 1",
"channel_preset_command": "Comando",
"channel_preset_tactical": "Tattico",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Impossibile caricare i canali chat di questo incidente. Tocca di nuovo per riprovare.",
"close_call_failed": "Il comando è stato terminato, ma non è stato possibile chiudere la chiamata.",
"close_call_queued": "La chiusura della chiamata è in coda e verrà inviata automaticamente.",
"close_call_skipped": "La chiamata è rimasta aperta perché il comando non è stato chiuso.",
"close_channels": "Chiudi tutto",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Chat di comando",
+ "command_chat_short": "Comando",
+ "command_chat_unavailable": "Non è ancora stato creato un canale chat di comando per questo incidente.",
"command_details": "Dettagli del comando",
"crew_placeholder": "Nome della persona o della squadra",
"current_commander": "CI",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Sposta in Risorse",
"delete_lane_release_resources": "Rilascia dall'incidente",
"delete_lane_resources_message": "Questa corsia ha {{count}} risorse assegnate. Spostale nell'elenco delle risorse o rilasciale dall'incidente.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Dettagli",
+ "dispatch_chat": "Centrale",
+ "dispatch_chat_unavailable": "Non è ancora stato creato un canale della centrale per questo incidente.",
"drag_move_hint": "Tieni premuta una risorsa per trascinarla, oppure toccala e poi tocca una corsia.",
"edit_lane": "Modifica corsia",
"edit_lane_title": "Modifica {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Invia anche ai vice comandanti",
"info_save_error": "Salvataggio delle informazioni non riuscito",
"info_save_success": "Informazioni salvate",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Chat della corsia",
+ "lane_chat_unavailable": "Non è ancora stato creato un canale chat per questa corsia.",
"lane_color_label": "Colore corsia (opzionale — colora i suoi marker sulla mappa)",
"lane_limits_label": "Limiti corsia (opzionale)",
"lane_name_label": "Nome corsia",
@@ -622,8 +624,8 @@
"lead_external_label": "Contatto esterno",
"lead_name_placeholder": "Nome del contatto",
"lead_phone_placeholder": "Telefono",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Tutti i responsabili",
+ "leads_chat_unavailable": "Il canale Tutti i responsabili non è ancora stato creato per questo incidente.",
"limit_max_riding": "Max a bordo",
"limit_max_time": "Ruota dopo (min)",
"limit_max_units": "Max unità",
@@ -650,8 +652,8 @@
"message_commander": "Messaggio al comandante",
"message_commander_hint": "Inviato direttamente al comandante dell'incidente tramite i suoi canali di notifica.",
"message_commander_hint_named": "Inviato direttamente a {{name}} tramite i suoi canali di notifica.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Scrivi al responsabile",
+ "message_role_holder": "Invia messaggio",
"message_send_error": "Invio del messaggio non riuscito",
"message_send_success": "Messaggio inviato al comando",
"message_subject_placeholder": "Oggetto (facoltativo)",
@@ -850,7 +852,7 @@
"transfer_command": "Trasferisci comando",
"transfer_command_hint": "Passa il comando dell’incidente a un altro utente. La board resta condivisa.",
"transfer_error": "Trasferimento del comando non riuscito",
- "transfer_short": "Transfer",
+ "transfer_short": "Trasferisci",
"transfer_success": "Comando trasferito",
"transmission_log": "Registro trasmissioni",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Indietro",
"cancel": "Annulla",
"clear": "Cancella",
+ "clear_search": "Cancella ricerca",
"close": "Chiudi",
"confirm": "Conferma",
"confirm_location": "Conferma posizione",
@@ -1079,144 +1082,144 @@
"required": "Questo campo è obbligatorio"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "CONTROLLO DEL PERSONALE",
+ "briefing_action_plan": "Piano d'azione: {{text}}",
+ "briefing_address": "Luogo: {{address}}",
+ "briefing_command": "COMANDO",
+ "briefing_commander": "Comandante dell'incidente: {{name}}",
+ "briefing_established": "Comando istituito: {{time}} (attivo da {{duration}})",
+ "briefing_header": "BRIEFING DI TRASFERIMENTO DEL COMANDO — {{incident}}",
+ "briefing_icp": "Posto di comando: {{location}}",
+ "briefing_important": "Informazioni importanti: {{text}}",
+ "briefing_lane_row": "{{lane}} (resp.: {{lead}}) — {{count}} risorse: {{names}}",
+ "briefing_needs": "NECESSITÀ IN SOSPESO",
+ "briefing_no_lanes": "- Nessuna corsia istituita",
+ "briefing_no_needs": "- Nessuna",
+ "briefing_no_objectives": "- Nessun obiettivo registrato",
+ "briefing_no_par": "Nessun controllo del personale in corso.",
+ "briefing_objectives": "OBIETTIVI",
+ "briefing_organization": "ORGANIZZAZIONE E RISORSE",
+ "briefing_rehab": "Riabilitazione: {{location}}",
+ "briefing_situation": "SITUAZIONE",
+ "briefing_staging": "Area di attesa: {{location}}",
+ "briefing_type": "Tipo di incidente: {{type}}",
+ "check_action_plan": "Piano d'azione o obiettivi registrati",
+ "check_command": "Comando istituito con un comandante designato",
+ "check_icp": "Posizione del posto di comando impostata",
+ "check_par": "Controllo del personale / check-in attivo",
+ "check_safety": "Responsabile sicurezza assegnato",
+ "check_staging": "Area di attesa designata",
+ "checklist_confirm": "Elementi standard da confermare per {{type}}:",
+ "checklist_disclaimer": "Queste sono indicazioni ICS generali, non le procedure del tuo dipartimento — le tue procedure operative e il tuo giudizio hanno la precedenza.",
+ "checklist_done": "Già fatto sulla plancia: {{items}}.",
+ "checklist_header": "Checklist {{type}} per {{incident}}.",
+ "checklist_outstanding": "Non ancora presenti sulla plancia:",
+ "clear": "Cancella conversazione",
+ "command_post": "PC: {{location}}.",
+ "commander": "CI: {{name}}.",
+ "elapsed": "Comando attivo da {{duration}}.",
+ "empty": "Chiedi informazioni su questo incidente — controllo del personale, risorse, obiettivi, necessità, ruoli o cosa ti potrebbe sfuggire.",
+ "error": "Si è verificato un errore durante la risposta. Riprova.",
+ "estimated_end": "Fine stimata: {{time}}.",
+ "external_resources": "- Risorse esterne / di mutuo soccorso monitorate: {{count}}",
+ "important": "Importante: {{text}}",
+ "lane_empty": "Nessuna risorsa assegnata a questa corsia.",
+ "lane_header": "{{lane}} ({{type}}): {{count}} risorse.",
+ "lane_lead": "Responsabile: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Non trovo una corsia chiamata \"{{lane}}\". Corsie sulla plancia: {{lanes}}",
+ "lane_objective": "Obiettivo primario: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Traguardi comuni per {{type}} non ancora sulla plancia: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — aperta da {{age}}",
+ "needs_all_met": "Tutto ciò che è stato richiesto è stato coperto.",
+ "needs_header": "{{incident}}: {{outstanding}} necessità in sospeso ({{met}} soddisfatte, {{cancelled}} annullate).",
+ "no_answer": "Non sono riuscito a rispondere a questa domanda.",
+ "no_board": "Nessuna plancia di comando ancora caricata per questo incidente.",
+ "no_lanes": "ancora nessuna",
+ "no_lanes_yet": "Non sono ancora state create corsie su {{incident}}, quindi non c'è un'ampiezza di controllo da verificare.",
+ "no_lead": "nessun responsabile",
+ "no_needs": "Nessuna necessità registrata su {{incident}}.",
+ "no_notes": "Nessuna nota di stato registrata su {{incident}}.",
+ "no_objectives": "Nessun obiettivo tattico ancora definito su {{incident}}.",
+ "no_resources": "Nessuna risorsa ancora assegnata a {{incident}}.",
+ "no_rit": "Non vedo una corsia RIT/RIC su {{incident}}. In un intervento attivo conviene assegnarla prima che le squadre entrino nella struttura.",
+ "no_timers": "Nessun timer attivo su {{incident}}.",
+ "no_unassigned": "Tutto ciò che è monitorato su {{incident}} è assegnato a una corsia.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — {{count}} nota/e di stato:",
+ "objective_complete": "completato",
+ "objective_in_progress": "in corso",
+ "objective_overdue": " [oltre la scadenza]",
+ "objective_pending": "in attesa",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
+ "objective_summary": "Obiettivi: {{complete}} di {{total}} completati.",
+ "objectives_all_complete": "Tutti gli obiettivi sulla plancia sono completati.",
+ "objectives_header": "{{incident}}: {{complete}} di {{total}} obiettivi completati. Ancora aperti:",
"offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "offline_cannot_answer": "Non posso rispondere senza connessione. Offline posso comunque fornirti PAR, risorse, obiettivi, necessità, ruoli ICS, timer, il registro dell'incidente, un briefing e la checklist per questo tipo di incidente.",
+ "offline_hint": "Nessuna connessione — le risposte provengono dalla plancia salvata su questo dispositivo.",
+ "open_needs": "{{count}} necessità ancora aperte.",
+ "par_all_good": "Tutto il personale risulta presente.",
+ "par_counts": "{{green}} regolari, {{warning}} in scadenza, {{critical}} scaduti.",
+ "par_critical_header": "Scaduti — presenza non verificata:",
+ "par_due_row": "- {{name}}: scade tra {{minutes}} min",
+ "par_header": "PAR per {{incident}}: {{count}} persone monitorate.",
+ "par_none": "Nessun controllo del personale in corso su {{incident}} — nessuno ha effettuato il check-in e nessun timer di check-in è attivo.",
+ "par_overdue_row": "- {{name}}: scaduto da {{minutes}} min",
+ "par_summary": "PAR: {{total}} monitorati, {{critical}} scaduti, {{warning}} in scadenza.",
+ "par_warning_header": "Check-in in scadenza:",
+ "placeholder": "Chiedi informazioni su questo incidente",
+ "resource_counts": "{{units}} unità e {{personnel}} persone in {{lanes}} corsie ({{unassigned}} non assegnati).",
+ "resources_header": "{{incident}}: {{units}} unità e {{personnel}} persone operative.",
+ "rit_found": "{{lane}} è pronta con {{count}} risorsa/e.",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "Nessuno assegnato al ruolo {{role}} su {{incident}}.",
+ "role_unknown": "Non riconosco il ruolo \"{{role}}\". Chiedi i ruoli ICS per vedere tutte le assegnazioni.",
+ "roles_header": "{{incident}}: {{count}} ruolo/i ICS assegnato/i.",
+ "roles_unfilled": "Ruoli scoperti solitamente necessari per {{type}}: {{roles}}",
+ "send": "Invia",
+ "source_device": "Risposta elaborata su questo dispositivo",
+ "source_server": "Risposta di Resgrid",
+ "span_all_good": "L'ampiezza di controllo sembra adeguata — ogni corsia è entro i propri limiti e nessuna supera le {{ceiling}} risorse.",
+ "span_header": "{{incident}}: {{lanes}} corsie, {{resources}} risorse assegnate.",
+ "span_no_lead": "- Nessun responsabile assegnato: {{lanes}}",
+ "span_over_row": "- {{lane}} ha {{count}} risorse (limite {{limit}}) — valuta di suddividerla o di aggiungere un supervisore.",
+ "span_under_row": "- {{lane}} ha {{count}} risorse rispetto a un minimo di {{minimum}}.",
+ "staging": "Area di attesa: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Trasferimento del comando",
+ "missing": "Cosa mi sfugge?",
+ "on_scene": "Cosa c'è sul posto?",
+ "open_needs": "Necessità aperte",
+ "open_objectives": "Obiettivi aperti",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "Ultimi 30 minuti",
+ "rit": "Ho una RIT?",
+ "safety": "Responsabile sicurezza",
+ "span": "Ampiezza di controllo",
+ "status": "Stato dell'incidente",
+ "timers": "Timer",
+ "unassigned": "Non assegnati",
+ "unfilled_roles": "Ruoli ICS scoperti",
+ "wind": "Vento e meteo"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Controllo la plancia…",
+ "this_incident": "questo incidente",
+ "time_in_lane": "({{duration}} nella corsia)",
+ "timeline_empty": "Non è ancora stato registrato nulla su {{incident}}.",
+ "timeline_empty_window": "Nulla registrato negli ultimi {{minutes}} minuti su {{incident}}.",
+ "timeline_header": "{{incident}} — ultime {{count}} voci del registro:",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — ultimi {{minutes}} minuti ({{count}} voci):",
+ "timer_due_row": "- {{name}}: SCADE ORA",
+ "timer_no_due_row": "- {{name}}: attivo",
+ "timer_running_row": "- {{name}}: scade tra {{remaining}}",
+ "timers_header": "{{incident}}: {{count}} timer.",
+ "title": "Assistente incidente",
+ "unassigned_header": "Non assegnati su {{incident}} ({{count}}):",
+ "unassigned_line": "- Risorse non assegnate: {{count}}",
+ "unknown": "sconosciuto"
},
"incidents": {
"accountability": "Controllo del personale",
@@ -1304,13 +1307,16 @@
"2": "Annullata",
"3": "Infondata",
"4": "Fondata",
- "5": "Minore"
+ "5": "Minore",
+ "6": "Trasferito",
+ "7": "Falso allarme",
+ "8": "In attesa"
},
"unknown_priority": "Sconosciuta",
"unknown_user": "Utente sconosciuto"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Non sei autorizzato a usare l'app IC. Contatta l'amministratore del tuo dipartimento.",
"errorModal": {
"confirmButton": "OK",
"message": "Verificare nome utente e password e riprovare.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Accesso in corso...",
"login_button_success": "Accesso effettuato con successo",
"or_sign_in_with_password": "o accedi con password",
+ "otp_body": "Il tuo account ha l'autenticazione a due fattori attiva. Inserisci il codice attuale dalla tua app di autenticazione per completare l'accesso.",
+ "otp_invalid": "Il codice non è valido o è scaduto. Inserisci il codice attuale dalla tua app di autenticazione.",
+ "otp_placeholder": "Codice a 6 cifre",
+ "otp_title": "Verifica in due passaggi",
+ "otp_verify": "Verifica",
"password": "Password",
"password_incorrect": "La password non è corretta",
"password_placeholder": "Inserisci la tua password",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Esiste già un rapporto per questo equipaggio o persona in quel giorno.",
"timereports_locked": "Questo rapporto non può più essere modificato.",
"timereports_scope_invalid": "Questo equipaggio o questa persona non è attivo in questo dispiegamento.",
+ "timereports_status_transition_invalid": "Questo rapporto è già stato modificato, forse da un altro dispositivo. Aggiorna e riprova.",
"timereports_subject_covered": "Questo equipaggio o questa persona ha già ore su un altro rapporto di questo giorno.",
"timereports_time_invalid": "Inserisci gli orari come HH:mm.",
"validation": "Il server ha rifiutato la modifica."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Tutti i tipi di POI",
"poi_filter_placeholder": "Filtra per tipo",
"poi_filter_type": "Tipo di POI",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filtri",
"poi_not_found": "POI non trovato",
"poi_not_found_description": "Impossibile trovare questo POI.",
"poi_note": "Nota",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Ordina per",
"poi_sort_display": "Nome",
"poi_sort_placeholder": "Ordina POI",
"poi_sort_type": "Tipo",
diff --git a/src/translations/pl.json b/src/translations/pl.json
index 06e4e71..dc589b1 100644
--- a/src/translations/pl.json
+++ b/src/translations/pl.json
@@ -195,7 +195,7 @@
"contact": "Kontakt",
"dispatched": "Wysłane",
"history": "Historia",
- "info": "Info",
+ "info": "Informacje",
"protocols": "Protokoły",
"site": "Info o obiekcie",
"timeline": "Aktywność"
@@ -339,7 +339,7 @@
"acknowledge": "Potwierdź",
"add_gif": "Dodaj GIF",
"add_image": "Dodaj zdjęcie",
- "all_leads": "All Leads",
+ "all_leads": "Wszyscy dowódcy",
"are_typing": "{{count}} osób pisze...",
"assistant": "Asystent",
"attachment_failed": "Przesyłanie załącznika nie powiodło się",
@@ -353,8 +353,8 @@
"delete": "Usuń",
"delete_failed": "Nie można usunąć wiadomości",
"direct_message": "Wiadomość bezpośrednia",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Nie udało się otworzyć tej konwersacji.",
+ "dm_unavailable": "Ten kontakt nie ma konta Resgrid, więc nie można do niego napisać.",
"edit": "Edytuj",
"edit_failed": "Nie można edytować wiadomości",
"edit_message": "Edytuj wiadomość",
@@ -371,14 +371,14 @@
"flag_reason": "Dlaczego to zgłaszasz?",
"flag_sensitive": "Informacje poufne",
"flag_spam": "Spam",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "To zdarzenie zostało zamknięte. Konwersacja jest zachowana jako zapis z danej chwili — nie można dodawać nowych wiadomości ani ich edytować, ale nadal możesz zgłaszać treści.",
"group_name": "Nazwa grupy (opcjonalnie)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Czat dowodzenia",
+ "incident_lane_channel": "Czat odcinka",
"is_typing": "{{name}} pisze...",
"load_people_failed": "Nie można załadować osób",
"message_deleted": "Ta wiadomość została usunięta",
- "message_person": "Message {{name}}",
+ "message_person": "Wiadomość do {{name}}",
"moderator_delete": "Usuń (moderator)",
"moderator_removed": "Usunięte przez moderatora",
"new_direct_message": "Nowa wiadomość bezpośrednia",
@@ -386,8 +386,8 @@
"no_gifs": "Nie znaleziono plików GIF",
"no_people": "Nie znaleziono osób",
"open_assistant": "Otwórz asystenta",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Czat dowodzenia",
+ "open_lane_chat": "Czat odcinka",
"original_message": "Oryginalna wiadomość",
"permission_location_denied": "Odmówiono dostępu do lokalizacji",
"permission_photos_denied": "Odmówiono dostępu do biblioteki zdjęć",
@@ -440,6 +440,8 @@
"queued_offline": "Meldunek zakolejkowany do momentu przywrócenia połączenia",
"quick_check_in": "Szybki meldunek",
"select_type": "Wybierz typ meldunku",
+ "status_critical": "Krytyczny",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Zaległy",
"status_warning": "Ostrzeżenie",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "np. Taktyczny 1",
"channel_preset_command": "Dowodzenie",
"channel_preset_tactical": "Taktyczny",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Nie udało się załadować kanałów czatu tego zdarzenia. Dotknij ponownie, aby ponowić próbę.",
"close_call_failed": "Dowodzenie zostało zakończone, ale nie udało się zamknąć zgłoszenia.",
"close_call_queued": "Zamknięcie zgłoszenia zostało dodane do kolejki i zostanie wysłane automatycznie.",
"close_call_skipped": "Zgłoszenie pozostało otwarte, ponieważ dowodzenie nie zostało zakończone.",
"close_channels": "Zamknij wszystkie",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Czat dowodzenia",
+ "command_chat_short": "Dowodzenie",
+ "command_chat_unavailable": "Dla tego zdarzenia nie utworzono jeszcze kanału czatu dowodzenia.",
"command_details": "Szczegóły dowodzenia",
"crew_placeholder": "Nazwa osoby lub zastępu",
"current_commander": "KD",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Przenieś do zasobów",
"delete_lane_release_resources": "Zwolnij z incydentu",
"delete_lane_resources_message": "Ta sekcja ma {{count}} przydzielonych zasobów. Przenieś je z powrotem do listy zasobów lub zwolnij z incydentu.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Szczegóły",
+ "dispatch_chat": "Dyspozytornia",
+ "dispatch_chat_unavailable": "Dla tego zdarzenia nie utworzono jeszcze kanału dyspozytorni.",
"drag_move_hint": "Naciśnij i przytrzymaj zasób, aby go przeciągnąć, albo dotknij go, a następnie dotknij pasa.",
"edit_lane": "Edytuj odcinek",
"edit_lane_title": "Edytuj {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Wyślij również do zastępców dowodzącego",
"info_save_error": "Nie udało się zapisać informacji",
"info_save_success": "Zapisano informacje",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Czat odcinka",
+ "lane_chat_unavailable": "Dla tego odcinka nie utworzono jeszcze kanału czatu.",
"lane_color_label": "Kolor pasa (opcjonalnie — koloruje jego znaczniki na mapie)",
"lane_limits_label": "Limity pasa (opcjonalne)",
"lane_name_label": "Nazwa odcinka",
@@ -622,8 +624,8 @@
"lead_external_label": "Kontakt zewnętrzny",
"lead_name_placeholder": "Imię i nazwisko kontaktu",
"lead_phone_placeholder": "Telefon",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Wszyscy dowódcy",
+ "leads_chat_unavailable": "Kanał „Wszyscy dowódcy” nie został jeszcze utworzony dla tego zdarzenia.",
"limit_max_riding": "Maks. załoga",
"limit_max_time": "Zmiana po (min)",
"limit_max_units": "Maks. jednostek",
@@ -650,8 +652,8 @@
"message_commander": "Wiadomość do dowodzącego",
"message_commander_hint": "Trafia bezpośrednio do dowodzącego akcją przez jego kanały powiadomień.",
"message_commander_hint_named": "Trafia bezpośrednio do {{name}} przez jego kanały powiadomień.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Wiadomość do dowódcy",
+ "message_role_holder": "Napisz wiadomość",
"message_send_error": "Nie udało się wysłać wiadomości",
"message_send_success": "Wiadomość wysłana do dowodzenia",
"message_subject_placeholder": "Temat (opcjonalnie)",
@@ -832,7 +834,7 @@
"start_success": "Rozpoczęto dowodzenie dla tego zgłoszenia",
"start_time_label": "Czas rozpoczęcia",
"start_time_now": "Teraz",
- "start_timer": "Start",
+ "start_timer": "Uruchom",
"starting": "Rozpoczynanie...",
"structure_section": "Struktura dowodzenia",
"template_blank": "Pusta tablica",
@@ -850,7 +852,7 @@
"transfer_command": "Przekaż dowodzenie",
"transfer_command_hint": "Przekaż dowodzenie zdarzeniem innemu użytkownikowi. Tablica pozostaje wspólna.",
"transfer_error": "Nie udało się przekazać dowodzenia",
- "transfer_short": "Transfer",
+ "transfer_short": "Przekaż",
"transfer_success": "Dowodzenie przekazane",
"transmission_log": "Dziennik transmisji",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Wstecz",
"cancel": "Anuluj",
"clear": "Wyczyść",
+ "clear_search": "Wyczyść wyszukiwanie",
"close": "Zamknij",
"confirm": "Potwierdź",
"confirm_location": "Potwierdź lokalizację",
@@ -976,7 +979,7 @@
"mastodon": "Mastodon",
"mobile": "Komórka",
"name": "Nazwa",
- "noteAlert": "Alert",
+ "noteAlert": "Uwaga",
"noteType": "Typ",
"notes": "Notatki",
"notesTab": "Notatki",
@@ -1079,144 +1082,144 @@
"required": "To pole jest wymagane"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "KONTROLA STANU OSOBOWEGO",
+ "briefing_action_plan": "Plan działań: {{text}}",
+ "briefing_address": "Lokalizacja: {{address}}",
+ "briefing_command": "DOWODZENIE",
+ "briefing_commander": "Kierujący działaniem ratowniczym: {{name}}",
+ "briefing_established": "Dowodzenie ustanowione: {{time}} (trwa {{duration}})",
+ "briefing_header": "ODPRAWA PRZY PRZEKAZANIU DOWODZENIA — {{incident}}",
+ "briefing_icp": "Punkt dowodzenia: {{location}}",
+ "briefing_important": "Ważne informacje: {{text}}",
+ "briefing_lane_row": "{{lane}} (dowódca: {{lead}}) — zasoby ({{count}}): {{names}}",
+ "briefing_needs": "OTWARTE POTRZEBY",
+ "briefing_no_lanes": "- Nie utworzono odcinków",
+ "briefing_no_needs": "- Brak",
+ "briefing_no_objectives": "- Nie zapisano celów",
+ "briefing_no_par": "Kontrola stanu osobowego nie jest prowadzona.",
+ "briefing_objectives": "CELE",
+ "briefing_organization": "ORGANIZACJA I ZASOBY",
+ "briefing_rehab": "Strefa odpoczynku: {{location}}",
+ "briefing_situation": "SYTUACJA",
+ "briefing_staging": "Punkt koncentracji: {{location}}",
+ "briefing_type": "Rodzaj zdarzenia: {{type}}",
+ "check_action_plan": "Zapisano plan działań lub cele",
+ "check_command": "Ustanowiono dowodzenie z wyznaczonym KDR",
+ "check_icp": "Ustalono lokalizację punktu dowodzenia",
+ "check_par": "Prowadzona kontrola stanu / meldunki",
+ "check_safety": "Wyznaczono oficera bezpieczeństwa",
+ "check_staging": "Wyznaczono punkt koncentracji",
+ "checklist_confirm": "Standardowe punkty ({{type}}) do potwierdzenia:",
+ "checklist_disclaimer": "To ogólne wytyczne ICS, a nie zasady Twojej jednostki — pierwszeństwo mają Twoje procedury i Twoja ocena sytuacji.",
+ "checklist_done": "Już odnotowane na tablicy: {{items}}.",
+ "checklist_header": "Lista kontrolna ({{type}}) — {{incident}}.",
+ "checklist_outstanding": "Jeszcze niewidoczne na tablicy:",
+ "clear": "Wyczyść rozmowę",
+ "command_post": "Punkt dowodzenia: {{location}}.",
+ "commander": "KDR: {{name}}.",
+ "elapsed": "Dowodzenie trwa {{duration}}.",
+ "empty": "Zapytaj o to zdarzenie — kontrolę stanu, zasoby, cele, potrzeby, funkcje ICS lub o to, czego może brakować.",
+ "error": "Coś poszło nie tak podczas odpowiadania. Spróbuj ponownie.",
+ "estimated_end": "Szacowane zakończenie: {{time}}.",
+ "external_resources": "- Śledzone zasoby zewnętrzne / pomocy wzajemnej: {{count}}",
+ "important": "Ważne: {{text}}",
+ "lane_empty": "Do tego odcinka nic nie przydzielono.",
+ "lane_header": "{{lane}} ({{type}}) — zasoby: {{count}}.",
+ "lane_lead": "Dowódca: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Nie widzę odcinka o nazwie „{{lane}}”. Odcinki na tablicy: {{lanes}}",
+ "lane_objective": "Cel główny: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Typowe punkty kontrolne ({{type}}), których nie ma jeszcze na tablicy: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — otwarta od {{age}}",
+ "needs_all_met": "Wszystkie zgłoszone potrzeby zostały zaspokojone.",
+ "needs_header": "{{incident}} — otwarte potrzeby: {{outstanding}} (zaspokojone: {{met}}, zamknięte: {{cancelled}}).",
+ "no_answer": "Nie udało mi się na to odpowiedzieć.",
+ "no_board": "Dla tego zdarzenia nie załadowano jeszcze tablicy dowodzenia.",
+ "no_lanes": "jeszcze brak",
+ "no_lanes_yet": "{{incident}}: nie utworzono jeszcze odcinków, więc nie ma czego sprawdzać pod kątem rozpiętości kierowania.",
+ "no_lead": "brak dowódcy",
+ "no_needs": "{{incident}}: nie zarejestrowano żadnych potrzeb.",
+ "no_notes": "{{incident}}: nie zarejestrowano żadnych notatek o stanie.",
+ "no_objectives": "{{incident}}: nie wyznaczono jeszcze celów taktycznych.",
+ "no_resources": "{{incident}}: nic jeszcze nie przydzielono.",
+ "no_rit": "{{incident}}: nie widzę odcinka RIT/RIC (rota asekuracyjna). Przy trwającej akcji warto go wyznaczyć, zanim roty wejdą do wnętrza.",
+ "no_timers": "{{incident}}: brak uruchomionych liczników.",
+ "no_unassigned": "{{incident}}: wszystkie śledzone zasoby są przydzielone do odcinków.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — notatki o stanie ({{count}}):",
+ "objective_complete": "zakończony",
+ "objective_in_progress": "w toku",
+ "objective_overdue": " [po terminie]",
+ "objective_pending": "oczekujący",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
+ "objective_summary": "Cele: zakończone {{complete}} z {{total}}.",
+ "objectives_all_complete": "Wszystkie cele na tablicy są zakończone.",
+ "objectives_header": "{{incident}} — zakończone cele: {{complete}} z {{total}}. Nadal otwarte:",
"offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "offline_cannot_answer": "Nie mogę na to odpowiedzieć bez połączenia. W trybie offline nadal mogę podać PAR, zasoby, cele, potrzeby, funkcje ICS, liczniki, dziennik zdarzenia, odprawę i listę kontrolną dla tego typu zdarzenia.",
+ "offline_hint": "Brak połączenia — odpowiedzi pochodzą z tablicy zapisanej na tym urządzeniu.",
+ "open_needs": "Otwarte potrzeby: {{count}}.",
+ "par_all_good": "Wszyscy są rozliczeni.",
+ "par_counts": "Zielony: {{green}}, bliski termin: {{warning}}, po terminie: {{critical}}.",
+ "par_critical_header": "Po terminie — nierozliczeni:",
+ "par_due_row": "- {{name}}: termin za {{minutes}} min",
+ "par_header": "PAR — {{incident}} — śledzone osoby: {{count}}.",
+ "par_none": "{{incident}}: kontrola stanu osobowego nie jest prowadzona — nikt się nie zameldował i nie działa żaden licznik meldunków.",
+ "par_overdue_row": "- {{name}}: {{minutes}} min po terminie",
+ "par_summary": "PAR: śledzeni: {{total}}, po terminie: {{critical}}, bliski termin: {{warning}}.",
+ "par_warning_header": "Zbliża się termin meldunku:",
+ "placeholder": "Zapytaj o to zdarzenie",
+ "resource_counts": "Jednostki: {{units}}, personel: {{personnel}}, odcinki: {{lanes}} (nieprzydzielone: {{unassigned}}).",
+ "resources_header": "{{incident}} — jednostki w działaniach: {{units}}, personel: {{personnel}}.",
+ "rit_found": "{{lane}} jest w gotowości — zasoby: {{count}}.",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "{{incident}}: nie przydzielono funkcji: {{role}}.",
+ "role_unknown": "Nie rozpoznaję funkcji „{{role}}”. Zapytaj o funkcje ICS, aby zobaczyć wszystkie przydziały.",
+ "roles_header": "{{incident}} — przydzielone funkcje ICS: {{count}}.",
+ "roles_unfilled": "Nieobsadzone funkcje zwykle potrzebne przy zdarzeniu typu {{type}}: {{roles}}",
+ "send": "Wyślij",
+ "source_device": "Odpowiedź z tego urządzenia",
+ "source_server": "Odpowiedź z Resgrid",
+ "span_all_good": "Rozpiętość kierowania wygląda rozsądnie — każdy odcinek mieści się w swoich limitach i żaden nie ma więcej niż {{ceiling}} zasobów.",
+ "span_header": "{{incident}} — odcinki: {{lanes}}, przydzielone zasoby: {{resources}}.",
+ "span_no_lead": "- Brak wyznaczonego dowódcy: {{lanes}}",
+ "span_over_row": "- {{lane}}: zasoby {{count}} (limit {{limit}}) — rozważ podział odcinka lub wyznaczenie dodatkowego dowódcy.",
+ "span_under_row": "- {{lane}}: zasoby {{count}}, minimum {{minimum}}.",
+ "staging": "Punkt koncentracji: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Przekazanie dowodzenia",
+ "missing": "Czego brakuje?",
+ "on_scene": "Co jest na miejscu?",
+ "open_needs": "Otwarte potrzeby",
+ "open_objectives": "Otwarte cele",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "Ostatnie 30 minut",
+ "rit": "Czy mam RIT?",
+ "safety": "Oficer bezpieczeństwa",
+ "span": "Rozpiętość kierowania",
+ "status": "Stan zdarzenia",
+ "timers": "Liczniki",
+ "unassigned": "Nieprzydzielone",
+ "unfilled_roles": "Nieobsadzone funkcje ICS",
+ "wind": "Wiatr i pogoda"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Sprawdzam tablicę…",
+ "this_incident": "Bieżące zdarzenie",
+ "time_in_lane": "({{duration}} na odcinku)",
+ "timeline_empty": "{{incident}}: w dzienniku nie ma jeszcze wpisów.",
+ "timeline_empty_window": "{{incident}}: brak wpisów w dzienniku z ostatnich {{minutes}} minut.",
+ "timeline_header": "{{incident}} — ostatnie wpisy w dzienniku ({{count}}):",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — ostatnie {{minutes}} min (wpisy: {{count}}):",
+ "timer_due_row": "- {{name}}: DO WYKONANIA TERAZ",
+ "timer_no_due_row": "- {{name}}: działa",
+ "timer_running_row": "- {{name}}: termin za {{remaining}}",
+ "timers_header": "{{incident}} — liczniki: {{count}}.",
+ "title": "Asystent zdarzenia",
+ "unassigned_header": "{{incident}} — nieprzydzielone ({{count}}):",
+ "unassigned_line": "- Pula nieprzydzielonych: {{count}}",
+ "unknown": "nieznany"
},
"incidents": {
"accountability": "Kontrola personelu",
@@ -1304,13 +1307,16 @@
"2": "Anulowane",
"3": "Bezzasadne",
"4": "Zasadne",
- "5": "Drobne"
+ "5": "Drobne",
+ "6": "Przekazane",
+ "7": "Fałszywy alarm",
+ "8": "Oczekujące"
},
"unknown_priority": "Nieznany",
"unknown_user": "Nieznany użytkownik"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Nie masz uprawnień do korzystania z aplikacji IC. Skontaktuj się z administratorem swojej jednostki.",
"errorModal": {
"confirmButton": "OK",
"message": "Sprawdź nazwę użytkownika i hasło i spróbuj ponownie.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Logowanie...",
"login_button_success": "Zalogowano pomyślnie",
"or_sign_in_with_password": "lub zaloguj się hasłem",
+ "otp_body": "Twoje konto ma włączone uwierzytelnianie dwuskładnikowe. Wprowadź aktualny kod z aplikacji uwierzytelniającej, aby dokończyć logowanie.",
+ "otp_invalid": "Ten kod jest nieprawidłowy lub wygasł. Wprowadź aktualny kod z aplikacji uwierzytelniającej.",
+ "otp_placeholder": "6-cyfrowy kod",
+ "otp_title": "Weryfikacja dwuetapowa",
+ "otp_verify": "Zweryfikuj",
"password": "Hasło",
"password_incorrect": "Hasło jest nieprawidłowe",
"password_placeholder": "Wpisz hasło",
@@ -1382,7 +1393,7 @@
"no_maps_description": "Brak map dostępnych dla Twojego działu.",
"no_results": "Nie znaleziono wyników",
"outdoor": "Zewnętrzne",
- "region": "Region",
+ "region": "Obszar",
"search": "Szukaj map...",
"search_maps": "Szukaj map",
"search_placeholder": "Szukaj lokalizacji, stref, regionów...",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Raport dla tej załogi lub osoby już istnieje w tym dniu.",
"timereports_locked": "Tego raportu nie można już zmienić.",
"timereports_scope_invalid": "Ta załoga lub osoba nie jest aktywna w tym rozmieszczeniu.",
+ "timereports_status_transition_invalid": "Ten raport został już zmieniony, być może na innym urządzeniu. Odśwież i spróbuj ponownie.",
"timereports_subject_covered": "Ta załoga lub osoba ma już czas w innym raporcie z tego dnia.",
"timereports_time_invalid": "Wpisz godziny w formacie HH:mm.",
"validation": "Serwer odrzucił zmianę."
@@ -1837,7 +1849,7 @@
"title": "Tytuł",
"types": {
"call": "Zgłoszenie",
- "chat": "Chat",
+ "chat": "Czat",
"group_chat": "Chat grupowy",
"message": "Wiadomość",
"notification": "Powiadomienie"
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Wszystkie typy POI",
"poi_filter_placeholder": "Filtruj według typu",
"poi_filter_type": "Typ POI",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filtry",
"poi_not_found": "Nie znaleziono POI",
"poi_not_found_description": "Nie można znaleźć tego POI.",
"poi_note": "Notatka",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Sortuj według",
"poi_sort_display": "Nazwa",
"poi_sort_placeholder": "Sortuj POI",
"poi_sort_type": "Typ",
@@ -2167,7 +2179,7 @@
"stop_type_inspection": "Inspekcja",
"stop_type_pickup": "Odbiór",
"stop_type_service": "Serwis",
- "stop_type_standard": "Standard",
+ "stop_type_standard": "Standardowy",
"stops": "Postoje",
"stops_completed": "Postoje ukończone",
"summary_stats": "Podsumowanie",
@@ -2264,7 +2276,7 @@
"theme": {
"dark": "Ciemny",
"light": "Jasny",
- "system": "System",
+ "system": "Systemowy",
"title": "Motyw"
},
"title": "Ustawienia",
diff --git a/src/translations/sv.json b/src/translations/sv.json
index 7d355b7..6c6cd04 100644
--- a/src/translations/sv.json
+++ b/src/translations/sv.json
@@ -339,7 +339,7 @@
"acknowledge": "Kvittera",
"add_gif": "Lägg till GIF",
"add_image": "Lägg till bild",
- "all_leads": "All Leads",
+ "all_leads": "Alla ansvariga",
"are_typing": "{{count}} personer skriver...",
"assistant": "Assistent",
"attachment_failed": "Uppladdning av bilaga misslyckades",
@@ -353,8 +353,8 @@
"delete": "Ta bort",
"delete_failed": "Kunde inte ta bort meddelandet",
"direct_message": "Direktmeddelande",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Det gick inte att öppna konversationen.",
+ "dm_unavailable": "Kontakten har inget Resgrid-konto att skicka meddelanden till.",
"edit": "Redigera",
"edit_failed": "Kunde inte redigera meddelandet",
"edit_message": "Redigera meddelande",
@@ -371,14 +371,14 @@
"flag_reason": "Varför anmäler du det här?",
"flag_sensitive": "Känslig information",
"flag_spam": "Skräppost",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Den här insatsen är avslutad. Konversationen sparas som en ögonblicksbild — inga nya meddelanden eller ändringar, men du kan fortfarande anmäla innehåll.",
"group_name": "Gruppnamn (valfritt)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Ledningschatt",
+ "incident_lane_channel": "Sektorschatt",
"is_typing": "{{name}} skriver...",
"load_people_failed": "Kunde inte ladda personer",
"message_deleted": "Det här meddelandet har tagits bort",
- "message_person": "Message {{name}}",
+ "message_person": "Meddelande till {{name}}",
"moderator_delete": "Ta bort (moderator)",
"moderator_removed": "Borttaget av moderator",
"new_direct_message": "Nytt direktmeddelande",
@@ -386,8 +386,8 @@
"no_gifs": "Inga GIF:ar hittades",
"no_people": "Inga personer hittades",
"open_assistant": "Öppna assistenten",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Ledningschatt",
+ "open_lane_chat": "Sektorschatt",
"original_message": "Ursprungligt meddelande",
"permission_location_denied": "Platsåtkomst nekades",
"permission_photos_denied": "Åtkomst till fotobiblioteket nekades",
@@ -440,6 +440,8 @@
"queued_offline": "Incheckning köad för när anslutningen återställs",
"quick_check_in": "Snabb incheckning",
"select_type": "Välj incheckningstyp",
+ "status_critical": "Kritisk",
+ "status_green": "OK",
"status_ok": "OK",
"status_overdue": "Försenad",
"status_warning": "Varning",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "t.ex. Taktisk 1",
"channel_preset_command": "Ledning",
"channel_preset_tactical": "Taktisk",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Det gick inte att läsa in insatsens chattkanaler. Tryck igen för att försöka på nytt.",
"close_call_failed": "Ledningen avslutades, men samtalet kunde inte stängas.",
"close_call_queued": "Stängningen av samtalet ligger i kö och skickas automatiskt.",
"close_call_skipped": "Samtalet lämnades öppet eftersom ledningen inte avslutades.",
"close_channels": "Stäng alla",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Ledningschatt",
+ "command_chat_short": "Ledning",
+ "command_chat_unavailable": "Ingen ledningschatt har skapats för den här insatsen ännu.",
"command_details": "Ledningsdetaljer",
"crew_placeholder": "Namn på person eller styrka",
"current_commander": "IL",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Flytta till resurser",
"delete_lane_release_resources": "Frigör från insatsen",
"delete_lane_resources_message": "Detta körfält har {{count}} tilldelade resurser. Flytta dem tillbaka till resurslistan eller frigör dem från insatsen.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Detaljer",
+ "dispatch_chat": "Larmcentral",
+ "dispatch_chat_unavailable": "Ingen kanal till larmcentralen har skapats för den här insatsen ännu.",
"drag_move_hint": "Tryck och håll på en resurs för att dra den, eller tryck på den och sedan på en bana.",
"edit_lane": "Redigera sektor",
"edit_lane_title": "Redigera {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Skicka även till ställföreträdande chefer",
"info_save_error": "Det gick inte att spara informationen",
"info_save_success": "Informationen sparad",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Sektorschatt",
+ "lane_chat_unavailable": "Ingen chattkanal har skapats för den här sektorn ännu.",
"lane_color_label": "Filfärg (valfritt — färgar filens markörer på kartan)",
"lane_limits_label": "Filgränser (valfritt)",
"lane_name_label": "Sektorns namn",
@@ -622,8 +624,8 @@
"lead_external_label": "Extern kontakt",
"lead_name_placeholder": "Kontaktnamn",
"lead_phone_placeholder": "Telefon",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Alla ansvariga",
+ "leads_chat_unavailable": "Kanalen Alla ansvariga har inte skapats för den här insatsen ännu.",
"limit_max_riding": "Max ombord",
"limit_max_time": "Avlös efter (min)",
"limit_max_units": "Max enheter",
@@ -650,8 +652,8 @@
"message_commander": "Meddelande till insatschef",
"message_commander_hint": "Skickas direkt till insatschefen via dennes aviseringskanaler.",
"message_commander_hint_named": "Skickas direkt till {{name}} via dennes aviseringskanaler.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Meddelande till ansvarig",
+ "message_role_holder": "Skicka meddelande",
"message_send_error": "Det gick inte att skicka meddelandet",
"message_send_success": "Meddelande skickat till ledningen",
"message_subject_placeholder": "Ämne (valfritt)",
@@ -850,7 +852,7 @@
"transfer_command": "Överför befäl",
"transfer_command_hint": "Lämna över insatsbefälet till en annan användare. Tavlan förblir delad.",
"transfer_error": "Kunde inte överföra befälet",
- "transfer_short": "Transfer",
+ "transfer_short": "Överför",
"transfer_success": "Befälet överfört",
"transmission_log": "Sändningslogg",
"transmission_seconds": "{{count}}s",
@@ -870,6 +872,7 @@
"back": "Tillbaka",
"cancel": "Avbryt",
"clear": "Rensa",
+ "clear_search": "Rensa sökning",
"close": "Stäng",
"confirm": "Bekräfta",
"confirm_location": "Bekräfta plats",
@@ -1079,144 +1082,144 @@
"required": "Detta fält är obligatoriskt"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "PERSONALKONTROLL",
+ "briefing_action_plan": "Insatsplan: {{text}}",
+ "briefing_address": "Plats: {{address}}",
+ "briefing_command": "LEDNING",
+ "briefing_commander": "Insatschef: {{name}}",
+ "briefing_established": "Ledning etablerad: {{time}} (pågått {{duration}})",
+ "briefing_header": "GENOMGÅNG VID BEFÄLSÖVERLÄMNING — {{incident}}",
+ "briefing_icp": "Ledningsplats: {{location}}",
+ "briefing_important": "Viktig information: {{text}}",
+ "briefing_lane_row": "{{lane}} (ansvarig: {{lead}}) — {{count}} resurser: {{names}}",
+ "briefing_needs": "UTESTÅENDE BEHOV",
+ "briefing_no_lanes": "- Inga sektorer upprättade",
+ "briefing_no_needs": "- Inga",
+ "briefing_no_objectives": "- Inga mål registrerade",
+ "briefing_no_par": "Ingen personalkontroll pågår.",
+ "briefing_objectives": "MÅL",
+ "briefing_organization": "ORGANISATION OCH RESURSER",
+ "briefing_rehab": "Återhämtning: {{location}}",
+ "briefing_situation": "LÄGESBILD",
+ "briefing_staging": "Uppställningsplats: {{location}}",
+ "briefing_type": "Insatstyp: {{type}}",
+ "check_action_plan": "Insatsplan eller mål registrerade",
+ "check_command": "Ledning etablerad med namngiven insatschef",
+ "check_icp": "Ledningsplats angiven",
+ "check_par": "Personalkontroll / incheckning igång",
+ "check_safety": "Säkerhetsbefäl tilldelat",
+ "check_staging": "Uppställningsplats utsedd",
+ "checklist_confirm": "Standardpunkter för {{type}} att bekräfta:",
+ "checklist_disclaimer": "Detta är allmän ICS-vägledning, inte din organisations riktlinjer — era rutiner och ditt eget omdöme går först.",
+ "checklist_done": "Redan klart på tavlan: {{items}}.",
+ "checklist_header": "Checklista ({{type}}) för {{incident}}.",
+ "checklist_outstanding": "Syns inte på tavlan ännu:",
+ "clear": "Rensa konversation",
+ "command_post": "Ledningsplats: {{location}}.",
+ "commander": "IL: {{name}}.",
+ "elapsed": "Ledningen har pågått {{duration}}.",
+ "empty": "Fråga om den här insatsen — personalkontroll, resurser, mål, behov, roller eller vad du kan ha missat.",
+ "error": "Något gick fel när frågan skulle besvaras. Försök igen.",
+ "estimated_end": "Beräknat slut: {{time}}.",
+ "external_resources": "- Externa resurser / förstärkningsresurser som följs: {{count}}",
+ "important": "Viktigt: {{text}}",
+ "lane_empty": "Inget är tilldelat den här sektorn.",
+ "lane_header": "{{lane}} ({{type}}): {{count}} resurser.",
+ "lane_lead": "Ansvarig: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Jag hittar ingen sektor som heter \"{{lane}}\". Sektorer på tavlan: {{lanes}}",
+ "lane_objective": "Primärt mål: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Vanliga kontrollpunkter för {{type}} som inte finns på tavlan ännu: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — öppet i {{age}}",
+ "needs_all_met": "Allt som beställts har tillgodosetts.",
+ "needs_header": "{{incident}}: {{outstanding}} utestående behov ({{met}} uppfyllda, {{cancelled}} avbrutna).",
+ "no_answer": "Jag kunde inte svara på den frågan.",
+ "no_board": "Ingen ledningstavla har lästs in för den här insatsen ännu.",
+ "no_lanes": "inga ännu",
+ "no_lanes_yet": "Inga sektorer har skapats för {{incident}} ännu, så det finns inget ledningsspann att kontrollera.",
+ "no_lead": "ingen ansvarig",
+ "no_needs": "Inga behov har registrerats för {{incident}}.",
+ "no_notes": "Inga statusanteckningar har registrerats för {{incident}}.",
+ "no_objectives": "Inga taktiska mål har satts för {{incident}} ännu.",
+ "no_resources": "Inget är tilldelat {{incident}} ännu.",
+ "no_rit": "Jag ser ingen RIT/RIC-sektor för {{incident}}. Vid en pågående insats är det värt att tilldela en innan styrkor går in i byggnaden.",
+ "no_timers": "Inga timers är igång för {{incident}}.",
+ "no_unassigned": "Allt som följs för {{incident}} är placerat i en sektor.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — {{count}} statusanteckning(ar):",
+ "objective_complete": "slutfört",
+ "objective_in_progress": "pågående",
+ "objective_overdue": " [försenat]",
+ "objective_pending": "väntande",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
+ "objective_summary": "Mål: {{complete}} av {{total}} slutförda.",
+ "objectives_all_complete": "Alla mål på tavlan är slutförda.",
+ "objectives_header": "{{incident}}: {{complete}} av {{total}} mål slutförda. Fortfarande öppna:",
"offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "offline_cannot_answer": "Jag kan inte svara på det utan anslutning. Offline kan jag fortfarande ge dig PAR, resurser, mål, behov, ICS-roller, timers, insatsloggen, en genomgång och checklistan för den här insatstypen.",
+ "offline_hint": "Ingen anslutning — svaren kommer från tavlan som sparats på den här enheten.",
+ "open_needs": "{{count}} behov fortfarande öppna.",
+ "par_all_good": "Alla är redovisade.",
+ "par_counts": "{{green}} gröna, {{warning}} närmar sig, {{critical}} försenade.",
+ "par_critical_header": "Försenade — ej redovisade:",
+ "par_due_row": "- {{name}}: dags om {{minutes}} min",
+ "par_header": "PAR för {{incident}}: {{count}} personer följs.",
+ "par_none": "Ingen personalkontroll pågår för {{incident}} — ingen har checkat in och ingen incheckningstimer är igång.",
+ "par_overdue_row": "- {{name}}: {{minutes}} min försenad",
+ "par_summary": "PAR: {{total}} följs, {{critical}} försenade, {{warning}} närmar sig.",
+ "par_warning_header": "Närmar sig incheckning:",
+ "placeholder": "Fråga om den här insatsen",
+ "resource_counts": "{{units}} enheter och {{personnel}} personer fördelade på {{lanes}} sektorer ({{unassigned}} ej tilldelade).",
+ "resources_header": "{{incident}}: {{units}} enheter och {{personnel}} personer i arbete.",
+ "rit_found": "{{lane}} står i beredskap med {{count}} resurs(er).",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "{{role}} är inte tillsatt för {{incident}}.",
+ "role_unknown": "Jag känner inte igen rollen \"{{role}}\". Fråga efter ICS-roller för att se alla tilldelningar.",
+ "roles_header": "{{incident}}: {{count}} ICS-roll(er) tillsatta.",
+ "roles_unfilled": "Otillsatta roller som vanligtvis behövs vid {{type}}: {{roles}}",
+ "send": "Skicka",
+ "source_device": "Besvarat på den här enheten",
+ "source_server": "Besvarat av Resgrid",
+ "span_all_good": "Ledningsspannet ser rimligt ut — alla sektorer är inom sina gränser och ingen sektor har fler än {{ceiling}} resurser.",
+ "span_header": "{{incident}}: {{lanes}} sektorer, {{resources}} resurser tilldelade.",
+ "span_no_lead": "- Ingen ansvarig tilldelad: {{lanes}}",
+ "span_over_row": "- {{lane}} har {{count}} resurser (gräns {{limit}}) — överväg att dela upp sektorn eller tillsätta ett befäl.",
+ "span_under_row": "- {{lane}} har {{count}} resurser, minimum är {{minimum}}.",
+ "staging": "Uppställningsplats: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Befälsöverlämning",
+ "missing": "Vad har jag missat?",
+ "on_scene": "Vad finns på plats?",
+ "open_needs": "Öppna behov",
+ "open_objectives": "Öppna mål",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
+ "recent": "Senaste 30 minuterna",
+ "rit": "Har jag en RIT?",
+ "safety": "Säkerhetsbefäl",
+ "span": "Ledningsspann",
+ "status": "Insatsstatus",
"timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "unassigned": "Ej tilldelade",
+ "unfilled_roles": "Otillsatta ICS-roller",
+ "wind": "Vind och väder"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Kontrollerar tavlan…",
+ "this_incident": "den här insatsen",
+ "time_in_lane": "({{duration}} i sektorn)",
+ "timeline_empty": "Inget har loggats för {{incident}} ännu.",
+ "timeline_empty_window": "Inget har loggats de senaste {{minutes}} minuterna för {{incident}}.",
+ "timeline_header": "{{incident}} — senaste {{count}} logginläggen:",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — senaste {{minutes}} minuterna ({{count}} inlägg):",
+ "timer_due_row": "- {{name}}: DAGS NU",
+ "timer_no_due_row": "- {{name}}: igång",
+ "timer_running_row": "- {{name}}: dags om {{remaining}}",
+ "timers_header": "Timers för {{incident}}: {{count}}.",
+ "title": "Insatsassistent",
+ "unassigned_header": "Ej tilldelade för {{incident}} ({{count}}):",
+ "unassigned_line": "- Resurspool (ej tilldelade): {{count}}",
+ "unknown": "okänd"
},
"incidents": {
"accountability": "Personalkontroll",
@@ -1304,13 +1307,16 @@
"2": "Avbrutet",
"3": "Ogrundat",
"4": "Grundat",
- "5": "Mindre"
+ "5": "Mindre",
+ "6": "Överfört",
+ "7": "Falskt larm",
+ "8": "Väntande"
},
"unknown_priority": "Okänd",
"unknown_user": "Okänd användare"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Du har inte behörighet att använda IC-appen. Kontakta din organisations administratör.",
"errorModal": {
"confirmButton": "OK",
"message": "Kontrollera ditt användarnamn och lösenord och försök igen.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Loggar in...",
"login_button_success": "Inloggningen lyckades",
"or_sign_in_with_password": "eller logga in med lösenord",
+ "otp_body": "Ditt konto har tvåfaktorsautentisering aktiverad. Ange den aktuella koden från din autentiseringsapp för att slutföra inloggningen.",
+ "otp_invalid": "Koden är ogiltig eller har gått ut. Ange den aktuella koden från din autentiseringsapp.",
+ "otp_placeholder": "6-siffrig kod",
+ "otp_title": "Tvåstegsverifiering",
+ "otp_verify": "Verifiera",
"password": "Lösenord",
"password_incorrect": "Lösenordet var felaktigt",
"password_placeholder": "Ange ditt lösenord",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Det finns redan en rapport för den besättningen eller personen den dagen.",
"timereports_locked": "Rapporten kan inte längre ändras.",
"timereports_scope_invalid": "Den besättningen eller personen är inte aktiv i denna insats.",
+ "timereports_status_transition_invalid": "Rapporten har redan ändrats, kanske på en annan enhet. Uppdatera och försök igen.",
"timereports_subject_covered": "Den besättningen eller personen har redan tid i en annan rapport denna dag.",
"timereports_time_invalid": "Ange tider som HH:mm.",
"validation": "Servern avvisade ändringen."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Alla POI-typer",
"poi_filter_placeholder": "Filtrera efter typ",
"poi_filter_type": "POI-typ",
- "poi_filters": "POI Filters",
+ "poi_filters": "Filter",
"poi_not_found": "POI hittades inte",
"poi_not_found_description": "Denna POI kunde inte hittas.",
"poi_note": "Anteckning",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Sortera efter",
"poi_sort_display": "Namn",
"poi_sort_placeholder": "Sortera POI",
"poi_sort_type": "Typ",
diff --git a/src/translations/uk.json b/src/translations/uk.json
index 4898a2e..083af43 100644
--- a/src/translations/uk.json
+++ b/src/translations/uk.json
@@ -339,7 +339,7 @@
"acknowledge": "Підтвердити",
"add_gif": "Додати GIF",
"add_image": "Додати зображення",
- "all_leads": "All Leads",
+ "all_leads": "Усі керівники",
"are_typing": "{{count}} осіб набирають повідомлення...",
"assistant": "Помічник",
"attachment_failed": "Не вдалося завантажити вкладення",
@@ -353,13 +353,13 @@
"delete": "Видалити",
"delete_failed": "Не вдалося видалити повідомлення",
"direct_message": "Пряме повідомлення",
- "dm_failed": "Couldn't open that conversation.",
- "dm_unavailable": "That contact has no Resgrid account to message.",
+ "dm_failed": "Не вдалося відкрити цю розмову.",
+ "dm_unavailable": "У цього контакту немає облікового запису Resgrid для повідомлень.",
"edit": "Редагувати",
"edit_failed": "Не вдалося відредагувати повідомлення",
"edit_message": "Редагувати повідомлення",
"edited": "(відредаговано)",
- "emoji": "Emoji",
+ "emoji": "Емодзі",
"empty": "Ще немає розмов. Розпочніть особисте повідомлення або створіть групу.",
"failed_tap_retry": "Помилка — торкніться, щоб повторити",
"flag": "Поскаржитися",
@@ -371,14 +371,14 @@
"flag_reason": "Чому ви повідомляєте про це?",
"flag_sensitive": "Конфіденційна інформація",
"flag_spam": "Спам",
- "frozen_notice": "This incident is closed. The conversation is kept as a point-in-time record — no new messages or edits, but you can still flag content.",
+ "frozen_notice": "Цей інцидент закрито. Розмову збережено як запис на певний момент часу — нові повідомлення та редагування недоступні, але ви все ще можете поскаржитися на вміст.",
"group_name": "Назва групи (необов'язково)",
- "incident_command_channel": "Command chat",
- "incident_lane_channel": "Lane chat",
+ "incident_command_channel": "Чат командування",
+ "incident_lane_channel": "Чат ділянки",
"is_typing": "{{name}} набирає повідомлення...",
"load_people_failed": "Не вдалося завантажити людей",
"message_deleted": "Це повідомлення було видалено",
- "message_person": "Message {{name}}",
+ "message_person": "Написати: {{name}}",
"moderator_delete": "Видалити (модератор)",
"moderator_removed": "Видалено модератором",
"new_direct_message": "Нове особисте повідомлення",
@@ -386,8 +386,8 @@
"no_gifs": "GIF не знайдено",
"no_people": "Людей не знайдено",
"open_assistant": "Відкрити помічника",
- "open_command_chat": "Command chat",
- "open_lane_chat": "Lane chat",
+ "open_command_chat": "Чат командування",
+ "open_lane_chat": "Чат ділянки",
"original_message": "Початкове повідомлення",
"permission_location_denied": "Доступ до геолокації відхилено",
"permission_photos_denied": "Доступ до фотогалереї відхилено",
@@ -440,6 +440,8 @@
"queued_offline": "Реєстрацію поставлено в чергу до відновлення з'єднання",
"quick_check_in": "Швидка реєстрація",
"select_type": "Оберіть тип реєстрації",
+ "status_critical": "Критично",
+ "status_green": "ОК",
"status_ok": "ОК",
"status_overdue": "Прострочено",
"status_warning": "Попередження",
@@ -490,14 +492,14 @@
"channel_name_placeholder": "напр. Тактичний 1",
"channel_preset_command": "Командування",
"channel_preset_tactical": "Тактичний",
- "chat_load_failed": "Couldn't load this incident's chat channels. Tap again to retry.",
+ "chat_load_failed": "Не вдалося завантажити чат-канали цього інциденту. Торкніться ще раз, щоб повторити.",
"close_call_failed": "Командування завершено, але виклик закрити не вдалося.",
"close_call_queued": "Закриття виклику поставлено в чергу, його буде надіслано автоматично.",
"close_call_skipped": "Виклик залишено відкритим, бо командування не завершено.",
"close_channels": "Закрити всі",
- "command_chat": "Command chat",
- "command_chat_short": "Command",
- "command_chat_unavailable": "No command chat channel has been created for this incident yet.",
+ "command_chat": "Чат командування",
+ "command_chat_short": "Командування",
+ "command_chat_unavailable": "Для цього інциденту ще не створено каналу чату командування.",
"command_details": "Деталі управління",
"crew_placeholder": "Ім'я особи або назва ланки",
"current_commander": "КР",
@@ -506,9 +508,9 @@
"delete_lane_move_resources": "Перемістити до ресурсів",
"delete_lane_release_resources": "Звільнити з інциденту",
"delete_lane_resources_message": "Ця смуга має {{count}} призначених ресурсів. Поверніть їх до списку ресурсів або звільніть з інциденту.",
- "details_short": "Details",
- "dispatch_chat": "Dispatch",
- "dispatch_chat_unavailable": "No dispatch channel has been created for this incident yet.",
+ "details_short": "Деталі",
+ "dispatch_chat": "Диспетчер",
+ "dispatch_chat_unavailable": "Для цього інциденту ще не створено диспетчерського каналу.",
"drag_move_hint": "Натисніть і утримуйте ресурс, щоб перетягнути його, або торкніться ресурсу, а потім смуги.",
"edit_lane": "Редагувати ділянку",
"edit_lane_title": "Редагувати {{lane}}",
@@ -598,8 +600,8 @@
"include_deputies": "Також надіслати заступникам керівника",
"info_save_error": "Не вдалося зберегти інформацію",
"info_save_success": "Інформацію збережено",
- "lane_chat": "Lane chat",
- "lane_chat_unavailable": "No chat channel has been created for this lane yet.",
+ "lane_chat": "Чат ділянки",
+ "lane_chat_unavailable": "Для цієї ділянки ще не створено каналу чату.",
"lane_color_label": "Колір смуги (необов’язково — фарбує її маркери на мапі)",
"lane_limits_label": "Ліміти смуги (необов’язково)",
"lane_name_label": "Назва ділянки",
@@ -622,8 +624,8 @@
"lead_external_label": "Зовнішній контакт",
"lead_name_placeholder": "Ім'я контакту",
"lead_phone_placeholder": "Телефон",
- "leads_chat": "All Leads",
- "leads_chat_unavailable": "The All Leads channel hasn't been created for this incident yet.",
+ "leads_chat": "Усі керівники",
+ "leads_chat_unavailable": "Канал «Усі керівники» для цього інциденту ще не створено.",
"limit_max_riding": "Макс. екіпаж",
"limit_max_time": "Ротація після (хв)",
"limit_max_units": "Макс. підрозділів",
@@ -650,8 +652,8 @@
"message_commander": "Повідомлення керівнику",
"message_commander_hint": "Надсилається безпосередньо керівнику інциденту через його канали сповіщень.",
"message_commander_hint_named": "Надсилається безпосередньо {{name}} через канали сповіщень.",
- "message_lead": "Message lead",
- "message_role_holder": "Message",
+ "message_lead": "Написати керівнику",
+ "message_role_holder": "Написати",
"message_send_error": "Не вдалося надіслати повідомлення",
"message_send_success": "Повідомлення надіслано керівництву",
"message_subject_placeholder": "Тема (необов'язково)",
@@ -850,7 +852,7 @@
"transfer_command": "Передати командування",
"transfer_command_hint": "Передайте командування інцидентом іншому користувачу. Дошка залишається спільною.",
"transfer_error": "Не вдалося передати командування",
- "transfer_short": "Transfer",
+ "transfer_short": "Передати",
"transfer_success": "Командування передано",
"transmission_log": "Журнал передач",
"transmission_seconds": "{{count}}с",
@@ -870,6 +872,7 @@
"back": "Назад",
"cancel": "Скасувати",
"clear": "Очистити",
+ "clear_search": "Очистити пошук",
"close": "Закрити",
"confirm": "Підтвердити",
"confirm_location": "Підтвердити місцезнаходження",
@@ -1079,144 +1082,144 @@
"required": "Це поле обов'язкове"
},
"incident_assistant": {
- "briefing_accountability": "ACCOUNTABILITY",
- "briefing_action_plan": "Action plan: {{text}}",
- "briefing_address": "Location: {{address}}",
- "briefing_command": "COMMAND",
- "briefing_commander": "Incident Commander: {{name}}",
- "briefing_established": "Command established: {{time}} (running {{duration}})",
- "briefing_header": "TRANSFER OF COMMAND BRIEFING — {{incident}}",
- "briefing_icp": "Command post: {{location}}",
- "briefing_important": "Important information: {{text}}",
- "briefing_lane_row": "{{lane}} (lead: {{lead}}) — {{count}} resources: {{names}}",
- "briefing_needs": "OUTSTANDING NEEDS",
- "briefing_no_lanes": "- No lanes established",
- "briefing_no_needs": "- None",
- "briefing_no_objectives": "- No objectives recorded",
- "briefing_no_par": "No personnel accountability is being tracked.",
- "briefing_objectives": "OBJECTIVES",
- "briefing_organization": "ORGANIZATION AND RESOURCES",
- "briefing_rehab": "Rehab: {{location}}",
- "briefing_situation": "SITUATION",
- "briefing_staging": "Staging: {{location}}",
- "briefing_type": "Incident type: {{type}}",
- "check_action_plan": "Action plan or objectives recorded",
- "check_command": "Command established with a named IC",
- "check_icp": "Command post location set",
- "check_par": "Accountability / check-in running",
- "check_safety": "Safety Officer assigned",
- "check_staging": "Staging designated",
- "checklist_confirm": "Standard {{type}} items to confirm:",
- "checklist_disclaimer": "This is general ICS guidance, not your department's policy — your SOGs and your judgement come first.",
- "checklist_done": "Already done on the board: {{items}}.",
- "checklist_header": "{{type}} checklist for {{incident}}.",
- "checklist_outstanding": "Not showing on the board yet:",
- "clear": "Clear conversation",
- "command_post": "ICP: {{location}}.",
- "commander": "IC: {{name}}.",
- "elapsed": "Command running {{duration}}.",
- "empty": "Ask about this incident — accountability, resources, objectives, needs, positions, or what you might be missing.",
- "error": "Something went wrong answering that. Try again.",
- "estimated_end": "Estimated end: {{time}}.",
- "external_resources": "- External / mutual aid resources tracked: {{count}}",
- "important": "Important: {{text}}",
- "lane_empty": "Nothing is assigned to this lane.",
- "lane_header": "{{lane}} ({{type}}): {{count}} resources.",
- "lane_lead": "Lead: {{name}}.",
+ "briefing_accountability": "ОБЛІК ПЕРСОНАЛУ",
+ "briefing_action_plan": "План дій: {{text}}",
+ "briefing_address": "Місце події: {{address}}",
+ "briefing_command": "КОМАНДУВАННЯ",
+ "briefing_commander": "Керівник інциденту: {{name}}",
+ "briefing_established": "Командування прийнято: {{time}} (триває {{duration}})",
+ "briefing_header": "БРИФІНГ ПЕРЕДАЧІ КОМАНДУВАННЯ — {{incident}}",
+ "briefing_icp": "Командний пункт: {{location}}",
+ "briefing_important": "Важлива інформація: {{text}}",
+ "briefing_lane_row": "{{lane}} (керівник: {{lead}}) — {{count}} рес.: {{names}}",
+ "briefing_needs": "НЕЗАКРИТІ ПОТРЕБИ",
+ "briefing_no_lanes": "- Ділянок не створено",
+ "briefing_no_needs": "- Немає",
+ "briefing_no_objectives": "- Цілей не зафіксовано",
+ "briefing_no_par": "Облік персоналу не ведеться.",
+ "briefing_objectives": "ЦІЛІ",
+ "briefing_organization": "ОРГАНІЗАЦІЯ ТА РЕСУРСИ",
+ "briefing_rehab": "Зона відновлення: {{location}}",
+ "briefing_situation": "ОБСТАНОВКА",
+ "briefing_staging": "Зона очікування: {{location}}",
+ "briefing_type": "Тип інциденту: {{type}}",
+ "check_action_plan": "План дій або цілі зафіксовано",
+ "check_command": "Командування прийнято, керівника інциденту визначено",
+ "check_icp": "Місце командного пункту визначено",
+ "check_par": "Облік персоналу / реєстрація ведеться",
+ "check_safety": "Офіцера безпеки призначено",
+ "check_staging": "Зону очікування визначено",
+ "checklist_confirm": "Стандартні пункти для типу «{{type}}», які слід підтвердити:",
+ "checklist_disclaimer": "Це загальні рекомендації ICS, а не правила вашого підрозділу — ваші стандартні оперативні процедури та власне судження мають пріоритет.",
+ "checklist_done": "Уже виконано на дошці: {{items}}.",
+ "checklist_header": "Контрольний перелік для типу «{{type}}» — {{incident}}.",
+ "checklist_outstanding": "Ще не відображено на дошці:",
+ "clear": "Очистити розмову",
+ "command_post": "Командний пункт: {{location}}.",
+ "commander": "КР: {{name}}.",
+ "elapsed": "Командування триває {{duration}}.",
+ "empty": "Запитайте про цей інцидент — облік персоналу, ресурси, цілі, потреби, посади або те, що могло бути пропущено.",
+ "error": "Під час відповіді сталася помилка. Спробуйте ще раз.",
+ "estimated_end": "Орієнтовне завершення: {{time}}.",
+ "external_resources": "- Зовнішні ресурси / ресурси взаємодопомоги: {{count}}",
+ "important": "Важливо: {{text}}",
+ "lane_empty": "На цю ділянку нічого не призначено.",
+ "lane_header": "{{lane}} ({{type}}): ресурсів — {{count}}.",
+ "lane_lead": "Керівник: {{name}}.",
"lane_line": "- {{lane}}: {{count}} — {{names}}",
- "lane_not_found": "I don't see a lane called \"{{lane}}\". Lanes on the board: {{lanes}}",
- "lane_objective": "Primary objective: {{name}} ({{progress}}%).",
- "missing_benchmarks": "Common {{type}} benchmarks not on the board yet: {{benchmarks}}",
- "need_row": "- {{name}} [{{category}}] {{quantity}} — open {{age}}",
- "needs_all_met": "Everything ordered has been filled.",
- "needs_header": "{{incident}}: {{outstanding}} needs outstanding ({{met}} met, {{cancelled}} cancelled).",
- "no_answer": "I couldn't answer that one.",
- "no_board": "No command board is loaded for this incident yet.",
- "no_lanes": "none yet",
- "no_lanes_yet": "No lanes have been created on {{incident}} yet, so there's no span of control to check.",
- "no_lead": "no lead",
- "no_needs": "No needs have been recorded on {{incident}}.",
- "no_notes": "No status notes have been recorded on {{incident}}.",
- "no_objectives": "No tactical objectives have been set on {{incident}} yet.",
- "no_resources": "Nothing is assigned to {{incident}} yet.",
- "no_rit": "I don't see a RIT/RIC lane on {{incident}}. On a working incident that's worth assigning before crews go interior.",
- "no_timers": "No timers are running on {{incident}}.",
- "no_unassigned": "Everything tracked on {{incident}} is placed in a lane.",
+ "lane_not_found": "Не бачу ділянки «{{lane}}». Ділянки на дошці: {{lanes}}",
+ "lane_objective": "Основна ціль: {{name}} ({{progress}}%).",
+ "missing_benchmarks": "Типові контрольні точки для типу «{{type}}», яких ще немає на дошці: {{benchmarks}}",
+ "need_row": "- {{name}} [{{category}}] {{quantity}} — відкрита вже {{age}}",
+ "needs_all_met": "Усе замовлене забезпечено.",
+ "needs_header": "{{incident}}: незакритих потреб — {{outstanding}} (виконано: {{met}}, скасовано: {{cancelled}}).",
+ "no_answer": "Не вдалося відповісти на це запитання.",
+ "no_board": "Для цього інциденту ще не завантажено дошку управління.",
+ "no_lanes": "ще немає",
+ "no_lanes_yet": "{{incident}}: ділянок ще не створено, тому норму керованості перевірити неможливо.",
+ "no_lead": "не призначено",
+ "no_needs": "{{incident}}: потреб не зареєстровано.",
+ "no_notes": "{{incident}}: нотаток про стан не зафіксовано.",
+ "no_objectives": "{{incident}}: тактичних цілей ще не встановлено.",
+ "no_resources": "{{incident}}: ще нічого не призначено.",
+ "no_rit": "{{incident}}: не бачу ділянки RIT/RIC (резервної рятувальної групи). На активному інциденті її варто призначити до того, як ланки увійдуть усередину.",
+ "no_timers": "{{incident}}: жоден таймер не запущено.",
+ "no_unassigned": "{{incident}}: усе, що відстежується, розподілено по ділянках.",
"note_row": "- {{time}}: {{body}} ({{who}})",
- "notes_header": "{{incident}} — {{count}} status note(s):",
- "objective_complete": "complete",
- "objective_in_progress": "in progress",
- "objective_overdue": " [past target]",
- "objective_pending": "pending",
+ "notes_header": "{{incident}} — нотатки про стан ({{count}}):",
+ "objective_complete": "виконано",
+ "objective_in_progress": "виконується",
+ "objective_overdue": " [термін минув]",
+ "objective_pending": "очікує",
"objective_row": "{{name}} — {{status}} ({{progress}}%){{overdue}}",
- "objective_summary": "Objectives: {{complete}} of {{total}} complete.",
- "objectives_all_complete": "Every objective on the board is complete.",
- "objectives_header": "{{incident}}: {{complete}} of {{total}} objectives complete. Still open:",
- "offline_badge": "Offline",
- "offline_cannot_answer": "I can't answer that without a connection. Offline I can still give you PAR, resources, objectives, needs, ICS positions, timers, the incident log, a briefing, and the checklist for this incident type.",
- "offline_hint": "No connection — answers come from the board cached on this device.",
- "open_needs": "{{count}} needs still open.",
- "par_all_good": "Everyone is accounted for.",
- "par_counts": "{{green}} green, {{warning}} approaching, {{critical}} overdue.",
- "par_critical_header": "Overdue — not accounted for:",
- "par_due_row": "- {{name}}: due in {{minutes}} min",
- "par_header": "PAR for {{incident}}: {{count}} personnel tracked.",
- "par_none": "No personnel accountability is being tracked on {{incident}} — nobody has checked in and no check-in timer is running.",
- "par_overdue_row": "- {{name}}: {{minutes}} min overdue",
- "par_summary": "PAR: {{total}} tracked, {{critical}} overdue, {{warning}} approaching.",
- "par_warning_header": "Approaching check-in:",
- "placeholder": "Ask about this incident",
- "resource_counts": "{{units}} units and {{personnel}} personnel across {{lanes}} lanes ({{unassigned}} unassigned).",
- "resources_header": "{{incident}}: {{units}} units and {{personnel}} personnel working.",
- "rit_found": "{{lane}} is standing by with {{count}} resource(s).",
+ "objective_summary": "Цілі: виконано {{complete}} з {{total}}.",
+ "objectives_all_complete": "Усі цілі на дошці виконано.",
+ "objectives_header": "{{incident}}: виконано {{complete}} з {{total}} цілей. Ще відкриті:",
+ "offline_badge": "Офлайн",
+ "offline_cannot_answer": "Без з'єднання я не можу на це відповісти. Офлайн я все ще можу надати PAR, ресурси, цілі, потреби, посади ICS, таймери, журнал інциденту, брифінг і контрольний перелік для цього типу інциденту.",
+ "offline_hint": "Немає з'єднання — відповіді формуються з дошки, збереженої на цьому пристрої.",
+ "open_needs": "Незакритих потреб: {{count}}.",
+ "par_all_good": "Весь персонал враховано.",
+ "par_counts": "У нормі: {{green}}, наближається термін: {{warning}}, прострочено: {{critical}}.",
+ "par_critical_header": "Прострочено — не враховано:",
+ "par_due_row": "- {{name}}: через {{minutes}} хв",
+ "par_header": "PAR — {{incident}}: відстежується осіб: {{count}}.",
+ "par_none": "{{incident}}: облік персоналу не ведеться — ніхто не зареєструвався, і таймер реєстрації не запущено.",
+ "par_overdue_row": "- {{name}}: прострочено на {{minutes}} хв",
+ "par_summary": "PAR: відстежується {{total}}, прострочено {{critical}}, наближається термін {{warning}}.",
+ "par_warning_header": "Наближається час реєстрації:",
+ "placeholder": "Запитайте про цей інцидент",
+ "resource_counts": "Підрозділів: {{units}}, персоналу: {{personnel}}, ділянок: {{lanes}} (не призначено: {{unassigned}}).",
+ "resources_header": "{{incident}}: у роботі підрозділів: {{units}}, персоналу: {{personnel}}.",
+ "rit_found": "{{lane}} у готовності, ресурсів: {{count}}.",
"role_filled": "{{role}}: {{name}}",
"role_row": "{{role}}: {{name}}",
- "role_unfilled": "No {{role}} is assigned on {{incident}}.",
- "role_unknown": "I don't recognize the position \"{{role}}\". Ask for ICS roles to see everything assigned.",
- "roles_header": "{{incident}}: {{count}} ICS position(s) assigned.",
- "roles_unfilled": "Unfilled positions a {{type}} usually needs: {{roles}}",
- "send": "Send",
- "source_device": "Answered on this device",
- "source_server": "Answered by Resgrid",
- "span_all_good": "Span of control looks reasonable — every lane is within its limits and no lane is over {{ceiling}} resources.",
- "span_header": "{{incident}}: {{lanes}} lanes, {{resources}} resources assigned.",
- "span_no_lead": "- No lead assigned: {{lanes}}",
- "span_over_row": "- {{lane}} is carrying {{count}} resources (limit {{limit}}) — consider splitting it or adding a supervisor.",
- "span_under_row": "- {{lane}} has {{count}} resources against a minimum of {{minimum}}.",
- "staging": "Staging: {{location}}.",
+ "role_unfilled": "{{incident}}: роль «{{role}}» не призначено.",
+ "role_unknown": "Не розпізнаю посаду «{{role}}». Запитайте про ролі ICS, щоб побачити всі призначення.",
+ "roles_header": "{{incident}}: призначено посад ICS: {{count}}.",
+ "roles_unfilled": "Незаповнені посади, зазвичай потрібні для типу «{{type}}»: {{roles}}",
+ "send": "Надіслати",
+ "source_device": "Відповідь сформовано на цьому пристрої",
+ "source_server": "Відповідь від Resgrid",
+ "span_all_good": "Норма керованості в порядку — кожна ділянка в межах своїх лімітів, і жодна не має понад {{ceiling}} ресурсів.",
+ "span_header": "{{incident}}: ділянок — {{lanes}}, призначено ресурсів — {{resources}}.",
+ "span_no_lead": "- Без керівника: {{lanes}}",
+ "span_over_row": "- {{lane}}: ресурсів {{count}} при ліміті {{limit}} — варто розділити ділянку або додати керівника.",
+ "span_under_row": "- {{lane}}: ресурсів {{count}} при мінімумі {{minimum}}.",
+ "staging": "Зона очікування: {{location}}.",
"suggestions": {
- "briefing": "Transfer of command",
- "missing": "What am I missing?",
- "on_scene": "What's on scene?",
- "open_needs": "Open needs",
- "open_objectives": "Open objectives",
+ "briefing": "Передача командування",
+ "missing": "Що пропущено?",
+ "on_scene": "Що на місці?",
+ "open_needs": "Незакриті потреби",
+ "open_objectives": "Відкриті цілі",
"par": "PAR",
- "recent": "Last 30 minutes",
- "rit": "Do I have a RIT?",
- "safety": "Safety Officer",
- "span": "Span of control",
- "status": "Incident status",
- "timers": "Timers",
- "unassigned": "Unassigned",
- "unfilled_roles": "Unfilled ICS positions",
- "wind": "Wind and weather"
+ "recent": "Останні 30 хвилин",
+ "rit": "Чи є група RIT?",
+ "safety": "Офіцер безпеки",
+ "span": "Норма керованості",
+ "status": "Стан інциденту",
+ "timers": "Таймери",
+ "unassigned": "Не призначено",
+ "unfilled_roles": "Незаповнені посади ICS",
+ "wind": "Вітер і погода"
},
- "thinking": "Checking the board…",
- "this_incident": "this incident",
- "time_in_lane": "({{duration}} in lane)",
- "timeline_empty": "Nothing has been logged on {{incident}} yet.",
- "timeline_empty_window": "Nothing has been logged in the last {{minutes}} minutes on {{incident}}.",
- "timeline_header": "{{incident}} — last {{count}} log entries:",
+ "thinking": "Перевіряю дошку…",
+ "this_incident": "Цей інцидент",
+ "time_in_lane": "({{duration}} на ділянці)",
+ "timeline_empty": "{{incident}}: у журналі ще немає записів.",
+ "timeline_empty_window": "{{incident}}: за останні {{minutes}} хв у журналі немає записів.",
+ "timeline_header": "{{incident}} — останні записи журналу ({{count}}):",
"timeline_row": "- {{time}}: {{description}}{{who}}",
- "timeline_window_header": "{{incident}} — last {{minutes}} minutes ({{count}} entries):",
- "timer_due_row": "- {{name}}: DUE NOW",
- "timer_no_due_row": "- {{name}}: running",
- "timer_running_row": "- {{name}}: due in {{remaining}}",
- "timers_header": "{{incident}}: {{count}} timer(s).",
- "title": "Incident Assistant",
- "unassigned_header": "Unassigned on {{incident}} ({{count}}):",
- "unassigned_line": "- Unassigned pool: {{count}}",
- "unknown": "unknown"
+ "timeline_window_header": "{{incident}} — останні {{minutes}} хв (записів: {{count}}):",
+ "timer_due_row": "- {{name}}: НАСТАВ ЧАС",
+ "timer_no_due_row": "- {{name}}: працює",
+ "timer_running_row": "- {{name}}: залишилося {{remaining}}",
+ "timers_header": "{{incident}}: таймерів — {{count}}.",
+ "title": "Помічник інциденту",
+ "unassigned_header": "{{incident}} — не призначено ({{count}}):",
+ "unassigned_line": "- Не призначено: {{count}}",
+ "unknown": "невідомо"
},
"incidents": {
"accountability": "Облік персоналу",
@@ -1304,13 +1307,16 @@
"2": "Скасований",
"3": "Безпідставний",
"4": "Обґрунтований",
- "5": "Незначний"
+ "5": "Незначний",
+ "6": "Передано",
+ "7": "Хибна тривога",
+ "8": "В очікуванні"
},
"unknown_priority": "Невідомий",
"unknown_user": "Невідомий користувач"
},
"login": {
- "command_not_authorized": "You are not authorized to use the IC app. Contact your department administrator.",
+ "command_not_authorized": "Ви не маєте доступу до додатка IC. Зверніться до адміністратора свого підрозділу.",
"errorModal": {
"confirmButton": "OK",
"message": "Перевірте ім'я користувача та пароль і спробуйте ще раз.",
@@ -1323,6 +1329,11 @@
"login_button_loading": "Вхід...",
"login_button_success": "Вхід виконано успішно",
"or_sign_in_with_password": "або увійдіть з паролем",
+ "otp_body": "Для вашого облікового запису ввімкнено двофакторну автентифікацію. Введіть поточний код із застосунку автентифікації, щоб завершити вхід.",
+ "otp_invalid": "Цей код недійсний або прострочений. Введіть поточний код із застосунку автентифікації.",
+ "otp_placeholder": "6-значний код",
+ "otp_title": "Двофакторна перевірка",
+ "otp_verify": "Підтвердити",
"password": "Пароль",
"password_incorrect": "Пароль невірний",
"password_placeholder": "Введіть пароль",
@@ -1661,6 +1672,7 @@
"timereports_date_exists": "Звіт для цього екіпажу або особи за той день уже існує.",
"timereports_locked": "Цей звіт більше не можна змінити.",
"timereports_scope_invalid": "Цей екіпаж або особа не активні в цьому розгортанні.",
+ "timereports_status_transition_invalid": "Цей звіт уже змінено, можливо, на іншому пристрої. Оновіть і спробуйте ще раз.",
"timereports_subject_covered": "Цей екіпаж або особа вже мають час в іншому звіті за цей день.",
"timereports_time_invalid": "Вводьте час у форматі HH:mm.",
"validation": "Сервер відхилив зміну."
@@ -2123,11 +2135,11 @@
"poi_filter_all_types": "Усі типи POI",
"poi_filter_placeholder": "Фільтрувати за типом",
"poi_filter_type": "Тип POI",
- "poi_filters": "POI Filters",
+ "poi_filters": "Фільтри",
"poi_not_found": "POI не знайдено",
"poi_not_found_description": "Цей POI не вдалося знайти.",
"poi_note": "Нотатка",
- "poi_sort_by": "Sort by",
+ "poi_sort_by": "Сортувати за",
"poi_sort_display": "Назва",
"poi_sort_placeholder": "Сортувати POI",
"poi_sort_type": "Тип",
@@ -2226,7 +2238,7 @@
"help_center": "Центр допомоги",
"italian": "Італійська",
"keep_alive": "Підтримувати активність",
- "keep_alive_warning": "Увага: Увімкнення keep alive запобігає переходу пристрою в режим сну та може значно збільшити споживання заряду батареї.",
+ "keep_alive_warning": "Увага: увімкнення функції «Підтримувати активність» не дає пристрою переходити в режим сну та може значно збільшити споживання заряду батареї.",
"keep_screen_on": "Тримати екран увімкненим",
"language": "Мова",
"links": "Посилання",
diff --git a/theme-tokens.css b/theme-tokens.css
index 8bbcad9..f2066c4 100644
--- a/theme-tokens.css
+++ b/theme-tokens.css
@@ -145,6 +145,8 @@
@media (prefers-color-scheme: dark) {
:root {
+ /* Primary is the light ramp reversed, like every other scale: 600+ must be light, since
+ text-primary-600, focus borders and checked controls sit on dark surfaces here. */
--color-primary-0: 15 23 42;
--color-primary-50: 30 41 59;
--color-primary-100: 39 49 66;
@@ -152,11 +154,11 @@
--color-primary-300: 51 65 85;
--color-primary-400: 71 85 105;
--color-primary-500: 100 116 139;
- --color-primary-600: 71 85 105;
- --color-primary-700: 51 65 85;
- --color-primary-800: 45 55 72;
- --color-primary-900: 39 49 66;
- --color-primary-950: 30 41 59;
+ --color-primary-600: 148 163 184;
+ --color-primary-700: 203 213 225;
+ --color-primary-800: 226 232 240;
+ --color-primary-900: 241 245 249;
+ --color-primary-950: 248 250 252;
--color-secondary-0: 11 12 12;
--color-secondary-50: 24 23 23;
--color-secondary-100: 39 38 38;
@@ -284,11 +286,11 @@
--color-primary-300: 51 65 85;
--color-primary-400: 71 85 105;
--color-primary-500: 100 116 139;
- --color-primary-600: 71 85 105;
- --color-primary-700: 51 65 85;
- --color-primary-800: 45 55 72;
- --color-primary-900: 39 49 66;
- --color-primary-950: 30 41 59;
+ --color-primary-600: 148 163 184;
+ --color-primary-700: 203 213 225;
+ --color-primary-800: 226 232 240;
+ --color-primary-900: 241 245 249;
+ --color-primary-950: 248 250 252;
--color-secondary-0: 11 12 12;
--color-secondary-50: 24 23 23;
--color-secondary-100: 39 38 38;