mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(admin): rebuild the addons page as a tile grid with sub-shelves
The three stacked sections become one grid of cards per group, with the type shown on the tile itself. Bag tracking, the collab features and the photo providers hang off their parent addon as compact shelf rows instead of full-width siblings, and each group header counts how many of its addons are on. Along the way: - naver_list_import, airtrail and llm_parsing get catalog entries, so their names and descriptions translate like everyone else - dark detection moves into a shared useIsDark hook that watches the .dark class instead of re-deriving settings + matchMedia locally - the ToggleSwitch knob uses --accent-text so a light accent in dark mode no longer renders white-on-white
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// FE-ADMIN-ADDON-001 to FE-ADMIN-ADDON-025
|
||||
import { render, screen, waitFor, within } from '../../../tests/helpers/render';
|
||||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { delay, http, HttpResponse } from 'msw';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
@@ -44,20 +44,16 @@ function modelsRoute(names: string[], seen?: (string | null)[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** The pill toggle of a top-level addon row. */
|
||||
/** Every switch is labelled with the thing it controls, top-level tile or shelf
|
||||
* row alike, so one lookup covers both. */
|
||||
function addonToggle(name: string): HTMLElement {
|
||||
const row = screen.getByText(name).closest('.px-6.py-4') as HTMLElement;
|
||||
return within(row).getByRole('button');
|
||||
return screen.getByRole('button', { name });
|
||||
}
|
||||
|
||||
/** The pill toggle of an indented sub-row (bag tracking, collab feature, photo provider). */
|
||||
function subToggle(label: string): HTMLElement {
|
||||
const row = screen.getByText(label).closest('.flex.items-center.gap-4') as HTMLElement;
|
||||
return within(row).getByRole('button');
|
||||
}
|
||||
const subToggle = addonToggle;
|
||||
|
||||
function isOn(toggle: HTMLElement): boolean {
|
||||
return toggle.style.background === 'var(--text-primary)';
|
||||
return toggle.getAttribute('aria-pressed') === 'true';
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -82,6 +78,7 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.documentElement.classList.remove('dark');
|
||||
});
|
||||
|
||||
describe('AddonManager', () => {
|
||||
@@ -144,13 +141,10 @@ describe('AddonManager', () => {
|
||||
render(<><ToastContainer /><AddonManager /></>);
|
||||
await screen.findByText('Todo List');
|
||||
|
||||
// Get toggle button - use getAllByRole since there might be multiple buttons
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const toggleBtn = buttons.find(b => b.classList.contains('rounded-full'));
|
||||
expect(toggleBtn).toBeInTheDocument();
|
||||
const toggleBtn = addonToggle('Todo List');
|
||||
expect(isOn(toggleBtn)).toBe(false);
|
||||
|
||||
// Before click - disabled state (border-primary bg)
|
||||
await user.click(toggleBtn!);
|
||||
await user.click(toggleBtn);
|
||||
|
||||
// After click - success toast
|
||||
await screen.findByText('Addon updated');
|
||||
@@ -169,17 +163,14 @@ describe('AddonManager', () => {
|
||||
render(<><ToastContainer /><AddonManager /></>);
|
||||
await screen.findByText('Todo List');
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const toggleBtn = buttons.find(b => b.classList.contains('rounded-full'));
|
||||
await user.click(toggleBtn!);
|
||||
await user.click(addonToggle('Todo List'));
|
||||
|
||||
// Error toast appears
|
||||
await screen.findByText('Failed to update addon');
|
||||
|
||||
// The disabled text should be back after rollback
|
||||
// The switch is back off after rollback
|
||||
await waitFor(() => {
|
||||
const disabledTexts = screen.getAllByText('Disabled');
|
||||
expect(disabledTexts.length).toBeGreaterThan(0);
|
||||
expect(isOn(addonToggle('Todo List'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,13 +186,7 @@ describe('AddonManager', () => {
|
||||
<AddonManager bagTrackingEnabled={false} onToggleBagTracking={mockToggle} />
|
||||
);
|
||||
await screen.findByText('Bag Tracking');
|
||||
const bagTrackingToggle = screen.getAllByRole('button').find(b =>
|
||||
b.closest('[style*="paddingLeft: 70"]') !== null || b.closest('div')?.textContent?.includes('Bag Tracking')
|
||||
);
|
||||
// Click the bag tracking toggle button (the h-6 w-11 button near "Bag Tracking")
|
||||
const allBtns = screen.getAllByRole('button').filter(b => b.classList.contains('rounded-full'));
|
||||
// There should be two toggle buttons: one for the addon, one for bag tracking
|
||||
await user.click(allBtns[allBtns.length - 1]);
|
||||
await user.click(subToggle('Bag Tracking'));
|
||||
expect(mockToggle).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -251,9 +236,10 @@ describe('AddonManager', () => {
|
||||
// Journey addon is rendered
|
||||
expect(screen.getByText('Journey')).toBeInTheDocument();
|
||||
|
||||
// Toggle buttons: journey toggle + 2 provider toggles
|
||||
const toggleBtns = screen.getAllByRole('button').filter(b => b.classList.contains('rounded-full'));
|
||||
expect(toggleBtns.length).toBe(3);
|
||||
// Journey's own switch plus one per provider, each labelled with what it controls
|
||||
expect(isOn(addonToggle('Journey'))).toBe(true);
|
||||
expect(isOn(subToggle('Unsplash'))).toBe(true);
|
||||
expect(isOn(subToggle('Pexels'))).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-ADDON-011: icon falls back to Puzzle when icon name unknown', async () => {
|
||||
@@ -278,7 +264,8 @@ describe('AddonManager', () => {
|
||||
});
|
||||
|
||||
it('FE-ADMIN-ADDON-013: dark mode swaps the wordmark in the header', async () => {
|
||||
seedStore(useSettingsStore, { settings: { dark_mode: 'dark' } });
|
||||
// The wordmark reads the .dark class, the same source applyAppearance() writes.
|
||||
document.documentElement.classList.add('dark');
|
||||
render(<AddonManager />);
|
||||
|
||||
await screen.findByText('No addons available');
|
||||
@@ -309,11 +296,11 @@ describe('AddonManager', () => {
|
||||
|
||||
await screen.findByText('Immich');
|
||||
// immich and synologyphotos ship a vendor glyph, unsplash does not
|
||||
const immichRow = screen.getByText('Immich').closest('.flex.items-center.gap-4') as HTMLElement;
|
||||
const immichRow = screen.getByText('Immich').closest('li') as HTMLElement;
|
||||
expect(immichRow.querySelector('svg')).toBeInTheDocument();
|
||||
const synologyRow = screen.getByText('Synology Photos').closest('.flex.items-center.gap-4') as HTMLElement;
|
||||
const synologyRow = screen.getByText('Synology Photos').closest('li') as HTMLElement;
|
||||
expect(synologyRow.querySelector('svg')).toBeInTheDocument();
|
||||
const unsplashRow = screen.getByText('Unsplash').closest('.flex.items-center.gap-4') as HTMLElement;
|
||||
const unsplashRow = screen.getByText('Unsplash').closest('li') as HTMLElement;
|
||||
expect(unsplashRow.querySelector('svg')).not.toBeInTheDocument();
|
||||
|
||||
expect(isOn(subToggle('Immich'))).toBe(true);
|
||||
@@ -396,7 +383,9 @@ describe('AddonManager', () => {
|
||||
render(<AddonManager />);
|
||||
|
||||
await screen.findByText('AI Parsing');
|
||||
expect(screen.getByText('Extract bookings from files')).toBeInTheDocument();
|
||||
// The catalog key now exists, so the tile shows the translation rather than falling
|
||||
// back to the English description the server sent.
|
||||
expect(screen.getByText('Reads bookings the built-in parser cannot, using an AI model you choose')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Connection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useEffect, useState, type ComponentType } from 'react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import { useIsDark } from '../../hooks/useIsDark'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, MessageCircle, StickyNote, BarChart3, Sparkles, Luggage, Plane, Server, Cloud, Bookmark } from 'lucide-react'
|
||||
import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, MessageCircle, StickyNote, BarChart3, Sparkles, Luggage, Plane, Server, Cloud, Bookmark, Users, Loader2 } from 'lucide-react'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
import AddonTile from './AddonTile'
|
||||
import AddonSubRow from './AddonSubRow'
|
||||
|
||||
// Keys are the `icon` column from the addons table (see server seeds.ts); anything
|
||||
// unknown falls back to Puzzle. Users/Sparkles cover collab and llm_parsing, which
|
||||
// used to land on the fallback.
|
||||
const ICON_MAP = {
|
||||
ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Plane, Bookmark,
|
||||
ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Plane, Bookmark, Users, Sparkles,
|
||||
}
|
||||
|
||||
function ImmichIcon({ size = 14 }: { size?: number }) {
|
||||
@@ -60,6 +66,14 @@ function AddonIcon({ name, size = 20 }: AddonIconProps) {
|
||||
return <Icon size={size} />
|
||||
}
|
||||
|
||||
/** What each type means, shown on the tile itself now that the sections are gone. The
|
||||
* hint rides along as the tooltip, so "Global" still explains itself. */
|
||||
const TYPE_META: Record<string, { icon: ComponentType<{ size?: number }>; labelKey: string; hintKey: string }> = {
|
||||
trip: { icon: Briefcase, labelKey: 'admin.addons.type.trip', hintKey: 'admin.addons.tripHint' },
|
||||
global: { icon: Globe, labelKey: 'admin.addons.type.global', hintKey: 'admin.addons.globalHint' },
|
||||
integration: { icon: Link2, labelKey: 'admin.addons.type.integration', hintKey: 'admin.addons.integrationHint' },
|
||||
}
|
||||
|
||||
interface CollabFeatures { chat: boolean; notes: boolean; polls: boolean; whatsnext: boolean }
|
||||
|
||||
const COLLAB_SUB_FEATURES = [
|
||||
@@ -71,8 +85,7 @@ const COLLAB_SUB_FEATURES = [
|
||||
|
||||
export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, collabFeatures, onToggleCollabFeature }: { bagTrackingEnabled?: boolean; onToggleBagTracking?: () => void; collabFeatures?: CollabFeatures; onToggleCollabFeature?: (key: string) => void }) {
|
||||
const { t } = useTranslation()
|
||||
const dm = useSettingsStore(s => s.settings.dark_mode)
|
||||
const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
const dark = useIsDark()
|
||||
const toast = useToast()
|
||||
const refreshGlobalAddons = useAddonStore(s => s.loadAddons)
|
||||
const [addons, setAddons] = useState<Addon[]>([])
|
||||
@@ -94,16 +107,16 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
|
||||
}
|
||||
}
|
||||
|
||||
/** Optimistic flip, per-row rollback on failure. Rolling the whole addons
|
||||
* snapshot back instead would undo a toggle the user hit in parallel. */
|
||||
const handleToggle = async (addon: Addon) => {
|
||||
const newEnabled = !addon.enabled
|
||||
// Optimistic update
|
||||
setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: newEnabled } : a))
|
||||
try {
|
||||
await adminApi.updateAddon(addon.id, { enabled: newEnabled })
|
||||
refreshGlobalAddons()
|
||||
toast.success(t('admin.addons.toast.updated'))
|
||||
} catch (err: unknown) {
|
||||
// Rollback
|
||||
setAddons(prev => prev.map(a => a.id === addon.id ? { ...a, enabled: !newEnabled } : a))
|
||||
toast.error(t('admin.addons.toast.error'))
|
||||
}
|
||||
@@ -118,24 +131,7 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
|
||||
return addon.type === 'trip' && (addon.icon === 'Image' || haystack.includes('photo') || haystack.includes('memories'))
|
||||
}
|
||||
|
||||
const handleTogglePhotoProvider = async (providerAddon: Addon) => {
|
||||
const enableProvider = !providerAddon.enabled
|
||||
const prev = addons
|
||||
|
||||
setAddons(current => current.map(a => a.id === providerAddon.id ? { ...a, enabled: enableProvider } : a))
|
||||
|
||||
try {
|
||||
await adminApi.updateAddon(providerAddon.id, { enabled: enableProvider })
|
||||
refreshGlobalAddons()
|
||||
toast.success(t('admin.addons.toast.updated'))
|
||||
} catch {
|
||||
setAddons(prev)
|
||||
toast.error(t('admin.addons.toast.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const photoProviderAddons = addons.filter(isPhotoProviderAddon)
|
||||
const photosAddon = addons.filter(a => a.type === 'trip').find(isPhotosAddon)
|
||||
const tripAddons = addons.filter(a => a.type === 'trip' && !isPhotosAddon(a))
|
||||
const globalAddons = addons.filter(a => a.type === 'global')
|
||||
const integrationAddons = addons.filter(a => a.type === 'integration')
|
||||
@@ -144,173 +140,163 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
|
||||
label: provider.name,
|
||||
description: provider.description,
|
||||
enabled: provider.enabled,
|
||||
toggle: () => handleTogglePhotoProvider(provider),
|
||||
toggle: () => handleToggle(provider),
|
||||
}))
|
||||
const photosDerivedEnabled = providerOptions.some(p => p.enabled)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-8 h-8 border-2 border-slate-200 border-t-slate-900 rounded-full animate-spin mx-auto" style={{ borderTopColor: 'var(--text-primary)' }}></div>
|
||||
<div className="grid place-items-center py-16">
|
||||
<Loader2 size={22} className="animate-spin text-content-faint" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Bag tracking, the collab features and the photo providers all hang off their
|
||||
* parent as shelf rows. The conditions are deliberately the ones that were here
|
||||
* before — note that the providers render even when Journey itself is off. */
|
||||
const shelfFor = (addon: Addon) => {
|
||||
if (addon.id === 'packing' && addon.enabled && onToggleBagTracking) {
|
||||
return (
|
||||
<AddonSubRow
|
||||
icon={<Luggage size={14} />}
|
||||
title={t('admin.bagTracking.title')}
|
||||
description={t('admin.bagTracking.subtitle')}
|
||||
enabled={!!bagTrackingEnabled}
|
||||
onToggle={onToggleBagTracking}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (addon.id === 'collab' && addon.enabled && collabFeatures && onToggleCollabFeature) {
|
||||
return COLLAB_SUB_FEATURES.map(feat => {
|
||||
const Icon = feat.icon
|
||||
return (
|
||||
<AddonSubRow
|
||||
key={feat.key}
|
||||
icon={<Icon size={14} />}
|
||||
title={t(feat.titleKey)}
|
||||
description={t(feat.subtitleKey)}
|
||||
enabled={collabFeatures[feat.key]}
|
||||
onToggle={() => onToggleCollabFeature(feat.key)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
if (addon.id === 'journey' && providerOptions.length > 0) {
|
||||
return providerOptions.map(provider => {
|
||||
const ProviderIcon = PROVIDER_ICONS[provider.key]
|
||||
return (
|
||||
<AddonSubRow
|
||||
key={provider.key}
|
||||
icon={ProviderIcon ? <ProviderIcon size={14} /> : undefined}
|
||||
title={provider.label}
|
||||
description={provider.description}
|
||||
enabled={provider.enabled}
|
||||
onToggle={provider.toggle}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const tile = (addon: Addon) => {
|
||||
const label = getAddonLabel(t, addon)
|
||||
return (
|
||||
<AddonTile
|
||||
key={addon.id}
|
||||
icon={<AddonIcon name={addon.icon} size={18} />}
|
||||
name={label.name}
|
||||
description={label.description}
|
||||
enabled={addon.enabled}
|
||||
onToggle={() => handleToggle(addon)}
|
||||
>
|
||||
{shelfFor(addon)}
|
||||
</AddonTile>
|
||||
)
|
||||
}
|
||||
|
||||
const llmAddon = integrationAddons.find(a => a.id === 'llm_parsing')
|
||||
|
||||
/* One column per type, side by side, instead of three stacked sections. Stacked, each
|
||||
section opened its own set of columns and the tall tiles (Collab 261px, Journey
|
||||
185px) set a section's height while its neighbours ran out early. Side by side the
|
||||
column heading carries the type, so the tiles need no badge of their own. */
|
||||
const groups = [
|
||||
{ key: 'trip', addons: tripAddons },
|
||||
{ key: 'global', addons: globalAddons },
|
||||
{ key: 'integration', addons: integrationAddons },
|
||||
].filter(g => g.addons.length > 0)
|
||||
const enabledCount = addons.filter(a => a.type !== 'photo_provider' && a.enabled).length
|
||||
const totalCount = tripAddons.length + globalAddons.length + integrationAddons.length
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="rounded-xl border overflow-hidden bg-surface-card border-edge">
|
||||
<div className="px-6 py-4 border-b border-edge-secondary">
|
||||
<h2 className="font-semibold text-content">{t('admin.addons.title')}</h2>
|
||||
<p className="text-xs mt-1 text-content-muted" style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||
{t('admin.addons.subtitleBefore')}<img src={dark ? '/text-light.svg' : '/text-dark.svg'} alt="TREK" style={{ height: 11, display: 'inline', verticalAlign: 'middle', opacity: 0.7 }} />{t('admin.addons.subtitleAfter')}
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||||
<div>
|
||||
<h2 className="text-subtitle font-semibold tracking-tight text-content">{t('admin.addons.title')}</h2>
|
||||
<p className="mt-1 flex flex-wrap items-center gap-1 text-caption text-content-muted">
|
||||
{t('admin.addons.subtitleBefore')}
|
||||
<img src={dark ? '/text-light.svg' : '/text-dark.svg'} alt="TREK" style={{ height: 11, verticalAlign: 'middle', opacity: 0.7 }} />
|
||||
{t('admin.addons.subtitleAfter')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{addons.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-content-faint">
|
||||
{t('admin.addons.noAddons')}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Trip Addons */}
|
||||
{tripAddons.length > 0 && (
|
||||
<div>
|
||||
<div className="px-6 py-2.5 border-b flex items-center gap-2 bg-surface-secondary border-edge-secondary">
|
||||
<Briefcase size={13} className="text-content-muted" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-content-muted">
|
||||
{t('admin.addons.type.trip')} — {t('admin.addons.tripHint')}
|
||||
</span>
|
||||
</div>
|
||||
{tripAddons.map(addon => (
|
||||
<div key={addon.id}>
|
||||
<AddonRow addon={addon} onToggle={handleToggle} t={t} />
|
||||
{addon.id === 'packing' && addon.enabled && onToggleBagTracking && (
|
||||
<div className="flex items-center gap-4 px-6 py-3 border-b border-edge-secondary bg-surface-secondary" style={{ paddingLeft: 70 }}>
|
||||
<Luggage size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="text-sm font-medium text-content-secondary">{t('admin.bagTracking.title')}</div>
|
||||
<div className="text-xs mt-0.5 text-content-faint">{t('admin.bagTracking.subtitle')}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`hidden sm:inline text-xs font-medium ${bagTrackingEnabled ? 'text-content' : 'text-content-faint'}`}>
|
||||
{bagTrackingEnabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
|
||||
</span>
|
||||
<button type="button" onClick={onToggleBagTracking}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
style={{ background: bagTrackingEnabled ? 'var(--text-primary)' : 'var(--border-primary)' }}>
|
||||
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
|
||||
style={{ transform: bagTrackingEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{addon.id === 'collab' && addon.enabled && collabFeatures && onToggleCollabFeature && (
|
||||
<div className="px-6 py-3 border-b border-edge-secondary bg-surface-secondary" style={{ paddingLeft: 70 }}>
|
||||
<div className="space-y-2">
|
||||
{COLLAB_SUB_FEATURES.map(feat => {
|
||||
const enabled = collabFeatures[feat.key]
|
||||
const Icon = feat.icon
|
||||
return (
|
||||
<div key={feat.key} className="flex items-center gap-4" style={{ minHeight: 32 }}>
|
||||
<Icon size={14} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="text-sm font-medium text-content-secondary">{t(feat.titleKey)}</div>
|
||||
<div className="text-xs mt-0.5 text-content-faint">{t(feat.subtitleKey)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`hidden sm:inline text-xs font-medium ${enabled ? 'text-content' : 'text-content-faint'}`}>
|
||||
{enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
|
||||
</span>
|
||||
<button type="button" onClick={() => onToggleCollabFeature(feat.key)}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
style={{ background: enabled ? 'var(--text-primary)' : 'var(--border-primary)' }}>
|
||||
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
|
||||
style={{ transform: enabled ? 'translateX(20px)' : 'translateX(0)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Addons */}
|
||||
{globalAddons.length > 0 && (
|
||||
<div>
|
||||
<div className="px-6 py-2.5 border-b border-t flex items-center gap-2 bg-surface-secondary border-edge-secondary">
|
||||
<Globe size={13} className="text-content-muted" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-content-muted">
|
||||
{t('admin.addons.type.global')} — {t('admin.addons.globalHint')}
|
||||
</span>
|
||||
</div>
|
||||
{globalAddons.map(addon => (
|
||||
<div key={addon.id}>
|
||||
<AddonRow addon={addon} onToggle={handleToggle} t={t} />
|
||||
{/* Memories providers as sub-items under Journey addon */}
|
||||
{addon.id === 'journey' && providerOptions.length > 0 && (
|
||||
<div className="px-6 py-3 border-b border-edge-secondary bg-surface-secondary" style={{ paddingLeft: 70 }}>
|
||||
<div className="space-y-2">
|
||||
{providerOptions.map(provider => {
|
||||
const ProviderIcon = PROVIDER_ICONS[provider.key]
|
||||
return (
|
||||
<div key={provider.key} className="flex items-center gap-4" style={{ minHeight: 32 }}>
|
||||
{ProviderIcon && <span className="text-content-faint"><ProviderIcon size={14} /></span>}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="text-sm font-medium text-content-secondary">{provider.label}</div>
|
||||
<div className="text-xs mt-0.5 text-content-faint">{provider.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`hidden sm:inline text-xs font-medium ${provider.enabled ? 'text-content' : 'text-content-faint'}`}>
|
||||
{provider.enabled ? t('admin.addons.enabled') : t('admin.addons.disabled')}
|
||||
</span>
|
||||
<button type="button"
|
||||
onClick={provider.toggle}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
style={{ background: provider.enabled ? 'var(--text-primary)' : 'var(--border-primary)' }}
|
||||
>
|
||||
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
|
||||
style={{ transform: provider.enabled ? 'translateX(20px)' : 'translateX(0)' }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Integration Addons */}
|
||||
{integrationAddons.length > 0 && (
|
||||
<div>
|
||||
<div className="px-6 py-2.5 border-b border-t flex items-center gap-2 bg-surface-secondary border-edge-secondary">
|
||||
<Link2 size={13} className="text-content-muted" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-content-muted">
|
||||
{t('admin.addons.type.integration')} — {t('admin.addons.integrationHint')}
|
||||
</span>
|
||||
</div>
|
||||
{integrationAddons.map(addon => (
|
||||
<div key={addon.id}>
|
||||
<AddonRow addon={addon} onToggle={handleToggle} t={t} />
|
||||
{addon.id === 'llm_parsing' && addon.enabled && (
|
||||
<LlmParsingConfig addon={addon} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{totalCount > 0 && (
|
||||
<span
|
||||
className="shrink-0 text-caption tabular-nums text-content-faint"
|
||||
title={t('admin.addons.group.count', { enabled: enabledCount, total: totalCount })}
|
||||
>
|
||||
{enabledCount}/{totalCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{addons.length === 0 ? (
|
||||
<EmptyState scene="idle" title={t('admin.addons.noAddons')} />
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 items-start gap-x-4 gap-y-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{groups.map(g => {
|
||||
const meta = TYPE_META[g.key]
|
||||
return (
|
||||
<section key={g.key}>
|
||||
<GroupHead icon={meta.icon} label={t(meta.labelKey)} hint={t(meta.hintKey)} addons={g.addons} t={t} />
|
||||
<div className="space-y-3">{g.addons.map(tile)}</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* The AI-parsing form is far too wide for a tile, so it sits as a band under
|
||||
the columns — no span, no reflow, and the next service that needs settings
|
||||
stacks below it. */}
|
||||
{llmAddon?.enabled && <LlmParsingConfig addon={llmAddon} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupHead({ icon: Icon, label, hint, addons, t }: {
|
||||
icon: ComponentType<{ size?: number; className?: string }>
|
||||
label: string
|
||||
hint: string
|
||||
addons: Addon[]
|
||||
t: (key: string, params?: Record<string, unknown>) => string
|
||||
}) {
|
||||
const enabled = addons.filter(a => a.enabled).length
|
||||
return (
|
||||
<div
|
||||
className="mb-3 rounded-xl border border-edge-secondary bg-surface-secondary px-3 py-2"
|
||||
title={t('admin.addons.group.count', { enabled, total: addons.length })}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={13} className="shrink-0 text-content-muted" />
|
||||
<h3 className="min-w-0 flex-1 truncate text-caption font-semibold uppercase tracking-[0.06em] text-content">{label}</h3>
|
||||
<span className="shrink-0 rounded-full bg-surface px-1.5 py-0.5 text-caption font-medium tabular-nums text-content-muted">
|
||||
{enabled}/{addons.length}
|
||||
</span>
|
||||
</div>
|
||||
{/* Says what the type means — the only place that still explains it. */}
|
||||
<p className="mt-0.5 text-caption text-content-faint">{hint}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -408,9 +394,9 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
}
|
||||
}
|
||||
|
||||
const fieldCls = 'w-full rounded-lg border border-edge-secondary bg-surface px-3 py-2 text-sm text-content placeholder:text-content-faint transition-colors focus:border-edge focus:outline-none'
|
||||
const labelCls = 'mb-1.5 block text-xs font-medium text-content-secondary'
|
||||
const sectionCls = 'text-[11px] font-semibold uppercase tracking-wide text-content-faint'
|
||||
const fieldCls = 'w-full rounded-lg border border-edge-secondary bg-surface px-3 py-2 text-body text-content placeholder:text-content-faint transition-colors focus:border-edge focus:outline-none'
|
||||
const labelCls = 'mb-1.5 block text-caption font-medium text-content-secondary'
|
||||
const sectionCls = 'text-caption font-semibold uppercase tracking-wide text-content-faint'
|
||||
|
||||
const providerOptions = [
|
||||
{ value: 'local', label: 'Local · OpenAI-compatible', icon: <Server size={14} />, badge: 'Ollama' },
|
||||
@@ -419,9 +405,13 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="border-b border-edge-secondary bg-surface-secondary py-5 pr-6 pl-[70px]">
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<p className="text-xs text-content-faint">
|
||||
<section className="mt-3 overflow-hidden rounded-xl border border-edge bg-surface-secondary">
|
||||
<header className="flex items-center gap-2.5 border-b border-edge-secondary bg-surface-tertiary px-4 py-2.5">
|
||||
<Sparkles size={14} className="shrink-0 text-content-muted" />
|
||||
<span className="text-caption font-semibold uppercase tracking-[0.08em] text-content-secondary">{addon.name}</span>
|
||||
</header>
|
||||
<div className="max-w-2xl space-y-6 p-4">
|
||||
<p className="text-caption text-content-faint">
|
||||
Set instance-wide config (applies to all users). Leave blank to let each user configure their own provider.
|
||||
</p>
|
||||
|
||||
@@ -464,7 +454,7 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
{loadingModels ? 'Loading…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
{modelsErr && <p className="text-xs text-rose-600">{modelsErr}</p>}
|
||||
{modelsErr && <p className="text-caption text-danger">{modelsErr}</p>}
|
||||
{!modelsErr && installed.length === 0 && !loadingModels && (
|
||||
<p className="text-xs text-content-faint">No models installed yet — pull one below.</p>
|
||||
)}
|
||||
@@ -496,7 +486,7 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-content">{m.label}</span>
|
||||
{m.recommended && (
|
||||
<span className="rounded-md bg-[rgba(16,185,129,0.15)] px-1.5 py-px text-[10px] font-semibold text-emerald-600">Recommended</span>
|
||||
<span className="rounded-md bg-success-soft px-1.5 py-px text-caption font-semibold text-success">Recommended</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-content-faint">{m.note}</div>
|
||||
@@ -505,7 +495,7 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-surface-tertiary">
|
||||
<div className="h-full bg-accent transition-[width] duration-200" style={{ width: `${pullPct}%` }} />
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-content-faint">{pullStatus}{pullPct ? ` · ${pullPct}%` : ''}</div>
|
||||
<div className="mt-0.5 text-caption text-content-faint">{pullStatus}{pullPct ? ` · ${pullPct}%` : ''}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -527,22 +517,14 @@ function LlmParsingConfig({ addon }: { addon: Addon }) {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<button type="button" onClick={save} disabled={saving} className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-accent-text transition-opacity disabled:opacity-60">
|
||||
<button type="button" onClick={save} disabled={saving} className="rounded-lg bg-accent px-4 py-2 text-body font-medium text-accent-text transition-opacity disabled:opacity-60">
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
interface AddonRowProps {
|
||||
addon: Addon
|
||||
onToggle: (addon: Addon) => void
|
||||
t: (key: string) => string
|
||||
statusOverride?: boolean
|
||||
hideToggle?: boolean
|
||||
}
|
||||
|
||||
function getAddonLabel(t: (key: string) => string, addon: Addon): { name: string; description: string } {
|
||||
const nameKey = `admin.addons.catalog.${addon.id}.name`
|
||||
const descKey = `admin.addons.catalog.${addon.id}.description`
|
||||
@@ -555,56 +537,3 @@ function getAddonLabel(t: (key: string) => string, addon: Addon): { name: string
|
||||
}
|
||||
}
|
||||
|
||||
function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statusOverride, hideToggle }: AddonRowProps & { nameOverride?: string; descriptionOverride?: string }) {
|
||||
const isComingSoon = false
|
||||
const label = getAddonLabel(t, addon)
|
||||
const displayName = nameOverride || label.name
|
||||
const displayDescription = descriptionOverride || label.description
|
||||
const enabledState = statusOverride ?? addon.enabled
|
||||
return (
|
||||
<div className="flex items-center gap-4 px-6 py-4 border-b transition-colors hover:opacity-95 border-edge-secondary" style={{ opacity: isComingSoon ? 0.5 : 1, pointerEvents: isComingSoon ? 'none' : 'auto' }}>
|
||||
{/* Icon */}
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0 bg-surface-secondary text-content">
|
||||
<AddonIcon name={addon.icon} size={20} />
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-content">{displayName}</span>
|
||||
{isComingSoon && (
|
||||
<span className="text-[9px] font-semibold px-2 py-0.5 rounded-full text-content-faint bg-surface-tertiary">
|
||||
Coming Soon
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-surface-secondary text-content-muted">
|
||||
{addon.type === 'global' ? t('admin.addons.type.global') : addon.type === 'integration' ? t('admin.addons.type.integration') : t('admin.addons.type.trip')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs mt-0.5 text-content-muted">{displayDescription}</p>
|
||||
</div>
|
||||
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`hidden sm:inline text-xs font-medium ${(enabledState && !isComingSoon) ? 'text-content' : 'text-content-faint'}`}>
|
||||
{isComingSoon ? t('admin.addons.disabled') : enabledState ? t('admin.addons.enabled') : t('admin.addons.disabled')}
|
||||
</span>
|
||||
{!hideToggle && (
|
||||
<button type="button"
|
||||
onClick={() => !isComingSoon && onToggle(addon)}
|
||||
disabled={isComingSoon}
|
||||
className="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||
style={{ background: (enabledState && !isComingSoon) ? 'var(--text-primary)' : 'var(--border-primary)', cursor: isComingSoon ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-4 w-4 transform rounded-full transition-transform bg-surface-card"
|
||||
style={{
|
||||
transform: (enabledState && !isComingSoon) ? 'translateX(22px)' : 'translateX(4px)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import ToggleSwitch from '../Settings/ToggleSwitch'
|
||||
|
||||
/**
|
||||
* One child row inside an {@link AddonTile}'s sub-shelf: bag tracking under
|
||||
* Lists, a collab feature under Collab, a photo provider under Journey.
|
||||
*
|
||||
* A photo provider without a vendor mark has to stay free of `<svg>`
|
||||
* (FE-ADMIN-ADDON-015), which is why `icon` falls back to a fixed-width spacer
|
||||
* rather than a placeholder glyph. Nothing else belongs in this row either: no
|
||||
* chevron, no status dot, no lucide fallback.
|
||||
*
|
||||
* The row carries its title only; the description rides along as the `title`
|
||||
* tooltip, because a second line of text is what makes a child weigh as much as
|
||||
* an addon.
|
||||
*/
|
||||
export default function AddonSubRow({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
enabled,
|
||||
onToggle,
|
||||
}: {
|
||||
/** Vendor mark or lucide glyph at 14px. Omit it and the slot keeps its width. */
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<li className="flex min-h-[36px] items-center gap-3">
|
||||
{icon ? <span className="shrink-0 text-content-faint">{icon}</span> : <span className="w-3.5 shrink-0" aria-hidden />}
|
||||
<span className="min-w-0 flex-1 truncate text-caption font-medium text-content-secondary" title={description}>
|
||||
{title}
|
||||
</span>
|
||||
<ToggleSwitch on={enabled} onToggle={onToggle} label={title} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import ToggleSwitch from '../Settings/ToggleSwitch'
|
||||
|
||||
/**
|
||||
* One addon in the admin grid: a card whose fill carries the on/off state, with
|
||||
* an optional shelf at the foot holding its sub-features.
|
||||
*
|
||||
* The fill ladder is `--bg-secondary` (on) against `--bg-card` (the panel it
|
||||
* sits on) — that pair reads in both themes, because secondary is *darker* than
|
||||
* the card in light and *lighter* in dark. The obvious alternative, on =
|
||||
* `bg-surface-card`, inverts in dark and turns the disabled tiles into the loud
|
||||
* ones.
|
||||
*
|
||||
* Hover and focus ride on border and fill rather than a shadow: `--shadow-card`
|
||||
* is only declared in `:root`, so the `.dark` palette never overrides it and a
|
||||
* shadow-based affordance would be invisible there.
|
||||
*
|
||||
* The card is not a click target — only the switches are. No lift, no pointer
|
||||
* cursor, no `role="button"`.
|
||||
*/
|
||||
export default function AddonTile({
|
||||
icon,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
onToggle,
|
||||
children,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
name: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
onToggle: () => void
|
||||
/** The sub-shelf: `<AddonSubRow>` children. */
|
||||
children?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<article
|
||||
className={`flex flex-col overflow-hidden rounded-xl border transition-[background-color,border-color] duration-150 focus-within:ring-2 focus-within:ring-accent ${
|
||||
enabled
|
||||
? 'border-edge bg-surface-secondary'
|
||||
: 'border-edge-secondary bg-surface hover:border-edge hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3 p-4">
|
||||
<div
|
||||
className={`grid h-10 w-10 shrink-0 place-items-center rounded-xl ${
|
||||
enabled ? 'border border-accent bg-accent-subtle text-accent-on' : 'bg-surface-tertiary text-content-faint'
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* The name stays full-strength when off: it is the tile's identity, not its
|
||||
state. Greying it out reads as "broken" rather than "not enabled". */}
|
||||
<h4 className="truncate text-body font-semibold text-content" title={name}>
|
||||
{name}
|
||||
</h4>
|
||||
<p
|
||||
className={`mt-0.5 line-clamp-2 text-caption ${enabled ? 'text-content-muted' : 'text-content-faint'}`}
|
||||
title={description}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ToggleSwitch on={enabled} onToggle={onToggle} label={name} />
|
||||
</div>
|
||||
|
||||
{children && (
|
||||
<div className="border-t border-edge-secondary bg-surface-tertiary px-4 py-2.5">
|
||||
{/* Sitting inside the parent on its own fill is enough to say these belong
|
||||
to it — no rail, no indent guides. */}
|
||||
<ul className="space-y-0.5">{children}</ul>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -9,10 +9,14 @@ export default function ToggleSwitch({ on, onToggle, label }: { on: boolean; onT
|
||||
background: on ? 'var(--accent, #111827)' : 'var(--border-primary, #d1d5db)',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
{/* The knob reads against its own track: --accent-text is what the palette
|
||||
already picked to sit on --accent, so a light accent gets a dark knob
|
||||
instead of the white-on-near-white it used to be in dark mode. */}
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: on ? 22 : 2,
|
||||
width: 20, height: 20, borderRadius: '50%', background: 'white',
|
||||
transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
width: 20, height: 20, borderRadius: '50%',
|
||||
background: on ? 'var(--accent-text, #ffffff)' : 'var(--bg-card, #ffffff)',
|
||||
transition: 'left 0.2s', boxShadow: 'var(--shadow-sm, 0 1px 3px rgba(0,0,0,0.2))',
|
||||
}} />
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Returns true while the dark palette is active, read off the `.dark` class on
|
||||
* `<html>` — the single source of truth that `applyAppearance()` writes.
|
||||
*
|
||||
* Deriving it from the settings store instead (`dark_mode === 'auto' &&
|
||||
* matchMedia(...)`) duplicates theme state into React and misses an OS-level
|
||||
* switch under `auto`, because nothing re-renders when the media query flips.
|
||||
* The observer sees every path, including the pre-paint replay in
|
||||
* `public/theme-boot.js`.
|
||||
*
|
||||
* Only for the handful of places that need the flag in JS — an `<img src>` that
|
||||
* differs per theme. Colours belong on tokens, not on this hook.
|
||||
*/
|
||||
export function useIsDark(): boolean {
|
||||
const [dark, setDark] = useState<boolean>(
|
||||
() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark'),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return
|
||||
const mo = new MutationObserver(() => setDark(document.documentElement.classList.contains('dark')))
|
||||
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
return () => mo.disconnect()
|
||||
}, [])
|
||||
|
||||
return dark
|
||||
}
|
||||
@@ -513,7 +513,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'القوائم',
|
||||
'admin.addons.catalog.packing.description': 'قوائم التعبئة والمهام لرحلاتك',
|
||||
'admin.addons.catalog.budget.name': 'التكاليف',
|
||||
'admin.addons.catalog.budget.description': 'تتبع النفقات وخطط ميزانية الرحلة',
|
||||
'admin.addons.catalog.budget.description': 'تتبّع مصاريف الرحلة وقسّمها بين المسافرين',
|
||||
'admin.addons.catalog.documents.name': 'المستندات',
|
||||
'admin.addons.catalog.documents.description': 'حفظ وإدارة وثائق السفر',
|
||||
'admin.addons.catalog.vacay.name': 'الإجازة',
|
||||
@@ -521,12 +521,18 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'الأطلس',
|
||||
'admin.addons.catalog.atlas.description': 'خريطة العالم مع الدول التي تمت زيارتها وإحصائيات السفر',
|
||||
'admin.addons.catalog.collab.name': 'التعاون',
|
||||
'admin.addons.catalog.collab.description': 'ملاحظات واستطلاعات ودردشة لحظية لتخطيط الرحلة',
|
||||
'admin.addons.catalog.collab.description': 'ملاحظات واستطلاعات ومحادثة واقتراحات للتخطيط المشترك',
|
||||
'admin.addons.catalog.memories.name': 'صور (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'شارك صور رحلتك عبر Immich',
|
||||
'admin.addons.catalog.mcp.description': 'بروتوكول سياق النموذج لتكامل مساعد الذكاء الاصطناعي',
|
||||
'admin.addons.subtitleBefore': 'فعّل أو عطّل الميزات لتخصيص تجربة ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'استيراد الأماكن من قائمة Naver Maps مشتركة',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'مزامنة الرحلات من خادم AirTrail الخاص بك',
|
||||
'admin.addons.catalog.llm_parsing.name': 'التحليل بالذكاء الاصطناعي',
|
||||
'admin.addons.catalog.llm_parsing.description': 'يقرأ الحجوزات التي يعجز المحلل المدمج عن فهمها، بنموذج ذكاء اصطناعي تختاره',
|
||||
'admin.addons.enabled': 'مفعّل',
|
||||
'admin.addons.disabled': 'معطّل',
|
||||
'admin.addons.type.trip': 'رحلة',
|
||||
@@ -537,6 +543,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'خدمات الواجهة الخلفية وتكاملات API بدون صفحة مخصصة',
|
||||
'admin.addons.toast.updated': 'تم تحديث الإضافة',
|
||||
'admin.addons.toast.error': 'فشل تحديث الإضافة',
|
||||
'admin.addons.group.count': '{enabled} من {total} مُفعّل',
|
||||
'admin.addons.noAddons': 'لا توجد إضافات متاحة',
|
||||
'admin.weather.title': 'بيانات الطقس',
|
||||
'admin.weather.badge': 'منذ 24 مارس 2026',
|
||||
@@ -645,8 +652,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Trip tracking & travel journal with check-ins, photos, and daily stories', // en-fallback
|
||||
'admin.addons.catalog.collections.name': 'المجموعات',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'مكتبة أماكن شخصية — احفظ الأماكن عبر الرحلات في قوائم مسماة، وانسخها إلى أي رحلة، وشاركها مع الآخرين',
|
||||
'admin.addons.catalog.collections.description': 'اجمع الأماكن من أي رحلة في قوائم مسماة وأعد استخدامها',
|
||||
'admin.passkey.title': 'تسجيل الدخول بمفتاح المرور',
|
||||
'admin.passkey.cardHint': 'اسمح للمستخدمين بتسجيل الدخول باستخدام مفاتيح المرور (WebAuthn). معطّل افتراضيًا.',
|
||||
'admin.passkey.login': 'تفعيل تسجيل الدخول بمفتاح المرور',
|
||||
|
||||
@@ -504,7 +504,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listas',
|
||||
'admin.addons.catalog.packing.description': 'Listas de bagagem e tarefas a fazer para suas viagens',
|
||||
'admin.addons.catalog.budget.name': 'Custos',
|
||||
'admin.addons.catalog.budget.description': 'Acompanhe despesas e planeje o orçamento da viagem',
|
||||
'admin.addons.catalog.budget.description': 'Acompanhe as despesas da viagem e divida-as entre os viajantes',
|
||||
'admin.addons.catalog.documents.name': 'Documentos',
|
||||
'admin.addons.catalog.documents.description': 'Armazene e gerencie documentos de viagem',
|
||||
'admin.addons.catalog.vacay.name': 'Férias',
|
||||
@@ -512,11 +512,17 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa mundial com países visitados e estatísticas',
|
||||
'admin.addons.catalog.collab.name': 'Colab',
|
||||
'admin.addons.catalog.collab.description': 'Notas, enquetes e chat em tempo real para planejar a viagem',
|
||||
'admin.addons.catalog.collab.description': 'Notas, enquetes, chat e sugestões para planejar em grupo',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol para integração com assistentes de IA',
|
||||
'admin.addons.subtitleBefore': 'Ative ou desative recursos para personalizar sua ',
|
||||
'admin.addons.subtitleAfter': ' experiência.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importe lugares de uma lista compartilhada do Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sincronize voos da sua instância AirTrail auto-hospedada',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Análise por IA',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Lê reservas que o analisador integrado não entende, usando um modelo de IA à sua escolha',
|
||||
'admin.addons.enabled': 'Ativado',
|
||||
'admin.addons.disabled': 'Desativado',
|
||||
'admin.addons.type.trip': 'Viagem',
|
||||
@@ -526,6 +532,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.globalHint': 'Disponível como seção própria na navegação principal',
|
||||
'admin.addons.toast.updated': 'Complemento atualizado',
|
||||
'admin.addons.toast.error': 'Falha ao atualizar complemento',
|
||||
'admin.addons.group.count': '{enabled} de {total} ativados',
|
||||
'admin.addons.noAddons': 'Nenhum complemento disponível',
|
||||
'admin.addons.integrationHint': 'Serviços de backend e integrações de API sem página dedicada',
|
||||
'admin.weather.title': 'Dados meteorológicos',
|
||||
@@ -661,8 +668,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Rastreamento de viagens e diário de viajante com check-ins, fotos e histórias diárias',
|
||||
'admin.addons.catalog.collections.name': 'Coleções',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Biblioteca pessoal de lugares — salve lugares entre viagens em listas nomeadas, copie para qualquer viagem, compartilhe com outras pessoas',
|
||||
'admin.addons.catalog.collections.description': 'Reúna lugares de qualquer viagem em listas nomeadas e reutilize-os',
|
||||
'admin.passkey.title': 'Login com passkey',
|
||||
'admin.passkey.cardHint': 'Permite que os usuários entrem com passkeys (WebAuthn). Desativado por padrão.',
|
||||
'admin.passkey.login': 'Ativar login com passkey',
|
||||
|
||||
@@ -171,6 +171,12 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.subtitle': 'Activa o desactiva funcions per personalitzar la teva experiència a TREK.',
|
||||
'admin.addons.subtitleBefore': 'Activa o desactiva funcions per personalitzar la teva experiència a ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importa llocs des d’una llista compartida de Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sincronitza vols des de la teva instància autoallotjada d’AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Anàlisi amb IA',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Llegeix reserves que l’analitzador integrat no entén, amb un model d’IA que triïs',
|
||||
'admin.addons.enabled': 'Actiu',
|
||||
'admin.addons.disabled': 'Desactivat',
|
||||
'admin.addons.type.trip': 'Viatge',
|
||||
@@ -181,6 +187,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': "Serveis backend i integracions d'API sense pàgina dedicada",
|
||||
'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.noAddons': 'No hi ha complements disponibles',
|
||||
'admin.weather.title': 'Dades meteorològiques',
|
||||
'admin.weather.badge': 'Des del 24 de març de 2026',
|
||||
@@ -275,7 +282,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Llistes',
|
||||
'admin.addons.catalog.packing.description': "Llistes d'equipatge i tasques pendents per als teus viatges",
|
||||
'admin.addons.catalog.budget.name': 'Pressupost',
|
||||
'admin.addons.catalog.budget.description': 'Controla les despeses i planifica el pressupost del viatge',
|
||||
'admin.addons.catalog.budget.description': 'Controla les despeses del viatge i reparteix-les entre els viatgers',
|
||||
'admin.addons.catalog.documents.name': 'Documents',
|
||||
'admin.addons.catalog.documents.description': 'Guarda i gestiona la documentació del viatge',
|
||||
'admin.addons.catalog.vacay.name': 'Vacances',
|
||||
@@ -283,10 +290,9 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atles',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa del món amb els països visitats i estadístiques de viatge',
|
||||
'admin.addons.catalog.collab.name': 'Col·laboració',
|
||||
'admin.addons.catalog.collab.description': 'Notes, enquestes i xat en temps real per organitzar el viatge',
|
||||
'admin.addons.catalog.collab.description': 'Notes, enquestes, xat i suggeriments per planificar en grup',
|
||||
'admin.addons.catalog.collections.name': 'Col·leccions',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Permet agrupar i organitzar complements de catàleg en col·leccions temàtiques o paquets personalitzats.',
|
||||
'admin.addons.catalog.collections.description': 'Recull llocs de qualsevol viatge en llistes amb nom i reutilitza’ls',
|
||||
'admin.oidcOnlyMode': 'Desactivar autenticació per contrasenya',
|
||||
'admin.oidcOnlyModeHint':
|
||||
"Si està activat, només es permet l'inici de sessió amb SSO. L'inici de sessió i registre amb contrasenya es bloquegen.",
|
||||
|
||||
@@ -502,7 +502,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Seznamy',
|
||||
'admin.addons.catalog.packing.description': 'Balicí seznamy a úkoly pro vaše výlety',
|
||||
'admin.addons.catalog.budget.name': 'Náklady',
|
||||
'admin.addons.catalog.budget.description': 'Sledování výdajů a plánování rozpočtu cesty',
|
||||
'admin.addons.catalog.budget.description': 'Sledujte výdaje na cestu a rozdělte je mezi cestující',
|
||||
'admin.addons.catalog.documents.name': 'Dokumenty',
|
||||
'admin.addons.catalog.documents.description': 'Ukládání a správa cestovních dokladů',
|
||||
'admin.addons.catalog.vacay.name': 'Dovolená (Vacay)',
|
||||
@@ -510,7 +510,13 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa světa s navštívenými zeměmi a statistikami',
|
||||
'admin.addons.catalog.collab.name': 'Spolupráce',
|
||||
'admin.addons.catalog.collab.description': 'Poznámky v reálném čase, hlasování a chat pro plánování',
|
||||
'admin.addons.catalog.collab.description': 'Poznámky, ankety, chat a návrhy pro společné plánování',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importujte místa ze sdíleného seznamu Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Synchronizujte lety z vlastní instance AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Analýza pomocí AI',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Přečte rezervace, které vestavěný analyzátor nezvládne, pomocí zvoleného modelu AI',
|
||||
'admin.addons.enabled': 'Povoleno',
|
||||
'admin.addons.disabled': 'Zakázáno',
|
||||
'admin.addons.type.trip': 'Cesta',
|
||||
@@ -521,6 +527,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Backendové služby a API integrace bez vlastní stránky',
|
||||
'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.noAddons': 'Žádné doplňky nejsou k dispozici',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol pro integraci AI asistentů',
|
||||
@@ -655,8 +662,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Cestovní deník',
|
||||
'admin.addons.catalog.journey.description': 'Sledování cest a cestovní deník s odbaveními, fotkami a denními příběhy',
|
||||
'admin.addons.catalog.collections.name': 'Sbírky',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Osobní knihovna míst — ukládejte místa napříč výlety do pojmenovaných seznamů, kopírujte do libovolného výletu, sdílejte s ostatními',
|
||||
'admin.addons.catalog.collections.description': 'Sbírejte místa ze všech cest do pojmenovaných seznamů a znovu je používejte',
|
||||
'admin.passkey.title': 'Přihlášení přístupovým klíčem',
|
||||
'admin.passkey.cardHint':
|
||||
'Umožněte uživatelům přihlašovat se pomocí přístupových klíčů (WebAuthn). Ve výchozím nastavení vypnuto.',
|
||||
|
||||
@@ -509,7 +509,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listen',
|
||||
'admin.addons.catalog.packing.description': 'Packlisten und To-Do-Aufgaben für deine Reisen',
|
||||
'admin.addons.catalog.budget.name': 'Kosten',
|
||||
'admin.addons.catalog.budget.description': 'Ausgaben verfolgen und Reisebudget planen',
|
||||
'admin.addons.catalog.budget.description': 'Reisekosten erfassen und unter den Mitreisenden aufteilen',
|
||||
'admin.addons.catalog.documents.name': 'Dokumente',
|
||||
'admin.addons.catalog.documents.description': 'Reisedokumente speichern und verwalten',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -517,13 +517,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Weltkarte mit besuchten Ländern und Reisestatistiken',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'Echtzeit-Notizen, Umfragen und Chat für die Reiseplanung',
|
||||
'admin.addons.catalog.collab.description': 'Notizen, Umfragen, Chat und Vorschläge für die gemeinsame Planung',
|
||||
'admin.addons.catalog.memories.name': 'Fotos (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Reisefotos über deine Immich-Instanz teilen',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol für die KI-Assistenten-Integration',
|
||||
'admin.addons.subtitleBefore': 'Aktiviere oder deaktiviere Funktionen, um ',
|
||||
'admin.addons.subtitleAfter': ' nach deinen Wünschen anzupassen.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Orte aus einer geteilten Naver-Maps-Liste importieren',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Flüge aus deiner selbst gehosteten AirTrail-Instanz synchronisieren',
|
||||
'admin.addons.catalog.llm_parsing.name': 'KI-Auswertung',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Liest Buchungen, an denen der eingebaute Parser scheitert, mit einem KI-Modell deiner Wahl',
|
||||
'admin.addons.enabled': 'Aktiviert',
|
||||
'admin.addons.disabled': 'Deaktiviert',
|
||||
'admin.addons.type.trip': 'Reise',
|
||||
@@ -534,6 +540,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Backend-Dienste und API-Integrationen ohne eigene Seite',
|
||||
'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.noAddons': 'Keine Addons verfügbar',
|
||||
'admin.weather.title': 'Wetterdaten',
|
||||
'admin.weather.badge': 'Seit 24. März 2026',
|
||||
@@ -668,8 +675,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Journey',
|
||||
'admin.addons.catalog.journey.description': 'Reise-Tracking & Tagebuch mit Check-ins, Fotos und Tagesberichten',
|
||||
'admin.addons.catalog.collections.name': 'Sammlungen',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Persönliche Orte-Bibliothek — speichere Orte aus Reisen in benannten Listen, kopiere sie in jede Reise, teile sie mit anderen',
|
||||
'admin.addons.catalog.collections.description': 'Orte aus allen Reisen in benannten Listen sammeln und wiederverwenden',
|
||||
'admin.passkey.title': 'Passkey-Anmeldung',
|
||||
'admin.passkey.cardHint': 'Erlaube Benutzern die Anmeldung mit Passkeys (WebAuthn). Standardmäßig deaktiviert.',
|
||||
'admin.passkey.login': 'Passkey-Anmeldung aktivieren',
|
||||
|
||||
@@ -547,7 +547,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Lists',
|
||||
'admin.addons.catalog.packing.description': 'Packing lists and to-do tasks for your trips',
|
||||
'admin.addons.catalog.budget.name': 'Costs',
|
||||
'admin.addons.catalog.budget.description': 'Track expenses and plan your trip budget',
|
||||
'admin.addons.catalog.budget.description': 'Track trip expenses and split them between travellers',
|
||||
'admin.addons.catalog.documents.name': 'Documents',
|
||||
'admin.addons.catalog.documents.description': 'Store and manage travel documents',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -555,13 +555,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'World map with visited countries and travel stats',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'Real-time notes, polls, and chat for trip planning',
|
||||
'admin.addons.catalog.collab.description': 'Notes, polls, chat and suggestions for planning together',
|
||||
'admin.addons.catalog.memories.name': 'Photos (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Share trip photos via your Immich instance',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol for AI assistant integration',
|
||||
'admin.addons.subtitleBefore': 'Enable or disable features to customize your ',
|
||||
'admin.addons.subtitleAfter': ' experience.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Import places from a shared Naver Maps list',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sync flights from your self-hosted AirTrail instance',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI Parsing',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Reads bookings the built-in parser cannot, using an AI model you choose',
|
||||
'admin.addons.enabled': 'Enabled',
|
||||
'admin.addons.disabled': 'Disabled',
|
||||
'admin.addons.type.trip': 'Trip',
|
||||
@@ -572,6 +578,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Backend services and API integrations with no dedicated page',
|
||||
'admin.addons.toast.updated': 'Addon updated',
|
||||
'admin.addons.toast.error': 'Failed to update addon',
|
||||
'admin.addons.group.count': '{enabled} of {total} enabled',
|
||||
'admin.addons.noAddons': 'No addons available',
|
||||
'admin.weather.title': 'Weather Data',
|
||||
'admin.weather.badge': 'Since March 24, 2026',
|
||||
@@ -663,8 +670,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Trip tracking & travel journal with check-ins, photos, and daily stories',
|
||||
'admin.addons.catalog.collections.name': 'Collections',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Personal place library — save places across trips into named lists, copy into any trip, share with others',
|
||||
'admin.addons.catalog.collections.description': 'Collect places from any trip into named lists and reuse them',
|
||||
'admin.passkey.title': 'Passkey login',
|
||||
'admin.passkey.cardHint': 'Let users sign in with passkeys (WebAuthn). Off by default.',
|
||||
'admin.passkey.login': 'Enable passkey login',
|
||||
|
||||
@@ -510,6 +510,12 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.subtitle': 'Activa o desactiva funciones para personalizar tu experiencia en TREK.',
|
||||
'admin.addons.subtitleBefore': 'Activa o desactiva funciones para personalizar tu experiencia en ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importa lugares desde una lista compartida de Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sincroniza vuelos desde tu instancia autoalojada de AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Análisis con IA',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Lee reservas que el analizador integrado no entiende, con un modelo de IA que tú elijas',
|
||||
'admin.addons.enabled': 'Activo',
|
||||
'admin.addons.disabled': 'Desactivado',
|
||||
'admin.addons.type.trip': 'Viaje',
|
||||
@@ -520,6 +526,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Servicios backend e integraciones de API sin página dedicada',
|
||||
'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.noAddons': 'No hay complementos disponibles',
|
||||
'admin.weather.title': 'Datos meteorológicos',
|
||||
'admin.weather.badge': 'Desde el 24 de marzo de 2026',
|
||||
@@ -615,7 +622,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listas',
|
||||
'admin.addons.catalog.packing.description': 'Listas de equipaje y tareas pendientes para tus viajes',
|
||||
'admin.addons.catalog.budget.name': 'Costes',
|
||||
'admin.addons.catalog.budget.description': 'Controla los gastos y planifica el presupuesto del viaje',
|
||||
'admin.addons.catalog.budget.description': 'Controla los gastos del viaje y repártelos entre los viajeros',
|
||||
'admin.addons.catalog.documents.name': 'Documentos',
|
||||
'admin.addons.catalog.documents.description': 'Guarda y gestiona la documentación del viaje',
|
||||
'admin.addons.catalog.vacay.name': 'Vacaciones',
|
||||
@@ -623,7 +630,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa del mundo con los países visitados y estadísticas de viaje',
|
||||
'admin.addons.catalog.collab.name': 'Colaboración',
|
||||
'admin.addons.catalog.collab.description': 'Notas, encuestas y chat en tiempo real para organizar el viaje',
|
||||
'admin.addons.catalog.collab.description': 'Notas, encuestas, chat y sugerencias para planificar en grupo',
|
||||
'admin.oidcOnlyMode': 'Desactivar autenticación por contraseña',
|
||||
'admin.oidcOnlyModeHint':
|
||||
'Si está activado, solo se permite el inicio de sesión con SSO. El inicio de sesión y registro con contraseña se bloquean.',
|
||||
@@ -676,8 +683,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Seguimiento de viajes y diario de viajero con registros de ubicación, fotos e historias diarias',
|
||||
'admin.addons.catalog.collections.name': 'Colecciones',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Biblioteca personal de lugares: guarda lugares de tus viajes en listas con nombre, cópialos a cualquier viaje y compártelos con otros',
|
||||
'admin.addons.catalog.collections.description': 'Reúne lugares de cualquier viaje en listas con nombre y reutilízalos',
|
||||
'admin.passkey.title': 'Inicio de sesión con passkey',
|
||||
'admin.passkey.cardHint':
|
||||
'Permite que los usuarios inicien sesión con passkeys (WebAuthn). Desactivado de forma predeterminada.',
|
||||
|
||||
@@ -518,7 +518,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listes',
|
||||
'admin.addons.catalog.packing.description': 'Listes de bagages et tâches à faire pour vos voyages',
|
||||
'admin.addons.catalog.budget.name': 'Coûts',
|
||||
'admin.addons.catalog.budget.description': 'Suivez les dépenses et planifiez votre budget de voyage',
|
||||
'admin.addons.catalog.budget.description': 'Suivez les dépenses du voyage et répartissez-les entre les voyageurs',
|
||||
'admin.addons.catalog.documents.name': 'Documents',
|
||||
'admin.addons.catalog.documents.description': 'Stockez et gérez vos documents de voyage',
|
||||
'admin.addons.catalog.vacay.name': 'Vacances',
|
||||
@@ -526,9 +526,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Carte du monde avec pays visités et statistiques de voyage',
|
||||
'admin.addons.catalog.collab.name': 'Collaboration',
|
||||
'admin.addons.catalog.collab.description': 'Notes en temps réel, sondages et chat pour la planification de voyage',
|
||||
'admin.addons.catalog.collab.description': 'Notes, sondages, chat et suggestions pour planifier à plusieurs',
|
||||
'admin.addons.subtitleBefore': 'Activez ou désactivez des fonctionnalités pour personnaliser votre expérience ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importez des lieux depuis une liste Naver Maps partagée',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Synchronisez les vols depuis votre instance AirTrail auto-hébergée',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Analyse par IA',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Lit les réservations que l’analyseur intégré ne comprend pas, via un modèle d’IA de votre choix',
|
||||
'admin.addons.enabled': 'Activé',
|
||||
'admin.addons.disabled': 'Désactivé',
|
||||
'admin.addons.type.trip': 'Voyage',
|
||||
@@ -539,6 +545,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Services backend et intégrations API sans page dédiée',
|
||||
'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.noAddons': 'Aucune extension disponible',
|
||||
'admin.weather.title': 'Données météo',
|
||||
'admin.weather.badge': 'Depuis le 24 mars 2026',
|
||||
@@ -674,8 +681,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Journal de voyage',
|
||||
'admin.addons.catalog.journey.description': 'Suivi de voyages et journal avec check-ins, photos et récits quotidiens',
|
||||
'admin.addons.catalog.collections.name': 'Collections',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Bibliothèque personnelle de lieux — enregistrez des lieux de vos voyages dans des listes nommées, copiez-les dans n’importe quel voyage et partagez-les',
|
||||
'admin.addons.catalog.collections.description': 'Rassemblez des lieux de tous vos voyages dans des listes nommées et réutilisez-les',
|
||||
'admin.passkey.title': 'Connexion par passkey',
|
||||
'admin.passkey.cardHint':
|
||||
'Permettez aux utilisateurs de se connecter avec des passkeys (WebAuthn). Désactivé par défaut.',
|
||||
|
||||
@@ -563,7 +563,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Λίστες',
|
||||
'admin.addons.catalog.packing.description': 'Λίστες πακεταρίσματος και εργασίες προς εκτέλεση για τα ταξίδια σας',
|
||||
'admin.addons.catalog.budget.name': 'Κόστη',
|
||||
'admin.addons.catalog.budget.description': 'Παρακολουθήστε τα έξοδα και σχεδιάστε τον προϋπολογισμό του ταξιδιού σας',
|
||||
'admin.addons.catalog.budget.description': 'Παρακολουθήστε τα έξοδα του ταξιδιού και μοιράστε τα στους ταξιδιώτες',
|
||||
'admin.addons.catalog.documents.name': 'Έγγραφα',
|
||||
'admin.addons.catalog.documents.description': 'Αποθηκεύστε και διαχειριστείτε ταξιδιωτικά έγγραφα',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -572,8 +572,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.description':
|
||||
'Παγκόσμιος χάρτης με χώρες που έχετε επισκεφθεί και ταξιδιωτικά στατιστικά',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'Σημειώσεις, δημοσκοπήσεις και συνομιλία πραγματικού χρόνου για σχεδιασμό ταξιδιού',
|
||||
'admin.addons.catalog.collab.description': 'Σημειώσεις, ψηφοφορίες, συνομιλία και προτάσεις για κοινό σχεδιασμό',
|
||||
'admin.addons.catalog.memories.name': 'Φωτογραφίες (Immich)',
|
||||
'admin.addons.catalog.memories.description':
|
||||
'Μοιραστείτε φωτογραφίες ταξιδιού μέσω της δικής σας εγκατάστασης Immich',
|
||||
@@ -582,6 +581,12 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.subtitleBefore':
|
||||
'Ενεργοποιήστε ή απενεργοποιήστε λειτουργίες για να εξατομικεύσετε την εμπειρία σας στο ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Εισαγωγή τοποθεσιών από κοινόχρηστη λίστα Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Συγχρονισμός πτήσεων από τη δική σας εγκατάσταση AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Ανάλυση με τεχνητή νοημοσύνη',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Διαβάζει κρατήσεις που δεν καταλαβαίνει ο ενσωματωμένος αναλυτής, με μοντέλο ΤΝ της επιλογής σας',
|
||||
'admin.addons.enabled': 'Ενεργοποιημένο',
|
||||
'admin.addons.disabled': 'Απενεργοποιημένο',
|
||||
'admin.addons.type.trip': 'Ταξίδι',
|
||||
@@ -592,6 +597,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Υπηρεσίες backend και ενσωματώσεις API χωρίς ειδική σελίδα',
|
||||
'admin.addons.toast.updated': 'Το πρόσθετο ενημερώθηκε',
|
||||
'admin.addons.toast.error': 'Αποτυχία ενημέρωσης προσθέτου',
|
||||
'admin.addons.group.count': '{enabled} από {total} ενεργοποιημένα',
|
||||
'admin.addons.noAddons': 'Δεν υπάρχουν διαθέσιμα πρόσθετα',
|
||||
'admin.weather.title': 'Δεδομένα Καιρού',
|
||||
'admin.weather.badge': 'Από 24 Μαρτίου 2026',
|
||||
@@ -684,8 +690,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Παρακολούθηση ταξιδιών & ημερολόγιο ταξιδιών με αφίξεις, φωτογραφίες και καθημερινές ιστορίες',
|
||||
'admin.addons.catalog.collections.name': 'Συλλογές',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Προσωπική βιβλιοθήκη μερών — αποθήκευσε μέρη από όλα τα ταξίδια σε ονομασμένες λίστες, αντίγραψέ τα σε οποιοδήποτε ταξίδι, μοιράσου τα με άλλους',
|
||||
'admin.addons.catalog.collections.description': 'Συγκεντρώστε τοποθεσίες από κάθε ταξίδι σε λίστες με όνομα και χρησιμοποιήστε τις ξανά',
|
||||
'admin.passkey.title': 'Σύνδεση με passkey',
|
||||
'admin.passkey.cardHint':
|
||||
'Επιτρέψτε στους χρήστες να συνδέονται με passkeys (WebAuthn). Απενεργοποιημένο από προεπιλογή.',
|
||||
|
||||
@@ -514,7 +514,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listák',
|
||||
'admin.addons.catalog.packing.description': 'Csomagolási listák és teendők az utazásaidhoz',
|
||||
'admin.addons.catalog.budget.name': 'Költségek',
|
||||
'admin.addons.catalog.budget.description': 'Kiadások nyomon követése és az utazási költségvetés tervezése',
|
||||
'admin.addons.catalog.budget.description': 'Kövesd az utazás kiadásait, és oszd meg őket az utastársak között',
|
||||
'admin.addons.catalog.documents.name': 'Dokumentumok',
|
||||
'admin.addons.catalog.documents.description': 'Úti dokumentumok tárolása és kezelése',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -522,13 +522,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlasz',
|
||||
'admin.addons.catalog.atlas.description': 'Világtérkép meglátogatott országokkal és utazási statisztikákkal',
|
||||
'admin.addons.catalog.collab.name': 'Együttműködés',
|
||||
'admin.addons.catalog.collab.description': 'Valós idejű jegyzetek, szavazások és csevegés az utazás tervezéséhez',
|
||||
'admin.addons.catalog.collab.description': 'Jegyzetek, szavazások, csevegés és javaslatok a közös tervezéshez',
|
||||
'admin.addons.catalog.memories.name': 'Fotók (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Utazási fotók megosztása az Immich példányon keresztül',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol AI asszisztens integrációhoz',
|
||||
'admin.addons.subtitleBefore': 'Funkciók engedélyezése vagy letiltása a ',
|
||||
'admin.addons.subtitleAfter': ' testreszabásához.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Helyek importálása megosztott Naver Maps-listából',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Járatok szinkronizálása a saját üzemeltetésű AirTrail-példányodból',
|
||||
'admin.addons.catalog.llm_parsing.name': 'MI-elemzés',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Beolvassa azokat a foglalásokat, amelyeket a beépített értelmező nem tud, az általad választott MI-modellel',
|
||||
'admin.addons.enabled': 'Engedélyezve',
|
||||
'admin.addons.disabled': 'Letiltva',
|
||||
'admin.addons.type.trip': 'Utazás',
|
||||
@@ -539,6 +545,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Háttérszolgáltatások és API integrációk dedikált oldal nélkül',
|
||||
'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.noAddons': 'Nincsenek elérhető bővítmények',
|
||||
'admin.weather.title': 'Időjárás adatok',
|
||||
'admin.weather.badge': '2026. március 24. óta',
|
||||
@@ -674,8 +681,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Utazáskövetés és útinapló bejelentkezésekkel, fotókkal és napi történetekkel',
|
||||
'admin.addons.catalog.collections.name': 'Gyűjtemények',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Személyes helykönyvtár — ments helyeket utazásokon átívelően elnevezett listákba, másold bármelyik utazásba, oszd meg másokkal',
|
||||
'admin.addons.catalog.collections.description': 'Gyűjtsd össze bármely utazás helyeit elnevezett listákba, és használd fel őket újra',
|
||||
'admin.passkey.title': 'Passkey-bejelentkezés',
|
||||
'admin.passkey.cardHint':
|
||||
'Engedélyezd a felhasználóknak a bejelentkezést passkey-vel (WebAuthn). Alapból kikapcsolva.',
|
||||
|
||||
@@ -556,7 +556,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Daftar',
|
||||
'admin.addons.catalog.packing.description': 'Daftar packing dan tugas to-do untuk perjalananmu',
|
||||
'admin.addons.catalog.budget.name': 'Biaya',
|
||||
'admin.addons.catalog.budget.description': 'Lacak pengeluaran dan rencanakan anggaran perjalananmu',
|
||||
'admin.addons.catalog.budget.description': 'Catat pengeluaran perjalanan dan bagi di antara para pelancong',
|
||||
'admin.addons.catalog.documents.name': 'Dokumen',
|
||||
'admin.addons.catalog.documents.description': 'Simpan dan kelola dokumen perjalanan',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -564,13 +564,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Peta dunia dengan negara yang pernah dikunjungi dan statistik perjalanan',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'Catatan real-time, polling, dan chat untuk perencanaan perjalanan',
|
||||
'admin.addons.catalog.collab.description': 'Catatan, jajak pendapat, obrolan, dan saran untuk merencanakan bersama',
|
||||
'admin.addons.catalog.memories.name': 'Foto (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Bagikan foto perjalanan melalui instans Immich kamu',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol untuk integrasi asisten AI',
|
||||
'admin.addons.subtitleBefore': 'Aktifkan atau nonaktifkan fitur untuk menyesuaikan ',
|
||||
'admin.addons.subtitleAfter': ' kamu.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Impor tempat dari daftar Naver Maps yang dibagikan',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sinkronkan penerbangan dari instans AirTrail milikmu sendiri',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Analisis AI',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Membaca pemesanan yang tidak dipahami pengurai bawaan, dengan model AI pilihanmu',
|
||||
'admin.addons.enabled': 'Aktif',
|
||||
'admin.addons.disabled': 'Nonaktif',
|
||||
'admin.addons.type.trip': 'Perjalanan',
|
||||
@@ -581,6 +587,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Layanan backend dan integrasi API tanpa halaman tersendiri',
|
||||
'admin.addons.toast.updated': 'Addon diperbarui',
|
||||
'admin.addons.toast.error': 'Gagal memperbarui addon',
|
||||
'admin.addons.group.count': '{enabled} dari {total} aktif',
|
||||
'admin.addons.noAddons': 'Tidak ada addon yang tersedia',
|
||||
'admin.weather.title': 'Data Cuaca',
|
||||
'admin.weather.badge': 'Sejak 24 Maret 2026',
|
||||
@@ -671,8 +678,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Journey',
|
||||
'admin.addons.catalog.journey.description': 'Pelacakan perjalanan & jurnal dengan check-in, foto, dan cerita harian',
|
||||
'admin.addons.catalog.collections.name': 'Koleksi',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Pustaka tempat pribadi — simpan tempat lintas perjalanan ke dalam daftar bernama, salin ke perjalanan mana pun, bagikan dengan orang lain',
|
||||
'admin.addons.catalog.collections.description': 'Kumpulkan tempat dari perjalanan mana pun ke dalam daftar bernama dan gunakan lagi',
|
||||
'admin.passkey.title': 'Login dengan passkey',
|
||||
'admin.passkey.cardHint': 'Izinkan pengguna masuk dengan passkey (WebAuthn). Nonaktif secara default.',
|
||||
'admin.passkey.login': 'Aktifkan login dengan passkey',
|
||||
|
||||
@@ -509,7 +509,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Liste',
|
||||
'admin.addons.catalog.packing.description': 'Liste di imballaggio e attività da svolgere per i tuoi viaggi',
|
||||
'admin.addons.catalog.budget.name': 'Costi',
|
||||
'admin.addons.catalog.budget.description': 'Tieni traccia delle spese e pianifica il budget del tuo viaggio',
|
||||
'admin.addons.catalog.budget.description': 'Tieni traccia delle spese del viaggio e dividile tra i viaggiatori',
|
||||
'admin.addons.catalog.documents.name': 'Documenti',
|
||||
'admin.addons.catalog.documents.description': 'Archivia e gestisci i documenti di viaggio',
|
||||
'admin.addons.catalog.vacay.name': 'Ferie',
|
||||
@@ -517,13 +517,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlante',
|
||||
'admin.addons.catalog.atlas.description': 'Mappa del mondo con paesi visitati e statistiche di viaggio',
|
||||
'admin.addons.catalog.collab.name': 'Collaborazione',
|
||||
'admin.addons.catalog.collab.description': 'Note, sondaggi e chat in tempo reale per la pianificazione del viaggio',
|
||||
'admin.addons.catalog.collab.description': 'Note, sondaggi, chat e suggerimenti per pianificare insieme',
|
||||
'admin.addons.catalog.memories.name': 'Foto (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Condividi le foto del viaggio tramite la tua istanza Immich',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': "Model Context Protocol per l'integrazione di assistenti AI",
|
||||
'admin.addons.subtitleBefore': 'Abilita o disabilita le funzionalità per personalizzare la tua ',
|
||||
'admin.addons.subtitleAfter': ' esperienza.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importa luoghi da un elenco condiviso di Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Sincronizza i voli dalla tua istanza AirTrail self-hosted',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Analisi con IA',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Legge le prenotazioni che l’analizzatore integrato non riesce a interpretare, con un modello di IA a tua scelta',
|
||||
'admin.addons.enabled': 'Abilitato',
|
||||
'admin.addons.disabled': 'Disabilitato',
|
||||
'admin.addons.type.trip': 'Viaggio',
|
||||
@@ -534,6 +540,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Servizi backend e integrazioni API senza pagina dedicata',
|
||||
'admin.addons.toast.updated': 'Modulo aggiornato',
|
||||
'admin.addons.toast.error': 'Impossibile aggiornare il modulo',
|
||||
'admin.addons.group.count': '{enabled} di {total} attivi',
|
||||
'admin.addons.noAddons': 'Nessun modulo disponibile',
|
||||
'admin.weather.title': 'Dati meteo',
|
||||
'admin.weather.badge': 'Dal 24 marzo 2026',
|
||||
@@ -669,8 +676,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Diario di viaggio',
|
||||
'admin.addons.catalog.journey.description': 'Tracciamento viaggi e diario con check-in, foto e storie quotidiane',
|
||||
'admin.addons.catalog.collections.name': 'Raccolte',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Libreria personale di luoghi: salva luoghi dai tuoi viaggi in liste con nome, copiali in qualsiasi viaggio e condividili con altri',
|
||||
'admin.addons.catalog.collections.description': 'Raccogli luoghi da tutti i viaggi in elenchi con nome e riutilizzali',
|
||||
'admin.passkey.title': 'Accesso con passkey',
|
||||
'admin.passkey.cardHint':
|
||||
'Consenti agli utenti di accedere con le passkey (WebAuthn). Disattivato per impostazione predefinita.',
|
||||
|
||||
@@ -521,7 +521,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'リスト',
|
||||
'admin.addons.catalog.packing.description': '旅行用の持ち物リストとToDo',
|
||||
'admin.addons.catalog.budget.name': '費用',
|
||||
'admin.addons.catalog.budget.description': '支出の管理と予算計画',
|
||||
'admin.addons.catalog.budget.description': '旅行の支出を記録し、同行者と分担',
|
||||
'admin.addons.catalog.documents.name': 'ドキュメント',
|
||||
'admin.addons.catalog.documents.description': '旅行書類の保存・管理',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -529,13 +529,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': '訪問国と旅行統計の世界地図',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'リアルタイムのメモ、投票、チャット',
|
||||
'admin.addons.catalog.collab.description': 'メモ、投票、チャット、提案で一緒に計画',
|
||||
'admin.addons.catalog.memories.name': '写真(Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Immichで旅行写真を共有',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'AI連携のためのModel Context Protocol',
|
||||
'admin.addons.subtitleBefore': '機能を有効/無効にして ',
|
||||
'admin.addons.subtitleAfter': ' をカスタマイズします。',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': '共有された Naver マップのリストから場所をインポート',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': '自分でホストしている AirTrail から フライトを同期',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI 解析',
|
||||
'admin.addons.catalog.llm_parsing.description': '内蔵パーサーが読めない予約を、選んだ AI モデルで読み取ります',
|
||||
'admin.addons.enabled': '有効',
|
||||
'admin.addons.disabled': '無効',
|
||||
'admin.addons.type.trip': '旅行',
|
||||
@@ -546,6 +552,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': '専用ページのないバックエンド/API連携',
|
||||
'admin.addons.toast.updated': 'アドオンを更新しました',
|
||||
'admin.addons.toast.error': 'アドオンの更新に失敗しました',
|
||||
'admin.addons.group.count': '{total} 件中 {enabled} 件が有効',
|
||||
'admin.addons.noAddons': '利用可能なアドオンはありません',
|
||||
'admin.weather.title': '天気データ',
|
||||
'admin.weather.badge': '2026年3月24日以降',
|
||||
@@ -635,8 +642,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': '日記',
|
||||
'admin.addons.catalog.journey.description': 'チェックイン、写真、日ごとのストーリーで旅を記録',
|
||||
'admin.addons.catalog.collections.name': 'コレクション',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'個人の場所ライブラリ — 旅行をまたいで場所を名前付きリストに保存し、どの旅行にもコピーして、他の人と共有',
|
||||
'admin.addons.catalog.collections.description': 'どの旅行の場所も名前付きリストにまとめて再利用',
|
||||
'admin.passkey.title': 'パスキーログイン',
|
||||
'admin.passkey.cardHint': 'ユーザーがパスキー(WebAuthn)でサインインできるようにします。既定では無効です。',
|
||||
'admin.passkey.login': 'パスキーログインを有効化',
|
||||
|
||||
@@ -521,7 +521,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': '목록',
|
||||
'admin.addons.catalog.packing.description': '여행을 위한 짐 목록 및 할 일 작업',
|
||||
'admin.addons.catalog.budget.name': '비용',
|
||||
'admin.addons.catalog.budget.description': '지출 추적 및 여행 예산 계획',
|
||||
'admin.addons.catalog.budget.description': '여행 지출을 기록하고 동행자끼리 나누기',
|
||||
'admin.addons.catalog.documents.name': '문서',
|
||||
'admin.addons.catalog.documents.description': '여행 서류 저장 및 관리',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -529,13 +529,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': '방문한 나라와 여행 통계가 있는 세계 지도',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': '여행 계획을 위한 실시간 메모, 투표, 채팅',
|
||||
'admin.addons.catalog.collab.description': '메모, 투표, 채팅, 제안으로 함께 계획하기',
|
||||
'admin.addons.catalog.memories.name': '사진 (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Immich 인스턴스를 통해 여행 사진 공유',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'AI 어시스턴트 통합을 위한 모델 컨텍스트 프로토콜',
|
||||
'admin.addons.subtitleBefore': '기능을 활성화 또는 비활성화하여 ',
|
||||
'admin.addons.subtitleAfter': ' 경험을 맞춤 설정하세요.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': '공유된 네이버 지도 목록에서 장소 가져오기',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': '직접 운영하는 AirTrail 인스턴스에서 항공편 동기화',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI 분석',
|
||||
'admin.addons.catalog.llm_parsing.description': '기본 분석기가 읽지 못하는 예약을 원하는 AI 모델로 읽어냅니다',
|
||||
'admin.addons.enabled': '활성화됨',
|
||||
'admin.addons.disabled': '비활성화됨',
|
||||
'admin.addons.type.trip': '여행',
|
||||
@@ -546,6 +552,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': '전용 페이지가 없는 백엔드 서비스 및 API 통합',
|
||||
'admin.addons.toast.updated': '애드온이 업데이트되었습니다',
|
||||
'admin.addons.toast.error': '애드온 업데이트 실패',
|
||||
'admin.addons.group.count': '{total}개 중 {enabled}개 활성화됨',
|
||||
'admin.addons.noAddons': '사용 가능한 애드온이 없습니다',
|
||||
'admin.weather.title': '날씨 데이터',
|
||||
'admin.weather.badge': '2026년 3월 24일부터',
|
||||
@@ -633,8 +640,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Journey',
|
||||
'admin.addons.catalog.journey.description': '체크인, 사진, 일별 이야기가 있는 여행 기록 및 여행 일지',
|
||||
'admin.addons.catalog.collections.name': '컬렉션',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'개인 장소 라이브러리 — 여행 전반의 장소를 이름이 있는 목록에 저장하고, 어떤 여행에도 복사하고, 다른 사람과 공유하세요',
|
||||
'admin.addons.catalog.collections.description': '어떤 여행의 장소든 이름 붙인 목록에 모아 다시 사용',
|
||||
'admin.passkey.title': '패스키 로그인',
|
||||
'admin.passkey.cardHint': '사용자가 패스키(WebAuthn)로 로그인할 수 있게 합니다. 기본값은 꺼짐입니다.',
|
||||
'admin.passkey.login': '패스키 로그인 활성화',
|
||||
|
||||
@@ -512,7 +512,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Lijsten',
|
||||
'admin.addons.catalog.packing.description': 'Paklijsten en to-dotaken voor je reizen',
|
||||
'admin.addons.catalog.budget.name': 'Onkosten',
|
||||
'admin.addons.catalog.budget.description': 'Houd uitgaven bij en plan je reisbudget',
|
||||
'admin.addons.catalog.budget.description': 'Houd reiskosten bij en verdeel ze onder de reisgenoten',
|
||||
'admin.addons.catalog.documents.name': 'Documenten',
|
||||
'admin.addons.catalog.documents.description': 'Bewaar en beheer reisdocumenten',
|
||||
'admin.addons.catalog.vacay.name': 'Vakantie',
|
||||
@@ -520,9 +520,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Wereldkaart met bezochte landen en reisstatistieken',
|
||||
'admin.addons.catalog.collab.name': 'Samenwerking',
|
||||
'admin.addons.catalog.collab.description': 'Realtime notities, polls en chat voor het plannen van reizen',
|
||||
'admin.addons.catalog.collab.description': 'Notities, peilingen, chat en suggesties om samen te plannen',
|
||||
'admin.addons.subtitleBefore': 'Schakel functies in of uit om je ',
|
||||
'admin.addons.subtitleAfter': '-ervaring aan te passen.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importeer plaatsen uit een gedeelde Naver Maps-lijst',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Synchroniseer vluchten vanuit je zelfgehoste AirTrail-instantie',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI-analyse',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Leest boekingen die de ingebouwde parser niet begrijpt, met een AI-model naar keuze',
|
||||
'admin.addons.enabled': 'Ingeschakeld',
|
||||
'admin.addons.disabled': 'Uitgeschakeld',
|
||||
'admin.addons.type.trip': 'Reis',
|
||||
@@ -533,6 +539,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Backenddiensten en API-integraties zonder eigen pagina',
|
||||
'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.noAddons': 'Geen add-ons beschikbaar',
|
||||
'admin.weather.title': 'Weergegevens',
|
||||
'admin.weather.badge': 'Sinds 24 maart 2026',
|
||||
@@ -668,8 +675,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Reisverslag',
|
||||
'admin.addons.catalog.journey.description': "Reistracking & reisdagboek met check-ins, foto's en dagelijkse verhalen",
|
||||
'admin.addons.catalog.collections.name': 'Collecties',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Persoonlijke plekkenbibliotheek — sla plekken uit je reizen op in benoemde lijsten, kopieer ze naar elke reis en deel ze met anderen',
|
||||
'admin.addons.catalog.collections.description': 'Verzamel plaatsen uit al je reizen in benoemde lijsten en gebruik ze opnieuw',
|
||||
'admin.passkey.title': 'Inloggen met passkey',
|
||||
'admin.passkey.cardHint': 'Laat gebruikers inloggen met passkeys (WebAuthn). Standaard uit.',
|
||||
'admin.passkey.login': 'Inloggen met passkey inschakelen',
|
||||
|
||||
@@ -506,7 +506,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listy',
|
||||
'admin.addons.catalog.packing.description': 'Listy pakowania i zadania do wykonania dla Twoich podróży',
|
||||
'admin.addons.catalog.budget.name': 'Koszty',
|
||||
'admin.addons.catalog.budget.description': 'Śledź wydatki i planuj budżet podróży',
|
||||
'admin.addons.catalog.budget.description': 'Śledź wydatki na podróż i dziel je między uczestników',
|
||||
'admin.addons.catalog.documents.name': 'Dokumenty',
|
||||
'admin.addons.catalog.documents.description': 'Przechowuj i zarządzaj dokumentami podróżnymi',
|
||||
'admin.addons.catalog.vacay.name': 'Urlopy',
|
||||
@@ -514,13 +514,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Mapa świata z odwiedzonymi krajami i statystykami podróży',
|
||||
'admin.addons.catalog.collab.name': 'Współpraca',
|
||||
'admin.addons.catalog.collab.description': 'Notatki w czasie rzeczywistym, ankiety i czat do planowania podróży',
|
||||
'admin.addons.catalog.collab.description': 'Notatki, ankiety, czat i podpowiedzi do wspólnego planowania',
|
||||
'admin.addons.catalog.memories.name': 'Zdjęcia (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Udostępniaj zdjęcia z podróży za pośrednictwem swojej instancji Immich',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Model Context Protocol dla integracji asystenta AI',
|
||||
'admin.addons.subtitleBefore': 'Włączaj lub wyłączaj funkcje, aby dostosować swoje doświadczenie w ',
|
||||
'admin.addons.subtitleAfter': '.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importuj miejsca z udostępnionej listy Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Synchronizuj loty z własnej instancji AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Analiza AI',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Odczytuje rezerwacje, których nie rozumie wbudowany parser, przy użyciu wybranego modelu AI',
|
||||
'admin.addons.enabled': 'Włączone',
|
||||
'admin.addons.disabled': 'Wyłączone',
|
||||
'admin.addons.type.trip': 'Podróż',
|
||||
@@ -531,6 +537,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Usługi backendowe i integracje API bez dedykowanej strony',
|
||||
'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.noAddons': 'Brak dostępnych dodatków',
|
||||
'admin.weather.title': 'Dane pogodowe',
|
||||
'admin.weather.badge': 'Od 24 marca 2026',
|
||||
@@ -677,8 +684,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Śledzenie podróży i dziennik z zameldowaniami, zdjęciami i codziennymi historiami',
|
||||
'admin.addons.catalog.collections.name': 'Kolekcje',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Osobista biblioteka miejsc — zapisuj miejsca z podróży na nazwanych listach, kopiuj je do dowolnej podróży i udostępniaj innym',
|
||||
'admin.addons.catalog.collections.description': 'Zbieraj miejsca ze wszystkich podróży na nazwane listy i używaj ich ponownie',
|
||||
'admin.passkey.title': 'Logowanie kluczem dostępu',
|
||||
'admin.passkey.cardHint': 'Pozwól użytkownikom logować się kluczami dostępu (WebAuthn). Domyślnie wyłączone.',
|
||||
'admin.passkey.login': 'Włącz logowanie kluczem dostępu',
|
||||
|
||||
@@ -514,7 +514,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Списки',
|
||||
'admin.addons.catalog.packing.description': 'Списки вещей и задачи для ваших поездок',
|
||||
'admin.addons.catalog.budget.name': 'Расходы',
|
||||
'admin.addons.catalog.budget.description': 'Отслеживайте расходы и планируйте бюджет поездки',
|
||||
'admin.addons.catalog.budget.description': 'Учитывайте расходы на поездку и делите их между участниками',
|
||||
'admin.addons.catalog.documents.name': 'Документы',
|
||||
'admin.addons.catalog.documents.description': 'Храните и управляйте документами для путешествий',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -522,9 +522,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Карта мира с посещёнными странами и статистикой путешествий',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'Заметки в реальном времени, опросы и чат для планирования поездок',
|
||||
'admin.addons.catalog.collab.description': 'Заметки, опросы, чат и подсказки для совместного планирования',
|
||||
'admin.addons.subtitleBefore': 'Включайте или отключайте функции для настройки ',
|
||||
'admin.addons.subtitleAfter': ' под себя.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Импорт мест из общего списка Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Синхронизация рейсов с вашего сервера AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Разбор с помощью ИИ',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Читает брони, которые не понимает встроенный анализатор, с помощью выбранной вами модели ИИ',
|
||||
'admin.addons.enabled': 'Включено',
|
||||
'admin.addons.disabled': 'Отключено',
|
||||
'admin.addons.type.trip': 'Поездка',
|
||||
@@ -535,6 +541,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Фоновые сервисы и API-интеграции без отдельной страницы',
|
||||
'admin.addons.toast.updated': 'Дополнение обновлено',
|
||||
'admin.addons.toast.error': 'Не удалось обновить дополнение',
|
||||
'admin.addons.group.count': 'Включено {enabled} из {total}',
|
||||
'admin.addons.noAddons': 'Нет доступных дополнений',
|
||||
'admin.weather.title': 'Данные о погоде',
|
||||
'admin.weather.badge': 'С 24 марта 2026',
|
||||
@@ -669,8 +676,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Отслеживание поездок и дневник путешествий с отметками, фото и ежедневными историями',
|
||||
'admin.addons.catalog.collections.name': 'Коллекции',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Личная библиотека мест — сохраняйте места из разных поездок в именованные списки, копируйте в любую поездку, делитесь с другими',
|
||||
'admin.addons.catalog.collections.description': 'Собирайте места из любых поездок в именованные списки и используйте их снова',
|
||||
'admin.passkey.title': 'Вход по passkey',
|
||||
'admin.passkey.cardHint': 'Разрешите пользователям входить с помощью passkeys (WebAuthn). По умолчанию выключено.',
|
||||
'admin.passkey.login': 'Включить вход по passkey',
|
||||
|
||||
@@ -567,7 +567,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listor',
|
||||
'admin.addons.catalog.packing.description': 'Packlistor och saker att göra inför dina resor',
|
||||
'admin.addons.catalog.budget.name': 'Kostnader',
|
||||
'admin.addons.catalog.budget.description': 'Håll koll på utgifterna och planera din resebudget',
|
||||
'admin.addons.catalog.budget.description': 'Håll koll på resans utgifter och dela dem mellan resenärerna',
|
||||
'admin.addons.catalog.documents.name': 'Dokument',
|
||||
'admin.addons.catalog.documents.description': 'Spara och hantera resedokument',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -575,13 +575,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Världskarta med besökta länder och resestatistik',
|
||||
'admin.addons.catalog.collab.name': 'Samarbete',
|
||||
'admin.addons.catalog.collab.description': 'Noteringar, omröstningar och chatt i realtid för resplanering',
|
||||
'admin.addons.catalog.collab.description': 'Anteckningar, omröstningar, chatt och förslag för gemensam planering',
|
||||
'admin.addons.catalog.memories.name': 'Foton (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Dela resefoton via din Immich-instans',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Modellkontextprotokoll för integration av AI-assistenter',
|
||||
'admin.addons.subtitleBefore': 'Aktivera eller inaktivera funktioner för att anpassa din ',
|
||||
'admin.addons.subtitleAfter': ' erfarenhet.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Importera platser från en delad Naver Maps-lista',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Synkronisera flyg från din egen AirTrail-instans',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI-tolkning',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Läser bokningar som den inbyggda tolken inte klarar, med en AI-modell du väljer',
|
||||
'admin.addons.enabled': 'Aktiverad',
|
||||
'admin.addons.disabled': 'Inaktiverad',
|
||||
'admin.addons.type.trip': 'Resa',
|
||||
@@ -592,6 +598,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Backend-tjänster och API-integrationer utan egen sida',
|
||||
'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.noAddons': 'Inga tillägg tillgängliga',
|
||||
'admin.weather.title': 'Väderdata',
|
||||
'admin.weather.badge': 'Sedan den 24 mars 2026',
|
||||
@@ -684,8 +691,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': 'Journey',
|
||||
'admin.addons.catalog.journey.description': 'Resespårning och resedagbok med incheckningar, foton och dagliga inlägg',
|
||||
'admin.addons.catalog.collections.name': 'Samlingar',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Personligt platsbibliotek — spara platser mellan resor i namngivna listor, kopiera till valfri resa, dela med andra',
|
||||
'admin.addons.catalog.collections.description': 'Samla platser från alla resor i namngivna listor och återanvänd dem',
|
||||
'admin.passkey.title': 'Inloggningsnyckel',
|
||||
'admin.passkey.cardHint': 'Låter användare att logga in med inloggningsnyckel (WebAuthn). Avstängt som standard.',
|
||||
'admin.passkey.login': 'Aktivera inloggningsnyckel',
|
||||
|
||||
@@ -553,7 +553,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Listeler',
|
||||
'admin.addons.catalog.packing.description': 'Seyahatleriniz için paket listeleri ve yapılacak işler',
|
||||
'admin.addons.catalog.budget.name': 'Maliyetler',
|
||||
'admin.addons.catalog.budget.description': 'Harcamaları takip edin ve seyahat bütçenizi planlayın',
|
||||
'admin.addons.catalog.budget.description': 'Gezi masraflarını takip edin ve yolcular arasında paylaştırın',
|
||||
'admin.addons.catalog.documents.name': 'Belgeler',
|
||||
'admin.addons.catalog.documents.description': 'Seyahat belgelerini saklayın ve yönetin',
|
||||
'admin.addons.catalog.vacay.name': 'Tatil',
|
||||
@@ -561,7 +561,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Ziyaret edilen ülkeleri ve seyahat istatistiklerini içeren dünya haritası',
|
||||
'admin.addons.catalog.collab.name': 'İş birliği',
|
||||
'admin.addons.catalog.collab.description': 'Seyahat planlaması için gerçek zamanlı notlar, anketler ve sohbet',
|
||||
'admin.addons.catalog.collab.description': 'Birlikte planlamak için notlar, anketler, sohbet ve öneriler',
|
||||
'admin.addons.catalog.memories.name': 'Fotoğraflar (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Immich örneğiniz aracılığıyla gezi fotoğraflarını paylaşın',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
@@ -569,6 +569,12 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.subtitleBefore':
|
||||
'Kişiselleştirmenizi özelleştirmek için özellikleri etkinleştirin veya devre dışı bırakın.',
|
||||
'admin.addons.subtitleAfter': 'deneyim.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Paylaşılan bir Naver Haritalar listesinden yerleri içe aktarın',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Kendi sunucunuzdaki AirTrail örneğinden uçuşları eşitleyin',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Yapay zekâ ile ayrıştırma',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Yerleşik ayrıştırıcının okuyamadığı rezervasyonları, seçtiğiniz yapay zekâ modeliyle okur',
|
||||
'admin.addons.enabled': 'Etkinleştirilmiş',
|
||||
'admin.addons.disabled': 'Engelli',
|
||||
'admin.addons.type.trip': 'Seyahat',
|
||||
@@ -579,6 +585,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Özel bir sayfa olmadan arka uç hizmetleri ve API entegrasyonları',
|
||||
'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.noAddons': 'Kullanılabilir eklenti yok',
|
||||
'admin.weather.title': 'Hava Durumu Verileri',
|
||||
'admin.weather.badge': "24 Mart 2026'dan beri",
|
||||
@@ -671,8 +678,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Check-in, fotoğraf ve günlük hikâyelerle seyahat takibi ve seyahat günlüğü',
|
||||
'admin.addons.catalog.collections.name': 'Koleksiyonlar',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Kişisel yer kitaplığı — yerleri geziler arasında adlandırılmış listelere kaydet, herhangi bir geziye kopyala, başkalarıyla paylaş',
|
||||
'admin.addons.catalog.collections.description': 'Her geziden yerleri adlandırılmış listelerde toplayın ve yeniden kullanın',
|
||||
'admin.passkey.title': 'Passkey ile oturum açma',
|
||||
'admin.passkey.cardHint':
|
||||
'Kullanıcıların passkey (WebAuthn) ile oturum açmasına izin verin. Varsayılan olarak kapalı.',
|
||||
|
||||
@@ -511,7 +511,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Списки',
|
||||
'admin.addons.catalog.packing.description': 'Списки речей та завдання для ваших подорожей',
|
||||
'admin.addons.catalog.budget.name': 'Витрати',
|
||||
'admin.addons.catalog.budget.description': 'Відстежуйте витрати та плануйте бюджет подорожі',
|
||||
'admin.addons.catalog.budget.description': 'Стежте за витратами на подорож і діліть їх між учасниками',
|
||||
'admin.addons.catalog.documents.name': 'Документи',
|
||||
'admin.addons.catalog.documents.description': 'Зберігайте та керуйте документами для подорожей',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -519,9 +519,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': 'Карта світу з відвіданими країнами та статистикою подорожей',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': 'Нотатки в реальному часі, опитування та чат для планування подорожей',
|
||||
'admin.addons.catalog.collab.description': 'Нотатки, опитування, чат і підказки для спільного планування',
|
||||
'admin.addons.subtitleBefore': 'Увімкніть або вимкніть функції для налаштування ',
|
||||
'admin.addons.subtitleAfter': ' під себе.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Імпорт місць зі спільного списку Naver Maps',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Синхронізація рейсів із вашого сервера AirTrail',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Розбір за допомогою ШІ',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Читає бронювання, які не розпізнає вбудований аналізатор, за допомогою обраної вами моделі ШІ',
|
||||
'admin.addons.enabled': 'Увімкнено',
|
||||
'admin.addons.disabled': 'Вимкнено',
|
||||
'admin.addons.type.trip': 'Поїздка',
|
||||
@@ -532,6 +538,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Фонові сервіси та API-інтеграції без окремої сторінки',
|
||||
'admin.addons.toast.updated': 'Доповнення оновлено',
|
||||
'admin.addons.toast.error': 'Не вдалося оновити доповнення',
|
||||
'admin.addons.group.count': 'Увімкнено {enabled} з {total}',
|
||||
'admin.addons.noAddons': 'Немає доступних доповнень',
|
||||
'admin.weather.title': 'Дані про погоду',
|
||||
'admin.weather.badge': 'З 24 березня 2026',
|
||||
@@ -665,8 +672,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Відстеження поїздок і щоденник подорожей з позначками, фото та щоденними історіями',
|
||||
'admin.addons.catalog.collections.name': 'Колекції',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Особиста бібліотека місць — зберігайте місця з різних подорожей у іменовані списки, копіюйте в будь-яку подорож, діліться з іншими',
|
||||
'admin.addons.catalog.collections.description': 'Збирайте місця з будь-яких подорожей у іменовані списки та використовуйте їх знову',
|
||||
'admin.passkey.title': 'Вхід за допомогою passkey',
|
||||
'admin.passkey.cardHint':
|
||||
'Дозволити користувачам входити за допомогою passkey (WebAuthn). За замовчуванням вимкнено.',
|
||||
|
||||
@@ -509,7 +509,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': 'Danh sách',
|
||||
'admin.addons.catalog.packing.description': 'Danh sách đóng gói và nhiệm vụ cần làm cho chuyến đi của bạn',
|
||||
'admin.addons.catalog.budget.name': 'Chi phí',
|
||||
'admin.addons.catalog.budget.description': 'Theo dõi chi phí và lập kế hoạch ngân sách chuyến đi của bạn',
|
||||
'admin.addons.catalog.budget.description': 'Theo dõi chi phí chuyến đi và chia cho những người cùng đi',
|
||||
'admin.addons.catalog.documents.name': 'Tài liệu',
|
||||
'admin.addons.catalog.documents.description': 'Lưu trữ và quản lý tài liệu du lịch',
|
||||
'admin.addons.catalog.vacay.name': 'Kỳ nghỉ',
|
||||
@@ -517,14 +517,19 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Bản đồ',
|
||||
'admin.addons.catalog.atlas.description': 'Bản đồ thế giới với các quốc gia đã ghé thăm và số liệu thống kê du lịch',
|
||||
'admin.addons.catalog.collab.name': 'Cộng tác',
|
||||
'admin.addons.catalog.collab.description':
|
||||
'Ghi chú, thăm dò ý kiến và trò chuyện theo thời gian thực để lập kế hoạch chuyến đi',
|
||||
'admin.addons.catalog.collab.description': 'Ghi chú, bình chọn, trò chuyện và gợi ý để cùng lên kế hoạch',
|
||||
'admin.addons.catalog.memories.name': 'Hình ảnh (Immich)',
|
||||
'admin.addons.catalog.memories.description': 'Chia sẻ ảnh chuyến đi qua phiên bản Immich của bạn',
|
||||
'admin.addons.catalog.mcp.name': 'MCP',
|
||||
'admin.addons.catalog.mcp.description': 'Giao thức ngữ cảnh mẫu để tích hợp trợ lý AI',
|
||||
'admin.addons.subtitleBefore': 'Bật hoặc tắt các tính năng để tùy chỉnh',
|
||||
'admin.addons.subtitleAfter': 'kinh nghiệm.',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': 'Nhập địa điểm từ danh sách Naver Maps được chia sẻ',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': 'Đồng bộ chuyến bay từ máy chủ AirTrail của bạn',
|
||||
'admin.addons.catalog.llm_parsing.name': 'Phân tích bằng AI',
|
||||
'admin.addons.catalog.llm_parsing.description': 'Đọc các đặt chỗ mà bộ phân tích tích hợp không hiểu, bằng mô hình AI bạn chọn',
|
||||
'admin.addons.enabled': 'Đã bật',
|
||||
'admin.addons.disabled': 'Tắt',
|
||||
'admin.addons.type.trip': 'Chuyến đi',
|
||||
@@ -535,6 +540,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': 'Dịch vụ phụ trợ và tích hợp API không có trang chuyên dụng',
|
||||
'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.noAddons': 'Không có tiện ích bổ sung nào',
|
||||
'admin.weather.title': 'Dữ liệu thời tiết',
|
||||
'admin.weather.badge': 'Kể từ ngày 24 tháng 3 năm 2026',
|
||||
@@ -627,8 +633,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.description':
|
||||
'Theo dõi chuyến đi & nhật ký du lịch với nhận phòng, ảnh và câu chuyện hàng ngày',
|
||||
'admin.addons.catalog.collections.name': 'Bộ sưu tập',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'Thư viện địa điểm cá nhân — lưu địa điểm qua các chuyến đi vào danh sách có tên, sao chép vào bất kỳ chuyến đi nào, chia sẻ với người khác',
|
||||
'admin.addons.catalog.collections.description': 'Gom địa điểm từ mọi chuyến đi vào danh sách có tên và dùng lại',
|
||||
'admin.passkey.title': 'Đăng nhập bằng mật mã',
|
||||
'admin.passkey.cardHint': 'Cho phép người dùng đăng nhập bằng mật mã (WebAuthn). Tắt theo mặc định.',
|
||||
'admin.passkey.login': 'Kích hoạt đăng nhập bằng mật mã',
|
||||
|
||||
@@ -507,7 +507,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': '行李',
|
||||
'admin.addons.catalog.packing.description': '每次旅行的行李準備清單',
|
||||
'admin.addons.catalog.budget.name': '費用',
|
||||
'admin.addons.catalog.budget.description': '跟蹤支出並規劃旅行預算',
|
||||
'admin.addons.catalog.budget.description': '記錄行程開銷並在同行者之間分攤',
|
||||
'admin.addons.catalog.documents.name': '文件',
|
||||
'admin.addons.catalog.documents.description': '儲存和管理旅行文件',
|
||||
'admin.addons.catalog.vacay.name': 'Vacay',
|
||||
@@ -515,9 +515,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': 'Atlas',
|
||||
'admin.addons.catalog.atlas.description': '標記已訪問國家和旅行統計的世界地圖',
|
||||
'admin.addons.catalog.collab.name': 'Collab',
|
||||
'admin.addons.catalog.collab.description': '旅行規劃的即時筆記、投票和聊天',
|
||||
'admin.addons.catalog.collab.description': '備註、投票、聊天與建議,方便一起規劃',
|
||||
'admin.addons.subtitleBefore': '啟用或停用功能以自定義你的 ',
|
||||
'admin.addons.subtitleAfter': ' 體驗。',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': '從共享的 Naver 地圖清單匯入地點',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': '從你自架的 AirTrail 執行個體同步航班',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI 解析',
|
||||
'admin.addons.catalog.llm_parsing.description': '用你選擇的 AI 模型讀取內建解析器無法識別的訂位',
|
||||
'admin.addons.enabled': '已啟用',
|
||||
'admin.addons.disabled': '已停用',
|
||||
'admin.addons.type.trip': '旅行',
|
||||
@@ -528,6 +534,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': '後端服務和 API 整合,無專屬頁面',
|
||||
'admin.addons.toast.updated': '擴充套件已更新',
|
||||
'admin.addons.toast.error': '更新擴充套件失敗',
|
||||
'admin.addons.group.count': '{total} 項中已啟用 {enabled} 項',
|
||||
'admin.addons.noAddons': '暫無可用擴充套件',
|
||||
'admin.weather.title': '天氣資料',
|
||||
'admin.weather.badge': '自 2026 年 3 月 24 日起',
|
||||
@@ -614,8 +621,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': '旅程',
|
||||
'admin.addons.catalog.journey.description': '旅行追蹤與旅行日誌,包含打卡、照片和每日故事',
|
||||
'admin.addons.catalog.collections.name': '收藏',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'個人地點庫 — 將各行程的地點儲存到具名清單中,複製到任意行程,並與他人分享',
|
||||
'admin.addons.catalog.collections.description': '把任何行程的地點收進命名清單,隨時再次使用',
|
||||
'admin.passkey.title': 'Passkey 登入',
|
||||
'admin.passkey.cardHint': '讓使用者使用 Passkey(WebAuthn)登入。預設為關閉。',
|
||||
'admin.passkey.login': '啟用 Passkey 登入',
|
||||
|
||||
@@ -467,7 +467,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.packing.name': '列表',
|
||||
'admin.addons.catalog.packing.description': '行程打包清单与待办任务',
|
||||
'admin.addons.catalog.budget.name': '费用',
|
||||
'admin.addons.catalog.budget.description': '跟踪支出并规划旅行预算',
|
||||
'admin.addons.catalog.budget.description': '记录行程开销并在同行者之间分摊',
|
||||
'admin.addons.catalog.documents.name': '文档',
|
||||
'admin.addons.catalog.documents.description': '存储和管理旅行文档',
|
||||
'admin.addons.catalog.vacay.name': '假期',
|
||||
@@ -475,9 +475,15 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.atlas.name': '足迹',
|
||||
'admin.addons.catalog.atlas.description': '标记已访问国家和旅行统计的世界地图',
|
||||
'admin.addons.catalog.collab.name': '协作',
|
||||
'admin.addons.catalog.collab.description': '旅行规划的实时笔记、投票和聊天',
|
||||
'admin.addons.catalog.collab.description': '备忘、投票、聊天和建议,方便一起规划',
|
||||
'admin.addons.subtitleBefore': '启用或禁用功能以自定义你的 ',
|
||||
'admin.addons.subtitleAfter': ' 体验。',
|
||||
'admin.addons.catalog.naver_list_import.name': 'Naver List Import',
|
||||
'admin.addons.catalog.naver_list_import.description': '从共享的 Naver 地图列表导入地点',
|
||||
'admin.addons.catalog.airtrail.name': 'AirTrail',
|
||||
'admin.addons.catalog.airtrail.description': '从你自建的 AirTrail 实例同步航班',
|
||||
'admin.addons.catalog.llm_parsing.name': 'AI 解析',
|
||||
'admin.addons.catalog.llm_parsing.description': '用你选择的 AI 模型读取内置解析器无法识别的预订',
|
||||
'admin.addons.enabled': '已启用',
|
||||
'admin.addons.disabled': '已禁用',
|
||||
'admin.addons.type.trip': '旅行',
|
||||
@@ -488,6 +494,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.integrationHint': '后端服务和 API 集成,无专属页面',
|
||||
'admin.addons.toast.updated': '扩展已更新',
|
||||
'admin.addons.toast.error': '更新扩展失败',
|
||||
'admin.addons.group.count': '{total} 项中已启用 {enabled} 项',
|
||||
'admin.addons.noAddons': '暂无可用扩展',
|
||||
'admin.weather.title': '天气数据',
|
||||
'admin.weather.badge': '自 2026 年 3 月 24 日起',
|
||||
@@ -612,8 +619,7 @@ const admin: TranslationStrings = {
|
||||
'admin.addons.catalog.journey.name': '旅程',
|
||||
'admin.addons.catalog.journey.description': '旅行追踪与旅行日志,包含签到、照片和每日故事',
|
||||
'admin.addons.catalog.collections.name': '收藏',
|
||||
'admin.addons.catalog.collections.description':
|
||||
'个人地点库 — 将各行程的地点保存到命名列表中,复制到任意行程,并与他人共享',
|
||||
'admin.addons.catalog.collections.description': '把任意行程的地点收进命名列表,随时再次使用',
|
||||
'admin.passkey.title': '通行密钥登录',
|
||||
'admin.passkey.cardHint': '允许用户使用通行密钥(WebAuthn)登录。默认关闭。',
|
||||
'admin.passkey.login': '启用通行密钥登录',
|
||||
|
||||
Reference in New Issue
Block a user