From 94ac7673a526806c6278c09bb61ba07916faeb52 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 13 Nov 2026 23:18:28 +0100 Subject: [PATCH] feat(settings): one look for every user and admin settings page The settings pages had grown into a mix of styles: folds, dialogs behind rows, full-width rows next to boxed ones, pills for every figure and buttons in four sizes. Every page now uses the same kit. - Cards keep their tinted head band; every part of a card is a rounded block with its own small head (SettingsBlock), and rows sit in rounded boxes. - Nothing folds and nothing hides behind a dialog: the MCP config, OAuth scopes, mail how-to, AI privacy note and addon sub-options are all shown. - Each page is split into two columns balanced by length (SettingsColumns). - Long explanations moved into an info mark next to the label (HintMark). - One button size everywhere (text-control); tab names are one word. - Admin: the old Settings tab is split into Security and Services (?tab=settings still opens Security, and the phone keeps its single Settings section for both); the Integration group is gone, live flights and mail import sit next to the addons they configure. - Addons: one card per type with rows, no search, filter or fold. - Quota, TREK API facts, mail import status and Google's daily limit are compact lines and strips instead of tiles and pills. - Language is a select; routing has its own card under the defaults; storage saves from the foot of its categories card. - Unused keys, FoldSummary and the page-width wrapper are removed. --- .../components/Admin/AddonManager.test.tsx | 72 +---- client/src/components/Admin/AddonManager.tsx | 151 ++-------- client/src/components/Admin/AddonSubRow.tsx | 3 +- client/src/components/Admin/AddonTile.tsx | 112 ++------ .../components/Admin/AdminMcpTokensPanel.tsx | 10 +- .../components/Admin/AdminPluginsPanel.tsx | 6 +- client/src/components/Admin/AuditLogPanel.tsx | 5 +- client/src/components/Admin/BackupPanel.tsx | 5 +- .../src/components/Admin/CategoryManager.tsx | 2 +- .../Admin/DefaultUserSettingsTab.tsx | 54 ++-- client/src/components/Admin/GitHubPanel.tsx | 2 - .../components/Admin/GoogleDailyLimitRow.tsx | 42 ++- .../Admin/PackingTemplateManager.tsx | 2 +- .../components/Admin/PlaceContribPanel.tsx | 18 +- .../Admin/RoutingInstanceFields.tsx | 19 +- .../components/Admin/SchoolHolidayCatalog.tsx | 8 +- .../Admin/ServiceIdentityPanel.test.tsx | 9 +- .../components/Admin/ServiceIdentityPanel.tsx | 67 +++-- .../components/Admin/addons/AiAddonTile.tsx | 102 +++---- .../components/Admin/addons/AiAdminConfig.tsx | 1 - .../Admin/addons/ParsingSettings.tsx | 3 - .../Admin/mailIngest/MailIngestAdminCard.tsx | 145 ++++------ .../Admin/paddle/PaddleAutoLoadParts.tsx | 2 - .../Admin/paddle/PaddleRegionsPanel.tsx | 3 +- .../Admin/storage/AdminStoragePanel.tsx | 79 +++--- .../Admin/storage/StorageReclaimSection.tsx | 2 - .../src/components/OAuth/ScopeGroupPicker.tsx | 1 - client/src/components/Settings/AccountTab.tsx | 120 ++++---- .../AirTrailConnectionSection.test.tsx | 3 - .../Settings/AirTrailConnectionSection.tsx | 6 +- .../Settings/ApiKeysSection.test.tsx | 3 - .../components/Settings/ApiKeysSection.tsx | 3 +- .../Settings/AppearanceSettingsTab.tsx | 25 +- .../DawarichConnectionSection.test.tsx | 3 - .../Settings/DawarichConnectionSection.tsx | 6 +- .../Settings/DisplaySettingsTab.test.tsx | 90 +----- .../Settings/DisplaySettingsTab.tsx | 86 ++---- .../Settings/IntegrationsTab.test.tsx | 78 +++--- .../components/Settings/IntegrationsTab.tsx | 145 ++++------ .../Settings/LlmConnectionSection.test.tsx | 3 - .../Settings/LlmConnectionSection.tsx | 3 +- .../components/Settings/MailImportSection.tsx | 16 +- .../components/Settings/MapSettingsTab.tsx | 1 - .../components/Settings/NotificationsTab.tsx | 13 +- client/src/components/Settings/OfflineTab.tsx | 90 +++--- .../components/Settings/PasskeysSection.tsx | 2 +- .../Settings/PhotoProvidersSection.test.tsx | 3 - .../Settings/PhotoProvidersSection.tsx | 1 - .../components/Settings/PluginSettingsTab.tsx | 7 +- client/src/components/Settings/Section.tsx | 14 +- .../WandererConnectionSection.test.tsx | 3 - .../Settings/WandererConnectionSection.tsx | 4 +- .../components/Settings/WandererWatchRow.tsx | 1 - .../src/components/Settings/WebPushCard.tsx | 2 - .../components/Settings/ai/AiConnectPanel.tsx | 34 +-- .../ai/AiConnectionSection.mapFollow.test.tsx | 3 - .../Settings/ai/AiConnectionSection.test.tsx | 3 - .../Settings/ai/AiConnectionSection.tsx | 12 +- .../Settings/ai/ChatGptPasteStep.tsx | 3 - .../components/Settings/settingsKit.test.tsx | 65 ----- .../src/components/Settings/settingsKit.tsx | 261 +++++++++++------- .../components/Settings/useMcpIntegration.ts | 6 - client/src/mobile/screens/admin/MAdmin.tsx | 5 +- client/src/pages/AdminPage.tsx | 39 +-- client/src/pages/SettingsPage.test.tsx | 3 - .../pages/admin/AdminNotificationsPanel.tsx | 9 +- .../src/pages/admin/AdminNotificationsTab.tsx | 218 +++++++-------- .../src/pages/admin/AdminSettingsTab.test.tsx | 26 +- client/src/pages/admin/AdminSettingsTab.tsx | 198 ++++++++----- client/src/pages/admin/AdminUserModals.tsx | 4 - client/src/pages/admin/AdminUsersTab.tsx | 4 +- client/src/pages/admin/FoldSummary.test.tsx | 35 --- client/src/pages/admin/FoldSummary.tsx | 21 -- client/src/pages/admin/GoogleOptions.tsx | 21 +- client/src/pages/admin/ProviderBlock.tsx | 20 +- client/src/pages/admin/TrekApiCard.tsx | 115 +------- client/src/pages/admin/useAdmin.ts | 12 +- client/tailwind.config.js | 2 + client/tests/helpers/settingsCards.ts | 26 -- .../tests/unit/mobile/admin/MAdmin.test.tsx | 16 +- .../admin/MAdminMcpTokensPanel.test.tsx | 2 +- shared/src/i18n/ar/admin.ts | 30 +- shared/src/i18n/ar/mailAdmin.ts | 1 - shared/src/i18n/az/admin.ts | 22 +- shared/src/i18n/az/mailAdmin.ts | 1 - shared/src/i18n/br/admin.ts | 22 +- shared/src/i18n/br/mailAdmin.ts | 1 - shared/src/i18n/ca/admin.ts | 22 +- shared/src/i18n/ca/mailAdmin.ts | 1 - shared/src/i18n/cs/admin.ts | 24 +- shared/src/i18n/cs/mailAdmin.ts | 1 - shared/src/i18n/de/admin.ts | 22 +- shared/src/i18n/de/mailAdmin.ts | 1 - shared/src/i18n/en/admin.ts | 22 +- shared/src/i18n/en/mailAdmin.ts | 1 - shared/src/i18n/es/admin.ts | 24 +- shared/src/i18n/es/mailAdmin.ts | 1 - shared/src/i18n/et/admin.ts | 22 +- shared/src/i18n/et/mailAdmin.ts | 1 - shared/src/i18n/fr/admin.ts | 22 +- shared/src/i18n/fr/mailAdmin.ts | 1 - shared/src/i18n/gr/admin.ts | 22 +- shared/src/i18n/gr/mailAdmin.ts | 1 - shared/src/i18n/he/admin.ts | 28 +- shared/src/i18n/he/mailAdmin.ts | 1 - shared/src/i18n/hu/admin.ts | 26 +- shared/src/i18n/hu/mailAdmin.ts | 1 - shared/src/i18n/id/admin.ts | 20 +- shared/src/i18n/id/mailAdmin.ts | 1 - shared/src/i18n/it/admin.ts | 22 +- shared/src/i18n/it/mailAdmin.ts | 1 - shared/src/i18n/ja/admin.ts | 20 +- shared/src/i18n/ja/mailAdmin.ts | 1 - shared/src/i18n/ko/admin.ts | 20 +- shared/src/i18n/ko/mailAdmin.ts | 1 - shared/src/i18n/nl/admin.ts | 22 +- shared/src/i18n/nl/mailAdmin.ts | 1 - shared/src/i18n/pl/admin.ts | 26 +- shared/src/i18n/pl/mailAdmin.ts | 1 - shared/src/i18n/ru/admin.ts | 26 +- shared/src/i18n/ru/mailAdmin.ts | 1 - shared/src/i18n/sk/admin.ts | 24 +- shared/src/i18n/sk/mailAdmin.ts | 1 - shared/src/i18n/sv/admin.ts | 22 +- shared/src/i18n/sv/mailAdmin.ts | 1 - shared/src/i18n/th/admin.ts | 20 +- shared/src/i18n/th/mailAdmin.ts | 1 - shared/src/i18n/tr/admin.ts | 22 +- shared/src/i18n/tr/mailAdmin.ts | 1 - shared/src/i18n/uk/admin.ts | 26 +- shared/src/i18n/uk/mailAdmin.ts | 1 - shared/src/i18n/uk/settings.ts | 2 +- shared/src/i18n/vi/admin.ts | 20 +- shared/src/i18n/vi/mailAdmin.ts | 1 - shared/src/i18n/zh-TW/admin.ts | 20 +- shared/src/i18n/zh-TW/mailAdmin.ts | 1 - shared/src/i18n/zh/admin.ts | 20 +- shared/src/i18n/zh/mailAdmin.ts | 1 - 138 files changed, 1299 insertions(+), 2188 deletions(-) delete mode 100644 client/src/components/Settings/settingsKit.test.tsx delete mode 100644 client/src/pages/admin/FoldSummary.test.tsx delete mode 100644 client/src/pages/admin/FoldSummary.tsx delete mode 100644 client/tests/helpers/settingsCards.ts diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 8338b5a63..e61dc2914 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -53,12 +53,6 @@ function isOn(toggle: HTMLElement): boolean { return toggle.getAttribute('aria-pressed') === 'true'; } -/** A shelf of more than one row starts folded; open every one so its rows can be read. */ -async function unfoldShelves(): Promise { - const bars = await screen.findAllByRole('button', { name: /\d+ options?/, expanded: false }); - for (const bar of bars) await userEvent.click(bar); -} - beforeAll(() => { Object.defineProperty(window, 'matchMedia', { writable: true, @@ -236,7 +230,6 @@ describe('AddonManager', () => { ) ); render(); - await unfoldShelves(); // Provider sub-rows are visible under Journey addon await screen.findByText('Unsplash'); @@ -272,15 +265,6 @@ describe('AddonManager', () => { expect(screen.getByText('No addons available')).toBeInTheDocument(); }); - it('FE-ADMIN-ADDON-013: dark mode swaps the wordmark in the header', async () => { - // The wordmark reads the .dark class, the same source applyAppearance() writes. - document.documentElement.classList.add('dark'); - render(); - - await screen.findByText('No addons available'); - expect(screen.getByAltText('TREK')).toHaveAttribute('src', '/text-light.svg'); - }); - it('FE-ADMIN-ADDON-014: photo-flavoured trip addons are hidden from the trip section', async () => { server.use(addonsRoute([ buildAddon({ id: 'photos', name: 'Memories', icon: 'Image' }), @@ -302,7 +286,6 @@ describe('AddonManager', () => { buildAddon({ id: 'unsplash', name: 'Unsplash', description: 'Stock photos', type: 'photo_provider', enabled: false }), ])); render(); - await unfoldShelves(); await screen.findByText('Immich'); // immich and synologyphotos ship a vendor glyph, unsplash does not @@ -437,7 +420,6 @@ describe('AddonManager', () => { ])); render(); - await unfoldShelves(); await screen.findByText('Paperless-ngx'); const paperlessRow = screen.getByText('Paperless-ngx').closest('li') as HTMLElement; @@ -536,7 +518,6 @@ describe('AddonManager', () => { />, ); - await unfoldShelves(); await screen.findByText('Chat'); expect(screen.getByText('Notes')).toBeInTheDocument(); @@ -567,7 +548,6 @@ describe('AddonManager', () => { onToggleCollabFeature={vi.fn()} /> ); - await unfoldShelves(); const links = await screen.findByText('Links'); expect(links).toHaveAttribute('title'); @@ -623,7 +603,7 @@ describe('AddonManager', () => { await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()); }); - it('FE-ADMIN-ADDON-040: a shelf of several options starts folded and says how many there are and how many are on', async () => { + it('FE-ADMIN-ADDON-040: the options of an addon are listed under it at once, nothing to unfold', async () => { server.use(addonsRoute([buildAddon({ id: 'collab', name: 'Collab', enabled: true })])); render( { /> ); - const bar = await screen.findByRole('button', { name: /5 options/, expanded: false }); - expect(bar).toHaveTextContent('2 on'); - expect(screen.queryByText('Chat')).not.toBeInTheDocument(); - - await userEvent.click(bar); - expect(bar).toHaveAttribute('aria-expanded', 'true'); - expect(screen.getByText('Chat')).toBeInTheDocument(); - - await userEvent.click(bar); - expect(screen.queryByText('Chat')).not.toBeInTheDocument(); + expect(await screen.findByRole('button', { name: 'Chat' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Polls' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /\d+ options?/ })).not.toBeInTheDocument(); }); it('FE-ADMIN-ADDON-041: a shelf of one option shows it at once, without a fold', async () => { @@ -668,10 +641,6 @@ describe('AddonManager', () => { ); - // Folded like Collab's and Documents' options until the bar is opened. - await screen.findByRole('button', { name: /\d+ options?/, expanded: false }); - expect(screen.queryByRole('button', { name: 'wanderer' })).not.toBeInTheDocument(); - await unfoldShelves(); const toggle = await screen.findByRole('button', { name: 'wanderer' }); expect(isOn(toggle)).toBe(false); await userEvent.click(toggle); @@ -685,8 +654,7 @@ describe('AddonManager', () => { server.use(addonsRoute([buildAddon({ id: 'tours', name: 'Tours', icon: 'Mountain', enabled: true })])); render(); - await screen.findByRole('button', { name: /\d+ options?/, expanded: false }); - await unfoldShelves(); + await screen.findByText('Tours'); expect(screen.queryByRole('button', { name: 'wanderer' })).not.toBeInTheDocument(); }); @@ -702,7 +670,6 @@ describe('AddonManager', () => { ); - await unfoldShelves(); await userEvent.click(await screen.findByRole('button', { name: 'wanderer' })); await waitFor(() => expect(isOn(screen.getByRole('button', { name: 'wanderer' }))).toBe(false)); @@ -723,33 +690,4 @@ describe('AddonManager', () => { expect(roadtrip.querySelector('svg.lucide-car-front')).toBeInTheDocument(); expect(tours.querySelector('svg.lucide-puzzle')).not.toBeInTheDocument(); }); - - it('FE-ADMIN-ADDON-045: finds an addon by name or description and shows the ones on or off', async () => { - const user = userEvent.setup(); - server.use( - addonsRoute([ - buildAddon({ id: 'x-lists', name: 'Todo List', description: 'Track tasks', type: 'trip', enabled: true }), - buildAddon({ id: 'x-costs', name: 'Budget', description: 'Split costs', type: 'trip', enabled: false }), - buildAddon({ id: 'x-holiday', name: 'Vacay', description: 'Holiday planner', type: 'global', enabled: true }), - ]) - ); - render(); - await screen.findByText('Todo List'); - - await user.type(screen.getByRole('searchbox', { name: 'Search addons' }), 'costs'); - expect(screen.getByText('Budget')).toBeInTheDocument(); - expect(screen.queryByText('Todo List')).toBeNull(); - expect(screen.queryByText('Vacay')).toBeNull(); - - await user.clear(screen.getByRole('searchbox', { name: 'Search addons' })); - await user.click(screen.getByRole('button', { name: 'On' })); - expect(screen.getByText('Todo List')).toBeInTheDocument(); - expect(screen.getByText('Vacay')).toBeInTheDocument(); - expect(screen.queryByText('Budget')).toBeNull(); - - await user.click(screen.getByRole('button', { name: 'Off' })); - await user.type(screen.getByRole('searchbox', { name: 'Search addons' }), 'nothing like this'); - expect(screen.getByText('No addon matches the search or the filter.')).toBeInTheDocument(); - }); }); - diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index a5939fb5b..623f93b56 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -1,7 +1,6 @@ -import { useState, type ComponentType } from 'react' +import type { ComponentType, ReactNode } from 'react' import { useTranslation } from '../../i18n' -import { useIsDark } from '../../hooks/useIsDark' -import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Sparkles, Luggage, Plane, Bookmark, Users, Loader2, Mountain, CarFront, Search } from 'lucide-react' +import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Sparkles, Luggage, Plane, Bookmark, Users, Loader2, Mountain, CarFront } from 'lucide-react' import EmptyState from '../shared/EmptyState' import DawarichIcon from '../shared/DawarichIcon' import AirTrailIcon from '../shared/AirTrailIcon' @@ -9,8 +8,7 @@ import { DOCUMENT_PROVIDER_ICONS } from '../shared/documentProviderIconMap' import AddonTile from './AddonTile' import AddonSubRow from './AddonSubRow' import { Tooltip } from '../shared/Tooltip' -import { fs } from '../shared/DialogShell' -import { SettingsCard, StatusPill } from '../Settings/settingsKit' +import { SettingsCard, SettingsColumns, StatusPill } from '../Settings/settingsKit' import { type Addon, type CollabFeatures, COLLAB_SUB_FEATURES, getAddonLabel } from './addons/addonModel' import { useAddonManager } from './addons/useAddonManager' import AiAddonTile from './addons/AiAddonTile' @@ -67,9 +65,15 @@ const TYPE_META: Record; labelK integration: { icon: Link2, labelKey: 'admin.addons.type.integration', hintKey: 'admin.addons.integrationHint' }, } -export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, collabFeatures, onToggleCollabFeature }: { bagTrackingEnabled?: boolean; onToggleBagTracking?: () => void; collabFeatures?: CollabFeatures; onToggleCollabFeature?: (key: string) => void }) { +export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, collabFeatures, onToggleCollabFeature, aside }: { + bagTrackingEnabled?: boolean + onToggleBagTracking?: () => void + collabFeatures?: CollabFeatures + onToggleCollabFeature?: (key: string) => void + /** Cards that set up what the integrations reach (flight status, booking mails), under them in the right column. */ + aside?: ReactNode +}) { const { t } = useTranslation() - const dark = useIsDark() const { addons, loading, handleToggle, tripAddons, globalAddons, integrationAddons, aiAddons, providerOptions, documentProviderOptions, toursWanderer, @@ -77,9 +81,6 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, toggleToursWanderer, mapSwitches, } = useAddonManager() - const [query, setQuery] = useState('') - const [view, setView] = useState('all') - if (loading) { return ( @@ -210,32 +211,12 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, { key: 'global', addons: globalAddons }, { key: 'integration', addons: integrationAddons }, ].filter(g => g.addons.length > 0 || (g.key === 'integration' && aiAddons.length > 0)) - const enabledCount = addons.filter(a => a.type !== 'photo_provider' && a.type !== 'document_provider' && a.enabled).length - const totalCount = tripAddons.length + globalAddons.length + integrationAddons.length + aiAddons.length - - const subtitle = ( - - {t('admin.addons.subtitleBefore')} - TREK - {t('admin.addons.subtitleAfter')} - - ) - - /** Whether an addon is shown under the search and the filter. */ - const shown = (addon: Addon) => { - if (view === 'on' && !addon.enabled) return false - if (view === 'off' && addon.enabled) return false - const q = query.trim().toLocaleLowerCase() - if (!q) return true - const label = getAddonLabel(t, addon) - return `${label.name} ${label.description ?? ''}`.toLocaleLowerCase().includes(q) - } const card = (g: (typeof groups)[number]) => { const meta = TYPE_META[g.key] const all = g.key === 'integration' ? [...aiAddons, ...g.addons] : g.addons - const visible = g.addons.filter(shown) - const ai = g.key === 'integration' && aiAddons.some(shown) + const visible = g.addons + const ai = g.key === 'integration' && aiAddons.length > 0 if (!visible.length && !ai) return null const on = all.filter(a => a.enabled).length return ( @@ -249,8 +230,8 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, 0 ? 'success' : 'neutral'}>{on}/{all.length} )} - bodyClassName="p-3" > + {/* The addons in one rounded block of rows, as every settings card holds its rows. */}
{/* The AI addons share a connection and a settings dialog, so they head the card as one tile. */} {ai && } @@ -263,103 +244,13 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, const [first, ...rest] = groups const cards = { start: first ? card(first) : null, end: rest.map(card).filter(Boolean) } - return ( - <> - 0 ? ( - - - 0 ? 'success' : 'neutral'}>{enabledCount}/{totalCount} - - - ) : undefined} - > - {addons.length === 0 - ? - : ({ key: g.key, addons: g.key === 'integration' ? [...aiAddons, ...g.addons] : g.addons }))} query={query} onQuery={setQuery} view={view} onView={setView} t={t} />} + if (addons.length === 0) { + return ( + + + ) + } - {addons.length > 0 && (cards.start || cards.end.length > 0 ? ( -
-
{cards.start}
-
{cards.end}
-
- ) : ( -

- {t('admin.addons.noMatch')} -

- ))} - - ) -} - -type AddonView = 'all' | 'on' | 'off' - -/** - * The head card's body: a tile per type with how many of it are on, then a - * search over the addons' names and descriptions and a switch between all, - * the ones on and the ones off. - */ -function AddonToolbar({ groups, query, onQuery, view, onView, t }: { - groups: { key: string; addons: Addon[] }[] - query: string - onQuery: (q: string) => void - view: AddonView - onView: (v: AddonView) => void - t: (key: string, params?: Record) => string -}) { - return ( - <> -
- {groups.map(g => { - const meta = TYPE_META[g.key] - const Icon = meta.icon - const on = g.addons.filter(a => a.enabled).length - return ( -
- - - - - {t(meta.labelKey)} - {t('admin.addons.group.count', { enabled: on, total: g.addons.length })} - - 0 ? 'text-content' : 'text-content-faint'}`} style={fs(18, 'body')} aria-hidden>{on} -
- ) - })} -
-
- -
- {(['all', 'on', 'off'] as const).map(v => ( - - ))} -
-
- - ) + return {cards.end}{aside}} /> } diff --git a/client/src/components/Admin/AddonSubRow.tsx b/client/src/components/Admin/AddonSubRow.tsx index 4586fad1f..d8a9ba472 100644 --- a/client/src/components/Admin/AddonSubRow.tsx +++ b/client/src/components/Admin/AddonSubRow.tsx @@ -33,7 +33,8 @@ export default function AddonSubRow({ onToggle: () => void }) { return ( -
  • + // Indented under the addon's name (16 + 32 + 12 px), one step quieter than it. +
  • {icon ? {icon} : } diff --git a/client/src/components/Admin/AddonTile.tsx b/client/src/components/Admin/AddonTile.tsx index dff37d989..953fc3047 100644 --- a/client/src/components/Admin/AddonTile.tsx +++ b/client/src/components/Admin/AddonTile.tsx @@ -1,30 +1,18 @@ -import { Children, type ReactNode, isValidElement, useId, useState } from 'react' +import { Children, type ReactNode } from 'react' import ToggleSwitch from '../Settings/ToggleSwitch' +import { HintMark } from '../Settings/settingsKit' import { fs } from '../shared/DialogShell' -import { ChevronDown } from 'lucide-react' -import { useTranslation } from '../../i18n' - -/** How many of the shelf's rows are switched on, read from the rows' own `enabled`. */ -function enabledRows(rows: ReactNode[]): number { - return rows.filter((row) => isValidElement<{ enabled?: boolean }>(row) && row.props.enabled === true).length -} /** - * One addon in its type's card: a row like the bag sidebar's (#2541), with the - * icon tile on the left, name and description beside it and the switch on the - * right. Its sub-features sit under it in a grey inset box of hairline rows. + * One addon in its type's card, as a settings row: the icon tile, the name with + * its description behind an info mark, and the switch on the right. Its + * sub-features follow as indented rows while it is on, in the same list and with + * the same hairlines: no box of their own, nothing folded away. * * The on/off state rides on the icon tile and the switch: a switched-off addon - * greys its tile and its description, but keeps its name at full strength. The - * name is the tile's identity, not its state; greying it out reads as "broken" - * rather than "not enabled". - * - * The row is not a click target, only the switches are. No lift, no pointer - * cursor, no `role="button"`. - * - * A shelf of more than one row starts folded behind a bar that says how many - * options there are and how many are on, so a page of addons stays short and - * the options are still plain to see. + * greys its tile but keeps its name at full strength. The name is the row's + * identity, not its state; greying it out reads as "broken" rather than "not + * enabled". The row is not a click target, only the switches are. */ export default function AddonTile({ icon, @@ -32,90 +20,42 @@ export default function AddonTile({ description, enabled, onToggle, + trailing, children, }: { icon: ReactNode name: string description?: string enabled: boolean - onToggle: () => void - /** The sub-shelf: `` children. */ + /** The addon's switch; leave it out for a row that shows a state instead (`trailing`). */ + onToggle?: () => void + /** Shown in place of the switch. */ + trailing?: ReactNode + /** The sub-features: `` children. */ children?: ReactNode }) { - const { t } = useTranslation() - const shelfId = useId() - const [open, setOpen] = useState(false) - const rows = Children.toArray(children) - const foldable = rows.length > 1 - const active = enabledRows(rows) - return ( -
    -
    +
    +
    {icon}
    - -
    -

    - {name} -

    -

    - {description} -

    +
    +

    {name}

    + {description && }
    - -
    - +
    + {trailing ?? (onToggle && )}
    - - {rows.length > 0 && ( -
    - {foldable && ( - - )} - {/* Sitting inside the parent on its own fill is enough to say these belong - to it: no rail, no indent guides. */} - {(!foldable || open) && ( -
      {children}
    - )} -
    - )} + {/* An empty shelf (a tours addon without its options here) draws no line. */} + {Children.count(children) > 0 &&
      {children}
    }
    ) } diff --git a/client/src/components/Admin/AdminMcpTokensPanel.tsx b/client/src/components/Admin/AdminMcpTokensPanel.tsx index 6f9e3a9bd..ed45dc2c6 100644 --- a/client/src/components/Admin/AdminMcpTokensPanel.tsx +++ b/client/src/components/Admin/AdminMcpTokensPanel.tsx @@ -5,7 +5,7 @@ import { useFormatDate } from '../../hooks/useDateFormat' import ConfirmDialog from '../shared/ConfirmDialog' import { Tooltip } from '../shared/Tooltip' import { fs } from '../shared/DialogShell' -import { SETTINGS_ICON_BUTTON, SettingRows, SettingsCard, SettingsHint, StatusPill } from '../Settings/settingsKit' +import { SETTINGS_ICON_BUTTON, SettingRows, SettingsCard, SettingsHint, StatusPill, SettingsColumns } from '../Settings/settingsKit' import { SCOPES_PREVIEW, useMcpTokensAdmin } from './useMcpTokensAdmin' const ROW = 'flex flex-wrap items-center gap-x-4 gap-y-2 px-3.5 py-3' @@ -72,6 +72,9 @@ export default function AdminMcpTokensPanel() { return (
    + {/* The sessions and the tokens side by side: two lists of the same kind. */} + {/* OAuth Sessions. The tab's own subtitle sits in its band: the sidebar already names the tab, so a bare heading over the cards only repeated it. */} )} - + } + right={<> {/* MCP Tokens */} )} + } + /> {/* Revoke OAuth session */} setLinkPath(e.target.value)} spellCheck={false} placeholder={t('admin.plugins.devLinkPathPlaceholder')} className={`${INPUT} font-mono`} /> -
    @@ -223,7 +223,7 @@ export default function AdminPluginsPanel() { surface="var(--bg-secondary)" size={88} action={( - )} @@ -627,7 +627,7 @@ function EgressDialog({ data, draft, setDraft, saving, error, onSave, onClose, t className={`${INPUT} flex-1 font-mono`} />
    diff --git a/client/src/components/Admin/AuditLogPanel.tsx b/client/src/components/Admin/AuditLogPanel.tsx index fde48298c..f852f51dd 100644 --- a/client/src/components/Admin/AuditLogPanel.tsx +++ b/client/src/components/Admin/AuditLogPanel.tsx @@ -68,14 +68,14 @@ export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): R icon={ClipboardList} title={t('admin.tabs.audit')} hint={t('admin.audit.subtitle')} - badge={{t('admin.audit.showing', { count: entries.length, total })}} + // The count only once there is one: never "0 of 0" while the first page loads. + badge={total > 0 ? {t('admin.audit.showing', { count: entries.length, total })} : undefined} action={ diff --git a/client/src/components/Admin/BackupPanel.tsx b/client/src/components/Admin/BackupPanel.tsx index 57a8533fc..fce3cf10e 100644 --- a/client/src/components/Admin/BackupPanel.tsx +++ b/client/src/components/Admin/BackupPanel.tsx @@ -56,7 +56,6 @@ export default function BackupPanel() { onClick={() => fileInputRef.current?.click()} disabled={isUploading} className={SETTINGS_BUTTON} - style={fs(12.5, 'body')} title={isUploading ? t('backup.uploading') : t('backup.upload')} > {isUploading @@ -69,7 +68,6 @@ export default function BackupPanel() { onClick={handleCreate} disabled={isCreating} className={SETTINGS_BUTTON_PRIMARY} - style={fs(12.5, 'body')} title={isCreating ? t('backup.creating') : t('backup.create')} > {isCreating @@ -97,7 +95,7 @@ export default function BackupPanel() { surface="var(--bg-secondary)" className="!py-6" action={ - @@ -295,7 +293,6 @@ export default function BackupPanel() { onClick={handleSaveAutoSettings} disabled={autoSettingsSaving || !autoSettingsDirty} className={SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {autoSettingsSaving ? diff --git a/client/src/components/Admin/CategoryManager.tsx b/client/src/components/Admin/CategoryManager.tsx index a4edae032..2913785f3 100644 --- a/client/src/components/Admin/CategoryManager.tsx +++ b/client/src/components/Admin/CategoryManager.tsx @@ -135,7 +135,7 @@ export default function CategoryManager() { hint={t('categories.subtitle')} badge={!isLoading && categories.length > 0 ? {categories.length} : undefined} action={ - diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index abc4f6c76..d9d502bd5 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -1,8 +1,8 @@ import React from 'react' -import { Loader2, Map as MapIcon, Settings2 } from 'lucide-react' +import { Loader2, Map as MapIcon, Settings2, Route } from 'lucide-react' import { useTranslation } from '../../i18n' import Section from '../Settings/Section' -import { SettingRows, SettingsHint } from '../Settings/settingsKit' +import { SettingRow, SettingRows, SettingsColumns } from '../Settings/settingsKit' import { fs } from '../shared/DialogShell' import { EditorField, INPUT, Segmented } from '../shared/dialogParts' import CustomSelect from '../shared/CustomSelect' @@ -34,15 +34,8 @@ function ChoiceRow({ stacked?: boolean children: React.ReactNode }) { - return ( -
    - - {hint &&

    {hint}

    } -
    {children}
    -
    - ) + // The settings row every page uses, so a default reads like the setting it presets. + return } /** A wide field box inside the white rows, for the map card's token and style. */ @@ -114,23 +107,11 @@ export default function DefaultUserSettingsTab(): React.ReactElement { const darkMode = defaults.dark_mode return ( -
    - - {t('admin.defaultSettings.description')} - + - {/* Two columns from xl up, the same idea as the Settings tab: as one flat list - the map fields sat a screen below the units while the right half of the page - stayed empty. Grouped by subject rather than by height — what a user ends up - seeing on the left, everything that configures the map on the right. - The columns are deliberately uneven: the left one never needs more than the - widest option row, while the right one holds four text fields whose values - are 60-character tile URLs. Only the column gap is set, because Section - carries its own bottom margin and a row gap would double it once the grid - collapses. Two cards can sit straight in the grid; a third would need the - explicit per-column stacks the Settings tab uses, or it leaves a hole. */} -
    -
    +
    {/* Color Mode */} {t('settings.colorMode')} }> @@ -207,7 +188,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement { {/* Default Currency */} - {t('settings.currency')} }> + {t('settings.currency')} } hint={t('settings.currencyHint')}>
    - {t('settings.currencyHint')}
    - + {/* Which engines answer routes, under the defaults: on its own card it keeps + the two columns about as long as each other. */} + {!managed && ( +
    + +
    + )} + + } + right={
    {/* Map Tile URL */} {t('settings.mapTemplate')}} hint={t('settings.mapDefaultHint')}> @@ -270,7 +259,6 @@ export default function DefaultUserSettingsTab(): React.ReactElement { /> )} - {!managed && }
    {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} {React.createElement(MapView as any, { @@ -386,7 +374,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement { )}
    -
    -
    + } + /> ) } diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index ecdb620e1..6bd222ced 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -205,7 +205,6 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b target="_blank" rel="noopener noreferrer" className={`${SETTINGS_BUTTON} no-underline`} - style={fs(12.5, 'body')} > GitHub @@ -298,7 +297,6 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b onClick={handleLoadMore} disabled={loadingMore} className={SETTINGS_BUTTON} - style={fs(12.5, 'body')} > {loadingMore ? : } {loadingMore ? t('admin.github.loading') : t('admin.github.loadMore')} diff --git a/client/src/components/Admin/GoogleDailyLimitRow.tsx b/client/src/components/Admin/GoogleDailyLimitRow.tsx index f398d291a..6ccb14611 100644 --- a/client/src/components/Admin/GoogleDailyLimitRow.tsx +++ b/client/src/components/Admin/GoogleDailyLimitRow.tsx @@ -1,44 +1,40 @@ -import { AlertTriangle, Check } from 'lucide-react' +import { Check } from 'lucide-react' import { useTranslation } from '../../i18n' import { NumericInput } from '../shared/NumericInput' import { fs } from '../shared/DialogShell' import { INPUT } from '../shared/dialogParts' -import { SETTINGS_BUTTON_PRIMARY, StatusPill } from '../Settings/settingsKit' +import { HintMark, SETTINGS_BUTTON_PRIMARY } from '../Settings/settingsKit' import { useGoogleQuota } from './useGoogleQuota' /** * The daily ceiling on Google calls (#1582), as one row of the Google options: - * today's count as a pill beside the name, the number on the right, a save + * today's count as a quiet line under the name, the number on the right, a save * button only while there is something to save. Laid out like settingsKit's - * SettingRow, but with the pill outside the
  • diff --git a/client/src/components/Admin/paddle/PaddleAutoLoadParts.tsx b/client/src/components/Admin/paddle/PaddleAutoLoadParts.tsx index dc434a5a2..4bec7cf0a 100644 --- a/client/src/components/Admin/paddle/PaddleAutoLoadParts.tsx +++ b/client/src/components/Admin/paddle/PaddleAutoLoadParts.tsx @@ -107,7 +107,6 @@ function RequestRow({ request, admin }: { request: PaddleRegionPending; admin: A disabled={admin.busyId !== null} onClick={() => void admin.decline(request.id)} className={SETTINGS_BUTTON} - style={fs(12.5, 'body')} > {t('admin.paddleRegions.requests.decline')} @@ -117,7 +116,6 @@ function RequestRow({ request, admin }: { request: PaddleRegionPending; admin: A disabled={admin.busyId !== null} onClick={() => void admin.approve(request.id)} className={SETTINGS_BUTTON} - style={fs(12.5, 'body')} > {busy ? : } {t('admin.paddleRegions.requests.approve')} diff --git a/client/src/components/Admin/paddle/PaddleRegionsPanel.tsx b/client/src/components/Admin/paddle/PaddleRegionsPanel.tsx index 10f333bbf..ca590760c 100644 --- a/client/src/components/Admin/paddle/PaddleRegionsPanel.tsx +++ b/client/src/components/Admin/paddle/PaddleRegionsPanel.tsx @@ -66,7 +66,7 @@ function Panel() { style={fs(12.5, 'body')} > {admin.error} - @@ -159,7 +159,6 @@ function CatalogSearch({ admin }: { admin: Admin }) { disabled={admin.busyId !== null} onClick={() => void admin.add(hit.id)} className={SETTINGS_BUTTON} - style={fs(12.5, 'body')} > {admin.busyId === hit.id ? ( diff --git a/client/src/components/Admin/storage/AdminStoragePanel.tsx b/client/src/components/Admin/storage/AdminStoragePanel.tsx index 108a1ae73..93e943bc3 100644 --- a/client/src/components/Admin/storage/AdminStoragePanel.tsx +++ b/client/src/components/Admin/storage/AdminStoragePanel.tsx @@ -29,14 +29,7 @@ import CustomSelect from '../../shared/CustomSelect' import { Tooltip } from '../../shared/Tooltip' import { NEUTRAL_TINT, fs } from '../../shared/DialogShell' import Section from '../../Settings/Section' -import { - SETTINGS_BUTTON, - SETTINGS_BUTTON_PRIMARY, - SETTINGS_ICON_BUTTON, - SettingRows, - SettingsHint, - StatusPill, -} from '../../Settings/settingsKit' +import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SETTINGS_ICON_BUTTON, SettingRows, SettingsHint, StatusPill, SettingsColumns } from '../../Settings/settingsKit' import BackendForm from './BackendForm' import StorageReclaimSection from './StorageReclaimSection' import { CACHE_CATEGORIES, primaryNameOf } from './storageModel' @@ -142,6 +135,10 @@ export default function AdminStoragePanel(): React.ReactElement { )} + {/* Two columns by length: the health and the backends on the left, which + category goes where on the right, its save bar at the card's foot. */} +
    {state.health.replicaFailures.length === 0 ? (

    @@ -180,7 +177,6 @@ export default function AdminStoragePanel(): React.ReactElement {

    - + {/* Under the backends it cleans up after, which keeps the two columns even. */} + void handleReclaimConfirm(request)} + onCheck={() => void handleReclaimCheck()} + onDelete={(enabled) => void handleReclaimDelete(enabled)} + /> + } + right={<>
    {STORAGE_CATEGORIES.map((category) => { @@ -544,26 +551,24 @@ export default function AdminStoragePanel(): React.ReactElement { ) })}
    + {/* Saving is part of the card it saves, under a hairline at its foot. */} +
    + + {admin.dirty && {t('storage.unsaved')}} + {migrationQueue.length > 0 && ( + + {t('storage.migrate.queued', { categories: categoryNames(t, migrationQueue.map((c) => c.category)) })} + + )} +
    - -
    - - {admin.dirty && {t('storage.unsaved')}} - {migrationQueue.length > 0 && ( - - {t('storage.migrate.queued', { categories: categoryNames(t, migrationQueue.map((c) => c.category)) })} - - )} -
    - {migratePromptOpen && (
    )} + } + /> {admin.saveError && (
    @@ -629,17 +633,6 @@ export default function AdminStoragePanel(): React.ReactElement {
    )} -
    - void handleReclaimConfirm(request)} - onCheck={() => void handleReclaimCheck()} - onDelete={(enabled) => void handleReclaimDelete(enabled)} - /> -
    {editing && ( @@ -132,7 +131,6 @@ export default function StorageReclaimSection({ diff --git a/client/src/components/Settings/AccountTab.tsx b/client/src/components/Settings/AccountTab.tsx index 39d383389..5c55781ab 100644 --- a/client/src/components/Settings/AccountTab.tsx +++ b/client/src/components/Settings/AccountTab.tsx @@ -9,9 +9,7 @@ import ConfirmDialog from '../shared/ConfirmDialog' import { Tooltip } from '../shared/Tooltip' import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import { EditorField, GRID_2, INPUT, PANEL } from '../shared/dialogParts' -import { - SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill, -} from './settingsKit' +import { SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill, SettingsColumns } from './settingsKit' import { stripTrailingSlashes, useAccountSettings } from './useAccountSettings' import AccountDemoLock from './AccountDemoLock' @@ -76,6 +74,10 @@ export default function AccountTab(): React.ReactElement { {/* The shared demo account: everything here is shown, nothing can be changed. */} + {/* Two columns by length: who you are and the second factor on the left, + the password and passkeys on the right. */} +
    {/* Who is signed in: the picture with its two actions, the role and the SSO link. */}
    @@ -169,54 +171,6 @@ export default function AccountTab(): React.ReactElement {
    - {/* Change Password */} - {!oidcOnlyMode && ( -
    - - setCurrentPassword(e.target.value)} - placeholder={t('settings.currentPassword')} - className={INPUT} - /> - -
    - - setNewPassword(e.target.value)} - placeholder={t('settings.newPassword')} - className={INPUT} - /> - - - setConfirmPassword(e.target.value)} - placeholder={t('settings.confirmPassword')} - className={INPUT} - /> - -
    - -
    - -
    -
    - )} - {/* MFA */}
    {mfaRequiredByPolicy && ( @@ -236,7 +190,6 @@ export default function AccountTab(): React.ReactElement { disabled={mfaLoading} onClick={startMfaSetup} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {mfaLoading ? : } {t('settings.mfa.setup')} @@ -296,7 +249,7 @@ export default function AccountTab(): React.ReactElement { {user?.mfa_enabled && (
    -

    {t('settings.mfa.disableTitle')}

    +

    {t('settings.mfa.disableTitle')}

    {t('settings.mfa.disableHint')}
    @@ -319,7 +272,7 @@ export default function AccountTab(): React.ReactElement { className={`${INPUT} font-geist tabular-nums`} />
    -
    +
    @@ -361,9 +314,60 @@ export default function AccountTab(): React.ReactElement { )}
    + } + right={<> + {/* Change Password */} + {!oidcOnlyMode && ( +
    + + setCurrentPassword(e.target.value)} + placeholder={t('settings.currentPassword')} + className={INPUT} + /> + +
    + + setNewPassword(e.target.value)} + placeholder={t('settings.newPassword')} + className={INPUT} + /> + + + setConfirmPassword(e.target.value)} + placeholder={t('settings.confirmPassword')} + className={INPUT} + /> + +
    + +
    + +
    +
    + )} {/* Passkeys */} + } + />
    {/* Delete Account Blocked */} diff --git a/client/src/components/Settings/AirTrailConnectionSection.test.tsx b/client/src/components/Settings/AirTrailConnectionSection.test.tsx index a26f00002..cefc47a5c 100644 --- a/client/src/components/Settings/AirTrailConnectionSection.test.tsx +++ b/client/src/components/Settings/AirTrailConnectionSection.test.tsx @@ -6,7 +6,6 @@ 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; @@ -38,8 +37,6 @@ 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 c7df67706..8feb470ae 100644 --- a/client/src/components/Settings/AirTrailConnectionSection.tsx +++ b/client/src/components/Settings/AirTrailConnectionSection.tsx @@ -5,7 +5,7 @@ import { useAirTrailSettings } from './useAirTrailSettings' import AirTrailIcon from '../shared/AirTrailIcon' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill } from './settingsKit' +import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, StatusPill } from './settingsKit' import { EditorField, GRID_2, INPUT } from '../shared/dialogParts' import { fs } from '../shared/DialogShell' @@ -41,9 +41,10 @@ export default function AirTrailConnectionSection(): React.ReactElement { return (
    }> {connected ? t('settings.airtrail.connected') : t('settings.airtrail.notConnected')} @@ -107,7 +108,6 @@ export default function AirTrailConnectionSection(): React.ReactElement { - {t('settings.airtrail.hint')}
    ) } diff --git a/client/src/components/Settings/ApiKeysSection.test.tsx b/client/src/components/Settings/ApiKeysSection.test.tsx index 7bc304a51..fccc31aaa 100644 --- a/client/src/components/Settings/ApiKeysSection.test.tsx +++ b/client/src/components/Settings/ApiKeysSection.test.tsx @@ -9,7 +9,6 @@ import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser } from '../../../tests/helpers/factories'; import { ToastContainer } from '../shared/Toast'; import ApiKeysSection from './ApiKeysSection'; -import { openSettingsCards } from '../../../tests/helpers/settingsCards'; const clipboardWriteText = vi.fn().mockResolvedValue(undefined); @@ -22,8 +21,6 @@ beforeAll(() => { }); 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 175d80480..6c9984640 100644 --- a/client/src/components/Settings/ApiKeysSection.tsx +++ b/client/src/components/Settings/ApiKeysSection.tsx @@ -239,12 +239,11 @@ export default function ApiKeysSection(): React.ReactElement { <>
    0 ? {keys.length}/{MAX_KEYS} : undefined} action={( - )} diff --git a/client/src/components/Settings/AppearanceSettingsTab.tsx b/client/src/components/Settings/AppearanceSettingsTab.tsx index 3833d77d2..bcda684db 100644 --- a/client/src/components/Settings/AppearanceSettingsTab.tsx +++ b/client/src/components/Settings/AppearanceSettingsTab.tsx @@ -5,7 +5,7 @@ import { DialogSection, fs } from '../shared/DialogShell' import { Segmented } from '../shared/dialogParts' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { SETTINGS_BUTTON, SettingRow, SettingRows, StatusPill } from './settingsKit' +import { SETTINGS_BUTTON, SettingRow, SettingRows, SettingsColumns, StatusPill } from './settingsKit' import { APPEARANCE_SCHEMES, CUSTOM_ACCENT_PRESETS } from '../../theme/schemes' import { APPEARANCE_SCALE_MIN, APPEARANCE_SCALE_MAX } from '@trek/shared' import { DESKTOP_GROUPS, MOBILE_GROUPS, isHex } from './appearanceModel' @@ -44,6 +44,8 @@ export default function AppearanceSettingsTab(): React.ReactElement { return ( <> + {/* ── Theme ───────────────────────────────────────────────── */}
    @@ -70,7 +72,8 @@ export default function AppearanceSettingsTab(): React.ReactElement { {/* Color scheme swatches */} -
    + {/* As many per line as fit at a width that holds every name in full. */} +
    {APPEARANCE_SCHEMES.map((s) => { const active = cfg.schemeId === s.id const dot = isDark ? s.swatch.dark : s.swatch.light @@ -208,6 +211,8 @@ export default function AppearanceSettingsTab(): React.ReactElement {
    + } + right={<> {/* ── Dashboard widgets ───────────────────────────────────── */}
    setWidget('desktop', g.master as string, !masterOn)} label={tr(`settings.appearance.widget.${g.master}`, WIDGET_LABELS[g.master])} />} - > - {/* Its widgets only matter while the column is shown: dimmed, not hidden, while it is off. */} - - {rows} - - + /> + {/* Its widgets, indented under it in the same block. They only matter while the + column is shown: dimmed, not hidden, while it is off. */} +
    + {rows} +
    ) })} @@ -265,9 +270,11 @@ export default function AppearanceSettingsTab(): React.ReactElement {
    + } + />
    - diff --git a/client/src/components/Settings/DawarichConnectionSection.test.tsx b/client/src/components/Settings/DawarichConnectionSection.test.tsx index 14765012f..7a3f78e7c 100644 --- a/client/src/components/Settings/DawarichConnectionSection.test.tsx +++ b/client/src/components/Settings/DawarichConnectionSection.test.tsx @@ -46,7 +46,6 @@ 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', () => ({ @@ -157,8 +156,6 @@ 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 95dd046c3..e88c323ee 100644 --- a/client/src/components/Settings/DawarichConnectionSection.tsx +++ b/client/src/components/Settings/DawarichConnectionSection.tsx @@ -6,7 +6,7 @@ import { useDawarichConnection } from '../../hooks/useDawarichConnection' import DawarichIcon from '../shared/DawarichIcon' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, SettingsHint, StatusPill } from './settingsKit' +import { SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, StatusPill } from './settingsKit' import { EditorField, GRID_2, INPUT } from '../shared/dialogParts' import { fs } from '../shared/DialogShell' @@ -30,16 +30,16 @@ export default function DawarichConnectionSection(): React.ReactElement { return (
    }> {S.connected ? t('dawarich.connected') : t('dawarich.notConnected')} } > - {t('dawarich.intro')}
    diff --git a/client/src/components/Settings/DisplaySettingsTab.test.tsx b/client/src/components/Settings/DisplaySettingsTab.test.tsx index 7d7d28426..9226c2e7a 100644 --- a/client/src/components/Settings/DisplaySettingsTab.test.tsx +++ b/client/src/components/Settings/DisplaySettingsTab.test.tsx @@ -1,5 +1,5 @@ // FE-COMP-DISPLAY-001 to FE-COMP-DISPLAY-057 -import { render, screen, within, fireEvent } from '../../../tests/helpers/render'; +import { render, screen, within } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { server } from '../../../tests/helpers/msw/server'; @@ -57,20 +57,20 @@ describe('DisplaySettingsTab', () => { expect(screen.getByText(/12h/i)).toBeInTheDocument(); }); - it('FE-COMP-DISPLAY-015: clicking a language button calls updateSetting with that language code', async () => { + it('FE-COMP-DISPLAY-015: picking a language in the select calls updateSetting with its code', async () => { const user = userEvent.setup(); const updateSetting = vi.fn().mockResolvedValue(undefined); seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }), updateSetting }); render(); - await user.click(screen.getByText('Deutsch')); + await user.click(screen.getByRole('button', { name: 'Language' })); + await user.click(await screen.findByText('Deutsch')); expect(updateSetting).toHaveBeenCalledWith('language', 'de'); }); - it('FE-COMP-DISPLAY-016: active language button is visually highlighted', () => { + it('FE-COMP-DISPLAY-016: the language select shows the current language', () => { seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }) }); render(); - // "English" is on the chip and on the compact trigger; only the chip is a pressed choice. - expect(screen.getByRole('button', { name: 'English', pressed: true })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Language' })).toHaveTextContent('English'); }); it('FE-COMP-DISPLAY-017: shows Temperature section label', () => { @@ -199,7 +199,7 @@ describe('DisplaySettingsTab – Display currency', () => { render(); await user.click(screen.getByRole('button', { name: 'Same as the app' })); - // The language grid shows the same name, so the option is the last one on screen. + // The app language is German here, so the only English on screen is the place option. const options = await screen.findAllByText('English'); await user.click(options[options.length - 1]); @@ -232,85 +232,19 @@ describe('DisplaySettingsTab – Display currency', () => { }); }); -// ── Compact language dropdown (035–039) ─────────────────────────────────────── +// ── Language errors (037) ───────────────────────────────────────────────────── -function mobileLangWrap(): HTMLElement { - return optionBlock('Language').querySelector('.sm\\:hidden') as HTMLElement; -} - -describe('DisplaySettingsTab – Compact language picker', () => { - it('FE-COMP-DISPLAY-035: the compact trigger shows the active language and opens the list', async () => { - const user = userEvent.setup(); - render(); - const wrap = mobileLangWrap(); - - expect(within(wrap).getAllByRole('button')).toHaveLength(1); - expect(within(wrap).getByText('English')).toBeInTheDocument(); - - await user.click(within(wrap).getByRole('button')); - - expect(within(wrap).getAllByRole('button').length).toBeGreaterThan(1); - }); - - it('FE-COMP-DISPLAY-036: picking a language from the list saves it and closes the list', async () => { - const user = userEvent.setup(); - const updateSetting = vi.fn().mockResolvedValue(undefined); - seedStore(useSettingsStore, { settings: buildSettings({ language: 'en' }), updateSetting }); - render(); - const wrap = mobileLangWrap(); - - await user.click(within(wrap).getByRole('button')); - await user.click(within(wrap).getByText('Deutsch').closest('button')!); - - expect(updateSetting).toHaveBeenCalledWith('language', 'de'); - expect(within(wrap).getAllByRole('button')).toHaveLength(1); - }); - - it('FE-COMP-DISPLAY-037: a rejected pick from the list surfaces the error', async () => { - const user = userEvent.setup(); - seedFailing('Language locked'); - render(<>); - const wrap = mobileLangWrap(); - - await user.click(within(wrap).getByRole('button')); - await user.click(within(wrap).getByText('Deutsch').closest('button')!); - - expect(await screen.findByText('Language locked')).toBeInTheDocument(); - }); - - it('FE-COMP-DISPLAY-048: a rejected pick from the desktop grid surfaces the error', async () => { +describe('DisplaySettingsTab – Language select', () => { + it('FE-COMP-DISPLAY-037: a rejected language pick surfaces the error', async () => { const user = userEvent.setup(); seedFailing('Language locked'); render(<>); - await user.click(screen.getByText('Deutsch')); + await user.click(screen.getByRole('button', { name: 'Language' })); + await user.click(await screen.findByText('Deutsch')); expect(await screen.findByText('Language locked')).toBeInTheDocument(); }); - - it('FE-COMP-DISPLAY-038: a mousedown outside the picker closes it', async () => { - const user = userEvent.setup(); - render(); - const wrap = mobileLangWrap(); - - await user.click(within(wrap).getByRole('button')); - expect(within(wrap).getAllByRole('button').length).toBeGreaterThan(1); - - fireEvent.mouseDown(document.body); - - expect(within(wrap).getAllByRole('button')).toHaveLength(1); - }); - - it('FE-COMP-DISPLAY-039: a mousedown inside the picker keeps it open', async () => { - const user = userEvent.setup(); - render(); - const wrap = mobileLangWrap(); - - await user.click(within(wrap).getByRole('button')); - fireEvent.mouseDown(within(wrap).getByText('Deutsch')); - - expect(within(wrap).getAllByRole('button').length).toBeGreaterThan(1); - }); }); // ── Map & privacy toggles (040–047) ─────────────────────────────────────────── diff --git a/client/src/components/Settings/DisplaySettingsTab.tsx b/client/src/components/Settings/DisplaySettingsTab.tsx index 89f73647c..4e46bc995 100644 --- a/client/src/components/Settings/DisplaySettingsTab.tsx +++ b/client/src/components/Settings/DisplaySettingsTab.tsx @@ -1,17 +1,14 @@ -import React, { useState, useEffect, useRef } from 'react' -import { Languages, Map, ChevronDown, Rocket } from 'lucide-react' +import React, { useState, useEffect } from 'react' +import { Languages, Map, Rocket } from 'lucide-react' import { SUPPORTED_LANGUAGES, useTranslation } from '../../i18n' import { DEFAULT_SETTINGS } from '../../store/settingsStore' import CustomSelect from '../shared/CustomSelect' import { Segmented } from '../shared/dialogParts' -import { fs } from '../shared/DialogShell' -import { POPOVER } from '../Packing/packingPopoverStyles' -import { PopoverItem } from '../Packing/PackingPopover' import { preferredNavAppOptions } from '../Planner/placeNavigation' import { SYMBOLS, currenciesWith } from '../Budget/BudgetPanel.constants' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { ChoiceChips, SettingRow, SettingRows } from './settingsKit' +import { SettingRow, SettingRows, SettingsColumns } from './settingsKit' import { useSettingSaver } from './useSettingSaver' import { TRIP_TAB_IDS, TRIP_TAB_LABEL_KEYS } from '../../constants/tripTabs' import { DEFAULT_START_PAGE, DEFAULT_START_TRIP_TAB } from '../../utils/startDestination' @@ -31,18 +28,6 @@ export default function DisplaySettingsTab(): React.ReactElement { const demoLocked = useDemoAccountLock() const [tempUnit, setTempUnit] = useState(settings.temperature_unit || DEFAULT_SETTINGS.temperature_unit) const [distanceUnit, setDistanceUnit] = useState(settings.distance_unit || DEFAULT_SETTINGS.distance_unit) - const [langOpen, setLangOpen] = useState(false) - const langDropdownRef = useRef(null) - - useEffect(() => { - if (!langOpen) return - const handler = (e: MouseEvent) => { - if (langDropdownRef.current && !langDropdownRef.current.contains(e.target as Node)) setLangOpen(false) - } - document.addEventListener('mousedown', handler) - return () => document.removeEventListener('mousedown', handler) - }, [langOpen]) - useEffect(() => { setTempUnit(settings.temperature_unit || DEFAULT_SETTINGS.temperature_unit) }, [settings.temperature_unit]) @@ -55,12 +40,12 @@ export default function DisplaySettingsTab(): React.ReactElement { const startPage = settings.start_page === 'active_trip' ? 'active_trip' : DEFAULT_START_PAGE const startTripTab = settings.start_trip_tab || DEFAULT_START_TRIP_TAB - const currentLanguage = SUPPORTED_LANGUAGES.find(o => o.value === settings.language) || SUPPORTED_LANGUAGES[0] const pickLanguage = (value: string) => save('language', value) return ( - <> +
    {/* Where opening TREK lands */} @@ -127,47 +112,23 @@ export default function DisplaySettingsTab(): React.ReactElement { } /> - {/* Language: every option at a glance on a wide window, a compact list on a narrow one. - The shared demo account always runs in English. */} + {/* Language: a select like every other choice in this card. The shared + demo account always runs in English. */} - -
    - ({ value: opt.value, label: opt.label }))} - onChange={pickLanguage} - /> -
    -
    - - {langOpen && ( -
    - {SUPPORTED_LANGUAGES.map(opt => ( - { - setLangOpen(false) - await pickLanguage(opt.value) - }} - /> - ))} -
    - )} -
    -
    + + void pickLanguage(String(v))} + options={SUPPORTED_LANGUAGES.map(opt => ({ value: opt.value, label: opt.label }))} + searchable + ariaLabel={t('settings.language')} + /> +
    + } + /> {/* Place names (#1799): searches can answer in another language than the app. */} @@ -286,6 +247,8 @@ export default function DisplaySettingsTab(): React.ReactElement {
    + } + right={
    {/* Preferred map app (#2423): opt-in, the picker stays the default */} @@ -327,7 +290,8 @@ export default function DisplaySettingsTab(): React.ReactElement { saveOnOff('optimize_from_accommodation', value)} />
    - + } + /> ) } diff --git a/client/src/components/Settings/IntegrationsTab.test.tsx b/client/src/components/Settings/IntegrationsTab.test.tsx index 02e0b6005..acef51176 100644 --- a/client/src/components/Settings/IntegrationsTab.test.tsx +++ b/client/src/components/Settings/IntegrationsTab.test.tsx @@ -10,7 +10,6 @@ import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser } from '../../../tests/helpers/factories'; import { ToastContainer } from '../shared/Toast'; import IntegrationsTab from './IntegrationsTab'; -import { openSettingsCards } from '../../../tests/helpers/settingsCards'; function enableMcp() { seedStore(useAddonStore, { @@ -32,6 +31,11 @@ function mcpEndpointRow() { return { code: row.querySelector('code')!, copy: row.querySelector('button')! }; } +/** Waits for the MCP card: its endpoint, clients and tokens are on the page, nothing to open. */ +async function openMcp() { + await screen.findByText('MCP Endpoint'); +} + const clipboardWriteText = vi.fn().mockResolvedValue(undefined); function enableAddons(ids: string[]) { @@ -51,8 +55,6 @@ beforeAll(() => { }); beforeEach(() => { - // The cards start shut; these tests read into them. - openSettingsCards(); clipboardWriteText.mockClear(); resetAllStores(); useAssistantStore.setState({ status: null }); @@ -91,21 +93,18 @@ describe('IntegrationsTab', () => { it('FE-COMP-INTEGRATIONS-004: MCP endpoint URL is displayed', async () => { enableMcp(); render(); - await screen.findByText('MCP Configuration'); + await openMcp(); const { code } = mcpEndpointRow(); expect(code).not.toBeNull(); expect(code.textContent).toContain('/mcp'); }); - it('FE-COMP-INTEGRATIONS-005: JSON config block is rendered when expanded', async () => { - const user = userEvent.setup(); + it('FE-COMP-INTEGRATIONS-005: JSON config block is always rendered', async () => { enableMcp(); render(); - await screen.findByText('MCP Configuration'); - // Config is collapsed by default — no
     yet
    -    expect(document.querySelector('pre')).toBeNull();
    -    // Expand by clicking the "Client Configuration" toggle
    -    await user.click(screen.getByRole('button', { name: /Client Configuration/i }));
    +    await openMcp();
    +    // The client config is always shown: nothing folds away in the settings.
    +    expect(screen.getByText('Client Configuration')).toBeInTheDocument();
         const preEl = document.querySelector('pre');
         expect(preEl).not.toBeNull();
         expect(preEl!.textContent).toContain('mcpServers');
    @@ -115,7 +114,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         expect(await screen.findByText('No tokens yet. Create one to connect MCP clients.')).toBeInTheDocument();
       });
    @@ -134,7 +133,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await screen.findByText('My Token');
         expect(await screen.findByText('Other Token')).toBeInTheDocument();
    @@ -144,7 +143,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         const createBtn = screen.getByRole('button', { name: /Create New Token/i });
         await user.click(createBtn);
    @@ -155,7 +154,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await user.click(screen.getByRole('button', { name: /Create New Token/i }));
         await screen.findByText('Create API Token');
    @@ -167,7 +166,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await user.click(screen.getByRole('button', { name: /Create New Token/i }));
         await screen.findByText('Create API Token');
    @@ -194,7 +193,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await user.click(screen.getByRole('button', { name: /Create New Token/i }));
         await screen.findByText('Create API Token');
    @@ -224,7 +223,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await user.click(screen.getByRole('button', { name: /Create New Token/i }));
         await screen.findByText('Create API Token');
    @@ -250,7 +249,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await screen.findByText('Delete Me');
         await user.click(screen.getByLabelText('Delete Token'));
    @@ -276,7 +275,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await screen.findByText('Delete Me');
         await user.click(screen.getByLabelText('Delete Token'));
    @@ -294,7 +293,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         // Spy after userEvent.setup() may have replaced navigator.clipboard
         const writeSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
         await user.click(mcpEndpointRow().copy);
    @@ -305,7 +304,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
         const { copy } = mcpEndpointRow();
         await user.click(copy);
    @@ -334,7 +333,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await screen.findByText('Cancel Token');
         await user.click(screen.getByLabelText('Delete Token'));
    @@ -365,7 +364,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /API Tokens/i }));
         await user.click(screen.getByRole('button', { name: /Create New Token/i }));
         await screen.findByText('Create API Token');
    @@ -380,7 +379,7 @@ describe('IntegrationsTab', () => {
       it('FE-COMP-INTEGRATIONS-019: default tab is OAuth 2.1 Clients — OAuth hint visible, token list hidden', async () => {
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         // OAuth hint is visible on the default tab
         expect(screen.getByText(/Register OAuth 2\.1 clients/i)).toBeInTheDocument();
         // API Tokens "no tokens" message is not rendered
    @@ -391,7 +390,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         // Default: OAuth hint visible, token list absent
         expect(screen.getByText(/Register OAuth 2\.1 clients/i)).toBeInTheDocument();
         expect(screen.queryByText('No tokens yet. Create one to connect MCP clients.')).toBeNull();
    @@ -475,7 +474,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         expect(screen.getByText('Claude.ai')).toBeInTheDocument();
    @@ -486,7 +485,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         // Presets render as buttons — click "Claude.ai" preset
    @@ -516,7 +515,7 @@ describe('IntegrationsTab', () => {
         );
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
     
    @@ -550,7 +549,7 @@ describe('IntegrationsTab', () => {
         );
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'TC2');
    @@ -646,7 +645,7 @@ describe('IntegrationsTab', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         const createBtn = screen.getByRole('button', { name: /Register Client/i });
    @@ -665,7 +664,7 @@ describe('IntegrationsTab', () => {
         );
         enableMcp();
         render(<>);
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client');
    @@ -680,7 +679,7 @@ describe('IntegrationsTab', () => {
         server.use(http.post('/api/oauth/clients', () => HttpResponse.error()));
         enableMcp();
         render(<>);
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
         await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client');
    @@ -710,7 +709,7 @@ const oneToken = [
     ];
     
     async function openTokensTab(user: ReturnType) {
    -  await screen.findByText('MCP Configuration');
    +  await openMcp();
       await user.click(screen.getByRole('button', { name: /API Tokens/i }));
     }
     
    @@ -725,10 +724,9 @@ describe('IntegrationsTab – copy actions and cancels', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         const writeSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
     
    -    await user.click(screen.getByRole('button', { name: /Client Configuration/i }));
         await user.click(labelledCopyButton());
     
         expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('mcp'));
    @@ -742,7 +740,6 @@ describe('IntegrationsTab – copy actions and cancels', () => {
         await openTokensTab(user);
         const writeSpy = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue(undefined);
     
    -    await user.click(screen.getByRole('button', { name: /Client Configuration/i }));
         await user.click(labelledCopyButton());
     
         expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('mcp'));
    @@ -790,7 +787,7 @@ describe('IntegrationsTab – copy actions and cancels', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
     
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
    @@ -803,7 +800,7 @@ describe('IntegrationsTab – copy actions and cancels', () => {
         const user = userEvent.setup();
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await screen.findByText('Register OAuth Client');
     
    @@ -834,7 +831,7 @@ describe('IntegrationsTab – copy actions and cancels', () => {
         );
         enableMcp();
         render();
    -    await screen.findByText('MCP Configuration');
    +    await openMcp();
         await user.click(screen.getByRole('button', { name: /New Client/i }));
         await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Copyable');
         await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://localhost');
    @@ -1015,6 +1012,7 @@ describe('IntegrationsTab – failure toasts', () => {
     
         enableAddons(['ai_assistant']);
         const { unmount } = render();
    +    // Its own card on the page, the connection form open in it.
         expect(await screen.findByText('What would you like to chat with?')).toBeInTheDocument();
         unmount();
     
    diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx
    index abe042a56..c0d8496f7 100644
    --- a/client/src/components/Settings/IntegrationsTab.tsx
    +++ b/client/src/components/Settings/IntegrationsTab.tsx
    @@ -3,7 +3,7 @@ import React, { useId } from 'react'
     import { useTranslation } from '../../i18n'
     import { useFormatDate } from '../../hooks/useDateFormat'
     import { useToast } from '../shared/Toast'
    -import { Trash2, Copy, Terminal, Plus, Check, Key, KeyRound, ChevronRight, RefreshCw, AlertTriangle, Link2 } from 'lucide-react'
    +import { Trash2, Copy, Terminal, Plus, Check, Key, KeyRound, RefreshCw, AlertTriangle, Link2 } from 'lucide-react'
     import PhotoProvidersSection from './PhotoProvidersSection'
     import AirTrailConnectionSection from './AirTrailConnectionSection'
     import MailImportSection from './MailImportSection'
    @@ -16,7 +16,7 @@ import ConfirmDialog from '../shared/ConfirmDialog'
     import { Tooltip } from '../shared/Tooltip'
     import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
     import { EditorField, INPUT, Segmented, TEXTAREA } from '../shared/dialogParts'
    -import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SETTINGS_ICON_BUTTON, SettingRows, SettingsHint, StatusPill } from './settingsKit'
    +import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SETTINGS_ICON_BUTTON, SettingRows, SettingsColumns, SettingsHint, StatusPill } from './settingsKit'
     import { OAUTH_PRESETS, useMcpIntegration, type OAuthClient } from './useMcpIntegration'
     import { useIntegrationGates } from './useIntegrationGates'
     import WandererConnectionSection from './WandererConnectionSection'
    @@ -29,47 +29,43 @@ export default function IntegrationsTab(): React.ReactElement {
       const demoLocked = useDemoAccountLock()
       return (
         <>
    -      {/* Two columns from xl up, as on the admin settings: what connects your
    -          trips with other services on the left, AI and developer access on the
    -          right. Flex, not a grid: a column with nothing to show (a managed
    -          install without the mail import) is empty and drops out, and the other
    -          takes the whole width instead of leaving a hole. The cards carry their
    -          own bottom margin, so only the gap between the columns is set. */}
    -      
    -
    - {/* Forwarded booking mails: hides itself where this server or plan has no import. */} - - {/* Immich, Synology Photos and AirTrail all connect to a server the reader - runs themselves. On a managed install there is none, and every one of - these would ask for an address that cannot be reached from here. */} - {!managed && } - {S.airtrailEnabled && !managed && } - {S.dawarichEnabled && !managed && } - {S.wandererEnabled && !managed && } -
    -
    - {/* Which model reads a booking, and what that costs, comes with the instance on - a managed install. The per-user fallback exists for people who supply their - own key, and there nobody does. */} - {/* The assistant in TREK, with the same managed rule as AI parsing: there the - operator decides which model the install talks to. */} - {S.aiConnectionEnabled && !managed && ( - - )} - {S.llmEnabled && S.ownParsing && !managed && } - {/* Above MCP on purpose: an API key needs no addon, and someone looking for - one should not have to read past a section about AI assistants. */} - - - - {/* TREK inside Claude rides on the MCP server, so it follows that addon. */} - {S.mcpEnabled && ( - - + {/* What connects your trips with other services on the left, AI and + developer access on the right, every card open on the page. */} + + {/* Forwarded booking mails: hides itself where this server or plan has no import. */} + + {/* Immich, Synology Photos and AirTrail all connect to a server the reader + runs themselves. On a managed install there is none, and every one of + these would ask for an address that cannot be reached from here. */} + {!managed && } + {S.airtrailEnabled && !managed && } + {S.dawarichEnabled && !managed && } + {S.wandererEnabled && !managed && } + {/* The assistant and AI parsing: on a managed install the operator decides + which model the install talks to, so neither is offered there. */} + {S.aiConnectionEnabled && !managed && ( + + )} + {S.llmEnabled && S.ownParsing && !managed && } + + } + right={ + <> + {/* An API key needs no addon, so it leads, ahead of the MCP row. */} + + - )} -
    -
    + {/* TREK inside Claude rides on the MCP server, so it follows that addon. */} + {S.mcpEnabled && ( + + + + )} + + } + /> @@ -151,10 +147,8 @@ function EmptyHint({ children }: { children: React.ReactNode }) { ) } -/** The mcp-remote snippet, folded away until asked for. */ -function ConfigBlock({ open, onToggle, json, hint, copied, onCopy, t }: { - open: boolean - onToggle: () => void +/** The mcp-remote snippet, shown with its copy button. */ +function ConfigBlock({ json, hint, copied, onCopy, t }: { json: string hint: string copied: boolean @@ -163,33 +157,26 @@ function ConfigBlock({ open, onToggle, json, hint, copied, onCopy, t }: { }) { return (
    - - {open && ( -
    -
    - -
    -
    -            {json}
    -          
    - {hint} -
    - )} + +
    +
    +
    +          {json}
    +        
    + {hint} +
    ) } /** One registered OAuth client: what it is, the scopes it may use, and its two actions. */ function OAuthClientRow({ client, S }: { client: OAuthClient; S: IntegrationsState }) { - const { t, formatDate, oauthScopesExpanded, setOauthScopesExpanded, setOauthRotateId, setOauthDeleteId } = S - const expanded = !!oauthScopesExpanded[client.id] + const { t, formatDate, setOauthRotateId, setOauthDeleteId } = S return (
    @@ -205,17 +192,10 @@ function OAuthClientRow({ client, S }: { client: OAuthClient; S: IntegrationsSta {t('settings.mcp.tokenCreatedAt')} {formatDate(new Date(client.created_at))}
    - {(expanded ? client.allowed_scopes : client.allowed_scopes.slice(0, 5)).map(s => ( + {/* Every scope the client may use, none folded behind a "+N". */} + {client.allowed_scopes.map(s => ( {s} ))} - {client.allowed_scopes.length > 5 && ( - - )}
    @@ -233,13 +213,11 @@ function OAuthClientRow({ client, S }: { client: OAuthClient; S: IntegrationsSta /** The OAuth tab: the snippet, the registered clients and the sessions they hold. */ function OAuthTab({ S }: { S: IntegrationsState }) { const { - t, formatDate, oauthClients, oauthSessions, setOauthCreateOpen, setOauthNewName, setOauthNewUris, setOauthNewScopes, setOauthCreatedClient, setOauthRevokeId, setOauthIsMachine, configOpenOAuth, setConfigOpenOAuth, copiedKey, mcpJsonConfigOAuth, handleCopy, + t, formatDate, oauthClients, oauthSessions, setOauthCreateOpen, setOauthNewName, setOauthNewUris, setOauthNewScopes, setOauthCreatedClient, setOauthRevokeId, setOauthIsMachine, copiedKey, mcpJsonConfigOAuth, handleCopy, } = S return ( <> setConfigOpenOAuth(o => !o)} json={mcpJsonConfigOAuth} hint={t('settings.mcp.clientConfigHintOAuth')} copied={copiedKey === 'json-oauth'} @@ -251,7 +229,7 @@ function OAuthTab({ S }: { S: IntegrationsState }) {
    {t('settings.oauth.clientsHint')}
    @@ -295,14 +273,12 @@ function OAuthTab({ S }: { S: IntegrationsState }) { /** The deprecated API tokens tab: the warning, the snippet and the tokens. */ function ApiTokensTab({ S }: { S: IntegrationsState }) { - const { t, formatDate, configOpenToken, setConfigOpenToken, mcpTokens, setMcpModalOpen, setMcpNewName, setMcpCreatedToken, setMcpDeleteId, copiedKey, mcpJsonConfig, handleCopy } = S + const { t, formatDate, mcpTokens, setMcpModalOpen, setMcpNewName, setMcpCreatedToken, setMcpDeleteId, copiedKey, mcpJsonConfig, handleCopy } = S return ( <> {t('settings.mcp.apiTokensDeprecated')} setConfigOpenToken(o => !o)} json={mcpJsonConfig} hint={t('settings.mcp.clientConfigHint')} copied={copiedKey === 'json-token'} @@ -312,7 +288,7 @@ function ApiTokensTab({ S }: { S: IntegrationsState }) {
    @@ -352,13 +328,12 @@ function IntegrationsMcpSection({ S }: { S: IntegrationsState }) { return (
    0 ? }>{t('settings.mcp.appsConnected', { count: connected })} - : {t('settings.notSetUp')} + : }>{t('settings.notSetUp')} } > {/* Endpoint URL */} diff --git a/client/src/components/Settings/LlmConnectionSection.test.tsx b/client/src/components/Settings/LlmConnectionSection.test.tsx index 29a3250f2..aeb982a32 100644 --- a/client/src/components/Settings/LlmConnectionSection.test.tsx +++ b/client/src/components/Settings/LlmConnectionSection.test.tsx @@ -8,7 +8,6 @@ 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); @@ -51,8 +50,6 @@ 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 1cfdc52e1..6181ca303 100644 --- a/client/src/components/Settings/LlmConnectionSection.tsx +++ b/client/src/components/Settings/LlmConnectionSection.tsx @@ -45,7 +45,6 @@ export default function LlmConnectionSection(): React.ReactElement { return (
    } @@ -117,5 +116,5 @@ function ParsingState() { const model = useSettingsStore(s => s.settings.llm_model) return model ? }>{model} - : {t('settings.notSetUp')} + : }>{t('settings.notSetUp')} } diff --git a/client/src/components/Settings/MailImportSection.tsx b/client/src/components/Settings/MailImportSection.tsx index 8dc2561c5..7a43c3ef9 100644 --- a/client/src/components/Settings/MailImportSection.tsx +++ b/client/src/components/Settings/MailImportSection.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, Copy, FileUp, Inbox, Loader2, Mail, RefreshCw } from 'lucide-react'; +import { Copy, FileUp, Inbox, Loader2, Mail, RefreshCw } from 'lucide-react'; import { useRef, type ReactElement } from 'react'; import { useTranslation } from '../../i18n'; import { openMailInbox } from '../MailIngest/mailInboxOpener'; @@ -31,7 +31,6 @@ export default function MailImportSection(): ReactElement | null { return (
    {t('mailInbox.settings.addressHint')} -
    - - - {t('mailInbox.settings.howTo')} - +
    +
    {t('mailInbox.settings.howTo')}
    • {t('mailInbox.settings.howToGmail')}
    • {t('mailInbox.settings.howToOutlook')}
    • {t('mailInbox.settings.howToManual')}
    -
    + )} diff --git a/client/src/components/Settings/MapSettingsTab.tsx b/client/src/components/Settings/MapSettingsTab.tsx index 05033b42b..4a2b30824 100644 --- a/client/src/components/Settings/MapSettingsTab.tsx +++ b/client/src/components/Settings/MapSettingsTab.tsx @@ -379,7 +379,6 @@ export default function MapSettingsTab(): React.ReactElement { onClick={saveMapSettings} disabled={saving} className={SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {saving ? : } {t('settings.saveMap')} diff --git a/client/src/components/Settings/NotificationsTab.tsx b/client/src/components/Settings/NotificationsTab.tsx index bc36483a0..9eddfae70 100644 --- a/client/src/components/Settings/NotificationsTab.tsx +++ b/client/src/components/Settings/NotificationsTab.tsx @@ -119,10 +119,10 @@ export default function NotificationsTab(): React.ReactElement { aria-label={t('settings.webhookUrl.label')} className={`${INPUT} min-w-[220px] flex-1`} /> - - @@ -164,17 +164,17 @@ export default function NotificationsTab(): React.ReactElement { className={`${INPUT} flex-1`} /> {ntfyTokenIsSet && ( - )}
    - -
    @@ -198,7 +198,7 @@ export default function NotificationsTab(): React.ReactElement { {/* Unconfigured is the common case on first use — send them to where the credentials actually live rather than just naming the place. */} {!ch.configured && ch.settingsPath && ( - + {t('settings.notificationPreferences.configure')} )} @@ -206,7 +206,6 @@ export default function NotificationsTab(): React.ReactElement { onClick={() => testChannel(ch)} disabled={!ch.configured || channelTesting === ch.id} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {t('settings.notificationPreferences.sendTest')} diff --git a/client/src/components/Settings/OfflineTab.tsx b/client/src/components/Settings/OfflineTab.tsx index f10077ea9..e4a88f079 100644 --- a/client/src/components/Settings/OfflineTab.tsx +++ b/client/src/components/Settings/OfflineTab.tsx @@ -14,7 +14,7 @@ import React, { useState } from 'react' import { RefreshCw, Trash2, Database, CloudOff, Download, Check, GitMerge, Map as MapIcon, AlertTriangle } from 'lucide-react' import Section from './Section' import ToggleSwitch from './ToggleSwitch' -import { SettingRow, SettingRows, SettingsHint, StatusPill, SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER } from './settingsKit' +import { SettingRow, SettingRows, SettingsHint, StatusPill, SETTINGS_BUTTON, SETTINGS_BUTTON_DANGER, SettingsColumns } from './settingsKit' import { useOfflineSettings, offlineNoticeKey, isOfflineNoticeWarning, offlineProgressLabel, offlineRowMeta } from './useOfflineSettings' import { useTranslation } from '../../i18n' import CustomSelect from '../shared/CustomSelect' @@ -49,6 +49,10 @@ export default function OfflineTab(): React.ReactElement { return (
    + {/* Two columns by length: what the mode does and what is cached on the + left, the per-trip choice of what to keep on the right. */} + {/* Offline mode + prepare */}
    @@ -71,7 +75,6 @@ export default function OfflineTab(): React.ReactElement { onClick={runPrepare} disabled={preparing || offline} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {preparing ? @@ -82,7 +85,6 @@ export default function OfflineTab(): React.ReactElement { onClick={handleResync} disabled={syncing || offline} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {syncing ? t('settings.offline.resyncing') : t('settings.offline.resync')} @@ -130,10 +132,10 @@ export default function OfflineTab(): React.ReactElement { {t('settings.offline.conflicts.item', { name: conflictName(c) })}
    - -
    @@ -163,41 +165,6 @@ export default function OfflineTab(): React.ReactElement {
    )} - {/* What to store offline */} -
    - - } - /> - - {allTrips.length > 0 && ( -
    -

    {t('settings.offline.storage.tripsTitle')}

    - - {allTrips.map((trip) => { - const { on, dateEligible } = tripStorageState(trip) - return ( -
    -
    -
    - {trip.title} -
    -
    - {on ? t('settings.offline.storage.tripOn') : t('settings.offline.storage.tripOff')} - {!dateEligible && `. ${t('settings.offline.storage.tripFinished')}`} -
    -
    - handleToggleTrip(trip)} label={trip.title} /> -
    - ) - })} -
    -
    - )} -
    - {/* Cache stats + list + clear */}
    setConfirmClear(true)} disabled={clearing || !canClear} className={SETTINGS_BUTTON_DANGER} - style={fs(12.5, 'body')} > {t('settings.offline.clear')} @@ -225,11 +191,11 @@ export default function OfflineTab(): React.ReactElement {
    {t('settings.offline.failed.hint')}
    - - @@ -268,6 +234,44 @@ export default function OfflineTab(): React.ReactElement { )}
    + } + right={<> + {/* What to store offline */} +
    + + } + /> + + {allTrips.length > 0 && ( +
    +

    {t('settings.offline.storage.tripsTitle')}

    + + {allTrips.map((trip) => { + const { on, dateEligible } = tripStorageState(trip) + return ( +
    +
    +
    + {trip.title} +
    +
    + {on ? t('settings.offline.storage.tripOn') : t('settings.offline.storage.tripOff')} + {!dateEligible && `. ${t('settings.offline.storage.tripFinished')}`} +
    +
    + handleToggleTrip(trip)} label={trip.title} /> +
    + ) + })} +
    +
    + )} +
    + } + /> 0 ? {pk.creds.length} : undefined} // The way in sits on the band, like the planner's "add" actions; the step-up opens in the body. action={pk.canAdd && !pk.addOpen ? ( - diff --git a/client/src/components/Settings/PhotoProvidersSection.test.tsx b/client/src/components/Settings/PhotoProvidersSection.test.tsx index d7ef70671..db953ba5f 100644 --- a/client/src/components/Settings/PhotoProvidersSection.test.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.test.tsx @@ -9,7 +9,6 @@ import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser } from '../../../tests/helpers/factories'; import { ToastContainer } from '../shared/Toast'; import PhotoProvidersSection from './PhotoProvidersSection'; -import { openSettingsCards } from '../../../tests/helpers/settingsCards'; const fakeProvider = { id: 'immich', @@ -46,8 +45,6 @@ function seedMemoriesEnabled(providers = [fakeProvider]) { } beforeEach(() => { - // 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 42e108884..5a3db7064 100644 --- a/client/src/components/Settings/PhotoProvidersSection.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.tsx @@ -36,7 +36,6 @@ export default function PhotoProvidersSection(): React.ReactElement { return (
    {t('settings.plugins.oauth.connected')} : t('settings.plugins.oauth.notConnected')} control={state.connected - ? - : } + ? + : } /> ) @@ -158,7 +158,7 @@ function PluginSettingsForm({ id, name, icon }: { id: string; name: string; icon ))}
    - @@ -180,7 +180,6 @@ function PluginSettingsForm({ id, name, icon }: { id: string; name: string; icon onClick={() => runAction(a)} disabled={running !== null} className={a.danger ? SETTINGS_BUTTON_DANGER : SETTINGS_BUTTON} - style={fs(13, 'body')} > {running === a.key && } {a.label} diff --git a/client/src/components/Settings/Section.tsx b/client/src/components/Settings/Section.tsx index 8033a358f..70390ce0c 100644 --- a/client/src/components/Settings/Section.tsx +++ b/client/src/components/Settings/Section.tsx @@ -9,22 +9,24 @@ interface SectionProps { * standing in for a glyph gets sized by the same utilities. */ icon: LucideIcon | React.ComponentType<{ className?: string }> + /** The icon is a brand badge that fills its tile instead of a glyph in it. */ + iconFill?: boolean badge?: React.ReactNode /** One line under the title in the head band. */ hint?: React.ReactNode /** A control on the right of the head band. */ action?: React.ReactNode - /** Where a link lands and, for a collapsible card, what it is remembered by. */ + /** Where a link lands: `#id` scrolls to the card, or opens it from an overview. */ id?: string - /** Shut until its head is clicked, the badge saying its state meanwhile (see settingsKit). */ - collapsible?: boolean children: React.ReactNode } -/** A settings group: the planner's card with a head band (see settingsKit). */ -export default function Section({ title, icon, badge, hint, action, id, collapsible, children }: SectionProps): React.ReactElement { +/** + * A settings group: the planner's card with a head band (see settingsKit). + */ +export default function Section({ title, icon, iconFill, badge, hint, action, id, children }: SectionProps): React.ReactElement { return ( - + {children} ) diff --git a/client/src/components/Settings/WandererConnectionSection.test.tsx b/client/src/components/Settings/WandererConnectionSection.test.tsx index acd542904..58b72d54b 100644 --- a/client/src/components/Settings/WandererConnectionSection.test.tsx +++ b/client/src/components/Settings/WandererConnectionSection.test.tsx @@ -4,7 +4,6 @@ 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'; @@ -32,8 +31,6 @@ 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 2301b6f88..4a79c1e35 100644 --- a/client/src/components/Settings/WandererConnectionSection.tsx +++ b/client/src/components/Settings/WandererConnectionSection.tsx @@ -11,7 +11,6 @@ import { SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, - SettingsHint, StatusPill, } from './settingsKit'; import ToggleSwitch from './ToggleSwitch'; @@ -32,8 +31,8 @@ export default function WandererConnectionSection(): React.ReactElement { return (
    } > - {t('wanderer.intro')}
    diff --git a/client/src/components/Settings/WandererWatchRow.tsx b/client/src/components/Settings/WandererWatchRow.tsx index 59bef0b74..dfc608f5f 100644 --- a/client/src/components/Settings/WandererWatchRow.tsx +++ b/client/src/components/Settings/WandererWatchRow.tsx @@ -48,7 +48,6 @@ function HintsButton({ W }: { W: WandererWatchController }) { onClick={W.openHints} disabled={count === 0} className={`${SETTINGS_BUTTON} relative`} - style={fs(12.5, 'body')} > {t('wanderer.watch.hints')} diff --git a/client/src/components/Settings/WebPushCard.tsx b/client/src/components/Settings/WebPushCard.tsx index 31d434d26..6f044aefc 100644 --- a/client/src/components/Settings/WebPushCard.tsx +++ b/client/src/components/Settings/WebPushCard.tsx @@ -32,7 +32,6 @@ export default function WebPushCard(): React.ReactElement { onClick={push.sendTest} disabled={!push.canTest} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {t('settings.notificationPreferences.sendTest')} @@ -41,7 +40,6 @@ export default function WebPushCard(): React.ReactElement { onClick={push.toggle} disabled={push.busy} className={push.on ? SETTINGS_BUTTON : SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {push.switchLabel} diff --git a/client/src/components/Settings/ai/AiConnectPanel.tsx b/client/src/components/Settings/ai/AiConnectPanel.tsx index cc600da75..a705ca8d8 100644 --- a/client/src/components/Settings/ai/AiConnectPanel.tsx +++ b/client/src/components/Settings/ai/AiConnectPanel.tsx @@ -1,5 +1,5 @@ import type { AiApiKeyKind, AiConnectionScope, AiParsingPrefill } from '@trek/shared'; -import { ChevronRight, KeyRound, Loader2, Server, ShieldCheck } from 'lucide-react'; +import { KeyRound, Loader2, Server, ShieldCheck } from 'lucide-react'; import React, { useId, useState } from 'react'; import { useTranslation } from '../../../i18n'; import { useAssistantStore } from '../../../store/assistantStore'; @@ -193,7 +193,6 @@ function KeyForm({ scope, keyConnect }: { scope: AiConnectionScope; keyConnect: onClick={() => void submit()} disabled={!kind || testing} className={`${SETTINGS_BUTTON_PRIMARY} whitespace-nowrap`} - style={fs(13, 'body')} > {testing && } {testing ? t('assistant.connect.testing') : t('assistant.connect.save')} @@ -225,7 +224,6 @@ function KeyForm({ scope, keyConnect }: { scope: AiConnectionScope; keyConnect: onClick={() => void openrouter.start()} disabled={openrouter.busy} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {openrouter.busy ? ( @@ -298,7 +296,6 @@ function CustomForm({ custom }: { custom: ReturnType }) onClick={() => void custom.submit()} disabled={!custom.ready || custom.saving} className={SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {custom.saving && } {custom.saving ? t('assistant.connect.testing') : t('assistant.connect.save')} @@ -314,7 +311,6 @@ function CustomForm({ custom }: { custom: ReturnType }) */ function PrivacyNote({ provider }: { provider: string }) { const { t } = useTranslation(); - const [open, setOpen] = useState(false); return (
    {provider && ( @@ -322,28 +318,16 @@ function PrivacyNote({ provider }: { provider: string }) { {t('assistant.privacy.banner', { provider })}

    )} - - {open && ( -
      -
    • {t('assistant.privacy.shared')}
    • -
    • {t('assistant.privacy.notShared')}
    • - {provider &&
    • {t('assistant.privacy.provider', { provider })}
    • } -
    - )} +
    +
      +
    • {t('assistant.privacy.shared')}
    • +
    • {t('assistant.privacy.notShared')}
    • + {provider &&
    • {t('assistant.privacy.provider', { provider })}
    • } +
    ); } diff --git a/client/src/components/Settings/ai/AiConnectionSection.mapFollow.test.tsx b/client/src/components/Settings/ai/AiConnectionSection.mapFollow.test.tsx index 0e86ddcaa..db8d1ebfa 100644 --- a/client/src/components/Settings/ai/AiConnectionSection.mapFollow.test.tsx +++ b/client/src/components/Settings/ai/AiConnectionSection.mapFollow.test.tsx @@ -4,7 +4,6 @@ import type { AiStatusResponse } from '@trek/shared'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { buildUser } from '../../../../tests/helpers/factories'; import { render, screen } 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'; @@ -31,8 +30,6 @@ const INSTANCE: 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.test.tsx b/client/src/components/Settings/ai/AiConnectionSection.test.tsx index dd294fa86..f7c7fb0c0 100644 --- a/client/src/components/Settings/ai/AiConnectionSection.test.tsx +++ b/client/src/components/Settings/ai/AiConnectionSection.test.tsx @@ -4,7 +4,6 @@ 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'; @@ -49,8 +48,6 @@ 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 7e2f063b1..c654c6929 100644 --- a/client/src/components/Settings/ai/AiConnectionSection.tsx +++ b/client/src/components/Settings/ai/AiConnectionSection.tsx @@ -65,7 +65,7 @@ function OpenAssistantButton({ S }: { S: AiConnections }) { const { t } = useTranslation(); if (!S.canOpen) return null; return ( - ); @@ -85,7 +85,6 @@ function OwnConnection({ S, own }: { S: AiConnections; own: AiConnectionView }) type="button" onClick={() => S.repair(repairMode)} className={SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {t(repairMode === 'chatgpt' ? 'assistant.errorAction.signIn' : 'assistant.errorAction.replaceKey')} @@ -98,7 +97,6 @@ function OwnConnection({ S, own }: { S: AiConnections; own: AiConnectionView }) onClick={() => void signOut()} disabled={busy} className={SETTINGS_BUTTON} - style={fs(13, 'body')} > {t('assistant.connect.signOut')} @@ -108,7 +106,6 @@ function OwnConnection({ S, own }: { S: AiConnections; own: AiConnectionView }) onClick={askDisconnect} disabled={busy} className={SETTINGS_BUTTON_DANGER} - style={fs(13, 'body')} > {t('assistant.connect.disconnect')} @@ -157,7 +154,7 @@ function InstanceConnection({ S }: { S: AiConnections }) { > {S.status?.canConnectOwn && !S.panel && ( - )} @@ -213,7 +210,7 @@ function LockedOwnConnection() { + } @@ -230,7 +227,7 @@ function ConnectPanel({ S }: { S: AiConnections }) {
    {S.view !== 'setup' && ( - )} @@ -283,7 +280,6 @@ export default function AiConnectionSection({ return (
    {flow ? : } {t('assistant.connect.chatgptOpen')} @@ -163,7 +162,6 @@ export default function ChatGptPasteStep({ onClick={() => void submit()} disabled={busy || !flow} className={SETTINGS_BUTTON_PRIMARY} - style={fs(13, 'body')} > {busy && } {t('assistant.connect.save')} @@ -174,7 +172,6 @@ export default function ChatGptPasteStep({ onClick={pasteFromClipboard} disabled={!flow} className={`${SETTINGS_BUTTON} whitespace-nowrap`} - style={fs(13, 'body')} > {t('assistant.connect.chatgptPaste')} diff --git a/client/src/components/Settings/settingsKit.test.tsx b/client/src/components/Settings/settingsKit.test.tsx deleted file mode 100644 index 0f6f45e03..000000000 --- a/client/src/components/Settings/settingsKit.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// FE-SETTINGS-KIT-001 to FE-SETTINGS-KIT-004 -import { fireEvent, render, screen } from '@testing-library/react'; -import { afterEach, describe, expect, it } from 'vitest'; - -import { SettingsCard, StatusPill, settingsCardOpenKey } from './settingsKit'; - -function card(props: { collapsible?: boolean; id?: string } = {}) { - 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 bd04603c7..c226e06b6 100644 --- a/client/src/components/Settings/settingsKit.tsx +++ b/client/src/components/Settings/settingsKit.tsx @@ -1,5 +1,6 @@ -import { useEffect, useId, useRef, useState, type ReactNode } from 'react' -import { ChevronDown, type LucideIcon } from 'lucide-react' +import { createContext, useContext, type ReactNode } from 'react' +import { Info, type LucideIcon } from 'lucide-react' +import { Tooltip } from '../shared/Tooltip' import { NEUTRAL_TINT, fs } from '../shared/DialogShell' /* @@ -12,6 +13,29 @@ import { NEUTRAL_TINT, fs } from '../shared/DialogShell' type IconLike = LucideIcon | React.ComponentType<{ className?: string; size?: number; strokeWidth?: number }> +/** + * A card's or a row's icon in its framed tile. A lucide glyph sits in the middle + * at glyph size; a brand mark that is itself a badge (`fill`, as Dawarich's and + * AirTrail's are) fills the tile corner to corner, the way the addon manager + * shows them, instead of floating small inside a second frame. + */ +function IconTile({ icon: Icon, fill = false, tone = 'neutral' }: { + icon: IconLike + fill?: boolean + tone?: 'neutral' | 'danger' +}) { + const box = 'h-8 w-8 rounded-[10px]' + if (fill) { + const Mark = Icon as React.ComponentType<{ fill?: boolean }> + return + } + return ( + + + + ) +} + /** The page's own bar: icon tile, title and subtitle on the left, actions on the right. */ export function SettingsHeader({ icon: Icon, title, subtitle, actions }: { icon: IconLike @@ -33,56 +57,89 @@ 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}` +/** True inside a card's body: a card there is drawn as a block of it (see SettingsCard). */ +const CardBodyContext = createContext(false) + /** - * 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. + * One part of a settings card: a rounded white block with a small head row + * (title, a pill, an action) and its content under a hairline. Every part of a + * card is one of these, so a card reads as separate, labelled pieces. */ -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] +export function SettingsBlock({ id, title, hint, badge, action, tone = 'neutral', flush = false, children }: { + id?: string + /** The content runs to the block's edges: for a list of rows, which bring their own padding. */ + flush?: boolean + title: ReactNode + hint?: ReactNode + badge?: ReactNode + action?: ReactNode + tone?: 'neutral' | 'danger' + children?: ReactNode +}) { + return ( +
    + {/* A tinted head row, so a block's title reads as its heading. */} +
    +
    +
    +

    {title}

    + {badge} +
    + {hint &&

    {hint}

    } +
    + {action &&
    {action}
    } +
    + {children != null && ( + // Inside the block its own rows and fields: a card in it is not nested again. + // Its hairline goes with it, so a block whose content renders nothing is its head alone. +
    + {children} +
    + )} +
    + ) +} + +/** + * A card's head band: icon tile, title with its badge, a short hint under it, + * an action on the right. The same over a card of fields and over a list of + * rows, so every group on a settings page starts the same way. + */ +function CardHead({ icon: Icon, iconFill = false, title, hint, badge, action, tone = 'neutral' }: { + icon?: IconLike + iconFill?: boolean + title: ReactNode + hint?: ReactNode + badge?: ReactNode + action?: ReactNode + tone?: 'neutral' | 'danger' +}) { + const tint = tone === 'danger' ? 'color-mix(in srgb, var(--danger) 9%, transparent)' : NEUTRAL_TINT + return ( +
    + {Icon && } +
    +
    +

    {title}

    + {badge} +
    + {hint &&

    {hint}

    } +
    + {action &&
    {action}
    } +
    + ) } /** * 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, collapsible = false }: { +export function SettingsCard({ icon: Icon, iconFill = false, title, hint, badge, action, tone = 'neutral', children, id, bodyClassName, flush = false }: { icon?: IconLike + /** The icon is a brand badge that fills its tile (see IconTile). */ + iconFill?: boolean title: ReactNode /** One line under the title in the band. */ hint?: ReactNode @@ -95,68 +152,47 @@ 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]) + // Inside another card's body a card is a block of it, never a card in a card: + // the rounded white block every part of a card is, with a small head row. + const nested = useContext(CardBodyContext) + if (nested) { + return ( + + {children} + + ) + } return ( -
    -
    - {Icon && ( - - - - )} -
    -
    -

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

    - {badge && {badge}} -
    - {hint &&

    {hint}

    } -
    - {action &&
    {action}
    } - {collapsible && ( - - )} -
    - {/* 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/admin/useAdmin.ts b/client/src/pages/admin/useAdmin.ts index 9c5ced821..e73f1293e 100644 --- a/client/src/pages/admin/useAdmin.ts +++ b/client/src/pages/admin/useAdmin.ts @@ -21,10 +21,17 @@ import { passwordErrorKey } from '../../utils/passwordError' * sidebar. Labels and icons stay in AdminPage; this is only the vocabulary. */ const ADMIN_TAB_IDS = [ - 'users', 'defaults', 'config', 'settings', 'addons', 'plugins', 'storage', + 'users', 'defaults', 'config', 'security', 'services', 'addons', 'plugins', 'storage', 'notifications', 'mcp-tokens', 'github', 'backup', 'audit', 'dev-notifications', ] +/** + * Tab ids that were renamed, so a bookmark, a wiki page or a mail that names the + * old one still lands. "Settings" was one tab for sign-in, services and the + * danger zone; it opens on security, where the danger zone went too. + */ +const ADMIN_TAB_ALIASES: Record = { settings: 'security' } + /** * The ones a managed install deliberately does not offer. * @@ -64,7 +71,8 @@ export function useAdmin() { // on every change, so the address bar keeps saying where the reader is. const [searchParams, setSearchParams] = useSearchParams() const [activeTab, setActiveTabState] = useState(() => { - const asked = searchParams.get('tab') + const raw = searchParams.get('tab') + const asked = raw ? (ADMIN_TAB_ALIASES[raw] ?? raw) : null const known = [...ADMIN_TAB_IDS, ...managedAdminTabs.map(tab => tab.id)] return asked && known.includes(asked) ? asked : 'users' }) diff --git a/client/tailwind.config.js b/client/tailwind.config.js index f7a858da2..124599788 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -116,6 +116,8 @@ export default { subtitle: ['calc(18px * var(--fs-scale-subtitle, 1))', '1.35'], body: ['calc(14px * var(--fs-scale-body, 1))', '1.5'], caption: ['calc(12px * var(--fs-scale-caption, 1))', '1.4'], + // Buttons and segmented controls on the settings pages: between caption and body. + control: ['calc(13px * var(--fs-scale-body, 1))', '1.3'], }, }, }, diff --git a/client/tests/helpers/settingsCards.ts b/client/tests/helpers/settingsCards.ts deleted file mode 100644 index d36185d6a..000000000 --- a/client/tests/helpers/settingsCards.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/client/tests/unit/mobile/admin/MAdmin.test.tsx b/client/tests/unit/mobile/admin/MAdmin.test.tsx index 519ebbab8..3b7909dba 100644 --- a/client/tests/unit/mobile/admin/MAdmin.test.tsx +++ b/client/tests/unit/mobile/admin/MAdmin.test.tsx @@ -145,7 +145,7 @@ describe('MAdmin', () => { fireEvent.click(screen.getByRole('button', { name: 'Administration' })); // All standard sections are listed - expect(screen.getByRole('button', { name: 'User Defaults' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Defaults' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Backup' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Audit' })).toBeInTheDocument(); @@ -156,6 +156,14 @@ describe('MAdmin', () => { expect(screen.queryByText('alice')).not.toBeInTheDocument(); }); + it('FE-MOB-ADMIN-002b: a link to the desktop Security or Services tab opens the Settings section', async () => { + for (const tab of ['security', 'services', 'settings']) { + const { unmount } = render(, { initialEntries: [`/admin?tab=${tab}`] }); + await waitFor(() => expect(screen.getByText('Authentication Methods')).toBeInTheDocument()); + unmount(); + } + }); + it('FE-MOB-ADMIN-003: the back button leaves for the dashboard', async () => { renderAdmin(); await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()); @@ -180,7 +188,7 @@ describe('MAdmin', () => { fireEvent.click(screen.getByRole('button', { name: 'Administration' })); - expect(screen.queryByRole('button', { name: 'MCP Access' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'MCP' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Dev: Notifications' })).not.toBeInTheDocument(); }); @@ -193,7 +201,7 @@ describe('MAdmin', () => { renderAdmin(); await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()); - await openSection('MCP Access'); + await openSection('MCP'); expect(screen.getByTestId('mcp-tokens-panel')).toBeInTheDocument(); }); @@ -212,7 +220,7 @@ describe('MAdmin', () => { renderAdmin(); await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument()); - await openSection('User Defaults'); + await openSection('Defaults'); expect(screen.getByTestId('default-user-settings')).toBeInTheDocument(); await openSection('Personalization'); diff --git a/client/tests/unit/mobile/admin/MAdminMcpTokensPanel.test.tsx b/client/tests/unit/mobile/admin/MAdminMcpTokensPanel.test.tsx index 9943581da..84ff1106d 100644 --- a/client/tests/unit/mobile/admin/MAdminMcpTokensPanel.test.tsx +++ b/client/tests/unit/mobile/admin/MAdminMcpTokensPanel.test.tsx @@ -73,7 +73,7 @@ describe('MAdminMcpTokensPanel', () => { it('FE-MOB-MCPTOK-001: renders the section header and both sub-section titles', async () => { renderPanel(); - expect(screen.getByText('MCP Access')).toBeInTheDocument(); + expect(screen.getByText('MCP')).toBeInTheDocument(); expect(screen.getByText('Manage OAuth sessions and API tokens across all users')).toBeInTheDocument(); expect(screen.getByText('OAuth Sessions')).toBeInTheDocument(); expect(screen.getByText('API Tokens')).toBeInTheDocument(); diff --git a/shared/src/i18n/ar/admin.ts b/shared/src/i18n/ar/admin.ts index 50ccde97b..0f90e0c6e 100644 --- a/shared/src/i18n/ar/admin.ts +++ b/shared/src/i18n/ar/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'الإشعارات', + 'admin.notifications.channels': 'القنوات', + 'admin.notifications.adminAlerts': 'تنبيهات المسؤول', 'admin.notifications.hint': 'اختر قناة إشعارات واحدة. يمكن تفعيل واحدة فقط في كل مرة.', 'admin.notifications.none': 'معطّل', 'admin.notifications.email': 'البريد الإلكتروني (SMTP)', @@ -131,6 +133,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'فشل إنشاء رابط الدعوة', 'admin.invite.deleteError': 'فشل حذف رابط الدعوة', 'admin.tabs.settings': 'الإعدادات', + 'admin.tabs.security': 'الأمان', + 'admin.tabs.services': 'الخدمات', 'admin.allowRegistration': 'السماح بالتسجيل', 'admin.allowRegistrationHint': 'يمكن للمستخدمين الجدد التسجيل بأنفسهم', 'admin.requireMfa': 'فرض المصادقة الثنائية (2FA)', @@ -174,10 +178,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'مشمول', 'admin.trekApi.notIncluded': 'غير مشمول', 'admin.trekApi.quota.title': 'حصة اليوم', - 'admin.trekApi.quota.used': 'المستخدَم', 'admin.trekApi.quota.remaining': 'المتبقي', 'admin.trekApi.quota.resets': 'إعادة الضبط عند', - 'admin.trekApi.quota.limit': 'الحد', 'admin.trekApi.quota.key': 'المفتاح', 'admin.trekApi.quota.state.connected': 'متصل', 'admin.trekApi.quota.state.registering': 'جارٍ الإعداد', @@ -297,8 +299,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'ما التالي', 'admin.collab.whatsnext.subtitle': 'اقتراحات الأنشطة والخطوات التالية', 'admin.tabs.config': 'التخصيص', - 'admin.tabs.defaults': 'الإعدادات الافتراضية', + 'admin.tabs.defaults': 'الافتراضيات', 'admin.defaultSettings.title': 'إعدادات المستخدم الافتراضية', + 'admin.defaultSettings.routing': 'التوجيه', 'admin.defaultSettings.description': 'تعيين الإعدادات الافتراضية على مستوى النظام. سيرى المستخدمون الذين لم يغيروا إعدادًا هذه القيم. تحظى تغييراتهم دائمًا بالأولوية.', 'admin.defaultSettings.saved': 'تم حفظ الإعداد الافتراضي', @@ -764,24 +767,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'تم تحديث الإضافة', 'admin.addons.toast.error': 'فشل تحديث الإضافة', 'admin.addons.group.count': '{enabled} من {total} مُفعّل', - 'admin.addons.search': 'البحث في الإضافات', - 'admin.addons.filter': 'عرض', - 'admin.addons.filter.all': 'الكل', - 'admin.addons.filter.on': 'مفعّلة', - 'admin.addons.filter.off': 'معطّلة', - 'admin.addons.noMatch': 'لا توجد إضافة تطابق البحث أو عامل التصفية.', - 'admin.addons.subOptions': '{count} خيار', - 'admin.addons.subOptions.zero': 'لا توجد خيارات', - 'admin.addons.subOptions.one': 'خيار واحد', - 'admin.addons.subOptions.two': 'خياران', - 'admin.addons.subOptions.few': '{count} خيارات', - 'admin.addons.subOptions.many': '{count} خيارًا', - 'admin.addons.subOptionsOn': 'المُفعّل: {count}', - 'admin.addons.subOptionsOn.zero': 'لا شيء مُفعّل', - 'admin.addons.subOptionsOn.one': 'واحد مُفعّل', - 'admin.addons.subOptionsOn.two': 'اثنان مُفعّلان', - 'admin.addons.subOptionsOn.few': 'المُفعّل: {count}', - 'admin.addons.subOptionsOn.many': 'المُفعّل: {count}', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'يمكن للمستخدمين ربط خادم wanderer الخاص بهم واستيراد دروبه كجولات', 'admin.toursHikingMap.title': 'خريطة المشي (OpenHikingMap)', @@ -806,7 +791,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'مجاني، بدون مفتاح API', 'admin.weather.locationHint': 'يعتمد الطقس على أول مكان بإحداثيات في كل يوم. إذا لم يكن هناك مكان مخصص ليوم ما، يُستخدم أي مكان من قائمة الأماكن كمرجع.', - 'admin.tabs.mcpTokens': 'وصول MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'وصول MCP', 'admin.mcpTokens.subtitle': 'إدارة جلسات OAuth ورموز API لجميع المستخدمين', 'admin.mcpTokens.sectionTitle': 'رموز API', @@ -952,7 +937,6 @@ const admin: TranslationStrings = { 'يُستخدم لكل مستخدم لم يُدخل مفتاحه الخاص، حتى يحصل الخادم بأكمله على بلاطات CARTO بدون علامة مائية. يُخزَّن مشفّرًا.', 'admin.group.users': 'المستخدمون', 'admin.group.config': 'الإعدادات', - 'admin.group.integration': 'التكاملات', 'admin.group.maintenance': 'الصيانة', 'admin.invite.tripLabel': 'إضافة إلى رحلة (اختياري)', 'admin.invite.tripNone': 'بدون رحلة', diff --git a/shared/src/i18n/ar/mailAdmin.ts b/shared/src/i18n/ar/mailAdmin.ts index ac096f3e5..31129e691 100644 --- a/shared/src/i18n/ar/mailAdmin.ts +++ b/shared/src/i18n/ar/mailAdmin.ts @@ -37,7 +37,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'أدخل منفذاً بين 1 و65535.', 'mailAdmin.problem.templateToken': 'يحتاج العنوان إلى {token} في موضع رمز كل شخص.', 'mailAdmin.problem.dailyInvalid': 'أدخل عدداً صحيحاً من 0 إلى 1000.', - 'mailAdmin.status.title': 'الحالة', 'mailAdmin.status.lastRead': 'آخر قراءة {time}', 'mailAdmin.status.never': 'أبداً', 'mailAdmin.status.mails': 'الرسائل، 24 ساعة', diff --git a/shared/src/i18n/az/admin.ts b/shared/src/i18n/az/admin.ts index 0bd05ab19..4c46f3542 100644 --- a/shared/src/i18n/az/admin.ts +++ b/shared/src/i18n/az/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Bildirişlər', + 'admin.notifications.channels': 'Kanallar', + 'admin.notifications.adminAlerts': 'Admin bildirişləri', 'admin.notifications.hint': 'Bir bildiriş kanalı seçin. Eyni anda yalnız biri aktiv ola bilər.', 'admin.notifications.none': 'Deaktivdir', 'admin.notifications.email': 'E-poçt (SMTP)', @@ -130,6 +132,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Dəvət keçidi yaratmaq mümkün olmadı', 'admin.invite.deleteError': 'Dəvət keçidini silmək mümkün olmadı', 'admin.tabs.settings': 'Tənzimləmələr', + 'admin.tabs.security': 'Təhlükəsizlik', + 'admin.tabs.services': 'Xidmətlər', 'admin.allowRegistration': 'Qeydiyyata icazə ver', 'admin.allowRegistrationHint': 'Yeni istifadəçilər özləri qeydiyyatdan keçə bilərlər', 'admin.authMethods': 'Doğrulama üsulları', @@ -191,10 +195,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Daxildir', 'admin.trekApi.notIncluded': 'Daxil deyil', 'admin.trekApi.quota.title': 'Bugünkü limit', - 'admin.trekApi.quota.used': 'İstifadə edilib', 'admin.trekApi.quota.remaining': 'Qalıb', 'admin.trekApi.quota.resets': 'Sıfırlanma vaxtı', - 'admin.trekApi.quota.limit': 'Gündəlik limit', 'admin.trekApi.quota.key': 'Açar', 'admin.trekApi.quota.state.connected': 'Qoşulub', 'admin.trekApi.quota.state.registering': 'Qurulur', @@ -320,8 +322,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Növbədə nə var', 'admin.collab.whatsnext.subtitle': 'Fəaliyyət təklifləri və növbəti addımlar', 'admin.tabs.config': 'Fərdiləşdirmə', - 'admin.tabs.defaults': 'İstifadəçi standartları', + 'admin.tabs.defaults': 'Defoltlar', 'admin.defaultSettings.title': 'Standart istifadəçi tənzimləmələri', + 'admin.defaultSettings.routing': 'Marşrutlar', 'admin.defaultSettings.description': 'Bütün sistem üçün standart dəyərləri təyin edin. Tənzimləməni dəyişməmiş istifadəçilər bu dəyərləri görəcəklər. Onların öz dəyişiklikləri həmişə üstün tutulur.', 'admin.defaultSettings.saved': 'Standart dəyər yadda saxlanıldı', @@ -809,16 +812,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Əlavə yeniləndi', 'admin.addons.toast.error': 'Əlavəni yeniləmək mümkün olmadı', 'admin.addons.group.count': '{total} əlavədən {enabled} aktivdir', - 'admin.addons.search': 'Əlavələrdə axtar', - 'admin.addons.filter': 'Göstər', - 'admin.addons.filter.all': 'Hamısı', - 'admin.addons.filter.on': 'Aktiv', - 'admin.addons.filter.off': 'Deaktiv', - 'admin.addons.noMatch': 'Axtarışa və ya filtrə uyğun əlavə yoxdur.', - 'admin.addons.subOptions': '{count} seçim', - 'admin.addons.subOptions.one': '{count} seçim', - 'admin.addons.subOptionsOn': '{count} aktiv', - 'admin.addons.subOptionsOn.one': '{count} aktiv', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'İstifadəçilər öz wanderer instansiyalarını qoşa və onun cığırlarını tur kimi idxal edə bilərlər', @@ -844,7 +837,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Pulsuzdur, API açarı tələb olunmur', 'admin.weather.locationHint': 'Hava məlumatları hər gün üçün koordinatları olan ilk məkana əsaslanır. Günə heç bir məkan təyin edilməyibsə, məkan siyahısındakı istənilən məkan istinad kimi istifadə olunur.', - 'admin.tabs.mcpTokens': 'MCP girişi', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP girişi', 'admin.mcpTokens.subtitle': 'Bütün istifadəçilər üzrə OAuth sessiyalarını və API tokenlərini idarə edin', 'admin.mcpTokens.sectionTitle': 'API tokenləri', @@ -949,7 +942,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.one': '{count} keçid açarı silindi', 'admin.group.users': 'İstifadəçilər', 'admin.group.config': 'Konfiqurasiya', - 'admin.group.integration': 'İnteqrasiyalar', 'admin.group.maintenance': 'Texniki xidmət', 'admin.invite.tripLabel': 'Səyahətə əlavə et (istəyə bağlı)', 'admin.invite.tripNone': 'Səyahət yoxdur', diff --git a/shared/src/i18n/az/mailAdmin.ts b/shared/src/i18n/az/mailAdmin.ts index 613d6d92e..5d7953039 100644 --- a/shared/src/i18n/az/mailAdmin.ts +++ b/shared/src/i18n/az/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '1 ilə 65535 arasında port daxil et.', 'mailAdmin.problem.templateToken': 'Ünvanda hər şəxsin kodu üçün {token} olmalıdır.', 'mailAdmin.problem.dailyInvalid': '0 ilə 1000 arasında tam ədəd daxil et.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Son oxuma {time}', 'mailAdmin.status.never': 'heç vaxt', 'mailAdmin.status.mails': 'Məktublar, 24 saat', diff --git a/shared/src/i18n/br/admin.ts b/shared/src/i18n/br/admin.ts index 2bea2d4bf..d9a2c47ac 100644 --- a/shared/src/i18n/br/admin.ts +++ b/shared/src/i18n/br/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notificações', + 'admin.notifications.channels': 'Canais', + 'admin.notifications.adminAlerts': 'Alertas do admin', 'admin.notifications.hint': 'Escolha um canal de notificação. Apenas um pode estar ativo por vez.', 'admin.notifications.none': 'Desativado', 'admin.notifications.email': 'E-mail (SMTP)', @@ -74,6 +76,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Falha ao criar link de convite', 'admin.invite.deleteError': 'Falha ao excluir link de convite', 'admin.tabs.settings': 'Configurações', + 'admin.tabs.security': 'Segurança', + 'admin.tabs.services': 'Serviços', 'admin.allowRegistration': 'Permitir cadastro', 'admin.allowRegistrationHint': 'Novos usuários podem se cadastrar sozinhos', 'admin.authMethods': 'Métodos de autenticação', @@ -135,10 +139,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Incluído', 'admin.trekApi.notIncluded': 'Não incluído', 'admin.trekApi.quota.title': 'Cota de hoje', - 'admin.trekApi.quota.used': 'Usado', 'admin.trekApi.quota.remaining': 'Restante', 'admin.trekApi.quota.resets': 'Reinicia às', - 'admin.trekApi.quota.limit': 'Limite', 'admin.trekApi.quota.key': 'Chave', 'admin.trekApi.quota.state.connected': 'Conectado', 'admin.trekApi.quota.state.registering': 'Configurando', @@ -261,8 +263,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Próximos passos', 'admin.collab.whatsnext.subtitle': 'Sugestões de atividades e próximos passos', 'admin.tabs.config': 'Personalização', - 'admin.tabs.defaults': 'Padrões do usuário', + 'admin.tabs.defaults': 'Padrões', 'admin.defaultSettings.title': 'Configurações padrão do usuário', + 'admin.defaultSettings.routing': 'Rotas', 'admin.defaultSettings.description': 'Defina padrões para toda a instância. Usuários que não alteraram uma configuração verão esses valores. As próprias alterações deles sempre têm prioridade.', 'admin.defaultSettings.saved': 'Padrão salvo', @@ -718,16 +721,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Complemento atualizado', 'admin.addons.toast.error': 'Falha ao atualizar complemento', 'admin.addons.group.count': '{enabled} de {total} ativados', - 'admin.addons.search': 'Pesquisar complementos', - 'admin.addons.filter': 'Mostrar', - 'admin.addons.filter.all': 'Todos', - 'admin.addons.filter.on': 'Ativos', - 'admin.addons.filter.off': 'Inativos', - 'admin.addons.noMatch': 'Nenhum complemento corresponde à pesquisa ou ao filtro.', - 'admin.addons.subOptions': '{count} opções', - 'admin.addons.subOptions.one': '{count} opção', - 'admin.addons.subOptionsOn': '{count} ativas', - 'admin.addons.subOptionsOn.one': '{count} ativa', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Os usuários podem conectar a própria instância do wanderer e importar as trilhas dela como passeios', @@ -802,7 +795,7 @@ const admin: TranslationStrings = { 'admin.update.wikiLink': 'Abrir o guia de atualização', 'admin.update.reloadHint': 'Recarregue a página em alguns segundos.', 'admin.tabs.permissions': 'Permissões', - 'admin.tabs.mcpTokens': 'Acesso MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Acesso MCP', 'admin.mcpTokens.subtitle': 'Gerenciar sessões OAuth e tokens de API de todos os usuários', 'admin.mcpTokens.sectionTitle': 'Tokens de API', @@ -924,7 +917,6 @@ const admin: TranslationStrings = { "Usada para todos os usuários que não inseriram a própria chave, assim toda a instância recebe blocos do CARTO sem marca d'água. Armazenada de forma criptografada.", 'admin.group.users': 'Usuários', 'admin.group.config': 'Configuração', - 'admin.group.integration': 'Integrações', 'admin.group.maintenance': 'Manutenção', 'admin.invite.tripLabel': 'Adicionar à viagem (opcional)', 'admin.invite.tripNone': 'Nenhuma viagem', diff --git a/shared/src/i18n/br/mailAdmin.ts b/shared/src/i18n/br/mailAdmin.ts index 4f7b27a9d..ed48cb4a6 100644 --- a/shared/src/i18n/br/mailAdmin.ts +++ b/shared/src/i18n/br/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Digite uma porta entre 1 e 65535.', 'mailAdmin.problem.templateToken': 'O endereço precisa de {token} para o código de cada pessoa.', 'mailAdmin.problem.dailyInvalid': 'Digite um número inteiro de 0 a 1000.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Última leitura {time}', 'mailAdmin.status.never': 'nunca', 'mailAdmin.status.mails': 'E-mails, 24 h', diff --git a/shared/src/i18n/ca/admin.ts b/shared/src/i18n/ca/admin.ts index 3c6d7e593..d312ad90b 100644 --- a/shared/src/i18n/ca/admin.ts +++ b/shared/src/i18n/ca/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notificacions', + 'admin.notifications.channels': 'Canals', + 'admin.notifications.adminAlerts': 'Avisos d’administració', 'admin.notifications.hint': 'Tria un canal per rebre notificacions. Només un pot estar actiu alhora.', 'admin.notifications.none': 'Desactivat', 'admin.notifications.email': 'Correu (SMTP)', @@ -80,6 +82,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': "Error en crear l'enllaç", 'admin.invite.deleteError': "Error en eliminar l'enllaç", 'admin.tabs.settings': 'Configuració', + 'admin.tabs.security': 'Seguretat', + 'admin.tabs.services': 'Serveis', 'admin.allowRegistration': 'Permetre el registre', 'admin.allowRegistrationHint': 'Els nous usuaris es poden registrar per si mateixos', 'admin.authMethods': "Mètodes d'autenticació", @@ -141,10 +145,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Inclòs', 'admin.trekApi.notIncluded': 'No inclòs', 'admin.trekApi.quota.title': "Quota d'avui", - 'admin.trekApi.quota.used': 'Utilitzat', 'admin.trekApi.quota.remaining': 'Restant', 'admin.trekApi.quota.resets': 'Es reinicia a les', - 'admin.trekApi.quota.limit': 'Límit', 'admin.trekApi.quota.key': 'Clau', 'admin.trekApi.quota.state.connected': 'Connectat', 'admin.trekApi.quota.state.registering': "S'està configurant", @@ -250,8 +252,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Què ve després', 'admin.collab.whatsnext.subtitle': "Suggeriments d'activitats i propers passos", 'admin.tabs.config': 'Personalització', - 'admin.tabs.defaults': 'Valors per defecte', + 'admin.tabs.defaults': 'Predeterminats', 'admin.defaultSettings.title': 'Configuració per defecte dels usuaris', + 'admin.defaultSettings.routing': 'Rutes', 'admin.defaultSettings.description': 'Estableix valors per defecte per a tota la instància. Els usuaris que no hagin canviat una opció veuran aquests valors. Els seus propis canvis sempre tenen prioritat.', 'admin.defaultSettings.saved': 'Per defecte desat', @@ -314,16 +317,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Complement actualitzat', 'admin.addons.toast.error': "No s'ha pogut actualitzar el complement", 'admin.addons.group.count': '{enabled} de {total} actius', - 'admin.addons.search': 'Cerca complements', - 'admin.addons.filter': 'Mostra', - 'admin.addons.filter.all': 'Tots', - 'admin.addons.filter.on': 'Actius', - 'admin.addons.filter.off': 'Inactius', - 'admin.addons.noMatch': 'Cap complement coincideix amb la cerca o el filtre.', - 'admin.addons.subOptions': '{count} opcions', - 'admin.addons.subOptions.one': '{count} opció', - 'admin.addons.subOptionsOn': '{count} actives', - 'admin.addons.subOptionsOn.one': '{count} activa', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Els usuaris poden connectar la seva pròpia instància de wanderer i importar-ne els senders com a excursions', @@ -349,7 +342,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Gratuït, sense necessitat de clau API', 'admin.weather.locationHint': 'El temps es basa en el primer lloc amb coordenades de cada dia. Si no hi ha cap lloc assignat a un dia, es fa servir com a referència qualsevol lloc de la llista.', - 'admin.tabs.mcpTokens': 'Accés MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Accés MCP', 'admin.mcpTokens.subtitle': "Gestionar sessions OAuth i tokens d'API de tots els usuaris", 'admin.mcpTokens.sectionTitle': "Tokens d'API", @@ -453,7 +446,6 @@ const admin: TranslationStrings = { 'admin.tabs.permissions': 'Permisos', 'admin.group.users': 'Usuaris', 'admin.group.config': 'Configuració', - 'admin.group.integration': 'Integracions', 'admin.group.maintenance': 'Manteniment', 'admin.notifications.emailPanel.title': 'Correu (SMTP)', 'admin.notifications.webhookPanel.title': 'Webhook', diff --git a/shared/src/i18n/ca/mailAdmin.ts b/shared/src/i18n/ca/mailAdmin.ts index 16a45f5fc..d3fd9dbf0 100644 --- a/shared/src/i18n/ca/mailAdmin.ts +++ b/shared/src/i18n/ca/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Introdueix un port entre 1 i 65535.', 'mailAdmin.problem.templateToken': 'L’adreça necessita {token} per al codi de cada persona.', 'mailAdmin.problem.dailyInvalid': 'Introdueix un nombre enter de 0 a 1000.', - 'mailAdmin.status.title': 'Estat', 'mailAdmin.status.lastRead': 'Darrera lectura {time}', 'mailAdmin.status.never': 'mai', 'mailAdmin.status.mails': 'Correus, 24 h', diff --git a/shared/src/i18n/cs/admin.ts b/shared/src/i18n/cs/admin.ts index 039f68437..2dc079b57 100644 --- a/shared/src/i18n/cs/admin.ts +++ b/shared/src/i18n/cs/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Oznámení', + 'admin.notifications.channels': 'Kanály', + 'admin.notifications.adminAlerts': 'Upozornění pro správce', 'admin.notifications.hint': 'Vyberte kanál oznámení. Současně může být aktivní pouze jeden.', 'admin.notifications.none': 'Vypnuto', 'admin.notifications.email': 'E-mail (SMTP)', @@ -78,6 +80,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Nepodařilo se vytvořit pozvánku', 'admin.invite.deleteError': 'Nepodařilo se smazat pozvánku', 'admin.tabs.settings': 'Nastavení', + 'admin.tabs.security': 'Zabezpečení', + 'admin.tabs.services': 'Služby', 'admin.allowRegistration': 'Povolit registraci', 'admin.allowRegistrationHint': 'Noví uživatelé se mohou sami registrovat', 'admin.authMethods': 'Metody ověřování', @@ -136,10 +140,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Obsahuje', 'admin.trekApi.notIncluded': 'Neobsahuje', 'admin.trekApi.quota.title': 'Dnešní limit', - 'admin.trekApi.quota.used': 'Využito', 'admin.trekApi.quota.remaining': 'Zbývá', 'admin.trekApi.quota.resets': 'Obnoví se v', - 'admin.trekApi.quota.limit': 'Denní limit', 'admin.trekApi.quota.key': 'Klíč', 'admin.trekApi.quota.state.connected': 'Připojeno', 'admin.trekApi.quota.state.registering': 'Nastavuje se', @@ -262,8 +264,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Co dál', 'admin.collab.whatsnext.subtitle': 'Návrhy aktivit a další kroky', 'admin.tabs.config': 'Personalizace', - 'admin.tabs.defaults': 'Výchozí nastavení uživatele', + 'admin.tabs.defaults': 'Výchozí', 'admin.defaultSettings.title': 'Výchozí nastavení uživatele', + 'admin.defaultSettings.routing': 'Trasy', 'admin.defaultSettings.description': 'Nastavte výchozí hodnoty pro celou instanci. Uživatelé, kteří nezměnili nastavení, uvidí tyto hodnoty. Jejich vlastní změny mají vždy přednost.', 'admin.defaultSettings.saved': 'Výchozí nastavení uloženo', @@ -717,18 +720,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Doplněk byl aktualizován', 'admin.addons.toast.error': 'Aktualizace doplňku se nezdařila', 'admin.addons.group.count': '{enabled} z {total} zapnuto', - 'admin.addons.search': 'Hledat doplňky', - 'admin.addons.filter': 'Zobrazit', - 'admin.addons.filter.all': 'Vše', - 'admin.addons.filter.on': 'Zapnuté', - 'admin.addons.filter.off': 'Vypnuté', - 'admin.addons.noMatch': 'Hledání ani filtru neodpovídá žádný doplněk.', - 'admin.addons.subOptions': '{count} možností', - 'admin.addons.subOptions.one': '{count} možnost', - 'admin.addons.subOptions.few': '{count} možnosti', - 'admin.addons.subOptionsOn': 'zapnuto: {count}', - 'admin.addons.subOptionsOn.one': 'zapnuto: {count}', - 'admin.addons.subOptionsOn.few': 'zapnuto: {count}', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Uživatelé mohou připojit vlastní instanci wanderer a importovat z ní trasy jako výlety', @@ -760,7 +751,7 @@ const admin: TranslationStrings = { 'admin.audit.col.resource': 'Zdroj', 'admin.audit.col.ip': 'IP', 'admin.audit.col.details': 'Detaily', - 'admin.tabs.mcpTokens': 'MCP přístup', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP přístup', 'admin.mcpTokens.subtitle': 'Správa OAuth relací a API tokenů všech uživatelů', 'admin.mcpTokens.sectionTitle': 'API tokeny', @@ -928,7 +919,6 @@ const admin: TranslationStrings = { 'Použije se pro každého uživatele, který nezadal vlastní klíč, takže celá instance získá dlaždice CARTO bez vodoznaku. Ukládá se šifrovaně.', 'admin.group.users': 'Uživatelé', 'admin.group.config': 'Konfigurace', - 'admin.group.integration': 'Integrace', 'admin.group.maintenance': 'Údržba', 'admin.invite.tripLabel': 'Přidat k cestě (volitelné)', 'admin.invite.tripNone': 'Žádná cesta', diff --git a/shared/src/i18n/cs/mailAdmin.ts b/shared/src/i18n/cs/mailAdmin.ts index dfbdd4266..861a4ff52 100644 --- a/shared/src/i18n/cs/mailAdmin.ts +++ b/shared/src/i18n/cs/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Zadej port mezi 1 a 65535.', 'mailAdmin.problem.templateToken': 'Adresa musí obsahovat {token} pro kód každého uživatele.', 'mailAdmin.problem.dailyInvalid': 'Zadej celé číslo od 0 do 1000.', - 'mailAdmin.status.title': 'Stav', 'mailAdmin.status.lastRead': 'Naposledy přečteno {time}', 'mailAdmin.status.never': 'nikdy', 'mailAdmin.status.mails': 'E-maily, 24 h', diff --git a/shared/src/i18n/de/admin.ts b/shared/src/i18n/de/admin.ts index b0f8d58ca..e98d06203 100644 --- a/shared/src/i18n/de/admin.ts +++ b/shared/src/i18n/de/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Benachrichtigungen', + 'admin.notifications.channels': 'Kanäle', + 'admin.notifications.adminAlerts': 'Admin-Meldungen', 'admin.notifications.hint': 'Wählen Sie einen Benachrichtigungskanal. Es kann nur einer gleichzeitig aktiv sein.', 'admin.notifications.none': 'Deaktiviert', 'admin.notifications.email': 'E-Mail (SMTP)', @@ -75,6 +77,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Fehler beim Erstellen des Einladungslinks', 'admin.invite.deleteError': 'Fehler beim Löschen des Einladungslinks', 'admin.tabs.settings': 'Einstellungen', + 'admin.tabs.security': 'Sicherheit', + 'admin.tabs.services': 'Dienste', 'admin.allowRegistration': 'Registrierung erlauben', 'admin.allowRegistrationHint': 'Neue Benutzer können sich selbst registrieren', 'admin.authMethods': 'Anmeldemethoden', @@ -137,10 +141,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Enthalten', 'admin.trekApi.notIncluded': 'Nicht enthalten', 'admin.trekApi.quota.title': 'Kontingent heute', - 'admin.trekApi.quota.used': 'Verbraucht', 'admin.trekApi.quota.remaining': 'Verbleibend', 'admin.trekApi.quota.resets': 'Zurückgesetzt um', - 'admin.trekApi.quota.limit': 'Tageslimit', 'admin.trekApi.quota.key': 'Schlüssel', 'admin.trekApi.quota.state.connected': 'Verbunden', 'admin.trekApi.quota.state.registering': 'Wird eingerichtet', @@ -265,8 +267,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Was kommt als Nächstes', 'admin.collab.whatsnext.subtitle': 'Aktivitätsvorschläge und nächste Schritte', 'admin.tabs.config': 'Personalisierung', - 'admin.tabs.defaults': 'Benutzer-Standards', + 'admin.tabs.defaults': 'Standards', 'admin.defaultSettings.title': 'Standard-Benutzereinstellungen', + 'admin.defaultSettings.routing': 'Routing', 'admin.defaultSettings.description': 'Instanzweite Standards festlegen. Benutzer, die eine Einstellung nicht geändert haben, sehen diese Werte. Eigene Änderungen haben immer Vorrang.', 'admin.defaultSettings.saved': 'Standard gespeichert', @@ -727,16 +730,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Addon aktualisiert', 'admin.addons.toast.error': 'Addon konnte nicht aktualisiert werden', 'admin.addons.group.count': '{enabled} von {total} aktiviert', - 'admin.addons.search': 'Addons durchsuchen', - 'admin.addons.filter': 'Anzeigen', - 'admin.addons.filter.all': 'Alle', - 'admin.addons.filter.on': 'An', - 'admin.addons.filter.off': 'Aus', - 'admin.addons.noMatch': 'Kein Addon passt zur Suche oder zum Filter.', - 'admin.addons.subOptions': '{count} Optionen', - 'admin.addons.subOptions.one': '{count} Option', - 'admin.addons.subOptionsOn': '{count} aktiv', - 'admin.addons.subOptionsOn.one': '{count} aktiv', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Nutzer können ihre eigene wanderer-Instanz verbinden und deren Trails als Touren importieren', @@ -762,7 +755,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Kostenlos, kein API-Schlüssel erforderlich', 'admin.weather.locationHint': 'Das Wetter wird anhand des ersten Ortes mit Koordinaten im jeweiligen Tag berechnet. Ist kein Ort am Tag eingeplant, wird ein beliebiger Ort aus der Ortsliste als Referenz verwendet.', - 'admin.tabs.mcpTokens': 'MCP-Zugang', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP-Zugang', 'admin.mcpTokens.subtitle': 'OAuth-Sitzungen und API-Tokens aller Benutzer verwalten', 'admin.mcpTokens.sectionTitle': 'API-Tokens', @@ -933,7 +926,6 @@ const admin: TranslationStrings = { 'Wird für jeden Nutzer verwendet, der keinen eigenen Key eingetragen hat, damit die ganze Instanz CARTO-Kacheln ohne Wasserzeichen bekommt. Verschlüsselt gespeichert.', 'admin.group.users': 'Benutzer', 'admin.group.config': 'Konfiguration', - 'admin.group.integration': 'Integrationen', 'admin.group.maintenance': 'Wartung', 'admin.invite.tripLabel': 'Zu Trip hinzufügen (optional)', 'admin.invite.tripNone': 'Kein Trip', diff --git a/shared/src/i18n/de/mailAdmin.ts b/shared/src/i18n/de/mailAdmin.ts index 67e830d56..f50c8ce59 100644 --- a/shared/src/i18n/de/mailAdmin.ts +++ b/shared/src/i18n/de/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Gib einen Port zwischen 1 und 65535 ein.', 'mailAdmin.problem.templateToken': 'Die Adresse braucht {token} für den persönlichen Code.', 'mailAdmin.problem.dailyInvalid': 'Gib eine ganze Zahl von 0 bis 1000 ein.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Zuletzt gelesen {time}', 'mailAdmin.status.never': 'noch nie', 'mailAdmin.status.mails': 'Mails, 24 Std.', diff --git a/shared/src/i18n/en/admin.ts b/shared/src/i18n/en/admin.ts index beed9f9ff..cfd1a8262 100644 --- a/shared/src/i18n/en/admin.ts +++ b/shared/src/i18n/en/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notifications', + 'admin.notifications.channels': 'Channels', + 'admin.notifications.adminAlerts': 'Admin alerts', 'admin.notifications.hint': 'Choose one notification channel. Only one can be active at a time.', 'admin.notifications.none': 'Disabled', 'admin.notifications.email': 'Email (SMTP)', @@ -126,6 +128,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Failed to create invite link', 'admin.invite.deleteError': 'Failed to delete invite link', 'admin.tabs.settings': 'Settings', + 'admin.tabs.security': 'Security', + 'admin.tabs.services': 'Services', 'admin.allowRegistration': 'Allow Registration', 'admin.allowRegistrationHint': 'New users can register themselves', 'admin.authMethods': 'Authentication Methods', @@ -186,10 +190,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Included', 'admin.trekApi.notIncluded': 'Not included', 'admin.trekApi.quota.title': "Today's allowance", - 'admin.trekApi.quota.used': 'Used', 'admin.trekApi.quota.remaining': 'Remaining', 'admin.trekApi.quota.resets': 'Resets at', - 'admin.trekApi.quota.limit': 'Daily limit', 'admin.trekApi.quota.key': 'Key', 'admin.trekApi.quota.state.connected': 'Connected', 'admin.trekApi.quota.state.registering': 'Setting up', @@ -311,8 +313,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': "What's Next", 'admin.collab.whatsnext.subtitle': 'Activity suggestions and next steps', 'admin.tabs.config': 'Personalization', - 'admin.tabs.defaults': 'User Defaults', + 'admin.tabs.defaults': 'Defaults', 'admin.defaultSettings.title': 'Default User Settings', + 'admin.defaultSettings.routing': 'Routing', 'admin.defaultSettings.description': 'Set instance-wide defaults. Users who have not changed a setting will see these values. Their own changes always take priority.', 'admin.defaultSettings.saved': 'Default saved', @@ -770,16 +773,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Addon updated', 'admin.addons.toast.error': 'Failed to update addon', 'admin.addons.group.count': '{enabled} of {total} enabled', - 'admin.addons.search': 'Search addons', - 'admin.addons.filter': 'Show', - 'admin.addons.filter.all': 'All', - 'admin.addons.filter.on': 'On', - 'admin.addons.filter.off': 'Off', - 'admin.addons.noMatch': 'No addon matches the search or the filter.', - 'admin.addons.subOptions': '{count} options', - 'admin.addons.subOptions.one': '{count} option', - 'admin.addons.subOptionsOn': '{count} on', - 'admin.addons.subOptionsOn.one': '{count} on', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Users can connect their own wanderer instance and import its trails as tours', 'admin.toursHikingMap.title': 'Hiking map (OpenHikingMap)', @@ -804,7 +797,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Free, no API key required', 'admin.weather.locationHint': 'Weather is based on the first place with coordinates in each day. If no place is assigned to a day, any place from the place list is used as a reference.', - 'admin.tabs.mcpTokens': 'MCP Access', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP Access', 'admin.mcpTokens.subtitle': 'Manage OAuth sessions and API tokens across all users', 'admin.mcpTokens.sectionTitle': 'API Tokens', @@ -904,7 +897,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.one': 'Removed {count} passkey', 'admin.group.users': 'Users', 'admin.group.config': 'Configuration', - 'admin.group.integration': 'Integrations', 'admin.group.maintenance': 'Maintenance', 'admin.invite.tripLabel': 'Add to trip (optional)', 'admin.invite.tripNone': 'No trip', diff --git a/shared/src/i18n/en/mailAdmin.ts b/shared/src/i18n/en/mailAdmin.ts index 6e12043c0..9933cd773 100644 --- a/shared/src/i18n/en/mailAdmin.ts +++ b/shared/src/i18n/en/mailAdmin.ts @@ -38,7 +38,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Enter a port between 1 and 65535.', 'mailAdmin.problem.templateToken': 'The address needs {token} for each person’s code.', 'mailAdmin.problem.dailyInvalid': 'Enter a whole number from 0 to 1000.', - 'mailAdmin.status.title': 'Status', 'mailAdmin.status.lastRead': 'Last read {time}', 'mailAdmin.status.never': 'never', 'mailAdmin.status.mails': 'Mails, 24 h', diff --git a/shared/src/i18n/es/admin.ts b/shared/src/i18n/es/admin.ts index e8cad1d89..bb7e5ca23 100644 --- a/shared/src/i18n/es/admin.ts +++ b/shared/src/i18n/es/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notificaciones', + 'admin.notifications.channels': 'Canales', + 'admin.notifications.adminAlerts': 'Avisos de administración', 'admin.notifications.hint': 'Elija un canal de notificación. Solo uno puede estar activo a la vez.', 'admin.notifications.none': 'Desactivado', 'admin.notifications.email': 'Correo (SMTP)', @@ -21,7 +23,7 @@ const admin: TranslationStrings = { 'admin.subtitle': 'Gestión de usuarios y ajustes del sistema', 'admin.tabs.users': 'Usuarios', 'admin.tabs.categories': 'Categorías', - 'admin.tabs.backup': 'Copia de seguridad', + 'admin.tabs.backup': 'Respaldo', 'admin.tabs.audit': 'Auditoría', 'admin.stats.users': 'Usuarios', 'admin.stats.users.one': 'Usuario', @@ -75,6 +77,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Error al crear el enlace', 'admin.invite.deleteError': 'Error al eliminar el enlace', 'admin.tabs.settings': 'Ajustes', + 'admin.tabs.security': 'Seguridad', + 'admin.tabs.services': 'Servicios', 'admin.allowRegistration': 'Permitir el registro', 'admin.allowRegistrationHint': 'Los nuevos usuarios pueden registrarse por sí mismos', 'admin.authMethods': 'Métodos de autenticación', @@ -136,10 +140,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Incluido', 'admin.trekApi.notIncluded': 'No incluido', 'admin.trekApi.quota.title': 'Cupo de hoy', - 'admin.trekApi.quota.used': 'Usado', 'admin.trekApi.quota.remaining': 'Restante', 'admin.trekApi.quota.resets': 'Se reinicia a las', - 'admin.trekApi.quota.limit': 'Límite', 'admin.trekApi.quota.key': 'Clave', 'admin.trekApi.quota.state.connected': 'Conectado', 'admin.trekApi.quota.state.registering': 'Configurando', @@ -262,8 +264,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Qué sigue', 'admin.collab.whatsnext.subtitle': 'Sugerencias de actividades y próximos pasos', 'admin.tabs.config': 'Personalización', - 'admin.tabs.defaults': 'Valores predeterminados', + 'admin.tabs.defaults': 'Predeterminados', 'admin.defaultSettings.title': 'Configuración predeterminada de usuarios', + 'admin.defaultSettings.routing': 'Rutas', 'admin.defaultSettings.description': 'Establece valores predeterminados para toda la instancia. Los usuarios que no hayan cambiado una opción verán estos valores. Sus propios cambios siempre tienen prioridad.', 'admin.defaultSettings.saved': 'Predeterminado guardado', @@ -706,16 +709,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Complemento actualizado', 'admin.addons.toast.error': 'No se pudo actualizar el complemento', 'admin.addons.group.count': '{enabled} de {total} activados', - 'admin.addons.search': 'Buscar complementos', - 'admin.addons.filter': 'Mostrar', - 'admin.addons.filter.all': 'Todos', - 'admin.addons.filter.on': 'Activos', - 'admin.addons.filter.off': 'Inactivos', - 'admin.addons.noMatch': 'Ningún complemento coincide con la búsqueda o el filtro.', - 'admin.addons.subOptions': '{count} opciones', - 'admin.addons.subOptions.one': '{count} opción', - 'admin.addons.subOptionsOn': '{count} activas', - 'admin.addons.subOptionsOn.one': '{count} activa', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Los usuarios pueden conectar su propia instancia de wanderer e importar sus senderos como excursiones', @@ -741,7 +734,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Gratis, sin necesidad de clave API', 'admin.weather.locationHint': 'El tiempo se basa en el primer lugar con coordenadas de cada día. Si no hay ningún lugar asignado a un día, se usa como referencia cualquier lugar de la lista.', - 'admin.tabs.mcpTokens': 'Acceso MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Acceso MCP', 'admin.mcpTokens.subtitle': 'Gestionar sesiones OAuth y tokens de API de todos los usuarios', 'admin.mcpTokens.sectionTitle': 'Tokens de API', @@ -940,7 +933,6 @@ const admin: TranslationStrings = { 'Se usa para cada usuario que no haya introducido su propia clave, de modo que toda la instancia obtenga teselas de CARTO sin marca de agua. Se almacena cifrada.', 'admin.group.users': 'Usuarios', 'admin.group.config': 'Configuración', - 'admin.group.integration': 'Integraciones', 'admin.group.maintenance': 'Mantenimiento', 'admin.invite.tripLabel': 'Añadir a un viaje (opcional)', 'admin.invite.tripNone': 'Sin viaje', diff --git a/shared/src/i18n/es/mailAdmin.ts b/shared/src/i18n/es/mailAdmin.ts index b808eb6ee..70ce9cc1b 100644 --- a/shared/src/i18n/es/mailAdmin.ts +++ b/shared/src/i18n/es/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Introduce un puerto entre 1 y 65535.', 'mailAdmin.problem.templateToken': 'La dirección necesita {token} para el código de cada persona.', 'mailAdmin.problem.dailyInvalid': 'Introduce un número entero de 0 a 1000.', - 'mailAdmin.status.title': 'Estado', 'mailAdmin.status.lastRead': 'Última lectura {time}', 'mailAdmin.status.never': 'nunca', 'mailAdmin.status.mails': 'Correos, 24 h', diff --git a/shared/src/i18n/et/admin.ts b/shared/src/i18n/et/admin.ts index 2c5126f0c..1048a533b 100644 --- a/shared/src/i18n/et/admin.ts +++ b/shared/src/i18n/et/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Teavitused', + 'admin.notifications.channels': 'Kanalid', + 'admin.notifications.adminAlerts': 'Halduri teavitused', 'admin.notifications.hint': 'Vali üks teavituskanal. Korraga saab aktiivne olla ainult üks kanal.', 'admin.notifications.none': 'Keelatud', 'admin.notifications.email': 'E-post (SMTP)', @@ -128,6 +130,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Kutselingi loomine ebaõnnestus', 'admin.invite.deleteError': 'Kutselingi kustutamine ebaõnnestus', 'admin.tabs.settings': 'Seaded', + 'admin.tabs.security': 'Turvalisus', + 'admin.tabs.services': 'Teenused', 'admin.allowRegistration': 'Luba registreerumine', 'admin.allowRegistrationHint': 'Uued kasutajad saavad ise registreeruda', 'admin.authMethods': 'Autentimisviisid', @@ -188,10 +192,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Olemas', 'admin.trekApi.notIncluded': 'Puudub', 'admin.trekApi.quota.title': 'Tänane kvoot', - 'admin.trekApi.quota.used': 'Kasutatud', 'admin.trekApi.quota.remaining': 'Järel', 'admin.trekApi.quota.resets': 'Lähtestub kell', - 'admin.trekApi.quota.limit': 'Limiit', 'admin.trekApi.quota.key': 'Võti', 'admin.trekApi.quota.state.connected': 'Ühendatud', 'admin.trekApi.quota.state.registering': 'Seadistamisel', @@ -314,8 +316,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Mis järgmiseks', 'admin.collab.whatsnext.subtitle': 'Tegevussoovitused ja järgmised sammud', 'admin.tabs.config': 'Isikupärastamine', - 'admin.tabs.defaults': 'Kasutaja vaikevalikud', + 'admin.tabs.defaults': 'Vaikeväärtused', 'admin.defaultSettings.title': 'Kasutajate vaikeseaded', + 'admin.defaultSettings.routing': 'Marsruudid', 'admin.defaultSettings.description': 'Määra kogu paigalduse vaikeseaded. Neid väärtusi näevad kasutajad, kes pole vastavat seadet muutnud. Kasutaja enda muudatused on alati ülimuslikud.', 'admin.defaultSettings.saved': 'Vaikeväärtus salvestatud', @@ -788,16 +791,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Lisamoodul uuendatud', 'admin.addons.toast.error': 'Lisamooduli uuendamine ebaõnnestus', 'admin.addons.group.count': 'Lubatud {enabled}/{total}', - 'admin.addons.search': 'Otsi lisasid', - 'admin.addons.filter': 'Näita', - 'admin.addons.filter.all': 'Kõik', - 'admin.addons.filter.on': 'Sees', - 'admin.addons.filter.off': 'Väljas', - 'admin.addons.noMatch': 'Otsingule ega filtrile ei vasta ükski lisa.', - 'admin.addons.subOptions': '{count} valikut', - 'admin.addons.subOptions.one': '{count} valik', - 'admin.addons.subOptionsOn': '{count} lubatud', - 'admin.addons.subOptionsOn.one': '{count} lubatud', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Kasutajad saavad ühendada oma wanderer-serveri ja importida selle matkarajad retkedena', @@ -824,7 +817,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Tasuta, API-võtit pole vaja', 'admin.weather.locationHint': 'Ilm põhineb iga päeva esimesel koordinaatidega kohal. Kui päevale pole ühtegi kohta määratud, kasutatakse võrdluskohana mõnda kohta kohtade nimekirjast.', - 'admin.tabs.mcpTokens': 'MCP juurdepääs', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP juurdepääs', 'admin.mcpTokens.subtitle': 'Halda kõigi kasutajate OAuth-seansse ja API-pääsutõendeid', 'admin.mcpTokens.sectionTitle': 'API-pääsutõendid', @@ -926,7 +919,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.one': 'Eemaldatud {count} pääsuvõti', 'admin.group.users': 'Kasutajad', 'admin.group.config': 'Seadistus', - 'admin.group.integration': 'Integratsioonid', 'admin.group.maintenance': 'Hooldus', 'admin.invite.tripLabel': 'Lisa reisile (valikuline)', 'admin.invite.tripNone': 'Reis puudub', diff --git a/shared/src/i18n/et/mailAdmin.ts b/shared/src/i18n/et/mailAdmin.ts index 1c5e39713..0ca231879 100644 --- a/shared/src/i18n/et/mailAdmin.ts +++ b/shared/src/i18n/et/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Sisesta port vahemikus 1 kuni 65535.', 'mailAdmin.problem.templateToken': 'Aadressis peab iga inimese koodi jaoks olema {token}.', 'mailAdmin.problem.dailyInvalid': 'Sisesta täisarv vahemikus 0 kuni 1000.', - 'mailAdmin.status.title': 'Olek', 'mailAdmin.status.lastRead': 'Viimati loetud {time}', 'mailAdmin.status.never': 'mitte kunagi', 'mailAdmin.status.mails': 'Kirjad, 24 t', diff --git a/shared/src/i18n/fr/admin.ts b/shared/src/i18n/fr/admin.ts index fa8738e45..056fee79f 100644 --- a/shared/src/i18n/fr/admin.ts +++ b/shared/src/i18n/fr/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notifications', // same-as-en + 'admin.notifications.channels': 'Canaux', + 'admin.notifications.adminAlerts': 'Alertes admin', 'admin.notifications.hint': 'Choisissez un canal de notification. Un seul peut être actif à la fois.', 'admin.notifications.none': 'Désactivé', 'admin.notifications.email': 'E-mail (SMTP)', @@ -74,6 +76,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Erreur lors de la création du lien', 'admin.invite.deleteError': 'Erreur lors de la suppression du lien', 'admin.tabs.settings': 'Paramètres', + 'admin.tabs.security': 'Sécurité', + 'admin.tabs.services': 'Services', 'admin.allowRegistration': 'Autoriser les inscriptions', 'admin.allowRegistrationHint': "Les nouveaux utilisateurs peuvent s'inscrire eux-mêmes", 'admin.authMethods': "Méthodes d'authentification", @@ -137,10 +141,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Inclus', 'admin.trekApi.notIncluded': 'Non inclus', 'admin.trekApi.quota.title': 'Quota du jour', - 'admin.trekApi.quota.used': 'Utilisé', 'admin.trekApi.quota.remaining': 'Restant', 'admin.trekApi.quota.resets': 'Remise à zéro à', - 'admin.trekApi.quota.limit': 'Limite', 'admin.trekApi.quota.key': 'Clé', 'admin.trekApi.quota.state.connected': 'Connecté', 'admin.trekApi.quota.state.registering': 'Configuration en cours', @@ -266,8 +268,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Et ensuite', 'admin.collab.whatsnext.subtitle': "Suggestions d'activités et prochaines étapes", 'admin.tabs.config': 'Personnalisation', - 'admin.tabs.defaults': 'Valeurs par défaut', + 'admin.tabs.defaults': 'Préréglages', 'admin.defaultSettings.title': 'Paramètres utilisateur par défaut', + 'admin.defaultSettings.routing': 'Itinéraires', 'admin.defaultSettings.description': "Définissez des valeurs par défaut pour toute l'instance. Les utilisateurs n'ayant pas modifié un paramètre verront ces valeurs. Leurs propres modifications ont toujours la priorité.", 'admin.defaultSettings.saved': 'Valeur par défaut enregistrée', @@ -732,16 +735,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Extension mise à jour', 'admin.addons.toast.error': "Échec de la mise à jour de l'extension", 'admin.addons.group.count': '{enabled} sur {total} activés', - 'admin.addons.search': 'Rechercher des modules', - 'admin.addons.filter': 'Afficher', - 'admin.addons.filter.all': 'Tous', - 'admin.addons.filter.on': 'Activés', - 'admin.addons.filter.off': 'Désactivés', - 'admin.addons.noMatch': 'Aucun module ne correspond à la recherche ou au filtre.', - 'admin.addons.subOptions': '{count} options', // same-as-en - 'admin.addons.subOptions.one': '{count} option', // same-as-en - 'admin.addons.subOptionsOn': '{count} activées', - 'admin.addons.subOptionsOn.one': '{count} activée', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Les utilisateurs peuvent connecter leur propre instance wanderer et importer ses sentiers comme sorties', @@ -781,7 +774,7 @@ const admin: TranslationStrings = { 'admin.audit.col.resource': 'Ressource', 'admin.audit.col.ip': 'IP', 'admin.audit.col.details': 'Détails', - 'admin.tabs.mcpTokens': 'Accès MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Accès MCP', 'admin.mcpTokens.subtitle': 'Gérer les sessions OAuth et les tokens API de tous les utilisateurs', 'admin.mcpTokens.sectionTitle': 'Tokens API', @@ -940,7 +933,6 @@ const admin: TranslationStrings = { "Utilisée pour chaque utilisateur n'ayant pas saisi sa propre clé, ainsi toute l'instance reçoit les tuiles CARTO sans filigrane. Stockée de façon chiffrée.", 'admin.group.users': 'Utilisateurs', 'admin.group.config': 'Configuration', // same-as-en - 'admin.group.integration': 'Intégrations', 'admin.group.maintenance': 'Maintenance', // same-as-en 'admin.invite.tripLabel': 'Ajouter à un voyage (facultatif)', 'admin.invite.tripNone': 'Aucun voyage', diff --git a/shared/src/i18n/fr/mailAdmin.ts b/shared/src/i18n/fr/mailAdmin.ts index e7f70d256..f4e680020 100644 --- a/shared/src/i18n/fr/mailAdmin.ts +++ b/shared/src/i18n/fr/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Saisissez un port entre 1 et 65535.', 'mailAdmin.problem.templateToken': 'L’adresse doit contenir {token} pour le code de chaque personne.', 'mailAdmin.problem.dailyInvalid': 'Saisissez un nombre entier de 0 à 1000.', - 'mailAdmin.status.title': 'État', 'mailAdmin.status.lastRead': 'Dernière lecture {time}', 'mailAdmin.status.never': 'jamais', 'mailAdmin.status.mails': 'E-mails, 24 h', diff --git a/shared/src/i18n/gr/admin.ts b/shared/src/i18n/gr/admin.ts index 10c36b99a..40417b9ee 100644 --- a/shared/src/i18n/gr/admin.ts +++ b/shared/src/i18n/gr/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Ειδοποιήσεις', + 'admin.notifications.channels': 'Κανάλια', + 'admin.notifications.adminAlerts': 'Ειδοποιήσεις διαχειριστή', 'admin.notifications.hint': 'Επιλέξτε ένα κανάλι ειδοποιήσεων. Μόνο ένα μπορεί να είναι ενεργό κάθε φορά.', 'admin.notifications.none': 'Απενεργοποιημένο', 'admin.notifications.email': 'Email (SMTP)', @@ -130,6 +132,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Αποτυχία δημιουργίας συνδέσμου πρόσκλησης', 'admin.invite.deleteError': 'Αποτυχία διαγραφής συνδέσμου πρόσκλησης', 'admin.tabs.settings': 'Ρυθμίσεις', + 'admin.tabs.security': 'Ασφάλεια', + 'admin.tabs.services': 'Υπηρεσίες', 'admin.allowRegistration': 'Να επιτρέπονται οι εγγραφές', 'admin.allowRegistrationHint': 'Οι νέοι χρήστες μπορούν να εγγράφονται μόνοι τους', 'admin.authMethods': 'Μέθοδοι Ελέγχου Ταυτότητας', @@ -191,10 +195,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Περιλαμβάνεται', 'admin.trekApi.notIncluded': 'Δεν περιλαμβάνεται', 'admin.trekApi.quota.title': 'Σημερινό όριο', - 'admin.trekApi.quota.used': 'Χρησιμοποιήθηκαν', 'admin.trekApi.quota.remaining': 'Απομένουν', 'admin.trekApi.quota.resets': 'Μηδενίζεται στις', - 'admin.trekApi.quota.limit': 'Όριο', 'admin.trekApi.quota.key': 'Κλειδί', 'admin.trekApi.quota.state.connected': 'Συνδεδεμένο', 'admin.trekApi.quota.state.registering': 'Ρύθμιση σε εξέλιξη', @@ -320,8 +322,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Τι Ακολουθεί', 'admin.collab.whatsnext.subtitle': 'Προτάσεις δραστηριοτήτων και επόμενα βήματα', 'admin.tabs.config': 'Εξατομίκευση', - 'admin.tabs.defaults': 'Προεπιλογές Χρήστη', + 'admin.tabs.defaults': 'Προεπιλογές', 'admin.defaultSettings.title': 'Προεπιλεγμένες Ρυθμίσεις Χρήστη', + 'admin.defaultSettings.routing': 'Δρομολόγηση', 'admin.defaultSettings.description': 'Ορίστε προεπιλογές για όλη την υπηρεσία. Οι χρήστες που δεν έχουν αλλάξει μια ρύθμιση θα βλέπουν αυτές τις τιμές. Οι δικές τους αλλαγές έχουν πάντα προτεραιότητα.', 'admin.defaultSettings.saved': 'Η προεπιλογή αποθηκεύτηκε', @@ -794,16 +797,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Το πρόσθετο ενημερώθηκε', 'admin.addons.toast.error': 'Αποτυχία ενημέρωσης προσθέτου', 'admin.addons.group.count': '{enabled} από {total} ενεργοποιημένα', - 'admin.addons.search': 'Αναζήτηση πρόσθετων', - 'admin.addons.filter': 'Εμφάνιση', - 'admin.addons.filter.all': 'Όλα', - 'admin.addons.filter.on': 'Ενεργά', - 'admin.addons.filter.off': 'Ανενεργά', - 'admin.addons.noMatch': 'Κανένα πρόσθετο δεν ταιριάζει με την αναζήτηση ή το φίλτρο.', - 'admin.addons.subOptions': '{count} επιλογές', - 'admin.addons.subOptions.one': '{count} επιλογή', - 'admin.addons.subOptionsOn': '{count} ενεργές', - 'admin.addons.subOptionsOn.one': '{count} ενεργή', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Οι χρήστες μπορούν να συνδέσουν τη δική τους εγκατάσταση wanderer και να εισαγάγουν τα μονοπάτια της ως εκδρομές', @@ -829,7 +822,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Δωρεάν, δεν απαιτείται κλειδί API', 'admin.weather.locationHint': 'Ο καιρός βασίζεται στον πρώτο τόπο με συντεταγμένες σε κάθε ημέρα. Αν δεν έχει ανατεθεί τόπος σε μια ημέρα, χρησιμοποιείται ως αναφορά οποιοσδήποτε τόπος από τη λίστα.', - 'admin.tabs.mcpTokens': 'Πρόσβαση MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Πρόσβαση MCP', 'admin.mcpTokens.subtitle': 'Διαχείριση συνεδριών OAuth και διακριτικών API για όλους τους χρήστες', 'admin.mcpTokens.sectionTitle': 'Διακριτικά API', @@ -949,7 +942,6 @@ const admin: TranslationStrings = { 'Χρησιμοποιείται για κάθε χρήστη που δεν έχει εισαγάγει το δικό του κλειδί, έτσι ολόκληρη η εγκατάσταση λαμβάνει πλακίδια CARTO χωρίς υδατογράφημα. Αποθηκεύεται κρυπτογραφημένο.', 'admin.group.users': 'Χρήστες', 'admin.group.config': 'Ρυθμίσεις', - 'admin.group.integration': 'Ενσωματώσεις', 'admin.group.maintenance': 'Συντήρηση', 'admin.invite.tripLabel': 'Προσθήκη σε ταξίδι (προαιρετικό)', 'admin.invite.tripNone': 'Χωρίς ταξίδι', diff --git a/shared/src/i18n/gr/mailAdmin.ts b/shared/src/i18n/gr/mailAdmin.ts index d4d7e44bd..002841f9f 100644 --- a/shared/src/i18n/gr/mailAdmin.ts +++ b/shared/src/i18n/gr/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Εισήγαγε θύρα από 1 έως 65535.', 'mailAdmin.problem.templateToken': 'Η διεύθυνση χρειάζεται {token} για τον κωδικό κάθε ατόμου.', 'mailAdmin.problem.dailyInvalid': 'Εισήγαγε έναν ακέραιο αριθμό από 0 έως 1000.', - 'mailAdmin.status.title': 'Κατάσταση', 'mailAdmin.status.lastRead': 'Τελευταία ανάγνωση {time}', 'mailAdmin.status.never': 'ποτέ', 'mailAdmin.status.mails': 'Email, 24 ώ', diff --git a/shared/src/i18n/he/admin.ts b/shared/src/i18n/he/admin.ts index 9bb83d10f..d57e78d04 100644 --- a/shared/src/i18n/he/admin.ts +++ b/shared/src/i18n/he/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'התראות', + 'admin.notifications.channels': 'ערוצים', + 'admin.notifications.adminAlerts': 'התראות מנהל', 'admin.notifications.hint': 'בחרו ערוץ התראות אחד. רק אחד יכול להיות פעיל בכל פעם.', 'admin.notifications.none': 'מושבת', 'admin.notifications.email': 'אימייל (SMTP)', @@ -71,7 +73,7 @@ const admin: TranslationStrings = { 'admin.tabs.categories': 'קטגוריות', 'admin.tabs.backup': 'גיבוי', 'admin.tabs.notifications': 'התראות', - 'admin.tabs.audit': 'יומן ביקורת', + 'admin.tabs.audit': 'ביקורת', 'admin.stats.users': 'משתמשים', 'admin.stats.users.one': 'משתמש', 'admin.stats.users.two': 'משתמשים', @@ -128,6 +130,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'יצירת קישור ההזמנה נכשלה', 'admin.invite.deleteError': 'מחיקת קישור ההזמנה נכשלה', 'admin.tabs.settings': 'הגדרות', + 'admin.tabs.security': 'אבטחה', + 'admin.tabs.services': 'שירותים', 'admin.allowRegistration': 'אפשור הרשמה', 'admin.allowRegistrationHint': 'משתמשים חדשים יכולים להירשם בעצמם', 'admin.authMethods': 'שיטות אימות', @@ -185,10 +189,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'כלול', 'admin.trekApi.notIncluded': 'לא כלול', 'admin.trekApi.quota.title': 'המכסה של היום', - 'admin.trekApi.quota.used': 'בשימוש', 'admin.trekApi.quota.remaining': 'נותר', 'admin.trekApi.quota.resets': 'מתאפס ב', - 'admin.trekApi.quota.limit': 'מגבלה יומית', 'admin.trekApi.quota.key': 'מפתח', 'admin.trekApi.quota.state.connected': 'מחובר', 'admin.trekApi.quota.state.registering': 'בהגדרה', @@ -306,8 +308,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'מה הלאה', 'admin.collab.whatsnext.subtitle': 'הצעות לפעילויות וצעדים הבאים', 'admin.tabs.config': 'התאמה אישית', - 'admin.tabs.defaults': 'ברירות מחדל למשתמשים', + 'admin.tabs.defaults': 'ברירות־מחדל', 'admin.defaultSettings.title': 'הגדרות ברירת מחדל למשתמשים', + 'admin.defaultSettings.routing': 'ניתוב', 'admin.defaultSettings.description': 'הגדירו ברירות מחדל לכל המופע. משתמשים שלא שינו הגדרה יראו את הערכים האלה. השינויים שלהם תמיד קודמים.', 'admin.defaultSettings.saved': 'ברירת המחדל נשמרה', @@ -353,7 +356,7 @@ const admin: TranslationStrings = { 'admin.packingTemplates.deleteCategoryError': 'מחיקת הקטגוריה נכשלה', 'admin.packingTemplates.deleteItemError': 'מחיקת הפריט נכשלה', 'admin.packingTemplates.saveError': 'השמירה נכשלה', - 'admin.tabs.addons': 'תוספים מובנים', + 'admin.tabs.addons': 'תוספים', 'admin.tabs.plugins': 'תוספים', 'admin.tabs.storage': 'אחסון', 'admin.plugins.rescan': 'סריקה מחדש', @@ -758,18 +761,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'התוסף עודכן', 'admin.addons.toast.error': 'עדכון התוסף נכשל', 'admin.addons.group.count': '{enabled} מתוך {total} מופעלים', - 'admin.addons.search': 'חיפוש תוספים', - 'admin.addons.filter': 'הצגה', - 'admin.addons.filter.all': 'הכול', - 'admin.addons.filter.on': 'פועלים', - 'admin.addons.filter.off': 'כבויים', - 'admin.addons.noMatch': 'אין תוסף שמתאים לחיפוש או למסנן.', - 'admin.addons.subOptions': '{count} אפשרויות', - 'admin.addons.subOptions.one': 'אפשרות אחת', - 'admin.addons.subOptions.two': 'שתי אפשרויות', - 'admin.addons.subOptionsOn': '{count} פועלות', - 'admin.addons.subOptionsOn.one': 'אחת פועלת', - 'admin.addons.subOptionsOn.two': 'שתיים פועלות', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'משתמשים יכולים לחבר מופע wanderer משלהם ולייבא את השבילים שלו כמסלולים', 'admin.toursHikingMap.title': 'מפת הליכה (OpenHikingMap)', @@ -794,7 +785,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'חינם, בלי מפתח API', 'admin.weather.locationHint': 'מזג האוויר מבוסס על המקום הראשון עם קואורדינטות בכל יום. אם לא שויך מקום ליום, משמש כנקודת ייחוס מקום כלשהו מרשימת המקומות.', - 'admin.tabs.mcpTokens': 'גישת MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'גישת MCP', 'admin.mcpTokens.subtitle': 'ניהול הפעלות OAuth ואסימוני API של כל המשתמשים', 'admin.mcpTokens.sectionTitle': 'אסימוני API', @@ -890,7 +881,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.two': 'הוסרו שני מפתחות גישה', 'admin.group.users': 'משתמשים', 'admin.group.config': 'הגדרות', - 'admin.group.integration': 'שילובים', 'admin.group.maintenance': 'תחזוקה', 'admin.invite.tripLabel': 'הוספה לטיול (לא חובה)', 'admin.invite.tripNone': 'ללא טיול', diff --git a/shared/src/i18n/he/mailAdmin.ts b/shared/src/i18n/he/mailAdmin.ts index a7e67cfd5..df6f9682f 100644 --- a/shared/src/i18n/he/mailAdmin.ts +++ b/shared/src/i18n/he/mailAdmin.ts @@ -37,7 +37,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'הזינו פורט בין 1 ל-65535.', 'mailAdmin.problem.templateToken': 'הכתובת צריכה {token} במקום הקוד של כל אדם.', 'mailAdmin.problem.dailyInvalid': 'הזינו מספר שלם בין 0 ל-1000.', - 'mailAdmin.status.title': 'סטטוס', 'mailAdmin.status.lastRead': 'נקרא לאחרונה {time}', 'mailAdmin.status.never': 'אף פעם', 'mailAdmin.status.mails': 'דוא״ל, 24 שעות', diff --git a/shared/src/i18n/hu/admin.ts b/shared/src/i18n/hu/admin.ts index ba8549191..dc16a47a0 100644 --- a/shared/src/i18n/hu/admin.ts +++ b/shared/src/i18n/hu/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Értesítések', + 'admin.notifications.channels': 'Csatornák', + 'admin.notifications.adminAlerts': 'Rendszergazdai értesítések', 'admin.notifications.hint': 'Válasszon értesítési csatornát. Egyszerre csak egy lehet aktív.', 'admin.notifications.none': 'Kikapcsolva', 'admin.notifications.email': 'E-mail (SMTP)', @@ -21,7 +23,7 @@ const admin: TranslationStrings = { 'admin.subtitle': 'Felhasználókezelés és rendszerbeállítások', 'admin.tabs.users': 'Felhasználók', 'admin.tabs.categories': 'Kategóriák', - 'admin.tabs.backup': 'Biztonsági mentés', + 'admin.tabs.backup': 'Mentés', 'admin.stats.users': 'Felhasználó', 'admin.stats.users.one': 'Felhasználó', 'admin.stats.trips': 'Utazás', @@ -74,6 +76,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Nem sikerült létrehozni a meghívó linket', 'admin.invite.deleteError': 'Nem sikerült törölni a meghívó linket', 'admin.tabs.settings': 'Beállítások', + 'admin.tabs.security': 'Biztonság', + 'admin.tabs.services': 'Szolgáltatások', 'admin.allowRegistration': 'Regisztráció engedélyezése', 'admin.allowRegistrationHint': 'Új felhasználók regisztrálhatják magukat', 'admin.authMethods': 'Hitelesítési módok', @@ -135,10 +139,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Tartalmazza', 'admin.trekApi.notIncluded': 'Nem tartalmazza', 'admin.trekApi.quota.title': 'Mai keret', - 'admin.trekApi.quota.used': 'Felhasznált', 'admin.trekApi.quota.remaining': 'Hátralévő', 'admin.trekApi.quota.resets': 'Visszaáll ekkor', - 'admin.trekApi.quota.limit': 'Korlát', 'admin.trekApi.quota.key': 'Kulcs', 'admin.trekApi.quota.state.connected': 'Csatlakoztatva', 'admin.trekApi.quota.state.registering': 'Beállítás folyamatban', @@ -262,9 +264,10 @@ const admin: TranslationStrings = { 'admin.collab.polls.subtitle': 'Csoportos szavazások', 'admin.collab.whatsnext.title': 'Mi következik', 'admin.collab.whatsnext.subtitle': 'Tevékenységjavaslatok és következő lépések', - 'admin.tabs.config': 'Személyre szabás', - 'admin.tabs.defaults': 'Alapértelmezett beállítások', + 'admin.tabs.config': 'Testreszabás', + 'admin.tabs.defaults': 'Alapértékek', 'admin.defaultSettings.title': 'Alapértelmezett felhasználói beállítások', + 'admin.defaultSettings.routing': 'Útvonaltervezés', 'admin.defaultSettings.description': 'Állítson be alapértelmezett értékeket az egész példányra. Azok a felhasználók, akik nem módosítottak egy beállítást, ezeket az értékeket fogják látni. A saját módosításaik mindig elsőbbséget élveznek.', 'admin.defaultSettings.saved': 'Alapértelmezett mentve', @@ -731,16 +734,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Bővítmény frissítve', 'admin.addons.toast.error': 'Nem sikerült frissíteni a bővítményt', 'admin.addons.group.count': '{total} közül {enabled} bekapcsolva', - 'admin.addons.search': 'Bővítmények keresése', - 'admin.addons.filter': 'Megjelenítés', - 'admin.addons.filter.all': 'Mind', - 'admin.addons.filter.on': 'Be', - 'admin.addons.filter.off': 'Ki', - 'admin.addons.noMatch': 'Egy bővítmény sem felel meg a keresésnek vagy a szűrőnek.', - 'admin.addons.subOptions': '{count} beállítás', - 'admin.addons.subOptions.one': '{count} beállítás', - 'admin.addons.subOptionsOn': '{count} bekapcsolva', - 'admin.addons.subOptionsOn.one': '{count} bekapcsolva', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'A felhasználók csatlakoztathatják saját wanderer-példányukat, és túraként importálhatják annak túraútvonalait', @@ -779,7 +772,7 @@ const admin: TranslationStrings = { 'admin.audit.col.resource': 'Erőforrás', 'admin.audit.col.ip': 'IP', 'admin.audit.col.details': 'Részletek', - 'admin.tabs.mcpTokens': 'MCP hozzáférés', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP hozzáférés', 'admin.mcpTokens.subtitle': 'OAuth munkamenetek és API tokenek kezelése az összes felhasználó számára', 'admin.mcpTokens.sectionTitle': 'API tokenek', @@ -939,7 +932,6 @@ const admin: TranslationStrings = { 'Minden olyan felhasználóhoz használatos, aki nem adta meg a saját kulcsát, így az egész példány vízjel nélküli CARTO-csempéket kap. Titkosítva tárolódik.', 'admin.group.users': 'Felhasználók', 'admin.group.config': 'Konfiguráció', - 'admin.group.integration': 'Integrációk', 'admin.group.maintenance': 'Karbantartás', 'admin.invite.tripLabel': 'Hozzáadás utazáshoz (opcionális)', 'admin.invite.tripNone': 'Nincs utazás', diff --git a/shared/src/i18n/hu/mailAdmin.ts b/shared/src/i18n/hu/mailAdmin.ts index 2c18c5cc7..8fb113a93 100644 --- a/shared/src/i18n/hu/mailAdmin.ts +++ b/shared/src/i18n/hu/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Adj meg egy portot 1 és 65535 között.', 'mailAdmin.problem.templateToken': 'A címben szerepelnie kell a {token} helyőrzőnek az egyes személyek kódjához.', 'mailAdmin.problem.dailyInvalid': 'Adj meg egy egész számot 0 és 1000 között.', - 'mailAdmin.status.title': 'Állapot', 'mailAdmin.status.lastRead': 'Utolsó olvasás: {time}', 'mailAdmin.status.never': 'soha', 'mailAdmin.status.mails': 'E-mailek, 24 óra', diff --git a/shared/src/i18n/id/admin.ts b/shared/src/i18n/id/admin.ts index 9e5c974ce..56049354b 100644 --- a/shared/src/i18n/id/admin.ts +++ b/shared/src/i18n/id/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notifikasi', + 'admin.notifications.channels': 'Saluran', + 'admin.notifications.adminAlerts': 'Peringatan admin', 'admin.notifications.hint': 'Pilih satu saluran notifikasi. Hanya satu yang bisa aktif sekaligus.', 'admin.notifications.none': 'Dinonaktifkan', 'admin.notifications.email': 'Email (SMTP)', // same-as-en @@ -122,6 +124,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Gagal membuat tautan undangan', 'admin.invite.deleteError': 'Gagal menghapus tautan undangan', 'admin.tabs.settings': 'Pengaturan', + 'admin.tabs.security': 'Keamanan', + 'admin.tabs.services': 'Layanan', 'admin.allowRegistration': 'Izinkan Pendaftaran', 'admin.allowRegistrationHint': 'Pengguna baru dapat mendaftar sendiri', 'admin.authMethods': 'Metode Autentikasi', @@ -183,10 +187,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Termasuk', 'admin.trekApi.notIncluded': 'Tidak termasuk', 'admin.trekApi.quota.title': 'Kuota hari ini', - 'admin.trekApi.quota.used': 'Terpakai', 'admin.trekApi.quota.remaining': 'Tersisa', 'admin.trekApi.quota.resets': 'Direset pukul', - 'admin.trekApi.quota.limit': 'Batas', 'admin.trekApi.quota.key': 'Kunci', 'admin.trekApi.quota.state.connected': 'Terhubung', 'admin.trekApi.quota.state.registering': 'Sedang disiapkan', @@ -311,8 +313,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Selanjutnya', 'admin.collab.whatsnext.subtitle': 'Saran aktivitas dan langkah selanjutnya', 'admin.tabs.config': 'Personalisasi', - 'admin.tabs.defaults': 'Pengaturan Default Pengguna', + 'admin.tabs.defaults': 'Bawaan', 'admin.defaultSettings.title': 'Pengaturan Default Pengguna', + 'admin.defaultSettings.routing': 'Rute', 'admin.defaultSettings.description': 'Tetapkan nilai default untuk seluruh instance. Pengguna yang belum mengubah pengaturan akan melihat nilai-nilai ini. Perubahan mereka sendiri selalu diprioritaskan.', 'admin.defaultSettings.saved': 'Default disimpan', @@ -771,14 +774,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Addon diperbarui', 'admin.addons.toast.error': 'Gagal memperbarui addon', 'admin.addons.group.count': '{enabled} dari {total} aktif', - 'admin.addons.search': 'Cari add-on', - 'admin.addons.filter': 'Tampilkan', - 'admin.addons.filter.all': 'Semua', - 'admin.addons.filter.on': 'Aktif', - 'admin.addons.filter.off': 'Nonaktif', - 'admin.addons.noMatch': 'Tidak ada add-on yang cocok dengan pencarian atau filter.', - 'admin.addons.subOptions': '{count} opsi', - 'admin.addons.subOptionsOn': '{count} aktif', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Pengguna dapat menghubungkan instans wanderer milik sendiri dan mengimpor jalurnya sebagai tur', @@ -804,7 +799,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Gratis, tidak perlu kunci API', 'admin.weather.locationHint': 'Cuaca didasarkan pada tempat pertama dengan koordinat di setiap hari. Jika tidak ada tempat yang ditetapkan untuk suatu hari, tempat mana pun dari daftar tempat digunakan sebagai referensi.', - 'admin.tabs.mcpTokens': 'Akses MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Akses MCP', 'admin.mcpTokens.subtitle': 'Kelola sesi OAuth dan token API di semua pengguna', 'admin.mcpTokens.sectionTitle': 'Token API', @@ -919,7 +914,6 @@ const admin: TranslationStrings = { 'Digunakan untuk setiap pengguna yang belum memasukkan kunci mereka sendiri, sehingga seluruh instance mendapatkan ubin CARTO tanpa tanda air. Disimpan dalam bentuk terenkripsi.', 'admin.group.users': 'Pengguna', 'admin.group.config': 'Konfigurasi', - 'admin.group.integration': 'Integrasi', 'admin.group.maintenance': 'Pemeliharaan', 'admin.invite.tripLabel': 'Tambahkan ke perjalanan (opsional)', 'admin.invite.tripNone': 'Tanpa perjalanan', diff --git a/shared/src/i18n/id/mailAdmin.ts b/shared/src/i18n/id/mailAdmin.ts index 3a8159e49..5645f51d9 100644 --- a/shared/src/i18n/id/mailAdmin.ts +++ b/shared/src/i18n/id/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Masukkan port antara 1 dan 65535.', 'mailAdmin.problem.templateToken': 'Alamat memerlukan {token} untuk kode setiap orang.', 'mailAdmin.problem.dailyInvalid': 'Masukkan bilangan bulat dari 0 sampai 1000.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Terakhir dibaca {time}', 'mailAdmin.status.never': 'belum pernah', 'mailAdmin.status.mails': 'Email, 24 j', diff --git a/shared/src/i18n/it/admin.ts b/shared/src/i18n/it/admin.ts index 0caba9c2c..c089c6815 100644 --- a/shared/src/i18n/it/admin.ts +++ b/shared/src/i18n/it/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Notifiche', + 'admin.notifications.channels': 'Canali', + 'admin.notifications.adminAlerts': 'Avvisi admin', 'admin.notifications.hint': 'Scegli un canale di notifica. Solo uno può essere attivo alla volta.', 'admin.notifications.none': 'Disattivato', 'admin.notifications.email': 'E-mail (SMTP)', @@ -74,6 +76,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Impossibile creare il link di invito', 'admin.invite.deleteError': 'Impossibile eliminare il link di invito', 'admin.tabs.settings': 'Impostazioni', + 'admin.tabs.security': 'Sicurezza', + 'admin.tabs.services': 'Servizi', 'admin.allowRegistration': 'Consenti Registrazione', 'admin.allowRegistrationHint': 'I nuovi utenti possono registrarsi autonomamente', 'admin.authMethods': 'Metodi di autenticazione', @@ -135,10 +139,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Incluso', 'admin.trekApi.notIncluded': 'Non incluso', 'admin.trekApi.quota.title': 'Quota di oggi', - 'admin.trekApi.quota.used': 'Usata', 'admin.trekApi.quota.remaining': 'Rimanente', 'admin.trekApi.quota.resets': 'Si azzera alle', - 'admin.trekApi.quota.limit': 'Limite', 'admin.trekApi.quota.key': 'Chiave', 'admin.trekApi.quota.state.connected': 'Connesso', 'admin.trekApi.quota.state.registering': 'Configurazione in corso', @@ -262,8 +264,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Prossimi passi', 'admin.collab.whatsnext.subtitle': 'Suggerimenti attività e prossimi passi', 'admin.tabs.config': 'Personalizzazione', - 'admin.tabs.defaults': 'Impostazioni predefinite', + 'admin.tabs.defaults': 'Predefiniti', 'admin.defaultSettings.title': 'Impostazioni predefinite utente', + 'admin.defaultSettings.routing': 'Percorsi', 'admin.defaultSettings.description': "Imposta i valori predefiniti per l'intera istanza. Gli utenti che non hanno modificato un'impostazione vedranno questi valori. Le loro modifiche hanno sempre la priorità.", 'admin.defaultSettings.saved': 'Predefinito salvato', @@ -725,16 +728,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Modulo aggiornato', 'admin.addons.toast.error': 'Impossibile aggiornare il modulo', 'admin.addons.group.count': '{enabled} di {total} attivi', - 'admin.addons.search': 'Cerca componenti aggiuntivi', - 'admin.addons.filter': 'Mostra', - 'admin.addons.filter.all': 'Tutti', - 'admin.addons.filter.on': 'Attivi', - 'admin.addons.filter.off': 'Disattivi', - 'admin.addons.noMatch': 'Nessun componente corrisponde alla ricerca o al filtro.', - 'admin.addons.subOptions': '{count} opzioni', - 'admin.addons.subOptions.one': '{count} opzione', - 'admin.addons.subOptionsOn': '{count} attive', - 'admin.addons.subOptionsOn.one': '{count} attiva', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Gli utenti possono collegare la propria istanza wanderer e importarne i sentieri come escursioni', @@ -773,7 +766,7 @@ const admin: TranslationStrings = { 'admin.audit.col.resource': 'Risorsa', 'admin.audit.col.ip': 'IP', 'admin.audit.col.details': 'Dettagli', - 'admin.tabs.mcpTokens': 'Accesso MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Accesso MCP', 'admin.mcpTokens.subtitle': 'Gestisci le sessioni OAuth e i token API di tutti gli utenti', 'admin.mcpTokens.sectionTitle': 'Token API', @@ -933,7 +926,6 @@ const admin: TranslationStrings = { "Usata per ogni utente che non ha inserito una propria chiave, così tutta l'istanza ottiene le tessere CARTO senza filigrana. Archiviata in forma crittografata.", 'admin.group.users': 'Utenti', 'admin.group.config': 'Configurazione', - 'admin.group.integration': 'Integrazioni', 'admin.group.maintenance': 'Manutenzione', 'admin.invite.tripLabel': 'Aggiungi a un viaggio (opzionale)', 'admin.invite.tripNone': 'Nessun viaggio', diff --git a/shared/src/i18n/it/mailAdmin.ts b/shared/src/i18n/it/mailAdmin.ts index f625f3429..5f6072173 100644 --- a/shared/src/i18n/it/mailAdmin.ts +++ b/shared/src/i18n/it/mailAdmin.ts @@ -40,7 +40,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Inserisci una porta tra 1 e 65535.', 'mailAdmin.problem.templateToken': 'L’indirizzo richiede {token} per il codice di ogni persona.', 'mailAdmin.problem.dailyInvalid': 'Inserisci un numero intero da 0 a 1000.', - 'mailAdmin.status.title': 'Stato', 'mailAdmin.status.lastRead': 'Ultima lettura {time}', 'mailAdmin.status.never': 'mai', 'mailAdmin.status.mails': 'E-mail, 24 h', diff --git a/shared/src/i18n/ja/admin.ts b/shared/src/i18n/ja/admin.ts index e27482e36..e28871e3e 100644 --- a/shared/src/i18n/ja/admin.ts +++ b/shared/src/i18n/ja/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': '通知', + 'admin.notifications.channels': 'チャンネル', + 'admin.notifications.adminAlerts': '管理者向け通知', 'admin.notifications.hint': '通知チャネルを1つ選択してください。同時に有効にできるのは1つだけです。', 'admin.notifications.none': '無効', 'admin.notifications.email': 'メール(SMTP)', @@ -116,6 +118,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': '招待リンクの作成に失敗しました', 'admin.invite.deleteError': '招待リンクの削除に失敗しました', 'admin.tabs.settings': '設定', + 'admin.tabs.security': 'セキュリティ', + 'admin.tabs.services': 'サービス', 'admin.allowRegistration': '登録を許可', 'admin.allowRegistrationHint': '新規ユーザーの自己登録を許可します', 'admin.authMethods': '認証方法', @@ -173,10 +177,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': '含まれるもの', 'admin.trekApi.notIncluded': '含まれないもの', 'admin.trekApi.quota.title': '今日の上限', - 'admin.trekApi.quota.used': '使用済み', 'admin.trekApi.quota.remaining': '残り', 'admin.trekApi.quota.resets': 'リセット時刻', - 'admin.trekApi.quota.limit': '上限', 'admin.trekApi.quota.key': 'キー', 'admin.trekApi.quota.state.connected': '接続済み', 'admin.trekApi.quota.state.registering': '設定中', @@ -298,8 +300,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': '次にすること', 'admin.collab.whatsnext.subtitle': 'アクティビティ提案と次のステップ', 'admin.tabs.config': 'カスタマイズ', - 'admin.tabs.defaults': 'ユーザーのデフォルト', + 'admin.tabs.defaults': 'デフォルト', 'admin.defaultSettings.title': '既定のユーザー設定', + 'admin.defaultSettings.routing': 'ルート検索', 'admin.defaultSettings.description': 'インスタンス全体の既定値を設定します。設定を変更していないユーザーにはこれらの値が適用されます。各ユーザーの設定変更は常に優先されます。', 'admin.defaultSettings.saved': '既定値を保存しました', @@ -733,14 +736,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'アドオンを更新しました', 'admin.addons.toast.error': 'アドオンの更新に失敗しました', 'admin.addons.group.count': '{total} 件中 {enabled} 件が有効', - 'admin.addons.search': 'アドオンを検索', - 'admin.addons.filter': '表示', - 'admin.addons.filter.all': 'すべて', - 'admin.addons.filter.on': 'オン', - 'admin.addons.filter.off': 'オフ', - 'admin.addons.noMatch': '検索またはフィルターに一致するアドオンはありません。', - 'admin.addons.subOptions': '{count}件のオプション', - 'admin.addons.subOptionsOn': '{count}件が有効', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'ユーザーが自分のwandererインスタンスを接続し、そのトレイルをツアーとしてインポートできます', @@ -766,7 +761,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': '無料、APIキー不要', 'admin.weather.locationHint': '各日の座標付き最初の場所を基準にします。場所が未割り当ての場合は、場所一覧の任意の場所を参照します。', - 'admin.tabs.mcpTokens': 'MCPトークン', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCPトークン', 'admin.mcpTokens.subtitle': 'すべてのユーザーのAPIトークンを管理', 'admin.mcpTokens.sectionTitle': 'API トークン', @@ -879,7 +874,6 @@ const admin: TranslationStrings = { '自分のキーを入力していないすべてのユーザーに使用されます。これによりインスタンス全体で透かしのない CARTO タイルを利用できます。暗号化して保存されます。', 'admin.group.users': 'ユーザー', 'admin.group.config': '設定', - 'admin.group.integration': '連携', 'admin.group.maintenance': 'メンテナンス', 'admin.invite.tripLabel': '旅行に追加(任意)', 'admin.invite.tripNone': '旅行なし', diff --git a/shared/src/i18n/ja/mailAdmin.ts b/shared/src/i18n/ja/mailAdmin.ts index 5f3a70cb0..9ce20b253 100644 --- a/shared/src/i18n/ja/mailAdmin.ts +++ b/shared/src/i18n/ja/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '1から65535までのポート番号を入力してください。', 'mailAdmin.problem.templateToken': 'アドレスには、各ユーザーのコードの位置に{token}が必要です。', 'mailAdmin.problem.dailyInvalid': '0から1000までの整数を入力してください。', - 'mailAdmin.status.title': 'ステータス', 'mailAdmin.status.lastRead': '最終読み取り:{time}', 'mailAdmin.status.never': 'なし', 'mailAdmin.status.mails': 'メール、24時間', diff --git a/shared/src/i18n/ko/admin.ts b/shared/src/i18n/ko/admin.ts index 37f7dda60..4b44ecb5b 100644 --- a/shared/src/i18n/ko/admin.ts +++ b/shared/src/i18n/ko/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': '알림', + 'admin.notifications.channels': '채널', + 'admin.notifications.adminAlerts': '관리자 알림', 'admin.notifications.hint': '알림 채널을 하나 선택하세요. 한 번에 하나만 활성화할 수 있습니다.', 'admin.notifications.none': '비활성화', 'admin.notifications.email': '이메일 (SMTP)', @@ -121,6 +123,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': '초대 링크 생성 실패', 'admin.invite.deleteError': '초대 링크 삭제 실패', 'admin.tabs.settings': '설정', + 'admin.tabs.security': '보안', + 'admin.tabs.services': '서비스', 'admin.allowRegistration': '회원가입 허용', 'admin.allowRegistrationHint': '새 사용자가 직접 회원가입할 수 있습니다', 'admin.authMethods': '인증 방법', @@ -178,10 +182,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': '포함', 'admin.trekApi.notIncluded': '미포함', 'admin.trekApi.quota.title': '오늘의 할당량', - 'admin.trekApi.quota.used': '사용함', 'admin.trekApi.quota.remaining': '남음', 'admin.trekApi.quota.resets': '초기화 시각', - 'admin.trekApi.quota.limit': '한도', 'admin.trekApi.quota.key': '키', 'admin.trekApi.quota.state.connected': '연결됨', 'admin.trekApi.quota.state.registering': '설정 중', @@ -300,9 +302,10 @@ const admin: TranslationStrings = { 'admin.collab.polls.subtitle': '그룹 투표', 'admin.collab.whatsnext.title': '다음 할 일', 'admin.collab.whatsnext.subtitle': '활동 제안 및 다음 단계', - 'admin.tabs.config': '개인 설정', + 'admin.tabs.config': '개인화', 'admin.tabs.defaults': '기본값', 'admin.defaultSettings.title': '기본 사용자 설정', + 'admin.defaultSettings.routing': '경로', 'admin.defaultSettings.description': '인스턴스 전체 기본값을 설정합니다. 설정을 변경하지 않은 사용자에게 이 값이 표시됩니다. 사용자의 변경 사항이 항상 우선합니다.', 'admin.defaultSettings.saved': '기본값이 저장되었습니다', @@ -730,14 +733,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': '애드온이 업데이트되었습니다', 'admin.addons.toast.error': '애드온 업데이트 실패', 'admin.addons.group.count': '{total}개 중 {enabled}개 활성화됨', - 'admin.addons.search': '애드온 검색', - 'admin.addons.filter': '표시', - 'admin.addons.filter.all': '전체', - 'admin.addons.filter.on': '켜짐', - 'admin.addons.filter.off': '꺼짐', - 'admin.addons.noMatch': '검색 또는 필터와 일치하는 애드온이 없습니다.', - 'admin.addons.subOptions': '옵션 {count}개', - 'admin.addons.subOptionsOn': '{count}개 켜짐', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': '사용자가 자신의 wanderer 인스턴스를 연결하고 그 트레일을 투어로 가져올 수 있습니다', 'admin.toursHikingMap.title': '하이킹 지도 (OpenHikingMap)', @@ -762,7 +757,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': '무료, API 키 불필요', 'admin.weather.locationHint': '날씨는 각 날의 좌표가 있는 첫 번째 장소를 기준으로 합니다. 날에 배정된 장소가 없으면 장소 목록의 임의 장소가 참조로 사용됩니다.', - 'admin.tabs.mcpTokens': 'MCP 접근', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP 접근', 'admin.mcpTokens.subtitle': '모든 사용자의 OAuth 세션 및 API 토큰을 관리합니다', 'admin.mcpTokens.sectionTitle': 'API 토큰', @@ -872,7 +867,6 @@ const admin: TranslationStrings = { '자신의 키를 입력하지 않은 모든 사용자에게 사용됩니다. 이렇게 하면 인스턴스 전체에서 워터마크 없는 CARTO 타일을 사용할 수 있습니다. 암호화하여 저장됩니다.', 'admin.group.users': '사용자', 'admin.group.config': '설정', - 'admin.group.integration': '통합', 'admin.group.maintenance': '유지 관리', 'admin.invite.tripLabel': '여행에 추가 (선택 사항)', 'admin.invite.tripNone': '여행 없음', diff --git a/shared/src/i18n/ko/mailAdmin.ts b/shared/src/i18n/ko/mailAdmin.ts index 393be6ed8..b258a96b3 100644 --- a/shared/src/i18n/ko/mailAdmin.ts +++ b/shared/src/i18n/ko/mailAdmin.ts @@ -37,7 +37,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '1에서 65535 사이의 포트를 입력하세요.', 'mailAdmin.problem.templateToken': '주소에는 사용자별 코드가 들어갈 자리에 {token}이 필요합니다.', 'mailAdmin.problem.dailyInvalid': '0에서 1000 사이의 정수를 입력하세요.', - 'mailAdmin.status.title': '상태', 'mailAdmin.status.lastRead': '마지막으로 읽음: {time}', 'mailAdmin.status.never': '없음', 'mailAdmin.status.mails': '메일, 24시간', diff --git a/shared/src/i18n/nl/admin.ts b/shared/src/i18n/nl/admin.ts index 6fb4aadfb..aca234b84 100644 --- a/shared/src/i18n/nl/admin.ts +++ b/shared/src/i18n/nl/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Meldingen', + 'admin.notifications.channels': 'Kanalen', + 'admin.notifications.adminAlerts': 'Beheermeldingen', 'admin.notifications.hint': 'Kies een meldingskanaal. Er kan er slechts één tegelijk actief zijn.', 'admin.notifications.none': 'Uitgeschakeld', 'admin.notifications.email': 'E-mail (SMTP)', @@ -75,6 +77,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Fout bij aanmaken van link', 'admin.invite.deleteError': 'Fout bij verwijderen van link', 'admin.tabs.settings': 'Instellingen', + 'admin.tabs.security': 'Beveiliging', + 'admin.tabs.services': 'Diensten', 'admin.allowRegistration': 'Registratie toestaan', 'admin.allowRegistrationHint': 'Nieuwe gebruikers kunnen zichzelf registreren', 'admin.authMethods': 'Authenticatiemethoden', @@ -136,10 +140,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Inbegrepen', 'admin.trekApi.notIncluded': 'Niet inbegrepen', 'admin.trekApi.quota.title': 'Tegoed vandaag', - 'admin.trekApi.quota.used': 'Gebruikt', 'admin.trekApi.quota.remaining': 'Resterend', 'admin.trekApi.quota.resets': 'Wordt gereset om', - 'admin.trekApi.quota.limit': 'Limiet', 'admin.trekApi.quota.key': 'Sleutel', 'admin.trekApi.quota.state.connected': 'Verbonden', 'admin.trekApi.quota.state.registering': 'Wordt ingericht', @@ -263,8 +265,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Wat nu', 'admin.collab.whatsnext.subtitle': 'Activiteitssuggesties en volgende stappen', 'admin.tabs.config': 'Personalisatie', - 'admin.tabs.defaults': 'Standaardinstellingen', + 'admin.tabs.defaults': 'Standaarden', 'admin.defaultSettings.title': 'Standaard gebruikersinstellingen', + 'admin.defaultSettings.routing': 'Routeplanning', 'admin.defaultSettings.description': 'Stel instantiebrede standaardwaarden in. Gebruikers die een instelling niet hebben gewijzigd, zien deze waarden. Hun eigen wijzigingen hebben altijd voorrang.', 'admin.defaultSettings.saved': 'Standaard opgeslagen', @@ -724,16 +727,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Add-on bijgewerkt', 'admin.addons.toast.error': 'Add-on bijwerken mislukt', 'admin.addons.group.count': '{enabled} van {total} ingeschakeld', - 'admin.addons.search': 'Add-ons zoeken', - 'admin.addons.filter': 'Tonen', - 'admin.addons.filter.all': 'Alle', - 'admin.addons.filter.on': 'Aan', - 'admin.addons.filter.off': 'Uit', - 'admin.addons.noMatch': 'Geen add-on past bij de zoekopdracht of het filter.', - 'admin.addons.subOptions': '{count} opties', - 'admin.addons.subOptions.one': '{count} optie', - 'admin.addons.subOptionsOn': '{count} aan', - 'admin.addons.subOptionsOn.one': '{count} aan', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Gebruikers kunnen hun eigen wanderer-instantie koppelen en de wandelroutes daarvan als tochten importeren', @@ -759,7 +752,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Gratis, geen API-sleutel vereist', 'admin.weather.locationHint': 'Het weer is gebaseerd op de eerste plaats met coördinaten op elke dag. Als er geen plaats aan een dag is toegewezen, wordt een plaats uit de lijst als referentie gebruikt.', - 'admin.tabs.mcpTokens': 'MCP-toegang', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP-toegang', 'admin.mcpTokens.subtitle': 'OAuth-sessies en API-tokens van alle gebruikers beheren', 'admin.mcpTokens.sectionTitle': 'API-tokens', @@ -931,7 +924,6 @@ const admin: TranslationStrings = { 'Wordt gebruikt voor elke gebruiker die nog geen eigen sleutel heeft ingevoerd, zo krijgt de hele instantie CARTO-tegels zonder watermerk. Versleuteld opgeslagen.', 'admin.group.users': 'Gebruikers', 'admin.group.config': 'Configuratie', - 'admin.group.integration': 'Integraties', 'admin.group.maintenance': 'Onderhoud', 'admin.invite.tripLabel': 'Toevoegen aan reis (optioneel)', 'admin.invite.tripNone': 'Geen reis', diff --git a/shared/src/i18n/nl/mailAdmin.ts b/shared/src/i18n/nl/mailAdmin.ts index f1eb52960..37830c999 100644 --- a/shared/src/i18n/nl/mailAdmin.ts +++ b/shared/src/i18n/nl/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Voer een poort in tussen 1 en 65535.', 'mailAdmin.problem.templateToken': 'Het adres heeft {token} nodig voor de code van elke persoon.', 'mailAdmin.problem.dailyInvalid': 'Voer een geheel getal van 0 tot 1000 in.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Laatst gelezen {time}', 'mailAdmin.status.never': 'nooit', 'mailAdmin.status.mails': 'Mails, 24 u', diff --git a/shared/src/i18n/pl/admin.ts b/shared/src/i18n/pl/admin.ts index fbbd0b5f7..3b6bdcf3f 100644 --- a/shared/src/i18n/pl/admin.ts +++ b/shared/src/i18n/pl/admin.ts @@ -73,6 +73,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Nie udało się utworzyć linku zaproszenia', 'admin.invite.deleteError': 'Nie udało się usunąć linku zaproszenia', 'admin.tabs.settings': 'Ustawienia', + 'admin.tabs.security': 'Bezpieczeństwo', + 'admin.tabs.services': 'Usługi', 'admin.allowRegistration': 'Zezwól na rejestrację', 'admin.allowRegistrationHint': 'Nowi użytkownicy mogą się rejestrować samodzielnie', 'admin.authMethods': 'Metody uwierzytelniania', @@ -134,10 +136,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Zawiera', 'admin.trekApi.notIncluded': 'Nie zawiera', 'admin.trekApi.quota.title': 'Dzisiejszy limit', - 'admin.trekApi.quota.used': 'Wykorzystano', 'admin.trekApi.quota.remaining': 'Pozostało', 'admin.trekApi.quota.resets': 'Reset o', - 'admin.trekApi.quota.limit': 'Limit dzienny', 'admin.trekApi.quota.key': 'Klucz', 'admin.trekApi.quota.state.connected': 'Połączono', 'admin.trekApi.quota.state.registering': 'Konfigurowanie', @@ -263,8 +263,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Co dalej', 'admin.collab.whatsnext.subtitle': 'Sugestie aktywności i następne kroki', 'admin.tabs.config': 'Personalizacja', - 'admin.tabs.defaults': 'Domyślne ustawienia', + 'admin.tabs.defaults': 'Domyślne', 'admin.defaultSettings.title': 'Domyślne ustawienia użytkownika', + 'admin.defaultSettings.routing': 'Wyznaczanie tras', 'admin.defaultSettings.description': 'Ustaw domyślne wartości dla całej instancji. Użytkownicy, którzy nie zmienili ustawienia, zobaczą te wartości. Ich własne zmiany zawsze mają pierwszeństwo.', 'admin.defaultSettings.saved': 'Domyślne zapisane', @@ -740,20 +741,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Dodatek został zaktualizowany', 'admin.addons.toast.error': 'Nie udało się zaktualizować dodatku', 'admin.addons.group.count': '{enabled} z {total} włączonych', - 'admin.addons.search': 'Szukaj dodatków', - 'admin.addons.filter': 'Pokaż', - 'admin.addons.filter.all': 'Wszystkie', - 'admin.addons.filter.on': 'Włączone', - 'admin.addons.filter.off': 'Wyłączone', - 'admin.addons.noMatch': 'Żaden dodatek nie pasuje do wyszukiwania ani filtra.', - 'admin.addons.subOptions': '{count} opcji', - 'admin.addons.subOptions.one': '{count} opcja', - 'admin.addons.subOptions.few': '{count} opcje', - 'admin.addons.subOptions.many': '{count} opcji', - 'admin.addons.subOptionsOn': 'włączone: {count}', - 'admin.addons.subOptionsOn.one': 'włączone: {count}', - 'admin.addons.subOptionsOn.few': 'włączone: {count}', - 'admin.addons.subOptionsOn.many': 'włączone: {count}', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Użytkownicy mogą połączyć własną instancję wanderer i importować z niej szlaki jako wycieczki', @@ -779,7 +766,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Bezpłatnie, bez klucza API', 'admin.weather.locationHint': 'Pogoda jest określana na podstawie pierwszego miejsca z przypisanymi współrzędnymi w danym dniu. Jeśli do dnia nie przypisano żadnego miejsca, jako punkt odniesienia używane jest dowolne miejsce z listy.', - 'admin.tabs.mcpTokens': 'Dostęp MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'Dostęp MCP', 'admin.mcpTokens.subtitle': 'Zarządzaj sesjami OAuth i tokenami API dla wszystkich użytkowników', 'admin.mcpTokens.sectionTitle': 'Tokeny API', @@ -857,6 +844,8 @@ const admin: TranslationStrings = { 'admin.update.wikiLink': 'Otwórz przewodnik aktualizacji', 'admin.update.reloadHint': 'Proszę odświeżyć stronę za kilka sekund.', 'admin.notifications.title': 'Powiadomienia', + 'admin.notifications.channels': 'Kanały', + 'admin.notifications.adminAlerts': 'Alerty administratora', 'admin.notifications.hint': 'Wybierz jeden kanał powiadomień.', 'admin.notifications.none': 'Wyłączone', 'admin.notifications.email': 'E-mail (SMTP)', @@ -966,7 +955,6 @@ const admin: TranslationStrings = { 'Używany dla każdego użytkownika, który nie wprowadził własnego klucza, dzięki czemu cała instancja otrzymuje kafelki CARTO bez znaku wodnego. Przechowywany w postaci zaszyfrowanej.', 'admin.group.users': 'Użytkownicy', 'admin.group.config': 'Konfiguracja', - 'admin.group.integration': 'Integracje', 'admin.group.maintenance': 'Konserwacja', 'admin.invite.tripLabel': 'Dodaj do podróży (opcjonalnie)', 'admin.invite.tripNone': 'Brak podróży', diff --git a/shared/src/i18n/pl/mailAdmin.ts b/shared/src/i18n/pl/mailAdmin.ts index 10316acd8..7a9a21d4a 100644 --- a/shared/src/i18n/pl/mailAdmin.ts +++ b/shared/src/i18n/pl/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Wpisz port od 1 do 65535.', 'mailAdmin.problem.templateToken': 'Adres musi zawierać {token} dla kodu każdego użytkownika.', 'mailAdmin.problem.dailyInvalid': 'Wpisz liczbę całkowitą od 0 do 1000.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Ostatni odczyt {time}', 'mailAdmin.status.never': 'nigdy', 'mailAdmin.status.mails': 'Wiadomości, 24 h', diff --git a/shared/src/i18n/ru/admin.ts b/shared/src/i18n/ru/admin.ts index 74766e1a9..519bfb541 100644 --- a/shared/src/i18n/ru/admin.ts +++ b/shared/src/i18n/ru/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Уведомления', + 'admin.notifications.channels': 'Каналы', + 'admin.notifications.adminAlerts': 'Оповещения администратора', 'admin.notifications.hint': 'Выберите канал уведомлений. Одновременно может быть активен только один.', 'admin.notifications.none': 'Отключено', 'admin.notifications.email': 'Эл. почта (SMTP)', @@ -83,6 +85,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Ошибка при создании ссылки', 'admin.invite.deleteError': 'Ошибка при удалении ссылки', 'admin.tabs.settings': 'Настройки', + 'admin.tabs.security': 'Безопасность', + 'admin.tabs.services': 'Сервисы', 'admin.allowRegistration': 'Разрешить регистрацию', 'admin.allowRegistrationHint': 'Новые пользователи могут регистрироваться самостоятельно', 'admin.authMethods': 'Способы входа', @@ -144,10 +148,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Входит', 'admin.trekApi.notIncluded': 'Не входит', 'admin.trekApi.quota.title': 'Лимит на сегодня', - 'admin.trekApi.quota.used': 'Использовано', 'admin.trekApi.quota.remaining': 'Осталось', 'admin.trekApi.quota.resets': 'Сброс в', - 'admin.trekApi.quota.limit': 'Лимит', 'admin.trekApi.quota.key': 'Ключ', 'admin.trekApi.quota.state.connected': 'Подключено', 'admin.trekApi.quota.state.registering': 'Настройка', @@ -270,8 +272,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Что дальше', 'admin.collab.whatsnext.subtitle': 'Предложения активностей и следующие шаги', 'admin.tabs.config': 'Персонализация', - 'admin.tabs.defaults': 'Настройки по умолчанию', + 'admin.tabs.defaults': 'Умолчания', 'admin.defaultSettings.title': 'Настройки пользователей по умолчанию', + 'admin.defaultSettings.routing': 'Маршруты', 'admin.defaultSettings.description': 'Задайте значения по умолчанию для всего экземпляра. Пользователи, не изменившие параметр, увидят эти значения. Их собственные изменения всегда имеют приоритет.', 'admin.defaultSettings.saved': 'Значение по умолчанию сохранено', @@ -745,20 +748,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Дополнение обновлено', 'admin.addons.toast.error': 'Не удалось обновить дополнение', 'admin.addons.group.count': 'Включено {enabled} из {total}', - 'admin.addons.search': 'Поиск дополнений', - 'admin.addons.filter': 'Показать', - 'admin.addons.filter.all': 'Все', - 'admin.addons.filter.on': 'Включённые', - 'admin.addons.filter.off': 'Выключенные', - 'admin.addons.noMatch': 'Ни одно дополнение не подходит под поиск или фильтр.', - 'admin.addons.subOptions': '{count} параметра', - 'admin.addons.subOptions.one': '{count} параметр', - 'admin.addons.subOptions.few': '{count} параметра', - 'admin.addons.subOptions.many': '{count} параметров', - 'admin.addons.subOptionsOn': '{count} вкл.', - 'admin.addons.subOptionsOn.one': '{count} вкл.', - 'admin.addons.subOptionsOn.few': '{count} вкл.', - 'admin.addons.subOptionsOn.many': '{count} вкл.', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Пользователи могут подключить свой экземпляр wanderer и импортировать его маршруты как туры', @@ -784,7 +773,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Бесплатно, API-ключ не требуется', 'admin.weather.locationHint': 'Погода основана на первом месте с координатами в каждом дне. Если ни одно место не назначено на день, в качестве ориентира используется любое место из списка.', - 'admin.tabs.mcpTokens': 'MCP-доступ', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP-доступ', 'admin.mcpTokens.subtitle': 'Управление OAuth-сессиями и API-токенами всех пользователей', 'admin.mcpTokens.sectionTitle': 'API-токены', @@ -959,7 +948,6 @@ const admin: TranslationStrings = { 'Используется для каждого пользователя, который не ввёл собственный ключ, так весь сервер получает тайлы CARTO без водяного знака. Хранится в зашифрованном виде.', 'admin.group.users': 'Пользователи', 'admin.group.config': 'Конфигурация', - 'admin.group.integration': 'Интеграции', 'admin.group.maintenance': 'Обслуживание', 'admin.invite.tripLabel': 'Добавить в поездку (необязательно)', 'admin.invite.tripNone': 'Без поездки', diff --git a/shared/src/i18n/ru/mailAdmin.ts b/shared/src/i18n/ru/mailAdmin.ts index d612d0545..27a6fd767 100644 --- a/shared/src/i18n/ru/mailAdmin.ts +++ b/shared/src/i18n/ru/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Введите порт от 1 до 65535.', 'mailAdmin.problem.templateToken': 'Адрес должен содержать {token} для кода каждого пользователя.', 'mailAdmin.problem.dailyInvalid': 'Введите целое число от 0 до 1000.', - 'mailAdmin.status.title': 'Статус', 'mailAdmin.status.lastRead': 'Последнее чтение {time}', 'mailAdmin.status.never': 'никогда', 'mailAdmin.status.mails': 'Письма, 24 ч', diff --git a/shared/src/i18n/sk/admin.ts b/shared/src/i18n/sk/admin.ts index 0861abfee..d8b8f12eb 100644 --- a/shared/src/i18n/sk/admin.ts +++ b/shared/src/i18n/sk/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Oznámenia', + 'admin.notifications.channels': 'Kanály', + 'admin.notifications.adminAlerts': 'Upozornenia pre správcu', 'admin.notifications.hint': 'Vyberte jeden kanál oznámení. Súčasne môže byť aktívny len jeden.', 'admin.notifications.none': 'Vypnuté', 'admin.notifications.email': 'E-mail (SMTP)', @@ -130,6 +132,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Nepodarilo sa vytvoriť pozývací odkaz', 'admin.invite.deleteError': 'Nepodarilo sa odstrániť pozývací odkaz', 'admin.tabs.settings': 'Nastavenia', + 'admin.tabs.security': 'Zabezpečenie', + 'admin.tabs.services': 'Služby', 'admin.allowRegistration': 'Povoliť registráciu', 'admin.allowRegistrationHint': 'Noví používatelia sa môžu sami registrovať', 'admin.authMethods': 'Metódy overenia', @@ -189,10 +193,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Obsahuje', 'admin.trekApi.notIncluded': 'Neobsahuje', 'admin.trekApi.quota.title': 'Dnešný limit', - 'admin.trekApi.quota.used': 'Využité', 'admin.trekApi.quota.remaining': 'Zostáva', 'admin.trekApi.quota.resets': 'Obnoví sa o', - 'admin.trekApi.quota.limit': 'Denný limit', 'admin.trekApi.quota.key': 'Kľúč', 'admin.trekApi.quota.state.connected': 'Pripojené', 'admin.trekApi.quota.state.registering': 'Nastavuje sa', @@ -314,8 +316,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Čo ďalej', 'admin.collab.whatsnext.subtitle': 'Návrhy aktivít a ďalšie kroky', 'admin.tabs.config': 'Personalizácia', - 'admin.tabs.defaults': 'Predvolené nastavenia používateľa', + 'admin.tabs.defaults': 'Predvolené', 'admin.defaultSettings.title': 'Predvolené nastavenia používateľa', + 'admin.defaultSettings.routing': 'Trasy', 'admin.defaultSettings.description': 'Nastavte predvolené hodnoty pre celú inštanciu. Používatelia, ktorí nastavenie nezmenili, uvidia tieto hodnoty. Ich vlastné zmeny majú vždy prednosť.', 'admin.defaultSettings.saved': 'Predvolené nastavenie uložené', @@ -794,18 +797,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Doplnok aktualizovaný', 'admin.addons.toast.error': 'Aktualizácia doplnku zlyhala', 'admin.addons.group.count': '{enabled} z {total} zapnuté', - 'admin.addons.search': 'Hľadať doplnky', - 'admin.addons.filter': 'Zobraziť', - 'admin.addons.filter.all': 'Všetky', - 'admin.addons.filter.on': 'Zapnuté', - 'admin.addons.filter.off': 'Vypnuté', - 'admin.addons.noMatch': 'Vyhľadávaniu ani filtru nezodpovedá žiadny doplnok.', - 'admin.addons.subOptions': '{count} možností', - 'admin.addons.subOptions.one': '{count} možnosť', - 'admin.addons.subOptions.few': '{count} možnosti', - 'admin.addons.subOptionsOn': 'zapnuté: {count}', - 'admin.addons.subOptionsOn.one': 'zapnuté: {count}', - 'admin.addons.subOptionsOn.few': 'zapnuté: {count}', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Používatelia môžu pripojiť vlastnú inštanciu wanderer a importovať z nej trasy ako túry', @@ -831,7 +822,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Zdarma, bez nutnosti API kľúča', 'admin.weather.locationHint': 'Počasie sa určuje podľa prvého miesta so súradnicami v danom dni. Ak k dňu nie je priradené žiadne miesto, ako referencia sa použije ľubovoľné miesto zo zoznamu miest.', - 'admin.tabs.mcpTokens': 'MCP prístup', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP prístup', 'admin.mcpTokens.subtitle': 'Spravujte OAuth relácie a API tokeny naprieč všetkými používateľmi', 'admin.mcpTokens.sectionTitle': 'API tokeny', @@ -936,7 +927,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.few': 'Odstránené {count} prístupové kľúče', 'admin.group.users': 'Používatelia', 'admin.group.config': 'Konfigurácia', - 'admin.group.integration': 'Integrácie', 'admin.group.maintenance': 'Údržba', 'admin.invite.tripLabel': 'Pridať k ceste (voliteľné)', 'admin.invite.tripNone': 'Žiadna cesta', diff --git a/shared/src/i18n/sk/mailAdmin.ts b/shared/src/i18n/sk/mailAdmin.ts index de032d756..0a028a9ea 100644 --- a/shared/src/i18n/sk/mailAdmin.ts +++ b/shared/src/i18n/sk/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Zadaj port medzi 1 a 65535.', 'mailAdmin.problem.templateToken': 'Adresa musí obsahovať {token} pre kód každého používateľa.', 'mailAdmin.problem.dailyInvalid': 'Zadaj celé číslo od 0 do 1000.', - 'mailAdmin.status.title': 'Stav', 'mailAdmin.status.lastRead': 'Naposledy prečítané {time}', 'mailAdmin.status.never': 'nikdy', 'mailAdmin.status.mails': 'E-maily, 24 h', diff --git a/shared/src/i18n/sv/admin.ts b/shared/src/i18n/sv/admin.ts index fcd39dcaa..fd5042852 100644 --- a/shared/src/i18n/sv/admin.ts +++ b/shared/src/i18n/sv/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Meddelanden', + 'admin.notifications.channels': 'Kanaler', + 'admin.notifications.adminAlerts': 'Adminaviseringar', 'admin.notifications.hint': 'Välj en meddelandekanal. Endast en kan vara aktiv åt gången.', 'admin.notifications.none': 'Avaktiverad', 'admin.notifications.email': 'E-post (SMTP)', @@ -127,6 +129,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Det gick inte att skapa en inbjudningslänk', 'admin.invite.deleteError': 'Det gick inte att ta bort inbjudningslänken', 'admin.tabs.settings': 'Inställningar', + 'admin.tabs.security': 'Säkerhet', + 'admin.tabs.services': 'Tjänster', 'admin.allowRegistration': 'Tillåt registrering', 'admin.allowRegistrationHint': 'Nya användare kan registrera sig själva', 'admin.authMethods': 'Autentiseringsmetoder', @@ -188,10 +192,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Ingår', 'admin.trekApi.notIncluded': 'Ingår inte', 'admin.trekApi.quota.title': 'Dagens kvot', - 'admin.trekApi.quota.used': 'Använt', 'admin.trekApi.quota.remaining': 'Kvar', 'admin.trekApi.quota.resets': 'Nollställs kl.', - 'admin.trekApi.quota.limit': 'Gräns', 'admin.trekApi.quota.key': 'Nyckel', 'admin.trekApi.quota.state.connected': 'Ansluten', 'admin.trekApi.quota.state.registering': 'Konfigureras', @@ -316,8 +318,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Vad händer härnäst?', 'admin.collab.whatsnext.subtitle': 'Förslag på aktiviteter och nästa steg', 'admin.tabs.config': 'Anpassning', - 'admin.tabs.defaults': 'Användarinställningar', + 'admin.tabs.defaults': 'Standardvärden', 'admin.defaultSettings.title': 'Standardinställningar för användare', + 'admin.defaultSettings.routing': 'Ruttplanering', 'admin.defaultSettings.description': 'Ange standardvärden som gäller för hela instansen. Användare som inte har ändrat en inställning kommer att se dessa värden. Deras egna ändringar har alltid företräde.', 'admin.defaultSettings.saved': 'Standard sparad', @@ -792,16 +795,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Tillägget har uppdaterats', 'admin.addons.toast.error': 'Det gick inte att uppdatera tillägget', 'admin.addons.group.count': '{enabled} av {total} aktiverade', - 'admin.addons.search': 'Sök tillägg', - 'admin.addons.filter': 'Visa', - 'admin.addons.filter.all': 'Alla', - 'admin.addons.filter.on': 'På', - 'admin.addons.filter.off': 'Av', - 'admin.addons.noMatch': 'Inget tillägg matchar sökningen eller filtret.', - 'admin.addons.subOptions': '{count} alternativ', - 'admin.addons.subOptions.one': '{count} alternativ', - 'admin.addons.subOptionsOn': '{count} aktiva', - 'admin.addons.subOptionsOn.one': '{count} aktiv', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Användare kan ansluta sin egen wanderer-instans och importera dess leder som turer', 'admin.toursHikingMap.title': 'Vandringskarta (OpenHikingMap)', @@ -826,7 +819,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Gratis, ingen API-nyckel krävs', 'admin.weather.locationHint': 'Väderuppgifterna baseras på den plats med koordinater som anges först för varje dag. Om ingen plats har angetts för en dag används en valfri plats från platslistan som referens.', - 'admin.tabs.mcpTokens': 'MCP Åtkomst', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP Åtkomst', 'admin.mcpTokens.subtitle': 'Hantera OAuth-sessioner och API-token för alla användare', 'admin.mcpTokens.sectionTitle': 'API Token', @@ -928,7 +921,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone.one': 'Tog bort {count} inloggningsnyckel', 'admin.group.users': 'Användare', 'admin.group.config': 'Konfiguration', - 'admin.group.integration': 'Integrationer', 'admin.group.maintenance': 'Underhåll', 'admin.invite.tripLabel': 'Lägg till i resa (valfritt)', 'admin.invite.tripNone': 'Ingen resa', diff --git a/shared/src/i18n/sv/mailAdmin.ts b/shared/src/i18n/sv/mailAdmin.ts index 3dc024698..ae33381b0 100644 --- a/shared/src/i18n/sv/mailAdmin.ts +++ b/shared/src/i18n/sv/mailAdmin.ts @@ -38,7 +38,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Ange en port mellan 1 och 65535.', 'mailAdmin.problem.templateToken': 'Adressen behöver {token} för varje persons kod.', 'mailAdmin.problem.dailyInvalid': 'Ange ett heltal från 0 till 1000.', - 'mailAdmin.status.title': 'Status', // same-as-en 'mailAdmin.status.lastRead': 'Senast läst {time}', 'mailAdmin.status.never': 'aldrig', 'mailAdmin.status.mails': 'Mejl, 24 h', diff --git a/shared/src/i18n/th/admin.ts b/shared/src/i18n/th/admin.ts index 0924c09ea..ba25e7054 100644 --- a/shared/src/i18n/th/admin.ts +++ b/shared/src/i18n/th/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'การแจ้งเตือน', + 'admin.notifications.channels': 'ช่องทาง', + 'admin.notifications.adminAlerts': 'การแจ้งเตือนผู้ดูแล', 'admin.notifications.hint': 'เลือกช่องทางการแจ้งเตือนหนึ่งช่องทาง สามารถใช้งานได้ครั้งละหนึ่งรายการเท่านั้น', 'admin.notifications.none': 'ปิดการใช้งาน', 'admin.notifications.email': 'อีเมล์ (SMTP)', @@ -122,6 +124,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'ไม่สามารถสร้างลิงก์คำเชิญได้', 'admin.invite.deleteError': 'ลบลิงก์คำเชิญไม่สำเร็จ', 'admin.tabs.settings': 'การตั้งค่า', + 'admin.tabs.security': 'ความปลอดภัย', + 'admin.tabs.services': 'บริการ', 'admin.allowRegistration': 'อนุญาตให้ลงทะเบียน', 'admin.allowRegistrationHint': 'ผู้ใช้ใหม่สามารถลงทะเบียนด้วยตนเองได้', 'admin.authMethods': 'วิธีการรับรองความถูกต้อง', @@ -206,8 +210,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'อะไรต่อไป', 'admin.collab.whatsnext.subtitle': 'ข้อเสนอแนะกิจกรรมและขั้นตอนต่อไป', 'admin.tabs.config': 'การปรับเปลี่ยนในแบบของคุณ', - 'admin.tabs.defaults': 'ค่าเริ่มต้นของผู้ใช้', + 'admin.tabs.defaults': 'ค่าเริ่มต้น', 'admin.defaultSettings.title': 'การตั้งค่าผู้ใช้เริ่มต้น', + 'admin.defaultSettings.routing': 'การหาเส้นทาง', 'admin.defaultSettings.description': 'ตั้งค่าเริ่มต้นทั่วทั้งอินสแตนซ์ ผู้ใช้ที่ไม่ได้เปลี่ยนการตั้งค่าจะเห็นค่าเหล่านี้ การเปลี่ยนแปลงของพวกเขาเองมีความสำคัญเสมอ', 'admin.defaultSettings.saved': 'บันทึกค่าเริ่มต้นแล้ว', @@ -644,14 +649,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'อัพเดตแอดออนแล้ว', 'admin.addons.toast.error': 'ไม่สามารถอัปเดต addon', 'admin.addons.group.count': 'เปิดใช้งาน {enabled} จาก {total} แล้ว', - 'admin.addons.search': 'ค้นหาส่วนเสริม', - 'admin.addons.filter': 'แสดง', - 'admin.addons.filter.all': 'ทั้งหมด', - 'admin.addons.filter.on': 'เปิด', - 'admin.addons.filter.off': 'ปิด', - 'admin.addons.noMatch': 'ไม่มีส่วนเสริมที่ตรงกับการค้นหาหรือตัวกรอง', - 'admin.addons.subOptions': '{count} ตัวเลือก', - 'admin.addons.subOptionsOn': 'เปิดอยู่ {count}', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'ผู้ใช้เชื่อมต่ออินสแตนซ์ wanderer ของตนเองและนำเข้าเส้นทางเดินจากที่นั่นเป็นทัวร์ได้', @@ -677,7 +674,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'ฟรี ไม่ต้องใช้คีย์ API', 'admin.weather.locationHint': 'สภาพอากาศจะขึ้นอยู่กับสถานที่แรกพร้อมพิกัดในแต่ละวัน หากไม่มีการกำหนดสถานที่ในหนึ่งวัน สถานที่ใดๆ จากรายการสถานที่จะถูกนำมาใช้เป็นข้อมูลอ้างอิง', - 'admin.tabs.mcpTokens': 'การเข้าถึง MCP', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'การเข้าถึง MCP', 'admin.mcpTokens.subtitle': 'จัดการเซสชัน OAuth และโทเค็น API ให้กับผู้ใช้ทั้งหมด', 'admin.mcpTokens.sectionTitle': 'โทเค็น API', @@ -774,7 +771,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone': 'ลบพาสคีย์ {count} รายการแล้ว', 'admin.group.users': 'ผู้ใช้', 'admin.group.config': 'การกำหนดค่า', - 'admin.group.integration': 'การเชื่อมต่อ', 'admin.group.maintenance': 'การบำรุงรักษา', 'admin.invite.tripLabel': 'เพิ่มในการเดินทาง (ไม่บังคับ)', 'admin.invite.tripNone': 'ไม่มีการเดินทาง', @@ -817,10 +813,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'รวมอยู่', 'admin.trekApi.notIncluded': 'ไม่รวมอยู่', 'admin.trekApi.quota.title': 'โควตาวันนี้', - 'admin.trekApi.quota.used': 'ใช้ไปแล้ว', 'admin.trekApi.quota.remaining': 'คงเหลือ', 'admin.trekApi.quota.resets': 'รีเซ็ตเวลา', - 'admin.trekApi.quota.limit': 'ขีดจำกัด', 'admin.trekApi.quota.key': 'คีย์', 'admin.trekApi.quota.state.connected': 'เชื่อมต่อแล้ว', 'admin.trekApi.quota.state.registering': 'กำลังตั้งค่า', diff --git a/shared/src/i18n/th/mailAdmin.ts b/shared/src/i18n/th/mailAdmin.ts index d3c6e2327..fad054360 100644 --- a/shared/src/i18n/th/mailAdmin.ts +++ b/shared/src/i18n/th/mailAdmin.ts @@ -38,7 +38,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'กรอกพอร์ตระหว่าง 1 ถึง 65535', 'mailAdmin.problem.templateToken': 'ที่อยู่ต้องมี {token} ตรงตำแหน่งรหัสของแต่ละคน', 'mailAdmin.problem.dailyInvalid': 'กรอกจำนวนเต็มตั้งแต่ 0 ถึง 1000', - 'mailAdmin.status.title': 'สถานะ', 'mailAdmin.status.lastRead': 'อ่านล่าสุด {time}', 'mailAdmin.status.never': 'ไม่เคย', 'mailAdmin.status.mails': 'อีเมล 24 ชม.', diff --git a/shared/src/i18n/tr/admin.ts b/shared/src/i18n/tr/admin.ts index e30379919..65168ff5b 100644 --- a/shared/src/i18n/tr/admin.ts +++ b/shared/src/i18n/tr/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Bildirimler', + 'admin.notifications.channels': 'Kanallar', + 'admin.notifications.adminAlerts': 'Yönetici uyarıları', 'admin.notifications.hint': 'Bir bildirim kanalı seçin. Aynı anda yalnızca biri etkin olabilir.', 'admin.notifications.none': 'Kapalı', 'admin.notifications.email': 'E-posta (SMTP)', @@ -127,6 +129,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Davet bağlantısı oluşturulamadı', 'admin.invite.deleteError': 'Davet bağlantısı silinemedi', 'admin.tabs.settings': 'Ayarlar', + 'admin.tabs.security': 'Güvenlik', + 'admin.tabs.services': 'Hizmetler', 'admin.allowRegistration': 'Kayda İzin Ver', 'admin.allowRegistrationHint': 'Yeni kullanıcılar kendileri kayıt olabilirler', 'admin.authMethods': 'Kimlik Doğrulama Yöntemleri', @@ -188,10 +192,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Dahil', 'admin.trekApi.notIncluded': 'Dahil değil', 'admin.trekApi.quota.title': 'Bugünkü kota', - 'admin.trekApi.quota.used': 'Kullanılan', 'admin.trekApi.quota.remaining': 'Kalan', 'admin.trekApi.quota.resets': 'Sıfırlanma saati', - 'admin.trekApi.quota.limit': 'Sınır', 'admin.trekApi.quota.key': 'Anahtar', 'admin.trekApi.quota.state.connected': 'Bağlı', 'admin.trekApi.quota.state.registering': 'Kuruluyor', @@ -316,8 +318,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Sırada ne var', 'admin.collab.whatsnext.subtitle': 'Etkinlik önerileri ve sonraki adımlar', 'admin.tabs.config': 'Kişiselleştirme', - 'admin.tabs.defaults': 'Kullanıcı Varsayılanları', + 'admin.tabs.defaults': 'Varsayılanlar', 'admin.defaultSettings.title': 'Varsayılan Kullanıcı Ayarları', + 'admin.defaultSettings.routing': 'Rotalar', 'admin.defaultSettings.description': 'Örnek çapında varsayılanları ayarlayın. Herhangi bir ayarı değiştirmemiş olan kullanıcılar bu değerleri görecektir. Kendi değişiklikleri her zaman önceliklidir.', 'admin.defaultSettings.saved': 'Varsayılan kaydedildi', @@ -780,16 +783,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Eklenti güncellendi', 'admin.addons.toast.error': 'Eklenti güncellenemedi', 'admin.addons.group.count': '{total} eklentiden {enabled} tanesi etkin', - 'admin.addons.search': 'Eklentilerde ara', - 'admin.addons.filter': 'Göster', - 'admin.addons.filter.all': 'Tümü', - 'admin.addons.filter.on': 'Açık', - 'admin.addons.filter.off': 'Kapalı', - 'admin.addons.noMatch': 'Aramaya veya filtreye uyan eklenti yok.', - 'admin.addons.subOptions': '{count} seçenek', - 'admin.addons.subOptions.one': '{count} seçenek', - 'admin.addons.subOptionsOn': '{count} açık', - 'admin.addons.subOptionsOn.one': '{count} açık', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Kullanıcılar kendi wanderer sunucularını bağlayıp parkurlarını tur olarak içe aktarabilir', @@ -815,7 +808,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Ücretsiz, API anahtarı gerekmez', 'admin.weather.locationHint': 'Hava durumu her gün koordinatlarla ilk sırada yer alır. Bir güne herhangi bir yer atanmamışsa, yer listesindeki herhangi bir yer referans olarak kullanılır.', - 'admin.tabs.mcpTokens': 'MCP Erişimi', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP Erişimi', 'admin.mcpTokens.subtitle': 'Tüm Kullanıcılar genelinde OAuth Oturumlarını ve API belirteçlerini yönetin', 'admin.mcpTokens.sectionTitle': 'API Belirteçleri', @@ -935,7 +928,6 @@ const admin: TranslationStrings = { 'Kendi anahtarını girmemiş her kullanıcı için kullanılır, böylece tüm örnek filigransız CARTO döşemeleri alır. Şifrelenmiş olarak saklanır.', 'admin.group.users': 'Kullanıcılar', 'admin.group.config': 'Yapılandırma', - 'admin.group.integration': 'Entegrasyonlar', 'admin.group.maintenance': 'Bakım', 'admin.invite.tripLabel': 'Seyahate ekle (isteğe bağlı)', 'admin.invite.tripNone': 'Seyahat yok', diff --git a/shared/src/i18n/tr/mailAdmin.ts b/shared/src/i18n/tr/mailAdmin.ts index 8c85fad0e..1fb488abb 100644 --- a/shared/src/i18n/tr/mailAdmin.ts +++ b/shared/src/i18n/tr/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '1 ile 65535 arasında bir port gir.', 'mailAdmin.problem.templateToken': 'Adreste her kişinin kodu için {token} bulunmalı.', 'mailAdmin.problem.dailyInvalid': '0 ile 1000 arasında bir tam sayı gir.', - 'mailAdmin.status.title': 'Durum', 'mailAdmin.status.lastRead': 'Son okuma {time}', 'mailAdmin.status.never': 'hiç', 'mailAdmin.status.mails': 'E-postalar, 24 sa', diff --git a/shared/src/i18n/uk/admin.ts b/shared/src/i18n/uk/admin.ts index 940298c54..f4b317b6b 100644 --- a/shared/src/i18n/uk/admin.ts +++ b/shared/src/i18n/uk/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'n', + 'admin.notifications.channels': 'Канали', + 'admin.notifications.adminAlerts': 'Сповіщення адміністратора', 'admin.notifications.hint': 'Виберіть канал сповіщень. Одночасно може бути активним лише один.', 'admin.notifications.none': 'Вимкнено', 'admin.notifications.email': 'Ел. пошта (SMTP)', @@ -83,6 +85,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Помилка створення посилання', 'admin.invite.deleteError': 'Помилка видалення посилання', 'admin.tabs.settings': 'Налаштування', + 'admin.tabs.security': 'Безпека', + 'admin.tabs.services': 'Сервіси', 'admin.allowRegistration': 'Дозволити реєстрацію', 'admin.allowRegistrationHint': 'Нові користувачі можуть реєструватися самостійно', 'admin.authMethods': 'Методи автентифікації', @@ -144,10 +148,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Входить', 'admin.trekApi.notIncluded': 'Не входить', 'admin.trekApi.quota.title': 'Ліміт на сьогодні', - 'admin.trekApi.quota.used': 'Використано', 'admin.trekApi.quota.remaining': 'Залишилось', 'admin.trekApi.quota.resets': 'Скидання о', - 'admin.trekApi.quota.limit': 'Ліміт', 'admin.trekApi.quota.key': 'Ключ', 'admin.trekApi.quota.state.connected': 'Підключено', 'admin.trekApi.quota.state.registering': 'Налаштування', @@ -270,8 +272,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Що далі', 'admin.collab.whatsnext.subtitle': 'Пропозиції активностей та наступні кроки', 'admin.tabs.config': 'Персоналізація', - 'admin.tabs.defaults': 'Налаштування за замовчуванням', + 'admin.tabs.defaults': 'Типові', 'admin.defaultSettings.title': 'Налаштування користувачів за замовчуванням', + 'admin.defaultSettings.routing': 'Маршрути', 'admin.defaultSettings.description': 'Визначте значення за замовчуванням для всієї інстанції. Користувачі, які не змінювали параметр, побачать ці значення. Їхні власні зміни завжди мають пріоритет.', 'admin.defaultSettings.saved': 'Значення за замовчуванням збережено', @@ -741,20 +744,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Доповнення оновлено', 'admin.addons.toast.error': 'Не вдалося оновити доповнення', 'admin.addons.group.count': 'Увімкнено {enabled} з {total}', - 'admin.addons.search': 'Пошук доповнень', - 'admin.addons.filter': 'Показати', - 'admin.addons.filter.all': 'Усі', - 'admin.addons.filter.on': 'Увімкнені', - 'admin.addons.filter.off': 'Вимкнені', - 'admin.addons.noMatch': 'Жодне доповнення не відповідає пошуку чи фільтру.', - 'admin.addons.subOptions': '{count} параметра', - 'admin.addons.subOptions.one': '{count} параметр', - 'admin.addons.subOptions.few': '{count} параметри', - 'admin.addons.subOptions.many': '{count} параметрів', - 'admin.addons.subOptionsOn': '{count} увімк.', - 'admin.addons.subOptionsOn.one': '{count} увімк.', - 'admin.addons.subOptionsOn.few': '{count} увімк.', - 'admin.addons.subOptionsOn.many': '{count} увімк.', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Користувачі можуть підключити власний екземпляр wanderer та імпортувати його маршрути як тури', @@ -780,7 +769,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Безкоштовно, API-ключ не потрібен', 'admin.weather.locationHint': 'Погода базується на першому місці з координатами в кожному дні. Якщо жодне місце не призначено на день, як орієнтир використовується будь-яке місце зі списку.', - 'admin.tabs.mcpTokens': 'MCP-доступ', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP-доступ', 'admin.mcpTokens.subtitle': 'Управління OAuth-сеансами та API-токенами всіх користувачів', 'admin.mcpTokens.sectionTitle': 'API-токени', @@ -955,7 +944,6 @@ const admin: TranslationStrings = { 'Використовується для кожного користувача, який не ввів власний ключ, щоб увесь екземпляр отримував плитки CARTO без водяного знака. Зберігається в зашифрованому вигляді.', 'admin.group.users': 'Користувачі', 'admin.group.config': 'Налаштування', - 'admin.group.integration': 'Інтеграції', 'admin.group.maintenance': 'Обслуговування', 'admin.invite.tripLabel': 'Додати до подорожі (необов’язково)', 'admin.invite.tripNone': 'Без подорожі', diff --git a/shared/src/i18n/uk/mailAdmin.ts b/shared/src/i18n/uk/mailAdmin.ts index 2b9da846c..4fb550db5 100644 --- a/shared/src/i18n/uk/mailAdmin.ts +++ b/shared/src/i18n/uk/mailAdmin.ts @@ -39,7 +39,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Введіть порт від 1 до 65535.', 'mailAdmin.problem.templateToken': 'Адреса має містити {token} для коду кожного користувача.', 'mailAdmin.problem.dailyInvalid': 'Введіть ціле число від 0 до 1000.', - 'mailAdmin.status.title': 'Статус', 'mailAdmin.status.lastRead': 'Останнє читання {time}', 'mailAdmin.status.never': 'ніколи', 'mailAdmin.status.mails': 'Листи, 24 год', diff --git a/shared/src/i18n/uk/settings.ts b/shared/src/i18n/uk/settings.ts index 276bc6833..626f3bfb0 100644 --- a/shared/src/i18n/uk/settings.ts +++ b/shared/src/i18n/uk/settings.ts @@ -18,7 +18,7 @@ const settings: TranslationStrings = { 'settings.plugins.empty': 'Немає активних плагінів.', 'settings.plugins.saved': 'Налаштування збережено', 'settings.plugins.requiredMissing': '«{field}» обов’язкове', - 'settings.tabs.account': 'Обліковий запис', + 'settings.tabs.account': 'Акаунт', 'settings.tabs.offline': 'Офлайн', 'settings.tabs.device': 'Застосунок', 'settings.groups.account': 'Ваш обліковий запис', diff --git a/shared/src/i18n/vi/admin.ts b/shared/src/i18n/vi/admin.ts index f855a7fcc..4228edb0e 100644 --- a/shared/src/i18n/vi/admin.ts +++ b/shared/src/i18n/vi/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': 'Thông báo', + 'admin.notifications.channels': 'Kênh', + 'admin.notifications.adminAlerts': 'Cảnh báo quản trị', 'admin.notifications.hint': 'Chọn một kênh thông báo. Chỉ có một người có thể hoạt động tại một thời điểm.', 'admin.notifications.none': 'Tắt', 'admin.notifications.email': 'Email (SMTP)', // same-as-en @@ -123,6 +125,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': 'Không tạo được liên kết mời', 'admin.invite.deleteError': 'Không xóa được liên kết mời', 'admin.tabs.settings': 'Cài đặt', + 'admin.tabs.security': 'Bảo mật', + 'admin.tabs.services': 'Dịch vụ', 'admin.allowRegistration': 'Cho phép đăng ký', 'admin.allowRegistrationHint': 'Người dùng mới có thể tự đăng ký', 'admin.authMethods': 'Phương thức xác thực', @@ -183,10 +187,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': 'Có', 'admin.trekApi.notIncluded': 'Không có', 'admin.trekApi.quota.title': 'Hạn mức hôm nay', - 'admin.trekApi.quota.used': 'Đã dùng', 'admin.trekApi.quota.remaining': 'Còn lại', 'admin.trekApi.quota.resets': 'Đặt lại lúc', - 'admin.trekApi.quota.limit': 'Giới hạn', 'admin.trekApi.quota.key': 'Khóa', 'admin.trekApi.quota.state.connected': 'Đã kết nối', 'admin.trekApi.quota.state.registering': 'Đang thiết lập', @@ -311,8 +313,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': 'Tiếp theo là gì', 'admin.collab.whatsnext.subtitle': 'Đề xuất hoạt động và các bước tiếp theo', 'admin.tabs.config': 'Cá nhân hóa', - 'admin.tabs.defaults': 'Mặc định của người dùng', + 'admin.tabs.defaults': 'Mặc định', 'admin.defaultSettings.title': 'Cài đặt người dùng mặc định', + 'admin.defaultSettings.routing': 'Định tuyến', 'admin.defaultSettings.description': 'Đặt mặc định cho toàn phiên bản. Người dùng chưa thay đổi cài đặt sẽ thấy các giá trị này. Những thay đổi của riêng họ luôn được ưu tiên.', 'admin.defaultSettings.saved': 'Đã lưu mặc định', @@ -784,14 +787,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': 'Đã cập nhật tiện ích bổ sung', 'admin.addons.toast.error': 'Không thể cập nhật tiện ích bổ sung', 'admin.addons.group.count': '{enabled} trên {total} đã bật', - 'admin.addons.search': 'Tìm tiện ích', - 'admin.addons.filter': 'Hiển thị', - 'admin.addons.filter.all': 'Tất cả', - 'admin.addons.filter.on': 'Bật', - 'admin.addons.filter.off': 'Tắt', - 'admin.addons.noMatch': 'Không có tiện ích nào khớp với tìm kiếm hoặc bộ lọc.', - 'admin.addons.subOptions': '{count} tùy chọn', - 'admin.addons.subOptionsOn': '{count} đang bật', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': 'Người dùng có thể kết nối máy chủ wanderer riêng và nhập các cung đường của máy chủ đó thành chuyến tham quan', @@ -817,7 +812,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': 'Miễn phí, không cần khóa API', 'admin.weather.locationHint': 'Thời tiết dựa trên địa điểm đầu tiên có tọa độ mỗi ngày. Nếu không có địa điểm nào được chỉ định cho một ngày thì bất kỳ địa điểm nào trong danh sách địa điểm đều được sử dụng làm tham chiếu.', - 'admin.tabs.mcpTokens': 'MCP Truy cập', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP Truy cập', 'admin.mcpTokens.subtitle': 'Quản lý phiên OAuth và mã thông báo API trên tất cả người dùng', 'admin.mcpTokens.sectionTitle': 'API Mã thông báo', @@ -917,7 +912,6 @@ const admin: TranslationStrings = { 'admin.passkey.resetDone': 'Đã xóa {count} mật mã', 'admin.group.users': 'Người dùng', 'admin.group.config': 'Cấu hình', - 'admin.group.integration': 'Tích hợp', 'admin.group.maintenance': 'Bảo trì', 'admin.invite.tripLabel': 'Thêm vào chuyến đi (tùy chọn)', 'admin.invite.tripNone': 'Không có chuyến đi', diff --git a/shared/src/i18n/vi/mailAdmin.ts b/shared/src/i18n/vi/mailAdmin.ts index d5a54f855..32724032e 100644 --- a/shared/src/i18n/vi/mailAdmin.ts +++ b/shared/src/i18n/vi/mailAdmin.ts @@ -38,7 +38,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': 'Nhập một cổng từ 1 đến 65535.', 'mailAdmin.problem.templateToken': 'Địa chỉ cần có {token} ở vị trí mã của từng người.', 'mailAdmin.problem.dailyInvalid': 'Nhập một số nguyên từ 0 đến 1000.', - 'mailAdmin.status.title': 'Trạng thái', 'mailAdmin.status.lastRead': 'Đọc lần cuối {time}', 'mailAdmin.status.never': 'chưa bao giờ', 'mailAdmin.status.mails': 'Email, 24 giờ', diff --git a/shared/src/i18n/zh-TW/admin.ts b/shared/src/i18n/zh-TW/admin.ts index fe435bd48..2aa5e7d7b 100644 --- a/shared/src/i18n/zh-TW/admin.ts +++ b/shared/src/i18n/zh-TW/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': '通知', + 'admin.notifications.channels': '管道', + 'admin.notifications.adminAlerts': '管理員通知', 'admin.notifications.hint': '選擇一個通知渠道。一次只能啟用一個。', 'admin.notifications.none': '已停用', 'admin.notifications.email': '電子郵件 (SMTP)', @@ -119,6 +121,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': '建立連結失敗', 'admin.invite.deleteError': '刪除連結失敗', 'admin.tabs.settings': '設定', + 'admin.tabs.security': '安全性', + 'admin.tabs.services': '服務', 'admin.allowRegistration': '允許註冊', 'admin.allowRegistrationHint': '新使用者可以自行註冊', 'admin.authMethods': '驗證方式', @@ -173,10 +177,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': '包含', 'admin.trekApi.notIncluded': '不包含', 'admin.trekApi.quota.title': '今日額度', - 'admin.trekApi.quota.used': '已用', 'admin.trekApi.quota.remaining': '剩餘', 'admin.trekApi.quota.resets': '重設時間', - 'admin.trekApi.quota.limit': '上限', 'admin.trekApi.quota.key': '金鑰', 'admin.trekApi.quota.state.connected': '已連線', 'admin.trekApi.quota.state.registering': '設定中', @@ -292,8 +294,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': '下一步', 'admin.collab.whatsnext.subtitle': '活動建議和後續步驟', 'admin.tabs.config': '配置', - 'admin.tabs.defaults': '用戶預設設定', + 'admin.tabs.defaults': '預設值', 'admin.defaultSettings.title': '用戶預設設定', + 'admin.defaultSettings.routing': '路線規劃', 'admin.defaultSettings.description': '設定整個執行個體的預設值。未更改設定的用戶將看到這些值。用戶自己的更改始終優先。', 'admin.defaultSettings.saved': '預設值已儲存', @@ -701,14 +704,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': '擴充套件已更新', 'admin.addons.toast.error': '更新擴充套件失敗', 'admin.addons.group.count': '{total} 項中已啟用 {enabled} 項', - 'admin.addons.search': '搜尋外掛', - 'admin.addons.filter': '顯示', - 'admin.addons.filter.all': '全部', - 'admin.addons.filter.on': '已開啟', - 'admin.addons.filter.off': '已關閉', - 'admin.addons.noMatch': '沒有外掛符合搜尋或篩選條件。', - 'admin.addons.subOptions': '{count} 個選項', - 'admin.addons.subOptionsOn': '已開啟 {count} 個', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': '使用者可以連結自己的 wanderer 執行個體,並將其中的步道匯入為遊程', 'admin.toursHikingMap.title': '健行地圖 (OpenHikingMap)', @@ -733,7 +728,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': '免費,無需 API 金鑰', 'admin.weather.locationHint': '天氣基於每天中第一個有座標的地點。如果當天沒有分配地點,則使用地點列表中的任意地點作為參考。', - 'admin.tabs.mcpTokens': 'MCP 存取', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP 存取', 'admin.mcpTokens.subtitle': '管理所有使用者的 OAuth 工作階段和 API 令牌', 'admin.mcpTokens.sectionTitle': 'API 令牌', @@ -839,7 +834,6 @@ const admin: TranslationStrings = { '用於每一位尚未輸入自己金鑰的使用者,如此整個執行個體都能取得沒有浮水印的 CARTO 圖磚。以加密方式儲存。', 'admin.group.users': '使用者', 'admin.group.config': '設定', - 'admin.group.integration': '整合', 'admin.group.maintenance': '維護', 'admin.invite.tripLabel': '加入行程(選填)', 'admin.invite.tripNone': '不指定行程', diff --git a/shared/src/i18n/zh-TW/mailAdmin.ts b/shared/src/i18n/zh-TW/mailAdmin.ts index 202146174..0e78aaac5 100644 --- a/shared/src/i18n/zh-TW/mailAdmin.ts +++ b/shared/src/i18n/zh-TW/mailAdmin.ts @@ -37,7 +37,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '請輸入 1 到 65535 之間的連接埠。', 'mailAdmin.problem.templateToken': '地址中需要在每個人的驗證碼所在位置使用 {token}。', 'mailAdmin.problem.dailyInvalid': '請輸入 0 到 1000 之間的整數。', - 'mailAdmin.status.title': '狀態', 'mailAdmin.status.lastRead': '上次讀取 {time}', 'mailAdmin.status.never': '從未', 'mailAdmin.status.mails': '郵件,24 小時', diff --git a/shared/src/i18n/zh/admin.ts b/shared/src/i18n/zh/admin.ts index 79b5e750a..ebbe660b4 100644 --- a/shared/src/i18n/zh/admin.ts +++ b/shared/src/i18n/zh/admin.ts @@ -2,6 +2,8 @@ import type { TranslationStrings } from '../types'; const admin: TranslationStrings = { 'admin.notifications.title': '通知', + 'admin.notifications.channels': '渠道', + 'admin.notifications.adminAlerts': '管理员通知', 'admin.notifications.hint': '选择一个通知渠道。一次只能激活一个。', 'admin.notifications.none': '已禁用', 'admin.notifications.email': '电子邮件 (SMTP)', @@ -71,6 +73,8 @@ const admin: TranslationStrings = { 'admin.invite.createError': '创建链接失败', 'admin.invite.deleteError': '删除链接失败', 'admin.tabs.settings': '设置', + 'admin.tabs.security': '安全', + 'admin.tabs.services': '服务', 'admin.allowRegistration': '允许注册', 'admin.allowRegistrationHint': '新用户可以自行注册', 'admin.authMethods': '登录方式', @@ -125,10 +129,8 @@ const admin: TranslationStrings = { 'admin.trekApi.included': '包含', 'admin.trekApi.notIncluded': '不包含', 'admin.trekApi.quota.title': '今日额度', - 'admin.trekApi.quota.used': '已用', 'admin.trekApi.quota.remaining': '剩余', 'admin.trekApi.quota.resets': '重置时间', - 'admin.trekApi.quota.limit': '上限', 'admin.trekApi.quota.key': '密钥', 'admin.trekApi.quota.state.connected': '已连接', 'admin.trekApi.quota.state.registering': '正在设置', @@ -244,8 +246,9 @@ const admin: TranslationStrings = { 'admin.collab.whatsnext.title': '下一步', 'admin.collab.whatsnext.subtitle': '活动建议和后续步骤', 'admin.tabs.config': '个性化', - 'admin.tabs.defaults': '用户默认设置', + 'admin.tabs.defaults': '默认值', 'admin.defaultSettings.title': '用户默认设置', + 'admin.defaultSettings.routing': '路线规划', 'admin.defaultSettings.description': '设置实例范围的默认值。未更改设置的用户将看到这些值。用户自己的更改始终优先。', 'admin.defaultSettings.saved': '默认值已保存', 'admin.defaultSettings.reset': '重置为内置默认值', @@ -652,14 +655,6 @@ const admin: TranslationStrings = { 'admin.addons.toast.updated': '扩展已更新', 'admin.addons.toast.error': '更新扩展失败', 'admin.addons.group.count': '{total} 项中已启用 {enabled} 项', - 'admin.addons.search': '搜索插件', - 'admin.addons.filter': '显示', - 'admin.addons.filter.all': '全部', - 'admin.addons.filter.on': '已开启', - 'admin.addons.filter.off': '已关闭', - 'admin.addons.noMatch': '没有插件符合搜索或筛选条件。', - 'admin.addons.subOptions': '{count} 个选项', - 'admin.addons.subOptionsOn': '已开启 {count} 个', 'admin.toursWanderer.title': 'wanderer', 'admin.toursWanderer.subtitle': '用户可以连接自己的 wanderer 实例,并将其中的步道作为游览路线导入', 'admin.toursHikingMap.title': '徒步地图 (OpenHikingMap)', @@ -683,7 +678,7 @@ const admin: TranslationStrings = { 'admin.weather.requestsDesc': '免费,无需 API 密钥', 'admin.weather.locationHint': '天气基于每天中第一个有坐标的地点。如果当天没有分配地点,则使用地点列表中的任意地点作为参考。', - 'admin.tabs.mcpTokens': 'MCP 访问', + 'admin.tabs.mcpTokens': 'MCP', 'admin.mcpTokens.title': 'MCP 访问', 'admin.mcpTokens.subtitle': '管理所有用户的 OAuth 会话和 API 令牌', 'admin.mcpTokens.sectionTitle': 'API 令牌', @@ -838,7 +833,6 @@ const admin: TranslationStrings = { '用于所有未输入自己密钥的用户,这样整个实例都能获得无水印的 CARTO 瓦片。以加密方式存储。', 'admin.group.users': '用户', 'admin.group.config': '配置', - 'admin.group.integration': '集成', 'admin.group.maintenance': '维护', 'admin.invite.tripLabel': '添加到行程(可选)', 'admin.invite.tripNone': '不选择行程', diff --git a/shared/src/i18n/zh/mailAdmin.ts b/shared/src/i18n/zh/mailAdmin.ts index cabb65a38..cdc8cf78c 100644 --- a/shared/src/i18n/zh/mailAdmin.ts +++ b/shared/src/i18n/zh/mailAdmin.ts @@ -37,7 +37,6 @@ const mailAdmin: TranslationStrings = { 'mailAdmin.problem.portInvalid': '请输入 1 到 65535 之间的端口。', 'mailAdmin.problem.templateToken': '地址中需要在每个人的验证码所在位置使用 {token}。', 'mailAdmin.problem.dailyInvalid': '请输入 0 到 1000 之间的整数。', - 'mailAdmin.status.title': '状态', 'mailAdmin.status.lastRead': '上次读取 {time}', 'mailAdmin.status.never': '从未', 'mailAdmin.status.mails': '邮件,24 小时',