From 5fe3dea4cccf9a8d21bc5e7dc7fb64d9ffaf8e45 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 13 Nov 2026 21:57:20 +0100 Subject: [PATCH] feat(settings): collapsible settings cards, first on the integrations tab The settings pages list everything at once, so they read as a wall of forms. SettingsCard and Section take 'collapsible': the card starts shut and its badge says its state ('Connected', 'Not set up'); its title is the button, stretched over the head, the head's action stays its own button. The body stays mounted while shut. A link to #id opens the card and scrolls to it; the browser remembers a card left open. Every integrations card is collapsible now; AI parsing shows its saved model and MCP the apps signed in. The assistant's settings link opens the AI connection card. Tests that read into a card open it first (tests/helpers/settingsCards.ts). --- .../components/Assistant/assistantParts.ts | 3 +- .../AirTrailConnectionSection.test.tsx | 3 + .../Settings/AirTrailConnectionSection.tsx | 2 + .../Settings/ApiKeysSection.test.tsx | 3 + .../components/Settings/ApiKeysSection.tsx | 2 + .../DawarichConnectionSection.test.tsx | 3 + .../Settings/DawarichConnectionSection.tsx | 2 + .../Settings/IntegrationsTab.test.tsx | 3 + .../components/Settings/IntegrationsTab.tsx | 16 ++- .../Settings/LlmConnectionSection.test.tsx | 3 + .../Settings/LlmConnectionSection.tsx | 20 +++- .../components/Settings/MailImportSection.tsx | 2 + .../Settings/PhotoProvidersSection.test.tsx | 3 + .../Settings/PhotoProvidersSection.tsx | 2 + client/src/components/Settings/Section.tsx | 8 +- .../WandererConnectionSection.test.tsx | 3 + .../Settings/WandererConnectionSection.tsx | 2 + .../ai/AiConnectionSection.mapFollow.test.tsx | 3 + .../Settings/ai/AiConnectionSection.test.tsx | 3 + .../Settings/ai/AiConnectionSection.tsx | 2 + .../components/Settings/settingsKit.test.tsx | 65 ++++++++++++ .../src/components/Settings/settingsKit.tsx | 99 +++++++++++++++++-- client/src/pages/SettingsPage.test.tsx | 3 + client/tests/helpers/settingsCards.ts | 26 +++++ shared/src/i18n/ar/settings.ts | 7 ++ shared/src/i18n/az/settings.ts | 3 + shared/src/i18n/br/settings.ts | 3 + shared/src/i18n/ca/settings.ts | 3 + shared/src/i18n/cs/settings.ts | 4 + shared/src/i18n/de/settings.ts | 3 + shared/src/i18n/en/settings.ts | 3 + shared/src/i18n/es/settings.ts | 3 + shared/src/i18n/et/settings.ts | 3 + shared/src/i18n/fr/settings.ts | 3 + shared/src/i18n/gr/settings.ts | 3 + shared/src/i18n/he/settings.ts | 4 + shared/src/i18n/hu/settings.ts | 3 + shared/src/i18n/id/settings.ts | 2 + shared/src/i18n/it/settings.ts | 3 + shared/src/i18n/ja/settings.ts | 2 + shared/src/i18n/ko/settings.ts | 2 + shared/src/i18n/nl/settings.ts | 3 + shared/src/i18n/pl/settings.ts | 5 + shared/src/i18n/ru/settings.ts | 5 + shared/src/i18n/sk/settings.ts | 4 + shared/src/i18n/sv/settings.ts | 3 + shared/src/i18n/th/settings.ts | 2 + shared/src/i18n/tr/settings.ts | 3 + shared/src/i18n/uk/settings.ts | 5 + shared/src/i18n/vi/settings.ts | 2 + shared/src/i18n/zh-TW/settings.ts | 2 + shared/src/i18n/zh/settings.ts | 2 + 52 files changed, 355 insertions(+), 16 deletions(-) create mode 100644 client/src/components/Settings/settingsKit.test.tsx create mode 100644 client/tests/helpers/settingsCards.ts diff --git a/client/src/components/Assistant/assistantParts.ts b/client/src/components/Assistant/assistantParts.ts index 5ba2dd7ee..5d6050496 100644 --- a/client/src/components/Assistant/assistantParts.ts +++ b/client/src/components/Assistant/assistantParts.ts @@ -28,7 +28,8 @@ import type { CardEntry } from './cards/cardTypes'; */ /** Where the settings card for the user's own AI connection lives. */ -export const AI_SETTINGS_PATH = '/settings?tab=integrations'; +/** The AI connection card itself: the anchor opens it, shut as it otherwise starts. */ +export const AI_SETTINGS_PATH = '/settings?tab=integrations#ai-connection'; type ToolCallPart = Extract; diff --git a/client/src/components/Settings/AirTrailConnectionSection.test.tsx b/client/src/components/Settings/AirTrailConnectionSection.test.tsx index cefc47a5c..a26f00002 100644 --- a/client/src/components/Settings/AirTrailConnectionSection.test.tsx +++ b/client/src/components/Settings/AirTrailConnectionSection.test.tsx @@ -6,6 +6,7 @@ import { server } from '../../../tests/helpers/msw/server'; import { resetAllStores } from '../../../tests/helpers/store'; import { ToastContainer } from '../shared/Toast'; import AirTrailConnectionSection from './AirTrailConnectionSection'; +import { openSettingsCards } from '../../../tests/helpers/settingsCards'; interface SavedBody { url: string; @@ -37,6 +38,8 @@ function toggleNextTo(label: string): HTMLElement { } beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); stubSettings(); diff --git a/client/src/components/Settings/AirTrailConnectionSection.tsx b/client/src/components/Settings/AirTrailConnectionSection.tsx index ddb780706..c7df67706 100644 --- a/client/src/components/Settings/AirTrailConnectionSection.tsx +++ b/client/src/components/Settings/AirTrailConnectionSection.tsx @@ -40,6 +40,8 @@ export default function AirTrailConnectionSection(): React.ReactElement { return (
{ }); beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); clipboardWriteText.mockReset(); clipboardWriteText.mockResolvedValue(undefined); resetAllStores(); diff --git a/client/src/components/Settings/ApiKeysSection.tsx b/client/src/components/Settings/ApiKeysSection.tsx index c8f845756..175d80480 100644 --- a/client/src/components/Settings/ApiKeysSection.tsx +++ b/client/src/components/Settings/ApiKeysSection.tsx @@ -238,6 +238,8 @@ export default function ApiKeysSection(): React.ReactElement { return ( <>
0 ? {keys.length}/{MAX_KEYS} : undefined} diff --git a/client/src/components/Settings/DawarichConnectionSection.test.tsx b/client/src/components/Settings/DawarichConnectionSection.test.tsx index 7a3f78e7c..14765012f 100644 --- a/client/src/components/Settings/DawarichConnectionSection.test.tsx +++ b/client/src/components/Settings/DawarichConnectionSection.test.tsx @@ -46,6 +46,7 @@ import { render, screen, waitFor } from '../../../tests/helpers/render'; import { server } from '../../../tests/helpers/msw/server'; import { resetAllStores } from '../../../tests/helpers/store'; import DawarichConnectionSection from './DawarichConnectionSection'; +import { openSettingsCards } from '../../../tests/helpers/settingsCards'; const toast = { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }; vi.mock('../shared/Toast', () => ({ @@ -156,6 +157,8 @@ function testButton(): HTMLElement { } beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); stubSettings(); diff --git a/client/src/components/Settings/DawarichConnectionSection.tsx b/client/src/components/Settings/DawarichConnectionSection.tsx index 97c6c0704..95dd046c3 100644 --- a/client/src/components/Settings/DawarichConnectionSection.tsx +++ b/client/src/components/Settings/DawarichConnectionSection.tsx @@ -29,6 +29,8 @@ export default function DawarichConnectionSection(): React.ReactElement { return (
{ }); beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); clipboardWriteText.mockClear(); resetAllStores(); useAssistantStore.setState({ status: null }); diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx index 320baf737..abe042a56 100644 --- a/client/src/components/Settings/IntegrationsTab.tsx +++ b/client/src/components/Settings/IntegrationsTab.tsx @@ -346,9 +346,21 @@ function ApiTokensTab({ S }: { S: IntegrationsState }) { } function IntegrationsMcpSection({ S }: { S: IntegrationsState }) { - const { t, activeMcpTab, setActiveMcpTab, copiedKey, mcpEndpoint, handleCopy } = S + const { t, activeMcpTab, setActiveMcpTab, copiedKey, mcpEndpoint, handleCopy, oauthSessions } = S + // Connected means signed in now: an OAuth session, which is what an app holds while it reaches TREK. + const connected = oauthSessions.length return ( -
+
0 + ? }>{t('settings.mcp.appsConnected', { count: connected })} + : {t('settings.notSetUp')} + } + > {/* Endpoint URL */} handleCopy(mcpEndpoint, 'endpoint')} /> diff --git a/client/src/components/Settings/LlmConnectionSection.test.tsx b/client/src/components/Settings/LlmConnectionSection.test.tsx index aeb982a32..29a3250f2 100644 --- a/client/src/components/Settings/LlmConnectionSection.test.tsx +++ b/client/src/components/Settings/LlmConnectionSection.test.tsx @@ -8,6 +8,7 @@ import { useAuthStore } from '../../store/authStore'; import type { Settings } from '../../types'; import { ToastContainer } from '../shared/Toast'; import LlmConnectionSection from './LlmConnectionSection'; +import { openSettingsCards } from '../../../tests/helpers/settingsCards'; let loadSettings = vi.fn().mockResolvedValue(undefined); @@ -50,6 +51,8 @@ async function pickProvider(user: ReturnType, current: R } beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); seedRole('user'); diff --git a/client/src/components/Settings/LlmConnectionSection.tsx b/client/src/components/Settings/LlmConnectionSection.tsx index 34701c629..1cfdc52e1 100644 --- a/client/src/components/Settings/LlmConnectionSection.tsx +++ b/client/src/components/Settings/LlmConnectionSection.tsx @@ -4,7 +4,8 @@ import { useTranslation } from '../../i18n' import { useLlmConnection, type LlmProvider as Provider } from './useLlmConnection' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint } from './settingsKit' +import { SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill } from './settingsKit' +import { useSettingsStore } from '../../store/settingsStore' import { EditorField, GRID_2, INPUT, Segmented } from '../shared/dialogParts' import { fs } from '../shared/DialogShell' @@ -42,7 +43,13 @@ export default function LlmConnectionSection(): React.ReactElement { } = useLlmConnection() return ( -
+
} + > {t('settings.aiParsing.hint')} {/* Two hosted providers only (#1772), so a segmented pair rather than a list. */} @@ -103,3 +110,12 @@ export default function LlmConnectionSection(): React.ReactElement {
) } + +/** The saved model at a glance on the shut card, or that none is set; not the form's unsaved value. */ +function ParsingState() { + const { t } = useTranslation() + const model = useSettingsStore(s => s.settings.llm_model) + return model + ? }>{model} + : {t('settings.notSetUp')} +} diff --git a/client/src/components/Settings/MailImportSection.tsx b/client/src/components/Settings/MailImportSection.tsx index c1107d60e..8dc2561c5 100644 --- a/client/src/components/Settings/MailImportSection.tsx +++ b/client/src/components/Settings/MailImportSection.tsx @@ -30,6 +30,8 @@ export default function MailImportSection(): ReactElement | null { return (
{ + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true }); diff --git a/client/src/components/Settings/PhotoProvidersSection.tsx b/client/src/components/Settings/PhotoProvidersSection.tsx index a1012c97d..42e108884 100644 --- a/client/src/components/Settings/PhotoProvidersSection.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.tsx @@ -35,6 +35,8 @@ export default function PhotoProvidersSection(): React.ReactElement { return (
+ {children} ) diff --git a/client/src/components/Settings/WandererConnectionSection.test.tsx b/client/src/components/Settings/WandererConnectionSection.test.tsx index 58b72d54b..acd542904 100644 --- a/client/src/components/Settings/WandererConnectionSection.test.tsx +++ b/client/src/components/Settings/WandererConnectionSection.test.tsx @@ -4,6 +4,7 @@ import { http, HttpResponse } from 'msw'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { server } from '../../../tests/helpers/msw/server'; import { render, screen, waitFor } from '../../../tests/helpers/render'; +import { openSettingsCards } from '../../../tests/helpers/settingsCards'; import { resetAllStores } from '../../../tests/helpers/store'; import WandererConnectionSection from './WandererConnectionSection'; @@ -31,6 +32,8 @@ function settingsRoute(state: { current: typeof disconnected }) { } beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); }); diff --git a/client/src/components/Settings/WandererConnectionSection.tsx b/client/src/components/Settings/WandererConnectionSection.tsx index 58cf6a22c..2301b6f88 100644 --- a/client/src/components/Settings/WandererConnectionSection.tsx +++ b/client/src/components/Settings/WandererConnectionSection.tsx @@ -31,6 +31,8 @@ export default function WandererConnectionSection(): React.ReactElement { return (
{ + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); useAssistantStore.setState({ status: null }); diff --git a/client/src/components/Settings/ai/AiConnectionSection.test.tsx b/client/src/components/Settings/ai/AiConnectionSection.test.tsx index f7c7fb0c0..dd294fa86 100644 --- a/client/src/components/Settings/ai/AiConnectionSection.test.tsx +++ b/client/src/components/Settings/ai/AiConnectionSection.test.tsx @@ -4,6 +4,7 @@ import type { AiConnectionView, AiStatusResponse } from '@trek/shared'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { buildAdmin, buildUser } from '../../../../tests/helpers/factories'; import { render, screen, waitFor } from '../../../../tests/helpers/render'; +import { openSettingsCards } from '../../../../tests/helpers/settingsCards'; import { resetAllStores, seedStore } from '../../../../tests/helpers/store'; import { useAddonStore } from '../../../store/addonStore'; import { useAssistantStore } from '../../../store/assistantStore'; @@ -48,6 +49,8 @@ function status(over: Partial = {}): AiStatusResponse { } beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); vi.clearAllMocks(); useAssistantStore.setState({ status: null }); diff --git a/client/src/components/Settings/ai/AiConnectionSection.tsx b/client/src/components/Settings/ai/AiConnectionSection.tsx index 290c416c4..7e2f063b1 100644 --- a/client/src/components/Settings/ai/AiConnectionSection.tsx +++ b/client/src/components/Settings/ai/AiConnectionSection.tsx @@ -282,6 +282,8 @@ export default function AiConnectionSection({ return (
Connected} + action={} + {...props} + > + + + ); +} + +afterEach(() => { + window.location.hash = ''; +}); + +describe('SettingsCard', () => { + it('FE-SETTINGS-KIT-001: a collapsible card starts shut, its state still showing in the head', () => { + render(card({ collapsible: true, id: 'airtrail' })); + const head = screen.getByRole('button', { name: 'AirTrail' }); + expect(head).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByText('Connected')).toBeVisible(); + expect(screen.queryByRole('textbox', { name: 'Address' })).toBeNull(); + // The head's own action stays a button of its own. + expect(screen.getByRole('button', { name: 'Sync' })).toBeVisible(); + }); + + it('FE-SETTINGS-KIT-002: the head opens and shuts it, and the browser remembers it open', () => { + const { unmount } = render(card({ collapsible: true, id: 'airtrail' })); + fireEvent.click(screen.getByRole('button', { name: 'AirTrail' })); + expect(screen.getByRole('button', { name: 'AirTrail' })).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByRole('textbox', { name: 'Address' })).toBeVisible(); + expect(localStorage.getItem(settingsCardOpenKey('airtrail'))).toBe('1'); + + unmount(); + render(card({ collapsible: true, id: 'airtrail' })); + expect(screen.getByRole('textbox', { name: 'Address' })).toBeVisible(); + fireEvent.click(screen.getByRole('button', { name: 'AirTrail' })); + expect(screen.queryByRole('textbox', { name: 'Address' })).toBeNull(); + expect(localStorage.getItem(settingsCardOpenKey('airtrail'))).toBe('0'); + }); + + it('FE-SETTINGS-KIT-003: a link to the card opens it', () => { + window.location.hash = '#airtrail'; + render(card({ collapsible: true, id: 'airtrail' })); + expect(screen.getByRole('textbox', { name: 'Address' })).toBeVisible(); + }); + + it('FE-SETTINGS-KIT-004: a card that is not collapsible has no toggle and shows its body', () => { + render(card()); + expect(screen.queryByRole('button', { name: 'AirTrail' })).toBeNull(); + expect(screen.getByRole('heading', { name: 'AirTrail' })).toBeVisible(); + expect(screen.getByRole('textbox', { name: 'Address' })).toBeVisible(); + }); +}); diff --git a/client/src/components/Settings/settingsKit.tsx b/client/src/components/Settings/settingsKit.tsx index 150d156aa..bd04603c7 100644 --- a/client/src/components/Settings/settingsKit.tsx +++ b/client/src/components/Settings/settingsKit.tsx @@ -1,5 +1,5 @@ -import type { ReactNode } from 'react' -import type { LucideIcon } from 'lucide-react' +import { useEffect, useId, useRef, useState, type ReactNode } from 'react' +import { ChevronDown, type LucideIcon } from 'lucide-react' import { NEUTRAL_TINT, fs } from '../shared/DialogShell' /* @@ -33,12 +33,55 @@ export function SettingsHeader({ icon: Icon, title, subtitle, actions }: { ) } +/** Where a collapsible card remembers that its reader left it open. */ +export const settingsCardOpenKey = (id: string) => `trek-settings-open-${id}` + +/** + * Whether a collapsible card is open. Shut by default, so a page lists what it + * holds instead of everything at once; open when the address names it (a link + * to `#id` lands on it ready to use), or when its reader left it open last time. + * The browser remembers that per card as a convenience: without storage (a + * private window, blocked site data) every card simply starts shut. + */ +export function useCardOpen(id: string | undefined, collapsible: boolean): [boolean, () => void] { + const [open, setOpen] = useState(() => { + if (!collapsible) return true + if (id && window.location.hash === `#${id}`) return true + if (!id) return false + try { + return localStorage.getItem(settingsCardOpenKey(id)) === '1' + } catch { + // Storage refused: the card starts shut, as it would on a first visit. + return false + } + }) + const toggle = () => { + const next = !open + setOpen(next) + if (!id) return + try { + localStorage.setItem(settingsCardOpenKey(id), next ? '1' : '0') + } catch { + // Storage refused: the card still opens, it is only not remembered. + } + } + return [!collapsible || open, toggle] +} + /** * A group of settings: a card with a head band (icon tile, title, an optional * badge and an action on the right) and its body below. `tone` tints the band: * neutral for ordinary groups, danger for the destructive ones. + * + * `collapsible` shuts the body until the head is clicked. The badge is then the + * card's state at a glance ("Connected", "Off"), so a shut card still says what + * matters. The title holds the button (a heading inside a button is not valid + * HTML), stretched over the whole head; the badge and the action sit above it + * and stay clickable on their own. The body stays mounted while shut, so + * nothing typed into it is lost. A collapsible card needs an `id` to be linked + * to and remembered. */ -export function SettingsCard({ icon: Icon, title, hint, badge, action, tone = 'neutral', children, id, bodyClassName, flush = false }: { +export function SettingsCard({ icon: Icon, title, hint, badge, action, tone = 'neutral', children, id, bodyClassName, flush = false, collapsible = false }: { icon?: IconLike title: ReactNode /** One line under the title in the band. */ @@ -52,11 +95,24 @@ export function SettingsCard({ icon: Icon, title, hint, badge, action, tone = 'n bodyClassName?: string /** Inside another card: no gap below, the parent spaces its children. */ flush?: boolean + /** Shut until its head is clicked; see above. */ + collapsible?: boolean }) { const tint = tone === 'danger' ? 'color-mix(in srgb, var(--danger) 9%, transparent)' : NEUTRAL_TINT + const [open, toggle] = useCardOpen(id, collapsible) + const bodyId = useId() + const hasBody = children != null + const ref = useRef(null) + // A link to `#id` opened this card; bring it into view, as the router does not scroll to an anchor. + useEffect(() => { + if (id && window.location.hash === `#${id}`) ref.current?.scrollIntoView?.({ block: 'start' }) + }, [id]) return ( -
-
+
+
{Icon && ( @@ -64,14 +120,39 @@ export function SettingsCard({ icon: Icon, title, hint, badge, action, tone = 'n )}
-

{title}

- {badge} +

+ {collapsible ? ( + + ) : title} +

+ {badge && {badge}}
{hint &&

{hint}

}
- {action &&
{action}
} + {action &&
{action}
} + {collapsible && ( + + )}
- {children != null &&
{children}
} + {/* The hidden wrapper has no classes of its own: a display utility on it would beat the attribute. */} + {hasBody && ( + + )}
) } diff --git a/client/src/pages/SettingsPage.test.tsx b/client/src/pages/SettingsPage.test.tsx index 32753916f..be983340b 100644 --- a/client/src/pages/SettingsPage.test.tsx +++ b/client/src/pages/SettingsPage.test.tsx @@ -6,6 +6,7 @@ import { buildUser } from '../../tests/helpers/factories'; import { useAuthStore } from '../store/authStore'; import { useAddonStore } from '../store/addonStore'; import SettingsPage from './SettingsPage'; +import { openSettingsCards } from '../../tests/helpers/settingsCards'; // Mock heavy settings sub-tabs to focus on page-level concerns vi.mock('../components/Settings/DisplaySettingsTab', () => ({ @@ -35,6 +36,8 @@ vi.mock('../components/Settings/AboutTab', () => ({ })); beforeEach(() => { + // The cards start shut; these tests read into them. + openSettingsCards(); resetAllStores(); seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() }); }); diff --git a/client/tests/helpers/settingsCards.ts b/client/tests/helpers/settingsCards.ts new file mode 100644 index 000000000..d36185d6a --- /dev/null +++ b/client/tests/helpers/settingsCards.ts @@ -0,0 +1,26 @@ +import { settingsCardOpenKey } from '../../src/components/Settings/settingsKit'; + +/** The photo services the settings tests connect, each its own card (`photos-`). */ +const PHOTO_PROVIDERS = ['immich', 'synology', 'piwigo', 'nextcloud', 'google']; + +/** Every collapsible card of the integrations tab, by the id it is linked and remembered by. */ +export const INTEGRATION_CARDS: readonly string[] = [ + 'mail-import', + 'airtrail', + 'dawarich', + 'wanderer', + 'ai-connection', + 'ai-parsing', + 'api-keys', + 'mcp', + ...PHOTO_PROVIDERS.map((id) => `photos-${id}`), +]; + +/** + * Leaves settings cards open, as if their reader had left them so: a test then + * reads into a card without clicking its head first. Storage is cleared after + * every test (tests/setup.ts), so call it in a beforeEach. + */ +export function openSettingsCards(ids: readonly string[] = INTEGRATION_CARDS): void { + for (const id of ids) localStorage.setItem(settingsCardOpenKey(id), '1'); +} diff --git a/shared/src/i18n/ar/settings.ts b/shared/src/i18n/ar/settings.ts index 7d2a5cc3a..6e9fb9035 100644 --- a/shared/src/i18n/ar/settings.ts +++ b/shared/src/i18n/ar/settings.ts @@ -75,6 +75,13 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'لبيانات الطقس.', 'settings.keyPlaceholder': 'أدخل المفتاح...', 'settings.configured': 'مُعدّ', + 'settings.notSetUp': 'غير مُعدّ', + 'settings.mcp.appsConnected': '{count} تطبيق متصل', + 'settings.mcp.appsConnected.zero': '{count} تطبيقات متصلة', + 'settings.mcp.appsConnected.one': '{count} تطبيق متصل', + 'settings.mcp.appsConnected.two': '{count} تطبيقان متصلان', + 'settings.mcp.appsConnected.few': '{count} تطبيقات متصلة', + 'settings.mcp.appsConnected.many': '{count} تطبيقًا متصلًا', 'settings.saveKeys': 'حفظ المفاتيح', 'settings.display': 'العرض', 'settings.colorMode': 'نمط الألوان', diff --git a/shared/src/i18n/az/settings.ts b/shared/src/i18n/az/settings.ts index d05d8c967..feb2d9d59 100644 --- a/shared/src/i18n/az/settings.ts +++ b/shared/src/i18n/az/settings.ts @@ -101,6 +101,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Hava məlumatları üçün. openweathermap.org/api ünvanında pulsuzdur', 'settings.keyPlaceholder': 'Açarı daxil edin...', 'settings.configured': 'Konfiqurasiya edilib', + 'settings.notSetUp': 'Qurulmayıb', + 'settings.mcp.appsConnected': '{count} tətbiq qoşulub', + 'settings.mcp.appsConnected.one': '{count} tətbiq qoşulub', 'settings.saveKeys': 'Açarları yadda saxla', 'settings.display': 'Görünüş', diff --git a/shared/src/i18n/br/settings.ts b/shared/src/i18n/br/settings.ts index 73e27e8c0..b12dca353 100644 --- a/shared/src/i18n/br/settings.ts +++ b/shared/src/i18n/br/settings.ts @@ -82,6 +82,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Para dados meteorológicos. Grátis em openweathermap.org/api', 'settings.keyPlaceholder': 'Digite a chave...', 'settings.configured': 'Configurada', + 'settings.notSetUp': 'Não configurado', + 'settings.mcp.appsConnected': '{count} apps conectados', + 'settings.mcp.appsConnected.one': '{count} app conectado', 'settings.saveKeys': 'Salvar chaves', 'settings.display': 'Exibição', 'settings.colorMode': 'Tema de cores', diff --git a/shared/src/i18n/ca/settings.ts b/shared/src/i18n/ca/settings.ts index e705fbd01..dfa38d167 100644 --- a/shared/src/i18n/ca/settings.ts +++ b/shared/src/i18n/ca/settings.ts @@ -68,6 +68,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Per a dades meteorològiques. Gratuïta a openweathermap.org/api', 'settings.keyPlaceholder': 'Introdueix la clau...', 'settings.configured': 'Configurat', + 'settings.notSetUp': 'No configurat', + 'settings.mcp.appsConnected': '{count} aplicacions connectades', + 'settings.mcp.appsConnected.one': '{count} aplicació connectada', 'settings.saveKeys': 'Desa les claus', 'settings.display': 'Visualització', 'settings.colorMode': 'Mode de color', diff --git a/shared/src/i18n/cs/settings.ts b/shared/src/i18n/cs/settings.ts index 1f5605c3d..2d570e4d6 100644 --- a/shared/src/i18n/cs/settings.ts +++ b/shared/src/i18n/cs/settings.ts @@ -79,6 +79,10 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Pro předpověď počasí. Zdarma na openweathermap.org/api', 'settings.keyPlaceholder': 'Vložte klíč...', 'settings.configured': 'Nastaveno', + 'settings.notSetUp': 'Nenastaveno', + 'settings.mcp.appsConnected': '{count} aplikací připojeno', + 'settings.mcp.appsConnected.one': '{count} aplikace připojena', + 'settings.mcp.appsConnected.few': '{count} aplikace připojeny', 'settings.saveKeys': 'Uložit klíče', 'settings.display': 'Zobrazení', 'settings.colorMode': 'Barevné schéma', diff --git a/shared/src/i18n/de/settings.ts b/shared/src/i18n/de/settings.ts index 298719823..4d5a5fcdc 100644 --- a/shared/src/i18n/de/settings.ts +++ b/shared/src/i18n/de/settings.ts @@ -81,6 +81,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Für Wetterdaten. Kostenlos unter openweathermap.org/api', 'settings.keyPlaceholder': 'Schlüssel eingeben...', 'settings.configured': 'Konfiguriert', + 'settings.notSetUp': 'Nicht eingerichtet', + 'settings.mcp.appsConnected': '{count} Apps verbunden', + 'settings.mcp.appsConnected.one': '{count} App verbunden', 'settings.saveKeys': 'Schlüssel speichern', 'settings.display': 'Darstellung', 'settings.colorMode': 'Farbmodus', diff --git a/shared/src/i18n/en/settings.ts b/shared/src/i18n/en/settings.ts index 528c65aea..29517d852 100644 --- a/shared/src/i18n/en/settings.ts +++ b/shared/src/i18n/en/settings.ts @@ -88,6 +88,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'For weather data. Free at openweathermap.org/api', 'settings.keyPlaceholder': 'Enter key...', 'settings.configured': 'Configured', + 'settings.notSetUp': 'Not set up', + 'settings.mcp.appsConnected': '{count} apps connected', + 'settings.mcp.appsConnected.one': '{count} app connected', 'settings.saveKeys': 'Save Keys', 'settings.display': 'Display', 'settings.colorMode': 'Color Mode', diff --git a/shared/src/i18n/es/settings.ts b/shared/src/i18n/es/settings.ts index 3ca1f2f54..5a5d6abd4 100644 --- a/shared/src/i18n/es/settings.ts +++ b/shared/src/i18n/es/settings.ts @@ -82,6 +82,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Para datos meteorológicos. Gratis en openweathermap.org/api', 'settings.keyPlaceholder': 'Introduce la clave...', 'settings.configured': 'Configurado', + 'settings.notSetUp': 'Sin configurar', + 'settings.mcp.appsConnected': '{count} apps conectadas', + 'settings.mcp.appsConnected.one': '{count} app conectada', 'settings.saveKeys': 'Guardar claves', 'settings.display': 'Visualización', 'settings.colorMode': 'Modo de color', diff --git a/shared/src/i18n/et/settings.ts b/shared/src/i18n/et/settings.ts index 02aa53f70..dd0bef4d4 100644 --- a/shared/src/i18n/et/settings.ts +++ b/shared/src/i18n/et/settings.ts @@ -91,6 +91,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Ilmaandmete jaoks. Tasuta aadressil openweathermap.org/api', 'settings.keyPlaceholder': 'Sisesta võti...', 'settings.configured': 'Seadistatud', + 'settings.notSetUp': 'Seadistamata', + 'settings.mcp.appsConnected': '{count} rakendust ühendatud', + 'settings.mcp.appsConnected.one': '{count} rakendus ühendatud', 'settings.saveKeys': 'Salvesta võtmed', 'settings.display': 'Kuva', 'settings.colorMode': 'Värvirežiim', diff --git a/shared/src/i18n/fr/settings.ts b/shared/src/i18n/fr/settings.ts index 3a8345632..9ffbe4227 100644 --- a/shared/src/i18n/fr/settings.ts +++ b/shared/src/i18n/fr/settings.ts @@ -84,6 +84,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Pour les données météo. Gratuit sur openweathermap.org/api', 'settings.keyPlaceholder': 'Saisir la clé…', 'settings.configured': 'Configuré', + 'settings.notSetUp': 'Non configuré', + 'settings.mcp.appsConnected': '{count} applications connectées', + 'settings.mcp.appsConnected.one': '{count} application connectée', 'settings.saveKeys': 'Enregistrer les clés', 'settings.display': 'Affichage', 'settings.colorMode': 'Mode de couleur', diff --git a/shared/src/i18n/gr/settings.ts b/shared/src/i18n/gr/settings.ts index 902c5585c..03c276111 100644 --- a/shared/src/i18n/gr/settings.ts +++ b/shared/src/i18n/gr/settings.ts @@ -86,6 +86,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Για δεδομένα καιρού. Δωρεάν στο openweathermap.org/api', 'settings.keyPlaceholder': 'Εισάγετε κλειδί...', 'settings.configured': 'Διαμορφωμένο', + 'settings.notSetUp': 'Δεν έχει ρυθμιστεί', + 'settings.mcp.appsConnected': '{count} εφαρμογές συνδεδεμένες', + 'settings.mcp.appsConnected.one': '{count} εφαρμογή συνδεδεμένη', 'settings.saveKeys': 'Αποθήκευση Κλειδιών', 'settings.display': 'Εμφάνιση', 'settings.colorMode': 'Λειτουργία Χρώματος', diff --git a/shared/src/i18n/he/settings.ts b/shared/src/i18n/he/settings.ts index 600a8b685..81dfe75a6 100644 --- a/shared/src/i18n/he/settings.ts +++ b/shared/src/i18n/he/settings.ts @@ -88,6 +88,10 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'לנתוני מזג אוויר. חינם ב-openweathermap.org/api', 'settings.keyPlaceholder': 'הזינו מפתח...', 'settings.configured': 'מוגדר', + 'settings.notSetUp': 'לא הוגדר', + 'settings.mcp.appsConnected': '{count} אפליקציות מחוברות', + 'settings.mcp.appsConnected.one': '{count} אפליקציה מחוברת', + 'settings.mcp.appsConnected.two': '{count} אפליקציות מחוברות', 'settings.saveKeys': 'שמירת מפתחות', 'settings.display': 'תצוגה', 'settings.colorMode': 'מצב צבע', diff --git a/shared/src/i18n/hu/settings.ts b/shared/src/i18n/hu/settings.ts index 844785caa..c0c0d3323 100644 --- a/shared/src/i18n/hu/settings.ts +++ b/shared/src/i18n/hu/settings.ts @@ -82,6 +82,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Időjárás adatokhoz. Ingyenes: openweathermap.org/api', 'settings.keyPlaceholder': 'Kulcs megadása...', 'settings.configured': 'Konfigurálva', + 'settings.notSetUp': 'Nincs beállítva', + 'settings.mcp.appsConnected': '{count} alkalmazás csatlakoztatva', + 'settings.mcp.appsConnected.one': '{count} alkalmazás csatlakoztatva', 'settings.saveKeys': 'Kulcsok mentése', 'settings.display': 'Megjelenítés', 'settings.colorMode': 'Színmód', diff --git a/shared/src/i18n/id/settings.ts b/shared/src/i18n/id/settings.ts index 69b917747..6992f14aa 100644 --- a/shared/src/i18n/id/settings.ts +++ b/shared/src/i18n/id/settings.ts @@ -80,6 +80,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Untuk data cuaca. Gratis di openweathermap.org/api', 'settings.keyPlaceholder': 'Masukkan key...', 'settings.configured': 'Sudah dikonfigurasi', + 'settings.notSetUp': 'Belum diatur', + 'settings.mcp.appsConnected': '{count} aplikasi terhubung', 'settings.saveKeys': 'Simpan Keys', 'settings.display': 'Tampilan', 'settings.colorMode': 'Mode Warna', diff --git a/shared/src/i18n/it/settings.ts b/shared/src/i18n/it/settings.ts index f5ed79645..76846f29d 100644 --- a/shared/src/i18n/it/settings.ts +++ b/shared/src/i18n/it/settings.ts @@ -82,6 +82,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Per i dati meteo. Gratuita su openweathermap.org/api', 'settings.keyPlaceholder': 'Inserisci la chiave...', 'settings.configured': 'Configurata', + 'settings.notSetUp': 'Non configurato', + 'settings.mcp.appsConnected': '{count} app collegate', + 'settings.mcp.appsConnected.one': '{count} app collegata', 'settings.saveKeys': 'Salva Chiavi', 'settings.display': 'Visualizzazione', 'settings.colorMode': 'Modalità Colore', diff --git a/shared/src/i18n/ja/settings.ts b/shared/src/i18n/ja/settings.ts index 88ada5786..0e96d138a 100644 --- a/shared/src/i18n/ja/settings.ts +++ b/shared/src/i18n/ja/settings.ts @@ -79,6 +79,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': '天気情報用。openweathermap.org/api で無料取得', 'settings.keyPlaceholder': 'キーを入力…', 'settings.configured': '設定済み', + 'settings.notSetUp': '未設定', + 'settings.mcp.appsConnected': '{count} 個のアプリが接続済み', 'settings.saveKeys': 'キーを保存', 'settings.display': '表示', 'settings.colorMode': 'カラーモード', diff --git a/shared/src/i18n/ko/settings.ts b/shared/src/i18n/ko/settings.ts index 64db890ad..85f3b9283 100644 --- a/shared/src/i18n/ko/settings.ts +++ b/shared/src/i18n/ko/settings.ts @@ -80,6 +80,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': '날씨 데이터용. openweathermap.org/api에서 무료 발급', 'settings.keyPlaceholder': '키 입력...', 'settings.configured': '설정됨', + 'settings.notSetUp': '설정 안 됨', + 'settings.mcp.appsConnected': '앱 {count}개 연결됨', 'settings.saveKeys': '키 저장', 'settings.display': '화면', 'settings.colorMode': '색상 모드', diff --git a/shared/src/i18n/nl/settings.ts b/shared/src/i18n/nl/settings.ts index 191e99810..c603d817a 100644 --- a/shared/src/i18n/nl/settings.ts +++ b/shared/src/i18n/nl/settings.ts @@ -81,6 +81,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Voor weergegevens. Gratis op openweathermap.org/api', 'settings.keyPlaceholder': 'Sleutel invoeren...', 'settings.configured': 'Geconfigureerd', + 'settings.notSetUp': 'Niet ingesteld', + 'settings.mcp.appsConnected': '{count} apps verbonden', + 'settings.mcp.appsConnected.one': '{count} app verbonden', 'settings.saveKeys': 'Sleutels opslaan', 'settings.display': 'Weergave', 'settings.colorMode': 'Kleurmodus', diff --git a/shared/src/i18n/pl/settings.ts b/shared/src/i18n/pl/settings.ts index 1ead803f5..9476e1dfc 100644 --- a/shared/src/i18n/pl/settings.ts +++ b/shared/src/i18n/pl/settings.ts @@ -80,6 +80,11 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Do danych pogodowych. Bezpłatnie na openweathermap.org/api', 'settings.keyPlaceholder': 'Podaj klucz...', 'settings.configured': 'Skonfigurowano', + 'settings.notSetUp': 'Nie skonfigurowano', + 'settings.mcp.appsConnected': '{count} aplikacji połączonych', + 'settings.mcp.appsConnected.one': '{count} aplikacja połączona', + 'settings.mcp.appsConnected.few': '{count} aplikacje połączone', + 'settings.mcp.appsConnected.many': '{count} aplikacji połączonych', 'settings.saveKeys': 'Zapisz klucze', 'settings.display': 'Preferencje', 'settings.colorMode': 'Motyw', diff --git a/shared/src/i18n/ru/settings.ts b/shared/src/i18n/ru/settings.ts index d74a14cd0..6fe0cffde 100644 --- a/shared/src/i18n/ru/settings.ts +++ b/shared/src/i18n/ru/settings.ts @@ -80,6 +80,11 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Для данных о погоде. Бесплатно на openweathermap.org/api', 'settings.keyPlaceholder': 'Введите ключ...', 'settings.configured': 'Настроено', + 'settings.notSetUp': 'Не настроено', + 'settings.mcp.appsConnected': '{count} приложения подключено', + 'settings.mcp.appsConnected.one': '{count} приложение подключено', + 'settings.mcp.appsConnected.few': '{count} приложения подключены', + 'settings.mcp.appsConnected.many': '{count} приложений подключено', 'settings.saveKeys': 'Сохранить ключи', 'settings.display': 'Отображение', 'settings.colorMode': 'Цветовая схема', diff --git a/shared/src/i18n/sk/settings.ts b/shared/src/i18n/sk/settings.ts index 9786287a5..5d96296c5 100644 --- a/shared/src/i18n/sk/settings.ts +++ b/shared/src/i18n/sk/settings.ts @@ -88,6 +88,10 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Na dáta o počasí. Zadarmo na openweathermap.org/api', 'settings.keyPlaceholder': 'Zadajte kľúč...', 'settings.configured': 'Nastavené', + 'settings.notSetUp': 'Nenastavené', + 'settings.mcp.appsConnected': '{count} aplikácií pripojených', + 'settings.mcp.appsConnected.one': '{count} aplikácia pripojená', + 'settings.mcp.appsConnected.few': '{count} aplikácie pripojené', 'settings.saveKeys': 'Uložiť kľúče', 'settings.display': 'Zobrazenie', 'settings.colorMode': 'Farebný režim', diff --git a/shared/src/i18n/sv/settings.ts b/shared/src/i18n/sv/settings.ts index d061f40cf..8830af23f 100644 --- a/shared/src/i18n/sv/settings.ts +++ b/shared/src/i18n/sv/settings.ts @@ -81,6 +81,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'För väderdata. Gratis på openweathermap.org/api', 'settings.keyPlaceholder': 'Ange nyckel...', 'settings.configured': 'Inställd', + 'settings.notSetUp': 'Inte inställd', + 'settings.mcp.appsConnected': '{count} appar anslutna', + 'settings.mcp.appsConnected.one': '{count} app ansluten', 'settings.saveKeys': 'Spara nycklar', 'settings.display': 'Visning', 'settings.colorMode': 'Färgläge', diff --git a/shared/src/i18n/th/settings.ts b/shared/src/i18n/th/settings.ts index 6210c001f..dd0a2c30c 100644 --- a/shared/src/i18n/th/settings.ts +++ b/shared/src/i18n/th/settings.ts @@ -76,6 +76,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'สำหรับข้อมูลสภาพอากาศ ฟรีที่ openweathermap.org/api', 'settings.keyPlaceholder': 'ป้อนคีย์...', 'settings.configured': 'กำหนดค่าแล้ว', + 'settings.notSetUp': 'ยังไม่ได้ตั้งค่า', + 'settings.mcp.appsConnected': 'เชื่อมต่อแล้ว {count} แอป', 'settings.saveKeys': 'บันทึกคีย์', 'settings.display': 'จอแสดงผล', 'settings.colorMode': 'โหมดสี', diff --git a/shared/src/i18n/tr/settings.ts b/shared/src/i18n/tr/settings.ts index a33983109..91464688e 100644 --- a/shared/src/i18n/tr/settings.ts +++ b/shared/src/i18n/tr/settings.ts @@ -81,6 +81,9 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Hava verisi için. Ücretsiz: openweathermap.org/api', 'settings.keyPlaceholder': 'Anahtarı girin...', 'settings.configured': 'Yapılandırıldı', + 'settings.notSetUp': 'Ayarlanmadı', + 'settings.mcp.appsConnected': '{count} uygulama bağlı', + 'settings.mcp.appsConnected.one': '{count} uygulama bağlı', 'settings.saveKeys': 'Anahtarları Kaydet', 'settings.display': 'Görünüm', 'settings.colorMode': 'Renk Modu', diff --git a/shared/src/i18n/uk/settings.ts b/shared/src/i18n/uk/settings.ts index 63604cb5a..276bc6833 100644 --- a/shared/src/i18n/uk/settings.ts +++ b/shared/src/i18n/uk/settings.ts @@ -82,6 +82,11 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Для даних про погоду. Безкоштовно на openweathermap.org/api', 'settings.keyPlaceholder': 'Введіть ключ...', 'settings.configured': 'Налаштовано', + 'settings.notSetUp': 'Не налаштовано', + 'settings.mcp.appsConnected': '{count} застосунку підключено', + 'settings.mcp.appsConnected.one': '{count} застосунок підключено', + 'settings.mcp.appsConnected.few': '{count} застосунки підключено', + 'settings.mcp.appsConnected.many': '{count} застосунків підключено', 'settings.saveKeys': 'Зберегти ключі', 'settings.display': 'Відображення', 'settings.colorMode': 'Кольорова схема', diff --git a/shared/src/i18n/vi/settings.ts b/shared/src/i18n/vi/settings.ts index b2ddf72cc..b4c3b5355 100644 --- a/shared/src/i18n/vi/settings.ts +++ b/shared/src/i18n/vi/settings.ts @@ -82,6 +82,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': 'Đối với dữ liệu thời tiết. Miễn phí tại openweathermap.org/api', 'settings.keyPlaceholder': 'Nhập khóa...', 'settings.configured': 'Đã định cấu hình', + 'settings.notSetUp': 'Chưa thiết lập', + 'settings.mcp.appsConnected': '{count} ứng dụng đã kết nối', 'settings.saveKeys': 'Lưu chìa khóa', 'settings.display': 'Hiển thị', 'settings.colorMode': 'Chế độ màu', diff --git a/shared/src/i18n/zh-TW/settings.ts b/shared/src/i18n/zh-TW/settings.ts index 078663243..08fbe99de 100644 --- a/shared/src/i18n/zh-TW/settings.ts +++ b/shared/src/i18n/zh-TW/settings.ts @@ -77,6 +77,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': '用於天氣資料。在 openweathermap.org/api 免費獲取', 'settings.keyPlaceholder': '輸入金鑰...', 'settings.configured': '已配置', + 'settings.notSetUp': '未設定', + 'settings.mcp.appsConnected': '已連接 {count} 個應用程式', 'settings.saveKeys': '儲存金鑰', 'settings.display': '顯示', 'settings.colorMode': '顏色模式', diff --git a/shared/src/i18n/zh/settings.ts b/shared/src/i18n/zh/settings.ts index 41c201aba..31fbeaf18 100644 --- a/shared/src/i18n/zh/settings.ts +++ b/shared/src/i18n/zh/settings.ts @@ -77,6 +77,8 @@ const settings: TranslationStrings = { 'settings.weatherKeyHint': '用于天气数据。在 openweathermap.org/api 免费获取', 'settings.keyPlaceholder': '输入密钥...', 'settings.configured': '已配置', + 'settings.notSetUp': '未设置', + 'settings.mcp.appsConnected': '已连接 {count} 个应用', 'settings.saveKeys': '保存密钥', 'settings.display': '显示', 'settings.colorMode': '颜色模式',